RFC; global backends.

Per Buer per.buer at varnish-software.com
Tue Mar 24 19:10:39 UTC 2026


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: <https://vinyl-cache.org/lists/pipermail/vinyl-dev/attachments/20260324/8c11686b/attachment-0001.html>


More information about the vinyl-dev mailing list