use std::path::Path;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use super::{HostLeasePriorityClass, HostLeaseResourceKey};
const EXECUTION_CONTEXT_SCHEMA_VERSION: u32 = 1;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostLeasePathIdentity {
pub sha256: String,
}
impl HostLeasePathIdentity {
pub fn from_path(path: &Path) -> Self {
let digest = Sha256::digest(path.as_os_str().as_encoded_bytes());
Self {
sha256: hex::encode(digest),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum HostLeaseOperationKind {
Cargo,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostLeaseCargoExecutionContext {
pub workspace: HostLeasePathIdentity,
pub target_dir: HostLeasePathIdentity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_dir: Option<HostLeasePathIdentity>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "operation_kind", rename_all = "kebab-case")]
pub enum HostLeaseExecutionContext {
Cargo {
schema_version: u32,
context: HostLeaseCargoExecutionContext,
},
}
impl HostLeaseExecutionContext {
pub fn cargo(workspace: &Path, target_dir: &Path, build_dir: Option<&Path>) -> Self {
Self::Cargo {
schema_version: EXECUTION_CONTEXT_SCHEMA_VERSION,
context: HostLeaseCargoExecutionContext {
workspace: HostLeasePathIdentity::from_path(workspace),
target_dir: HostLeasePathIdentity::from_path(target_dir),
build_dir: build_dir.map(HostLeasePathIdentity::from_path),
},
}
}
pub const fn operation_kind(&self) -> HostLeaseOperationKind {
match self {
Self::Cargo { .. } => HostLeaseOperationKind::Cargo,
}
}
pub const fn cargo_context(&self) -> Option<&HostLeaseCargoExecutionContext> {
match self {
Self::Cargo { context, .. } => Some(context),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostLeaseProcessExit {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub code: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signal: Option<i32>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum HostLeaseRunReleaseOutcome {
Released,
AlreadyRecovered,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum HostLeaseRunStartFailure {
WorkerArguments,
WorkerSpawn,
WorkerExitedBeforeAcquire,
WorkerContextMismatch,
ResourceAcquire,
WorkerContract,
ReceiptTransition,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum HostLeaseRunLaunchFailure {
ArgumentEncoding,
ProcessReplace,
ProcessSpawn,
ProcessSupervision,
UnsupportedPlatform,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "kebab-case")]
pub enum HostLeaseRunState {
Pending {
requested_at_ms: i64,
},
Running {
lease_id: String,
acquired_at_ms: i64,
acquire_wait_ms: u64,
worker_pid: u32,
},
Deferred {
observed_at_ms: i64,
waited_ms: u64,
},
StartFailed {
observed_at_ms: i64,
error: HostLeaseRunStartFailure,
},
CancelledBeforeStart {
finished_at_ms: i64,
},
LaunchFailed {
lease_id: String,
release: HostLeaseRunReleaseOutcome,
observed_at_ms: i64,
error: HostLeaseRunLaunchFailure,
},
Completed {
lease_id: String,
acquire_wait_ms: u64,
hold_ms: u64,
worker_pid: u32,
exit: HostLeaseProcessExit,
release: HostLeaseRunReleaseOutcome,
finished_at_ms: i64,
},
Cancelled {
lease_id: String,
acquire_wait_ms: u64,
hold_ms: u64,
worker_pid: u32,
release: HostLeaseRunReleaseOutcome,
finished_at_ms: i64,
},
}
impl HostLeaseRunState {
pub(super) fn may_transition_to(&self, next: &Self) -> bool {
match (self, next) {
(
Self::Pending { .. },
Self::Running { .. }
| Self::Deferred { .. }
| Self::StartFailed { .. }
| Self::CancelledBeforeStart { .. },
) => true,
(
Self::Running {
lease_id,
acquire_wait_ms,
worker_pid,
..
},
Self::Completed {
lease_id: next_lease_id,
acquire_wait_ms: next_wait_ms,
worker_pid: next_worker_pid,
..
}
| Self::Cancelled {
lease_id: next_lease_id,
acquire_wait_ms: next_wait_ms,
worker_pid: next_worker_pid,
..
},
) => {
lease_id == next_lease_id
&& acquire_wait_ms == next_wait_ms
&& worker_pid == next_worker_pid
}
(
Self::Running { lease_id, .. },
Self::LaunchFailed {
lease_id: next_lease_id,
..
},
) => lease_id == next_lease_id,
_ => false,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostLeaseRunReceipt {
pub schema_version: u32,
pub run_id: String,
pub owner: String,
pub priority_class: HostLeasePriorityClass,
pub wait_limit_ms: u64,
pub resource: HostLeaseResourceKey,
pub execution_context: HostLeaseExecutionContext,
pub status: HostLeaseRunState,
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
#[test]
fn cargo_context_keeps_only_stable_path_identities() {
let context = HostLeaseExecutionContext::cargo(
Path::new("/workspace/project"),
Path::new("/tmp/target"),
Some(Path::new("/tmp/build")),
);
assert_eq!(context.operation_kind(), HostLeaseOperationKind::Cargo);
let cargo = context.cargo_context().expect("Cargo context is present");
assert_ne!(cargo.target_dir, cargo.build_dir.clone().unwrap());
assert_eq!(cargo.workspace.sha256.len(), 64);
}
#[test]
fn omitted_build_directory_is_not_invented() {
let context = HostLeaseExecutionContext::cargo(
Path::new("/workspace/project"),
Path::new("/tmp/target"),
None,
);
assert!(context
.cargo_context()
.expect("Cargo context is present")
.build_dir
.is_none());
}
}