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
runnerLiveSessionRunner
Properties
Runner
The runner this service drives.
public LiveSessionRunner Runner { get; }
Property Value
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
stoppingTokenCancellationTokenTriggered when StopAsync(CancellationToken) is called.
Returns
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
cancellationTokenCancellationTokenIndicates that the start process has been aborted.
Returns
StopAsync(CancellationToken)
Triggered when the application host is performing a graceful shutdown.
public override Task StopAsync(CancellationToken cancellationToken)
Parameters
cancellationTokenCancellationTokenIndicates that the shutdown process should no longer be graceful.