1use crate::uploads::{ConnectionUploads, UploadQueue, UploadSink};
2use crate::{
3 ActionPrediction, ActionPromiseCompletion, ActionPromiseState, CacheDigest, CacheDirectory,
4 LocalActionCache, LocalCas, MAX_STAGED_BLOB_PACK_BYTES, 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::{debug, 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, SystemTime};
18
19const ACTION_PROMISE_WAIT: Duration = Duration::from_secs(60 * 60);
23use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
24
25mod file_digest;
26mod manifest;
27mod prefetch;
28mod stats;
29mod wire;
30
31#[cfg(test)]
32pub(crate) use prefetch::select_prefetch_actions;
33
34pub use file_digest::{
35 FileDigestCache, FileDigestResolution, FileDigestScope, FileIdentity, FileObjectIdentity,
36 FileSnapshot, NoFileDigestCache, RecordedFileDigest, ValidatedFileDigest, digest_file,
37 digest_file_validated,
38};
39pub use manifest::{
40 is_task_identity, task_manifest_actions,
41 update_task_predictions_in_order as merge_task_action_predictions,
42};
43use manifest::{
44 merge_task_manifests, task_manifest_dir, update_task_predictions, validate_task_identity,
45 validate_task_manifest,
46};
47pub use stats::{AgentStats, CompilerStats};
48use wire::MAX_REQUEST_BYTES;
49pub use wire::{
50 AGENT_PROTOCOL_VERSION, ActionDiagnostic, AgentEvent, AgentEventObserver, AgentRequest,
51 AgentResponse, PinnedFile, PinnedState, RestoreStats, WrapperSpan, WrapperTiming,
52};
53
54const MAX_EXECUTABLE_IDENTITIES: usize = 64;
55const MAX_EXECUTABLE_IDENTITY_SIZE: usize = 64 * 1024;
56const MAX_EXECUTABLE_IDENTITY_BYTES: usize = 256 * 1024;
57const TASK_ACTION_MANIFEST_VERSION: u8 = 1;
58const MAX_TASK_ACTION_PREDICTIONS: usize = 16 * 1024;
59const TASK_ACTION_LRU_REFRESH_MARGIN: usize = MAX_TASK_ACTION_PREDICTIONS / 16;
60const MAX_DIAGNOSTIC_BYTES: usize = 4 * 1024;
62const MAX_DIAGNOSTICS: usize = 128;
68const ACTION_DIAGNOSTIC_PREFIX: &str = "@mbx-action-diagnostic\t";
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
71enum DiagnosticLevel {
72 Warning,
73 Error,
74}
75
76impl DiagnosticLevel {
77 fn label(self) -> &'static str {
78 match self {
79 Self::Warning => "warning",
80 Self::Error => "error",
81 }
82 }
83}
84
85const MAX_FILE_DIGEST_BATCH: usize = 16 * 1024;
87const MAX_FILE_DIGEST_ENTRIES: usize = 1024 * 1024;
92const MAX_CONCURRENT_FILE_DIGESTS: usize = 8;
95const MAX_REMOTE_TRANSFERS: usize = 64;
96const MAX_PREFETCH_TRANSFERS: usize = 48;
97const MAX_CONCURRENT_BLOB_PACKS: usize = 8;
103const MAX_PREFETCH_ACTIONS: usize = 1024;
114const MAX_PREFETCH_ACTION_WAVE: usize = 32;
119const MAX_PREFETCH_BATCH_LOOKUPS: usize = 1;
125const PREFETCH_ACTION_BATCH_DELAY: Duration = Duration::from_millis(5);
126const MAX_PREFETCH_DIRECTORY_OBJECTS: usize = 100_000;
127const MAX_PREFETCH_OBJECTS_PER_WAVE: usize = 100_000;
128const DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES: u64 = 5 * 1024 * 1024 * 1024;
129
130pub type TaskFallbacks = Arc<dyn Fn() -> Vec<String> + Send + Sync>;
133type FileDigestFlightKey = (FileDigestScope, FileIdentity);
134type FileDigestFlights = BTreeMap<FileDigestFlightKey, Weak<FileDigestFlight>>;
135
136struct FileDigestFlight {
137 lock: tokio::sync::Mutex<()>,
138 resolution: Mutex<Option<FileDigestResolution>>,
139}
140
141const NEWEST_MANIFEST_CANDIDATES: usize = 8;
153
154pub struct AgentRemoteCache {
156 pub client: RemoteCacheClient,
158 pub mode: RemoteCacheMode,
160 pub staging_dir: PathBuf,
162}
163
164#[derive(Default)]
165struct AtomicAgentStats {
166 wrapper_phases_ns: Mutex<BTreeMap<String, u64>>,
167 lookups: AtomicU64,
168 unconsulted: AtomicU64,
169 incremental_compilations: AtomicU64,
170 hits: AtomicU64,
171 stores: AtomicU64,
172 stored_bytes: AtomicU64,
173 verifications: AtomicU64,
174 divergences: AtomicU64,
175 downloaded_bytes: AtomicU64,
176 uploaded_bytes: AtomicU64,
177 background_uploads: AtomicU64,
178 background_upload_failures: AtomicU64,
179 remote_blob_pack_uploads: AtomicU64,
180 remote_blob_pack_upload_blobs: AtomicU64,
181 upload_drain_duration_ns: AtomicU64,
182 prefetched_actions: AtomicU64,
183 predictions_loaded: AtomicU64,
184 remote_failures: AtomicU64,
185 remote_manifest_lookups: AtomicU64,
186 remote_manifest_lookup_duration_ns: AtomicU64,
187 remote_action_lookups: AtomicU64,
188 remote_action_lookup_duration_ns: AtomicU64,
189 remote_blob_requests: AtomicU64,
190 remote_blob_pack_requests: AtomicU64,
191 remote_blob_pack_blobs: AtomicU64,
192 remote_blob_transfer_duration_ns: AtomicU64,
193 local_cas_write_duration_ns: AtomicU64,
194 prefetch_runs: AtomicU64,
195 prefetch_duration_ns: AtomicU64,
196 materialization_duration_ns: AtomicU64,
197 bypasses: Mutex<BTreeMap<String, u64>>,
198 avoided_compiler_duration_ns: AtomicU64,
199 compiler: Mutex<BTreeMap<String, CompilerStats>>,
200 unit_outcomes: Mutex<BTreeMap<String, BTreeSet<String>>>,
201 slow_compilations: Mutex<BTreeMap<String, u64>>,
202 restored_output_files: AtomicU64,
203 restored_output_bytes: AtomicU64,
204 reflinked_output_files: AtomicU64,
205 reflinked_output_bytes: AtomicU64,
206 copied_output_files: AtomicU64,
207 copied_output_bytes: AtomicU64,
208 reused_output_files: AtomicU64,
209 reused_output_bytes: AtomicU64,
210}
211
212struct AtomicDurationTimer<'a> {
213 started: Instant,
214 target: &'a AtomicU64,
215}
216
217impl<'a> AtomicDurationTimer<'a> {
218 fn start(target: &'a AtomicU64) -> Self {
219 Self {
220 started: Instant::now(),
221 target,
222 }
223 }
224}
225
226impl Drop for AtomicDurationTimer<'_> {
227 fn drop(&mut self) {
228 atomic_saturating_add(self.target, duration_ns(self.started));
229 }
230}
231
232fn duration_ns(started: Instant) -> u64 {
233 started.elapsed().as_nanos().try_into().unwrap_or(u64::MAX)
234}
235
236fn validate_crate_name(crate_name: Option<&str>) -> Result<()> {
237 if let Some(crate_name) = crate_name
238 && (crate_name.len() > 256 || crate_name.contains(['\0', '\n', '\r']))
239 {
240 bail!("invalid compiler crate name");
241 }
242 Ok(())
243}
244
245#[derive(serde::Deserialize)]
246struct ActionDiagnosticEnvelope {
247 outcome: String,
248 crate_name: Option<String>,
249 diagnostic: ActionDiagnostic,
250}
251
252pub(crate) fn lookup_outcome(outcome: &str) -> (&str, bool) {
259 match outcome {
260 "incremental-miss" => ("miss", true),
261 "incremental-unconsulted" => ("unconsulted", true),
262 other => (other, false),
263 }
264}
265
266fn parse_action_diagnostic(
267 message: &str,
268) -> Option<Result<(String, Option<String>, ActionDiagnostic)>> {
269 let payload = message.strip_prefix(ACTION_DIAGNOSTIC_PREFIX)?;
270 Some((|| {
271 let envelope: ActionDiagnosticEnvelope = serde_json::from_str(payload)?;
272 let (outcome, _) = lookup_outcome(&envelope.outcome);
276 if !matches!(outcome, "hit" | "miss" | "unconsulted") {
277 bail!("invalid action diagnostic outcome");
278 }
279 validate_crate_name(envelope.crate_name.as_deref())?;
280 Ok((
281 outcome.to_string(),
282 envelope.crate_name,
283 envelope.diagnostic,
284 ))
285 })())
286}
287
288fn atomic_saturating_add(target: &AtomicU64, value: u64) {
289 let _ = target.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
290 Some(current.saturating_add(value))
291 });
292}
293
294fn queue_prefetch_digest(
295 verified: &BTreeMap<CacheDigest, PathBuf>,
296 pending: &mut BTreeMap<CacheDigest, ()>,
297 digest: CacheDigest,
298) {
299 if verified.contains_key(&digest) || pending.contains_key(&digest) {
300 return;
301 }
302 pending.insert(digest, ());
303}
304
305fn queue_prefetch_directory(
306 seen: &BTreeMap<CacheDigest, ()>,
307 pending: &mut BTreeMap<CacheDigest, ()>,
308 digest: CacheDigest,
309 limit: usize,
310) -> bool {
311 if seen.contains_key(&digest) || pending.contains_key(&digest) {
312 return true;
313 }
314 if seen.len().saturating_add(pending.len()) >= limit {
315 return false;
316 }
317 pending.insert(digest, ());
318 true
319}
320
321pub type TaskLoader = dyn for<'a> Fn(&'a CacheAgent, &'a str) -> BoxFuture<'a, ()> + Send + Sync;
323
324#[derive(Clone)]
330pub struct CacheAgent {
331 cas: LocalCas,
332 actions: LocalActionCache,
333 verified_blobs: Arc<Mutex<BTreeMap<CacheDigest, VerifiedBlob>>>,
334 version: Arc<str>,
335 write_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
336 action_locks: Arc<Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>>,
337 stats: Arc<AtomicAgentStats>,
338 observer: Option<Arc<dyn AgentEventObserver>>,
339 observer_emission: Arc<Mutex<()>>,
340 task_loader: Option<Arc<TaskLoader>>,
343 executable_identities: Arc<Mutex<BTreeMap<ExecutableIdentityKey, Vec<u8>>>>,
344 identity_dir: Arc<PathBuf>,
346 manifest_dir: Arc<PathBuf>,
347 task_actions: Arc<Mutex<BTreeMap<String, TaskActionState>>>,
348 task_fallbacks: Arc<Mutex<BTreeMap<String, TaskFallbacks>>>,
350 next_task_run: Arc<AtomicU64>,
351 manifest_write_lock: Arc<Mutex<()>>,
352 remote: Option<Arc<RemoteCacheClient>>,
353 remote_mode: RemoteCacheMode,
354 remote_staging_dir: Arc<PathBuf>,
355 remote_download_limit: u64,
356 remote_download_bytes: Arc<AtomicU64>,
357 pending_remote_actions: Arc<Mutex<BTreeMap<CacheDigest, RemoteActionResult>>>,
358 remote_transfers: Arc<tokio::sync::Semaphore>,
359 prefetch_transfers: Arc<tokio::sync::Semaphore>,
360 prefetch_tasks: Arc<Mutex<Vec<tokio::task::JoinHandle<()>>>>,
361 diagnostics: Arc<Mutex<BTreeSet<(DiagnosticLevel, String)>>>,
366 file_digests: Arc<Mutex<BTreeMap<(FileDigestScope, PathBuf), RecordedFileDigest>>>,
369 file_digest_locks: Arc<Mutex<FileDigestFlights>>,
371 file_digest_permits: Arc<tokio::sync::Semaphore>,
372 #[cfg(test)]
373 file_digest_reads: Arc<AtomicU64>,
374 uploads: Option<UploadQueue>,
376}
377
378fn validate_file_digest_record(entry: &RecordedFileDigest) -> Result<()> {
381 if !entry.file.path.is_absolute() {
382 bail!("file-digest records need absolute paths");
383 }
384 entry.digest.validate()?;
385 if entry.file.len != entry.digest.size {
386 bail!("file-digest record length does not match its digest");
387 }
388 Ok(())
389}
390
391struct AgentUploadSink {
393 stats: Arc<AtomicAgentStats>,
394}
395
396impl UploadSink for AgentUploadSink {
397 fn record_blob_uploaded(&self, bytes: u64) {
398 self.stats
399 .background_uploads
400 .fetch_add(1, Ordering::Relaxed);
401 self.stats
402 .uploaded_bytes
403 .fetch_add(bytes, Ordering::Relaxed);
404 }
405
406 fn record_action_uploaded(&self) {
407 self.stats
408 .background_uploads
409 .fetch_add(1, Ordering::Relaxed);
410 }
411
412 fn record_blob_pack_uploaded(&self, blobs: u64) {
413 self.stats
414 .remote_blob_pack_uploads
415 .fetch_add(1, Ordering::Relaxed);
416 self.stats
417 .remote_blob_pack_upload_blobs
418 .fetch_add(blobs, Ordering::Relaxed);
419 }
420
421 fn record_upload_failure(&self) {
422 self.stats
423 .background_upload_failures
424 .fetch_add(1, Ordering::Relaxed);
425 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
426 }
427}
428
429#[derive(Debug, Clone)]
435struct VerifiedBlob {
436 path: PathBuf,
437 len: u64,
438 modified: SystemTime,
439}
440
441impl VerifiedBlob {
442 fn describe(path: &Path) -> Option<Self> {
445 let metadata = std::fs::metadata(path).ok()?;
446 Some(Self {
447 path: path.to_path_buf(),
448 len: metadata.len(),
449 modified: metadata.modified().ok()?,
450 })
451 }
452
453 fn is_unchanged(&self) -> bool {
455 let Ok(metadata) = std::fs::metadata(&self.path) else {
456 return false;
457 };
458 metadata.len() == self.len && metadata.modified().is_ok_and(|now| now == self.modified)
459 }
460}
461
462#[derive(Debug, Clone, Default)]
463struct TaskActionState {
464 manifest: String,
465 baseline_loaded: bool,
466 predictions: BTreeMap<CacheDigest, ActionPrediction>,
467 pending_predictions: BTreeMap<CacheDigest, ActionPrediction>,
468 changed_predictions: BTreeMap<CacheDigest, Option<ActionPrediction>>,
475 remote_predictions: Option<BTreeMap<CacheDigest, ActionPrediction>>,
480 remote_manifest: Option<TaskActionManifest>,
484 remote_publication_needed: bool,
488 prefetched_adapters: BTreeSet<String>,
489 remote_etag: Option<String>,
490}
491
492fn activate_prediction_adapter(
494 state: &mut TaskActionState,
495 invocation: &CacheDigest,
496) -> (Option<ActionPrediction>, Option<Vec<ActionPrediction>>) {
497 let prediction = state.predictions.get(invocation).cloned();
498 let prefetch = prediction.as_ref().and_then(|prediction| {
499 if !state.prefetched_adapters.insert(prediction.adapter.clone()) {
500 return None;
501 }
502 Some(
503 state
504 .predictions
505 .values()
506 .filter(|candidate| candidate.adapter == prediction.adapter)
507 .cloned()
508 .collect(),
509 )
510 });
511 (prediction, prefetch)
512}
513
514fn required_local_predictions(
517 predictions: &[ActionPrediction],
518 updates: &BTreeMap<CacheDigest, ActionPrediction>,
519 concurrent_updates: &BTreeMap<CacheDigest, ActionPrediction>,
520) -> BTreeSet<CacheDigest> {
521 let mut required: BTreeSet<_> = updates.keys().cloned().collect();
522 for prediction in predictions.iter().rev() {
523 if required.len() == MAX_TASK_ACTION_PREDICTIONS {
524 break;
525 }
526 if concurrent_updates.contains_key(&prediction.invocation) {
527 required.insert(prediction.invocation.clone());
528 }
529 }
530 required
531}
532
533fn concurrent_task_predictions(
536 predictions: &[ActionPrediction],
537 state: &TaskActionState,
538) -> BTreeMap<CacheDigest, ActionPrediction> {
539 predictions
540 .iter()
541 .filter(|prediction| {
542 let baseline = match state.changed_predictions.get(&prediction.invocation) {
543 Some(baseline) => baseline.as_ref(),
544 None => state.predictions.get(&prediction.invocation),
545 };
546 baseline != Some(*prediction)
547 })
548 .map(|prediction| (prediction.invocation.clone(), prediction.clone()))
549 .collect()
550}
551
552fn effective_task_updates(
557 predictions: &[ActionPrediction],
558 state: &TaskActionState,
559) -> BTreeMap<CacheDigest, ActionPrediction> {
560 let current: BTreeMap<_, _> = predictions
561 .iter()
562 .map(|prediction| (prediction.invocation.clone(), prediction))
563 .collect();
564 state
565 .pending_predictions
566 .iter()
567 .map(|(invocation, update)| {
568 let unchanged_hit = !state.changed_predictions.contains_key(invocation);
569 let effective = if unchanged_hit {
570 current.get(invocation).copied().unwrap_or(update).clone()
571 } else {
572 update.clone()
573 };
574 (invocation.clone(), effective)
575 })
576 .collect()
577}
578
579fn should_refresh_task_prediction_order(prediction_count: usize, has_recorded: bool) -> bool {
580 has_recorded && prediction_count >= MAX_TASK_ACTION_PREDICTIONS - TASK_ACTION_LRU_REFRESH_MARGIN
581}
582
583fn replaced_task_predictions(
586 remote: &BTreeMap<CacheDigest, ActionPrediction>,
587 candidates: &BTreeSet<CacheDigest>,
588) -> Vec<ActionPrediction> {
589 candidates
590 .iter()
591 .filter_map(|invocation| remote.get(invocation).cloned())
592 .collect()
593}
594
595fn remote_publication_precedence(
601 state: &TaskActionState,
602 completed: &[ActionPrediction],
603 concurrent_updates: &BTreeMap<CacheDigest, ActionPrediction>,
604 proven_uploads: &BTreeSet<CacheDigest>,
605 published: &BTreeMap<CacheDigest, &ActionPrediction>,
606 withheld: &BTreeSet<CacheDigest>,
607) -> (BTreeSet<CacheDigest>, BTreeSet<CacheDigest>) {
608 let publication_base_invocations: BTreeSet<_> = state
609 .remote_manifest
610 .iter()
611 .flat_map(|manifest| manifest.predictions.iter())
612 .map(|prediction| prediction.invocation.clone())
613 .collect();
614 let unchanged_hits: BTreeSet<_> = completed
615 .iter()
616 .filter(|prediction| {
617 !state
618 .changed_predictions
619 .contains_key(&prediction.invocation)
620 })
621 .map(|prediction| prediction.invocation.clone())
622 .collect();
623 let required = completed
624 .iter()
625 .filter(|prediction| !unchanged_hits.contains(&prediction.invocation))
626 .chain(concurrent_updates.values().filter(|prediction| {
627 state.remote_predictions.is_some() || proven_uploads.contains(&prediction.action)
628 }))
629 .filter(|prediction| {
630 !withheld.contains(&prediction.invocation)
631 && published.get(&prediction.invocation).copied() == Some(*prediction)
632 })
633 .map(|prediction| prediction.invocation.clone())
634 .collect();
635 let fallbacks = withheld
636 .iter()
637 .filter(|invocation| published.contains_key(*invocation))
638 .cloned()
639 .chain(
640 unchanged_hits
641 .into_iter()
642 .filter(|invocation| published.contains_key(invocation)),
643 )
644 .chain(
645 concurrent_updates
646 .iter()
647 .filter(|(invocation, prediction)| {
648 state.remote_predictions.is_none()
649 && !proven_uploads.contains(&prediction.action)
650 && published.contains_key(*invocation)
651 })
652 .map(|(invocation, _)| invocation.clone()),
653 )
654 .chain(
655 published
656 .keys()
657 .filter(|invocation| {
658 state.remote_predictions.is_none()
659 && state.remote_manifest.is_some()
660 && !publication_base_invocations.contains(*invocation)
661 })
662 .cloned(),
663 )
664 .collect();
665 (required, fallbacks)
666}
667
668fn unpublished_task_actions(
671 mut unproven: BTreeSet<CacheDigest>,
672 held: &BTreeSet<CacheDigest>,
673 failed: BTreeSet<CacheDigest>,
674) -> BTreeSet<CacheDigest> {
675 unproven.retain(|action| !held.contains(action));
676 unproven.extend(failed);
677 unproven
678}
679
680fn unproven_task_predictions<'a>(
682 predictions: impl IntoIterator<Item = &'a ActionPrediction>,
683 remote: &BTreeMap<CacheDigest, ActionPrediction>,
684) -> BTreeMap<CacheDigest, CacheDigest> {
685 predictions
686 .into_iter()
687 .filter(|prediction| remote.get(&prediction.invocation) != Some(*prediction))
688 .map(|prediction| (prediction.invocation.clone(), prediction.action.clone()))
689 .collect()
690}
691
692fn withhold_unpublished_updates(
695 manifest: &mut TaskActionManifest,
696 candidates: &BTreeSet<CacheDigest>,
697 replaced: &[ActionPrediction],
698 unpublished: &BTreeSet<CacheDigest>,
699) -> BTreeSet<CacheDigest> {
700 let withheld: BTreeSet<_> = manifest
701 .predictions
702 .iter()
703 .filter(|prediction| {
704 candidates.contains(&prediction.invocation) && unpublished.contains(&prediction.action)
705 })
706 .map(|prediction| prediction.invocation.clone())
707 .collect();
708 manifest
709 .predictions
710 .retain(|prediction| !withheld.contains(&prediction.invocation));
711 let mut retained: BTreeSet<_> = manifest
712 .predictions
713 .iter()
714 .map(|prediction| prediction.invocation.clone())
715 .collect();
716 for previous in replaced.iter().rev() {
717 if withheld.contains(&previous.invocation) && retained.insert(previous.invocation.clone()) {
718 manifest.predictions.push(previous.clone());
722 }
723 }
724 withheld
725}
726
727struct PrefetchedAction {
728 adapter: String,
729 result: RemoteActionResult,
730}
731
732struct RemoteDownloadReservation {
733 counter: Arc<AtomicU64>,
734 reserved: u64,
735 committed: bool,
736}
737
738impl RemoteDownloadReservation {
739 fn bytes(&self) -> u64 {
740 self.reserved
741 }
742
743 fn commit(mut self, bytes: u64) {
744 debug_assert!(bytes <= self.reserved);
745 self.counter
746 .fetch_sub(self.reserved.saturating_sub(bytes), Ordering::AcqRel);
747 self.committed = true;
748 }
749}
750
751impl Drop for RemoteDownloadReservation {
752 fn drop(&mut self) {
753 if !self.committed {
754 self.counter.fetch_sub(self.reserved, Ordering::AcqRel);
755 }
756 }
757}
758
759#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
760struct ExecutableIdentityKey {
761 executable: PathBuf,
762 environment: BTreeMap<String, Option<String>>,
763}
764
765const PERSISTED_EXECUTABLE_IDENTITY_VERSION: u8 = 2;
767
768#[derive(Debug, Serialize, Deserialize)]
774struct PersistedExecutableIdentity {
775 version: u8,
776 executable: PathBuf,
777 environment: BTreeMap<String, Option<String>>,
778 pins: Vec<PinnedFile>,
779 stdout: String,
780}
781
782impl CacheAgent {
783 pub fn new(cache_dir: impl Into<PathBuf>, version: impl Into<Arc<str>>) -> Self {
785 Self::build(cache_dir.into(), version.into(), None, 0)
786 }
787
788 pub fn new_remote(
790 cache_dir: impl Into<PathBuf>,
791 version: impl Into<Arc<str>>,
792 remote: AgentRemoteCache,
793 ) -> Self {
794 Self::build(
795 cache_dir.into(),
796 version.into(),
797 Some(remote),
798 DEFAULT_MAX_REMOTE_DOWNLOAD_BYTES,
799 )
800 }
801
802 pub fn new_remote_with_download_limit(
804 cache_dir: impl Into<PathBuf>,
805 version: impl Into<Arc<str>>,
806 remote: AgentRemoteCache,
807 max_remote_download_bytes: u64,
808 ) -> Self {
809 Self::build(
810 cache_dir.into(),
811 version.into(),
812 Some(remote),
813 max_remote_download_bytes,
814 )
815 }
816
817 fn build(
818 cache_dir: PathBuf,
819 version: Arc<str>,
820 remote: Option<AgentRemoteCache>,
821 remote_download_limit: u64,
822 ) -> Self {
823 let remote_mode = remote
824 .as_ref()
825 .map_or(RemoteCacheMode::ReadOnly, |remote| remote.mode);
826 let remote_staging_dir = remote.as_ref().map_or_else(
827 || cache_dir.join("remote"),
828 |remote| remote.staging_dir.clone(),
829 );
830 let remote = remote.map(|remote| Arc::new(remote.client));
831 let stats = Arc::new(AtomicAgentStats::default());
832 let remote_transfers = Arc::new(tokio::sync::Semaphore::new(MAX_REMOTE_TRANSFERS));
833 let uploads = remote
836 .clone()
837 .filter(|_| remote_mode.writes())
838 .map(|client| {
839 UploadQueue::new(
840 client,
841 Arc::new(AgentUploadSink {
842 stats: stats.clone(),
843 }),
844 remote_transfers.clone(),
845 )
846 });
847 Self {
848 cas: LocalCas::new(cache_dir.clone()),
849 actions: LocalActionCache::new(cache_dir.clone()),
850 verified_blobs: Arc::new(Mutex::new(BTreeMap::new())),
851 version,
852 write_locks: Arc::new(Mutex::new(BTreeMap::new())),
853 action_locks: Arc::new(Mutex::new(BTreeMap::new())),
854 stats,
855 observer: None,
856 observer_emission: Arc::new(Mutex::new(())),
857 task_loader: None,
858 executable_identities: Arc::new(Mutex::new(BTreeMap::new())),
859 identity_dir: Arc::new(cache_dir.join("executable-identities").join("v1")),
860 manifest_dir: Arc::new(task_manifest_dir(&cache_dir)),
861 task_actions: Arc::new(Mutex::new(BTreeMap::new())),
862 task_fallbacks: Arc::new(Mutex::new(BTreeMap::new())),
863 next_task_run: Arc::new(AtomicU64::new(0)),
864 manifest_write_lock: Arc::new(Mutex::new(())),
865 remote,
866 remote_mode,
867 remote_staging_dir: Arc::new(remote_staging_dir),
868 remote_download_limit,
869 remote_download_bytes: Arc::new(AtomicU64::new(0)),
870 pending_remote_actions: Arc::new(Mutex::new(BTreeMap::new())),
871 remote_transfers,
872 prefetch_transfers: Arc::new(tokio::sync::Semaphore::new(MAX_PREFETCH_TRANSFERS)),
873 prefetch_tasks: Arc::new(Mutex::new(Vec::new())),
874 diagnostics: Arc::new(Mutex::new(BTreeSet::new())),
875 file_digests: Arc::new(Mutex::new(BTreeMap::new())),
876 file_digest_locks: Arc::new(Mutex::new(BTreeMap::new())),
877 file_digest_permits: Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_FILE_DIGESTS)),
878 #[cfg(test)]
879 file_digest_reads: Arc::new(AtomicU64::new(0)),
880 uploads,
881 }
882 }
883
884 #[must_use]
886 pub fn with_observer(mut self, observer: Arc<dyn AgentEventObserver>) -> Self {
887 self.observer = Some(observer);
888 self
889 }
890
891 pub fn with_task_loader(mut self, loader: Arc<TaskLoader>) -> Self {
898 self.task_loader = Some(loader);
899 self
900 }
901
902 async fn ensure_task_loaded(&self, task: &str) {
905 let Some(loader) = &self.task_loader else {
906 return;
907 };
908 if self.task_actions.lock().unwrap().contains_key(task) {
909 return;
910 }
911 loader(self, task).await;
912 }
913
914 fn emit(&self, event: impl FnOnce() -> AgentEvent) {
915 if let Some(observer) = &self.observer {
916 let _emission = self.observer_emission.lock().unwrap();
917 observer.event(event());
918 }
919 }
920
921 fn emit_action(&self, diagnostic: Option<AgentEvent>, action: AgentEvent) {
922 if let Some(observer) = &self.observer {
923 let _emission = self.observer_emission.lock().unwrap();
924 if let Some(diagnostic) = diagnostic {
925 observer.event(diagnostic);
926 }
927 observer.event(action);
928 }
929 }
930
931 fn reserve_remote_download(&self, bytes: u64) -> Result<RemoteDownloadReservation> {
932 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
933 loop {
934 let next = current
935 .checked_add(bytes)
936 .ok_or_else(|| eyre::eyre!("remote cache download budget overflowed"))?;
937 if next > self.remote_download_limit {
938 bail!(
939 "remote cache download budget exceeded: {} bytes requested with {} of {} bytes already used",
940 bytes,
941 current,
942 self.remote_download_limit
943 );
944 }
945 match self.remote_download_bytes.compare_exchange_weak(
946 current,
947 next,
948 Ordering::AcqRel,
949 Ordering::Acquire,
950 ) {
951 Ok(_) => {
952 return Ok(RemoteDownloadReservation {
953 counter: self.remote_download_bytes.clone(),
954 reserved: bytes,
955 committed: false,
956 });
957 }
958 Err(observed) => current = observed,
959 }
960 }
961 }
962
963 fn reserve_remote_download_up_to(&self, requested: u64) -> Result<RemoteDownloadReservation> {
964 let mut current = self.remote_download_bytes.load(Ordering::Acquire);
965 loop {
966 let reserved = requested.min(self.remote_download_limit.saturating_sub(current));
967 let next = current + reserved;
968 match self.remote_download_bytes.compare_exchange_weak(
969 current,
970 next,
971 Ordering::AcqRel,
972 Ordering::Acquire,
973 ) {
974 Ok(_) => {
975 return Ok(RemoteDownloadReservation {
976 counter: self.remote_download_bytes.clone(),
977 reserved,
978 committed: false,
979 });
980 }
981 Err(observed) => current = observed,
982 }
983 }
984 }
985
986 pub async fn begin_task(&self, task: &str) -> Result<String> {
988 self.begin_task_with_remote_errors(task, false, None, true)
989 .await
990 }
991
992 pub async fn begin_task_on_prediction(&self, task: &str) -> Result<String> {
995 self.begin_task_with_remote_errors(task, false, None, false)
996 .await
997 }
998
999 pub async fn begin_session_task(&self, task: &str) -> Result<()> {
1005 self.begin_task_with_remote_errors(task, false, Some(task.to_string()), false)
1006 .await?;
1007 Ok(())
1008 }
1009
1010 pub fn register_task_fallbacks(
1021 &self,
1022 task: &str,
1023 fallbacks: impl Fn() -> Vec<String> + Send + Sync + 'static,
1024 ) -> Result<()> {
1025 validate_task_identity(task)?;
1026 self.task_fallbacks
1027 .lock()
1028 .unwrap()
1029 .insert(task.to_string(), Arc::new(fallbacks));
1030 Ok(())
1031 }
1032
1033 pub async fn prefetch_task(&self, task: &str) -> Result<String> {
1035 let run = self
1036 .begin_task_with_remote_errors(task, true, None, true)
1037 .await?;
1038 self.wait_for_prefetches().await;
1039 Ok(run)
1040 }
1041
1042 async fn begin_task_with_remote_errors(
1043 &self,
1044 task: &str,
1045 strict: bool,
1046 run: Option<String>,
1047 eager_prefetch: bool,
1048 ) -> Result<String> {
1049 validate_task_identity(task)?;
1050 let early_manifest = {
1056 let _write_guard = self.manifest_write_lock.lock().unwrap();
1057 let _file_guard = self.lock_task_manifest(task)?;
1058 self.load_task_manifest(task)?
1059 };
1060 let early_actions: BTreeSet<_> = early_manifest
1061 .iter()
1062 .flat_map(|manifest| manifest.predictions.iter())
1063 .map(|prediction| prediction.action.clone())
1064 .collect();
1065 if eager_prefetch {
1066 self.spawn_prefetch_predictions(
1067 early_manifest
1068 .as_ref()
1069 .map(|manifest| manifest.predictions.clone())
1070 .unwrap_or_default(),
1071 );
1072 }
1073 let (remote_manifest, mut remote_etag, mut remote_predictions, remote_missing) =
1074 if self.remote.is_some() && self.remote_mode.reads() {
1075 match self.get_remote_task_manifest(task).await {
1076 Ok(Some((manifest, etag))) => {
1077 let predictions = manifest
1078 .predictions
1079 .iter()
1080 .map(|prediction| (prediction.invocation.clone(), prediction.clone()))
1081 .collect();
1082 (Some(manifest), Some(etag), Some(predictions), false)
1083 }
1084 Ok(None) => (None, None, Some(BTreeMap::new()), true),
1085 Err(error) => {
1086 if strict {
1087 return Err(error).wrap_err_with(|| {
1088 format!("remote task action manifest lookup failed for {task}")
1089 });
1090 }
1091 self.note_remote_failure();
1092 warn!("remote task action manifest lookup failed for {task}: {error}");
1093 (None, None, None, false)
1094 }
1095 }
1096 } else {
1097 (None, None, None, false)
1098 };
1099 let remote_publication_base = remote_manifest.clone();
1100 let manifest = if self.remote.is_some() && self.remote_mode.reads() {
1104 let _write_guard = self.manifest_write_lock.lock().unwrap();
1105 let _file_guard = self.lock_task_manifest(task)?;
1106 let local_manifest = self.load_task_manifest(task)?;
1107 let local_baseline = local_manifest.clone();
1108 let manifest = match (remote_manifest, local_manifest) {
1109 (Some(remote), Some(local)) => {
1110 let required = local
1111 .predictions
1112 .iter()
1113 .map(|prediction| prediction.invocation.clone())
1114 .collect();
1115 match merge_task_manifests(
1116 task,
1117 remote,
1118 local.clone(),
1119 &required,
1120 &BTreeSet::new(),
1121 ) {
1122 Ok(manifest) => Some(manifest),
1123 Err(error) => {
1124 warn!("remote task action manifest merge failed for {task}: {error}");
1125 remote_etag = None;
1126 Some(local)
1127 }
1128 }
1129 }
1130 (Some(remote), None) => Some(remote),
1131 (None, local) => local,
1132 };
1133 if manifest != local_baseline
1134 && let Some(manifest) = &manifest
1135 {
1136 self.persist_task_manifest(manifest)?;
1137 }
1138 manifest
1139 } else {
1140 early_manifest
1141 };
1142 let (manifest, remote_etag, inherited) = match manifest {
1143 Some(manifest) => (Some(manifest), remote_etag, false),
1144 None => {
1147 let (manifest, inherited_provenance, inherited) =
1148 self.inherit_task_manifest(task, strict).await?;
1149 if inherited {
1150 remote_predictions = inherited_provenance;
1151 }
1152 (manifest, None, inherited)
1153 }
1154 };
1155 let mut state = if let Some(manifest) = manifest {
1156 let write_only_baseline = self.remote_mode.writes()
1157 && !self.remote_mode.reads()
1158 && !manifest.predictions.is_empty();
1159 TaskActionState {
1160 manifest: task.to_string(),
1161 baseline_loaded: true,
1162 predictions: manifest
1163 .predictions
1164 .into_iter()
1165 .map(|prediction| (prediction.invocation.clone(), prediction))
1166 .collect(),
1167 pending_predictions: BTreeMap::new(),
1168 changed_predictions: BTreeMap::new(),
1169 remote_predictions,
1170 remote_manifest: remote_publication_base,
1171 remote_publication_needed: self.remote.is_some()
1172 && self.remote_mode.writes()
1173 && (remote_missing || inherited || write_only_baseline),
1174 prefetched_adapters: BTreeSet::new(),
1175 remote_etag,
1176 }
1177 } else {
1178 TaskActionState {
1179 manifest: task.to_string(),
1180 baseline_loaded: true,
1181 remote_predictions,
1182 remote_manifest: remote_publication_base,
1183 remote_etag,
1184 ..TaskActionState::default()
1185 }
1186 };
1187 let run = run.unwrap_or_else(|| {
1188 let sequence = self.next_task_run.fetch_add(1, Ordering::Relaxed);
1189 CacheDigest::blake3(format!("{task}\0{}\0{sequence}", std::process::id()).as_bytes())
1190 .hash
1191 });
1192 self.stats.predictions_loaded.fetch_max(
1196 state.predictions.len().try_into().unwrap_or(u64::MAX),
1197 Ordering::Relaxed,
1198 );
1199 if eager_prefetch {
1200 state.prefetched_adapters.extend(
1201 state
1202 .predictions
1203 .values()
1204 .map(|prediction| prediction.adapter.clone()),
1205 );
1206 }
1207 let predictions = state
1211 .predictions
1212 .values()
1213 .filter(|prediction| !early_actions.contains(&prediction.action))
1214 .cloned()
1215 .collect();
1216 self.task_actions.lock().unwrap().insert(run.clone(), state);
1217 if eager_prefetch {
1218 self.spawn_prefetch_predictions(predictions);
1219 }
1220 Ok(run)
1221 }
1222
1223 async fn inherit_task_manifest(
1231 &self,
1232 task: &str,
1233 strict: bool,
1234 ) -> Result<(
1235 Option<TaskActionManifest>,
1236 Option<BTreeMap<CacheDigest, ActionPrediction>>,
1237 bool,
1238 )> {
1239 let fallbacks = self.task_fallbacks.lock().unwrap().get(task).cloned();
1240 let Some(fallbacks) = fallbacks else {
1241 return Ok((None, None, false));
1242 };
1243 let named = tokio::task::spawn_blocking(move || fallbacks()).await?;
1244 let mut tried = BTreeSet::from([task.to_string()]);
1245 let candidates = named.into_iter().map(|identity| (identity, true)).chain(
1246 self.newest_task_identities()
1247 .into_iter()
1248 .map(|identity| (identity, false)),
1249 );
1250 for (identity, named) in candidates {
1251 if !tried.insert(identity.clone()) || validate_task_identity(&identity).is_err() {
1252 continue;
1253 }
1254 let mut found = self.load_task_manifest(&identity)?;
1255 let mut provenance = None;
1256 if self.remote_mode.reads() && (found.is_some() || named) {
1257 match self.get_remote_task_manifest(&identity).await {
1258 Ok(Some((remote, _))) => {
1259 provenance = Some(
1260 remote
1261 .predictions
1262 .iter()
1263 .map(|prediction| {
1264 (prediction.invocation.clone(), prediction.clone())
1265 })
1266 .collect(),
1267 );
1268 if found.is_none() {
1269 found = Some(remote);
1270 }
1271 }
1272 Ok(None) => provenance = Some(BTreeMap::new()),
1273 Err(error) => {
1274 if strict && found.is_none() {
1279 return Err(error).wrap_err_with(|| {
1280 format!("remote task action manifest lookup failed for {identity}")
1281 });
1282 }
1283 self.note_remote_failure();
1284 warn!("remote task action manifest lookup failed for {identity}: {error}");
1285 }
1286 }
1287 }
1288 let Some(mut manifest) = found else {
1289 continue;
1290 };
1291 if manifest.predictions.is_empty()
1292 || (!named && manifest.predictions.len() > MAX_TASK_ACTION_PREDICTIONS / 2)
1293 {
1294 continue;
1295 }
1296 debug!(
1297 "no predictions were recorded for {task}; inheriting {} from {identity}",
1298 manifest.predictions.len()
1299 );
1300 manifest.task = task.to_string();
1301 validate_task_manifest(&manifest, task)?;
1302 let _write_guard = self.manifest_write_lock.lock().unwrap();
1303 let _file_guard = self.lock_task_manifest(task)?;
1304 if let Some(recorded) = self.load_task_manifest(task)? {
1308 return Ok((Some(recorded), None, false));
1309 }
1310 self.persist_task_manifest(&manifest)?;
1311 return Ok((Some(manifest), provenance, true));
1312 }
1313 Ok((None, None, false))
1314 }
1315
1316 fn newest_task_identities(&self) -> Vec<String> {
1318 let Ok(entries) = fs::read_dir(self.manifest_dir.as_path()) else {
1319 return Vec::new();
1320 };
1321 let mut manifests: Vec<(std::time::SystemTime, String)> = entries
1322 .filter_map(|entry| {
1323 let entry = entry.ok()?;
1324 let name = entry.file_name();
1325 let identity = name.to_str()?.strip_suffix(".json")?.to_string();
1326 validate_task_identity(&identity).ok()?;
1327 let modified = entry.metadata().ok()?.modified().ok()?;
1328 Some((modified, identity))
1329 })
1330 .collect();
1331 manifests.sort_by(|left, right| right.cmp(left));
1332 manifests
1333 .into_iter()
1334 .take(NEWEST_MANIFEST_CANDIDATES)
1335 .map(|(_, identity)| identity)
1336 .collect()
1337 }
1338
1339 pub async fn cancel_prefetches(&self) {
1341 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
1342 for task in &tasks {
1343 task.abort();
1344 }
1345 for task in tasks {
1346 if let Err(error) = task.await
1347 && !error.is_cancelled()
1348 {
1349 warn!("remote action prefetch task failed: {error}");
1350 }
1351 }
1352 }
1353
1354 pub async fn wait_for_uploads(&self) {
1361 let Some(uploads) = &self.uploads else {
1362 return;
1363 };
1364 let _timer = AtomicDurationTimer::start(&self.stats.upload_drain_duration_ns);
1365 uploads.drain().await;
1366 }
1367
1368 async fn wait_for_prefetches(&self) {
1369 let tasks = std::mem::take(&mut *self.prefetch_tasks.lock().unwrap());
1370 for task in tasks {
1371 if let Err(error) = task.await {
1372 warn!("remote action prefetch task failed: {error}");
1373 }
1374 }
1375 }
1376
1377 pub async fn commit_task(&self, run: &str) -> Result<()> {
1379 self.commit_task_actions(run).await.map(|_| ())
1380 }
1381
1382 pub async fn commit_task_actions(&self, run: &str) -> Result<Vec<ActionPrediction>> {
1388 validate_task_identity(run)?;
1389 let mut state = {
1390 let mut runs = self.task_actions.lock().unwrap();
1391 let state = runs
1392 .get(run)
1393 .ok_or_else(|| eyre::eyre!("task action manifest baseline was not loaded"))?;
1394 if !state.baseline_loaded {
1395 bail!("task action manifest baseline was not loaded");
1396 }
1397 debug!(
1402 "committing {} changed of {} recorded predictions for {}",
1403 state.changed_predictions.len(),
1404 state.pending_predictions.len(),
1405 state.manifest
1406 );
1407 let refresh_hit_order = should_refresh_task_prediction_order(
1408 state.predictions.len(),
1409 !state.pending_predictions.is_empty(),
1410 );
1411 if state.changed_predictions.is_empty()
1412 && !refresh_hit_order
1413 && !state.remote_publication_needed
1414 {
1415 let completed = state.pending_predictions.values().cloned().collect();
1416 runs.remove(run);
1417 return Ok(completed);
1418 }
1419 state.clone()
1420 };
1421 let task = state.manifest.clone();
1422 validate_task_identity(&task)?;
1423 let write_only = self.remote_mode.writes() && !self.remote_mode.reads();
1424 let write_only_remote = if write_only {
1430 match self.get_remote_task_manifest(&task).await {
1431 Ok(Some((manifest, etag))) => {
1432 let remote_predictions: BTreeMap<_, _> = manifest
1433 .predictions
1434 .iter()
1435 .map(|prediction| (prediction.invocation.clone(), prediction.clone()))
1436 .collect();
1437 let has_missing_fallback = state
1438 .predictions
1439 .keys()
1440 .any(|invocation| !remote_predictions.contains_key(invocation));
1441 state.remote_predictions = if has_missing_fallback {
1442 None
1443 } else {
1444 Some(remote_predictions)
1445 };
1446 state.remote_etag = Some(etag);
1447 state.remote_manifest = Some(manifest.clone());
1448 state.remote_publication_needed = has_missing_fallback;
1453 Some(manifest)
1454 }
1455 Ok(None) => {
1456 state.remote_predictions = if state.predictions.is_empty() {
1457 Some(BTreeMap::new())
1458 } else {
1459 None
1460 };
1461 state.remote_publication_needed = !state.predictions.is_empty();
1462 None
1463 }
1464 Err(error) => {
1465 state.remote_predictions = None;
1466 state.remote_publication_needed = false;
1467 self.note_remote_failure();
1468 warn!("remote task action manifest lookup failed for {task}: {error}");
1469 None
1470 }
1471 }
1472 } else {
1473 None
1474 };
1475 let completed = state
1476 .pending_predictions
1477 .values()
1478 .cloned()
1479 .collect::<Vec<_>>();
1480 if write_only
1481 && state.changed_predictions.is_empty()
1482 && !should_refresh_task_prediction_order(
1483 state.predictions.len(),
1484 !state.pending_predictions.is_empty(),
1485 )
1486 && !state.remote_publication_needed
1487 {
1488 self.task_actions.lock().unwrap().remove(run);
1489 return Ok(completed);
1490 }
1491 let (manifest, concurrent_updates) = {
1492 let _write_guard = self.manifest_write_lock.lock().unwrap();
1493 let _file_guard = self.lock_task_manifest(&task)?;
1494 let current_manifest = self.load_task_manifest(&task)?;
1495 let predictions = current_manifest
1496 .as_ref()
1497 .map(|manifest| manifest.predictions.clone())
1498 .unwrap_or_default();
1499 let concurrent_updates = concurrent_task_predictions(&predictions, &state);
1500 let effective_updates = effective_task_updates(&predictions, &state);
1501 let required =
1502 required_local_predictions(&predictions, &effective_updates, &concurrent_updates);
1503 let predictions = update_task_predictions(predictions, &effective_updates, &required)?;
1504 let manifest = TaskActionManifest {
1505 version: TASK_ACTION_MANIFEST_VERSION,
1506 task: task.clone(),
1507 predictions,
1508 };
1509 validate_task_manifest(&manifest, &task)?;
1510 if current_manifest.as_ref() != Some(&manifest) {
1511 self.persist_task_manifest(&manifest)?;
1512 }
1513 (manifest, concurrent_updates)
1514 };
1515 self.task_actions.lock().unwrap().remove(run);
1516 if self.remote_mode.writes() {
1517 let mut manifest = manifest;
1521 let (held, failed) = if let Some(uploads) = &self.uploads {
1522 let actions: Vec<CacheDigest> = manifest
1523 .predictions
1524 .iter()
1525 .map(|prediction| prediction.action.clone())
1526 .collect();
1527 uploads.wait_for_action_outcomes(&actions).await
1528 } else {
1529 (BTreeSet::new(), BTreeSet::new())
1530 };
1531 let proven_uploads = held.difference(&failed).cloned().collect();
1532 let (withhold_candidates, replaced, unpublished) =
1533 if let Some(remote_predictions) = &state.remote_predictions {
1534 let unproven_predictions =
1535 unproven_task_predictions(&manifest.predictions, remote_predictions);
1536 let unproven = unproven_predictions.values().cloned().collect();
1537 let candidates = unproven_predictions.keys().cloned().collect();
1538 let replaced = replaced_task_predictions(remote_predictions, &candidates);
1539 let unpublished = unpublished_task_actions(unproven, &held, failed);
1542 (candidates, replaced, unpublished)
1543 } else {
1544 let candidates: BTreeSet<_> = state
1549 .changed_predictions
1550 .keys()
1551 .filter(|invocation| {
1552 manifest
1553 .predictions
1554 .iter()
1555 .any(|prediction| &prediction.invocation == *invocation)
1556 })
1557 .cloned()
1558 .collect();
1559 let unproven = manifest
1560 .predictions
1561 .iter()
1562 .filter(|prediction| candidates.contains(&prediction.invocation))
1563 .map(|prediction| prediction.action.clone())
1564 .collect();
1565 let unpublished = unpublished_task_actions(unproven, &held, failed);
1566 (candidates, Vec::new(), unpublished)
1567 };
1568 let prediction_count = manifest.predictions.len();
1569 let withheld = withhold_unpublished_updates(
1570 &mut manifest,
1571 &withhold_candidates,
1572 &replaced,
1573 &unpublished,
1574 );
1575 if !withheld.is_empty() {
1576 warn!(
1579 "{} of {} predicted actions were not published, so the remote task action manifest omits them",
1580 withheld.len(),
1581 prediction_count
1582 );
1583 }
1584 let published: BTreeMap<_, _> = manifest
1585 .predictions
1586 .iter()
1587 .map(|prediction| (prediction.invocation.clone(), prediction))
1588 .collect();
1589 let (required, fallbacks) = remote_publication_precedence(
1590 &state,
1591 &completed,
1592 &concurrent_updates,
1593 &proven_uploads,
1594 &published,
1595 &withheld,
1596 );
1597 let publication_base = write_only_remote.or(state.remote_manifest);
1598 if let Some(remote) = publication_base {
1599 manifest = merge_task_manifests(&task, remote, manifest, &required, &fallbacks)?;
1600 }
1601 match self
1602 .put_remote_task_manifest(&task, manifest, state.remote_etag, &required, &fallbacks)
1603 .await
1604 {
1605 Ok(remote_manifest) => {
1606 let _write_guard = self.manifest_write_lock.lock().unwrap();
1607 let reconciliation = (|| {
1608 let _file_guard = self.lock_task_manifest(&task)?;
1609 let manifest = match self.load_task_manifest(&task)? {
1610 Some(local) => {
1611 let required = local
1612 .predictions
1613 .iter()
1614 .map(|prediction| prediction.invocation.clone())
1615 .collect();
1616 match merge_task_manifests(
1617 &task,
1618 remote_manifest,
1619 local.clone(),
1620 &required,
1621 &BTreeSet::new(),
1622 ) {
1623 Ok(manifest) => manifest,
1624 Err(error) => {
1625 warn!(
1626 "remote task action manifest merge failed for {task}: {error}"
1627 );
1628 local
1629 }
1630 }
1631 }
1632 None => remote_manifest,
1633 };
1634 self.persist_task_manifest(&manifest)
1635 })();
1636 if let Err(error) = reconciliation {
1637 warn!(
1638 "remote task action manifest reconciliation failed for {task}: {error}"
1639 );
1640 }
1641 }
1642 Err(error) => {
1643 self.note_remote_failure();
1644 warn!("remote task action manifest upload failed for {task}: {error}");
1645 }
1646 }
1647 }
1648 Ok(completed)
1649 }
1650
1651 fn task_manifest_path(&self, task: &str) -> PathBuf {
1652 self.manifest_dir.join(format!("{task}.json"))
1653 }
1654
1655 fn task_manifest_lock_path(&self, task: &str) -> PathBuf {
1656 self.manifest_dir.join("locks").join(format!("{task}.lock"))
1657 }
1658
1659 fn lock_task_manifest(&self, task: &str) -> Result<fslock::LockFile> {
1660 let path = self.task_manifest_lock_path(task);
1661 fs::create_dir_all(path.parent().expect("task manifest lock has a parent"))?;
1662 let mut lock = fslock::LockFile::open(&path)?;
1663 lock.lock()?;
1664 Ok(lock)
1665 }
1666
1667 fn load_task_manifest(&self, task: &str) -> Result<Option<TaskActionManifest>> {
1668 match fs::read(self.task_manifest_path(task)) {
1669 Ok(contents) => Ok(Some(self.parse_task_manifest(task, &contents, false)?)),
1670 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
1671 Err(error) => Err(error.into()),
1672 }
1673 }
1674
1675 fn parse_task_manifest(
1676 &self,
1677 task: &str,
1678 contents: &[u8],
1679 require_canonical: bool,
1680 ) -> Result<TaskActionManifest> {
1681 let manifest: TaskActionManifest = serde_json::from_slice(contents)?;
1682 validate_task_manifest(&manifest, task)?;
1683 if require_canonical && canonical_json(&manifest)? != contents {
1684 bail!("task action manifest is not canonical JSON");
1685 }
1686 Ok(manifest)
1687 }
1688
1689 fn task_manifest_selector(task: &str) -> Result<(Vec<u8>, CacheDigest)> {
1690 TaskActionManifest::selector(task)
1691 }
1692
1693 fn persist_task_manifest(&self, manifest: &TaskActionManifest) -> Result<()> {
1694 let bytes = canonical_json(manifest)?;
1695 fs::create_dir_all(self.manifest_dir.as_path())?;
1696 let mut temporary = tempfile::NamedTempFile::new_in(self.manifest_dir.as_path())?;
1697 std::io::Write::write_all(temporary.as_file_mut(), &bytes)?;
1698 temporary.as_file_mut().sync_all()?;
1699 temporary
1700 .persist(self.task_manifest_path(&manifest.task))
1701 .map_err(|error| error.error)?;
1702 Ok(())
1703 }
1704
1705 async fn get_remote_task_manifest(
1706 &self,
1707 task: &str,
1708 ) -> Result<Option<(TaskActionManifest, String)>> {
1709 let Some(remote) = &self.remote else {
1710 return Ok(None);
1711 };
1712 let (_, selector) = Self::task_manifest_selector(task)?;
1713 let _permit = self.remote_transfers.acquire().await?;
1714 self.stats
1715 .remote_manifest_lookups
1716 .fetch_add(1, Ordering::Relaxed);
1717 let _timer = AtomicDurationTimer::start(&self.stats.remote_manifest_lookup_duration_ns);
1718 let Some(remote_manifest) = remote.get_action_manifest(&selector).await? else {
1719 return Ok(None);
1720 };
1721 let manifest = self.parse_task_manifest(task, &remote_manifest.bytes, true)?;
1722 Ok(Some((manifest, remote_manifest.etag)))
1723 }
1724
1725 async fn put_remote_task_manifest(
1726 &self,
1727 task: &str,
1728 mut manifest: TaskActionManifest,
1729 mut expected_etag: Option<String>,
1730 required: &BTreeSet<CacheDigest>,
1731 fallbacks: &BTreeSet<CacheDigest>,
1732 ) -> Result<TaskActionManifest> {
1733 let Some(remote) = &self.remote else {
1734 return Ok(manifest);
1735 };
1736 let (_, selector) = Self::task_manifest_selector(task)?;
1737 for _ in 0..4 {
1738 let bytes = canonical_json(&manifest)?;
1739 let outcome = {
1740 let _permit = self.remote_transfers.acquire().await?;
1741 remote
1742 .put_action_manifest(&selector, &bytes, expected_etag.as_deref())
1743 .await?
1744 };
1745 match outcome {
1746 ManifestPutOutcome::Stored => return Ok(manifest),
1747 ManifestPutOutcome::PreconditionFailed => {
1748 let Some((remote_manifest, etag)) = self.get_remote_task_manifest(task).await?
1749 else {
1750 expected_etag = None;
1751 continue;
1752 };
1753 manifest =
1754 merge_task_manifests(task, remote_manifest, manifest, required, fallbacks)?;
1755 expected_etag = Some(etag);
1756 }
1757 }
1758 }
1759 bail!("remote task action manifest changed too frequently")
1760 }
1761
1762 pub fn stats(&self) -> AgentStats {
1764 AgentStats {
1765 session_duration_ns: 0,
1766 lookups: self.stats.lookups.load(Ordering::Relaxed),
1767 unconsulted: self.stats.unconsulted.load(Ordering::Relaxed),
1768 incremental_compilations: self.stats.incremental_compilations.load(Ordering::Relaxed),
1769 hits: self.stats.hits.load(Ordering::Relaxed),
1770 stores: self.stats.stores.load(Ordering::Relaxed),
1771 stored_bytes: self.stats.stored_bytes.load(Ordering::Relaxed),
1772 verifications: self.stats.verifications.load(Ordering::Relaxed),
1773 divergences: self.stats.divergences.load(Ordering::Relaxed),
1774 downloaded_bytes: self.stats.downloaded_bytes.load(Ordering::Relaxed),
1775 uploaded_bytes: self.stats.uploaded_bytes.load(Ordering::Relaxed),
1776 background_uploads: self.stats.background_uploads.load(Ordering::Relaxed),
1777 background_upload_failures: self
1778 .stats
1779 .background_upload_failures
1780 .load(Ordering::Relaxed),
1781 remote_blob_pack_uploads: self.stats.remote_blob_pack_uploads.load(Ordering::Relaxed),
1782 remote_blob_pack_upload_blobs: self
1783 .stats
1784 .remote_blob_pack_upload_blobs
1785 .load(Ordering::Relaxed),
1786 upload_drain_duration_ns: self.stats.upload_drain_duration_ns.load(Ordering::Relaxed),
1787 prefetched_actions: self.stats.prefetched_actions.load(Ordering::Relaxed),
1788 predictions_loaded: self.stats.predictions_loaded.load(Ordering::Relaxed),
1789 bypasses: self.stats.bypasses.lock().unwrap().clone(),
1790 avoided_compiler_duration_ns: self
1791 .stats
1792 .avoided_compiler_duration_ns
1793 .load(Ordering::Relaxed),
1794 wrapper_phases_ns: self.stats.wrapper_phases_ns.lock().unwrap().clone(),
1795 compiler: self.stats.compiler.lock().unwrap().clone(),
1796 slow_compilations: self.stats.slow_compilations.lock().unwrap().clone(),
1797 unit_outcomes: self.stats.unit_outcomes.lock().unwrap().clone(),
1798 remote_failures: self.stats.remote_failures.load(Ordering::Relaxed),
1799 remote_manifest_lookups: self.stats.remote_manifest_lookups.load(Ordering::Relaxed),
1800 remote_manifest_lookup_duration_ns: self
1801 .stats
1802 .remote_manifest_lookup_duration_ns
1803 .load(Ordering::Relaxed),
1804 remote_action_lookups: self.stats.remote_action_lookups.load(Ordering::Relaxed),
1805 remote_action_lookup_duration_ns: self
1806 .stats
1807 .remote_action_lookup_duration_ns
1808 .load(Ordering::Relaxed),
1809 remote_blob_requests: self.stats.remote_blob_requests.load(Ordering::Relaxed),
1810 remote_blob_pack_requests: self.stats.remote_blob_pack_requests.load(Ordering::Relaxed),
1811 remote_blob_pack_blobs: self.stats.remote_blob_pack_blobs.load(Ordering::Relaxed),
1812 remote_blob_transfer_duration_ns: self
1813 .stats
1814 .remote_blob_transfer_duration_ns
1815 .load(Ordering::Relaxed),
1816 local_cas_write_duration_ns: self
1817 .stats
1818 .local_cas_write_duration_ns
1819 .load(Ordering::Relaxed),
1820 prefetch_runs: self.stats.prefetch_runs.load(Ordering::Relaxed),
1821 prefetch_duration_ns: self.stats.prefetch_duration_ns.load(Ordering::Relaxed),
1822 materialization_duration_ns: self
1823 .stats
1824 .materialization_duration_ns
1825 .load(Ordering::Relaxed),
1826 restored_output_files: self.stats.restored_output_files.load(Ordering::Relaxed),
1827 restored_output_bytes: self.stats.restored_output_bytes.load(Ordering::Relaxed),
1828 reflinked_output_files: self.stats.reflinked_output_files.load(Ordering::Relaxed),
1829 reflinked_output_bytes: self.stats.reflinked_output_bytes.load(Ordering::Relaxed),
1830 copied_output_files: self.stats.copied_output_files.load(Ordering::Relaxed),
1831 copied_output_bytes: self.stats.copied_output_bytes.load(Ordering::Relaxed),
1832 reused_output_files: self.stats.reused_output_files.load(Ordering::Relaxed),
1833 reused_output_bytes: self.stats.reused_output_bytes.load(Ordering::Relaxed),
1834 }
1835 }
1836
1837 pub async fn handle_requests(
1844 &self,
1845 requests: impl IntoIterator<Item = AgentRequest>,
1846 ) -> Vec<AgentResponse> {
1847 let mut connection = ConnectionUploads::default();
1848 let mut responses = Vec::new();
1849 for request in requests {
1850 responses.push(self.respond_on(request, &mut connection).await);
1851 }
1852 responses
1853 }
1854
1855 fn write_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1856 Self::digest_lock(&self.write_locks, digest)
1857 }
1858
1859 fn action_lock(&self, digest: &CacheDigest) -> Arc<tokio::sync::Mutex<()>> {
1860 Self::digest_lock(&self.action_locks, digest)
1861 }
1862
1863 fn digest_lock(
1864 locks: &Mutex<BTreeMap<CacheDigest, Weak<tokio::sync::Mutex<()>>>>,
1865 digest: &CacheDigest,
1866 ) -> Arc<tokio::sync::Mutex<()>> {
1867 let mut locks = locks.lock().unwrap();
1868 locks.retain(|_, lock| lock.strong_count() > 0);
1869 if let Some(lock) = locks.get(digest).and_then(Weak::upgrade) {
1870 return lock;
1871 }
1872 let lock = Arc::new(tokio::sync::Mutex::new(()));
1873 locks.insert(digest.clone(), Arc::downgrade(&lock));
1874 lock
1875 }
1876
1877 #[cfg(test)]
1882 async fn respond(&self, request: AgentRequest) -> AgentResponse {
1883 self.respond_on(request, &mut ConnectionUploads::default())
1884 .await
1885 }
1886
1887 async fn respond_on(
1888 &self,
1889 request: AgentRequest,
1890 connection: &mut ConnectionUploads,
1891 ) -> AgentResponse {
1892 match &request {
1893 AgentRequest::FindActionPrediction { task, .. }
1894 | AgentRequest::RecordActionPrediction { task, .. } => {
1895 self.ensure_task_loaded(task).await;
1896 }
1897 _ => {}
1898 }
1899 let result = match request {
1900 AgentRequest::BeginTask { task } => self
1901 .begin_task(&task)
1902 .await
1903 .map(|run| AgentResponse::TaskBegun { run }),
1904 AgentRequest::CommitTask { run } => self
1905 .commit_task(&run)
1906 .await
1907 .map(|()| AgentResponse::TaskCommitted),
1908 AgentRequest::FindBlob { digest } => self.find_blob(&digest).await,
1909 AgentRequest::FindBlobs { digests } => self.find_blobs(digests).await,
1910 AgentRequest::StoreBlob { digest, source } => {
1911 self.store_blob(&digest, &source, connection).await
1912 }
1913 AgentRequest::FindActionResult { action } => {
1914 self.stats.lookups.fetch_add(1, Ordering::Relaxed);
1915 self.find_action_result(&action).await
1916 }
1917 AgentRequest::RecordActionHit {
1918 action,
1919 restore,
1920 crate_name,
1921 } => {
1922 let diagnostic = connection
1923 .take_action_diagnostic("hit", crate_name.as_deref())
1924 .map(|diagnostic| AgentEvent::ActionDiagnostic {
1925 outcome: "hit".into(),
1926 crate_name: crate_name.clone(),
1927 diagnostic,
1928 });
1929 self.record_action_hit(&action, restore, crate_name, diagnostic)
1930 }
1931 AgentRequest::RecordBypass { kind } => {
1932 *self
1933 .stats
1934 .bypasses
1935 .lock()
1936 .unwrap()
1937 .entry(kind.clone())
1938 .or_insert(0) += 1;
1939 self.emit(|| AgentEvent::Bypass { kind });
1940 Ok(AgentResponse::BypassRecorded)
1941 }
1942 AgentRequest::RecordUnconsulted => {
1943 self.stats.unconsulted.fetch_add(1, Ordering::Relaxed);
1944 self.emit(|| AgentEvent::Unconsulted);
1945 Ok(AgentResponse::UnconsultedRecorded)
1946 }
1947 AgentRequest::RecordWarning { message } => match parse_action_diagnostic(&message) {
1948 Some(Ok((outcome, crate_name, diagnostic))) => {
1949 connection.record_action_diagnostic(outcome, crate_name, diagnostic);
1950 Ok(AgentResponse::WarningRecorded)
1951 }
1952 Some(Err(error)) => Err(error),
1953 None => self.record_diagnostic(DiagnosticLevel::Warning, message),
1954 },
1955 AgentRequest::RecordDebug { target, message } => (|| {
1956 if target.is_empty()
1957 || target.len() > 256
1958 || target.contains(['\n', '\r', '\0'])
1959 || message.is_empty()
1960 || message.len() > MAX_DIAGNOSTIC_BYTES
1961 || message.contains(['\n', '\r', '\0'])
1962 {
1963 bail!("invalid shim debug record");
1964 }
1965 if target == "mbx::unit-outcome" {
1966 let (unit, outcome): (String, String) = serde_json::from_str(&message)?;
1967 let (outcome, _) = lookup_outcome(&outcome);
1971 if unit.len() > 256
1972 || !unit.contains(':')
1973 || !matches!(
1974 outcome,
1975 "hit" | "miss" | "bypass" | "unconsulted" | "verification"
1976 )
1977 {
1978 bail!("invalid unit outcome");
1979 }
1980 self.stats
1981 .unit_outcomes
1982 .lock()
1983 .unwrap()
1984 .entry(unit)
1985 .or_default()
1986 .insert(outcome.to_string());
1987 } else {
1988 debug!(target: &target, "{message}");
1989 }
1990 Ok(AgentResponse::DebugRecorded)
1991 })(),
1992 AgentRequest::RecordError { message } => {
1993 self.record_diagnostic(DiagnosticLevel::Error, message)
1994 }
1995 AgentRequest::FindFileDigests { scope, files } => self.find_file_digests(scope, files),
1996 AgentRequest::JoinActionPromise {
1997 adapter,
1998 invocation,
1999 } => self.join_action_promise(&adapter, &invocation).await,
2000 AgentRequest::CompleteActionPromise { claim, prediction } => {
2001 self.complete_action_promise(&claim, &prediction).await
2002 }
2003 AgentRequest::ResolveFileDigests { scope, files } => {
2004 self.resolve_file_digests(scope, files).await
2005 }
2006 AgentRequest::RecordFileDigests { scope, entries } => {
2007 self.record_file_digests(scope, entries)
2008 }
2009 AgentRequest::RecordWrapperTiming { timing } => (|| {
2010 if timing.spans.len() > 512
2011 || timing.phases_ns.len() > 32
2012 || timing.adapter.len() > 64
2013 || timing.unit.as_ref().is_some_and(|s| s.len() > 1024)
2014 || timing.phases_ns.keys().any(|s| s.len() > 64)
2015 || timing.spans.iter().any(|s| s.name.len() > 64)
2016 {
2017 bail!("wrapper timing exceeds telemetry limits");
2018 }
2019 {
2020 let mut phases = self.stats.wrapper_phases_ns.lock().unwrap();
2021 for (phase, duration) in &timing.phases_ns {
2022 let total = phases.entry(phase.clone()).or_default();
2023 *total = total.saturating_add(*duration);
2024 }
2025 }
2026 self.emit(|| AgentEvent::WrapperTiming { timing });
2027 Ok(AgentResponse::WrapperTimingRecorded)
2028 })(),
2029 AgentRequest::RecordCompilerInvocation {
2030 outcome,
2031 crate_name,
2032 duration_ns,
2033 } => {
2034 let (outcome, incremental) = lookup_outcome(&outcome);
2038 let outcome = outcome.to_string();
2039 let diagnostic = connection
2040 .take_action_diagnostic(&outcome, crate_name.as_deref())
2041 .map(|diagnostic| AgentEvent::ActionDiagnostic {
2042 outcome: outcome.clone(),
2043 crate_name: crate_name.clone(),
2044 diagnostic,
2045 });
2046 self.record_compiler_invocation(
2047 &outcome,
2048 incremental,
2049 crate_name.as_deref(),
2050 duration_ns,
2051 diagnostic,
2052 )
2053 }
2054 AgentRequest::RecordActionVerification { matched, restore } => {
2055 self.record_materialization(restore);
2056 self.stats.verifications.fetch_add(1, Ordering::Relaxed);
2057 if !matched {
2058 self.stats.divergences.fetch_add(1, Ordering::Relaxed);
2059 }
2060 self.emit(|| AgentEvent::Verification { matched, restore });
2061 Ok(AgentResponse::ActionVerificationRecorded)
2062 }
2063 AgentRequest::StoreActionResult { result } => {
2064 self.store_action_result(&result, connection).await
2065 }
2066 AgentRequest::FindActionPrediction { task, invocation } => {
2067 self.find_action_prediction(&task, &invocation)
2068 }
2069 AgentRequest::RecordActionPrediction { task, prediction } => {
2070 self.record_action_prediction(&task, prediction)
2071 }
2072 AgentRequest::FindExecutableIdentity {
2073 executable,
2074 environment,
2075 } => self.find_executable_identity(executable, environment),
2076 AgentRequest::StoreExecutableIdentity {
2077 executable,
2078 environment,
2079 stdout,
2080 pins,
2081 } => self.store_executable_identity(executable, environment, stdout, pins),
2082 AgentRequest::Hello { .. } => {
2083 Err(eyre::eyre!("hello is only valid as the first request"))
2084 }
2085 };
2086 result.unwrap_or_else(|error| AgentResponse::Error {
2087 message: error.to_string(),
2088 })
2089 }
2090
2091 async fn find_blob(&self, digest: &CacheDigest) -> Result<AgentResponse> {
2092 if let Some(path) = self.find_verified_blob(digest)? {
2093 return Ok(AgentResponse::Blob { path: Some(path) });
2094 }
2095 if !self.remote_mode.reads() {
2096 return Ok(AgentResponse::Blob { path: None });
2097 }
2098 let Some(remote) = &self.remote else {
2099 return Ok(AgentResponse::Blob { path: None });
2100 };
2101 match self.fetch_remote_blob(remote, digest).await {
2102 Ok(path) => Ok(AgentResponse::Blob { path: Some(path) }),
2103 Err(error) => {
2104 warn!(
2105 "remote cache blob lookup failed for {}: {error}",
2106 digest.hash
2107 );
2108 Ok(AgentResponse::Blob { path: None })
2109 }
2110 }
2111 }
2112
2113 async fn find_blobs(&self, digests: Vec<CacheDigest>) -> Result<AgentResponse> {
2114 let mut paths = BTreeMap::new();
2115 let mut missing = Vec::new();
2116 for digest in &digests {
2117 match self.find_verified_blob(digest)? {
2118 Some(path) => {
2119 paths.insert(digest.clone(), path);
2120 }
2121 None => {
2122 missing.push(digest.clone());
2123 }
2124 }
2125 }
2126
2127 if !missing.is_empty()
2128 && self.remote_mode.reads()
2129 && let Some(remote) = &self.remote
2130 {
2131 paths.extend(self.fetch_remote_blobs(remote, missing, None).await);
2132 }
2133
2134 Ok(AgentResponse::Blobs {
2135 paths: digests
2136 .into_iter()
2137 .map(|digest| paths.get(&digest).cloned())
2138 .collect(),
2139 })
2140 }
2141
2142 async fn store_blob(
2143 &self,
2144 digest: &CacheDigest,
2145 source: &Path,
2146 connection: &mut ConnectionUploads,
2147 ) -> Result<AgentResponse> {
2148 let path = {
2149 let lock = self.write_lock(digest);
2150 let _guard = lock.lock().await;
2151 if let Some(path) = self.find_verified_blob(digest)? {
2152 path
2153 } else {
2154 let path = self.cas.store_file(digest, source)?;
2155 self.remember_verified_blob(digest, &path);
2156 self.stats.stores.fetch_add(1, Ordering::Relaxed);
2157 self.stats
2158 .stored_bytes
2159 .fetch_add(digest.size, Ordering::Relaxed);
2160 path
2161 }
2162 };
2163 if let Some(uploads) = &self.uploads {
2166 uploads.queue_blob(digest, path.clone(), connection);
2167 }
2168 Ok(AgentResponse::Stored { path })
2169 }
2170
2171 fn find_verified_blob(&self, digest: &CacheDigest) -> Result<Option<PathBuf>> {
2172 let remembered = self.verified_blobs.lock().unwrap().get(digest).cloned();
2173 if let Some(remembered) = remembered {
2174 if remembered.is_unchanged() {
2185 return Ok(Some(remembered.path));
2186 }
2187 self.verified_blobs.lock().unwrap().remove(digest);
2188 }
2189 let path = self.cas.find(digest)?;
2190 if let Some(path) = &path {
2191 self.remember_verified_blob(digest, path);
2192 }
2193 Ok(path)
2194 }
2195
2196 fn remember_verified_blob(&self, digest: &CacheDigest, path: &Path) {
2197 let Some(verified) = VerifiedBlob::describe(path) else {
2198 return;
2202 };
2203 self.verified_blobs
2204 .lock()
2205 .unwrap()
2206 .insert(digest.clone(), verified);
2207 }
2208
2209 async fn find_action_result(&self, action: &CacheDigest) -> Result<AgentResponse> {
2210 if let Some(result) = self.actions.find(action)? {
2211 return Ok(AgentResponse::ActionResult {
2212 result: Some(result),
2213 });
2214 }
2215 if !self.remote_mode.reads() {
2216 return Ok(AgentResponse::ActionResult { result: None });
2217 }
2218 let Some(remote) = &self.remote else {
2219 return Ok(AgentResponse::ActionResult { result: None });
2220 };
2221 let lock = self.action_lock(action);
2222 let _guard = lock.lock().await;
2223 if let Some(result) = self.actions.find(action)? {
2224 return Ok(AgentResponse::ActionResult {
2225 result: Some(result),
2226 });
2227 }
2228 if let Some(result) = self
2229 .pending_remote_actions
2230 .lock()
2231 .unwrap()
2232 .get(action)
2233 .cloned()
2234 {
2235 return Ok(AgentResponse::ActionResult {
2236 result: Some(result),
2237 });
2238 }
2239 let _permit = self.remote_transfers.acquire().await?;
2240 match self.get_remote_action_result(remote, action).await {
2241 Ok(Some(result)) => {
2242 self.pending_remote_actions
2243 .lock()
2244 .unwrap()
2245 .insert(action.clone(), result.clone());
2246 Ok(AgentResponse::ActionResult {
2247 result: Some(result),
2248 })
2249 }
2250 Ok(None) => Ok(AgentResponse::ActionResult { result: None }),
2251 Err(error) => {
2252 self.note_remote_failure();
2253 warn!(
2254 "remote cache action lookup failed for {}: {error}",
2255 action.hash
2256 );
2257 Ok(AgentResponse::ActionResult { result: None })
2258 }
2259 }
2260 }
2261
2262 async fn store_action_result(
2263 &self,
2264 result: &RemoteActionResult,
2265 connection: &ConnectionUploads,
2266 ) -> Result<AgentResponse> {
2267 let path = self.actions.store(result)?;
2268 if let Some(uploads) = &self.uploads {
2269 uploads.queue_action_result(result, connection);
2270 }
2271 Ok(AgentResponse::ActionStored { path })
2272 }
2273
2274 async fn join_action_promise(
2275 &self,
2276 adapter: &str,
2277 invocation: &CacheDigest,
2278 ) -> Result<AgentResponse> {
2279 if !self.remote_mode.reads() || !self.remote_mode.writes() {
2280 return Ok(AgentResponse::ActionPromise {
2281 claim: None,
2282 prediction: None,
2283 });
2284 }
2285 let Some(remote) = &self.remote else {
2286 return Ok(AgentResponse::ActionPromise {
2287 claim: None,
2288 prediction: None,
2289 });
2290 };
2291 let deadline = Instant::now() + ACTION_PROMISE_WAIT;
2292 loop {
2293 let _permit = self.remote_transfers.acquire().await?;
2294 let state = remote.join_action_promise(invocation, adapter).await;
2295 drop(_permit);
2296 match state {
2297 Ok(Some(ActionPromiseState::Claimed { claim })) => {
2298 return Ok(AgentResponse::ActionPromise {
2299 claim: Some(claim),
2300 prediction: None,
2301 });
2302 }
2303 Ok(Some(ActionPromiseState::Complete { prediction })) => {
2304 return Ok(AgentResponse::ActionPromise {
2305 claim: None,
2306 prediction: Some(prediction),
2307 });
2308 }
2309 Ok(Some(ActionPromiseState::Pending { retry_after_ms }))
2310 if Instant::now() < deadline =>
2311 {
2312 tokio::time::sleep(Duration::from_millis(retry_after_ms.clamp(10, 5_000)))
2313 .await;
2314 }
2315 Ok(Some(ActionPromiseState::Pending { .. }) | None) => {
2316 return Ok(AgentResponse::ActionPromise {
2317 claim: None,
2318 prediction: None,
2319 });
2320 }
2321 Ok(Some(_)) => {
2322 return Ok(AgentResponse::ActionPromise {
2323 claim: None,
2324 prediction: None,
2325 });
2326 }
2327 Err(error) => {
2328 self.note_remote_failure();
2329 warn!(
2330 "remote cache action promise failed for {}: {error}",
2331 invocation.hash
2332 );
2333 return Ok(AgentResponse::ActionPromise {
2334 claim: None,
2335 prediction: None,
2336 });
2337 }
2338 }
2339 }
2340 }
2341
2342 async fn complete_action_promise(
2343 &self,
2344 claim: &str,
2345 prediction: &ActionPrediction,
2346 ) -> Result<AgentResponse> {
2347 prediction.validate()?;
2348 let Some(remote) = &self.remote else {
2349 return Ok(AgentResponse::ActionPromiseCompleted);
2350 };
2351 let Some(uploads) = &self.uploads else {
2352 return Ok(AgentResponse::ActionPromiseCompleted);
2353 };
2354 if uploads
2355 .wait_for_actions(std::slice::from_ref(&prediction.action))
2356 .await
2357 .contains(&prediction.action)
2358 {
2359 return Ok(AgentResponse::ActionPromiseCompleted);
2362 }
2363 let completion = ActionPromiseCompletion {
2364 claim: claim.to_string(),
2365 prediction: prediction.clone(),
2366 };
2367 let _permit = self.remote_transfers.acquire().await?;
2368 if let Err(error) = remote
2369 .complete_action_promise(&prediction.invocation, &completion)
2370 .await
2371 {
2372 self.note_remote_failure();
2373 warn!(
2374 "remote cache action promise completion failed for {}: {error}",
2375 prediction.invocation.hash
2376 );
2377 }
2378 Ok(AgentResponse::ActionPromiseCompleted)
2379 }
2380
2381 fn note_remote_failure(&self) {
2383 self.stats.remote_failures.fetch_add(1, Ordering::Relaxed);
2384 }
2385
2386 async fn get_remote_action_result(
2387 &self,
2388 remote: &RemoteCacheClient,
2389 action: &CacheDigest,
2390 ) -> Result<Option<RemoteActionResult>> {
2391 self.stats
2392 .remote_action_lookups
2393 .fetch_add(1, Ordering::Relaxed);
2394 let _timer = AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);
2395 remote.get_action_result(action).await
2396 }
2397
2398 fn record_action_hit(
2399 &self,
2400 action: &CacheDigest,
2401 restore: RestoreStats,
2402 crate_name: Option<String>,
2403 diagnostic: Option<AgentEvent>,
2404 ) -> Result<AgentResponse> {
2405 validate_crate_name(crate_name.as_deref())?;
2406 if self.actions.find(action)?.is_none() {
2407 let pending = self.pending_remote_actions.lock().unwrap().remove(action);
2408 if let Some(result) = pending {
2409 self.actions.store(&result)?;
2410 } else {
2411 bail!("cannot record a hit for a missing action result");
2412 }
2413 }
2414 self.record_restore(restore);
2415 self.stats.hits.fetch_add(1, Ordering::Relaxed);
2416 self.emit_action(
2417 diagnostic,
2418 AgentEvent::ActionHit {
2419 crate_name,
2420 restore,
2421 },
2422 );
2423 Ok(AgentResponse::ActionHitRecorded)
2424 }
2425
2426 fn record_restore(&self, restore: RestoreStats) {
2427 self.record_materialization(restore);
2428 atomic_saturating_add(
2429 &self.stats.avoided_compiler_duration_ns,
2430 restore.avoided_compiler_duration_ns,
2431 );
2432 atomic_saturating_add(&self.stats.restored_output_files, restore.output_files);
2433 atomic_saturating_add(&self.stats.restored_output_bytes, restore.output_bytes);
2434 atomic_saturating_add(
2435 &self.stats.reflinked_output_files,
2436 restore.reflinked_output_files,
2437 );
2438 atomic_saturating_add(
2439 &self.stats.reflinked_output_bytes,
2440 restore.reflinked_output_bytes,
2441 );
2442 atomic_saturating_add(&self.stats.copied_output_files, restore.copied_output_files);
2443 atomic_saturating_add(&self.stats.copied_output_bytes, restore.copied_output_bytes);
2444 atomic_saturating_add(&self.stats.reused_output_files, restore.reused_output_files);
2445 atomic_saturating_add(&self.stats.reused_output_bytes, restore.reused_output_bytes);
2446 }
2447
2448 fn record_compiler_invocation(
2449 &self,
2450 outcome: &str,
2451 incremental: bool,
2452 crate_name: Option<&str>,
2453 duration_ns: u64,
2454 diagnostic: Option<AgentEvent>,
2455 ) -> Result<AgentResponse> {
2456 if !matches!(outcome, "miss" | "unconsulted" | "bypass" | "verification") {
2457 bail!("invalid compiler invocation outcome");
2458 }
2459 validate_crate_name(crate_name)?;
2460 if incremental {
2461 self.stats
2462 .incremental_compilations
2463 .fetch_add(1, Ordering::Relaxed);
2464 }
2465 let mut compiler = self.stats.compiler.lock().unwrap();
2466 let stats = compiler.entry(outcome.to_string()).or_default();
2467 stats.invocations = stats.invocations.saturating_add(1);
2468 stats.duration_ns = stats.duration_ns.saturating_add(duration_ns);
2469 drop(compiler);
2470 if outcome != "verification"
2471 && let Some(crate_name) = crate_name.filter(|name| !name.is_empty())
2472 {
2473 let mut slow = self.stats.slow_compilations.lock().unwrap();
2474 let duration = slow.entry(crate_name.to_string()).or_default();
2475 *duration = duration.saturating_add(duration_ns);
2476 }
2477 self.emit_action(
2478 diagnostic,
2479 AgentEvent::CompilerInvocation {
2480 outcome: outcome.to_string(),
2481 crate_name: crate_name.map(str::to_string),
2482 duration_ns,
2483 },
2484 );
2485 Ok(AgentResponse::CompilerInvocationRecorded)
2486 }
2487
2488 fn record_materialization(&self, restore: RestoreStats) {
2489 atomic_saturating_add(&self.stats.materialization_duration_ns, restore.duration_ns);
2490 }
2491
2492 fn record_diagnostic(&self, level: DiagnosticLevel, message: String) -> Result<AgentResponse> {
2503 if message.is_empty()
2504 || message.len() > MAX_DIAGNOSTIC_BYTES
2505 || message.contains(['\n', '\r', '\0'])
2506 {
2507 bail!("invalid shim {}", level.label());
2508 }
2509 let mut diagnostics = self.diagnostics.lock().unwrap();
2510 let key = (level, message.clone());
2511 if !diagnostics.contains(&key) {
2512 if diagnostics.len() >= MAX_DIAGNOSTICS {
2516 bail!("shim diagnostic limit reached");
2517 }
2518 eprintln!("mbx[{}]: {message}", level.label());
2519 self.emit(|| match level {
2520 DiagnosticLevel::Warning => AgentEvent::Warning {
2521 message: message.clone(),
2522 },
2523 DiagnosticLevel::Error => AgentEvent::Error {
2524 message: message.clone(),
2525 },
2526 });
2527 diagnostics.insert(key);
2528 }
2529 Ok(match level {
2530 DiagnosticLevel::Warning => AgentResponse::WarningRecorded,
2531 DiagnosticLevel::Error => AgentResponse::ErrorRecorded,
2532 })
2533 }
2534
2535 fn find_file_digests(
2542 &self,
2543 scope: FileDigestScope,
2544 files: Vec<FileIdentity>,
2545 ) -> Result<AgentResponse> {
2546 if files.len() > MAX_FILE_DIGEST_BATCH {
2547 bail!("too many file-digest lookups in one request");
2548 }
2549 let ledger = self.file_digests.lock().unwrap();
2550 let digests = files
2551 .into_iter()
2552 .map(|file| {
2553 let recorded = ledger.get(&(scope, file.path.clone()))?;
2554 (recorded.file == file).then(|| recorded.digest.clone())
2555 })
2556 .collect();
2557 Ok(AgentResponse::FileDigests { digests })
2558 }
2559
2560 async fn resolve_file_digests(
2563 &self,
2564 scope: FileDigestScope,
2565 files: Vec<FileIdentity>,
2566 ) -> Result<AgentResponse> {
2567 if files.len() > MAX_FILE_DIGEST_BATCH {
2568 bail!("too many file-digest resolutions in one request");
2569 }
2570 for file in &files {
2571 if !file.path.is_absolute() {
2572 bail!("file-digest resolutions need absolute paths");
2573 }
2574 }
2575 let resolutions = stream::iter(
2576 files
2577 .into_iter()
2578 .map(|file| async move { self.resolve_file_digest(scope, file).await }),
2579 )
2580 .buffered(MAX_CONCURRENT_FILE_DIGESTS)
2581 .collect()
2582 .await;
2583 Ok(AgentResponse::FileDigestsResolved { resolutions })
2584 }
2585
2586 async fn resolve_file_digest(
2587 &self,
2588 scope: FileDigestScope,
2589 file: FileIdentity,
2590 ) -> FileDigestResolution {
2591 let lock = {
2592 let key = (scope, file.clone());
2593 let mut locks = self.file_digest_locks.lock().unwrap();
2594 locks.retain(|_, lock| lock.strong_count() > 0);
2595 if let Some(lock) = locks.get(&key).and_then(Weak::upgrade) {
2596 lock
2597 } else {
2598 let lock = Arc::new(FileDigestFlight {
2599 lock: tokio::sync::Mutex::new(()),
2600 resolution: Mutex::new(None),
2601 });
2602 locks.insert(key, Arc::downgrade(&lock));
2603 lock
2604 }
2605 };
2606 let _flight = lock.lock.lock().await;
2607 if let Some(resolution) = lock.resolution.lock().unwrap().clone() {
2608 return resolution;
2609 }
2610 if let Some(digest) = self
2611 .file_digests
2612 .lock()
2613 .unwrap()
2614 .get(&(scope, file.path.clone()))
2615 .filter(|recorded| recorded.file == file)
2616 .map(|recorded| recorded.digest.clone())
2617 .filter(|digest| digest.size == file.len && file.still_describes().unwrap_or(false))
2618 {
2619 return FileDigestResolution::Digest(digest);
2620 }
2621 let Ok(_permit) = self.file_digest_permits.acquire().await else {
2622 return FileDigestResolution::Unresolved;
2623 };
2624 #[cfg(test)]
2625 self.file_digest_reads.fetch_add(1, Ordering::Relaxed);
2626 let path = file.path.clone();
2627 let resolved =
2628 tokio::task::spawn_blocking(move || digest_file_validated(scope, &path)).await;
2629 let Ok(Ok(Some(observed))) = resolved else {
2630 return FileDigestResolution::Unresolved;
2631 };
2632 if observed.cache_identity.as_ref() != Some(&file) {
2633 return FileDigestResolution::Unresolved;
2634 }
2635 let resolution = match observed.resolution {
2636 FileDigestResolution::Digest(digest) if digest.size == file.len => {
2637 let _ = self.record_file_digests(
2638 scope,
2639 vec![RecordedFileDigest {
2640 file: file.clone(),
2641 digest: digest.clone(),
2642 }],
2643 );
2644 FileDigestResolution::Digest(digest)
2645 }
2646 FileDigestResolution::EmbeddedTimestampMacro => {
2647 FileDigestResolution::EmbeddedTimestampMacro
2648 }
2649 FileDigestResolution::Digest(_) | FileDigestResolution::Unresolved => {
2650 FileDigestResolution::Unresolved
2651 }
2652 };
2653 *lock.resolution.lock().unwrap() = Some(resolution.clone());
2654 resolution
2655 }
2656
2657 fn record_file_digests(
2663 &self,
2664 scope: FileDigestScope,
2665 entries: Vec<RecordedFileDigest>,
2666 ) -> Result<AgentResponse> {
2667 if entries.len() > MAX_FILE_DIGEST_BATCH {
2668 bail!("too many file-digest records in one request");
2669 }
2670 for entry in &entries {
2671 validate_file_digest_record(entry)?;
2672 }
2673 let mut ledger = self.file_digests.lock().unwrap();
2674 for entry in entries {
2675 if ledger.len() >= MAX_FILE_DIGEST_ENTRIES
2676 && !ledger.contains_key(&(scope, entry.file.path.clone()))
2677 {
2678 break;
2679 }
2680 ledger.insert((scope, entry.file.path.clone()), entry);
2681 }
2682 Ok(AgentResponse::FileDigestsRecorded)
2683 }
2684
2685 pub fn seed_file_digests(&self, entries: Vec<(FileDigestScope, RecordedFileDigest)>) -> usize {
2694 self.seed_file_digests_with(|| entries)
2695 }
2696
2697 pub fn seed_file_digests_with(
2703 &self,
2704 load: impl FnOnce() -> Vec<(FileDigestScope, RecordedFileDigest)>,
2705 ) -> usize {
2706 let mut ledger = self.file_digests.lock().unwrap();
2707 let entries = load();
2708 let mut seeded = 0;
2709 for (scope, entry) in entries {
2710 if ledger.len() >= MAX_FILE_DIGEST_ENTRIES {
2711 break;
2712 }
2713 if validate_file_digest_record(&entry).is_err() {
2714 continue;
2715 }
2716 if let std::collections::btree_map::Entry::Vacant(slot) =
2717 ledger.entry((scope, entry.file.path.clone()))
2718 {
2719 slot.insert(entry);
2720 seeded += 1;
2721 }
2722 }
2723 seeded
2724 }
2725
2726 pub fn file_digests(&self) -> Vec<(FileDigestScope, RecordedFileDigest)> {
2729 self.file_digests
2730 .lock()
2731 .unwrap()
2732 .iter()
2733 .map(|((scope, _), entry)| (*scope, entry.clone()))
2734 .collect()
2735 }
2736
2737 fn find_action_prediction(
2738 &self,
2739 task: &str,
2740 invocation: &CacheDigest,
2741 ) -> Result<AgentResponse> {
2742 validate_task_identity(task)?;
2743 invocation.validate()?;
2744 let (prediction, prefetch) = {
2745 let mut tasks = self.task_actions.lock().unwrap();
2746 let Some(state) = tasks.get_mut(task) else {
2747 return Ok(AgentResponse::ActionPrediction { prediction: None });
2748 };
2749 activate_prediction_adapter(state, invocation)
2750 };
2751 if let Some(prefetch) = prefetch {
2752 self.spawn_prefetch_predictions(prefetch);
2753 }
2754 Ok(AgentResponse::ActionPrediction { prediction })
2755 }
2756
2757 fn record_action_prediction(
2758 &self,
2759 task: &str,
2760 prediction: ActionPrediction,
2761 ) -> Result<AgentResponse> {
2762 validate_task_identity(task)?;
2763 prediction.validate()?;
2764 let mut tasks = self.task_actions.lock().unwrap();
2765 let state = tasks.entry(task.to_string()).or_default();
2766 if !state
2767 .pending_predictions
2768 .contains_key(&prediction.invocation)
2769 && state.pending_predictions.len() >= MAX_TASK_ACTION_PREDICTIONS
2770 {
2771 bail!("this task run contains too many action predictions");
2772 }
2773 let current = state.predictions.get(&prediction.invocation);
2774 if current != Some(&prediction) {
2775 match state.changed_predictions.get(&prediction.invocation) {
2776 Some(baseline) if baseline.as_ref() == Some(&prediction) => {
2778 state.changed_predictions.remove(&prediction.invocation);
2779 }
2780 Some(_) => {}
2782 None => {
2783 state
2784 .changed_predictions
2785 .insert(prediction.invocation.clone(), current.cloned());
2786 }
2787 }
2788 }
2789 state
2790 .predictions
2791 .insert(prediction.invocation.clone(), prediction.clone());
2792 state
2793 .pending_predictions
2794 .insert(prediction.invocation.clone(), prediction);
2795 Ok(AgentResponse::ActionPredictionRecorded)
2796 }
2797
2798 fn executable_identity_key(
2799 &self,
2800 executable: PathBuf,
2801 environment: BTreeMap<String, Option<String>>,
2802 ) -> Result<ExecutableIdentityKey> {
2803 if !environment.keys().all(|name| {
2809 matches!(
2810 name.as_str(),
2811 "COMPILER_PATH"
2812 | "GCC_EXEC_PREFIX"
2813 | "LIBRARY_PATH"
2814 | "MBX_FUSE_LD"
2815 | "PATH"
2816 | "RUSTUP_HOME"
2817 | "RUSTUP_TOOLCHAIN"
2818 | "SDKROOT"
2819 | "MACOSX_DEPLOYMENT_TARGET"
2820 | "LIB"
2821 | "UCRTVersion"
2822 | "UniversalCRTSdkDir"
2823 | "VCToolsInstallDir"
2824 | "VCToolsVersion"
2825 | "WindowsSdkDir"
2826 | "WindowsSDKVersion"
2827 )
2828 }) {
2829 bail!("executable identity contains an unsupported environment variable");
2830 }
2831 Ok(ExecutableIdentityKey {
2832 executable,
2833 environment,
2834 })
2835 }
2836
2837 fn find_executable_identity(
2838 &self,
2839 executable: PathBuf,
2840 environment: BTreeMap<String, Option<String>>,
2841 ) -> Result<AgentResponse> {
2842 let key = self.executable_identity_key(executable, environment)?;
2843 let remembered = self
2844 .executable_identities
2845 .lock()
2846 .unwrap()
2847 .get(&key)
2848 .cloned();
2849 let stdout = match remembered {
2850 Some(stdout) => Some(stdout),
2851 None => {
2852 let persisted = self.load_persisted_identity(&key);
2853 if let Some(stdout) = &persisted {
2854 let _ = self.remember_executable_identity(&key, stdout.clone());
2857 }
2858 persisted
2859 }
2860 };
2861 Ok(AgentResponse::ExecutableIdentity { stdout })
2862 }
2863
2864 fn store_executable_identity(
2865 &self,
2866 executable: PathBuf,
2867 environment: BTreeMap<String, Option<String>>,
2868 stdout: Vec<u8>,
2869 pins: Vec<PinnedFile>,
2870 ) -> Result<AgentResponse> {
2871 if stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE {
2872 bail!("executable identity exceeds {MAX_EXECUTABLE_IDENTITY_SIZE} bytes");
2873 }
2874 let key = self.executable_identity_key(executable, environment)?;
2875 self.remember_executable_identity(&key, stdout.clone())?;
2876 if pins.is_empty() {
2881 } else if !pins.iter().all(PinnedFile::holds) {
2882 debug!("executable identity was not persisted: a pinned file changed during the probe");
2883 } else if let Err(error) = self.persist_executable_identity(&key, pins, &stdout) {
2884 debug!("executable identity was not persisted: {error:#}");
2885 }
2886 Ok(AgentResponse::ExecutableIdentity {
2887 stdout: Some(stdout),
2888 })
2889 }
2890
2891 fn remember_executable_identity(
2892 &self,
2893 key: &ExecutableIdentityKey,
2894 stdout: Vec<u8>,
2895 ) -> Result<()> {
2896 let mut identities = self.executable_identities.lock().unwrap();
2897 let is_new = !identities.contains_key(key);
2898 let previous_size = identities.get(key).map_or(0, Vec::len);
2899 if is_new && identities.len() >= MAX_EXECUTABLE_IDENTITIES {
2900 bail!("executable identity cache contains too many entries");
2901 }
2902 let retained_bytes = identities.values().map(Vec::len).sum::<usize>();
2903 if retained_bytes - previous_size + stdout.len() > MAX_EXECUTABLE_IDENTITY_BYTES {
2904 bail!("executable identity cache contains too many bytes");
2905 }
2906 identities.insert(key.clone(), stdout);
2907 Ok(())
2908 }
2909
2910 fn persisted_identity_path(&self, key: &ExecutableIdentityKey) -> Result<PathBuf> {
2911 let selector = serde_json::to_vec(&(&key.executable, &key.environment))?;
2912 let digest = CacheDigest::blake3(&selector);
2913 Ok(self.identity_dir.join(format!("{}.json", digest.hash)))
2914 }
2915
2916 fn load_persisted_identity(&self, key: &ExecutableIdentityKey) -> Option<Vec<u8>> {
2919 let path = self.persisted_identity_path(key).ok()?;
2920 let bytes = fs::read(path).ok()?;
2921 let persisted: PersistedExecutableIdentity = serde_json::from_slice(&bytes).ok()?;
2922 if persisted.version != PERSISTED_EXECUTABLE_IDENTITY_VERSION
2923 || persisted.executable != key.executable
2924 || persisted.environment != key.environment
2925 || persisted.pins.is_empty()
2926 || persisted.stdout.len() > MAX_EXECUTABLE_IDENTITY_SIZE
2927 {
2928 return None;
2929 }
2930 persisted
2931 .pins
2932 .iter()
2933 .all(PinnedFile::holds)
2934 .then(|| persisted.stdout.into_bytes())
2935 }
2936
2937 fn persist_executable_identity(
2938 &self,
2939 key: &ExecutableIdentityKey,
2940 pins: Vec<PinnedFile>,
2941 stdout: &[u8],
2942 ) -> Result<()> {
2943 let Ok(stdout) = std::str::from_utf8(stdout) else {
2945 return Ok(());
2946 };
2947 let persisted = PersistedExecutableIdentity {
2948 version: PERSISTED_EXECUTABLE_IDENTITY_VERSION,
2949 executable: key.executable.clone(),
2950 environment: key.environment.clone(),
2951 pins,
2952 stdout: stdout.to_owned(),
2953 };
2954 let path = self.persisted_identity_path(key)?;
2955 fs::create_dir_all(self.identity_dir.as_path())?;
2956 let mut temporary = tempfile::NamedTempFile::new_in(self.identity_dir.as_path())?;
2958 std::io::Write::write_all(temporary.as_file_mut(), &serde_json::to_vec(&persisted)?)?;
2959 temporary.persist(path).map_err(|error| error.error)?;
2960 Ok(())
2961 }
2962
2963 pub async fn handle_connection<S>(&self, stream: S) -> Result<()>
2965 where
2966 S: AsyncRead + AsyncWrite + Unpin,
2967 {
2968 let (reader, mut writer) = tokio::io::split(stream);
2969 let mut reader = BufReader::new(reader);
2970 let hello = read_request(&mut reader)
2971 .await?
2972 .ok_or_else(|| eyre::eyre!("connection closed before the agent handshake"))?;
2973 let request: AgentRequest = serde_json::from_str(&hello)?;
2974 match request {
2975 AgentRequest::Hello {
2976 protocol,
2977 client_version,
2978 } if protocol == AGENT_PROTOCOL_VERSION && client_version == self.version.as_ref() => {}
2979 AgentRequest::Hello { protocol, .. } if protocol != AGENT_PROTOCOL_VERSION => {
2980 send_response(
2981 &mut writer,
2982 &AgentResponse::Error {
2983 message: format!(
2984 "unsupported agent protocol {protocol}; expected {AGENT_PROTOCOL_VERSION}"
2985 ),
2986 },
2987 )
2988 .await?;
2989 return Ok(());
2990 }
2991 AgentRequest::Hello { client_version, .. } => {
2992 send_response(
2993 &mut writer,
2994 &AgentResponse::Error {
2995 message: format!(
2996 "cache client {client_version} does not match agent {}",
2997 self.version
2998 ),
2999 },
3000 )
3001 .await?;
3002 return Ok(());
3003 }
3004 _ => bail!("the first agent request must be hello"),
3005 }
3006 send_response(
3007 &mut writer,
3008 &AgentResponse::Hello {
3009 protocol: AGENT_PROTOCOL_VERSION,
3010 agent_version: self.version.to_string(),
3011 },
3012 )
3013 .await?;
3014
3015 let mut connection = ConnectionUploads::default();
3019 while let Some(line) = read_request(&mut reader).await? {
3020 let response = match serde_json::from_str(&line) {
3021 Ok(request) => self.respond_on(request, &mut connection).await,
3022 Err(error) => AgentResponse::Error {
3023 message: format!("invalid agent request: {error}"),
3024 },
3025 };
3026 send_response(&mut writer, &response).await?;
3027 }
3028 Ok(())
3029 }
3030}
3031
3032async fn read_request<R>(reader: &mut R) -> Result<Option<String>>
3038where
3039 R: AsyncBufRead + Unpin,
3040{
3041 let mut line = Vec::new();
3042 loop {
3043 let available = reader.fill_buf().await?;
3044 if available.is_empty() {
3045 break;
3046 }
3047 let (consumed, complete) = match available.iter().position(|byte| *byte == b'\n') {
3048 Some(index) => (index, true),
3049 None => (available.len(), false),
3050 };
3051 if line.len() + consumed > MAX_REQUEST_BYTES {
3052 bail!("agent request exceeded {MAX_REQUEST_BYTES} bytes");
3053 }
3054 line.extend_from_slice(&available[..consumed]);
3055 reader.consume(consumed + usize::from(complete));
3057 if complete {
3058 return Ok(Some(String::from_utf8(line)?));
3059 }
3060 }
3061 if line.is_empty() {
3062 Ok(None)
3063 } else {
3064 Ok(Some(String::from_utf8(line)?))
3065 }
3066}
3067
3068async fn send_response(
3069 writer: &mut (impl AsyncWrite + Unpin),
3070 response: &AgentResponse,
3071) -> Result<()> {
3072 let mut encoded = serde_json::to_vec(response)?;
3073 encoded.push(b'\n');
3074 writer.write_all(&encoded).await?;
3075 writer.flush().await?;
3076 Ok(())
3077}
3078
3079#[cfg(test)]
3080#[path = "agent_tests.rs"]
3081mod tests;