fix(abcip): keep a sync Dispose, but one that never blocks on the teardown

F10 follow-through: removing IDisposable outright broke a synchronous
drv.Dispose() call site outside this lane (Runtime.Tests
AbCipAlmaNativeAlarmEndToEndTests). The sync Dispose() is back, but it starts
the same teardown as DisposeAsync and returns instead of parking the caller on
DisposeAsync().AsTask().GetAwaiter().GetResult(); a background fault is
logged. The runtime still takes the IAsyncDisposable path (preferred, never
both).

Refs #737
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 14:34:53 -04:00
parent aabe027d10
commit 8a941de4cd
2 changed files with 44 additions and 7 deletions
@@ -175,12 +175,10 @@ public sealed class AbCipLifecycleConcurrencyTests
created.Where(t => !t.Disposed).ShouldBeEmpty("every runtime a reader created must be disposed by some teardown");
}
/// <summary>F10: the driver exposes only an async dispose; a sync <c>Dispose()</c> that blocked on the async
/// teardown (sync-over-async) is gone, and the runtime takes the <see cref="IAsyncDisposable"/> path.</summary>
/// <summary>F10: DisposeAsync is the complete, idempotent teardown (the runtime prefers it).</summary>
[Fact]
public async Task Driver_is_async_disposable_only_and_DisposeAsync_stops_the_probe()
public async Task DisposeAsync_is_a_complete_idempotent_teardown()
{
typeof(IDisposable).IsAssignableFrom(typeof(AbCipDriver)).ShouldBeFalse();
var (drv, factory) = Build(probeReadDelay: TimeSpan.Zero);
await drv.InitializeAsync("{}", CancellationToken.None);
await WaitUntilAsync(() => factory.Tags.ContainsKey("ProbeTag"), "probe never started");
@@ -192,4 +190,27 @@ public sealed class AbCipLifecycleConcurrencyTests
loop.IsCompleted.ShouldBeTrue();
factory.Tags["ProbeTag"].Disposed.ShouldBeTrue();
}
/// <summary>
/// F10: the sync <c>Dispose()</c> no longer blocks the caller's thread on the async teardown (sync-over-async —
/// a dispatcher thread for up to the probe join). It starts the same teardown and returns; the teardown still
/// completes.
/// </summary>
[Fact]
public async Task Sync_Dispose_does_not_block_on_the_teardown_but_still_completes_it()
{
var (drv, factory) = Build(probeReadDelay: TimeSpan.FromMilliseconds(500));
await drv.InitializeAsync("{}", CancellationToken.None);
await WaitUntilAsync(() => factory.Tags.ContainsKey("ProbeTag"), "probe never started");
var loop = drv.GetDeviceState(Host)!.ProbeTask!;
await Task.Delay(20); // the probe is inside its slow, cancellation-deaf read
var sw = System.Diagnostics.Stopwatch.StartNew();
((IDisposable)drv).Dispose();
sw.Stop();
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromMilliseconds(250)); // did not wait out the 500 ms probe read
await WaitUntilAsync(() => loop.IsCompleted && factory.Tags["ProbeTag"].Disposed, "teardown never completed");
drv.GetDeviceState(Host).ShouldBeNull();
}
}