carlos / platform Public

activator: Backend seam takes the route — composition can dispatch on Backing

Acquire/Restore/HasSidecar carried bare host/dbPath strings (Eleven's
shape); every call site already holds the registry.Route, and the
coming unit-backed tenants need a composite backend that picks exec vs
systemctl by rt.Backing without a registry read of its own. Mechanical:
no behavior change, fakes updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 3428e5bc5ea423fad563837f3d58c4dce50f020b parent 9addab9
3 files changed, +32 −25
  • internal/activator/activator.go +21 −16
  • internal/activator/activator_test.go +5 −5
  • internal/activator/backend_exec.go +6 −4
diff --git a/internal/activator/activator.go b/internal/activator/activator.go
index 17615c317..e47ef06d5 100644
--- a/internal/activator/activator.go
+++ b/internal/activator/activator.go
@@ -117,14 +117,19 @@ func (c Config) hibernatePrefix(host string) string {
// wake coalescing, idle→sleep, and lease-lost stand-down need no AWS.
// Backend is the side-effect seam (Eleven's actBackend). ExecBackend is the
-// production implementation.
+// production implementation for exec-child instances; UnitBackend drives
+// carlos-app@ unit tenants; SplitBackend composes the two by the route's
+// Backing. Every method takes the full route (Eleven passed bare host/dbPath
+// strings) precisely so that composition can dispatch without a registry
+// read of its own.
type Backend interface {
// Acquire claims the per-host single-writer lease. Contention surfaces as
// errLeaseHeld so the edge can answer 503 + Retry-After.
- Acquire(ctx context.Context, host string) (Lease, error)
- // Restore materializes the local DB from S3 when it's absent (a no-op when
- // the warm-cache copy is still on disk).
- Restore(ctx context.Context, host, dbPath string) error
+ Acquire(ctx context.Context, rt registry.Route) (Lease, error)
+ // Restore materializes the local DB (rt.DBPath; the backend defaults it)
+ // from S3 when it's absent (a no-op when the warm-cache copy is still on
+ // disk).
+ Restore(ctx context.Context, rt registry.Route) error
// Start spawns the instance process and waits until its socket accepts. It
// also returns the build it execed (the live/<host> symlink target read at
// spawn), so the sweep can later spot a repoint under a still-running child
@@ -135,8 +140,9 @@ type Backend interface {
// delivery (unix seconds; 0 = nothing scheduled). Queried at sleep time,
// while the socket still answers, to feed the due-time index.
NextDue(ctx context.Context, rt registry.Route) (int64, error)
- // HasSidecar reports whether host has a sidecar staged on this box.
- HasSidecar(host string) bool
+ // HasSidecar reports whether the route's host has a sidecar staged on
+ // this box.
+ HasSidecar(rt registry.Route) bool
// StartSidecar spawns the host's sidecar. Fire-and-forget: a sidecar is a
// pure outbound client with no socket, so there's nothing to health-check —
// its own connect/backoff loop recovers from a slow start.
@@ -597,16 +603,12 @@ type wokenInstance struct {
func (a *Service) doWake(ctx context.Context, rt registry.Route) (*wokenInstance, error) {
host := rt.Host
- // The DB path rides the route; when empty the backend defaults it to
- // <DataDir>/<host>.db internally (Eleven defaulted here via a package
- // global — base dirs are backend fields now).
- dbPath := rt.DBPath
// 1. The lease first: everything after this line is done as the prefix's
// single writer. Contention is not retried here — on one box it means a
// previous incarnation hasn't released; tell the client to come back after
// the TTL instead of stalling its request.
- lease, err := a.backend.Acquire(ctx, host)
+ lease, err := a.backend.Acquire(ctx, rt)
if err != nil {
var held *errLeaseHeld
if errors.As(err, &held) {
@@ -621,7 +623,7 @@ func (a *Service) doWake(ctx context.Context, rt registry.Route) (*wokenInstance
// S3 state written by the true holder). When it's still on disk it is the
// warm cache: on one box, with the lease held, the local copy can only be
// what we ourselves checkpointed at the last sleep.
- if err := a.backend.Restore(ctx, host, dbPath); err != nil {
+ if err := a.backend.Restore(ctx, rt); err != nil {
_ = lease.Release(ctx)
return nil, fmt.Errorf("restore %s: %w", host, err)
}
@@ -638,7 +640,10 @@ func (a *Service) doWake(ctx context.Context, rt registry.Route) (*wokenInstance
// 4. Trade the held lease for running replication, at the route's
// replication-class sync interval (the per-plan durability ladder).
- rep, err := lease.Replicate(ctx, dbPath, replicationSyncInterval(rt.Replication))
+ // The DB path rides the route; when empty the backend defaults it to
+ // <DataDir>/<host>.db internally (Eleven defaulted here via a package
+ // global — base dirs are backend fields now).
+ rep, err := lease.Replicate(ctx, rt.DBPath, replicationSyncInterval(rt.Replication))
if err != nil {
stopCtx, cancel := context.WithTimeout(context.Background(), actStopTimeout)
defer cancel()
@@ -653,7 +658,7 @@ func (a *Service) doWake(ctx context.Context, rt registry.Route) (*wokenInstance
// failure is loud but never fails the wake: members come before bots, and
// the sweeper retries with backoff.
w := &wokenInstance{rep: rep, proc: proc, spawnVersion: spawnVer}
- if a.backend.HasSidecar(host) {
+ if a.backend.HasSidecar(rt) {
w.sidecarWanted = true
if sc, err := a.backend.StartSidecar(ctx, rt); err != nil {
log.Printf("activator: SIDECAR for %s failed to start: %v — serving without it (the sweep retries)", host, err)
@@ -844,7 +849,7 @@ func (a *Service) sweepOnce() {
// only hibernating hosts are activator children and their systemd
// sidecar units stay disabled — the staged sidecar has exactly one
// possible runner.
- if !e.sidecarWanted && a.backend.HasSidecar(e.host) {
+ if !e.sidecarWanted && a.backend.HasSidecar(e.route) {
e.sidecarWanted = true
log.Printf("activator: sidecar staged for awake %s — adopting", e.host)
}
diff --git a/internal/activator/activator_test.go b/internal/activator/activator_test.go
index c3970f40c..e7b05d609 100644
--- a/internal/activator/activator_test.go
+++ b/internal/activator/activator_test.go
@@ -160,7 +160,7 @@ func (b *fakeBackend) events() []string {
return append([]string(nil), b.order...)
}
-func (b *fakeBackend) Acquire(context.Context, string) (Lease, error) {
+func (b *fakeBackend) Acquire(context.Context, registry.Route) (Lease, error) {
b.mu.Lock()
b.acquires++
err := b.acquireErr
@@ -173,7 +173,7 @@ func (b *fakeBackend) Acquire(context.Context, string) (Lease, error) {
return &fakeLease{rep: rep, b: b}, nil
}
-func (b *fakeBackend) Restore(context.Context, string, string) error {
+func (b *fakeBackend) Restore(context.Context, registry.Route) error {
b.mu.Lock()
defer b.mu.Unlock()
b.restores++
@@ -209,7 +209,7 @@ func (b *fakeBackend) NextDue(context.Context, registry.Route) (int64, error) {
return b.dueVal, nil
}
-func (b *fakeBackend) HasSidecar(string) bool {
+func (b *fakeBackend) HasSidecar(registry.Route) bool {
b.mu.Lock()
defer b.mu.Unlock()
return b.sidecarStaged
@@ -1829,13 +1829,13 @@ func TestExecBackendNextDue(t *testing.T) {
// convention — no registry column to drift from what's actually staged.
func TestExecBackendHasSidecar(t *testing.T) {
b := testExecBackend(t)
- if b.HasSidecar("plain.example.com") {
+ if b.HasSidecar(testRoute("plain.example.com")) {
t.Fatal("HasSidecar true with no env file staged")
}
if err := os.WriteFile(filepath.Join(b.DataDir, "bots.example.com-sidecar.env"), []byte("CARLOS_BASE=x\n"), 0o600); err != nil {
t.Fatal(err)
}
- if !b.HasSidecar("bots.example.com") {
+ if !b.HasSidecar(testRoute("bots.example.com")) {
t.Fatal("HasSidecar false with the sidecar env file staged")
}
}
diff --git a/internal/activator/backend_exec.go b/internal/activator/backend_exec.go
index 37df59d7e..41f1d5912 100644
--- a/internal/activator/backend_exec.go
+++ b/internal/activator/backend_exec.go
@@ -79,7 +79,8 @@ func (b *ExecBackend) LiveVersion(host string) (string, error) {
// re-surfaced as errLeaseHeld (never a litestream type — the lease package's
// *HeldError is this package's only view of it) so the edge can 503 with a
// Retry-After that tracks the holder's expiry.
-func (b *ExecBackend) Acquire(ctx context.Context, host string) (Lease, error) {
+func (b *ExecBackend) Acquire(ctx context.Context, rt registry.Route) (Lease, error) {
+ host := rt.Host
held, err := lease.Acquire(ctx, b.Cfg.Bucket, b.Cfg.hibernatePrefix(host), b.Cfg.Region)
if err != nil {
var he *lease.HeldError
@@ -114,7 +115,8 @@ func (l *execLease) Replicate(ctx context.Context, dbPath string, syncInterval t
func (l *execLease) Release(ctx context.Context) error { return l.held.Release(ctx) }
-func (b *ExecBackend) Restore(ctx context.Context, host, dbPath string) error {
+func (b *ExecBackend) Restore(ctx context.Context, rt registry.Route) error {
+ host, dbPath := rt.Host, rt.DBPath
if dbPath == "" {
dbPath = b.defaultDBPath(host)
}
@@ -291,8 +293,8 @@ func (b *ExecBackend) sidecarEnvPath(host string) string {
// HasSidecar: a host has a sidecar iff its sidecar EnvironmentFile exists.
// Derived from the filesystem on purpose, the way systemd units would do it:
// no registry column to drift from what's actually staged.
-func (b *ExecBackend) HasSidecar(host string) bool {
- _, err := os.Stat(b.sidecarEnvPath(host))
+func (b *ExecBackend) HasSidecar(rt registry.Route) bool {
+ _, err := os.Stat(b.sidecarEnvPath(rt.Host))
return err == nil
}