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/shard_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::hash::Hasher;
16
use core::ops::BitXor;
17
use core::pin::Pin;
18
use std::hash::DefaultHasher;
19
use std::sync::Arc;
20
21
use async_trait::async_trait;
22
use futures::future::try_join_all;
23
use futures::stream::{FuturesUnordered, TryStreamExt};
24
use nativelink_config::stores::ShardSpec;
25
use nativelink_error::{Error, ResultExt, error_if};
26
use nativelink_metric::MetricsComponent;
27
use nativelink_util::buf_channel::{DropCloserReadHalf, DropCloserWriteHalf};
28
use nativelink_util::health_utils::{HealthStatusIndicator, default_health_status_indicator};
29
use nativelink_util::store_trait::{
30
    RemoveCallback, Store, StoreDriver, StoreKey, StoreLike, UploadSizeInfo,
31
};
32
33
#[derive(Debug, MetricsComponent)]
34
struct StoreAndWeight {
35
    #[metric(help = "The weight of the store")]
36
    weight: u32,
37
    #[metric(help = "The underlying store")]
38
    store: Store,
39
}
40
41
#[derive(Debug, MetricsComponent)]
42
pub struct ShardStore {
43
    // The weights will always be in ascending order a specific store is chosen based on
44
    // the hash of the key hash that is nearest-binary searched using the u32 as the index.
45
    #[metric(
46
        group = "stores",
47
        help = "The weights and stores that are used to determine which store to use"
48
    )]
49
    weights_and_stores: Vec<StoreAndWeight>,
50
}
51
52
impl ShardStore {
53
15
    pub fn new(spec: &ShardSpec, stores: Vec<Store>) -> Result<Arc<Self>, Error> {
54
0
        error_if!(
55
15
            spec.stores.len() != stores.len(),
56
            "Config shards do not match stores length"
57
        );
58
0
        error_if!(
59
15
            spec.stores.is_empty(),
60
            "ShardStore must have at least one store"
61
        );
62
15
        let total_weight: u64 = spec
63
15
            .stores
64
15
            .iter()
65
50
            .
map15
(|shard_config| u64::from(shard_config.weight.unwrap_or(1)))
66
15
            .sum();
67
15
        let mut weights: Vec<u32> = spec
68
15
            .stores
69
15
            .iter()
70
50
            .
map15
(|shard_config| {
71
50
                u32::try_from(
72
50
                    u64::from(u32::MAX) * u64::from(shard_config.weight.unwrap_or(1))
73
50
                        / total_weight,
74
                )
75
50
                .unwrap_or(u32::MAX)
76
50
            })
77
50
            .
scan15
(0, |state, weight| {
78
50
                *state += weight;
79
50
                Some(*state)
80
50
            })
81
15
            .collect();
82
        // Our last item should always be the max.
83
15
        *weights.last_mut().unwrap() = u32::MAX;
84
15
        Ok(Arc::new(Self {
85
15
            weights_and_stores: weights
86
15
                .into_iter()
87
15
                .zip(stores)
88
50
                .
map15
(|(weight, store)| StoreAndWeight { weight, store })
89
15
                .collect(),
90
        }))
91
15
    }
92
93
5.01k
    fn get_store_index(&self, store_key: &StoreKey) -> usize {
94
5.01k
        let key = match store_key {
95
5.01k
            StoreKey::Digest(digest) => {
96
                // Quote from std primitive array documentation:
97
                //     Array’s try_from(slice) implementations (and the corresponding slice.try_into()
98
                //     array implementations) succeed if the input slice length is the same as the result
99
                //     array length. They optimize especially well when the optimizer can easily determine
100
                //     the slice length, e.g. <[u8; 4]>::try_from(&slice[4..8]).unwrap(). Array implements
101
                //     TryFrom returning.
102
5.01k
                let size_bytes = digest.size_bytes().to_le_bytes();
103
5.01k
                0.bitxor(u32::from_le_bytes(
104
5.01k
                    digest.packed_hash()[0..4].try_into().unwrap(),
105
                ))
106
5.01k
                .bitxor(u32::from_le_bytes(
107
5.01k
                    digest.packed_hash()[4..8].try_into().unwrap(),
108
                ))
109
5.01k
                .bitxor(u32::from_le_bytes(
110
5.01k
                    digest.packed_hash()[8..12].try_into().unwrap(),
111
                ))
112
5.01k
                .bitxor(u32::from_le_bytes(
113
5.01k
                    digest.packed_hash()[12..16].try_into().unwrap(),
114
                ))
115
5.01k
                .bitxor(u32::from_le_bytes(
116
5.01k
                    digest.packed_hash()[16..20].try_into().unwrap(),
117
                ))
118
5.01k
                .bitxor(u32::from_le_bytes(
119
5.01k
                    digest.packed_hash()[20..24].try_into().unwrap(),
120
                ))
121
5.01k
                .bitxor(u32::from_le_bytes(
122
5.01k
                    digest.packed_hash()[24..28].try_into().unwrap(),
123
                ))
124
5.01k
                .bitxor(u32::from_le_bytes(
125
5.01k
                    digest.packed_hash()[28..32].try_into().unwrap(),
126
                ))
127
5.01k
                .bitxor(u32::from_le_bytes(size_bytes[0..4].try_into().unwrap()))
128
5.01k
                .bitxor(u32::from_le_bytes(size_bytes[4..8].try_into().unwrap()))
129
            }
130
0
            StoreKey::Str(s) => {
131
0
                let mut hasher = DefaultHasher::new();
132
0
                hasher.write(s.as_bytes());
133
0
                let key_u64 = hasher.finish();
134
0
                (key_u64 >> 32) as u32 // We only need the top 32 bits.
135
            }
136
        };
137
5.01k
        self.weights_and_stores
138
5.01k
            .binary_search_by_key(&key, |item| item.weight)
139
5.01k
            .unwrap_or_else(|index| index)
140
5.01k
    }
141
142
5.00k
    fn get_store(&self, key: &StoreKey) -> &Store {
143
5.00k
        let index = self.get_store_index(key);
144
5.00k
        &self.weights_and_stores[index].store
145
5.00k
    }
146
}
147
148
#[async_trait]
149
impl StoreDriver for ShardStore {
150
0
    async fn post_init(self: Arc<Self>) -> Result<(), Error> {
151
        let mut futures = vec![];
152
        for store_and_weight in &self.weights_and_stores {
153
            futures.push(store_and_weight.store.clone().into_inner().post_init());
154
        }
155
        try_join_all(futures).await?;
156
        Ok(())
157
0
    }
158
159
    async fn has_with_results(
160
        self: Pin<&Self>,
161
        keys: &[StoreKey<'_>],
162
        results: &mut [Option<u64>],
163
6
    ) -> Result<(), Error> {
164
        type KeyIdxVec = Vec<usize>;
165
        type KeyVec<'a> = Vec<StoreKey<'a>>;
166
167
        if keys.len() == 1 {
168
            // Hot path: It is very common to lookup only one key.
169
            let store_idx = self.get_store_index(&keys[0]);
170
            let store = &self.weights_and_stores[store_idx].store;
171
            return store
172
                .has_with_results(keys, results)
173
                .await
174
                .err_tip(|| "In ShardStore::has_with_results() for store {store_idx}}");
175
        }
176
        let mut keys_for_store: Vec<(KeyIdxVec, KeyVec)> = self
177
            .weights_and_stores
178
            .iter()
179
6
            .map(|_| (Vec::new(), Vec::new()))
180
            .collect();
181
        // Bucket each key into the store that it belongs to.
182
        keys.iter()
183
            .enumerate()
184
6
            .map(|(key_idx, key)| (key, key_idx, self.get_store_index(key)))
185
6
            .for_each(|(key, key_idx, store_idx)| {
186
6
                keys_for_store[store_idx].0.push(key_idx);
187
6
                keys_for_store[store_idx].1.push(key.borrow());
188
6
            });
189
190
        // Build all our futures for each store.
191
        let mut future_stream: FuturesUnordered<_> = keys_for_store
192
            .into_iter()
193
            .enumerate()
194
6
            .map(|(store_idx, (key_idxs, keys))| async move {
195
6
                let store = &self.weights_and_stores[store_idx].store;
196
6
                let mut inner_results = vec![None; keys.len()];
197
6
                store
198
6
                    .has_with_results(&keys, &mut inner_results)
199
6
                    .await
200
6
                    .err_tip(|| "In ShardStore::has_with_results() for store {store_idx}")
?0
;
201
6
                Result::<_, Error>::Ok((key_idxs, inner_results))
202
12
            })
203
            .collect();
204
205
        // Wait for all the stores to finish and populate our output results.
206
        while let Some((key_idxs, inner_results)) = future_stream.try_next().await? {
207
            for (key_idx, inner_result) in key_idxs.into_iter().zip(inner_results) {
208
                results[key_idx] = inner_result;
209
            }
210
        }
211
        Ok(())
212
6
    }
213
214
    async fn update(
215
        self: Pin<&Self>,
216
        key: StoreKey<'_>,
217
        reader: DropCloserReadHalf,
218
        size_info: UploadSizeInfo,
219
5.00k
    ) -> Result<u64, Error> {
220
        let store = self.get_store(&key);
221
        store
222
            .update(key, reader, size_info)
223
            .await
224
            .err_tip(|| "In ShardStore::update()")
225
5.00k
    }
226
227
    async fn get_part(
228
        self: Pin<&Self>,
229
        key: StoreKey<'_>,
230
        writer: &mut DropCloserWriteHalf,
231
        offset: u64,
232
        length: Option<u64>,
233
3
    ) -> Result<(), Error> {
234
        let store = self.get_store(&key);
235
        store
236
            .get_part(key, writer, offset, length)
237
            .await
238
            .err_tip(|| "In ShardStore::get_part()")
239
3
    }
240
241
0
    fn inner_store(&self, key: Option<StoreKey>) -> &'_ dyn StoreDriver {
242
0
        let Some(key) = key else {
243
0
            return self;
244
        };
245
0
        let index = self.get_store_index(&key);
246
0
        self.weights_and_stores[index].store.inner_store(Some(key))
247
0
    }
248
249
0
    fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) {
250
0
        self
251
0
    }
252
253
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> {
254
0
        self
255
0
    }
256
257
0
    fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> {
258
0
        for store in &self.weights_and_stores {
259
0
            store.store.register_remove_callback(callback.clone())?;
260
        }
261
0
        Ok(())
262
0
    }
263
}
264
265
default_health_status_indicator!(ShardStore);