Skip to content

Commit

Permalink
Add PrettyDuration, job timeout, job max schedule drift and job retry…
Browse files Browse the repository at this point in the history
… limit
  • Loading branch information
Arshia001 committed Jan 13, 2025
1 parent d1c6dff commit 1f6ced5
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 13 deletions.
62 changes: 53 additions & 9 deletions docs/schema/generated/jsonschema/types/AppConfigV1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": [
Expand All @@ -306,6 +321,24 @@
}
]
},
"retries": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"timeout": {
"anyOf": [
{
"$ref": "#/definitions/PrettyDuration"
},
{
"type": "null"
}
]
},
"volumes": {
"type": [
"array",
Expand Down Expand Up @@ -397,9 +430,13 @@
},
"timeout": {
"description": "Request timeout.\n\nFormat: 1s, 5m, 11h, ...",
"type": [
"string",
"null"
"anyOf": [
{
"$ref": "#/definitions/PrettyDuration"
},
{
"type": "null"
}
]
},
"unhealthy_threshold": {
Expand Down Expand Up @@ -492,9 +529,13 @@
},
"timeout": {
"description": "Request timeout.\n\nFormat: 1s, 5m, 11h, ...",
"type": [
"string",
"null"
"anyOf": [
{
"$ref": "#/definitions/PrettyDuration"
},
{
"type": "null"
}
]
}
}
Expand Down Expand Up @@ -593,6 +634,9 @@
"PackageSource": {
"type": "string"
},
"PrettyDuration": {
"type": "string"
},
"Redirect": {
"description": "App redirect configuration.",
"type": "object",
Expand Down
4 changes: 3 additions & 1 deletion lib/config/src/app/http.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use super::pretty_duration::PrettyDuration;

/// Defines an HTTP request.
#[derive(
schemars::JsonSchema, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone, Debug,
Expand All @@ -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>,
Expand Down
21 changes: 19 additions & 2 deletions lib/config/src/app/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
}),
};

Expand All @@ -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(),
Expand Down
4 changes: 3 additions & 1 deletion lib/config/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
mod healthcheck;
mod http;
mod job;
mod pretty_duration;

pub use self::{healthcheck::*, http::*, job::*};

use std::collections::HashMap;

use anyhow::{bail, Context};
use bytesize::ByteSize;
use pretty_duration::PrettyDuration;

use crate::package::PackageSource;

Expand Down Expand Up @@ -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.
Expand Down
195 changes: 195 additions & 0 deletions lib/config/src/app/pretty_duration.rs
Original file line number Diff line number Diff line change
@@ -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();
}
}

0 comments on commit 1f6ced5

Please sign in to comment.