Skip to content

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 into
mainfrom
issue-1207-chore-accounts-move-off-the-untagged-interface-cache
Oct 23, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
issue-1207-chore-accounts-move-off-the-untagged-interface-cache

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1207.

module/services/accounts/code/go.mod moves off the untagged pseudo-version onto tagged releases:

github.com/codefly-dev/interface-cache/go/cache  v0.2.2-0.20260926205419-327a6e1d9aba → v0.4.1
github.com/codefly-dev/service-redis/cache       v0.2.0                               → v0.3.0

go mod tidy changes exactly those four go.sum lines.

What the migration actually changed

The API the membership cache calls (New, WithTier, WithOrigin, WithNegativeTTL, SourceFunc, NewPartition, Get, Invalidate, ErrNotFound) compiled unchanged. Capabilities/Declarer and entry format v2 are the driver's side; the driver declares Leases, Notifies (with NoticeBound), Resyncs and Fences, and the stack checks that declaration at New. The real change is behaviour:

  • Fill coordination (needs a code change, and has one). go/cache v0.4 runs every stack in a declared Mode, and its default fill is FillSingleflight: it only leases a miss on the shared layer under FillLease. 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.New now asks for cache.WithMode(cache.FillLease()). The new pure test TestConcurrentMissesOnTwoInstancesLoadTheStoreOnce fails with that line removed (expected 1, actual 2 store 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.
  • Freshness. The default is now TTLBounded(longest tier TTL) = sharedTTL (30s), measured from the origin's Confirmed time instead of a per-tier FreshUntil stamp. The bound stays at the documented 30s ceiling.
  • The race the package doc cited (interface-cache#7, reported from feat(accounts): org membership on a codefly.dev/cache stack; revocation, nonces and rate limits as Redis state #942). In v0.4 a fill whose read raced a notice or a local write undoes its store. That is the StaleRefillAfterInvalidation pitfall, 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 its NoticeBound and the in-process tier is flushed.
  • Entry.Expires, Entry.NoStore, the retention clamp, judge checking Expires first, and observer-panic recovery / WithObserverErrors change 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.go runs cachetest.Run and cachetest.RunStack against the service-redis driver. The client comes from infra.NewRedisClient and the server is the one the pinned cache agent runs. Each namespace gets its own client, as each replica has. ExternalWrite does a plain SET of the driver's value key and Interrupt does a CLIENT KILL of the layer's tracking connection, matching service-redis's own harness at cache/v0.3.0. The test also checks that infra.MembershipCacheLayer is 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 (including NoticeBound, ExternalWrites and Resync), 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 is Declaration/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 used P… and i:…), 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:

  • The exposure is a bounded window of about 30s, which is already inside the ceiling pkg/membership documents for a membership write that skips invalidation. It is not a new kind of failure, only a second way to reach a documented one.
  • A 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.
  • Keeping a legacy key format alive is what the clean-sweep rule exists to prevent: delete, never fence. Old callers break loudly and get fixed at the caller.

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 of cache.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.go package doc: the stack's mode and why it uses FillLease, the staleness bound after an invalidation (the interface-cache#7 caveat replaced), and that Expires, NoStore and 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.mod and this PR.

Rebase order

#1125, #1124, #1076 and #1027 are all still open drafts. main had not moved past 877e8a283 when this was pushed, so nothing needed a rebase. Whichever of them lands after this takes a two-line go.mod/go.sum conflict on lines it does not otherwise change.

Verified locally

  • go build ./..., go vet on the touched packages, goimports -l clean
  • go test -race -count=10 ./pkg/membership/ passes
  • go test -tags pure ./pkg/membership/ ./pkg/infra/ passes
  • the redis-db suite above passes
  • pkg/business cardinality-quota tests (DB-backed, the other named consumer) pass
  • naming gate and scripts/check-boundaries.sh pass

Not verified

  • The full codefly ci run and the CI-only gates, which are left to CI.
  • The full pkg/business and pkg/adapters DB suites (only the cardinality-quota tests ran).
  • A booted graph or a real rolling deploy across the two versions. The rollout claim above comes from reading the key formats, not from running it.

🤖 Generated with Claude Code

… 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>
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 23, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 23, 2026
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 44b9bc4 Oct 23, 2026
25 checks passed
@antoinetoussaint-byte
antoinetoussaint-byte deleted the issue-1207-chore-accounts-move-off-the-untagged-interface-cache branch October 25, 2026 17:07
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.

chore(accounts): move off the untagged interface-cache pseudo-version onto go/cache v0.4.1

1 participant