From per.buer at varnish-software.com Tue Mar 24 19:10:39 2026 From: per.buer at varnish-software.com (Per Buer) Date: Tue, 24 Mar 2026 20:10:39 +0100 Subject: RFC; global backends. Message-ID: hi. I have a proposal. despite the pretty high impact, I think the proposal is reasonably simple. it's one of these "make a simple API and let the VMOD authors handle the rest". # proposal: global directors, dispatch, and declarative routing ## motivation backends in Vinyl are tied to VCL. when a VCL is discarded, its backends are destroyed. when a new VCL is loaded, backends are recreated. this makes VCL the single point of configuration for both infrastructure (what servers exist) and policy (how requests are handled). VCL conflates two concerns: defining backend topology and defining request handling policy. separating these would simplify Vinyl for the common case without removing flexibility for advanced users. long time users, having thousands of lines of VCL, would have a significant QoL improvement from this as it allows them to centralize infra. ## concept a global director registry in vinyld core. global directors exist independently of any VCL ? created and managed by VMODs, they live for as long as the VMOD keeps them registered, and can be referenced from any loaded VCL using a `global.` prefix. VCL-scoped backends (`backend foo { ... }`) continue to work as today. no existing configuration breaks. the core provides only the registry. VMODs create directors using existing infrastructure (`VRT_new_backend()`, the directors VMOD, etc.) and register them in the global namespace. domain-specific logic (health checking, load balancing, service discovery) stays in VMODs. ## core API the registry API exposed to VMODs: ```c int VGDR_Register(const char *name, struct director *d); struct director *VGDR_Lookup(const char *name); // refcounted void VGDR_Release(struct director *d); int VGDR_Remove(const char *name); ``` the registry is a name-to-director map protected by `vcl_mtx`. all registry operations; registration, removal, lookup, iteration, all run on the CLI thread (VCL init/fini and CLI commands). no worker threads touch the registry. `global.*` references in VCL resolve to director pointers at VCL init time; the generated C code stores the pointer in a variable. request threads use the pointer directly, never consulting the registry. this is the same pattern as VCL-local backends and means the registry requires no read-write lock or lock-free structure. multiple VMODs can register global directors. name uniqueness is enforced by `VGDR_Register()`, which fails on duplicate names. we could have a single VMOD restriction, but that would be overly nanny-like, wouldn't it? `VGDR_Lookup()` increments the director's existing `vcldir.refcnt` before returning the pointer. `VGDR_Release()` decrements it. this reuses the same per-director refcounting that `VRT_Assign_Backend()` already performs for dynamic directors, no new lifecycle mechanism is needed (needs a check with G) `VGDR_Remove()` unregisters the name and decrements the VMOD's reference. if in-flight requests still hold references, the director stays alive until the last `VGDR_Release()` (or `VRT_Assign_Backend(dst, NULL)`) drops the refcount to zero. at that point, the existing `vdire` resignation machinery handles cleanup ? if a CLI iteration is active, retirement is deferred to the resigning list; otherwise the director retires immediately. no separate cooldown mechanism is required. ## VCL integration global directors are referenced in VCL with a `global.` prefix: ```vcl sub vcl_recv { set req.backend_hint = global.api-service; } ``` the VCL compiler recognizes `global.*` as a separate namespace and defers resolution to VCL init time, similar to VMOD objects. if a global director does not exist when the VCL warms up, it is a runtime error ? same as a VMOD init failure. name collisions between VCL-local backends and global directors are detected at VCL warm time, not compile time. global directors are registered at runtime, so the compiler cannot know what names exist. if `VGDR_Lookup()` returns a name that collides with a VCL-local backend, VCL init fails, same failure mode as a missing global director. the `global.` prefix is handled as a special case in the expression parser, not as a symbol namespace. if a VMOD named `global` is imported in the same VCL, `import global;` claims the `global` symbol and `global.NAME` is interpreted as a VMOD method call instead of a global director reference. this is by design: the VCL that creates global directors imports the registering VMOD, but sub-VCLs that only consume global directors do not import it ? they use `global.NAME` directly. the two uses do not coexist in the same VCL. the VCL compiler requires at least one backend declaration per VCL. a sub-VCL that uses only global directors must include `backend dummy none;` to satisfy this requirement. ## implementation consequences **VCL discard and wrapped backends.** a global director that wraps a VCL-scoped backend (e.g., one created by a directors VMOD round-robin) has a dependency on the creating VCL. if that VCL is discarded, the wrapped backend is destroyed and the global director holds a dangling pointer. the existing VCL lifecycle protection APIs (`VRT_VCL_Prevent_Discard`, `VRT_VCL_Prevent_Cold`) cannot be called during `vcl_init` because `VCL_TEMP_INIT` is cold ? both functions assert a warm VCL. this is a known limitation. possible solutions: - a VCL event hook (`VCL_EVENT_WARM`) that fires after `vcl_init` completes, where `VRT_VCL_Prevent_Discard` can be called safely. - VMODs that create their own backends via `VRT_AddGlobalDirector` directly, bypassing VCL-scoped infrastructure entirely. these directors live on the global `vdire` and have no VCL dependency. - backend ownership transfer ? moving a VCL-scoped director onto the global `vdire`, detaching it from the creating VCL. the second option is the intended long-term path: service discovery VMODs create backends directly and manage their lifecycle. the wrapper pattern (wrapping an existing VCL-scoped director) is a transitional convenience. ### example: service discovery ```vcl import svc; sub vcl_init { new cluster = svc.endpoint_watcher(); } ``` this creates global directors like `global.api`, `global.web`, `global.static` ? updated as cluster state changes, surviving VCL swaps. global directors are opaque to VCL, so they can encapsulate arbitrarily complex routing logic. a topology-aware VMOD could register a single global director that understands the full request path ? edge, shield, origin ? and selects layers based on cluster availability, resource locality, and health status. VCL sets `req.backend_hint` and the director handles the rest. this moves fetch routing out of VCL into a reactive subsystem with access to real-time topology state. ## dispatch: the problem with return(vcl(...)) `return(vcl(...))` switches to another VCL but rolls back the request object first (`Req_Rollback()` in `cache_req_fsm.c`). the target VCL gets a clean, unmodified request. this makes sense when VCL switching means "hand this to a completely different configuration," but not for the dispatch model where a main VCL and sub-VCLs cooperate. if main.vcl sets `req.backend_hint = global.api-service` and then does `return(vcl(api_policy))`, the rollback wipes `req.backend_hint`. the sub-VCL would have to set it again, defeating centralized routing. ## return dispatch `return(dispatch(...))` ? a new VCL return that switches to another VCL without rolling back req state. the main VCL's modifications (backend hint, headers, etc.) survive into the target VCL. `return(vcl(...))` retains its current rollback semantics. the existing restriction that VCL switching only works on the first pass (`req.restarts == 0`) applies to `return(dispatch(...))` as well. ## implementation consequences **new return code.** add `VCL_RET_DISPATCH` alongside `VCL_RET_VCL`. the FSM handler in `cache_req_fsm.c` re-enters `vcl_recv` in the target VCL but skips `Req_Rollback()` and `cnt_recv_prep()`. **VPI_vcl_dispatch.** a new runtime function alongside `VPI_vcl_select()`. same VCL reference management (swaps `req->vcl`, records `vcl0` for the top request), but does not call `Req_Rollback()`. the VCL compiler emits `VPI_vcl_dispatch()` for `return(dispatch(...))` and `VPI_vcl_select()` for `return(vcl(...))`. both share the same `VPI_vcl_get()`/`VPI_vcl_rel()` init/fini pair for VCL label resolution. **sub-VCL behavior.** the sub-VCL sees the request as modified by main.vcl. `req.backend_hint` is set, headers added by main.vcl are present. the sub-VCL can override anything ? it has full control, it just starts from a non-pristine state. ## example: main VCL with dispatch ```vcl import svc; sub vcl_init { new cluster = svc.endpoint_watcher(); } sub vcl_recv { if (req.http.host == "api.example.com") { set req.backend_hint = global.api; return (dispatch(api_policy)); } if (req.http.host == "www.example.com") { set req.backend_hint = global.web; return (dispatch(www_policy)); } if (req.http.host == "static.example.com") { set req.backend_hint = global.static; return (dispatch(static_policy)); } return (synth(404)); } ``` a sub-VCL is pure policy ? no service discovery imports, no backend definitions beyond the required placeholder: ```vcl backend dummy none; sub vcl_recv { if (req.url ~ "^/admin/") { return (pass); } } sub vcl_backend_response { if (beresp.http.Cache-Control) { set beresp.ttl = 5m; } if (bereq.url ~ "\.(js|css|png)$") { set beresp.ttl = 1h; } } sub vcl_deliver { unset resp.http.X-Powered-By; } ``` ## out of scope / further work: declarative routing as a VMOD with global directors and dispatch in place, declarative routing requires no further core changes. a routing VMOD reads YAML or JSON config and handles both backend registration and request dispatch: ```vcl import routing; sub vcl_init { new router = routing.from_yaml("/etc/vinyl/routes.yaml"); } sub vcl_recv { return (dispatch(router.match(req))); } ``` the VMOD registers global directors from the config, resolves incoming requests to VCL labels based on routing rules, and can be updated at runtime without VCL recompilation. -- Per Buer Varnish Software -------------- next part -------------- An HTML attachment was scrubbed... URL: