Record a failed refresh on the identity row so another process does not re-present the token #8
Loading…
Reference in a new issue
No description provided.
Delete branch "heimdall/failed-refresh-marker"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes
joystick:follow_up:cross-process-failed-refresh-marker(findingcut-h1.s2.failed-attempt-re-presented-across-processes). Targetjoystick:target:r3tightensrefresh-once-per-tokento cover it.Change
linked_identities.refresh_failed_at(nullable TIMESTAMPTZ,ADD COLUMN IF NOT EXISTS). A refresh that fails or times out writes it inside the row-lock transaction (newCredentialDecisionkindfailed, which writes the marker alone).MANAGED_CREDENTIAL_REFRESH_BACKOFF_MS(60 s) of the marker presents nothing and throws the sameManagedCredentialRefreshFailed(502 at the route). Being turned away does not extend the backoff.upsertLinkedIdentitysets it NULL on conflict).docs/service-contract.md: the "residual risk" sentence is replaced by the mechanism.Behaviour change: a later request inside 60 s of a failure no longer retries the provider. The "single flight" test now asserts one provider call across both pools (it asserted one per pool); the two tests that retried straight after a failure now retry after the backoff.
Tests (test first; ran red, then green, on memory, postgres one app and postgres two apps against a live Postgres 16):
server_errorandhang, and a second process inside the backoff makes no token call and leaves the marker unmovedHand mutants, each applied, suite run, reverted; all killed, restored suite green (233 passed, 6 skipped):
backoff check ignored (14 fail),
<to<=(12), failure not recorded (26), turned-away caller extends backoff (12), backoff halved (12), postgres rotate/revoke keeps marker (8), postgres re-link keeps marker (4), in-memory rotate/revoke keeps marker (4), postgres marker not read back (18).Not done / known: a provider that rotates and consumed the token before an abort is covered only by the 60 s backoff, not by a distinct named state; I took the backoff form the follow-up named. Existing deployments get the column on
ensureSchema. 13 other test files fail to load in this checkout (missing cultmesh-ts/cultnet-ts/cultcache-ts builds); they do not touch this change. Not merged, not deployed.🤖 Generated with Claude Code
Head is now
9da4b12bc27d011cd0a1405d38e10da6cfc5f17e(pushed without force). Fixes for the two Mediums and the skew Low from Libby's read ata0bd983.encrypt) now returns thefaileddecision instead of rolling back, so the marker commits. A rotation Postgres refuses to write leaves the marker in its own statement after the rollback (rotate decisions only). Tests force both: an encrypt that throws on the new access token, and a trigger that refuses the rotation UPDATE on live Postgres, one pool and two (red before). The other process makes no token call.Hand mutants, each applied and reverted: failure stamped at attempt start, post-call throw rethrown, no marker after a refused rotation write, future stamp always holds, skew boundary inclusive, future stamps ignored; all caught. One survivor, triaged equivalent: writing the marker after a failed revoke write too (
decision.kind !== "keep"instead of"rotate"). A revoke consumes no token, so the extra marker only adds a harmless backoff; I kept rotate-only because that is the case the marker protects.Not covered: process death between the provider's answer and the commit. The transaction dies with the process and nothing records the attempt, so the next caller still replays the token. Closing it needs a write-ahead record of the attempt on a connection that does not wait on the row lock (a separate table), which also changes what concurrent callers see while an attempt is in flight. I did not take that without your say.
Head is now
1118f235b02b937fadb7569f40cbba61651c4353(docs only, no force): service-contract.md names the process-death residual and cites joystick:follow_up:write-ahead-refresh-attempt-record.Head is now
9c7d980165c867bde3ae499b6312498ae707feb1(no force). The fallback marker now covers revoke as well as rotate, so a rotating provider that answered without a refresh token and whose revoke write fails leaves the marker. Live-Postgres test (Joystick, both no-token modes, one pool and two) failed first; the rotate-only mutant fails it, so the earlier "equivalent" judgement is withdrawn. Docs: the window opens when the request is sent, cites the steward ruling and the write-ahead follow-up, periods fixed. Not covered: a failing marker UPDATE on a failed decision (the database error propagates, nothing further to write).