Table of Contents

Class LiveSessionService

Namespace
DatabentoDotNet.Extensions.Hosting
Assembly
DatabentoDotNet.Extensions.Hosting.dll

Runs one live session for as long as the host is up.

public sealed class LiveSessionService : BackgroundService, IHostedService, IDisposable
Inheritance
LiveSessionService
Implements
Inherited Members

Remarks

Thin by construction. Everything worth testing is in LiveSessionRunner, which takes a resolved session and a handler and needs no host and no container — so MockLiveGateway drives it directly. What is left here is the two lines that make a runner a hosted service, and they are the two lines below.

StartAsync(CancellationToken) is overridden so a bad session fails the host's boot. StartAsync(CancellationToken) awaits ExecuteAsync(CancellationToken) only until its first yield, so a session established inside ExecuteAsync would fail in the background with the host already up and serving traffic it cannot fulfil. Connecting, authenticating, subscribing and starting therefore happen here, before base.StartAsync.

Nothing here calls IHostApplicationLifetime. HostOptions.BackgroundServiceExceptionBehavior — a type this package does not reference, and does not need to; see Microsoft.Extensions.Hosting.Abstractions's own package-choice remarks in Directory.Packages.props — has defaulted to StopHost since .NET 6, so an exception out of ExecuteAsync(CancellationToken) — which is what a faulted handler becomes — already stops the host. A second mechanism for the same outcome would differ only in its log line.

StopAsync(CancellationToken) is overridden too, and for a reason as narrow as it is easy to delete by mistake — do not remove it as "redundant with the base class" without reading this first. BackgroundService.StartAsync schedules ExecuteAsync as Task.Run(() => ExecuteAsync(_stoppingCts.Token), _stoppingCts.Token) — the same token both gates whether the thread pool ever invokes the delegate and is the stoppingToken ExecuteAsync(CancellationToken) receives. If BackgroundService.StopAsync cancels that token before the thread pool dequeues the work item, Task.Run does not invoke the delegate at all — this is documented Run(Func<Task>, CancellationToken) behaviour, not a bug in it — and the returned task completes Canceled with RunAsync(CancellationToken) never entered. BackgroundService.StopAsync then awaits that task via WaitAsync(cancellationToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing), which swallows the resulting OperationCanceledException and returns as though shutdown completed cleanly. A caller who had just called host.StopAsync() would observe it return with no exception while Runner's State was still Running and Fault still null — a live session, already billed by StartSessionAsync(CancellationToken), left connected and undrained after the host reported itself stopped, and a health check reading Runner.State would call it healthy. Verified against the exact Microsoft.Extensions.Hosting.Abstractions net10.0 assembly this package references, and reproduced in isolation against the live runtime with a single-worker thread pool — see #95.

RunAsync's cooperative-cancellation catch admits exactly one exit, for the token this method itself was handed; any other exception raised inside its pump loop — including an unrelated OperationCanceledException — falls to the generic catch, which moves State off Running before it rethrows. So ExecuteTask ending Canceled is the common shape of "the delegate was never dispatched", but it is not a guarantee of it by itself — an async method that lets any OperationCanceledException escape also leaves its task Canceled, whether or not the delegate ran. What actually carries the correctness of the guard below is State still being Running: any exception RunAsync's own try/catch lets past it has already moved State away from Running, since the generic catch sets it before rethrowing — so it is the two conditions together, not IsCanceled on its own, that make it safe to run RunAsync again. That second run reuses RunAsync's own already-cancelled-token path (loop condition false on entry, straight to the shared close) — exactly what a dispatched-and-immediately-cancelled run would have done — so this is not a second code path to trust: it is the same one, invoked from the place that discovered it was never going to run on its own.

A narrower gap this fix does not close: if RunAsync is still genuinely running when HostOptions.ShutdownTimeout (thirty seconds, by default) elapses, ExecuteTask.IsCanceled is false — the task is merely incomplete, not cancelled — so the fallback correctly does not fire, and Runner.State can still be Running when StopAsync(CancellationToken) returns. That is out of scope for #95, which is the dispatch race described above, not a slow shutdown; closing it would mean guessing whether a task that may still be legitimately in flight has finished, which risks the same double-close this guard exists to avoid.

Constructors

LiveSessionService(LiveSessionRunner)

Creates a hosted service around one runner.

public LiveSessionService(LiveSessionRunner runner)

Parameters

runner LiveSessionRunner

Properties

Runner

The runner this service drives.

public LiveSessionRunner Runner { get; }

Property Value

LiveSessionRunner

Methods

ExecuteAsync(CancellationToken)

This method is called when the IHostedService starts. The implementation should return a task that represents the lifetime of the long running operation(s) being performed.

protected override Task ExecuteAsync(CancellationToken stoppingToken)

Parameters

stoppingToken CancellationToken

Triggered when StopAsync(CancellationToken) is called.

Returns

Task

A Task that represents the long running operations.

Remarks

See Worker Services in .NET for implementation guidelines.

StartAsync(CancellationToken)

Triggered when the application host is ready to start the service.

public override Task StartAsync(CancellationToken cancellationToken)

Parameters

cancellationToken CancellationToken

Indicates that the start process has been aborted.

Returns

Task

A Task that represents the asynchronous Start operation.

StopAsync(CancellationToken)

Triggered when the application host is performing a graceful shutdown.

public override Task StopAsync(CancellationToken cancellationToken)

Parameters

cancellationToken CancellationToken

Indicates that the shutdown process should no longer be graceful.

Returns

Task

A Task that represents the asynchronous Stop operation.