Skip to content

fix: unsubscribe Elapsed on Dispose for all doubles#184

Merged
Chris-Wolfgang merged 1 commit into
vNextfrom
fix/dispose-hygiene
Jun 25, 2026
Merged

fix: unsubscribe Elapsed on Dispose for all doubles#184
Chris-Wolfgang merged 1 commit into
vNextfrom
fix/dispose-hygiene

Conversation

@Chris-Wolfgang

Copy link
Copy Markdown
Owner

The leak

Now that the Abstractions base (0.14.0) is IDisposable/IAsyncDisposable, each double subscribes to the caller-owned injected IProgressTimer.Elapsed in CreateProgressTimer but never unsubscribes. The timer outlives the double (it's owned by the caller), so the closure — and the double it captures — stays rooted for the timer's lifetime. A test that injects a long-lived ManualProgressTimer across many doubles accumulates dead subscribers.

The fix

In all 6 doubles (TestExtractor, TestLoader, TestTransformer, FaultyExtractor, FaultyLoader, FaultyTransformer):

  • The CreateProgressTimer subscription now stores the handler in a new private Action? _elapsedHandler; field (inside the existing _progressTimerWired guard, so wire-once semantics are unchanged).
  • Each double adds protected override void Dispose(bool disposing) that removes the stored handler from the injected timer's Elapsed and clears the field, then calls base.Dispose(disposing).

The injected _progressTimer is intentionally not disposed — it is caller-owned. The extractors already dispose their owned enumerator in the worker finally, so nothing else needs disposing here.

Public API

The new protected override void Dispose(bool) on each unsealed public double is tracked by PublicApiAnalyzer (RS0016). Six entries added to PublicAPI.Unshipped.txt, file kept canonically sorted:

override Wolfgang.Etl.TestKit.FaultyExtractor<T>.Dispose(bool disposing) -> void
override Wolfgang.Etl.TestKit.FaultyLoader<T>.Dispose(bool disposing) -> void
override Wolfgang.Etl.TestKit.FaultyTransformer<T>.Dispose(bool disposing) -> void
override Wolfgang.Etl.TestKit.TestExtractor<T>.Dispose(bool disposing) -> void
override Wolfgang.Etl.TestKit.TestLoader<T>.Dispose(bool disposing) -> void
override Wolfgang.Etl.TestKit.TestTransformer<T>.Dispose(bool disposing) -> void

Verification

  • dotnet build ETL-Test-Kit.slnx -c Release → 0 warnings / 0 errors, all TFMs (net462; netstandard2.0; net481; net8.0; net10.0).
  • Tests -c Release -f net8.0 → all green (355 = 233 Xunit contract + 122 unit). No new tests added; existing disposal coverage still passes.

Stacked on #183 (fix/source-polish).

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant