solti_api/handler.rs
1//! # Handler trait.
2//!
3//! [`ApiHandler`] defines the transport-agnostic API surface.
4//! Implement this trait to plug custom logic (auth, rate limiting, metrics) between the wire layer and the supervisor.
5
6use std::pin::Pin;
7
8use async_trait::async_trait;
9use solti_model::{
10 AdmissionPolicy, OutputEvent, Task, TaskId, TaskPage, TaskQuery, TaskRun, TaskSpec,
11};
12use tokio_stream::Stream;
13
14use crate::error::ApiError;
15
16/// Boxed stream of [`OutputEvent`]s — the wire-side surface of live task logs.
17pub type OutputEventStream = Pin<Box<dyn Stream<Item = OutputEvent> + Send + 'static>>;
18
19/// Task execution API handler.
20///
21/// ## Also
22///
23/// - [`SupervisorApiAdapter`](crate::SupervisorApiAdapter) ready-to-use implementation.
24/// - [`ApiError`](crate::ApiError) error type returned by all methods.
25///
26/// This trait abstracts the backend implementation, allowing users to:
27/// - Use the provided [`SupervisorApiAdapter`](crate::SupervisorApiAdapter)
28/// - Implement custom handlers with additional logic (auth, rate limiting, etc.)
29///
30/// ## API surface
31///
32/// | Method | HTTP | gRPC |
33/// |--------------------|-----------------------------------|---------------------|
34/// | `submit_task` | `POST /api/v1/tasks` | `SubmitTask` |
35/// | `apply_task` | `PUT /api/v1/tasks` | `ApplyTask` |
36/// | `get_task_status` | `GET /api/v1/tasks/{id}` | `GetTaskStatus` |
37/// | `query_tasks` | `GET /api/v1/tasks` | `ListTasks` |
38/// | `list_task_runs` | `GET /api/v1/tasks/{id}/runs` | `ListTaskRuns` |
39/// | `delete_task` | `DELETE /api/v1/tasks/{id}` | `DeleteTask` |
40/// | `stream_task_logs` | `GET /api/v1/tasks/{id}/logs` | `StreamTaskLogs` |
41#[async_trait]
42pub trait ApiHandler: Send + Sync + 'static {
43 /// Submit a new task for execution.
44 async fn submit_task(&self, spec: TaskSpec) -> Result<TaskId, ApiError>;
45
46 /// Apply a spec to its slot (declarative upsert).
47 /// Returns the id of the task running in the slot after apply.
48 ///
49 /// Note: this **forces** [`AdmissionPolicy::Replace`], overriding any admission
50 /// policy on the supplied `spec` — that is the point of "apply" (latest spec
51 /// wins the slot). Use [`submit_task`](Self::submit_task) to honor the spec's
52 /// own admission policy.
53 async fn apply_task(&self, spec: TaskSpec) -> Result<TaskId, ApiError> {
54 self.submit_task(spec.with_admission(AdmissionPolicy::Replace))
55 .await
56 }
57
58 /// Get current status of a task by ID.
59 async fn get_task_status(&self, id: &TaskId) -> Result<Option<Task>, ApiError>;
60
61 /// Query tasks with combined filters and pagination.
62 ///
63 /// Supports filtering by slot and/or status simultaneously, with offset/limit pagination. Returns a page with total count.
64 async fn query_tasks(&self, query: TaskQuery) -> Result<TaskPage<Task>, ApiError>;
65
66 /// List execution history for a specific task (oldest first).
67 async fn list_task_runs(&self, id: &TaskId) -> Result<Vec<TaskRun>, ApiError>;
68
69 /// Stop a task and purge its run history.
70 ///
71 /// Idempotent:
72 /// returns `Ok(())` whether the task is currently registered on the agent.
73 /// Errors only on supervisor cancellation failures (timeout, internal error).
74 async fn delete_task(&self, id: &TaskId) -> Result<(), ApiError>;
75
76 /// Subscribe to the live-tail stream of stdout/stderr lines for a task.
77 ///
78 /// Returns an [`OutputEventStream`] that yields [`OutputEvent`]s in real time.
79 /// The stream covers all subsequent runs of the task (multi-run merge) and ends when the task is fully terminal and evicted.
80 async fn stream_task_logs(&self, _id: &TaskId) -> Result<OutputEventStream, ApiError> {
81 Ok(Box::pin(tokio_stream::empty()))
82 }
83}