Repository navigation
chore(accounts): go/cache v0.4.1 and service-redis/cache v0.3.0 — off the untagged pseudo-version - #1210
Merged
antoinetoussaint-byte merged 1 commit intoOct 23, 2026
Conversation
… the untagged pseudo-version accounts depended on interface-cache by a 2026-09-26 pseudo-version on the v0.2.x line and on the v0.2.0 redis driver. It now requires the tagged go/cache v0.4.1 and service-redis/cache v0.3.0. The API the membership cache calls compiled unchanged; what moved is behaviour. go/cache v0.4 runs every stack in a declared mode, and its default fill is singleflight: the stack leases a miss on the shared layer only under FillLease. The v0.2 stack leased every miss, which is what made a missing membership reach the store once across replicas (#927). The stack now asks for FillLease explicitly, and a pure test fails without it (two store loads instead of one). The package doc no longer cites interface-cache#7 as an open race: a fill whose read raced a notice now undoes its store (the StaleRefillAfterInvalidation pitfall), so after an invalidation another replica is stale until the notice arrives, with memoryTTL the bound only while a dropped tracking connection is undetected. The redis-db suite runs the codefly.dev/cache conformance suite (cachetest.Run and RunStack) against the service-redis driver over the client infra.NewRedisClient opens, on the server the pinned agent runs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Oct 23, 2026
antoinetoussaint-byte
deleted the
issue-1207-chore-accounts-move-off-the-untagged-interface-cache
branch
October 25, 2026 17:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1207.
module/services/accounts/code/go.modmoves off the untagged pseudo-version onto tagged releases:go mod tidychanges exactly those fourgo.sumlines.What the migration actually changed
The API the membership cache calls (
New,WithTier,WithOrigin,WithNegativeTTL,SourceFunc,NewPartition,Get,Invalidate,ErrNotFound) compiled unchanged.Capabilities/Declarerand entry format v2 are the driver's side; the driver declares Leases, Notifies (withNoticeBound), Resyncs and Fences, and the stack checks that declaration atNew. The real change is behaviour:Mode, and its default fill isFillSingleflight: it only leases a miss on the shared layer underFillLease. The v0.2 stack leased every miss, and that lease is how a missing membership reaches the store once across replicas (the Adopt codefly.dev/cache for org-membership caching; keep revocation, nonces and rate limits on plain Redis as state #927 acceptance).membership.Newnow asks forcache.WithMode(cache.FillLease()). The new pure testTestConcurrentMissesOnTwoInstancesLoadTheStoreOncefails with that line removed (expected 1, actual 2store loads, 3/3 runs). So a bump of the version string alone would have silently regressed this, and only the Docker-backed redis-db suite would have caught it.TTLBounded(longest tier TTL)=sharedTTL(30s), measured from the origin'sConfirmedtime instead of a per-tierFreshUntilstamp. The bound stays at the documented 30s ceiling.StaleRefillAfterInvalidationpitfall, and it now passes against this driver. After an invalidation, another replica is stale until the notice arrives (one round trip on a live tracking connection).memoryTTL(5s) is the bound only while a silently dropped tracking connection is still undetected, and the driver reports that gap within itsNoticeBoundand the in-process tier is flushed.Entry.Expires,Entry.NoStore, the retention clamp,judgecheckingExpiresfirst, and observer-panic recovery /WithObserverErrorschange nothing here. The membership origin sets neither field (a role carries no validity of its own, and every store answer is one to keep), and the stack registers no observer. The package doc now says so explicitly.Contract suite against this consumer's layer
pkg/redisintegration/cachetest_test.gorunscachetest.Runandcachetest.RunStackagainst the service-redis driver. The client comes frominfra.NewRedisClientand the server is the one the pinnedcacheagent runs. Each namespace gets its own client, as each replica has.ExternalWritedoes a plainSETof the driver's value key andInterruptdoes aCLIENT KILLof the layer's tracking connection, matching service-redis's own harness atcache/v0.3.0. The test also checks thatinfra.MembershipCacheLayeris the same driver and declares the same capabilities as the harness layer. The only difference is the fixed key prefix, which the suite replaces with a namespace per test.Local run (
go test -count=1 -p 1 -v ./pkg/redisintegration/, store and cache booted by codefly over Docker): the whole redis-db package passed, 15 top-level tests, 120 conformance subtests passed. That covers Layer, Declaration, Leaser, Notifier (includingNoticeBound,ExternalWritesandResync), Fencer, Partitions,ExternalWriteEvictsMemory,ResyncFlushesMemory, Modes (Freshness, Write, Fill including Lease and VersionFenced, Failure, Refusals), all 12 Pitfalls, and Simulation over seeds 1–6 plus the 4 regression seeds. The one skip isDeclaration/ByteBudget, because the driver deliberately declares no byte budget (maxmemory is the server's configuration). Nothing in the suite failed here.Rolling deploy: a bounded staleness window, no shim on purpose
While replicas on the previous release and replicas on this one run side by side, a membership invalidation does not cross between them, in either direction. An invalidation on an old replica does not evict a new replica's copies, and an invalidation on a new replica does not evict an old replica's copies. Either way, a replica can serve a stale role for up to
sharedTTL(30s) after the change, until its copy ages out. Once every replica runs this release, the window closes.Why it happens: the two versions do not share a keyspace. go/cache v0.3+ starts every layer key with its key format (
3P…for copies,3i:…for generations, where v0.2 usedP…andi:…), and driver v0.3.0 stores entry format v2. Neither version reads or bumps the other's keys.No cross-version shim was added, deliberately:
pkg/membershipdocuments for a membership write that skips invalidation. It is not a new kind of failure, only a second way to reach a documented one.LegacyMembershipKeyEvictor-style shim could only cover one direction (new replicas also deleting the old-format generation key). An old replica can never learn the new layout. Closing half the window while hard-coding two dead key layouts is worse than the window.This lives in the PR body, not in the package doc, because the package doc describes what is permanently true and this window exists only during this one deploy.
Reuse
Everything here is the platform's own: the interface's mode system (
WithMode(FillLease())), its conformance suite, and the driver's harness shape. Nothing new was built beside them.Classification
A fix at the place that owns the behaviour: the dependency floor in accounts'
go.mod, and the membership stack's own mode. No hack, no stopgap.pkg/infra/redis.go's existing STOPGAP (#927, building the layer directly instead ofcache.Open) is untouched. It depends on the redis agent pin, which is #1206's, and this PR does not touch the cache service manifest.Docs
module/services/accounts/TESTING.md: the redis-db target also runs the codefly.dev/cache conformance suite against the driver.pkg/membership/membership.gopackage doc: the stack's mode and why it usesFillLease, the staleness bound after an invalidation (the interface-cache#7 caveat replaced), and thatExpires,NoStoreand observers are unused.The module has no separate record of dependency floors (no changelog, no floors table; #1202 recorded its sdk-go floor the same way), so the floor is
go.modand this PR.Rebase order
#1125, #1124, #1076 and #1027 are all still open drafts.
mainhad not moved past877e8a283when this was pushed, so nothing needed a rebase. Whichever of them lands after this takes a two-linego.mod/go.sumconflict on lines it does not otherwise change.Verified locally
go build ./...,go veton the touched packages,goimports -lcleango test -race -count=10 ./pkg/membership/passesgo test -tags pure ./pkg/membership/ ./pkg/infra/passespkg/businesscardinality-quota tests (DB-backed, the other named consumer) passscripts/check-boundaries.shpassNot verified
codefly ci runand the CI-only gates, which are left to CI.pkg/businessandpkg/adaptersDB suites (only the cardinality-quota tests ran).🤖 Generated with Claude Code