Skip to content

Add RAII lease handle (LeaseScopeAsync) + leak metric; move Smtp.Pool tests out of CI - #3

Merged
marklauter merged 7 commits into
mainfrom
feat/lease-handle
Jun 15, 2026
Merged

Add RAII lease handle (LeaseScopeAsync) + leak metric; move Smtp.Pool tests out of CI#3
marklauter merged 7 commits into
mainfrom
feat/lease-handle

Conversation

@marklauter

Copy link
Copy Markdown
Owner

Summary

Two independent, additive changes on top of the already-merged metrics modernization:

  1. RAII lease handle — an ergonomic, leak-resistant way to consume the pool.
  2. Smtp.Pool test relocation — moves the sample's test projects under samples/ and out of the CI gate, plus some CI housekeeping.

No breaking changes in this PR. (The metrics rename to OTEL conventions already landed on main as metrics modernization; this branch builds on it.)

1. RAII lease handle (LeaseScopeAsync)

using var lease = await pool.LeaseScopeAsync(cancellationToken);
await lease.Item.SendAsync(message, cancellationToken);
  • LeaseScopeAsync (extension over the existing public LeaseAsync/Release) returns a disposable Lease<TPoolItem>; the using returns the item on scope exit.
  • Idempotent, single-shot return (interlocked) — immune to the double-release footgun.
  • If the pool was disposed while the item was out on lease, the lease disposes the orphaned item itself (closes the "dispose leased items yourself" footgun).
  • Leak observability: a Lease garbage-collected without being disposed is recorded on a new pool.leases.leaked counter (published on the MSL.Pool meter, tagged pool.name). The finalizer is suppressed up front in Dispose, so an unexpected Release failure still propagates but is never miscounted as a leak.
  • Purely additive — no change to Pool, IPool, or IPoolMetrics; works with any IPool implementation. The Smtp.Pool sample's send path now uses it.

2. Move Smtp.Pool tests into samples/, out of the CI gate

  • Smtp.Pool.Tests and Smtp.Pool.Integration.Tests move from tests/ to samples/ alongside the sample (git mv, history preserved).
  • Removed from Pool.slnx, so neither CI workflow runs them (both test Pool.slnx; the integration suite needs Docker and shouldn't gate the library).
  • New samples/Smtp.Pool.slnx (sample + its tests + Pool) keeps them runnable locally as a unit.
  • Includes CI housekeeping staged alongside: a setup-dotnet composite action, a dependabot config, and workflow trigger/cleanup updates.

Verification

  • dotnet test Pool.slnx94 tests, only Pool.Tests (smtp suite no longer in the CI solution).
  • dotnet build samples/Smtp.Pool.slnx → full graph, 0 warnings.
  • Relocated samples/Smtp.Pool.Tests43 pass, coverage above its floor (root Directory.Build.props still applies).
  • Full solution builds 0 warnings under AnalysisMode=all + warnings-as-errors; Pool.Tests coverage 100% line / 92% branch / 100% method (above the 95/90/95 ratchet).

Not run here: the smtp4dev integration suite — it requires Docker (Testcontainers). Compile-verified only; intentionally no longer in CI.

🤖 Generated with Claude Code

marklauter and others added 7 commits June 14, 2026 16:25
LeaseScopeAsync wraps a leased item in a disposable Lease<TPoolItem> so a
`using` returns it on scope exit. Disposal is idempotent and single-shot
(immune to the double-release footgun), and if the pool was disposed while
the item was out on lease the lease disposes the orphaned item itself.

A Lease garbage-collected without being disposed is recorded on a new
pool.leases.leaked counter (published on the MSL.Pool meter, tagged
pool.name) so leaks are observable. The finalizer is suppressed up front in
Dispose, so an unexpected Release failure still propagates but is never
miscounted as a leak.

Purely additive: builds on the existing public LeaseAsync/Release, so it
needs no change to Pool, IPool, or IPoolMetrics and works with any
IPool implementation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert the integration test's lease/try/finally to a scoped `using` lease,
and update SmtpConnection's send doc comment to recommend the scoped pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Smtp.Pool sample's unit and integration test projects move from tests/
to samples/ alongside the sample, with their ProjectReference paths fixed,
and are removed from Pool.slnx so neither CI workflow runs them (both test
Pool.slnx, and the integration suite needs Docker). A new
samples/Smtp.Pool.slnx (sample + its tests + Pool) keeps them runnable
locally as a unit.

Also includes CI housekeeping staged alongside: a setup-dotnet composite
action, a dependabot config, and workflow trigger/cleanup updates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@marklauter
marklauter merged commit bab8944 into main Jun 15, 2026
6 checks passed
@marklauter
marklauter deleted the feat/lease-handle branch June 15, 2026 03:05
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