fix(storage,di): invalidate the entity cache after commit; populate framework plumbing once (#450, #451) - #465
Merged
JabbaKadabra merged 1 commit intoJul 26, 2026
Conversation
…ramework plumbing once Two infrastructure defects that both come from a registration or ordering detail rather than from the logic on top of it. IEntityCache invalidation ran inside the still-open transaction. CanUseCache suppresses the cache for the *writing* flow only — ambient.IsActive is AsyncLocal — so between that invalidation and the commit a reader on another flow could miss the cache, read the pre-commit row and Set() it back, with nothing invalidating afterwards. The stale entity was then served until the 5-minute TTL expired, and because it carries a stale UpdatedAt — the optimistic-concurrency token — every write made against it failed the pre-check in UpdateCoreAsync. A millisecond race became minutes of failing writes on hot, cached, ingest-mutated entities like IAgent, which is the leading suspect for the agent-row conflicts seen during trace ingestion. InvalidateCacheEntry (and a new InvalidateCache for the whole-cache case) now invalidate immediately *and* again on commit, queued through RegisterPostCommit — the same deferral Notify already uses for change events. Every write path routes through them, including the bypass writes in AgentRepository and ArchivableRepository, so no site can drift back. Separately, RegisterServiceCollection builds a fresh ServiceCollection per call. AddHttpClient shares its plumbing through TryAddEnumerable, which dedupes only within one collection, so the four modules that call it (Api, Application, Licensing, Proxy) each contributed their own IHttpMessageHandlerBuilderFilter — and each filter's logging handler wrapped every outgoing request. That is the exact 4x duplication seen on the proxy's hottest path: one request, four identical sets of log lines with elapsed times differing by 0.01 ms. Populate now drops descriptors whose (service, implementation, lifetime) triple an earlier call already registered in the same container. An identical type-based registration can never mean two different things; genuine multi-registrations use distinct implementation types, and instance/factory descriptors — which carry the per-name client configuration — are untouched. Refs #450, #451 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX21BWN6krigPSJJQVNexi
JabbaKadabra
deleted the
fix/cache-invalidation-and-proxy-logging-450-451
branch
July 26, 2026 15:52
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.
Summary
Two infrastructure defects that each come from a registration or ordering detail rather than the logic on top of it.
#450 —
IEntityCacheinvalidation ran inside the still-open transaction.CanUseCachesuppresses the cache for the writing flow only (ambient.IsActiveisAsyncLocal), so between that invalidation and the commit a reader on another flow could miss the cache, read the pre-commit row andSet()it back — with nothing invalidating afterwards. The stale entity was then served until the 5-minute TTL expired, and it carries a staleUpdatedAt, which is the optimistic-concurrency token: every write made against it failed the pre-check inUpdateCoreAsync. A millisecond race became minutes of failing writes on hot, cached, ingest-mutated entities likeIAgent.#451 — every upstream proxy request was logged 4×.
RegisterServiceCollectionbuilds a freshServiceCollectionper call, andAddHttpClientshares its plumbing throughTryAddEnumerable, which dedupes only within one collection. The four modules that call it (Api, Application, Licensing, Proxy) therefore each contributed anIHttpMessageHandlerBuilderFilter, and each filter's logging handler wrapped every request — exactly the four identical sets of log lines with elapsed times 0.01 ms apart.Closes #450
Closes #451
Changes
AbstractRepository.InvalidateCacheEntry(and a newInvalidateCachefor the whole-cache case) invalidate now and again on commit, queued throughAmbientDbContext.RegisterPostCommit— the same deferralNotifyalready uses for change events.ArchivableRepository(archive/unarchive) andAgentRepository(SetCurrentVersionIdAsync, and the agent-version cache on create). Nocache?.Invalidate(...)call sites remain outside them.AutofacExtensions.RegisterServiceCollectiondrops descriptors whose (service, implementation, lifetime) triple an earlier call already populated into the same container. An identical type-based registration can never mean two different things; genuine multi-registrations use distinct implementation types, and instance/factory descriptors — which is what carries each named client's configuration — are left exactly as written.Verification
Both regression tests were confirmed to fail with the fix reverted and pass with it:
CachedRepositoryTests.UpdateAsync_WhenAConcurrentReaderRepopulatesMidTransaction_TheEntryIsDroppedAfterCommit— reads the row from a flow withExecutionContext.SuppressFlow()(so the ambientAsyncLocalis not inherited and the read repopulates the cache), then asserts the commit clears it. This tests the mechanism rather than provider isolation, since the EF in-memory provider has no real transaction.HttpClientRegistrationTests(Proxy.Tests) — four modules' worth ofAddHttpClient, asserting one filter of each kind, plus that every named client keeps its own configuration.AutofacExtensionsTests(Common.Tests) — dedupe, distinct implementations preserved, instance registrations untouched.Full backend suite green: Common 187, Storage 781, Domain 378, Application 474 (6 skipped), Api 471, Proxy 85, Infrastructure 53, Licensing 61, Messaging 9, Serialization 63. Build: 24 projects, 0 warnings.
Docs:
docs/database.mdgains a section on why invalidation happens twice;docs/architecture.mddocuments theRegisterServiceCollectiondedupe. Two[Unreleased] → Fixedchangelog entries.🤖 Generated with Claude Code
https://claude.ai/code/session_01UX21BWN6krigPSJJQVNexi