fix(subscriptions): prevent NRE when Ack races with Resubscribe#540
fix(subscriptions): prevent NRE when Ack races with Resubscribe#540alexeyzimarev merged 2 commits intodevfrom
Conversation
During resubscribe, DisposeCommitHandler() sets CheckpointCommitHandler to null. If the AsyncHandlingFilter worker thread is still processing a message, it calls Ack() → CheckpointCommitHandler!.Commit() and hits a NullReferenceException. The NRE cascades through Nack → Dropped → resubscribe in an infinite loop. Fix: capture CheckpointCommitHandler into a local variable and return early if null, using the standard pattern for concurrent null races. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Review Summary by QodoPrevent NRE when Ack races with Resubscribe
WalkthroughsDescription• Prevent NullReferenceException when Ack races with Resubscribe • Capture CheckpointCommitHandler locally before null check • Add deterministic test reproducing the race condition • Protect against concurrent handler disposal during message processing Diagramflowchart LR
A["AsyncHandlingFilter worker<br/>processes message"] -->|calls| B["Ack()"]
C["Resubscribe triggered"] -->|calls| D["DisposeCommitHandler<br/>nulls handler"]
B -->|reads| E["CheckpointCommitHandler<br/>local variable"]
D -->|concurrent write| F["CheckpointCommitHandler = null"]
E -->|null check| G["Return early if null<br/>prevents NRE"]
F -.->|race condition| B
File Changes1. src/Core/src/Eventuous.Subscriptions/EventSubscriptionWithCheckpoint.cs
|
Code Review by Qodo
1.
|
src/Core/test/Eventuous.Tests.Subscriptions/ResubscribeOnHandlerFailureTests.cs
Outdated
Show resolved
Hide resolved
Test Results 66 files + 44 66 suites +44 44m 39s ⏱️ + 30m 14s Results for commit aa1455a. ± Comparison against base commit 4ba72de. This pull request removes 5 and adds 16 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
- Use RunContinuationsAsynchronously on all TaskCompletionSource instances to prevent inline continuations altering the cross-thread race behavior - Update test comments to describe current behavior instead of referencing the old buggy code and line numbers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Ack()now capturesCheckpointCommitHandlerinto a local variable and returns early if null, preventing NRE whenDisposeCommitHandler()nulls it concurrently during resubscribeAsyncHandlingFilterworker thread callsAck()→CheckpointCommitHandler!.Commit()afterResubscribe()has already set the handler to null viaDisposeCommitHandler(), causing a NRE cascade (NRE → Nack → Dropped → resubscribe → repeat)Should_not_throw_nre_when_ack_races_with_resubscribethat deterministically reproduces the race using a slow handler + triggered dropTest plan
Assert.Failwith NRE atNackline 107)🤖 Generated with Claude Code