Skip to content

feat(runnerhub): demote the binding maps to a cache over session_bindings (RIG-3108) - #1095

Merged
trunk-io[bot] merged 4 commits into
mainfrom
compass-managed/rig-3108-pr3-hub-binding-cache
Sep 11, 2026
Merged

feat(runnerhub): demote the binding maps to a cache over session_bindings (RIG-3108)#1095
trunk-io[bot] merged 4 commits into
mainfrom
compass-managed/rig-3108-pr3-hub-binding-cache

Conversation

@rigel-mintaka

Copy link
Copy Markdown
Contributor

The hub's session<->account maps were the only record of a binding, so a
Server restart lost every live session's routing. They become a read-through
cache over the durable session_bindings table: writes go to the store first,
then the maps under h.mu, never holding the lock across a store call or a
sink.

The invariant conflict this resolves is that enroll clears bindings on a
Runner reconnect and fail-closed depends on that, while the rows survive.
A naive cache-miss-reads-the-table would repopulate exactly what the reap
dropped. So the two cases are distinguished by whether the hub already had a
Runner: a RE-enroll durably reaps via DeleteSessionBindingsForRunner and its
RETURNING rows drive the presence-offline and held-deliver edges, while a
first enroll on a fresh hub (a Server restart, sessions still live) keeps the
rows -- which is the availability property the change exists for.

Matt ruled the ctx fork: accountForSession and SessionForAccount both gain a
ctx parameter, including the public delivery.SessionResolver change and its
callers in delivery/dispatch.go, server/lifecycle.go and server/serve.go.

That ruling also closes a real hazard. deliverAck and forgeNotificationAck
reached accountForSession AFTER store.WithSystemRole, so a cache-miss table
read there would have run BYPASSRLS and could return a plausible row from an
arbitrary tenant. The binding is now resolved on the request ctx before the
escalation, and readThroughAllowed independently refuses any read-through
under a system-role ctx, so a future caller fails closed rather than reading
cross-tenant. TestSessionForAccountUnderSystemRoleIsUnscoped is unchanged and
not weakened: it pins the store's raw behaviour, which is untouched; the
hazard is closed one layer up.

Cross-instance invalidation is seamed but not wired: serve.go constructs no
fabric.Fabric, so SetRoutingFabric(nil) would be inert. RIG-3107 wires the
live NATS fabric and the subscribe loop.

Four red controls run individually and restored: dropping the enroll durable
reap, the displaced eviction, the subscribe-side eviction, and letting the
ack path inherit the system-role ctx each turn their own test red.

…ings (RIG-3108)

The hub's session<->account maps were the only record of a binding, so a
Server restart lost every live session's routing. They become a read-through
cache over the durable session_bindings table: writes go to the store first,
then the maps under h.mu, never holding the lock across a store call or a
sink.

The invariant conflict this resolves is that enroll clears bindings on a
Runner reconnect and fail-closed depends on that, while the rows survive.
A naive cache-miss-reads-the-table would repopulate exactly what the reap
dropped. So the two cases are distinguished by whether the hub already had a
Runner: a RE-enroll durably reaps via DeleteSessionBindingsForRunner and its
RETURNING rows drive the presence-offline and held-deliver edges, while a
first enroll on a fresh hub (a Server restart, sessions still live) keeps the
rows -- which is the availability property the change exists for.

Matt ruled the ctx fork: accountForSession and SessionForAccount both gain a
ctx parameter, including the public delivery.SessionResolver change and its
callers in delivery/dispatch.go, server/lifecycle.go and server/serve.go.

That ruling also closes a real hazard. deliverAck and forgeNotificationAck
reached accountForSession AFTER store.WithSystemRole, so a cache-miss table
read there would have run BYPASSRLS and could return a plausible row from an
arbitrary tenant. The binding is now resolved on the request ctx before the
escalation, and readThroughAllowed independently refuses any read-through
under a system-role ctx, so a future caller fails closed rather than reading
cross-tenant. TestSessionForAccountUnderSystemRoleIsUnscoped is unchanged and
not weakened: it pins the store's raw behaviour, which is untouched; the
hazard is closed one layer up.

Cross-instance invalidation is seamed but not wired: serve.go constructs no
fabric.Fabric, so SetRoutingFabric(nil) would be inert. RIG-3107 wires the
live NATS fabric and the subscribe loop.

Four red controls run individually and restored: dropping the enroll durable
reap, the displaced eviction, the subscribe-side eviction, and letting the
ack path inherit the system-role ctx each turn their own test red.
@trunk-io

trunk-io Bot commented Sep 11, 2026

Copy link
Copy Markdown

😎 Merged successfully - details.

@linear-code

linear-code Bot commented Sep 11, 2026

Copy link
Copy Markdown

RIG-3108

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-managed-rig-3108-pr3.compass-eng-docs.pages.dev

Deployed from compass-managed/rig-3108-pr3-hub-binding-cache at f014c6a.

…ding fakes faithful (RIG-3108)

Review fold on the session-binding cache.

A re-enroll whose durable reap FAULTED cleared the in-RAM maps but left the
rows, so the very next cache miss read one back and resurrected a session the
hub had just declared dead — read-through turned the fail-closed reconnect
contract into a resurrection. The hub now records that the table disagrees
with the cleared cache (reapStale) and refuses read-through until a later
reap succeeds. Both resolvers consult it. Verified as a control: reverting
the gate alone turns the new re-enroll case red.

The test doubles also modelled semantics the real store forbids, which is
what hid the gap:

- fakeBindingStore.RecordSessionBinding silently stole a session id already
  bound to a different account; the real store raises ErrConflict on
  session_bindings_session_key. It now does too, and records the runner id it
  was passed rather than a constant.
- fakeRoutingFabric.PublishBindingChange accepted anything; the real fabric
  rejects an invalid change and the hub only logs that, so a malformed
  publish would silently leave every peer cache stale. The fake now mirrors
  the rejection contract.

Adds the four durable-fault cases the fake was built for but no test drove
(record, reap, resolve, reverse-resolve), and a witness test pinning a known
gap: production mints session ids from a counter that resets on Runner
restart, so a joint restart re-mints an id over a surviving row and the
resulting ErrConflict is currently swallowed, leaving the durable row on the
old account. Harmless while one Server instance shadows it; it needs a
decision before multi-instance.
…he ctx signature (RIG-3108)

Three callers behind `//go:build pgtest && unix` were missed by the ctx
cutover: forge_notify_e2e_pgtest_test.go, lifecycle_wake_pgtest_test.go and
trace_continuity_e2e_pgtest_test.go. A default-tag build never compiles
them, so both the local gate and a plain grep of the callsites read clean;
CI's pgtest job caught it as `go/server [build failed]`.

Swept all 63 SessionForAccount/accountForSession callsites across every
build tag: these three were the only unmigrated ones. Verified with
`go vet -tags 'pgtest unix'`, which is the check that would have caught this
before the push.
…ap (RIG-3108)

Raising reapStale only after DeleteSessionBindingsForRunner RETURNED left the
hole it was meant to close, just narrower. enroll clears the maps, releases
the lock, then makes a store round-trip that on the fault path can block for
seconds; a resolver arriving in that gap missed the cleared cache, saw
reapStale still false, and read back a row the failing reap never deleted —
resurrecting a session the reconnect had just declared dead.

The flag is now raised inside the same critical section that clears the maps
and lowered only by a reap that succeeds, so read-through is refused from the
instant the cache is empty. Pessimistic-true during a successful reap costs
nothing: the maps are empty, the only durable-but-uncached rows are the dead
survivors, and a session promoted after the reconnect writes its cache entry
and hits.

Fenced by a deterministic test that parks the reap mid-flight and resolves
concurrently; it is red against the previous placement and green under -race.

Also documents the invariant the hub-wide flag depends on: it is correct only
because the MVP has one stable runner id, and a future multi-Runner change
must key it by runner id or one runner's successful reap will lower the flag
while another's un-reaped rows survive.
@rigel-mintaka
rigel-mintaka marked this pull request as ready for review September 11, 2026 21:08
@trunk-io
trunk-io Bot merged commit 28e8d53 into main Sep 11, 2026
15 checks passed
@trunk-io
trunk-io Bot deleted the compass-managed/rig-3108-pr3-hub-binding-cache branch September 11, 2026 23:33
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.

2 participants