dex_sdk/step_options.rs
1// Copyright (c) 2026 Super Durable, Inc.
2//
3// Licensed under the Sustainable Use License 1.0.
4// You may not use this file except in compliance with the License.
5// See the LICENSE file in the repository root.
6//
7// SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
9use std::marker::PhantomData;
10use std::time::Duration;
11
12use crate::attribute::AttributeLock;
13use crate::attribute::AttributeMapLoad;
14use crate::channel::{ChannelLoad, ChannelMapLoad};
15use crate::{AttributeMap, Channel, ChannelMap, RetryPolicy, Step, Value};
16
17/// Configures one Step's handler execution and persistence behavior.
18///
19/// New options preserve server timeout, retry, and durability defaults. `wait_for` failures fail the
20/// Flow by default. Attribute locks apply only to the matching handler call. The type system checks
21/// that an execute-failure recovery Step accepts the same input type.
22///
23/// `Input` is the Step input type and must implement [`Value`]. Builders consume the options
24/// and return the updated value. Durations must fit signed 32-bit whole seconds; invalid values
25/// fail when the Worker maps the Step definition or movement options.
26///
27/// Each method call retains its first loaded state snapshot and Attribute locks across retries
28/// and backoff. Later Attribute and Channel changes do not refresh that snapshot. Use a Channel
29/// wait in another Step to wait for state changes. Locks are released when the method call ends.
30/// A queued first attempt holds no locks before hand-out; a returned or retrying attempt keeps
31/// acquired locks while queued. Keep lock scope short to avoid blocking conflicting RPCs.
32///
33/// # Examples
34///
35/// ```
36/// use dex_sdk::{Attribute, RetryPolicy, StepOptions, WaitForFailurePolicy};
37/// use std::time::Duration;
38///
39/// let balance = Attribute::<i64>::new("balance");
40/// let options = StepOptions::<String>::new()
41/// .execute_method_timeout(Duration::from_secs(30))
42/// .execute_retry(RetryPolicy::new().maximum_attempts(3))
43/// .wait_for_failure(WaitForFailurePolicy::Proceed)
44/// .execute_lock(balance.lock());
45/// ```
46pub struct StepOptions<Input> {
47 pub(crate) wait_for_method_timeout: Option<Duration>,
48 pub(crate) execute_method_timeout: Option<Duration>,
49 pub(crate) heartbeat_timeout: Option<Duration>,
50 pub(crate) wait_for_retry: Option<RetryPolicy>,
51 pub(crate) execute_retry: Option<RetryPolicy>,
52 pub(crate) wait_for_failure: WaitForFailurePolicy,
53 pub(crate) wait_for_durability: StepDurability,
54 pub(crate) execute_durability: StepDurability,
55 pub(crate) wait_for_locks: Vec<AttributeLock>,
56 pub(crate) execute_locks: Vec<AttributeLock>,
57 pub(crate) wait_for_load_attribute_maps: Vec<AttributeMapLoad>,
58 pub(crate) wait_for_load_channels: Vec<ChannelLoad>,
59 pub(crate) wait_for_load_channel_maps: Vec<ChannelMapLoad>,
60 pub(crate) execute_load_attribute_maps: Vec<AttributeMapLoad>,
61 pub(crate) execute_load_channels: Vec<ChannelLoad>,
62 pub(crate) execute_load_channel_maps: Vec<ChannelMapLoad>,
63 pub(crate) execute_failure_step: Option<&'static str>,
64 marker: PhantomData<fn(Input)>,
65}
66
67#[derive(Clone)]
68pub(crate) struct ErasedStepOptions {
69 pub(crate) wait_for_method_timeout: Option<Duration>,
70 pub(crate) execute_method_timeout: Option<Duration>,
71 pub(crate) heartbeat_timeout: Option<Duration>,
72 pub(crate) wait_for_retry: Option<RetryPolicy>,
73 pub(crate) execute_retry: Option<RetryPolicy>,
74 pub(crate) wait_for_failure: WaitForFailurePolicy,
75 pub(crate) wait_for_durability: StepDurability,
76 pub(crate) execute_durability: StepDurability,
77 pub(crate) wait_for_locks: Vec<AttributeLock>,
78 pub(crate) execute_locks: Vec<AttributeLock>,
79 pub(crate) wait_for_load_attribute_maps: Vec<AttributeMapLoad>,
80 pub(crate) wait_for_load_channels: Vec<ChannelLoad>,
81 pub(crate) wait_for_load_channel_maps: Vec<ChannelMapLoad>,
82 pub(crate) execute_load_attribute_maps: Vec<AttributeMapLoad>,
83 pub(crate) execute_load_channels: Vec<ChannelLoad>,
84 pub(crate) execute_load_channel_maps: Vec<ChannelMapLoad>,
85 pub(crate) execute_failure_step: Option<&'static str>,
86}
87
88impl<Input> From<StepOptions<Input>> for ErasedStepOptions {
89 fn from(options: StepOptions<Input>) -> Self {
90 Self {
91 wait_for_method_timeout: options.wait_for_method_timeout,
92 execute_method_timeout: options.execute_method_timeout,
93 heartbeat_timeout: options.heartbeat_timeout,
94 wait_for_retry: options.wait_for_retry,
95 execute_retry: options.execute_retry,
96 wait_for_failure: options.wait_for_failure,
97 wait_for_durability: options.wait_for_durability,
98 execute_durability: options.execute_durability,
99 wait_for_locks: options.wait_for_locks,
100 execute_locks: options.execute_locks,
101 wait_for_load_attribute_maps: options.wait_for_load_attribute_maps,
102 wait_for_load_channels: options.wait_for_load_channels,
103 wait_for_load_channel_maps: options.wait_for_load_channel_maps,
104 execute_load_attribute_maps: options.execute_load_attribute_maps,
105 execute_load_channels: options.execute_load_channels,
106 execute_load_channel_maps: options.execute_load_channel_maps,
107 execute_failure_step: options.execute_failure_step,
108 }
109 }
110}
111
112impl<Input: Value> StepOptions<Input> {
113 /// Creates options that preserve server defaults and fail on exhausted `wait_for` retries.
114 pub fn new() -> Self {
115 Self {
116 wait_for_method_timeout: None,
117 execute_method_timeout: None,
118 heartbeat_timeout: None,
119 wait_for_retry: None,
120 execute_retry: None,
121 wait_for_failure: WaitForFailurePolicy::FailFlow,
122 wait_for_durability: StepDurability::Default,
123 execute_durability: StepDurability::Default,
124 wait_for_locks: Vec::new(),
125 execute_locks: Vec::new(),
126 wait_for_load_attribute_maps: Vec::new(),
127 wait_for_load_channels: Vec::new(),
128 wait_for_load_channel_maps: Vec::new(),
129 execute_load_attribute_maps: Vec::new(),
130 execute_load_channels: Vec::new(),
131 execute_load_channel_maps: Vec::new(),
132 execute_failure_step: None,
133 marker: PhantomData,
134 }
135 }
136
137 /// Sets the maximum duration of one `wait_for` handler attempt.
138 ///
139 /// Unset or zero uses two hours. With 1.35.0 (Server protocol 4): the timeout starts at
140 /// Worker hand-out,
141 /// excluding queue and lock waits, and is not shortened by retry total duration.
142 /// Each retry has a new timeout. Cancellation is cooperative; observe [`Context::is_cancelled`](crate::Context::is_cancelled)
143 /// and bound external calls with their own timeouts.
144 pub fn wait_for_method_timeout(mut self, value: Duration) -> Self {
145 self.wait_for_method_timeout = Some(value);
146 self
147 }
148
149 /// Sets the maximum duration of one `execute` handler attempt.
150 ///
151 /// Unset or zero uses two hours. With 1.35.0 (Server protocol 4): the timeout starts at
152 /// Worker hand-out,
153 /// excluding queue and lock waits, and is not shortened by retry total duration.
154 /// Each retry has a new timeout. Cancellation is cooperative; observe [`Context::is_cancelled`](crate::Context::is_cancelled)
155 /// and bound external calls with their own timeouts.
156 pub fn execute_method_timeout(mut self, value: Duration) -> Self {
157 self.execute_method_timeout = Some(value);
158 self
159 }
160
161 /// Sets the heartbeat timeout for `wait_for` and `execute` attempts.
162 ///
163 /// Zero selects the server default of one minute. Positive values must be whole seconds and at
164 /// least the server-configured minimum, which defaults to ten seconds. This option applies with
165 /// both synchronous and asynchronous durability.
166 /// Explicit heartbeats and Stream messages refresh liveness; only heartbeat values carry
167 /// retry checkpoints. A valueless heartbeat clears the checkpoint; a Stream message preserves it.
168 /// Raise this limit when healthy work can remain silent for more than a minute.
169 /// A method timeout still bounds the whole attempt, even when it keeps heartbeating.
170 pub fn heartbeat_timeout(mut self, value: Duration) -> Self {
171 self.heartbeat_timeout = Some(value);
172 self
173 }
174
175 /// Sets the retry policy for failed `wait_for` attempts.
176 ///
177 /// See [`RetryPolicy`] for defaults and the shared elapsed budget. Exhaustion fails the Flow
178 /// unless [`Self::wait_for_failure`] selects [`WaitForFailurePolicy::Proceed`].
179 pub fn wait_for_retry(mut self, value: RetryPolicy) -> Self {
180 self.wait_for_retry = Some(value);
181 self
182 }
183
184 /// Sets the retry policy for failed `execute` attempts.
185 ///
186 /// See [`RetryPolicy`] for defaults and the shared elapsed budget. Exhaustion fails the Flow
187 /// unless [`Self::on_execute_failure_proceed_to`] selects a recovery Step.
188 pub fn execute_retry(mut self, value: RetryPolicy) -> Self {
189 self.execute_retry = Some(value);
190 self
191 }
192
193 /// Selects whether exhausted `wait_for` retries fail or advance the Flow.
194 pub fn wait_for_failure(mut self, value: WaitForFailurePolicy) -> Self {
195 self.wait_for_failure = value;
196 self
197 }
198
199 /// Routes exhausted `execute` failures to a recovery Step with the same input type.
200 pub fn on_execute_failure_proceed_to<RecoveryStep>(mut self, step: &RecoveryStep) -> Self
201 where
202 RecoveryStep: Step<Input = Input>,
203 {
204 self.execute_failure_step = Some(step.step_type());
205 self
206 }
207
208 /// Overrides persistence durability for writes staged by `wait_for`.
209 pub fn wait_for_durability(mut self, value: StepDurability) -> Self {
210 self.wait_for_durability = value;
211 self
212 }
213
214 /// Overrides persistence durability for writes staged by `execute`.
215 pub fn execute_durability(mut self, value: StepDurability) -> Self {
216 self.execute_durability = value;
217 self
218 }
219
220 /// Adds an Attribute lock held for the `wait_for` invocation.
221 pub fn wait_for_lock(mut self, value: AttributeLock) -> Self {
222 self.wait_for_locks.push(value);
223 self
224 }
225
226 /// Adds an Attribute lock held for the `execute` invocation.
227 pub fn execute_lock(mut self, value: AttributeLock) -> Self {
228 self.execute_locks.push(value);
229 self
230 }
231
232 /// Loads every current instance of `attribute_map` for `wait_for`.
233 pub fn wait_for_load_attribute_map<T>(mut self, attribute_map: &AttributeMap<T>) -> Self {
234 self.wait_for_load_attribute_maps.push(AttributeMapLoad {
235 name: attribute_map.name().to_owned(),
236 instance: None,
237 });
238 self
239 }
240
241 /// Loads one AttributeMap instance for `wait_for`.
242 pub fn wait_for_load_attribute_map_instance(mut self, load: AttributeMapLoad) -> Self {
243 self.wait_for_load_attribute_maps.push(load);
244 self
245 }
246
247 /// Loads one Channel's pending messages for `wait_for`.
248 pub fn wait_for_load_channel<T>(mut self, channel: &Channel<T>) -> Self {
249 self.wait_for_load_channels.push(ChannelLoad {
250 name: channel.name().to_owned(),
251 });
252 self
253 }
254
255 /// Loads every current ChannelMap instance's pending messages for `wait_for`.
256 pub fn wait_for_load_channel_map<T>(mut self, channel_map: &ChannelMap<T>) -> Self {
257 self.wait_for_load_channel_maps.push(ChannelMapLoad {
258 name: channel_map.name().to_owned(),
259 instance: None,
260 });
261 self
262 }
263
264 /// Loads one ChannelMap instance's pending messages for `wait_for`.
265 pub fn wait_for_load_channel_map_instance(mut self, load: ChannelMapLoad) -> Self {
266 self.wait_for_load_channel_maps.push(load);
267 self
268 }
269
270 /// Loads every current instance of `attribute_map` for `execute`.
271 pub fn execute_load_attribute_map<T>(mut self, attribute_map: &AttributeMap<T>) -> Self {
272 self.execute_load_attribute_maps.push(AttributeMapLoad {
273 name: attribute_map.name().to_owned(),
274 instance: None,
275 });
276 self
277 }
278
279 /// Loads one AttributeMap instance for `execute`.
280 pub fn execute_load_attribute_map_instance(mut self, load: AttributeMapLoad) -> Self {
281 self.execute_load_attribute_maps.push(load);
282 self
283 }
284
285 /// Loads one Channel's pending messages for `execute`.
286 pub fn execute_load_channel<T>(mut self, channel: &Channel<T>) -> Self {
287 self.execute_load_channels.push(ChannelLoad {
288 name: channel.name().to_owned(),
289 });
290 self
291 }
292
293 /// Loads every current ChannelMap instance's pending messages for `execute`.
294 pub fn execute_load_channel_map<T>(mut self, channel_map: &ChannelMap<T>) -> Self {
295 self.execute_load_channel_maps.push(ChannelMapLoad {
296 name: channel_map.name().to_owned(),
297 instance: None,
298 });
299 self
300 }
301
302 /// Loads one ChannelMap instance's pending messages for `execute`.
303 pub fn execute_load_channel_map_instance(mut self, load: ChannelMapLoad) -> Self {
304 self.execute_load_channel_maps.push(load);
305 self
306 }
307}
308
309impl<Input: Value> Default for StepOptions<Input> {
310 fn default() -> Self {
311 Self::new()
312 }
313}
314
315#[derive(Clone, Copy, Debug, Eq, PartialEq)]
316/// Selects the terminal behavior after `wait_for` exhausts its retry policy.
317pub enum WaitForFailurePolicy {
318 /// Fails the Flow.
319 FailFlow,
320 /// Skips the failed wait and invokes `execute` with failure visible in [`crate::Context`].
321 Proceed,
322}
323
324#[derive(Clone, Copy, Debug, Eq, PartialEq)]
325/// Controls when staged Step persistence must be durably acknowledged.
326///
327/// Sustained throughput is about the same across modes. [`Self::Async`] lowers latency and makes
328/// individual Flows faster by acknowledging results before persistence completes.
329/// Method overrides take precedence over the Flow configuration, which defaults to Sync.
330/// WaitFor and Execute resolve their durability independently. Both modes enforce method,
331/// heartbeat, and retry policies and preserve accepted heartbeat checkpoints.
332///
333/// Async can lose an unpersisted result after a server failure, replaying the method and later
334/// work that depended on it. Use idempotent effects or Sync for costly replays. Sync does not
335/// remove the need for idempotency when a handler's external effect succeeds before its result.
336///
337/// With 1.35.0 (Server protocol 4): Sync persists attempt start before hand-out; Async persists
338/// it afterward.
339/// A server failure before an Async start is persisted can repeat the same attempt number without
340/// consuming a retry. Those executions may overlap, see different input, and repeat Stream output
341/// under the same source. Otherwise an interrupted attempt consumes a retry as possibly run.
342/// Durability changes affect only subsequently scheduled methods.
343pub enum StepDurability {
344 /// Uses the Flow or server default.
345 Default,
346 /// Waits for durable persistence before completing the handler request.
347 Sync,
348 /// Allows asynchronous persistence after accepting the handler result.
349 Async,
350}