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 are submitted directly via `SupervisorApi::submit_with_task`.
67//!
68//! ## Policies
69//!
70//! | Policy               | Controls                                                 |
71//! |----------------------|----------------------------------------------------------|
72//! | [`RestartPolicy`]    | When to restart: `Never`, `OnFailure`, `Always`          |
73//! | [`BackoffPolicy`]    | Delay between retries: initial, max, factor, jitter      |
74//! | [`JitterPolicy`]     | Jitter strategy: `None`, `Full`, `Equal`, `Decorrelated` |
75//! | [`AdmissionPolicy`]  | Duplicate handling: `DropIfRunning`, `Replace`, `Queue`  |
76//!
77//! ## Construction
78//!
79//! [`TaskSpec`] fields are private; construct via [`TaskSpec::builder`]:
80//!
81//! ```text
82//! let spec = TaskSpec::builder("my-slot", kind, 5_000u64)
83//!     .restart(RestartPolicy::OnFailure)
84//!     .build()?;
85//! ```
86//!
87//! See [`TaskSpecBuilder`] for the full API.
88//!
89//! ## Also
90//!
91//! - `solti-runner` consumes [`TaskSpec`] and [`TaskKind`] to build executable tasks.
92//! - `solti-core` manages [`Task`] lifecycle and state transitions.
93//! - `solti-api` serializes/deserializes model types over gRPC and HTTP.
94//!
95//! ## Domain types
96//!
97//! | Type               | Description                                               |
98//! |--------------------|-----------------------------------------------------------|
99//! | [`Slot`]           | Logical execution lane (newtype over `Arc<str>`)          |
100//! | [`TaskId`]         | Unique task identifier (newtype over `Arc<str>`)          |
101//! | [`Timeout`]        | Per-attempt timeout in milliseconds                       |
102//! | [`Labels`]         | Key-value metadata for routing and filtering              |
103//! | [`TaskEnv`]        | Ordered environment variables for task execution          |
104//! | [`Flag`]           | Boolean toggle with `enabled()`/`disabled()` constructors |
105//! | [`TaskQuery`]      | Builder for filtered, paginated task listing              |
106//! | [`TaskPage`]       | Paginated query result                                    |
107//! | [`TaskSpecBuilder`]| Validated builder for [`TaskSpec`]                        |
108//!
109//! ## Example
110//!
111//! ```rust
112//! use solti_model::{
113//!     BackoffPolicy, JitterPolicy,
114//!     RestartPolicy, SubprocessMode, SubprocessSpec, Task, TaskKind, TaskPhase, TaskSpec,
115//! };
116//!
117//! // 1) Build a task spec via the builder
118//! let spec = TaskSpec::builder(
119//!     "my-worker",
120//!     TaskKind::Subprocess(SubprocessSpec {
121//!         mode: SubprocessMode::Command {
122//!             command: "echo".into(),
123//!             args: vec!["hello".into()],
124//!         },
125//!         env: Default::default(),
126//!         cwd: None,
127//!         fail_on_non_zero: Default::default(),
128//!     }),
129//!     5_000u64,
130//! )
131//! .restart(RestartPolicy::OnFailure)
132//! .backoff(BackoffPolicy {
133//!     jitter: JitterPolicy::Equal,
134//!     first_ms: 1_000,
135//!     max_ms: 30_000,
136//!     factor: 2.0,
137//! })
138//! .build()
139//! .expect("spec should be valid");
140//!
141//! // 2) Validate at submit boundary (checks business rules like no Embedded)
142//! spec.validate().expect("spec should pass submit validation");
143//!
144//! // 3) Create a task resource (normally done by the supervisor)
145//! let task = Task::new("task-001".into(), spec);
146//! assert_eq!(task.slot(), "my-worker");
147//! assert_eq!(*task.phase(), TaskPhase::Pending);
148//! assert_eq!(task.metadata().resource_version, 1);
149//! ```
150
151mod domain;
152pub use domain::{
153    AGENT_ID_MAX_LEN, AdmissionPolicy, AgentId, BackoffPolicy, ContainerSpec, DEFAULT_LIMIT, Flag,
154    JitterPolicy, KeyValue, Labels, LabelsIter, MAX_LIMIT, MAX_SCRIPT_BODY_BYTES, OutputChunk,
155    OutputEvent, RestartPolicy, RunnerEnv, RunnerSelector, Runtime, SLOT_MAX_LEN, SelectorOperator,
156    SelectorRequirement, Slot, StreamKind, SubprocessMode, SubprocessSpec, TASK_ID_MAX_LEN,
157    TaskEnv, TaskId, TaskKind, TaskPage, TaskPhase, TaskQuery, Timeout, WasmSpec, merge_env,
158};
159
160mod resource;
161pub use resource::{ObjectMeta, Task, TaskRun, TaskSpec, TaskSpecBuilder, TaskStatus};
162
163mod error;
164pub use error::{ModelError, ModelResult};