use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use crate::daemon_core::core::NormalizedPath;
use crate::daemon_core::daemon::server::{
EmbeddedCompileRequest, EmbeddedDaemon, EmbeddedFlushReport, EmbeddedStatsSnapshot,
};
pub use crate::daemon_core::audit::{AuditConfig, AuditContext};
pub type Result<T> = std::result::Result<T, EmbeddedError>;
#[derive(Debug, thiserror::Error)]
pub enum EmbeddedError {
#[error("failed to start embedded zccache service: {0}")]
Start(String),
#[error("embedded zccache compile failed: {0}")]
Compile(String),
#[error("embedded zccache service is already shut down")]
ShutDown,
#[error("embedded zccache operation cancelled by host token")]
Cancelled,
}
#[derive(Clone)]
pub struct ZccacheService {
daemon: Arc<EmbeddedDaemon>,
shutdown: Arc<AtomicBool>,
cancellation: Option<CancellationToken>,
_host_inflight_guard: Option<Arc<crate::daemon_core::daemon::process::HostInFlightGuard>>,
audit_sink: Option<Arc<crate::daemon_core::audit_writer::AuditSink>>,
}
#[derive(Debug, Clone)]
pub struct ZccacheConfig {
pub host: HostIdentity,
pub cache_root: NormalizedPath,
pub audit: AuditConfig,
pub limits: ServiceLimits,
pub runtime: RuntimeHooks,
pub cancellation: Option<CancellationToken>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostIdentity {
pub product: String,
pub instance_id: String,
pub workspace_id: String,
}
impl HostIdentity {
pub fn default_for_product(product: impl Into<String>) -> Self {
use blake3::Hasher;
let product = product.into();
let mut hasher = Hasher::new();
hasher.update(product.as_bytes());
hasher.update(b"\0zccache-host-identity-v1\0");
if let Ok(exe) = std::env::current_exe() {
hasher.update(exe.as_os_str().to_string_lossy().as_bytes());
}
let bytes = hasher.finalize();
let mut hex = String::with_capacity(32);
for byte in &bytes.as_bytes()[..16] {
use std::fmt::Write;
let _ = write!(hex, "{byte:02x}");
}
Self {
product,
instance_id: hex.clone(),
workspace_id: hex,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RuntimeHooks {
pub service_name: Option<String>,
pub handle: Option<tokio::runtime::Handle>,
}
#[derive(Debug, Clone, Default)]
pub struct ServiceLimits {
pub max_parallel_compiles: Option<usize>,
pub host_in_flight: Option<Arc<std::sync::atomic::AtomicUsize>>,
}
#[derive(Debug, Clone)]
pub struct CompileRequest {
pub audit: AuditContext,
pub compiler: NormalizedPath,
pub args: Vec<String>,
pub cwd: NormalizedPath,
pub env: Vec<(String, String)>,
pub stdin: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct CompileResponse {
pub exit_code: i32,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub cached: bool,
pub cache_outcome: CacheOutcome,
pub compile_id: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheOutcome {
Hit,
Miss,
Error,
}
#[derive(Debug, Clone)]
pub enum CompileChunk {
Stdout(Vec<u8>),
Stderr(Vec<u8>),
Done {
exit_code: i32,
cached: bool,
cache_outcome: CacheOutcome,
compile_id: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShutdownMode {
Graceful,
Force,
}
#[derive(Debug, Clone)]
pub struct ShutdownReport {
pub mode: ShutdownMode,
pub flushed: FlushReport,
}
#[derive(Debug, Clone)]
pub struct FlushReport {
pub pending_writes_drained: bool,
pub artifact_entries: u64,
pub metadata_entries: u64,
}
#[derive(Debug, Clone)]
pub struct ServiceStats {
pub cache_root: NormalizedPath,
pub uptime_secs: u64,
pub total_compilations: u64,
pub cache_hits: u64,
pub cache_misses: u64,
pub non_cacheable: u64,
pub compile_errors: u64,
pub compile_errors_cached: u64,
pub time_saved_ms: u64,
pub artifact_count: u64,
pub cache_size_bytes: u64,
pub metadata_entries: u64,
pub dep_graph_contexts: u64,
pub dep_graph_files: u64,
pub sessions_total: u64,
pub sessions_active: u64,
pub phase_profile: crate::daemon_core::protocol::PhaseProfileSummary,
}
impl ZccacheService {
pub async fn start(config: ZccacheConfig) -> Result<Self> {
let endpoint = embedded_endpoint(&config.host);
let cache_root =
crate::daemon_core::core::config::effective_cache_root_from_top_level(&config.cache_root);
let daemon = EmbeddedDaemon::start(endpoint, cache_root, config.runtime.handle.clone())
.await
.map_err(|err| EmbeddedError::Start(err.to_string()))?;
let host_inflight_guard = config
.limits
.host_in_flight
.map(crate::daemon_core::daemon::process::register_host_in_flight_counter)
.map(Arc::new);
let audit_sink =
crate::daemon_core::audit_writer::AuditSink::start(&config.audit, config.runtime.handle.clone())
.map_err(|err| EmbeddedError::Start(err.to_string()))?
.map(Arc::new);
Ok(Self {
daemon: Arc::new(daemon),
shutdown: Arc::new(AtomicBool::new(false)),
cancellation: config.cancellation,
_host_inflight_guard: host_inflight_guard,
audit_sink,
})
}
pub async fn compile(&self, request: CompileRequest) -> Result<CompileResponse> {
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let mut done = None;
self.compile_streaming(request, |chunk| match chunk {
CompileChunk::Stdout(bytes) => stdout.extend_from_slice(&bytes),
CompileChunk::Stderr(bytes) => stderr.extend_from_slice(&bytes),
CompileChunk::Done {
exit_code,
cached,
cache_outcome,
compile_id,
} => done = Some((exit_code, cached, cache_outcome, compile_id)),
})
.await?;
let (exit_code, cached, cache_outcome, compile_id) = done.ok_or_else(|| {
EmbeddedError::Compile("streaming compile completed without a Done event".to_string())
})?;
Ok(CompileResponse {
exit_code,
stdout,
stderr,
cached,
cache_outcome,
compile_id,
})
}
async fn compile_inner(&self, request: CompileRequest) -> Result<CompileResponse> {
let compile_id = request
.audit
.compile_id
.clone()
.or_else(|| request.audit.command_id.clone())
.map(String::from)
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
if self.shutdown.load(Ordering::Acquire) {
return Err(EmbeddedError::ShutDown);
}
if let Some(token) = &self.cancellation {
if token.is_cancelled() {
return Err(EmbeddedError::Cancelled);
}
}
let compile_future = self.daemon.compile(EmbeddedCompileRequest {
compiler: request.compiler.into_path_buf(),
args: request.args,
cwd: request.cwd.into_path_buf(),
env: Some(request.env),
stdin: request.stdin,
});
let response = match &self.cancellation {
Some(token) => {
let cancelled = token.cancelled();
tokio::select! {
biased;
() = cancelled => return Err(EmbeddedError::Cancelled),
result = compile_future => result.map_err(EmbeddedError::Compile)?,
}
}
None => compile_future.await.map_err(EmbeddedError::Compile)?,
};
let cache_outcome = if response.exit_code != 0 {
CacheOutcome::Error
} else if response.cached {
CacheOutcome::Hit
} else {
CacheOutcome::Miss
};
Ok(CompileResponse {
exit_code: response.exit_code,
stdout: response.stdout.as_ref().clone(),
stderr: response.stderr.as_ref().clone(),
cached: response.cached,
cache_outcome,
compile_id,
})
}
pub async fn compile_streaming<F>(&self, request: CompileRequest, mut on_chunk: F) -> Result<()>
where
F: FnMut(CompileChunk),
{
const CHUNK_BYTES: usize = 64 * 1024;
let (sender, mut receiver) = tokio::sync::mpsc::channel(8);
let context = crate::daemon_core::daemon::compile_output::OutputContext::new(sender);
let compile =
crate::daemon_core::daemon::compile_output::scope(context.clone(), self.compile_inner(request));
tokio::pin!(compile);
let response = loop {
tokio::select! {
biased;
chunk = receiver.recv() => {
if let Some(chunk) = chunk {
emit_output_chunk(&mut on_chunk, chunk);
}
}
result = &mut compile => break result?,
}
};
while let Ok(chunk) = receiver.try_recv() {
emit_output_chunk(&mut on_chunk, chunk);
}
if !context.was_live() {
for chunk in response.stdout.chunks(CHUNK_BYTES) {
on_chunk(CompileChunk::Stdout(chunk.to_vec()));
}
for chunk in response.stderr.chunks(CHUNK_BYTES) {
on_chunk(CompileChunk::Stderr(chunk.to_vec()));
}
}
on_chunk(CompileChunk::Done {
exit_code: response.exit_code,
cached: response.cached,
cache_outcome: response.cache_outcome,
compile_id: response.compile_id,
});
Ok(())
}
pub async fn stats(&self) -> Result<ServiceStats> {
if self.shutdown.load(Ordering::Acquire) {
return Err(EmbeddedError::ShutDown);
}
Ok(ServiceStats::from_snapshot(self.daemon.stats().await))
}
pub async fn flush(&self) -> Result<FlushReport> {
if self.shutdown.load(Ordering::Acquire) {
return Err(EmbeddedError::ShutDown);
}
if let Some(token) = &self.cancellation {
if token.is_cancelled() {
return Err(EmbeddedError::Cancelled);
}
}
let flush_future = self.daemon.flush();
let report = match &self.cancellation {
Some(token) => {
let cancelled = token.cancelled();
tokio::select! {
biased;
() = cancelled => return Err(EmbeddedError::Cancelled),
report = flush_future => report,
}
}
None => flush_future.await,
};
if let Some(sink) = &self.audit_sink {
let _ = sink.flush().await;
}
Ok(FlushReport::from_report(report))
}
pub async fn shutdown(self, mode: ShutdownMode) -> Result<ShutdownReport> {
if self.shutdown.swap(true, Ordering::AcqRel) {
return Err(EmbeddedError::ShutDown);
}
let report = self.daemon.shutdown().await;
if matches!(mode, ShutdownMode::Graceful) {
if let Some(sink) = &self.audit_sink {
let _ = sink.shutdown().await;
}
}
Ok(ShutdownReport {
mode,
flushed: FlushReport::from_report(report),
})
}
}
fn emit_output_chunk<F>(on_chunk: &mut F, chunk: crate::daemon_core::daemon::compile_output::OutputChunk)
where
F: FnMut(CompileChunk),
{
match chunk {
crate::daemon_core::daemon::compile_output::OutputChunk::Stdout(bytes) => {
on_chunk(CompileChunk::Stdout(bytes));
}
crate::daemon_core::daemon::compile_output::OutputChunk::Stderr(bytes) => {
on_chunk(CompileChunk::Stderr(bytes));
}
}
}
impl ServiceStats {
fn from_snapshot(snapshot: EmbeddedStatsSnapshot) -> Self {
let status = snapshot.status;
Self {
cache_root: status.cache_dir,
uptime_secs: status.uptime_secs,
total_compilations: status.total_compilations,
cache_hits: status.cache_hits,
cache_misses: status.cache_misses,
non_cacheable: status.non_cacheable,
compile_errors: status.compile_errors,
compile_errors_cached: status.compile_errors_cached,
time_saved_ms: status.time_saved_ms,
artifact_count: status.artifact_count,
cache_size_bytes: status.cache_size_bytes,
metadata_entries: status.metadata_entries,
dep_graph_contexts: status.dep_graph_contexts,
dep_graph_files: status.dep_graph_files,
sessions_total: status.sessions_total,
sessions_active: status.sessions_active,
phase_profile: snapshot.phase_profile,
}
}
}
impl FlushReport {
fn from_report(report: EmbeddedFlushReport) -> Self {
Self {
pending_writes_drained: report.pending_writes_drained,
artifact_entries: report.artifact_entries,
metadata_entries: report.metadata_entries,
}
}
}
fn embedded_endpoint(host: &HostIdentity) -> String {
format!(
"embedded:{}:{}:{}",
sanitize_identity(&host.product),
sanitize_identity(&host.instance_id),
sanitize_identity(&host.workspace_id)
)
}
fn sanitize_identity(value: &str) -> String {
value
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
ch
} else {
'_'
}
})
.collect()
}
#[cfg(test)]
mod streaming_tests {
use super::*;
use tempfile::TempDir;
async fn start_test_service(temp: &TempDir) -> ZccacheService {
let mut audit = AuditConfig::default();
audit.mode = crate::daemon_core::audit::AuditMode::Off;
ZccacheService::start(ZccacheConfig {
host: HostIdentity {
product: "streaming-test".into(),
instance_id: uuid::Uuid::new_v4().to_string(),
workspace_id: "streaming-workspace".into(),
},
cache_root: temp.path().join("cache").into(),
audit,
limits: ServiceLimits::default(),
runtime: RuntimeHooks::default(),
cancellation: None,
})
.await
.expect("service start")
}
#[test]
fn compile_chunk_done_carries_outcome_fields() {
let done = CompileChunk::Done {
exit_code: 0,
cached: true,
cache_outcome: CacheOutcome::Hit,
compile_id: "test-id".to_string(),
};
let CompileChunk::Done {
exit_code,
cached,
cache_outcome,
compile_id,
} = done
else {
panic!("constructor must produce a Done variant");
};
assert_eq!(exit_code, 0);
assert!(cached);
assert_eq!(cache_outcome, CacheOutcome::Hit);
assert_eq!(compile_id, "test-id");
}
#[test]
fn compile_chunk_stdout_stderr_carry_bytes() {
match CompileChunk::Stdout(b"hello".to_vec()) {
CompileChunk::Stdout(bytes) => assert_eq!(bytes, b"hello"),
other => panic!("expected Stdout, got {other:?}"),
}
match CompileChunk::Stderr(b"warn".to_vec()) {
CompileChunk::Stderr(bytes) => assert_eq!(bytes, b"warn"),
other => panic!("expected Stderr, got {other:?}"),
}
}
#[tokio::test]
async fn cache_hit_replays_byte_identical_streams() {
let Some(compiler) = crate::daemon_core::test_support::find_clang() else {
return;
};
let temp = TempDir::new().expect("tempdir");
let source = temp.path().join("warning.c");
let output = temp.path().join("warning.o");
std::fs::write(
&source,
"#warning stream-replay\nint value(void) { return 1; }\n",
)
.expect("source");
let service = start_test_service(&temp).await;
let request = CompileRequest {
audit: AuditContext::new(
crate::daemon_core::audit::AuditId::new("stream-run").expect("id"),
crate::daemon_core::audit::AuditId::new("stream-trace").expect("id"),
),
compiler,
args: vec![
"-c".into(),
source.to_string_lossy().into_owned(),
"-o".into(),
output.to_string_lossy().into_owned(),
],
cwd: temp.path().into(),
env: Vec::new(),
stdin: Vec::new(),
};
let mut miss_stdout = Vec::new();
let mut miss_stderr = Vec::new();
let mut miss_cached = None;
service
.compile_streaming(request.clone(), |chunk| match chunk {
CompileChunk::Stdout(bytes) => miss_stdout.extend(bytes),
CompileChunk::Stderr(bytes) => miss_stderr.extend(bytes),
CompileChunk::Done { cached, .. } => miss_cached = Some(cached),
})
.await
.expect("cache miss compile");
assert_eq!(miss_cached, Some(false));
assert!(!miss_stderr.is_empty());
std::fs::remove_file(&output).expect("remove first output");
let mut hit_stdout = Vec::new();
let mut hit_stderr = Vec::new();
let mut hit_cached = None;
service
.compile_streaming(request, |chunk| match chunk {
CompileChunk::Stdout(bytes) => hit_stdout.extend(bytes),
CompileChunk::Stderr(bytes) => hit_stderr.extend(bytes),
CompileChunk::Done { cached, .. } => hit_cached = Some(cached),
})
.await
.expect("cache hit compile");
assert_eq!(hit_cached, Some(true));
assert_eq!(hit_stdout, miss_stdout);
assert_eq!(hit_stderr, miss_stderr);
assert!(output.exists(), "cache hit must restore the output file");
service
.shutdown(ShutdownMode::Graceful)
.await
.expect("shutdown");
}
}
#[cfg(test)]
mod cancellation_tests {
use super::*;
use std::path::PathBuf;
use tempfile::TempDir;
use tokio_util::sync::CancellationToken;
fn fake_compile_request() -> CompileRequest {
CompileRequest {
audit: AuditContext::new(
crate::daemon_core::audit::AuditId::new("test-run").expect("non-empty"),
crate::daemon_core::audit::AuditId::new("test-trace").expect("non-empty"),
),
compiler: PathBuf::from("/nonexistent/compiler-that-never-runs").into(),
args: vec!["--version".into()],
cwd: std::env::current_dir().expect("cwd").into(),
env: Vec::new(),
stdin: Vec::new(),
}
}
async fn start_service_with_token(
temp: &TempDir,
token: Option<CancellationToken>,
instance_id: &str,
) -> Result<ZccacheService> {
let mut audit = AuditConfig::default();
audit.mode = crate::daemon_core::audit::AuditMode::Off;
ZccacheService::start(ZccacheConfig {
host: HostIdentity {
product: "zccache-test".into(),
instance_id: instance_id.into(),
workspace_id: instance_id.into(),
},
cache_root: temp.path().join("zccache").into(),
audit,
limits: ServiceLimits::default(),
runtime: RuntimeHooks::default(),
cancellation: token,
})
.await
}
#[tokio::test]
async fn precancelled_token_returns_cancelled_immediately() {
let temp = TempDir::new().expect("temp cache root");
let token = CancellationToken::new();
token.cancel();
let service = start_service_with_token(&temp, Some(token), "precancel")
.await
.expect("service start");
let outcome = service.compile(fake_compile_request()).await;
assert!(
matches!(outcome, Err(EmbeddedError::Cancelled)),
"pre-cancelled token must short-circuit compile(), got {outcome:?}"
);
let report = service.shutdown(ShutdownMode::Graceful).await;
assert!(report.is_ok(), "shutdown after Cancelled must succeed");
}
#[tokio::test]
async fn token_fired_during_compile_returns_cancelled() {
let temp = TempDir::new().expect("temp cache root");
let token = CancellationToken::new();
let token_clone = token.clone();
let service = start_service_with_token(&temp, Some(token), "midflight")
.await
.expect("service start");
let canceller = tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
token_clone.cancel();
});
let outcome = service.compile(fake_compile_request()).await;
canceller.await.expect("canceller task joined");
match outcome {
Err(EmbeddedError::Cancelled) | Err(EmbeddedError::Compile(_)) => {}
other => panic!("mid-flight cancel must yield Cancelled or Compile, got {other:?}"),
}
let report = service.shutdown(ShutdownMode::Graceful).await;
assert!(
report.is_ok(),
"shutdown after mid-flight cancel must succeed"
);
}
#[tokio::test]
async fn no_token_preserves_pre_923_behavior() {
let temp = TempDir::new().expect("temp cache root");
let service = start_service_with_token(&temp, None, "no-token")
.await
.expect("service start");
let outcome = service.compile(fake_compile_request()).await;
if let Err(EmbeddedError::Cancelled) = outcome {
panic!("cancellation: None must never yield Cancelled");
}
let report = service.shutdown(ShutdownMode::Graceful).await;
assert!(report.is_ok());
}
#[tokio::test]
async fn precancelled_token_short_circuits_flush() {
let temp = TempDir::new().expect("temp cache root");
let token = CancellationToken::new();
token.cancel();
let service = start_service_with_token(&temp, Some(token), "flush-cancel")
.await
.expect("service start");
let outcome = service.flush().await;
assert!(
matches!(outcome, Err(EmbeddedError::Cancelled)),
"pre-cancelled token must short-circuit flush(), got {outcome:?}"
);
let _ = service.shutdown(ShutdownMode::Graceful).await;
}
}
#[cfg(test)]
mod host_identity_tests {
use super::*;
#[test]
fn default_for_product_is_stable_within_one_process() {
let a = HostIdentity::default_for_product("soldr");
let b = HostIdentity::default_for_product("soldr");
assert_eq!(a, b, "same product must yield same identity");
assert_eq!(a.product, "soldr");
assert_eq!(a.workspace_id, a.instance_id);
}
#[test]
fn default_for_product_differs_per_product() {
let soldr = HostIdentity::default_for_product("soldr");
let fbuild = HostIdentity::default_for_product("fbuild");
assert_ne!(soldr, fbuild);
assert_ne!(soldr.instance_id, fbuild.instance_id);
}
#[test]
fn default_for_product_instance_id_is_16_bytes_of_hex() {
let id = HostIdentity::default_for_product("zccache-test");
assert_eq!(id.instance_id.len(), 32);
assert!(id.instance_id.chars().all(|c| c.is_ascii_hexdigit()));
}
}
#[cfg(test)]
mod runtime_hooks_tests {
use super::*;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use tempfile::TempDir;
#[test]
fn runtime_hooks_default_is_none() {
let hooks = RuntimeHooks::default();
assert!(hooks.handle.is_none());
assert!(hooks.service_name.is_none());
}
#[test]
fn explicit_handle_owns_background_spawns() {
let host_rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.thread_name("host-runtime-worker")
.build()
.expect("failed to build host runtime");
let host_handle = host_rt.handle().clone();
let landed_on_host: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let temp = TempDir::new().expect("temp cache root");
let cache_root: NormalizedPath = temp.path().join("zccache").into();
let landed_clone = Arc::clone(&landed_on_host);
let host_handle_clone = host_handle.clone();
let service = host_rt.block_on(async move {
let mut audit = AuditConfig::default();
audit.mode = crate::daemon_core::audit::AuditMode::Off;
ZccacheService::start(ZccacheConfig {
host: HostIdentity {
product: "zccache-test".into(),
instance_id: "runtime-hooks".into(),
workspace_id: "runtime-hooks".into(),
},
cache_root,
audit,
limits: ServiceLimits::default(),
runtime: RuntimeHooks {
service_name: Some("runtime-hooks-test".into()),
handle: Some(host_handle_clone),
},
cancellation: None,
})
.await
});
let service = service.expect("service start");
let landed_clone2 = Arc::clone(&landed_clone);
let probe = host_handle.spawn(async move {
if std::thread::current()
.name()
.map(|n| n.starts_with("host-runtime-worker"))
.unwrap_or(false)
{
landed_clone2.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
});
host_rt.block_on(probe).expect("probe ran on host runtime");
assert!(
landed_on_host.load(std::sync::atomic::Ordering::Relaxed) >= 1,
"task spawned via supplied handle must run on host runtime workers"
);
let _ = host_rt.block_on(service.shutdown(ShutdownMode::Graceful));
}
}
#[cfg(test)]
mod journal_tests {
use super::*;
use std::path::PathBuf;
use tempfile::TempDir;
fn unreachable_compile_request() -> CompileRequest {
CompileRequest {
audit: AuditContext::new(
crate::daemon_core::audit::AuditId::new("journal-run").expect("non-empty"),
crate::daemon_core::audit::AuditId::new("journal-trace").expect("non-empty"),
),
compiler: PathBuf::from("/nonexistent/compiler-that-never-runs").into(),
args: vec!["--version".into()],
cwd: std::env::current_dir().expect("cwd").into(),
env: Vec::new(),
stdin: Vec::new(),
}
}
#[tokio::test]
async fn embedded_compile_writes_compile_journal() {
let temp = TempDir::new().expect("temp cache root");
let mut audit = AuditConfig::default();
audit.mode = crate::daemon_core::audit::AuditMode::Off;
let service = ZccacheService::start(ZccacheConfig {
host: HostIdentity {
product: "zccache-test".into(),
instance_id: "embedded-journal".into(),
workspace_id: "embedded-journal".into(),
},
cache_root: temp.path().join("zccache").into(),
audit,
limits: ServiceLimits::default(),
runtime: RuntimeHooks::default(),
cancellation: None,
})
.await
.expect("service start");
let _ = service.compile(unreachable_compile_request()).await;
fn find_journal(dir: &std::path::Path) -> Option<std::path::PathBuf> {
let entries = std::fs::read_dir(dir).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(found) = find_journal(&path) {
return Some(found);
}
} else if path.file_name().and_then(|n| n.to_str()) == Some("compile_journal.jsonl")
{
return Some(path);
}
}
None
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
let content = loop {
let content = find_journal(temp.path()).and_then(|p| std::fs::read_to_string(p).ok());
match content {
Some(c) if !c.trim().is_empty() => break c,
_ if std::time::Instant::now() > deadline => {
panic!("embedded compile produced no compile_journal.jsonl record")
}
_ => tokio::time::sleep(std::time::Duration::from_millis(25)).await,
}
};
let line = content.lines().next().expect("at least one journal line");
let v: serde_json::Value = serde_json::from_str(line).expect("valid JSON journal line");
assert_eq!(
v["outcome"], "error",
"unspawnable compiler must journal as error: {v}"
);
assert!(
v["compiler"]
.as_str()
.unwrap_or_default()
.contains("compiler-that-never-runs"),
"journal must record the embedded compiler path: {v}"
);
let report = service.shutdown(ShutdownMode::Graceful).await;
assert!(report.is_ok(), "shutdown after journaled compile succeeds");
}
}