Summary
Mutating operations that change auth or schema state — add_role / alter_role / drop_role, user create/alter/drop, and schema DDL — return HTTP 200 after the durable write + a local cache update + an ITC broadcast send, but before other worker threads have rebuilt their in-memory caches. The cross-worker propagation is fire-and-forget, so a request routed to a lagging worker observes the old state.
This is an internal consistency-invariant gap, not just a test artifact. The invariant we'd expect: once a role/user/schema mutation returns success, the change is in effect for subsequent requests. Today it's eventually-consistent across the worker pool.
Where it lives
security/role.ts — alterRole (~L122), dropRole (~L189), add-role path: await signalling.signalUserChange(new UserEventMsg(process.pid)).
utility/signalling.ts — signalUserChange / signalSchemaChange: call serverItcHandlers.user(...) (updates the local worker only, synchronously) then return sendItcEvent(...) (broadcasts to other workers). The awaited value is the send, not remote acknowledgment.
security/user.ts — each worker's usersWithRolesMap is rebuilt by setUsersWithRolesCache() only when it processes the inbound ITC event.
security/permissionsTranslator.js — translateRolePermissions expands per-attribute permissions from the (possibly stale) cached role; a stale role with no attribute_permissions for a table yields an empty attr-perm map.
Observable symptoms
- Spurious 403 on grant. Admin grants attribute access via
alter_role, gets 200, then a query for the newly-granted column hits a lagging worker → 403 … "Attribute 'X' does not exist on 'schema.table'" (the empty attr-perm map path in checkAttributePerms, utility/operation_authorization.ts:765-773).
- Brief continued access on revoke. A revoke is similarly delayed on lagging workers — a short window where the old (broader) permissions still apply. (Live subscriptions have separate continuous re-auth; this is about request-path auth.)
- Intermediate/partial-looking states. Because different workers can be at different role versions, a 403 can appear with a different body than the final state (e.g. empty
unauthorized_access, a subset of invalid_schema_items).
- Schema variant. The same pattern affects schema propagation — e.g.
describe_all on a worker that hasn't yet seen a just-created schema returns it as missing/empty (integrationTests/apiTests/blob.test.mjs "all three device-type schemas are visible in describe_all").
CI impact
This is the root cause behind the recurring Integration Tests shard 5/6 flakes (and others) in northwind.test.mjs (cross-schema-join role tests) and the schema-visibility flake in blob.test.mjs. Test-side mitigations are in flight (#1474, #1475 — poll for propagation with waitFor), but those are per-call-site whack-a-mole and easy to get subtly wrong (the first waitFor predicate in #1475 was too loose and still flaked). Related closed test-side issue: #1222.
Proposed direction (needs design buy-in)
Make mutating role/user/schema ops await cross-worker propagation before returning, OR expose an internal propagation barrier:
- Add an ack to the ITC round-trip:
sendItcEvent resolves once all live workers have processed the user/schema event (each replies after setUsersWithRolesCache() / schema reload). Mutating ops await that instead of the bare send.
- Bound it with a timeout + worker-death handling so a stuck/dying worker can't hang an admin op (degrade to best-effort + log, since the durable write already succeeded).
Trade-offs
- Adds latency to admin/DDL ops (rare, so likely acceptable) and some ITC complexity.
- Eliminates the entire mutate-then-read flake class (auth and schema) at the source, instead of per-test polling.
- Alternative (lighter) option: keep ops async but provide a documented/internal "propagation settled" signal the test harness (and careful clients) can await.
Filing for discussion before any product change — the test-side PRs (#1474/#1475) unblock CI in the meantime.
Summary
Mutating operations that change auth or schema state —
add_role/alter_role/drop_role, user create/alter/drop, and schema DDL — return HTTP 200 after the durable write + a local cache update + an ITC broadcast send, but before other worker threads have rebuilt their in-memory caches. The cross-worker propagation is fire-and-forget, so a request routed to a lagging worker observes the old state.This is an internal consistency-invariant gap, not just a test artifact. The invariant we'd expect: once a role/user/schema mutation returns success, the change is in effect for subsequent requests. Today it's eventually-consistent across the worker pool.
Where it lives
security/role.ts—alterRole(~L122),dropRole(~L189), add-role path:await signalling.signalUserChange(new UserEventMsg(process.pid)).utility/signalling.ts—signalUserChange/signalSchemaChange: callserverItcHandlers.user(...)(updates the local worker only, synchronously) thenreturn sendItcEvent(...)(broadcasts to other workers). The awaited value is the send, not remote acknowledgment.security/user.ts— each worker'susersWithRolesMapis rebuilt bysetUsersWithRolesCache()only when it processes the inbound ITC event.security/permissionsTranslator.js—translateRolePermissionsexpands per-attribute permissions from the (possibly stale) cached role; a stale role with noattribute_permissionsfor a table yields an empty attr-perm map.Observable symptoms
alter_role, gets 200, then a query for the newly-granted column hits a lagging worker →403 … "Attribute 'X' does not exist on 'schema.table'"(the empty attr-perm map path incheckAttributePerms,utility/operation_authorization.ts:765-773).unauthorized_access, a subset ofinvalid_schema_items).describe_allon a worker that hasn't yet seen a just-created schema returns it as missing/empty (integrationTests/apiTests/blob.test.mjs"all three device-type schemas are visible in describe_all").CI impact
This is the root cause behind the recurring Integration Tests shard 5/6 flakes (and others) in
northwind.test.mjs(cross-schema-join role tests) and the schema-visibility flake inblob.test.mjs. Test-side mitigations are in flight (#1474, #1475 — poll for propagation withwaitFor), but those are per-call-site whack-a-mole and easy to get subtly wrong (the firstwaitForpredicate in #1475 was too loose and still flaked). Related closed test-side issue: #1222.Proposed direction (needs design buy-in)
Make mutating role/user/schema ops await cross-worker propagation before returning, OR expose an internal propagation barrier:
sendItcEventresolves once all live workers have processed the user/schema event (each replies aftersetUsersWithRolesCache()/ schema reload). Mutating opsawaitthat instead of the bare send.Trade-offs
Filing for discussion before any product change — the test-side PRs (#1474/#1475) unblock CI in the meantime.