Skip to main content

dex_sdk/
worker_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::path::PathBuf;
10use std::sync::Arc;
11use std::time::Duration;
12
13use crate::{Credential, FlowDefinitionDocument, FlowServiceTlsOptions};
14
15/// Configures outbound polling for a Worker.
16///
17/// Pass this to [`WorkerOptions::pull`]. The Worker polls its sticky queue and each registered
18/// Flow type through exactly `max_concurrent_tasks` long-lived slot streams. An omitted task limit
19/// uses the CPU count. An omitted drain timeout uses 30 seconds.
20///
21/// Changed in 1.35.0: each execution slot owns one stream, replacing per-queue idle streams.
22/// Start requires protocol 4 (server-zd newer than v0.1.16); upgrade the Server before the SDK.
23///
24/// ```no_run
25/// use std::sync::Arc;
26/// use dex_sdk::{BlobCache, BlobCacheConfig, Client, ClientOptions, PullWorkerOptions,
27///     Registry, Worker, WorkerOptions};
28///
29/// let cache = Arc::new(BlobCache::open(BlobCacheConfig::new(
30///     "/tmp/dex-pull-blobs", 64 * 1024 * 1024, 0,
31/// )?)?);
32/// let registry = Registry::new();
33/// let worker = Worker::try_new(registry.clone(), cache.clone(),
34///     WorkerOptions::new().pull(PullWorkerOptions::new().max_concurrent_tasks(8)))?;
35/// let client = Client::try_new(registry, cache,
36///     ClientOptions::new())?;
37/// // Run worker.start() on a dedicated thread; call worker.stop() during shutdown.
38/// # Ok::<(), Box<dyn std::error::Error>>(())
39/// ```
40#[derive(Clone, Debug, Default)]
41pub struct PullWorkerOptions {
42    pub(crate) max_concurrent_tasks: Option<usize>,
43    pub(crate) reserved_rpc_tasks: Option<usize>,
44    pub(crate) idle_streams_per_queue: Option<usize>,
45    pub(crate) app_worker_id: Option<String>,
46    pub(crate) stop_drain_timeout: Option<Duration>,
47    pub(crate) stop_on_permanent_stream_rejection: bool,
48}
49
50impl PullWorkerOptions {
51    /// Creates pull options with CPU-count slot concurrency and a 30-second drain.
52    ///
53    /// The app Worker ID is generated from process identity and eight random base32 characters.
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// Sets the maximum number of concurrent Step and RPC handlers.
59    ///
60    /// The default is the available CPU count. Zero returns [`crate::SdkError::FlowDefinition`]
61    /// from [`crate::Worker::try_new`]. Each slot owns one long-lived poll stream and executes
62    /// every task assigned to it. Changed in 1.35.0: [`crate::Worker::start`] fails if this value
63    /// exceeds the Server's reported per-Worker limit, configured by `matching.maxConcurrentTasksCap`.
64    pub fn max_concurrent_tasks(mut self, value: usize) -> Self {
65        self.max_concurrent_tasks = Some(value);
66        self
67    }
68
69    /// Reserves slots that accept only RPCs, protecting RPC latency from long-running Steps.
70    ///
71    /// Changed in 1.23.0: reserved RPC capacity remains available under a Step backlog.
72    /// Changed in 1.35.0: reservations select RPC-only streams instead of declining
73    /// assigned Steps; the Worker executes every assignment.
74    ///
75    /// The default reserves one slot when resolved concurrency is at least two, otherwise zero.
76    /// The remaining slots accept Steps and RPCs. The Server selects eligible slots; the Worker
77    /// never declines a Step for capacity. Set zero to allow Steps on every slot. A one-slot
78    /// Worker with no explicit reservation warns that RPCs may wait behind Steps.
79    /// Readiness requires a registered Step-and-RPC slot and, with a reservation, an RPC-only slot.
80    /// `value` must be less than resolved `max_concurrent_tasks`; otherwise
81    /// [`crate::Worker::try_new`] returns [`crate::SdkError::FlowDefinition`].
82    /// Returns these options with the reservation applied.
83    ///
84    /// ```
85    /// use dex_sdk::PullWorkerOptions;
86    /// let pull = PullWorkerOptions::new().max_concurrent_tasks(8).reserved_rpc_tasks(2);
87    /// ```
88    pub fn reserved_rpc_tasks(mut self, value: usize) -> Self {
89        self.reserved_rpc_tasks = Some(value);
90        self
91    }
92
93    /// Retains the former per-queue idle-stream setting for source compatibility.
94    ///
95    /// Changed in 1.35.0: this setting became a compatibility-only no-op.
96    ///
97    /// Deprecated: `value` is accepted, including zero, and has no effect. Every execution slot
98    /// now owns exactly one long-lived stream. Use [`Self::max_concurrent_tasks`] to select the
99    /// total stream count. Returns these options with the compatibility value retained.
100    #[deprecated(note = "one long-lived stream per execution slot; this setting has no effect")]
101    pub fn idle_streams_per_queue(mut self, value: usize) -> Self {
102        self.idle_streams_per_queue = Some(value);
103        self
104    }
105
106    /// Overrides the process Worker ID used for sticky routing.
107    ///
108    /// Use a new ID after each process restart. It must contain 1–63 ASCII letters, digits,
109    /// dots, underscores, or hyphens and must differ from every registered Flow type.
110    /// Invalid values return [`crate::SdkError::FlowDefinition`] from [`crate::Worker::try_new`].
111    /// If omitted, the Worker generates a fresh ID during construction.
112    pub fn app_worker_id(mut self, value: impl Into<String>) -> Self {
113        self.app_worker_id = Some(value.into());
114        self
115    }
116
117    /// Stops when a permanent rejection leaves a required slot kind without any registered stream.
118    ///
119    /// Defaults to false, retrying indefinitely. When enabled, [`crate::Worker::start`] drains
120    /// within the configured timeout, cancels remaining handlers, and waits for their return.
121    /// It returns a service error exposing [`crate::SdkError::pull_registration_error`] before or after initial readiness.
122    /// Calling [`crate::Worker::stop`] afterward remains safe.
123    pub fn stop_on_permanent_stream_rejection(mut self, value: bool) -> Self {
124        self.stop_on_permanent_stream_rejection = value;
125        self
126    }
127
128    /// Bounds graceful drain before active handlers are cancelled.
129    ///
130    /// The default is 30 seconds. Zero returns [`crate::SdkError::FlowDefinition`] from
131    /// [`crate::Worker::try_new`].
132    /// A busy server-directed Reconnect uses this duration to wait for server EOF after the final frame.
133    /// During Stop, idle streams half-close immediately. Busy streams finish and send their final
134    /// output before half-closing. Later task frames are dropped without invoking handlers, and
135    /// half-closed streams keep reading until the Server ends them or the drain deadline expires.
136    /// At the deadline, a stream with a running handler is reset instead of half-closed.
137    /// [`crate::Worker::start`] waits for cancelled handlers to return before it exits.
138    /// Every other stream still waiting for the Server to end it is reset at the deadline.
139    /// A stream whose last final output Dex has not yet acknowledged with a new task waits up to
140    /// five seconds past this deadline for the Server to end it, then is reset.
141    /// Stop can therefore return up to five seconds after this deadline.
142    pub fn stop_drain_timeout(mut self, value: Duration) -> Self {
143        self.stop_drain_timeout = Some(value);
144        self
145    }
146}
147
148#[derive(Clone, Debug)]
149/// Configures a pull [`crate::Worker`] and its outbound Dex connection.
150///
151/// Defaults connect to Dex at `127.0.0.1:8801`, use [`PullWorkerOptions::default`], and allow
152/// two minutes for Attribute-index synchronization. Workers need only an outbound connection.
153/// Changed in 1.25.0: every Worker uses pull mode; no listener or Worker target is configured.
154pub struct WorkerOptions {
155    server_address: String,
156    pull: PullWorkerOptions,
157    attribute_index_sync_timeout: Duration,
158    credential: Option<Arc<dyn Credential>>,
159    flow_service_tls: Option<FlowServiceTlsOptions>,
160    flow_definition_paths: Vec<PathBuf>,
161    flow_definitions: Vec<FlowDefinitionDocument>,
162}
163
164impl WorkerOptions {
165    /// Creates Worker options with local development defaults.
166    pub fn new() -> Self {
167        Self {
168            server_address: "127.0.0.1:8801".to_string(),
169            pull: PullWorkerOptions::default(),
170            attribute_index_sync_timeout: Duration::from_secs(120),
171            credential: None,
172            flow_service_tls: None,
173            flow_definition_paths: Vec::new(),
174            flow_definitions: Vec::new(),
175        }
176    }
177
178    /// Sets the Flow Definition Graph files that the Worker uploads to the Dex Server at start.
179    ///
180    /// Dex Web draws each Flow type's graph from the uploaded document. Generate the files with
181    /// `dexcli visualize`. Each path must exist and be readable. An explicitly listed file must be
182    /// a Flow Definition Graph 2.x: a UTF-8 JSON object whose `schemaVersion` starts with `2.` and
183    /// whose `flow.name` is a non-empty string; its extension is not checked. A directory is read
184    /// recursively for files whose extension is `.json` in any case; a file there that is not a
185    /// Flow Definition Graph 2.x is skipped with an info log, so a directory may hold other JSON
186    /// files.
187    ///
188    /// The Flow type of a file is its `flow.name`. Its generation time is its top-level
189    /// `generatedAt`, which must be strict RFC 3339 such as `2026-11-01T00:00:00Z`, or the file
190    /// modification time when that is missing. Every generation time is truncated to whole
191    /// milliseconds.
192    ///
193    /// [`crate::Worker::try_new`] reads the files and returns [`crate::SdkError::FlowDefinition`]
194    /// for a missing or unreadable path, an invalid explicit file, an invalid `generatedAt`, or a
195    /// Flow type configured twice. [`crate::Worker::start`] uploads only documents for Flow types
196    /// that the Registry registers, after protocol negotiation and Attribute-index synchronization
197    /// and before it opens task streams, and sends nothing when none is left. An absolute
198    /// top-level `source.path`, including a Windows drive or UNC path, is replaced by a path
199    /// relative to the working directory, or by the file name when the file is elsewhere or the
200    /// working directory is unknown. Requests hold at most 256 documents and 16 MiB of content and
201    /// share one [`Self::attribute_index_sync_timeout`] deadline. [`crate::Worker::stop`] cancels
202    /// an upload in progress. An upload failure, including an older Server without upload
203    /// support, is logged as a warning; the Worker still starts. The default is no paths. Each
204    /// call replaces the previous paths.
205    ///
206    /// Since 1.45.0.
207    ///
208    /// ```
209    /// use dex_sdk::WorkerOptions;
210    /// let options = WorkerOptions::new()
211    ///     .server_address("dex:8801")
212    ///     .flow_definition_paths(["flow-definitions"]);
213    /// ```
214    pub fn flow_definition_paths<P: Into<PathBuf>>(
215        mut self,
216        value: impl IntoIterator<Item = P>,
217    ) -> Self {
218        self.flow_definition_paths = value.into_iter().map(Into::into).collect();
219        self
220    }
221
222    /// Sets in-memory Flow Definition Graph documents that the Worker uploads at start.
223    ///
224    /// Use this when the application builds documents itself, for example from a template with
225    /// a fixed generation time. Uploading, filtering to registered Flow types, the `source.path`
226    /// rewrite, and failure handling work as for [`Self::flow_definition_paths`]. A Flow type may
227    /// come from only one document across both settings. The default is no documents. Each call
228    /// replaces the previous documents. [`crate::Worker::try_new`] returns
229    /// [`crate::SdkError::FlowDefinition`] when a document is not a UTF-8 JSON object whose
230    /// `schemaVersion` starts with `2.`, its `flow.name` differs from its Flow type, or two sources share a Flow type. See [`FlowDefinitionDocument`] for an
231    /// example.
232    ///
233    /// Since 1.45.0.
234    pub fn flow_definitions(
235        mut self,
236        value: impl IntoIterator<Item = FlowDefinitionDocument>,
237    ) -> Self {
238        self.flow_definitions = value.into_iter().collect();
239        self
240    }
241
242    pub(crate) fn take_flow_definition_sources(
243        &mut self,
244    ) -> (Vec<PathBuf>, Vec<FlowDefinitionDocument>) {
245        (
246            std::mem::take(&mut self.flow_definition_paths),
247            std::mem::take(&mut self.flow_definitions),
248        )
249    }
250
251    /// Sets the Dex FlowService address used by the Worker.
252    ///
253    /// With TLS, accepts `host:port`, `dns:///host:port`, or `dns://authority/host:port`.
254    pub fn server_address(mut self, value: impl Into<String>) -> Self {
255        self.server_address = value.into();
256        self
257    }
258
259    /// Enables TLS for every outbound Worker FlowService connection.
260    ///
261    /// Omitted by default, preserving plaintext gRPC. Takes ownership of `value` and returns
262    /// these options with TLS enabled. [`crate::Worker::try_new`] validates the address,
263    /// additional roots, and server name, returning [`crate::SdkError::InvalidArgument`] for
264    /// invalid input. See [`FlowServiceTlsOptions`] for defaults and a complete example.
265    pub fn flow_service_tls(mut self, value: FlowServiceTlsOptions) -> Self {
266        self.flow_service_tls = Some(value);
267        self
268    }
269
270    /// Sends a bearer token on FlowService calls and pull Worker task streams.
271    ///
272    /// The default sends no authorization metadata. The credential may be called concurrently;
273    /// errors fail calls locally. Pull streams evaluate it at opening and again on reconnect.
274    /// Enable [`Self::flow_service_tls`] to protect the token on the network.
275    pub fn credential(mut self, value: Arc<dyn Credential>) -> Self {
276        self.credential = Some(value);
277        self
278    }
279
280    /// Configures pull capacity, queue registration, and graceful shutdown.
281    ///
282    /// Every Worker uses pull; this builder overrides [`PullWorkerOptions::default`].
283    /// Takes ownership of `value` and returns these Worker options with its settings applied.
284    /// [`crate::Worker::try_new`] validates capacity, identity, and timeout settings.
285    /// See [`PullWorkerOptions`] for defaults and a complete Worker and Client example.
286    pub fn pull(mut self, value: PullWorkerOptions) -> Self {
287        self.pull = value;
288        self
289    }
290
291    /// Sets the startup Indexed Attribute synchronization deadline.
292    ///
293    /// The default is two minutes. [`Worker::try_new`](crate::Worker::try_new)
294    /// rejects [`Duration::ZERO`].
295    pub fn attribute_index_sync_timeout(mut self, value: Duration) -> Self {
296        self.attribute_index_sync_timeout = value;
297        self
298    }
299
300    pub(crate) fn pull_value(&self) -> &PullWorkerOptions {
301        &self.pull
302    }
303
304    pub(crate) fn server_address_value(&self) -> &str {
305        &self.server_address
306    }
307
308    pub(crate) fn attribute_index_sync_timeout_value(&self) -> Duration {
309        self.attribute_index_sync_timeout
310    }
311
312    pub(crate) fn flow_service_tls_value(&self) -> Option<&FlowServiceTlsOptions> {
313        self.flow_service_tls.as_ref()
314    }
315
316    pub(crate) fn credential_value(&self) -> Option<Arc<dyn Credential>> {
317        self.credential.clone()
318    }
319}
320
321impl Default for WorkerOptions {
322    fn default() -> Self {
323        Self::new()
324    }
325}
326
327pub(crate) struct WorkerEnvironmentConfig {
328    pub(crate) identity: String,
329}
330
331impl WorkerEnvironmentConfig {
332    pub(crate) fn from_environment() -> Self {
333        let identity = ["DEX_WORKER_INSTANCE", "POD_NAME", "HOSTNAME"]
334            .into_iter()
335            .filter_map(std::env::var_os)
336            .map(|value| value.to_string_lossy().into_owned())
337            .find(|value| !value.trim().is_empty())
338            .unwrap_or_else(|| gethostname::gethostname().to_string_lossy().into_owned());
339        Self { identity }
340    }
341}