Skip to content

fix: guard best-effort cache writes and ref-count hub channel membership - #177

Merged
ivanball merged 2 commits into
mainfrom
fix/bughunt-cache-guards-hub-refcount
Jul 30, 2026
Merged

fix: guard best-effort cache writes and ref-count hub channel membership#177
ivanball merged 2 commits into
mainfrom
fix/bughunt-cache-guards-hub-refcount

Conversation

@ivanball

Copy link
Copy Markdown
Owner

Fixes four findings from the 2026-07-30 bug hunt (Common-side items): H15, L1, M28, H13.

Changes

  • H15 - CachingCommandDecorator: the post-commit RemoveByPrefixAsync invalidation is now wrapped in a guarded try/catch that logs a warning and swallows. A best-effort cache invalidation must never turn a committed command into a failure response (which invites a duplicate client retry).
  • L1 - same call site: the invalidation now runs with CancellationToken.None instead of the request token, so a client disconnect right after commit no longer cancels the invalidation and leaves stale cache for everyone else.
  • M28 - CachingQueryDecorator: the post-read cache populate (SetAsync) is guarded the same way (OCE still propagates via an exception filter), so a transient cache-store blip no longer fails a query whose handler already produced a valid result.
  • H13 - NotificationHubService: channel membership is now reference-counted via a new internal ChannelReferenceCounter. The server-side JoinChannel fires only on the 0-to-1 transition and LeaveChannel only on 1-to-0, honoring the documented multicast contract (an always-mounted listener and a page can observe the same channel; navigating away from the page no longer silently kills the listener's subscription for the rest of the circuit). Reconnect replays the counter snapshot. Public API surface unchanged.

Both decorators now take a required ILogger<TSelf> constructor parameter (repo idiom, DI-resolved via Scrutor decoration; no production call-site changes). This is a constructor change on public types, so the next release is a minor bump.

Tests

8 new tests including two regression locks (HandleAsync_SuccessfulCacheInvalidatingCommand_InvalidatesWithNonCancellableToken, Release_WithOutstandingRefs_DoesNotSignalServerLeave); 13 existing construction sites moved to NullLogger<...>.Instance. Full solution: 2523 passed, 0 failed.

🤖 Generated with Claude Code

ivanball and others added 2 commits July 30, 2026 12:42
H15/L1: CachingCommandDecorator now invalidates on CancellationToken.None
and swallows plus logs invalidation failures, so a committed command is
never turned into a failure by a cache blip or a client disconnect.

M28: CachingQueryDecorator guards the post-read cache populate, so a cache
fault after a successful read no longer fails the query. Only the populate
is guarded; the read paths and real cancellation still propagate.

H13: NotificationHubService ref-counts channel membership instead of using
set semantics, so the first leaver no longer cuts the channel off for every
other subscriber in the circuit. Public API surface is unchanged.

Both caching decorators take an ILogger<TSelf> third constructor argument,
matching the ValidatingCommandDecorator idiom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ators

The required ILogger<TSelf> parameter added in the previous commit broke the
consumer source build: MMCA.Helpdesk hand-constructs both caching decorators
in TicketCacheInvalidationTests, and its own CI builds against the released
v1.133.0 packages where the 3-argument constructor does not exist, so a
required-only parameter deadlocks the two repos.

Both decorators keep the 3-argument primary constructor and regain a public
2-argument overload chaining to it with NullLogger<TSelf>.Instance, so both
call shapes compile.

Verified that container resolution still selects the logger-bearing
constructor, so production keeps emitting cache-failure warnings rather than
silently binding the no-op logger. CachingDecoratorConstructorSelectionTests
pins that behavior for both decorators and pins the 2-argument call shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ivanball
ivanball merged commit 0fa488c into main Jul 30, 2026
24 of 25 checks passed
@ivanball
ivanball deleted the fix/bughunt-cache-guards-hub-refcount branch July 30, 2026 17:12
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.

1 participant