Skip to main content

solti_model/
lib.rs

1//! # solti-model
2//!
3//! Domain model for the solti task execution system.
4//!
5//! This crate defines the core resource types.
6//!
7//! ## Architecture
8//!
9//! ```text
10//!  ┌──────────────────────────────────────────────────────────┐
11//!  │                      Task                                │
12//!  │                                                          │
13//!  │  ObjectMeta            TaskSpec            TaskStatus    │
14//!  │  ├─ id: TaskId         ├─ slot: Slot       ├─ phase      │
15//!  │  ├─ resource_version   ├─ kind: TaskKind   ├─ attempt    │
16//!  │  ├─ created_at         ├─ timeout          ├─ exit_code  │
17//!  │  └─ updated_at         ├─ restart          └─ error      │
18//!  │                        ├─ backoff                        │
19//!  │                        ├─ admission                      │
20//!  │                        ├─ runner_selector                │
21//!  │                        └─ labels                         │
22//!  └──────────────────────────────────────────────────────────┘
23//! ```
24//!
25//! ## Resource model
26//!
27//! | Section         | Type             | Responsibility                                                  |
28//! |-----------------|------------------|-----------------------------------------------------------------|
29//! | **metadata**    | [`ObjectMeta`]   | Identity, versioning, timestamps                                |
30//! | **status**      | [`TaskStatus`]   | Observed state: phase, attempt count, exit code, last error     |
31//! | **spec**        | [`TaskSpec`]     | Desired state (private fields; build via [`TaskSpec::builder`]) |
32//!
33//! Slot and labels live in `spec` as the single source of truth.
34//! [`Task`] provides convenience accessors ([`Task::slot`], [`Task::labels`]) that delegate to `spec`.
35//!
36//! ## Versioning
37//!
38//! [`ObjectMeta::resource_version`] is a monotonic counter bumped on every change
39//! (spec or status) for optimistic concurrency.
40//!
41//! ## Task lifecycle
42//!
43//! ```text
44//!  Pending ──► Running ──► Succeeded
45//!                │
46//!                ├──► Failed ──► (restart) ──► Running
47//!                ├──► Timeout
48//!                ├──► Canceled
49//!                └──► Exhausted (max retries reached)
50//! ```
51//!
52//! Terminal phases: `Succeeded`, `Failed`, `Timeout`, `Canceled`, `Exhausted`.
53//! See [`TaskPhase::is_terminal`].
54//!
55//! ## Task kinds
56//!
57//! [`TaskKind`] defines what a task actually runs:
58//!
59//! | Variant        | Description                                          |
60//! |----------------|------------------------------------------------------|
61//! | `Subprocess`   | External process (`command`, `args`, `env`, `cwd`)   |
62//! | `Wasm`         | WebAssembly module                                   |
63//! | `Container`    | OCI container image                                  |
64//! | `Embedded`     | Code-defined task (in-process `TaskRef`)             |
65//!
66//! `Subprocess` tasks go through `solti_runner::RunnerRouter`; `Embedded` tasks
67//! are submitted directly via `SupervisorApi::submit_with_task`.
68//!
69//! ## Policies
70//!
71//! | Policy               | Controls                                                 |
72//! |----------------------|----------------------------------------------------------|
73//! | [`RestartPolicy`]    | When to restart: `Never`, `OnFailure`, `Always`          |
74//! | [`BackoffPolicy`]    | Delay between retries: initial, max, factor, jitter      |
75//! | [`JitterPolicy`]     | Jitter strategy: `None`, `Full`, `Equal`, `Decorrelated` |
76//! | [`AdmissionPolicy`]  | Duplicate handling: `DropIfRunning`, `Replace`, `Queue`  |
77//!
78//! ## Construction
79//!
80//! [`TaskSpec`] fields are private; construct via [`TaskSpec::builder`]:
81//!
82//! ```text
83//! let spec = TaskSpec::builder("my-slot", kind, 5_000u64)
84//!     .restart(RestartPolicy::OnFailure)
85//!     .build()?;
86//! ```
87//!
88//! See [`TaskSpecBuilder`] for the full API.
89//!
90//! ## Also
91//!
92//! - `solti-runner` consumes [`TaskSpec`] and [`TaskKind`] to build executable tasks.
93//! - `solti-core` manages [`Task`] lifecycle and state transitions.
94//! - `solti-api` serializes/deserializes model types over gRPC and HTTP.
95//!
96//! ## Domain types
97//!
98//! | Type               | Description                                               |
99//! |--------------------|-----------------------------------------------------------|
100//! | [`Slot`]           | Logical execution lane (newtype over `Arc<str>`)          |
101//! | [`TaskId`]         | Unique task identifier (newtype over `Arc<str>`)          |
102//! | [`Timeout`]        | Per-attempt timeout in milliseconds                       |
103//! | [`Labels`]         | Key-value metadata for routing and filtering              |
104//! | [`TaskEnv`]        | Ordered environment variables for task execution          |
105//! | [`Flag`]           | Boolean toggle with `enabled()`/`disabled()` constructors |
106//! | [`TaskQuery`]      | Builder for filtered, paginated task listing              |
107//! | [`TaskPage`]       | Paginated query result                                    |
108//! | [`TaskSpecBuilder`]| Validated builder for [`TaskSpec`]                        |
109//!
110//! ## Example
111//!
112//! ```rust
113//! use solti_model::{
114//!     BackoffPolicy, JitterPolicy,
115//!     RestartPolicy, SubprocessMode, SubprocessSpec, Task, TaskKind, TaskPhase, TaskSpec,
116//! };
117//!
118//! // 1) Build a task spec via the builder
119//! let spec = TaskSpec::builder(
120//!     "my-worker",
121//!     TaskKind::Subprocess(SubprocessSpec {
122//!         mode: SubprocessMode::Command {
123//!             command: "echo".into(),
124//!             args: vec!["hello".into()],
125//!         },
126//!         env: Default::default(),
127//!         cwd: None,
128//!         fail_on_non_zero: Default::default(),
129//!     }),
130//!     5_000u64,
131//! )
132//! .restart(RestartPolicy::OnFailure)
133//! .backoff(BackoffPolicy {
134//!     jitter: JitterPolicy::Equal,
135//!     first_ms: 1_000,
136//!     max_ms: 30_000,
137//!     factor: 2.0,
138//! })
139//! .build()
140//! .expect("spec should be valid");
141//!
142//! // 2) Validate at submit boundary (checks business rules like no Embedded)
143//! spec.validate().expect("spec should pass submit validation");
144//!
145//! // 3) Create a task resource (normally done by the supervisor)
146//! let task = Task::new("task-001".into(), spec);
147//! assert_eq!(task.slot(), "my-worker");
148//! assert_eq!(*task.phase(), TaskPhase::Pending);
149//! assert_eq!(task.metadata().resource_version, 1);
150//! ```
151
152mod domain;
153pub use domain::{
154    AGENT_ID_MAX_LEN, AdmissionPolicy, AgentId, BackoffPolicy, ContainerSpec, DEFAULT_LIMIT, Flag,
155    JitterPolicy, KeyValue, Labels, LabelsIter, MAX_LIMIT, MAX_SCRIPT_BODY_BYTES, RestartPolicy,
156    RunnerEnv, RunnerSelector, Runtime, SLOT_MAX_LEN, SelectorOperator, SelectorRequirement, Slot,
157    SubprocessMode, SubprocessSpec, TASK_ID_MAX_LEN, TaskEnv, TaskId, TaskKind, TaskPage,
158    TaskPhase, TaskQuery, Timeout, WasmSpec, merge_env,
159};
160
161mod resource;
162pub use resource::{ObjectMeta, Task, TaskRun, TaskSpec, TaskSpecBuilder, TaskStatus};
163
164mod error;
165pub use error::{ModelError, ModelResult};