use std::borrow::Cow;
use anyhow::Result;
use chrono::DateTime;
use chrono::Utc;
use futures::future::BoxFuture;
use serde::Deserialize;
use serde::Serialize;
use tes::v1::types::requests::GetTaskParams;
use tes::v1::types::requests::ListTasksParams;
use tes::v1::types::requests::Task as RequestTask;
use tes::v1::types::responses::OutputFile;
use tes::v1::types::responses::TaskResponse;
use tes::v1::types::task::Executor;
use tes::v1::types::task::Input;
use tes::v1::types::task::Output;
use tes::v1::types::task::State;
#[cfg(feature = "postgres")]
pub mod postgres;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("page token `{0}` is not valid")]
InvalidPageToken(String),
#[cfg(feature = "postgres")]
#[error(transparent)]
Postgres(#[from] postgres::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
pub type DatabaseResult<T> = Result<T, Error>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskIo {
pub inputs: Vec<Input>,
pub outputs: Vec<Output>,
}
#[derive(Debug, Clone)]
pub struct TerminatedContainer<'a> {
pub name: &'a str,
pub executor_index: Option<i32>,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub stdout: Option<Cow<'a, str>>,
pub stderr: Option<Cow<'a, str>>,
pub exit_code: i32,
}
#[derive(Debug, Clone)]
pub struct TaskTemplateData {
pub id: String,
pub username: String,
pub preemptible: bool,
pub cpu: Option<i32>,
pub memory: Option<f64>,
pub disk: Option<f64>,
pub inputs: Vec<Input>,
pub outputs: Vec<Output>,
pub volumes: Vec<String>,
pub executors: Vec<Executor>,
}
#[derive(Debug, Clone, Default)]
pub struct ContainerUsageSample {
pub tes_id: String,
pub pod_name: String,
pub container_name: String,
pub memory_bytes: Option<i64>,
pub cpu_seconds: Option<f64>,
pub start_time_seconds: Option<f64>,
}
impl ContainerUsageSample {
pub fn is_empty(&self) -> bool {
self.memory_bytes.is_none() && self.cpu_seconds.is_none()
}
}
pub fn normalize_cpu_seconds(cpu_seconds: f64) -> Option<f64> {
if !cpu_seconds.is_finite() || cpu_seconds < 0.0 {
None
} else if cpu_seconds == 0.0 {
Some(0.0)
} else {
Some(cpu_seconds)
}
}
#[async_trait::async_trait]
pub trait Database: Send + Sync + 'static {
async fn insert_task(&self, username: &str, task: &RequestTask) -> DatabaseResult<String>;
async fn get_task(
&self,
username: &str,
tes_id: &str,
params: GetTaskParams,
) -> DatabaseResult<TaskResponse>;
async fn get_tasks(
&self,
username: &str,
params: ListTasksParams,
) -> DatabaseResult<(Vec<TaskResponse>, Option<String>)>;
async fn get_task_template_data(&self, tes_id: &str) -> DatabaseResult<TaskTemplateData>;
async fn get_in_progress_tasks(&self, before: DateTime<Utc>) -> DatabaseResult<Vec<String>>;
async fn update_task_state<'a>(
&self,
tes_id: &str,
state: State,
messages: &[&str],
containers: Option<BoxFuture<'a, Result<Vec<TerminatedContainer<'a>>>>>,
outputs: Option<&[OutputFile]>,
) -> DatabaseResult<bool>;
async fn append_system_log(&self, tes_id: &str, messages: &[&str]) -> DatabaseResult<()>;
async fn add_task_resource_usage_samples(
&self,
samples: &[ContainerUsageSample],
) -> DatabaseResult<()>;
async fn insert_error(
&self,
source: &str,
tes_id: Option<&str>,
message: &str,
) -> DatabaseResult<()>;
}
#[macro_export]
macro_rules! format_log_message {
($($arg:tt)*) => { format!("[{ts}] {args}", ts = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.6fZ"), args = format_args!($($arg)*)) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cpu_seconds_require_finite_non_negative_values() {
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.001] {
assert_eq!(normalize_cpu_seconds(value), None);
}
assert_eq!(normalize_cpu_seconds(1.5), Some(1.5));
}
#[test]
fn cpu_seconds_normalize_negative_zero() {
let value = normalize_cpu_seconds(-0.0).expect("negative zero should be valid");
assert_eq!(value, 0.0);
assert!(!value.is_sign_negative());
}
}