Skip to main content

lfsx_server/storage/s3/
usage.rs

1use std::collections::HashMap;
2
3use futures_util::StreamExt;
4
5use super::{S3Store, SIZES_AT_ONCE, sizes};
6use crate::namespace::Namespace;
7
8// What a repository holds, which is the size index read back. It lives beside
9// the index rather than among the object operations, because the two only make
10// sense together: one writes the numbers and the other adds them up.
11
12impl S3Store {
13    // What the bucket holds for this repository, counted from its markers and
14    // the sizes recorded beside them. The markers are empty, so their own size
15    // says nothing, and the objects they claim live under names this prefix
16    // never reaches: the index is what closes that gap.
17    //
18    // The figure is still cached the way the local one is. A listing is cheap
19    // and a quota is checked on every negotiation, which is often enough that
20    // cheap is not the same as free.
21    pub async fn usage_of(&self, ns: &Namespace) -> (u64, u64) {
22        // One listing, which returns the markers and the size index together
23        // because they share the repository's prefix. In a bucket this server
24        // wrote, that is the whole measurement: no request is spent per object.
25        let keys = match self.keys.keys(&Self::own_prefix(ns)).await {
26            Ok(keys) => keys,
27            Err(error) => {
28                // A capacity figure that silently reads zero is worse than one
29                // that is missing, because it looks like an answer.
30                tracing::warn!(%error, "the object store could not be listed");
31                return (0, 0);
32            }
33        };
34
35        let mut indexed = HashMap::new();
36        let mut held = Vec::new();
37
38        for key in keys {
39            if let Some((oid, size)) = sizes::read(&key) {
40                indexed.insert(oid, size);
41            } else if let Some(oid) = key.rsplit('/').next()
42                && crate::storage::LocalStore::validate_oid(oid).is_ok()
43            {
44                held.push(oid.to_owned());
45            }
46        }
47
48        // Only what a marker claims is counted. An index entry whose marker has
49        // gone is inert rather than wrong, which is why a sweep that fails to
50        // tidy one costs an empty key and nothing else.
51        let objects = held.len() as u64;
52        let mut bytes = held.iter().filter_map(|oid| indexed.get(oid)).sum();
53
54        let unindexed: Vec<String> = held
55            .into_iter()
56            .filter(|oid| !indexed.contains_key(oid))
57            .collect();
58
59        if !unindexed.is_empty() {
60            bytes += self.measure_and_index(ns, unindexed).await;
61        }
62
63        (objects, bytes)
64    }
65
66    // The old way, for the objects the index does not cover, and it writes what
67    // it learns so it covers them next time.
68    //
69    // That is the whole migration. A bucket written before the index has markers
70    // and no sizes, and the first reading measures it exactly as this server
71    // always did and leaves the answer behind. There is nothing to run and no
72    // flag to set: it converges by being used.
73    async fn measure_and_index(&self, ns: &Namespace, oids: Vec<String>) -> u64 {
74        tracing::info!(
75            count = oids.len(),
76            "measuring objects the size index does not cover yet, and indexing them"
77        );
78
79        futures_util::stream::iter(oids)
80            .map(|oid| {
81                let store = &self;
82                async move {
83                    let size = store.size_of(&oid).await.unwrap_or_default();
84
85                    if let Err(error) = sizes::write(&store.keys, ns, &oid, size).await {
86                        tracing::warn!(%error, oid, "an object could not be added to the size index");
87                    }
88
89                    size
90                }
91            })
92            .buffer_unordered(SIZES_AT_ONCE)
93            .fold(0, |held, size| async move { held + size })
94            .await
95    }
96}