1use crate::uploads::{ConnectionUploads, UploadQueue, UploadSink};
2use crate::{
3 ActionPrediction, ActionPromiseCompletion, ActionPromiseState, BlobPackLimits, CacheDigest,
4 CacheDirectory, LocalActionCache, LocalCas, MAX_STAGED_BLOB_PACK_BYTES,
5 MAX_STAGED_BLOB_PACK_ITEMS, ManifestPutOutcome, RemoteActionResult, RemoteCacheClient,
6 RemoteCacheMode, RustcMetadata, TaskActionManifest, blob_pack_chunk, canonical_json,
7};
8use eyre::{Context, Result, bail};
9use futures_util::{FutureExt, StreamExt, future::BoxFuture, stream};
10use log::warn;
11use std::collections::{BTreeMap, BTreeSet};
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::sync::{Arc, Mutex, Weak};
16use std::time::{Duration, Instant, SystemTime};
17
18const ACTION_PROMISE_WAIT: Duration = Duration::from_secs(60 * 60);
22use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
23
24mod file_digest;
25mod manifest;
26mod prefetch;
27mod stats;
28mod wire;
29
30#[cfg(test)]
31pub(crate) use prefetch::select_prefetch_actions;
32
33pub use file_digest::{
34 FileDigestCache, FileDigestScope, FileIdentity, NoFileDigestCache, RecordedFileDigest,
35};
36pub use manifest::{is_task_identity, task_manifest_actions};
37use manifest::{
38 merge_remote_task_manifest, merge_task_manifests, task_manifest_dir, validate_task_identity,
39 validate_task_manifest,
40};
41pub use stats::{AgentStats, CompilerStats};
42use wire::MAX_REQUEST_BYTES;
43pub use wire::{
44 AGENT_PROTOCOL_VERSION, ActionDiagnostic, AgentEvent, AgentEventObserver, AgentRequest,
45 AgentResponse, RestoreStats,
46};
47
48const MAX_EXECUTABLE_IDENTITIES: usize = 64;
49const MAX_EXECUTABLE_IDENTITY_SIZE: usize = 64 * 1024;
50const MAX_EXECUTABLE_IDENTITY_BYTES: usize = 256 * 1024;
51const TASK_ACTION_MANIFEST_VERSION: u8 = 1;
52const MAX_TASK_ACTION_PREDICTIONS: usize = 16 * 1024;
53const MAX_WARNING_BYTES: usize = 4 * 1024;
55const MAX_WARNINGS: usize = 128;
61const ACTION_DIAGNOSTIC_PREFIX: &str = "@mbx-action-diagnostic\t";
62const MAX_FILE_DIGEST_BATCH: usize = 16 * 1024;
64const MAX_FILE_DIGEST_ENTRIES: usize = 1024 * 1024;
69const MAX_REMOTE_TRANSFERS: usize = 64;
70const MAX_PREFETCH_TRANSFERS: usize = 48;
71const MAX_PREFETCH_ACTIONS: usize = 256;
81const MAX_PREFETCH_ACTION_WAVE: usize = 32;
86const MAX_PREFETCH_BATCH_LOOKUPS: usize = 1;
92const PREFETCH_ACTION_BATCH_DELAY: Duration = Duration::from_millis(5);
93const MAX_PREFETCH_DIRECTORY_OBJECTS: usize = 100_000;
94const MAX_PREFETCH_OBJECTS_PER_WAVE: usize = 100_000;
95const DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES: u64 = 5 * 1024 * 1024 * 1024;
96
97pub struct AgentRemoteCache {
99 pub client: RemoteCacheClient,
101 pub mode: RemoteCacheMode,
103 pub staging_dir: PathBuf,
105}
106
107#[derive(Default)]
108struct AtomicAgentStats {
109 lookups: AtomicU64,
110 unconsulted: AtomicU64,
111 hits: AtomicU64,
112 stores: AtomicU64,
113 stored_bytes: AtomicU64,
114 verifications: AtomicU64,
115 divergences: AtomicU64,
116 downloaded_bytes: AtomicU64,
117 uploaded_bytes: AtomicU64,
118 background_uploads: AtomicU64,
119 background_upload_failures: AtomicU64,
120 remote_blob_pack_uploads: AtomicU64,
121 remote_blob_pack_upload_blobs: AtomicU64,
122 upload_drain_duration_ns: AtomicU64,
123 prefetched_actions: AtomicU64,
124 predictions_loaded: AtomicU64,
125 remote_failures: AtomicU64,
126 remote_manifest_lookups: AtomicU64,
127 remote_manifest_lookup_duration_ns: AtomicU64,
128 remote_action_lookups: AtomicU64,
129 remote_action_lookup_duration_ns: AtomicU64,
130 remote_blob_requests: AtomicU64,
131 remote_blob_pack_requests: AtomicU64,
132 remote_blob_pack_blobs: AtomicU64,
133 remote_blob_transfer_duration_ns: AtomicU64,
134 local_cas_write_duration_ns: AtomicU64,
135 prefetch_runs: AtomicU64,
136 prefetch_duration_ns: AtomicU64,
137 materialization_duration_ns: AtomicU64,
138 bypasses: Mutex<BTreeMap<String, u64>>,
139 avoided_compiler_duration_ns: AtomicU64,
140 compiler: Mutex<BTreeMap<String, CompilerStats>>,
141 slow_compilations: Mutex<BTreeMap<String, u64>>,
142 restored_output_files: AtomicU64,
143 restored_output_bytes: AtomicU64,
144 reflinked_output_files: AtomicU64,
145 reflinked_output_bytes: AtomicU64,
146 copied_output_files: AtomicU64,
147 copied_output_bytes: AtomicU64,
148 reused_output_files: AtomicU64,
149 reused_output_bytes: AtomicU64,
150}
151
152struct AtomicDurationTimer<'a> {
153 started: Instant,
154 target: &'a AtomicU64,
155}
156
157impl<'a> AtomicDurationTimer<'a> {
158 fn start(target: &'a AtomicU64) -> Self {
159 Self {
160 started: Instant::now(),
161 target,
162 }
163 }
164}
165
166impl Drop for AtomicDurationTimer<'_> {
167 fn drop(&mut self) {
168 atomic_saturating_add(self.target, duration_ns(self.started));
169 }
170}
171
172fn duration_ns(started: Instant) -> u64 {
173 started.elapsed().as_nanos().try_into().unwrap_or(u64::MAX)
174}
175
176fn validate_crate_name(crate_name: Option<&str>) -> Result<()> {
177 if let Some(crate_name) = crate_name
178 && (crate_name.len() > 256 || crate_name.contains(['\0', '\n', '\r']))
179 {
180 bail!("invalid compiler crate name");
181 }
182 Ok(())
183}
184
185#[derive(serde::Deserialize)]
186struct ActionDiagnosticEnvelope {
187 outcome: String,
188 crate_name: Option<String>,
189 diagnostic: ActionDiagnostic,
190}
191
192fn parse_action_diagnostic(
193 message: &str,
194) -> Option<Result<(String, Option<String>, ActionDiagnostic)>> {
195 let payload = message.strip_prefix(ACTION_DIAGNOSTIC_PREFIX)?;
196 Some((|| {
197 let envelope: ActionDiagnosticEnvelope = serde_json::from_str(payload)?;
198 if !matches!(envelope.outcome.as_str(), "hit" | "miss") {
199 bail!("invalid action diagnostic outcome");
200 }
201 validate_crate_name(envelope.crate_name.as_deref())?;
202 Ok((envelope.outcome, envelope.crate_name, envelope.diagnostic))
203 })())
204}
205
206fn atomic_saturating_add(target: &AtomicU64, value: u64) {
207 let _ = target.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
208 Some(current.saturating_add(value))
209 });
210}
211
212fn queue_prefetch_digest(
213 verified: &BTreeMap<CacheDigest, PathBuf>,
214 pending: &mut BTreeMap<CacheDigest, ()>,
215 digest: CacheDigest,
216) {
217 if verified.contains_key(&digest) || pending.contains_key(&digest) {
218 return;
219 }
220 pending.insert(digest, ());
221}
222
223fn queue_prefetch_directory(
224 seen: &BTreeMap<CacheDigest, ()>,
225 pending: &mut BTreeMap<CacheDigest, ()>,
226 digest: CacheDigest,
227 limit: usize,
228) -> bool {
229 if seen.contains_key(&digest) || pending.contains_key(&digest) {
230 return true;
231 }
232 if seen.len().saturating_add(pending.len()) >= limit {
233 return false;
234 }
235 pending.insert(digest, ());
236 true
237}
238
239#[derive(Clone)]
245pub struct CacheAgent {
246 cas: LocalCas,
247 actions: LocalActionCache,
248 verified_blobs: Arc<Mutex<BTreeMap<CacheDigest, VerifiedBlob>>>,
249 version: Arc<str>,
250 write_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
251 action_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
252 stats: Arc<AtomicAgentStats>,
253 observer: Option<Arc<dyn AgentEventObserver>>,
254 observer_emission: Arc<Mutex<()>>,
255 executable_identities: Arc<Mutex<BTreeMap<ExecutableIdentityKey, Vec<u8>>>>,
256 manifest_dir: Arc<PathBuf>,
257 task_actions: Arc<Mutex<BTreeMap<String, TaskActionState>>>,
258 next_task_run: Arc<AtomicU64>,
259 manifest_write_lock: Arc<Mutex<()>>,
260 remote: Option<Arc<RemoteCacheClient>>,
261 remote_mode: RemoteCacheMode,
262 remote_staging_dir: Arc<PathBuf>,
263 remote_download_limit: u64,
264 remote_download_bytes: Arc<AtomicU64>,
265 pending_remote_actions: Arc<Mutex<BTreeMap<CacheDigest, RemoteActionResult>>>,
266 remote_transfers: Arc<tokio::sync::Semaphore>,
267 prefetch_transfers: Arc<tokio::sync::Semaphore>,
268 prefetch_tasks: Arc<Mutex<Vec<tokio::task::JoinHandle<()>>>>,
269 warnings: Arc<Mutex<BTreeSet<String>>>,
272 file_digests: Arc<Mutex<BTreeMap<(FileDigestScope, PathBuf), RecordedFileDigest>>>,
275 uploads: Option<UploadQueue>,
277}
278
279fn validate_file_digest_record(entry: &RecordedFileDigest) -> Result<()> {
282 if !entry.file.path.is_absolute() {
283 bail!("file-digest records need absolute paths");
284 }
285 entry.digest.validate()?;
286 if entry.file.len != entry.digest.size {
287 bail!("file-digest record length does not match its digest");
288 }
289 Ok(())
290}
291
292struct AgentUploadSink {
294 stats: Arc<AtomicAgentStats>,
295}
296
297impl UploadSink for AgentUploadSink {
298 fn record_blob_uploaded(&self, bytes: u64) {
299 self.stats
300 .background_uploads
301 .fetch_add(1, Ordering::Relaxed);
302 self.stats
303 .uploaded_bytes
304 .fetch_add(bytes, Ordering::Relaxed);
305 }
306
307 fn record_action_uploaded(&self) {
308 self.stats
309 .background_uploads
310 .fetch_add(1, Ordering::Relaxed);
311 }
312
313 fn record_blob_pack_uploaded(&self, blobs: u64) {
314 self.stats
315 .remote_blob_pack_uploads
316 .fetch_add(1, Ordering::Relaxed);
317 self.stats
318 .remote_blob_pack_upload_blobs
319 .fetch_add(blobs, Ordering::Relaxed);
320 }
321
322 fn record_upload_failure(&self) {
323 self.stats
324 .background_upload_failures
325 .fetch_add(1, Ordering::Relaxed);
326 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
327 }
328}
329
330#[derive(Debug, Clone)]
336struct VerifiedBlob {
337 path: PathBuf,
338 len: u64,
339 modified: SystemTime,
340}
341
342impl VerifiedBlob {
343 fn describe(path: &Path) -> Option<Self> {
346 let metadata = std::fs::metadata(path).ok()?;
347 Some(Self {
348 path: path.to_path_buf(),
349 len: metadata.len(),
350 modified: metadata.modified().ok()?,
351 })
352 }
353
354 fn is_unchanged(&self) -> bool {
356 let Ok(metadata) = std::fs::metadata(&self.path) else {
357 return false;
358 };
359 metadata.len() == self.len && metadata.modified().is_ok_and(|now| now == self.modified)
360 }
361}
362
363#[derive(Debug, Clone, Default)]
364struct TaskActionState {
365 manifest: String,
366 baseline_loaded: bool,
367 predictions: BTreeMap<CacheDigest, ActionPrediction>,
368 pending_predictions: BTreeMap<CacheDigest, ActionPrediction>,
369 remote_etag: Option<String>,
370}
371
372struct PrefetchedAction {
373 adapter: String,
374 result: RemoteActionResult,
375}
376
377struct RemoteDownloadReservation {
378 counter: Arc<AtomicU64>,
379 reserved: u64,
380 committed: bool,
381}
382
383impl RemoteDownloadReservation {
384 fn bytes(&self) -> u64 {
385 self.reserved
386 }
387
388 fn commit(mut self, bytes: u64) {
389 debug_assert!(bytes <= self.reserved);
390 self.counter
391 .fetch_sub(self.reserved.saturating_sub(bytes), Ordering::AcqRel);
392 self.committed = true;
393 }
394}
395
396impl Drop for RemoteDownloadReservation {
397 fn drop(&mut self) {
398 if !self.committed {
399 self.counter.fetch_sub(self.reserved, Ordering::AcqRel);
400 }
401 }
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
405struct ExecutableIdentityKey {
406 executable: PathBuf,
407 environment: BTreeMap<String, Option<String>>,
408}
409
410impl CacheAgent {
411 pub fn new(cache_dir: impl Into<PathBuf>, version: impl Into<Arc<str>>) -> Self {
413 Self::build(cache_dir.into(), version.into(), None, 0)
414 }
415
416 pub fn new_remote(
418 cache_dir: impl Into<PathBuf>,
419 version: impl Into<Arc<str>>,
420 remote: AgentRemoteCache,
421 ) -> Self {
422 Self::build(
423 cache_dir.into(),
424 version.into(),
425 Some(remote),
426 DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES,
427 )
428 }
429
430 pub fn new_remote_with_download_limit(
432 cache_dir: impl Into<PathBuf>,
433 version: impl Into<Arc<str>>,
434 remote: AgentRemoteCache,
435 max_remote_download_bytes: u64,
436 ) -> Self {
437 Self::build(
438 cache_dir.into(),
439 version.into(),
440 Some(remote),
441 max_remote_download_bytes,
442 )
443 }
444
445 fn build(
446 cache_dir: PathBuf,
447 version: Arc<str>,
448 remote: Option<AgentRemoteCache>,
449 remote_download_limit: u64,
450 ) -> Self {
451 let remote_mode = remote
452 .as_ref()
453 .map_or(RemoteCacheMode::ReadOnly, |remote| remote.mode);
454 let remote_staging_dir = remote.as_ref().map_or_else(
455 || cache_dir.join("remote"),
456 |remote| remote.staging_dir.clone(),
457 );
458 let remote = remote.map(|remote| Arc::new(remote.client));
459 let stats = Arc::new(AtomicAgentStats::default());
460 let remote_transfers = Arc::new(tokio::sync::Semaphore::new(MAX_REMOTE_TRANSFERS));
461 let uploads = remote
464 .clone()
465 .filter(|_| remote_mode.writes())
466 .map(|client| {
467 UploadQueue::new(
468 client,
469 Arc::new(AgentUploadSink {
470 stats: stats.clone(),
471 }),
472 remote_transfers.clone(),
473 )
474 });
475 Self {
476 cas: LocalCas::new(cache_dir.clone()),
477 actions: LocalActionCache::new(cache_dir.clone()),
478 verified_blobs: Arc::new(Mutex::new(BTreeMap::new())),
479 version,
480 write_locks: Arc::new(Mutex::new(BTreeMap::new())),
481 action_locks: Arc::new(Mutex::new(BTreeMap::new())),
482 stats,
483 observer: None,
484 observer_emission: Arc::new(Mutex::new(())),
485 executable_identities: Arc::new(Mutex::new(BTreeMap::new())),
486 manifest_dir: Arc::new(task_manifest_dir(&cache_dir)),
487 task_actions: Arc::new(Mutex::new(BTreeMap::new())),
488 next_task_run: Arc::new(AtomicU64::new(0)),
489 manifest_write_lock: Arc::new(Mutex::new(())),
490 remote,
491 remote_mode,
492 remote_staging_dir: Arc::new(remote_staging_dir),
493 remote_download_limit,
494 remote_download_bytes: Arc::new(AtomicU64::new(0)),
495 pending_remote_actions: Arc::new(Mutex::new(BTreeMap::new())),
496 remote_transfers,
497 prefetch_transfers: Arc::new(tokio::sync::Semaphore::new(MAX_PREFETCH_TRANSFERS)),
498 prefetch_tasks: Arc::new(Mutex::new(Vec::new())),
499 warnings: Arc::new(Mutex::new(BTreeSet::new())),
500 file_digests: Arc::new(Mutex::new(BTreeMap::new())),
501 uploads,
502 }
503 }
504
505 #[must_use]
507 pub fn with_observer(mut self, observer: Arc<dyn AgentEventObserver>) -> Self {
508 self.observer = Some(observer);
509 self
510 }
511
512 fn emit(&self, event: impl FnOnce() -> AgentEvent) {
513 if let Some(observer) = &self.observer {
514 let _emission = self.observer_emission.lock().unwrap();
515 observer.event(event());
516 }
517 }
518
519 fn emit_action(&self, diagnostic: Option<AgentEvent>, action: AgentEvent) {
520 if let Some(observer) = &self.observer {
521 let _emission = self.observer_emission.lock().unwrap();
522 if let Some(diagnostic) = diagnostic {
523 observer.event(diagnostic);
524 }
525 observer.event(action);
526 }
527 }
528
529 fn reserve_remote_download(&self, bytes: u64) -> Result<RemoteDownloadReservation> {
530 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
531 loop {
532 let next = current
533 .checked_add(bytes)
534 .ok_or_else(|| eyre::eyre!("remote cache download budget overflowed"))?;
535 if next > self.remote_download_limit {
536 bail!(
537 "remote cache download budget exceeded: {} bytes requested with {} of {} bytes already used",
538 bytes,
539 current,
540 self.remote_download_limit
541 );
542 }
543 match self.remote_download_bytes.compare_exchange_weak(
544 current,
545 next,
546 Ordering::AcqRel,
547 Ordering::Acquire,
548 ) {
549 Ok(_) => {
550 return Ok(RemoteDownloadReservation {
551 counter: self.remote_download_bytes.clone(),
552 reserved: bytes,
553 committed: false,
554 });
555 }
556 Err(observed) => current = observed,
557 }
558 }
559 }
560
561 fn reserve_remote_download_up_to(&self, requested: u64) -> Result<RemoteDownloadReservation> {
562 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
563 loop {
564 let reserved = requested.min(self.remote_download_limit.saturating_sub(current));
565 let next = current + reserved;
566 match self.remote_download_bytes.compare_exchange_weak(
567 current,
568 next,
569 Ordering::AcqRel,
570 Ordering::Acquire,
571 ) {
572 Ok(_) => {
573 return Ok(RemoteDownloadReservation {
574 counter: self.remote_download_bytes.clone(),
575 reserved,
576 committed: false,
577 });
578 }
579 Err(observed) => current = observed,
580 }
581 }
582 }
583
584 pub async fn begin_task(&self, task: &str) -> Result<String> {
586 self.begin_task_with_remote_errors(task, false, None).await
587 }
588
589 pub async fn begin_session_task(&self, task: &str) -> Result<()> {
595 self.begin_task_with_remote_errors(task, false, Some(task.to_string()))
596 .await?;
597 Ok(())
598 }
599
600 pub async fn prefetch_task(&self, task: &str) -> Result<String> {
602 let run = self.begin_task_with_remote_errors(task, true, None).await?;
603 self.wait_for_prefetches().await;
604 Ok(run)
605 }
606
607 async fn begin_task_with_remote_errors(
608 &self,
609 task: &str,
610 strict: bool,
611 run: Option<String>,
612 ) -> Result<String> {
613 validate_task_identity(task)?;
614 let early_predictions = {
620 let _write_guard = self.manifest_write_lock.lock().unwrap();
621 let _file_guard = self.lock_task_manifest(task)?;
622 self.load_task_manifest(task)?
623 .map(|manifest| manifest.predictions)
624 .unwrap_or_default()
625 };
626 let early_actions: BTreeSet<_> = early_predictions
627 .iter()
628 .map(|prediction| prediction.action.clone())
629 .collect();
630 self.spawn_prefetch_predictions(early_predictions);
631 let (remote_manifest, mut remote_etag) = if self.remote_mode.reads() {
632 match self.get_remote_task_manifest(task).await {
633 Ok(Some((manifest, etag))) => (Some(manifest), Some(etag)),
634 Ok(None) => (None, None),
635 Err(error) => {
636 if strict {
637 return Err(error).wrap_err_with(|| {
638 format!("remote task action manifest lookup failed for {task}")
639 });
640 }
641 self.note_remote_failure();
642 warn!("remote task action manifest lookup failed for {task}: {error}");
643 (None, None)
644 }
645 }
646 } else {
647 (None, None)
648 };
649 let manifest = {
650 let _write_guard = self.manifest_write_lock.lock().unwrap();
651 let _file_guard = self.lock_task_manifest(task)?;
652 let local_manifest = self.load_task_manifest(task)?;
653 let manifest = match (remote_manifest, local_manifest) {
654 (Some(remote), Some(local)) => {
655 let (manifest, merged) = merge_remote_task_manifest(task, remote, local);
656 if !merged {
657 remote_etag = None;
658 }
659 Some(manifest)
660 }
661 (Some(remote), None) => Some(remote),
662 (None, local) => local,
663 };
664 if let Some(manifest) = &manifest {
665 self.persist_task_manifest(manifest)?;
666 }
667 manifest
668 };
669 let state = if let Some(manifest) = manifest {
670 TaskActionState {
671 manifest: task.to_string(),
672 baseline_loaded: true,
673 predictions: manifest
674 .predictions
675 .into_iter()
676 .map(|prediction| (prediction.invocation.clone(), prediction))
677 .collect(),
678 pending_predictions: BTreeMap::new(),
679 remote_etag,
680 }
681 } else {
682 TaskActionState {
683 manifest: task.to_string(),
684 baseline_loaded: true,
685 remote_etag,
686 ..TaskActionState::default()
687 }
688 };
689 let run = run.unwrap_or_else(|| {
690 let sequence = self.next_task_run.fetch_add(1, Ordering::Relaxed);
691 CacheDigest::blake3(format!("{task}\0{}\0{sequence}", std::process::id()).as_bytes())
692 .hash
693 });
694 self.stats.predictions_loaded.fetch_max(
698 state.predictions.len().try_into().unwrap_or(u64::MAX),
699 Ordering::Relaxed,
700 );
701 let predictions = state
705 .predictions
706 .values()
707 .filter(|prediction| !early_actions.contains(&prediction.action))
708 .cloned()
709 .collect();
710 self.task_actions.lock().unwrap().insert(run.clone(), state);
711 self.spawn_prefetch_predictions(predictions);
712 Ok(run)
713 }
714
715 pub async fn cancel_prefetches(&self) {
717 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
718 for task in &tasks {
719 task.abort();
720 }
721 for task in tasks {
722 if let Err(error) = task.await
723 && !error.is_cancelled()
724 {
725 warn!("remote action prefetch task failed: {error}");
726 }
727 }
728 }
729
730 pub async fn wait_for_uploads(&self) {
737 let Some(uploads) = &self.uploads else {
738 return;
739 };
740 let _timer = AtomicDurationTimer::start(&self.stats.upload_drain_duration_ns);
741 uploads.drain().await;
742 }
743
744 async fn wait_for_prefetches(&self) {
745 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
746 for task in tasks {
747 if let Err(error) = task.await {
748 warn!("remote action prefetch task failed: {error}");
749 }
750 }
751 }
752
753 pub async fn commit_task(&self, run: &str) -> Result<()> {
755 self.commit_task_actions(run).await.map(|_| ())
756 }
757
758 pub async fn commit_task_actions(&self, run: &str) -> Result<Vec<ActionPrediction>> {
764 validate_task_identity(run)?;
765 let state = self
766 .task_actions
767 .lock()
768 .unwrap()
769 .get(run)
770 .cloned()
771 .ok_or_else(|| eyre::eyre!("task action manifest baseline was not loaded"))?;
772 if !state.baseline_loaded {
773 bail!("task action manifest baseline was not loaded");
774 }
775 let task = state.manifest;
776 validate_task_identity(&task)?;
777 let completed = state
778 .pending_predictions
779 .values()
780 .cloned()
781 .collect::<Vec<_>>();
782 let (manifest, introduced) = {
783 let _write_guard = self.manifest_write_lock.lock().unwrap();
784 let _file_guard = self.lock_task_manifest(&task)?;
785 let mut predictions = self
786 .load_task_manifest(&task)?
787 .map(|manifest| {
788 manifest
789 .predictions
790 .into_iter()
791 .map(|prediction| (prediction.invocation.clone(), prediction))
792 .collect::<BTreeMap<_, _>>()
793 })
794 .unwrap_or_default();
795 let introduced: BTreeSet<CacheDigest> = state
800 .pending_predictions
801 .keys()
802 .filter(|invocation| !predictions.contains_key(*invocation))
803 .cloned()
804 .collect();
805 predictions.extend(state.pending_predictions);
810 let manifest = TaskActionManifest {
811 version: TASK_ACTION_MANIFEST_VERSION,
812 task: task.clone(),
813 predictions: predictions.into_values().collect(),
814 };
815 validate_task_manifest(&manifest, &task)?;
816 self.persist_task_manifest(&manifest)?;
817 (manifest, introduced)
818 };
819 self.task_actions.lock().unwrap().remove(run);
820 if self.remote_mode.writes() {
821 let mut manifest = manifest;
825 if let Some(uploads) = &self.uploads {
826 let actions: Vec<CacheDigest> = manifest
827 .predictions
828 .iter()
829 .map(|prediction| prediction.action.clone())
830 .collect();
831 let unpublished = uploads.wait_for_actions(&actions).await;
832 let withheld = manifest
833 .predictions
834 .iter()
835 .filter(|prediction| {
836 introduced.contains(&prediction.invocation)
837 && unpublished.contains(&prediction.action)
838 })
839 .count();
840 if withheld > 0 {
841 warn!(
844 "{withheld} of {} predicted actions were not published, so the remote task action manifest omits them",
845 manifest.predictions.len()
846 );
847 manifest.predictions.retain(|prediction| {
848 !(introduced.contains(&prediction.invocation)
849 && unpublished.contains(&prediction.action))
850 });
851 }
852 }
853 match self
854 .put_remote_task_manifest(&task, manifest, state.remote_etag)
855 .await
856 {
857 Ok(remote_manifest) => {
858 let _write_guard = self.manifest_write_lock.lock().unwrap();
859 let reconciliation = (|| {
860 let _file_guard = self.lock_task_manifest(&task)?;
861 let manifest = match self.load_task_manifest(&task)? {
862 Some(local) => {
863 merge_remote_task_manifest(&task, remote_manifest, local).0
864 }
865 None => remote_manifest,
866 };
867 self.persist_task_manifest(&manifest)
868 })();
869 if let Err(error) = reconciliation {
870 warn!(
871 "remote task action manifest reconciliation failed for {task}: {error}"
872 );
873 }
874 }
875 Err(error) => {
876 self.note_remote_failure();
877 warn!("remote task action manifest upload failed for {task}: {error}");
878 }
879 }
880 }
881 Ok(completed)
882 }
883
884 fn task_manifest_path(&self, task: &str) -> PathBuf {
885 self.manifest_dir.join(format!("{task}.json"))
886 }
887
888 fn task_manifest_lock_path(&self, task: &str) -> PathBuf {
889 self.manifest_dir.join("locks").join(format!("{task}.lock"))
890 }
891
892 fn lock_task_manifest(&self, task: &str) -> Result<fslock::LockFile> {
893 let path = self.task_manifest_lock_path(task);
894 fs::create_dir_all(path.parent().expect("task manifest lock has a parent"))?;
895 let mut lock = fslock::LockFile::open(&path)?;
896 lock.lock()?;
897 Ok(lock)
898 }
899
900 fn load_task_manifest(&self, task: &str) -> Result<Option<TaskActionManifest>> {
901 match fs::read(self.task_manifest_path(task)) {
902 Ok(contents) => Ok(Some(self.parse_task_manifest(task, &contents, false)?)),
903 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
904 Err(error) => Err(error.into()),
905 }
906 }
907
908 fn parse_task_manifest(
909 &self,
910 task: &str,
911 contents: &[u8],
912 require_canonical: bool,
913 ) -> Result<TaskActionManifest> {
914 let manifest: TaskActionManifest = serde_json::from_slice(contents)?;
915 validate_task_manifest(&manifest, task)?;
916 if require_canonical && canonical_json(&manifest)? != contents {
917 bail!("task action manifest is not canonical JSON");
918 }
919 Ok(manifest)
920 }
921
922 fn task_manifest_selector(task: &str) -> Result<(Vec<u8>, CacheDigest)> {
923 TaskActionManifest::selector(task)
924 }
925
926 fn persist_task_manifest(&self, manifest: &TaskActionManifest) -> Result<()> {
927 let bytes = canonical_json(manifest)?;
928 fs::create_dir_all(self.manifest_dir.as_path())?;
929 let mut temporary = tempfile::NamedTempFile::new_in(self.manifest_dir.as_path())?;
930 std::io::Write::write_all(temporary.as_file_mut(), &bytes)?;
931 temporary.as_file_mut().sync_all()?;
932 temporary
933 .persist(self.task_manifest_path(&manifest.task))
934 .map_err(|error| error.error)?;
935 Ok(())
936 }
937
938 async fn get_remote_task_manifest(
939 &self,
940 task: &str,
941 ) -> Result<Option<(TaskActionManifest, String)>> {
942 let Some(remote) = &self.remote else {
943 return Ok(None);
944 };
945 let (_, selector) = Self::task_manifest_selector(task)?;
946 let _permit = self.remote_transfers.acquire().await?;
947 self.stats
948 .remote_manifest_lookups
949 .fetch_add(1, Ordering::Relaxed);
950 let _timer = AtomicDurationTimer::start(&self.stats.remote_manifest_lookup_duration_ns);
951 let Some(remote_manifest) = remote.get_action_manifest(&selector).await? else {
952 return Ok(None);
953 };
954 let manifest = self.parse_task_manifest(task, &remote_manifest.bytes, true)?;
955 Ok(Some((manifest, remote_manifest.etag)))
956 }
957
958 async fn put_remote_task_manifest(
959 &self,
960 task: &str,
961 mut manifest: TaskActionManifest,
962 mut expected_etag: Option<String>,
963 ) -> Result<TaskActionManifest> {
964 let Some(remote) = &self.remote else {
965 return Ok(manifest);
966 };
967 let (_, selector) = Self::task_manifest_selector(task)?;
968 for _ in 0..4 {
969 let bytes = canonical_json(&manifest)?;
970 let outcome = {
971 let _permit = self.remote_transfers.acquire().await?;
972 remote
973 .put_action_manifest(&selector, &bytes, expected_etag.as_deref())
974 .await?
975 };
976 match outcome {
977 ManifestPutOutcome::Stored => return Ok(manifest),
978 ManifestPutOutcome::PreconditionFailed => {
979 let Some((remote_manifest, etag)) = self.get_remote_task_manifest(task).await?
980 else {
981 expected_etag = None;
982 continue;
983 };
984 manifest = merge_task_manifests(task, Some(remote_manifest), manifest)?;
985 expected_etag = Some(etag);
986 }
987 }
988 }
989 bail!("remote task action manifest changed too frequently")
990 }
991
992 pub fn stats(&self) -> AgentStats {
994 AgentStats {
995 session_duration_ns: 0,
996 lookups: self.stats.lookups.load(Ordering::Relaxed),
997 unconsulted: self.stats.unconsulted.load(Ordering::Relaxed),
998 hits: self.stats.hits.load(Ordering::Relaxed),
999 stores: self.stats.stores.load(Ordering::Relaxed),
1000 stored_bytes: self.stats.stored_bytes.load(Ordering::Relaxed),
1001 verifications: self.stats.verifications.load(Ordering::Relaxed),
1002 divergences: self.stats.divergences.load(Ordering::Relaxed),
1003 downloaded_bytes: self.stats.downloaded_bytes.load(Ordering::Relaxed),
1004 uploaded_bytes: self.stats.uploaded_bytes.load(Ordering::Relaxed),
1005 background_uploads: self.stats.background_uploads.load(Ordering::Relaxed),
1006 background_upload_failures: self
1007 .stats
1008 .background_upload_failures
1009 .load(Ordering::Relaxed),
1010 remote_blob_pack_uploads: self.stats.remote_blob_pack_uploads.load(Ordering::Relaxed),
1011 remote_blob_pack_upload_blobs: self
1012 .stats
1013 .remote_blob_pack_upload_blobs
1014 .load(Ordering::Relaxed),
1015 upload_drain_duration_ns: self.stats.upload_drain_duration_ns.load(Ordering::Relaxed),
1016 prefetched_actions: self.stats.prefetched_actions.load(Ordering::Relaxed),
1017 predictions_loaded: self.stats.predictions_loaded.load(Ordering::Relaxed),
1018 bypasses: self.stats.bypasses.lock().unwrap().clone(),
1019 avoided_compiler_duration_ns: self
1020 .stats
1021 .avoided_compiler_duration_ns
1022 .load(Ordering::Relaxed),
1023 compiler: self.stats.compiler.lock().unwrap().clone(),
1024 slow_compilations: self.stats.slow_compilations.lock().unwrap().clone(),
1025 remote_failures: self.stats.remote_failures.load(Ordering::Relaxed),
1026 remote_manifest_lookups: self.stats.remote_manifest_lookups.load(Ordering::Relaxed),
1027 remote_manifest_lookup_duration_ns: self
1028 .stats
1029 .remote_manifest_lookup_duration_ns
1030 .load(Ordering::Relaxed),
1031 remote_action_lookups: self.stats.remote_action_lookups.load(Ordering::Relaxed),
1032 remote_action_lookup_duration_ns: self
1033 .stats
1034 .remote_action_lookup_duration_ns
1035 .load(Ordering::Relaxed),
1036 remote_blob_requests: self.stats.remote_blob_requests.load(Ordering::Relaxed),
1037 remote_blob_pack_requests: self.stats.remote_blob_pack_requests.load(Ordering::Relaxed),
1038 remote_blob_pack_blobs: self.stats.remote_blob_pack_blobs.load(Ordering::Relaxed),
1039 remote_blob_transfer_duration_ns: self
1040 .stats
1041 .remote_blob_transfer_duration_ns
1042 .load(Ordering::Relaxed),
1043 local_cas_write_duration_ns: self
1044 .stats
1045 .local_cas_write_duration_ns
1046 .load(Ordering::Relaxed),
1047 prefetch_runs: self.stats.prefetch_runs.load(Ordering::Relaxed),
1048 prefetch_duration_ns: self.stats.prefetch_duration_ns.load(Ordering::Relaxed),
1049 materialization_duration_ns: self
1050 .stats
1051 .materialization_duration_ns
1052 .load(Ordering::Relaxed),
1053 restored_output_files: self.stats.restored_output_files.load(Ordering::Relaxed),
1054 restored_output_bytes: self.stats.restored_output_bytes.load(Ordering::Relaxed),
1055 reflinked_output_files: self.stats.reflinked_output_files.load(Ordering::Relaxed),
1056 reflinked_output_bytes: self.stats.reflinked_output_bytes.load(Ordering::Relaxed),
1057 copied_output_files: self.stats.copied_output_files.load(Ordering::Relaxed),
1058 copied_output_bytes: self.stats.copied_output_bytes.load(Ordering::Relaxed),
1059 reused_output_files: self.stats.reused_output_files.load(Ordering::Relaxed),
1060 reused_output_bytes: self.stats.reused_output_bytes.load(Ordering::Relaxed),
1061 }
1062 }
1063
1064 pub async fn handle_requests(
1071 &self,
1072 requests: impl IntoIterator<Item = AgentRequest>,
1073 ) -> Vec<AgentResponse> {
1074 let mut connection = ConnectionUploads::default();
1075 let mut responses = Vec::new();
1076 for request in requests {
1077 responses.push(self.respond_on(request, &mut connection).await);
1078 }
1079 responses
1080 }
1081
1082 fn write_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1083 Self::digest_lock(&self.write_locks, digest)
1084 }
1085
1086 fn action_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1087 Self::digest_lock(&self.action_locks, digest)
1088 }
1089
1090 fn digest_lock(
1091 locks: &Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>,
1092 digest: &CacheDigest,
1093 ) -> Arc<tokio::sync::Mutex<()>> {
1094 let mut locks = locks.lock().unwrap();
1095 locks.retain(|_, lock| lock.strong_count() > 0);
1096 if let Some(lock) = locks.get(digest).and_then(Weak::upgrade) {
1097 return lock;
1098 }
1099 let lock = Arc::new(tokio::sync::Mutex::new(()));
1100 locks.insert(digest.clone(), Arc::downgrade(&lock));
1101 lock
1102 }
1103
1104 #[cfg(test)]
1109 async fn respond(&self, request: AgentRequest) -> AgentResponse {
1110 self.respond_on(request, &mut ConnectionUploads::default())
1111 .await
1112 }
1113
1114 async fn respond_on(
1115 &self,
1116 request: AgentRequest,
1117 connection: &mut ConnectionUploads,
1118 ) -> AgentResponse {
1119 let result = match request {
1120 AgentRequest::BeginTask { task } => self
1121 .begin_task(&task)
1122 .await
1123 .map(|run| AgentResponse::TaskBegun { run }),
1124 AgentRequest::CommitTask { run } => self
1125 .commit_task(&run)
1126 .await
1127 .map(|()| AgentResponse::TaskCommitted),
1128 AgentRequest::FindBlob { digest } => self.find_blob(&digest).await,
1129 AgentRequest::FindBlobs { digests } => self.find_blobs(digests).await,
1130 AgentRequest::StoreBlob { digest, source } => {
1131 self.store_blob(&digest, &source, connection).await
1132 }
1133 AgentRequest::FindActionResult { action } => {
1134 self.stats.lookups.fetch_add(1, Ordering::Relaxed);
1135 self.find_action_result(&action).await
1136 }
1137 AgentRequest::RecordActionHit {
1138 action,
1139 restore,
1140 crate_name,
1141 } => {
1142 let diagnostic = connection
1143 .take_action_diagnostic("hit", crate_name.as_deref())
1144 .map(|diagnostic| AgentEvent::ActionDiagnostic {
1145 outcome: "hit".into(),
1146 crate_name: crate_name.clone(),
1147 diagnostic,
1148 });
1149 self.record_action_hit(&action, restore, crate_name, diagnostic)
1150 }
1151 AgentRequest::RecordBypass { kind } => {
1152 *self
1153 .stats
1154 .bypasses
1155 .lock()
1156 .unwrap()
1157 .entry(kind.clone())
1158 .or_insert(0) += 1;
1159 self.emit(|| AgentEvent::Bypass { kind });
1160 Ok(AgentResponse::BypassRecorded)
1161 }
1162 AgentRequest::RecordUnconsulted => {
1163 self.stats.unconsulted.fetch_add(1, Ordering::Relaxed);
1164 self.emit(|| AgentEvent::Unconsulted);
1165 Ok(AgentResponse::UnconsultedRecorded)
1166 }
1167 AgentRequest::RecordWarning { message } => match parse_action_diagnostic(&message) {
1168 Some(Ok((outcome, crate_name, diagnostic))) => {
1169 connection.record_action_diagnostic(outcome, crate_name, diagnostic);
1170 Ok(AgentResponse::WarningRecorded)
1171 }
1172 Some(Err(error)) => Err(error),
1173 None => self.record_warning(message),
1174 },
1175 AgentRequest::FindFileDigests { scope, files } => self.find_file_digests(scope, files),
1176 AgentRequest::JoinActionPromise {
1177 adapter,
1178 invocation,
1179 } => self.join_action_promise(&adapter, &invocation).await,
1180 AgentRequest::CompleteActionPromise { claim, prediction } => {
1181 self.complete_action_promise(&claim, &prediction).await
1182 }
1183 AgentRequest::RecordFileDigests { scope, entries } => {
1184 self.record_file_digests(scope, entries)
1185 }
1186 AgentRequest::RecordCompilerInvocation {
1187 outcome,
1188 crate_name,
1189 duration_ns,
1190 } => {
1191 let diagnostic = connection
1192 .take_action_diagnostic(&outcome, crate_name.as_deref())
1193 .map(|diagnostic| AgentEvent::ActionDiagnostic {
1194 outcome: outcome.clone(),
1195 crate_name: crate_name.clone(),
1196 diagnostic,
1197 });
1198 self.record_compiler_invocation(
1199 &outcome,
1200 crate_name.as_deref(),
1201 duration_ns,
1202 diagnostic,
1203 )
1204 }
1205 AgentRequest::RecordActionVerification { matched, restore } => {
1206 self.record_materialization(restore);
1207 self.stats.verifications.fetch_add(1, Ordering::Relaxed);
1208 if !matched {
1209 self.stats.divergences.fetch_add(1, Ordering::Relaxed);
1210 }
1211 self.emit(|| AgentEvent::Verification { matched, restore });
1212 Ok(AgentResponse::ActionVerificationRecorded)
1213 }
1214 AgentRequest::StoreActionResult { result } => {
1215 self.store_action_result(&result, connection).await
1216 }
1217 AgentRequest::FindActionPrediction { task, invocation } => {
1218 self.find_action_prediction(&task, &invocation)
1219 }
1220 AgentRequest::RecordActionPrediction { task, prediction } => {
1221 self.record_action_prediction(&task, prediction)
1222 }
1223 AgentRequest::FindExecutableIdentity {
1224 executable,
1225 environment,
1226 } => self.find_executable_identity(executable, environment),
1227 AgentRequest::StoreExecutableIdentity {
1228 executable,
1229 environment,
1230 stdout,
1231 } => self.store_executable_identity(executable, environment, stdout),
1232 AgentRequest::Hello { .. } => {
1233 Err(eyre::eyre!("hello is only valid as the first request"))
1234 }
1235 };
1236 result.unwrap_or_else(|error| AgentResponse::Error {
1237 message: error.to_string(),
1238 })
1239 }
1240
1241 async fn find_blob(&self, digest: &CacheDigest) -> Result<AgentResponse> {
1242 if let Some(path) = self.find_verified_blob(digest)? {
1243 return Ok(AgentResponse::Blob { path: Some(path) });
1244 }
1245 if !self.remote_mode.reads() {
1246 return Ok(AgentResponse::Blob { path: None });
1247 }
1248 let Some(remote) = &self.remote else {
1249 return Ok(AgentResponse::Blob { path: None });
1250 };
1251 match self.fetch_remote_blob(remote, digest).await {
1252 Ok(path) => Ok(AgentResponse::Blob { path: Some(path) }),
1253 Err(error) => {
1254 warn!(
1255 "remote cache blob lookup failed for {}: {error}",
1256 digest.hash
1257 );
1258 Ok(AgentResponse::Blob { path: None })
1259 }
1260 }
1261 }
1262
1263 async fn find_blobs(&self, digests: Vec<CacheDigest>) -> Result<AgentResponse> {
1264 let mut paths = BTreeMap::new();
1265 let mut missing = Vec::new();
1266 for digest in &digests {
1267 match self.find_verified_blob(digest)? {
1268 Some(path) => {
1269 paths.insert(digest.clone(), path);
1270 }
1271 None => {
1272 missing.push(digest.clone());
1273 }
1274 }
1275 }
1276
1277 if !missing.is_empty()
1278 && self.remote_mode.reads()
1279 && let Some(remote) = &self.remote
1280 {
1281 paths.extend(self.fetch_remote_blobs(remote, missing, None).await);
1282 }
1283
1284 Ok(AgentResponse::Blobs {
1285 paths: digests
1286 .into_iter()
1287 .map(|digest| paths.get(&digest).cloned())
1288 .collect(),
1289 })
1290 }
1291
1292 async fn store_blob(
1293 &self,
1294 digest: &CacheDigest,
1295 source: &Path,
1296 connection: &mut ConnectionUploads,
1297 ) -> Result<AgentResponse> {
1298 let path = {
1299 let lock = self.write_lock(digest);
1300 let _guard = lock.lock().await;
1301 if let Some(path) = self.find_verified_blob(digest)? {
1302 path
1303 } else {
1304 let path = self.cas.store_file(digest, source)?;
1305 self.remember_verified_blob(digest, &path);
1306 self.stats.stores.fetch_add(1, Ordering::Relaxed);
1307 self.stats
1308 .stored_bytes
1309 .fetch_add(digest.size, Ordering::Relaxed);
1310 path
1311 }
1312 };
1313 if let Some(uploads) = &self.uploads {
1316 uploads.queue_blob(digest, path.clone(), connection);
1317 }
1318 Ok(AgentResponse::Stored { path })
1319 }
1320
1321 fn find_verified_blob(&self, digest: &CacheDigest) -> Result<Option<PathBuf>> {
1322 let remembered = self.verified_blobs.lock().unwrap().get(digest).cloned();
1323 if let Some(remembered) = remembered {
1324 if remembered.is_unchanged() {
1335 return Ok(Some(remembered.path));
1336 }
1337 self.verified_blobs.lock().unwrap().remove(digest);
1338 }
1339 let path = self.cas.find(digest)?;
1340 if let Some(path) = &path {
1341 self.remember_verified_blob(digest, path);
1342 }
1343 Ok(path)
1344 }
1345
1346 fn remember_verified_blob(&self, digest: &CacheDigest, path: &Path) {
1347 let Some(verified) = VerifiedBlob::describe(path) else {
1348 return;
1352 };
1353 self.verified_blobs
1354 .lock()
1355 .unwrap()
1356 .insert(digest.clone(), verified);
1357 }
1358
1359 async fn find_action_result(&self, action: &CacheDigest) -> Result<AgentResponse> {
1360 if let Some(result) = self.actions.find(action)? {
1361 return Ok(AgentResponse::ActionResult {
1362 result: Some(result),
1363 });
1364 }
1365 if !self.remote_mode.reads() {
1366 return Ok(AgentResponse::ActionResult { result: None });
1367 }
1368 let Some(remote) = &self.remote else {
1369 return Ok(AgentResponse::ActionResult { result: None });
1370 };
1371 let lock = self.action_lock(action);
1372 let _guard = lock.lock().await;
1373 if let Some(result) = self.actions.find(action)? {
1374 return Ok(AgentResponse::ActionResult {
1375 result: Some(result),
1376 });
1377 }
1378 if let Some(result) = self
1379 .pending_remote_actions
1380 .lock()
1381 .unwrap()
1382 .get(action)
1383 .cloned()
1384 {
1385 return Ok(AgentResponse::ActionResult {
1386 result: Some(result),
1387 });
1388 }
1389 let _permit = self.remote_transfers.acquire().await?;
1390 match self.get_remote_action_result(remote, action).await {
1391 Ok(Some(result)) => {
1392 self.pending_remote_actions
1393 .lock()
1394 .unwrap()
1395 .insert(action.clone(), result.clone());
1396 Ok(AgentResponse::ActionResult {
1397 result: Some(result),
1398 })
1399 }
1400 Ok(None) => Ok(AgentResponse::ActionResult { result: None }),
1401 Err(error) => {
1402 self.note_remote_failure();
1403 warn!(
1404 "remote cache action lookup failed for {}: {error}",
1405 action.hash
1406 );
1407 Ok(AgentResponse::ActionResult { result: None })
1408 }
1409 }
1410 }
1411
1412 async fn store_action_result(
1413 &self,
1414 result: &RemoteActionResult,
1415 connection: &ConnectionUploads,
1416 ) -> Result<AgentResponse> {
1417 let path = self.actions.store(result)?;
1418 if let Some(uploads) = &self.uploads {
1419 uploads.queue_action_result(result, connection);
1420 }
1421 Ok(AgentResponse::ActionStored { path })
1422 }
1423
1424 async fn join_action_promise(
1425 &self,
1426 adapter: &str,
1427 invocation: &CacheDigest,
1428 ) -> Result<AgentResponse> {
1429 if !self.remote_mode.reads() || !self.remote_mode.writes() {
1430 return Ok(AgentResponse::ActionPromise {
1431 claim: None,
1432 prediction: None,
1433 });
1434 }
1435 let Some(remote) = &self.remote else {
1436 return Ok(AgentResponse::ActionPromise {
1437 claim: None,
1438 prediction: None,
1439 });
1440 };
1441 let deadline = Instant::now() + ACTION_PROMISE_WAIT;
1442 loop {
1443 let _permit = self.remote_transfers.acquire().await?;
1444 let state = remote.join_action_promise(invocation, adapter).await;
1445 drop(_permit);
1446 match state {
1447 Ok(Some(ActionPromiseState::Claimed { claim })) => {
1448 return Ok(AgentResponse::ActionPromise {
1449 claim: Some(claim),
1450 prediction: None,
1451 });
1452 }
1453 Ok(Some(ActionPromiseState::Complete { prediction })) => {
1454 return Ok(AgentResponse::ActionPromise {
1455 claim: None,
1456 prediction: Some(prediction),
1457 });
1458 }
1459 Ok(Some(ActionPromiseState::Pending { retry_after_ms }))
1460 if Instant::now() < deadline =>
1461 {
1462 tokio::time::sleep(Duration::from_millis(retry_after_ms.clamp(10, 5_000)))
1463 .await;
1464 }
1465 Ok(Some(ActionPromiseState::Pending { .. }) | None) => {
1466 return Ok(AgentResponse::ActionPromise {
1467 claim: None,
1468 prediction: None,
1469 });
1470 }
1471 Ok(Some(_)) => {
1472 return Ok(AgentResponse::ActionPromise {
1473 claim: None,
1474 prediction: None,
1475 });
1476 }
1477 Err(error) => {
1478 self.note_remote_failure();
1479 warn!(
1480 "remote cache action promise failed for {}: {error}",
1481 invocation.hash
1482 );
1483 return Ok(AgentResponse::ActionPromise {
1484 claim: None,
1485 prediction: None,
1486 });
1487 }
1488 }
1489 }
1490 }
1491
1492 async fn complete_action_promise(
1493 &self,
1494 claim: &str,
1495 prediction: &ActionPrediction,
1496 ) -> Result<AgentResponse> {
1497 prediction.validate()?;
1498 let Some(remote) = &self.remote else {
1499 return Ok(AgentResponse::ActionPromiseCompleted);
1500 };
1501 let Some(uploads) = &self.uploads else {
1502 return Ok(AgentResponse::ActionPromiseCompleted);
1503 };
1504 if uploads
1505 .wait_for_actions(std::slice::from_ref(&prediction.action))
1506 .await
1507 .contains(&prediction.action)
1508 {
1509 return Ok(AgentResponse::ActionPromiseCompleted);
1512 }
1513 let completion = ActionPromiseCompletion {
1514 claim: claim.to_string(),
1515 prediction: prediction.clone(),
1516 };
1517 let _permit = self.remote_transfers.acquire().await?;
1518 if let Err(error) = remote
1519 .complete_action_promise(&prediction.invocation, &completion)
1520 .await
1521 {
1522 self.note_remote_failure();
1523 warn!(
1524 "remote cache action promise completion failed for {}: {error}",
1525 prediction.invocation.hash
1526 );
1527 }
1528 Ok(AgentResponse::ActionPromiseCompleted)
1529 }
1530
1531 fn note_remote_failure(&self) {
1533 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
1534 }
1535
1536 async fn get_remote_action_result(
1537 &self,
1538 remote: &RemoteCacheClient,
1539 action: &CacheDigest,
1540 ) -> Result<Option<RemoteActionResult>> {
1541 self.stats
1542 .remote_action_lookups
1543 .fetch_add(1, Ordering::Relaxed);
1544 let _timer = AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);
1545 remote.get_action_result(action).await
1546 }
1547
1548 fn record_action_hit(
1549 &self,
1550 action: &CacheDigest,
1551 restore: RestoreStats,
1552 crate_name: Option<String>,
1553 diagnostic: Option<AgentEvent>,
1554 ) -> Result<AgentResponse> {
1555 validate_crate_name(crate_name.as_deref())?;
1556 if self.actions.find(action)?.is_none() {
1557 let pending = self.pending_remote_actions.lock().unwrap().remove(action);
1558 if let Some(result) = pending {
1559 self.actions.store(&result)?;
1560 } else {
1561 bail!("cannot record a hit for a missing action result");
1562 }
1563 }
1564 self.record_restore(restore);
1565 self.stats.hits.fetch_add(1, Ordering::Relaxed);
1566 self.emit_action(
1567 diagnostic,
1568 AgentEvent::ActionHit {
1569 crate_name,
1570 restore,
1571 },
1572 );
1573 Ok(AgentResponse::ActionHitRecorded)
1574 }
1575
1576 fn record_restore(&self, restore: RestoreStats) {
1577 self.record_materialization(restore);
1578 atomic_saturating_add(
1579 &self.stats.avoided_compiler_duration_ns,
1580 restore.avoided_compiler_duration_ns,
1581 );
1582 atomic_saturating_add(&self.stats.restored_output_files, restore.output_files);
1583 atomic_saturating_add(&self.stats.restored_output_bytes, restore.output_bytes);
1584 atomic_saturating_add(
1585 &self.stats.reflinked_output_files,
1586 restore.reflinked_output_files,
1587 );
1588 atomic_saturating_add(
1589 &self.stats.reflinked_output_bytes,
1590 restore.reflinked_output_bytes,
1591 );
1592 atomic_saturating_add(&self.stats.copied_output_files, restore.copied_output_files);
1593 atomic_saturating_add(&self.stats.copied_output_bytes, restore.copied_output_bytes);
1594 atomic_saturating_add(&self.stats.reused_output_files, restore.reused_output_files);
1595 atomic_saturating_add(&self.stats.reused_output_bytes, restore.reused_output_bytes);
1596 }
1597
1598 fn record_compiler_invocation(
1599 &self,
1600 outcome: &str,
1601 crate_name: Option<&str>,
1602 duration_ns: u64,
1603 diagnostic: Option<AgentEvent>,
1604 ) -> Result<AgentResponse> {
1605 if !matches!(
1606 outcome,
1607 "miss" | "unconsulted" | "bypass" | "verification" | "incremental"
1608 ) {
1609 bail!("invalid compiler invocation outcome");
1610 }
1611 validate_crate_name(crate_name)?;
1612 let mut compiler = self.stats.compiler.lock().unwrap();
1613 let stats = compiler.entry(outcome.to_string()).or_default();
1614 stats.invocations = stats.invocations.saturating_add(1);
1615 stats.duration_ns = stats.duration_ns.saturating_add(duration_ns);
1616 drop(compiler);
1617 if outcome != "verification"
1618 && let Some(crate_name) = crate_name.filter(|name| !name.is_empty())
1619 {
1620 let mut slow = self.stats.slow_compilations.lock().unwrap();
1621 let duration = slow.entry(crate_name.to_string()).or_default();
1622 *duration = duration.saturating_add(duration_ns);
1623 }
1624 self.emit_action(
1625 diagnostic,
1626 AgentEvent::CompilerInvocation {
1627 outcome: outcome.to_string(),
1628 crate_name: crate_name.map(str::to_string),
1629 duration_ns,
1630 },
1631 );
1632 Ok(AgentResponse::CompilerInvocationRecorded)
1633 }
1634
1635 fn record_materialization(&self, restore: RestoreStats) {
1636 atomic_saturating_add(&self.stats.materialization_duration_ns, restore.duration_ns);
1637 }
1638
1639 fn record_warning(&self, message: String) -> Result<AgentResponse> {
1648 if message.is_empty()
1649 || message.len() > MAX_WARNING_BYTES
1650 || message.contains(['\n', '\r', '\0'])
1651 {
1652 bail!("invalid shim warning");
1653 }
1654 let mut warnings = self.warnings.lock().unwrap();
1655 if !warnings.contains(&message) && warnings.len() < MAX_WARNINGS {
1656 eprintln!("mbx[warning]: {message}");
1657 self.emit(|| AgentEvent::Warning {
1658 message: message.clone(),
1659 });
1660 warnings.insert(message);
1661 }
1662 Ok(AgentResponse::WarningRecorded)
1663 }
1664
1665 fn find_file_digests(
1672 &self,
1673 scope: FileDigestScope,
1674 files: Vec<FileIdentity>,
1675 ) -> Result<AgentResponse> {
1676 if files.len() > MAX_FILE_DIGEST_BATCH {
1677 bail!("too many file-digest lookups in one request");
1678 }
1679 let ledger = self.file_digests.lock().unwrap();
1680 let digests = files
1681 .into_iter()
1682 .map(|file| {
1683 let recorded = ledger.get(&(scope, file.path.clone()))?;
1684 (recorded.file == file).then(|| recorded.digest.clone())
1685 })
1686 .collect();
1687 Ok(AgentResponse::FileDigests { digests })
1688 }
1689
1690 fn record_file_digests(
1696 &self,
1697 scope: FileDigestScope,
1698 entries: Vec<RecordedFileDigest>,
1699 ) -> Result<AgentResponse> {
1700 if entries.len() > MAX_FILE_DIGEST_BATCH {
1701 bail!("too many file-digest records in one request");
1702 }
1703 for entry in &entries {
1704 validate_file_digest_record(entry)?;
1705 }
1706 let mut ledger = self.file_digests.lock().unwrap();
1707 for entry in entries {
1708 if ledger.len() >= MAX_FILE_DIGEST_ENTRIES
1709 && !ledger.contains_key(&(scope, entry.file.path.clone()))
1710 {
1711 break;
1712 }
1713 ledger.insert((scope, entry.file.path.clone()), entry);
1714 }
1715 Ok(AgentResponse::FileDigestsRecorded)
1716 }
1717
1718 pub fn seed_file_digests(&self, entries: Vec<(FileDigestScope, RecordedFileDigest)>) -> usize {
1727 let mut ledger = self.file_digests.lock().unwrap();
1728 let mut seeded = 0;
1729 for (scope, entry) in entries {
1730 if ledger.len() >= MAX_FILE_DIGEST_ENTRIES {
1731 break;
1732 }
1733 if validate_file_digest_record(&entry).is_err() {
1734 continue;
1735 }
1736 if let std::collections::btree_map::Entry::Vacant(slot) =
1737 ledger.entry((scope, entry.file.path.clone()))
1738 {
1739 slot.insert(entry);
1740 seeded += 1;
1741 }
1742 }
1743 seeded
1744 }
1745
1746 pub fn file_digests(&self) -> Vec<(FileDigestScope, RecordedFileDigest)> {
1749 self.file_digests
1750 .lock()
1751 .unwrap()
1752 .iter()
1753 .map(|((scope, _), entry)| (*scope, entry.clone()))
1754 .collect()
1755 }
1756
1757 fn find_action_prediction(
1758 &self,
1759 task: &str,
1760 invocation: &CacheDigest,
1761 ) -> Result<AgentResponse> {
1762 validate_task_identity(task)?;
1763 invocation.validate()?;
1764 let prediction = self
1765 .task_actions
1766 .lock()
1767 .unwrap()
1768 .get(task)
1769 .and_then(|state| state.predictions.get(invocation))
1770 .cloned();
1771 Ok(AgentResponse::ActionPrediction { prediction })
1772 }
1773
1774 fn record_action_prediction(
1775 &self,
1776 task: &str,
1777 prediction: ActionPrediction,
1778 ) -> Result<AgentResponse> {
1779 validate_task_identity(task)?;
1780 prediction.validate()?;
1781 let mut tasks = self.task_actions.lock().unwrap();
1782 let state = tasks.entry(task.to_string()).or_default();
1783 if !state.predictions.contains_key(&prediction.invocation)
1784 && state.predictions.len() >= MAX_TASK_ACTION_PREDICTIONS
1785 {
1786 bail!("task action manifest contains too many predictions");
1787 }
1788 state
1789 .predictions
1790 .insert(prediction.invocation.clone(), prediction.clone());
1791 state
1792 .pending_predictions
1793 .insert(prediction.invocation.clone(), prediction);
1794 Ok(AgentResponse::ActionPredictionRecorded)
1795 }
1796
1797 fn executable_identity_key(
1798 &self,
1799 executable: PathBuf,
1800 environment: BTreeMap<String, Option<String>>,
1801 ) -> Result<ExecutableIdentityKey> {
1802 if !environment.keys().all(|name| {
1807 matches!(
1808 name.as_str(),
1809 "MBX_FUSE_LD"
1810 | "RUSTUP_HOME"
1811 | "RUSTUP_TOOLCHAIN"
1812 | "SDKROOT"
1813 | "MACOSX_DEPLOYMENT_TARGET"
1814 | "LIB"
1815 | "UCRTVersion"
1816 | "UniversalCRTSdkDir"
1817 | "VCToolsInstallDir"
1818 | "VCToolsVersion"
1819 | "WindowsSdkDir"
1820 | "WindowsSDKVersion"
1821 )
1822 }) {
1823 bail!("executable identity contains an unsupported environment variable");
1824 }
1825 Ok(ExecutableIdentityKey {
1826 executable,
1827 environment,
1828 })
1829 }
1830
1831 fn find_executable_identity(
1832 &self,
1833 executable: PathBuf,
1834 environment: BTreeMap<String, Option<String>>,
1835 ) -> Result<AgentResponse> {
1836 let key = self.executable_identity_key(executable, environment)?;
1837 let stdout = self
1838 .executable_identities
1839 .lock()
1840 .unwrap()
1841 .get(&key)
1842 .cloned();
1843 Ok(AgentResponse::ExecutableIdentity { stdout })
1844 }
1845
1846 fn store_executable_identity(
1847 &self,
1848 executable: PathBuf,
1849 environment: BTreeMap<String, Option<String>>,
1850 stdout: Vec<u8>,
1851 ) -> Result<AgentResponse> {
1852 if stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE {
1853 bail!("executable identity exceeds {MAX_EXECUTABLE_IDENTITY_SIZE} bytes");
1854 }
1855 let key = self.executable_identity_key(executable, environment)?;
1856 let mut identities = self.executable_identities.lock().unwrap();
1857 let is_new = !identities.contains_key(&key);
1858 let previous_size = identities.get(&key).map_or(0, Vec::len);
1859 if is_new && identities.len() >= MAX_EXECUTABLE_IDENTITIES {
1860 bail!("executable identity cache contains too many entries");
1861 }
1862 let retained_bytes = identities.values().map(Vec::len).sum::<usize>();
1863 if retained_bytes - previous_size + stdout.len() > MAX_EXECUTABLE_IDENTITY_BYTES {
1864 bail!("executable identity cache contains too many bytes");
1865 }
1866 identities.insert(key, stdout.clone());
1867 Ok(AgentResponse::ExecutableIdentity {
1868 stdout: Some(stdout),
1869 })
1870 }
1871
1872 pub async fn handle_connection<S>(&self, stream: S) -> Result<()>
1874 where
1875 S: AsyncRead + AsyncWrite + Unpin,
1876 {
1877 let (reader, mut writer) = tokio::io::split(stream);
1878 let mut reader = BufReader::new(reader);
1879 let hello = read_request(&mut reader)
1880 .await?
1881 .ok_or_else(|| eyre::eyre!("connection closed before the agent handshake"))?;
1882 let request: AgentRequest = serde_json::from_str(&hello)?;
1883 match request {
1884 AgentRequest::Hello {
1885 protocol,
1886 client_version,
1887 } if protocol == AGENT_PROTOCOL_VERSION && client_version == self.version.as_ref() => {}
1888 AgentRequest::Hello { protocol, .. } if protocol != AGENT_PROTOCOL_VERSION => {
1889 send_response(
1890 &mut writer,
1891 &AgentResponse::Error {
1892 message: format!(
1893 "unsupported agent protocol {protocol}; expected {AGENT_PROTOCOL_VERSION}"
1894 ),
1895 },
1896 )
1897 .await?;
1898 return Ok(());
1899 }
1900 AgentRequest::Hello { client_version, .. } => {
1901 send_response(
1902 &mut writer,
1903 &AgentResponse::Error {
1904 message: format!(
1905 "cache client {client_version} does not match agent {}",
1906 self.version
1907 ),
1908 },
1909 )
1910 .await?;
1911 return Ok(());
1912 }
1913 _ => bail!("the first agent request must be hello"),
1914 }
1915 send_response(
1916 &mut writer,
1917 &AgentResponse::Hello {
1918 protocol: AGENT_PROTOCOL_VERSION,
1919 agent_version: self.version.to_string(),
1920 },
1921 )
1922 .await?;
1923
1924 let mut connection = ConnectionUploads::default();
1928 while let Some(line) = read_request(&mut reader).await? {
1929 let response = match serde_json::from_str(&line) {
1930 Ok(request) => self.respond_on(request, &mut connection).await,
1931 Err(error) => AgentResponse::Error {
1932 message: format!("invalid agent request: {error}"),
1933 },
1934 };
1935 send_response(&mut writer, &response).await?;
1936 }
1937 Ok(())
1938 }
1939}
1940
1941async fn read_request<R>(reader: &mut R) -> Result<Option<String>>
1947where
1948 R: AsyncBufRead + Unpin,
1949{
1950 let mut line = Vec::new();
1951 loop {
1952 let available = reader.fill_buf().await?;
1953 if available.is_empty() {
1954 break;
1955 }
1956 let (consumed, complete) = match available.iter().position(|byte| *byte == b'\n') {
1957 Some(index) => (index, true),
1958 None => (available.len(), false),
1959 };
1960 if line.len() + consumed > MAX_REQUEST_BYTES {
1961 bail!("agent request exceeded {MAX_REQUEST_BYTES} bytes");
1962 }
1963 line.extend_from_slice(&available[..consumed]);
1964 reader.consume(consumed + usize::from(complete));
1966 if complete {
1967 return Ok(Some(String::from_utf8(line)?));
1968 }
1969 }
1970 if line.is_empty() {
1971 Ok(None)
1972 } else {
1973 Ok(Some(String::from_utf8(line)?))
1974 }
1975}
1976
1977async fn send_response(
1978 writer: &mut (impl AsyncWrite + Unpin),
1979 response: &AgentResponse,
1980) -> Result<()> {
1981 let mut encoded = serde_json::to_vec(response)?;
1982 encoded.push(b'\n');
1983 writer.write_all(&encoded).await?;
1984 writer.flush().await?;
1985 Ok(())
1986}
1987
1988#[cfg(test)]
1989#[path = "agent_tests.rs"]
1990mod tests;