Skip to main content

mbx_cache_core/
agent.rs

1use crate::uploads::{ConnectionUploads, UploadQueue, UploadSink};
2use crate::{
3    ActionPrediction, BlobPackLimits, CacheDigest, CacheDirectory, LocalActionCache, LocalCas,
4    MAX_STAGED_BLOB_PACK_BYTES, MAX_STAGED_BLOB_PACK_ITEMS, ManifestPutOutcome, RemoteActionResult,
5    RemoteCacheClient, RemoteCacheMode, RustcMetadata, TaskActionManifest, blob_pack_chunk,
6    canonical_json,
7};
8use eyre::{Context, Result, bail};
9use futures_util::{FutureExt, StreamExt, future::BoxFuture, stream};
10use log::warn;
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, BTreeSet};
13use std::fs;
14use std::path::{Path, PathBuf};
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::{Arc, Mutex, Weak};
17use std::time::{Duration, Instant};
18use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
19
20const MAX_EXECUTABLE_IDENTITIES: usize = 64;
21const MAX_EXECUTABLE_IDENTITY_SIZE: usize = 64 * 1024;
22const MAX_EXECUTABLE_IDENTITY_BYTES: usize = 256 * 1024;
23const TASK_ACTION_MANIFEST_VERSION: u8 = 1;
24const MAX_TASK_ACTION_PREDICTIONS: usize = 16 * 1024;
25const MAX_REMOTE_TRANSFERS: usize = 64;
26const MAX_PREFETCH_TRANSFERS: usize = 48;
27const MAX_PREFETCH_ACTION_BATCH: usize = 256;
28/// Batched action lookups issued at once, well inside the transfer budget: each
29/// asks about hundreds of actions, so a handful covers a large workspace.
30const MAX_PREFETCH_BATCH_LOOKUPS: usize = 4;
31const PREFETCH_ACTION_BATCH_DELAY: Duration = Duration::from_millis(5);
32const MAX_PREFETCH_DIRECTORY_OBJECTS: usize = 100_000;
33const MAX_PREFETCH_OBJECTS_PER_WAVE: usize = 100_000;
34const DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES: u64 = 5 * 1024 * 1024 * 1024;
35
36/// Remote action-cache access owned by one task session.
37pub struct AgentRemoteCache {
38    /// Remote protocol client used by the agent.
39    pub client: RemoteCacheClient,
40    /// Permitted remote read/write operations.
41    pub mode: RemoteCacheMode,
42    /// Directory used for verified downloads before CAS ingestion.
43    pub staging_dir: PathBuf,
44}
45
46/// Wire protocol version used between an in-process cache agent and its shims.
47pub const AGENT_PROTOCOL_VERSION: u8 = 3;
48/// Largest single protocol request the agent will read.
49///
50/// Requests are small JSON objects; the largest legitimate ones carry an output
51/// tree or a batch of digests, which stay far below this.
52const MAX_REQUEST_BYTES: usize = 16 * 1024 * 1024;
53
54/// A request accepted by the task-scoped cache agent.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(tag = "type", rename_all = "snake_case")]
57pub enum AgentRequest {
58    /// Negotiate protocol and application versions.
59    Hello {
60        /// Agent protocol version understood by the caller.
61        protocol: u8,
62        /// Human-readable mbx client version.
63        client_version: String,
64    },
65    /// Begin a prediction-manifest run for a stable Cargo or task identity.
66    BeginTask {
67        /// Stable 64-character lowercase hexadecimal task identity.
68        task: String,
69    },
70    /// Commit predictions collected by an earlier [`Self::BeginTask`].
71    CommitTask {
72        /// Opaque run identifier returned by the agent.
73        run: String,
74    },
75    /// Resolve a blob to a session-verified local CAS path.
76    FindBlob {
77        /// Blob to resolve.
78        digest: CacheDigest,
79    },
80    /// Resolve blobs to session-verified local CAS paths.
81    FindBlobs {
82        /// Blobs to resolve, preserving request order in the response.
83        digests: Vec<CacheDigest>,
84    },
85    /// Import a file into the local content-addressed store.
86    StoreBlob {
87        /// Digest the source must match.
88        digest: CacheDigest,
89        /// File to verify and import.
90        source: PathBuf,
91    },
92    /// Look up an action-result record.
93    FindActionResult {
94        /// Action digest to resolve.
95        action: CacheDigest,
96    },
97    /// Account for a successfully restored cache hit.
98    RecordActionHit {
99        /// Action that supplied the outputs.
100        action: CacheDigest,
101        /// Restoration work performed by the adapter.
102        restore: RestoreStats,
103        /// Compiler crate name, when the invocation supplied one.
104        #[serde(default)]
105        crate_name: Option<String>,
106    },
107    /// A compilation the adapter declined to cache, grouped by reason.
108    RecordBypass {
109        /// Stable, low-cardinality bypass-reason name.
110        kind: String,
111    },
112    /// A compilation the adapter could not look up, having no key to look up
113    /// with. Distinct from a bypass: these are cached once compiled.
114    RecordUnconsulted,
115    /// Account for one real compiler invocation performed by the adapter.
116    RecordCompilerInvocation {
117        /// Stable outcome category such as `miss`, `unconsulted`, `bypass`, or
118        /// `incremental` for a compilation the adapter deliberately ran with
119        /// incremental state instead of publishing.
120        outcome: String,
121        /// Compiler crate name, when the invocation supplied one.
122        crate_name: Option<String>,
123        /// Wall time spent running the compiler.
124        duration_ns: u64,
125    },
126    /// Account for a cache hit that was rebuilt for correctness verification.
127    RecordActionVerification {
128        /// Whether rebuilt and cached outputs matched.
129        matched: bool,
130        /// Restoration work performed before rebuilding.
131        restore: RestoreStats,
132    },
133    /// Store an action-result record locally and enqueue remote publication.
134    StoreActionResult {
135        /// Action-result record to store.
136        result: RemoteActionResult,
137    },
138    /// Find an earlier input prediction for a task and invocation.
139    FindActionPrediction {
140        /// Stable task identity.
141        task: String,
142        /// Digest of the compiler invocation without discovered inputs.
143        invocation: CacheDigest,
144    },
145    /// Record an input prediction after a successful compilation.
146    RecordActionPrediction {
147        /// Stable task identity.
148        task: String,
149        /// Adapter-owned prediction record.
150        prediction: ActionPrediction,
151    },
152    /// Find cached identity output for an executable and environment.
153    FindExecutableIdentity {
154        /// Executable whose identity command would run.
155        executable: PathBuf,
156        /// Environment variables affecting identity output.
157        environment: BTreeMap<String, Option<String>>,
158    },
159    /// Cache identity output for an executable and environment.
160    StoreExecutableIdentity {
161        /// Executable whose identity command ran.
162        executable: PathBuf,
163        /// Environment variables affecting identity output.
164        environment: BTreeMap<String, Option<String>>,
165        /// Captured identity-command standard output.
166        stdout: Vec<u8>,
167    },
168}
169
170/// Local output restoration work performed by one action-cache adapter hit.
171#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(deny_unknown_fields)]
173pub struct RestoreStats {
174    /// Cumulative time spent materializing and validating output files.
175    pub duration_ns: u64,
176    /// Compiler wall time recorded when this action was originally produced.
177    /// Zero means no timing hint was available.
178    pub avoided_compiler_duration_ns: u64,
179    /// Number of compiler output files restored.
180    pub output_files: u64,
181    /// Declared size of compiler output files restored.
182    pub output_bytes: u64,
183    /// Number of restored output files that share data blocks with the CAS.
184    pub reflinked_output_files: u64,
185    /// Declared size of restored outputs that share data blocks with the CAS.
186    pub reflinked_output_bytes: u64,
187    /// Number of restored output files that required a byte-for-byte copy.
188    pub copied_output_files: u64,
189    /// Declared size of restored outputs that required a byte-for-byte copy.
190    pub copied_output_bytes: u64,
191}
192
193/// One accounted cache decision, as it happens.
194///
195/// The agent already folds every one of these into [`AgentStats`]; an observer
196/// sees the same decisions individually, before that summing loses the crate
197/// they belong to. Delivered synchronously from the request handler, so an
198/// observer that blocks slows the build it is watching.
199#[derive(Debug, Clone)]
200#[non_exhaustive]
201pub enum AgentEvent {
202    /// An action's outputs were restored from cache.
203    ActionHit {
204        /// Compiler crate name, when the invocation supplied one.
205        crate_name: Option<String>,
206        /// Restoration work performed by the adapter.
207        restore: RestoreStats,
208    },
209    /// A compilation the adapter declined to cache.
210    Bypass {
211        /// Stable, low-cardinality bypass-reason name.
212        kind: String,
213    },
214    /// A compilation no lookup was possible for.
215    Unconsulted,
216    /// A real compiler invocation ran.
217    CompilerInvocation {
218        /// Stable outcome category such as `miss`, `unconsulted`, or `bypass`.
219        outcome: String,
220        /// Compiler crate name, when the invocation supplied one.
221        crate_name: Option<String>,
222        /// Wall time spent running the compiler.
223        duration_ns: u64,
224    },
225    /// A hit was rebuilt to verify it.
226    Verification {
227        /// Whether rebuilt and cached outputs matched.
228        matched: bool,
229        /// Restoration work performed before rebuilding.
230        restore: RestoreStats,
231    },
232}
233
234/// A sink for [`AgentEvent`]s observed during one session.
235pub trait AgentEventObserver: Send + Sync {
236    /// Handle one event. Must not panic, and should not block.
237    fn event(&self, event: AgentEvent);
238}
239
240/// A response returned by the task-scoped cache agent.
241#[derive(Debug, Clone, Serialize, Deserialize)]
242#[serde(tag = "type", rename_all = "snake_case")]
243pub enum AgentResponse {
244    /// Successful protocol negotiation.
245    Hello {
246        /// Agent protocol version.
247        protocol: u8,
248        /// Human-readable agent version.
249        agent_version: String,
250    },
251    /// A task prediction run was begun.
252    TaskBegun {
253        /// Opaque run identifier to pass to compiler shims and commit later.
254        run: String,
255    },
256    /// A task prediction run was committed.
257    TaskCommitted,
258    /// A local CAS path already verified against the requested digest.
259    Blob {
260        /// Verified local path, or `None` on a cache miss.
261        path: Option<PathBuf>,
262    },
263    /// Local CAS paths already verified against the requested digests.
264    Blobs {
265        /// Verified local paths or misses, in request order.
266        paths: Vec<Option<PathBuf>>,
267    },
268    /// A blob was stored locally.
269    Stored {
270        /// Path of the stored object in the local CAS.
271        path: PathBuf,
272    },
273    /// Result of an action lookup.
274    ActionResult {
275        /// Validated action result, or `None` on a cache miss.
276        result: Option<RemoteActionResult>,
277    },
278    /// Hit statistics were updated.
279    ActionHitRecorded,
280    /// Verification statistics were updated.
281    ActionVerificationRecorded,
282    /// Bypass statistics were updated.
283    BypassRecorded,
284    /// Unconsulted-compilation statistics were updated.
285    UnconsultedRecorded,
286    /// Compiler invocation accounting was recorded.
287    CompilerInvocationRecorded,
288    /// An action result was stored.
289    ActionStored {
290        /// Path of the stored local action-result record.
291        path: PathBuf,
292    },
293    /// Result of an input-prediction lookup.
294    ActionPrediction {
295        /// Matching prediction, or `None` when none is known.
296        prediction: Option<ActionPrediction>,
297    },
298    /// An input prediction was recorded.
299    ActionPredictionRecorded,
300    /// Result of an executable-identity lookup.
301    ExecutableIdentity {
302        /// Captured output, or `None` when no identity is cached.
303        stdout: Option<Vec<u8>>,
304    },
305    /// The request failed without terminating the agent connection.
306    Error {
307        /// Human-readable failure description.
308        message: String,
309    },
310}
311
312/// Aggregate cache activity for one task session.
313///
314/// The agent produces these; nothing outside this crate has cause to build one.
315/// Saying so keeps a new counter from being a breaking change, which is what
316/// this type exists to accumulate -- reach for [`AgentStats::default`] and
317/// assign the fields a test needs.
318#[derive(Debug, Clone, Default, PartialEq, Eq)]
319#[non_exhaustive]
320pub struct AgentStats {
321    /// End-to-end lifetime of the task-scoped cache session.
322    pub session_duration_ns: u64,
323    /// Number of action-result lookups.
324    pub lookups: u64,
325    /// Compilations no action-result lookup was possible for, because no usable
326    /// action key was available.
327    ///
328    /// Counted separately from a miss, which is a lookup that found nothing.
329    /// Both compile, but only a miss says a lookup happened.
330    pub unconsulted: u64,
331    /// Number of lookups that found a valid local action result.
332    pub hits: u64,
333    /// Number of newly stored content-addressed objects.
334    pub stores: u64,
335    /// Total size of newly stored objects.
336    pub stored_bytes: u64,
337    /// Number of cache hits compiled again for qualification.
338    pub verifications: u64,
339    /// Number of qualification builds that diverged from the cached result.
340    pub divergences: u64,
341    /// CAS payload bytes downloaded from the remote cache.
342    pub downloaded_bytes: u64,
343    /// CAS payload bytes uploaded to the remote cache.
344    pub uploaded_bytes: u64,
345    /// Objects published to the remote cache after the build asked for them.
346    ///
347    /// A store request returns once the object is in the local CAS, so the
348    /// upload it implies happens off the build's critical path. This counts the
349    /// blobs and action results that publication actually completed for.
350    pub background_uploads: u64,
351    /// Queued uploads that did not publish, having been reported and recovered
352    /// from.
353    pub background_upload_failures: u64,
354    /// Framed requests that published several blobs at once.
355    pub remote_blob_pack_uploads: u64,
356    /// Blobs published through those framed requests.
357    pub remote_blob_pack_upload_blobs: u64,
358    /// Time the session spent waiting for queued uploads once the build ended.
359    pub upload_drain_duration_ns: u64,
360    /// Complete actions staged before an adapter requested them.
361    pub prefetched_actions: u64,
362    /// Compilations that were not cacheable, counted by reason.
363    pub bypasses: BTreeMap<String, u64>,
364    /// Estimated compiler time avoided by restored action hits.
365    pub avoided_compiler_duration_ns: u64,
366    /// Real compiler work performed in this session, grouped by outcome.
367    pub compiler: BTreeMap<String, CompilerStats>,
368    /// Cumulative real compiler time by crate name.
369    pub slow_compilations: BTreeMap<String, u64>,
370    /// Remote cache operations that failed and were degraded to a local result.
371    ///
372    /// A remote cache that cannot be reached, or that answers in a way this
373    /// client refuses, costs hit rate rather than correctness, so every one of
374    /// these is recovered from rather than raised. Counting them is what keeps a
375    /// remote that is failing every request from reading as one that merely had
376    /// nothing to offer.
377    pub remote_failures: u64,
378    /// Number of task manifest requests made to the remote cache.
379    pub remote_manifest_lookups: u64,
380    /// Cumulative time spent requesting remote task manifests.
381    pub remote_manifest_lookup_duration_ns: u64,
382    /// Number of action-result requests made to the remote cache.
383    pub remote_action_lookups: u64,
384    /// Cumulative time spent requesting remote action results.
385    pub remote_action_lookup_duration_ns: u64,
386    /// Number of blob requests made to the remote cache.
387    pub remote_blob_requests: u64,
388    /// Number of packed blob requests made to the remote cache.
389    pub remote_blob_pack_requests: u64,
390    /// Number of verified blobs received through packed responses.
391    pub remote_blob_pack_blobs: u64,
392    /// Cumulative time spent downloading and verifying remote blobs.
393    pub remote_blob_transfer_duration_ns: u64,
394    /// Cumulative time spent ingesting downloaded blobs into the local CAS.
395    pub local_cas_write_duration_ns: u64,
396    /// Number of speculative prefetch runs started for task manifests.
397    pub prefetch_runs: u64,
398    /// Cumulative wall time of speculative task-manifest prefetch runs.
399    pub prefetch_duration_ns: u64,
400    /// Cumulative time spent staging or materializing and validating cached outputs.
401    pub materialization_duration_ns: u64,
402    /// Number of compiler output files restored from action hits.
403    pub restored_output_files: u64,
404    /// Declared size of compiler output files restored from action hits.
405    pub restored_output_bytes: u64,
406    /// Number of restored output files materialized with filesystem reflinks.
407    pub reflinked_output_files: u64,
408    /// Declared size of outputs materialized with filesystem reflinks.
409    pub reflinked_output_bytes: u64,
410    /// Number of restored output files materialized by copying their bytes.
411    pub copied_output_files: u64,
412    /// Declared size of outputs materialized by copying their bytes.
413    pub copied_output_bytes: u64,
414}
415
416/// Count and cumulative wall time for one compiler-invocation outcome.
417///
418/// Non-exhaustive for the same reason as [`AgentStats`], which holds these:
419/// leaving the outer bag open does not help if describing an outcome in more
420/// detail still breaks the type inside it.
421#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
422#[non_exhaustive]
423pub struct CompilerStats {
424    /// Number of compiler invocations observed.
425    pub invocations: u64,
426    /// Cumulative wall time spent in those invocations.
427    pub duration_ns: u64,
428}
429
430impl CompilerStats {
431    /// The counts observed for one outcome.
432    pub fn new(invocations: u64, duration_ns: u64) -> Self {
433        Self {
434            invocations,
435            duration_ns,
436        }
437    }
438}
439
440#[derive(Default)]
441struct AtomicAgentStats {
442    lookups: AtomicU64,
443    unconsulted: AtomicU64,
444    hits: AtomicU64,
445    stores: AtomicU64,
446    stored_bytes: AtomicU64,
447    verifications: AtomicU64,
448    divergences: AtomicU64,
449    downloaded_bytes: AtomicU64,
450    uploaded_bytes: AtomicU64,
451    background_uploads: AtomicU64,
452    background_upload_failures: AtomicU64,
453    remote_blob_pack_uploads: AtomicU64,
454    remote_blob_pack_upload_blobs: AtomicU64,
455    upload_drain_duration_ns: AtomicU64,
456    prefetched_actions: AtomicU64,
457    remote_failures: AtomicU64,
458    remote_manifest_lookups: AtomicU64,
459    remote_manifest_lookup_duration_ns: AtomicU64,
460    remote_action_lookups: AtomicU64,
461    remote_action_lookup_duration_ns: AtomicU64,
462    remote_blob_requests: AtomicU64,
463    remote_blob_pack_requests: AtomicU64,
464    remote_blob_pack_blobs: AtomicU64,
465    remote_blob_transfer_duration_ns: AtomicU64,
466    local_cas_write_duration_ns: AtomicU64,
467    prefetch_runs: AtomicU64,
468    prefetch_duration_ns: AtomicU64,
469    materialization_duration_ns: AtomicU64,
470    bypasses: Mutex<BTreeMap<String, u64>>,
471    avoided_compiler_duration_ns: AtomicU64,
472    compiler: Mutex<BTreeMap<String, CompilerStats>>,
473    slow_compilations: Mutex<BTreeMap<String, u64>>,
474    restored_output_files: AtomicU64,
475    restored_output_bytes: AtomicU64,
476    reflinked_output_files: AtomicU64,
477    reflinked_output_bytes: AtomicU64,
478    copied_output_files: AtomicU64,
479    copied_output_bytes: AtomicU64,
480}
481
482struct AtomicDurationTimer<'a> {
483    started: Instant,
484    target: &'a AtomicU64,
485}
486
487impl<'a> AtomicDurationTimer<'a> {
488    fn start(target: &'a AtomicU64) -> Self {
489        Self {
490            started: Instant::now(),
491            target,
492        }
493    }
494}
495
496impl Drop for AtomicDurationTimer<'_> {
497    fn drop(&mut self) {
498        atomic_saturating_add(self.target, duration_ns(self.started));
499    }
500}
501
502fn duration_ns(started: Instant) -> u64 {
503    started.elapsed().as_nanos().try_into().unwrap_or(u64::MAX)
504}
505
506fn validate_crate_name(crate_name: Option<&str>) -> Result<()> {
507    if let Some(crate_name) = crate_name
508        && (crate_name.len() > 256 || crate_name.contains(['\0', '\n', '\r']))
509    {
510        bail!("invalid compiler crate name");
511    }
512    Ok(())
513}
514
515fn atomic_saturating_add(target: &AtomicU64, value: u64) {
516    let _ = target.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
517        Some(current.saturating_add(value))
518    });
519}
520
521fn queue_prefetch_digest(
522    verified: &BTreeMap<CacheDigest, PathBuf>,
523    pending: &mut BTreeMap<CacheDigest, ()>,
524    digest: CacheDigest,
525) {
526    if verified.contains_key(&digest) || pending.contains_key(&digest) {
527        return;
528    }
529    pending.insert(digest, ());
530}
531
532fn queue_prefetch_directory(
533    seen: &BTreeMap<CacheDigest, ()>,
534    pending: &mut BTreeMap<CacheDigest, ()>,
535    digest: CacheDigest,
536    limit: usize,
537) -> bool {
538    if seen.contains_key(&digest) || pending.contains_key(&digest) {
539        return true;
540    }
541    if seen.len().saturating_add(pending.len()) >= limit {
542        return false;
543    }
544    pending.insert(digest, ());
545    true
546}
547
548/// Shared state for an agent hosted by the process that owns a build session.
549///
550/// Transport listeners deliberately live in the embedder so the session
551/// lifecycle owns them. This type only contains ecosystem-independent CAS and
552/// protocol logic.
553#[derive(Clone)]
554pub struct CacheAgent {
555    cas: LocalCas,
556    actions: LocalActionCache,
557    verified_blobs: Arc<Mutex<BTreeMap<CacheDigest, PathBuf>>>,
558    version: Arc<str>,
559    write_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
560    action_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
561    stats: Arc<AtomicAgentStats>,
562    observer: Option<Arc<dyn AgentEventObserver>>,
563    executable_identities: Arc<Mutex<BTreeMap<ExecutableIdentityKey, Vec<u8>>>>,
564    manifest_dir: Arc<PathBuf>,
565    task_actions: Arc<Mutex<BTreeMap<String, TaskActionState>>>,
566    next_task_run: Arc<AtomicU64>,
567    manifest_write_lock: Arc<Mutex<()>>,
568    remote: Option<Arc<RemoteCacheClient>>,
569    remote_mode: RemoteCacheMode,
570    remote_staging_dir: Arc<PathBuf>,
571    remote_download_limit: u64,
572    remote_download_bytes: Arc<AtomicU64>,
573    pending_remote_actions: Arc<Mutex<BTreeMap<CacheDigest, RemoteActionResult>>>,
574    remote_transfers: Arc<tokio::sync::Semaphore>,
575    prefetch_transfers: Arc<tokio::sync::Semaphore>,
576    prefetch_tasks: Arc<Mutex<Vec<tokio::task::JoinHandle<()>>>>,
577    /// Deferred remote publication, present only when the session may write.
578    uploads: Option<UploadQueue>,
579}
580
581/// Records background upload activity against a session's statistics.
582struct AgentUploadSink {
583    stats: Arc<AtomicAgentStats>,
584}
585
586impl UploadSink for AgentUploadSink {
587    fn record_blob_uploaded(&self, bytes: u64) {
588        self.stats
589            .background_uploads
590            .fetch_add(1, Ordering::Relaxed);
591        self.stats
592            .uploaded_bytes
593            .fetch_add(bytes, Ordering::Relaxed);
594    }
595
596    fn record_action_uploaded(&self) {
597        self.stats
598            .background_uploads
599            .fetch_add(1, Ordering::Relaxed);
600    }
601
602    fn record_blob_pack_uploaded(&self, blobs: u64) {
603        self.stats
604            .remote_blob_pack_uploads
605            .fetch_add(1, Ordering::Relaxed);
606        self.stats
607            .remote_blob_pack_upload_blobs
608            .fetch_add(blobs, Ordering::Relaxed);
609    }
610
611    fn record_upload_failure(&self) {
612        self.stats
613            .background_upload_failures
614            .fetch_add(1, Ordering::Relaxed);
615        self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
616    }
617}
618
619#[derive(Debug, Clone, Default)]
620struct TaskActionState {
621    manifest: String,
622    baseline_loaded: bool,
623    predictions: BTreeMap<CacheDigest, ActionPrediction>,
624    pending_predictions: BTreeMap<CacheDigest, ActionPrediction>,
625    remote_etag: Option<String>,
626}
627
628struct PrefetchedAction {
629    adapter: String,
630    result: RemoteActionResult,
631}
632
633struct RemoteDownloadReservation {
634    counter: Arc<AtomicU64>,
635    reserved: u64,
636    committed: bool,
637}
638
639impl RemoteDownloadReservation {
640    fn bytes(&self) -> u64 {
641        self.reserved
642    }
643
644    fn commit(mut self, bytes: u64) {
645        debug_assert!(bytes <= self.reserved);
646        self.counter
647            .fetch_sub(self.reserved.saturating_sub(bytes), Ordering::AcqRel);
648        self.committed = true;
649    }
650}
651
652impl Drop for RemoteDownloadReservation {
653    fn drop(&mut self) {
654        if !self.committed {
655            self.counter.fetch_sub(self.reserved, Ordering::AcqRel);
656        }
657    }
658}
659
660#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
661struct ExecutableIdentityKey {
662    executable: PathBuf,
663    environment: BTreeMap<String, Option<String>>,
664}
665
666impl CacheAgent {
667    /// Create an agent backed by the cache rooted at `cache_dir`.
668    pub fn new(cache_dir: impl Into<PathBuf>, version: impl Into<Arc<str>>) -> Self {
669        Self::build(cache_dir.into(), version.into(), None, 0)
670    }
671
672    /// Create an agent with local-first access to a remote action cache.
673    pub fn new_remote(
674        cache_dir: impl Into<PathBuf>,
675        version: impl Into<Arc<str>>,
676        remote: AgentRemoteCache,
677    ) -> Self {
678        Self::build(
679            cache_dir.into(),
680            version.into(),
681            Some(remote),
682            DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES,
683        )
684    }
685
686    /// Create a remote agent with a cumulative download budget for the session.
687    pub fn new_remote_with_download_limit(
688        cache_dir: impl Into<PathBuf>,
689        version: impl Into<Arc<str>>,
690        remote: AgentRemoteCache,
691        max_remote_download_bytes: u64,
692    ) -> Self {
693        Self::build(
694            cache_dir.into(),
695            version.into(),
696            Some(remote),
697            max_remote_download_bytes,
698        )
699    }
700
701    fn build(
702        cache_dir: PathBuf,
703        version: Arc<str>,
704        remote: Option<AgentRemoteCache>,
705        remote_download_limit: u64,
706    ) -> Self {
707        let remote_mode = remote
708            .as_ref()
709            .map_or(RemoteCacheMode::ReadOnly, |remote| remote.mode);
710        let remote_staging_dir = remote.as_ref().map_or_else(
711            || cache_dir.join("remote"),
712            |remote| remote.staging_dir.clone(),
713        );
714        let remote = remote.map(|remote| Arc::new(remote.client));
715        let stats = Arc::new(AtomicAgentStats::default());
716        let remote_transfers = Arc::new(tokio::sync::Semaphore::new(MAX_REMOTE_TRANSFERS));
717        // A session that cannot publish never queues an upload, so it does not
718        // need somewhere to queue one.
719        let uploads = remote
720            .clone()
721            .filter(|_| remote_mode.writes())
722            .map(|client| {
723                UploadQueue::new(
724                    client,
725                    Arc::new(AgentUploadSink {
726                        stats: stats.clone(),
727                    }),
728                    remote_transfers.clone(),
729                )
730            });
731        Self {
732            cas: LocalCas::new(cache_dir.clone()),
733            actions: LocalActionCache::new(cache_dir.clone()),
734            verified_blobs: Arc::new(Mutex::new(BTreeMap::new())),
735            version,
736            write_locks: Arc::new(Mutex::new(BTreeMap::new())),
737            action_locks: Arc::new(Mutex::new(BTreeMap::new())),
738            stats,
739            observer: None,
740            executable_identities: Arc::new(Mutex::new(BTreeMap::new())),
741            manifest_dir: Arc::new(task_manifest_dir(&cache_dir)),
742            task_actions: Arc::new(Mutex::new(BTreeMap::new())),
743            next_task_run: Arc::new(AtomicU64::new(0)),
744            manifest_write_lock: Arc::new(Mutex::new(())),
745            remote,
746            remote_mode,
747            remote_staging_dir: Arc::new(remote_staging_dir),
748            remote_download_limit,
749            remote_download_bytes: Arc::new(AtomicU64::new(0)),
750            pending_remote_actions: Arc::new(Mutex::new(BTreeMap::new())),
751            remote_transfers,
752            prefetch_transfers: Arc::new(tokio::sync::Semaphore::new(MAX_PREFETCH_TRANSFERS)),
753            prefetch_tasks: Arc::new(Mutex::new(Vec::new())),
754            uploads,
755        }
756    }
757
758    /// Report each accounted cache decision to `observer` as it happens.
759    #[must_use]
760    pub fn with_observer(mut self, observer: Arc<dyn AgentEventObserver>) -> Self {
761        self.observer = Some(observer);
762        self
763    }
764
765    fn emit(&self, event: impl FnOnce() -> AgentEvent) {
766        if let Some(observer) = &self.observer {
767            observer.event(event());
768        }
769    }
770
771    fn reserve_remote_download(&self, bytes: u64) -> Result<RemoteDownloadReservation> {
772        let mut current = self.remote_download_bytes.load(Ordering::Acquire);
773        loop {
774            let next = current
775                .checked_add(bytes)
776                .ok_or_else(|| eyre::eyre!("remote cache download budget overflowed"))?;
777            if next > self.remote_download_limit {
778                bail!(
779                    "remote cache download budget exceeded: {} bytes requested with {} of {} bytes already used",
780                    bytes,
781                    current,
782                    self.remote_download_limit
783                );
784            }
785            match self.remote_download_bytes.compare_exchange_weak(
786                current,
787                next,
788                Ordering::AcqRel,
789                Ordering::Acquire,
790            ) {
791                Ok(_) => {
792                    return Ok(RemoteDownloadReservation {
793                        counter: self.remote_download_bytes.clone(),
794                        reserved: bytes,
795                        committed: false,
796                    });
797                }
798                Err(observed) => current = observed,
799            }
800        }
801    }
802
803    fn reserve_remote_download_up_to(&self, requested: u64) -> Result<RemoteDownloadReservation> {
804        let mut current = self.remote_download_bytes.load(Ordering::Acquire);
805        loop {
806            let reserved = requested.min(self.remote_download_limit.saturating_sub(current));
807            let next = current + reserved;
808            match self.remote_download_bytes.compare_exchange_weak(
809                current,
810                next,
811                Ordering::AcqRel,
812                Ordering::Acquire,
813            ) {
814                Ok(_) => {
815                    return Ok(RemoteDownloadReservation {
816                        counter: self.remote_download_bytes.clone(),
817                        reserved,
818                        committed: false,
819                    });
820                }
821                Err(observed) => current = observed,
822            }
823        }
824    }
825
826    /// Load the last committed action manifest for a task into this session.
827    pub async fn begin_task(&self, task: &str) -> Result<String> {
828        self.begin_task_with_remote_errors(task, false).await
829    }
830
831    /// Load a task and finish its prefetch, surfacing remote lookup failures.
832    pub async fn prefetch_task(&self, task: &str) -> Result<String> {
833        let run = self.begin_task_with_remote_errors(task, true).await?;
834        self.wait_for_prefetches().await;
835        Ok(run)
836    }
837
838    async fn begin_task_with_remote_errors(&self, task: &str, strict: bool) -> Result<String> {
839        validate_task_identity(task)?;
840        let (remote_manifest, mut remote_etag) = if self.remote_mode.reads() {
841            match self.get_remote_task_manifest(task).await {
842                Ok(Some((manifest, etag))) => (Some(manifest), Some(etag)),
843                Ok(None) => (None, None),
844                Err(error) => {
845                    if strict {
846                        return Err(error).wrap_err_with(|| {
847                            format!("remote task action manifest lookup failed for {task}")
848                        });
849                    }
850                    self.note_remote_failure();
851                    warn!("remote task action manifest lookup failed for {task}: {error}");
852                    (None, None)
853                }
854            }
855        } else {
856            (None, None)
857        };
858        let manifest = {
859            let _write_guard = self.manifest_write_lock.lock().unwrap();
860            let _file_guard = self.lock_task_manifest(task)?;
861            let local_manifest = self.load_task_manifest(task)?;
862            let manifest = match (remote_manifest, local_manifest) {
863                (Some(remote), Some(local)) => {
864                    let (manifest, merged) = merge_remote_task_manifest(task, remote, local);
865                    if !merged {
866                        remote_etag = None;
867                    }
868                    Some(manifest)
869                }
870                (Some(remote), None) => Some(remote),
871                (None, local) => local,
872            };
873            if let Some(manifest) = &manifest {
874                self.persist_task_manifest(manifest)?;
875            }
876            manifest
877        };
878        let state = if let Some(manifest) = manifest {
879            TaskActionState {
880                manifest: task.to_string(),
881                baseline_loaded: true,
882                predictions: manifest
883                    .predictions
884                    .into_iter()
885                    .map(|prediction| (prediction.invocation.clone(), prediction))
886                    .collect(),
887                pending_predictions: BTreeMap::new(),
888                remote_etag,
889            }
890        } else {
891            TaskActionState {
892                manifest: task.to_string(),
893                baseline_loaded: true,
894                remote_etag,
895                ..TaskActionState::default()
896            }
897        };
898        let sequence = self.next_task_run.fetch_add(1, Ordering::Relaxed);
899        let run =
900            CacheDigest::blake3(format!("{task}\0{}\0{sequence}", std::process::id()).as_bytes())
901                .hash;
902        let predictions = state.predictions.values().cloned().collect();
903        self.task_actions.lock().unwrap().insert(run.clone(), state);
904        self.spawn_prefetch_predictions(predictions);
905        Ok(run)
906    }
907
908    /// Cancel speculative downloads before the owning session exits.
909    pub async fn cancel_prefetches(&self) {
910        let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
911        for task in &tasks {
912            task.abort();
913        }
914        for task in tasks {
915            if let Err(error) = task.await
916                && !error.is_cancelled()
917            {
918                warn!("remote action prefetch task failed: {error}");
919            }
920        }
921    }
922
923    /// Publish everything a build queued, before the session stops.
924    ///
925    /// Store requests return once an object is durable locally, so at this point
926    /// the remote cache may still be behind the local one. Uploads run on the
927    /// session's runtime and are abandoned if it goes away, so a session that
928    /// wants them published has to wait here.
929    pub async fn wait_for_uploads(&self) {
930        let Some(uploads) = &self.uploads else {
931            return;
932        };
933        let _timer = AtomicDurationTimer::start(&self.stats.upload_drain_duration_ns);
934        uploads.drain().await;
935    }
936
937    async fn wait_for_prefetches(&self) {
938        let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
939        for task in tasks {
940            if let Err(error) = task.await {
941                warn!("remote action prefetch task failed: {error}");
942            }
943        }
944    }
945
946    /// Atomically publish the completed actions collected by a task run.
947    pub async fn commit_task(&self, run: &str) -> Result<()> {
948        validate_task_identity(run)?;
949        let state = self
950            .task_actions
951            .lock()
952            .unwrap()
953            .get(run)
954            .cloned()
955            .ok_or_else(|| eyre::eyre!("task action manifest baseline was not loaded"))?;
956        if !state.baseline_loaded {
957            bail!("task action manifest baseline was not loaded");
958        }
959        let task = state.manifest;
960        validate_task_identity(&task)?;
961        let (manifest, introduced) = {
962            let _write_guard = self.manifest_write_lock.lock().unwrap();
963            let _file_guard = self.lock_task_manifest(&task)?;
964            let mut predictions = self
965                .load_task_manifest(&task)?
966                .map(|manifest| {
967                    manifest
968                        .predictions
969                        .into_iter()
970                        .map(|prediction| (prediction.invocation.clone(), prediction))
971                        .collect::<BTreeMap<_, _>>()
972                })
973                .unwrap_or_default();
974            // Which predictions this run adds, as opposed to inherits. Only a
975            // new one may be withheld for a failed upload: retracting an
976            // inherited one would un-advertise a result that is plausibly still
977            // on the server from whichever session put it there.
978            let introduced: BTreeSet<CacheDigest> = state
979                .pending_predictions
980                .keys()
981                .filter(|invocation| !predictions.contains_key(*invocation))
982                .cloned()
983                .collect();
984            // Only publish predictions recorded by this run. `predictions`
985            // also contains the baseline loaded by `begin_task`; extending
986            // with that snapshot would overwrite newer entries committed by
987            // another agent process after this run began.
988            predictions.extend(state.pending_predictions);
989            let manifest = TaskActionManifest {
990                version: TASK_ACTION_MANIFEST_VERSION,
991                task: task.clone(),
992                predictions: predictions.into_values().collect(),
993            };
994            validate_task_manifest(&manifest, &task)?;
995            self.persist_task_manifest(&manifest)?;
996            (manifest, introduced)
997        };
998        self.task_actions.lock().unwrap().remove(run);
999        if self.remote_mode.writes() {
1000            // A manifest advertises the actions it predicts, so it must not
1001            // reach the remote cache before the results a reader would then go
1002            // looking for -- nor name a result that never got there at all.
1003            let mut manifest = manifest;
1004            if let Some(uploads) = &self.uploads {
1005                let actions: Vec<CacheDigest> = manifest
1006                    .predictions
1007                    .iter()
1008                    .map(|prediction| prediction.action.clone())
1009                    .collect();
1010                let unpublished = uploads.wait_for_actions(&actions).await;
1011                let withheld = manifest
1012                    .predictions
1013                    .iter()
1014                    .filter(|prediction| {
1015                        introduced.contains(&prediction.invocation)
1016                            && unpublished.contains(&prediction.action)
1017                    })
1018                    .count();
1019                if withheld > 0 {
1020                    // The local manifest keeps them: this checkout can still use
1021                    // what it built, and a later session can publish it.
1022                    warn!(
1023                        "{withheld} of {} predicted actions were not published, so the remote task action manifest omits them",
1024                        manifest.predictions.len()
1025                    );
1026                    manifest.predictions.retain(|prediction| {
1027                        !(introduced.contains(&prediction.invocation)
1028                            && unpublished.contains(&prediction.action))
1029                    });
1030                }
1031            }
1032            match self
1033                .put_remote_task_manifest(&task, manifest, state.remote_etag)
1034                .await
1035            {
1036                Ok(remote_manifest) => {
1037                    let _write_guard = self.manifest_write_lock.lock().unwrap();
1038                    let reconciliation = (|| {
1039                        let _file_guard = self.lock_task_manifest(&task)?;
1040                        let manifest = match self.load_task_manifest(&task)? {
1041                            Some(local) => {
1042                                merge_remote_task_manifest(&task, remote_manifest, local).0
1043                            }
1044                            None => remote_manifest,
1045                        };
1046                        self.persist_task_manifest(&manifest)
1047                    })();
1048                    if let Err(error) = reconciliation {
1049                        warn!(
1050                            "remote task action manifest reconciliation failed for {task}: {error}"
1051                        );
1052                    }
1053                }
1054                Err(error) => {
1055                    self.note_remote_failure();
1056                    warn!("remote task action manifest upload failed for {task}: {error}");
1057                }
1058            }
1059        }
1060        Ok(())
1061    }
1062
1063    fn task_manifest_path(&self, task: &str) -> PathBuf {
1064        self.manifest_dir.join(format!("{task}.json"))
1065    }
1066
1067    fn task_manifest_lock_path(&self, task: &str) -> PathBuf {
1068        self.manifest_dir.join("locks").join(format!("{task}.lock"))
1069    }
1070
1071    fn lock_task_manifest(&self, task: &str) -> Result<fslock::LockFile> {
1072        let path = self.task_manifest_lock_path(task);
1073        fs::create_dir_all(path.parent().expect("task manifest lock has a parent"))?;
1074        let mut lock = fslock::LockFile::open(&path)?;
1075        lock.lock()?;
1076        Ok(lock)
1077    }
1078
1079    fn load_task_manifest(&self, task: &str) -> Result<Option<TaskActionManifest>> {
1080        match fs::read(self.task_manifest_path(task)) {
1081            Ok(contents) => Ok(Some(self.parse_task_manifest(task, &contents, false)?)),
1082            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
1083            Err(error) => Err(error.into()),
1084        }
1085    }
1086
1087    fn parse_task_manifest(
1088        &self,
1089        task: &str,
1090        contents: &[u8],
1091        require_canonical: bool,
1092    ) -> Result<TaskActionManifest> {
1093        let manifest: TaskActionManifest = serde_json::from_slice(contents)?;
1094        validate_task_manifest(&manifest, task)?;
1095        if require_canonical && canonical_json(&manifest)? != contents {
1096            bail!("task action manifest is not canonical JSON");
1097        }
1098        Ok(manifest)
1099    }
1100
1101    fn task_manifest_selector(task: &str) -> Result<(Vec<u8>, CacheDigest)> {
1102        TaskActionManifest::selector(task)
1103    }
1104
1105    fn persist_task_manifest(&self, manifest: &TaskActionManifest) -> Result<()> {
1106        let bytes = canonical_json(manifest)?;
1107        fs::create_dir_all(self.manifest_dir.as_path())?;
1108        let mut temporary = tempfile::NamedTempFile::new_in(self.manifest_dir.as_path())?;
1109        std::io::Write::write_all(temporary.as_file_mut(), &bytes)?;
1110        temporary.as_file_mut().sync_all()?;
1111        temporary
1112            .persist(self.task_manifest_path(&manifest.task))
1113            .map_err(|error| error.error)?;
1114        Ok(())
1115    }
1116
1117    async fn get_remote_task_manifest(
1118        &self,
1119        task: &str,
1120    ) -> Result<Option<(TaskActionManifest, String)>> {
1121        let Some(remote) = &self.remote else {
1122            return Ok(None);
1123        };
1124        let (_, selector) = Self::task_manifest_selector(task)?;
1125        let _permit = self.remote_transfers.acquire().await?;
1126        self.stats
1127            .remote_manifest_lookups
1128            .fetch_add(1, Ordering::Relaxed);
1129        let _timer = AtomicDurationTimer::start(&self.stats.remote_manifest_lookup_duration_ns);
1130        let Some(remote_manifest) = remote.get_action_manifest(&selector).await? else {
1131            return Ok(None);
1132        };
1133        let manifest = self.parse_task_manifest(task, &remote_manifest.bytes, true)?;
1134        Ok(Some((manifest, remote_manifest.etag)))
1135    }
1136
1137    async fn put_remote_task_manifest(
1138        &self,
1139        task: &str,
1140        mut manifest: TaskActionManifest,
1141        mut expected_etag: Option<String>,
1142    ) -> Result<TaskActionManifest> {
1143        let Some(remote) = &self.remote else {
1144            return Ok(manifest);
1145        };
1146        let (_, selector) = Self::task_manifest_selector(task)?;
1147        for _ in 0..4 {
1148            let bytes = canonical_json(&manifest)?;
1149            let outcome = {
1150                let _permit = self.remote_transfers.acquire().await?;
1151                remote
1152                    .put_action_manifest(&selector, &bytes, expected_etag.as_deref())
1153                    .await?
1154            };
1155            match outcome {
1156                ManifestPutOutcome::Stored => return Ok(manifest),
1157                ManifestPutOutcome::PreconditionFailed => {
1158                    let Some((remote_manifest, etag)) = self.get_remote_task_manifest(task).await?
1159                    else {
1160                        expected_etag = None;
1161                        continue;
1162                    };
1163                    manifest = merge_task_manifests(task, Some(remote_manifest), manifest)?;
1164                    expected_etag = Some(etag);
1165                }
1166            }
1167        }
1168        bail!("remote task action manifest changed too frequently")
1169    }
1170
1171    /// Return a snapshot of this session's cache activity.
1172    pub fn stats(&self) -> AgentStats {
1173        AgentStats {
1174            session_duration_ns: 0,
1175            lookups: self.stats.lookups.load(Ordering::Relaxed),
1176            unconsulted: self.stats.unconsulted.load(Ordering::Relaxed),
1177            hits: self.stats.hits.load(Ordering::Relaxed),
1178            stores: self.stats.stores.load(Ordering::Relaxed),
1179            stored_bytes: self.stats.stored_bytes.load(Ordering::Relaxed),
1180            verifications: self.stats.verifications.load(Ordering::Relaxed),
1181            divergences: self.stats.divergences.load(Ordering::Relaxed),
1182            downloaded_bytes: self.stats.downloaded_bytes.load(Ordering::Relaxed),
1183            uploaded_bytes: self.stats.uploaded_bytes.load(Ordering::Relaxed),
1184            background_uploads: self.stats.background_uploads.load(Ordering::Relaxed),
1185            background_upload_failures: self
1186                .stats
1187                .background_upload_failures
1188                .load(Ordering::Relaxed),
1189            remote_blob_pack_uploads: self.stats.remote_blob_pack_uploads.load(Ordering::Relaxed),
1190            remote_blob_pack_upload_blobs: self
1191                .stats
1192                .remote_blob_pack_upload_blobs
1193                .load(Ordering::Relaxed),
1194            upload_drain_duration_ns: self.stats.upload_drain_duration_ns.load(Ordering::Relaxed),
1195            prefetched_actions: self.stats.prefetched_actions.load(Ordering::Relaxed),
1196            bypasses: self.stats.bypasses.lock().unwrap().clone(),
1197            avoided_compiler_duration_ns: self
1198                .stats
1199                .avoided_compiler_duration_ns
1200                .load(Ordering::Relaxed),
1201            compiler: self.stats.compiler.lock().unwrap().clone(),
1202            slow_compilations: self.stats.slow_compilations.lock().unwrap().clone(),
1203            remote_failures: self.stats.remote_failures.load(Ordering::Relaxed),
1204            remote_manifest_lookups: self.stats.remote_manifest_lookups.load(Ordering::Relaxed),
1205            remote_manifest_lookup_duration_ns: self
1206                .stats
1207                .remote_manifest_lookup_duration_ns
1208                .load(Ordering::Relaxed),
1209            remote_action_lookups: self.stats.remote_action_lookups.load(Ordering::Relaxed),
1210            remote_action_lookup_duration_ns: self
1211                .stats
1212                .remote_action_lookup_duration_ns
1213                .load(Ordering::Relaxed),
1214            remote_blob_requests: self.stats.remote_blob_requests.load(Ordering::Relaxed),
1215            remote_blob_pack_requests: self.stats.remote_blob_pack_requests.load(Ordering::Relaxed),
1216            remote_blob_pack_blobs: self.stats.remote_blob_pack_blobs.load(Ordering::Relaxed),
1217            remote_blob_transfer_duration_ns: self
1218                .stats
1219                .remote_blob_transfer_duration_ns
1220                .load(Ordering::Relaxed),
1221            local_cas_write_duration_ns: self
1222                .stats
1223                .local_cas_write_duration_ns
1224                .load(Ordering::Relaxed),
1225            prefetch_runs: self.stats.prefetch_runs.load(Ordering::Relaxed),
1226            prefetch_duration_ns: self.stats.prefetch_duration_ns.load(Ordering::Relaxed),
1227            materialization_duration_ns: self
1228                .stats
1229                .materialization_duration_ns
1230                .load(Ordering::Relaxed),
1231            restored_output_files: self.stats.restored_output_files.load(Ordering::Relaxed),
1232            restored_output_bytes: self.stats.restored_output_bytes.load(Ordering::Relaxed),
1233            reflinked_output_files: self.stats.reflinked_output_files.load(Ordering::Relaxed),
1234            reflinked_output_bytes: self.stats.reflinked_output_bytes.load(Ordering::Relaxed),
1235            copied_output_files: self.stats.copied_output_files.load(Ordering::Relaxed),
1236            copied_output_bytes: self.stats.copied_output_bytes.load(Ordering::Relaxed),
1237        }
1238    }
1239
1240    /// Handle requests without a transport connection.
1241    ///
1242    /// Persistent wrappers use this entry point when Cargo invokes mbx outside
1243    /// an orchestrated session. It intentionally has the same response
1244    /// semantics as [`Self::handle_connection`], while leaving framing and the
1245    /// version handshake to callers that actually cross a process boundary.
1246    pub async fn handle_requests(
1247        &self,
1248        requests: impl IntoIterator<Item = AgentRequest>,
1249    ) -> Vec<AgentResponse> {
1250        let mut connection = ConnectionUploads::default();
1251        let mut responses = Vec::new();
1252        for request in requests {
1253            responses.push(self.respond_on(request, &mut connection).await);
1254        }
1255        responses
1256    }
1257
1258    fn write_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1259        Self::digest_lock(&self.write_locks, digest)
1260    }
1261
1262    fn action_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1263        Self::digest_lock(&self.action_locks, digest)
1264    }
1265
1266    fn digest_lock(
1267        locks: &Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>,
1268        digest: &CacheDigest,
1269    ) -> Arc<tokio::sync::Mutex<()>> {
1270        let mut locks = locks.lock().unwrap();
1271        locks.retain(|_, lock| lock.strong_count() > 0);
1272        if let Some(lock) = locks.get(digest).and_then(Weak::upgrade) {
1273            return lock;
1274        }
1275        let lock = Arc::new(tokio::sync::Mutex::new(()));
1276        locks.insert(digest.clone(), Arc::downgrade(&lock));
1277        lock
1278    }
1279
1280    fn spawn_prefetch_predictions(&self, predictions: Vec<ActionPrediction>) {
1281        if predictions.is_empty() || !self.remote_mode.reads() || self.remote.is_none() {
1282            return;
1283        }
1284        let agent = self.clone();
1285        let task = tokio::spawn(async move {
1286            agent.prefetch_predictions(predictions.iter()).await;
1287        });
1288        self.prefetch_tasks.lock().unwrap().push(task);
1289    }
1290
1291    async fn prefetch_predictions<'a>(
1292        &self,
1293        predictions: impl Iterator<Item = &'a ActionPrediction>,
1294    ) {
1295        if !self.remote_mode.reads() || self.remote.is_none() {
1296            return;
1297        }
1298        self.stats.prefetch_runs.fetch_add(1, Ordering::Relaxed);
1299        let _timer = AtomicDurationTimer::start(&self.stats.prefetch_duration_ns);
1300        let mut actions = BTreeMap::new();
1301        for prediction in predictions {
1302            actions
1303                .entry(prediction.action.clone())
1304                .or_insert_with(|| prediction.adapter.clone());
1305        }
1306        // One request per predicted action is the bulk of a prefetch's latency on
1307        // a large workspace, so ask for them together where the server allows it.
1308        match self.prefetch_action_batches(&actions).await {
1309            Ok(true) => return,
1310            Ok(false) => {}
1311            Err(error) => {
1312                self.note_remote_failure();
1313                warn!("remote action batch lookup failed: {error}");
1314            }
1315        }
1316        let mut actions = actions.into_iter();
1317        let mut tasks = tokio::task::JoinSet::new();
1318        for _ in 0..MAX_PREFETCH_TRANSFERS {
1319            let Some((action, adapter)) = actions.next() else {
1320                break;
1321            };
1322            let agent = self.clone();
1323            tasks.spawn(async move { agent.resolve_prefetch_action(action, adapter).await });
1324        }
1325        let mut resolved = Vec::new();
1326        while !tasks.is_empty() {
1327            let result = if resolved.is_empty() {
1328                tasks.join_next().await
1329            } else {
1330                match tokio::time::timeout(PREFETCH_ACTION_BATCH_DELAY, tasks.join_next()).await {
1331                    Ok(result) => result,
1332                    Err(_) => {
1333                        self.prefetch_resolved_actions(std::mem::take(&mut resolved))
1334                            .await;
1335                        continue;
1336                    }
1337                }
1338            };
1339            let Some(result) = result else {
1340                break;
1341            };
1342            match result {
1343                Ok(Ok(Some(action))) => resolved.push(action),
1344                Ok(Ok(None)) => {}
1345                Ok(Err(error)) => {
1346                    self.note_remote_failure();
1347                    warn!("remote action prefetch failed: {error}");
1348                }
1349                Err(error) => {
1350                    self.note_remote_failure();
1351                    warn!("remote action prefetch task failed: {error}");
1352                }
1353            }
1354            if let Some((action, adapter)) = actions.next() {
1355                let agent = self.clone();
1356                tasks.spawn(async move { agent.resolve_prefetch_action(action, adapter).await });
1357            }
1358            if resolved.len() == MAX_PREFETCH_ACTION_BATCH {
1359                self.prefetch_resolved_actions(std::mem::take(&mut resolved))
1360                    .await;
1361            }
1362        }
1363        if !resolved.is_empty() {
1364            self.prefetch_resolved_actions(resolved).await;
1365        }
1366    }
1367
1368    /// Resolve predicted actions in batched lookups, staging what comes back.
1369    ///
1370    /// Returns whether the batch extension answered. A server without it, or one
1371    /// that stops answering part way through, leaves the per-action path to
1372    /// resolve whatever is left -- the results already staged here are memoized,
1373    /// so nothing is looked up twice.
1374    async fn prefetch_action_batches(
1375        &self,
1376        actions: &BTreeMap<CacheDigest, String>,
1377    ) -> Result<bool> {
1378        let Some(remote) = self.remote.as_deref() else {
1379            return Ok(false);
1380        };
1381        let Some(limit) = remote.action_batch_limit().await? else {
1382            return Ok(false);
1383        };
1384        let wanted: Vec<CacheDigest> = actions
1385            .keys()
1386            .filter(|action| !self.action_is_staged(action))
1387            .cloned()
1388            .collect();
1389        if wanted.is_empty() {
1390            return Ok(true);
1391        }
1392        let mut chunks: Vec<Vec<CacheDigest>> = Vec::new();
1393        for chunk in wanted.chunks(limit) {
1394            chunks.push(chunk.to_vec());
1395        }
1396        let mut lookups = stream::iter(chunks)
1397            .map(|chunk| async move {
1398                let _prefetch_permit = self.prefetch_transfers.acquire().await?;
1399                let _permit = self.remote_transfers.acquire().await?;
1400                self.stats
1401                    .remote_action_lookups
1402                    .fetch_add(1, Ordering::Relaxed);
1403                let _timer =
1404                    AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);
1405                remote.get_action_results(&chunk).await
1406            })
1407            .buffer_unordered(MAX_PREFETCH_BATCH_LOOKUPS);
1408        let mut answered = true;
1409        while let Some(lookup) = lookups.next().await {
1410            let results = match lookup {
1411                Ok(Some(results)) => results,
1412                // Advertised but unavailable. What is left falls back.
1413                Ok(None) => {
1414                    answered = false;
1415                    continue;
1416                }
1417                Err(error) => {
1418                    self.note_remote_failure();
1419                    warn!("remote action batch lookup failed: {error}");
1420                    answered = false;
1421                    continue;
1422                }
1423            };
1424            let mut resolved = Vec::with_capacity(results.len());
1425            for result in results {
1426                let Some(adapter) = actions.get(&result.action).cloned() else {
1427                    continue;
1428                };
1429                let lock = self.action_lock(&result.action);
1430                let _guard = lock.lock().await;
1431                if self.actions.find(&result.action)?.is_some() {
1432                    continue;
1433                }
1434                self.pending_remote_actions
1435                    .lock()
1436                    .unwrap()
1437                    .insert(result.action.clone(), result.clone());
1438                resolved.push(PrefetchedAction { adapter, result });
1439            }
1440            while !resolved.is_empty() {
1441                let wave = resolved
1442                    .drain(..resolved.len().min(MAX_PREFETCH_ACTION_BATCH))
1443                    .collect();
1444                self.prefetch_resolved_actions(wave).await;
1445            }
1446        }
1447        Ok(answered)
1448    }
1449
1450    /// Whether an action's result is already local or already looked up.
1451    fn action_is_staged(&self, action: &CacheDigest) -> bool {
1452        if self
1453            .pending_remote_actions
1454            .lock()
1455            .unwrap()
1456            .contains_key(action)
1457        {
1458            return true;
1459        }
1460        self.actions.find(action).is_ok_and(|found| found.is_some())
1461    }
1462
1463    #[cfg(test)]
1464    async fn prefetch_action(&self, action: CacheDigest, adapter: String) -> Result<()> {
1465        if let Some(action) = self.resolve_prefetch_action(action, adapter).await? {
1466            self.prefetch_resolved_actions(vec![action]).await;
1467        }
1468        Ok(())
1469    }
1470
1471    async fn resolve_prefetch_action(
1472        &self,
1473        action: CacheDigest,
1474        adapter: String,
1475    ) -> Result<Option<PrefetchedAction>> {
1476        let remote = self
1477            .remote
1478            .as_ref()
1479            .ok_or_else(|| eyre::eyre!("remote cache is not configured"))?;
1480        let result = {
1481            let lock = self.action_lock(&action);
1482            let _guard = lock.lock().await;
1483            if self.actions.find(&action)?.is_some() {
1484                return Ok(None);
1485            }
1486            if let Some(result) = self
1487                .pending_remote_actions
1488                .lock()
1489                .unwrap()
1490                .get(&action)
1491                .cloned()
1492            {
1493                result
1494            } else {
1495                let _prefetch_permit = self.prefetch_transfers.acquire().await?;
1496                let result = {
1497                    let _permit = self.remote_transfers.acquire().await?;
1498                    self.get_remote_action_result(remote, &action).await?
1499                };
1500                let Some(result) = result else {
1501                    return Ok(None);
1502                };
1503                self.pending_remote_actions
1504                    .lock()
1505                    .unwrap()
1506                    .insert(action.clone(), result.clone());
1507                result
1508            }
1509        };
1510        Ok(Some(PrefetchedAction { adapter, result }))
1511    }
1512
1513    fn prefetch_resolved_actions(&self, actions: Vec<PrefetchedAction>) -> BoxFuture<'_, ()> {
1514        self.prefetch_resolved_actions_inner(actions).boxed()
1515    }
1516
1517    async fn prefetch_resolved_actions_inner(&self, actions: Vec<PrefetchedAction>) {
1518        let Some(remote) = self.remote.as_deref() else {
1519            return;
1520        };
1521        if actions.is_empty() {
1522            return;
1523        }
1524
1525        let mut top_level = BTreeMap::new();
1526        for action in &actions {
1527            for digest in [
1528                Some(&action.result.action),
1529                action.result.metadata.as_ref(),
1530                action.result.output_root.as_ref(),
1531            ]
1532            .into_iter()
1533            .flatten()
1534            {
1535                top_level.insert(digest.clone(), ());
1536            }
1537        }
1538        let mut verified = self
1539            .fetch_remote_blobs(
1540                remote,
1541                top_level.into_keys().collect(),
1542                Some(&self.prefetch_transfers),
1543            )
1544            .await;
1545
1546        let mut next = BTreeMap::new();
1547        let mut pending_directories = BTreeMap::new();
1548        let mut parsed_directories = BTreeMap::new();
1549        let mut rustc_metadata = BTreeMap::new();
1550        for action in &actions {
1551            if action.adapter == "rustc"
1552                && let Some(metadata_digest) = &action.result.metadata
1553            {
1554                match verified
1555                    .get(metadata_digest)
1556                    .ok_or_else(|| eyre::eyre!("remote rustc action metadata is missing"))
1557                    .and_then(|path| Self::parse_rustc_metadata(path))
1558                {
1559                    Ok(metadata) => {
1560                        queue_prefetch_digest(&verified, &mut next, metadata.stdout.clone());
1561                        queue_prefetch_digest(&verified, &mut next, metadata.stderr.clone());
1562                        rustc_metadata.insert(metadata_digest.clone(), metadata);
1563                    }
1564                    Err(error) => warn!(
1565                        "remote rustc action metadata prefetch failed for {}: {error}",
1566                        action.result.action.hash
1567                    ),
1568                }
1569            }
1570            if let Some(output_root) = &action.result.output_root {
1571                pending_directories.insert(output_root.clone(), ());
1572            }
1573        }
1574
1575        let mut seen_directories = BTreeMap::new();
1576        loop {
1577            let mut following = BTreeMap::new();
1578            let mut directory_limit_exceeded = false;
1579            for digest in pending_directories.into_keys() {
1580                following.remove(&digest);
1581                if seen_directories.insert(digest.clone(), ()).is_some() {
1582                    continue;
1583                }
1584                if seen_directories.len() > MAX_PREFETCH_DIRECTORY_OBJECTS {
1585                    warn!("remote action output tree is too large to prefetch");
1586                    following.clear();
1587                    break;
1588                }
1589                match verified
1590                    .get(&digest)
1591                    .ok_or_else(|| eyre::eyre!("remote action output directory is missing"))
1592                    .and_then(|path| Self::parse_cache_directory(path))
1593                {
1594                    Ok(directory) => {
1595                        for file in &directory.files {
1596                            queue_prefetch_digest(&verified, &mut next, file.digest.clone());
1597                            if next.len() >= MAX_PREFETCH_OBJECTS_PER_WAVE {
1598                                self.flush_prefetch_digest_batch(remote, &mut verified, &mut next)
1599                                    .await;
1600                            }
1601                        }
1602                        for child in &directory.directories {
1603                            if !queue_prefetch_directory(
1604                                &seen_directories,
1605                                &mut following,
1606                                child.digest.clone(),
1607                                MAX_PREFETCH_DIRECTORY_OBJECTS,
1608                            ) {
1609                                warn!("remote action output tree is too large to prefetch");
1610                                directory_limit_exceeded = true;
1611                                break;
1612                            }
1613                            queue_prefetch_digest(&verified, &mut next, child.digest.clone());
1614                            if next.len() >= MAX_PREFETCH_OBJECTS_PER_WAVE {
1615                                self.flush_prefetch_digest_batch(remote, &mut verified, &mut next)
1616                                    .await;
1617                            }
1618                        }
1619                        parsed_directories.insert(digest, directory);
1620                    }
1621                    Err(error) => warn!(
1622                        "remote action output directory prefetch failed for {}: {error}",
1623                        digest.hash
1624                    ),
1625                }
1626                if directory_limit_exceeded {
1627                    following.clear();
1628                    break;
1629                }
1630            }
1631            self.flush_prefetch_digest_batch(remote, &mut verified, &mut next)
1632                .await;
1633            if following.is_empty() {
1634                break;
1635            }
1636            pending_directories = following;
1637        }
1638
1639        for action in actions {
1640            match Self::validate_prefetched_action(
1641                &action,
1642                &verified,
1643                &rustc_metadata,
1644                &parsed_directories,
1645            ) {
1646                Ok(()) => {
1647                    if let Err(error) = self.actions.store(&action.result) {
1648                        warn!(
1649                            "remote action prefetch could not publish {}: {error}",
1650                            action.result.action.hash
1651                        );
1652                        continue;
1653                    }
1654                    self.pending_remote_actions
1655                        .lock()
1656                        .unwrap()
1657                        .remove(&action.result.action);
1658                    self.stats
1659                        .prefetched_actions
1660                        .fetch_add(1, Ordering::Relaxed);
1661                }
1662                Err(error) => warn!(
1663                    "remote action prefetch was incomplete for {}: {error}",
1664                    action.result.action.hash
1665                ),
1666            }
1667        }
1668    }
1669
1670    async fn flush_prefetch_digest_batch(
1671        &self,
1672        remote: &RemoteCacheClient,
1673        verified: &mut BTreeMap<CacheDigest, PathBuf>,
1674        pending: &mut BTreeMap<CacheDigest, ()>,
1675    ) {
1676        if pending.is_empty() {
1677            return;
1678        }
1679        let digests = std::mem::take(pending).into_keys().collect();
1680        verified.extend(
1681            self.fetch_remote_blobs(remote, digests, Some(&self.prefetch_transfers))
1682                .await,
1683        );
1684    }
1685
1686    async fn fetch_remote_blobs(
1687        &self,
1688        remote: &RemoteCacheClient,
1689        digests: Vec<CacheDigest>,
1690        prefetch_limit: Option<&tokio::sync::Semaphore>,
1691    ) -> BTreeMap<CacheDigest, PathBuf> {
1692        let mut verified = BTreeMap::new();
1693        let mut missing = BTreeMap::new();
1694        for digest in digests {
1695            match self.find_verified_blob(&digest) {
1696                Ok(Some(path)) => {
1697                    verified.insert(digest, path);
1698                }
1699                Ok(None) => {
1700                    missing.insert(digest, ());
1701                }
1702                Err(error) => warn!(
1703                    "local cache blob lookup failed for {}: {error}",
1704                    digest.hash
1705                ),
1706            }
1707        }
1708        if missing.is_empty() {
1709            return verified;
1710        }
1711
1712        let mut pack_candidates = missing.clone();
1713        while !pack_candidates.is_empty() {
1714            let candidates = match blob_pack_chunk(
1715                &pack_candidates.keys().cloned().collect::<Vec<_>>(),
1716                BlobPackLimits {
1717                    max_items: MAX_STAGED_BLOB_PACK_ITEMS,
1718                    max_bytes: MAX_STAGED_BLOB_PACK_BYTES,
1719                },
1720            ) {
1721                Ok(candidates) if !candidates.is_empty() => candidates,
1722                Ok(_) => break,
1723                Err(error) => {
1724                    warn!("remote cache blob pack skipped: {error}");
1725                    break;
1726                }
1727            };
1728            // A pack and an individual fetch share these per-digest locks. Hold
1729            // them through ingestion so overlapping prefetch and foreground
1730            // requests cannot download or charge the same object twice.
1731            let mut pack_guards = BTreeMap::new();
1732            for digest in candidates {
1733                let guard = self.write_lock(&digest).lock_owned().await;
1734                match self.find_verified_blob(&digest) {
1735                    Ok(Some(path)) => {
1736                        pack_candidates.remove(&digest);
1737                        missing.remove(&digest);
1738                        verified.insert(digest, path);
1739                    }
1740                    Ok(None) => {
1741                        pack_guards.insert(digest, guard);
1742                    }
1743                    Err(error) => {
1744                        warn!(
1745                            "local cache blob lookup failed for {}: {error}",
1746                            digest.hash
1747                        );
1748                        pack_guards.insert(digest, guard);
1749                    }
1750                }
1751            }
1752            let requested = pack_guards.keys().cloned().collect::<Vec<_>>();
1753            if requested.is_empty() {
1754                continue;
1755            }
1756            let requested_bytes = requested
1757                .iter()
1758                .fold(0_u64, |total, digest| total.saturating_add(digest.size));
1759            let pack_reservation = match self
1760                .reserve_remote_download_up_to(requested_bytes.min(MAX_STAGED_BLOB_PACK_BYTES))
1761            {
1762                Ok(reservation) if reservation.bytes() > 0 => reservation,
1763                Ok(_) => break,
1764                Err(error) => {
1765                    warn!("remote cache blob pack skipped: {error}");
1766                    break;
1767                }
1768            };
1769            let (pack, transfer_duration_ns) = {
1770                let _prefetch_permit = match prefetch_limit {
1771                    Some(limit) => match limit.acquire().await {
1772                        Ok(permit) => Some(permit),
1773                        Err(error) => {
1774                            warn!(
1775                                "remote cache blob pack could not acquire prefetch limit: {error}"
1776                            );
1777                            break;
1778                        }
1779                    },
1780                    None => None,
1781                };
1782                let _transfer_permit = match self.remote_transfers.acquire().await {
1783                    Ok(permit) => permit,
1784                    Err(error) => {
1785                        warn!("remote cache blob pack could not acquire transfer limit: {error}");
1786                        break;
1787                    }
1788                };
1789                let transfer_started = Instant::now();
1790                let pack = remote
1791                    .get_blob_pack_with_limit(
1792                        &requested,
1793                        self.remote_staging_dir.as_path(),
1794                        pack_reservation.bytes(),
1795                    )
1796                    .await;
1797                (pack, duration_ns(transfer_started))
1798            };
1799            let pack = match pack {
1800                Ok(Some(pack)) => pack,
1801                Ok(None) => break,
1802                Err(error) => {
1803                    atomic_saturating_add(
1804                        &self.stats.remote_blob_transfer_duration_ns,
1805                        transfer_duration_ns,
1806                    );
1807                    warn!(
1808                        "remote cache blob pack failed; falling back to individual blobs: {error}"
1809                    );
1810                    break;
1811                }
1812            };
1813            pack_reservation.commit(pack.payload_bytes);
1814            atomic_saturating_add(
1815                &self.stats.remote_blob_transfer_duration_ns,
1816                transfer_duration_ns,
1817            );
1818            atomic_saturating_add(&self.stats.remote_blob_pack_requests, pack.requests);
1819            atomic_saturating_add(&self.stats.remote_blob_pack_blobs, pack.blob_count);
1820            atomic_saturating_add(&self.stats.downloaded_bytes, pack.payload_bytes);
1821            if pack.requested.is_empty() {
1822                // The server's negotiated cap can be smaller than the local
1823                // staging cap used to select this locked slice. Fall back for
1824                // this slice, but keep packing later candidates that may fit.
1825                for digest in &requested {
1826                    pack_candidates.remove(digest);
1827                }
1828                continue;
1829            }
1830            for digest in &pack.requested {
1831                pack_candidates.remove(digest);
1832            }
1833            let mut ingests = stream::iter(pack.blobs.into_iter().map(|(digest, source)| {
1834                let digest_for_result = digest.clone();
1835                let guard = pack_guards
1836                    .remove(&digest)
1837                    .expect("requested packed blob has a write lock");
1838                async move {
1839                    (
1840                        digest_for_result,
1841                        self.ingest_packed_blob(digest, source, guard).await,
1842                    )
1843                }
1844            }))
1845            .buffer_unordered(MAX_PREFETCH_TRANSFERS);
1846            while let Some((digest, result)) = ingests.next().await {
1847                match result {
1848                    Ok(path) => {
1849                        missing.remove(&digest);
1850                        verified.insert(digest, path);
1851                    }
1852                    Err(error) => warn!(
1853                        "remote cache packed blob ingest failed for {}: {error}",
1854                        digest.hash
1855                    ),
1856                }
1857            }
1858        }
1859
1860        let mut transfers = stream::iter(missing.into_keys().map(|digest| {
1861            let digest_for_result = digest.clone();
1862            async move {
1863                (
1864                    digest_for_result,
1865                    self.fetch_remote_blob_with_limit(remote, &digest, prefetch_limit)
1866                        .await,
1867                )
1868            }
1869        }))
1870        .buffer_unordered(MAX_PREFETCH_TRANSFERS);
1871        while let Some((digest, result)) = transfers.next().await {
1872            match result {
1873                Ok(path) => {
1874                    verified.insert(digest, path);
1875                }
1876                Err(error) => warn!(
1877                    "remote cache blob prefetch failed for {}: {error}",
1878                    digest.hash
1879                ),
1880            }
1881        }
1882        verified
1883    }
1884
1885    async fn ingest_packed_blob(
1886        &self,
1887        digest: CacheDigest,
1888        source: PathBuf,
1889        _guard: tokio::sync::OwnedMutexGuard<()>,
1890    ) -> Result<PathBuf> {
1891        let digest_size = digest.size;
1892        let agent = self.clone();
1893        let (path, stored, cas_duration_ns) = tokio::task::spawn_blocking(move || {
1894            if let Some(path) = agent.find_verified_blob(&digest)? {
1895                return Ok::<_, eyre::Report>((path, false, 0));
1896            }
1897            let cas_started = Instant::now();
1898            let path = agent.cas.store_verified_file(&digest, &source)?;
1899            let cas_duration_ns = duration_ns(cas_started);
1900            agent.remember_verified_blob(&digest, &path);
1901            Ok((path, true, cas_duration_ns))
1902        })
1903        .await??;
1904        atomic_saturating_add(&self.stats.local_cas_write_duration_ns, cas_duration_ns);
1905        if stored {
1906            self.stats.stores.fetch_add(1, Ordering::Relaxed);
1907            atomic_saturating_add(&self.stats.stored_bytes, digest_size);
1908        }
1909        Ok(path)
1910    }
1911
1912    fn parse_rustc_metadata(path: &Path) -> Result<RustcMetadata> {
1913        let bytes = fs::read(path)?;
1914        let metadata: RustcMetadata = serde_json::from_slice(&bytes)?;
1915        if metadata.version != 1 || metadata.kind != "rustc" || canonical_json(&metadata)? != bytes
1916        {
1917            bail!("remote rustc action metadata is invalid");
1918        }
1919        Ok(metadata)
1920    }
1921
1922    fn parse_cache_directory(path: &Path) -> Result<CacheDirectory> {
1923        let bytes = fs::read(path)?;
1924        let directory: CacheDirectory = serde_json::from_slice(&bytes)?;
1925        if directory.version != 1 || canonical_json(&directory)? != bytes {
1926            bail!("remote action output directory is invalid");
1927        }
1928        Ok(directory)
1929    }
1930
1931    #[cfg(test)]
1932    fn load_cache_directory(&self, digest: &CacheDigest) -> Result<CacheDirectory> {
1933        let path = self
1934            .find_verified_blob(digest)?
1935            .ok_or_else(|| eyre::eyre!("remote action output directory is missing"))?;
1936        Self::parse_cache_directory(&path)
1937    }
1938
1939    fn validate_prefetched_action(
1940        action: &PrefetchedAction,
1941        verified: &BTreeMap<CacheDigest, PathBuf>,
1942        rustc_metadata: &BTreeMap<CacheDigest, RustcMetadata>,
1943        directories: &BTreeMap<CacheDigest, CacheDirectory>,
1944    ) -> Result<()> {
1945        if !verified.contains_key(&action.result.action) {
1946            bail!("remote action descriptor is missing");
1947        }
1948        if let Some(metadata) = &action.result.metadata {
1949            if action.adapter == "rustc" {
1950                let metadata = rustc_metadata
1951                    .get(metadata)
1952                    .ok_or_else(|| eyre::eyre!("remote rustc action metadata is missing"))?;
1953                for digest in [&metadata.stdout, &metadata.stderr] {
1954                    if !verified.contains_key(digest) {
1955                        bail!("remote rustc action diagnostic blob is missing");
1956                    }
1957                }
1958            } else if !verified.contains_key(metadata) {
1959                bail!("remote action metadata is missing");
1960            }
1961        }
1962        let mut pending = action
1963            .result
1964            .output_root
1965            .iter()
1966            .cloned()
1967            .collect::<Vec<_>>();
1968        let mut seen = BTreeMap::new();
1969        while let Some(digest) = pending.pop() {
1970            if seen.insert(digest.clone(), ()).is_some() {
1971                continue;
1972            }
1973            if seen.len() > MAX_PREFETCH_DIRECTORY_OBJECTS {
1974                bail!("remote action output tree is too large");
1975            }
1976            let directory = directories
1977                .get(&digest)
1978                .ok_or_else(|| eyre::eyre!("remote action output directory is missing"))?;
1979            for file in &directory.files {
1980                if !verified.contains_key(&file.digest) {
1981                    bail!("remote action output file is missing");
1982                }
1983            }
1984            pending.extend(
1985                directory
1986                    .directories
1987                    .iter()
1988                    .map(|directory| directory.digest.clone()),
1989            );
1990        }
1991        Ok(())
1992    }
1993
1994    #[cfg(test)]
1995    async fn prefetch_output_tree(
1996        &self,
1997        remote: &RemoteCacheClient,
1998        output_root: &CacheDigest,
1999    ) -> Result<()> {
2000        let mut pending = vec![output_root.clone()];
2001        let mut seen = BTreeMap::new();
2002        while let Some(digest) = pending.pop() {
2003            if seen.insert(digest.clone(), ()).is_some() {
2004                continue;
2005            }
2006            if seen.len() > MAX_PREFETCH_DIRECTORY_OBJECTS {
2007                bail!("remote action output tree is too large");
2008            }
2009            self.fetch_remote_blob_with_limit(remote, &digest, Some(&self.prefetch_transfers))
2010                .await?;
2011            let directory = self.load_cache_directory(&digest)?;
2012            let mut transfers = stream::iter(directory.files.into_iter().map(|file| async move {
2013                self.fetch_remote_blob_with_limit(
2014                    remote,
2015                    &file.digest,
2016                    Some(&self.prefetch_transfers),
2017                )
2018                .await
2019                .map(|_| ())
2020            }))
2021            .buffer_unordered(MAX_PREFETCH_TRANSFERS);
2022            while let Some(result) = transfers.next().await {
2023                result?;
2024            }
2025            pending.extend(
2026                directory
2027                    .directories
2028                    .into_iter()
2029                    .map(|directory| directory.digest),
2030            );
2031        }
2032        Ok(())
2033    }
2034
2035    async fn fetch_remote_blob(
2036        &self,
2037        remote: &RemoteCacheClient,
2038        digest: &CacheDigest,
2039    ) -> Result<PathBuf> {
2040        self.fetch_remote_blob_with_limit(remote, digest, None)
2041            .await
2042    }
2043
2044    async fn fetch_remote_blob_with_limit(
2045        &self,
2046        remote: &RemoteCacheClient,
2047        digest: &CacheDigest,
2048        prefetch_limit: Option<&tokio::sync::Semaphore>,
2049    ) -> Result<PathBuf> {
2050        let lock = self.write_lock(digest);
2051        let _guard = lock.lock().await;
2052        if let Some(path) = self.find_verified_blob(digest)? {
2053            return Ok(path);
2054        }
2055        let _prefetch_permit = match prefetch_limit {
2056            Some(limit) => Some(limit.acquire().await?),
2057            None => None,
2058        };
2059        let _permit = self.remote_transfers.acquire().await?;
2060        let reservation = self.reserve_remote_download(digest.size)?;
2061        self.stats
2062            .remote_blob_requests
2063            .fetch_add(1, Ordering::Relaxed);
2064        let transfer_timer =
2065            AtomicDurationTimer::start(&self.stats.remote_blob_transfer_duration_ns);
2066        let temporary = remote
2067            .get_blob_file(digest, self.remote_staging_dir.as_path())
2068            .await?;
2069        drop(transfer_timer);
2070        let _cas_timer = AtomicDurationTimer::start(&self.stats.local_cas_write_duration_ns);
2071        let path = self.cas.store_verified_file(digest, temporary.path())?;
2072        reservation.commit(digest.size);
2073        self.remember_verified_blob(digest, &path);
2074        self.stats.stores.fetch_add(1, Ordering::Relaxed);
2075        self.stats
2076            .stored_bytes
2077            .fetch_add(digest.size, Ordering::Relaxed);
2078        self.stats
2079            .downloaded_bytes
2080            .fetch_add(digest.size, Ordering::Relaxed);
2081        Ok(path)
2082    }
2083
2084    /// Answer one request outside any connection.
2085    ///
2086    /// An upload queued this way has no sibling requests to order against, so
2087    /// the queue falls back to treating every blob it holds as a prerequisite.
2088    #[cfg(test)]
2089    async fn respond(&self, request: AgentRequest) -> AgentResponse {
2090        self.respond_on(request, &mut ConnectionUploads::default())
2091            .await
2092    }
2093
2094    async fn respond_on(
2095        &self,
2096        request: AgentRequest,
2097        connection: &mut ConnectionUploads,
2098    ) -> AgentResponse {
2099        let result = match request {
2100            AgentRequest::BeginTask { task } => self
2101                .begin_task(&task)
2102                .await
2103                .map(|run| AgentResponse::TaskBegun { run }),
2104            AgentRequest::CommitTask { run } => self
2105                .commit_task(&run)
2106                .await
2107                .map(|()| AgentResponse::TaskCommitted),
2108            AgentRequest::FindBlob { digest } => self.find_blob(&digest).await,
2109            AgentRequest::FindBlobs { digests } => self.find_blobs(digests).await,
2110            AgentRequest::StoreBlob { digest, source } => {
2111                self.store_blob(&digest, &source, connection).await
2112            }
2113            AgentRequest::FindActionResult { action } => {
2114                self.stats.lookups.fetch_add(1, Ordering::Relaxed);
2115                self.find_action_result(&action).await
2116            }
2117            AgentRequest::RecordActionHit {
2118                action,
2119                restore,
2120                crate_name,
2121            } => self.record_action_hit(&action, restore, crate_name),
2122            AgentRequest::RecordBypass { kind } => {
2123                *self
2124                    .stats
2125                    .bypasses
2126                    .lock()
2127                    .unwrap()
2128                    .entry(kind.clone())
2129                    .or_insert(0) += 1;
2130                self.emit(|| AgentEvent::Bypass { kind });
2131                Ok(AgentResponse::BypassRecorded)
2132            }
2133            AgentRequest::RecordUnconsulted => {
2134                self.stats.unconsulted.fetch_add(1, Ordering::Relaxed);
2135                self.emit(|| AgentEvent::Unconsulted);
2136                Ok(AgentResponse::UnconsultedRecorded)
2137            }
2138            AgentRequest::RecordCompilerInvocation {
2139                outcome,
2140                crate_name,
2141                duration_ns,
2142            } => self.record_compiler_invocation(&outcome, crate_name.as_deref(), duration_ns),
2143            AgentRequest::RecordActionVerification { matched, restore } => {
2144                self.record_materialization(restore);
2145                self.stats.verifications.fetch_add(1, Ordering::Relaxed);
2146                if !matched {
2147                    self.stats.divergences.fetch_add(1, Ordering::Relaxed);
2148                }
2149                self.emit(|| AgentEvent::Verification { matched, restore });
2150                Ok(AgentResponse::ActionVerificationRecorded)
2151            }
2152            AgentRequest::StoreActionResult { result } => {
2153                self.store_action_result(&result, connection).await
2154            }
2155            AgentRequest::FindActionPrediction { task, invocation } => {
2156                self.find_action_prediction(&task, &invocation)
2157            }
2158            AgentRequest::RecordActionPrediction { task, prediction } => {
2159                self.record_action_prediction(&task, prediction)
2160            }
2161            AgentRequest::FindExecutableIdentity {
2162                executable,
2163                environment,
2164            } => self.find_executable_identity(executable, environment),
2165            AgentRequest::StoreExecutableIdentity {
2166                executable,
2167                environment,
2168                stdout,
2169            } => self.store_executable_identity(executable, environment, stdout),
2170            AgentRequest::Hello { .. } => {
2171                Err(eyre::eyre!("hello is only valid as the first request"))
2172            }
2173        };
2174        result.unwrap_or_else(|error| AgentResponse::Error {
2175            message: error.to_string(),
2176        })
2177    }
2178
2179    async fn find_blob(&self, digest: &CacheDigest) -> Result<AgentResponse> {
2180        if let Some(path) = self.find_verified_blob(digest)? {
2181            return Ok(AgentResponse::Blob { path: Some(path) });
2182        }
2183        if !self.remote_mode.reads() {
2184            return Ok(AgentResponse::Blob { path: None });
2185        }
2186        let Some(remote) = &self.remote else {
2187            return Ok(AgentResponse::Blob { path: None });
2188        };
2189        match self.fetch_remote_blob(remote, digest).await {
2190            Ok(path) => Ok(AgentResponse::Blob { path: Some(path) }),
2191            Err(error) => {
2192                warn!(
2193                    "remote cache blob lookup failed for {}: {error}",
2194                    digest.hash
2195                );
2196                Ok(AgentResponse::Blob { path: None })
2197            }
2198        }
2199    }
2200
2201    async fn find_blobs(&self, digests: Vec<CacheDigest>) -> Result<AgentResponse> {
2202        let mut paths = BTreeMap::new();
2203        let mut missing = Vec::new();
2204        for digest in &digests {
2205            match self.find_verified_blob(digest)? {
2206                Some(path) => {
2207                    paths.insert(digest.clone(), path);
2208                }
2209                None => {
2210                    missing.push(digest.clone());
2211                }
2212            }
2213        }
2214
2215        if !missing.is_empty()
2216            && self.remote_mode.reads()
2217            && let Some(remote) = &self.remote
2218        {
2219            paths.extend(self.fetch_remote_blobs(remote, missing, None).await);
2220        }
2221
2222        Ok(AgentResponse::Blobs {
2223            paths: digests
2224                .into_iter()
2225                .map(|digest| paths.get(&digest).cloned())
2226                .collect(),
2227        })
2228    }
2229
2230    async fn store_blob(
2231        &self,
2232        digest: &CacheDigest,
2233        source: &Path,
2234        connection: &mut ConnectionUploads,
2235    ) -> Result<AgentResponse> {
2236        let path = {
2237            let lock = self.write_lock(digest);
2238            let _guard = lock.lock().await;
2239            if let Some(path) = self.find_verified_blob(digest)? {
2240                path
2241            } else {
2242                let path = self.cas.store_file(digest, source)?;
2243                self.remember_verified_blob(digest, &path);
2244                self.stats.stores.fetch_add(1, Ordering::Relaxed);
2245                self.stats
2246                    .stored_bytes
2247                    .fetch_add(digest.size, Ordering::Relaxed);
2248                path
2249            }
2250        };
2251        // The object is durable locally, so the build has what it needs and the
2252        // remote publication can happen after this request returns.
2253        if let Some(uploads) = &self.uploads {
2254            uploads.queue_blob(digest, path.clone(), connection);
2255        }
2256        Ok(AgentResponse::Stored { path })
2257    }
2258
2259    fn find_verified_blob(&self, digest: &CacheDigest) -> Result<Option<PathBuf>> {
2260        let remembered = self.verified_blobs.lock().unwrap().get(digest).cloned();
2261        if let Some(path) = remembered {
2262            if digest.matches_file(&path).unwrap_or(false) {
2263                return Ok(Some(path));
2264            }
2265            self.verified_blobs.lock().unwrap().remove(digest);
2266        }
2267        let path = self.cas.find(digest)?;
2268        if let Some(path) = &path {
2269            self.remember_verified_blob(digest, path);
2270        }
2271        Ok(path)
2272    }
2273
2274    fn remember_verified_blob(&self, digest: &CacheDigest, path: &Path) {
2275        self.verified_blobs
2276            .lock()
2277            .unwrap()
2278            .insert(digest.clone(), path.to_path_buf());
2279    }
2280
2281    async fn find_action_result(&self, action: &CacheDigest) -> Result<AgentResponse> {
2282        if let Some(result) = self.actions.find(action)? {
2283            return Ok(AgentResponse::ActionResult {
2284                result: Some(result),
2285            });
2286        }
2287        if !self.remote_mode.reads() {
2288            return Ok(AgentResponse::ActionResult { result: None });
2289        }
2290        let Some(remote) = &self.remote else {
2291            return Ok(AgentResponse::ActionResult { result: None });
2292        };
2293        let lock = self.action_lock(action);
2294        let _guard = lock.lock().await;
2295        if let Some(result) = self.actions.find(action)? {
2296            return Ok(AgentResponse::ActionResult {
2297                result: Some(result),
2298            });
2299        }
2300        if let Some(result) = self
2301            .pending_remote_actions
2302            .lock()
2303            .unwrap()
2304            .get(action)
2305            .cloned()
2306        {
2307            return Ok(AgentResponse::ActionResult {
2308                result: Some(result),
2309            });
2310        }
2311        let _permit = self.remote_transfers.acquire().await?;
2312        match self.get_remote_action_result(remote, action).await {
2313            Ok(Some(result)) => {
2314                self.pending_remote_actions
2315                    .lock()
2316                    .unwrap()
2317                    .insert(action.clone(), result.clone());
2318                Ok(AgentResponse::ActionResult {
2319                    result: Some(result),
2320                })
2321            }
2322            Ok(None) => Ok(AgentResponse::ActionResult { result: None }),
2323            Err(error) => {
2324                self.note_remote_failure();
2325                warn!(
2326                    "remote cache action lookup failed for {}: {error}",
2327                    action.hash
2328                );
2329                Ok(AgentResponse::ActionResult { result: None })
2330            }
2331        }
2332    }
2333
2334    async fn store_action_result(
2335        &self,
2336        result: &RemoteActionResult,
2337        connection: &ConnectionUploads,
2338    ) -> Result<AgentResponse> {
2339        let path = self.actions.store(result)?;
2340        if let Some(uploads) = &self.uploads {
2341            uploads.queue_action_result(result, connection);
2342        }
2343        Ok(AgentResponse::ActionStored { path })
2344    }
2345
2346    /// Record that a remote operation failed and the build carried on without it.
2347    fn note_remote_failure(&self) {
2348        self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
2349    }
2350
2351    async fn get_remote_action_result(
2352        &self,
2353        remote: &RemoteCacheClient,
2354        action: &CacheDigest,
2355    ) -> Result<Option<RemoteActionResult>> {
2356        self.stats
2357            .remote_action_lookups
2358            .fetch_add(1, Ordering::Relaxed);
2359        let _timer = AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);
2360        remote.get_action_result(action).await
2361    }
2362
2363    fn record_action_hit(
2364        &self,
2365        action: &CacheDigest,
2366        restore: RestoreStats,
2367        crate_name: Option<String>,
2368    ) -> Result<AgentResponse> {
2369        validate_crate_name(crate_name.as_deref())?;
2370        if self.actions.find(action)?.is_none() {
2371            let pending = self.pending_remote_actions.lock().unwrap().remove(action);
2372            if let Some(result) = pending {
2373                self.actions.store(&result)?;
2374            } else {
2375                bail!("cannot record a hit for a missing action result");
2376            }
2377        }
2378        self.record_restore(restore);
2379        self.stats.hits.fetch_add(1, Ordering::Relaxed);
2380        self.emit(|| AgentEvent::ActionHit {
2381            crate_name,
2382            restore,
2383        });
2384        Ok(AgentResponse::ActionHitRecorded)
2385    }
2386
2387    fn record_restore(&self, restore: RestoreStats) {
2388        self.record_materialization(restore);
2389        atomic_saturating_add(
2390            &self.stats.avoided_compiler_duration_ns,
2391            restore.avoided_compiler_duration_ns,
2392        );
2393        atomic_saturating_add(&self.stats.restored_output_files, restore.output_files);
2394        atomic_saturating_add(&self.stats.restored_output_bytes, restore.output_bytes);
2395        atomic_saturating_add(
2396            &self.stats.reflinked_output_files,
2397            restore.reflinked_output_files,
2398        );
2399        atomic_saturating_add(
2400            &self.stats.reflinked_output_bytes,
2401            restore.reflinked_output_bytes,
2402        );
2403        atomic_saturating_add(&self.stats.copied_output_files, restore.copied_output_files);
2404        atomic_saturating_add(&self.stats.copied_output_bytes, restore.copied_output_bytes);
2405    }
2406
2407    fn record_compiler_invocation(
2408        &self,
2409        outcome: &str,
2410        crate_name: Option<&str>,
2411        duration_ns: u64,
2412    ) -> Result<AgentResponse> {
2413        if !matches!(
2414            outcome,
2415            "miss" | "unconsulted" | "bypass" | "verification" | "incremental"
2416        ) {
2417            bail!("invalid compiler invocation outcome");
2418        }
2419        validate_crate_name(crate_name)?;
2420        let mut compiler = self.stats.compiler.lock().unwrap();
2421        let stats = compiler.entry(outcome.to_string()).or_default();
2422        stats.invocations = stats.invocations.saturating_add(1);
2423        stats.duration_ns = stats.duration_ns.saturating_add(duration_ns);
2424        drop(compiler);
2425        if outcome != "verification"
2426            && let Some(crate_name) = crate_name.filter(|name| !name.is_empty())
2427        {
2428            let mut slow = self.stats.slow_compilations.lock().unwrap();
2429            let duration = slow.entry(crate_name.to_string()).or_default();
2430            *duration = duration.saturating_add(duration_ns);
2431        }
2432        self.emit(|| AgentEvent::CompilerInvocation {
2433            outcome: outcome.to_string(),
2434            crate_name: crate_name.map(str::to_string),
2435            duration_ns,
2436        });
2437        Ok(AgentResponse::CompilerInvocationRecorded)
2438    }
2439
2440    fn record_materialization(&self, restore: RestoreStats) {
2441        atomic_saturating_add(&self.stats.materialization_duration_ns, restore.duration_ns);
2442    }
2443
2444    fn find_action_prediction(
2445        &self,
2446        task: &str,
2447        invocation: &CacheDigest,
2448    ) -> Result<AgentResponse> {
2449        validate_task_identity(task)?;
2450        invocation.validate()?;
2451        let prediction = self
2452            .task_actions
2453            .lock()
2454            .unwrap()
2455            .get(task)
2456            .and_then(|state| state.predictions.get(invocation))
2457            .cloned();
2458        Ok(AgentResponse::ActionPrediction { prediction })
2459    }
2460
2461    fn record_action_prediction(
2462        &self,
2463        task: &str,
2464        prediction: ActionPrediction,
2465    ) -> Result<AgentResponse> {
2466        validate_task_identity(task)?;
2467        validate_action_prediction(&prediction)?;
2468        let mut tasks = self.task_actions.lock().unwrap();
2469        let state = tasks.entry(task.to_string()).or_default();
2470        if !state.predictions.contains_key(&prediction.invocation)
2471            && state.predictions.len() >= MAX_TASK_ACTION_PREDICTIONS
2472        {
2473            bail!("task action manifest contains too many predictions");
2474        }
2475        state
2476            .predictions
2477            .insert(prediction.invocation.clone(), prediction.clone());
2478        state
2479            .pending_predictions
2480            .insert(prediction.invocation.clone(), prediction);
2481        Ok(AgentResponse::ActionPredictionRecorded)
2482    }
2483
2484    fn executable_identity_key(
2485        &self,
2486        executable: PathBuf,
2487        environment: BTreeMap<String, Option<String>>,
2488    ) -> Result<ExecutableIdentityKey> {
2489        // Restricted to the variables that actually select what an identity
2490        // probe reports: the toolchain rustup resolves, and the SDK a linker
2491        // driver builds against. Anything else would let one key stand for two
2492        // different compilers.
2493        if !environment.keys().all(|name| {
2494            matches!(
2495                name.as_str(),
2496                "RUSTUP_HOME" | "RUSTUP_TOOLCHAIN" | "SDKROOT" | "MACOSX_DEPLOYMENT_TARGET"
2497            )
2498        }) {
2499            bail!("executable identity contains an unsupported environment variable");
2500        }
2501        Ok(ExecutableIdentityKey {
2502            executable,
2503            environment,
2504        })
2505    }
2506
2507    fn find_executable_identity(
2508        &self,
2509        executable: PathBuf,
2510        environment: BTreeMap<String, Option<String>>,
2511    ) -> Result<AgentResponse> {
2512        let key = self.executable_identity_key(executable, environment)?;
2513        let stdout = self
2514            .executable_identities
2515            .lock()
2516            .unwrap()
2517            .get(&key)
2518            .cloned();
2519        Ok(AgentResponse::ExecutableIdentity { stdout })
2520    }
2521
2522    fn store_executable_identity(
2523        &self,
2524        executable: PathBuf,
2525        environment: BTreeMap<String, Option<String>>,
2526        stdout: Vec<u8>,
2527    ) -> Result<AgentResponse> {
2528        if stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE {
2529            bail!("executable identity exceeds {MAX_EXECUTABLE_IDENTITY_SIZE} bytes");
2530        }
2531        let key = self.executable_identity_key(executable, environment)?;
2532        let mut identities = self.executable_identities.lock().unwrap();
2533        let is_new = !identities.contains_key(&key);
2534        let previous_size = identities.get(&key).map_or(0, Vec::len);
2535        if is_new && identities.len() >= MAX_EXECUTABLE_IDENTITIES {
2536            bail!("executable identity cache contains too many entries");
2537        }
2538        let retained_bytes = identities.values().map(Vec::len).sum::<usize>();
2539        if retained_bytes - previous_size + stdout.len() > MAX_EXECUTABLE_IDENTITY_BYTES {
2540            bail!("executable identity cache contains too many bytes");
2541        }
2542        identities.insert(key, stdout.clone());
2543        Ok(AgentResponse::ExecutableIdentity {
2544            stdout: Some(stdout),
2545        })
2546    }
2547
2548    /// Serve newline-delimited protocol requests on an authenticated session stream.
2549    pub async fn handle_connection<S>(&self, stream: S) -> Result<()>
2550    where
2551        S: AsyncRead + AsyncWrite + Unpin,
2552    {
2553        let (reader, mut writer) = tokio::io::split(stream);
2554        let mut reader = BufReader::new(reader);
2555        let hello = read_request(&mut reader)
2556            .await?
2557            .ok_or_else(|| eyre::eyre!("connection closed before the agent handshake"))?;
2558        let request: AgentRequest = serde_json::from_str(&hello)?;
2559        match request {
2560            AgentRequest::Hello {
2561                protocol,
2562                client_version,
2563            } if protocol == AGENT_PROTOCOL_VERSION && client_version == self.version.as_ref() => {}
2564            AgentRequest::Hello { protocol, .. } if protocol != AGENT_PROTOCOL_VERSION => {
2565                send_response(
2566                    &mut writer,
2567                    &AgentResponse::Error {
2568                        message: format!(
2569                            "unsupported agent protocol {protocol}; expected {AGENT_PROTOCOL_VERSION}"
2570                        ),
2571                    },
2572                )
2573                .await?;
2574                return Ok(());
2575            }
2576            AgentRequest::Hello { client_version, .. } => {
2577                send_response(
2578                    &mut writer,
2579                    &AgentResponse::Error {
2580                        message: format!(
2581                            "cache client {client_version} does not match agent {}",
2582                            self.version
2583                        ),
2584                    },
2585                )
2586                .await?;
2587                return Ok(());
2588            }
2589            _ => bail!("the first agent request must be hello"),
2590        }
2591        send_response(
2592            &mut writer,
2593            &AgentResponse::Hello {
2594                protocol: AGENT_PROTOCOL_VERSION,
2595                agent_version: self.version.to_string(),
2596            },
2597        )
2598        .await?;
2599
2600        // Tickets accumulate for the life of the connection because a shim
2601        // publishes a compilation's blobs and the action result naming them over
2602        // one connection, in that order.
2603        let mut connection = ConnectionUploads::default();
2604        while let Some(line) = read_request(&mut reader).await? {
2605            let response = match serde_json::from_str(&line) {
2606                Ok(request) => self.respond_on(request, &mut connection).await,
2607                Err(error) => AgentResponse::Error {
2608                    message: format!("invalid agent request: {error}"),
2609                },
2610            };
2611            send_response(&mut writer, &response).await?;
2612        }
2613        Ok(())
2614    }
2615}
2616
2617/// Read one newline-delimited request, refusing one that grows past the cap.
2618///
2619/// Any process running as this user can open the session socket, so a request
2620/// that never terminates its line must not be able to grow the agent's memory
2621/// without bound.
2622async fn read_request<R>(reader: &mut R) -> Result<Option<String>>
2623where
2624    R: AsyncBufRead + Unpin,
2625{
2626    let mut line = Vec::new();
2627    loop {
2628        let available = reader.fill_buf().await?;
2629        if available.is_empty() {
2630            break;
2631        }
2632        let (consumed, complete) = match available.iter().position(|byte| *byte == b'\n') {
2633            Some(index) => (index, true),
2634            None => (available.len(), false),
2635        };
2636        if line.len() + consumed > MAX_REQUEST_BYTES {
2637            bail!("agent request exceeded {MAX_REQUEST_BYTES} bytes");
2638        }
2639        line.extend_from_slice(&available[..consumed]);
2640        // The newline itself is consumed but never kept.
2641        reader.consume(consumed + usize::from(complete));
2642        if complete {
2643            return Ok(Some(String::from_utf8(line)?));
2644        }
2645    }
2646    if line.is_empty() {
2647        Ok(None)
2648    } else {
2649        Ok(Some(String::from_utf8(line)?))
2650    }
2651}
2652
2653/// Whether `task` is a well-formed task action identity.
2654///
2655/// Identities name files and directories in the store, so anything that reads
2656/// the store back has to be able to tell an identity from whatever else a user
2657/// left lying there.
2658pub fn is_task_identity(task: &str) -> bool {
2659    task.len() == 64
2660        && task
2661            .bytes()
2662            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2663}
2664
2665fn validate_task_identity(task: &str) -> Result<()> {
2666    if !is_task_identity(task) {
2667        bail!("invalid task action identity");
2668    }
2669    Ok(())
2670}
2671
2672/// Where a store keeps its task prediction manifests.
2673fn task_manifest_dir(store: &Path) -> PathBuf {
2674    store.join("task-manifests").join("v1")
2675}
2676
2677/// The action digests a task's prediction manifest recorded.
2678///
2679/// Read straight off disk rather than through an agent, because a collector
2680/// needs the action set of tasks no session is running. A manifest that is
2681/// missing or no longer parseable yields no actions rather than an error: this
2682/// is a prediction index, so the worst a thin answer costs is a cold prefetch,
2683/// or an object collected earlier than it deserved.
2684pub fn task_manifest_actions(store: &Path, task: &str) -> Result<Vec<CacheDigest>> {
2685    validate_task_identity(task)?;
2686    let path = task_manifest_dir(store).join(format!("{task}.json"));
2687    let bytes = match fs::read(&path) {
2688        Ok(bytes) => bytes,
2689        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
2690        Err(error) => {
2691            return Err(error).wrap_err_with(|| format!("failed to read {}", path.display()));
2692        }
2693    };
2694    let Ok(manifest) = serde_json::from_slice::<TaskActionManifest>(&bytes) else {
2695        return Ok(Vec::new());
2696    };
2697    if validate_task_manifest(&manifest, task).is_err() {
2698        return Ok(Vec::new());
2699    }
2700    Ok(manifest
2701        .predictions
2702        .into_iter()
2703        .map(|prediction| prediction.action)
2704        .collect())
2705}
2706
2707fn validate_action_prediction(prediction: &ActionPrediction) -> Result<()> {
2708    if prediction.validate() {
2709        Ok(())
2710    } else {
2711        bail!("invalid action prediction")
2712    }
2713}
2714
2715fn validate_task_manifest(manifest: &TaskActionManifest, task: &str) -> Result<()> {
2716    if manifest.task == task && manifest.validate() {
2717        Ok(())
2718    } else {
2719        bail!("invalid task action manifest")
2720    }
2721}
2722
2723fn merge_task_manifests(
2724    task: &str,
2725    base: Option<TaskActionManifest>,
2726    update: TaskActionManifest,
2727) -> Result<TaskActionManifest> {
2728    validate_task_manifest(&update, task)?;
2729    let mut predictions = BTreeMap::new();
2730    if let Some(base) = base {
2731        validate_task_manifest(&base, task)?;
2732        predictions.extend(
2733            base.predictions
2734                .into_iter()
2735                .map(|prediction| (prediction.invocation.clone(), prediction)),
2736        );
2737    }
2738    predictions.extend(
2739        update
2740            .predictions
2741            .into_iter()
2742            .map(|prediction| (prediction.invocation.clone(), prediction)),
2743    );
2744    let manifest = TaskActionManifest {
2745        version: TASK_ACTION_MANIFEST_VERSION,
2746        task: task.to_owned(),
2747        predictions: predictions.into_values().collect(),
2748    };
2749    validate_task_manifest(&manifest, task)?;
2750    Ok(manifest)
2751}
2752
2753fn merge_remote_task_manifest(
2754    task: &str,
2755    remote: TaskActionManifest,
2756    local: TaskActionManifest,
2757) -> (TaskActionManifest, bool) {
2758    match merge_task_manifests(task, Some(remote), local.clone()) {
2759        Ok(manifest) => (manifest, true),
2760        Err(error) => {
2761            warn!("remote task action manifest merge failed for {task}: {error}");
2762            (local, false)
2763        }
2764    }
2765}
2766
2767async fn send_response(
2768    writer: &mut (impl AsyncWrite + Unpin),
2769    response: &AgentResponse,
2770) -> Result<()> {
2771    let mut encoded = serde_json::to_vec(response)?;
2772    encoded.push(b'\n');
2773    writer.write_all(&encoded).await?;
2774    writer.flush().await?;
2775    Ok(())
2776}
2777
2778#[cfg(test)]
2779#[path = "agent_tests.rs"]
2780mod tests;