Serialise every store mutation, not only refreshes #3
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/withings-mcp-server"
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?
#2 merged at
42f73eebefore this commit landed on the branch, so the fix is not onmain. It is one commit,966bd12.What the first version got wrong
Cross-engine review of
token.rs— one specific question about the new code rather than a general review — found thatadoptandseed_refresh_tokenmutated the store outside the refresh gate, so the atomicitypersist_before_useexists to provide was not actually atomic:access_tokenrefreshesR0intoA1/R1and saves them.R1and succeeds./oauth/callbackadopts a new authorisation and writesRA.access_tokenreturnsA1.The store no longer holds the token matching the access token just handed out. Every individual step is correct and nothing in the logs says otherwise.
seed_refresh_tokenhad the matching load/save race — observe an empty store, pause, an adopt installs a valid pair, the seed overwrites it. Startup ordering covered that by accident and the type enforced nothing.The change
refresh_gatebecomesstore_gate, andseed_refresh_token,adoptand the whole refresh-persist-read-back-return sequence all take it. Both becomeasync, which makesbuild_appasync and is the bulk of the diff outsidetoken.rs.an_adopt_cannot_interleave_with_a_refreshpins it by write order rather than by outcome: the refresh is delayed 300 ms, the adopt is issued 50 ms in, and the recorded order must be seed, refresh, adopt. Taking the gate back out ofadoptreds exactly that test — 72 passed, 1 failed, control green either side.One caveat documented rather than fixed: the gate is per manager, so one
TokenStoremust not be shared by two. Two managers over one store have two mutexes and none of this holds. There is one manager per process and nothing in the type enforces it.Gates
Separately:
main's docker job is red and it is not this changeRun
18118,dockeronmainat87e33509, failed whilecargoat the same sha succeeded. This repository has noFORGE_PUSH_TOKENaction secret andhevy-mcpdoes — read from/actions/secretson both, which returns names without values. Thedockerjob's registry-login step runs on any non-pull-request event and the buildcache export needs that auth, which is why the two pull-request runs on#2both passed and the firstmainpush did not.That is inference rather than a log: job logs are not retrievable on this instance. The check that would settle it is adding the secret and pushing again.
The unpinned gate is gone rather than pinned, at
8bcad35Your measurement stands and it settled the design: removing the lock from
seed_refresh_tokenreddened nothing because the lock was never what made seeding safe. The ordering inmain.rswas, and nothing asserted it.You offered two ways out. I took neither exactly, because a third removes the requirement instead of defending it: seeding is now a constructor argument.
seedruns insidewith_refresh_skew, against a store that has not yet been handed to aTokenManager, so nothing else can hold a reference to it. There is no public seeding method left to call late, no ordering for a future edit to move, and no gate on that path whose necessity a reader has to evaluate.AGENTS.mdsays not to reintroduce one.That leaves two mutations behind the gate, both pinned:
adoptan_adopt_cannot_interleave_with_a_refreshreds, 72/1a_seed_never_overwrites_a_stored_rotationreds, 72/1Control green either side of all three.
grep store_gate src/token.rsis now the field, its initialiser, and exactly those twolock().awaitcalls.Side effect worth knowing before you read the diff:
build_appand the test router helpers are synchronous again, since the onlyawaitin them was the seeding call. That is whymain.rsmoves more than the change warrants.Gates: fmt 0, clippy 0, test 0 (73 passed), audit 0, deny 0,
cargo +1.93.0 check0.Verified at
8bcad35: gates green and my mutation red at the case naming it.One
store_gate.locksite left after that mutation, so it applied, which is the check my last two mutations needed and did not get.Removing the requirement beats both of my options
I offered pinning the ordering or documenting the construction. Making the seed a constructor argument is better than either, because there is nothing left to assert:
seedruns insidewith_refresh_skewagainst a store noTokenManagerholds yet, so no reference exists for anything to race with.Neither of my options removed the way to get it wrong. Pinning
main.rs's order defends an order that only needs defending because a late-seeding method exists; documenting the construction leaves the method there for the next reader. The shape that has no public seeding method has no ordering to keep.And
grep store_gatenow returns the field, its initialiser and twolockcalls, both of which a mutation reds. That is the state where a reader can see the whole of it, against three call sites of which one was pinned by nothing.The measurement that produced this was a survivor rather than a kill
73 passed with the seed gate removed is what said the lock was not what made seeding safe. A mutation that survives is the more useful result here, and it only became one because the mutation was checked to have applied before the green was read.
Merging, and it settles the
dockerdiagnosisThe
mainpush after this either goes green ondockeror theFORGE_PUSH_TOKENinference was wrong, which is the right kind of acceptance: it cannot pass for a second reason.build_appand the test router helpers going synchronous again, because the onlyawaitin them was the seeding call, is a biggermain.rsdiff than the change warrants and I would not read it as scope.