Skip to main content

mbx_cache_core/
agent.rs

1use crate::uploads::{ConnectionUploads, UploadQueue, UploadSink};
2use crate::{
3    ActionPrediction, ActionPromiseCompletion, ActionPromiseState, CacheDigest, CacheDirectory,
4    LocalActionCache, LocalCas, MAX_STAGED_BLOB_PACK_BYTES, ManifestPutOutcome, RemoteActionResult,
5    RemoteCacheClient, RemoteCacheMode, RustcMetadata, TaskActionManifest, blob_pack_chunk,
6    canonical_json,
7};
8use eyre::{Context, Result, bail};
9use futures_util::{FutureExt, StreamExt, future::BoxFuture, stream};
10use log::{debug, warn};
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, BTreeSet};
13use std::fs;
14use std::path::{Path, PathBuf};
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::{Arc, Mutex, Weak};
17use std::time::{Duration, Instant, SystemTime};
18
19/// Fleet claims are leases rather than correctness locks. This only bounds how
20/// long one shim waits through repeated pending responses before degrading to
21/// an ordinary compilation; the server independently expires abandoned claims.
22const ACTION_PROMISE_WAIT: Duration = Duration::from_secs(60 * 60);
23use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
24
25mod file_digest;
26mod manifest;
27mod prefetch;
28mod stats;
29mod wire;
30
31#[cfg(test)]
32pub(crate) use prefetch::select_prefetch_actions;
33
34pub use file_digest::{
35    FileDigestCache, FileDigestResolution, FileDigestScope, FileIdentity, FileObjectIdentity,
36    FileSnapshot, NoFileDigestCache, RecordedFileDigest, digest_file,
37};
38pub use manifest::{is_task_identity, task_manifest_actions};
39use manifest::{
40    merge_remote_task_manifest, merge_task_manifests, task_manifest_dir, validate_task_identity,
41    validate_task_manifest,
42};
43pub use stats::{AgentStats, CompilerStats};
44use wire::MAX_REQUEST_BYTES;
45pub use wire::{
46    AGENT_PROTOCOL_VERSION, ActionDiagnostic, AgentEvent, AgentEventObserver, AgentRequest,
47    AgentResponse, PinnedFile, PinnedState, RestoreStats, WrapperSpan, WrapperTiming,
48};
49
50const MAX_EXECUTABLE_IDENTITIES: usize = 64;
51const MAX_EXECUTABLE_IDENTITY_SIZE: usize = 64 * 1024;
52const MAX_EXECUTABLE_IDENTITY_BYTES: usize = 256 * 1024;
53const TASK_ACTION_MANIFEST_VERSION: u8 = 1;
54const MAX_TASK_ACTION_PREDICTIONS: usize = 16 * 1024;
55/// Longest shim diagnostic the agent accepts, in bytes.
56const MAX_DIAGNOSTIC_BYTES: usize = 4 * 1024;
57/// Most distinct shim diagnostics one session surfaces before going quiet.
58///
59/// A failure mode that repeats across a build tends to repeat with the same
60/// message, which deduplication already collapses; the cap only guards
61/// against a message that embeds something unique per compilation.
62const MAX_DIAGNOSTICS: usize = 128;
63const ACTION_DIAGNOSTIC_PREFIX: &str = "@mbx-action-diagnostic\t";
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
66enum DiagnosticLevel {
67    Warning,
68    Error,
69}
70
71impl DiagnosticLevel {
72    fn label(self) -> &'static str {
73        match self {
74            Self::Warning => "warning",
75            Self::Error => "error",
76        }
77    }
78}
79
80/// Most file identities one digest lookup or record may carry.
81const MAX_FILE_DIGEST_BATCH: usize = 16 * 1024;
82/// Most recorded file digests one session retains across every scope.
83///
84/// Roughly two orders of magnitude above the largest workspace measured; the
85/// cap exists so a pathological build bounds the agent instead of growing it.
86const MAX_FILE_DIGEST_ENTRIES: usize = 1024 * 1024;
87/// Distinct cold file reads allowed at once. Identical reads coalesce before
88/// this limit, while the limit prevents a wide cold build from flooding NFS.
89const MAX_CONCURRENT_FILE_DIGESTS: usize = 8;
90const MAX_REMOTE_TRANSFERS: usize = 64;
91const MAX_PREFETCH_TRANSFERS: usize = 48;
92/// Most blob packs downloaded at once.
93///
94/// Packs are individually bounded to keep their staging footprint predictable.
95/// Allowing a few independent streams prevents one large Azure object stream
96/// from serializing an entire workspace restore.
97const MAX_CONCURRENT_BLOB_PACKS: usize = 8;
98/// Most predicted actions whose complete output closures are downloaded
99/// speculatively for one task.
100///
101/// Manifests deliberately retain predictions across compatible builds, so a
102/// large workspace can describe far more actions than the next invocation
103/// will request. Keep the most expensive recorded actions warm and let
104/// foreground lookups fetch only unusually large tails on demand. The remote
105/// download-byte budget remains the primary bound on speculative transfer. A
106/// 1,024-action ceiling covers the measured large-workspace manifests while
107/// retaining a guard against pathological manifests and stale prediction sets.
108const MAX_PREFETCH_ACTIONS: usize = 1024;
109// Resolve speculative actions progressively so the most valuable predictions
110// become usable first and cancellation at the end of a build abandons a small
111// tail instead of one workspace-sized transfer wave. Action-result lookup is
112// still batched independently; this only bounds each output-closure download.
113const MAX_PREFETCH_ACTION_WAVE: usize = 32;
114/// Batched action lookups issued at once.
115///
116/// One request already asks about hundreds of actions. Serial batches keep a
117/// fleet of concurrent CI jobs from multiplying metadata pressure while blob
118/// transfers from earlier answers are also underway.
119const MAX_PREFETCH_BATCH_LOOKUPS: usize = 1;
120const PREFETCH_ACTION_BATCH_DELAY: Duration = Duration::from_millis(5);
121const MAX_PREFETCH_DIRECTORY_OBJECTS: usize = 100_000;
122const MAX_PREFETCH_OBJECTS_PER_WAVE: usize = 100_000;
123const DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES: u64 = 5 * 1024 * 1024 * 1024;
124
125/// Names the identities a task may inherit predictions from, consulted only
126/// once nothing has been recorded under its own.
127pub type TaskFallbacks = Arc<dyn Fn() -> Vec<String> + Send + Sync>;
128type FileDigestFlightKey = (FileDigestScope, FileIdentity);
129type FileDigestFlights = BTreeMap<FileDigestFlightKey, Weak<FileDigestFlight>>;
130
131struct FileDigestFlight {
132    lock: tokio::sync::Mutex<()>,
133    resolution: Mutex<Option<FileDigestResolution>>,
134}
135
136/// How many of the store's most recently written manifests a task with
137/// fallbacks tries after the named identities yield nothing.
138///
139/// A runner that restored a cache bundle holds the manifests of the builds
140/// that produced it and no version-control history to name them by, so what
141/// was written last is the best remaining guess at the lockfile before this
142/// one. Each candidate costs one parse, and a manifest from an unrelated
143/// workspace only fails to match. One is skipped when it fills more than half
144/// the prediction limit: an inherited manifest is kept under the new identity,
145/// and a foreign one that large would leave this workspace's own recordings
146/// no room.
147const NEWEST_MANIFEST_CANDIDATES: usize = 8;
148
149/// Remote action-cache access owned by one task session.
150pub struct AgentRemoteCache {
151    /// Remote protocol client used by the agent.
152    pub client: RemoteCacheClient,
153    /// Permitted remote read/write operations.
154    pub mode: RemoteCacheMode,
155    /// Directory used for verified downloads before CAS ingestion.
156    pub staging_dir: PathBuf,
157}
158
159#[derive(Default)]
160struct AtomicAgentStats {
161    wrapper_phases_ns: Mutex<BTreeMap<String, u64>>,
162    lookups: AtomicU64,
163    unconsulted: AtomicU64,
164    hits: AtomicU64,
165    stores: AtomicU64,
166    stored_bytes: AtomicU64,
167    verifications: AtomicU64,
168    divergences: AtomicU64,
169    downloaded_bytes: AtomicU64,
170    uploaded_bytes: AtomicU64,
171    background_uploads: AtomicU64,
172    background_upload_failures: AtomicU64,
173    remote_blob_pack_uploads: AtomicU64,
174    remote_blob_pack_upload_blobs: AtomicU64,
175    upload_drain_duration_ns: AtomicU64,
176    prefetched_actions: AtomicU64,
177    predictions_loaded: AtomicU64,
178    remote_failures: AtomicU64,
179    remote_manifest_lookups: AtomicU64,
180    remote_manifest_lookup_duration_ns: AtomicU64,
181    remote_action_lookups: AtomicU64,
182    remote_action_lookup_duration_ns: AtomicU64,
183    remote_blob_requests: AtomicU64,
184    remote_blob_pack_requests: AtomicU64,
185    remote_blob_pack_blobs: AtomicU64,
186    remote_blob_transfer_duration_ns: AtomicU64,
187    local_cas_write_duration_ns: AtomicU64,
188    prefetch_runs: AtomicU64,
189    prefetch_duration_ns: AtomicU64,
190    materialization_duration_ns: AtomicU64,
191    bypasses: Mutex<BTreeMap<String, u64>>,
192    avoided_compiler_duration_ns: AtomicU64,
193    compiler: Mutex<BTreeMap<String, CompilerStats>>,
194    slow_compilations: Mutex<BTreeMap<String, u64>>,
195    restored_output_files: AtomicU64,
196    restored_output_bytes: AtomicU64,
197    reflinked_output_files: AtomicU64,
198    reflinked_output_bytes: AtomicU64,
199    copied_output_files: AtomicU64,
200    copied_output_bytes: AtomicU64,
201    reused_output_files: AtomicU64,
202    reused_output_bytes: AtomicU64,
203}
204
205struct AtomicDurationTimer<'a> {
206    started: Instant,
207    target: &'a AtomicU64,
208}
209
210impl<'a> AtomicDurationTimer<'a> {
211    fn start(target: &'a AtomicU64) -> Self {
212        Self {
213            started: Instant::now(),
214            target,
215        }
216    }
217}
218
219impl Drop for AtomicDurationTimer<'_> {
220    fn drop(&mut self) {
221        atomic_saturating_add(self.target, duration_ns(self.started));
222    }
223}
224
225fn duration_ns(started: Instant) -> u64 {
226    started.elapsed().as_nanos().try_into().unwrap_or(u64::MAX)
227}
228
229fn validate_crate_name(crate_name: Option<&str>) -> Result<()> {
230    if let Some(crate_name) = crate_name
231        && (crate_name.len() > 256 || crate_name.contains(['\0', '\n', '\r']))
232    {
233        bail!("invalid compiler crate name");
234    }
235    Ok(())
236}
237
238#[derive(serde::Deserialize)]
239struct ActionDiagnosticEnvelope {
240    outcome: String,
241    crate_name: Option<String>,
242    diagnostic: ActionDiagnostic,
243}
244
245fn parse_action_diagnostic(
246    message: &str,
247) -> Option<Result<(String, Option<String>, ActionDiagnostic)>> {
248    let payload = message.strip_prefix(ACTION_DIAGNOSTIC_PREFIX)?;
249    Some((|| {
250        let envelope: ActionDiagnosticEnvelope = serde_json::from_str(payload)?;
251        if !matches!(envelope.outcome.as_str(), "hit" | "miss") {
252            bail!("invalid action diagnostic outcome");
253        }
254        validate_crate_name(envelope.crate_name.as_deref())?;
255        Ok((envelope.outcome, envelope.crate_name, envelope.diagnostic))
256    })())
257}
258
259fn atomic_saturating_add(target: &AtomicU64, value: u64) {
260    let _ = target.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
261        Some(current.saturating_add(value))
262    });
263}
264
265fn queue_prefetch_digest(
266    verified: &BTreeMap<CacheDigest, PathBuf>,
267    pending: &mut BTreeMap<CacheDigest, ()>,
268    digest: CacheDigest,
269) {
270    if verified.contains_key(&digest) || pending.contains_key(&digest) {
271        return;
272    }
273    pending.insert(digest, ());
274}
275
276fn queue_prefetch_directory(
277    seen: &BTreeMap<CacheDigest, ()>,
278    pending: &mut BTreeMap<CacheDigest, ()>,
279    digest: CacheDigest,
280    limit: usize,
281) -> bool {
282    if seen.contains_key(&digest) || pending.contains_key(&digest) {
283        return true;
284    }
285    if seen.len().saturating_add(pending.len()) >= limit {
286        return false;
287    }
288    pending.insert(digest, ());
289    true
290}
291
292/// See [`CacheAgent::with_task_loader`].
293pub type TaskLoader = dyn for<'a> Fn(&'a CacheAgent, &'a str) -> BoxFuture<'a, ()> + Send + Sync;
294
295/// Shared state for an agent hosted by the process that owns a build session.
296///
297/// Transport listeners deliberately live in the embedder so the session
298/// lifecycle owns them. This type only contains ecosystem-independent CAS and
299/// protocol logic.
300#[derive(Clone)]
301pub struct CacheAgent {
302    cas: LocalCas,
303    actions: LocalActionCache,
304    verified_blobs: Arc<Mutex<BTreeMap<CacheDigest, VerifiedBlob>>>,
305    version: Arc<str>,
306    write_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
307    action_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
308    stats: Arc<AtomicAgentStats>,
309    observer: Option<Arc<dyn AgentEventObserver>>,
310    observer_emission: Arc<Mutex<()>>,
311    /// Loads a task the first time a request names it, so a connection that
312    /// only reports a bypass never waits for a manifest it will not read.
313    task_loader: Option<Arc<TaskLoader>>,
314    executable_identities: Arc<Mutex<BTreeMap<ExecutableIdentityKey, Vec<u8>>>>,
315    /// Where identities pinned to their executables outlive the session.
316    identity_dir: Arc<PathBuf>,
317    manifest_dir: Arc<PathBuf>,
318    task_actions: Arc<Mutex<BTreeMap<String, TaskActionState>>>,
319    /// Where a task may inherit predictions from, by task identity.
320    task_fallbacks: Arc<Mutex<BTreeMap<String, TaskFallbacks>>>,
321    next_task_run: Arc<AtomicU64>,
322    manifest_write_lock: Arc<Mutex<()>>,
323    remote: Option<Arc<RemoteCacheClient>>,
324    remote_mode: RemoteCacheMode,
325    remote_staging_dir: Arc<PathBuf>,
326    remote_download_limit: u64,
327    remote_download_bytes: Arc<AtomicU64>,
328    pending_remote_actions: Arc<Mutex<BTreeMap<CacheDigest, RemoteActionResult>>>,
329    remote_transfers: Arc<tokio::sync::Semaphore>,
330    prefetch_transfers: Arc<tokio::sync::Semaphore>,
331    prefetch_tasks: Arc<Mutex<Vec<tokio::task::JoinHandle<()>>>>,
332    /// Distinct shim diagnostics already surfaced, so a diagnostic that fires
333    /// once per compilation is printed once per session. The severity is part
334    /// of each key: a fatal reason must still be surfaced after the same text
335    /// appeared as a warning.
336    diagnostics: Arc<Mutex<BTreeSet<(DiagnosticLevel, String)>>>,
337    /// Digests of files shims hashed or wrote this session, keyed by scope and
338    /// path, each entry standing while its recorded identity matches the disk.
339    file_digests: Arc<Mutex<BTreeMap<(FileDigestScope, PathBuf), RecordedFileDigest>>>,
340    /// Per-identity flights that make concurrent cold lookups share one read.
341    file_digest_locks: Arc<Mutex<FileDigestFlights>>,
342    file_digest_permits: Arc<tokio::sync::Semaphore>,
343    #[cfg(test)]
344    file_digest_reads: Arc<AtomicU64>,
345    /// Deferred remote publication, present only when the session may write.
346    uploads: Option<UploadQueue>,
347}
348
349/// What every file-digest record must satisfy before the ledger will answer
350/// with it, whether a shim sent it or an earlier session left it behind.
351fn validate_file_digest_record(entry: &RecordedFileDigest) -> Result<()> {
352    if !entry.file.path.is_absolute() {
353        bail!("file-digest records need absolute paths");
354    }
355    entry.digest.validate()?;
356    if entry.file.len != entry.digest.size {
357        bail!("file-digest record length does not match its digest");
358    }
359    Ok(())
360}
361
362/// Records background upload activity against a session's statistics.
363struct AgentUploadSink {
364    stats: Arc<AtomicAgentStats>,
365}
366
367impl UploadSink for AgentUploadSink {
368    fn record_blob_uploaded(&self, bytes: u64) {
369        self.stats
370            .background_uploads
371            .fetch_add(1, Ordering::Relaxed);
372        self.stats
373            .uploaded_bytes
374            .fetch_add(bytes, Ordering::Relaxed);
375    }
376
377    fn record_action_uploaded(&self) {
378        self.stats
379            .background_uploads
380            .fetch_add(1, Ordering::Relaxed);
381    }
382
383    fn record_blob_pack_uploaded(&self, blobs: u64) {
384        self.stats
385            .remote_blob_pack_uploads
386            .fetch_add(1, Ordering::Relaxed);
387        self.stats
388            .remote_blob_pack_upload_blobs
389            .fetch_add(blobs, Ordering::Relaxed);
390    }
391
392    fn record_upload_failure(&self) {
393        self.stats
394            .background_upload_failures
395            .fetch_add(1, Ordering::Relaxed);
396        self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
397    }
398}
399
400/// A CAS blob whose contents this session hashed, and the file identity that
401/// says the hash still describes what is on disk.
402///
403/// Length alone would miss an overwrite that keeps the size; the modification
404/// time is what makes a rewrite visible without reading the bytes again.
405#[derive(Debug, Clone)]
406struct VerifiedBlob {
407    path: PathBuf,
408    len: u64,
409    modified: SystemTime,
410}
411
412impl VerifiedBlob {
413    /// Describe a file that was just verified, or nothing when the filesystem
414    /// will not report a modification time to compare against later.
415    fn describe(path: &Path) -> Option<Self> {
416        let metadata = std::fs::metadata(path).ok()?;
417        Some(Self {
418            path: path.to_path_buf(),
419            len: metadata.len(),
420            modified: metadata.modified().ok()?,
421        })
422    }
423
424    /// Whether the file still has the identity it had when it was verified.
425    fn is_unchanged(&self) -> bool {
426        let Ok(metadata) = std::fs::metadata(&self.path) else {
427            return false;
428        };
429        metadata.len() == self.len && metadata.modified().is_ok_and(|now| now == self.modified)
430    }
431}
432
433#[derive(Debug, Clone, Default)]
434struct TaskActionState {
435    manifest: String,
436    baseline_loaded: bool,
437    predictions: BTreeMap<CacheDigest, ActionPrediction>,
438    pending_predictions: BTreeMap<CacheDigest, ActionPrediction>,
439    /// The pending predictions that differ from what the manifest already
440    /// held when this run loaded it, each with what it held: nothing, or
441    /// the baseline's own prediction. A hit re-records its prediction
442    /// byte-for-byte so the receipt can name it, and that is no reason to
443    /// rewrite the manifest; nor is a prediction that moved and then moved
444    /// back, which is why the baseline value is kept rather than a flag.
445    changed_predictions: BTreeMap<CacheDigest, Option<ActionPrediction>>,
446    prefetched_adapters: BTreeSet<String>,
447    remote_etag: Option<String>,
448}
449
450/// Match an invocation and, on an adapter's first match, select its prefetch wave.
451fn activate_prediction_adapter(
452    state: &mut TaskActionState,
453    invocation: &CacheDigest,
454) -> (Option<ActionPrediction>, Option<Vec<ActionPrediction>>) {
455    let prediction = state.predictions.get(invocation).cloned();
456    let prefetch = prediction.as_ref().and_then(|prediction| {
457        if !state.prefetched_adapters.insert(prediction.adapter.clone()) {
458            return None;
459        }
460        Some(
461            state
462                .predictions
463                .values()
464                .filter(|candidate| candidate.adapter == prediction.adapter)
465                .cloned()
466                .collect(),
467        )
468    });
469    (prediction, prefetch)
470}
471
472struct PrefetchedAction {
473    adapter: String,
474    result: RemoteActionResult,
475}
476
477struct RemoteDownloadReservation {
478    counter: Arc<AtomicU64>,
479    reserved: u64,
480    committed: bool,
481}
482
483impl RemoteDownloadReservation {
484    fn bytes(&self) -> u64 {
485        self.reserved
486    }
487
488    fn commit(mut self, bytes: u64) {
489        debug_assert!(bytes <= self.reserved);
490        self.counter
491            .fetch_sub(self.reserved.saturating_sub(bytes), Ordering::AcqRel);
492        self.committed = true;
493    }
494}
495
496impl Drop for RemoteDownloadReservation {
497    fn drop(&mut self) {
498        if !self.committed {
499            self.counter.fetch_sub(self.reserved, Ordering::AcqRel);
500        }
501    }
502}
503
504#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
505struct ExecutableIdentityKey {
506    executable: PathBuf,
507    environment: BTreeMap<String, Option<String>>,
508}
509
510// 2: pins carry the state the shim saw, and cover the search directories.
511const PERSISTED_EXECUTABLE_IDENTITY_VERSION: u8 = 2;
512
513/// An executable identity kept across sessions.
514///
515/// It stands while every pinned file is as the probing shim saw it. The
516/// probe reads those files and nothing else, so an unchanged set of them
517/// would print the same answer again.
518#[derive(Debug, Serialize, Deserialize)]
519struct PersistedExecutableIdentity {
520    version: u8,
521    executable: PathBuf,
522    environment: BTreeMap<String, Option<String>>,
523    pins: Vec<PinnedFile>,
524    stdout: String,
525}
526
527impl CacheAgent {
528    /// Create an agent backed by the cache rooted at `cache_dir`.
529    pub fn new(cache_dir: impl Into<PathBuf>, version: impl Into<Arc<str>>) -> Self {
530        Self::build(cache_dir.into(), version.into(), None, 0)
531    }
532
533    /// Create an agent with local-first access to a remote action cache.
534    pub fn new_remote(
535        cache_dir: impl Into<PathBuf>,
536        version: impl Into<Arc<str>>,
537        remote: AgentRemoteCache,
538    ) -> Self {
539        Self::build(
540            cache_dir.into(),
541            version.into(),
542            Some(remote),
543            DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES,
544        )
545    }
546
547    /// Create a remote agent with a cumulative download budget for the session.
548    pub fn new_remote_with_download_limit(
549        cache_dir: impl Into<PathBuf>,
550        version: impl Into<Arc<str>>,
551        remote: AgentRemoteCache,
552        max_remote_download_bytes: u64,
553    ) -> Self {
554        Self::build(
555            cache_dir.into(),
556            version.into(),
557            Some(remote),
558            max_remote_download_bytes,
559        )
560    }
561
562    fn build(
563        cache_dir: PathBuf,
564        version: Arc<str>,
565        remote: Option<AgentRemoteCache>,
566        remote_download_limit: u64,
567    ) -> Self {
568        let remote_mode = remote
569            .as_ref()
570            .map_or(RemoteCacheMode::ReadOnly, |remote| remote.mode);
571        let remote_staging_dir = remote.as_ref().map_or_else(
572            || cache_dir.join("remote"),
573            |remote| remote.staging_dir.clone(),
574        );
575        let remote = remote.map(|remote| Arc::new(remote.client));
576        let stats = Arc::new(AtomicAgentStats::default());
577        let remote_transfers = Arc::new(tokio::sync::Semaphore::new(MAX_REMOTE_TRANSFERS));
578        // A session that cannot publish never queues an upload, so it does not
579        // need somewhere to queue one.
580        let uploads = remote
581            .clone()
582            .filter(|_| remote_mode.writes())
583            .map(|client| {
584                UploadQueue::new(
585                    client,
586                    Arc::new(AgentUploadSink {
587                        stats: stats.clone(),
588                    }),
589                    remote_transfers.clone(),
590                )
591            });
592        Self {
593            cas: LocalCas::new(cache_dir.clone()),
594            actions: LocalActionCache::new(cache_dir.clone()),
595            verified_blobs: Arc::new(Mutex::new(BTreeMap::new())),
596            version,
597            write_locks: Arc::new(Mutex::new(BTreeMap::new())),
598            action_locks: Arc::new(Mutex::new(BTreeMap::new())),
599            stats,
600            observer: None,
601            observer_emission: Arc::new(Mutex::new(())),
602            task_loader: None,
603            executable_identities: Arc::new(Mutex::new(BTreeMap::new())),
604            identity_dir: Arc::new(cache_dir.join("executable-identities").join("v1")),
605            manifest_dir: Arc::new(task_manifest_dir(&cache_dir)),
606            task_actions: Arc::new(Mutex::new(BTreeMap::new())),
607            task_fallbacks: Arc::new(Mutex::new(BTreeMap::new())),
608            next_task_run: Arc::new(AtomicU64::new(0)),
609            manifest_write_lock: Arc::new(Mutex::new(())),
610            remote,
611            remote_mode,
612            remote_staging_dir: Arc::new(remote_staging_dir),
613            remote_download_limit,
614            remote_download_bytes: Arc::new(AtomicU64::new(0)),
615            pending_remote_actions: Arc::new(Mutex::new(BTreeMap::new())),
616            remote_transfers,
617            prefetch_transfers: Arc::new(tokio::sync::Semaphore::new(MAX_PREFETCH_TRANSFERS)),
618            prefetch_tasks: Arc::new(Mutex::new(Vec::new())),
619            diagnostics: Arc::new(Mutex::new(BTreeSet::new())),
620            file_digests: Arc::new(Mutex::new(BTreeMap::new())),
621            file_digest_locks: Arc::new(Mutex::new(BTreeMap::new())),
622            file_digest_permits: Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_FILE_DIGESTS)),
623            #[cfg(test)]
624            file_digest_reads: Arc::new(AtomicU64::new(0)),
625            uploads,
626        }
627    }
628
629    /// Report each accounted cache decision to `observer` as it happens.
630    #[must_use]
631    pub fn with_observer(mut self, observer: Arc<dyn AgentEventObserver>) -> Self {
632        self.observer = Some(observer);
633        self
634    }
635
636    /// Load tasks on demand.
637    ///
638    /// The loader is called with the agent and the task's identity before the
639    /// first request that names a task this agent has not begun, and is
640    /// expected to begin it; a second call for the same task must be a no-op.
641    /// Requests that name no task never wait on it.
642    pub fn with_task_loader(mut self, loader: Arc<TaskLoader>) -> Self {
643        self.task_loader = Some(loader);
644        self
645    }
646
647    /// Make sure `task` has been begun before a request reads or records
648    /// under it.
649    async fn ensure_task_loaded(&self, task: &str) {
650        let Some(loader) = &self.task_loader else {
651            return;
652        };
653        if self.task_actions.lock().unwrap().contains_key(task) {
654            return;
655        }
656        loader(self, task).await;
657    }
658
659    fn emit(&self, event: impl FnOnce() -> AgentEvent) {
660        if let Some(observer) = &self.observer {
661            let _emission = self.observer_emission.lock().unwrap();
662            observer.event(event());
663        }
664    }
665
666    fn emit_action(&self, diagnostic: Option<AgentEvent>, action: AgentEvent) {
667        if let Some(observer) = &self.observer {
668            let _emission = self.observer_emission.lock().unwrap();
669            if let Some(diagnostic) = diagnostic {
670                observer.event(diagnostic);
671            }
672            observer.event(action);
673        }
674    }
675
676    fn reserve_remote_download(&self, bytes: u64) -> Result<RemoteDownloadReservation> {
677        let mut current = self.remote_download_bytes.load(Ordering::Acquire);
678        loop {
679            let next = current
680                .checked_add(bytes)
681                .ok_or_else(|| eyre::eyre!("remote cache download budget overflowed"))?;
682            if next > self.remote_download_limit {
683                bail!(
684                    "remote cache download budget exceeded: {} bytes requested with {} of {} bytes already used",
685                    bytes,
686                    current,
687                    self.remote_download_limit
688                );
689            }
690            match self.remote_download_bytes.compare_exchange_weak(
691                current,
692                next,
693                Ordering::AcqRel,
694                Ordering::Acquire,
695            ) {
696                Ok(_) => {
697                    return Ok(RemoteDownloadReservation {
698                        counter: self.remote_download_bytes.clone(),
699                        reserved: bytes,
700                        committed: false,
701                    });
702                }
703                Err(observed) => current = observed,
704            }
705        }
706    }
707
708    fn reserve_remote_download_up_to(&self, requested: u64) -> Result<RemoteDownloadReservation> {
709        let mut current = self.remote_download_bytes.load(Ordering::Acquire);
710        loop {
711            let reserved = requested.min(self.remote_download_limit.saturating_sub(current));
712            let next = current + reserved;
713            match self.remote_download_bytes.compare_exchange_weak(
714                current,
715                next,
716                Ordering::AcqRel,
717                Ordering::Acquire,
718            ) {
719                Ok(_) => {
720                    return Ok(RemoteDownloadReservation {
721                        counter: self.remote_download_bytes.clone(),
722                        reserved,
723                        committed: false,
724                    });
725                }
726                Err(observed) => current = observed,
727            }
728        }
729    }
730
731    /// Load the last committed action manifest for a task into this session.
732    pub async fn begin_task(&self, task: &str) -> Result<String> {
733        self.begin_task_with_remote_errors(task, false, None, true)
734            .await
735    }
736
737    /// Load a task but defer each adapter's speculative downloads until its
738    /// first prediction matches the current build.
739    pub async fn begin_task_on_prediction(&self, task: &str) -> Result<String> {
740        self.begin_task_with_remote_errors(task, false, None, false)
741            .await
742    }
743
744    /// Load a task using its identity as the run key.
745    ///
746    /// A task-scoped process has only one run for an identity, so it can defer
747    /// this work until its first client connects while putting the identity in
748    /// the client's environment ahead of time.
749    pub async fn begin_session_task(&self, task: &str) -> Result<()> {
750        self.begin_task_with_remote_errors(task, false, Some(task.to_string()), false)
751            .await?;
752        Ok(())
753    }
754
755    /// Name where a task's predictions may come from when nothing has been
756    /// recorded under its own identity.
757    ///
758    /// The identities are produced on demand rather than taken up front,
759    /// because finding them can cost a few version-control commands and most
760    /// builds never need them. The first manifest found, locally and then on
761    /// the remote, is adopted under the task's own identity, so the commands
762    /// that follow, tests and lints included, start from it too, and a trusted
763    /// build publishes it there. When none of the named identities has one,
764    /// the store's newest manifests are tried.
765    pub fn register_task_fallbacks(
766        &self,
767        task: &str,
768        fallbacks: impl Fn() -> Vec<String> + Send + Sync + 'static,
769    ) -> Result<()> {
770        validate_task_identity(task)?;
771        self.task_fallbacks
772            .lock()
773            .unwrap()
774            .insert(task.to_string(), Arc::new(fallbacks));
775        Ok(())
776    }
777
778    /// Load a task and finish its prefetch, surfacing remote lookup failures.
779    pub async fn prefetch_task(&self, task: &str) -> Result<String> {
780        let run = self
781            .begin_task_with_remote_errors(task, true, None, true)
782            .await?;
783        self.wait_for_prefetches().await;
784        Ok(run)
785    }
786
787    async fn begin_task_with_remote_errors(
788        &self,
789        task: &str,
790        strict: bool,
791        run: Option<String>,
792        eager_prefetch: bool,
793    ) -> Result<String> {
794        validate_task_identity(task)?;
795        // The local manifest is already enough to start useful work. Do not
796        // leave those predictions idle while a high-latency remote answers the
797        // manifest lookup; the authoritative snapshot is loaded again below
798        // before it is merged, so another process can still update it while
799        // this request is in flight.
800        let early_manifest = {
801            let _write_guard = self.manifest_write_lock.lock().unwrap();
802            let _file_guard = self.lock_task_manifest(task)?;
803            self.load_task_manifest(task)?
804        };
805        let early_actions: BTreeSet<_> = early_manifest
806            .iter()
807            .flat_map(|manifest| manifest.predictions.iter())
808            .map(|prediction| prediction.action.clone())
809            .collect();
810        if eager_prefetch {
811            self.spawn_prefetch_predictions(
812                early_manifest
813                    .as_ref()
814                    .map(|manifest| manifest.predictions.clone())
815                    .unwrap_or_default(),
816            );
817        }
818        let (remote_manifest, mut remote_etag) = if self.remote_mode.reads() {
819            match self.get_remote_task_manifest(task).await {
820                Ok(Some((manifest, etag))) => (Some(manifest), Some(etag)),
821                Ok(None) => (None, None),
822                Err(error) => {
823                    if strict {
824                        return Err(error).wrap_err_with(|| {
825                            format!("remote task action manifest lookup failed for {task}")
826                        });
827                    }
828                    self.note_remote_failure();
829                    warn!("remote task action manifest lookup failed for {task}: {error}");
830                    (None, None)
831                }
832            }
833        } else {
834            (None, None)
835        };
836        // Without a remote there is nothing to reconcile: the manifest just
837        // read is the whole truth, and writing it back byte for byte would
838        // serialize every prediction for nothing.
839        let manifest = if self.remote.is_some() && self.remote_mode.reads() {
840            let _write_guard = self.manifest_write_lock.lock().unwrap();
841            let _file_guard = self.lock_task_manifest(task)?;
842            let local_manifest = self.load_task_manifest(task)?;
843            let manifest = match (remote_manifest, local_manifest) {
844                (Some(remote), Some(local)) => {
845                    let (manifest, merged) = merge_remote_task_manifest(task, remote, local);
846                    if !merged {
847                        remote_etag = None;
848                    }
849                    Some(manifest)
850                }
851                (Some(remote), None) => Some(remote),
852                (None, local) => local,
853            };
854            if let Some(manifest) = &manifest {
855                self.persist_task_manifest(manifest)?;
856            }
857            manifest
858        } else {
859            early_manifest
860        };
861        let (manifest, remote_etag) = match manifest {
862            Some(manifest) => (Some(manifest), remote_etag),
863            // Inherited from another identity, so the remote's copy of that
864            // one is no precondition for publishing under this one.
865            None => (self.inherit_task_manifest(task, strict).await?, None),
866        };
867        let mut state = if let Some(manifest) = manifest {
868            TaskActionState {
869                manifest: task.to_string(),
870                baseline_loaded: true,
871                predictions: manifest
872                    .predictions
873                    .into_iter()
874                    .map(|prediction| (prediction.invocation.clone(), prediction))
875                    .collect(),
876                pending_predictions: BTreeMap::new(),
877                changed_predictions: BTreeMap::new(),
878                prefetched_adapters: BTreeSet::new(),
879                remote_etag,
880            }
881        } else {
882            TaskActionState {
883                manifest: task.to_string(),
884                baseline_loaded: true,
885                remote_etag,
886                ..TaskActionState::default()
887            }
888        };
889        let run = run.unwrap_or_else(|| {
890            let sequence = self.next_task_run.fetch_add(1, Ordering::Relaxed);
891            CacheDigest::blake3(format!("{task}\0{}\0{sequence}", std::process::id()).as_bytes())
892                .hash
893        });
894        // The largest baseline rather than a sum: beginning the same task again
895        // in one session reloads the same manifest, and counting it twice would
896        // overstate what there was to match.
897        self.stats.predictions_loaded.fetch_max(
898            state.predictions.len().try_into().unwrap_or(u64::MAX),
899            Ordering::Relaxed,
900        );
901        if eager_prefetch {
902            state.prefetched_adapters.extend(
903                state
904                    .predictions
905                    .values()
906                    .map(|prediction| prediction.adapter.clone()),
907            );
908        }
909        // The early local wave already owns these actions. Only launch a
910        // second wave for predictions learned from the remote or from a local
911        // writer that committed while its lookup was in flight.
912        let predictions = state
913            .predictions
914            .values()
915            .filter(|prediction| !early_actions.contains(&prediction.action))
916            .cloned()
917            .collect();
918        self.task_actions.lock().unwrap().insert(run.clone(), state);
919        if eager_prefetch {
920            self.spawn_prefetch_predictions(predictions);
921        }
922        Ok(run)
923    }
924
925    /// Adopt the manifest of a fallback identity for a task that has none.
926    ///
927    /// The named identities are tried in the order given, each in the local
928    /// store and then on the remote, and then the store's newest manifests,
929    /// locally only. The first one found is persisted under `task` so the
930    /// next command sees a complete baseline rather than only what this one
931    /// recorded, and a later session finds it without asking again.
932    async fn inherit_task_manifest(
933        &self,
934        task: &str,
935        strict: bool,
936    ) -> Result<Option<TaskActionManifest>> {
937        let fallbacks = self.task_fallbacks.lock().unwrap().get(task).cloned();
938        let Some(fallbacks) = fallbacks else {
939            return Ok(None);
940        };
941        let named = tokio::task::spawn_blocking(move || fallbacks()).await?;
942        let mut tried = BTreeSet::from([task.to_string()]);
943        let candidates = named.into_iter().map(|identity| (identity, true)).chain(
944            self.newest_task_identities()
945                .into_iter()
946                .map(|identity| (identity, false)),
947        );
948        for (identity, named) in candidates {
949            if !tried.insert(identity.clone()) || validate_task_identity(&identity).is_err() {
950                continue;
951            }
952            let mut found = self.load_task_manifest(&identity)?;
953            if found.is_none() && named && self.remote_mode.reads() {
954                found = match self.get_remote_task_manifest(&identity).await {
955                    Ok(manifest) => manifest.map(|(manifest, _)| manifest),
956                    Err(error) => {
957                        if strict {
958                            return Err(error).wrap_err_with(|| {
959                                format!("remote task action manifest lookup failed for {identity}")
960                            });
961                        }
962                        self.note_remote_failure();
963                        warn!("remote task action manifest lookup failed for {identity}: {error}");
964                        None
965                    }
966                };
967            }
968            let Some(mut manifest) = found else {
969                continue;
970            };
971            if manifest.predictions.is_empty()
972                || (!named && manifest.predictions.len() > MAX_TASK_ACTION_PREDICTIONS / 2)
973            {
974                continue;
975            }
976            debug!(
977                "no predictions were recorded for {task}; inheriting {} from {identity}",
978                manifest.predictions.len()
979            );
980            manifest.task = task.to_string();
981            validate_task_manifest(&manifest, task)?;
982            let _write_guard = self.manifest_write_lock.lock().unwrap();
983            let _file_guard = self.lock_task_manifest(task)?;
984            // Another process may have recorded this identity while the
985            // fallbacks were being found. What it wrote describes this
986            // lockfile and wins over an inheritance.
987            if let Some(recorded) = self.load_task_manifest(task)? {
988                return Ok(Some(recorded));
989            }
990            self.persist_task_manifest(&manifest)?;
991            return Ok(Some(manifest));
992        }
993        Ok(None)
994    }
995
996    /// The identities of the store's most recently written manifests.
997    fn newest_task_identities(&self) -> Vec<String> {
998        let Ok(entries) = fs::read_dir(self.manifest_dir.as_path()) else {
999            return Vec::new();
1000        };
1001        let mut manifests: Vec<(std::time::SystemTime, String)> = entries
1002            .filter_map(|entry| {
1003                let entry = entry.ok()?;
1004                let name = entry.file_name();
1005                let identity = name.to_str()?.strip_suffix(".json")?.to_string();
1006                validate_task_identity(&identity).ok()?;
1007                let modified = entry.metadata().ok()?.modified().ok()?;
1008                Some((modified, identity))
1009            })
1010            .collect();
1011        manifests.sort_by(|left, right| right.cmp(left));
1012        manifests
1013            .into_iter()
1014            .take(NEWEST_MANIFEST_CANDIDATES)
1015            .map(|(_, identity)| identity)
1016            .collect()
1017    }
1018
1019    /// Cancel speculative downloads before the owning session exits.
1020    pub async fn cancel_prefetches(&self) {
1021        let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
1022        for task in &tasks {
1023            task.abort();
1024        }
1025        for task in tasks {
1026            if let Err(error) = task.await
1027                && !error.is_cancelled()
1028            {
1029                warn!("remote action prefetch task failed: {error}");
1030            }
1031        }
1032    }
1033
1034    /// Publish everything a build queued, before the session stops.
1035    ///
1036    /// Store requests return once an object is durable locally, so at this point
1037    /// the remote cache may still be behind the local one. Uploads run on the
1038    /// session's runtime and are abandoned if it goes away, so a session that
1039    /// wants them published has to wait here.
1040    pub async fn wait_for_uploads(&self) {
1041        let Some(uploads) = &self.uploads else {
1042            return;
1043        };
1044        let _timer = AtomicDurationTimer::start(&self.stats.upload_drain_duration_ns);
1045        uploads.drain().await;
1046    }
1047
1048    async fn wait_for_prefetches(&self) {
1049        let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
1050        for task in tasks {
1051            if let Err(error) = task.await {
1052                warn!("remote action prefetch task failed: {error}");
1053            }
1054        }
1055    }
1056
1057    /// Atomically publish the completed actions collected by a task run.
1058    pub async fn commit_task(&self, run: &str) -> Result<()> {
1059        self.commit_task_actions(run).await.map(|_| ())
1060    }
1061
1062    /// Publish a task run and return exactly the predictions completed by it.
1063    ///
1064    /// The persisted task manifest also carries predictions inherited from
1065    /// earlier runs. Callers that need a receipt for this one run must not
1066    /// mistake that cumulative manifest for the work the run completed.
1067    pub async fn commit_task_actions(&self, run: &str) -> Result<Vec<ActionPrediction>> {
1068        validate_task_identity(run)?;
1069        let state = {
1070            let mut runs = self.task_actions.lock().unwrap();
1071            let state = runs
1072                .get(run)
1073                .ok_or_else(|| eyre::eyre!("task action manifest baseline was not loaded"))?;
1074            if !state.baseline_loaded {
1075                bail!("task action manifest baseline was not loaded");
1076            }
1077            // A run that predicted nothing new leaves the manifest as it found
1078            // it. Rewriting it would serialize every inherited prediction to
1079            // say so, and a remote that is only read has nothing to learn
1080            // either. Decided before the baseline is cloned: the baseline is
1081            // every prediction the manifest holds.
1082            debug!(
1083                "committing {} changed of {} recorded predictions for {}",
1084                state.changed_predictions.len(),
1085                state.pending_predictions.len(),
1086                state.manifest
1087            );
1088            if state.changed_predictions.is_empty()
1089                && (self.remote.is_none() || !self.remote_mode.writes())
1090            {
1091                let completed = state.pending_predictions.values().cloned().collect();
1092                runs.remove(run);
1093                return Ok(completed);
1094            }
1095            state.clone()
1096        };
1097        let task = state.manifest;
1098        validate_task_identity(&task)?;
1099        let completed = state
1100            .pending_predictions
1101            .values()
1102            .cloned()
1103            .collect::<Vec<_>>();
1104        let (manifest, introduced) = {
1105            let _write_guard = self.manifest_write_lock.lock().unwrap();
1106            let _file_guard = self.lock_task_manifest(&task)?;
1107            let mut predictions = self
1108                .load_task_manifest(&task)?
1109                .map(|manifest| {
1110                    manifest
1111                        .predictions
1112                        .into_iter()
1113                        .map(|prediction| (prediction.invocation.clone(), prediction))
1114                        .collect::<BTreeMap<_, _>>()
1115                })
1116                .unwrap_or_default();
1117            // Which predictions this run adds, as opposed to inherits. Only a
1118            // new one may be withheld for a failed upload: retracting an
1119            // inherited one would un-advertise a result that is plausibly still
1120            // on the server from whichever session put it there.
1121            let introduced: BTreeSet<CacheDigest> = state
1122                .pending_predictions
1123                .keys()
1124                .filter(|invocation| !predictions.contains_key(*invocation))
1125                .cloned()
1126                .collect();
1127            // Only publish predictions recorded by this run. `predictions`
1128            // also contains the baseline loaded by `begin_task`; extending
1129            // with that snapshot would overwrite newer entries committed by
1130            // another agent process after this run began.
1131            predictions.extend(state.pending_predictions);
1132            let manifest = TaskActionManifest {
1133                version: TASK_ACTION_MANIFEST_VERSION,
1134                task: task.clone(),
1135                predictions: predictions.into_values().collect(),
1136            };
1137            validate_task_manifest(&manifest, &task)?;
1138            self.persist_task_manifest(&manifest)?;
1139            (manifest, introduced)
1140        };
1141        self.task_actions.lock().unwrap().remove(run);
1142        if self.remote_mode.writes() {
1143            // A manifest advertises the actions it predicts, so it must not
1144            // reach the remote cache before the results a reader would then go
1145            // looking for -- nor name a result that never got there at all.
1146            let mut manifest = manifest;
1147            if let Some(uploads) = &self.uploads {
1148                let actions: Vec<CacheDigest> = manifest
1149                    .predictions
1150                    .iter()
1151                    .map(|prediction| prediction.action.clone())
1152                    .collect();
1153                let unpublished = uploads.wait_for_actions(&actions).await;
1154                let withheld = manifest
1155                    .predictions
1156                    .iter()
1157                    .filter(|prediction| {
1158                        introduced.contains(&prediction.invocation)
1159                            && unpublished.contains(&prediction.action)
1160                    })
1161                    .count();
1162                if withheld > 0 {
1163                    // The local manifest keeps them: this checkout can still use
1164                    // what it built, and a later session can publish it.
1165                    warn!(
1166                        "{withheld} of {} predicted actions were not published, so the remote task action manifest omits them",
1167                        manifest.predictions.len()
1168                    );
1169                    manifest.predictions.retain(|prediction| {
1170                        !(introduced.contains(&prediction.invocation)
1171                            && unpublished.contains(&prediction.action))
1172                    });
1173                }
1174            }
1175            match self
1176                .put_remote_task_manifest(&task, manifest, state.remote_etag)
1177                .await
1178            {
1179                Ok(remote_manifest) => {
1180                    let _write_guard = self.manifest_write_lock.lock().unwrap();
1181                    let reconciliation = (|| {
1182                        let _file_guard = self.lock_task_manifest(&task)?;
1183                        let manifest = match self.load_task_manifest(&task)? {
1184                            Some(local) => {
1185                                merge_remote_task_manifest(&task, remote_manifest, local).0
1186                            }
1187                            None => remote_manifest,
1188                        };
1189                        self.persist_task_manifest(&manifest)
1190                    })();
1191                    if let Err(error) = reconciliation {
1192                        warn!(
1193                            "remote task action manifest reconciliation failed for {task}: {error}"
1194                        );
1195                    }
1196                }
1197                Err(error) => {
1198                    self.note_remote_failure();
1199                    warn!("remote task action manifest upload failed for {task}: {error}");
1200                }
1201            }
1202        }
1203        Ok(completed)
1204    }
1205
1206    fn task_manifest_path(&self, task: &str) -> PathBuf {
1207        self.manifest_dir.join(format!("{task}.json"))
1208    }
1209
1210    fn task_manifest_lock_path(&self, task: &str) -> PathBuf {
1211        self.manifest_dir.join("locks").join(format!("{task}.lock"))
1212    }
1213
1214    fn lock_task_manifest(&self, task: &str) -> Result<fslock::LockFile> {
1215        let path = self.task_manifest_lock_path(task);
1216        fs::create_dir_all(path.parent().expect("task manifest lock has a parent"))?;
1217        let mut lock = fslock::LockFile::open(&path)?;
1218        lock.lock()?;
1219        Ok(lock)
1220    }
1221
1222    fn load_task_manifest(&self, task: &str) -> Result<Option<TaskActionManifest>> {
1223        match fs::read(self.task_manifest_path(task)) {
1224            Ok(contents) => Ok(Some(self.parse_task_manifest(task, &contents, false)?)),
1225            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
1226            Err(error) => Err(error.into()),
1227        }
1228    }
1229
1230    fn parse_task_manifest(
1231        &self,
1232        task: &str,
1233        contents: &[u8],
1234        require_canonical: bool,
1235    ) -> Result<TaskActionManifest> {
1236        let manifest: TaskActionManifest = serde_json::from_slice(contents)?;
1237        validate_task_manifest(&manifest, task)?;
1238        if require_canonical && canonical_json(&manifest)? != contents {
1239            bail!("task action manifest is not canonical JSON");
1240        }
1241        Ok(manifest)
1242    }
1243
1244    fn task_manifest_selector(task: &str) -> Result<(Vec<u8>, CacheDigest)> {
1245        TaskActionManifest::selector(task)
1246    }
1247
1248    fn persist_task_manifest(&self, manifest: &TaskActionManifest) -> Result<()> {
1249        let bytes = canonical_json(manifest)?;
1250        fs::create_dir_all(self.manifest_dir.as_path())?;
1251        let mut temporary = tempfile::NamedTempFile::new_in(self.manifest_dir.as_path())?;
1252        std::io::Write::write_all(temporary.as_file_mut(), &bytes)?;
1253        temporary.as_file_mut().sync_all()?;
1254        temporary
1255            .persist(self.task_manifest_path(&manifest.task))
1256            .map_err(|error| error.error)?;
1257        Ok(())
1258    }
1259
1260    async fn get_remote_task_manifest(
1261        &self,
1262        task: &str,
1263    ) -> Result<Option<(TaskActionManifest, String)>> {
1264        let Some(remote) = &self.remote else {
1265            return Ok(None);
1266        };
1267        let (_, selector) = Self::task_manifest_selector(task)?;
1268        let _permit = self.remote_transfers.acquire().await?;
1269        self.stats
1270            .remote_manifest_lookups
1271            .fetch_add(1, Ordering::Relaxed);
1272        let _timer = AtomicDurationTimer::start(&self.stats.remote_manifest_lookup_duration_ns);
1273        let Some(remote_manifest) = remote.get_action_manifest(&selector).await? else {
1274            return Ok(None);
1275        };
1276        let manifest = self.parse_task_manifest(task, &remote_manifest.bytes, true)?;
1277        Ok(Some((manifest, remote_manifest.etag)))
1278    }
1279
1280    async fn put_remote_task_manifest(
1281        &self,
1282        task: &str,
1283        mut manifest: TaskActionManifest,
1284        mut expected_etag: Option<String>,
1285    ) -> Result<TaskActionManifest> {
1286        let Some(remote) = &self.remote else {
1287            return Ok(manifest);
1288        };
1289        let (_, selector) = Self::task_manifest_selector(task)?;
1290        for _ in 0..4 {
1291            let bytes = canonical_json(&manifest)?;
1292            let outcome = {
1293                let _permit = self.remote_transfers.acquire().await?;
1294                remote
1295                    .put_action_manifest(&selector, &bytes, expected_etag.as_deref())
1296                    .await?
1297            };
1298            match outcome {
1299                ManifestPutOutcome::Stored => return Ok(manifest),
1300                ManifestPutOutcome::PreconditionFailed => {
1301                    let Some((remote_manifest, etag)) = self.get_remote_task_manifest(task).await?
1302                    else {
1303                        expected_etag = None;
1304                        continue;
1305                    };
1306                    manifest = merge_task_manifests(task, Some(remote_manifest), manifest)?;
1307                    expected_etag = Some(etag);
1308                }
1309            }
1310        }
1311        bail!("remote task action manifest changed too frequently")
1312    }
1313
1314    /// Return a snapshot of this session's cache activity.
1315    pub fn stats(&self) -> AgentStats {
1316        AgentStats {
1317            session_duration_ns: 0,
1318            lookups: self.stats.lookups.load(Ordering::Relaxed),
1319            unconsulted: self.stats.unconsulted.load(Ordering::Relaxed),
1320            hits: self.stats.hits.load(Ordering::Relaxed),
1321            stores: self.stats.stores.load(Ordering::Relaxed),
1322            stored_bytes: self.stats.stored_bytes.load(Ordering::Relaxed),
1323            verifications: self.stats.verifications.load(Ordering::Relaxed),
1324            divergences: self.stats.divergences.load(Ordering::Relaxed),
1325            downloaded_bytes: self.stats.downloaded_bytes.load(Ordering::Relaxed),
1326            uploaded_bytes: self.stats.uploaded_bytes.load(Ordering::Relaxed),
1327            background_uploads: self.stats.background_uploads.load(Ordering::Relaxed),
1328            background_upload_failures: self
1329                .stats
1330                .background_upload_failures
1331                .load(Ordering::Relaxed),
1332            remote_blob_pack_uploads: self.stats.remote_blob_pack_uploads.load(Ordering::Relaxed),
1333            remote_blob_pack_upload_blobs: self
1334                .stats
1335                .remote_blob_pack_upload_blobs
1336                .load(Ordering::Relaxed),
1337            upload_drain_duration_ns: self.stats.upload_drain_duration_ns.load(Ordering::Relaxed),
1338            prefetched_actions: self.stats.prefetched_actions.load(Ordering::Relaxed),
1339            predictions_loaded: self.stats.predictions_loaded.load(Ordering::Relaxed),
1340            bypasses: self.stats.bypasses.lock().unwrap().clone(),
1341            avoided_compiler_duration_ns: self
1342                .stats
1343                .avoided_compiler_duration_ns
1344                .load(Ordering::Relaxed),
1345            wrapper_phases_ns: self.stats.wrapper_phases_ns.lock().unwrap().clone(),
1346            compiler: self.stats.compiler.lock().unwrap().clone(),
1347            slow_compilations: self.stats.slow_compilations.lock().unwrap().clone(),
1348            remote_failures: self.stats.remote_failures.load(Ordering::Relaxed),
1349            remote_manifest_lookups: self.stats.remote_manifest_lookups.load(Ordering::Relaxed),
1350            remote_manifest_lookup_duration_ns: self
1351                .stats
1352                .remote_manifest_lookup_duration_ns
1353                .load(Ordering::Relaxed),
1354            remote_action_lookups: self.stats.remote_action_lookups.load(Ordering::Relaxed),
1355            remote_action_lookup_duration_ns: self
1356                .stats
1357                .remote_action_lookup_duration_ns
1358                .load(Ordering::Relaxed),
1359            remote_blob_requests: self.stats.remote_blob_requests.load(Ordering::Relaxed),
1360            remote_blob_pack_requests: self.stats.remote_blob_pack_requests.load(Ordering::Relaxed),
1361            remote_blob_pack_blobs: self.stats.remote_blob_pack_blobs.load(Ordering::Relaxed),
1362            remote_blob_transfer_duration_ns: self
1363                .stats
1364                .remote_blob_transfer_duration_ns
1365                .load(Ordering::Relaxed),
1366            local_cas_write_duration_ns: self
1367                .stats
1368                .local_cas_write_duration_ns
1369                .load(Ordering::Relaxed),
1370            prefetch_runs: self.stats.prefetch_runs.load(Ordering::Relaxed),
1371            prefetch_duration_ns: self.stats.prefetch_duration_ns.load(Ordering::Relaxed),
1372            materialization_duration_ns: self
1373                .stats
1374                .materialization_duration_ns
1375                .load(Ordering::Relaxed),
1376            restored_output_files: self.stats.restored_output_files.load(Ordering::Relaxed),
1377            restored_output_bytes: self.stats.restored_output_bytes.load(Ordering::Relaxed),
1378            reflinked_output_files: self.stats.reflinked_output_files.load(Ordering::Relaxed),
1379            reflinked_output_bytes: self.stats.reflinked_output_bytes.load(Ordering::Relaxed),
1380            copied_output_files: self.stats.copied_output_files.load(Ordering::Relaxed),
1381            copied_output_bytes: self.stats.copied_output_bytes.load(Ordering::Relaxed),
1382            reused_output_files: self.stats.reused_output_files.load(Ordering::Relaxed),
1383            reused_output_bytes: self.stats.reused_output_bytes.load(Ordering::Relaxed),
1384        }
1385    }
1386
1387    /// Handle requests without a transport connection.
1388    ///
1389    /// Persistent wrappers use this entry point when Cargo invokes mbx outside
1390    /// an orchestrated session. It intentionally has the same response
1391    /// semantics as [`Self::handle_connection`], while leaving framing and the
1392    /// version handshake to callers that actually cross a process boundary.
1393    pub async fn handle_requests(
1394        &self,
1395        requests: impl IntoIterator<Item = AgentRequest>,
1396    ) -> Vec<AgentResponse> {
1397        let mut connection = ConnectionUploads::default();
1398        let mut responses = Vec::new();
1399        for request in requests {
1400            responses.push(self.respond_on(request, &mut connection).await);
1401        }
1402        responses
1403    }
1404
1405    fn write_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1406        Self::digest_lock(&self.write_locks, digest)
1407    }
1408
1409    fn action_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1410        Self::digest_lock(&self.action_locks, digest)
1411    }
1412
1413    fn digest_lock(
1414        locks: &Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>,
1415        digest: &CacheDigest,
1416    ) -> Arc<tokio::sync::Mutex<()>> {
1417        let mut locks = locks.lock().unwrap();
1418        locks.retain(|_, lock| lock.strong_count() > 0);
1419        if let Some(lock) = locks.get(digest).and_then(Weak::upgrade) {
1420            return lock;
1421        }
1422        let lock = Arc::new(tokio::sync::Mutex::new(()));
1423        locks.insert(digest.clone(), Arc::downgrade(&lock));
1424        lock
1425    }
1426
1427    /// Answer one request outside any connection.
1428    ///
1429    /// An upload queued this way has no sibling requests to order against, so
1430    /// the queue falls back to treating every blob it holds as a prerequisite.
1431    #[cfg(test)]
1432    async fn respond(&self, request: AgentRequest) -> AgentResponse {
1433        self.respond_on(request, &mut ConnectionUploads::default())
1434            .await
1435    }
1436
1437    async fn respond_on(
1438        &self,
1439        request: AgentRequest,
1440        connection: &mut ConnectionUploads,
1441    ) -> AgentResponse {
1442        match &request {
1443            AgentRequest::FindActionPrediction { task, .. }
1444            | AgentRequest::RecordActionPrediction { task, .. } => {
1445                self.ensure_task_loaded(task).await;
1446            }
1447            _ => {}
1448        }
1449        let result = match request {
1450            AgentRequest::BeginTask { task } => self
1451                .begin_task(&task)
1452                .await
1453                .map(|run| AgentResponse::TaskBegun { run }),
1454            AgentRequest::CommitTask { run } => self
1455                .commit_task(&run)
1456                .await
1457                .map(|()| AgentResponse::TaskCommitted),
1458            AgentRequest::FindBlob { digest } => self.find_blob(&digest).await,
1459            AgentRequest::FindBlobs { digests } => self.find_blobs(digests).await,
1460            AgentRequest::StoreBlob { digest, source } => {
1461                self.store_blob(&digest, &source, connection).await
1462            }
1463            AgentRequest::FindActionResult { action } => {
1464                self.stats.lookups.fetch_add(1, Ordering::Relaxed);
1465                self.find_action_result(&action).await
1466            }
1467            AgentRequest::RecordActionHit {
1468                action,
1469                restore,
1470                crate_name,
1471            } => {
1472                let diagnostic = connection
1473                    .take_action_diagnostic("hit", crate_name.as_deref())
1474                    .map(|diagnostic| AgentEvent::ActionDiagnostic {
1475                        outcome: "hit".into(),
1476                        crate_name: crate_name.clone(),
1477                        diagnostic,
1478                    });
1479                self.record_action_hit(&action, restore, crate_name, diagnostic)
1480            }
1481            AgentRequest::RecordBypass { kind } => {
1482                *self
1483                    .stats
1484                    .bypasses
1485                    .lock()
1486                    .unwrap()
1487                    .entry(kind.clone())
1488                    .or_insert(0) += 1;
1489                self.emit(|| AgentEvent::Bypass { kind });
1490                Ok(AgentResponse::BypassRecorded)
1491            }
1492            AgentRequest::RecordUnconsulted => {
1493                self.stats.unconsulted.fetch_add(1, Ordering::Relaxed);
1494                self.emit(|| AgentEvent::Unconsulted);
1495                Ok(AgentResponse::UnconsultedRecorded)
1496            }
1497            AgentRequest::RecordWarning { message } => match parse_action_diagnostic(&message) {
1498                Some(Ok((outcome, crate_name, diagnostic))) => {
1499                    connection.record_action_diagnostic(outcome, crate_name, diagnostic);
1500                    Ok(AgentResponse::WarningRecorded)
1501                }
1502                Some(Err(error)) => Err(error),
1503                None => self.record_diagnostic(DiagnosticLevel::Warning, message),
1504            },
1505            AgentRequest::RecordDebug { target, message } => (|| {
1506                if target.is_empty()
1507                    || target.len() > 256
1508                    || target.contains(['\n', '\r', '\0'])
1509                    || message.is_empty()
1510                    || message.len() > MAX_DIAGNOSTIC_BYTES
1511                    || message.contains(['\n', '\r', '\0'])
1512                {
1513                    bail!("invalid shim debug record");
1514                }
1515                debug!(target: &target, "{message}");
1516                Ok(AgentResponse::DebugRecorded)
1517            })(),
1518            AgentRequest::RecordError { message } => {
1519                self.record_diagnostic(DiagnosticLevel::Error, message)
1520            }
1521            AgentRequest::FindFileDigests { scope, files } => self.find_file_digests(scope, files),
1522            AgentRequest::JoinActionPromise {
1523                adapter,
1524                invocation,
1525            } => self.join_action_promise(&adapter, &invocation).await,
1526            AgentRequest::CompleteActionPromise { claim, prediction } => {
1527                self.complete_action_promise(&claim, &prediction).await
1528            }
1529            AgentRequest::ResolveFileDigests { scope, files } => {
1530                self.resolve_file_digests(scope, files).await
1531            }
1532            AgentRequest::RecordFileDigests { scope, entries } => {
1533                self.record_file_digests(scope, entries)
1534            }
1535            AgentRequest::RecordWrapperTiming { timing } => (|| {
1536                if timing.spans.len() > 512
1537                    || timing.phases_ns.len() > 32
1538                    || timing.adapter.len() > 64
1539                    || timing.unit.as_ref().is_some_and(|s| s.len() > 1024)
1540                    || timing.phases_ns.keys().any(|s| s.len() > 64)
1541                    || timing.spans.iter().any(|s| s.name.len() > 64)
1542                {
1543                    bail!("wrapper timing exceeds telemetry limits");
1544                }
1545                {
1546                    let mut phases = self.stats.wrapper_phases_ns.lock().unwrap();
1547                    for (phase, duration) in &timing.phases_ns {
1548                        let total = phases.entry(phase.clone()).or_default();
1549                        *total = total.saturating_add(*duration);
1550                    }
1551                }
1552                self.emit(|| AgentEvent::WrapperTiming { timing });
1553                Ok(AgentResponse::WrapperTimingRecorded)
1554            })(),
1555            AgentRequest::RecordCompilerInvocation {
1556                outcome,
1557                crate_name,
1558                duration_ns,
1559            } => {
1560                let diagnostic = connection
1561                    .take_action_diagnostic(&outcome, crate_name.as_deref())
1562                    .map(|diagnostic| AgentEvent::ActionDiagnostic {
1563                        outcome: outcome.clone(),
1564                        crate_name: crate_name.clone(),
1565                        diagnostic,
1566                    });
1567                self.record_compiler_invocation(
1568                    &outcome,
1569                    crate_name.as_deref(),
1570                    duration_ns,
1571                    diagnostic,
1572                )
1573            }
1574            AgentRequest::RecordActionVerification { matched, restore } => {
1575                self.record_materialization(restore);
1576                self.stats.verifications.fetch_add(1, Ordering::Relaxed);
1577                if !matched {
1578                    self.stats.divergences.fetch_add(1, Ordering::Relaxed);
1579                }
1580                self.emit(|| AgentEvent::Verification { matched, restore });
1581                Ok(AgentResponse::ActionVerificationRecorded)
1582            }
1583            AgentRequest::StoreActionResult { result } => {
1584                self.store_action_result(&result, connection).await
1585            }
1586            AgentRequest::FindActionPrediction { task, invocation } => {
1587                self.find_action_prediction(&task, &invocation)
1588            }
1589            AgentRequest::RecordActionPrediction { task, prediction } => {
1590                self.record_action_prediction(&task, prediction)
1591            }
1592            AgentRequest::FindExecutableIdentity {
1593                executable,
1594                environment,
1595            } => self.find_executable_identity(executable, environment),
1596            AgentRequest::StoreExecutableIdentity {
1597                executable,
1598                environment,
1599                stdout,
1600                pins,
1601            } => self.store_executable_identity(executable, environment, stdout, pins),
1602            AgentRequest::Hello { .. } => {
1603                Err(eyre::eyre!("hello is only valid as the first request"))
1604            }
1605        };
1606        result.unwrap_or_else(|error| AgentResponse::Error {
1607            message: error.to_string(),
1608        })
1609    }
1610
1611    async fn find_blob(&self, digest: &CacheDigest) -> Result<AgentResponse> {
1612        if let Some(path) = self.find_verified_blob(digest)? {
1613            return Ok(AgentResponse::Blob { path: Some(path) });
1614        }
1615        if !self.remote_mode.reads() {
1616            return Ok(AgentResponse::Blob { path: None });
1617        }
1618        let Some(remote) = &self.remote else {
1619            return Ok(AgentResponse::Blob { path: None });
1620        };
1621        match self.fetch_remote_blob(remote, digest).await {
1622            Ok(path) => Ok(AgentResponse::Blob { path: Some(path) }),
1623            Err(error) => {
1624                warn!(
1625                    "remote cache blob lookup failed for {}: {error}",
1626                    digest.hash
1627                );
1628                Ok(AgentResponse::Blob { path: None })
1629            }
1630        }
1631    }
1632
1633    async fn find_blobs(&self, digests: Vec<CacheDigest>) -> Result<AgentResponse> {
1634        let mut paths = BTreeMap::new();
1635        let mut missing = Vec::new();
1636        for digest in &digests {
1637            match self.find_verified_blob(digest)? {
1638                Some(path) => {
1639                    paths.insert(digest.clone(), path);
1640                }
1641                None => {
1642                    missing.push(digest.clone());
1643                }
1644            }
1645        }
1646
1647        if !missing.is_empty()
1648            && self.remote_mode.reads()
1649            && let Some(remote) = &self.remote
1650        {
1651            paths.extend(self.fetch_remote_blobs(remote, missing, None).await);
1652        }
1653
1654        Ok(AgentResponse::Blobs {
1655            paths: digests
1656                .into_iter()
1657                .map(|digest| paths.get(&digest).cloned())
1658                .collect(),
1659        })
1660    }
1661
1662    async fn store_blob(
1663        &self,
1664        digest: &CacheDigest,
1665        source: &Path,
1666        connection: &mut ConnectionUploads,
1667    ) -> Result<AgentResponse> {
1668        let path = {
1669            let lock = self.write_lock(digest);
1670            let _guard = lock.lock().await;
1671            if let Some(path) = self.find_verified_blob(digest)? {
1672                path
1673            } else {
1674                let path = self.cas.store_file(digest, source)?;
1675                self.remember_verified_blob(digest, &path);
1676                self.stats.stores.fetch_add(1, Ordering::Relaxed);
1677                self.stats
1678                    .stored_bytes
1679                    .fetch_add(digest.size, Ordering::Relaxed);
1680                path
1681            }
1682        };
1683        // The object is durable locally, so the build has what it needs and the
1684        // remote publication can happen after this request returns.
1685        if let Some(uploads) = &self.uploads {
1686            uploads.queue_blob(digest, path.clone(), connection);
1687        }
1688        Ok(AgentResponse::Stored { path })
1689    }
1690
1691    fn find_verified_blob(&self, digest: &CacheDigest) -> Result<Option<PathBuf>> {
1692        let remembered = self.verified_blobs.lock().unwrap().get(digest).cloned();
1693        if let Some(remembered) = remembered {
1694            // The contents behind this digest were hashed in full when the
1695            // session first reached for them. Hashing again on every later
1696            // lookup would re-read each dependency's rlib once per crate that
1697            // links it, so what is rechecked here is that the file has not
1698            // been written since: an overwrite moves the modification time,
1699            // and truncation or eviction changes the length or removes it.
1700            // Only a replacement that reproduces both -- which no writer in
1701            // the store does, since blobs are published by rename under a
1702            // content-derived name -- would go unnoticed until `mbx cache
1703            // verify` reads the store back in full.
1704            if remembered.is_unchanged() {
1705                return Ok(Some(remembered.path));
1706            }
1707            self.verified_blobs.lock().unwrap().remove(digest);
1708        }
1709        let path = self.cas.find(digest)?;
1710        if let Some(path) = &path {
1711            self.remember_verified_blob(digest, path);
1712        }
1713        Ok(path)
1714    }
1715
1716    fn remember_verified_blob(&self, digest: &CacheDigest, path: &Path) {
1717        let Some(verified) = VerifiedBlob::describe(path) else {
1718            // Without an identity to compare against there is nothing to
1719            // shortcut safely, so the digest stays unremembered and every
1720            // lookup re-reads it.
1721            return;
1722        };
1723        self.verified_blobs
1724            .lock()
1725            .unwrap()
1726            .insert(digest.clone(), verified);
1727    }
1728
1729    async fn find_action_result(&self, action: &CacheDigest) -> Result<AgentResponse> {
1730        if let Some(result) = self.actions.find(action)? {
1731            return Ok(AgentResponse::ActionResult {
1732                result: Some(result),
1733            });
1734        }
1735        if !self.remote_mode.reads() {
1736            return Ok(AgentResponse::ActionResult { result: None });
1737        }
1738        let Some(remote) = &self.remote else {
1739            return Ok(AgentResponse::ActionResult { result: None });
1740        };
1741        let lock = self.action_lock(action);
1742        let _guard = lock.lock().await;
1743        if let Some(result) = self.actions.find(action)? {
1744            return Ok(AgentResponse::ActionResult {
1745                result: Some(result),
1746            });
1747        }
1748        if let Some(result) = self
1749            .pending_remote_actions
1750            .lock()
1751            .unwrap()
1752            .get(action)
1753            .cloned()
1754        {
1755            return Ok(AgentResponse::ActionResult {
1756                result: Some(result),
1757            });
1758        }
1759        let _permit = self.remote_transfers.acquire().await?;
1760        match self.get_remote_action_result(remote, action).await {
1761            Ok(Some(result)) => {
1762                self.pending_remote_actions
1763                    .lock()
1764                    .unwrap()
1765                    .insert(action.clone(), result.clone());
1766                Ok(AgentResponse::ActionResult {
1767                    result: Some(result),
1768                })
1769            }
1770            Ok(None) => Ok(AgentResponse::ActionResult { result: None }),
1771            Err(error) => {
1772                self.note_remote_failure();
1773                warn!(
1774                    "remote cache action lookup failed for {}: {error}",
1775                    action.hash
1776                );
1777                Ok(AgentResponse::ActionResult { result: None })
1778            }
1779        }
1780    }
1781
1782    async fn store_action_result(
1783        &self,
1784        result: &RemoteActionResult,
1785        connection: &ConnectionUploads,
1786    ) -> Result<AgentResponse> {
1787        let path = self.actions.store(result)?;
1788        if let Some(uploads) = &self.uploads {
1789            uploads.queue_action_result(result, connection);
1790        }
1791        Ok(AgentResponse::ActionStored { path })
1792    }
1793
1794    async fn join_action_promise(
1795        &self,
1796        adapter: &str,
1797        invocation: &CacheDigest,
1798    ) -> Result<AgentResponse> {
1799        if !self.remote_mode.reads() || !self.remote_mode.writes() {
1800            return Ok(AgentResponse::ActionPromise {
1801                claim: None,
1802                prediction: None,
1803            });
1804        }
1805        let Some(remote) = &self.remote else {
1806            return Ok(AgentResponse::ActionPromise {
1807                claim: None,
1808                prediction: None,
1809            });
1810        };
1811        let deadline = Instant::now() + ACTION_PROMISE_WAIT;
1812        loop {
1813            let _permit = self.remote_transfers.acquire().await?;
1814            let state = remote.join_action_promise(invocation, adapter).await;
1815            drop(_permit);
1816            match state {
1817                Ok(Some(ActionPromiseState::Claimed { claim })) => {
1818                    return Ok(AgentResponse::ActionPromise {
1819                        claim: Some(claim),
1820                        prediction: None,
1821                    });
1822                }
1823                Ok(Some(ActionPromiseState::Complete { prediction })) => {
1824                    return Ok(AgentResponse::ActionPromise {
1825                        claim: None,
1826                        prediction: Some(prediction),
1827                    });
1828                }
1829                Ok(Some(ActionPromiseState::Pending { retry_after_ms }))
1830                    if Instant::now() < deadline =>
1831                {
1832                    tokio::time::sleep(Duration::from_millis(retry_after_ms.clamp(10, 5_000)))
1833                        .await;
1834                }
1835                Ok(Some(ActionPromiseState::Pending { .. }) | None) => {
1836                    return Ok(AgentResponse::ActionPromise {
1837                        claim: None,
1838                        prediction: None,
1839                    });
1840                }
1841                Ok(Some(_)) => {
1842                    return Ok(AgentResponse::ActionPromise {
1843                        claim: None,
1844                        prediction: None,
1845                    });
1846                }
1847                Err(error) => {
1848                    self.note_remote_failure();
1849                    warn!(
1850                        "remote cache action promise failed for {}: {error}",
1851                        invocation.hash
1852                    );
1853                    return Ok(AgentResponse::ActionPromise {
1854                        claim: None,
1855                        prediction: None,
1856                    });
1857                }
1858            }
1859        }
1860    }
1861
1862    async fn complete_action_promise(
1863        &self,
1864        claim: &str,
1865        prediction: &ActionPrediction,
1866    ) -> Result<AgentResponse> {
1867        prediction.validate()?;
1868        let Some(remote) = &self.remote else {
1869            return Ok(AgentResponse::ActionPromiseCompleted);
1870        };
1871        let Some(uploads) = &self.uploads else {
1872            return Ok(AgentResponse::ActionPromiseCompleted);
1873        };
1874        if uploads
1875            .wait_for_actions(std::slice::from_ref(&prediction.action))
1876            .await
1877            .contains(&prediction.action)
1878        {
1879            // Never promise an action result the server does not hold. The
1880            // server lease expires and another runner gets to repair it.
1881            return Ok(AgentResponse::ActionPromiseCompleted);
1882        }
1883        let completion = ActionPromiseCompletion {
1884            claim: claim.to_string(),
1885            prediction: prediction.clone(),
1886        };
1887        let _permit = self.remote_transfers.acquire().await?;
1888        if let Err(error) = remote
1889            .complete_action_promise(&prediction.invocation, &completion)
1890            .await
1891        {
1892            self.note_remote_failure();
1893            warn!(
1894                "remote cache action promise completion failed for {}: {error}",
1895                prediction.invocation.hash
1896            );
1897        }
1898        Ok(AgentResponse::ActionPromiseCompleted)
1899    }
1900
1901    /// Record that a remote operation failed and the build carried on without it.
1902    fn note_remote_failure(&self) {
1903        self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
1904    }
1905
1906    async fn get_remote_action_result(
1907        &self,
1908        remote: &RemoteCacheClient,
1909        action: &CacheDigest,
1910    ) -> Result<Option<RemoteActionResult>> {
1911        self.stats
1912            .remote_action_lookups
1913            .fetch_add(1, Ordering::Relaxed);
1914        let _timer = AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);
1915        remote.get_action_result(action).await
1916    }
1917
1918    fn record_action_hit(
1919        &self,
1920        action: &CacheDigest,
1921        restore: RestoreStats,
1922        crate_name: Option<String>,
1923        diagnostic: Option<AgentEvent>,
1924    ) -> Result<AgentResponse> {
1925        validate_crate_name(crate_name.as_deref())?;
1926        if self.actions.find(action)?.is_none() {
1927            let pending = self.pending_remote_actions.lock().unwrap().remove(action);
1928            if let Some(result) = pending {
1929                self.actions.store(&result)?;
1930            } else {
1931                bail!("cannot record a hit for a missing action result");
1932            }
1933        }
1934        self.record_restore(restore);
1935        self.stats.hits.fetch_add(1, Ordering::Relaxed);
1936        self.emit_action(
1937            diagnostic,
1938            AgentEvent::ActionHit {
1939                crate_name,
1940                restore,
1941            },
1942        );
1943        Ok(AgentResponse::ActionHitRecorded)
1944    }
1945
1946    fn record_restore(&self, restore: RestoreStats) {
1947        self.record_materialization(restore);
1948        atomic_saturating_add(
1949            &self.stats.avoided_compiler_duration_ns,
1950            restore.avoided_compiler_duration_ns,
1951        );
1952        atomic_saturating_add(&self.stats.restored_output_files, restore.output_files);
1953        atomic_saturating_add(&self.stats.restored_output_bytes, restore.output_bytes);
1954        atomic_saturating_add(
1955            &self.stats.reflinked_output_files,
1956            restore.reflinked_output_files,
1957        );
1958        atomic_saturating_add(
1959            &self.stats.reflinked_output_bytes,
1960            restore.reflinked_output_bytes,
1961        );
1962        atomic_saturating_add(&self.stats.copied_output_files, restore.copied_output_files);
1963        atomic_saturating_add(&self.stats.copied_output_bytes, restore.copied_output_bytes);
1964        atomic_saturating_add(&self.stats.reused_output_files, restore.reused_output_files);
1965        atomic_saturating_add(&self.stats.reused_output_bytes, restore.reused_output_bytes);
1966    }
1967
1968    fn record_compiler_invocation(
1969        &self,
1970        outcome: &str,
1971        crate_name: Option<&str>,
1972        duration_ns: u64,
1973        diagnostic: Option<AgentEvent>,
1974    ) -> Result<AgentResponse> {
1975        if !matches!(
1976            outcome,
1977            "miss" | "unconsulted" | "bypass" | "verification" | "incremental"
1978        ) {
1979            bail!("invalid compiler invocation outcome");
1980        }
1981        validate_crate_name(crate_name)?;
1982        let mut compiler = self.stats.compiler.lock().unwrap();
1983        let stats = compiler.entry(outcome.to_string()).or_default();
1984        stats.invocations = stats.invocations.saturating_add(1);
1985        stats.duration_ns = stats.duration_ns.saturating_add(duration_ns);
1986        drop(compiler);
1987        if outcome != "verification"
1988            && let Some(crate_name) = crate_name.filter(|name| !name.is_empty())
1989        {
1990            let mut slow = self.stats.slow_compilations.lock().unwrap();
1991            let duration = slow.entry(crate_name.to_string()).or_default();
1992            *duration = duration.saturating_add(duration_ns);
1993        }
1994        self.emit_action(
1995            diagnostic,
1996            AgentEvent::CompilerInvocation {
1997                outcome: outcome.to_string(),
1998                crate_name: crate_name.map(str::to_string),
1999                duration_ns,
2000            },
2001        );
2002        Ok(AgentResponse::CompilerInvocationRecorded)
2003    }
2004
2005    fn record_materialization(&self, restore: RestoreStats) {
2006        atomic_saturating_add(&self.stats.materialization_duration_ns, restore.duration_ns);
2007    }
2008
2009    /// Surface a shim diagnostic on this process's stderr.
2010    ///
2011    /// The agent lives in the process that owns the session, so printing here
2012    /// reaches the terminal running the build rather than the stderr of the
2013    /// compilation the shim stands in for -- which build scripts read as part
2014    /// of the compiler's answer. Deduplicated because one cause tends to fire
2015    /// once per compilation, and capped so a message unique per compilation
2016    /// cannot scroll the build away. The level is part of the deduplication
2017    /// key, so a fatal reason is never hidden by an earlier warning with the
2018    /// same text.
2019    fn record_diagnostic(&self, level: DiagnosticLevel, message: String) -> Result<AgentResponse> {
2020        if message.is_empty()
2021            || message.len() > MAX_DIAGNOSTIC_BYTES
2022            || message.contains(['\n', '\r', '\0'])
2023        {
2024            bail!("invalid shim {}", level.label());
2025        }
2026        let mut diagnostics = self.diagnostics.lock().unwrap();
2027        let key = (level, message.clone());
2028        if !diagnostics.contains(&key) {
2029            // An acknowledgement promises the diagnostic was surfaced. A
2030            // fatal shim error must be able to fall back locally when this
2031            // channel can no longer display it.
2032            if diagnostics.len() >= MAX_DIAGNOSTICS {
2033                bail!("shim diagnostic limit reached");
2034            }
2035            eprintln!("mbx[{}]: {message}", level.label());
2036            self.emit(|| match level {
2037                DiagnosticLevel::Warning => AgentEvent::Warning {
2038                    message: message.clone(),
2039                },
2040                DiagnosticLevel::Error => AgentEvent::Error {
2041                    message: message.clone(),
2042                },
2043            });
2044            diagnostics.insert(key);
2045        }
2046        Ok(match level {
2047            DiagnosticLevel::Warning => AgentResponse::WarningRecorded,
2048            DiagnosticLevel::Error => AgentResponse::ErrorRecorded,
2049        })
2050    }
2051
2052    /// Answer file-digest lookups from the session ledger.
2053    ///
2054    /// A recorded digest is returned only when the requested identity matches
2055    /// the recorded one exactly. The caller statted the file to build the
2056    /// identity, so this is the [`VerifiedBlob`] freshness check with the
2057    /// stat moved to the side that was already making it.
2058    fn find_file_digests(
2059        &self,
2060        scope: FileDigestScope,
2061        files: Vec<FileIdentity>,
2062    ) -> Result<AgentResponse> {
2063        if files.len() > MAX_FILE_DIGEST_BATCH {
2064            bail!("too many file-digest lookups in one request");
2065        }
2066        let ledger = self.file_digests.lock().unwrap();
2067        let digests = files
2068            .into_iter()
2069            .map(|file| {
2070                let recorded = ledger.get(&(scope, file.path.clone()))?;
2071                (recorded.file == file).then(|| recorded.digest.clone())
2072            })
2073            .collect();
2074        Ok(AgentResponse::FileDigests { digests })
2075    }
2076
2077    /// Resolve ledger misses inside the agent so concurrent shims that name the
2078    /// same NFS object wait for and reuse one read instead of stampeding it.
2079    async fn resolve_file_digests(
2080        &self,
2081        scope: FileDigestScope,
2082        files: Vec<FileIdentity>,
2083    ) -> Result<AgentResponse> {
2084        if files.len() > MAX_FILE_DIGEST_BATCH {
2085            bail!("too many file-digest resolutions in one request");
2086        }
2087        for file in &files {
2088            if !file.path.is_absolute() {
2089                bail!("file-digest resolutions need absolute paths");
2090            }
2091        }
2092        let resolutions = stream::iter(
2093            files
2094                .into_iter()
2095                .map(|file| async move { self.resolve_file_digest(scope, file).await }),
2096        )
2097        .buffered(MAX_CONCURRENT_FILE_DIGESTS)
2098        .collect()
2099        .await;
2100        Ok(AgentResponse::FileDigestsResolved { resolutions })
2101    }
2102
2103    async fn resolve_file_digest(
2104        &self,
2105        scope: FileDigestScope,
2106        file: FileIdentity,
2107    ) -> FileDigestResolution {
2108        let lock = {
2109            let key = (scope, file.clone());
2110            let mut locks = self.file_digest_locks.lock().unwrap();
2111            locks.retain(|_, lock| lock.strong_count() > 0);
2112            if let Some(lock) = locks.get(&key).and_then(Weak::upgrade) {
2113                lock
2114            } else {
2115                let lock = Arc::new(FileDigestFlight {
2116                    lock: tokio::sync::Mutex::new(()),
2117                    resolution: Mutex::new(None),
2118                });
2119                locks.insert(key, Arc::downgrade(&lock));
2120                lock
2121            }
2122        };
2123        let _flight = lock.lock.lock().await;
2124        if let Some(resolution) = lock.resolution.lock().unwrap().clone() {
2125            return resolution;
2126        }
2127        if let Some(digest) = self
2128            .file_digests
2129            .lock()
2130            .unwrap()
2131            .get(&(scope, file.path.clone()))
2132            .filter(|recorded| recorded.file == file)
2133            .map(|recorded| recorded.digest.clone())
2134        {
2135            return FileDigestResolution::Digest(digest);
2136        }
2137        let Ok(_permit) = self.file_digest_permits.acquire().await else {
2138            return FileDigestResolution::Unresolved;
2139        };
2140        #[cfg(test)]
2141        self.file_digest_reads.fetch_add(1, Ordering::Relaxed);
2142        let path = file.path.clone();
2143        let resolved = tokio::task::spawn_blocking(move || {
2144            let resolution = digest_file(scope, &path)?;
2145            let current = std::fs::metadata(&path)
2146                .and_then(|metadata| FileIdentity::for_digest_cache(&path, &metadata));
2147            Ok::<_, std::io::Error>((resolution, current?))
2148        })
2149        .await;
2150        let Ok(Ok((resolution, current))) = resolved else {
2151            return FileDigestResolution::Unresolved;
2152        };
2153        let resolution = match resolution {
2154            FileDigestResolution::Digest(digest)
2155                if current.as_ref() == Some(&file) && digest.size == file.len =>
2156            {
2157                let _ = self.record_file_digests(
2158                    scope,
2159                    vec![RecordedFileDigest {
2160                        file: file.clone(),
2161                        digest: digest.clone(),
2162                    }],
2163                );
2164                FileDigestResolution::Digest(digest)
2165            }
2166            FileDigestResolution::EmbeddedTimestampMacro if current.as_ref() == Some(&file) => {
2167                FileDigestResolution::EmbeddedTimestampMacro
2168            }
2169            FileDigestResolution::Digest(_)
2170            | FileDigestResolution::EmbeddedTimestampMacro
2171            | FileDigestResolution::Unresolved => FileDigestResolution::Unresolved,
2172        };
2173        *lock.resolution.lock().unwrap() = Some(resolution.clone());
2174        resolution
2175    }
2176
2177    /// Record digests of files a shim read in full, for later reuse.
2178    ///
2179    /// Capped rather than evicted: a session that outgrows the cap loses the
2180    /// shortcut for the overflow and nothing else, and no workload observed so
2181    /// far comes near it.
2182    fn record_file_digests(
2183        &self,
2184        scope: FileDigestScope,
2185        entries: Vec<RecordedFileDigest>,
2186    ) -> Result<AgentResponse> {
2187        if entries.len() > MAX_FILE_DIGEST_BATCH {
2188            bail!("too many file-digest records in one request");
2189        }
2190        for entry in &entries {
2191            validate_file_digest_record(entry)?;
2192        }
2193        let mut ledger = self.file_digests.lock().unwrap();
2194        for entry in entries {
2195            if ledger.len() >= MAX_FILE_DIGEST_ENTRIES
2196                && !ledger.contains_key(&(scope, entry.file.path.clone()))
2197            {
2198                break;
2199            }
2200            ledger.insert((scope, entry.file.path.clone()), entry);
2201        }
2202        Ok(AgentResponse::FileDigestsRecorded)
2203    }
2204
2205    /// Start the file-digest ledger from entries an earlier session left
2206    /// behind, so a file nothing has touched since is not read again by the
2207    /// first compilation of this session that names it.
2208    ///
2209    /// Each entry stands only while its recorded identity still matches the
2210    /// disk, the same rule a lookup applies to what this session recorded, so
2211    /// a stale seed costs a hash and nothing else. Entries this session has
2212    /// already recorded are kept over seeded ones. Returns how many were taken.
2213    pub fn seed_file_digests(&self, entries: Vec<(FileDigestScope, RecordedFileDigest)>) -> usize {
2214        self.seed_file_digests_with(|| entries)
2215    }
2216
2217    /// Seed the ledger from entries produced under its lock.
2218    ///
2219    /// A lookup that arrives while `load` is still reading waits for it rather
2220    /// than missing, so a caller can start the read in the background and let
2221    /// whatever else the build is doing overlap it.
2222    pub fn seed_file_digests_with(
2223        &self,
2224        load: impl FnOnce() -> Vec<(FileDigestScope, RecordedFileDigest)>,
2225    ) -> usize {
2226        let mut ledger = self.file_digests.lock().unwrap();
2227        let entries = load();
2228        let mut seeded = 0;
2229        for (scope, entry) in entries {
2230            if ledger.len() >= MAX_FILE_DIGEST_ENTRIES {
2231                break;
2232            }
2233            if validate_file_digest_record(&entry).is_err() {
2234                continue;
2235            }
2236            if let std::collections::btree_map::Entry::Vacant(slot) =
2237                ledger.entry((scope, entry.file.path.clone()))
2238            {
2239                slot.insert(entry);
2240                seeded += 1;
2241            }
2242        }
2243        seeded
2244    }
2245
2246    /// Everything the file-digest ledger holds, for a later session to start
2247    /// from.
2248    pub fn file_digests(&self) -> Vec<(FileDigestScope, RecordedFileDigest)> {
2249        self.file_digests
2250            .lock()
2251            .unwrap()
2252            .iter()
2253            .map(|((scope, _), entry)| (*scope, entry.clone()))
2254            .collect()
2255    }
2256
2257    fn find_action_prediction(
2258        &self,
2259        task: &str,
2260        invocation: &CacheDigest,
2261    ) -> Result<AgentResponse> {
2262        validate_task_identity(task)?;
2263        invocation.validate()?;
2264        let (prediction, prefetch) = {
2265            let mut tasks = self.task_actions.lock().unwrap();
2266            let Some(state) = tasks.get_mut(task) else {
2267                return Ok(AgentResponse::ActionPrediction { prediction: None });
2268            };
2269            activate_prediction_adapter(state, invocation)
2270        };
2271        if let Some(prefetch) = prefetch {
2272            self.spawn_prefetch_predictions(prefetch);
2273        }
2274        Ok(AgentResponse::ActionPrediction { prediction })
2275    }
2276
2277    fn record_action_prediction(
2278        &self,
2279        task: &str,
2280        prediction: ActionPrediction,
2281    ) -> Result<AgentResponse> {
2282        validate_task_identity(task)?;
2283        prediction.validate()?;
2284        let mut tasks = self.task_actions.lock().unwrap();
2285        let state = tasks.entry(task.to_string()).or_default();
2286        if !state.predictions.contains_key(&prediction.invocation)
2287            && state.predictions.len() >= MAX_TASK_ACTION_PREDICTIONS
2288        {
2289            bail!("task action manifest contains too many predictions");
2290        }
2291        let current = state.predictions.get(&prediction.invocation);
2292        if current != Some(&prediction) {
2293            match state.changed_predictions.get(&prediction.invocation) {
2294                // Back to what the manifest held: no longer a change.
2295                Some(baseline) if baseline.as_ref() == Some(&prediction) => {
2296                    state.changed_predictions.remove(&prediction.invocation);
2297                }
2298                // Changed again; the baseline it left is already recorded.
2299                Some(_) => {}
2300                None => {
2301                    state
2302                        .changed_predictions
2303                        .insert(prediction.invocation.clone(), current.cloned());
2304                }
2305            }
2306        }
2307        state
2308            .predictions
2309            .insert(prediction.invocation.clone(), prediction.clone());
2310        state
2311            .pending_predictions
2312            .insert(prediction.invocation.clone(), prediction);
2313        Ok(AgentResponse::ActionPredictionRecorded)
2314    }
2315
2316    fn executable_identity_key(
2317        &self,
2318        executable: PathBuf,
2319        environment: BTreeMap<String, Option<String>>,
2320    ) -> Result<ExecutableIdentityKey> {
2321        // Restricted to the variables that actually select what an identity
2322        // probe reports: the toolchain rustup resolves, the SDK a linker
2323        // driver builds against, a `-fuse-ld` linker selection, and the
2324        // search path a driver finds its linker on. Anything else would let
2325        // one key stand for two different compilers.
2326        if !environment.keys().all(|name| {
2327            matches!(
2328                name.as_str(),
2329                "COMPILER_PATH"
2330                    | "GCC_EXEC_PREFIX"
2331                    | "LIBRARY_PATH"
2332                    | "MBX_FUSE_LD"
2333                    | "PATH"
2334                    | "RUSTUP_HOME"
2335                    | "RUSTUP_TOOLCHAIN"
2336                    | "SDKROOT"
2337                    | "MACOSX_DEPLOYMENT_TARGET"
2338                    | "LIB"
2339                    | "UCRTVersion"
2340                    | "UniversalCRTSdkDir"
2341                    | "VCToolsInstallDir"
2342                    | "VCToolsVersion"
2343                    | "WindowsSdkDir"
2344                    | "WindowsSDKVersion"
2345            )
2346        }) {
2347            bail!("executable identity contains an unsupported environment variable");
2348        }
2349        Ok(ExecutableIdentityKey {
2350            executable,
2351            environment,
2352        })
2353    }
2354
2355    fn find_executable_identity(
2356        &self,
2357        executable: PathBuf,
2358        environment: BTreeMap<String, Option<String>>,
2359    ) -> Result<AgentResponse> {
2360        let key = self.executable_identity_key(executable, environment)?;
2361        let remembered = self
2362            .executable_identities
2363            .lock()
2364            .unwrap()
2365            .get(&key)
2366            .cloned();
2367        let stdout = match remembered {
2368            Some(stdout) => Some(stdout),
2369            None => {
2370                let persisted = self.load_persisted_identity(&key);
2371                if let Some(stdout) = &persisted {
2372                    // Memoized like a fresh probe: the pins were checked once
2373                    // for this session, which is as often as a probe runs.
2374                    let _ = self.remember_executable_identity(&key, stdout.clone());
2375                }
2376                persisted
2377            }
2378        };
2379        Ok(AgentResponse::ExecutableIdentity { stdout })
2380    }
2381
2382    fn store_executable_identity(
2383        &self,
2384        executable: PathBuf,
2385        environment: BTreeMap<String, Option<String>>,
2386        stdout: Vec<u8>,
2387        pins: Vec<PinnedFile>,
2388    ) -> Result<AgentResponse> {
2389        if stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE {
2390            bail!("executable identity exceeds {MAX_EXECUTABLE_IDENTITY_SIZE} bytes");
2391        }
2392        let key = self.executable_identity_key(executable, environment)?;
2393        self.remember_executable_identity(&key, stdout.clone())?;
2394        // Best-effort, like the ledger: a session that cannot write it probes
2395        // again next time, which is what it would have done anyway. A pin
2396        // that no longer holds means a file moved under the probe, and what
2397        // it printed describes neither the old file nor the new one.
2398        if pins.is_empty() {
2399        } else if !pins.iter().all(PinnedFile::holds) {
2400            debug!("executable identity was not persisted: a pinned file changed during the probe");
2401        } else if let Err(error) = self.persist_executable_identity(&key, pins, &stdout) {
2402            debug!("executable identity was not persisted: {error:#}");
2403        }
2404        Ok(AgentResponse::ExecutableIdentity {
2405            stdout: Some(stdout),
2406        })
2407    }
2408
2409    fn remember_executable_identity(
2410        &self,
2411        key: &ExecutableIdentityKey,
2412        stdout: Vec<u8>,
2413    ) -> Result<()> {
2414        let mut identities = self.executable_identities.lock().unwrap();
2415        let is_new = !identities.contains_key(key);
2416        let previous_size = identities.get(key).map_or(0, Vec::len);
2417        if is_new && identities.len() >= MAX_EXECUTABLE_IDENTITIES {
2418            bail!("executable identity cache contains too many entries");
2419        }
2420        let retained_bytes = identities.values().map(Vec::len).sum::<usize>();
2421        if retained_bytes - previous_size + stdout.len() > MAX_EXECUTABLE_IDENTITY_BYTES {
2422            bail!("executable identity cache contains too many bytes");
2423        }
2424        identities.insert(key.clone(), stdout);
2425        Ok(())
2426    }
2427
2428    fn persisted_identity_path(&self, key: &ExecutableIdentityKey) -> Result<PathBuf> {
2429        let selector = serde_json::to_vec(&(&key.executable, &key.environment))?;
2430        let digest = CacheDigest::blake3(&selector);
2431        Ok(self.identity_dir.join(format!("{}.json", digest.hash)))
2432    }
2433
2434    /// What an earlier session recorded for `key`, if every file it was
2435    /// pinned to is still as that session saw it.
2436    fn load_persisted_identity(&self, key: &ExecutableIdentityKey) -> Option<Vec<u8>> {
2437        let path = self.persisted_identity_path(key).ok()?;
2438        let bytes = fs::read(path).ok()?;
2439        let persisted: PersistedExecutableIdentity = serde_json::from_slice(&bytes).ok()?;
2440        if persisted.version != PERSISTED_EXECUTABLE_IDENTITY_VERSION
2441            || persisted.executable != key.executable
2442            || persisted.environment != key.environment
2443            || persisted.pins.is_empty()
2444            || persisted.stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE
2445        {
2446            return None;
2447        }
2448        persisted
2449            .pins
2450            .iter()
2451            .all(PinnedFile::holds)
2452            .then(|| persisted.stdout.into_bytes())
2453    }
2454
2455    fn persist_executable_identity(
2456        &self,
2457        key: &ExecutableIdentityKey,
2458        pins: Vec<PinnedFile>,
2459        stdout: &[u8],
2460    ) -> Result<()> {
2461        // An identity that is not text is kept for this session only.
2462        let Ok(stdout) = std::str::from_utf8(stdout) else {
2463            return Ok(());
2464        };
2465        let persisted = PersistedExecutableIdentity {
2466            version: PERSISTED_EXECUTABLE_IDENTITY_VERSION,
2467            executable: key.executable.clone(),
2468            environment: key.environment.clone(),
2469            pins,
2470            stdout: stdout.to_owned(),
2471        };
2472        let path = self.persisted_identity_path(key)?;
2473        fs::create_dir_all(self.identity_dir.as_path())?;
2474        // Not synced: a torn record reads as no record, and costs one probe.
2475        let mut temporary = tempfile::NamedTempFile::new_in(self.identity_dir.as_path())?;
2476        std::io::Write::write_all(temporary.as_file_mut(), &serde_json::to_vec(&persisted)?)?;
2477        temporary.persist(path).map_err(|error| error.error)?;
2478        Ok(())
2479    }
2480
2481    /// Serve newline-delimited protocol requests on an authenticated session stream.
2482    pub async fn handle_connection<S>(&self, stream: S) -> Result<()>
2483    where
2484        S: AsyncRead + AsyncWrite + Unpin,
2485    {
2486        let (reader, mut writer) = tokio::io::split(stream);
2487        let mut reader = BufReader::new(reader);
2488        let hello = read_request(&mut reader)
2489            .await?
2490            .ok_or_else(|| eyre::eyre!("connection closed before the agent handshake"))?;
2491        let request: AgentRequest = serde_json::from_str(&hello)?;
2492        match request {
2493            AgentRequest::Hello {
2494                protocol,
2495                client_version,
2496            } if protocol == AGENT_PROTOCOL_VERSION && client_version == self.version.as_ref() => {}
2497            AgentRequest::Hello { protocol, .. } if protocol != AGENT_PROTOCOL_VERSION => {
2498                send_response(
2499                    &mut writer,
2500                    &AgentResponse::Error {
2501                        message: format!(
2502                            "unsupported agent protocol {protocol}; expected {AGENT_PROTOCOL_VERSION}"
2503                        ),
2504                    },
2505                )
2506                .await?;
2507                return Ok(());
2508            }
2509            AgentRequest::Hello { client_version, .. } => {
2510                send_response(
2511                    &mut writer,
2512                    &AgentResponse::Error {
2513                        message: format!(
2514                            "cache client {client_version} does not match agent {}",
2515                            self.version
2516                        ),
2517                    },
2518                )
2519                .await?;
2520                return Ok(());
2521            }
2522            _ => bail!("the first agent request must be hello"),
2523        }
2524        send_response(
2525            &mut writer,
2526            &AgentResponse::Hello {
2527                protocol: AGENT_PROTOCOL_VERSION,
2528                agent_version: self.version.to_string(),
2529            },
2530        )
2531        .await?;
2532
2533        // Tickets accumulate for the life of the connection because a shim
2534        // publishes a compilation's blobs and the action result naming them over
2535        // one connection, in that order.
2536        let mut connection = ConnectionUploads::default();
2537        while let Some(line) = read_request(&mut reader).await? {
2538            let response = match serde_json::from_str(&line) {
2539                Ok(request) => self.respond_on(request, &mut connection).await,
2540                Err(error) => AgentResponse::Error {
2541                    message: format!("invalid agent request: {error}"),
2542                },
2543            };
2544            send_response(&mut writer, &response).await?;
2545        }
2546        Ok(())
2547    }
2548}
2549
2550/// Read one newline-delimited request, refusing one that grows past the cap.
2551///
2552/// Any process running as this user can open the session socket, so a request
2553/// that never terminates its line must not be able to grow the agent's memory
2554/// without bound.
2555async fn read_request<R>(reader: &mut R) -> Result<Option<String>>
2556where
2557    R: AsyncBufRead + Unpin,
2558{
2559    let mut line = Vec::new();
2560    loop {
2561        let available = reader.fill_buf().await?;
2562        if available.is_empty() {
2563            break;
2564        }
2565        let (consumed, complete) = match available.iter().position(|byte| *byte == b'\n') {
2566            Some(index) => (index, true),
2567            None => (available.len(), false),
2568        };
2569        if line.len() + consumed > MAX_REQUEST_BYTES {
2570            bail!("agent request exceeded {MAX_REQUEST_BYTES} bytes");
2571        }
2572        line.extend_from_slice(&available[..consumed]);
2573        // The newline itself is consumed but never kept.
2574        reader.consume(consumed + usize::from(complete));
2575        if complete {
2576            return Ok(Some(String::from_utf8(line)?));
2577        }
2578    }
2579    if line.is_empty() {
2580        Ok(None)
2581    } else {
2582        Ok(Some(String::from_utf8(line)?))
2583    }
2584}
2585
2586async fn send_response(
2587    writer: &mut (impl AsyncWrite + Unpin),
2588    response: &AgentResponse,
2589) -> Result<()> {
2590    let mut encoded = serde_json::to_vec(response)?;
2591    encoded.push(b'\n');
2592    writer.write_all(&encoded).await?;
2593    writer.flush().await?;
2594    Ok(())
2595}
2596
2597#[cfg(test)]
2598#[path = "agent_tests.rs"]
2599mod tests;