Skip to main content

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}