Skip to main content

vyre_runtime/pipeline_cache/
remote.rs

1//! HTTPS-backed read-through cache. Feature-gated on `remote-cache` so library
2//! users who only want disk caching don't pull in `ureq`.
3
4use super::disk::read_verified_cache_blob;
5use super::fingerprint::PipelineFingerprint;
6use super::store::PipelineCacheStore;
7
8const HEADER_FINGERPRINT: &str = "x-vyre-cache-fingerprint";
9const HEADER_SOURCE_PROVENANCE: &str = "x-vyre-cache-source-provenance";
10const HEADER_DEVICE_COMPATIBILITY: &str = "x-vyre-cache-device-compatibility";
11
12/// HTTPS-backed cache that reads pre-compiled artifacts from a
13/// base URL. Feature-gated on `remote-cache` so library users who only
14/// want disk caching don't pull in `ureq`.
15///
16/// Writes are **no-ops**  -  `RemoteCache` is a read-through layer.
17/// Publishing to a remote registry is a separate `vyre publish-cache`
18/// xtask, not part of this runtime.
19pub struct RemoteCache {
20    base_url: String,
21    agent: ureq::Agent,
22    expected_source_provenance: Option<String>,
23    expected_device_compatibility: Option<String>,
24}
25
26impl RemoteCache {
27    /// Construct from a base URL. The cache fetches
28    /// `<base_url>/<fp_hex>.bin` for each lookup.
29    #[must_use]
30    pub fn new(base_url: impl Into<String>) -> Self {
31        Self {
32            base_url: base_url.into(),
33            agent: ureq::Agent::new_with_defaults(),
34            expected_source_provenance: None,
35            expected_device_compatibility: None,
36        }
37    }
38
39    /// Require exact source provenance and device compatibility metadata on
40    /// every remote hit.
41    #[must_use]
42    pub fn with_required_metadata(
43        mut self,
44        source_provenance: impl Into<String>,
45        device_compatibility: impl Into<String>,
46    ) -> Self {
47        self.expected_source_provenance = Some(source_provenance.into());
48        self.expected_device_compatibility = Some(device_compatibility.into());
49        self
50    }
51
52    fn metadata_expectation(&self) -> RemoteMetadataExpectation<'_> {
53        RemoteMetadataExpectation {
54            expected_source_provenance: self.expected_source_provenance.as_deref(),
55            expected_device_compatibility: self.expected_device_compatibility.as_deref(),
56        }
57    }
58}
59
60impl PipelineCacheStore for RemoteCache {
61    fn get(&self, fp: &PipelineFingerprint) -> Option<Vec<u8>> {
62        let url = format!("{}/{}.bin", self.base_url.trim_end_matches('/'), fp.hex());
63        let mut resp = self.agent.get(&url).call().ok()?;
64        if !remote_metadata_allows(
65            fp,
66            header_value(&resp, HEADER_FINGERPRINT),
67            header_value(&resp, HEADER_SOURCE_PROVENANCE),
68            header_value(&resp, HEADER_DEVICE_COMPATIBILITY),
69            self.metadata_expectation(),
70        ) {
71            return None;
72        }
73        read_verified_cache_blob(resp.body_mut().as_reader())
74    }
75
76    fn put(&self, _fp: PipelineFingerprint, _artifact: Vec<u8>) {
77        // Remote cache is read-through; publishing is a separate flow.
78    }
79}
80
81fn header_value<'a>(resp: &'a ureq::http::Response<ureq::Body>, name: &str) -> Option<&'a str> {
82    resp.headers()
83        .get(name)
84        .and_then(|value| value.to_str().ok())
85}
86
87#[derive(Debug, Clone, Copy)]
88struct RemoteMetadataExpectation<'a> {
89    expected_source_provenance: Option<&'a str>,
90    expected_device_compatibility: Option<&'a str>,
91}
92
93fn remote_metadata_allows(
94    fp: &PipelineFingerprint,
95    fingerprint: Option<&str>,
96    source_provenance: Option<&str>,
97    device_compatibility: Option<&str>,
98    expectation: RemoteMetadataExpectation<'_>,
99) -> bool {
100    let expected_fingerprint = fp.hex();
101    let Some(fingerprint) = fingerprint else {
102        return false;
103    };
104    if fingerprint != expected_fingerprint {
105        return false;
106    }
107    let Some(source_provenance) = source_provenance.filter(|value| !value.is_empty()) else {
108        return false;
109    };
110    let Some(device_compatibility) = device_compatibility.filter(|value| !value.is_empty()) else {
111        return false;
112    };
113    if let Some(expected) = expectation.expected_source_provenance {
114        if source_provenance != expected {
115            return false;
116        }
117    }
118    if let Some(expected) = expectation.expected_device_compatibility {
119        if device_compatibility != expected {
120            return false;
121        }
122    }
123    true
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::pipeline_cache::test_helpers::tiny_program;
130
131    #[test]
132    fn remote_cache_owns_reusable_http_agent() {
133        let cache = RemoteCache::new("https://cache.invalid/root/");
134
135        assert_eq!(cache.base_url, "https://cache.invalid/root/");
136        let _shared_agent = cache.agent.clone();
137    }
138
139    #[test]
140    fn remote_cache_metadata_accepts_matching_fingerprint_source_and_device() {
141        let fp = PipelineFingerprint::of(&tiny_program());
142        let fp_hex = fp.hex();
143        let expectation = RemoteMetadataExpectation {
144            expected_source_provenance: Some("git:abc123"),
145            expected_device_compatibility: Some("cuda-sm90"),
146        };
147
148        assert!(remote_metadata_allows(
149            &fp,
150            Some(&fp_hex),
151            Some("git:abc123"),
152            Some("cuda-sm90"),
153            expectation,
154        ));
155    }
156
157    #[test]
158    fn remote_cache_metadata_rejects_stale_source_device_and_fingerprint() {
159        let fp = PipelineFingerprint::of(&tiny_program());
160        let fp_hex = fp.hex();
161        let mut other = fp;
162        other.0[0] ^= 0xFF;
163        let other_hex = other.hex();
164        let expectation = RemoteMetadataExpectation {
165            expected_source_provenance: Some("git:abc123"),
166            expected_device_compatibility: Some("cuda-sm90"),
167        };
168
169        assert!(!remote_metadata_allows(
170            &fp,
171            Some(&other_hex),
172            Some("git:abc123"),
173            Some("cuda-sm90"),
174            expectation,
175        ));
176        assert!(!remote_metadata_allows(
177            &fp,
178            Some(&fp_hex),
179            Some("git:stale"),
180            Some("cuda-sm90"),
181            expectation,
182        ));
183        assert!(!remote_metadata_allows(
184            &fp,
185            Some(&fp_hex),
186            Some("git:abc123"),
187            Some("metal-apple7"),
188            expectation,
189        ));
190        assert!(!remote_metadata_allows(
191            &fp,
192            Some(&fp_hex),
193            None,
194            Some("cuda-sm90"),
195            expectation,
196        ));
197    }
198}