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