/build/source/nativelink-store/src/cache_metrics_store.rs
Line | Count | Source |
1 | | // Copyright 2026 Trace Machina, Inc. 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::borrow::BorrowMut; |
16 | | use core::ops::Bound; |
17 | | use core::pin::Pin; |
18 | | use std::ffi::OsString; |
19 | | use std::sync::Arc; |
20 | | use std::time::Instant; |
21 | | |
22 | | use async_trait::async_trait; |
23 | | use bytes::Bytes; |
24 | | use nativelink_config::stores::CacheMetricsSpec; |
25 | | use nativelink_error::{Code, Error}; |
26 | | use nativelink_metric::{ |
27 | | MetricFieldData, MetricKind, MetricPublishKnownKindData, MetricsComponent, group, publish, |
28 | | }; |
29 | | use nativelink_util::buf_channel::{DropCloserReadHalf, DropCloserWriteHalf}; |
30 | | use nativelink_util::fs; |
31 | | use nativelink_util::health_utils::{HealthRegistryBuilder, HealthStatusIndicator}; |
32 | | use nativelink_util::metrics::{CACHE_METRICS, CACHE_TYPE, CacheMetricAttrs}; |
33 | | use nativelink_util::store_trait::{ |
34 | | RemoveCallback, Store, StoreDriver, StoreKey, StoreLike, StoreOptimizations, UploadSizeInfo, |
35 | | }; |
36 | | use opentelemetry::KeyValue; |
37 | | |
38 | | #[derive(Debug)] |
39 | | pub struct CacheMetricsStore { |
40 | | backend: Store, |
41 | | cache_type: String, |
42 | | allow_remove: bool, |
43 | | attrs: CacheMetricAttrs, |
44 | | } |
45 | | |
46 | | impl CacheMetricsStore { |
47 | 17 | pub fn new(spec: &CacheMetricsSpec, backend: Store) -> Arc<Self> { |
48 | 17 | let base_attrs = [KeyValue::new(CACHE_TYPE, spec.cache_type.clone())]; |
49 | | // A store that tracks its own size reports it under the same cache |
50 | | // type as the operation metrics. Other stores report no size. |
51 | 17 | backend |
52 | 17 | .inner_store(None::<StoreKey<'_>>) |
53 | 17 | .enable_cache_size_metrics(&base_attrs); |
54 | 17 | Arc::new(Self { |
55 | 17 | backend, |
56 | 17 | cache_type: spec.cache_type.clone(), |
57 | 17 | allow_remove: spec.allow_remove, |
58 | 17 | attrs: CacheMetricAttrs::new(&base_attrs), |
59 | 17 | }) |
60 | 17 | } |
61 | | |
62 | 95 | fn duration_ms(start: Instant) -> f64 { |
63 | 95 | start.elapsed().as_secs_f64() * 1000.0 |
64 | 95 | } |
65 | | |
66 | 0 | const fn size_info_bytes(size_info: UploadSizeInfo) -> Option<u64> { |
67 | 0 | match size_info { |
68 | 0 | UploadSizeInfo::ExactSize(size) => Some(size), |
69 | 0 | UploadSizeInfo::MaxSize(_) => None, |
70 | | } |
71 | 0 | } |
72 | | |
73 | 95 | fn record_duration(&self, start: Instant, attrs: &[KeyValue]) { |
74 | 95 | CACHE_METRICS |
75 | 95 | .cache_operation_duration |
76 | 95 | .record(Self::duration_ms(start), attrs); |
77 | 95 | } |
78 | | |
79 | 9 | fn record_write_io(&self, bytes: Option<u64>) { |
80 | 9 | if let Some(bytes) = bytes { |
81 | 9 | CACHE_METRICS |
82 | 9 | .cache_io |
83 | 9 | .add(bytes, self.attrs.write_success()); |
84 | 9 | CACHE_METRICS |
85 | 9 | .cache_entry_size |
86 | 9 | .record(bytes, self.attrs.write_success()); |
87 | 9 | }0 |
88 | 9 | } |
89 | | } |
90 | | |
91 | | impl MetricsComponent for CacheMetricsStore { |
92 | 0 | fn publish( |
93 | 0 | &self, |
94 | 0 | _kind: MetricKind, |
95 | 0 | _field_metadata: MetricFieldData, |
96 | 0 | ) -> Result<MetricPublishKnownKindData, nativelink_metric::Error> { |
97 | 0 | publish!( |
98 | 0 | "cache_type", |
99 | 0 | &self.cache_type, |
100 | 0 | MetricKind::String, |
101 | 0 | "Low-cardinality cache type label emitted on cache metrics" |
102 | | ); |
103 | 0 | let _enter = group!("backend").entered(); |
104 | 0 | self.backend |
105 | 0 | .publish(MetricKind::Component, MetricFieldData::default())?; |
106 | 0 | Ok(MetricPublishKnownKindData::Component) |
107 | 0 | } |
108 | | } |
109 | | |
110 | | #[async_trait] |
111 | | impl StoreDriver for CacheMetricsStore { |
112 | 12 | async fn post_init(self: Arc<Self>) -> Result<(), Error> { |
113 | | self.backend.clone().into_inner().post_init().await?; |
114 | | Ok(()) |
115 | 12 | } |
116 | | |
117 | 22 | async fn remove(self: Pin<&Self>, key: StoreKey<'_>) -> Result<bool, Error> { |
118 | | let start = Instant::now(); |
119 | | // Backends such as memory and filesystem cannot infer whether their |
120 | | // copy is disposable. Require an explicit opt-in on this tier. |
121 | | let result = if self.allow_remove { |
122 | | self.backend.as_store_driver_pin().remove(key).await |
123 | | } else { |
124 | | Ok(false) |
125 | | }; |
126 | | let attrs = match &result { |
127 | | Ok(true) => self.attrs.delete_success(), |
128 | | Ok(false) => self.attrs.delete_miss(), |
129 | | Err(_) => self.attrs.delete_error(), |
130 | | }; |
131 | | CACHE_METRICS.cache_operations.add(1, attrs); |
132 | | self.record_duration(start, attrs); |
133 | | result |
134 | 22 | } |
135 | | |
136 | | /// Not counted as a delete: this tier's own copy is never dropped. |
137 | 8 | async fn remove_cached(self: Pin<&Self>, key: StoreKey<'_>) -> Result<bool, Error> { |
138 | | if self.allow_remove { |
139 | | self.backend.as_store_driver_pin().remove_cached(key).await |
140 | | } else { |
141 | | Ok(false) |
142 | | } |
143 | 8 | } |
144 | | |
145 | | async fn has_with_results( |
146 | | self: Pin<&Self>, |
147 | | keys: &[StoreKey<'_>], |
148 | | results: &mut [Option<u64>], |
149 | 36 | ) -> Result<(), Error> { |
150 | | let start = Instant::now(); |
151 | | let result = self.backend.has_with_results(keys, results).await; |
152 | | if result.is_ok() { |
153 | 36 | let hits = results.iter().filter(|result| result.is_some()).count(); |
154 | | let misses = results.len().saturating_sub(hits); |
155 | | if hits > 0 { |
156 | | CACHE_METRICS |
157 | | .cache_operations |
158 | | .add(hits as u64, self.attrs.read_hit()); |
159 | | } |
160 | | if misses > 0 { |
161 | | CACHE_METRICS |
162 | | .cache_operations |
163 | | .add(misses as u64, self.attrs.read_miss()); |
164 | | } |
165 | | let duration_attrs = if hits > 0 { |
166 | | self.attrs.read_hit() |
167 | | } else { |
168 | | self.attrs.read_miss() |
169 | | }; |
170 | | self.record_duration(start, duration_attrs); |
171 | | } else { |
172 | | CACHE_METRICS |
173 | | .cache_operations |
174 | | .add(keys.len() as u64, self.attrs.read_error()); |
175 | | self.record_duration(start, self.attrs.read_error()); |
176 | | } |
177 | | result |
178 | 36 | } |
179 | | |
180 | | async fn list( |
181 | | self: Pin<&Self>, |
182 | | range: (Bound<StoreKey<'_>>, Bound<StoreKey<'_>>), |
183 | | handler: &mut (dyn for<'a> FnMut(&'a StoreKey) -> bool + Send + Sync + '_), |
184 | 0 | ) -> Result<u64, Error> { |
185 | | self.backend.list(range, handler).await |
186 | 0 | } |
187 | | |
188 | | async fn update( |
189 | | self: Pin<&Self>, |
190 | | key: StoreKey<'_>, |
191 | | reader: DropCloserReadHalf, |
192 | | upload_size: UploadSizeInfo, |
193 | 10 | ) -> Result<u64, Error> { |
194 | | let start = Instant::now(); |
195 | | let result = self.backend.update(key, reader, upload_size).await; |
196 | | if let Ok(size) = &result { |
197 | | CACHE_METRICS |
198 | | .cache_operations |
199 | | .add(1, self.attrs.write_success()); |
200 | | self.record_write_io(Some(*size)); |
201 | | self.record_duration(start, self.attrs.write_success()); |
202 | | } else { |
203 | | CACHE_METRICS |
204 | | .cache_operations |
205 | | .add(1, self.attrs.write_error()); |
206 | | self.record_duration(start, self.attrs.write_error()); |
207 | | } |
208 | | result |
209 | 10 | } |
210 | | |
211 | 26 | fn optimized_for(&self, optimization: StoreOptimizations) -> bool { |
212 | 26 | self.backend.optimized_for(optimization) |
213 | 26 | } |
214 | | |
215 | | async fn update_with_whole_file( |
216 | | self: Pin<&Self>, |
217 | | key: StoreKey<'_>, |
218 | | path: OsString, |
219 | | file: fs::FileSlot, |
220 | | upload_size: UploadSizeInfo, |
221 | 0 | ) -> Result<(u64, Option<fs::FileSlot>), Error> { |
222 | | let start = Instant::now(); |
223 | | let bytes = Self::size_info_bytes(upload_size); |
224 | | let result = self |
225 | | .backend |
226 | | .update_with_whole_file(key, path, file, upload_size) |
227 | | .await; |
228 | | if result.is_ok() { |
229 | | CACHE_METRICS |
230 | | .cache_operations |
231 | | .add(1, self.attrs.write_success()); |
232 | | self.record_write_io(bytes); |
233 | | self.record_duration(start, self.attrs.write_success()); |
234 | | } else { |
235 | | CACHE_METRICS |
236 | | .cache_operations |
237 | | .add(1, self.attrs.write_error()); |
238 | | self.record_duration(start, self.attrs.write_error()); |
239 | | } |
240 | | result |
241 | 0 | } |
242 | | |
243 | 1 | async fn update_oneshot(self: Pin<&Self>, key: StoreKey<'_>, data: Bytes) -> Result<(), Error> { |
244 | | let start = Instant::now(); |
245 | | let bytes = data.len() as u64; |
246 | | let result = self.backend.update_oneshot(key, data).await; |
247 | | if result.is_ok() { |
248 | | CACHE_METRICS |
249 | | .cache_operations |
250 | | .add(1, self.attrs.write_success()); |
251 | | self.record_write_io(Some(bytes)); |
252 | | self.record_duration(start, self.attrs.write_success()); |
253 | | } else { |
254 | | CACHE_METRICS |
255 | | .cache_operations |
256 | | .add(1, self.attrs.write_error()); |
257 | | self.record_duration(start, self.attrs.write_error()); |
258 | | } |
259 | | result |
260 | 1 | } |
261 | | |
262 | | async fn get_part( |
263 | | self: Pin<&Self>, |
264 | | key: StoreKey<'_>, |
265 | | writer: &mut DropCloserWriteHalf, |
266 | | offset: u64, |
267 | | length: Option<u64>, |
268 | 26 | ) -> Result<(), Error> { |
269 | | let start = Instant::now(); |
270 | | let result = self |
271 | | .backend |
272 | | .get_part(key, writer.borrow_mut(), offset, length) |
273 | | .await; |
274 | | match &result { |
275 | | Ok(()) => { |
276 | | CACHE_METRICS.cache_operations.add(1, self.attrs.read_hit()); |
277 | | CACHE_METRICS |
278 | | .cache_io |
279 | | .add(writer.get_bytes_written(), self.attrs.read_hit()); |
280 | | self.record_duration(start, self.attrs.read_hit()); |
281 | | } |
282 | | Err(err) if err.code == Code::NotFound => { |
283 | | CACHE_METRICS |
284 | | .cache_operations |
285 | | .add(1, self.attrs.read_miss()); |
286 | | self.record_duration(start, self.attrs.read_miss()); |
287 | | } |
288 | | Err(_) => { |
289 | | CACHE_METRICS |
290 | | .cache_operations |
291 | | .add(1, self.attrs.read_error()); |
292 | | self.record_duration(start, self.attrs.read_error()); |
293 | | } |
294 | | } |
295 | | result |
296 | 26 | } |
297 | | |
298 | 27 | fn inner_store(&self, _key: Option<StoreKey>) -> &dyn StoreDriver { |
299 | 27 | self |
300 | 27 | } |
301 | | |
302 | 1 | fn as_any(&self) -> &(dyn core::any::Any + Sync + Send + 'static) { |
303 | 1 | self |
304 | 1 | } |
305 | | |
306 | 0 | fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> { |
307 | 0 | self |
308 | 0 | } |
309 | | |
310 | 0 | fn register_health(self: Arc<Self>, registry: &mut HealthRegistryBuilder) { |
311 | 0 | self.backend.clone().register_health(registry); |
312 | 0 | } |
313 | | |
314 | 0 | fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> { |
315 | 0 | self.backend.register_remove_callback(callback) |
316 | 0 | } |
317 | | } |
318 | | |
319 | | #[async_trait] |
320 | | impl HealthStatusIndicator for CacheMetricsStore { |
321 | 0 | fn get_name(&self) -> &'static str { |
322 | 0 | "CacheMetricsStore" |
323 | 0 | } |
324 | | |
325 | | async fn check_health( |
326 | | &self, |
327 | | namespace: std::borrow::Cow<'static, str>, |
328 | 0 | ) -> nativelink_util::health_utils::HealthStatus { |
329 | | self.backend |
330 | | .as_store_driver_pin() |
331 | | .check_health(namespace) |
332 | | .await |
333 | 0 | } |
334 | | } |