Skip to content

feat(lock): distributed lock seam + pg_advisory_lock for a remote backend (spec 21) - #112

Merged
gustavobertoi merged 1 commit into
mainfrom
feat/remote-distlock
Jul 1, 2026
Merged

feat(lock): distributed lock seam + pg_advisory_lock for a remote backend (spec 21)#112
gustavobertoi merged 1 commit into
mainfrom
feat/remote-distlock

Conversation

@gustavobertoi

Copy link
Copy Markdown
Contributor

First increment of the multi-cloud / remote team backend (spec 21, the full-scope path). This lands the correctness gate — the one thing spec 21 calls "the central unsolved problem."

Why

The concurrency spine's local gofrs/flock is a lockfile under XDG_RUNTIME_DIR — it cannot serialize two developers on two machines mutating the same remote backend's ledger rows / provisioning / port allocation. Without a distributed lock, two ups from two laptops race CREATE ROLE, port allocation, and ref rows with no protection.

What (Q-REMOTE-LOCK, RESOLVED)

A Locker seam whose remote implementation is a session-scoped pg_advisory_lock on the shared cluster Postgres — the DB we already run is the coordinator, so zero new infra and no daemon, and session scope means the lock auto-releases on a crash (a kill -9 on one machine lets another machine's next command reconcile cleanly).

  • internal/lock/distlock.goLocker interface, FileLocker (today's flock, verbatim), LockerFor(remote, subject, path, connect): remote+connector → PGLocker, else the local flock. A remote backend with no reachable cluster yet degrades safely to single-machine correctness. internal/lock stays a dependency-free leaf (plain bool, no docker import → no cycle).
  • internal/lock/pglock.goPGLocker hashes the subject to a deterministic bigint (AdvisoryKey, FNV-64a) so every client of one cluster agrees on the key; polls pg_try_advisory_lock (not the blocking form) to honor ctx cancel/timeout; opens a fresh session per WithLock. Documents the transaction-pooling-pgbouncer gotcha.
  • internal/provision/advisory.go — the pgx/v5 session (AdvisoryConn + PGLockConnector), kept out of internal/lock to preserve the leaf.

Tests (offline, no live Postgres)

AdvisoryKey determinism; acquire/run/release ordering; fn-error still releases; connect/TryLock error paths; ctx-timeout on a permanently-held key; LockerFor selection; FileLocker; and a 50-goroutine mutual-exclusion test over an in-memory advisory server (compare-and-set) under -race.

Follow-ups (not in this PR)

Threading the orchestrator's ~25 lock.WithLock call sites through the selected Locker, and reaching the remote cluster's Postgres from the laptop (SSH local-forward) so the connector is non-nil on a remote backend. Then per-user tenant isolation + the remote-authoritative ledger.

🤖 Generated with Claude Code

…kend (spec 21)

The concurrency spine's local gofrs/flock cannot serialize two developers on two
machines mutating the SAME remote backend's ledger rows / provisioning / port
allocation — the central unsolved problem for a team/cloud shared backend
(spec 21). This lands the correctness gate: a Locker seam whose remote
implementation is a session-scoped pg_advisory_lock on the shared cluster
Postgres (Q-REMOTE-LOCK RESOLVED — the DB we already run is the coordinator, so
zero new infra and no daemon; session-scoped means it auto-releases on a crash so
another machine reconciles cleanly).

- internal/lock/distlock.go: the Locker interface, FileLocker (today's flock,
  verbatim), and LockerFor(remote, subject, path, connect) — remote+connector →
  PGLocker, else the local flock (a remote backend with no reachable cluster yet
  degrades safely to single-machine correctness). internal/lock stays a
  dependency-free leaf (plain bool, no docker import → no cycle).
- internal/lock/pglock.go: PGLocker takes a session-scoped pg_advisory_lock keyed
  by AdvisoryKey (FNV-64a subject hash → deterministic bigint, so every client of
  one cluster hashes the same subject to the same key). It polls
  pg_try_advisory_lock (not the blocking form) to honor ctx cancel/timeout, and
  opens a fresh session per WithLock so the lock never outlives its section.
  AdvisoryConn is the injectable session seam (transaction-pooling pgbouncer
  breaks session advisory locks — documented).
- internal/provision/advisory.go: the pgx/v5-backed AdvisoryConn + PGLockConnector
  (the production session), kept out of internal/lock to preserve the leaf.

Tests (offline, no live PG): AdvisoryKey determinism, acquire/run/release order,
fn-error release, connect/try errors, ctx-timeout on a held key, LockerFor
selection, FileLocker, and a 50-goroutine mutual-exclusion test over an in-memory
advisory server (compare-and-set) run under -race. This is the foundation; wiring
the orchestrator's ~25 lock call sites through the selected Locker + reaching the
remote cluster over an SSH forward are the follow-ups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gustavobertoi
gustavobertoi merged commit d8e0ecf into main Jul 1, 2026
4 checks passed
@gustavobertoi
gustavobertoi deleted the feat/remote-distlock branch July 1, 2026 22:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant