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};