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}