Skip to content

fix(itc): await cross-worker propagation for role/schema mutations (#1497)#1499

Merged
kriszyp merged 3 commits into
mainfrom
kris/itc-await-1497
Jun 29, 2026
Merged

fix(itc): await cross-worker propagation for role/schema mutations (#1497)#1499
kriszyp merged 3 commits into
mainfrom
kris/itc-await-1497

Conversation

@kriszyp

@kriszyp kriszyp commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Addresses the eventually-consistent ITC gap in #1497 across three layers:

  1. Call-site awaitsaddRole, dropRole, createSchema, dropSchema, createAttribute, and dropAttribute returned success before sibling workers had rebuilt their cached auth/schema state, because they fired signalUserChange / signalSchemaChange without awaiting the returned promise. Made consistent with siblings (alterRole, add/alter/dropUser) that already awaited.

  2. Originating-worker local rebuildsignalUserChange / signalSchemaChange called the local serverItcHandlers.user/schema() fire-and-forget, then awaited only the cross-worker broadcast. So even if a caller awaited, the originating worker returned before its own usersWithRolesMap was rebuilt. Fixed with async function + await Promise.all([localHandler, broadcast]) in utility/signalling.ts.

  3. Ack timeout backstopbroadcastWithAcknowledgement had no timeout: a wedged-but-alive worker (hung ITC handler, not dead) would block admin ops forever. Added a best-effort 30 s timeout: remaining pending acks are resolved, thread IDs are logged, and the call returns so the durable write isn't rolled back.

Root cause summary

The cross-worker ack machinery the issue proposes building already exists:

  • signalUserChange / signalSchemaChange return broadcastWithAcknowledgement (server/threads/manageThreads.js), which resolves only after every live worker acks (a port close-listener releases the wait if a worker dies).
  • Each worker acks from its ITC handler (server/threads/itc.js) only after awaiting userHandler / schemaHandler — i.e. after setUsersWithRolesCache() / schema reload completes.

The bugs were: (a) six entry points dropped the returned promise; (b) the signal helpers themselves fired the local rebuild fire-and-forget.

Scope

createTable / dropTable propagate through a different layer (harperBridge → ResourceBridge → Table apply path) — they don't use these signal helpers and are intentionally untouched. tokenAuthentication.ts signals on every login; awaiting there would add cross-worker latency for no auth-visibility benefit.

Validation

Cannot reproduce the flake locally — the integration harness hardcodes THREADS_COUNT=1, which prevents the multi-thread routing race. Using CI as the repro environment; watching whether the northwind cross-3-schema-join flake (the alter_role stale-partial-403 from #1497 symptom 3) resolves after the local-rebuild fix.

🤖 Generated with Claude Code — KrAIs

…1497)

addRole, dropRole, createSchema, dropSchema, createAttribute and dropAttribute
fired signalUserChange / signalSchemaChange without awaiting, returning success
before sibling workers rebuilt their cached auth/schema state. A request routed
to a lagging worker then observed stale state — spurious 403s on a fresh grant,
brief continued access after a revoke, and describe_all reporting a just-created
schema as missing.

The ack machinery already exists: signal* returns broadcastWithAcknowledgement,
which resolves only once every live worker has processed the event and acked, and
each worker acks from its ITC handler only after rebuilding its cache. These call
sites just dropped that promise. Awaiting it makes them consistent with the
already-correct alterRole and add/alter/dropUser paths, enforcing the invariant
that once a role/schema mutation returns success the change is in effect on every
worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@kriszyp kriszyp requested review from DavidCockerill and removed request for cb1kenobi and heskew June 26, 2026 12:03
Kris Zyp and others added 2 commits June 26, 2026 06:17
…1497)

broadcastWithAcknowledgement waited indefinitely for every live worker to ack.
A dying worker is released by the port close listener, but a wedged-but-alive
worker (event loop blocked, port still open) could hang a mutating admin/DDL op
forever now that those ops await propagation.

Add a bounded timeout (default 30s, overridable per call). On timeout we drive
the still-pending per-port handlers through the same cleanup path an ack/close
uses, log which worker threads didn't ack, and resolve best-effort — the durable
write already succeeded and the health monitor restarts a truly stuck worker.
A Set of pending handlers makes each port's handler idempotent across the ack,
close, and timeout paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
signalUserChange / signalSchemaChange fired the local ITC handler
(serverItcHandlers.user/.schema — which rebuilds THIS thread's auth/schema
cache) without awaiting it, then returned the broadcast promise. So even after a
mutating op awaited the signal, the originating worker returned success before
its own cache finished rebuilding, and the next request it served observed stale
state. Receivers were already covered (itc.js awaits the handler before acking);
only the originator's local rebuild was fire-and-forget.

This is the mechanism behind the THREADS_COUNT=1 northwind cross-schema-join
flake: the worker that ran alter_role served the follow-up query off its own
not-yet-rebuilt usersWithRolesMap. Await both legs (local handler + broadcast)
so the op returns only once every thread, including the originator, is current.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kriszyp kriszyp merged commit 2d14d5d into main Jun 29, 2026
81 of 82 checks passed
@kriszyp kriszyp deleted the kris/itc-await-1497 branch June 29, 2026 14:56
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