| | | 1 | | namespace 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> |
| | | 8 | | public 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> |
| | 39844 | 47 | | 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> |
| | 13486 | 53 | | 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> |
| | 40989 | 59 | | 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> |
| | 26683 | 65 | | 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> |
| | 13032 | 81 | | 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> |
| | 12993 | 88 | | 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> |
| | 13243 | 98 | | 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> |
| | 2028 | 121 | | 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> |
| | 15004 | 136 | | 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> |
| | 18315 | 145 | | 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> |
| | 2190 | 155 | | 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 | | { |
| | 1605 | 164 | | if (MaxInProcessParkDuration is { } park) |
| | 12 | 165 | | AsyncResponseChannelOptions.EnsureTimerBacked(park, nameof(DurableFlowOptions), nameof(MaxInProcessParkDurat |
| | 1593 | 166 | | } |
| | | 167 | | } |