< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.DurableFlowOptions
Assembly: AsyncResponse.Core
File(s): /_/src/AsyncResponse.Core/DurableFlowOptions.cs
Line coverage
100%
Covered lines: 14
Uncovered lines: 0
Coverable lines: 14
Total lines: 167
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/_/src/AsyncResponse.Core/DurableFlowOptions.cs

#LineLine coverage
 1namespace AsyncResponse;
 2
 3/// <summary>
 4/// Common options for durable flows (<see cref="IDurableFlows"/>), configured on the selected
 5/// <c>With*DurableFlows(...)</c> registration. Provider option types derive from this class so
 6/// flow-engine and state-store settings live in one configuration block.
 7/// </summary>
 8public class DurableFlowOptions
 9{
 10    /// <summary>
 11    /// The portable flow-id length contract in UTF-16 characters: the longest final flow id every
 12    /// bundled state store accepts (SQL Server, MySQL, Oracle, and EF Core declare <c>flow_id</c>
 13    /// as a 400-character column). Every id is validated when its state is created — root ids
 14    /// passed to <see cref="IDurableFlows.StartAsync{TFlow,TInput}"/>, composed child ids
 15    /// (<c>{parentId}:{stepName}</c>), and scheduled occurrence ids
 16    /// (<c>sched:{name}:{timestamp}</c>, validated at registration) — so an id cannot work on one
 17    /// store and fail on another, or work as a root and fail once a suffix is appended.
 18    /// <para>
 19    /// Length is only one of the three portability rules; see <see cref="MaxFlowIdBytes"/> and the
 20    /// character restrictions documented with it.
 21    /// </para>
 22    /// </summary>
 23    public const int MaxFlowIdLength = 400;
 24
 25    /// <summary>
 26    /// The portable flow-id size contract in UTF-8 <em>bytes</em> — the Cosmos DB id limit, which
 27    /// a 400-character id cannot be assumed to satisfy: characters outside the Basic Latin range
 28    /// cost two to four bytes each, so 400 CJK characters are 1200 bytes.
 29    /// <para>
 30    /// Ids must also avoid <c>/</c>, <c>\</c>, <c>?</c> and <c>#</c> (Cosmos rejects them outright)
 31    /// and control characters. Ids are compared ORDINALLY everywhere, and the relational stores
 32    /// pin a binary collation on the column so the database agrees — two ids differing only in
 33    /// case are two different flows.
 34    /// </para>
 35    /// </summary>
 36    public const int MaxFlowIdBytes = 1023;
 37
 38    /// <summary>
 39    /// How long persisted flow state lives; the TTL is refreshed on every checkpoint, so it bounds
 40    /// the *idle* time of a run, not its total duration. Must comfortably exceed the longest gap
 41    /// between checkpoints (typically the longest awaited step) — the default is deliberately
 42    /// double the 7-day default step-timeout chain (<see cref="DefaultStepTimeout"/> →
 43    /// channel <c>DefaultTimeout</c> → <c>RecoveryStateExpiry</c>), so a step that waits out the
 44    /// full default timeout still faults and checkpoints before its ledger can expire, instead of
 45    /// racing it. Default: 14 days.
 46    /// </summary>
 3984447    public TimeSpan StateExpiry { get; set; } = TimeSpan.FromDays(14);
 48
 49    /// <summary>
 50    /// Default timeout for awaited steps that don't pass one explicitly. <c>null</c> uses the
 51    /// configured channel's default wait timeout.
 52    /// </summary>
 1348653    public TimeSpan? DefaultStepTimeout { get; set; }
 54
 55    /// <summary>
 56    /// Distributed execution-lease duration. A worker renews the lease while flow code is running;
 57    /// another replica may take over after this interval if the worker disappears. Default: 1 minute.
 58    /// </summary>
 4098959    public TimeSpan ExecutionLeaseDuration { get; set; } = TimeSpan.FromMinutes(1);
 60
 61    /// <summary>
 62    /// How often an active execution renews its lease. Must be shorter than
 63    /// <see cref="ExecutionLeaseDuration"/>. Default: 20 seconds.
 64    /// </summary>
 2668365    public TimeSpan ExecutionLeaseRenewInterval { get; set; } = TimeSpan.FromSeconds(20);
 66
 67    /// <summary>
 68    /// The longest one wake-up stays parked behind an execution lease held by another worker
 69    /// because of the expiry the STORE reports for it. A wake-up that meets a held lease waits the
 70    /// holder's <em>persisted</em> expiry out (so a deployment that shortened
 71    /// <see cref="ExecutionLeaseDuration"/> still takes over a crashed owner's longer lease), and
 72    /// that expiry is data the waiting host does not control: a store clock hours ahead of this
 73    /// host, or an expiry column that reads back shifted, would otherwise park the delivery — and
 74    /// its worker slot — for as long as the bad value says, polling the store the whole time.
 75    /// Past this budget the wake-up fails with <c>DurableFlowLeaseContendedException</c> and the
 76    /// worker transport redelivers it, exactly as when the lease outlives its persisted expiry.
 77    /// This host's own lease window (<see cref="ExecutionLeaseDuration"/> +
 78    /// <see cref="ExecutionLeaseRenewInterval"/>) is always waited, whatever this is set to. Raise
 79    /// it when a deployment legitimately issues leases longer than the default. Default: 1 hour.
 80    /// </summary>
 1303281    public TimeSpan MaxLeaseContentionWait { get; set; } = TimeSpan.FromHours(1);
 82
 83    /// <summary>
 84    /// Minimum interval between persistence writes made only by <c>ReportProgressAsync</c>.
 85    /// Reports inside the interval update the in-memory flow state and are coalesced into the next
 86    /// checkpoint or flow outcome. Set to zero to persist every report. Default: 1 second.
 87    /// </summary>
 1299388    public TimeSpan ProgressPersistenceInterval { get; set; } = TimeSpan.FromSeconds(1);
 89
 90    /// <summary>
 91    /// Timer remainders at or under this threshold wait in process (under the execution lease)
 92    /// instead of suspending the run for a delayed wake-up job — a broker round-trip for a
 93    /// two-second sleep costs more than it frees. Longer remainders suspend when the registered
 94    /// transport supports native delayed delivery (<see cref="IDelayedWorkerTransport"/>); on
 95    /// transports without it every timer waits in process regardless of this value. Zero always
 96    /// prefers suspension. Default: 10 seconds.
 97    /// </summary>
 1324398    public TimeSpan TimerInProcessThreshold { get; set; } = TimeSpan.FromSeconds(10);
 99
 100    /// <summary>
 101    /// The longest a durable timer holds ONE worker delivery while it waits in process. A timer
 102    /// that cannot suspend (the transport has no native delayed delivery, or the remainder is under
 103    /// <see cref="TimerInProcessThreshold"/>) waits under the execution lease with its delivery
 104    /// unsettled, and some brokers cap how long that may last no matter how alive the handler is
 105    /// (<see cref="IWorkerTransportInFlightLimit"/>: Google Pub/Sub's <c>MaxTotalAckExtension</c>,
 106    /// RabbitMQ's <c>consumer_timeout</c>, SQS's 12-hour visibility ceiling) — past it the broker
 107    /// hands the same job to another consumer while the first handler is still sleeping. A longer
 108    /// sleep is therefore waited in hops: the timer parks for at most this long, then checkpoints,
 109    /// publishes an immediate wake-up for the run and ends the delivery; the wake-up replays to
 110    /// the same timer (its due time is checkpointed) and parks the next hop under a fresh
 111    /// delivery whose in-flight clock starts again.
 112    /// <para>
 113    /// <c>null</c> (the default) derives the hop from the transport: half of the ceiling it
 114    /// advertises, or no bound at all — one wait for the whole remainder — when it advertises
 115    /// none. A value can only shorten a transport-derived hop (the shorter of the two applies) or
 116    /// supply one for a transport that advertises no ceiling, e.g. a custom transport or a broker
 117    /// policy the library cannot see. Must be positive and at most the .NET timer ceiling
 118    /// (~49.7 days). Awaited-response steps are not hopped; see the durable-flows guide.
 119    /// </para>
 120    /// </summary>
 2028121    public TimeSpan? MaxInProcessParkDuration { get; set; }
 122
 123    /// <summary>
 124    /// Ledger size, in bytes (estimated from the serialized input, step results, values, and
 125    /// context), past which the executor logs a warning naming the flow — once when the threshold
 126    /// is first crossed and again at each doubling, so a long run logs a handful of times, not once
 127    /// per step. Every checkpoint rewrites the <em>whole</em> ledger, so persistence cost grows
 128    /// with each completed step: a run of N steps with similar result sizes serializes about N²/2
 129    /// step-results over its lifetime, and the store's <c>MaxStateBytes</c> (or the provider's
 130    /// item cap) is the hard limit. The warning is the early signal to keep step results small
 131    /// (persist large data yourself and pass references) or to partition a long history into child
 132    /// flows. <c>null</c> disables it. Default: 512 KiB — under the smallest bundled hard cap
 133    /// (DynamoDB's 350 KB item, whose store defaults <c>MaxStateBytes</c> to 350 000) users should
 134    /// lower it accordingly.
 135    /// </summary>
 15004136    public long? LedgerSizeWarningBytes { get; set; } = 512 * 1024;
 137
 138    /// <summary>
 139    /// Maximum distinct steps retained in one run. Default: 256. A new step beyond this budget
 140    /// fails terminally BEFORE its side effects; replay of existing steps is always allowed.
 141    /// Bound long histories with child flows and keep large results in external storage.
 142    /// Every checkpoint still writes the whole ledger: this bounds history growth, it does not
 143    /// make checkpoints incremental. Set null to opt out after measuring the workload.
 144    /// </summary>
 18315145    public int? MaxRetainedSteps { get; set; } = 256;
 146
 147    /// <summary>
 148    /// Accepts the risk of running the worker subscriber in early ACK (<c>AckAfterEnqueue</c>)
 149    /// while durable flows are registered, suppressing the startup error. Durable-flow wake-ups
 150    /// ride the worker queue and rely on broker redelivery for crash recovery; with early ACK, a
 151    /// process crash after the ACK but before execution strands the run as <c>Running</c> with no
 152    /// lease and no queued job, and only an operator <c>ResumeAsync(flowId)</c> can revive it.
 153    /// Leave <c>false</c> (the default) unless that loss mode is acceptable. Default: false.
 154    /// </summary>
 2190155    public bool AllowEarlyAckWorkerSubscriber { get; set; }
 156
 157    /// <summary>
 158    /// Startup validation of <see cref="MaxInProcessParkDuration"/>: the hop arms a BCL timer, so a
 159    /// non-positive or over-ceiling value would pass registration and throw only when the first
 160    /// timer parks — inside a delivery, as a retriable failure that dead-letters the run.
 161    /// </summary>
 162    internal void ValidateInProcessPark()
 163    {
 1605164        if (MaxInProcessParkDuration is { } park)
 12165            AsyncResponseChannelOptions.EnsureTimerBacked(park, nameof(DurableFlowOptions), nameof(MaxInProcessParkDurat
 1593166    }
 167}