Coverage Report

Created: 2026-07-21 15:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-store/src/cache_metrics_store.rs
Line
Count
Source
1
// Copyright 2026 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::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
    attrs: CacheMetricAttrs,
43
}
44
45
impl CacheMetricsStore {
46
2
    pub fn new(spec: &CacheMetricsSpec, backend: Store) -> Arc<Self> {
47
2
        Arc::new(Self {
48
2
            backend,
49
2
            cache_type: spec.cache_type.clone(),
50
2
            attrs: CacheMetricAttrs::new(&[KeyValue::new(CACHE_TYPE, spec.cache_type.clone())]),
51
2
        })
52
2
    }
53
54
4
    fn duration_ms(start: Instant) -> f64 {
55
4
        start.elapsed().as_secs_f64() * 1000.0
56
4
    }
57
58
0
    const fn size_info_bytes(size_info: UploadSizeInfo) -> Option<u64> {
59
0
        match size_info {
60
0
            UploadSizeInfo::ExactSize(size) => Some(size),
61
0
            UploadSizeInfo::MaxSize(_) => None,
62
        }
63
0
    }
64
65
4
    fn record_duration(&self, start: Instant, attrs: &[KeyValue]) {
66
4
        CACHE_METRICS
67
4
            .cache_operation_duration
68
4
            .record(Self::duration_ms(start), attrs);
69
4
    }
70
71
1
    fn record_write_io(&self, bytes: Option<u64>) {
72
1
        if let Some(bytes) = bytes {
73
1
            CACHE_METRICS
74
1
                .cache_io
75
1
                .add(bytes, self.attrs.write_success());
76
1
            CACHE_METRICS
77
1
                .cache_entry_size
78
1
                .record(bytes, self.attrs.write_success());
79
1
        
}0
80
1
    }
81
}
82
83
impl MetricsComponent for CacheMetricsStore {
84
0
    fn publish(
85
0
        &self,
86
0
        _kind: MetricKind,
87
0
        _field_metadata: MetricFieldData,
88
0
    ) -> Result<MetricPublishKnownKindData, nativelink_metric::Error> {
89
0
        publish!(
90
0
            "cache_type",
91
0
            &self.cache_type,
92
0
            MetricKind::String,
93
0
            "Low-cardinality cache type label emitted on cache metrics"
94
        );
95
0
        let _enter = group!("backend").entered();
96
0
        self.backend
97
0
            .publish(MetricKind::Component, MetricFieldData::default())?;
98
0
        Ok(MetricPublishKnownKindData::Component)
99
0
    }
100
}
101
102
#[async_trait]
103
impl StoreDriver for CacheMetricsStore {
104
0
    async fn post_init(self: Arc<Self>) -> Result<(), Error> {
105
        self.backend.clone().into_inner().post_init().await?;
106
        Ok(())
107
0
    }
108
109
    async fn has_with_results(
110
        self: Pin<&Self>,
111
        keys: &[StoreKey<'_>],
112
        results: &mut [Option<u64>],
113
1
    ) -> Result<(), Error> {
114
        let start = Instant::now();
115
        let result = self.backend.has_with_results(keys, results).await;
116
        if result.is_ok() {
117
1
            let hits = results.iter().filter(|result| result.is_some()).count();
118
            let misses = results.len().saturating_sub(hits);
119
            if hits > 0 {
120
                CACHE_METRICS
121
                    .cache_operations
122
                    .add(hits as u64, self.attrs.read_hit());
123
            }
124
            if misses > 0 {
125
                CACHE_METRICS
126
                    .cache_operations
127
                    .add(misses as u64, self.attrs.read_miss());
128
            }
129
            let duration_attrs = if hits > 0 {
130
                self.attrs.read_hit()
131
            } else {
132
                self.attrs.read_miss()
133
            };
134
            self.record_duration(start, duration_attrs);
135
        } else {
136
            CACHE_METRICS
137
                .cache_operations
138
                .add(keys.len() as u64, self.attrs.read_error());
139
            self.record_duration(start, self.attrs.read_error());
140
        }
141
        result
142
1
    }
143
144
    async fn list(
145
        self: Pin<&Self>,
146
        range: (Bound<StoreKey<'_>>, Bound<StoreKey<'_>>),
147
        handler: &mut (dyn for<'a> FnMut(&'a StoreKey) -> bool + Send + Sync + '_),
148
0
    ) -> Result<u64, Error> {
149
        self.backend.list(range, handler).await
150
0
    }
151
152
    async fn update(
153
        self: Pin<&Self>,
154
        key: StoreKey<'_>,
155
        reader: DropCloserReadHalf,
156
        upload_size: UploadSizeInfo,
157
0
    ) -> Result<u64, Error> {
158
        let start = Instant::now();
159
        let result = self.backend.update(key, reader, upload_size).await;
160
        if let Ok(size) = &result {
161
            CACHE_METRICS
162
                .cache_operations
163
                .add(1, self.attrs.write_success());
164
            self.record_write_io(Some(*size));
165
            self.record_duration(start, self.attrs.write_success());
166
        } else {
167
            CACHE_METRICS
168
                .cache_operations
169
                .add(1, self.attrs.write_error());
170
            self.record_duration(start, self.attrs.write_error());
171
        }
172
        result
173
0
    }
174
175
0
    fn optimized_for(&self, optimization: StoreOptimizations) -> bool {
176
0
        self.backend.optimized_for(optimization)
177
0
    }
178
179
    async fn update_with_whole_file(
180
        self: Pin<&Self>,
181
        key: StoreKey<'_>,
182
        path: OsString,
183
        file: fs::FileSlot,
184
        upload_size: UploadSizeInfo,
185
0
    ) -> Result<(u64, Option<fs::FileSlot>), Error> {
186
        let start = Instant::now();
187
        let bytes = Self::size_info_bytes(upload_size);
188
        let result = self
189
            .backend
190
            .update_with_whole_file(key, path, file, upload_size)
191
            .await;
192
        if result.is_ok() {
193
            CACHE_METRICS
194
                .cache_operations
195
                .add(1, self.attrs.write_success());
196
            self.record_write_io(bytes);
197
            self.record_duration(start, self.attrs.write_success());
198
        } else {
199
            CACHE_METRICS
200
                .cache_operations
201
                .add(1, self.attrs.write_error());
202
            self.record_duration(start, self.attrs.write_error());
203
        }
204
        result
205
0
    }
206
207
1
    async fn update_oneshot(self: Pin<&Self>, key: StoreKey<'_>, data: Bytes) -> Result<(), Error> {
208
        let start = Instant::now();
209
        let bytes = data.len() as u64;
210
        let result = self.backend.update_oneshot(key, data).await;
211
        if result.is_ok() {
212
            CACHE_METRICS
213
                .cache_operations
214
                .add(1, self.attrs.write_success());
215
            self.record_write_io(Some(bytes));
216
            self.record_duration(start, self.attrs.write_success());
217
        } else {
218
            CACHE_METRICS
219
                .cache_operations
220
                .add(1, self.attrs.write_error());
221
            self.record_duration(start, self.attrs.write_error());
222
        }
223
        result
224
1
    }
225
226
    async fn get_part(
227
        self: Pin<&Self>,
228
        key: StoreKey<'_>,
229
        writer: &mut DropCloserWriteHalf,
230
        offset: u64,
231
        length: Option<u64>,
232
2
    ) -> Result<(), Error> {
233
        let start = Instant::now();
234
        let result = self
235
            .backend
236
            .get_part(key, writer.borrow_mut(), offset, length)
237
            .await;
238
        match &result {
239
            Ok(()) => {
240
                CACHE_METRICS.cache_operations.add(1, self.attrs.read_hit());
241
                CACHE_METRICS
242
                    .cache_io
243
                    .add(writer.get_bytes_written(), self.attrs.read_hit());
244
                self.record_duration(start, self.attrs.read_hit());
245
            }
246
            Err(err) if err.code == Code::NotFound => {
247
                CACHE_METRICS
248
                    .cache_operations
249
                    .add(1, self.attrs.read_miss());
250
                self.record_duration(start, self.attrs.read_miss());
251
            }
252
            Err(_) => {
253
                CACHE_METRICS
254
                    .cache_operations
255
                    .add(1, self.attrs.read_error());
256
                self.record_duration(start, self.attrs.read_error());
257
            }
258
        }
259
        result
260
2
    }
261
262
1
    fn inner_store(&self, _key: Option<StoreKey>) -> &dyn StoreDriver {
263
1
        self
264
1
    }
265
266
1
    fn as_any(&self) -> &(dyn core::any::Any + Sync + Send + 'static) {
267
1
        self
268
1
    }
269
270
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> {
271
0
        self
272
0
    }
273
274
0
    fn register_health(self: Arc<Self>, registry: &mut HealthRegistryBuilder) {
275
0
        self.backend.clone().register_health(registry);
276
0
    }
277
278
0
    fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> {
279
0
        self.backend.register_remove_callback(callback)
280
0
    }
281
}
282
283
#[async_trait]
284
impl HealthStatusIndicator for CacheMetricsStore {
285
0
    fn get_name(&self) -> &'static str {
286
0
        "CacheMetricsStore"
287
0
    }
288
289
    async fn check_health(
290
        &self,
291
        namespace: std::borrow::Cow<'static, str>,
292
0
    ) -> nativelink_util::health_utils::HealthStatus {
293
        self.backend
294
            .as_store_driver_pin()
295
            .check_health(namespace)
296
            .await
297
0
    }
298
}