First pass at web interface is working
This commit is contained in:
parent
4d2a71d028
commit
6b49038db6
@@ -3,7 +3,8 @@
|
|||||||
"HOME": "/tmp/world_test"
|
"HOME": "/tmp/world_test"
|
||||||
},
|
},
|
||||||
"calendars": {
|
"calendars": {
|
||||||
"std": { "mask": [ "Mon", "Tue", "Wed", "Thu", "Fri" ] }
|
"std": { "mask": [ "Mon", "Tue", "Wed", "Thu", "Fri" ] },
|
||||||
|
"weekly": { "mask": [ "Fri" ] }
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"task_a": {
|
"task_a": {
|
||||||
@@ -11,24 +12,39 @@
|
|||||||
"down": { "command": "/bin/rm ${HOME}/task_a_${yyyymmdd}${hhmmss}", "resources": { "cores": 1 } },
|
"down": { "command": "/bin/rm ${HOME}/task_a_${yyyymmdd}${hhmmss}", "resources": { "cores": 1 } },
|
||||||
"check": { "command": "/bin/test -e ${HOME}/task_a_${yyyymmdd}${hhmmss}", "resources": { "cores": 1 } },
|
"check": { "command": "/bin/test -e ${HOME}/task_a_${yyyymmdd}${hhmmss}", "resources": { "cores": 1 } },
|
||||||
|
|
||||||
"provides": [ "task_a" ],
|
"provides": [ "alpha" ],
|
||||||
|
|
||||||
"calendar_name": "std",
|
"calendar_name": "std",
|
||||||
"times": [ "09:00:00", "12:00:00"],
|
"times": [ "09:00:00", "12:00:00"],
|
||||||
"timezone": "America/New_York",
|
"timezone": "America/New_York",
|
||||||
|
|
||||||
"valid_from": "2022-01-01T09:00:00",
|
"valid_from": "2021-01-01T09:00:00",
|
||||||
"valid_to": "2023-01-08T09:00:00"
|
"valid_to": "2022-06-01T09:00:00"
|
||||||
},
|
},
|
||||||
|
"task_a_new": {
|
||||||
|
"up": { "command": "/usr//bin/touch ${HOME}/task_a_${yyyymmdd}${hhmmss}", "resources": { "cores": 1 } },
|
||||||
|
"down": { "command": "/bin/rm ${HOME}/task_a_${yyyymmdd}${hhmmss}", "resources": { "cores": 1 } },
|
||||||
|
"check": { "command": "/bin/test -e ${HOME}/task_a_${yyyymmdd}${hhmmss}", "resources": { "cores": 1 } },
|
||||||
|
|
||||||
|
"provides": [ "alpha" ],
|
||||||
|
|
||||||
|
"calendar_name": "std",
|
||||||
|
"times": [ "09:00:00", "12:00:00"],
|
||||||
|
"timezone": "America/New_York",
|
||||||
|
|
||||||
|
"valid_from": "2022-06-01T09:00:00",
|
||||||
|
"valid_to": "2023-05-01T09:00:00"
|
||||||
|
},
|
||||||
|
|
||||||
"task_b": {
|
"task_b": {
|
||||||
"up": { "command": "/usr//bin/touch ${HOME}/task_b_${yyyymmdd}${hhmmss}", "resources": { "cores": 1 } },
|
"up": { "command": "/usr//bin/touch ${HOME}/task_b_${yyyymmdd}${hhmmss}", "resources": { "cores": 1 } },
|
||||||
"down": { "command": "/bin/rm ${HOME}/task_b_${yyyymmdd}${hhmmss}", "resources": { "cores": 1 } },
|
"down": { "command": "/bin/rm ${HOME}/task_b_${yyyymmdd}${hhmmss}", "resources": { "cores": 1 } },
|
||||||
"check": { "command": "/bin/test -e ${HOME}/task_b_${yyyymmdd}${hhmmss}", "resources": { "cores": 1 } },
|
"check": { "command": "/bin/test -e ${HOME}/task_b_${yyyymmdd}${hhmmss}", "resources": { "cores": 1 } },
|
||||||
|
|
||||||
"provides": [ "task_b" ],
|
"provides": [ "beta" ],
|
||||||
"requires": [ { "resource": "task_a", "offset": 0 } ],
|
"requires": [ { "resource": "alpha", "offset": 0 } ],
|
||||||
|
|
||||||
"calendar_name": "std",
|
"calendar_name": "weekly",
|
||||||
"times": [ "17:00:00" ],
|
"times": [ "17:00:00" ],
|
||||||
"timezone": "America/New_York",
|
"timezone": "America/New_York",
|
||||||
|
|
||||||
|
|||||||
+46
-3
@@ -1,6 +1,7 @@
|
|||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
use actix_web::{error, middleware::Logger, web, App, HttpResponse, HttpServer, Responder};
|
use actix_web::{error, middleware::Logger, web, App, HttpResponse, HttpServer, Responder};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use log::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
@@ -126,22 +127,29 @@ async fn get_state(state: web::Data<AppState>) -> impl Responder {
|
|||||||
]
|
]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
struct TimelineInterval {
|
struct TimelineInterval {
|
||||||
time_range: [DateTime<Utc>; 2],
|
time_range: [DateTime<Utc>; 2],
|
||||||
val: ActionState,
|
val: ActionState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
struct TimelineLabel {
|
struct TimelineLabel {
|
||||||
label: String,
|
label: String,
|
||||||
data: Vec<TimelineInterval>,
|
data: Vec<TimelineInterval>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
struct TimelineGroup {
|
struct TimelineGroup {
|
||||||
group: String,
|
group: String,
|
||||||
data: Vec<TimelineLabel>,
|
data: Vec<TimelineLabel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn timeline(span: web::Query<Interval>, state: web::Data<AppState>) -> impl Responder {
|
async fn get_detailed_timeline(
|
||||||
|
span: web::Json<Interval>,
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
) -> impl Responder {
|
||||||
let interval = span.into_inner();
|
let interval = span.into_inner();
|
||||||
|
|
||||||
let (response, rx) = oneshot::channel();
|
let (response, rx) = oneshot::channel();
|
||||||
@@ -151,7 +159,38 @@ async fn timeline(span: web::Query<Interval>, state: web::Data<AppState>) -> imp
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
match rx.await {
|
match rx.await {
|
||||||
Ok(world) => HttpResponse::Ok().json(world),
|
Ok(actions) => {
|
||||||
|
let mut timeline = Vec::new();
|
||||||
|
info!(
|
||||||
|
"Querying for actions over {}, got {} responses.",
|
||||||
|
interval,
|
||||||
|
actions.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
for (resource, tasks) in actions {
|
||||||
|
let mut group = TimelineGroup {
|
||||||
|
group: resource.clone(),
|
||||||
|
data: Vec::new(),
|
||||||
|
};
|
||||||
|
for (task_name, intervals) in tasks.into_iter() {
|
||||||
|
let data = intervals
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| TimelineInterval {
|
||||||
|
time_range: [a.interval.start, a.interval.end],
|
||||||
|
val: a.state,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
group.data.push(TimelineLabel {
|
||||||
|
label: task_name,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
timeline.push(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse::Ok().json(timeline)
|
||||||
|
}
|
||||||
Err(error) => HttpResponse::BadRequest().json(SimpleError {
|
Err(error) => HttpResponse::BadRequest().json(SimpleError {
|
||||||
error: format!("{:?}", error),
|
error: format!("{:?}", error),
|
||||||
}),
|
}),
|
||||||
@@ -295,7 +334,11 @@ async fn main() -> std::io::Result<()> {
|
|||||||
))
|
))
|
||||||
.app_data(json_config)
|
.app_data(json_config)
|
||||||
.route("/ready", web::get().to(ready))
|
.route("/ready", web::get().to(ready))
|
||||||
.service(web::scope("/api/v1/").route("", web::get().to(get_state)))
|
.service(
|
||||||
|
web::scope("/api/v1")
|
||||||
|
.route("/state", web::get().to(get_state))
|
||||||
|
.route("/details", web::post().to(get_detailed_timeline)),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.bind(config.server.listen_spec())?
|
.bind(config.server.listen_spec())?
|
||||||
.run()
|
.run()
|
||||||
|
|||||||
+20
-4
@@ -24,8 +24,8 @@ pub enum ActionState {
|
|||||||
#[derive(Debug, Clone, Copy, Serialize)]
|
#[derive(Debug, Clone, Copy, Serialize)]
|
||||||
pub struct Action {
|
pub struct Action {
|
||||||
task: usize,
|
task: usize,
|
||||||
interval: Interval,
|
pub interval: Interval,
|
||||||
state: ActionState,
|
pub state: ActionState,
|
||||||
// kill: Option<oneshot::Receiver<()>>,
|
// kill: Option<oneshot::Receiver<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +273,8 @@ impl Runner {
|
|||||||
let res = rx.await.unwrap();
|
let res = rx.await.unwrap();
|
||||||
res
|
res
|
||||||
};
|
};
|
||||||
let target = current.clone();
|
// let target = current.clone();
|
||||||
|
let target = ResourceInterval::new();
|
||||||
|
|
||||||
let end_state = tasks.coverage();
|
let end_state = tasks.coverage();
|
||||||
let mut runner = Runner {
|
let mut runner = Runner {
|
||||||
@@ -306,6 +307,15 @@ impl Runner {
|
|||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.fold(Vec::new(), |mut acc, (idx, task)| {
|
.fold(Vec::new(), |mut acc, (idx, task)| {
|
||||||
|
let get_state = |intv: Interval| {
|
||||||
|
if task.provides.iter().all(|res| {
|
||||||
|
self.current.contains_key(res) && self.current[res].has_subset(intv)
|
||||||
|
}) {
|
||||||
|
ActionState::Completed
|
||||||
|
} else {
|
||||||
|
ActionState::Queued
|
||||||
|
}
|
||||||
|
};
|
||||||
let res: Vec<Action> = task
|
let res: Vec<Action> = task
|
||||||
.generate_intervals(&new_required)
|
.generate_intervals(&new_required)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -314,7 +324,7 @@ impl Runner {
|
|||||||
|interval| Action {
|
|interval| Action {
|
||||||
task: idx,
|
task: idx,
|
||||||
interval,
|
interval,
|
||||||
state: ActionState::Queued,
|
state: get_state(interval),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -393,6 +403,12 @@ impl Runner {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Filtered {} actions down to {}",
|
||||||
|
self.actions.len(),
|
||||||
|
actions.len()
|
||||||
|
);
|
||||||
|
|
||||||
for action in actions {
|
for action in actions {
|
||||||
let task = &self.tasks[action.task];
|
let task = &self.tasks[action.task];
|
||||||
for resource in &task.provides {
|
for resource in &task.provides {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::executors::TaskAttempt;
|
use crate::executors::TaskAttempt;
|
||||||
|
use crate::runner::ActionState;
|
||||||
|
|
||||||
/// Messages for interacting with an Executor
|
/// Messages for interacting with an Executor
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ pub async fn start_redis_storage(
|
|||||||
let payload = serde_json::to_string(&attempt).unwrap();
|
let payload = serde_json::to_string(&attempt).unwrap();
|
||||||
conn.rpush(&tag, &payload).await?;
|
conn.rpush(&tag, &payload).await?;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
SetTaskIntervalState {
|
||||||
|
task_name,
|
||||||
|
interval,
|
||||||
|
state,
|
||||||
|
} => {
|
||||||
|
let map = format!("{}:task_interval_states", prefix);
|
||||||
|
let key = format!("{}_{}-{}", task_name, interval.start, interval.end);
|
||||||
|
let value = serde_json::to_string(&state).unwrap();
|
||||||
|
conn.hset(&map, &key, &value).await?;
|
||||||
|
}
|
||||||
|
*/
|
||||||
StoreState { state } => {
|
StoreState { state } => {
|
||||||
let tag = format!("{}:state", prefix);
|
let tag = format!("{}:state", prefix);
|
||||||
let payload = serde_json::to_string(&state).unwrap();
|
let payload = serde_json::to_string(&state).unwrap();
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<html>
|
||||||
|
<script src="https://unpkg.com/timelines-chart"></script>
|
||||||
|
</html>
|
||||||
|
<body>
|
||||||
|
<div id="timeline"></div>
|
||||||
|
<script>
|
||||||
|
fetch("http://localhost:2503/api/v1/details",
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: '{ "start": "2021-09-01T00:00:00Z", "end": "2022-10-01T00:00:00Z" }'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not OK');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((payload) => {
|
||||||
|
console.log(payload);
|
||||||
|
payload.map((group) => {
|
||||||
|
Object.values(group.data).map((label) => {
|
||||||
|
label.data.map((interval) => {
|
||||||
|
interval.timeRange = interval.timeRange.map((t) => new Date(t));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
TimelinesChart()
|
||||||
|
(document.body)
|
||||||
|
.timeFormat("%Y-%m-%dT%H:%M:%S.%LZ") // ISO 8601 format
|
||||||
|
.zScaleLabel('State')
|
||||||
|
.zQualitative(true)
|
||||||
|
.useUtc(false)
|
||||||
|
.data(payload)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch(err => { throw err });
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user