refactor: rewrite BrighterAsyncContext for correctness and modernization#4076
Conversation
|
Thanks @thomhurst I'll take a peek. |
iancooper
left a comment
There was a problem hiding this comment.
Thanks @thomhurst. I was a aware of a couple of cases where we seemed to block the thread, so its great that you managed to track them down. The additional operation is an interesting solution to one of the problems
Code reviewFound 1 issue:
🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Rewrites BrighterAsyncContext and its collaborators (SynchronizationContext, TaskScheduler, TaskQueue, Scope, TaskExtensions, TaskFactoryExtensions) to fix correctness bugs in the AsyncEx-derived implementation and bring the code in line with modern .NET conventions. Correctness fixes: - Outstanding-operations counter no longer leaks when TryAdd fails on a completed queue (previously caused shutdown hangs). - Late Post after the pump shuts down is absorbed silently (TryAdd / CompleteAdding now tolerate both InvalidOperationException and ObjectDisposedException, in the correct catch order). - Late Send after shutdown throws ObjectDisposedException instead of blocking forever on a task that will never run. - Run(Func<Task>) / Run<T>(Func<Task<T>>) wrap the Unwrap proxy in try/finally so OperationCompleted cannot swallow the user's exception. - Execute refuses re-entry via Interlocked.CompareExchange, enforcing the single-thread invariant. - Dispose is idempotent. - GetScheduledTasks takes a ToArray snapshot rather than iterating the live BlockingCollection, and absorbs ObjectDisposedException. Modernization: - All types sealed where appropriate. - Public API tightened: internal on Enqueue / OperationStarted / OperationCompleted / GetScheduledTasks; removed unused ExecuteImmediately and Timeout/OutstandingOperations setters. - Tuple<Task,bool> replaced with ValueTuple on the hot path. - Static lambdas with state parameters eliminate per-Enqueue closure allocations. - ArgumentNullException.ThrowIfNull (under #if NET) with netstandard2.0 fallbacks. - TaskExtensions.WaitAndUnwrapException(CancellationToken) uses Task.WaitAsync on net6+, drops the unreachable-after-Throw code path, and correctly unwraps the inner exception on netstandard2.0. - XML docs rewritten for accuracy; MIT attribution added to TaskExtensions. Tests: - 6 new cases cover post-shutdown Post/Send, double-Dispose, nested Run, concurrent-Execute rejection, and a 1000-iteration Send-after-shutdown stress test. All 31 BrighterSynchronizationContextsTests and all 38 Proactor smoke tests pass. Breaking changes (major bump): - BrighterAsyncContext and BrighterSynchronizationContext are now sealed. - Enqueue, OperationStarted, OperationCompleted, GetScheduledTasks on BrighterAsyncContext went from public to internal. - ExecuteImmediately removed. - BrighterSynchronizationContext.Timeout property removed. - OutstandingOperations setter removed (getter kept).
…yAdd fails BrighterAsyncContext.Enqueue attached the OperationCompleted continuation before calling TryAdd. If TryAdd failed, the counter was balanced manually, but the continuation remained wired to the task. A caller that later inline-executed the stranded task via TryExecuteTaskInline would fire the continuation and decrement the counter a second time, underflowing it and spuriously triggering CompleteAdding while operations were still in flight. Attach the continuation only on the success path. ExecuteSynchronously ensures the continuation still fires exactly once if the task is already complete by the time we attach it.
658d45a to
2387e54
Compare
There was a problem hiding this comment.
Code Health Improved
(1 files improve in Code Health)
Gates Passed
4 Quality Gates Passed
See analysis details in CodeScene
View Improvements
| File | Code Health Impact | Categories Improved |
|---|---|---|
| BrighterAsyncContext.cs | 9.10 → 9.39 | Code Duplication |
Quality Gate Profile: Clean Code Collective
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.
Summary
Rewrites
BrighterAsyncContextand its collaborators insrc/Paramore.Brighter/Tasks/to fix correctness bugs inherited from the AsyncEx-derived original implementation and modernize the code for current .NET conventions. Adds a thorough test suite covering shutdown races, concurrent-Execute, nested Run, and a 1000-iteration stress test.Correctness fixes
Enqueuenow balances the counter whenTryAddfails on a closed queue. The previous code left the counter stuck andRunwould hang forever.Postafter shutdown —BrighterTaskQueue.TryAdd/CompleteAdding/GetScheduledTaskstolerate bothInvalidOperationException(completed-for-adding) andObjectDisposedException(queue disposed), in the correct subclass-first catch order. Stray async continuations posting back afterRunreturns are absorbed silently.Sendafter shutdown — now throwsObjectDisposedExceptioninstead of blocking indefinitely on a task the pump will never execute.Run(Func<Task>)/Run<T>(Func<Task<T>>)— continuation now doestry { WaitAndUnwrapException } finally { OperationCompleted }, soOperationCompletedcannot mask the user's exception.Execute— enforces the single-thread invariant viaInterlocked.CompareExchange, throwingInvalidOperationExceptionon re-entry instead of silently violating it.Dispose— guarded byInterlocked.Exchange(ref _disposed, 1); double-dispose no longer throwsObjectDisposedExceptionfrom the underlyingBlockingCollection.GetScheduledTasks— takes aToArray()snapshot rather than iterating the liveBlockingCollection(which is undocumented against concurrent Add/Take), and absorbsObjectDisposedException.TaskExtensions.WaitAndUnwrapException(CancellationToken)— usesTask.WaitAsync(ct)onnet6+; onnetstandard2.0unwraps viaex.InnerException(the previous code captured the wrappingAggregateExceptioninstead, double-wrapping the thrown exception).Modernization
BrighterAsyncContextandBrighterSynchronizationContextare nowsealed.Enqueue,OperationStarted,OperationCompleted,GetScheduledTasksonBrighterAsyncContextmoved frompublictointernal.Tuple<Task, bool>replaced with(Task, bool)ValueTupleon the hot path.Enqueueclosure allocations.ArgumentNullException.ThrowIfNullunder#if NETwith netstandard2.0 fallbacks.TaskExtensions.ExecuteImmediately,TryExecuteNewThread,BrighterSynchronizationContext.Timeout,OutstandingOperationssetter.BrighterAsyncContextandBrighterSynchronizationContextare nowsealed.Enqueue,OperationStarted,OperationCompleted,GetScheduledTaskswent frompublictointernal.ExecuteImmediatelyremoved.BrighterSynchronizationContext.Timeoutproperty removed.OutstandingOperationssetter removed (getter kept and now returns a correctVolatile.Readof the internal counter; before, the setter was a dead auto-prop that always returned 0).No in-repo consumers use any of the removed/tightened surfaces beyond
Run(...)andCurrent.Test coverage
Added (6 cases):
Post_AfterContextDisposed_DoesNotThrowSend_AfterContextDisposed_ThrowsSend_AfterShutdown_StressIterations_NeverHangsAndOnlyThrowsObjectDisposed— 1000 iterations, 5s bounded wait per iterationDispose_CalledTwice_DoesNotThrowRun_NestedInsideOuterRun_DoesNotDeadlockExecute_CalledConcurrently_ThrowsInvalidOperationExceptionTest plan
netstandard2.0,net8.0,net9.0,net10.0.BrighterSynchronizationContextsTests— 31/31 passed in ~1s.Commits
build: bump OpenTelemetry packages to 1.15.3 for GHSA-g94r-2vxg-569j— CVE fix required for master to restore. Happy to split into a separate PR if preferred.refactor: rewrite BrighterAsyncContext for correctness and modernization— the rewrite itself.Review process
This PR was developed with multiple review passes (code-reuse, code-quality, efficiency, and three thorough correctness passes). Each pass surfaced specific findings that were addressed before the next. Review trail visible in the commit history if useful.