Pass three with right-half intervals
This commit is contained in:
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user