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