Add PrettyDuration, job timeout, job max schedule drift and job retry limit

This commit is contained in:
Arshia Ghafoori
2025-01-13 12:24:55 +00:00
parent d1c6dffe31
commit 1f6ced514c
5 changed files with 273 additions and 13 deletions

View File

@@ -152,9 +152,13 @@
"properties": {
"max_age": {
"description": "Maximum age of snapshots.\n\nFormat: 5m, 1h, 2d, ...\n\nAfter the specified time new snapshots will be created, and the old ones discarded.",
"type": [
"string",
"null"
"anyOf": [
{
"$ref": "#/definitions/PrettyDuration"
},
{
"type": "null"
}
]
},
"requests": {
@@ -295,6 +299,17 @@
"type": "string"
}
},
"max_schedule_drift": {
"description": "Don't start job if past the due time by this amount, instead opting to wait for the next instance of it to be triggered.",
"anyOf": [
{
"$ref": "#/definitions/PrettyDuration"
},
{
"type": "null"
}
]
},
"package": {
"description": "The package that contains the command to run. Defaults to the app config's package.",
"anyOf": [
@@ -306,6 +321,24 @@
}
]
},
"retries": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"timeout": {
"anyOf": [
{
"$ref": "#/definitions/PrettyDuration"
},
{
"type": "null"
}
]
},
"volumes": {
"type": [
"array",
@@ -397,9 +430,13 @@
},
"timeout": {
"description": "Request timeout.\n\nFormat: 1s, 5m, 11h, ...",
"type": [
"string",
"null"
"anyOf": [
{
"$ref": "#/definitions/PrettyDuration"
},
{
"type": "null"
}
]
},
"unhealthy_threshold": {
@@ -492,9 +529,13 @@
},
"timeout": {
"description": "Request timeout.\n\nFormat: 1s, 5m, 11h, ...",
"type": [
"string",
"null"
"anyOf": [
{
"$ref": "#/definitions/PrettyDuration"
},
{
"type": "null"
}
]
}
}
@@ -593,6 +634,9 @@
"PackageSource": {
"type": "string"
},
"PrettyDuration": {
"type": "string"
},
"Redirect": {
"description": "App redirect configuration.",
"type": "object",

View File

@@ -1,3 +1,5 @@
use super::pretty_duration::PrettyDuration;
/// Defines an HTTP request.
#[derive(
schemars::JsonSchema, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone, Debug,
@@ -24,7 +26,7 @@ pub struct HttpRequest {
///
/// Format: 1s, 5m, 11h, ...
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<String>,
pub timeout: Option<PrettyDuration>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expect: Option<HttpRequestExpect>,

View File

@@ -4,7 +4,7 @@ use serde::{de::Error, Deserialize, Serialize};
use crate::package::PackageSource;
use super::{AppConfigCapabilityMemoryV1, AppVolume, HttpRequest};
use super::{pretty_duration::PrettyDuration, AppConfigCapabilityMemoryV1, AppVolume, HttpRequest};
/// Job configuration.
#[derive(
@@ -69,6 +69,18 @@ pub struct ExecutableJob {
#[serde(skip_serializing_if = "Option::is_none")]
pub volumes: Option<Vec<AppVolume>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<PrettyDuration>,
/// Don't start job if past the due time by this amount,
/// instead opting to wait for the next instance of it
/// to be triggered.
#[serde(skip_serializing_if = "Option::is_none")]
pub max_schedule_drift: Option<PrettyDuration>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retries: Option<u32>,
}
#[derive(
@@ -215,6 +227,9 @@ mod tests {
name: "vol".to_owned(),
mount: "/path/to/volume".to_owned(),
}]),
timeout: Some("1m".parse().unwrap()),
max_schedule_drift: Some("2h".parse().unwrap()),
retries: None,
}),
};
@@ -234,7 +249,9 @@ execute:
limit: '1000.0 MB'
volumes:
- name: vol
mount: /path/to/volume"#;
mount: /path/to/volume
timeout: '1m'
max_schedule_drift: '2h'"#;
assert_eq!(
serialized.trim(),

View File

@@ -3,6 +3,7 @@
mod healthcheck;
mod http;
mod job;
mod pretty_duration;
pub use self::{healthcheck::*, http::*, job::*};
@@ -10,6 +11,7 @@ use std::collections::HashMap;
use anyhow::{bail, Context};
use bytesize::ByteSize;
use pretty_duration::PrettyDuration;
use crate::package::PackageSource;
@@ -256,7 +258,7 @@ pub struct AppConfigCapabilityInstaBootV1 {
/// After the specified time new snapshots will be created, and the old
/// ones discarded.
#[serde(skip_serializing_if = "Option::is_none")]
pub max_age: Option<String>,
pub max_age: Option<PrettyDuration>,
}
/// App redirect configuration.

View File

@@ -0,0 +1,195 @@
use std::{
borrow::Cow,
fmt::{Debug, Display},
str::FromStr,
time::Duration,
};
use schemars::JsonSchema;
use serde::{de::Error, Deserialize, Serialize};
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct PrettyDuration {
unit: DurationUnit,
amount: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum DurationUnit {
Seconds,
Minutes,
Hours,
Days,
}
impl PrettyDuration {
pub fn as_duration(&self) -> Duration {
match self.unit {
DurationUnit::Seconds => Duration::from_secs(self.amount),
DurationUnit::Minutes => Duration::from_secs(self.amount * 60),
DurationUnit::Hours => Duration::from_secs(self.amount * 60 * 60),
DurationUnit::Days => Duration::from_secs(self.amount * 60 * 60 * 24),
}
}
}
impl Default for PrettyDuration {
fn default() -> Self {
Self {
unit: DurationUnit::Seconds,
amount: 0,
}
}
}
impl PartialOrd for PrettyDuration {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PrettyDuration {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_duration().cmp(&other.as_duration())
}
}
impl JsonSchema for PrettyDuration {
fn schema_name() -> String {
"PrettyDuration".to_owned()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
String::json_schema(gen)
}
}
impl Display for DurationUnit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DurationUnit::Seconds => write!(f, "s"),
DurationUnit::Minutes => write!(f, "m"),
DurationUnit::Hours => write!(f, "h"),
DurationUnit::Days => write!(f, "d"),
}
}
}
impl FromStr for DurationUnit {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"s" | "S" => Ok(Self::Seconds),
"m" | "M" => Ok(Self::Minutes),
"h" | "H" => Ok(Self::Hours),
"d" | "D" => Ok(Self::Days),
_ => Err(()),
}
}
}
impl Display for PrettyDuration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.amount, self.unit)
}
}
impl Debug for PrettyDuration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<Self as Display>::fmt(self, f)
}
}
impl FromStr for PrettyDuration {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (amount_str, unit_str) = s.split_at_checked(s.len() - 1).ok_or(())?;
Ok(Self {
unit: unit_str.parse()?,
amount: amount_str.parse().map_err(|_| ())?,
})
}
}
impl Serialize for PrettyDuration {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for PrettyDuration {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let repr: Cow<'de, str> = Cow::deserialize(deserializer)?;
repr.parse()
.map_err(|()| D::Error::custom("Failed to parse value as a duration"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
pub fn pretty_duration_serialize() {
assert_eq!(
PrettyDuration {
unit: DurationUnit::Seconds,
amount: 1234
}
.to_string(),
"1234s"
);
assert_eq!(
PrettyDuration {
unit: DurationUnit::Minutes,
amount: 345
}
.to_string(),
"345m"
);
assert_eq!(
PrettyDuration {
unit: DurationUnit::Hours,
amount: 56
}
.to_string(),
"56h"
);
assert_eq!(
PrettyDuration {
unit: DurationUnit::Days,
amount: 7
}
.to_string(),
"7d"
);
}
#[test]
pub fn pretty_duration_deserialize() {
fn assert_deserializes_to(repr1: &str, repr2: &str, unit: DurationUnit, amount: u64) {
let duration = PrettyDuration { unit, amount };
assert_eq!(duration, repr1.parse().unwrap());
assert_eq!(duration, repr2.parse().unwrap());
}
assert_deserializes_to("12s", "12S", DurationUnit::Seconds, 12);
assert_deserializes_to("34m", "34M", DurationUnit::Minutes, 34);
assert_deserializes_to("56h", "56H", DurationUnit::Hours, 56);
assert_deserializes_to("7d", "7D", DurationUnit::Days, 7);
}
#[test]
#[should_panic]
pub fn cant_parse_nagative_duration() {
_ = "-12s".parse::<PrettyDuration>().unwrap();
}
}