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