Skip to main content

mbx_cache_core/
agent.rs

1use crate::uploads::{ConnectionUploads, UploadQueue, UploadSink};
2use crate::{
3    ActionPrediction, ActionPromiseCompletion, ActionPromiseState, BlobPackLimits, CacheDigest,
4    CacheDirectory, LocalActionCache, LocalCas, MAX_STAGED_BLOB_PACK_BYTES,
5    MAX_STAGED_BLOB_PACK_ITEMS, ManifestPutOutcome, RemoteActionResult, RemoteCacheClient,
6    RemoteCacheMode, RustcMetadata, TaskActionManifest, blob_pack_chunk, canonical_json,
7};
8use eyre::{Context, Result, bail};
9use futures_util::{FutureExt, StreamExt, future::BoxFuture, stream};
10use log::warn;
11use std::collections::{BTreeMap, BTreeSet};
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::sync::{Arc, Mutex, Weak};
16use std::time::{Duration, Instant, SystemTime};
17
18/// Fleet claims are leases rather than correctness locks. This only bounds how
19/// long one shim waits through repeated pending responses before degrading to
20/// an ordinary compilation; the server independently expires abandoned claims.
21const ACTION_PROMISE_WAIT: Duration = Duration::from_secs(60 * 60);
22use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
23
24mod file_digest;
25mod manifest;
26mod prefetch;
27mod stats;
28mod wire;
29
30pub use file_digest::{
31    FileDigestCache, FileDigestScope, FileIdentity, NoFileDigestCache, RecordedFileDigest,
32};
33pub use manifest::{is_task_identity, task_manifest_actions};
34use manifest::{
35    merge_remote_task_manifest, merge_task_manifests, task_manifest_dir, validate_task_identity,
36    validate_task_manifest,
37};
38pub use stats::{AgentStats, CompilerStats};
39use wire::MAX_REQUEST_BYTES;
40pub use wire::{
41    AGENT_PROTOCOL_VERSION, AgentEvent, AgentEventObserver, AgentRequest, AgentResponse,
42    RestoreStats,
43};
44
45const MAX_EXECUTABLE_IDENTITIES: usize = 64;
46const MAX_EXECUTABLE_IDENTITY_SIZE: usize = 64 * 1024;
47const MAX_EXECUTABLE_IDENTITY_BYTES: usize = 256 * 1024;
48const TASK_ACTION_MANIFEST_VERSION: u8 = 1;
49const MAX_TASK_ACTION_PREDICTIONS: usize = 16 * 1024;
50/// Longest shim diagnostic the agent accepts, in bytes.
51const MAX_WARNING_BYTES: usize = 4 * 1024;
52/// Most distinct shim diagnostics one session surfaces before going quiet.
53///
54/// A failure mode that repeats across a build tends to repeat with the same
55/// message, which deduplication already collapses; the cap only guards
56/// against a message that embeds something unique per compilation.
57const MAX_WARNINGS: usize = 128;
58/// Most file identities one digest lookup or record may carry.
59const MAX_FILE_DIGEST_BATCH: usize = 16 * 1024;
60/// Most recorded file digests one session retains across every scope.
61///
62/// Roughly two orders of magnitude above the largest workspace measured; the
63/// cap exists so a pathological build bounds the agent instead of growing it.
64const MAX_FILE_DIGEST_ENTRIES: usize = 1024 * 1024;
65const MAX_REMOTE_TRANSFERS: usize = 64;
66const MAX_PREFETCH_TRANSFERS: usize = 48;
67const MAX_PREFETCH_ACTION_BATCH: usize = 256;
68/// Batched action lookups issued at once, well inside the transfer budget: each
69/// asks about hundreds of actions, so a handful covers a large workspace.
70const MAX_PREFETCH_BATCH_LOOKUPS: usize = 4;
71const PREFETCH_ACTION_BATCH_DELAY: Duration = Duration::from_millis(5);
72const MAX_PREFETCH_DIRECTORY_OBJECTS: usize = 100_000;
73const MAX_PREFETCH_OBJECTS_PER_WAVE: usize = 100_000;
74const DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES: u64 = 5 * 1024 * 1024 * 1024;
75
76/// Remote action-cache access owned by one task session.
77pub struct AgentRemoteCache {
78    /// Remote protocol client used by the agent.
79    pub client: RemoteCacheClient,
80    /// Permitted remote read/write operations.
81    pub mode: RemoteCacheMode,
82    /// Directory used for verified downloads before CAS ingestion.
83    pub staging_dir: PathBuf,
84}
85
86#[derive(Default)]
87struct AtomicAgentStats {
88    lookups: AtomicU64,
89    unconsulted: AtomicU64,
90    hits: AtomicU64,
91    stores: AtomicU64,
92    stored_bytes: AtomicU64,
93    verifications: AtomicU64,
94    divergences: AtomicU64,
95    downloaded_bytes: AtomicU64,
96    uploaded_bytes: AtomicU64,
97    background_uploads: AtomicU64,
98    background_upload_failures: AtomicU64,
99    remote_blob_pack_uploads: AtomicU64,
100    remote_blob_pack_upload_blobs: AtomicU64,
101    upload_drain_duration_ns: AtomicU64,
102    prefetched_actions: AtomicU64,
103    predictions_loaded: AtomicU64,
104    remote_failures: AtomicU64,
105    remote_manifest_lookups: AtomicU64,
106    remote_manifest_lookup_duration_ns: AtomicU64,
107    remote_action_lookups: AtomicU64,
108    remote_action_lookup_duration_ns: AtomicU64,
109    remote_blob_requests: AtomicU64,
110    remote_blob_pack_requests: AtomicU64,
111    remote_blob_pack_blobs: AtomicU64,
112    remote_blob_transfer_duration_ns: AtomicU64,
113    local_cas_write_duration_ns: AtomicU64,
114    prefetch_runs: AtomicU64,
115    prefetch_duration_ns: AtomicU64,
116    materialization_duration_ns: AtomicU64,
117    bypasses: Mutex<BTreeMap<String, u64>>,
118    avoided_compiler_duration_ns: AtomicU64,
119    compiler: Mutex<BTreeMap<String, CompilerStats>>,
120    slow_compilations: Mutex<BTreeMap<String, u64>>,
121    restored_output_files: AtomicU64,
122    restored_output_bytes: AtomicU64,
123    reflinked_output_files: AtomicU64,
124    reflinked_output_bytes: AtomicU64,
125    copied_output_files: AtomicU64,
126    copied_output_bytes: AtomicU64,
127    reused_output_files: AtomicU64,
128    reused_output_bytes: AtomicU64,
129}
130
131struct AtomicDurationTimer<'a> {
132    started: Instant,
133    target: &'a AtomicU64,
134}
135
136impl<'a> AtomicDurationTimer<'a> {
137    fn start(target: &'a AtomicU64) -> Self {
138        Self {
139            started: Instant::now(),
140            target,
141        }
142    }
143}
144
145impl Drop for AtomicDurationTimer<'_> {
146    fn drop(&mut self) {
147        atomic_saturating_add(self.target, duration_ns(self.started));
148    }
149}
150
151fn duration_ns(started: Instant) -> u64 {
152    started.elapsed().as_nanos().try_into().unwrap_or(u64::MAX)
153}
154
155fn validate_crate_name(crate_name: Option<&str>) -> Result<()> {
156    if let Some(crate_name) = crate_name
157        && (crate_name.len() > 256 || crate_name.contains(['\0', '\n', '\r']))
158    {
159        bail!("invalid compiler crate name");
160    }
161    Ok(())
162}
163
164fn atomic_saturating_add(target: &AtomicU64, value: u64) {
165    let _ = target.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
166        Some(current.saturating_add(value))
167    });
168}
169
170fn queue_prefetch_digest(
171    verified: &BTreeMap<CacheDigest, PathBuf>,
172    pending: &mut BTreeMap<CacheDigest, ()>,
173    digest: CacheDigest,
174) {
175    if verified.contains_key(&digest) || pending.contains_key(&digest) {
176        return;
177    }
178    pending.insert(digest, ());
179}
180
181fn queue_prefetch_directory(
182    seen: &BTreeMap<CacheDigest, ()>,
183    pending: &mut BTreeMap<CacheDigest, ()>,
184    digest: CacheDigest,
185    limit: usize,
186) -> bool {
187    if seen.contains_key(&digest) || pending.contains_key(&digest) {
188        return true;
189    }
190    if seen.len().saturating_add(pending.len()) >= limit {
191        return false;
192    }
193    pending.insert(digest, ());
194    true
195}
196
197/// Shared state for an agent hosted by the process that owns a build session.
198///
199/// Transport listeners deliberately live in the embedder so the session
200/// lifecycle owns them. This type only contains ecosystem-independent CAS and
201/// protocol logic.
202#[derive(Clone)]
203pub struct CacheAgent {
204    cas: LocalCas,
205    actions: LocalActionCache,
206    verified_blobs: Arc<Mutex<BTreeMap<CacheDigest, VerifiedBlob>>>,
207    version: Arc<str>,
208    write_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
209    action_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
210    stats: Arc<AtomicAgentStats>,
211    observer: Option<Arc<dyn AgentEventObserver>>,
212    executable_identities: Arc<Mutex<BTreeMap<ExecutableIdentityKey, Vec<u8>>>>,
213    manifest_dir: Arc<PathBuf>,
214    task_actions: Arc<Mutex<BTreeMap<String, TaskActionState>>>,
215    next_task_run: Arc<AtomicU64>,
216    manifest_write_lock: Arc<Mutex<()>>,
217    remote: Option<Arc<RemoteCacheClient>>,
218    remote_mode: RemoteCacheMode,
219    remote_staging_dir: Arc<PathBuf>,
220    remote_download_limit: u64,
221    remote_download_bytes: Arc<AtomicU64>,
222    pending_remote_actions: Arc<Mutex<BTreeMap<CacheDigest, RemoteActionResult>>>,
223    remote_transfers: Arc<tokio::sync::Semaphore>,
224    prefetch_transfers: Arc<tokio::sync::Semaphore>,
225    prefetch_tasks: Arc<Mutex<Vec<tokio::task::JoinHandle<()>>>>,
226    /// Distinct shim diagnostics already surfaced, so a warning that fires
227    /// once per compilation is printed once per session.
228    warnings: Arc<Mutex<BTreeSet<String>>>,
229    /// Digests of files shims hashed or wrote this session, keyed by scope and
230    /// path, each entry standing while its recorded identity matches the disk.
231    file_digests: Arc<Mutex<BTreeMap<(FileDigestScope, PathBuf), RecordedFileDigest>>>,
232    /// Deferred remote publication, present only when the session may write.
233    uploads: Option<UploadQueue>,
234}
235
236/// Records background upload activity against a session's statistics.
237struct AgentUploadSink {
238    stats: Arc<AtomicAgentStats>,
239}
240
241impl UploadSink for AgentUploadSink {
242    fn record_blob_uploaded(&self, bytes: u64) {
243        self.stats
244            .background_uploads
245            .fetch_add(1, Ordering::Relaxed);
246        self.stats
247            .uploaded_bytes
248            .fetch_add(bytes, Ordering::Relaxed);
249    }
250
251    fn record_action_uploaded(&self) {
252        self.stats
253            .background_uploads
254            .fetch_add(1, Ordering::Relaxed);
255    }
256
257    fn record_blob_pack_uploaded(&self, blobs: u64) {
258        self.stats
259            .remote_blob_pack_uploads
260            .fetch_add(1, Ordering::Relaxed);
261        self.stats
262            .remote_blob_pack_upload_blobs
263            .fetch_add(blobs, Ordering::Relaxed);
264    }
265
266    fn record_upload_failure(&self) {
267        self.stats
268            .background_upload_failures
269            .fetch_add(1, Ordering::Relaxed);
270        self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
271    }
272}
273
274/// A CAS blob whose contents this session hashed, and the file identity that
275/// says the hash still describes what is on disk.
276///
277/// Length alone would miss an overwrite that keeps the size; the modification
278/// time is what makes a rewrite visible without reading the bytes again.
279#[derive(Debug, Clone)]
280struct VerifiedBlob {
281    path: PathBuf,
282    len: u64,
283    modified: SystemTime,
284}
285
286impl VerifiedBlob {
287    /// Describe a file that was just verified, or nothing when the filesystem
288    /// will not report a modification time to compare against later.
289    fn describe(path: &Path) -> Option<Self> {
290        let metadata = std::fs::metadata(path).ok()?;
291        Some(Self {
292            path: path.to_path_buf(),
293            len: metadata.len(),
294            modified: metadata.modified().ok()?,
295        })
296    }
297
298    /// Whether the file still has the identity it had when it was verified.
299    fn is_unchanged(&self) -> bool {
300        let Ok(metadata) = std::fs::metadata(&self.path) else {
301            return false;
302        };
303        metadata.len() == self.len && metadata.modified().is_ok_and(|now| now == self.modified)
304    }
305}
306
307#[derive(Debug, Clone, Default)]
308struct TaskActionState {
309    manifest: String,
310    baseline_loaded: bool,
311    predictions: BTreeMap<CacheDigest, ActionPrediction>,
312    pending_predictions: BTreeMap<CacheDigest, ActionPrediction>,
313    remote_etag: Option<String>,
314}
315
316struct PrefetchedAction {
317    adapter: String,
318    result: RemoteActionResult,
319}
320
321struct RemoteDownloadReservation {
322    counter: Arc<AtomicU64>,
323    reserved: u64,
324    committed: bool,
325}
326
327impl RemoteDownloadReservation {
328    fn bytes(&self) -> u64 {
329        self.reserved
330    }
331
332    fn commit(mut self, bytes: u64) {
333        debug_assert!(bytes <= self.reserved);
334        self.counter
335            .fetch_sub(self.reserved.saturating_sub(bytes), Ordering::AcqRel);
336        self.committed = true;
337    }
338}
339
340impl Drop for RemoteDownloadReservation {
341    fn drop(&mut self) {
342        if !self.committed {
343            self.counter.fetch_sub(self.reserved, Ordering::AcqRel);
344        }
345    }
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
349struct ExecutableIdentityKey {
350    executable: PathBuf,
351    environment: BTreeMap<String, Option<String>>,
352}
353
354impl CacheAgent {
355    /// Create an agent backed by the cache rooted at `cache_dir`.
356    pub fn new(cache_dir: impl Into<PathBuf>, version: impl Into<Arc<str>>) -> Self {
357        Self::build(cache_dir.into(), version.into(), None, 0)
358    }
359
360    /// Create an agent with local-first access to a remote action cache.
361    pub fn new_remote(
362        cache_dir: impl Into<PathBuf>,
363        version: impl Into<Arc<str>>,
364        remote: AgentRemoteCache,
365    ) -> Self {
366        Self::build(
367            cache_dir.into(),
368            version.into(),
369            Some(remote),
370            DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES,
371        )
372    }
373
374    /// Create a remote agent with a cumulative download budget for the session.
375    pub fn new_remote_with_download_limit(
376        cache_dir: impl Into<PathBuf>,
377        version: impl Into<Arc<str>>,
378        remote: AgentRemoteCache,
379        max_remote_download_bytes: u64,
380    ) -> Self {
381        Self::build(
382            cache_dir.into(),
383            version.into(),
384            Some(remote),
385            max_remote_download_bytes,
386        )
387    }
388
389    fn build(
390        cache_dir: PathBuf,
391        version: Arc<str>,
392        remote: Option<AgentRemoteCache>,
393        remote_download_limit: u64,
394    ) -> Self {
395        let remote_mode = remote
396            .as_ref()
397            .map_or(RemoteCacheMode::ReadOnly, |remote| remote.mode);
398        let remote_staging_dir = remote.as_ref().map_or_else(
399            || cache_dir.join("remote"),
400            |remote| remote.staging_dir.clone(),
401        );
402        let remote = remote.map(|remote| Arc::new(remote.client));
403        let stats = Arc::new(AtomicAgentStats::default());
404        let remote_transfers = Arc::new(tokio::sync::Semaphore::new(MAX_REMOTE_TRANSFERS));
405        // A session that cannot publish never queues an upload, so it does not
406        // need somewhere to queue one.
407        let uploads = remote
408            .clone()
409            .filter(|_| remote_mode.writes())
410            .map(|client| {
411                UploadQueue::new(
412                    client,
413                    Arc::new(AgentUploadSink {
414                        stats: stats.clone(),
415                    }),
416                    remote_transfers.clone(),
417                )
418            });
419        Self {
420            cas: LocalCas::new(cache_dir.clone()),
421            actions: LocalActionCache::new(cache_dir.clone()),
422            verified_blobs: Arc::new(Mutex::new(BTreeMap::new())),
423            version,
424            write_locks: Arc::new(Mutex::new(BTreeMap::new())),
425            action_locks: Arc::new(Mutex::new(BTreeMap::new())),
426            stats,
427            observer: None,
428            executable_identities: Arc::new(Mutex::new(BTreeMap::new())),
429            manifest_dir: Arc::new(task_manifest_dir(&cache_dir)),
430            task_actions: Arc::new(Mutex::new(BTreeMap::new())),
431            next_task_run: Arc::new(AtomicU64::new(0)),
432            manifest_write_lock: Arc::new(Mutex::new(())),
433            remote,
434            remote_mode,
435            remote_staging_dir: Arc::new(remote_staging_dir),
436            remote_download_limit,
437            remote_download_bytes: Arc::new(AtomicU64::new(0)),
438            pending_remote_actions: Arc::new(Mutex::new(BTreeMap::new())),
439            remote_transfers,
440            prefetch_transfers: Arc::new(tokio::sync::Semaphore::new(MAX_PREFETCH_TRANSFERS)),
441            prefetch_tasks: Arc::new(Mutex::new(Vec::new())),
442            warnings: Arc::new(Mutex::new(BTreeSet::new())),
443            file_digests: Arc::new(Mutex::new(BTreeMap::new())),
444            uploads,
445        }
446    }
447
448    /// Report each accounted cache decision to `observer` as it happens.
449    #[must_use]
450    pub fn with_observer(mut self, observer: Arc<dyn AgentEventObserver>) -> Self {
451        self.observer = Some(observer);
452        self
453    }
454
455    fn emit(&self, event: impl FnOnce() -> AgentEvent) {
456        if let Some(observer) = &self.observer {
457            observer.event(event());
458        }
459    }
460
461    fn reserve_remote_download(&self, bytes: u64) -> Result<RemoteDownloadReservation> {
462        let mut current = self.remote_download_bytes.load(Ordering::Acquire);
463        loop {
464            let next = current
465                .checked_add(bytes)
466                .ok_or_else(|| eyre::eyre!("remote cache download budget overflowed"))?;
467            if next > self.remote_download_limit {
468                bail!(
469                    "remote cache download budget exceeded: {} bytes requested with {} of {} bytes already used",
470                    bytes,
471                    current,
472                    self.remote_download_limit
473                );
474            }
475            match self.remote_download_bytes.compare_exchange_weak(
476                current,
477                next,
478                Ordering::AcqRel,
479                Ordering::Acquire,
480            ) {
481                Ok(_) => {
482                    return Ok(RemoteDownloadReservation {
483                        counter: self.remote_download_bytes.clone(),
484                        reserved: bytes,
485                        committed: false,
486                    });
487                }
488                Err(observed) => current = observed,
489            }
490        }
491    }
492
493    fn reserve_remote_download_up_to(&self, requested: u64) -> Result<RemoteDownloadReservation> {
494        let mut current = self.remote_download_bytes.load(Ordering::Acquire);
495        loop {
496            let reserved = requested.min(self.remote_download_limit.saturating_sub(current));
497            let next = current + reserved;
498            match self.remote_download_bytes.compare_exchange_weak(
499                current,
500                next,
501                Ordering::AcqRel,
502                Ordering::Acquire,
503            ) {
504                Ok(_) => {
505                    return Ok(RemoteDownloadReservation {
506                        counter: self.remote_download_bytes.clone(),
507                        reserved,
508                        committed: false,
509                    });
510                }
511                Err(observed) => current = observed,
512            }
513        }
514    }
515
516    /// Load the last committed action manifest for a task into this session.
517    pub async fn begin_task(&self, task: &str) -> Result<String> {
518        self.begin_task_with_remote_errors(task, false).await
519    }
520
521    /// Load a task and finish its prefetch, surfacing remote lookup failures.
522    pub async fn prefetch_task(&self, task: &str) -> Result<String> {
523        let run = self.begin_task_with_remote_errors(task, true).await?;
524        self.wait_for_prefetches().await;
525        Ok(run)
526    }
527
528    async fn begin_task_with_remote_errors(&self, task: &str, strict: bool) -> Result<String> {
529        validate_task_identity(task)?;
530        // The local manifest is already enough to start useful work. Do not
531        // leave those predictions idle while a high-latency remote answers the
532        // manifest lookup; the authoritative snapshot is loaded again below
533        // before it is merged, so another process can still update it while
534        // this request is in flight.
535        let early_predictions = {
536            let _write_guard = self.manifest_write_lock.lock().unwrap();
537            let _file_guard = self.lock_task_manifest(task)?;
538            self.load_task_manifest(task)?
539                .map(|manifest| manifest.predictions)
540                .unwrap_or_default()
541        };
542        let early_actions: BTreeSet<_> = early_predictions
543            .iter()
544            .map(|prediction| prediction.action.clone())
545            .collect();
546        self.spawn_prefetch_predictions(early_predictions);
547        let (remote_manifest, mut remote_etag) = if self.remote_mode.reads() {
548            match self.get_remote_task_manifest(task).await {
549                Ok(Some((manifest, etag))) => (Some(manifest), Some(etag)),
550                Ok(None) => (None, None),
551                Err(error) => {
552                    if strict {
553                        return Err(error).wrap_err_with(|| {
554                            format!("remote task action manifest lookup failed for {task}")
555                        });
556                    }
557                    self.note_remote_failure();
558                    warn!("remote task action manifest lookup failed for {task}: {error}");
559                    (None, None)
560                }
561            }
562        } else {
563            (None, None)
564        };
565        let manifest = {
566            let _write_guard = self.manifest_write_lock.lock().unwrap();
567            let _file_guard = self.lock_task_manifest(task)?;
568            let local_manifest = self.load_task_manifest(task)?;
569            let manifest = match (remote_manifest, local_manifest) {
570                (Some(remote), Some(local)) => {
571                    let (manifest, merged) = merge_remote_task_manifest(task, remote, local);
572                    if !merged {
573                        remote_etag = None;
574                    }
575                    Some(manifest)
576                }
577                (Some(remote), None) => Some(remote),
578                (None, local) => local,
579            };
580            if let Some(manifest) = &manifest {
581                self.persist_task_manifest(manifest)?;
582            }
583            manifest
584        };
585        let state = if let Some(manifest) = manifest {
586            TaskActionState {
587                manifest: task.to_string(),
588                baseline_loaded: true,
589                predictions: manifest
590                    .predictions
591                    .into_iter()
592                    .map(|prediction| (prediction.invocation.clone(), prediction))
593                    .collect(),
594                pending_predictions: BTreeMap::new(),
595                remote_etag,
596            }
597        } else {
598            TaskActionState {
599                manifest: task.to_string(),
600                baseline_loaded: true,
601                remote_etag,
602                ..TaskActionState::default()
603            }
604        };
605        let sequence = self.next_task_run.fetch_add(1, Ordering::Relaxed);
606        let run =
607            CacheDigest::blake3(format!("{task}\0{}\0{sequence}", std::process::id()).as_bytes())
608                .hash;
609        // The largest baseline rather than a sum: beginning the same task again
610        // in one session reloads the same manifest, and counting it twice would
611        // overstate what there was to match.
612        self.stats.predictions_loaded.fetch_max(
613            state.predictions.len().try_into().unwrap_or(u64::MAX),
614            Ordering::Relaxed,
615        );
616        // The early local wave already owns these actions. Only launch a
617        // second wave for predictions learned from the remote or from a local
618        // writer that committed while its lookup was in flight.
619        let predictions = state
620            .predictions
621            .values()
622            .filter(|prediction| !early_actions.contains(&prediction.action))
623            .cloned()
624            .collect();
625        self.task_actions.lock().unwrap().insert(run.clone(), state);
626        self.spawn_prefetch_predictions(predictions);
627        Ok(run)
628    }
629
630    /// Cancel speculative downloads before the owning session exits.
631    pub async fn cancel_prefetches(&self) {
632        let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
633        for task in &tasks {
634            task.abort();
635        }
636        for task in tasks {
637            if let Err(error) = task.await
638                && !error.is_cancelled()
639            {
640                warn!("remote action prefetch task failed: {error}");
641            }
642        }
643    }
644
645    /// Publish everything a build queued, before the session stops.
646    ///
647    /// Store requests return once an object is durable locally, so at this point
648    /// the remote cache may still be behind the local one. Uploads run on the
649    /// session's runtime and are abandoned if it goes away, so a session that
650    /// wants them published has to wait here.
651    pub async fn wait_for_uploads(&self) {
652        let Some(uploads) = &self.uploads else {
653            return;
654        };
655        let _timer = AtomicDurationTimer::start(&self.stats.upload_drain_duration_ns);
656        uploads.drain().await;
657    }
658
659    async fn wait_for_prefetches(&self) {
660        let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
661        for task in tasks {
662            if let Err(error) = task.await {
663                warn!("remote action prefetch task failed: {error}");
664            }
665        }
666    }
667
668    /// Atomically publish the completed actions collected by a task run.
669    pub async fn commit_task(&self, run: &str) -> Result<()> {
670        self.commit_task_actions(run).await.map(|_| ())
671    }
672
673    /// Publish a task run and return exactly the predictions completed by it.
674    ///
675    /// The persisted task manifest also carries predictions inherited from
676    /// earlier runs. Callers that need a receipt for this one run must not
677    /// mistake that cumulative manifest for the work the run completed.
678    pub async fn commit_task_actions(&self, run: &str) -> Result<Vec<ActionPrediction>> {
679        validate_task_identity(run)?;
680        let state = self
681            .task_actions
682            .lock()
683            .unwrap()
684            .get(run)
685            .cloned()
686            .ok_or_else(|| eyre::eyre!("task action manifest baseline was not loaded"))?;
687        if !state.baseline_loaded {
688            bail!("task action manifest baseline was not loaded");
689        }
690        let task = state.manifest;
691        validate_task_identity(&task)?;
692        let completed = state
693            .pending_predictions
694            .values()
695            .cloned()
696            .collect::<Vec<_>>();
697        let (manifest, introduced) = {
698            let _write_guard = self.manifest_write_lock.lock().unwrap();
699            let _file_guard = self.lock_task_manifest(&task)?;
700            let mut predictions = self
701                .load_task_manifest(&task)?
702                .map(|manifest| {
703                    manifest
704                        .predictions
705                        .into_iter()
706                        .map(|prediction| (prediction.invocation.clone(), prediction))
707                        .collect::<BTreeMap<_, _>>()
708                })
709                .unwrap_or_default();
710            // Which predictions this run adds, as opposed to inherits. Only a
711            // new one may be withheld for a failed upload: retracting an
712            // inherited one would un-advertise a result that is plausibly still
713            // on the server from whichever session put it there.
714            let introduced: BTreeSet<CacheDigest> = state
715                .pending_predictions
716                .keys()
717                .filter(|invocation| !predictions.contains_key(*invocation))
718                .cloned()
719                .collect();
720            // Only publish predictions recorded by this run. `predictions`
721            // also contains the baseline loaded by `begin_task`; extending
722            // with that snapshot would overwrite newer entries committed by
723            // another agent process after this run began.
724            predictions.extend(state.pending_predictions);
725            let manifest = TaskActionManifest {
726                version: TASK_ACTION_MANIFEST_VERSION,
727                task: task.clone(),
728                predictions: predictions.into_values().collect(),
729            };
730            validate_task_manifest(&manifest, &task)?;
731            self.persist_task_manifest(&manifest)?;
732            (manifest, introduced)
733        };
734        self.task_actions.lock().unwrap().remove(run);
735        if self.remote_mode.writes() {
736            // A manifest advertises the actions it predicts, so it must not
737            // reach the remote cache before the results a reader would then go
738            // looking for -- nor name a result that never got there at all.
739            let mut manifest = manifest;
740            if let Some(uploads) = &self.uploads {
741                let actions: Vec<CacheDigest> = manifest
742                    .predictions
743                    .iter()
744                    .map(|prediction| prediction.action.clone())
745                    .collect();
746                let unpublished = uploads.wait_for_actions(&actions).await;
747                let withheld = manifest
748                    .predictions
749                    .iter()
750                    .filter(|prediction| {
751                        introduced.contains(&prediction.invocation)
752                            && unpublished.contains(&prediction.action)
753                    })
754                    .count();
755                if withheld > 0 {
756                    // The local manifest keeps them: this checkout can still use
757                    // what it built, and a later session can publish it.
758                    warn!(
759                        "{withheld} of {} predicted actions were not published, so the remote task action manifest omits them",
760                        manifest.predictions.len()
761                    );
762                    manifest.predictions.retain(|prediction| {
763                        !(introduced.contains(&prediction.invocation)
764                            && unpublished.contains(&prediction.action))
765                    });
766                }
767            }
768            match self
769                .put_remote_task_manifest(&task, manifest, state.remote_etag)
770                .await
771            {
772                Ok(remote_manifest) => {
773                    let _write_guard = self.manifest_write_lock.lock().unwrap();
774                    let reconciliation = (|| {
775                        let _file_guard = self.lock_task_manifest(&task)?;
776                        let manifest = match self.load_task_manifest(&task)? {
777                            Some(local) => {
778                                merge_remote_task_manifest(&task, remote_manifest, local).0
779                            }
780                            None => remote_manifest,
781                        };
782                        self.persist_task_manifest(&manifest)
783                    })();
784                    if let Err(error) = reconciliation {
785                        warn!(
786                            "remote task action manifest reconciliation failed for {task}: {error}"
787                        );
788                    }
789                }
790                Err(error) => {
791                    self.note_remote_failure();
792                    warn!("remote task action manifest upload failed for {task}: {error}");
793                }
794            }
795        }
796        Ok(completed)
797    }
798
799    fn task_manifest_path(&self, task: &str) -> PathBuf {
800        self.manifest_dir.join(format!("{task}.json"))
801    }
802
803    fn task_manifest_lock_path(&self, task: &str) -> PathBuf {
804        self.manifest_dir.join("locks").join(format!("{task}.lock"))
805    }
806
807    fn lock_task_manifest(&self, task: &str) -> Result<fslock::LockFile> {
808        let path = self.task_manifest_lock_path(task);
809        fs::create_dir_all(path.parent().expect("task manifest lock has a parent"))?;
810        let mut lock = fslock::LockFile::open(&path)?;
811        lock.lock()?;
812        Ok(lock)
813    }
814
815    fn load_task_manifest(&self, task: &str) -> Result<Option<TaskActionManifest>> {
816        match fs::read(self.task_manifest_path(task)) {
817            Ok(contents) => Ok(Some(self.parse_task_manifest(task, &contents, false)?)),
818            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
819            Err(error) => Err(error.into()),
820        }
821    }
822
823    fn parse_task_manifest(
824        &self,
825        task: &str,
826        contents: &[u8],
827        require_canonical: bool,
828    ) -> Result<TaskActionManifest> {
829        let manifest: TaskActionManifest = serde_json::from_slice(contents)?;
830        validate_task_manifest(&manifest, task)?;
831        if require_canonical && canonical_json(&manifest)? != contents {
832            bail!("task action manifest is not canonical JSON");
833        }
834        Ok(manifest)
835    }
836
837    fn task_manifest_selector(task: &str) -> Result<(Vec<u8>, CacheDigest)> {
838        TaskActionManifest::selector(task)
839    }
840
841    fn persist_task_manifest(&self, manifest: &TaskActionManifest) -> Result<()> {
842        let bytes = canonical_json(manifest)?;
843        fs::create_dir_all(self.manifest_dir.as_path())?;
844        let mut temporary = tempfile::NamedTempFile::new_in(self.manifest_dir.as_path())?;
845        std::io::Write::write_all(temporary.as_file_mut(), &bytes)?;
846        temporary.as_file_mut().sync_all()?;
847        temporary
848            .persist(self.task_manifest_path(&manifest.task))
849            .map_err(|error| error.error)?;
850        Ok(())
851    }
852
853    async fn get_remote_task_manifest(
854        &self,
855        task: &str,
856    ) -> Result<Option<(TaskActionManifest, String)>> {
857        let Some(remote) = &self.remote else {
858            return Ok(None);
859        };
860        let (_, selector) = Self::task_manifest_selector(task)?;
861        let _permit = self.remote_transfers.acquire().await?;
862        self.stats
863            .remote_manifest_lookups
864            .fetch_add(1, Ordering::Relaxed);
865        let _timer = AtomicDurationTimer::start(&self.stats.remote_manifest_lookup_duration_ns);
866        let Some(remote_manifest) = remote.get_action_manifest(&selector).await? else {
867            return Ok(None);
868        };
869        let manifest = self.parse_task_manifest(task, &remote_manifest.bytes, true)?;
870        Ok(Some((manifest, remote_manifest.etag)))
871    }
872
873    async fn put_remote_task_manifest(
874        &self,
875        task: &str,
876        mut manifest: TaskActionManifest,
877        mut expected_etag: Option<String>,
878    ) -> Result<TaskActionManifest> {
879        let Some(remote) = &self.remote else {
880            return Ok(manifest);
881        };
882        let (_, selector) = Self::task_manifest_selector(task)?;
883        for _ in 0..4 {
884            let bytes = canonical_json(&manifest)?;
885            let outcome = {
886                let _permit = self.remote_transfers.acquire().await?;
887                remote
888                    .put_action_manifest(&selector, &bytes, expected_etag.as_deref())
889                    .await?
890            };
891            match outcome {
892                ManifestPutOutcome::Stored => return Ok(manifest),
893                ManifestPutOutcome::PreconditionFailed => {
894                    let Some((remote_manifest, etag)) = self.get_remote_task_manifest(task).await?
895                    else {
896                        expected_etag = None;
897                        continue;
898                    };
899                    manifest = merge_task_manifests(task, Some(remote_manifest), manifest)?;
900                    expected_etag = Some(etag);
901                }
902            }
903        }
904        bail!("remote task action manifest changed too frequently")
905    }
906
907    /// Return a snapshot of this session's cache activity.
908    pub fn stats(&self) -> AgentStats {
909        AgentStats {
910            session_duration_ns: 0,
911            lookups: self.stats.lookups.load(Ordering::Relaxed),
912            unconsulted: self.stats.unconsulted.load(Ordering::Relaxed),
913            hits: self.stats.hits.load(Ordering::Relaxed),
914            stores: self.stats.stores.load(Ordering::Relaxed),
915            stored_bytes: self.stats.stored_bytes.load(Ordering::Relaxed),
916            verifications: self.stats.verifications.load(Ordering::Relaxed),
917            divergences: self.stats.divergences.load(Ordering::Relaxed),
918            downloaded_bytes: self.stats.downloaded_bytes.load(Ordering::Relaxed),
919            uploaded_bytes: self.stats.uploaded_bytes.load(Ordering::Relaxed),
920            background_uploads: self.stats.background_uploads.load(Ordering::Relaxed),
921            background_upload_failures: self
922                .stats
923                .background_upload_failures
924                .load(Ordering::Relaxed),
925            remote_blob_pack_uploads: self.stats.remote_blob_pack_uploads.load(Ordering::Relaxed),
926            remote_blob_pack_upload_blobs: self
927                .stats
928                .remote_blob_pack_upload_blobs
929                .load(Ordering::Relaxed),
930            upload_drain_duration_ns: self.stats.upload_drain_duration_ns.load(Ordering::Relaxed),
931            prefetched_actions: self.stats.prefetched_actions.load(Ordering::Relaxed),
932            predictions_loaded: self.stats.predictions_loaded.load(Ordering::Relaxed),
933            bypasses: self.stats.bypasses.lock().unwrap().clone(),
934            avoided_compiler_duration_ns: self
935                .stats
936                .avoided_compiler_duration_ns
937                .load(Ordering::Relaxed),
938            compiler: self.stats.compiler.lock().unwrap().clone(),
939            slow_compilations: self.stats.slow_compilations.lock().unwrap().clone(),
940            remote_failures: self.stats.remote_failures.load(Ordering::Relaxed),
941            remote_manifest_lookups: self.stats.remote_manifest_lookups.load(Ordering::Relaxed),
942            remote_manifest_lookup_duration_ns: self
943                .stats
944                .remote_manifest_lookup_duration_ns
945                .load(Ordering::Relaxed),
946            remote_action_lookups: self.stats.remote_action_lookups.load(Ordering::Relaxed),
947            remote_action_lookup_duration_ns: self
948                .stats
949                .remote_action_lookup_duration_ns
950                .load(Ordering::Relaxed),
951            remote_blob_requests: self.stats.remote_blob_requests.load(Ordering::Relaxed),
952            remote_blob_pack_requests: self.stats.remote_blob_pack_requests.load(Ordering::Relaxed),
953            remote_blob_pack_blobs: self.stats.remote_blob_pack_blobs.load(Ordering::Relaxed),
954            remote_blob_transfer_duration_ns: self
955                .stats
956                .remote_blob_transfer_duration_ns
957                .load(Ordering::Relaxed),
958            local_cas_write_duration_ns: self
959                .stats
960                .local_cas_write_duration_ns
961                .load(Ordering::Relaxed),
962            prefetch_runs: self.stats.prefetch_runs.load(Ordering::Relaxed),
963            prefetch_duration_ns: self.stats.prefetch_duration_ns.load(Ordering::Relaxed),
964            materialization_duration_ns: self
965                .stats
966                .materialization_duration_ns
967                .load(Ordering::Relaxed),
968            restored_output_files: self.stats.restored_output_files.load(Ordering::Relaxed),
969            restored_output_bytes: self.stats.restored_output_bytes.load(Ordering::Relaxed),
970            reflinked_output_files: self.stats.reflinked_output_files.load(Ordering::Relaxed),
971            reflinked_output_bytes: self.stats.reflinked_output_bytes.load(Ordering::Relaxed),
972            copied_output_files: self.stats.copied_output_files.load(Ordering::Relaxed),
973            copied_output_bytes: self.stats.copied_output_bytes.load(Ordering::Relaxed),
974            reused_output_files: self.stats.reused_output_files.load(Ordering::Relaxed),
975            reused_output_bytes: self.stats.reused_output_bytes.load(Ordering::Relaxed),
976        }
977    }
978
979    /// Handle requests without a transport connection.
980    ///
981    /// Persistent wrappers use this entry point when Cargo invokes mbx outside
982    /// an orchestrated session. It intentionally has the same response
983    /// semantics as [`Self::handle_connection`], while leaving framing and the
984    /// version handshake to callers that actually cross a process boundary.
985    pub async fn handle_requests(
986        &self,
987        requests: impl IntoIterator<Item = AgentRequest>,
988    ) -> Vec<AgentResponse> {
989        let mut connection = ConnectionUploads::default();
990        let mut responses = Vec::new();
991        for request in requests {
992            responses.push(self.respond_on(request, &mut connection).await);
993        }
994        responses
995    }
996
997    fn write_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
998        Self::digest_lock(&self.write_locks, digest)
999    }
1000
1001    fn action_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1002        Self::digest_lock(&self.action_locks, digest)
1003    }
1004
1005    fn digest_lock(
1006        locks: &Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>,
1007        digest: &CacheDigest,
1008    ) -> Arc<tokio::sync::Mutex<()>> {
1009        let mut locks = locks.lock().unwrap();
1010        locks.retain(|_, lock| lock.strong_count() > 0);
1011        if let Some(lock) = locks.get(digest).and_then(Weak::upgrade) {
1012            return lock;
1013        }
1014        let lock = Arc::new(tokio::sync::Mutex::new(()));
1015        locks.insert(digest.clone(), Arc::downgrade(&lock));
1016        lock
1017    }
1018
1019    /// Answer one request outside any connection.
1020    ///
1021    /// An upload queued this way has no sibling requests to order against, so
1022    /// the queue falls back to treating every blob it holds as a prerequisite.
1023    #[cfg(test)]
1024    async fn respond(&self, request: AgentRequest) -> AgentResponse {
1025        self.respond_on(request, &mut ConnectionUploads::default())
1026            .await
1027    }
1028
1029    async fn respond_on(
1030        &self,
1031        request: AgentRequest,
1032        connection: &mut ConnectionUploads,
1033    ) -> AgentResponse {
1034        let result = match request {
1035            AgentRequest::BeginTask { task } => self
1036                .begin_task(&task)
1037                .await
1038                .map(|run| AgentResponse::TaskBegun { run }),
1039            AgentRequest::CommitTask { run } => self
1040                .commit_task(&run)
1041                .await
1042                .map(|()| AgentResponse::TaskCommitted),
1043            AgentRequest::FindBlob { digest } => self.find_blob(&digest).await,
1044            AgentRequest::FindBlobs { digests } => self.find_blobs(digests).await,
1045            AgentRequest::StoreBlob { digest, source } => {
1046                self.store_blob(&digest, &source, connection).await
1047            }
1048            AgentRequest::FindActionResult { action } => {
1049                self.stats.lookups.fetch_add(1, Ordering::Relaxed);
1050                self.find_action_result(&action).await
1051            }
1052            AgentRequest::RecordActionHit {
1053                action,
1054                restore,
1055                crate_name,
1056            } => self.record_action_hit(&action, restore, crate_name),
1057            AgentRequest::RecordBypass { kind } => {
1058                *self
1059                    .stats
1060                    .bypasses
1061                    .lock()
1062                    .unwrap()
1063                    .entry(kind.clone())
1064                    .or_insert(0) += 1;
1065                self.emit(|| AgentEvent::Bypass { kind });
1066                Ok(AgentResponse::BypassRecorded)
1067            }
1068            AgentRequest::RecordUnconsulted => {
1069                self.stats.unconsulted.fetch_add(1, Ordering::Relaxed);
1070                self.emit(|| AgentEvent::Unconsulted);
1071                Ok(AgentResponse::UnconsultedRecorded)
1072            }
1073            AgentRequest::RecordWarning { message } => self.record_warning(message),
1074            AgentRequest::FindFileDigests { scope, files } => self.find_file_digests(scope, files),
1075            AgentRequest::JoinActionPromise {
1076                adapter,
1077                invocation,
1078            } => self.join_action_promise(&adapter, &invocation).await,
1079            AgentRequest::CompleteActionPromise { claim, prediction } => {
1080                self.complete_action_promise(&claim, &prediction).await
1081            }
1082            AgentRequest::RecordFileDigests { scope, entries } => {
1083                self.record_file_digests(scope, entries)
1084            }
1085            AgentRequest::RecordCompilerInvocation {
1086                outcome,
1087                crate_name,
1088                duration_ns,
1089            } => self.record_compiler_invocation(&outcome, crate_name.as_deref(), duration_ns),
1090            AgentRequest::RecordActionVerification { matched, restore } => {
1091                self.record_materialization(restore);
1092                self.stats.verifications.fetch_add(1, Ordering::Relaxed);
1093                if !matched {
1094                    self.stats.divergences.fetch_add(1, Ordering::Relaxed);
1095                }
1096                self.emit(|| AgentEvent::Verification { matched, restore });
1097                Ok(AgentResponse::ActionVerificationRecorded)
1098            }
1099            AgentRequest::StoreActionResult { result } => {
1100                self.store_action_result(&result, connection).await
1101            }
1102            AgentRequest::FindActionPrediction { task, invocation } => {
1103                self.find_action_prediction(&task, &invocation)
1104            }
1105            AgentRequest::RecordActionPrediction { task, prediction } => {
1106                self.record_action_prediction(&task, prediction)
1107            }
1108            AgentRequest::FindExecutableIdentity {
1109                executable,
1110                environment,
1111            } => self.find_executable_identity(executable, environment),
1112            AgentRequest::StoreExecutableIdentity {
1113                executable,
1114                environment,
1115                stdout,
1116            } => self.store_executable_identity(executable, environment, stdout),
1117            AgentRequest::Hello { .. } => {
1118                Err(eyre::eyre!("hello is only valid as the first request"))
1119            }
1120        };
1121        result.unwrap_or_else(|error| AgentResponse::Error {
1122            message: error.to_string(),
1123        })
1124    }
1125
1126    async fn find_blob(&self, digest: &CacheDigest) -> Result<AgentResponse> {
1127        if let Some(path) = self.find_verified_blob(digest)? {
1128            return Ok(AgentResponse::Blob { path: Some(path) });
1129        }
1130        if !self.remote_mode.reads() {
1131            return Ok(AgentResponse::Blob { path: None });
1132        }
1133        let Some(remote) = &self.remote else {
1134            return Ok(AgentResponse::Blob { path: None });
1135        };
1136        match self.fetch_remote_blob(remote, digest).await {
1137            Ok(path) => Ok(AgentResponse::Blob { path: Some(path) }),
1138            Err(error) => {
1139                warn!(
1140                    "remote cache blob lookup failed for {}: {error}",
1141                    digest.hash
1142                );
1143                Ok(AgentResponse::Blob { path: None })
1144            }
1145        }
1146    }
1147
1148    async fn find_blobs(&self, digests: Vec<CacheDigest>) -> Result<AgentResponse> {
1149        let mut paths = BTreeMap::new();
1150        let mut missing = Vec::new();
1151        for digest in &digests {
1152            match self.find_verified_blob(digest)? {
1153                Some(path) => {
1154                    paths.insert(digest.clone(), path);
1155                }
1156                None => {
1157                    missing.push(digest.clone());
1158                }
1159            }
1160        }
1161
1162        if !missing.is_empty()
1163            && self.remote_mode.reads()
1164            && let Some(remote) = &self.remote
1165        {
1166            paths.extend(self.fetch_remote_blobs(remote, missing, None).await);
1167        }
1168
1169        Ok(AgentResponse::Blobs {
1170            paths: digests
1171                .into_iter()
1172                .map(|digest| paths.get(&digest).cloned())
1173                .collect(),
1174        })
1175    }
1176
1177    async fn store_blob(
1178        &self,
1179        digest: &CacheDigest,
1180        source: &Path,
1181        connection: &mut ConnectionUploads,
1182    ) -> Result<AgentResponse> {
1183        let path = {
1184            let lock = self.write_lock(digest);
1185            let _guard = lock.lock().await;
1186            if let Some(path) = self.find_verified_blob(digest)? {
1187                path
1188            } else {
1189                let path = self.cas.store_file(digest, source)?;
1190                self.remember_verified_blob(digest, &path);
1191                self.stats.stores.fetch_add(1, Ordering::Relaxed);
1192                self.stats
1193                    .stored_bytes
1194                    .fetch_add(digest.size, Ordering::Relaxed);
1195                path
1196            }
1197        };
1198        // The object is durable locally, so the build has what it needs and the
1199        // remote publication can happen after this request returns.
1200        if let Some(uploads) = &self.uploads {
1201            uploads.queue_blob(digest, path.clone(), connection);
1202        }
1203        Ok(AgentResponse::Stored { path })
1204    }
1205
1206    fn find_verified_blob(&self, digest: &CacheDigest) -> Result<Option<PathBuf>> {
1207        let remembered = self.verified_blobs.lock().unwrap().get(digest).cloned();
1208        if let Some(remembered) = remembered {
1209            // The contents behind this digest were hashed in full when the
1210            // session first reached for them. Hashing again on every later
1211            // lookup would re-read each dependency's rlib once per crate that
1212            // links it, so what is rechecked here is that the file has not
1213            // been written since: an overwrite moves the modification time,
1214            // and truncation or eviction changes the length or removes it.
1215            // Only a replacement that reproduces both -- which no writer in
1216            // the store does, since blobs are published by rename under a
1217            // content-derived name -- would go unnoticed until `mbx cache
1218            // verify` reads the store back in full.
1219            if remembered.is_unchanged() {
1220                return Ok(Some(remembered.path));
1221            }
1222            self.verified_blobs.lock().unwrap().remove(digest);
1223        }
1224        let path = self.cas.find(digest)?;
1225        if let Some(path) = &path {
1226            self.remember_verified_blob(digest, path);
1227        }
1228        Ok(path)
1229    }
1230
1231    fn remember_verified_blob(&self, digest: &CacheDigest, path: &Path) {
1232        let Some(verified) = VerifiedBlob::describe(path) else {
1233            // Without an identity to compare against there is nothing to
1234            // shortcut safely, so the digest stays unremembered and every
1235            // lookup re-reads it.
1236            return;
1237        };
1238        self.verified_blobs
1239            .lock()
1240            .unwrap()
1241            .insert(digest.clone(), verified);
1242    }
1243
1244    async fn find_action_result(&self, action: &CacheDigest) -> Result<AgentResponse> {
1245        if let Some(result) = self.actions.find(action)? {
1246            return Ok(AgentResponse::ActionResult {
1247                result: Some(result),
1248            });
1249        }
1250        if !self.remote_mode.reads() {
1251            return Ok(AgentResponse::ActionResult { result: None });
1252        }
1253        let Some(remote) = &self.remote else {
1254            return Ok(AgentResponse::ActionResult { result: None });
1255        };
1256        let lock = self.action_lock(action);
1257        let _guard = lock.lock().await;
1258        if let Some(result) = self.actions.find(action)? {
1259            return Ok(AgentResponse::ActionResult {
1260                result: Some(result),
1261            });
1262        }
1263        if let Some(result) = self
1264            .pending_remote_actions
1265            .lock()
1266            .unwrap()
1267            .get(action)
1268            .cloned()
1269        {
1270            return Ok(AgentResponse::ActionResult {
1271                result: Some(result),
1272            });
1273        }
1274        let _permit = self.remote_transfers.acquire().await?;
1275        match self.get_remote_action_result(remote, action).await {
1276            Ok(Some(result)) => {
1277                self.pending_remote_actions
1278                    .lock()
1279                    .unwrap()
1280                    .insert(action.clone(), result.clone());
1281                Ok(AgentResponse::ActionResult {
1282                    result: Some(result),
1283                })
1284            }
1285            Ok(None) => Ok(AgentResponse::ActionResult { result: None }),
1286            Err(error) => {
1287                self.note_remote_failure();
1288                warn!(
1289                    "remote cache action lookup failed for {}: {error}",
1290                    action.hash
1291                );
1292                Ok(AgentResponse::ActionResult { result: None })
1293            }
1294        }
1295    }
1296
1297    async fn store_action_result(
1298        &self,
1299        result: &RemoteActionResult,
1300        connection: &ConnectionUploads,
1301    ) -> Result<AgentResponse> {
1302        let path = self.actions.store(result)?;
1303        if let Some(uploads) = &self.uploads {
1304            uploads.queue_action_result(result, connection);
1305        }
1306        Ok(AgentResponse::ActionStored { path })
1307    }
1308
1309    async fn join_action_promise(
1310        &self,
1311        adapter: &str,
1312        invocation: &CacheDigest,
1313    ) -> Result<AgentResponse> {
1314        if !self.remote_mode.reads() || !self.remote_mode.writes() {
1315            return Ok(AgentResponse::ActionPromise {
1316                claim: None,
1317                prediction: None,
1318            });
1319        }
1320        let Some(remote) = &self.remote else {
1321            return Ok(AgentResponse::ActionPromise {
1322                claim: None,
1323                prediction: None,
1324            });
1325        };
1326        let deadline = Instant::now() + ACTION_PROMISE_WAIT;
1327        loop {
1328            let _permit = self.remote_transfers.acquire().await?;
1329            let state = remote.join_action_promise(invocation, adapter).await;
1330            drop(_permit);
1331            match state {
1332                Ok(Some(ActionPromiseState::Claimed { claim })) => {
1333                    return Ok(AgentResponse::ActionPromise {
1334                        claim: Some(claim),
1335                        prediction: None,
1336                    });
1337                }
1338                Ok(Some(ActionPromiseState::Complete { prediction })) => {
1339                    return Ok(AgentResponse::ActionPromise {
1340                        claim: None,
1341                        prediction: Some(prediction),
1342                    });
1343                }
1344                Ok(Some(ActionPromiseState::Pending { retry_after_ms }))
1345                    if Instant::now() < deadline =>
1346                {
1347                    tokio::time::sleep(Duration::from_millis(retry_after_ms.clamp(10, 5_000)))
1348                        .await;
1349                }
1350                Ok(Some(ActionPromiseState::Pending { .. }) | None) => {
1351                    return Ok(AgentResponse::ActionPromise {
1352                        claim: None,
1353                        prediction: None,
1354                    });
1355                }
1356                Ok(Some(_)) => {
1357                    return Ok(AgentResponse::ActionPromise {
1358                        claim: None,
1359                        prediction: None,
1360                    });
1361                }
1362                Err(error) => {
1363                    self.note_remote_failure();
1364                    warn!(
1365                        "remote cache action promise failed for {}: {error}",
1366                        invocation.hash
1367                    );
1368                    return Ok(AgentResponse::ActionPromise {
1369                        claim: None,
1370                        prediction: None,
1371                    });
1372                }
1373            }
1374        }
1375    }
1376
1377    async fn complete_action_promise(
1378        &self,
1379        claim: &str,
1380        prediction: &ActionPrediction,
1381    ) -> Result<AgentResponse> {
1382        prediction.validate()?;
1383        let Some(remote) = &self.remote else {
1384            return Ok(AgentResponse::ActionPromiseCompleted);
1385        };
1386        let Some(uploads) = &self.uploads else {
1387            return Ok(AgentResponse::ActionPromiseCompleted);
1388        };
1389        if uploads
1390            .wait_for_actions(std::slice::from_ref(&prediction.action))
1391            .await
1392            .contains(&prediction.action)
1393        {
1394            // Never promise an action result the server does not hold. The
1395            // server lease expires and another runner gets to repair it.
1396            return Ok(AgentResponse::ActionPromiseCompleted);
1397        }
1398        let completion = ActionPromiseCompletion {
1399            claim: claim.to_string(),
1400            prediction: prediction.clone(),
1401        };
1402        let _permit = self.remote_transfers.acquire().await?;
1403        if let Err(error) = remote
1404            .complete_action_promise(&prediction.invocation, &completion)
1405            .await
1406        {
1407            self.note_remote_failure();
1408            warn!(
1409                "remote cache action promise completion failed for {}: {error}",
1410                prediction.invocation.hash
1411            );
1412        }
1413        Ok(AgentResponse::ActionPromiseCompleted)
1414    }
1415
1416    /// Record that a remote operation failed and the build carried on without it.
1417    fn note_remote_failure(&self) {
1418        self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
1419    }
1420
1421    async fn get_remote_action_result(
1422        &self,
1423        remote: &RemoteCacheClient,
1424        action: &CacheDigest,
1425    ) -> Result<Option<RemoteActionResult>> {
1426        self.stats
1427            .remote_action_lookups
1428            .fetch_add(1, Ordering::Relaxed);
1429        let _timer = AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);
1430        remote.get_action_result(action).await
1431    }
1432
1433    fn record_action_hit(
1434        &self,
1435        action: &CacheDigest,
1436        restore: RestoreStats,
1437        crate_name: Option<String>,
1438    ) -> Result<AgentResponse> {
1439        validate_crate_name(crate_name.as_deref())?;
1440        if self.actions.find(action)?.is_none() {
1441            let pending = self.pending_remote_actions.lock().unwrap().remove(action);
1442            if let Some(result) = pending {
1443                self.actions.store(&result)?;
1444            } else {
1445                bail!("cannot record a hit for a missing action result");
1446            }
1447        }
1448        self.record_restore(restore);
1449        self.stats.hits.fetch_add(1, Ordering::Relaxed);
1450        self.emit(|| AgentEvent::ActionHit {
1451            crate_name,
1452            restore,
1453        });
1454        Ok(AgentResponse::ActionHitRecorded)
1455    }
1456
1457    fn record_restore(&self, restore: RestoreStats) {
1458        self.record_materialization(restore);
1459        atomic_saturating_add(
1460            &self.stats.avoided_compiler_duration_ns,
1461            restore.avoided_compiler_duration_ns,
1462        );
1463        atomic_saturating_add(&self.stats.restored_output_files, restore.output_files);
1464        atomic_saturating_add(&self.stats.restored_output_bytes, restore.output_bytes);
1465        atomic_saturating_add(
1466            &self.stats.reflinked_output_files,
1467            restore.reflinked_output_files,
1468        );
1469        atomic_saturating_add(
1470            &self.stats.reflinked_output_bytes,
1471            restore.reflinked_output_bytes,
1472        );
1473        atomic_saturating_add(&self.stats.copied_output_files, restore.copied_output_files);
1474        atomic_saturating_add(&self.stats.copied_output_bytes, restore.copied_output_bytes);
1475        atomic_saturating_add(&self.stats.reused_output_files, restore.reused_output_files);
1476        atomic_saturating_add(&self.stats.reused_output_bytes, restore.reused_output_bytes);
1477    }
1478
1479    fn record_compiler_invocation(
1480        &self,
1481        outcome: &str,
1482        crate_name: Option<&str>,
1483        duration_ns: u64,
1484    ) -> Result<AgentResponse> {
1485        if !matches!(
1486            outcome,
1487            "miss" | "unconsulted" | "bypass" | "verification" | "incremental"
1488        ) {
1489            bail!("invalid compiler invocation outcome");
1490        }
1491        validate_crate_name(crate_name)?;
1492        let mut compiler = self.stats.compiler.lock().unwrap();
1493        let stats = compiler.entry(outcome.to_string()).or_default();
1494        stats.invocations = stats.invocations.saturating_add(1);
1495        stats.duration_ns = stats.duration_ns.saturating_add(duration_ns);
1496        drop(compiler);
1497        if outcome != "verification"
1498            && let Some(crate_name) = crate_name.filter(|name| !name.is_empty())
1499        {
1500            let mut slow = self.stats.slow_compilations.lock().unwrap();
1501            let duration = slow.entry(crate_name.to_string()).or_default();
1502            *duration = duration.saturating_add(duration_ns);
1503        }
1504        self.emit(|| AgentEvent::CompilerInvocation {
1505            outcome: outcome.to_string(),
1506            crate_name: crate_name.map(str::to_string),
1507            duration_ns,
1508        });
1509        Ok(AgentResponse::CompilerInvocationRecorded)
1510    }
1511
1512    fn record_materialization(&self, restore: RestoreStats) {
1513        atomic_saturating_add(&self.stats.materialization_duration_ns, restore.duration_ns);
1514    }
1515
1516    /// Surface a shim diagnostic on this process's stderr.
1517    ///
1518    /// The agent lives in the process that owns the session, so printing here
1519    /// reaches the terminal running the build rather than the stderr of the
1520    /// compilation the shim stands in for -- which build scripts read as part
1521    /// of the compiler's answer. Deduplicated because one cause tends to fire
1522    /// once per compilation, and capped so a message unique per compilation
1523    /// cannot scroll the build away.
1524    fn record_warning(&self, message: String) -> Result<AgentResponse> {
1525        if message.is_empty()
1526            || message.len() > MAX_WARNING_BYTES
1527            || message.contains(['\n', '\r', '\0'])
1528        {
1529            bail!("invalid shim warning");
1530        }
1531        let mut warnings = self.warnings.lock().unwrap();
1532        if !warnings.contains(&message) && warnings.len() < MAX_WARNINGS {
1533            eprintln!("mbx[warning]: {message}");
1534            self.emit(|| AgentEvent::Warning {
1535                message: message.clone(),
1536            });
1537            warnings.insert(message);
1538        }
1539        Ok(AgentResponse::WarningRecorded)
1540    }
1541
1542    /// Answer file-digest lookups from the session ledger.
1543    ///
1544    /// A recorded digest is returned only when the requested identity matches
1545    /// the recorded one exactly. The caller statted the file to build the
1546    /// identity, so this is the [`VerifiedBlob`] freshness check with the
1547    /// stat moved to the side that was already making it.
1548    fn find_file_digests(
1549        &self,
1550        scope: FileDigestScope,
1551        files: Vec<FileIdentity>,
1552    ) -> Result<AgentResponse> {
1553        if files.len() > MAX_FILE_DIGEST_BATCH {
1554            bail!("too many file-digest lookups in one request");
1555        }
1556        let ledger = self.file_digests.lock().unwrap();
1557        let digests = files
1558            .into_iter()
1559            .map(|file| {
1560                let recorded = ledger.get(&(scope, file.path.clone()))?;
1561                (recorded.file == file).then(|| recorded.digest.clone())
1562            })
1563            .collect();
1564        Ok(AgentResponse::FileDigests { digests })
1565    }
1566
1567    /// Record digests of files a shim read in full, for later reuse.
1568    ///
1569    /// Capped rather than evicted: a session that outgrows the cap loses the
1570    /// shortcut for the overflow and nothing else, and no workload observed so
1571    /// far comes near it.
1572    fn record_file_digests(
1573        &self,
1574        scope: FileDigestScope,
1575        entries: Vec<RecordedFileDigest>,
1576    ) -> Result<AgentResponse> {
1577        if entries.len() > MAX_FILE_DIGEST_BATCH {
1578            bail!("too many file-digest records in one request");
1579        }
1580        for entry in &entries {
1581            if !entry.file.path.is_absolute() {
1582                bail!("file-digest records need absolute paths");
1583            }
1584            entry.digest.validate()?;
1585            if entry.file.len != entry.digest.size {
1586                bail!("file-digest record length does not match its digest");
1587            }
1588        }
1589        let mut ledger = self.file_digests.lock().unwrap();
1590        for entry in entries {
1591            if ledger.len() >= MAX_FILE_DIGEST_ENTRIES
1592                && !ledger.contains_key(&(scope, entry.file.path.clone()))
1593            {
1594                break;
1595            }
1596            ledger.insert((scope, entry.file.path.clone()), entry);
1597        }
1598        Ok(AgentResponse::FileDigestsRecorded)
1599    }
1600
1601    fn find_action_prediction(
1602        &self,
1603        task: &str,
1604        invocation: &CacheDigest,
1605    ) -> Result<AgentResponse> {
1606        validate_task_identity(task)?;
1607        invocation.validate()?;
1608        let prediction = self
1609            .task_actions
1610            .lock()
1611            .unwrap()
1612            .get(task)
1613            .and_then(|state| state.predictions.get(invocation))
1614            .cloned();
1615        Ok(AgentResponse::ActionPrediction { prediction })
1616    }
1617
1618    fn record_action_prediction(
1619        &self,
1620        task: &str,
1621        prediction: ActionPrediction,
1622    ) -> Result<AgentResponse> {
1623        validate_task_identity(task)?;
1624        prediction.validate()?;
1625        let mut tasks = self.task_actions.lock().unwrap();
1626        let state = tasks.entry(task.to_string()).or_default();
1627        if !state.predictions.contains_key(&prediction.invocation)
1628            && state.predictions.len() >= MAX_TASK_ACTION_PREDICTIONS
1629        {
1630            bail!("task action manifest contains too many predictions");
1631        }
1632        state
1633            .predictions
1634            .insert(prediction.invocation.clone(), prediction.clone());
1635        state
1636            .pending_predictions
1637            .insert(prediction.invocation.clone(), prediction);
1638        Ok(AgentResponse::ActionPredictionRecorded)
1639    }
1640
1641    fn executable_identity_key(
1642        &self,
1643        executable: PathBuf,
1644        environment: BTreeMap<String, Option<String>>,
1645    ) -> Result<ExecutableIdentityKey> {
1646        // Restricted to the variables that actually select what an identity
1647        // probe reports: the toolchain rustup resolves, and the SDK a linker
1648        // driver builds against. Anything else would let one key stand for two
1649        // different compilers.
1650        if !environment.keys().all(|name| {
1651            matches!(
1652                name.as_str(),
1653                "RUSTUP_HOME"
1654                    | "RUSTUP_TOOLCHAIN"
1655                    | "SDKROOT"
1656                    | "MACOSX_DEPLOYMENT_TARGET"
1657                    | "LIB"
1658                    | "UCRTVersion"
1659                    | "UniversalCRTSdkDir"
1660                    | "VCToolsInstallDir"
1661                    | "VCToolsVersion"
1662                    | "WindowsSdkDir"
1663                    | "WindowsSDKVersion"
1664            )
1665        }) {
1666            bail!("executable identity contains an unsupported environment variable");
1667        }
1668        Ok(ExecutableIdentityKey {
1669            executable,
1670            environment,
1671        })
1672    }
1673
1674    fn find_executable_identity(
1675        &self,
1676        executable: PathBuf,
1677        environment: BTreeMap<String, Option<String>>,
1678    ) -> Result<AgentResponse> {
1679        let key = self.executable_identity_key(executable, environment)?;
1680        let stdout = self
1681            .executable_identities
1682            .lock()
1683            .unwrap()
1684            .get(&key)
1685            .cloned();
1686        Ok(AgentResponse::ExecutableIdentity { stdout })
1687    }
1688
1689    fn store_executable_identity(
1690        &self,
1691        executable: PathBuf,
1692        environment: BTreeMap<String, Option<String>>,
1693        stdout: Vec<u8>,
1694    ) -> Result<AgentResponse> {
1695        if stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE {
1696            bail!("executable identity exceeds {MAX_EXECUTABLE_IDENTITY_SIZE} bytes");
1697        }
1698        let key = self.executable_identity_key(executable, environment)?;
1699        let mut identities = self.executable_identities.lock().unwrap();
1700        let is_new = !identities.contains_key(&key);
1701        let previous_size = identities.get(&key).map_or(0, Vec::len);
1702        if is_new && identities.len() >= MAX_EXECUTABLE_IDENTITIES {
1703            bail!("executable identity cache contains too many entries");
1704        }
1705        let retained_bytes = identities.values().map(Vec::len).sum::<usize>();
1706        if retained_bytes - previous_size + stdout.len() > MAX_EXECUTABLE_IDENTITY_BYTES {
1707            bail!("executable identity cache contains too many bytes");
1708        }
1709        identities.insert(key, stdout.clone());
1710        Ok(AgentResponse::ExecutableIdentity {
1711            stdout: Some(stdout),
1712        })
1713    }
1714
1715    /// Serve newline-delimited protocol requests on an authenticated session stream.
1716    pub async fn handle_connection<S>(&self, stream: S) -> Result<()>
1717    where
1718        S: AsyncRead + AsyncWrite + Unpin,
1719    {
1720        let (reader, mut writer) = tokio::io::split(stream);
1721        let mut reader = BufReader::new(reader);
1722        let hello = read_request(&mut reader)
1723            .await?
1724            .ok_or_else(|| eyre::eyre!("connection closed before the agent handshake"))?;
1725        let request: AgentRequest = serde_json::from_str(&hello)?;
1726        match request {
1727            AgentRequest::Hello {
1728                protocol,
1729                client_version,
1730            } if protocol == AGENT_PROTOCOL_VERSION && client_version == self.version.as_ref() => {}
1731            AgentRequest::Hello { protocol, .. } if protocol != AGENT_PROTOCOL_VERSION => {
1732                send_response(
1733                    &mut writer,
1734                    &AgentResponse::Error {
1735                        message: format!(
1736                            "unsupported agent protocol {protocol}; expected {AGENT_PROTOCOL_VERSION}"
1737                        ),
1738                    },
1739                )
1740                .await?;
1741                return Ok(());
1742            }
1743            AgentRequest::Hello { client_version, .. } => {
1744                send_response(
1745                    &mut writer,
1746                    &AgentResponse::Error {
1747                        message: format!(
1748                            "cache client {client_version} does not match agent {}",
1749                            self.version
1750                        ),
1751                    },
1752                )
1753                .await?;
1754                return Ok(());
1755            }
1756            _ => bail!("the first agent request must be hello"),
1757        }
1758        send_response(
1759            &mut writer,
1760            &AgentResponse::Hello {
1761                protocol: AGENT_PROTOCOL_VERSION,
1762                agent_version: self.version.to_string(),
1763            },
1764        )
1765        .await?;
1766
1767        // Tickets accumulate for the life of the connection because a shim
1768        // publishes a compilation's blobs and the action result naming them over
1769        // one connection, in that order.
1770        let mut connection = ConnectionUploads::default();
1771        while let Some(line) = read_request(&mut reader).await? {
1772            let response = match serde_json::from_str(&line) {
1773                Ok(request) => self.respond_on(request, &mut connection).await,
1774                Err(error) => AgentResponse::Error {
1775                    message: format!("invalid agent request: {error}"),
1776                },
1777            };
1778            send_response(&mut writer, &response).await?;
1779        }
1780        Ok(())
1781    }
1782}
1783
1784/// Read one newline-delimited request, refusing one that grows past the cap.
1785///
1786/// Any process running as this user can open the session socket, so a request
1787/// that never terminates its line must not be able to grow the agent's memory
1788/// without bound.
1789async fn read_request<R>(reader: &mut R) -> Result<Option<String>>
1790where
1791    R: AsyncBufRead + Unpin,
1792{
1793    let mut line = Vec::new();
1794    loop {
1795        let available = reader.fill_buf().await?;
1796        if available.is_empty() {
1797            break;
1798        }
1799        let (consumed, complete) = match available.iter().position(|byte| *byte == b'\n') {
1800            Some(index) => (index, true),
1801            None => (available.len(), false),
1802        };
1803        if line.len() + consumed > MAX_REQUEST_BYTES {
1804            bail!("agent request exceeded {MAX_REQUEST_BYTES} bytes");
1805        }
1806        line.extend_from_slice(&available[..consumed]);
1807        // The newline itself is consumed but never kept.
1808        reader.consume(consumed + usize::from(complete));
1809        if complete {
1810            return Ok(Some(String::from_utf8(line)?));
1811        }
1812    }
1813    if line.is_empty() {
1814        Ok(None)
1815    } else {
1816        Ok(Some(String::from_utf8(line)?))
1817    }
1818}
1819
1820async fn send_response(
1821    writer: &mut (impl AsyncWrite + Unpin),
1822    response: &AgentResponse,
1823) -> Result<()> {
1824    let mut encoded = serde_json::to_vec(response)?;
1825    encoded.push(b'\n');
1826    writer.write_all(&encoded).await?;
1827    writer.flush().await?;
1828    Ok(())
1829}
1830
1831#[cfg(test)]
1832#[path = "agent_tests.rs"]
1833mod tests;