1use crate::uploads::{ConnectionUploads, UploadQueue, UploadSink};
2use crate::{
3 ActionPrediction, BlobPackLimits, CacheDigest, CacheDirectory, LocalActionCache, LocalCas,
4 MAX_STAGED_BLOB_PACK_BYTES, MAX_STAGED_BLOB_PACK_ITEMS, ManifestPutOutcome, RemoteActionResult,
5 RemoteCacheClient, RemoteCacheMode, RustcMetadata, TaskActionManifest, blob_pack_chunk,
6 canonical_json,
7};
8use eyre::{Context, Result, bail};
9use futures_util::{FutureExt, StreamExt, future::BoxFuture, stream};
10use log::warn;
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, BTreeSet};
13use std::fs;
14use std::path::{Path, PathBuf};
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::{Arc, Mutex, Weak};
17use std::time::{Duration, Instant};
18use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
19
20const MAX_EXECUTABLE_IDENTITIES: usize = 64;
21const MAX_EXECUTABLE_IDENTITY_SIZE: usize = 64 * 1024;
22const MAX_EXECUTABLE_IDENTITY_BYTES: usize = 256 * 1024;
23const TASK_ACTION_MANIFEST_VERSION: u8 = 1;
24const MAX_TASK_ACTION_PREDICTIONS: usize = 16 * 1024;
25const MAX_REMOTE_TRANSFERS: usize = 64;
26const MAX_PREFETCH_TRANSFERS: usize = 48;
27const MAX_PREFETCH_ACTION_BATCH: usize = 256;
28const MAX_PREFETCH_BATCH_LOOKUPS: usize = 4;
31const PREFETCH_ACTION_BATCH_DELAY: Duration = Duration::from_millis(5);
32const MAX_PREFETCH_DIRECTORY_OBJECTS: usize = 100_000;
33const MAX_PREFETCH_OBJECTS_PER_WAVE: usize = 100_000;
34const DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES: u64 = 5 * 1024 * 1024 * 1024;
35
36pub struct AgentRemoteCache {
38 pub client: RemoteCacheClient,
40 pub mode: RemoteCacheMode,
42 pub staging_dir: PathBuf,
44}
45
46pub const AGENT_PROTOCOL_VERSION: u8 = 3;
48const MAX_REQUEST_BYTES: usize = 16 * 1024 * 1024;
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(tag = "type", rename_all = "snake_case")]
57pub enum AgentRequest {
58 Hello {
60 protocol: u8,
62 client_version: String,
64 },
65 BeginTask {
67 task: String,
69 },
70 CommitTask {
72 run: String,
74 },
75 FindBlob {
77 digest: CacheDigest,
79 },
80 FindBlobs {
82 digests: Vec<CacheDigest>,
84 },
85 StoreBlob {
87 digest: CacheDigest,
89 source: PathBuf,
91 },
92 FindActionResult {
94 action: CacheDigest,
96 },
97 RecordActionHit {
99 action: CacheDigest,
101 restore: RestoreStats,
103 #[serde(default)]
105 crate_name: Option<String>,
106 },
107 RecordBypass {
109 kind: String,
111 },
112 RecordUnconsulted,
115 RecordCompilerInvocation {
117 outcome: String,
121 crate_name: Option<String>,
123 duration_ns: u64,
125 },
126 RecordActionVerification {
128 matched: bool,
130 restore: RestoreStats,
132 },
133 StoreActionResult {
135 result: RemoteActionResult,
137 },
138 FindActionPrediction {
140 task: String,
142 invocation: CacheDigest,
144 },
145 RecordActionPrediction {
147 task: String,
149 prediction: ActionPrediction,
151 },
152 FindExecutableIdentity {
154 executable: PathBuf,
156 environment: BTreeMap<String, Option<String>>,
158 },
159 StoreExecutableIdentity {
161 executable: PathBuf,
163 environment: BTreeMap<String, Option<String>>,
165 stdout: Vec<u8>,
167 },
168}
169
170#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(deny_unknown_fields)]
173pub struct RestoreStats {
174 pub duration_ns: u64,
176 pub avoided_compiler_duration_ns: u64,
179 pub output_files: u64,
181 pub output_bytes: u64,
183 pub reflinked_output_files: u64,
185 pub reflinked_output_bytes: u64,
187 pub copied_output_files: u64,
189 pub copied_output_bytes: u64,
191}
192
193#[derive(Debug, Clone)]
200#[non_exhaustive]
201pub enum AgentEvent {
202 ActionHit {
204 crate_name: Option<String>,
206 restore: RestoreStats,
208 },
209 Bypass {
211 kind: String,
213 },
214 Unconsulted,
216 CompilerInvocation {
218 outcome: String,
220 crate_name: Option<String>,
222 duration_ns: u64,
224 },
225 Verification {
227 matched: bool,
229 restore: RestoreStats,
231 },
232}
233
234pub trait AgentEventObserver: Send + Sync {
236 fn event(&self, event: AgentEvent);
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242#[serde(tag = "type", rename_all = "snake_case")]
243pub enum AgentResponse {
244 Hello {
246 protocol: u8,
248 agent_version: String,
250 },
251 TaskBegun {
253 run: String,
255 },
256 TaskCommitted,
258 Blob {
260 path: Option<PathBuf>,
262 },
263 Blobs {
265 paths: Vec<Option<PathBuf>>,
267 },
268 Stored {
270 path: PathBuf,
272 },
273 ActionResult {
275 result: Option<RemoteActionResult>,
277 },
278 ActionHitRecorded,
280 ActionVerificationRecorded,
282 BypassRecorded,
284 UnconsultedRecorded,
286 CompilerInvocationRecorded,
288 ActionStored {
290 path: PathBuf,
292 },
293 ActionPrediction {
295 prediction: Option<ActionPrediction>,
297 },
298 ActionPredictionRecorded,
300 ExecutableIdentity {
302 stdout: Option<Vec<u8>>,
304 },
305 Error {
307 message: String,
309 },
310}
311
312#[derive(Debug, Clone, Default, PartialEq, Eq)]
319#[non_exhaustive]
320pub struct AgentStats {
321 pub session_duration_ns: u64,
323 pub lookups: u64,
325 pub unconsulted: u64,
331 pub hits: u64,
333 pub stores: u64,
335 pub stored_bytes: u64,
337 pub verifications: u64,
339 pub divergences: u64,
341 pub downloaded_bytes: u64,
343 pub uploaded_bytes: u64,
345 pub background_uploads: u64,
351 pub background_upload_failures: u64,
354 pub remote_blob_pack_uploads: u64,
356 pub remote_blob_pack_upload_blobs: u64,
358 pub upload_drain_duration_ns: u64,
360 pub prefetched_actions: u64,
362 pub bypasses: BTreeMap<String, u64>,
364 pub avoided_compiler_duration_ns: u64,
366 pub compiler: BTreeMap<String, CompilerStats>,
368 pub slow_compilations: BTreeMap<String, u64>,
370 pub remote_failures: u64,
378 pub remote_manifest_lookups: u64,
380 pub remote_manifest_lookup_duration_ns: u64,
382 pub remote_action_lookups: u64,
384 pub remote_action_lookup_duration_ns: u64,
386 pub remote_blob_requests: u64,
388 pub remote_blob_pack_requests: u64,
390 pub remote_blob_pack_blobs: u64,
392 pub remote_blob_transfer_duration_ns: u64,
394 pub local_cas_write_duration_ns: u64,
396 pub prefetch_runs: u64,
398 pub prefetch_duration_ns: u64,
400 pub materialization_duration_ns: u64,
402 pub restored_output_files: u64,
404 pub restored_output_bytes: u64,
406 pub reflinked_output_files: u64,
408 pub reflinked_output_bytes: u64,
410 pub copied_output_files: u64,
412 pub copied_output_bytes: u64,
414}
415
416#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
422#[non_exhaustive]
423pub struct CompilerStats {
424 pub invocations: u64,
426 pub duration_ns: u64,
428}
429
430impl CompilerStats {
431 pub fn new(invocations: u64, duration_ns: u64) -> Self {
433 Self {
434 invocations,
435 duration_ns,
436 }
437 }
438}
439
440#[derive(Default)]
441struct AtomicAgentStats {
442 lookups: AtomicU64,
443 unconsulted: AtomicU64,
444 hits: AtomicU64,
445 stores: AtomicU64,
446 stored_bytes: AtomicU64,
447 verifications: AtomicU64,
448 divergences: AtomicU64,
449 downloaded_bytes: AtomicU64,
450 uploaded_bytes: AtomicU64,
451 background_uploads: AtomicU64,
452 background_upload_failures: AtomicU64,
453 remote_blob_pack_uploads: AtomicU64,
454 remote_blob_pack_upload_blobs: AtomicU64,
455 upload_drain_duration_ns: AtomicU64,
456 prefetched_actions: AtomicU64,
457 remote_failures: AtomicU64,
458 remote_manifest_lookups: AtomicU64,
459 remote_manifest_lookup_duration_ns: AtomicU64,
460 remote_action_lookups: AtomicU64,
461 remote_action_lookup_duration_ns: AtomicU64,
462 remote_blob_requests: AtomicU64,
463 remote_blob_pack_requests: AtomicU64,
464 remote_blob_pack_blobs: AtomicU64,
465 remote_blob_transfer_duration_ns: AtomicU64,
466 local_cas_write_duration_ns: AtomicU64,
467 prefetch_runs: AtomicU64,
468 prefetch_duration_ns: AtomicU64,
469 materialization_duration_ns: AtomicU64,
470 bypasses: Mutex<BTreeMap<String, u64>>,
471 avoided_compiler_duration_ns: AtomicU64,
472 compiler: Mutex<BTreeMap<String, CompilerStats>>,
473 slow_compilations: Mutex<BTreeMap<String, u64>>,
474 restored_output_files: AtomicU64,
475 restored_output_bytes: AtomicU64,
476 reflinked_output_files: AtomicU64,
477 reflinked_output_bytes: AtomicU64,
478 copied_output_files: AtomicU64,
479 copied_output_bytes: AtomicU64,
480}
481
482struct AtomicDurationTimer<'a> {
483 started: Instant,
484 target: &'a AtomicU64,
485}
486
487impl<'a> AtomicDurationTimer<'a> {
488 fn start(target: &'a AtomicU64) -> Self {
489 Self {
490 started: Instant::now(),
491 target,
492 }
493 }
494}
495
496impl Drop for AtomicDurationTimer<'_> {
497 fn drop(&mut self) {
498 atomic_saturating_add(self.target, duration_ns(self.started));
499 }
500}
501
502fn duration_ns(started: Instant) -> u64 {
503 started.elapsed().as_nanos().try_into().unwrap_or(u64::MAX)
504}
505
506fn validate_crate_name(crate_name: Option<&str>) -> Result<()> {
507 if let Some(crate_name) = crate_name
508 && (crate_name.len() > 256 || crate_name.contains(['\0', '\n', '\r']))
509 {
510 bail!("invalid compiler crate name");
511 }
512 Ok(())
513}
514
515fn atomic_saturating_add(target: &AtomicU64, value: u64) {
516 let _ = target.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
517 Some(current.saturating_add(value))
518 });
519}
520
521fn queue_prefetch_digest(
522 verified: &BTreeMap<CacheDigest, PathBuf>,
523 pending: &mut BTreeMap<CacheDigest, ()>,
524 digest: CacheDigest,
525) {
526 if verified.contains_key(&digest) || pending.contains_key(&digest) {
527 return;
528 }
529 pending.insert(digest, ());
530}
531
532fn queue_prefetch_directory(
533 seen: &BTreeMap<CacheDigest, ()>,
534 pending: &mut BTreeMap<CacheDigest, ()>,
535 digest: CacheDigest,
536 limit: usize,
537) -> bool {
538 if seen.contains_key(&digest) || pending.contains_key(&digest) {
539 return true;
540 }
541 if seen.len().saturating_add(pending.len()) >= limit {
542 return false;
543 }
544 pending.insert(digest, ());
545 true
546}
547
548#[derive(Clone)]
554pub struct CacheAgent {
555 cas: LocalCas,
556 actions: LocalActionCache,
557 verified_blobs: Arc<Mutex<BTreeMap<CacheDigest, PathBuf>>>,
558 version: Arc<str>,
559 write_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
560 action_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
561 stats: Arc<AtomicAgentStats>,
562 observer: Option<Arc<dyn AgentEventObserver>>,
563 executable_identities: Arc<Mutex<BTreeMap<ExecutableIdentityKey, Vec<u8>>>>,
564 manifest_dir: Arc<PathBuf>,
565 task_actions: Arc<Mutex<BTreeMap<String, TaskActionState>>>,
566 next_task_run: Arc<AtomicU64>,
567 manifest_write_lock: Arc<Mutex<()>>,
568 remote: Option<Arc<RemoteCacheClient>>,
569 remote_mode: RemoteCacheMode,
570 remote_staging_dir: Arc<PathBuf>,
571 remote_download_limit: u64,
572 remote_download_bytes: Arc<AtomicU64>,
573 pending_remote_actions: Arc<Mutex<BTreeMap<CacheDigest, RemoteActionResult>>>,
574 remote_transfers: Arc<tokio::sync::Semaphore>,
575 prefetch_transfers: Arc<tokio::sync::Semaphore>,
576 prefetch_tasks: Arc<Mutex<Vec<tokio::task::JoinHandle<()>>>>,
577 uploads: Option<UploadQueue>,
579}
580
581struct AgentUploadSink {
583 stats: Arc<AtomicAgentStats>,
584}
585
586impl UploadSink for AgentUploadSink {
587 fn record_blob_uploaded(&self, bytes: u64) {
588 self.stats
589 .background_uploads
590 .fetch_add(1, Ordering::Relaxed);
591 self.stats
592 .uploaded_bytes
593 .fetch_add(bytes, Ordering::Relaxed);
594 }
595
596 fn record_action_uploaded(&self) {
597 self.stats
598 .background_uploads
599 .fetch_add(1, Ordering::Relaxed);
600 }
601
602 fn record_blob_pack_uploaded(&self, blobs: u64) {
603 self.stats
604 .remote_blob_pack_uploads
605 .fetch_add(1, Ordering::Relaxed);
606 self.stats
607 .remote_blob_pack_upload_blobs
608 .fetch_add(blobs, Ordering::Relaxed);
609 }
610
611 fn record_upload_failure(&self) {
612 self.stats
613 .background_upload_failures
614 .fetch_add(1, Ordering::Relaxed);
615 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
616 }
617}
618
619#[derive(Debug, Clone, Default)]
620struct TaskActionState {
621 manifest: String,
622 baseline_loaded: bool,
623 predictions: BTreeMap<CacheDigest, ActionPrediction>,
624 pending_predictions: BTreeMap<CacheDigest, ActionPrediction>,
625 remote_etag: Option<String>,
626}
627
628struct PrefetchedAction {
629 adapter: String,
630 result: RemoteActionResult,
631}
632
633struct RemoteDownloadReservation {
634 counter: Arc<AtomicU64>,
635 reserved: u64,
636 committed: bool,
637}
638
639impl RemoteDownloadReservation {
640 fn bytes(&self) -> u64 {
641 self.reserved
642 }
643
644 fn commit(mut self, bytes: u64) {
645 debug_assert!(bytes <= self.reserved);
646 self.counter
647 .fetch_sub(self.reserved.saturating_sub(bytes), Ordering::AcqRel);
648 self.committed = true;
649 }
650}
651
652impl Drop for RemoteDownloadReservation {
653 fn drop(&mut self) {
654 if !self.committed {
655 self.counter.fetch_sub(self.reserved, Ordering::AcqRel);
656 }
657 }
658}
659
660#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
661struct ExecutableIdentityKey {
662 executable: PathBuf,
663 environment: BTreeMap<String, Option<String>>,
664}
665
666impl CacheAgent {
667 pub fn new(cache_dir: impl Into<PathBuf>, version: impl Into<Arc<str>>) -> Self {
669 Self::build(cache_dir.into(), version.into(), None, 0)
670 }
671
672 pub fn new_remote(
674 cache_dir: impl Into<PathBuf>,
675 version: impl Into<Arc<str>>,
676 remote: AgentRemoteCache,
677 ) -> Self {
678 Self::build(
679 cache_dir.into(),
680 version.into(),
681 Some(remote),
682 DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES,
683 )
684 }
685
686 pub fn new_remote_with_download_limit(
688 cache_dir: impl Into<PathBuf>,
689 version: impl Into<Arc<str>>,
690 remote: AgentRemoteCache,
691 max_remote_download_bytes: u64,
692 ) -> Self {
693 Self::build(
694 cache_dir.into(),
695 version.into(),
696 Some(remote),
697 max_remote_download_bytes,
698 )
699 }
700
701 fn build(
702 cache_dir: PathBuf,
703 version: Arc<str>,
704 remote: Option<AgentRemoteCache>,
705 remote_download_limit: u64,
706 ) -> Self {
707 let remote_mode = remote
708 .as_ref()
709 .map_or(RemoteCacheMode::ReadOnly, |remote| remote.mode);
710 let remote_staging_dir = remote.as_ref().map_or_else(
711 || cache_dir.join("remote"),
712 |remote| remote.staging_dir.clone(),
713 );
714 let remote = remote.map(|remote| Arc::new(remote.client));
715 let stats = Arc::new(AtomicAgentStats::default());
716 let remote_transfers = Arc::new(tokio::sync::Semaphore::new(MAX_REMOTE_TRANSFERS));
717 let uploads = remote
720 .clone()
721 .filter(|_| remote_mode.writes())
722 .map(|client| {
723 UploadQueue::new(
724 client,
725 Arc::new(AgentUploadSink {
726 stats: stats.clone(),
727 }),
728 remote_transfers.clone(),
729 )
730 });
731 Self {
732 cas: LocalCas::new(cache_dir.clone()),
733 actions: LocalActionCache::new(cache_dir.clone()),
734 verified_blobs: Arc::new(Mutex::new(BTreeMap::new())),
735 version,
736 write_locks: Arc::new(Mutex::new(BTreeMap::new())),
737 action_locks: Arc::new(Mutex::new(BTreeMap::new())),
738 stats,
739 observer: None,
740 executable_identities: Arc::new(Mutex::new(BTreeMap::new())),
741 manifest_dir: Arc::new(task_manifest_dir(&cache_dir)),
742 task_actions: Arc::new(Mutex::new(BTreeMap::new())),
743 next_task_run: Arc::new(AtomicU64::new(0)),
744 manifest_write_lock: Arc::new(Mutex::new(())),
745 remote,
746 remote_mode,
747 remote_staging_dir: Arc::new(remote_staging_dir),
748 remote_download_limit,
749 remote_download_bytes: Arc::new(AtomicU64::new(0)),
750 pending_remote_actions: Arc::new(Mutex::new(BTreeMap::new())),
751 remote_transfers,
752 prefetch_transfers: Arc::new(tokio::sync::Semaphore::new(MAX_PREFETCH_TRANSFERS)),
753 prefetch_tasks: Arc::new(Mutex::new(Vec::new())),
754 uploads,
755 }
756 }
757
758 #[must_use]
760 pub fn with_observer(mut self, observer: Arc<dyn AgentEventObserver>) -> Self {
761 self.observer = Some(observer);
762 self
763 }
764
765 fn emit(&self, event: impl FnOnce() -> AgentEvent) {
766 if let Some(observer) = &self.observer {
767 observer.event(event());
768 }
769 }
770
771 fn reserve_remote_download(&self, bytes: u64) -> Result<RemoteDownloadReservation> {
772 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
773 loop {
774 let next = current
775 .checked_add(bytes)
776 .ok_or_else(|| eyre::eyre!("remote cache download budget overflowed"))?;
777 if next > self.remote_download_limit {
778 bail!(
779 "remote cache download budget exceeded: {} bytes requested with {} of {} bytes already used",
780 bytes,
781 current,
782 self.remote_download_limit
783 );
784 }
785 match self.remote_download_bytes.compare_exchange_weak(
786 current,
787 next,
788 Ordering::AcqRel,
789 Ordering::Acquire,
790 ) {
791 Ok(_) => {
792 return Ok(RemoteDownloadReservation {
793 counter: self.remote_download_bytes.clone(),
794 reserved: bytes,
795 committed: false,
796 });
797 }
798 Err(observed) => current = observed,
799 }
800 }
801 }
802
803 fn reserve_remote_download_up_to(&self, requested: u64) -> Result<RemoteDownloadReservation> {
804 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
805 loop {
806 let reserved = requested.min(self.remote_download_limit.saturating_sub(current));
807 let next = current + reserved;
808 match self.remote_download_bytes.compare_exchange_weak(
809 current,
810 next,
811 Ordering::AcqRel,
812 Ordering::Acquire,
813 ) {
814 Ok(_) => {
815 return Ok(RemoteDownloadReservation {
816 counter: self.remote_download_bytes.clone(),
817 reserved,
818 committed: false,
819 });
820 }
821 Err(observed) => current = observed,
822 }
823 }
824 }
825
826 pub async fn begin_task(&self, task: &str) -> Result<String> {
828 self.begin_task_with_remote_errors(task, false).await
829 }
830
831 pub async fn prefetch_task(&self, task: &str) -> Result<String> {
833 let run = self.begin_task_with_remote_errors(task, true).await?;
834 self.wait_for_prefetches().await;
835 Ok(run)
836 }
837
838 async fn begin_task_with_remote_errors(&self, task: &str, strict: bool) -> Result<String> {
839 validate_task_identity(task)?;
840 let (remote_manifest, mut remote_etag) = if self.remote_mode.reads() {
841 match self.get_remote_task_manifest(task).await {
842 Ok(Some((manifest, etag))) => (Some(manifest), Some(etag)),
843 Ok(None) => (None, None),
844 Err(error) => {
845 if strict {
846 return Err(error).wrap_err_with(|| {
847 format!("remote task action manifest lookup failed for {task}")
848 });
849 }
850 self.note_remote_failure();
851 warn!("remote task action manifest lookup failed for {task}: {error}");
852 (None, None)
853 }
854 }
855 } else {
856 (None, None)
857 };
858 let manifest = {
859 let _write_guard = self.manifest_write_lock.lock().unwrap();
860 let _file_guard = self.lock_task_manifest(task)?;
861 let local_manifest = self.load_task_manifest(task)?;
862 let manifest = match (remote_manifest, local_manifest) {
863 (Some(remote), Some(local)) => {
864 let (manifest, merged) = merge_remote_task_manifest(task, remote, local);
865 if !merged {
866 remote_etag = None;
867 }
868 Some(manifest)
869 }
870 (Some(remote), None) => Some(remote),
871 (None, local) => local,
872 };
873 if let Some(manifest) = &manifest {
874 self.persist_task_manifest(manifest)?;
875 }
876 manifest
877 };
878 let state = if let Some(manifest) = manifest {
879 TaskActionState {
880 manifest: task.to_string(),
881 baseline_loaded: true,
882 predictions: manifest
883 .predictions
884 .into_iter()
885 .map(|prediction| (prediction.invocation.clone(), prediction))
886 .collect(),
887 pending_predictions: BTreeMap::new(),
888 remote_etag,
889 }
890 } else {
891 TaskActionState {
892 manifest: task.to_string(),
893 baseline_loaded: true,
894 remote_etag,
895 ..TaskActionState::default()
896 }
897 };
898 let sequence = self.next_task_run.fetch_add(1, Ordering::Relaxed);
899 let run =
900 CacheDigest::blake3(format!("{task}\0{}\0{sequence}", std::process::id()).as_bytes())
901 .hash;
902 let predictions = state.predictions.values().cloned().collect();
903 self.task_actions.lock().unwrap().insert(run.clone(), state);
904 self.spawn_prefetch_predictions(predictions);
905 Ok(run)
906 }
907
908 pub async fn cancel_prefetches(&self) {
910 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
911 for task in &tasks {
912 task.abort();
913 }
914 for task in tasks {
915 if let Err(error) = task.await
916 && !error.is_cancelled()
917 {
918 warn!("remote action prefetch task failed: {error}");
919 }
920 }
921 }
922
923 pub async fn wait_for_uploads(&self) {
930 let Some(uploads) = &self.uploads else {
931 return;
932 };
933 let _timer = AtomicDurationTimer::start(&self.stats.upload_drain_duration_ns);
934 uploads.drain().await;
935 }
936
937 async fn wait_for_prefetches(&self) {
938 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
939 for task in tasks {
940 if let Err(error) = task.await {
941 warn!("remote action prefetch task failed: {error}");
942 }
943 }
944 }
945
946 pub async fn commit_task(&self, run: &str) -> Result<()> {
948 validate_task_identity(run)?;
949 let state = self
950 .task_actions
951 .lock()
952 .unwrap()
953 .get(run)
954 .cloned()
955 .ok_or_else(|| eyre::eyre!("task action manifest baseline was not loaded"))?;
956 if !state.baseline_loaded {
957 bail!("task action manifest baseline was not loaded");
958 }
959 let task = state.manifest;
960 validate_task_identity(&task)?;
961 let (manifest, introduced) = {
962 let _write_guard = self.manifest_write_lock.lock().unwrap();
963 let _file_guard = self.lock_task_manifest(&task)?;
964 let mut predictions = self
965 .load_task_manifest(&task)?
966 .map(|manifest| {
967 manifest
968 .predictions
969 .into_iter()
970 .map(|prediction| (prediction.invocation.clone(), prediction))
971 .collect::<BTreeMap<_, _>>()
972 })
973 .unwrap_or_default();
974 let introduced: BTreeSet<CacheDigest> = state
979 .pending_predictions
980 .keys()
981 .filter(|invocation| !predictions.contains_key(*invocation))
982 .cloned()
983 .collect();
984 predictions.extend(state.pending_predictions);
989 let manifest = TaskActionManifest {
990 version: TASK_ACTION_MANIFEST_VERSION,
991 task: task.clone(),
992 predictions: predictions.into_values().collect(),
993 };
994 validate_task_manifest(&manifest, &task)?;
995 self.persist_task_manifest(&manifest)?;
996 (manifest, introduced)
997 };
998 self.task_actions.lock().unwrap().remove(run);
999 if self.remote_mode.writes() {
1000 let mut manifest = manifest;
1004 if let Some(uploads) = &self.uploads {
1005 let actions: Vec<CacheDigest> = manifest
1006 .predictions
1007 .iter()
1008 .map(|prediction| prediction.action.clone())
1009 .collect();
1010 let unpublished = uploads.wait_for_actions(&actions).await;
1011 let withheld = manifest
1012 .predictions
1013 .iter()
1014 .filter(|prediction| {
1015 introduced.contains(&prediction.invocation)
1016 && unpublished.contains(&prediction.action)
1017 })
1018 .count();
1019 if withheld > 0 {
1020 warn!(
1023 "{withheld} of {} predicted actions were not published, so the remote task action manifest omits them",
1024 manifest.predictions.len()
1025 );
1026 manifest.predictions.retain(|prediction| {
1027 !(introduced.contains(&prediction.invocation)
1028 && unpublished.contains(&prediction.action))
1029 });
1030 }
1031 }
1032 match self
1033 .put_remote_task_manifest(&task, manifest, state.remote_etag)
1034 .await
1035 {
1036 Ok(remote_manifest) => {
1037 let _write_guard = self.manifest_write_lock.lock().unwrap();
1038 let reconciliation = (|| {
1039 let _file_guard = self.lock_task_manifest(&task)?;
1040 let manifest = match self.load_task_manifest(&task)? {
1041 Some(local) => {
1042 merge_remote_task_manifest(&task, remote_manifest, local).0
1043 }
1044 None => remote_manifest,
1045 };
1046 self.persist_task_manifest(&manifest)
1047 })();
1048 if let Err(error) = reconciliation {
1049 warn!(
1050 "remote task action manifest reconciliation failed for {task}: {error}"
1051 );
1052 }
1053 }
1054 Err(error) => {
1055 self.note_remote_failure();
1056 warn!("remote task action manifest upload failed for {task}: {error}");
1057 }
1058 }
1059 }
1060 Ok(())
1061 }
1062
1063 fn task_manifest_path(&self, task: &str) -> PathBuf {
1064 self.manifest_dir.join(format!("{task}.json"))
1065 }
1066
1067 fn task_manifest_lock_path(&self, task: &str) -> PathBuf {
1068 self.manifest_dir.join("locks").join(format!("{task}.lock"))
1069 }
1070
1071 fn lock_task_manifest(&self, task: &str) -> Result<fslock::LockFile> {
1072 let path = self.task_manifest_lock_path(task);
1073 fs::create_dir_all(path.parent().expect("task manifest lock has a parent"))?;
1074 let mut lock = fslock::LockFile::open(&path)?;
1075 lock.lock()?;
1076 Ok(lock)
1077 }
1078
1079 fn load_task_manifest(&self, task: &str) -> Result<Option<TaskActionManifest>> {
1080 match fs::read(self.task_manifest_path(task)) {
1081 Ok(contents) => Ok(Some(self.parse_task_manifest(task, &contents, false)?)),
1082 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
1083 Err(error) => Err(error.into()),
1084 }
1085 }
1086
1087 fn parse_task_manifest(
1088 &self,
1089 task: &str,
1090 contents: &[u8],
1091 require_canonical: bool,
1092 ) -> Result<TaskActionManifest> {
1093 let manifest: TaskActionManifest = serde_json::from_slice(contents)?;
1094 validate_task_manifest(&manifest, task)?;
1095 if require_canonical && canonical_json(&manifest)? != contents {
1096 bail!("task action manifest is not canonical JSON");
1097 }
1098 Ok(manifest)
1099 }
1100
1101 fn task_manifest_selector(task: &str) -> Result<(Vec<u8>, CacheDigest)> {
1102 TaskActionManifest::selector(task)
1103 }
1104
1105 fn persist_task_manifest(&self, manifest: &TaskActionManifest) -> Result<()> {
1106 let bytes = canonical_json(manifest)?;
1107 fs::create_dir_all(self.manifest_dir.as_path())?;
1108 let mut temporary = tempfile::NamedTempFile::new_in(self.manifest_dir.as_path())?;
1109 std::io::Write::write_all(temporary.as_file_mut(), &bytes)?;
1110 temporary.as_file_mut().sync_all()?;
1111 temporary
1112 .persist(self.task_manifest_path(&manifest.task))
1113 .map_err(|error| error.error)?;
1114 Ok(())
1115 }
1116
1117 async fn get_remote_task_manifest(
1118 &self,
1119 task: &str,
1120 ) -> Result<Option<(TaskActionManifest, String)>> {
1121 let Some(remote) = &self.remote else {
1122 return Ok(None);
1123 };
1124 let (_, selector) = Self::task_manifest_selector(task)?;
1125 let _permit = self.remote_transfers.acquire().await?;
1126 self.stats
1127 .remote_manifest_lookups
1128 .fetch_add(1, Ordering::Relaxed);
1129 let _timer = AtomicDurationTimer::start(&self.stats.remote_manifest_lookup_duration_ns);
1130 let Some(remote_manifest) = remote.get_action_manifest(&selector).await? else {
1131 return Ok(None);
1132 };
1133 let manifest = self.parse_task_manifest(task, &remote_manifest.bytes, true)?;
1134 Ok(Some((manifest, remote_manifest.etag)))
1135 }
1136
1137 async fn put_remote_task_manifest(
1138 &self,
1139 task: &str,
1140 mut manifest: TaskActionManifest,
1141 mut expected_etag: Option<String>,
1142 ) -> Result<TaskActionManifest> {
1143 let Some(remote) = &self.remote else {
1144 return Ok(manifest);
1145 };
1146 let (_, selector) = Self::task_manifest_selector(task)?;
1147 for _ in 0..4 {
1148 let bytes = canonical_json(&manifest)?;
1149 let outcome = {
1150 let _permit = self.remote_transfers.acquire().await?;
1151 remote
1152 .put_action_manifest(&selector, &bytes, expected_etag.as_deref())
1153 .await?
1154 };
1155 match outcome {
1156 ManifestPutOutcome::Stored => return Ok(manifest),
1157 ManifestPutOutcome::PreconditionFailed => {
1158 let Some((remote_manifest, etag)) = self.get_remote_task_manifest(task).await?
1159 else {
1160 expected_etag = None;
1161 continue;
1162 };
1163 manifest = merge_task_manifests(task, Some(remote_manifest), manifest)?;
1164 expected_etag = Some(etag);
1165 }
1166 }
1167 }
1168 bail!("remote task action manifest changed too frequently")
1169 }
1170
1171 pub fn stats(&self) -> AgentStats {
1173 AgentStats {
1174 session_duration_ns: 0,
1175 lookups: self.stats.lookups.load(Ordering::Relaxed),
1176 unconsulted: self.stats.unconsulted.load(Ordering::Relaxed),
1177 hits: self.stats.hits.load(Ordering::Relaxed),
1178 stores: self.stats.stores.load(Ordering::Relaxed),
1179 stored_bytes: self.stats.stored_bytes.load(Ordering::Relaxed),
1180 verifications: self.stats.verifications.load(Ordering::Relaxed),
1181 divergences: self.stats.divergences.load(Ordering::Relaxed),
1182 downloaded_bytes: self.stats.downloaded_bytes.load(Ordering::Relaxed),
1183 uploaded_bytes: self.stats.uploaded_bytes.load(Ordering::Relaxed),
1184 background_uploads: self.stats.background_uploads.load(Ordering::Relaxed),
1185 background_upload_failures: self
1186 .stats
1187 .background_upload_failures
1188 .load(Ordering::Relaxed),
1189 remote_blob_pack_uploads: self.stats.remote_blob_pack_uploads.load(Ordering::Relaxed),
1190 remote_blob_pack_upload_blobs: self
1191 .stats
1192 .remote_blob_pack_upload_blobs
1193 .load(Ordering::Relaxed),
1194 upload_drain_duration_ns: self.stats.upload_drain_duration_ns.load(Ordering::Relaxed),
1195 prefetched_actions: self.stats.prefetched_actions.load(Ordering::Relaxed),
1196 bypasses: self.stats.bypasses.lock().unwrap().clone(),
1197 avoided_compiler_duration_ns: self
1198 .stats
1199 .avoided_compiler_duration_ns
1200 .load(Ordering::Relaxed),
1201 compiler: self.stats.compiler.lock().unwrap().clone(),
1202 slow_compilations: self.stats.slow_compilations.lock().unwrap().clone(),
1203 remote_failures: self.stats.remote_failures.load(Ordering::Relaxed),
1204 remote_manifest_lookups: self.stats.remote_manifest_lookups.load(Ordering::Relaxed),
1205 remote_manifest_lookup_duration_ns: self
1206 .stats
1207 .remote_manifest_lookup_duration_ns
1208 .load(Ordering::Relaxed),
1209 remote_action_lookups: self.stats.remote_action_lookups.load(Ordering::Relaxed),
1210 remote_action_lookup_duration_ns: self
1211 .stats
1212 .remote_action_lookup_duration_ns
1213 .load(Ordering::Relaxed),
1214 remote_blob_requests: self.stats.remote_blob_requests.load(Ordering::Relaxed),
1215 remote_blob_pack_requests: self.stats.remote_blob_pack_requests.load(Ordering::Relaxed),
1216 remote_blob_pack_blobs: self.stats.remote_blob_pack_blobs.load(Ordering::Relaxed),
1217 remote_blob_transfer_duration_ns: self
1218 .stats
1219 .remote_blob_transfer_duration_ns
1220 .load(Ordering::Relaxed),
1221 local_cas_write_duration_ns: self
1222 .stats
1223 .local_cas_write_duration_ns
1224 .load(Ordering::Relaxed),
1225 prefetch_runs: self.stats.prefetch_runs.load(Ordering::Relaxed),
1226 prefetch_duration_ns: self.stats.prefetch_duration_ns.load(Ordering::Relaxed),
1227 materialization_duration_ns: self
1228 .stats
1229 .materialization_duration_ns
1230 .load(Ordering::Relaxed),
1231 restored_output_files: self.stats.restored_output_files.load(Ordering::Relaxed),
1232 restored_output_bytes: self.stats.restored_output_bytes.load(Ordering::Relaxed),
1233 reflinked_output_files: self.stats.reflinked_output_files.load(Ordering::Relaxed),
1234 reflinked_output_bytes: self.stats.reflinked_output_bytes.load(Ordering::Relaxed),
1235 copied_output_files: self.stats.copied_output_files.load(Ordering::Relaxed),
1236 copied_output_bytes: self.stats.copied_output_bytes.load(Ordering::Relaxed),
1237 }
1238 }
1239
1240 pub async fn handle_requests(
1247 &self,
1248 requests: impl IntoIterator<Item = AgentRequest>,
1249 ) -> Vec<AgentResponse> {
1250 let mut connection = ConnectionUploads::default();
1251 let mut responses = Vec::new();
1252 for request in requests {
1253 responses.push(self.respond_on(request, &mut connection).await);
1254 }
1255 responses
1256 }
1257
1258 fn write_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1259 Self::digest_lock(&self.write_locks, digest)
1260 }
1261
1262 fn action_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1263 Self::digest_lock(&self.action_locks, digest)
1264 }
1265
1266 fn digest_lock(
1267 locks: &Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>,
1268 digest: &CacheDigest,
1269 ) -> Arc<tokio::sync::Mutex<()>> {
1270 let mut locks = locks.lock().unwrap();
1271 locks.retain(|_, lock| lock.strong_count() > 0);
1272 if let Some(lock) = locks.get(digest).and_then(Weak::upgrade) {
1273 return lock;
1274 }
1275 let lock = Arc::new(tokio::sync::Mutex::new(()));
1276 locks.insert(digest.clone(), Arc::downgrade(&lock));
1277 lock
1278 }
1279
1280 fn spawn_prefetch_predictions(&self, predictions: Vec<ActionPrediction>) {
1281 if predictions.is_empty() || !self.remote_mode.reads() || self.remote.is_none() {
1282 return;
1283 }
1284 let agent = self.clone();
1285 let task = tokio::spawn(async move {
1286 agent.prefetch_predictions(predictions.iter()).await;
1287 });
1288 self.prefetch_tasks.lock().unwrap().push(task);
1289 }
1290
1291 async fn prefetch_predictions<'a>(
1292 &self,
1293 predictions: impl Iterator<Item = &'a ActionPrediction>,
1294 ) {
1295 if !self.remote_mode.reads() || self.remote.is_none() {
1296 return;
1297 }
1298 self.stats.prefetch_runs.fetch_add(1, Ordering::Relaxed);
1299 let _timer = AtomicDurationTimer::start(&self.stats.prefetch_duration_ns);
1300 let mut actions = BTreeMap::new();
1301 for prediction in predictions {
1302 actions
1303 .entry(prediction.action.clone())
1304 .or_insert_with(|| prediction.adapter.clone());
1305 }
1306 match self.prefetch_action_batches(&actions).await {
1309 Ok(true) => return,
1310 Ok(false) => {}
1311 Err(error) => {
1312 self.note_remote_failure();
1313 warn!("remote action batch lookup failed: {error}");
1314 }
1315 }
1316 let mut actions = actions.into_iter();
1317 let mut tasks = tokio::task::JoinSet::new();
1318 for _ in 0..MAX_PREFETCH_TRANSFERS {
1319 let Some((action, adapter)) = actions.next() else {
1320 break;
1321 };
1322 let agent = self.clone();
1323 tasks.spawn(async move { agent.resolve_prefetch_action(action, adapter).await });
1324 }
1325 let mut resolved = Vec::new();
1326 while !tasks.is_empty() {
1327 let result = if resolved.is_empty() {
1328 tasks.join_next().await
1329 } else {
1330 match tokio::time::timeout(PREFETCH_ACTION_BATCH_DELAY, tasks.join_next()).await {
1331 Ok(result) => result,
1332 Err(_) => {
1333 self.prefetch_resolved_actions(std::mem::take(&mut resolved))
1334 .await;
1335 continue;
1336 }
1337 }
1338 };
1339 let Some(result) = result else {
1340 break;
1341 };
1342 match result {
1343 Ok(Ok(Some(action))) => resolved.push(action),
1344 Ok(Ok(None)) => {}
1345 Ok(Err(error)) => {
1346 self.note_remote_failure();
1347 warn!("remote action prefetch failed: {error}");
1348 }
1349 Err(error) => {
1350 self.note_remote_failure();
1351 warn!("remote action prefetch task failed: {error}");
1352 }
1353 }
1354 if let Some((action, adapter)) = actions.next() {
1355 let agent = self.clone();
1356 tasks.spawn(async move { agent.resolve_prefetch_action(action, adapter).await });
1357 }
1358 if resolved.len() == MAX_PREFETCH_ACTION_BATCH {
1359 self.prefetch_resolved_actions(std::mem::take(&mut resolved))
1360 .await;
1361 }
1362 }
1363 if !resolved.is_empty() {
1364 self.prefetch_resolved_actions(resolved).await;
1365 }
1366 }
1367
1368 async fn prefetch_action_batches(
1375 &self,
1376 actions: &BTreeMap<CacheDigest, String>,
1377 ) -> Result<bool> {
1378 let Some(remote) = self.remote.as_deref() else {
1379 return Ok(false);
1380 };
1381 let Some(limit) = remote.action_batch_limit().await? else {
1382 return Ok(false);
1383 };
1384 let wanted: Vec<CacheDigest> = actions
1385 .keys()
1386 .filter(|action| !self.action_is_staged(action))
1387 .cloned()
1388 .collect();
1389 if wanted.is_empty() {
1390 return Ok(true);
1391 }
1392 let mut chunks: Vec<Vec<CacheDigest>> = Vec::new();
1393 for chunk in wanted.chunks(limit) {
1394 chunks.push(chunk.to_vec());
1395 }
1396 let mut lookups = stream::iter(chunks)
1397 .map(|chunk| async move {
1398 let _prefetch_permit = self.prefetch_transfers.acquire().await?;
1399 let _permit = self.remote_transfers.acquire().await?;
1400 self.stats
1401 .remote_action_lookups
1402 .fetch_add(1, Ordering::Relaxed);
1403 let _timer =
1404 AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);
1405 remote.get_action_results(&chunk).await
1406 })
1407 .buffer_unordered(MAX_PREFETCH_BATCH_LOOKUPS);
1408 let mut answered = true;
1409 while let Some(lookup) = lookups.next().await {
1410 let results = match lookup {
1411 Ok(Some(results)) => results,
1412 Ok(None) => {
1414 answered = false;
1415 continue;
1416 }
1417 Err(error) => {
1418 self.note_remote_failure();
1419 warn!("remote action batch lookup failed: {error}");
1420 answered = false;
1421 continue;
1422 }
1423 };
1424 let mut resolved = Vec::with_capacity(results.len());
1425 for result in results {
1426 let Some(adapter) = actions.get(&result.action).cloned() else {
1427 continue;
1428 };
1429 let lock = self.action_lock(&result.action);
1430 let _guard = lock.lock().await;
1431 if self.actions.find(&result.action)?.is_some() {
1432 continue;
1433 }
1434 self.pending_remote_actions
1435 .lock()
1436 .unwrap()
1437 .insert(result.action.clone(), result.clone());
1438 resolved.push(PrefetchedAction { adapter, result });
1439 }
1440 while !resolved.is_empty() {
1441 let wave = resolved
1442 .drain(..resolved.len().min(MAX_PREFETCH_ACTION_BATCH))
1443 .collect();
1444 self.prefetch_resolved_actions(wave).await;
1445 }
1446 }
1447 Ok(answered)
1448 }
1449
1450 fn action_is_staged(&self, action: &CacheDigest) -> bool {
1452 if self
1453 .pending_remote_actions
1454 .lock()
1455 .unwrap()
1456 .contains_key(action)
1457 {
1458 return true;
1459 }
1460 self.actions.find(action).is_ok_and(|found| found.is_some())
1461 }
1462
1463 #[cfg(test)]
1464 async fn prefetch_action(&self, action: CacheDigest, adapter: String) -> Result<()> {
1465 if let Some(action) = self.resolve_prefetch_action(action, adapter).await? {
1466 self.prefetch_resolved_actions(vec![action]).await;
1467 }
1468 Ok(())
1469 }
1470
1471 async fn resolve_prefetch_action(
1472 &self,
1473 action: CacheDigest,
1474 adapter: String,
1475 ) -> Result<Option<PrefetchedAction>> {
1476 let remote = self
1477 .remote
1478 .as_ref()
1479 .ok_or_else(|| eyre::eyre!("remote cache is not configured"))?;
1480 let result = {
1481 let lock = self.action_lock(&action);
1482 let _guard = lock.lock().await;
1483 if self.actions.find(&action)?.is_some() {
1484 return Ok(None);
1485 }
1486 if let Some(result) = self
1487 .pending_remote_actions
1488 .lock()
1489 .unwrap()
1490 .get(&action)
1491 .cloned()
1492 {
1493 result
1494 } else {
1495 let _prefetch_permit = self.prefetch_transfers.acquire().await?;
1496 let result = {
1497 let _permit = self.remote_transfers.acquire().await?;
1498 self.get_remote_action_result(remote, &action).await?
1499 };
1500 let Some(result) = result else {
1501 return Ok(None);
1502 };
1503 self.pending_remote_actions
1504 .lock()
1505 .unwrap()
1506 .insert(action.clone(), result.clone());
1507 result
1508 }
1509 };
1510 Ok(Some(PrefetchedAction { adapter, result }))
1511 }
1512
1513 fn prefetch_resolved_actions(&self, actions: Vec<PrefetchedAction>) -> BoxFuture<'_, ()> {
1514 self.prefetch_resolved_actions_inner(actions).boxed()
1515 }
1516
1517 async fn prefetch_resolved_actions_inner(&self, actions: Vec<PrefetchedAction>) {
1518 let Some(remote) = self.remote.as_deref() else {
1519 return;
1520 };
1521 if actions.is_empty() {
1522 return;
1523 }
1524
1525 let mut top_level = BTreeMap::new();
1526 for action in &actions {
1527 for digest in [
1528 Some(&action.result.action),
1529 action.result.metadata.as_ref(),
1530 action.result.output_root.as_ref(),
1531 ]
1532 .into_iter()
1533 .flatten()
1534 {
1535 top_level.insert(digest.clone(), ());
1536 }
1537 }
1538 let mut verified = self
1539 .fetch_remote_blobs(
1540 remote,
1541 top_level.into_keys().collect(),
1542 Some(&self.prefetch_transfers),
1543 )
1544 .await;
1545
1546 let mut next = BTreeMap::new();
1547 let mut pending_directories = BTreeMap::new();
1548 let mut parsed_directories = BTreeMap::new();
1549 let mut rustc_metadata = BTreeMap::new();
1550 for action in &actions {
1551 if action.adapter == "rustc"
1552 && let Some(metadata_digest) = &action.result.metadata
1553 {
1554 match verified
1555 .get(metadata_digest)
1556 .ok_or_else(|| eyre::eyre!("remote rustc action metadata is missing"))
1557 .and_then(|path| Self::parse_rustc_metadata(path))
1558 {
1559 Ok(metadata) => {
1560 queue_prefetch_digest(&verified, &mut next, metadata.stdout.clone());
1561 queue_prefetch_digest(&verified, &mut next, metadata.stderr.clone());
1562 rustc_metadata.insert(metadata_digest.clone(), metadata);
1563 }
1564 Err(error) => warn!(
1565 "remote rustc action metadata prefetch failed for {}: {error}",
1566 action.result.action.hash
1567 ),
1568 }
1569 }
1570 if let Some(output_root) = &action.result.output_root {
1571 pending_directories.insert(output_root.clone(), ());
1572 }
1573 }
1574
1575 let mut seen_directories = BTreeMap::new();
1576 loop {
1577 let mut following = BTreeMap::new();
1578 let mut directory_limit_exceeded = false;
1579 for digest in pending_directories.into_keys() {
1580 following.remove(&digest);
1581 if seen_directories.insert(digest.clone(), ()).is_some() {
1582 continue;
1583 }
1584 if seen_directories.len() > MAX_PREFETCH_DIRECTORY_OBJECTS {
1585 warn!("remote action output tree is too large to prefetch");
1586 following.clear();
1587 break;
1588 }
1589 match verified
1590 .get(&digest)
1591 .ok_or_else(|| eyre::eyre!("remote action output directory is missing"))
1592 .and_then(|path| Self::parse_cache_directory(path))
1593 {
1594 Ok(directory) => {
1595 for file in &directory.files {
1596 queue_prefetch_digest(&verified, &mut next, file.digest.clone());
1597 if next.len() >= MAX_PREFETCH_OBJECTS_PER_WAVE {
1598 self.flush_prefetch_digest_batch(remote, &mut verified, &mut next)
1599 .await;
1600 }
1601 }
1602 for child in &directory.directories {
1603 if !queue_prefetch_directory(
1604 &seen_directories,
1605 &mut following,
1606 child.digest.clone(),
1607 MAX_PREFETCH_DIRECTORY_OBJECTS,
1608 ) {
1609 warn!("remote action output tree is too large to prefetch");
1610 directory_limit_exceeded = true;
1611 break;
1612 }
1613 queue_prefetch_digest(&verified, &mut next, child.digest.clone());
1614 if next.len() >= MAX_PREFETCH_OBJECTS_PER_WAVE {
1615 self.flush_prefetch_digest_batch(remote, &mut verified, &mut next)
1616 .await;
1617 }
1618 }
1619 parsed_directories.insert(digest, directory);
1620 }
1621 Err(error) => warn!(
1622 "remote action output directory prefetch failed for {}: {error}",
1623 digest.hash
1624 ),
1625 }
1626 if directory_limit_exceeded {
1627 following.clear();
1628 break;
1629 }
1630 }
1631 self.flush_prefetch_digest_batch(remote, &mut verified, &mut next)
1632 .await;
1633 if following.is_empty() {
1634 break;
1635 }
1636 pending_directories = following;
1637 }
1638
1639 for action in actions {
1640 match Self::validate_prefetched_action(
1641 &action,
1642 &verified,
1643 &rustc_metadata,
1644 &parsed_directories,
1645 ) {
1646 Ok(()) => {
1647 if let Err(error) = self.actions.store(&action.result) {
1648 warn!(
1649 "remote action prefetch could not publish {}: {error}",
1650 action.result.action.hash
1651 );
1652 continue;
1653 }
1654 self.pending_remote_actions
1655 .lock()
1656 .unwrap()
1657 .remove(&action.result.action);
1658 self.stats
1659 .prefetched_actions
1660 .fetch_add(1, Ordering::Relaxed);
1661 }
1662 Err(error) => warn!(
1663 "remote action prefetch was incomplete for {}: {error}",
1664 action.result.action.hash
1665 ),
1666 }
1667 }
1668 }
1669
1670 async fn flush_prefetch_digest_batch(
1671 &self,
1672 remote: &RemoteCacheClient,
1673 verified: &mut BTreeMap<CacheDigest, PathBuf>,
1674 pending: &mut BTreeMap<CacheDigest, ()>,
1675 ) {
1676 if pending.is_empty() {
1677 return;
1678 }
1679 let digests = std::mem::take(pending).into_keys().collect();
1680 verified.extend(
1681 self.fetch_remote_blobs(remote, digests, Some(&self.prefetch_transfers))
1682 .await,
1683 );
1684 }
1685
1686 async fn fetch_remote_blobs(
1687 &self,
1688 remote: &RemoteCacheClient,
1689 digests: Vec<CacheDigest>,
1690 prefetch_limit: Option<&tokio::sync::Semaphore>,
1691 ) -> BTreeMap<CacheDigest, PathBuf> {
1692 let mut verified = BTreeMap::new();
1693 let mut missing = BTreeMap::new();
1694 for digest in digests {
1695 match self.find_verified_blob(&digest) {
1696 Ok(Some(path)) => {
1697 verified.insert(digest, path);
1698 }
1699 Ok(None) => {
1700 missing.insert(digest, ());
1701 }
1702 Err(error) => warn!(
1703 "local cache blob lookup failed for {}: {error}",
1704 digest.hash
1705 ),
1706 }
1707 }
1708 if missing.is_empty() {
1709 return verified;
1710 }
1711
1712 let mut pack_candidates = missing.clone();
1713 while !pack_candidates.is_empty() {
1714 let candidates = match blob_pack_chunk(
1715 &pack_candidates.keys().cloned().collect::<Vec<_>>(),
1716 BlobPackLimits {
1717 max_items: MAX_STAGED_BLOB_PACK_ITEMS,
1718 max_bytes: MAX_STAGED_BLOB_PACK_BYTES,
1719 },
1720 ) {
1721 Ok(candidates) if !candidates.is_empty() => candidates,
1722 Ok(_) => break,
1723 Err(error) => {
1724 warn!("remote cache blob pack skipped: {error}");
1725 break;
1726 }
1727 };
1728 let mut pack_guards = BTreeMap::new();
1732 for digest in candidates {
1733 let guard = self.write_lock(&digest).lock_owned().await;
1734 match self.find_verified_blob(&digest) {
1735 Ok(Some(path)) => {
1736 pack_candidates.remove(&digest);
1737 missing.remove(&digest);
1738 verified.insert(digest, path);
1739 }
1740 Ok(None) => {
1741 pack_guards.insert(digest, guard);
1742 }
1743 Err(error) => {
1744 warn!(
1745 "local cache blob lookup failed for {}: {error}",
1746 digest.hash
1747 );
1748 pack_guards.insert(digest, guard);
1749 }
1750 }
1751 }
1752 let requested = pack_guards.keys().cloned().collect::<Vec<_>>();
1753 if requested.is_empty() {
1754 continue;
1755 }
1756 let requested_bytes = requested
1757 .iter()
1758 .fold(0_u64, |total, digest| total.saturating_add(digest.size));
1759 let pack_reservation = match self
1760 .reserve_remote_download_up_to(requested_bytes.min(MAX_STAGED_BLOB_PACK_BYTES))
1761 {
1762 Ok(reservation) if reservation.bytes() > 0 => reservation,
1763 Ok(_) => break,
1764 Err(error) => {
1765 warn!("remote cache blob pack skipped: {error}");
1766 break;
1767 }
1768 };
1769 let (pack, transfer_duration_ns) = {
1770 let _prefetch_permit = match prefetch_limit {
1771 Some(limit) => match limit.acquire().await {
1772 Ok(permit) => Some(permit),
1773 Err(error) => {
1774 warn!(
1775 "remote cache blob pack could not acquire prefetch limit: {error}"
1776 );
1777 break;
1778 }
1779 },
1780 None => None,
1781 };
1782 let _transfer_permit = match self.remote_transfers.acquire().await {
1783 Ok(permit) => permit,
1784 Err(error) => {
1785 warn!("remote cache blob pack could not acquire transfer limit: {error}");
1786 break;
1787 }
1788 };
1789 let transfer_started = Instant::now();
1790 let pack = remote
1791 .get_blob_pack_with_limit(
1792 &requested,
1793 self.remote_staging_dir.as_path(),
1794 pack_reservation.bytes(),
1795 )
1796 .await;
1797 (pack, duration_ns(transfer_started))
1798 };
1799 let pack = match pack {
1800 Ok(Some(pack)) => pack,
1801 Ok(None) => break,
1802 Err(error) => {
1803 atomic_saturating_add(
1804 &self.stats.remote_blob_transfer_duration_ns,
1805 transfer_duration_ns,
1806 );
1807 warn!(
1808 "remote cache blob pack failed; falling back to individual blobs: {error}"
1809 );
1810 break;
1811 }
1812 };
1813 pack_reservation.commit(pack.payload_bytes);
1814 atomic_saturating_add(
1815 &self.stats.remote_blob_transfer_duration_ns,
1816 transfer_duration_ns,
1817 );
1818 atomic_saturating_add(&self.stats.remote_blob_pack_requests, pack.requests);
1819 atomic_saturating_add(&self.stats.remote_blob_pack_blobs, pack.blob_count);
1820 atomic_saturating_add(&self.stats.downloaded_bytes, pack.payload_bytes);
1821 if pack.requested.is_empty() {
1822 for digest in &requested {
1826 pack_candidates.remove(digest);
1827 }
1828 continue;
1829 }
1830 for digest in &pack.requested {
1831 pack_candidates.remove(digest);
1832 }
1833 let mut ingests = stream::iter(pack.blobs.into_iter().map(|(digest, source)| {
1834 let digest_for_result = digest.clone();
1835 let guard = pack_guards
1836 .remove(&digest)
1837 .expect("requested packed blob has a write lock");
1838 async move {
1839 (
1840 digest_for_result,
1841 self.ingest_packed_blob(digest, source, guard).await,
1842 )
1843 }
1844 }))
1845 .buffer_unordered(MAX_PREFETCH_TRANSFERS);
1846 while let Some((digest, result)) = ingests.next().await {
1847 match result {
1848 Ok(path) => {
1849 missing.remove(&digest);
1850 verified.insert(digest, path);
1851 }
1852 Err(error) => warn!(
1853 "remote cache packed blob ingest failed for {}: {error}",
1854 digest.hash
1855 ),
1856 }
1857 }
1858 }
1859
1860 let mut transfers = stream::iter(missing.into_keys().map(|digest| {
1861 let digest_for_result = digest.clone();
1862 async move {
1863 (
1864 digest_for_result,
1865 self.fetch_remote_blob_with_limit(remote, &digest, prefetch_limit)
1866 .await,
1867 )
1868 }
1869 }))
1870 .buffer_unordered(MAX_PREFETCH_TRANSFERS);
1871 while let Some((digest, result)) = transfers.next().await {
1872 match result {
1873 Ok(path) => {
1874 verified.insert(digest, path);
1875 }
1876 Err(error) => warn!(
1877 "remote cache blob prefetch failed for {}: {error}",
1878 digest.hash
1879 ),
1880 }
1881 }
1882 verified
1883 }
1884
1885 async fn ingest_packed_blob(
1886 &self,
1887 digest: CacheDigest,
1888 source: PathBuf,
1889 _guard: tokio::sync::OwnedMutexGuard<()>,
1890 ) -> Result<PathBuf> {
1891 let digest_size = digest.size;
1892 let agent = self.clone();
1893 let (path, stored, cas_duration_ns) = tokio::task::spawn_blocking(move || {
1894 if let Some(path) = agent.find_verified_blob(&digest)? {
1895 return Ok::<_, eyre::Report>((path, false, 0));
1896 }
1897 let cas_started = Instant::now();
1898 let path = agent.cas.store_verified_file(&digest, &source)?;
1899 let cas_duration_ns = duration_ns(cas_started);
1900 agent.remember_verified_blob(&digest, &path);
1901 Ok((path, true, cas_duration_ns))
1902 })
1903 .await??;
1904 atomic_saturating_add(&self.stats.local_cas_write_duration_ns, cas_duration_ns);
1905 if stored {
1906 self.stats.stores.fetch_add(1, Ordering::Relaxed);
1907 atomic_saturating_add(&self.stats.stored_bytes, digest_size);
1908 }
1909 Ok(path)
1910 }
1911
1912 fn parse_rustc_metadata(path: &Path) -> Result<RustcMetadata> {
1913 let bytes = fs::read(path)?;
1914 let metadata: RustcMetadata = serde_json::from_slice(&bytes)?;
1915 if metadata.version != 1 || metadata.kind != "rustc" || canonical_json(&metadata)? != bytes
1916 {
1917 bail!("remote rustc action metadata is invalid");
1918 }
1919 Ok(metadata)
1920 }
1921
1922 fn parse_cache_directory(path: &Path) -> Result<CacheDirectory> {
1923 let bytes = fs::read(path)?;
1924 let directory: CacheDirectory = serde_json::from_slice(&bytes)?;
1925 if directory.version != 1 || canonical_json(&directory)? != bytes {
1926 bail!("remote action output directory is invalid");
1927 }
1928 Ok(directory)
1929 }
1930
1931 #[cfg(test)]
1932 fn load_cache_directory(&self, digest: &CacheDigest) -> Result<CacheDirectory> {
1933 let path = self
1934 .find_verified_blob(digest)?
1935 .ok_or_else(|| eyre::eyre!("remote action output directory is missing"))?;
1936 Self::parse_cache_directory(&path)
1937 }
1938
1939 fn validate_prefetched_action(
1940 action: &PrefetchedAction,
1941 verified: &BTreeMap<CacheDigest, PathBuf>,
1942 rustc_metadata: &BTreeMap<CacheDigest, RustcMetadata>,
1943 directories: &BTreeMap<CacheDigest, CacheDirectory>,
1944 ) -> Result<()> {
1945 if !verified.contains_key(&action.result.action) {
1946 bail!("remote action descriptor is missing");
1947 }
1948 if let Some(metadata) = &action.result.metadata {
1949 if action.adapter == "rustc" {
1950 let metadata = rustc_metadata
1951 .get(metadata)
1952 .ok_or_else(|| eyre::eyre!("remote rustc action metadata is missing"))?;
1953 for digest in [&metadata.stdout, &metadata.stderr] {
1954 if !verified.contains_key(digest) {
1955 bail!("remote rustc action diagnostic blob is missing");
1956 }
1957 }
1958 } else if !verified.contains_key(metadata) {
1959 bail!("remote action metadata is missing");
1960 }
1961 }
1962 let mut pending = action
1963 .result
1964 .output_root
1965 .iter()
1966 .cloned()
1967 .collect::<Vec<_>>();
1968 let mut seen = BTreeMap::new();
1969 while let Some(digest) = pending.pop() {
1970 if seen.insert(digest.clone(), ()).is_some() {
1971 continue;
1972 }
1973 if seen.len() > MAX_PREFETCH_DIRECTORY_OBJECTS {
1974 bail!("remote action output tree is too large");
1975 }
1976 let directory = directories
1977 .get(&digest)
1978 .ok_or_else(|| eyre::eyre!("remote action output directory is missing"))?;
1979 for file in &directory.files {
1980 if !verified.contains_key(&file.digest) {
1981 bail!("remote action output file is missing");
1982 }
1983 }
1984 pending.extend(
1985 directory
1986 .directories
1987 .iter()
1988 .map(|directory| directory.digest.clone()),
1989 );
1990 }
1991 Ok(())
1992 }
1993
1994 #[cfg(test)]
1995 async fn prefetch_output_tree(
1996 &self,
1997 remote: &RemoteCacheClient,
1998 output_root: &CacheDigest,
1999 ) -> Result<()> {
2000 let mut pending = vec![output_root.clone()];
2001 let mut seen = BTreeMap::new();
2002 while let Some(digest) = pending.pop() {
2003 if seen.insert(digest.clone(), ()).is_some() {
2004 continue;
2005 }
2006 if seen.len() > MAX_PREFETCH_DIRECTORY_OBJECTS {
2007 bail!("remote action output tree is too large");
2008 }
2009 self.fetch_remote_blob_with_limit(remote, &digest, Some(&self.prefetch_transfers))
2010 .await?;
2011 let directory = self.load_cache_directory(&digest)?;
2012 let mut transfers = stream::iter(directory.files.into_iter().map(|file| async move {
2013 self.fetch_remote_blob_with_limit(
2014 remote,
2015 &file.digest,
2016 Some(&self.prefetch_transfers),
2017 )
2018 .await
2019 .map(|_| ())
2020 }))
2021 .buffer_unordered(MAX_PREFETCH_TRANSFERS);
2022 while let Some(result) = transfers.next().await {
2023 result?;
2024 }
2025 pending.extend(
2026 directory
2027 .directories
2028 .into_iter()
2029 .map(|directory| directory.digest),
2030 );
2031 }
2032 Ok(())
2033 }
2034
2035 async fn fetch_remote_blob(
2036 &self,
2037 remote: &RemoteCacheClient,
2038 digest: &CacheDigest,
2039 ) -> Result<PathBuf> {
2040 self.fetch_remote_blob_with_limit(remote, digest, None)
2041 .await
2042 }
2043
2044 async fn fetch_remote_blob_with_limit(
2045 &self,
2046 remote: &RemoteCacheClient,
2047 digest: &CacheDigest,
2048 prefetch_limit: Option<&tokio::sync::Semaphore>,
2049 ) -> Result<PathBuf> {
2050 let lock = self.write_lock(digest);
2051 let _guard = lock.lock().await;
2052 if let Some(path) = self.find_verified_blob(digest)? {
2053 return Ok(path);
2054 }
2055 let _prefetch_permit = match prefetch_limit {
2056 Some(limit) => Some(limit.acquire().await?),
2057 None => None,
2058 };
2059 let _permit = self.remote_transfers.acquire().await?;
2060 let reservation = self.reserve_remote_download(digest.size)?;
2061 self.stats
2062 .remote_blob_requests
2063 .fetch_add(1, Ordering::Relaxed);
2064 let transfer_timer =
2065 AtomicDurationTimer::start(&self.stats.remote_blob_transfer_duration_ns);
2066 let temporary = remote
2067 .get_blob_file(digest, self.remote_staging_dir.as_path())
2068 .await?;
2069 drop(transfer_timer);
2070 let _cas_timer = AtomicDurationTimer::start(&self.stats.local_cas_write_duration_ns);
2071 let path = self.cas.store_verified_file(digest, temporary.path())?;
2072 reservation.commit(digest.size);
2073 self.remember_verified_blob(digest, &path);
2074 self.stats.stores.fetch_add(1, Ordering::Relaxed);
2075 self.stats
2076 .stored_bytes
2077 .fetch_add(digest.size, Ordering::Relaxed);
2078 self.stats
2079 .downloaded_bytes
2080 .fetch_add(digest.size, Ordering::Relaxed);
2081 Ok(path)
2082 }
2083
2084 #[cfg(test)]
2089 async fn respond(&self, request: AgentRequest) -> AgentResponse {
2090 self.respond_on(request, &mut ConnectionUploads::default())
2091 .await
2092 }
2093
2094 async fn respond_on(
2095 &self,
2096 request: AgentRequest,
2097 connection: &mut ConnectionUploads,
2098 ) -> AgentResponse {
2099 let result = match request {
2100 AgentRequest::BeginTask { task } => self
2101 .begin_task(&task)
2102 .await
2103 .map(|run| AgentResponse::TaskBegun { run }),
2104 AgentRequest::CommitTask { run } => self
2105 .commit_task(&run)
2106 .await
2107 .map(|()| AgentResponse::TaskCommitted),
2108 AgentRequest::FindBlob { digest } => self.find_blob(&digest).await,
2109 AgentRequest::FindBlobs { digests } => self.find_blobs(digests).await,
2110 AgentRequest::StoreBlob { digest, source } => {
2111 self.store_blob(&digest, &source, connection).await
2112 }
2113 AgentRequest::FindActionResult { action } => {
2114 self.stats.lookups.fetch_add(1, Ordering::Relaxed);
2115 self.find_action_result(&action).await
2116 }
2117 AgentRequest::RecordActionHit {
2118 action,
2119 restore,
2120 crate_name,
2121 } => self.record_action_hit(&action, restore, crate_name),
2122 AgentRequest::RecordBypass { kind } => {
2123 *self
2124 .stats
2125 .bypasses
2126 .lock()
2127 .unwrap()
2128 .entry(kind.clone())
2129 .or_insert(0) += 1;
2130 self.emit(|| AgentEvent::Bypass { kind });
2131 Ok(AgentResponse::BypassRecorded)
2132 }
2133 AgentRequest::RecordUnconsulted => {
2134 self.stats.unconsulted.fetch_add(1, Ordering::Relaxed);
2135 self.emit(|| AgentEvent::Unconsulted);
2136 Ok(AgentResponse::UnconsultedRecorded)
2137 }
2138 AgentRequest::RecordCompilerInvocation {
2139 outcome,
2140 crate_name,
2141 duration_ns,
2142 } => self.record_compiler_invocation(&outcome, crate_name.as_deref(), duration_ns),
2143 AgentRequest::RecordActionVerification { matched, restore } => {
2144 self.record_materialization(restore);
2145 self.stats.verifications.fetch_add(1, Ordering::Relaxed);
2146 if !matched {
2147 self.stats.divergences.fetch_add(1, Ordering::Relaxed);
2148 }
2149 self.emit(|| AgentEvent::Verification { matched, restore });
2150 Ok(AgentResponse::ActionVerificationRecorded)
2151 }
2152 AgentRequest::StoreActionResult { result } => {
2153 self.store_action_result(&result, connection).await
2154 }
2155 AgentRequest::FindActionPrediction { task, invocation } => {
2156 self.find_action_prediction(&task, &invocation)
2157 }
2158 AgentRequest::RecordActionPrediction { task, prediction } => {
2159 self.record_action_prediction(&task, prediction)
2160 }
2161 AgentRequest::FindExecutableIdentity {
2162 executable,
2163 environment,
2164 } => self.find_executable_identity(executable, environment),
2165 AgentRequest::StoreExecutableIdentity {
2166 executable,
2167 environment,
2168 stdout,
2169 } => self.store_executable_identity(executable, environment, stdout),
2170 AgentRequest::Hello { .. } => {
2171 Err(eyre::eyre!("hello is only valid as the first request"))
2172 }
2173 };
2174 result.unwrap_or_else(|error| AgentResponse::Error {
2175 message: error.to_string(),
2176 })
2177 }
2178
2179 async fn find_blob(&self, digest: &CacheDigest) -> Result<AgentResponse> {
2180 if let Some(path) = self.find_verified_blob(digest)? {
2181 return Ok(AgentResponse::Blob { path: Some(path) });
2182 }
2183 if !self.remote_mode.reads() {
2184 return Ok(AgentResponse::Blob { path: None });
2185 }
2186 let Some(remote) = &self.remote else {
2187 return Ok(AgentResponse::Blob { path: None });
2188 };
2189 match self.fetch_remote_blob(remote, digest).await {
2190 Ok(path) => Ok(AgentResponse::Blob { path: Some(path) }),
2191 Err(error) => {
2192 warn!(
2193 "remote cache blob lookup failed for {}: {error}",
2194 digest.hash
2195 );
2196 Ok(AgentResponse::Blob { path: None })
2197 }
2198 }
2199 }
2200
2201 async fn find_blobs(&self, digests: Vec<CacheDigest>) -> Result<AgentResponse> {
2202 let mut paths = BTreeMap::new();
2203 let mut missing = Vec::new();
2204 for digest in &digests {
2205 match self.find_verified_blob(digest)? {
2206 Some(path) => {
2207 paths.insert(digest.clone(), path);
2208 }
2209 None => {
2210 missing.push(digest.clone());
2211 }
2212 }
2213 }
2214
2215 if !missing.is_empty()
2216 && self.remote_mode.reads()
2217 && let Some(remote) = &self.remote
2218 {
2219 paths.extend(self.fetch_remote_blobs(remote, missing, None).await);
2220 }
2221
2222 Ok(AgentResponse::Blobs {
2223 paths: digests
2224 .into_iter()
2225 .map(|digest| paths.get(&digest).cloned())
2226 .collect(),
2227 })
2228 }
2229
2230 async fn store_blob(
2231 &self,
2232 digest: &CacheDigest,
2233 source: &Path,
2234 connection: &mut ConnectionUploads,
2235 ) -> Result<AgentResponse> {
2236 let path = {
2237 let lock = self.write_lock(digest);
2238 let _guard = lock.lock().await;
2239 if let Some(path) = self.find_verified_blob(digest)? {
2240 path
2241 } else {
2242 let path = self.cas.store_file(digest, source)?;
2243 self.remember_verified_blob(digest, &path);
2244 self.stats.stores.fetch_add(1, Ordering::Relaxed);
2245 self.stats
2246 .stored_bytes
2247 .fetch_add(digest.size, Ordering::Relaxed);
2248 path
2249 }
2250 };
2251 if let Some(uploads) = &self.uploads {
2254 uploads.queue_blob(digest, path.clone(), connection);
2255 }
2256 Ok(AgentResponse::Stored { path })
2257 }
2258
2259 fn find_verified_blob(&self, digest: &CacheDigest) -> Result<Option<PathBuf>> {
2260 let remembered = self.verified_blobs.lock().unwrap().get(digest).cloned();
2261 if let Some(path) = remembered {
2262 if digest.matches_file(&path).unwrap_or(false) {
2263 return Ok(Some(path));
2264 }
2265 self.verified_blobs.lock().unwrap().remove(digest);
2266 }
2267 let path = self.cas.find(digest)?;
2268 if let Some(path) = &path {
2269 self.remember_verified_blob(digest, path);
2270 }
2271 Ok(path)
2272 }
2273
2274 fn remember_verified_blob(&self, digest: &CacheDigest, path: &Path) {
2275 self.verified_blobs
2276 .lock()
2277 .unwrap()
2278 .insert(digest.clone(), path.to_path_buf());
2279 }
2280
2281 async fn find_action_result(&self, action: &CacheDigest) -> Result<AgentResponse> {
2282 if let Some(result) = self.actions.find(action)? {
2283 return Ok(AgentResponse::ActionResult {
2284 result: Some(result),
2285 });
2286 }
2287 if !self.remote_mode.reads() {
2288 return Ok(AgentResponse::ActionResult { result: None });
2289 }
2290 let Some(remote) = &self.remote else {
2291 return Ok(AgentResponse::ActionResult { result: None });
2292 };
2293 let lock = self.action_lock(action);
2294 let _guard = lock.lock().await;
2295 if let Some(result) = self.actions.find(action)? {
2296 return Ok(AgentResponse::ActionResult {
2297 result: Some(result),
2298 });
2299 }
2300 if let Some(result) = self
2301 .pending_remote_actions
2302 .lock()
2303 .unwrap()
2304 .get(action)
2305 .cloned()
2306 {
2307 return Ok(AgentResponse::ActionResult {
2308 result: Some(result),
2309 });
2310 }
2311 let _permit = self.remote_transfers.acquire().await?;
2312 match self.get_remote_action_result(remote, action).await {
2313 Ok(Some(result)) => {
2314 self.pending_remote_actions
2315 .lock()
2316 .unwrap()
2317 .insert(action.clone(), result.clone());
2318 Ok(AgentResponse::ActionResult {
2319 result: Some(result),
2320 })
2321 }
2322 Ok(None) => Ok(AgentResponse::ActionResult { result: None }),
2323 Err(error) => {
2324 self.note_remote_failure();
2325 warn!(
2326 "remote cache action lookup failed for {}: {error}",
2327 action.hash
2328 );
2329 Ok(AgentResponse::ActionResult { result: None })
2330 }
2331 }
2332 }
2333
2334 async fn store_action_result(
2335 &self,
2336 result: &RemoteActionResult,
2337 connection: &ConnectionUploads,
2338 ) -> Result<AgentResponse> {
2339 let path = self.actions.store(result)?;
2340 if let Some(uploads) = &self.uploads {
2341 uploads.queue_action_result(result, connection);
2342 }
2343 Ok(AgentResponse::ActionStored { path })
2344 }
2345
2346 fn note_remote_failure(&self) {
2348 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
2349 }
2350
2351 async fn get_remote_action_result(
2352 &self,
2353 remote: &RemoteCacheClient,
2354 action: &CacheDigest,
2355 ) -> Result<Option<RemoteActionResult>> {
2356 self.stats
2357 .remote_action_lookups
2358 .fetch_add(1, Ordering::Relaxed);
2359 let _timer = AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);
2360 remote.get_action_result(action).await
2361 }
2362
2363 fn record_action_hit(
2364 &self,
2365 action: &CacheDigest,
2366 restore: RestoreStats,
2367 crate_name: Option<String>,
2368 ) -> Result<AgentResponse> {
2369 validate_crate_name(crate_name.as_deref())?;
2370 if self.actions.find(action)?.is_none() {
2371 let pending = self.pending_remote_actions.lock().unwrap().remove(action);
2372 if let Some(result) = pending {
2373 self.actions.store(&result)?;
2374 } else {
2375 bail!("cannot record a hit for a missing action result");
2376 }
2377 }
2378 self.record_restore(restore);
2379 self.stats.hits.fetch_add(1, Ordering::Relaxed);
2380 self.emit(|| AgentEvent::ActionHit {
2381 crate_name,
2382 restore,
2383 });
2384 Ok(AgentResponse::ActionHitRecorded)
2385 }
2386
2387 fn record_restore(&self, restore: RestoreStats) {
2388 self.record_materialization(restore);
2389 atomic_saturating_add(
2390 &self.stats.avoided_compiler_duration_ns,
2391 restore.avoided_compiler_duration_ns,
2392 );
2393 atomic_saturating_add(&self.stats.restored_output_files, restore.output_files);
2394 atomic_saturating_add(&self.stats.restored_output_bytes, restore.output_bytes);
2395 atomic_saturating_add(
2396 &self.stats.reflinked_output_files,
2397 restore.reflinked_output_files,
2398 );
2399 atomic_saturating_add(
2400 &self.stats.reflinked_output_bytes,
2401 restore.reflinked_output_bytes,
2402 );
2403 atomic_saturating_add(&self.stats.copied_output_files, restore.copied_output_files);
2404 atomic_saturating_add(&self.stats.copied_output_bytes, restore.copied_output_bytes);
2405 }
2406
2407 fn record_compiler_invocation(
2408 &self,
2409 outcome: &str,
2410 crate_name: Option<&str>,
2411 duration_ns: u64,
2412 ) -> Result<AgentResponse> {
2413 if !matches!(
2414 outcome,
2415 "miss" | "unconsulted" | "bypass" | "verification" | "incremental"
2416 ) {
2417 bail!("invalid compiler invocation outcome");
2418 }
2419 validate_crate_name(crate_name)?;
2420 let mut compiler = self.stats.compiler.lock().unwrap();
2421 let stats = compiler.entry(outcome.to_string()).or_default();
2422 stats.invocations = stats.invocations.saturating_add(1);
2423 stats.duration_ns = stats.duration_ns.saturating_add(duration_ns);
2424 drop(compiler);
2425 if outcome != "verification"
2426 && let Some(crate_name) = crate_name.filter(|name| !name.is_empty())
2427 {
2428 let mut slow = self.stats.slow_compilations.lock().unwrap();
2429 let duration = slow.entry(crate_name.to_string()).or_default();
2430 *duration = duration.saturating_add(duration_ns);
2431 }
2432 self.emit(|| AgentEvent::CompilerInvocation {
2433 outcome: outcome.to_string(),
2434 crate_name: crate_name.map(str::to_string),
2435 duration_ns,
2436 });
2437 Ok(AgentResponse::CompilerInvocationRecorded)
2438 }
2439
2440 fn record_materialization(&self, restore: RestoreStats) {
2441 atomic_saturating_add(&self.stats.materialization_duration_ns, restore.duration_ns);
2442 }
2443
2444 fn find_action_prediction(
2445 &self,
2446 task: &str,
2447 invocation: &CacheDigest,
2448 ) -> Result<AgentResponse> {
2449 validate_task_identity(task)?;
2450 invocation.validate()?;
2451 let prediction = self
2452 .task_actions
2453 .lock()
2454 .unwrap()
2455 .get(task)
2456 .and_then(|state| state.predictions.get(invocation))
2457 .cloned();
2458 Ok(AgentResponse::ActionPrediction { prediction })
2459 }
2460
2461 fn record_action_prediction(
2462 &self,
2463 task: &str,
2464 prediction: ActionPrediction,
2465 ) -> Result<AgentResponse> {
2466 validate_task_identity(task)?;
2467 validate_action_prediction(&prediction)?;
2468 let mut tasks = self.task_actions.lock().unwrap();
2469 let state = tasks.entry(task.to_string()).or_default();
2470 if !state.predictions.contains_key(&prediction.invocation)
2471 && state.predictions.len() >= MAX_TASK_ACTION_PREDICTIONS
2472 {
2473 bail!("task action manifest contains too many predictions");
2474 }
2475 state
2476 .predictions
2477 .insert(prediction.invocation.clone(), prediction.clone());
2478 state
2479 .pending_predictions
2480 .insert(prediction.invocation.clone(), prediction);
2481 Ok(AgentResponse::ActionPredictionRecorded)
2482 }
2483
2484 fn executable_identity_key(
2485 &self,
2486 executable: PathBuf,
2487 environment: BTreeMap<String, Option<String>>,
2488 ) -> Result<ExecutableIdentityKey> {
2489 if !environment.keys().all(|name| {
2494 matches!(
2495 name.as_str(),
2496 "RUSTUP_HOME" | "RUSTUP_TOOLCHAIN" | "SDKROOT" | "MACOSX_DEPLOYMENT_TARGET"
2497 )
2498 }) {
2499 bail!("executable identity contains an unsupported environment variable");
2500 }
2501 Ok(ExecutableIdentityKey {
2502 executable,
2503 environment,
2504 })
2505 }
2506
2507 fn find_executable_identity(
2508 &self,
2509 executable: PathBuf,
2510 environment: BTreeMap<String, Option<String>>,
2511 ) -> Result<AgentResponse> {
2512 let key = self.executable_identity_key(executable, environment)?;
2513 let stdout = self
2514 .executable_identities
2515 .lock()
2516 .unwrap()
2517 .get(&key)
2518 .cloned();
2519 Ok(AgentResponse::ExecutableIdentity { stdout })
2520 }
2521
2522 fn store_executable_identity(
2523 &self,
2524 executable: PathBuf,
2525 environment: BTreeMap<String, Option<String>>,
2526 stdout: Vec<u8>,
2527 ) -> Result<AgentResponse> {
2528 if stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE {
2529 bail!("executable identity exceeds {MAX_EXECUTABLE_IDENTITY_SIZE} bytes");
2530 }
2531 let key = self.executable_identity_key(executable, environment)?;
2532 let mut identities = self.executable_identities.lock().unwrap();
2533 let is_new = !identities.contains_key(&key);
2534 let previous_size = identities.get(&key).map_or(0, Vec::len);
2535 if is_new && identities.len() >= MAX_EXECUTABLE_IDENTITIES {
2536 bail!("executable identity cache contains too many entries");
2537 }
2538 let retained_bytes = identities.values().map(Vec::len).sum::<usize>();
2539 if retained_bytes - previous_size + stdout.len() > MAX_EXECUTABLE_IDENTITY_BYTES {
2540 bail!("executable identity cache contains too many bytes");
2541 }
2542 identities.insert(key, stdout.clone());
2543 Ok(AgentResponse::ExecutableIdentity {
2544 stdout: Some(stdout),
2545 })
2546 }
2547
2548 pub async fn handle_connection<S>(&self, stream: S) -> Result<()>
2550 where
2551 S: AsyncRead + AsyncWrite + Unpin,
2552 {
2553 let (reader, mut writer) = tokio::io::split(stream);
2554 let mut reader = BufReader::new(reader);
2555 let hello = read_request(&mut reader)
2556 .await?
2557 .ok_or_else(|| eyre::eyre!("connection closed before the agent handshake"))?;
2558 let request: AgentRequest = serde_json::from_str(&hello)?;
2559 match request {
2560 AgentRequest::Hello {
2561 protocol,
2562 client_version,
2563 } if protocol == AGENT_PROTOCOL_VERSION && client_version == self.version.as_ref() => {}
2564 AgentRequest::Hello { protocol, .. } if protocol != AGENT_PROTOCOL_VERSION => {
2565 send_response(
2566 &mut writer,
2567 &AgentResponse::Error {
2568 message: format!(
2569 "unsupported agent protocol {protocol}; expected {AGENT_PROTOCOL_VERSION}"
2570 ),
2571 },
2572 )
2573 .await?;
2574 return Ok(());
2575 }
2576 AgentRequest::Hello { client_version, .. } => {
2577 send_response(
2578 &mut writer,
2579 &AgentResponse::Error {
2580 message: format!(
2581 "cache client {client_version} does not match agent {}",
2582 self.version
2583 ),
2584 },
2585 )
2586 .await?;
2587 return Ok(());
2588 }
2589 _ => bail!("the first agent request must be hello"),
2590 }
2591 send_response(
2592 &mut writer,
2593 &AgentResponse::Hello {
2594 protocol: AGENT_PROTOCOL_VERSION,
2595 agent_version: self.version.to_string(),
2596 },
2597 )
2598 .await?;
2599
2600 let mut connection = ConnectionUploads::default();
2604 while let Some(line) = read_request(&mut reader).await? {
2605 let response = match serde_json::from_str(&line) {
2606 Ok(request) => self.respond_on(request, &mut connection).await,
2607 Err(error) => AgentResponse::Error {
2608 message: format!("invalid agent request: {error}"),
2609 },
2610 };
2611 send_response(&mut writer, &response).await?;
2612 }
2613 Ok(())
2614 }
2615}
2616
2617async fn read_request<R>(reader: &mut R) -> Result<Option<String>>
2623where
2624 R: AsyncBufRead + Unpin,
2625{
2626 let mut line = Vec::new();
2627 loop {
2628 let available = reader.fill_buf().await?;
2629 if available.is_empty() {
2630 break;
2631 }
2632 let (consumed, complete) = match available.iter().position(|byte| *byte == b'\n') {
2633 Some(index) => (index, true),
2634 None => (available.len(), false),
2635 };
2636 if line.len() + consumed > MAX_REQUEST_BYTES {
2637 bail!("agent request exceeded {MAX_REQUEST_BYTES} bytes");
2638 }
2639 line.extend_from_slice(&available[..consumed]);
2640 reader.consume(consumed + usize::from(complete));
2642 if complete {
2643 return Ok(Some(String::from_utf8(line)?));
2644 }
2645 }
2646 if line.is_empty() {
2647 Ok(None)
2648 } else {
2649 Ok(Some(String::from_utf8(line)?))
2650 }
2651}
2652
2653pub fn is_task_identity(task: &str) -> bool {
2659 task.len() == 64
2660 && task
2661 .bytes()
2662 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2663}
2664
2665fn validate_task_identity(task: &str) -> Result<()> {
2666 if !is_task_identity(task) {
2667 bail!("invalid task action identity");
2668 }
2669 Ok(())
2670}
2671
2672fn task_manifest_dir(store: &Path) -> PathBuf {
2674 store.join("task-manifests").join("v1")
2675}
2676
2677pub fn task_manifest_actions(store: &Path, task: &str) -> Result<Vec<CacheDigest>> {
2685 validate_task_identity(task)?;
2686 let path = task_manifest_dir(store).join(format!("{task}.json"));
2687 let bytes = match fs::read(&path) {
2688 Ok(bytes) => bytes,
2689 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
2690 Err(error) => {
2691 return Err(error).wrap_err_with(|| format!("failed to read {}", path.display()));
2692 }
2693 };
2694 let Ok(manifest) = serde_json::from_slice::<TaskActionManifest>(&bytes) else {
2695 return Ok(Vec::new());
2696 };
2697 if validate_task_manifest(&manifest, task).is_err() {
2698 return Ok(Vec::new());
2699 }
2700 Ok(manifest
2701 .predictions
2702 .into_iter()
2703 .map(|prediction| prediction.action)
2704 .collect())
2705}
2706
2707fn validate_action_prediction(prediction: &ActionPrediction) -> Result<()> {
2708 if prediction.validate() {
2709 Ok(())
2710 } else {
2711 bail!("invalid action prediction")
2712 }
2713}
2714
2715fn validate_task_manifest(manifest: &TaskActionManifest, task: &str) -> Result<()> {
2716 if manifest.task == task && manifest.validate() {
2717 Ok(())
2718 } else {
2719 bail!("invalid task action manifest")
2720 }
2721}
2722
2723fn merge_task_manifests(
2724 task: &str,
2725 base: Option<TaskActionManifest>,
2726 update: TaskActionManifest,
2727) -> Result<TaskActionManifest> {
2728 validate_task_manifest(&update, task)?;
2729 let mut predictions = BTreeMap::new();
2730 if let Some(base) = base {
2731 validate_task_manifest(&base, task)?;
2732 predictions.extend(
2733 base.predictions
2734 .into_iter()
2735 .map(|prediction| (prediction.invocation.clone(), prediction)),
2736 );
2737 }
2738 predictions.extend(
2739 update
2740 .predictions
2741 .into_iter()
2742 .map(|prediction| (prediction.invocation.clone(), prediction)),
2743 );
2744 let manifest = TaskActionManifest {
2745 version: TASK_ACTION_MANIFEST_VERSION,
2746 task: task.to_owned(),
2747 predictions: predictions.into_values().collect(),
2748 };
2749 validate_task_manifest(&manifest, task)?;
2750 Ok(manifest)
2751}
2752
2753fn merge_remote_task_manifest(
2754 task: &str,
2755 remote: TaskActionManifest,
2756 local: TaskActionManifest,
2757) -> (TaskActionManifest, bool) {
2758 match merge_task_manifests(task, Some(remote), local.clone()) {
2759 Ok(manifest) => (manifest, true),
2760 Err(error) => {
2761 warn!("remote task action manifest merge failed for {task}: {error}");
2762 (local, false)
2763 }
2764 }
2765}
2766
2767async fn send_response(
2768 writer: &mut (impl AsyncWrite + Unpin),
2769 response: &AgentResponse,
2770) -> Result<()> {
2771 let mut encoded = serde_json::to_vec(response)?;
2772 encoded.push(b'\n');
2773 writer.write_all(&encoded).await?;
2774 writer.flush().await?;
2775 Ok(())
2776}
2777
2778#[cfg(test)]
2779#[path = "agent_tests.rs"]
2780mod tests;