Skip to content

feat(server): central-mode durable objects — leases, embedded replicas, sync-before-serve (scale-out phase 3) #432

Description

@davidwkeith

Phase 3 of the horizontal scale-out plan (spec/scale-out.md §15): bring the Tier 2 (actor) cohort — solid-pod, activitypub, remotestorage, webauthn, atproto-pds — up in central storage mode, with the Durable Object guarantees enforced across replicas. Builds on #428/#430 (LibsqlKv) and #431 (central mode + Tier 1); fleet pollers/cron/drain are phase 4.

What

Per spec/scale-out.md §6, adopting @dwk/deno-host's implemented per-request-lease model unchanged (v1):

  1. DO namespace over the coordination store: compose @dwk/deno-host's createDurableObjectNamespace with LibsqlKv — per-event lease acquire/release (["dwk_lease", className, idHex], CAS + expireIn crash safety net), process-local per-id chain within a replica, KV lease across replicas. LeaseContendedError maps to 503 + Retry-After at the host.
  2. getStorageClient(idHex) → per-object libSQL database via embedded replica: the libsql npm package's sync client against storage.libsql.replicaDir scratch files (a rebuildable cache; ephemeral disk suffices). This is the first hard dependency on the libsql native module loading on Node in this repo — verify early.
  3. The sync-before-serve rule (§6.2 — load-bearing): after acquiring an id's lease and before the event runs, the embedded replica MUST sync from the primary — a different replica may have written since this one last held the id; skipping the sync serves stale reads and is a correctness bug. Note: createDurableObjectNamespace currently calls getStorageClient once per id and caches the instance, with no per-dispatch hook — this likely needs a small, contract-reviewed extension to @dwk/deno-host (e.g. an onLeaseAcquired/sync callback in DurableObjectNamespaceOptions), amended in the same PR per the host-contract §8 growth discipline's spirit.
  4. Alarms: the KV-indexed due/by-id schedule works as-is over LibsqlKv (proven in feat(server): add LibsqlKv, the DenoKvLike coordination store over libSQL #430's tests); wire pollAlarms to a per-replica jittered interval timer (~1 s default — real timers, better than platform-cron granularity), claim-based so concurrent pollers are safe. Sweep LibsqlKv.sweepExpired from the same tick.
  5. WebSockets (§6.4, v1 stance): sockets stay pinned to the terminating replica via the existing web-socket-upgrade.ts bridging; document the lease/socket window and the LB-affinity recommendation rather than architecting around it (v2 residency is the eventual fix — see the phase 6 issue).

Testing (spec §14 items 2–3)

Two DwkServer instances sharing one LibsqlKv + SQL backing + S3 fake:

  • Racing same-pod writes from both replicas serialize — one wins the lease, the other queues or 503s; never interleaved state.
  • Sync-before-serve regression: write a pod resource via replica A, immediately read via replica B, assert B sees it (the test that fails if the sync rule is skipped).
  • An alarm scheduled via A fires exactly once though both replicas poll (activitypub Accept-delivery retry as the lifecycle).
  • Kill replica A mid-request (simulate: never release) → B recovers the id after leaseTtlMs.
  • Tier 2 lifecycles across replicas, modeled on the existing phase5-*.integration.test.ts suite.

Out of scope

  • Queue pollers, cron tick lease, drain, readiness (phase 4).
  • Residency leases / internal forwarding (phase 6, demand-gated).
  • Live Turso/sqld verification (phase 5's checklist; the embedded-replica native-module smoke test above is the one early exception).

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions