Safer controller dispose: preserve state and aggregate exceptions#16
Safer controller dispose: preserve state and aggregate exceptions#16imurashka wants to merge 1 commit into
Conversation
eldarmguseynov
left a comment
There was a problem hiding this comment.
Thank you for the fix. What do you think about optimizations around disposables?
I will be honest with you, I didn't think about optimization here too much. Right now, it looks pretty optimized, as I see. Espesially after small tweaks you proposed in method DisposeMany and ControllerBase class. |
|
@eldarmguseynov hello! Is there any chance that you will merge this anytime soon? |
|
Related to https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1065 Dispose should not throw any exception. As for the use of Debug.LogError, many projects use their own logger, and we avoided using Debug.LogError in the controllers core. The idea of a lazy composite disposable object is good, and we can use pooling instead of new. @eldarmguseynov what do you think? |
|
hey, @perepechenko, thanks for the review! so here is what i changed based on it: Debug.LogException removed from core. replaced with a static event went with static event on about lazy composite disposable — i added it, then removed it. about pooling composite disposable — i think it makes sense but want to do it in a separate PR to keep this one focused on the exception handling change. also fixed a bug in |
db32819 to
d4c33eb
Compare
d4c33eb to
43701e6
Compare
|
Warning Review limit reached
Next review available in: 52 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds disposal exception aggregation and handling across controller disposal paths: new ControllerDisposeAggregateException, utilities to execute and flatten exceptions (ExecuteAndCollectExceptions, CollectExceptions), event UnobservedDisposeException, integration into DisposeInternal and ControllerCompositeDisposable, expanded unit tests, metadata, and an added README section (duplicated). Changes
Sequence DiagramsequenceDiagram
participant Caller as Client
participant CB as ControllerBase
participant Pool as ListPool<Exception>
participant ECAE as ExecuteAndCollectExceptions
participant COL as CollectExceptions
participant Handler as UnobservedDisposeExceptionHandler
Caller->>CB: Dispose()
CB->>Pool: Rent exception list
CB->>ECAE: ExecuteAndCollectExceptions(list, cancel lifetime token)
ECAE->>ECAE: run action
ECAE-->>COL: exception
COL->>COL: flatten AggregateException -> collect leaves
COL->>Handler: raise UnobservedDisposeException for each new leaf
Handler-->>COL: handler runs
COL-->>ECAE: return with leaves collected
CB->>ECAE: ExecuteAndCollectExceptions(list, dispose composite)
ECAE->>ECAE: run action
ECAE-->>COL: exception
COL->>COL: flatten & collect leaves, raise events as needed
COL-->>ECAE: return
alt list not empty
CB->>CB: throw ControllerDisposeAggregateException(list)
end
CB->>CB: finally: release children, set state Disposed
CB->>Pool: Return exception list
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/ControllersTree/Tests/ControllersWithResultBaseTests.cs (1)
218-228:⚠️ Potential issue | 🟡 MinorAssertion message is inconsistent with the expected exception type.
The test now catches
AggregateExceptionbut the assertion message on line 227 still says "TestControllersException expected". Consider updating the message for clarity:📝 Suggested fix
- Assert.IsTrue(exceptionThrown, "TestControllersException expected"); + Assert.IsTrue(exceptionThrown, "AggregateException expected");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ControllersTree/Tests/ControllersWithResultBaseTests.cs` around lines 218 - 228, The assertion message is misleading: the catch now handles AggregateException but Assert.IsTrue(exceptionThrown, "TestControllersException expected") still references TestControllersException; update the assertion message in ControllersWithResultBaseTests (the test method containing the catch for AggregateException) to reflect AggregateException (e.g., "AggregateException expected") or change the test to assert the specific exception type via Assert.Throws if you prefer a stronger check; locate the Assert.IsTrue(exceptionThrown, ...) and replace the message accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/ControllersTree/Core/Controllers/ControllerBase.Exceptions.cs`:
- Around line 16-19: RaiseUnhandledDisposeException currently invokes
UnobservedDisposeException directly which lets a subscriber exception escape and
abort CollectExceptions/DisposeMany; change it to retrieve
UnobservedDisposeException?.GetInvocationList() and invoke each delegate in a
try/catch so subscriber exceptions are caught and do not propagate (optionally
log or aggregate them) ensuring no exception escapes
RaiseUnhandledDisposeException and the exception-collection loop in
CollectExceptions can continue. Include references to
UnobservedDisposeException, RaiseUnhandledDisposeException, CollectExceptions,
and DisposeMany when making the change.
---
Outside diff comments:
In `@src/ControllersTree/Tests/ControllersWithResultBaseTests.cs`:
- Around line 218-228: The assertion message is misleading: the catch now
handles AggregateException but Assert.IsTrue(exceptionThrown,
"TestControllersException expected") still references TestControllersException;
update the assertion message in ControllersWithResultBaseTests (the test method
containing the catch for AggregateException) to reflect AggregateException
(e.g., "AggregateException expected") or change the test to assert the specific
exception type via Assert.Throws if you prefer a stronger check; locate the
Assert.IsTrue(exceptionThrown, ...) and replace the message accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f9adf36d-c9e1-4686-8b14-0beced5df591
📒 Files selected for processing (10)
README.mdsrc/ControllersTree/Core/AssemblyInfo.cssrc/ControllersTree/Core/Controllers/ControllerBase.Exceptions.cssrc/ControllersTree/Core/Controllers/ControllerBase.Exceptions.cs.metasrc/ControllersTree/Core/Controllers/ControllerBase.cssrc/ControllersTree/Core/Utils/ControllerCompositeDisposable.cssrc/ControllersTree/Tests/ControllerBaseExceptionsTests.cssrc/ControllersTree/Tests/ControllerBaseExceptionsTests.cs.metasrc/ControllersTree/Tests/ControllerCompositeDisposableTests.cssrc/ControllersTree/Tests/ControllersWithResultBaseTests.cs
43701e6 to
489a8a5
Compare
|
@eldarmguseynov @perepechenko hey guys! Any chance you will merge this to main? |
140f184 to
5cc5866
Compare
|
Changes vs main:
|
|
@eldarmguseynov @perepechenko hey! cleaned up this PR as we discussed on a call |
bae004a to
38bc520
Compare
A controller could stay in a non-Disposed state when a disposable threw during dispose, leaving the pooled child list unreleased. A single dispose exception was also thrown unwrapped, so callers had to handle different exception shapes.
Dispose now always completes the cleanup and always surfaces dispose exceptions as one AggregateException. The controller ends in Disposed, and callers handle a single exception type.