1use std::fmt;
4use std::time::Duration;
5
6#[derive(Clone, PartialEq, Eq)]
12pub struct TransportCredentials {
13 secret: Vec<u8>,
14}
15
16impl TransportCredentials {
17 #[must_use]
19 pub fn new(secret: impl Into<Vec<u8>>) -> Self {
20 Self {
21 secret: secret.into(),
22 }
23 }
24
25 #[must_use]
27 pub fn secret(&self) -> &[u8] {
28 &self.secret
29 }
30}
31
32impl fmt::Debug for TransportCredentials {
33 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34 formatter
35 .debug_struct("TransportCredentials")
36 .field("secret", &"<redacted>")
37 .finish()
38 }
39}
40
41#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct ReconnectConfig {
75 pub initial_backoff: Duration,
77 pub max_backoff: Duration,
81 pub max_attempts: usize,
83}
84
85impl ReconnectConfig {
86 #[must_use]
88 pub const fn new(
89 initial_backoff: Duration,
90 max_backoff: Duration,
91 max_attempts: usize,
92 ) -> Self {
93 Self {
94 initial_backoff,
95 max_backoff,
96 max_attempts,
97 }
98 }
99}
100
101#[derive(Clone, Debug, PartialEq, Eq)]
115pub struct WorkerConfig {
116 pub namespaces: Vec<String>,
121 pub subject: String,
123 pub endpoint: String,
125 pub task_queue: String,
128 pub node: String,
131 pub identity: String,
133 pub max_concurrency: usize,
135 pub reconnect: ReconnectConfig,
137 pub transport_credentials: Option<TransportCredentials>,
139}
140
141const DEFAULT_WORKER_NAMESPACE: &str = "default";
142const DEFAULT_WORKER_SUBJECT: &str = "worker";
143
144const DEFAULT_WORKER_NODE: &str = "localhost";
148
149#[must_use]
154pub fn default_node() -> String {
155 std::env::var("HOSTNAME")
156 .ok()
157 .filter(|hostname| !hostname.is_empty())
158 .or_else(|| {
159 hostname::get()
160 .ok()
161 .and_then(|raw| raw.into_string().ok())
162 .filter(|hostname| !hostname.is_empty())
163 })
164 .unwrap_or_else(|| String::from(DEFAULT_WORKER_NODE))
165}
166
167impl WorkerConfig {
168 #[must_use]
171 pub const fn builder() -> WorkerConfigBuilder {
172 WorkerConfigBuilder::new()
173 }
174
175 #[must_use]
177 pub fn new(
178 endpoint: impl Into<String>,
179 task_queue: impl Into<String>,
180 identity: impl Into<String>,
181 max_concurrency: usize,
182 reconnect: ReconnectConfig,
183 transport_credentials: Option<TransportCredentials>,
184 ) -> Self {
185 Self {
186 namespaces: vec![String::from(DEFAULT_WORKER_NAMESPACE)],
187 subject: String::from(DEFAULT_WORKER_SUBJECT),
188 endpoint: endpoint.into(),
189 task_queue: task_queue.into(),
190 node: default_node(),
191 identity: identity.into(),
192 max_concurrency,
193 reconnect,
194 transport_credentials,
195 }
196 }
197}
198
199#[derive(Clone, Debug, Default)]
201pub struct WorkerConfigBuilder {
202 namespaces: Option<Vec<String>>,
203 subject: Option<String>,
204 endpoint: Option<String>,
205 task_queue: Option<String>,
206 node: Option<String>,
207 identity: Option<String>,
208 max_concurrency: Option<usize>,
209 reconnect_initial_backoff: Option<Duration>,
210 reconnect_max_backoff: Option<Duration>,
211 reconnect_max_attempts: Option<usize>,
212 transport_credentials: Option<TransportCredentials>,
213}
214
215impl WorkerConfigBuilder {
216 #[must_use]
218 pub const fn new() -> Self {
219 Self {
220 namespaces: None,
221 subject: None,
222 endpoint: None,
223 task_queue: None,
224 node: None,
225 identity: None,
226 max_concurrency: None,
227 reconnect_initial_backoff: None,
228 reconnect_max_backoff: None,
229 reconnect_max_attempts: None,
230 transport_credentials: None,
231 }
232 }
233
234 #[must_use]
237 pub fn namespaces(mut self, namespaces: impl IntoIterator<Item = String>) -> Self {
238 self.namespaces = Some(namespaces.into_iter().collect());
239 self
240 }
241
242 #[must_use]
245 pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
246 self.namespaces = Some(vec![namespace.into()]);
247 self
248 }
249
250 #[must_use]
252 pub fn node(mut self, node: impl Into<String>) -> Self {
253 self.node = Some(node.into());
254 self
255 }
256
257 #[must_use]
259 pub fn subject(mut self, subject: impl Into<String>) -> Self {
260 self.subject = Some(subject.into());
261 self
262 }
263
264 #[must_use]
266 pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
267 self.endpoint = Some(endpoint.into());
268 self
269 }
270
271 #[must_use]
273 pub fn task_queue(mut self, task_queue: impl Into<String>) -> Self {
274 self.task_queue = Some(task_queue.into());
275 self
276 }
277
278 #[must_use]
280 pub fn identity(mut self, identity: impl Into<String>) -> Self {
281 self.identity = Some(identity.into());
282 self
283 }
284
285 #[must_use]
287 pub const fn max_concurrency(mut self, max_concurrency: usize) -> Self {
288 self.max_concurrency = Some(max_concurrency);
289 self
290 }
291
292 #[must_use]
294 pub const fn reconnect_initial_backoff(mut self, delay: Duration) -> Self {
295 self.reconnect_initial_backoff = Some(delay);
296 self
297 }
298
299 #[must_use]
301 pub const fn reconnect_max_backoff(mut self, delay: Duration) -> Self {
302 self.reconnect_max_backoff = Some(delay);
303 self
304 }
305
306 #[must_use]
308 pub const fn reconnect_max_attempts(mut self, attempts: usize) -> Self {
309 self.reconnect_max_attempts = Some(attempts);
310 self
311 }
312
313 #[must_use]
315 pub fn transport_credentials(mut self, credentials: TransportCredentials) -> Self {
316 self.transport_credentials = Some(credentials);
317 self
318 }
319
320 pub fn build(self) -> Result<WorkerConfig, WorkerConfigBuildError> {
326 let namespaces = self
327 .namespaces
328 .filter(|namespaces| !namespaces.is_empty())
329 .unwrap_or_else(|| vec![String::from(DEFAULT_WORKER_NAMESPACE)]);
330 Ok(WorkerConfig {
331 namespaces,
332 subject: self
333 .subject
334 .unwrap_or_else(|| String::from(DEFAULT_WORKER_SUBJECT)),
335 endpoint: self
336 .endpoint
337 .ok_or(WorkerConfigBuildError::MissingEndpoint)?,
338 task_queue: self
339 .task_queue
340 .ok_or(WorkerConfigBuildError::MissingTaskQueue)?,
341 node: self.node.unwrap_or_else(default_node),
342 identity: self
343 .identity
344 .ok_or(WorkerConfigBuildError::MissingIdentity)?,
345 max_concurrency: self
346 .max_concurrency
347 .ok_or(WorkerConfigBuildError::MissingMaxConcurrency)?,
348 reconnect: ReconnectConfig {
349 initial_backoff: self
350 .reconnect_initial_backoff
351 .ok_or(WorkerConfigBuildError::MissingReconnectInitialBackoff)?,
352 max_backoff: self
353 .reconnect_max_backoff
354 .ok_or(WorkerConfigBuildError::MissingReconnectMaxBackoff)?,
355 max_attempts: self
356 .reconnect_max_attempts
357 .ok_or(WorkerConfigBuildError::MissingReconnectMaxAttempts)?,
358 },
359 transport_credentials: self.transport_credentials,
360 })
361 }
362}
363
364#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
366pub enum WorkerConfigBuildError {
367 #[error("worker endpoint is required")]
369 MissingEndpoint,
370 #[error("worker task queue is required")]
372 MissingTaskQueue,
373 #[error("worker identity is required")]
375 MissingIdentity,
376 #[error("worker max_concurrency is required")]
378 MissingMaxConcurrency,
379 #[error("worker reconnect_initial_backoff is required")]
381 MissingReconnectInitialBackoff,
382 #[error("worker reconnect_max_backoff is required")]
384 MissingReconnectMaxBackoff,
385 #[error("worker reconnect_max_attempts is required")]
387 MissingReconnectMaxAttempts,
388}
389
390#[cfg(test)]
391mod tests {
392 use std::time::Duration;
393
394 use super::{TransportCredentials, WorkerConfig};
395
396 #[test]
397 fn worker_config_builder_round_trips_fields() -> Result<(), Box<dyn std::error::Error>> {
398 let credentials = TransportCredentials::new(b"secret-token".to_vec());
399 let config = WorkerConfig::builder()
400 .endpoint("http://127.0.0.1:50051")
401 .task_queue("payments")
402 .identity("worker-a")
403 .max_concurrency(7)
404 .reconnect_initial_backoff(Duration::from_millis(10))
405 .reconnect_max_backoff(Duration::from_millis(100))
406 .reconnect_max_attempts(3)
407 .namespace("payments")
408 .subject("worker-a")
409 .transport_credentials(credentials.clone())
410 .build()?;
411
412 assert_eq!(config.namespaces, vec![String::from("payments")]);
413 assert_eq!(config.subject, "worker-a");
414 assert_eq!(config.endpoint, "http://127.0.0.1:50051");
415 assert_eq!(config.task_queue, "payments");
416 assert!(!config.node.is_empty(), "node defaults to the hostname");
417 assert_eq!(config.identity, "worker-a");
418 assert_eq!(config.max_concurrency, 7);
419 assert_eq!(config.reconnect.initial_backoff, Duration::from_millis(10));
420 assert_eq!(config.reconnect.max_backoff, Duration::from_millis(100));
421 assert_eq!(config.reconnect.max_attempts, 3);
422 assert_eq!(config.transport_credentials, Some(credentials));
423 assert!(!format!("{config:?}").contains("secret-token"));
424
425 Ok(())
426 }
427
428 #[test]
429 fn worker_config_new_uses_auth_metadata_defaults() {
430 let config = WorkerConfig::new(
431 "http://127.0.0.1:50051",
432 "default",
433 "worker-a",
434 4,
435 super::ReconnectConfig::new(Duration::from_millis(10), Duration::from_millis(100), 3),
436 None,
437 );
438
439 assert_eq!(config.namespaces, vec![String::from("default")]);
440 assert_eq!(config.subject, "worker");
441 assert!(!config.node.is_empty(), "node defaults to the hostname");
442 }
443
444 #[test]
445 fn worker_config_builder_carries_namespace_set_and_node()
446 -> Result<(), Box<dyn std::error::Error>> {
447 let config = WorkerConfig::builder()
448 .endpoint("http://127.0.0.1:50051")
449 .task_queue("default")
450 .identity("worker-a")
451 .max_concurrency(1)
452 .reconnect_initial_backoff(Duration::from_millis(5))
453 .reconnect_max_backoff(Duration::from_millis(20))
454 .reconnect_max_attempts(3)
455 .namespaces([String::from("a"), String::from("b")])
456 .node("host-7")
457 .build()?;
458
459 assert_eq!(
460 config.namespaces,
461 vec![String::from("a"), String::from("b")]
462 );
463 assert_eq!(config.node, "host-7");
464 Ok(())
465 }
466}