/build/source/nativelink-store/src/verify_store.rs
Line | Count | Source |
1 | | // Copyright 2024 The NativeLink Authors. All rights reserved. |
2 | | // |
3 | | // Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // See LICENSE file for details |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | |
15 | | use core::pin::Pin; |
16 | | use std::sync::Arc; |
17 | | |
18 | | use async_trait::async_trait; |
19 | | use nativelink_config::stores::VerifySpec; |
20 | | use nativelink_error::{Code, Error, ResultExt, make_err, make_input_err}; |
21 | | use nativelink_metric::MetricsComponent; |
22 | | use nativelink_util::buf_channel::{ |
23 | | DropCloserReadHalf, DropCloserWriteHalf, make_buf_channel_pair, |
24 | | }; |
25 | | use nativelink_util::common::PackedHash; |
26 | | use nativelink_util::digest_hasher::{ |
27 | | DigestHasher, DigestHasherFunc, digest_hasher_func_from_context, |
28 | | }; |
29 | | use nativelink_util::health_utils::{HealthStatusIndicator, default_health_status_indicator}; |
30 | | use nativelink_util::metrics_utils::CounterWithTime; |
31 | | use nativelink_util::store_trait::{ |
32 | | RemoveCallback, Store, StoreDriver, StoreKey, StoreLike, UploadSizeInfo, |
33 | | }; |
34 | | use tracing::warn; |
35 | | |
36 | | #[derive(Debug, MetricsComponent)] |
37 | | pub struct VerifyStore { |
38 | | #[metric(group = "inner_store")] |
39 | | inner_store: Store, |
40 | | #[metric(help = "If the verification store is verifying the size of the data")] |
41 | | verify_size: bool, |
42 | | #[metric(help = "If the verification store is verifying the hash of the data")] |
43 | | verify_hash: bool, |
44 | | |
45 | | // Metrics. |
46 | | #[metric(help = "Number of failures the verification store had due to size mismatches")] |
47 | | size_verification_failures: CounterWithTime, |
48 | | #[metric(help = "Number of failures the verification store had due to hash mismatches")] |
49 | | hash_verification_failures: CounterWithTime, |
50 | | } |
51 | | |
52 | | impl VerifyStore { |
53 | 38 | pub fn new(spec: &VerifySpec, inner_store: Store) -> Arc<Self> { |
54 | 38 | Arc::new(Self { |
55 | 38 | inner_store, |
56 | 38 | verify_size: spec.verify_size, |
57 | 38 | verify_hash: spec.verify_hash, |
58 | 38 | size_verification_failures: CounterWithTime::default(), |
59 | 38 | hash_verification_failures: CounterWithTime::default(), |
60 | 38 | }) |
61 | 38 | } |
62 | | |
63 | 17 | async fn inner_check_update<D: DigestHasher>( |
64 | 17 | &self, |
65 | 17 | tx: &mut DropCloserWriteHalf, |
66 | 17 | mut rx: DropCloserReadHalf, |
67 | 17 | maybe_expected_digest_size: Option<u64>, |
68 | 17 | original_hash: &PackedHash, |
69 | 17 | digest_function: Option<DigestHasherFunc>, |
70 | 17 | mut maybe_hasher: Option<&mut D>, |
71 | 17 | ) -> Result<u64, Error> { |
72 | 17 | let mut sum_size: u64 = 0; |
73 | | loop { |
74 | 38 | let chunk = rx |
75 | 38 | .recv() |
76 | 38 | .await |
77 | 38 | .err_tip(|| "Failed to read chunk in check_update in verify store")?0 ; |
78 | 38 | sum_size += chunk.len() as u64; |
79 | | |
80 | | // Ensure if a user sends us too much data we fail quickly. |
81 | 38 | if let Some(expected_size28 ) = maybe_expected_digest_size { |
82 | 28 | match sum_size.cmp(&expected_size) { |
83 | | core::cmp::Ordering::Greater => { |
84 | 1 | self.size_verification_failures.inc(); |
85 | 1 | return Err(make_input_err!( |
86 | 1 | "Expected size {} but already received {} on insert", |
87 | 1 | expected_size, |
88 | 1 | sum_size |
89 | 1 | )); |
90 | | } |
91 | | core::cmp::Ordering::Equal => { |
92 | | // Ensure our next chunk is the EOF chunk. |
93 | | // If this was an error it'll be caught on the .recv() |
94 | | // on next cycle. |
95 | 12 | if let Ok(eof_chunk) = rx.peek().await |
96 | 12 | && !eof_chunk.is_empty() |
97 | | { |
98 | 0 | self.size_verification_failures.inc(); |
99 | 0 | return Err(make_input_err!( |
100 | 0 | "Expected EOF chunk when exact size was hit on insert in verify store - {}", |
101 | 0 | expected_size, |
102 | 0 | )); |
103 | 12 | } |
104 | | } |
105 | 15 | core::cmp::Ordering::Less => {} |
106 | | } |
107 | 10 | } |
108 | | |
109 | | // If is EOF. |
110 | 37 | if chunk.is_empty() { |
111 | 13 | if let Some(expected_size8 ) = maybe_expected_digest_size |
112 | 8 | && sum_size != expected_size |
113 | | { |
114 | 2 | self.size_verification_failures.inc(); |
115 | 2 | return Err(make_input_err!( |
116 | 2 | "Expected size {} but got size {} on insert", |
117 | 2 | expected_size, |
118 | 2 | sum_size |
119 | 2 | )); |
120 | 11 | } |
121 | 11 | if let Some(hasher8 ) = maybe_hasher.as_mut() { |
122 | 8 | let digest = hasher.finalize_digest(); |
123 | 8 | let hash_result = digest.packed_hash(); |
124 | 8 | if original_hash != hash_result { |
125 | 2 | self.hash_verification_failures.inc(); |
126 | 2 | let Some(digest_function) = digest_function else { |
127 | 0 | return Err(make_input_err!( |
128 | 0 | "Hash verification failed without a digest function" |
129 | 0 | )); |
130 | | }; |
131 | 2 | return Err(make_input_err!( |
132 | 2 | "Hash verification using {digest_function} failed: client declared {digest_function}:{original_hash}, but the server computed {digest_function}:{hash_result}", |
133 | 2 | )); |
134 | 6 | } |
135 | 3 | } |
136 | 9 | tx.send_eof().err_tip(|| "In verify_store::check_update")?0 ; |
137 | 9 | break; |
138 | 24 | } |
139 | | |
140 | | // This will allows us to hash while sending data to another thread. |
141 | 24 | let write_future = tx.send(chunk.clone()); |
142 | | |
143 | 24 | if let Some(hasher9 ) = maybe_hasher.as_mut() { |
144 | 9 | hasher.update(chunk.as_ref()); |
145 | 15 | } |
146 | | |
147 | 24 | write_future |
148 | 24 | .await |
149 | 24 | .err_tip(|| "Failed to write chunk to inner store in verify store")?3 ; |
150 | | } |
151 | 9 | Ok(sum_size) |
152 | 17 | } |
153 | | } |
154 | | |
155 | | impl VerifyStore { |
156 | 43 | async fn inner_check_read( |
157 | 43 | &self, |
158 | 43 | mut rx: DropCloserReadHalf, |
159 | 43 | writer: &mut DropCloserWriteHalf, |
160 | 43 | expected: u64, |
161 | 43 | key: StoreKey<'_>, |
162 | 43 | ) -> Result<(), Error> { |
163 | 43 | let mut sum_size: u64 = 0; |
164 | | loop { |
165 | 74 | let chunk64 = rx |
166 | 74 | .recv() |
167 | 74 | .await |
168 | 74 | .err_tip(|| "Failed to read chunk in check_read in verify store")?10 ; |
169 | 64 | if chunk.is_empty() { |
170 | 32 | if sum_size != expected { |
171 | 17 | self.size_verification_failures.inc(); |
172 | 17 | return Err(make_err!( |
173 | 17 | Code::DataLoss, |
174 | 17 | "Read {sum_size} bytes of the {expected} expected for {key:?} in verify store" |
175 | 17 | )); |
176 | 15 | } |
177 | 15 | return writer.send_eof().err_tip(|| "In verify_store::check_read"); |
178 | 32 | } |
179 | 32 | sum_size += chunk.len() as u64; |
180 | 32 | if sum_size > expected { |
181 | 1 | self.size_verification_failures.inc(); |
182 | 1 | return Err(make_err!( |
183 | 1 | Code::DataLoss, |
184 | 1 | "Read {sum_size} bytes, more than the {expected} expected for {key:?} in verify store" |
185 | 1 | )); |
186 | 31 | } |
187 | 31 | writer |
188 | 31 | .send(chunk) |
189 | 31 | .await |
190 | 31 | .err_tip(|| "Failed to forward chunk in verify store")?0 ; |
191 | | } |
192 | 43 | } |
193 | | } |
194 | | |
195 | | #[async_trait] |
196 | | impl StoreDriver for VerifyStore { |
197 | 15 | async fn post_init(self: Arc<Self>) -> Result<(), Error> { |
198 | | self.inner_store.clone().into_inner().post_init().await?; |
199 | | Ok(()) |
200 | 15 | } |
201 | | |
202 | | async fn has_with_results( |
203 | | self: Pin<&Self>, |
204 | | digests: &[StoreKey<'_>], |
205 | | results: &mut [Option<u64>], |
206 | 1 | ) -> Result<(), Error> { |
207 | | self.inner_store.has_with_results(digests, results).await |
208 | 1 | } |
209 | | |
210 | | async fn update( |
211 | | self: Pin<&Self>, |
212 | | key: StoreKey<'_>, |
213 | | reader: DropCloserReadHalf, |
214 | | size_info: UploadSizeInfo, |
215 | 17 | ) -> Result<u64, Error> { |
216 | | let StoreKey::Digest(digest) = key else { |
217 | | return Err(make_input_err!( |
218 | | "Only digests are supported in VerifyStore. Got {key:?}" |
219 | | )); |
220 | | }; |
221 | | let digest_size = digest.size_bytes(); |
222 | | if let UploadSizeInfo::ExactSize(expected_size) = size_info |
223 | | && self.verify_size |
224 | | && expected_size != digest_size |
225 | | { |
226 | | self.size_verification_failures.inc(); |
227 | | return Err(make_input_err!( |
228 | | "Expected size to match. Got {} but digest says {} on update", |
229 | | expected_size, |
230 | | digest_size |
231 | | )); |
232 | | } |
233 | | |
234 | | let digest_function = if self.verify_hash { |
235 | | Some(digest_hasher_func_from_context()) |
236 | | } else { |
237 | | None |
238 | | }; |
239 | 8 | let mut hasher = digest_function.map(|digest_function| digest_function.hasher()); |
240 | | |
241 | | let maybe_digest_size = if self.verify_size { |
242 | | Some(digest_size) |
243 | | } else { |
244 | | None |
245 | | }; |
246 | | let (tx, rx) = make_buf_channel_pair(); |
247 | | |
248 | | let update_fut = self.inner_store.update(digest, rx, size_info); |
249 | 17 | let check_fut = async move { |
250 | 17 | let mut tx = tx; |
251 | 17 | let result = self |
252 | 17 | .inner_check_update( |
253 | 17 | &mut tx, |
254 | 17 | reader, |
255 | 17 | maybe_digest_size, |
256 | 17 | digest.packed_hash(), |
257 | 17 | digest_function, |
258 | 17 | hasher.as_mut(), |
259 | 17 | ) |
260 | 17 | .await; |
261 | | // Inspect the channel before dropping it. Keep it in this future so |
262 | | // a validation failure also wakes the inner store's reader. |
263 | 17 | (result, tx.is_pipe_broken()) |
264 | 17 | }; |
265 | | |
266 | | let (update_res, (check_res, forwarding_failed)) = tokio::join!(update_fut, check_fut); |
267 | | |
268 | | match (update_res, check_res) { |
269 | | // A failed send is a consequence of the inner store's failure, not |
270 | | // a validation error. Preserve its code and both error messages. |
271 | | (Err(update_err), Err(check_err)) if forwarding_failed => { |
272 | | Err(update_err.merge(check_err)) |
273 | | } |
274 | | // Size/hash or input-stream failures remain the checker's verdict. |
275 | | (_, Err(e)) | (Err(e), Ok(_)) => Err(e), |
276 | | (Ok(size), Ok(_)) => Ok(size), |
277 | | } |
278 | 17 | } |
279 | | |
280 | | async fn get_part( |
281 | | self: Pin<&Self>, |
282 | | key: StoreKey<'_>, |
283 | | writer: &mut DropCloserWriteHalf, |
284 | | offset: u64, |
285 | | length: Option<u64>, |
286 | 43 | ) -> Result<(), Error> { |
287 | | // A digest names its size, so a read that ends short of it is a |
288 | | // truncated blob, whatever tier below lost the tail: handed on as |
289 | | // EOF it would be cached as complete by every store above and fed |
290 | | // to actions as an input. The hash is not recomputed on reads. |
291 | | let expected = match &key { |
292 | | StoreKey::Digest(digest) if self.verify_size => { |
293 | | let to_end = digest.size_bytes().saturating_sub(offset); |
294 | 3 | Some(length.map_or(to_end, |length| length.min(to_end))) |
295 | | } |
296 | | _ => None, |
297 | | }; |
298 | | let Some(expected) = expected else { |
299 | | return self.inner_store.get_part(key, writer, offset, length).await; |
300 | | }; |
301 | | let (mut tx, rx) = make_buf_channel_pair(); |
302 | | // The writer half lives inside the read future, so an inner read that |
303 | | // fails after some data closes the channel as it returns; held out |
304 | | // here it kept the forwarding loop waiting for bytes that never came. |
305 | | let read_key = key.borrow().into_owned(); |
306 | 43 | let read_fut = async move { |
307 | 43 | self.inner_store |
308 | 43 | .get_part(read_key, &mut tx, offset, length) |
309 | 43 | .await |
310 | 43 | }; |
311 | | let check_fut = self.inner_check_read(rx, writer, expected, key.borrow()); |
312 | | let (read_res, check_res) = tokio::join!(read_fut, check_fut); |
313 | | // The checker's verdict first: on a read past the expected size it |
314 | | // drops its receiver and the inner send then fails with Internal, |
315 | | // which must not hide the DataLoss. Otherwise the inner error (a |
316 | | // NotFound from below) comes before the checker's closed-channel |
317 | | // Internal, as `update` orders the same pair. |
318 | | let result = match (read_res, check_res) { |
319 | | (_, Err(e)) if e.code == Code::DataLoss => Err(e), |
320 | | (Err(e), _) | (Ok(()), Err(e)) => Err(e), |
321 | | (Ok(()), Ok(())) => Ok(()), |
322 | | }; |
323 | | if let Err(err) = &result |
324 | | && err.code == Code::DataLoss |
325 | | { |
326 | | // Keep the durable copy unless it is the wrong size too: a bad |
327 | | // cache copy must not cost the only good one. Read it back |
328 | | // rather than trust `has`, which for a filesystem store is the |
329 | | // size indexed at write time, not the file's. |
330 | | let inner = self.inner_store.as_store_driver_pin(); |
331 | 22 | let removed = async { |
332 | 22 | let cached = inner.remove_cached(key.borrow()).await?0 ; |
333 | 22 | let durable_is_bad = match &key { |
334 | 22 | StoreKey::Digest(digest) => { |
335 | 22 | let (mut tx, mut rx) = make_buf_channel_pair(); |
336 | 22 | let read_key = key.borrow().into_owned(); |
337 | 22 | let read_fut = async move { |
338 | 22 | self.inner_store.get_part(read_key, &mut tx, 0, None).await |
339 | 22 | }; |
340 | 22 | let (read_res, drained) = tokio::join!(read_fut, rx.drain()); |
341 | 22 | match read_res.and(drained) { |
342 | 19 | Ok(size) => size != digest.size_bytes(), |
343 | | // Other errors mean unchecked, not bad. |
344 | 3 | Err(err) => err.code == Code::DataLoss, |
345 | | } |
346 | | } |
347 | 0 | StoreKey::Str(_) => false, |
348 | | }; |
349 | 22 | if durable_is_bad { |
350 | 13 | return Ok(inner.remove(key.borrow()).await?0 || cached1 ); |
351 | 9 | } |
352 | 9 | Ok::<_, Error>(cached) |
353 | 22 | } |
354 | | .await; |
355 | | match removed { |
356 | | Ok(removed) => warn!( |
357 | | ?key, |
358 | | removed, "Dropped a copy that failed size verification" |
359 | | ), |
360 | | Err(remove_err) => warn!( |
361 | | ?key, |
362 | | ?remove_err, |
363 | | "Could not drop a copy that failed size verification" |
364 | | ), |
365 | | } |
366 | | } |
367 | | result |
368 | 43 | } |
369 | | |
370 | 0 | async fn remove(self: Pin<&Self>, key: StoreKey<'_>) -> Result<bool, Error> { |
371 | | self.inner_store.as_store_driver_pin().remove(key).await |
372 | 0 | } |
373 | | |
374 | 0 | async fn remove_cached(self: Pin<&Self>, key: StoreKey<'_>) -> Result<bool, Error> { |
375 | | self.inner_store |
376 | | .as_store_driver_pin() |
377 | | .remove_cached(key) |
378 | | .await |
379 | 0 | } |
380 | | |
381 | 6 | fn inner_store(&self, _digest: Option<StoreKey>) -> &'_ dyn StoreDriver { |
382 | 6 | self |
383 | 6 | } |
384 | | |
385 | 5 | fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) { |
386 | 5 | self |
387 | 5 | } |
388 | | |
389 | 0 | fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> { |
390 | 0 | self |
391 | 0 | } |
392 | | |
393 | 0 | fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> { |
394 | 0 | self.inner_store.register_remove_callback(callback) |
395 | 0 | } |
396 | | } |
397 | | |
398 | | default_health_status_indicator!(VerifyStore); |