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