1use crate::uploads::{ConnectionUploads, UploadQueue, UploadSink};
2use crate::{
3 ActionPrediction, ActionPromiseCompletion, ActionPromiseState, CacheDigest, CacheDirectory,
4 LocalActionCache, LocalCas, MAX_STAGED_BLOB_PACK_BYTES, ManifestPutOutcome, RemoteActionResult,
5 RemoteCacheClient, RemoteCacheMode, RustcMetadata, TaskActionManifest, blob_pack_chunk,
6 canonical_json,
7};
8use eyre::{Context, Result, bail};
9use futures_util::{FutureExt, StreamExt, future::BoxFuture, stream};
10use log::{debug, info, warn};
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, BTreeSet};
13use std::fs;
14use std::path::{Path, PathBuf};
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::{Arc, Mutex, Weak};
17use std::time::{Duration, Instant, SystemTime};
18
19const ACTION_PROMISE_WAIT: Duration = Duration::from_secs(60 * 60);
23use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
24
25mod file_digest;
26mod manifest;
27mod prefetch;
28mod stats;
29mod wire;
30
31#[cfg(test)]
32pub(crate) use prefetch::select_prefetch_actions;
33
34pub use file_digest::{
35 FileDigestCache, FileDigestResolution, FileDigestScope, FileIdentity, FileObjectIdentity,
36 FileSnapshot, NoFileDigestCache, RecordedFileDigest, digest_file,
37};
38pub use manifest::{is_task_identity, task_manifest_actions};
39use manifest::{
40 merge_remote_task_manifest, merge_task_manifests, task_manifest_dir, validate_task_identity,
41 validate_task_manifest,
42};
43pub use stats::{AgentStats, CompilerStats};
44use wire::MAX_REQUEST_BYTES;
45pub use wire::{
46 AGENT_PROTOCOL_VERSION, ActionDiagnostic, AgentEvent, AgentEventObserver, AgentRequest,
47 AgentResponse, PinnedFile, PinnedState, RestoreStats, WrapperSpan, WrapperTiming,
48};
49
50const MAX_EXECUTABLE_IDENTITIES: usize = 64;
51const MAX_EXECUTABLE_IDENTITY_SIZE: usize = 64 * 1024;
52const MAX_EXECUTABLE_IDENTITY_BYTES: usize = 256 * 1024;
53const TASK_ACTION_MANIFEST_VERSION: u8 = 1;
54const MAX_TASK_ACTION_PREDICTIONS: usize = 16 * 1024;
55const MAX_WARNING_BYTES: usize = 4 * 1024;
57const MAX_WARNINGS: usize = 128;
63const ACTION_DIAGNOSTIC_PREFIX: &str = "@mbx-action-diagnostic\t";
64const MAX_FILE_DIGEST_BATCH: usize = 16 * 1024;
66const MAX_FILE_DIGEST_ENTRIES: usize = 1024 * 1024;
71const MAX_CONCURRENT_FILE_DIGESTS: usize = 8;
74const MAX_REMOTE_TRANSFERS: usize = 64;
75const MAX_PREFETCH_TRANSFERS: usize = 48;
76const MAX_CONCURRENT_BLOB_PACKS: usize = 8;
82const MAX_PREFETCH_ACTIONS: usize = 1024;
93const MAX_PREFETCH_ACTION_WAVE: usize = 32;
98const MAX_PREFETCH_BATCH_LOOKUPS: usize = 1;
104const PREFETCH_ACTION_BATCH_DELAY: Duration = Duration::from_millis(5);
105const MAX_PREFETCH_DIRECTORY_OBJECTS: usize = 100_000;
106const MAX_PREFETCH_OBJECTS_PER_WAVE: usize = 100_000;
107const DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES: u64 = 5 * 1024 * 1024 * 1024;
108
109pub type TaskFallbacks = Arc<dyn Fn() -> Vec<String> + Send + Sync>;
112type FileDigestFlightKey = (FileDigestScope, FileIdentity);
113type FileDigestFlights = BTreeMap<FileDigestFlightKey, Weak<FileDigestFlight>>;
114
115struct FileDigestFlight {
116 lock: tokio::sync::Mutex<()>,
117 resolution: Mutex<Option<FileDigestResolution>>,
118}
119
120const NEWEST_MANIFEST_CANDIDATES: usize = 8;
132
133pub struct AgentRemoteCache {
135 pub client: RemoteCacheClient,
137 pub mode: RemoteCacheMode,
139 pub staging_dir: PathBuf,
141}
142
143#[derive(Default)]
144struct AtomicAgentStats {
145 wrapper_phases_ns: Mutex<BTreeMap<String, u64>>,
146 lookups: AtomicU64,
147 unconsulted: AtomicU64,
148 hits: AtomicU64,
149 stores: AtomicU64,
150 stored_bytes: AtomicU64,
151 verifications: AtomicU64,
152 divergences: AtomicU64,
153 downloaded_bytes: AtomicU64,
154 uploaded_bytes: AtomicU64,
155 background_uploads: AtomicU64,
156 background_upload_failures: AtomicU64,
157 remote_blob_pack_uploads: AtomicU64,
158 remote_blob_pack_upload_blobs: AtomicU64,
159 upload_drain_duration_ns: AtomicU64,
160 prefetched_actions: AtomicU64,
161 predictions_loaded: AtomicU64,
162 remote_failures: AtomicU64,
163 remote_manifest_lookups: AtomicU64,
164 remote_manifest_lookup_duration_ns: AtomicU64,
165 remote_action_lookups: AtomicU64,
166 remote_action_lookup_duration_ns: AtomicU64,
167 remote_blob_requests: AtomicU64,
168 remote_blob_pack_requests: AtomicU64,
169 remote_blob_pack_blobs: AtomicU64,
170 remote_blob_transfer_duration_ns: AtomicU64,
171 local_cas_write_duration_ns: AtomicU64,
172 prefetch_runs: AtomicU64,
173 prefetch_duration_ns: AtomicU64,
174 materialization_duration_ns: AtomicU64,
175 bypasses: Mutex<BTreeMap<String, u64>>,
176 avoided_compiler_duration_ns: AtomicU64,
177 compiler: Mutex<BTreeMap<String, CompilerStats>>,
178 slow_compilations: Mutex<BTreeMap<String, u64>>,
179 restored_output_files: AtomicU64,
180 restored_output_bytes: AtomicU64,
181 reflinked_output_files: AtomicU64,
182 reflinked_output_bytes: AtomicU64,
183 copied_output_files: AtomicU64,
184 copied_output_bytes: AtomicU64,
185 reused_output_files: AtomicU64,
186 reused_output_bytes: AtomicU64,
187}
188
189struct AtomicDurationTimer<'a> {
190 started: Instant,
191 target: &'a AtomicU64,
192}
193
194impl<'a> AtomicDurationTimer<'a> {
195 fn start(target: &'a AtomicU64) -> Self {
196 Self {
197 started: Instant::now(),
198 target,
199 }
200 }
201}
202
203impl Drop for AtomicDurationTimer<'_> {
204 fn drop(&mut self) {
205 atomic_saturating_add(self.target, duration_ns(self.started));
206 }
207}
208
209fn duration_ns(started: Instant) -> u64 {
210 started.elapsed().as_nanos().try_into().unwrap_or(u64::MAX)
211}
212
213fn validate_crate_name(crate_name: Option<&str>) -> Result<()> {
214 if let Some(crate_name) = crate_name
215 && (crate_name.len() > 256 || crate_name.contains(['\0', '\n', '\r']))
216 {
217 bail!("invalid compiler crate name");
218 }
219 Ok(())
220}
221
222#[derive(serde::Deserialize)]
223struct ActionDiagnosticEnvelope {
224 outcome: String,
225 crate_name: Option<String>,
226 diagnostic: ActionDiagnostic,
227}
228
229fn parse_action_diagnostic(
230 message: &str,
231) -> Option<Result<(String, Option<String>, ActionDiagnostic)>> {
232 let payload = message.strip_prefix(ACTION_DIAGNOSTIC_PREFIX)?;
233 Some((|| {
234 let envelope: ActionDiagnosticEnvelope = serde_json::from_str(payload)?;
235 if !matches!(envelope.outcome.as_str(), "hit" | "miss") {
236 bail!("invalid action diagnostic outcome");
237 }
238 validate_crate_name(envelope.crate_name.as_deref())?;
239 Ok((envelope.outcome, envelope.crate_name, envelope.diagnostic))
240 })())
241}
242
243fn atomic_saturating_add(target: &AtomicU64, value: u64) {
244 let _ = target.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
245 Some(current.saturating_add(value))
246 });
247}
248
249fn queue_prefetch_digest(
250 verified: &BTreeMap<CacheDigest, PathBuf>,
251 pending: &mut BTreeMap<CacheDigest, ()>,
252 digest: CacheDigest,
253) {
254 if verified.contains_key(&digest) || pending.contains_key(&digest) {
255 return;
256 }
257 pending.insert(digest, ());
258}
259
260fn queue_prefetch_directory(
261 seen: &BTreeMap<CacheDigest, ()>,
262 pending: &mut BTreeMap<CacheDigest, ()>,
263 digest: CacheDigest,
264 limit: usize,
265) -> bool {
266 if seen.contains_key(&digest) || pending.contains_key(&digest) {
267 return true;
268 }
269 if seen.len().saturating_add(pending.len()) >= limit {
270 return false;
271 }
272 pending.insert(digest, ());
273 true
274}
275
276pub type TaskLoader = dyn for<'a> Fn(&'a CacheAgent, &'a str) -> BoxFuture<'a, ()> + Send + Sync;
278
279#[derive(Clone)]
285pub struct CacheAgent {
286 cas: LocalCas,
287 actions: LocalActionCache,
288 verified_blobs: Arc<Mutex<BTreeMap<CacheDigest, VerifiedBlob>>>,
289 version: Arc<str>,
290 write_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
291 action_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
292 stats: Arc<AtomicAgentStats>,
293 observer: Option<Arc<dyn AgentEventObserver>>,
294 observer_emission: Arc<Mutex<()>>,
295 task_loader: Option<Arc<TaskLoader>>,
298 executable_identities: Arc<Mutex<BTreeMap<ExecutableIdentityKey, Vec<u8>>>>,
299 identity_dir: Arc<PathBuf>,
301 manifest_dir: Arc<PathBuf>,
302 task_actions: Arc<Mutex<BTreeMap<String, TaskActionState>>>,
303 task_fallbacks: Arc<Mutex<BTreeMap<String, TaskFallbacks>>>,
305 next_task_run: Arc<AtomicU64>,
306 manifest_write_lock: Arc<Mutex<()>>,
307 remote: Option<Arc<RemoteCacheClient>>,
308 remote_mode: RemoteCacheMode,
309 remote_staging_dir: Arc<PathBuf>,
310 remote_download_limit: u64,
311 remote_download_bytes: Arc<AtomicU64>,
312 pending_remote_actions: Arc<Mutex<BTreeMap<CacheDigest, RemoteActionResult>>>,
313 remote_transfers: Arc<tokio::sync::Semaphore>,
314 prefetch_transfers: Arc<tokio::sync::Semaphore>,
315 prefetch_tasks: Arc<Mutex<Vec<tokio::task::JoinHandle<()>>>>,
316 warnings: Arc<Mutex<BTreeSet<String>>>,
319 file_digests: Arc<Mutex<BTreeMap<(FileDigestScope, PathBuf), RecordedFileDigest>>>,
322 file_digest_locks: Arc<Mutex<FileDigestFlights>>,
324 file_digest_permits: Arc<tokio::sync::Semaphore>,
325 #[cfg(test)]
326 file_digest_reads: Arc<AtomicU64>,
327 uploads: Option<UploadQueue>,
329}
330
331fn validate_file_digest_record(entry: &RecordedFileDigest) -> Result<()> {
334 if !entry.file.path.is_absolute() {
335 bail!("file-digest records need absolute paths");
336 }
337 entry.digest.validate()?;
338 if entry.file.len != entry.digest.size {
339 bail!("file-digest record length does not match its digest");
340 }
341 Ok(())
342}
343
344struct AgentUploadSink {
346 stats: Arc<AtomicAgentStats>,
347}
348
349impl UploadSink for AgentUploadSink {
350 fn record_blob_uploaded(&self, bytes: u64) {
351 self.stats
352 .background_uploads
353 .fetch_add(1, Ordering::Relaxed);
354 self.stats
355 .uploaded_bytes
356 .fetch_add(bytes, Ordering::Relaxed);
357 }
358
359 fn record_action_uploaded(&self) {
360 self.stats
361 .background_uploads
362 .fetch_add(1, Ordering::Relaxed);
363 }
364
365 fn record_blob_pack_uploaded(&self, blobs: u64) {
366 self.stats
367 .remote_blob_pack_uploads
368 .fetch_add(1, Ordering::Relaxed);
369 self.stats
370 .remote_blob_pack_upload_blobs
371 .fetch_add(blobs, Ordering::Relaxed);
372 }
373
374 fn record_upload_failure(&self) {
375 self.stats
376 .background_upload_failures
377 .fetch_add(1, Ordering::Relaxed);
378 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
379 }
380}
381
382#[derive(Debug, Clone)]
388struct VerifiedBlob {
389 path: PathBuf,
390 len: u64,
391 modified: SystemTime,
392}
393
394impl VerifiedBlob {
395 fn describe(path: &Path) -> Option<Self> {
398 let metadata = std::fs::metadata(path).ok()?;
399 Some(Self {
400 path: path.to_path_buf(),
401 len: metadata.len(),
402 modified: metadata.modified().ok()?,
403 })
404 }
405
406 fn is_unchanged(&self) -> bool {
408 let Ok(metadata) = std::fs::metadata(&self.path) else {
409 return false;
410 };
411 metadata.len() == self.len && metadata.modified().is_ok_and(|now| now == self.modified)
412 }
413}
414
415#[derive(Debug, Clone, Default)]
416struct TaskActionState {
417 manifest: String,
418 baseline_loaded: bool,
419 predictions: BTreeMap<CacheDigest, ActionPrediction>,
420 pending_predictions: BTreeMap<CacheDigest, ActionPrediction>,
421 changed_predictions: BTreeMap<CacheDigest, Option<ActionPrediction>>,
428 prefetched_adapters: BTreeSet<String>,
429 remote_etag: Option<String>,
430}
431
432fn activate_prediction_adapter(
434 state: &mut TaskActionState,
435 invocation: &CacheDigest,
436) -> (Option<ActionPrediction>, Option<Vec<ActionPrediction>>) {
437 let prediction = state.predictions.get(invocation).cloned();
438 let prefetch = prediction.as_ref().and_then(|prediction| {
439 if !state.prefetched_adapters.insert(prediction.adapter.clone()) {
440 return None;
441 }
442 Some(
443 state
444 .predictions
445 .values()
446 .filter(|candidate| candidate.adapter == prediction.adapter)
447 .cloned()
448 .collect(),
449 )
450 });
451 (prediction, prefetch)
452}
453
454struct PrefetchedAction {
455 adapter: String,
456 result: RemoteActionResult,
457}
458
459struct RemoteDownloadReservation {
460 counter: Arc<AtomicU64>,
461 reserved: u64,
462 committed: bool,
463}
464
465impl RemoteDownloadReservation {
466 fn bytes(&self) -> u64 {
467 self.reserved
468 }
469
470 fn commit(mut self, bytes: u64) {
471 debug_assert!(bytes <= self.reserved);
472 self.counter
473 .fetch_sub(self.reserved.saturating_sub(bytes), Ordering::AcqRel);
474 self.committed = true;
475 }
476}
477
478impl Drop for RemoteDownloadReservation {
479 fn drop(&mut self) {
480 if !self.committed {
481 self.counter.fetch_sub(self.reserved, Ordering::AcqRel);
482 }
483 }
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
487struct ExecutableIdentityKey {
488 executable: PathBuf,
489 environment: BTreeMap<String, Option<String>>,
490}
491
492const PERSISTED_EXECUTABLE_IDENTITY_VERSION: u8 = 2;
494
495#[derive(Debug, Serialize, Deserialize)]
501struct PersistedExecutableIdentity {
502 version: u8,
503 executable: PathBuf,
504 environment: BTreeMap<String, Option<String>>,
505 pins: Vec<PinnedFile>,
506 stdout: String,
507}
508
509impl CacheAgent {
510 pub fn new(cache_dir: impl Into<PathBuf>, version: impl Into<Arc<str>>) -> Self {
512 Self::build(cache_dir.into(), version.into(), None, 0)
513 }
514
515 pub fn new_remote(
517 cache_dir: impl Into<PathBuf>,
518 version: impl Into<Arc<str>>,
519 remote: AgentRemoteCache,
520 ) -> Self {
521 Self::build(
522 cache_dir.into(),
523 version.into(),
524 Some(remote),
525 DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES,
526 )
527 }
528
529 pub fn new_remote_with_download_limit(
531 cache_dir: impl Into<PathBuf>,
532 version: impl Into<Arc<str>>,
533 remote: AgentRemoteCache,
534 max_remote_download_bytes: u64,
535 ) -> Self {
536 Self::build(
537 cache_dir.into(),
538 version.into(),
539 Some(remote),
540 max_remote_download_bytes,
541 )
542 }
543
544 fn build(
545 cache_dir: PathBuf,
546 version: Arc<str>,
547 remote: Option<AgentRemoteCache>,
548 remote_download_limit: u64,
549 ) -> Self {
550 let remote_mode = remote
551 .as_ref()
552 .map_or(RemoteCacheMode::ReadOnly, |remote| remote.mode);
553 let remote_staging_dir = remote.as_ref().map_or_else(
554 || cache_dir.join("remote"),
555 |remote| remote.staging_dir.clone(),
556 );
557 let remote = remote.map(|remote| Arc::new(remote.client));
558 let stats = Arc::new(AtomicAgentStats::default());
559 let remote_transfers = Arc::new(tokio::sync::Semaphore::new(MAX_REMOTE_TRANSFERS));
560 let uploads = remote
563 .clone()
564 .filter(|_| remote_mode.writes())
565 .map(|client| {
566 UploadQueue::new(
567 client,
568 Arc::new(AgentUploadSink {
569 stats: stats.clone(),
570 }),
571 remote_transfers.clone(),
572 )
573 });
574 Self {
575 cas: LocalCas::new(cache_dir.clone()),
576 actions: LocalActionCache::new(cache_dir.clone()),
577 verified_blobs: Arc::new(Mutex::new(BTreeMap::new())),
578 version,
579 write_locks: Arc::new(Mutex::new(BTreeMap::new())),
580 action_locks: Arc::new(Mutex::new(BTreeMap::new())),
581 stats,
582 observer: None,
583 observer_emission: Arc::new(Mutex::new(())),
584 task_loader: None,
585 executable_identities: Arc::new(Mutex::new(BTreeMap::new())),
586 identity_dir: Arc::new(cache_dir.join("executable-identities").join("v1")),
587 manifest_dir: Arc::new(task_manifest_dir(&cache_dir)),
588 task_actions: Arc::new(Mutex::new(BTreeMap::new())),
589 task_fallbacks: Arc::new(Mutex::new(BTreeMap::new())),
590 next_task_run: Arc::new(AtomicU64::new(0)),
591 manifest_write_lock: Arc::new(Mutex::new(())),
592 remote,
593 remote_mode,
594 remote_staging_dir: Arc::new(remote_staging_dir),
595 remote_download_limit,
596 remote_download_bytes: Arc::new(AtomicU64::new(0)),
597 pending_remote_actions: Arc::new(Mutex::new(BTreeMap::new())),
598 remote_transfers,
599 prefetch_transfers: Arc::new(tokio::sync::Semaphore::new(MAX_PREFETCH_TRANSFERS)),
600 prefetch_tasks: Arc::new(Mutex::new(Vec::new())),
601 warnings: Arc::new(Mutex::new(BTreeSet::new())),
602 file_digests: Arc::new(Mutex::new(BTreeMap::new())),
603 file_digest_locks: Arc::new(Mutex::new(BTreeMap::new())),
604 file_digest_permits: Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_FILE_DIGESTS)),
605 #[cfg(test)]
606 file_digest_reads: Arc::new(AtomicU64::new(0)),
607 uploads,
608 }
609 }
610
611 #[must_use]
613 pub fn with_observer(mut self, observer: Arc<dyn AgentEventObserver>) -> Self {
614 self.observer = Some(observer);
615 self
616 }
617
618 pub fn with_task_loader(mut self, loader: Arc<TaskLoader>) -> Self {
625 self.task_loader = Some(loader);
626 self
627 }
628
629 async fn ensure_task_loaded(&self, task: &str) {
632 let Some(loader) = &self.task_loader else {
633 return;
634 };
635 if self.task_actions.lock().unwrap().contains_key(task) {
636 return;
637 }
638 loader(self, task).await;
639 }
640
641 fn emit(&self, event: impl FnOnce() -> AgentEvent) {
642 if let Some(observer) = &self.observer {
643 let _emission = self.observer_emission.lock().unwrap();
644 observer.event(event());
645 }
646 }
647
648 fn emit_action(&self, diagnostic: Option<AgentEvent>, action: AgentEvent) {
649 if let Some(observer) = &self.observer {
650 let _emission = self.observer_emission.lock().unwrap();
651 if let Some(diagnostic) = diagnostic {
652 observer.event(diagnostic);
653 }
654 observer.event(action);
655 }
656 }
657
658 fn reserve_remote_download(&self, bytes: u64) -> Result<RemoteDownloadReservation> {
659 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
660 loop {
661 let next = current
662 .checked_add(bytes)
663 .ok_or_else(|| eyre::eyre!("remote cache download budget overflowed"))?;
664 if next > self.remote_download_limit {
665 bail!(
666 "remote cache download budget exceeded: {} bytes requested with {} of {} bytes already used",
667 bytes,
668 current,
669 self.remote_download_limit
670 );
671 }
672 match self.remote_download_bytes.compare_exchange_weak(
673 current,
674 next,
675 Ordering::AcqRel,
676 Ordering::Acquire,
677 ) {
678 Ok(_) => {
679 return Ok(RemoteDownloadReservation {
680 counter: self.remote_download_bytes.clone(),
681 reserved: bytes,
682 committed: false,
683 });
684 }
685 Err(observed) => current = observed,
686 }
687 }
688 }
689
690 fn reserve_remote_download_up_to(&self, requested: u64) -> Result<RemoteDownloadReservation> {
691 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
692 loop {
693 let reserved = requested.min(self.remote_download_limit.saturating_sub(current));
694 let next = current + reserved;
695 match self.remote_download_bytes.compare_exchange_weak(
696 current,
697 next,
698 Ordering::AcqRel,
699 Ordering::Acquire,
700 ) {
701 Ok(_) => {
702 return Ok(RemoteDownloadReservation {
703 counter: self.remote_download_bytes.clone(),
704 reserved,
705 committed: false,
706 });
707 }
708 Err(observed) => current = observed,
709 }
710 }
711 }
712
713 pub async fn begin_task(&self, task: &str) -> Result<String> {
715 self.begin_task_with_remote_errors(task, false, None, true)
716 .await
717 }
718
719 pub async fn begin_task_on_prediction(&self, task: &str) -> Result<String> {
722 self.begin_task_with_remote_errors(task, false, None, false)
723 .await
724 }
725
726 pub async fn begin_session_task(&self, task: &str) -> Result<()> {
732 self.begin_task_with_remote_errors(task, false, Some(task.to_string()), false)
733 .await?;
734 Ok(())
735 }
736
737 pub fn register_task_fallbacks(
748 &self,
749 task: &str,
750 fallbacks: impl Fn() -> Vec<String> + Send + Sync + 'static,
751 ) -> Result<()> {
752 validate_task_identity(task)?;
753 self.task_fallbacks
754 .lock()
755 .unwrap()
756 .insert(task.to_string(), Arc::new(fallbacks));
757 Ok(())
758 }
759
760 pub async fn prefetch_task(&self, task: &str) -> Result<String> {
762 let run = self
763 .begin_task_with_remote_errors(task, true, None, true)
764 .await?;
765 self.wait_for_prefetches().await;
766 Ok(run)
767 }
768
769 async fn begin_task_with_remote_errors(
770 &self,
771 task: &str,
772 strict: bool,
773 run: Option<String>,
774 eager_prefetch: bool,
775 ) -> Result<String> {
776 validate_task_identity(task)?;
777 let early_manifest = {
783 let _write_guard = self.manifest_write_lock.lock().unwrap();
784 let _file_guard = self.lock_task_manifest(task)?;
785 self.load_task_manifest(task)?
786 };
787 let early_actions: BTreeSet<_> = early_manifest
788 .iter()
789 .flat_map(|manifest| manifest.predictions.iter())
790 .map(|prediction| prediction.action.clone())
791 .collect();
792 if eager_prefetch {
793 self.spawn_prefetch_predictions(
794 early_manifest
795 .as_ref()
796 .map(|manifest| manifest.predictions.clone())
797 .unwrap_or_default(),
798 );
799 }
800 let (remote_manifest, mut remote_etag) = if self.remote_mode.reads() {
801 match self.get_remote_task_manifest(task).await {
802 Ok(Some((manifest, etag))) => (Some(manifest), Some(etag)),
803 Ok(None) => (None, None),
804 Err(error) => {
805 if strict {
806 return Err(error).wrap_err_with(|| {
807 format!("remote task action manifest lookup failed for {task}")
808 });
809 }
810 self.note_remote_failure();
811 warn!("remote task action manifest lookup failed for {task}: {error}");
812 (None, None)
813 }
814 }
815 } else {
816 (None, None)
817 };
818 let manifest = if self.remote.is_some() && self.remote_mode.reads() {
822 let _write_guard = self.manifest_write_lock.lock().unwrap();
823 let _file_guard = self.lock_task_manifest(task)?;
824 let local_manifest = self.load_task_manifest(task)?;
825 let manifest = match (remote_manifest, local_manifest) {
826 (Some(remote), Some(local)) => {
827 let (manifest, merged) = merge_remote_task_manifest(task, remote, local);
828 if !merged {
829 remote_etag = None;
830 }
831 Some(manifest)
832 }
833 (Some(remote), None) => Some(remote),
834 (None, local) => local,
835 };
836 if let Some(manifest) = &manifest {
837 self.persist_task_manifest(manifest)?;
838 }
839 manifest
840 } else {
841 early_manifest
842 };
843 let (manifest, remote_etag) = match manifest {
844 Some(manifest) => (Some(manifest), remote_etag),
845 None => (self.inherit_task_manifest(task, strict).await?, None),
848 };
849 let mut state = if let Some(manifest) = manifest {
850 TaskActionState {
851 manifest: task.to_string(),
852 baseline_loaded: true,
853 predictions: manifest
854 .predictions
855 .into_iter()
856 .map(|prediction| (prediction.invocation.clone(), prediction))
857 .collect(),
858 pending_predictions: BTreeMap::new(),
859 changed_predictions: BTreeMap::new(),
860 prefetched_adapters: BTreeSet::new(),
861 remote_etag,
862 }
863 } else {
864 TaskActionState {
865 manifest: task.to_string(),
866 baseline_loaded: true,
867 remote_etag,
868 ..TaskActionState::default()
869 }
870 };
871 let run = run.unwrap_or_else(|| {
872 let sequence = self.next_task_run.fetch_add(1, Ordering::Relaxed);
873 CacheDigest::blake3(format!("{task}\0{}\0{sequence}", std::process::id()).as_bytes())
874 .hash
875 });
876 self.stats.predictions_loaded.fetch_max(
880 state.predictions.len().try_into().unwrap_or(u64::MAX),
881 Ordering::Relaxed,
882 );
883 if eager_prefetch {
884 state.prefetched_adapters.extend(
885 state
886 .predictions
887 .values()
888 .map(|prediction| prediction.adapter.clone()),
889 );
890 }
891 let predictions = state
895 .predictions
896 .values()
897 .filter(|prediction| !early_actions.contains(&prediction.action))
898 .cloned()
899 .collect();
900 self.task_actions.lock().unwrap().insert(run.clone(), state);
901 if eager_prefetch {
902 self.spawn_prefetch_predictions(predictions);
903 }
904 Ok(run)
905 }
906
907 async fn inherit_task_manifest(
915 &self,
916 task: &str,
917 strict: bool,
918 ) -> Result<Option<TaskActionManifest>> {
919 let fallbacks = self.task_fallbacks.lock().unwrap().get(task).cloned();
920 let Some(fallbacks) = fallbacks else {
921 return Ok(None);
922 };
923 let named = tokio::task::spawn_blocking(move || fallbacks()).await?;
924 let mut tried = BTreeSet::from([task.to_string()]);
925 let candidates = named.into_iter().map(|identity| (identity, true)).chain(
926 self.newest_task_identities()
927 .into_iter()
928 .map(|identity| (identity, false)),
929 );
930 for (identity, named) in candidates {
931 if !tried.insert(identity.clone()) || validate_task_identity(&identity).is_err() {
932 continue;
933 }
934 let mut found = self.load_task_manifest(&identity)?;
935 if found.is_none() && named && self.remote_mode.reads() {
936 found = match self.get_remote_task_manifest(&identity).await {
937 Ok(manifest) => manifest.map(|(manifest, _)| manifest),
938 Err(error) => {
939 if strict {
940 return Err(error).wrap_err_with(|| {
941 format!("remote task action manifest lookup failed for {identity}")
942 });
943 }
944 self.note_remote_failure();
945 warn!("remote task action manifest lookup failed for {identity}: {error}");
946 None
947 }
948 };
949 }
950 let Some(mut manifest) = found else {
951 continue;
952 };
953 if manifest.predictions.is_empty()
954 || (!named && manifest.predictions.len() > MAX_TASK_ACTION_PREDICTIONS / 2)
955 {
956 continue;
957 }
958 info!(
959 "no predictions were recorded for {task}; inheriting {} from {identity}",
960 manifest.predictions.len()
961 );
962 manifest.task = task.to_string();
963 validate_task_manifest(&manifest, task)?;
964 let _write_guard = self.manifest_write_lock.lock().unwrap();
965 let _file_guard = self.lock_task_manifest(task)?;
966 if let Some(recorded) = self.load_task_manifest(task)? {
970 return Ok(Some(recorded));
971 }
972 self.persist_task_manifest(&manifest)?;
973 return Ok(Some(manifest));
974 }
975 Ok(None)
976 }
977
978 fn newest_task_identities(&self) -> Vec<String> {
980 let Ok(entries) = fs::read_dir(self.manifest_dir.as_path()) else {
981 return Vec::new();
982 };
983 let mut manifests: Vec<(std::time::SystemTime, String)> = entries
984 .filter_map(|entry| {
985 let entry = entry.ok()?;
986 let name = entry.file_name();
987 let identity = name.to_str()?.strip_suffix(".json")?.to_string();
988 validate_task_identity(&identity).ok()?;
989 let modified = entry.metadata().ok()?.modified().ok()?;
990 Some((modified, identity))
991 })
992 .collect();
993 manifests.sort_by(|left, right| right.cmp(left));
994 manifests
995 .into_iter()
996 .take(NEWEST_MANIFEST_CANDIDATES)
997 .map(|(_, identity)| identity)
998 .collect()
999 }
1000
1001 pub async fn cancel_prefetches(&self) {
1003 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
1004 for task in &tasks {
1005 task.abort();
1006 }
1007 for task in tasks {
1008 if let Err(error) = task.await
1009 && !error.is_cancelled()
1010 {
1011 warn!("remote action prefetch task failed: {error}");
1012 }
1013 }
1014 }
1015
1016 pub async fn wait_for_uploads(&self) {
1023 let Some(uploads) = &self.uploads else {
1024 return;
1025 };
1026 let _timer = AtomicDurationTimer::start(&self.stats.upload_drain_duration_ns);
1027 uploads.drain().await;
1028 }
1029
1030 async fn wait_for_prefetches(&self) {
1031 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
1032 for task in tasks {
1033 if let Err(error) = task.await {
1034 warn!("remote action prefetch task failed: {error}");
1035 }
1036 }
1037 }
1038
1039 pub async fn commit_task(&self, run: &str) -> Result<()> {
1041 self.commit_task_actions(run).await.map(|_| ())
1042 }
1043
1044 pub async fn commit_task_actions(&self, run: &str) -> Result<Vec<ActionPrediction>> {
1050 validate_task_identity(run)?;
1051 let state = {
1052 let mut runs = self.task_actions.lock().unwrap();
1053 let state = runs
1054 .get(run)
1055 .ok_or_else(|| eyre::eyre!("task action manifest baseline was not loaded"))?;
1056 if !state.baseline_loaded {
1057 bail!("task action manifest baseline was not loaded");
1058 }
1059 debug!(
1065 "committing {} changed of {} recorded predictions for {}",
1066 state.changed_predictions.len(),
1067 state.pending_predictions.len(),
1068 state.manifest
1069 );
1070 if state.changed_predictions.is_empty()
1071 && (self.remote.is_none() || !self.remote_mode.writes())
1072 {
1073 let completed = state.pending_predictions.values().cloned().collect();
1074 runs.remove(run);
1075 return Ok(completed);
1076 }
1077 state.clone()
1078 };
1079 let task = state.manifest;
1080 validate_task_identity(&task)?;
1081 let completed = state
1082 .pending_predictions
1083 .values()
1084 .cloned()
1085 .collect::<Vec<_>>();
1086 let (manifest, introduced) = {
1087 let _write_guard = self.manifest_write_lock.lock().unwrap();
1088 let _file_guard = self.lock_task_manifest(&task)?;
1089 let mut predictions = self
1090 .load_task_manifest(&task)?
1091 .map(|manifest| {
1092 manifest
1093 .predictions
1094 .into_iter()
1095 .map(|prediction| (prediction.invocation.clone(), prediction))
1096 .collect::<BTreeMap<_, _>>()
1097 })
1098 .unwrap_or_default();
1099 let introduced: BTreeSet<CacheDigest> = state
1104 .pending_predictions
1105 .keys()
1106 .filter(|invocation| !predictions.contains_key(*invocation))
1107 .cloned()
1108 .collect();
1109 predictions.extend(state.pending_predictions);
1114 let manifest = TaskActionManifest {
1115 version: TASK_ACTION_MANIFEST_VERSION,
1116 task: task.clone(),
1117 predictions: predictions.into_values().collect(),
1118 };
1119 validate_task_manifest(&manifest, &task)?;
1120 self.persist_task_manifest(&manifest)?;
1121 (manifest, introduced)
1122 };
1123 self.task_actions.lock().unwrap().remove(run);
1124 if self.remote_mode.writes() {
1125 let mut manifest = manifest;
1129 if let Some(uploads) = &self.uploads {
1130 let actions: Vec<CacheDigest> = manifest
1131 .predictions
1132 .iter()
1133 .map(|prediction| prediction.action.clone())
1134 .collect();
1135 let unpublished = uploads.wait_for_actions(&actions).await;
1136 let withheld = manifest
1137 .predictions
1138 .iter()
1139 .filter(|prediction| {
1140 introduced.contains(&prediction.invocation)
1141 && unpublished.contains(&prediction.action)
1142 })
1143 .count();
1144 if withheld > 0 {
1145 warn!(
1148 "{withheld} of {} predicted actions were not published, so the remote task action manifest omits them",
1149 manifest.predictions.len()
1150 );
1151 manifest.predictions.retain(|prediction| {
1152 !(introduced.contains(&prediction.invocation)
1153 && unpublished.contains(&prediction.action))
1154 });
1155 }
1156 }
1157 match self
1158 .put_remote_task_manifest(&task, manifest, state.remote_etag)
1159 .await
1160 {
1161 Ok(remote_manifest) => {
1162 let _write_guard = self.manifest_write_lock.lock().unwrap();
1163 let reconciliation = (|| {
1164 let _file_guard = self.lock_task_manifest(&task)?;
1165 let manifest = match self.load_task_manifest(&task)? {
1166 Some(local) => {
1167 merge_remote_task_manifest(&task, remote_manifest, local).0
1168 }
1169 None => remote_manifest,
1170 };
1171 self.persist_task_manifest(&manifest)
1172 })();
1173 if let Err(error) = reconciliation {
1174 warn!(
1175 "remote task action manifest reconciliation failed for {task}: {error}"
1176 );
1177 }
1178 }
1179 Err(error) => {
1180 self.note_remote_failure();
1181 warn!("remote task action manifest upload failed for {task}: {error}");
1182 }
1183 }
1184 }
1185 Ok(completed)
1186 }
1187
1188 fn task_manifest_path(&self, task: &str) -> PathBuf {
1189 self.manifest_dir.join(format!("{task}.json"))
1190 }
1191
1192 fn task_manifest_lock_path(&self, task: &str) -> PathBuf {
1193 self.manifest_dir.join("locks").join(format!("{task}.lock"))
1194 }
1195
1196 fn lock_task_manifest(&self, task: &str) -> Result<fslock::LockFile> {
1197 let path = self.task_manifest_lock_path(task);
1198 fs::create_dir_all(path.parent().expect("task manifest lock has a parent"))?;
1199 let mut lock = fslock::LockFile::open(&path)?;
1200 lock.lock()?;
1201 Ok(lock)
1202 }
1203
1204 fn load_task_manifest(&self, task: &str) -> Result<Option<TaskActionManifest>> {
1205 match fs::read(self.task_manifest_path(task)) {
1206 Ok(contents) => Ok(Some(self.parse_task_manifest(task, &contents, false)?)),
1207 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
1208 Err(error) => Err(error.into()),
1209 }
1210 }
1211
1212 fn parse_task_manifest(
1213 &self,
1214 task: &str,
1215 contents: &[u8],
1216 require_canonical: bool,
1217 ) -> Result<TaskActionManifest> {
1218 let manifest: TaskActionManifest = serde_json::from_slice(contents)?;
1219 validate_task_manifest(&manifest, task)?;
1220 if require_canonical && canonical_json(&manifest)? != contents {
1221 bail!("task action manifest is not canonical JSON");
1222 }
1223 Ok(manifest)
1224 }
1225
1226 fn task_manifest_selector(task: &str) -> Result<(Vec<u8>, CacheDigest)> {
1227 TaskActionManifest::selector(task)
1228 }
1229
1230 fn persist_task_manifest(&self, manifest: &TaskActionManifest) -> Result<()> {
1231 let bytes = canonical_json(manifest)?;
1232 fs::create_dir_all(self.manifest_dir.as_path())?;
1233 let mut temporary = tempfile::NamedTempFile::new_in(self.manifest_dir.as_path())?;
1234 std::io::Write::write_all(temporary.as_file_mut(), &bytes)?;
1235 temporary.as_file_mut().sync_all()?;
1236 temporary
1237 .persist(self.task_manifest_path(&manifest.task))
1238 .map_err(|error| error.error)?;
1239 Ok(())
1240 }
1241
1242 async fn get_remote_task_manifest(
1243 &self,
1244 task: &str,
1245 ) -> Result<Option<(TaskActionManifest, String)>> {
1246 let Some(remote) = &self.remote else {
1247 return Ok(None);
1248 };
1249 let (_, selector) = Self::task_manifest_selector(task)?;
1250 let _permit = self.remote_transfers.acquire().await?;
1251 self.stats
1252 .remote_manifest_lookups
1253 .fetch_add(1, Ordering::Relaxed);
1254 let _timer = AtomicDurationTimer::start(&self.stats.remote_manifest_lookup_duration_ns);
1255 let Some(remote_manifest) = remote.get_action_manifest(&selector).await? else {
1256 return Ok(None);
1257 };
1258 let manifest = self.parse_task_manifest(task, &remote_manifest.bytes, true)?;
1259 Ok(Some((manifest, remote_manifest.etag)))
1260 }
1261
1262 async fn put_remote_task_manifest(
1263 &self,
1264 task: &str,
1265 mut manifest: TaskActionManifest,
1266 mut expected_etag: Option<String>,
1267 ) -> Result<TaskActionManifest> {
1268 let Some(remote) = &self.remote else {
1269 return Ok(manifest);
1270 };
1271 let (_, selector) = Self::task_manifest_selector(task)?;
1272 for _ in 0..4 {
1273 let bytes = canonical_json(&manifest)?;
1274 let outcome = {
1275 let _permit = self.remote_transfers.acquire().await?;
1276 remote
1277 .put_action_manifest(&selector, &bytes, expected_etag.as_deref())
1278 .await?
1279 };
1280 match outcome {
1281 ManifestPutOutcome::Stored => return Ok(manifest),
1282 ManifestPutOutcome::PreconditionFailed => {
1283 let Some((remote_manifest, etag)) = self.get_remote_task_manifest(task).await?
1284 else {
1285 expected_etag = None;
1286 continue;
1287 };
1288 manifest = merge_task_manifests(task, Some(remote_manifest), manifest)?;
1289 expected_etag = Some(etag);
1290 }
1291 }
1292 }
1293 bail!("remote task action manifest changed too frequently")
1294 }
1295
1296 pub fn stats(&self) -> AgentStats {
1298 AgentStats {
1299 session_duration_ns: 0,
1300 lookups: self.stats.lookups.load(Ordering::Relaxed),
1301 unconsulted: self.stats.unconsulted.load(Ordering::Relaxed),
1302 hits: self.stats.hits.load(Ordering::Relaxed),
1303 stores: self.stats.stores.load(Ordering::Relaxed),
1304 stored_bytes: self.stats.stored_bytes.load(Ordering::Relaxed),
1305 verifications: self.stats.verifications.load(Ordering::Relaxed),
1306 divergences: self.stats.divergences.load(Ordering::Relaxed),
1307 downloaded_bytes: self.stats.downloaded_bytes.load(Ordering::Relaxed),
1308 uploaded_bytes: self.stats.uploaded_bytes.load(Ordering::Relaxed),
1309 background_uploads: self.stats.background_uploads.load(Ordering::Relaxed),
1310 background_upload_failures: self
1311 .stats
1312 .background_upload_failures
1313 .load(Ordering::Relaxed),
1314 remote_blob_pack_uploads: self.stats.remote_blob_pack_uploads.load(Ordering::Relaxed),
1315 remote_blob_pack_upload_blobs: self
1316 .stats
1317 .remote_blob_pack_upload_blobs
1318 .load(Ordering::Relaxed),
1319 upload_drain_duration_ns: self.stats.upload_drain_duration_ns.load(Ordering::Relaxed),
1320 prefetched_actions: self.stats.prefetched_actions.load(Ordering::Relaxed),
1321 predictions_loaded: self.stats.predictions_loaded.load(Ordering::Relaxed),
1322 bypasses: self.stats.bypasses.lock().unwrap().clone(),
1323 avoided_compiler_duration_ns: self
1324 .stats
1325 .avoided_compiler_duration_ns
1326 .load(Ordering::Relaxed),
1327 wrapper_phases_ns: self.stats.wrapper_phases_ns.lock().unwrap().clone(),
1328 compiler: self.stats.compiler.lock().unwrap().clone(),
1329 slow_compilations: self.stats.slow_compilations.lock().unwrap().clone(),
1330 remote_failures: self.stats.remote_failures.load(Ordering::Relaxed),
1331 remote_manifest_lookups: self.stats.remote_manifest_lookups.load(Ordering::Relaxed),
1332 remote_manifest_lookup_duration_ns: self
1333 .stats
1334 .remote_manifest_lookup_duration_ns
1335 .load(Ordering::Relaxed),
1336 remote_action_lookups: self.stats.remote_action_lookups.load(Ordering::Relaxed),
1337 remote_action_lookup_duration_ns: self
1338 .stats
1339 .remote_action_lookup_duration_ns
1340 .load(Ordering::Relaxed),
1341 remote_blob_requests: self.stats.remote_blob_requests.load(Ordering::Relaxed),
1342 remote_blob_pack_requests: self.stats.remote_blob_pack_requests.load(Ordering::Relaxed),
1343 remote_blob_pack_blobs: self.stats.remote_blob_pack_blobs.load(Ordering::Relaxed),
1344 remote_blob_transfer_duration_ns: self
1345 .stats
1346 .remote_blob_transfer_duration_ns
1347 .load(Ordering::Relaxed),
1348 local_cas_write_duration_ns: self
1349 .stats
1350 .local_cas_write_duration_ns
1351 .load(Ordering::Relaxed),
1352 prefetch_runs: self.stats.prefetch_runs.load(Ordering::Relaxed),
1353 prefetch_duration_ns: self.stats.prefetch_duration_ns.load(Ordering::Relaxed),
1354 materialization_duration_ns: self
1355 .stats
1356 .materialization_duration_ns
1357 .load(Ordering::Relaxed),
1358 restored_output_files: self.stats.restored_output_files.load(Ordering::Relaxed),
1359 restored_output_bytes: self.stats.restored_output_bytes.load(Ordering::Relaxed),
1360 reflinked_output_files: self.stats.reflinked_output_files.load(Ordering::Relaxed),
1361 reflinked_output_bytes: self.stats.reflinked_output_bytes.load(Ordering::Relaxed),
1362 copied_output_files: self.stats.copied_output_files.load(Ordering::Relaxed),
1363 copied_output_bytes: self.stats.copied_output_bytes.load(Ordering::Relaxed),
1364 reused_output_files: self.stats.reused_output_files.load(Ordering::Relaxed),
1365 reused_output_bytes: self.stats.reused_output_bytes.load(Ordering::Relaxed),
1366 }
1367 }
1368
1369 pub async fn handle_requests(
1376 &self,
1377 requests: impl IntoIterator<Item = AgentRequest>,
1378 ) -> Vec<AgentResponse> {
1379 let mut connection = ConnectionUploads::default();
1380 let mut responses = Vec::new();
1381 for request in requests {
1382 responses.push(self.respond_on(request, &mut connection).await);
1383 }
1384 responses
1385 }
1386
1387 fn write_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1388 Self::digest_lock(&self.write_locks, digest)
1389 }
1390
1391 fn action_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1392 Self::digest_lock(&self.action_locks, digest)
1393 }
1394
1395 fn digest_lock(
1396 locks: &Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>,
1397 digest: &CacheDigest,
1398 ) -> Arc<tokio::sync::Mutex<()>> {
1399 let mut locks = locks.lock().unwrap();
1400 locks.retain(|_, lock| lock.strong_count() > 0);
1401 if let Some(lock) = locks.get(digest).and_then(Weak::upgrade) {
1402 return lock;
1403 }
1404 let lock = Arc::new(tokio::sync::Mutex::new(()));
1405 locks.insert(digest.clone(), Arc::downgrade(&lock));
1406 lock
1407 }
1408
1409 #[cfg(test)]
1414 async fn respond(&self, request: AgentRequest) -> AgentResponse {
1415 self.respond_on(request, &mut ConnectionUploads::default())
1416 .await
1417 }
1418
1419 async fn respond_on(
1420 &self,
1421 request: AgentRequest,
1422 connection: &mut ConnectionUploads,
1423 ) -> AgentResponse {
1424 match &request {
1425 AgentRequest::FindActionPrediction { task, .. }
1426 | AgentRequest::RecordActionPrediction { task, .. } => {
1427 self.ensure_task_loaded(task).await;
1428 }
1429 _ => {}
1430 }
1431 let result = match request {
1432 AgentRequest::BeginTask { task } => self
1433 .begin_task(&task)
1434 .await
1435 .map(|run| AgentResponse::TaskBegun { run }),
1436 AgentRequest::CommitTask { run } => self
1437 .commit_task(&run)
1438 .await
1439 .map(|()| AgentResponse::TaskCommitted),
1440 AgentRequest::FindBlob { digest } => self.find_blob(&digest).await,
1441 AgentRequest::FindBlobs { digests } => self.find_blobs(digests).await,
1442 AgentRequest::StoreBlob { digest, source } => {
1443 self.store_blob(&digest, &source, connection).await
1444 }
1445 AgentRequest::FindActionResult { action } => {
1446 self.stats.lookups.fetch_add(1, Ordering::Relaxed);
1447 self.find_action_result(&action).await
1448 }
1449 AgentRequest::RecordActionHit {
1450 action,
1451 restore,
1452 crate_name,
1453 } => {
1454 let diagnostic = connection
1455 .take_action_diagnostic("hit", crate_name.as_deref())
1456 .map(|diagnostic| AgentEvent::ActionDiagnostic {
1457 outcome: "hit".into(),
1458 crate_name: crate_name.clone(),
1459 diagnostic,
1460 });
1461 self.record_action_hit(&action, restore, crate_name, diagnostic)
1462 }
1463 AgentRequest::RecordBypass { kind } => {
1464 *self
1465 .stats
1466 .bypasses
1467 .lock()
1468 .unwrap()
1469 .entry(kind.clone())
1470 .or_insert(0) += 1;
1471 self.emit(|| AgentEvent::Bypass { kind });
1472 Ok(AgentResponse::BypassRecorded)
1473 }
1474 AgentRequest::RecordUnconsulted => {
1475 self.stats.unconsulted.fetch_add(1, Ordering::Relaxed);
1476 self.emit(|| AgentEvent::Unconsulted);
1477 Ok(AgentResponse::UnconsultedRecorded)
1478 }
1479 AgentRequest::RecordWarning { message } => match parse_action_diagnostic(&message) {
1480 Some(Ok((outcome, crate_name, diagnostic))) => {
1481 connection.record_action_diagnostic(outcome, crate_name, diagnostic);
1482 Ok(AgentResponse::WarningRecorded)
1483 }
1484 Some(Err(error)) => Err(error),
1485 None => self.record_warning(message),
1486 },
1487 AgentRequest::FindFileDigests { scope, files } => self.find_file_digests(scope, files),
1488 AgentRequest::JoinActionPromise {
1489 adapter,
1490 invocation,
1491 } => self.join_action_promise(&adapter, &invocation).await,
1492 AgentRequest::CompleteActionPromise { claim, prediction } => {
1493 self.complete_action_promise(&claim, &prediction).await
1494 }
1495 AgentRequest::ResolveFileDigests { scope, files } => {
1496 self.resolve_file_digests(scope, files).await
1497 }
1498 AgentRequest::RecordFileDigests { scope, entries } => {
1499 self.record_file_digests(scope, entries)
1500 }
1501 AgentRequest::RecordWrapperTiming { timing } => (|| {
1502 if timing.spans.len() > 512
1503 || timing.phases_ns.len() > 32
1504 || timing.adapter.len() > 64
1505 || timing.unit.as_ref().is_some_and(|s| s.len() > 1024)
1506 || timing.phases_ns.keys().any(|s| s.len() > 64)
1507 || timing.spans.iter().any(|s| s.name.len() > 64)
1508 {
1509 bail!("wrapper timing exceeds telemetry limits");
1510 }
1511 {
1512 let mut phases = self.stats.wrapper_phases_ns.lock().unwrap();
1513 for (phase, duration) in &timing.phases_ns {
1514 let total = phases.entry(phase.clone()).or_default();
1515 *total = total.saturating_add(*duration);
1516 }
1517 }
1518 self.emit(|| AgentEvent::WrapperTiming { timing });
1519 Ok(AgentResponse::WrapperTimingRecorded)
1520 })(),
1521 AgentRequest::RecordCompilerInvocation {
1522 outcome,
1523 crate_name,
1524 duration_ns,
1525 } => {
1526 let diagnostic = connection
1527 .take_action_diagnostic(&outcome, crate_name.as_deref())
1528 .map(|diagnostic| AgentEvent::ActionDiagnostic {
1529 outcome: outcome.clone(),
1530 crate_name: crate_name.clone(),
1531 diagnostic,
1532 });
1533 self.record_compiler_invocation(
1534 &outcome,
1535 crate_name.as_deref(),
1536 duration_ns,
1537 diagnostic,
1538 )
1539 }
1540 AgentRequest::RecordActionVerification { matched, restore } => {
1541 self.record_materialization(restore);
1542 self.stats.verifications.fetch_add(1, Ordering::Relaxed);
1543 if !matched {
1544 self.stats.divergences.fetch_add(1, Ordering::Relaxed);
1545 }
1546 self.emit(|| AgentEvent::Verification { matched, restore });
1547 Ok(AgentResponse::ActionVerificationRecorded)
1548 }
1549 AgentRequest::StoreActionResult { result } => {
1550 self.store_action_result(&result, connection).await
1551 }
1552 AgentRequest::FindActionPrediction { task, invocation } => {
1553 self.find_action_prediction(&task, &invocation)
1554 }
1555 AgentRequest::RecordActionPrediction { task, prediction } => {
1556 self.record_action_prediction(&task, prediction)
1557 }
1558 AgentRequest::FindExecutableIdentity {
1559 executable,
1560 environment,
1561 } => self.find_executable_identity(executable, environment),
1562 AgentRequest::StoreExecutableIdentity {
1563 executable,
1564 environment,
1565 stdout,
1566 pins,
1567 } => self.store_executable_identity(executable, environment, stdout, pins),
1568 AgentRequest::Hello { .. } => {
1569 Err(eyre::eyre!("hello is only valid as the first request"))
1570 }
1571 };
1572 result.unwrap_or_else(|error| AgentResponse::Error {
1573 message: error.to_string(),
1574 })
1575 }
1576
1577 async fn find_blob(&self, digest: &CacheDigest) -> Result<AgentResponse> {
1578 if let Some(path) = self.find_verified_blob(digest)? {
1579 return Ok(AgentResponse::Blob { path: Some(path) });
1580 }
1581 if !self.remote_mode.reads() {
1582 return Ok(AgentResponse::Blob { path: None });
1583 }
1584 let Some(remote) = &self.remote else {
1585 return Ok(AgentResponse::Blob { path: None });
1586 };
1587 match self.fetch_remote_blob(remote, digest).await {
1588 Ok(path) => Ok(AgentResponse::Blob { path: Some(path) }),
1589 Err(error) => {
1590 warn!(
1591 "remote cache blob lookup failed for {}: {error}",
1592 digest.hash
1593 );
1594 Ok(AgentResponse::Blob { path: None })
1595 }
1596 }
1597 }
1598
1599 async fn find_blobs(&self, digests: Vec<CacheDigest>) -> Result<AgentResponse> {
1600 let mut paths = BTreeMap::new();
1601 let mut missing = Vec::new();
1602 for digest in &digests {
1603 match self.find_verified_blob(digest)? {
1604 Some(path) => {
1605 paths.insert(digest.clone(), path);
1606 }
1607 None => {
1608 missing.push(digest.clone());
1609 }
1610 }
1611 }
1612
1613 if !missing.is_empty()
1614 && self.remote_mode.reads()
1615 && let Some(remote) = &self.remote
1616 {
1617 paths.extend(self.fetch_remote_blobs(remote, missing, None).await);
1618 }
1619
1620 Ok(AgentResponse::Blobs {
1621 paths: digests
1622 .into_iter()
1623 .map(|digest| paths.get(&digest).cloned())
1624 .collect(),
1625 })
1626 }
1627
1628 async fn store_blob(
1629 &self,
1630 digest: &CacheDigest,
1631 source: &Path,
1632 connection: &mut ConnectionUploads,
1633 ) -> Result<AgentResponse> {
1634 let path = {
1635 let lock = self.write_lock(digest);
1636 let _guard = lock.lock().await;
1637 if let Some(path) = self.find_verified_blob(digest)? {
1638 path
1639 } else {
1640 let path = self.cas.store_file(digest, source)?;
1641 self.remember_verified_blob(digest, &path);
1642 self.stats.stores.fetch_add(1, Ordering::Relaxed);
1643 self.stats
1644 .stored_bytes
1645 .fetch_add(digest.size, Ordering::Relaxed);
1646 path
1647 }
1648 };
1649 if let Some(uploads) = &self.uploads {
1652 uploads.queue_blob(digest, path.clone(), connection);
1653 }
1654 Ok(AgentResponse::Stored { path })
1655 }
1656
1657 fn find_verified_blob(&self, digest: &CacheDigest) -> Result<Option<PathBuf>> {
1658 let remembered = self.verified_blobs.lock().unwrap().get(digest).cloned();
1659 if let Some(remembered) = remembered {
1660 if remembered.is_unchanged() {
1671 return Ok(Some(remembered.path));
1672 }
1673 self.verified_blobs.lock().unwrap().remove(digest);
1674 }
1675 let path = self.cas.find(digest)?;
1676 if let Some(path) = &path {
1677 self.remember_verified_blob(digest, path);
1678 }
1679 Ok(path)
1680 }
1681
1682 fn remember_verified_blob(&self, digest: &CacheDigest, path: &Path) {
1683 let Some(verified) = VerifiedBlob::describe(path) else {
1684 return;
1688 };
1689 self.verified_blobs
1690 .lock()
1691 .unwrap()
1692 .insert(digest.clone(), verified);
1693 }
1694
1695 async fn find_action_result(&self, action: &CacheDigest) -> Result<AgentResponse> {
1696 if let Some(result) = self.actions.find(action)? {
1697 return Ok(AgentResponse::ActionResult {
1698 result: Some(result),
1699 });
1700 }
1701 if !self.remote_mode.reads() {
1702 return Ok(AgentResponse::ActionResult { result: None });
1703 }
1704 let Some(remote) = &self.remote else {
1705 return Ok(AgentResponse::ActionResult { result: None });
1706 };
1707 let lock = self.action_lock(action);
1708 let _guard = lock.lock().await;
1709 if let Some(result) = self.actions.find(action)? {
1710 return Ok(AgentResponse::ActionResult {
1711 result: Some(result),
1712 });
1713 }
1714 if let Some(result) = self
1715 .pending_remote_actions
1716 .lock()
1717 .unwrap()
1718 .get(action)
1719 .cloned()
1720 {
1721 return Ok(AgentResponse::ActionResult {
1722 result: Some(result),
1723 });
1724 }
1725 let _permit = self.remote_transfers.acquire().await?;
1726 match self.get_remote_action_result(remote, action).await {
1727 Ok(Some(result)) => {
1728 self.pending_remote_actions
1729 .lock()
1730 .unwrap()
1731 .insert(action.clone(), result.clone());
1732 Ok(AgentResponse::ActionResult {
1733 result: Some(result),
1734 })
1735 }
1736 Ok(None) => Ok(AgentResponse::ActionResult { result: None }),
1737 Err(error) => {
1738 self.note_remote_failure();
1739 warn!(
1740 "remote cache action lookup failed for {}: {error}",
1741 action.hash
1742 );
1743 Ok(AgentResponse::ActionResult { result: None })
1744 }
1745 }
1746 }
1747
1748 async fn store_action_result(
1749 &self,
1750 result: &RemoteActionResult,
1751 connection: &ConnectionUploads,
1752 ) -> Result<AgentResponse> {
1753 let path = self.actions.store(result)?;
1754 if let Some(uploads) = &self.uploads {
1755 uploads.queue_action_result(result, connection);
1756 }
1757 Ok(AgentResponse::ActionStored { path })
1758 }
1759
1760 async fn join_action_promise(
1761 &self,
1762 adapter: &str,
1763 invocation: &CacheDigest,
1764 ) -> Result<AgentResponse> {
1765 if !self.remote_mode.reads() || !self.remote_mode.writes() {
1766 return Ok(AgentResponse::ActionPromise {
1767 claim: None,
1768 prediction: None,
1769 });
1770 }
1771 let Some(remote) = &self.remote else {
1772 return Ok(AgentResponse::ActionPromise {
1773 claim: None,
1774 prediction: None,
1775 });
1776 };
1777 let deadline = Instant::now() + ACTION_PROMISE_WAIT;
1778 loop {
1779 let _permit = self.remote_transfers.acquire().await?;
1780 let state = remote.join_action_promise(invocation, adapter).await;
1781 drop(_permit);
1782 match state {
1783 Ok(Some(ActionPromiseState::Claimed { claim })) => {
1784 return Ok(AgentResponse::ActionPromise {
1785 claim: Some(claim),
1786 prediction: None,
1787 });
1788 }
1789 Ok(Some(ActionPromiseState::Complete { prediction })) => {
1790 return Ok(AgentResponse::ActionPromise {
1791 claim: None,
1792 prediction: Some(prediction),
1793 });
1794 }
1795 Ok(Some(ActionPromiseState::Pending { retry_after_ms }))
1796 if Instant::now() < deadline =>
1797 {
1798 tokio::time::sleep(Duration::from_millis(retry_after_ms.clamp(10, 5_000)))
1799 .await;
1800 }
1801 Ok(Some(ActionPromiseState::Pending { .. }) | None) => {
1802 return Ok(AgentResponse::ActionPromise {
1803 claim: None,
1804 prediction: None,
1805 });
1806 }
1807 Ok(Some(_)) => {
1808 return Ok(AgentResponse::ActionPromise {
1809 claim: None,
1810 prediction: None,
1811 });
1812 }
1813 Err(error) => {
1814 self.note_remote_failure();
1815 warn!(
1816 "remote cache action promise failed for {}: {error}",
1817 invocation.hash
1818 );
1819 return Ok(AgentResponse::ActionPromise {
1820 claim: None,
1821 prediction: None,
1822 });
1823 }
1824 }
1825 }
1826 }
1827
1828 async fn complete_action_promise(
1829 &self,
1830 claim: &str,
1831 prediction: &ActionPrediction,
1832 ) -> Result<AgentResponse> {
1833 prediction.validate()?;
1834 let Some(remote) = &self.remote else {
1835 return Ok(AgentResponse::ActionPromiseCompleted);
1836 };
1837 let Some(uploads) = &self.uploads else {
1838 return Ok(AgentResponse::ActionPromiseCompleted);
1839 };
1840 if uploads
1841 .wait_for_actions(std::slice::from_ref(&prediction.action))
1842 .await
1843 .contains(&prediction.action)
1844 {
1845 return Ok(AgentResponse::ActionPromiseCompleted);
1848 }
1849 let completion = ActionPromiseCompletion {
1850 claim: claim.to_string(),
1851 prediction: prediction.clone(),
1852 };
1853 let _permit = self.remote_transfers.acquire().await?;
1854 if let Err(error) = remote
1855 .complete_action_promise(&prediction.invocation, &completion)
1856 .await
1857 {
1858 self.note_remote_failure();
1859 warn!(
1860 "remote cache action promise completion failed for {}: {error}",
1861 prediction.invocation.hash
1862 );
1863 }
1864 Ok(AgentResponse::ActionPromiseCompleted)
1865 }
1866
1867 fn note_remote_failure(&self) {
1869 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
1870 }
1871
1872 async fn get_remote_action_result(
1873 &self,
1874 remote: &RemoteCacheClient,
1875 action: &CacheDigest,
1876 ) -> Result<Option<RemoteActionResult>> {
1877 self.stats
1878 .remote_action_lookups
1879 .fetch_add(1, Ordering::Relaxed);
1880 let _timer = AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);
1881 remote.get_action_result(action).await
1882 }
1883
1884 fn record_action_hit(
1885 &self,
1886 action: &CacheDigest,
1887 restore: RestoreStats,
1888 crate_name: Option<String>,
1889 diagnostic: Option<AgentEvent>,
1890 ) -> Result<AgentResponse> {
1891 validate_crate_name(crate_name.as_deref())?;
1892 if self.actions.find(action)?.is_none() {
1893 let pending = self.pending_remote_actions.lock().unwrap().remove(action);
1894 if let Some(result) = pending {
1895 self.actions.store(&result)?;
1896 } else {
1897 bail!("cannot record a hit for a missing action result");
1898 }
1899 }
1900 self.record_restore(restore);
1901 self.stats.hits.fetch_add(1, Ordering::Relaxed);
1902 self.emit_action(
1903 diagnostic,
1904 AgentEvent::ActionHit {
1905 crate_name,
1906 restore,
1907 },
1908 );
1909 Ok(AgentResponse::ActionHitRecorded)
1910 }
1911
1912 fn record_restore(&self, restore: RestoreStats) {
1913 self.record_materialization(restore);
1914 atomic_saturating_add(
1915 &self.stats.avoided_compiler_duration_ns,
1916 restore.avoided_compiler_duration_ns,
1917 );
1918 atomic_saturating_add(&self.stats.restored_output_files, restore.output_files);
1919 atomic_saturating_add(&self.stats.restored_output_bytes, restore.output_bytes);
1920 atomic_saturating_add(
1921 &self.stats.reflinked_output_files,
1922 restore.reflinked_output_files,
1923 );
1924 atomic_saturating_add(
1925 &self.stats.reflinked_output_bytes,
1926 restore.reflinked_output_bytes,
1927 );
1928 atomic_saturating_add(&self.stats.copied_output_files, restore.copied_output_files);
1929 atomic_saturating_add(&self.stats.copied_output_bytes, restore.copied_output_bytes);
1930 atomic_saturating_add(&self.stats.reused_output_files, restore.reused_output_files);
1931 atomic_saturating_add(&self.stats.reused_output_bytes, restore.reused_output_bytes);
1932 }
1933
1934 fn record_compiler_invocation(
1935 &self,
1936 outcome: &str,
1937 crate_name: Option<&str>,
1938 duration_ns: u64,
1939 diagnostic: Option<AgentEvent>,
1940 ) -> Result<AgentResponse> {
1941 if !matches!(
1942 outcome,
1943 "miss" | "unconsulted" | "bypass" | "verification" | "incremental"
1944 ) {
1945 bail!("invalid compiler invocation outcome");
1946 }
1947 validate_crate_name(crate_name)?;
1948 let mut compiler = self.stats.compiler.lock().unwrap();
1949 let stats = compiler.entry(outcome.to_string()).or_default();
1950 stats.invocations = stats.invocations.saturating_add(1);
1951 stats.duration_ns = stats.duration_ns.saturating_add(duration_ns);
1952 drop(compiler);
1953 if outcome != "verification"
1954 && let Some(crate_name) = crate_name.filter(|name| !name.is_empty())
1955 {
1956 let mut slow = self.stats.slow_compilations.lock().unwrap();
1957 let duration = slow.entry(crate_name.to_string()).or_default();
1958 *duration = duration.saturating_add(duration_ns);
1959 }
1960 self.emit_action(
1961 diagnostic,
1962 AgentEvent::CompilerInvocation {
1963 outcome: outcome.to_string(),
1964 crate_name: crate_name.map(str::to_string),
1965 duration_ns,
1966 },
1967 );
1968 Ok(AgentResponse::CompilerInvocationRecorded)
1969 }
1970
1971 fn record_materialization(&self, restore: RestoreStats) {
1972 atomic_saturating_add(&self.stats.materialization_duration_ns, restore.duration_ns);
1973 }
1974
1975 fn record_warning(&self, message: String) -> Result<AgentResponse> {
1984 if message.is_empty()
1985 || message.len() > MAX_WARNING_BYTES
1986 || message.contains(['\n', '\r', '\0'])
1987 {
1988 bail!("invalid shim warning");
1989 }
1990 let mut warnings = self.warnings.lock().unwrap();
1991 if !warnings.contains(&message) && warnings.len() < MAX_WARNINGS {
1992 eprintln!("mbx[warning]: {message}");
1993 self.emit(|| AgentEvent::Warning {
1994 message: message.clone(),
1995 });
1996 warnings.insert(message);
1997 }
1998 Ok(AgentResponse::WarningRecorded)
1999 }
2000
2001 fn find_file_digests(
2008 &self,
2009 scope: FileDigestScope,
2010 files: Vec<FileIdentity>,
2011 ) -> Result<AgentResponse> {
2012 if files.len() > MAX_FILE_DIGEST_BATCH {
2013 bail!("too many file-digest lookups in one request");
2014 }
2015 let ledger = self.file_digests.lock().unwrap();
2016 let digests = files
2017 .into_iter()
2018 .map(|file| {
2019 let recorded = ledger.get(&(scope, file.path.clone()))?;
2020 (recorded.file == file).then(|| recorded.digest.clone())
2021 })
2022 .collect();
2023 Ok(AgentResponse::FileDigests { digests })
2024 }
2025
2026 async fn resolve_file_digests(
2029 &self,
2030 scope: FileDigestScope,
2031 files: Vec<FileIdentity>,
2032 ) -> Result<AgentResponse> {
2033 if files.len() > MAX_FILE_DIGEST_BATCH {
2034 bail!("too many file-digest resolutions in one request");
2035 }
2036 for file in &files {
2037 if !file.path.is_absolute() {
2038 bail!("file-digest resolutions need absolute paths");
2039 }
2040 }
2041 let resolutions = stream::iter(
2042 files
2043 .into_iter()
2044 .map(|file| async move { self.resolve_file_digest(scope, file).await }),
2045 )
2046 .buffered(MAX_CONCURRENT_FILE_DIGESTS)
2047 .collect()
2048 .await;
2049 Ok(AgentResponse::FileDigestsResolved { resolutions })
2050 }
2051
2052 async fn resolve_file_digest(
2053 &self,
2054 scope: FileDigestScope,
2055 file: FileIdentity,
2056 ) -> FileDigestResolution {
2057 let lock = {
2058 let key = (scope, file.clone());
2059 let mut locks = self.file_digest_locks.lock().unwrap();
2060 locks.retain(|_, lock| lock.strong_count() > 0);
2061 if let Some(lock) = locks.get(&key).and_then(Weak::upgrade) {
2062 lock
2063 } else {
2064 let lock = Arc::new(FileDigestFlight {
2065 lock: tokio::sync::Mutex::new(()),
2066 resolution: Mutex::new(None),
2067 });
2068 locks.insert(key, Arc::downgrade(&lock));
2069 lock
2070 }
2071 };
2072 let _flight = lock.lock.lock().await;
2073 if let Some(resolution) = lock.resolution.lock().unwrap().clone() {
2074 return resolution;
2075 }
2076 if let Some(digest) = self
2077 .file_digests
2078 .lock()
2079 .unwrap()
2080 .get(&(scope, file.path.clone()))
2081 .filter(|recorded| recorded.file == file)
2082 .map(|recorded| recorded.digest.clone())
2083 {
2084 return FileDigestResolution::Digest(digest);
2085 }
2086 let Ok(_permit) = self.file_digest_permits.acquire().await else {
2087 return FileDigestResolution::Unresolved;
2088 };
2089 #[cfg(test)]
2090 self.file_digest_reads.fetch_add(1, Ordering::Relaxed);
2091 let path = file.path.clone();
2092 let resolved = tokio::task::spawn_blocking(move || {
2093 let resolution = digest_file(scope, &path)?;
2094 let current = std::fs::metadata(&path)
2095 .and_then(|metadata| FileIdentity::for_digest_cache(&path, &metadata));
2096 Ok::<_, std::io::Error>((resolution, current?))
2097 })
2098 .await;
2099 let Ok(Ok((resolution, current))) = resolved else {
2100 return FileDigestResolution::Unresolved;
2101 };
2102 let resolution = match resolution {
2103 FileDigestResolution::Digest(digest)
2104 if current.as_ref() == Some(&file) && digest.size == file.len =>
2105 {
2106 let _ = self.record_file_digests(
2107 scope,
2108 vec![RecordedFileDigest {
2109 file: file.clone(),
2110 digest: digest.clone(),
2111 }],
2112 );
2113 FileDigestResolution::Digest(digest)
2114 }
2115 FileDigestResolution::EmbeddedTimestampMacro if current.as_ref() == Some(&file) => {
2116 FileDigestResolution::EmbeddedTimestampMacro
2117 }
2118 FileDigestResolution::Digest(_)
2119 | FileDigestResolution::EmbeddedTimestampMacro
2120 | FileDigestResolution::Unresolved => FileDigestResolution::Unresolved,
2121 };
2122 *lock.resolution.lock().unwrap() = Some(resolution.clone());
2123 resolution
2124 }
2125
2126 fn record_file_digests(
2132 &self,
2133 scope: FileDigestScope,
2134 entries: Vec<RecordedFileDigest>,
2135 ) -> Result<AgentResponse> {
2136 if entries.len() > MAX_FILE_DIGEST_BATCH {
2137 bail!("too many file-digest records in one request");
2138 }
2139 for entry in &entries {
2140 validate_file_digest_record(entry)?;
2141 }
2142 let mut ledger = self.file_digests.lock().unwrap();
2143 for entry in entries {
2144 if ledger.len() >= MAX_FILE_DIGEST_ENTRIES
2145 && !ledger.contains_key(&(scope, entry.file.path.clone()))
2146 {
2147 break;
2148 }
2149 ledger.insert((scope, entry.file.path.clone()), entry);
2150 }
2151 Ok(AgentResponse::FileDigestsRecorded)
2152 }
2153
2154 pub fn seed_file_digests(&self, entries: Vec<(FileDigestScope, RecordedFileDigest)>) -> usize {
2163 self.seed_file_digests_with(|| entries)
2164 }
2165
2166 pub fn seed_file_digests_with(
2172 &self,
2173 load: impl FnOnce() -> Vec<(FileDigestScope, RecordedFileDigest)>,
2174 ) -> usize {
2175 let mut ledger = self.file_digests.lock().unwrap();
2176 let entries = load();
2177 let mut seeded = 0;
2178 for (scope, entry) in entries {
2179 if ledger.len() >= MAX_FILE_DIGEST_ENTRIES {
2180 break;
2181 }
2182 if validate_file_digest_record(&entry).is_err() {
2183 continue;
2184 }
2185 if let std::collections::btree_map::Entry::Vacant(slot) =
2186 ledger.entry((scope, entry.file.path.clone()))
2187 {
2188 slot.insert(entry);
2189 seeded += 1;
2190 }
2191 }
2192 seeded
2193 }
2194
2195 pub fn file_digests(&self) -> Vec<(FileDigestScope, RecordedFileDigest)> {
2198 self.file_digests
2199 .lock()
2200 .unwrap()
2201 .iter()
2202 .map(|((scope, _), entry)| (*scope, entry.clone()))
2203 .collect()
2204 }
2205
2206 fn find_action_prediction(
2207 &self,
2208 task: &str,
2209 invocation: &CacheDigest,
2210 ) -> Result<AgentResponse> {
2211 validate_task_identity(task)?;
2212 invocation.validate()?;
2213 let (prediction, prefetch) = {
2214 let mut tasks = self.task_actions.lock().unwrap();
2215 let Some(state) = tasks.get_mut(task) else {
2216 return Ok(AgentResponse::ActionPrediction { prediction: None });
2217 };
2218 activate_prediction_adapter(state, invocation)
2219 };
2220 if let Some(prefetch) = prefetch {
2221 self.spawn_prefetch_predictions(prefetch);
2222 }
2223 Ok(AgentResponse::ActionPrediction { prediction })
2224 }
2225
2226 fn record_action_prediction(
2227 &self,
2228 task: &str,
2229 prediction: ActionPrediction,
2230 ) -> Result<AgentResponse> {
2231 validate_task_identity(task)?;
2232 prediction.validate()?;
2233 let mut tasks = self.task_actions.lock().unwrap();
2234 let state = tasks.entry(task.to_string()).or_default();
2235 if !state.predictions.contains_key(&prediction.invocation)
2236 && state.predictions.len() >= MAX_TASK_ACTION_PREDICTIONS
2237 {
2238 bail!("task action manifest contains too many predictions");
2239 }
2240 let current = state.predictions.get(&prediction.invocation);
2241 if current != Some(&prediction) {
2242 match state.changed_predictions.get(&prediction.invocation) {
2243 Some(baseline) if baseline.as_ref() == Some(&prediction) => {
2245 state.changed_predictions.remove(&prediction.invocation);
2246 }
2247 Some(_) => {}
2249 None => {
2250 state
2251 .changed_predictions
2252 .insert(prediction.invocation.clone(), current.cloned());
2253 }
2254 }
2255 }
2256 state
2257 .predictions
2258 .insert(prediction.invocation.clone(), prediction.clone());
2259 state
2260 .pending_predictions
2261 .insert(prediction.invocation.clone(), prediction);
2262 Ok(AgentResponse::ActionPredictionRecorded)
2263 }
2264
2265 fn executable_identity_key(
2266 &self,
2267 executable: PathBuf,
2268 environment: BTreeMap<String, Option<String>>,
2269 ) -> Result<ExecutableIdentityKey> {
2270 if !environment.keys().all(|name| {
2276 matches!(
2277 name.as_str(),
2278 "COMPILER_PATH"
2279 | "GCC_EXEC_PREFIX"
2280 | "LIBRARY_PATH"
2281 | "MBX_FUSE_LD"
2282 | "PATH"
2283 | "RUSTUP_HOME"
2284 | "RUSTUP_TOOLCHAIN"
2285 | "SDKROOT"
2286 | "MACOSX_DEPLOYMENT_TARGET"
2287 | "LIB"
2288 | "UCRTVersion"
2289 | "UniversalCRTSdkDir"
2290 | "VCToolsInstallDir"
2291 | "VCToolsVersion"
2292 | "WindowsSdkDir"
2293 | "WindowsSDKVersion"
2294 )
2295 }) {
2296 bail!("executable identity contains an unsupported environment variable");
2297 }
2298 Ok(ExecutableIdentityKey {
2299 executable,
2300 environment,
2301 })
2302 }
2303
2304 fn find_executable_identity(
2305 &self,
2306 executable: PathBuf,
2307 environment: BTreeMap<String, Option<String>>,
2308 ) -> Result<AgentResponse> {
2309 let key = self.executable_identity_key(executable, environment)?;
2310 let remembered = self
2311 .executable_identities
2312 .lock()
2313 .unwrap()
2314 .get(&key)
2315 .cloned();
2316 let stdout = match remembered {
2317 Some(stdout) => Some(stdout),
2318 None => {
2319 let persisted = self.load_persisted_identity(&key);
2320 if let Some(stdout) = &persisted {
2321 let _ = self.remember_executable_identity(&key, stdout.clone());
2324 }
2325 persisted
2326 }
2327 };
2328 Ok(AgentResponse::ExecutableIdentity { stdout })
2329 }
2330
2331 fn store_executable_identity(
2332 &self,
2333 executable: PathBuf,
2334 environment: BTreeMap<String, Option<String>>,
2335 stdout: Vec<u8>,
2336 pins: Vec<PinnedFile>,
2337 ) -> Result<AgentResponse> {
2338 if stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE {
2339 bail!("executable identity exceeds {MAX_EXECUTABLE_IDENTITY_SIZE} bytes");
2340 }
2341 let key = self.executable_identity_key(executable, environment)?;
2342 self.remember_executable_identity(&key, stdout.clone())?;
2343 if pins.is_empty() {
2348 } else if !pins.iter().all(PinnedFile::holds) {
2349 debug!("executable identity was not persisted: a pinned file changed during the probe");
2350 } else if let Err(error) = self.persist_executable_identity(&key, pins, &stdout) {
2351 debug!("executable identity was not persisted: {error:#}");
2352 }
2353 Ok(AgentResponse::ExecutableIdentity {
2354 stdout: Some(stdout),
2355 })
2356 }
2357
2358 fn remember_executable_identity(
2359 &self,
2360 key: &ExecutableIdentityKey,
2361 stdout: Vec<u8>,
2362 ) -> Result<()> {
2363 let mut identities = self.executable_identities.lock().unwrap();
2364 let is_new = !identities.contains_key(key);
2365 let previous_size = identities.get(key).map_or(0, Vec::len);
2366 if is_new && identities.len() >= MAX_EXECUTABLE_IDENTITIES {
2367 bail!("executable identity cache contains too many entries");
2368 }
2369 let retained_bytes = identities.values().map(Vec::len).sum::<usize>();
2370 if retained_bytes - previous_size + stdout.len() > MAX_EXECUTABLE_IDENTITY_BYTES {
2371 bail!("executable identity cache contains too many bytes");
2372 }
2373 identities.insert(key.clone(), stdout);
2374 Ok(())
2375 }
2376
2377 fn persisted_identity_path(&self, key: &ExecutableIdentityKey) -> Result<PathBuf> {
2378 let selector = serde_json::to_vec(&(&key.executable, &key.environment))?;
2379 let digest = CacheDigest::blake3(&selector);
2380 Ok(self.identity_dir.join(format!("{}.json", digest.hash)))
2381 }
2382
2383 fn load_persisted_identity(&self, key: &ExecutableIdentityKey) -> Option<Vec<u8>> {
2386 let path = self.persisted_identity_path(key).ok()?;
2387 let bytes = fs::read(path).ok()?;
2388 let persisted: PersistedExecutableIdentity = serde_json::from_slice(&bytes).ok()?;
2389 if persisted.version != PERSISTED_EXECUTABLE_IDENTITY_VERSION
2390 || persisted.executable != key.executable
2391 || persisted.environment != key.environment
2392 || persisted.pins.is_empty()
2393 || persisted.stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE
2394 {
2395 return None;
2396 }
2397 persisted
2398 .pins
2399 .iter()
2400 .all(PinnedFile::holds)
2401 .then(|| persisted.stdout.into_bytes())
2402 }
2403
2404 fn persist_executable_identity(
2405 &self,
2406 key: &ExecutableIdentityKey,
2407 pins: Vec<PinnedFile>,
2408 stdout: &[u8],
2409 ) -> Result<()> {
2410 let Ok(stdout) = std::str::from_utf8(stdout) else {
2412 return Ok(());
2413 };
2414 let persisted = PersistedExecutableIdentity {
2415 version: PERSISTED_EXECUTABLE_IDENTITY_VERSION,
2416 executable: key.executable.clone(),
2417 environment: key.environment.clone(),
2418 pins,
2419 stdout: stdout.to_owned(),
2420 };
2421 let path = self.persisted_identity_path(key)?;
2422 fs::create_dir_all(self.identity_dir.as_path())?;
2423 let mut temporary = tempfile::NamedTempFile::new_in(self.identity_dir.as_path())?;
2425 std::io::Write::write_all(temporary.as_file_mut(), &serde_json::to_vec(&persisted)?)?;
2426 temporary.persist(path).map_err(|error| error.error)?;
2427 Ok(())
2428 }
2429
2430 pub async fn handle_connection<S>(&self, stream: S) -> Result<()>
2432 where
2433 S: AsyncRead + AsyncWrite + Unpin,
2434 {
2435 let (reader, mut writer) = tokio::io::split(stream);
2436 let mut reader = BufReader::new(reader);
2437 let hello = read_request(&mut reader)
2438 .await?
2439 .ok_or_else(|| eyre::eyre!("connection closed before the agent handshake"))?;
2440 let request: AgentRequest = serde_json::from_str(&hello)?;
2441 match request {
2442 AgentRequest::Hello {
2443 protocol,
2444 client_version,
2445 } if protocol == AGENT_PROTOCOL_VERSION && client_version == self.version.as_ref() => {}
2446 AgentRequest::Hello { protocol, .. } if protocol != AGENT_PROTOCOL_VERSION => {
2447 send_response(
2448 &mut writer,
2449 &AgentResponse::Error {
2450 message: format!(
2451 "unsupported agent protocol {protocol}; expected {AGENT_PROTOCOL_VERSION}"
2452 ),
2453 },
2454 )
2455 .await?;
2456 return Ok(());
2457 }
2458 AgentRequest::Hello { client_version, .. } => {
2459 send_response(
2460 &mut writer,
2461 &AgentResponse::Error {
2462 message: format!(
2463 "cache client {client_version} does not match agent {}",
2464 self.version
2465 ),
2466 },
2467 )
2468 .await?;
2469 return Ok(());
2470 }
2471 _ => bail!("the first agent request must be hello"),
2472 }
2473 send_response(
2474 &mut writer,
2475 &AgentResponse::Hello {
2476 protocol: AGENT_PROTOCOL_VERSION,
2477 agent_version: self.version.to_string(),
2478 },
2479 )
2480 .await?;
2481
2482 let mut connection = ConnectionUploads::default();
2486 while let Some(line) = read_request(&mut reader).await? {
2487 let response = match serde_json::from_str(&line) {
2488 Ok(request) => self.respond_on(request, &mut connection).await,
2489 Err(error) => AgentResponse::Error {
2490 message: format!("invalid agent request: {error}"),
2491 },
2492 };
2493 send_response(&mut writer, &response).await?;
2494 }
2495 Ok(())
2496 }
2497}
2498
2499async fn read_request<R>(reader: &mut R) -> Result<Option<String>>
2505where
2506 R: AsyncBufRead + Unpin,
2507{
2508 let mut line = Vec::new();
2509 loop {
2510 let available = reader.fill_buf().await?;
2511 if available.is_empty() {
2512 break;
2513 }
2514 let (consumed, complete) = match available.iter().position(|byte| *byte == b'\n') {
2515 Some(index) => (index, true),
2516 None => (available.len(), false),
2517 };
2518 if line.len() + consumed > MAX_REQUEST_BYTES {
2519 bail!("agent request exceeded {MAX_REQUEST_BYTES} bytes");
2520 }
2521 line.extend_from_slice(&available[..consumed]);
2522 reader.consume(consumed + usize::from(complete));
2524 if complete {
2525 return Ok(Some(String::from_utf8(line)?));
2526 }
2527 }
2528 if line.is_empty() {
2529 Ok(None)
2530 } else {
2531 Ok(Some(String::from_utf8(line)?))
2532 }
2533}
2534
2535async fn send_response(
2536 writer: &mut (impl AsyncWrite + Unpin),
2537 response: &AgentResponse,
2538) -> Result<()> {
2539 let mut encoded = serde_json::to_vec(response)?;
2540 encoded.push(b'\n');
2541 writer.write_all(&encoded).await?;
2542 writer.flush().await?;
2543 Ok(())
2544}
2545
2546#[cfg(test)]
2547#[path = "agent_tests.rs"]
2548mod tests;