Skip to main content

planetary_db/
lib.rs

1//! Implementation of database support for Planetary.
2
3use std::borrow::Cow;
4
5use anyhow::Result;
6use chrono::DateTime;
7use chrono::Utc;
8use futures::future::BoxFuture;
9use serde::Deserialize;
10use serde::Serialize;
11use tes::v1::types::requests::GetTaskParams;
12use tes::v1::types::requests::ListTasksParams;
13use tes::v1::types::requests::Task as RequestTask;
14use tes::v1::types::responses::OutputFile;
15use tes::v1::types::responses::TaskResponse;
16use tes::v1::types::task::Executor;
17use tes::v1::types::task::Input;
18use tes::v1::types::task::Output;
19use tes::v1::types::task::State;
20
21#[cfg(feature = "postgres")]
22pub mod postgres;
23
24/// Represents a database error.
25#[derive(Debug, thiserror::Error)]
26pub enum Error {
27    /// The provided page token wasn't valid.
28    #[error("page token `{0}` is not valid")]
29    InvalidPageToken(String),
30    /// A PostgreSQL error occurred.
31    #[cfg(feature = "postgres")]
32    #[error(transparent)]
33    Postgres(#[from] postgres::Error),
34    /// Another type of error occurred during the database operation.
35    #[error(transparent)]
36    Other(#[from] anyhow::Error),
37}
38
39/// The result type for database operations.
40pub type DatabaseResult<T> = Result<T, Error>;
41
42/// Represents information about a task's inputs and outputs.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct TaskIo {
45    /// The list of inputs for the task.
46    pub inputs: Vec<Input>,
47    /// The list of outputs for the task.
48    pub outputs: Vec<Output>,
49}
50
51/// Represents information about a terminated container.
52#[derive(Debug, Clone)]
53pub struct TerminatedContainer<'a> {
54    /// The name of the container.
55    pub name: &'a str,
56    /// The index of the executor.
57    ///
58    /// This is `None` when the container was not an executor.
59    pub executor_index: Option<i32>,
60    /// The start time of the container.
61    pub start_time: DateTime<Utc>,
62    /// The end time of the container.
63    pub end_time: DateTime<Utc>,
64    /// The stdout of the container.
65    pub stdout: Option<Cow<'a, str>>,
66    /// The stderr of the container.
67    pub stderr: Option<Cow<'a, str>>,
68    /// The exit code of the container.
69    pub exit_code: i32,
70}
71
72/// Represents data used for rendering task resource templates.
73#[derive(Debug, Clone)]
74pub struct TaskTemplateData {
75    /// The TES identifier of the task.
76    pub id: String,
77    /// The username associated with the task.
78    pub username: String,
79    /// Whether or not the task is preemptible.
80    pub preemptible: bool,
81    /// The requested CPU cores for the task.
82    ///
83    /// This is `None` when the default CPU cores should be used.
84    pub cpu: Option<i32>,
85    /// The requested memory for the task (in gigabytes).
86    ///
87    /// This is `None` when the default memory should be used.
88    pub memory: Option<f64>,
89    /// The requested disk for the task (in gigabytes).
90    ///
91    /// This is `None` when the default disk size should be used.
92    pub disk: Option<f64>,
93    /// The task's inputs.
94    pub inputs: Vec<Input>,
95    /// The task's outputs.
96    pub outputs: Vec<Output>,
97    /// The task's volumes.
98    pub volumes: Vec<String>,
99    /// The task's executors.
100    pub executors: Vec<Executor>,
101}
102
103/// A single resource usage observation for one container of a task's pod.
104///
105/// Containers are identified by their pod name and Kubernetes container name
106/// (`inputs`, `executor-N`, or `outputs`). Each dimension is optional; a
107/// dimension that could not be measured for a sampling round is `None` and
108/// does not affect the container's aggregate.
109#[derive(Debug, Clone, Default)]
110pub struct ContainerUsageSample {
111    /// The TES identifier of the task.
112    pub tes_id: String,
113    /// The name of the pod hosting the container.
114    pub pod_name: String,
115    /// The name of the container within the pod.
116    pub container_name: String,
117    /// The sampled working set memory of the container, in bytes.
118    pub memory_bytes: Option<i64>,
119    /// The observed cumulative CPU time of the container instance, in
120    /// seconds.
121    pub cpu_seconds: Option<f64>,
122    /// The container instance's start time, in seconds since the Unix epoch
123    /// of the node's clock, used to detect container restarts.
124    pub start_time_seconds: Option<f64>,
125}
126
127impl ContainerUsageSample {
128    /// Whether the sample carries no measurements.
129    pub fn is_empty(&self) -> bool {
130        self.memory_bytes.is_none() && self.cpu_seconds.is_none()
131    }
132}
133
134/// Returns a finite, non-negative CPU observation with negative zero
135/// normalized to zero.
136pub fn normalize_cpu_seconds(cpu_seconds: f64) -> Option<f64> {
137    if !cpu_seconds.is_finite() || cpu_seconds < 0.0 {
138        None
139    } else if cpu_seconds == 0.0 {
140        Some(0.0)
141    } else {
142        Some(cpu_seconds)
143    }
144}
145
146/// An abstraction for the planetary database.
147#[async_trait::async_trait]
148pub trait Database: Send + Sync + 'static {
149    /// Inserts a task into the database for the specified user.
150    ///
151    /// Note: it is expected that the newly inserted task has the `UNKNOWN`
152    /// state.
153    ///
154    /// Returns the generated TES task identifier.
155    async fn insert_task(&self, username: &str, task: &RequestTask) -> DatabaseResult<String>;
156
157    /// Gets a task from the database for the specified user.
158    async fn get_task(
159        &self,
160        username: &str,
161        tes_id: &str,
162        params: GetTaskParams,
163    ) -> DatabaseResult<TaskResponse>;
164
165    /// Gets tasks from the database for the specified user.
166    ///
167    /// Returns a list of tasks and the page token to use for the next request.
168    async fn get_tasks(
169        &self,
170        username: &str,
171        params: ListTasksParams,
172    ) -> DatabaseResult<(Vec<TaskResponse>, Option<String>)>;
173
174    /// Gets a task's template data for use in rendering Kubernetes resource
175    /// templates.
176    ///
177    /// NOTE: this database operation does _not_ filter the template data by
178    /// user; it should only be used from internal contexts (e.g. from the
179    /// Orchestrator) where authorization is assumed.
180    async fn get_task_template_data(&self, tes_id: &str) -> DatabaseResult<TaskTemplateData>;
181
182    /// Gets the TES identifiers of in-progress tasks.
183    ///
184    /// Only tasks created before the given datetime are returned.
185    ///
186    /// An in-progress task is in one of the following states:
187    ///
188    /// * Unknown
189    /// * Queued
190    /// * Initializing
191    /// * Running
192    async fn get_in_progress_tasks(&self, before: DateTime<Utc>) -> DatabaseResult<Vec<String>>;
193
194    /// Updates the state of a task.
195    ///
196    /// The provided message is added to the task's system log if the task is
197    /// transitioned to the given state.
198    ///
199    /// The given future for retrieving the terminated containers will be called
200    /// if the task is transitioned to the given state; the returned containers
201    /// are then recorded in the database.
202    ///
203    /// The given output files will be associated with the task if the task
204    /// transitions to the given state; if `None`, the task's output files will
205    /// not be set.
206    ///
207    /// Returns `Ok(true)` if the status was updated or `Ok(false)` if the
208    /// task's current state cannot be transitioned to the given state.
209    async fn update_task_state<'a>(
210        &self,
211        tes_id: &str,
212        state: State,
213        messages: &[&str],
214        containers: Option<BoxFuture<'a, Result<Vec<TerminatedContainer<'a>>>>>,
215        outputs: Option<&[OutputFile]>,
216    ) -> DatabaseResult<bool>;
217
218    /// Appends the given messages to the task's system log.
219    async fn append_system_log(&self, tes_id: &str, messages: &[&str]) -> DatabaseResult<()>;
220
221    /// Records per-container resource usage observations.
222    ///
223    /// CPU counters are folded idempotently against durable per-container
224    /// baselines, including after retries, missed rounds, and container
225    /// restarts.
226    async fn add_task_resource_usage_samples(
227        &self,
228        samples: &[ContainerUsageSample],
229    ) -> DatabaseResult<()>;
230
231    /// Inserts an internal system error with the database.
232    async fn insert_error(
233        &self,
234        source: &str,
235        tes_id: Option<&str>,
236        message: &str,
237    ) -> DatabaseResult<()>;
238}
239
240/// Formats a log message by including a time stamp.
241#[macro_export]
242macro_rules! format_log_message {
243    ($($arg:tt)*) => { format!("[{ts}] {args}", ts = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.6fZ"), args = format_args!($($arg)*)) }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn cpu_seconds_require_finite_non_negative_values() {
252        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.001] {
253            assert_eq!(normalize_cpu_seconds(value), None);
254        }
255
256        assert_eq!(normalize_cpu_seconds(1.5), Some(1.5));
257    }
258
259    #[test]
260    fn cpu_seconds_normalize_negative_zero() {
261        let value = normalize_cpu_seconds(-0.0).expect("negative zero should be valid");
262
263        assert_eq!(value, 0.0);
264        assert!(!value.is_sign_negative());
265    }
266}