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