Refactor UdpSourceBase to use proper Dispose pattern#7
Refactor UdpSourceBase to use proper Dispose pattern#7codegefluester merged 6 commits intofeat/udp-base-sourcefrom
Conversation
…sal in finally block Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>
Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR attempts to improve the disposal pattern in UdpSourceBase<T> by moving _listener.Dispose() from the finally block in ReadContinuousAsync to an overridden Dispose() method. The changes add thread-safety mechanisms including a dispose lock and disposed flag to prevent double disposal.
Changes:
- Moved
_listener.Dispose()from thefinallyblock inReadContinuousAsyncto a newDispose()override - Added thread-safety with
_disposeLockand_disposedflag - Implemented try-finally in
Dispose()to ensurebase.Dispose()is always called
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public override async IAsyncEnumerable<T> ReadContinuousAsync([EnumeratorCancellation] CancellationToken ct = default) | ||
| { | ||
| _isListening = true; | ||
| try | ||
| { | ||
| while (!ct.IsCancellationRequested) | ||
| { | ||
| var result = await _listener.ReceiveAsync(ct); | ||
| var data = result.Buffer; | ||
| // Process the received data and yield telemetry objects | ||
| foreach (var item in ProcessData(data)) | ||
| { | ||
| yield return item; | ||
| } | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| _isListening = false; | ||
| _listener.Dispose(); | ||
| } |
There was a problem hiding this comment.
Critical race condition: The removal of _listener.Dispose() from the finally block creates a scenario where the UdpClient could remain undisposed if ReadContinuousAsync completes naturally (via cancellation) but Dispose() is never called. This is a resource leak.
Additionally, there's a dangerous race condition where Dispose() can be called while _listener.ReceiveAsync(ct) on line 38 is in progress. When _listener.Dispose() executes on line 62 while a receive operation is active, it can cause unpredictable behavior including ObjectDisposedException being thrown into the async enumerable consumer.
The codebase pattern (see MemoryMappedFileSource.cs:38-42, TrackmaniaMemoryMappedFileSource.cs:125-131, and FileWatcherSourceBase.cs:205-212) is to dispose resources in the finally block of ReadContinuousAsync, not in a separate Dispose() method. This ensures proper cleanup regardless of how the async enumerable terminates.
Recommendation: Keep _listener.Dispose() in the finally block where it was originally. The override Dispose() method should call base.Dispose() only, and can optionally set a flag to prevent re-initialization if needed.
| private readonly object _disposeLock = new object(); | ||
| private bool _disposed; |
There was a problem hiding this comment.
The _disposed flag check doesn't prevent ReadContinuousAsync from using a disposed _listener. If Dispose() is called while ReadContinuousAsync is running, the _disposed flag will be set to true, but the running async method will continue to attempt to use _listener on line 38, which has just been disposed on line 62.
There's no check of the _disposed flag before or during ReadContinuousAsync execution. Consider adding a check at the start of ReadContinuousAsync to throw ObjectDisposedException if already disposed, and potentially in the while loop to exit gracefully if disposal occurs during iteration.
|
|
||
| public override void Dispose() | ||
| { | ||
| lock (_disposeLock) |
There was a problem hiding this comment.
The _disposeLock is used to synchronize Dispose() calls, but it doesn't protect against concurrent access between Dispose() and ReadContinuousAsync. The lock only prevents multiple Dispose() calls from running simultaneously, but doesn't prevent Dispose() from running while ReadContinuousAsync is accessing _listener on line 38.
If the intent is to allow Dispose() to be called from a different thread while ReadContinuousAsync is running (which is the typical pattern), then the _listener resource should not be disposed in the Dispose() method at all - it should only be disposed in the finally block of ReadContinuousAsync. The Dispose() method could instead set a cancellation flag that ReadContinuousAsync can check.
| _disposed = true; | ||
| try | ||
| { | ||
| _listener?.Dispose(); |
There was a problem hiding this comment.
The null-conditional operator is unnecessary here because _listener is initialized in the constructor (line 28) and is never set to null. If this is meant to handle scenarios where the constructor throws before initialization, note that partial construction is not a concern since an exception in the constructor would prevent the object from being created and Dispose wouldn't be called.
However, if the recommendation from the first comment is followed (to move disposal back to the finally block), then this line should be removed entirely from the Dispose() method.
| _listener?.Dispose(); | |
| _listener.Dispose(); |
| public override void Dispose() | ||
| { | ||
| lock (_disposeLock) | ||
| { | ||
| if (!_disposed) | ||
| { | ||
| _disposed = true; | ||
| try | ||
| { | ||
| _listener?.Dispose(); | ||
| } | ||
| finally | ||
| { | ||
| base.Dispose(); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential double-disposal issue: With the current implementation, if ReadContinuousAsync completes normally and then Dispose() is called, _listener will be disposed twice - once in the new Dispose() override (line 62) and potentially again if the pattern in the documentation is followed where finally blocks also dispose resources.
Additionally, this conflicts with the planned API refactoring documented in docs/telemetry-source-api-refactoring.md (lines 97-153), which shows that _listener.Dispose() should remain in the finally block even after the refactoring. The current change makes that future refactoring more complex.
UdpClient.Dispose() is idempotent (can be called multiple times safely), but this pattern is still confusing and unnecessary.
* Added F1 2025 packets * Added F1 2024 types * Added F1 2023 structs * Added F1 packets and fixtures * Added F1 2025 packets * Added F1 2024 types * Added F1 2023 structs * Added F1 packets and fixtures * Remove legacy F1 implementation workflow file * Remove debug tooling for F1 * Add F1 fixture files to test project * Add refactoring plan for telemetry source interface * Remove debug logging * Added test cases for F1 packet parsing (super basic) * Added tests for event details parsing in F1 2025 * Tested F1 recording and reading of session recording * Update GamesDat.Demo.Wpf/Resources/Styles.xaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Björn Kaiser <masterchiefs2002@hotmail.com> * Update GamesDat.Demo.Wpf/ViewModels/IRealtimeSource.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Björn Kaiser <masterchiefs2002@hotmail.com> * [WIP] WIP Address feedback on adding source for F1 games (#5) * Initial plan * Fix: Change m_zoneFlag from byte to sbyte in F12025 MarshalZone Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Apply BufferSize to UDP socket ReceiveBufferSize (#6) * Initial plan * Apply BufferSize to socket's ReceiveBufferSize property Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Refactor UdpSourceBase to use proper Dispose pattern (#7) * Initial plan * Refactor UdpSourceBase to use Dispose pattern instead of manual disposal in finally block Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Add thread-safety to Dispose method in UdpSourceBase Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Move base.Dispose() inside lock for better thread-safety Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Set disposed flag before disposal operations for better exception safety Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Add try-finally to ensure base.Dispose() is always called Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Make LiveryColour struct fields public for deserialization access (#8) * Initial plan * Make LiveryColour fields public to allow access Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Remove unused BytesToStruct method from F1RealtimeTelemetrySource (#9) * Initial plan * Remove unused BytesToStruct method from F1RealtimeTelemetrySource Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * [WIP] WIP address feedback from review on adding source for F1 games (#10) * Initial plan * Remove .claude/settings.local.json files from repository * Push changes - removed .claude/settings.local.json files Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Remove .claude/settings.local.json files and update .gitignore pattern --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Fix unsafe fixed buffer pointer handling in F1 telemetry extensions (#11) * Initial plan * Fix unsafe fixed buffer pointer handling and UTF-8 marshaling - Fix F1TelemetryFrameExtensions to use Unsafe.Read<T> and direct pointer access for fixed buffers - Change F12025 Participant m_name from ByValTStr to byte array for proper UTF-8 marshaling - UdpSourceBase Dispose() implementation already correct (from previous PR) Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Add Name property helper for UTF-8 decoded participant name Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Fix AccessViolationException in GetPacket by using Unsafe.AsPointer Use Unsafe.AsPointer(ref frame.RawData[0]) instead of Unsafe.Read to properly get a pointer to the fixed buffer. This resolves both the CS0213 compilation error and the runtime AccessViolationException. Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Fix frame extension and test --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> Co-authored-by: Björn Kaiser <bjoern@codegefluester.de> --------- Signed-off-by: Björn Kaiser <masterchiefs2002@hotmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Addresses feedback to replace manual disposal in finally block with proper IDisposable pattern.
Changes
_listener.Dispose()fromReadContinuousAsyncfinally block, now properly overridesDispose()methodbase.Dispose()executes even if listener disposal throwsBefore
After
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.