Class LiveSessionRunner
- Namespace
- DatabentoDotNet.Extensions.Hosting
- Assembly
- DatabentoDotNet.Extensions.Hosting.dll
Connects, authenticates, subscribes and starts one live session, then drains its records into an ILiveRecordHandler until the stream ends or the caller cancels.
public sealed class LiveSessionRunner : IAsyncDisposable
- Inheritance
-
LiveSessionRunner
- Implements
- Inherited Members
Remarks
Needs no host and no container. The constructor takes a ResolvedLiveSession,
a handler and a ReconnectSupervisor — nothing that only exists inside
Microsoft.Extensions.DependencyInjection or
Microsoft.Extensions.Hosting. That is what lets MockLiveGateway drive every
behaviour here on an ordinary dotnet test, with no IServiceProvider and no
IHostedService anywhere near it. The hosted service that wraps this in a
BackgroundService is thin enough to be uninteresting
precisely because everything interesting already works without it.
Starting is a separate call from running, and that split is deliberate.
StartAsync(CancellationToken) only awaits
ExecuteAsync until its first await that does not complete synchronously, so a
session that connected and authenticated inside the record loop would fail in the background —
with the host already reporting itself started. StartSessionAsync(CancellationToken) is the part a
hosted service's own StartAsync override awaits, so a rejected API key fails the boot
instead of vanishing into a background task nobody is watching.
The drain is synchronous and the fill is the only await, because there is no other
shape available. An async method cannot return a ref struct — there is no
Task<RecordRef>, and there never can be — so upstream's single-call
next_record() does not port. What does port is its fill_buf() /
try_next_record() pair: PumpAsync(LiveClient, CancellationToken) awaits
FillBufferAsync(CancellationToken) once per pass and hands every record the fill produced
to Drain(LiveClient), a plain synchronous method, before awaiting anything else. A
RecordRef local inside an async method is fine; only one that
survives an await is rejected, by the compiler, as CS4007 — which is the same lifetime
rule TryNextRecord(out RecordRef) already imposes.
No System.IO.Pipelines and no Channel<T> between the socket and the
handler. Both were considered and rejected: a ReadOnlySequence<T>
may be non-contiguous, which breaks the reinterpret cast every record decode depends on, and
either one adds a second buffering layer over the decoder's own. A channel would also force a
copy per record to cross it — the one thing this library exists not to do.
An exception from the handler, or from the client, ends the session. Swallowing one would lose market data invisibly, which is the failure class this codebase exists to convert into loud ones. Cancelling RunAsync(CancellationToken)'s token is the one way to stop without that being a fault: it moves State to Stopped rather than Faulted, because a host stopping a background service must not be reported as a failed session.
Constructors
LiveSessionRunner(ResolvedLiveSession, ILiveRecordHandler, ReconnectSupervisor, ILogger<LiveSessionRunner>?, LiveSessionMetrics?)
Creates a runner for one resolved session.
public LiveSessionRunner(ResolvedLiveSession session, ILiveRecordHandler handler, ReconnectSupervisor supervisor, ILogger<LiveSessionRunner>? logger = null, LiveSessionMetrics? metrics = null)
Parameters
sessionResolvedLiveSessionThe session to run.
handlerILiveRecordHandlerWhere every record and every flush goes.
supervisorReconnectSupervisorThe reconnection policy — unused by StartSessionAsync(CancellationToken) and RunAsync(CancellationToken) beyond RecordSuccess() until reconnection lands.
loggerILogger<LiveSessionRunner>Where
ExtensionsLogwrites. Defaults to Instance, so a caller who never configures logging pays nothing for it.metricsLiveSessionMetricsWhere the four instruments are published, or null to publish none. Last and optional for the same reason
loggeris: a caller who wants neither writes neither, and every existing three- and four-argument call site still compiles.
Properties
CloseTimeout
How long RunAsync(CancellationToken) waits for a courteous close before dropping the socket instead. Defaults to five seconds.
public Duration CloseTimeout { get; init; }
Property Value
Remarks
Configurable from a host as {section}:Live:{name}:CloseTimeout — see
CloseTimeout, which also explains why it is not derived
from the host's own ShutdownTimeout. The five seconds here is what both a session
that configures nothing and a runner constructed directly get.
Fault
Why the session faulted, or null while it has not.
public Exception? Fault { get; }
Property Value
Metadata
The DBN metadata the gateway sent when the session started, or null before StartSessionAsync(CancellationToken) has completed.
public Metadata? Metadata { get; }
Property Value
RecordsReceived
How many records this session has handed to the handler so far.
public long RecordsReceived { get; }
Property Value
Session
The session this runner is running.
public ResolvedLiveSession Session { get; }
Property Value
State
Where this session is in its lifecycle.
public LiveSessionState State { get; }
Property Value
Methods
AwaitCloseAsync(Task)
Half-closes, so the gateway gets to finish rather than having the socket dropped on it — but bounded, so a gateway that never answers cannot hold the host's shutdown open.
public Task<bool> AwaitCloseAsync(Task closing)
Parameters
closingTaskThe close to wait for — normally
LiveClient.CloseAsync().
Returns
Remarks
The losing task is left to complete on its own rather than cancelled. It holds a timer and nothing else, it finishes within CloseTimeout, and cancelling it would leave a faulted task nobody awaits — noise, in exchange for reclaiming one timer five seconds early.
The timer is a plain Delay(TimeSpan, CancellationToken), not
Delay. It once was; ReconnectSupervisor
is documented to hold the reconnect schedule and nothing else, and a caller can now replace
that seam to turn it into a synchronisation point for the backoff itself — see
LiveSessionReconnectTests. Routing this unrelated shutdown ceiling through the same
seam would corrupt that signal, not just be untidy, so this waits on the BCL primitive
directly. ToTimeSpan() converts CloseTimeout for the one
call that needs it; nothing here stores a TimeSpan.
DisposeAsync()
Disposes the underlying LiveClient, if one was built. Idempotent.
public ValueTask DisposeAsync()
Returns
RunAsync(CancellationToken)
Drains records into the handler until the gateway closes the stream or
cancellationToken is cancelled.
public Task RunAsync(CancellationToken cancellationToken)
Parameters
cancellationTokenCancellationTokenStops the loop. Cancelling it ends the session — see FillBufferAsync(CancellationToken) — but is reported as Stopped, not Faulted: a host stopping is not a failure.
Returns
Exceptions
- InvalidOperationException
StartSessionAsync(CancellationToken) has not completed.
StartSessionAsync(CancellationToken)
Connects, authenticates, sends every subscription in Session in order, and starts the session.
public Task StartSessionAsync(CancellationToken cancellationToken)
Parameters
cancellationTokenCancellationTokenCancels the handshake. Cancelling any step here leaves the underlying connection unusable — see AuthenticateAsync(CancellationToken) — so there is nothing to resume; construct another runner to try again.
Returns
Remarks
Separate from RunAsync(CancellationToken) on purpose. See the type-level remarks: this is the half a hosted service's own start-up awaits, so a rejected key or an unreachable gateway fails the host's boot rather than a background loop nobody is watching yet.
Exceptions
- InvalidOperationException
This runner has already been started.