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