Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/actions/experiments/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::prelude::*;
use crate::toolchain::Toolchain;
use chrono::Utc;

/// Action to create a new experiment with the given parameters.
pub struct CreateExperiment {
pub name: String,
pub toolchains: [Toolchain; 2],
Expand Down
1 change: 1 addition & 0 deletions src/actions/experiments/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::db::QueryUtils;
use crate::experiments::Experiment;
use crate::prelude::*;

/// Action to delete an experiment and all its associated data.
pub struct DeleteExperiment {
pub name: String,
}
Expand Down
1 change: 1 addition & 0 deletions src/actions/experiments/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::experiments::{Assignee, CapLints, CrateSelect, Experiment, Mode, Stat
use crate::prelude::*;
use crate::toolchain::Toolchain;

/// Action to modify parameters of a queued experiment. `None` fields are left unchanged.
pub struct EditExperiment {
pub name: String,
pub toolchains: [Option<Toolchain>; 2],
Expand Down
1 change: 1 addition & 0 deletions src/actions/experiments/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub use self::create::CreateExperiment;
pub use self::delete::DeleteExperiment;
pub use self::edit::EditExperiment;

/// Errors returned when creating, editing, or deleting experiments.
#[derive(Debug, thiserror::Error)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub enum ExperimentError {
Expand Down
1 change: 1 addition & 0 deletions src/actions/lists/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::actions::{Action, ActionsCtx};
use crate::crates::lists::{GitHubList, List, LocalList, RegistryList};
use crate::prelude::*;

/// Action to refresh the cached crate lists (registry, GitHub, local).
pub struct UpdateLists {
pub github: bool,
pub registry: bool,
Expand Down
5 changes: 5 additions & 0 deletions src/actions/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//! Command pattern for mutating experiment state — creating, editing, and deleting
//! experiments and updating their crate lists.

mod experiments;
mod lists;

Expand All @@ -8,10 +11,12 @@ use crate::config::Config;
use crate::db::Database;
use crate::prelude::*;

/// A mutation that can be applied to experiment state.
pub trait Action {
fn apply(self, ctx: &ActionsCtx) -> Fallible<()>;
}

/// Shared context (database + config) passed to every [`Action`].
pub struct ActionsCtx<'ctx> {
db: &'ctx Database,
config: &'ctx Config,
Expand Down
18 changes: 18 additions & 0 deletions src/agent/api.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! HTTP client that agents use to communicate with the crater server.

use std::time::Duration;

use crate::agent::Capabilities;
Expand All @@ -15,6 +17,7 @@ use reqwest::{Method, StatusCode};
use serde::de::DeserializeOwned;
use serde_json::json;

/// Errors returned by the crater server's agent API.
#[derive(Debug, thiserror::Error)]
pub enum AgentApiError {
#[error("invalid API endpoint called")]
Expand All @@ -29,6 +32,7 @@ pub enum AgentApiError {
InternalServerError(String),
}

/// Converts an HTTP response into a typed `ApiResponse`, mapping status codes to errors.
trait ResponseExt {
fn to_api_response<T: DeserializeOwned>(self) -> Fallible<T>;
}
Expand Down Expand Up @@ -64,13 +68,15 @@ impl ResponseExt for ::reqwest::blocking::Response {
}
}

/// HTTP client for the agent-to-server API.
pub struct AgentApi {
url: String,
token: String,
random_id: String,
}

impl AgentApi {
/// Creates a new API client targeting the given server URL with an auth token.
pub fn new(url: &str, token: &str) -> Self {
AgentApi {
url: url.to_string(),
Expand All @@ -79,6 +85,7 @@ impl AgentApi {
}
}

/// Builds an authenticated HTTP request to the given agent-api endpoint.
fn build_request(&self, method: Method, url: &str) -> RequestBuilder {
utils::http::prepare_sync(method, &format!("{}/agent-api/{url}", self.url)).header(
AUTHORIZATION,
Expand All @@ -89,6 +96,11 @@ impl AgentApi {
)
}

/// Retries a request with exponential backoff on transient failures.
// - Retries on ServerUnavailable, reqwest timeouts/connection errors,
// and SQLite "database is locked" errors.
// - Backs off from 16 s up to a cap of 8 minutes between attempts.
// - Non-transient errors are returned immediately.
fn retry<T, F: Fn(&Self) -> Fallible<T>>(&self, f: F) -> Fallible<T> {
let mut retry_interval = 16u64;
loop {
Expand Down Expand Up @@ -125,6 +137,7 @@ impl AgentApi {
}
}

/// Sends the agent's capabilities and receives its configuration from the server.
pub fn config(&self, caps: &Capabilities) -> Fallible<AgentConfig> {
self.retry(|this| {
this.build_request(Method::POST, "config")
Expand All @@ -134,6 +147,7 @@ impl AgentApi {
})
}

/// Polls the server for the next experiment to run, sleeping 120 s if none is available.
pub fn next_experiment(&self) -> Result<Experiment> {
self.retry(|this| loop {
let resp: Option<_> = this
Expand All @@ -153,6 +167,7 @@ impl AgentApi {
})
}

/// Requests the next crate to test for the given experiment, or `None` if the queue is empty.
pub fn next_crate(&self, ex: &str) -> Fallible<Option<Crate>> {
self.retry(|this| {
let resp: Option<Crate> = this
Expand All @@ -165,6 +180,7 @@ impl AgentApi {
})
}

/// Uploads a crate's build/test result and base64-encoded log to the server.
pub fn record_progress(
&self,
ex: &Experiment,
Expand Down Expand Up @@ -193,6 +209,7 @@ impl AgentApi {
})
}

/// Sends a heartbeat to the server to signal this agent is still alive.
pub fn heartbeat(&self) -> Fallible<()> {
self.retry(|this| {
let _: bool = this
Expand All @@ -206,6 +223,7 @@ impl AgentApi {
})
}

/// Reports an error encountered while running an experiment to the server.
pub fn report_error(&self, ex: &Experiment, error: String) -> Fallible<()> {
self.retry(|this| {
let _: bool = this
Expand Down
9 changes: 7 additions & 2 deletions src/agent/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//! Distributed worker agents that poll the server for experiment work and report
//! results back.

mod api;

pub use crate::agent::api::AgentApi;
Expand All @@ -19,6 +22,7 @@ use std::time::{Duration, Instant};
// Purge all the caches if the disk is more than 50% full.
const PURGE_CACHES_THRESHOLD: f32 = 0.5;

/// Set of capabilities (e.g. "linux", "windows") advertised by an agent.
#[derive(Default, Serialize, Deserialize)]
pub struct Capabilities {
#[serde(default)]
Expand Down Expand Up @@ -66,6 +70,7 @@ impl FromIterator<String> for Capabilities {
}
}

/// A connected worker agent that fetches experiments from the server and runs them.
pub struct Agent {
api: AgentApi,
pub config: Config,
Expand Down Expand Up @@ -99,8 +104,7 @@ impl Agent {

static HEALTH_CHECK: AtomicBool = AtomicBool::new(false);

// Should be called at least once every 5 minutes, otherwise instance is
// replaced.
/// Marks the agent as healthy, enabling the health-check endpoint.
pub fn set_healthy() {
HEALTH_CHECK.store(true, Ordering::SeqCst);
}
Expand Down Expand Up @@ -182,6 +186,7 @@ fn run_experiment(
Ok(())
}

/// Connects to the server and runs experiments in a loop until interrupted.
pub fn run(
url: &str,
token: &str,
Expand Down
11 changes: 11 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//! Configuration loading from `config.toml` — crate overrides, sandbox limits,
//! server labels, and bot ACLs.

use crate::crates::Crate;
use crate::prelude::*;
use crate::utils::size::Size;
Expand All @@ -17,10 +20,12 @@ fn default_config_file() -> PathBuf {
.into()
}

/// Error indicating the configuration file has validation errors.
#[derive(Debug, thiserror::Error)]
#[error("the configuration file has errors")]
pub struct BadConfig;

/// Per-crate overrides (skip, skip-tests, quiet, broken).
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct CrateConfig {
Expand All @@ -38,20 +43,23 @@ fn default_false() -> bool {
false
}

/// Server-specific configuration: bot ACL and GitHub label settings.
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ServerConfig {
pub bot_acl: BotACL,
pub labels: ServerLabels,
}

/// Access control list for who can trigger experiments via the GitHub bot.
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct BotACL {
pub rust_teams: bool,
pub github: Vec<String>,
}

/// GitHub label names applied to issues during the experiment lifecycle.
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ServerLabels {
Expand All @@ -61,6 +69,7 @@ pub struct ServerLabels {
pub experiment_completed: String,
}

/// Small set of crates used for demo/testing runs.
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DemoCrates {
Expand All @@ -69,6 +78,7 @@ pub struct DemoCrates {
pub local_crates: Vec<String>,
}

/// Resource limits for the build sandbox (memory, log size, log lines).
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct SandboxConfig {
Expand All @@ -77,6 +87,7 @@ pub struct SandboxConfig {
pub build_log_max_lines: usize,
}

/// Top-level configuration loaded from `config.toml`.
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
Expand Down
5 changes: 5 additions & 0 deletions src/crates/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//! Crate abstraction — registry crates, GitHub repos, local paths, git repos,
//! and crate list management.

pub(crate) mod lists;
mod sources;

Expand All @@ -14,12 +17,14 @@ use std::str::FromStr;
pub(crate) use crate::crates::sources::github::GitHubRepo;
pub(crate) use crate::crates::sources::registry::RegistryCrate;

/// A git repository identified by URL and optional pinned SHA.
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, Clone)]
pub struct GitRepo {
pub url: String,
pub sha: Option<String>,
}

/// A crate to be tested — registry, GitHub, local, filesystem path, or arbitrary git repo.
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, Clone)]
pub enum Crate {
Registry(RegistryCrate),
Expand Down
7 changes: 7 additions & 0 deletions src/db/migrations.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Schema migrations applied on database startup.

use crate::prelude::*;
use rand::distr::{Alphanumeric, SampleString};
use rusqlite::{Connection, Transaction};
Expand Down Expand Up @@ -366,6 +368,11 @@ fn migrations() -> Vec<(&'static str, MigrationKind)> {
migrations
}

/// Applies all pending migrations to the database.
// - Creates the `migrations` tracking table on first run (user_version == 0).
// - Loads the set of already-executed migration names.
// - Iterates the full migration list, running each unapplied one inside
// its own transaction and recording it in the `migrations` table.
pub fn execute(db: &mut Connection) -> Fallible<()> {
// If the database version is 0, create the migrations table and bump it
let version: i32 = db.query_row("PRAGMA user_version;", [], |r| r.get(0))?;
Expand Down
Loading