Add dispose exception observability via UnobservedDisposeException#16
Add dispose exception observability via UnobservedDisposeException#16imurashka wants to merge 1 commit intoPlaytikaOSS:mainfrom
Conversation
src/ControllersTree/Core/Utils/ControllerCompositeDisposable.cs
Outdated
Show resolved
Hide resolved
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
📝 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)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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
What changed
Added
ControllerBase.UnobservedDisposeException— a static event onControllerBase. Projects can subscribe to it on app start and route dispose exceptions to their own logger, crashlytics, etc.How exception aggregation works
Added
ExecuteAndCollectExceptionsutility method (internal static onControllerBase) that:AggregateExceptiondown to leaf exceptionsUnobservedDisposeExceptionfor each leaf that wasn't already raisedControllerDisposeAggregateExceptionis an internal marker — its children were already raised, so they won't be raised again in outer compositesThis guarantees each leaf exception is raised via event exactly once, even in deeply nested composite hierarchies.
Both
ControllerBase.DisposeInternal()andControllerCompositeDisposable.DisposeMany()use this shared method.Other changes
ControllerBase.DisposeInternal— if_disposables.Dispose()threw,ListPool<IController>.Releaseand_state = Disposedwere skipped. Now wrapped intry/finally.DisposeManywhen collection is empty.ControllerCompositeDisposablecreation —Initialize()always callsAddDisposable(_lifetimeTokenSource), so it's created every time anyway. Simpler to just create it eagerly.Tests
ControllerBaseExceptionsTests— 8 tests forExecuteAndCollectExceptionscovering plain exceptions,AggregateException,ControllerDisposeAggregateException, nested mixed hierarchies, no handler scenarioControllerDisposeAggregateExceptionREADME
Added "Handling Dispose Exceptions" section to README.md with setup example.
Summary by CodeRabbit
Release Notes
New Features
Documentation
Tests