use crate::iso_time::format_iso_milliseconds;
use crate::standards_review_client::{Gateway, Reply, SCRIPT_QUIET_WINDOW, post_decision};
use crate::standards_review_append::{deliver, names_commit};
use crate::standards_review_entry::{ReviewEntry, render_review, render_skipped};
use crate::standards_review_input::{LandedCommit, Plan, ReviewInput, build_input, plan_for, shared_log_path};
use crate::standards_review_request::{ENDPOINT, Judged, read_answer, request_body};
use crate::standards_review_text::seconds_with_tenth;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::Duration;
pub const LOG_VARIABLE: &str = "SHADOW_REVIEW_LOG";
pub const SHA_VARIABLE: &str = "SHADOW_REVIEW_SHA";
pub const ALLOW_VARIABLE: &str = "SHADOW_REVIEW_ALLOW";
pub const KEY_VARIABLES: [&str; 2] = crate::service_review_key::KEY_VARIABLES;
pub const NO_KEY_MESSAGE: &str = "no SHADOW_REVIEW_LLMGATEWAY_API_KEY or STOP_REMINDER_LLMGATEWAY_API_KEY in the environment";
#[derive(Clone, Debug)]
pub struct ReviewOptions {
pub log_path: PathBuf,
pub api_key: Option<String>,
pub endpoint: String,
pub quiet_window: Duration,
pub call_ceiling: Duration,
pub trusted_pem: Option<Vec<u8>>,
}
pub fn options_from_environment(common_dir: &Path, variable: &dyn Fn(&str) -> Option<String>) -> ReviewOptions {
let log_path: PathBuf = variable(LOG_VARIABLE).map_or_else(|| return shared_log_path(common_dir), PathBuf::from);
let api_key: Option<String> = variable(KEY_VARIABLES[0]).or_else(|| return variable(KEY_VARIABLES[1]));
return ReviewOptions { log_path, api_key, endpoint: String::from(ENDPOINT), quiet_window: SCRIPT_QUIET_WINDOW, call_ceiling: crate::wait_ceilings::REVIEW_CALL_CEILING, trusted_pem: None };
}
pub fn full_review_requested(variable: &dyn Fn(&str) -> Option<String>) -> bool {
return variable(ALLOW_VARIABLE).as_deref() == Some("1");
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ReviewOutcome {
NoEntry,
Skipped,
Reviewed(Judged),
Unread(String),
Unwritable(String),
AlreadyLogged,
}
fn ask_gateway(input: &ReviewInput, options: &ReviewOptions) -> Judged {
let failed = |message: String| return Judged { top: String::new(), outcome: format!("Luna failed: {message}") };
let key: &str = options.api_key.as_deref().unwrap_or("");
if key.is_empty() {
return failed(String::from(NO_KEY_MESSAGE));
}
let gateway: Gateway = Gateway { url: options.endpoint.as_str(), api_key: key, quiet_window: options.quiet_window, ceiling: options.call_ceiling, trusted_pem: options.trusted_pem.as_deref() };
let reply: Result<Reply, String> = post_decision(&gateway, request_body(input.model_input.as_str()).as_str());
return match reply.and_then(|answer| return read_answer(answer.status, answer.body.as_str())) {
Ok(judged) => judged,
Err(message) => failed(message),
};
}
pub fn review_landed(
names: &[String],
sha: &str,
subject: &str,
read: &dyn Fn(&[String]) -> Result<LandedCommit, String>,
options: &ReviewOptions,
now_millis: &dyn Fn() -> u64,
) -> ReviewOutcome {
let code_files: Vec<String> = match plan_for(names) {
Plan::Silent => return ReviewOutcome::NoEntry,
Plan::NoCode => {
return match deliver(options.log_path.as_path(), sha, render_skipped(sha, subject).as_str()) {
Ok(_) => ReviewOutcome::Skipped,
Err(message) => ReviewOutcome::Unwritable(message),
};
}
Plan::Review(files) => files,
};
if names_commit(options.log_path.as_path(), sha) {
return ReviewOutcome::AlreadyLogged;
}
let commit: LandedCommit = match read(code_files.as_slice()) {
Ok(read_commit) => read_commit,
Err(message) => return ReviewOutcome::Unread(message),
};
let input: ReviewInput = build_input(&commit, code_files.as_slice());
let started: u64 = now_millis();
let judged: Judged = ask_gateway(&input, options);
let finished: u64 = now_millis();
let entry: ReviewEntry = ReviewEntry {
date: format_iso_milliseconds(i64::try_from(finished).unwrap_or(i64::MAX)),
sha: String::from(sha),
subject: String::from(subject),
code_files: input.code_file_count,
diff_lines: input.diff_line_count,
truncated: input.truncated,
input_units: input.model_input_units,
seconds: seconds_with_tenth(finished.saturating_sub(started)),
top: judged.top.clone(),
outcome: judged.outcome.clone(),
};
return match deliver(options.log_path.as_path(), sha, render_review(&entry).as_str()) {
Ok(_) => ReviewOutcome::Reviewed(judged),
Err(message) => ReviewOutcome::Unwritable(message),
};
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Contained<T> {
Finished(T),
Panicked,
CeilingReached(Duration),
Busy,
}
pub struct Containment {
state: std::sync::atomic::AtomicU64,
}
const IDLE: u64 = 0;
const RUNNING: u64 = 1;
const ABANDONED: u64 = 2;
const STATUS: u64 = 3;
pub static RUNNER: Containment = Containment::new();
impl Containment {
pub const fn new() -> Self {
return Self { state: std::sync::atomic::AtomicU64::new(IDLE) };
}
pub fn outstanding(&self) -> usize {
return usize::from(self.state.load(std::sync::atomic::Ordering::SeqCst) & STATUS == ABANDONED);
}
fn admit(&self) -> Option<u64> {
use std::sync::atomic::Ordering;
let seen: u64 = self.state.load(Ordering::SeqCst);
if seen & STATUS != IDLE {
return None;
}
let generation: u64 = (seen >> 2) + 1;
return self.state.compare_exchange(seen, (generation << 2) | RUNNING, Ordering::SeqCst, Ordering::SeqCst).ok().map(|_| { return generation; });
}
fn change(&self, generation: u64, from: u64, to: u64) -> bool {
use std::sync::atomic::Ordering;
return self.state.compare_exchange((generation << 2) | from, (generation << 2) | to, Ordering::SeqCst, Ordering::SeqCst).is_ok();
}
}
impl Default for Containment {
fn default() -> Self {
return Self::new();
}
}
struct Release<'a> {
runner: &'a Containment,
generation: u64,
}
impl Drop for Release<'_> {
fn drop(&mut self) {
let _released: bool = self.runner.change(self.generation, RUNNING, IDLE) || self.runner.change(self.generation, ABANDONED, IDLE);
}
}
pub fn outstanding_reviews() -> usize {
return RUNNER.outstanding();
}
pub fn run_contained<T: Send + 'static>(job: impl FnOnce() -> T + Send + 'static, ceiling: Duration) -> Contained<T> {
return run_contained_in(&RUNNER, job, ceiling);
}
pub fn run_contained_in<T: Send + 'static>(runner: &'static Containment, job: impl FnOnce() -> T + Send + 'static, ceiling: Duration) -> Contained<T> {
let Some(generation) = runner.admit() else {
return Contained::Busy;
};
let (sender, receiver) = mpsc::channel::<T>();
let worker = std::thread::Builder::new().name(String::from("standards-review")).spawn(move || {
let release: Release<'static> = Release { runner, generation };
let value: T = job();
drop(release);
let _ = sender.send(value);
});
if worker.is_err() {
let _released: bool = runner.change(generation, RUNNING, IDLE);
return Contained::Panicked;
}
return match receiver.recv_timeout(ceiling) {
Ok(value) => Contained::Finished(value),
Err(mpsc::RecvTimeoutError::Disconnected) => Contained::Panicked,
Err(mpsc::RecvTimeoutError::Timeout) => {
if runner.change(generation, RUNNING, ABANDONED) {
Contained::CeilingReached(ceiling)
} else {
receiver.recv().map_or(Contained::Panicked, Contained::Finished)
}
}
}
}
#[cfg(test)]
#[path = "standards_review_run_tests.rs"]
mod tests;