Implement /api/runner/status

This commit is contained in:
Joscha 2023-08-10 18:47:44 +02:00
parent 5c8c037417
commit c713abc5d3
11 changed files with 296 additions and 29 deletions

View file

@ -8,7 +8,7 @@ mod repo;
use tracing::{debug_span, error, Instrument};
use super::{BenchRepo, Repo, Server};
use super::{Repo, Server};
async fn recurring_task(state: &Server, repo: Repo) {
async {
@ -28,7 +28,7 @@ async fn recurring_task(state: &Server, repo: Repo) {
.await;
}
pub(super) async fn run(server: Server, repo: Repo, bench_repo: Option<BenchRepo>) {
pub(super) async fn run(server: Server, repo: Repo) {
loop {
recurring_task(&server, repo.clone()).await;
tokio::time::sleep(server.config.repo_update_delay).await;

84
src/server/runners.rs Normal file
View file

@ -0,0 +1,84 @@
use std::collections::HashMap;
use gix::hashtable::HashSet;
use time::OffsetDateTime;
use crate::{config::Config, shared::RunnerStatus};
pub struct RunnerInfo {
pub secret: String,
pub last_seen: OffsetDateTime,
pub status: RunnerStatus,
}
impl RunnerInfo {
pub fn new(secret: String, last_seen: OffsetDateTime, status: RunnerStatus) -> Self {
Self {
secret,
last_seen,
status,
}
}
}
pub struct Runners {
config: &'static Config,
runners: HashMap<String, RunnerInfo>,
}
impl Runners {
pub fn new(config: &'static Config) -> Self {
Self {
config,
runners: HashMap::new(),
}
}
pub fn clean(&mut self, now: OffsetDateTime) {
self.runners
.retain(|_, v| now <= v.last_seen + self.config.web_runner_timeout)
}
pub fn verify(&self, name: &str, secret: &str) -> bool {
let Some(runner) = self.runners.get(name) else { return true; };
runner.secret == secret
}
pub fn update(&mut self, name: String, info: RunnerInfo) {
self.runners.insert(name, info);
}
fn oldest_working_on(&self, hash: &str) -> Option<&str> {
self.runners
.iter()
.filter_map(|(name, info)| match &info.status {
RunnerStatus::Working { hash: h, since, .. } if h == hash => Some((name, *since)),
_ => None,
})
.max_by_key(|(_, since)| *since)
.map(|(name, _)| name as &str)
}
pub fn should_abort_work(&self, name: &str) -> bool {
let Some(info) = self.runners.get(name) else { return false; };
let RunnerStatus::Working { hash, .. } = &info.status else { return false; };
let Some(oldest) = self.oldest_working_on(hash) else { return false; };
name != oldest
}
pub fn find_free_work<'a>(&self, hashes: &'a [String]) -> Option<&'a str> {
let covered = self
.runners
.values()
.filter_map(|info| match &info.status {
RunnerStatus::Working { hash, .. } => Some(hash),
_ => None,
})
.collect::<HashSet<_>>();
hashes
.iter()
.find(|hash| !covered.contains(hash))
.map(|hash| hash as &str)
}
}

View file

@ -1,3 +1,4 @@
mod api;
mod commit;
mod commit_hash;
mod index;
@ -47,6 +48,7 @@ pub async fn run(server: Server) -> somehow::Result<()> {
.route("/commit/:hash", get(commit_hash::get))
.route("/queue/", get(queue::get))
.route("/queue/table", get(queue::get_table))
.merge(api::router(&server))
.fallback(get(r#static::static_handler))
.with_state(server.clone());

91
src/server/web/api.rs Normal file
View file

@ -0,0 +1,91 @@
mod auth;
use std::sync::{Arc, Mutex};
use askama_axum::{IntoResponse, Response};
use axum::{
extract::State,
headers::{authorization::Basic, Authorization},
http::StatusCode,
routing::post,
Json, Router, TypedHeader,
};
use sqlx::SqlitePool;
use time::OffsetDateTime;
use crate::{
config::Config,
server::{
runners::{RunnerInfo, Runners},
BenchRepo, Server,
},
shared::{BenchMethod, RunnerRequest, ServerResponse, Work},
somehow,
};
async fn post_status(
TypedHeader(auth): TypedHeader<Authorization<Basic>>,
State(config): State<&'static Config>,
State(db): State<SqlitePool>,
State(bench_repo): State<Option<BenchRepo>>,
State(runners): State<Arc<Mutex<Runners>>>,
Json(request): Json<RunnerRequest>,
) -> somehow::Result<Response> {
let name = match auth::authenticate(config, auth) {
Ok(name) => name,
Err(response) => return Ok(response),
};
let now = OffsetDateTime::now_utc();
let queue = sqlx::query_scalar!(
"\
SELECT hash FROM queue \
ORDER BY priority DESC, unixepoch(date) DESC, hash ASC \
"
)
.fetch_all(&db)
.await?;
let mut guard = runners.lock().unwrap();
guard.clean(now);
if !guard.verify(&name, &request.secret) {
return Ok((StatusCode::UNAUTHORIZED, "invalid secret").into_response());
}
guard.update(
name.clone(),
RunnerInfo::new(request.secret, now, request.status),
);
let work = match request.request_work {
true => guard.find_free_work(&queue),
false => None,
};
let abort_work = guard.should_abort_work(&name);
drop(guard);
// Find new work
let work = if let Some(hash) = work {
let bench = match bench_repo {
Some(bench_repo) => BenchMethod::BenchRepo {
hash: bench_repo.0.to_thread_local().head_id()?.to_string(),
},
None => BenchMethod::Internal,
};
Some(Work {
hash: hash.to_string(),
bench,
})
} else {
None
};
Ok(Json(ServerResponse { work, abort_work }).into_response())
}
pub fn router(server: &Server) -> Router<Server> {
if server.repo.is_none() {
return Router::new().route("/api/runner/status", post(post_status));
}
// TODO Add routes
Router::new()
}

View file

@ -0,0 +1,41 @@
use askama_axum::IntoResponse;
use axum::{
headers::{authorization::Basic, Authorization},
http::{header, HeaderValue, StatusCode},
response::Response,
};
use crate::config::Config;
fn is_username_valid(username: &str) -> bool {
if username.is_empty() {
return false;
}
username
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
}
fn is_password_valid(password: &str, config: &'static Config) -> bool {
password == config.web_runner_token
}
pub fn authenticate(
config: &'static Config,
auth: Authorization<Basic>,
) -> Result<String, Response> {
if is_username_valid(auth.username()) && is_password_valid(auth.password(), config) {
return Ok(auth.username().to_string());
}
Err((
StatusCode::UNAUTHORIZED,
[(
header::WWW_AUTHENTICATE,
HeaderValue::from_str("Basic realm=\"runner api\"").unwrap(),
)],
"invalid credentials",
)
.into_response())
}