Skip to main content

ralph/contracts/config/
parallel.rs

1//! Parallel run-loop configuration.
2//!
3//! Responsibilities:
4//! - Define parallel config struct, merge behavior, and related enums.
5//!
6//! Not handled here:
7//! - Parallel execution logic (see `crate::parallel` module).
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12
13/// Parallel run-loop configuration.
14#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
15#[serde(default, deny_unknown_fields)]
16pub struct ParallelConfig {
17    /// Number of workers to run concurrently when parallel mode is enabled.
18    #[schemars(range(min = 2))]
19    pub workers: Option<u8>,
20
21    /// Root directory for parallel workspaces (relative to repo root if not absolute).
22    pub workspace_root: Option<PathBuf>,
23
24    /// Maximum number of push attempts before giving up.
25    #[schemars(range(min = 1))]
26    pub max_push_attempts: Option<u8>,
27
28    /// Backoff intervals in milliseconds for push retries.
29    pub push_backoff_ms: Option<Vec<u64>>,
30
31    /// Hours to retain blocked workspaces before cleanup.
32    #[schemars(range(min = 1))]
33    pub workspace_retention_hours: Option<u32>,
34}
35
36impl ParallelConfig {
37    pub fn merge_from(&mut self, other: Self) {
38        if other.workers.is_some() {
39            self.workers = other.workers;
40        }
41        if other.workspace_root.is_some() {
42            self.workspace_root = other.workspace_root;
43        }
44        if other.max_push_attempts.is_some() {
45            self.max_push_attempts = other.max_push_attempts;
46        }
47        if other.push_backoff_ms.is_some() {
48            self.push_backoff_ms = other.push_backoff_ms;
49        }
50        if other.workspace_retention_hours.is_some() {
51            self.workspace_retention_hours = other.workspace_retention_hours;
52        }
53    }
54}
55
56/// Default push backoff intervals in milliseconds.
57pub fn default_push_backoff_ms() -> Vec<u64> {
58    vec![500, 2000, 5000, 10000]
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn parallel_config_merge_prefers_other_when_some() {
67        let mut base = ParallelConfig {
68            workers: Some(2),
69            workspace_root: None,
70            max_push_attempts: Some(3),
71            push_backoff_ms: None,
72            workspace_retention_hours: Some(12),
73        };
74
75        let other = ParallelConfig {
76            workers: Some(4),
77            workspace_root: Some(PathBuf::from("/tmp/ws")),
78            max_push_attempts: None,
79            push_backoff_ms: Some(vec![1000, 2000]),
80            workspace_retention_hours: None,
81        };
82
83        base.merge_from(other);
84
85        assert_eq!(base.workers, Some(4));
86        assert_eq!(base.workspace_root, Some(PathBuf::from("/tmp/ws")));
87        assert_eq!(base.max_push_attempts, Some(3)); // unchanged
88        assert_eq!(base.push_backoff_ms, Some(vec![1000, 2000]));
89        assert_eq!(base.workspace_retention_hours, Some(12)); // unchanged
90    }
91
92    #[test]
93    fn default_push_backoff_ms_has_expected_values() {
94        let backoff = default_push_backoff_ms();
95        assert_eq!(backoff, vec![500, 2000, 5000, 10000]);
96    }
97}