Class StatementOutbox
A durable queue in front of an XapiClient: Statements are stored before EnqueueAsync(Statement, CancellationToken) returns, and delivered later, in order and in batches, by FlushAsync(CancellationToken) or by a background loop (RunAsync(CancellationToken)).
public sealed class StatementOutbox : IAsyncDisposable, IDisposable
- Inheritance
-
StatementOutbox
- Implements
- Inherited Members
Remarks
Delivery is at least once. Every Statement gets its id before it is stored, so a Statement sent again after a lost response is the same Statement to the LRS, which keeps the first copy: on a conforming LRS the result is exactly once.
Failed sends are classified (docs/outbox.md has the full table). Network errors, timeouts, 408, 429 and 5xx
are retried with exponential backoff and jitter, honouring Retry-After. A 400 for a batch splits it in
halves until the invalid Statement is found; only that one becomes a dead letter. A 409 is resolved by fetching
the stored Statement and comparing it with StatementEquivalence. A 413 halves the batch size.
401, 403 and responses that mean the endpoint is wrong pause the outbox; nothing is dead-lettered.
All members are thread-safe. One flush runs at a time; Statements can be enqueued while it runs. Events are raised on the thread that runs the flush, which is not the Unity main thread; exceptions thrown by handlers are caught and ignored, so a handler cannot stop delivery.
Attachments are not supported: the outbox does not store attachment data, so Statements with an Attachment
that has no fileUrl are rejected by EnqueueAsync(Statement, CancellationToken). Send those with
SaveStatementAsync(Statement, IReadOnlyList<AttachmentPayload>?, CancellationToken).
Constructors
StatementOutbox(XapiClient, OutboxStore, OutboxOptions?)
Creates an outbox.
public StatementOutbox(XapiClient client, OutboxStore store, OutboxOptions? options = null)
Parameters
clientXapiClientThe client to send with.
storeOutboxStoreWhere to keep the Statements: a FileOutboxStore, usually.
optionsOutboxOptionsThe settings, or null for the defaults. They are copied.
Exceptions
- ArgumentException
An option is out of range.
Methods
Dispose()
Tells the background loop started with RunAsync(CancellationToken) to stop, without waiting for it. Prefer DisposeAsync(), which waits. The store and the client are not disposed.
public void Dispose()
DisposeAsync()
Stops the background loop started with RunAsync(CancellationToken) and waits for it to end. The store and the client are not disposed. A flush started with FlushAsync(CancellationToken) is not waited for.
public ValueTask DisposeAsync()
Returns
- ValueTask
A task that completes when the loop has stopped.
EnqueueAsync(Statement, CancellationToken)
Stores a Statement for delivery. When the returned task completes, the Statement is in the store; with a FileOutboxStore, on disk.
public ValueTask<Guid> EnqueueAsync(Statement statement, CancellationToken cancellationToken = default)
Parameters
statementStatementThe Statement.
cancellationTokenCancellationTokenCancels the operation.
Returns
Remarks
A Statement without an id gets a new UUID version 7, and one without a timestamp gets the current time (to the millisecond), so that a Statement delivered hours later still says when it happened. The Statement is then validated with the xAPI 2.0.0 rules, which accept everything 1.0.3 does; a Statement that uses 2.0-only properties and is later sent to a 1.0.3 LRS becomes a dead letter.
Enqueueing a Statement whose id is already waiting does nothing. The background loop, if running, is woken.
Exceptions
- XapiValidationException
The Statement is invalid, or has an Attachment without a
fileUrl(attachment data is not supported).- OutboxFullException
MaxQueueSize Statements are already waiting.
FlushAsync(CancellationToken)
Sends pending Statements now, batch after batch, until none are left, a transient error occurs, or the LRS refuses the request. Waits for a flush already in progress first.
public Task<OutboxFlushResult> FlushAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationTokenCancels the flush. A request in flight is abandoned; its Statements stay queued.
Returns
- Task<OutboxFlushResult>
What the flush did and the state it left the outbox in.
Remarks
A manual flush does not wait for the retry delay of an earlier failure. While the outbox is Paused it sends nothing; call Resume() first. Errors are not thrown; they are in Status. Exceptions from the store are thrown.
GetDeadLettersAsync(CancellationToken)
Returns the dead letters, oldest first.
public ValueTask<IReadOnlyList<OutboxDeadLetter>> GetDeadLettersAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationTokenCancels the operation.
Returns
- ValueTask<IReadOnlyList<OutboxDeadLetter>>
The dead letters.
GetStatusAsync(CancellationToken)
Returns what the outbox is doing, how much is waiting, and the last error.
public ValueTask<OutboxStatus> GetStatusAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationTokenCancels the operation.
Returns
- ValueTask<OutboxStatus>
The status.
RemoveDeadLetterAsync(Guid, CancellationToken)
Deletes a dead letter for good.
public ValueTask<bool> RemoveDeadLetterAsync(Guid statementId, CancellationToken cancellationToken = default)
Parameters
statementIdGuidThe Statement id.
cancellationTokenCancellationTokenCancels the operation.
Returns
RequeueAllDeadLettersAsync(CancellationToken)
Moves every dead letter back to the end of the queue, oldest first, and wakes the background loop.
public ValueTask<int> RequeueAllDeadLettersAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationTokenCancels the operation.
Returns
RequeueDeadLetterAsync(Guid, CancellationToken)
Moves a dead letter back to the end of the queue, with no failed attempts, and wakes the background loop. Use it after fixing what made the LRS reject the Statement.
public ValueTask<bool> RequeueDeadLetterAsync(Guid statementId, CancellationToken cancellationToken = default)
Parameters
statementIdGuidThe Statement id.
cancellationTokenCancellationTokenCancels the operation.
Returns
Resume()
Ends a pause (Paused), for example after the credentials were fixed, and wakes the background loop. Does nothing when the outbox is not paused.
public void Resume()
RunAsync(CancellationToken)
Delivers Statements in the background until cancellationToken is cancelled or the outbox
is disposed: right away, whenever a Statement is enqueued, and at RetryAt after
a transient failure. While the outbox is paused it waits for Resume().
public Task RunAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationTokenStops the loop. A request in flight is abandoned; its Statements stay queued.
Returns
- Task
A task that completes, without an exception, when the loop has stopped.
Remarks
Needs no host or dependency injection, so it also suits Unity and desktop apps: start it once
(_ = outbox.RunAsync(token)) and cancel the token, or dispose the outbox, on exit. On net10.0,
AddOutbox runs it as a hosted service. FlushAsync(CancellationToken) can still be called while it runs.
Errors never end the loop. Failures of the store (a full disk, for example) are handled like transient failures and shown in LastError.
Exceptions
- InvalidOperationException
The loop is already running.
Events
DeadLettered
Raised after a Statement was moved to the dead letters.
public event EventHandler<OutboxDeadLetteredEventArgs>? DeadLettered
Event Type
Delivered
Raised after Statements were delivered, or found already stored and equivalent at the LRS.
public event EventHandler<OutboxDeliveredEventArgs>? Delivered
Event Type
Paused
Raised when the LRS refused a request and the outbox paused (Paused).
public event EventHandler<OutboxPausedEventArgs>? Paused