Pass three with right-half intervals

This commit is contained in:
Kinesin Data Technologies Incorporated
2022-09-28 21:06:18 -03:00
commit 811057ecad
9 changed files with 1510 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
use super::*;
use std::collections::HashSet;
pub fn default_dow_set() -> HashSet<Weekday> {
use Weekday::*;
HashSet::from([Mon, Tue, Wed, Thu, Fri])
}
// TODO
// - Make sure include and exclude are disjoint
/// Maintains a list of days that are considered active
#[derive(Clone, Serialize, Deserialize, Default, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Calendar {
/// Day of Week Mask
#[serde(default = "default_dow_set")]
pub mask: HashSet<Weekday>,
/// Dates to explicitly include
#[serde(default)]
pub exclude: HashSet<NaiveDate>,
/// Dates to explicitly include
#[serde(default)]
pub include: HashSet<NaiveDate>,
}
impl Calendar {
pub fn new() -> Self {
Calendar {
mask: default_dow_set(),
..Calendar::default()
}
}
pub fn includes(&self, date: NaiveDate) -> bool {
if self.exclude.contains(&date) {
false
} else if self.include.contains(&date) {
true
} else {
self.mask.contains(&date.weekday())
}
}
pub fn next(&self, date: NaiveDate) -> NaiveDate {
self.offset(date, 1)
}
pub fn prev(&self, date: NaiveDate) -> NaiveDate {
self.offset(date, -1)
}
pub fn offset(&self, mut date: NaiveDate, mut offset: i64) -> NaiveDate {
let incr = if offset < 0 { 1 } else { -1 };
while offset != 0 {
date = date + Duration::days(-1 * incr);
while !self.includes(date) {
date = date + Duration::days(-1 * incr);
}
offset += incr;
}
date
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_next() {
let cal = Calendar::new();
assert_eq!(
cal.next(NaiveDate::from_ymd(2022, 1, 1)),
NaiveDate::from_ymd(2022, 1, 3)
);
}
}
+172
View File
@@ -0,0 +1,172 @@
use super::*;
use std::ops::{Add, BitAnd, BitOr, Sub};
/*
These intervals are all half-open on the left, so:
(start, end)
This makes the end included in the interval for which it's
in charge of
*/
#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Ord, PartialOrd)]
pub struct Interval {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
}
impl Interval {
pub fn new<T: TimeZone>(start: DateTime<T>, end: DateTime<T>) -> Self {
let start = start.with_timezone(&Utc);
let end = end.with_timezone(&Utc);
if start > end {
Interval { end, start }
} else {
Interval { start, end }
}
}
pub fn is_empty(&self) -> bool {
return self.start == self.end;
}
pub fn len(&self) -> Duration {
self.end - self.start
}
pub fn contains<T: TimeZone>(&self, dt: DateTime<T>) -> bool {
return self.start < dt && dt <= self.end;
}
/// True if `other` is a subset of this interval
pub fn has_subset(&self, other: Interval) -> bool {
return self.start <= other.start && other.end <= self.end;
}
/// True if `other` overlaps or is immediately adjascent to self
pub fn is_contiguous(&self, other: Interval) -> bool {
return (self.start <= other.start && other.start <= self.end)
|| (other.start <= self.start && self.start <= other.end);
}
/// True if self intersection other is an empty set
pub fn is_disjoint(&self, other: Interval) -> bool {
return self.end <= other.start || other.end <= self.start;
}
pub fn intersection(&self, other: Interval) -> Interval {
if self.is_disjoint(other) {
Interval::new(self.start, self.start)
} else {
Interval {
start: std::cmp::max(self.start, other.start),
end: std::cmp::min(self.end, other.end),
}
}
}
}
impl BitAnd for Interval {
type Output = Interval;
fn bitand(self, other: Interval) -> Self::Output {
self.intersection(other)
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! dt {
( $x:literal ) => {
Utc.ymd(2022, 1, 1).and_hms($x, 0, 0)
};
}
macro_rules! intv {
( $x:literal, $y:literal ) => {
Interval::new(
Utc.ymd(2022, 1, 1).and_hms($x, 0, 0),
Utc.ymd(2022, 1, 1).and_hms($y, 0, 0),
)
};
}
/*
Intervals
*/
#[test]
fn test_interval_contains() {
let intv = intv!(2, 5);
// Ensure the interval is half-open on the right
assert!(!intv.contains(dt!(0)));
assert!(!intv.contains(dt!(1)));
assert!(!intv.contains(dt!(2)));
assert!(intv.contains(dt!(3)));
assert!(intv.contains(dt!(4)));
assert!(intv.contains(dt!(5)));
assert!(!intv.contains(dt!(6)));
assert!(!intv.contains(dt!(7)));
}
#[test]
fn test_interval_ordering() {
assert!(intv!(1, 2) < intv!(2, 3));
assert!(intv!(1, 3) < intv!(2, 4));
assert!(intv!(1, 3) < intv!(4, 5));
assert!(intv!(1, 3) < intv!(4, 6));
}
#[test]
fn test_is_disjoint() {
let int = intv!(2, 5);
assert!(int.is_disjoint(intv!(1, 2)));
assert!(!int.is_disjoint(intv!(1, 3)));
assert!(int.is_disjoint(intv!(5, 6)));
}
#[test]
fn test_is_contiguous() {
let int = intv!(3, 4);
assert!(!int.is_contiguous(intv!(1, 2)));
assert!(int.is_contiguous(intv!(2, 3)));
assert!(int.is_contiguous(intv!(1, 3)));
assert!(int.is_contiguous(intv!(4, 6)));
assert!(int.is_contiguous(intv!(1, 6)));
assert!(!int.is_contiguous(intv!(5, 6)));
}
#[test]
fn test_has_subset() {
let int = intv!(2, 5);
// Contains itself
assert!(int.has_subset(int));
assert!(int.has_subset(intv!(3, 4))); // Contains inner interval
assert!(!int.has_subset(intv!(1, 2))); // Left contiguous
assert!(!int.has_subset(intv!(1, 3))); // Left overlap
assert!(!int.has_subset(intv!(4, 6))); // Right overlap
assert!(!int.has_subset(intv!(5, 6))); // Right contiguous
assert!(!int.has_subset(intv!(1, 6))); // Outer scope
}
#[test]
fn test_intersection() {
let int = intv!(2, 5);
assert_eq!(int.intersection(int), int); // Union with itself
assert_eq!(int.intersection(intv!(1, 6)), int); // Union with itself
assert!(int.intersection(intv!(1, 2)).is_empty()); // Left
assert_eq!(int.intersection(intv!(1, 3)), intv!(2, 3)); // Left Overlap
assert_eq!(int.intersection(intv!(2, 3)), intv!(2, 3)); // Inner left
assert_eq!(int.intersection(intv!(3, 4)), intv!(3, 4)); // Inner
assert_eq!(int.intersection(intv!(4, 5)), intv!(4, 5)); // Right Inner
assert_eq!(int.intersection(intv!(4, 6)), intv!(4, 5)); // Inner
assert!(int.intersection(intv!(5, 6)).is_empty()); // Right
}
}
+247
View File
@@ -0,0 +1,247 @@
use super::*;
use std::convert::From;
use std::ops::{Add, BitAnd, BitOr, Deref, DerefMut, Not, Sub};
/// A coalescing set of intervals
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd)]
pub struct IntervalSet(Vec<Interval>);
impl IntervalSet {
pub fn new() -> Self {
IntervalSet(Vec::new())
}
pub fn start(&self) -> Option<DateTime<Utc>> {
if let Some(intv) = self.first() {
Some(intv.start)
} else {
None
}
}
pub fn end(&self) -> Option<DateTime<Utc>> {
if let Some(intv) = self.last() {
Some(intv.end)
} else {
None
}
}
/// Returns true if interval is a subset
pub fn has_subset(&self, interval: Interval) -> bool {
self.0.iter().any(|x| x.has_subset(interval))
}
pub fn contains<T: TimeZone>(&self, dt: DateTime<T>) -> bool {
self.0.iter().any(|x| x.contains(dt.with_timezone(&Utc)))
}
// Naive O(n^2) implementation
pub fn is_disjoint(&self, other: &IntervalSet) -> bool {
self.0
.iter()
.all(|x| other.iter().all(|y| x.is_disjoint(*y)))
}
pub fn intersection(&self, other: &IntervalSet) -> Self {
let mut res = IntervalSet(self.0.iter().fold(Vec::<Interval>::new(), |mut acc, x| {
let new_intervals: Vec<Interval> = other
.iter()
.map(|y| x.intersection(*y))
.filter(|x| !x.is_empty())
.collect();
acc.extend(new_intervals);
acc
}));
res.coalesce();
res
}
pub fn complement(&self) -> Self {
if self.is_empty() {
IntervalSet(vec![Interval::new(
DateTime::<Utc>::MIN_UTC,
DateTime::<Utc>::MAX_UTC,
)])
} else {
// Need to build the start of the range
let mut acc = Vec::new();
let mut last_end = DateTime::<Utc>::MIN_UTC;
for intv in &self.0 {
if intv.start == DateTime::<Utc>::MIN_UTC {
last_end = intv.end;
} else {
acc.push(Interval::new(last_end, intv.start));
last_end = intv.end;
}
}
if last_end != DateTime::<Utc>::MAX_UTC {
acc.push(Interval::new(last_end, DateTime::<Utc>::MAX_UTC));
}
IntervalSet(acc)
}
}
pub fn insert(&mut self, interval: Interval) {
let should_coalesce = self.0.iter().any(|intv| intv.is_contiguous(interval));
self.0.push(interval);
if should_coalesce {
self.coalesce();
}
}
pub fn merge(&mut self, other: &IntervalSet) {
self.0.extend(other.0.clone());
self.coalesce();
}
pub fn coalesce(&mut self) {
self.0.sort_unstable();
self.0 = self
.0
.iter()
.filter(|x| !x.is_empty())
.fold(Vec::new(), |mut acc, int| {
if let Some(lst) = acc.last_mut() {
if !lst.is_contiguous(*int) {
acc.push(*int)
} else {
lst.end = int.end
}
} else {
acc.push(*int);
}
acc
});
}
pub fn union(&self, other: &IntervalSet) -> Self {
let mut is = IntervalSet(self.0.iter().chain(other.0.iter()).copied().collect());
is.coalesce();
is
}
/// Subtract all intervals in `other` from self
/// both sides must be sorted
pub fn difference(&self, other: &Self) -> Self {
self.intersection(&other.complement())
}
}
impl Deref for IntervalSet {
type Target = Vec<Interval>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for IntervalSet {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<Interval> for IntervalSet {
fn from(interval: Interval) -> Self {
IntervalSet(vec![interval])
}
}
impl From<Vec<Interval>> for IntervalSet {
fn from(intervals: Vec<Interval>) -> Self {
let mut is = IntervalSet(intervals);
is.coalesce();
is
}
}
impl From<&[Interval]> for IntervalSet {
fn from(intervals: &[Interval]) -> Self {
let mut is = IntervalSet(intervals.to_vec());
is.coalesce();
is
}
}
impl Not for &IntervalSet {
type Output = IntervalSet;
fn not(self) -> Self::Output {
self.complement()
}
}
impl Add for &IntervalSet {
type Output = IntervalSet;
fn add(self, other: &IntervalSet) -> Self::Output {
self.union(other)
}
}
impl Sub for &IntervalSet {
type Output = IntervalSet;
fn sub(self, other: &IntervalSet) -> Self::Output {
self.difference(other)
}
}
impl BitOr for &IntervalSet {
type Output = IntervalSet;
fn bitor(self, other: &IntervalSet) -> Self::Output {
self.union(other)
}
}
impl BitAnd for &IntervalSet {
type Output = IntervalSet;
fn bitand(self, other: &IntervalSet) -> Self::Output {
self.intersection(other)
}
}
impl Not for IntervalSet {
type Output = IntervalSet;
fn not(self) -> Self::Output {
self.complement()
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! intv {
( $x:literal, $y:literal ) => {
Interval::new(
Utc.ymd(2022, 1, 1).and_hms($x, 0, 0),
Utc.ymd(2022, 1, 1).and_hms($y, 0, 0),
)
};
}
/*
Interval Set
*/
#[test]
fn test_intervalset_difference() {
let isa = IntervalSet(vec![intv!(1, 3), intv!(5, 6)]);
// Removing the entire span
let full = IntervalSet(vec![intv!(1, 6)]);
assert_eq!(isa.difference(&full), IntervalSet(vec![]));
assert_eq!(
isa.difference(&IntervalSet(vec![intv!(2, 5)])),
IntervalSet(vec![intv!(1, 2), intv!(5, 6)])
);
// TODO need more tests here
}
#[test]
fn test_intervalset_complement() {
// Complement's complement is the same
let is = IntervalSet(vec![intv!(2, 5), intv!(8, 20)]);
assert_eq!(is.complement().complement(), is);
// Complement with one end at min time
let is = IntervalSet(vec![
Interval::new(
DateTime::<Utc>::MIN_UTC,
Utc.ymd(2021, 12, 1).and_hms(0, 0, 0),
),
intv!(8, 20),
]);
assert_eq!(is.complement().complement(), is);
}
}
+27
View File
@@ -0,0 +1,27 @@
#![allow(unused_imports)]
#![allow(dead_code)]
use anyhow::{anyhow, Result};
use chrono::prelude::*;
use chrono::{Duration, TimeZone};
use chrono_tz::Tz;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use tokio::sync::{mpsc, oneshot};
use crate::calendar::*;
use crate::interval::*;
use crate::interval_set::*;
use crate::requirement::*;
use crate::schedule::*;
use crate::task::*;
pub type Resource = String;
pub type TaskDetails = serde_json::Value;
pub mod calendar;
pub mod interval;
pub mod interval_set;
pub mod requirement;
pub mod schedule;
pub mod task;
+179
View File
@@ -0,0 +1,179 @@
use super::*;
use std::path::Path;
pub trait Satisfiable {
/// Returns true if the requirement is satisfied now
fn is_satisfied(
&self,
time: &DateTime<Tz>,
schedule: &Schedule,
available: &HashMap<String, IntervalSet>,
) -> bool;
/// Returns true if the requirement could be satisfied at some point
/// in time
fn can_be_satisfied(
&self,
time: &DateTime<Tz>,
schedule: &Schedule,
available: &HashMap<String, IntervalSet>,
) -> bool;
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum AggregateRequirement {
All(Vec<Box<Requirement>>),
Any(Vec<Box<Requirement>>),
None(Vec<Box<Requirement>>),
}
impl Satisfiable for AggregateRequirement {
fn is_satisfied(
&self,
time: &DateTime<Tz>,
schedule: &Schedule,
available: &HashMap<Resource, IntervalSet>,
) -> bool {
match self {
AggregateRequirement::All(reqs) => reqs
.iter()
.all(|x| x.is_satisfied(time, schedule, available)),
AggregateRequirement::Any(reqs) => reqs
.iter()
.any(|x| x.is_satisfied(time, schedule, available)),
AggregateRequirement::None(reqs) => !reqs
.iter()
.any(|x| x.is_satisfied(time, schedule, available)),
}
}
fn can_be_satisfied(
&self,
time: &DateTime<Tz>,
schedule: &Schedule,
available: &HashMap<Resource, IntervalSet>,
) -> bool {
match self {
AggregateRequirement::All(reqs) => reqs
.iter()
.all(|x| x.can_be_satisfied(time, schedule, available)),
AggregateRequirement::Any(reqs) => reqs
.iter()
.any(|x| x.can_be_satisfied(time, schedule, available)),
AggregateRequirement::None(reqs) => !reqs
.iter()
.any(|x| x.can_be_satisfied(time, schedule, available)),
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "snake_case", untagged)]
pub enum SingleRequirement {
Offset { resource: String, offset: i32 },
File { path: String },
}
impl Satisfiable for SingleRequirement {
fn is_satisfied(
&self,
time: &DateTime<Tz>,
schedule: &Schedule,
available: &HashMap<Resource, IntervalSet>,
) -> bool {
match self {
//SingleRequirement::ResourceInterval { .. } => true,
SingleRequirement::Offset { resource, offset } => {
let intv = schedule.interval(*time, *offset);
match available.get(resource) {
Some(is) => is.has_subset(intv),
None => false,
}
}
SingleRequirement::File { path } => Path::new(path).exists(),
}
}
fn can_be_satisfied(
&self,
time: &DateTime<Tz>,
schedule: &Schedule,
available: &HashMap<Resource, IntervalSet>,
) -> bool {
match self {
SingleRequirement::Offset { resource, offset } => {
let intv = schedule.interval(*time, *offset);
match available.get(resource) {
Some(is) => is.has_subset(intv),
None => false,
}
}
SingleRequirement::File { .. } => true,
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
#[serde(untagged)]
pub enum Requirement {
One(SingleRequirement),
Group(AggregateRequirement),
}
impl Satisfiable for Requirement {
fn is_satisfied(
&self,
time: &DateTime<Tz>,
schedule: &Schedule,
available: &HashMap<Resource, IntervalSet>,
) -> bool {
match self {
Requirement::One(req) => req.is_satisfied(time, schedule, available),
Requirement::Group(req) => req.is_satisfied(time, schedule, available),
}
}
fn can_be_satisfied(
&self,
time: &DateTime<Tz>,
schedule: &Schedule,
available: &HashMap<Resource, IntervalSet>,
) -> bool {
match self {
Requirement::One(req) => req.can_be_satisfied(time, schedule, available),
Requirement::Group(req) => req.can_be_satisfied(time, schedule, available),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_complex_parse() {
let json = r#"{
"any": [
{ "all": [
{ "resource": "resource_a", "offset": -1 },
{ "resource": "resource_b", "offset": -1 }
]
},
{ "type": "file", "path": "/mnt/test/data_${yyyy}{$mm}{$dd}" }
]
}"#;
let res: serde_json::Result<Requirement> = serde_json::from_str(json);
assert!(res.is_ok());
}
#[test]
fn check_simple_parse() {
let json = r#"{ "type": "file", "path": "/mnt/test/data_${yyyy}{$mm}{$dd}" }"#;
let res: serde_json::Result<Requirement> = serde_json::from_str(json);
println!("{:?}", res);
assert!(res.is_ok());
}
// TODO Add tests for satisfies
}
+371
View File
@@ -0,0 +1,371 @@
use super::*;
use std::collections::HashSet;
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Schedule {
calendar: Calendar,
times: Vec<NaiveTime>,
timezone: Tz,
}
impl Schedule {
pub fn new(calendar: Calendar, times: Vec<NaiveTime>, timezone: Tz) -> Self {
let uniq: HashSet<NaiveTime> = HashSet::from_iter(times.iter().cloned());
let mut times = Vec::from_iter(uniq.iter().cloned());
times.sort();
Schedule {
calendar,
times,
timezone,
}
}
pub fn generate(&self, start: DateTime<Utc>, end: DateTime<Utc>) -> Vec<DateTime<Utc>> {
let st = start.with_timezone(&self.timezone);
let et = end.with_timezone(&self.timezone);
let mut date = st.date().naive_local();
let end_date = et.date().succ().naive_local();
let mut times = Vec::new();
while date < end_date {
if self.calendar.includes(date) {
for time in &self.times {
let dt = self
.timezone
.from_local_datetime(&date.and_time(*time))
.unwrap();
if dt > start && dt <= end {
times.push(dt.with_timezone(&Utc));
} else if end < dt {
break;
}
}
}
date = date.succ();
}
times
}
pub fn interval_utc(&self, dt: DateTime<Utc>, offset: i32) -> Interval {
// Need to get the current interval, then offset it
let at = dt.with_timezone(&self.timezone);
let rt = if self.times.iter().any(|x| *x == at.time()) {
at
} else {
self.prev_time(at)
};
let start = self.offset(rt, offset);
Interval::new(
start.with_timezone(&Utc),
self.next_time(start).with_timezone(&Utc),
)
}
pub fn interval(&self, dt: DateTime<Tz>, offset: i32) -> Interval {
// Need to get the current interval, then offset it
let at = dt.with_timezone(&self.timezone);
let rt = if self.times.iter().any(|x| *x == at.time()) {
at
} else {
self.prev_time(at)
};
let start = self.offset(rt, offset);
Interval::new(
start.with_timezone(&Utc),
self.next_time(start).with_timezone(&Utc),
)
}
pub fn next_time(&self, dt: DateTime<Tz>) -> DateTime<Tz> {
let st = dt.with_timezone(&self.timezone);
let mut date = st.date().naive_local();
let mut time = st.time();
// Handle case where we're not on a valid date
if !self.calendar.includes(date) {
date = self.calendar.next(date);
time = self.times[0] - Duration::milliseconds(1);
}
// Figure out the time slot
let time = match self.times.iter().find(|x| **x > time) {
Some(t) => date.and_time(*t),
None => self
.calendar
.next(date)
.and_time(*self.times.first().unwrap()),
};
// Cast into a timezone
self.timezone.from_local_datetime(&time).unwrap()
}
/// Given a time, generate the preceding interval according to the schedule
pub fn prev_time(&self, dt: DateTime<Tz>) -> DateTime<Tz> {
let st = dt.with_timezone(&self.timezone);
let mut date = st.date().naive_local();
let mut time = st.time();
// Handle case where we're not on a valid date
if !self.calendar.includes(date) {
date = self.calendar.prev(date);
time = *self.times.last().unwrap() + Duration::milliseconds(1);
}
// Figure out the time slot
let time = match self.times.iter().rev().find(|x| **x < time) {
Some(t) => date.and_time(*t),
None => self
.calendar
.prev(date)
.and_time(*self.times.last().unwrap()),
};
// Cast into a timezone
self.timezone.from_local_datetime(&time).unwrap()
}
/// Given a timestamp, return the scheduled time `offset`
pub fn offset(&self, mut dt: DateTime<Tz>, offset: i32) -> DateTime<Tz> {
if offset > 0 {
for _ in 0..offset {
dt = self.next_time(dt);
}
} else {
for _ in offset..0 {
dt = self.prev_time(dt);
}
}
dt
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_simple_generation() {
let timezone = chrono_tz::America::Halifax;
let sched = Schedule {
calendar: Calendar::new(),
times: vec![
NaiveTime::from_hms(10, 30, 0),
NaiveTime::from_hms(11, 30, 0),
],
timezone,
};
// Simple generation
let times = sched.generate(
timezone
.ymd(2022, 1, 3)
.and_hms(11, 0, 0)
.with_timezone(&Utc),
timezone
.ymd(2022, 1, 3)
.and_hms(12, 0, 0)
.with_timezone(&Utc),
);
assert_eq!(times.len(), 1);
assert_eq!(
times,
vec![timezone
.ymd(2022, 1, 3)
.and_hms(11, 30, 0)
.with_timezone(&Utc),]
);
// Generating scheduled times over a timerange
assert_eq!(
sched.generate(
timezone
.ymd(2021, 12, 31)
.and_hms(0, 0, 0)
.with_timezone(&Utc),
timezone
.ymd(2022, 1, 5)
.and_hms(0, 0, 0)
.with_timezone(&Utc),
),
vec![
timezone
.ymd(2021, 12, 31)
.and_hms(10, 30, 0)
.with_timezone(&Utc),
timezone
.ymd(2021, 12, 31)
.and_hms(11, 30, 0)
.with_timezone(&Utc),
timezone
.ymd(2022, 1, 3)
.and_hms(10, 30, 0)
.with_timezone(&Utc),
timezone
.ymd(2022, 1, 3)
.and_hms(11, 30, 0)
.with_timezone(&Utc),
timezone
.ymd(2022, 1, 4)
.and_hms(10, 30, 0)
.with_timezone(&Utc),
timezone
.ymd(2022, 1, 4)
.and_hms(11, 30, 0)
.with_timezone(&Utc),
]
);
}
#[test]
fn check_prev() {
let timezone = chrono_tz::America::Halifax;
let sched = Schedule {
calendar: Calendar::new(),
times: vec![
NaiveTime::from_hms(10, 30, 0),
NaiveTime::from_hms(11, 30, 0),
],
timezone,
};
assert_eq!(
sched.prev_time(timezone.ymd(2022, 1, 3).and_hms(11, 0, 0)),
timezone.ymd(2022, 1, 3).and_hms(10, 30, 0)
);
assert_eq!(
sched.prev_time(timezone.ymd(2022, 1, 3).and_hms(11, 30, 0)),
timezone.ymd(2022, 1, 3).and_hms(10, 30, 0)
);
}
#[test]
fn check_offset() {
let timezone = chrono_tz::America::Halifax;
let sched = Schedule {
calendar: Calendar::new(),
times: vec![
NaiveTime::from_hms(10, 30, 0),
NaiveTime::from_hms(11, 30, 0),
],
timezone,
};
// Asking for no offset should yield the same time
assert_eq!(
sched.offset(timezone.ymd(2022, 1, 3).and_hms(11, 0, 0), 0),
timezone.ymd(2022, 1, 3).and_hms(11, 0, 0)
);
// -1 is equivalent to prev
let test_time = timezone.ymd(2022, 1, 3).and_hms(11, 0, 0);
assert_eq!(sched.offset(test_time, -1), sched.prev_time(test_time));
assert_eq!(sched.offset(test_time, 1), sched.next_time(test_time));
}
#[test]
fn check_next() {
let timezone = chrono_tz::America::Halifax;
let sched = Schedule {
calendar: Calendar::new(),
times: vec![
NaiveTime::from_hms(10, 30, 0),
NaiveTime::from_hms(11, 30, 0),
],
timezone,
};
assert_eq!(
sched.next_time(timezone.ymd(2022, 1, 3).and_hms(11, 0, 0)),
timezone.ymd(2022, 1, 3).and_hms(11, 30, 0)
);
assert_eq!(
sched.next_time(timezone.ymd(2022, 1, 3).and_hms(11, 30, 0)),
timezone.ymd(2022, 1, 4).and_hms(10, 30, 0)
);
}
#[test]
fn check_transivity() {
let timezone = chrono_tz::America::Halifax;
let sched = Schedule {
calendar: Calendar::new(),
times: vec![
NaiveTime::from_hms(10, 30, 0),
NaiveTime::from_hms(11, 30, 0),
],
timezone,
};
// prev and next are reversible
let dt = sched.prev_time(timezone.ymd(2022, 1, 3).and_hms(11, 0, 0)); // 10:30 -> 11:30
assert_eq!(dt, sched.prev_time(sched.next_time(dt)));
}
#[test]
fn check_interval() {
let timezone = chrono_tz::America::Halifax;
let sched = Schedule {
calendar: Calendar::new(),
times: vec![
NaiveTime::from_hms(10, 30, 0),
NaiveTime::from_hms(11, 30, 0),
],
timezone,
};
// prev and next are reversible
let dt = timezone.ymd(2022, 1, 3).and_hms(11, 0, 0);
assert_eq!(
sched.interval(dt, 0),
Interval::new(
timezone
.ymd(2022, 1, 3)
.and_hms(10, 30, 0)
.with_timezone(&Utc),
timezone
.ymd(2022, 1, 3)
.and_hms(11, 30, 0)
.with_timezone(&Utc)
)
);
// Previous
assert_eq!(
sched.interval(dt, -1),
Interval::new(
timezone
.ymd(2021, 12, 31)
.and_hms(11, 30, 0)
.with_timezone(&Utc),
timezone
.ymd(2022, 1, 3)
.and_hms(10, 30, 0)
.with_timezone(&Utc)
)
);
// Next
assert_eq!(
sched.interval(dt, 1),
Interval::new(
timezone
.ymd(2022, 1, 3)
.and_hms(11, 30, 0)
.with_timezone(&Utc),
timezone
.ymd(2022, 1, 4)
.and_hms(10, 30, 0)
.with_timezone(&Utc)
)
);
}
}
+413
View File
@@ -0,0 +1,413 @@
use super::*;
fn default_bytes() -> usize {
20480
}
/// Options in how to handle task output. Some tasks can be quite
/// verbose, and the output may not be needed.
#[derive(Clone, Serialize, Deserialize, Copy, Debug, PartialEq, Hash, Eq)]
#[serde(deny_unknown_fields)]
pub struct TaskOutputOptions {
/// If true, output from successful tasks is discarded entirely, in
/// keeping with the UNIX philosophy of no news is good news
#[serde(default)]
pub discard_successful: bool,
/// If true, and output is not discarded, truncate the output of
/// each task to a maximum of the first / last `preserve` kb of
/// data
#[serde(default)]
pub truncate: bool,
/// Number of KB of output to preserve at the beginning of the ouptut
#[serde(default = "default_bytes")]
pub head_bytes: usize,
/// Number of KB of output to preserve at the end of the outut
#[serde(default = "default_bytes")]
pub tail_bytes: usize,
}
impl Default for TaskOutputOptions {
fn default() -> Self {
TaskOutputOptions {
discard_successful: true,
truncate: true,
head_bytes: default_bytes(),
tail_bytes: default_bytes(),
}
}
}
/// Defines the struct to parse for tasks
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
pub struct TaskDefinition {
/// Command to run to generate the resources for the given interval
pub up: TaskDetails,
/// Command to run to remove the resource for the given interval
/// If None, no additional action will happen when an interval goes stale
#[serde(default)]
pub down: Option<TaskDetails>,
/// Command to run to verify the resources exist and are correct.
/// Run before `up` to see if needed, and after `up` to verify output
/// If None, no check is run to see if up needs to run, and no post-up check occurs
/// to verify up succeeded
#[serde(default)]
pub check: Option<TaskDetails>,
/// Number of seconds
#[serde(default)]
pub alert_delay_seconds: Option<i64>,
#[serde(default)]
pub provides: HashSet<String>,
#[serde(default)]
pub requires: Vec<Requirement>,
pub calendar_name: String,
pub times: Vec<NaiveTime>,
pub timezone: Tz,
pub valid_from: NaiveDateTime,
#[serde(default)]
pub valid_to: Option<NaiveDateTime>,
}
impl TaskDefinition {
pub fn to_task(&self, calendar: &Calendar) -> Task {
let schedule = Schedule::new(calendar.clone(), self.times.clone(), self.timezone);
/*
The valid_{from,to} interval must be aligned to the actual schedule
*/
let start = schedule
.interval(
self.timezone.from_local_datetime(&self.valid_from).unwrap(),
0,
)
.start;
let end = match self.valid_to {
Some(nt) => self.timezone.from_local_datetime(&nt).unwrap(),
None => DateTime::<Utc>::MAX_UTC.with_timezone(&self.timezone),
};
let actual_end = schedule.interval(end, 0).start;
Task {
up: self.up.clone(),
down: self.down.clone(),
check: self.check.clone(),
provides: self.provides.clone(),
requires: self.requires.clone(),
schedule: schedule,
valid_over: IntervalSet::from(vec![Interval::new(start, actual_end)]),
timezone: self.timezone,
}
}
}
/*
No need for serialize / deserialize here, since we don't
need to transmit it anywhere. It is reconstituted by the
definition
*/
#[derive(Clone, Serialize, Debug)]
pub struct Task {
pub up: TaskDetails,
pub down: Option<TaskDetails>,
pub check: Option<TaskDetails>,
pub provides: HashSet<Resource>,
pub requires: Vec<Requirement>,
pub schedule: Schedule,
pub valid_over: IntervalSet,
pub timezone: Tz,
}
// Really need to rethink this valid_over and scheduling times. When generating
impl Task {
pub fn generate_times(
&self,
required: &HashMap<Resource, IntervalSet>,
) -> Result<Vec<DateTime<Utc>>> {
// Ensure that all intervals that are required are provided by this instance
let reqs: Vec<IntervalSet> = self
.provides
.iter()
.map(|res| {
if let Some(is) = required.get(res) {
is.intersection(&self.valid_over)
} else {
IntervalSet::new()
}
})
.collect();
if reqs.is_empty() {
Ok(Vec::new())
} else {
let ris = &reqs[0];
// Ensure that all intervals are the same
if !reqs[1..].iter().all(|is| is == ris) {
Err(anyhow!(
"Task produces multiple resources, but intervals are not consistent across needs"
))
} else {
Ok(ris.iter().fold(Vec::new(), |mut acc, intv| {
acc.extend(self.schedule.generate(
std::cmp::max(intv.start, self.valid_over.start().unwrap()),
std::cmp::min(intv.end, self.valid_over.end().unwrap()),
));
acc
}))
}
}
}
pub fn validity(&self, max_time: DateTime<Utc>) -> IntervalSet {
if self.valid_over.is_empty() {
IntervalSet::new()
} else {
let timeline =
IntervalSet::from(vec![Interval::new(self.valid_over[0].start, max_time)]);
self.valid_over.intersection(&timeline)
}
}
/// Returns true if this task can provide any resource that isn't currently available
/// as of the specified time
pub fn is_needed(&self, time: &DateTime<Tz>, available: &HashMap<String, IntervalSet>) -> bool {
let end_dt = time.with_timezone(&Utc);
let horizon_is = self
.valid_over
.difference(&IntervalSet::from(vec![Interval::new(
end_dt,
DateTime::<Utc>::MAX_UTC,
)]));
self.provides.iter().all(|res| {
if let Some(is) = available.get(res) {
!(&horizon_is - is).is_empty()
} else {
false
}
})
}
/// Returns true if all requirements are satisfied
pub fn can_run(&self, time: DateTime<Utc>, available: &HashMap<String, IntervalSet>) -> bool {
let local_time = time.with_timezone(&self.timezone);
self.requires
.iter()
.all(|req| req.is_satisfied(&local_time, &self.schedule, available))
}
pub fn can_be_satisfied(
&self,
time: DateTime<Utc>,
available: &HashMap<String, IntervalSet>,
) -> bool {
let local_time = time.with_timezone(&self.timezone);
self.requires
.iter()
.all(|req| req.can_be_satisfied(&local_time, &self.schedule, available))
}
pub fn up(&self, interval: &Interval) -> Result<HashSet<String>> {
if self.check(interval) {
Ok(self.provides.clone())
} else {
Ok(HashSet::new())
}
}
pub fn check(&self, _interval: &Interval) -> bool {
true
}
pub fn down(&self, _interval: &Interval) -> Result<HashSet<String>> {
Ok(HashSet::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono_tz::America::Halifax;
macro_rules! isv {
( $x:literal, $y:literal ) => {
IntervalSet::from(vec![Interval::new(
Utc.ymd(2022, 1, $x).and_hms(0, 0, 0),
Utc.ymd(2022, 1, $y).and_hms(0, 0, 0),
)])
};
}
#[test]
fn check_task_can_parse() {
// Spans a weekend
let task_json = r#"
{
"up": "/usr/bin/touch /tmp/a_${yyyymmdd}_${hhmmss}",
"down": "/usr/bin/rm /tmp/a_${yyyymmdd}_${hhmmss}",
"check": "/usr/bin/test -e /tmp/a_${yyyymmdd}_${hhmmss}",
"provides": [
"resource_a",
"resource_b"
],
"requires": [
{ "resource": "alpha", "offset": 0 },
{ "resource": "beta", "offset": -1 }
],
"calendar_name": "std",
"times": [ "09:00:00", "13:00:00", "15:00:00" ],
"timezone": "America/Halifax",
"valid_from": "2022-01-05T12:30:00",
"valid_to": "2022-01-11T00:00:00"
}
"#;
let task_def: TaskDefinition = serde_json::from_str(task_json).unwrap();
// Produces a std
let cal = Calendar::new();
let task = task_def.to_task(&cal);
// Assert the valid interval is correct
assert_eq!(
task.valid_over,
IntervalSet::from(vec![Interval::new(
Halifax.ymd(2022, 1, 5).and_hms(9, 0, 0),
Halifax.ymd(2022, 1, 10).and_hms(15, 0, 0)
)])
);
// No times when out of validity
let times = task
.generate_times(&HashMap::from([
("resource_a".to_owned(), isv!(13, 20)),
("resource_b".to_owned(), isv!(13, 20)),
]))
.unwrap();
assert!(times.is_empty());
// Requiring within a valid time range generates times
let times = task
.generate_times(&HashMap::from([
("resource_a".to_owned(), isv!(6, 8)),
("resource_b".to_owned(), isv!(6, 8)),
]))
.unwrap();
assert_eq!(times.len(), 6);
// Raise error if unequal requirements
let res = task.generate_times(&HashMap::from([
("resource_a".to_owned(), isv!(6, 7)),
("resource_b".to_owned(), isv!(6, 8)),
]));
assert!(res.is_err());
// Require that all times generated be within the
// valid_over
let res = task.generate_times(&HashMap::from([
("resource_a".to_owned(), isv!(1, 30)),
("resource_b".to_owned(), isv!(1, 30)),
]));
assert!(res.is_ok());
let times = res.unwrap();
assert!(times.iter().all(|time| task.valid_over.contains(*time)));
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TaskAttempt {
#[serde(default)]
pub task_name: String,
#[serde(default = "chrono::Utc::now")]
pub scheduled_time: DateTime<Utc>,
#[serde(default = "chrono::Utc::now")]
pub start_time: DateTime<Utc>,
#[serde(default = "chrono::Utc::now")]
pub stop_time: DateTime<Utc>,
#[serde(default)]
pub succeeded: bool,
#[serde(default)]
pub killed: bool,
#[serde(default)]
pub infra_failure: bool,
#[serde(default)]
pub output: String,
#[serde(default)]
pub error: String,
#[serde(default)]
pub executor: Vec<String>,
#[serde(default)]
pub exit_code: i32,
/// as a percentage
#[serde(default)]
pub max_cpu: f32,
/// as a percentage
#[serde(default)]
pub avg_cpu: f32,
/// In bytes
#[serde(default)]
pub max_rss: u64,
/// In bytes
#[serde(default)]
pub avg_rss: f32,
}
impl Default for TaskAttempt {
fn default() -> Self {
TaskAttempt {
task_name: String::new(),
scheduled_time: Utc::now(),
start_time: Utc::now(),
stop_time: Utc::now(),
succeeded: false,
killed: false,
infra_failure: false,
output: "".to_owned(),
error: "".to_owned(),
executor: Vec::new(),
exit_code: 0i32,
max_cpu: 0.0,
avg_cpu: 0.0,
max_rss: 0,
avg_rss: 0.0,
}
}
}
impl TaskAttempt {
#[must_use]
pub fn new() -> Self {
TaskAttempt::default()
}
}