Skip to main content

puffin/puffin_manager/
file_key.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (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//     http://www.apache.org/licenses/LICENSE-2.0
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
15use std::sync::Arc;
16
17use uuid::Uuid;
18
19/// Stable storage identity shared by file keys from one accessor.
20/// Fields stay separate so separators in paths or store names cannot cause collisions.
21/// Canonical names can use [`Self::new`]; backends whose metadata does not uniquely
22/// identify their endpoint or configuration use [`Self::unique`] and share that scope.
23#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24pub struct PuffinStorageScope {
25    scheme: String,
26    name: String,
27    root: String,
28    instance_id: Uuid,
29}
30
31impl PuffinStorageScope {
32    /// Identifies the object store backend, name and root.
33    /// Reconstructed accessors must use the same canonical values for the same storage.
34    /// The name must distinguish backends with different endpoints or configurations.
35    pub fn new(scheme: String, name: String, root: String) -> Self {
36        Self {
37            scheme,
38            name,
39            root,
40            instance_id: Uuid::nil(),
41        }
42    }
43
44    /// Creates isolated identity when store metadata alone cannot distinguish backends.
45    /// Accessors for the same live backend share this scope rather than recreating it.
46    pub fn unique(scheme: String, name: String, root: String) -> Self {
47        Self {
48            scheme,
49            name,
50            root,
51            instance_id: Uuid::new_v4(),
52        }
53    }
54}
55
56/// Immutable file identity for caches; the handle must include its physical version.
57/// Equal storage scopes compare by value even when independently allocated.
58#[derive(Debug, Clone, PartialEq, Eq, Hash)]
59pub struct PuffinFileKey<H> {
60    scope: Arc<PuffinStorageScope>,
61    path: Arc<str>,
62    handle: H,
63}
64
65impl<H> PuffinFileKey<H> {
66    /// Combines the accessor's resolved file path and versioned handle with its store scope.
67    pub fn new(scope: Arc<PuffinStorageScope>, path: String, handle: H) -> Self {
68        Self {
69            scope,
70            path: path.into(),
71            handle,
72        }
73    }
74
75    /// Converts the handle representation while preserving the same resolved storage location.
76    pub fn map_handle<T>(self, map: impl FnOnce(H) -> T) -> PuffinFileKey<T> {
77        PuffinFileKey {
78            scope: self.scope,
79            path: self.path,
80            handle: map(self.handle),
81        }
82    }
83
84    /// Returns the versioned handle for cache invalidation and file bookkeeping.
85    pub fn handle(&self) -> &H {
86        &self.handle
87    }
88
89    /// Conservative key size including the shared scope, but excluding any heap
90    /// allocations owned by the generic handle. Shared scope is charged per key.
91    pub fn estimated_size(&self) -> usize {
92        std::mem::size_of::<Self>()
93            + std::mem::size_of::<PuffinStorageScope>()
94            + self.scope.scheme.capacity()
95            + self.scope.name.capacity()
96            + self.scope.root.capacity()
97            + self.path.len()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use std::collections::HashSet;
104
105    use super::*;
106
107    fn scope(parts: [&str; 3]) -> Arc<PuffinStorageScope> {
108        let [scheme, name, root] = parts.map(str::to_string);
109        Arc::new(PuffinStorageScope::new(scheme, name, root))
110    }
111
112    #[test]
113    fn test_reconstructed_keys_share_identity() {
114        let parts = ["s3", "bucket", "prefix/"];
115        let original = PuffinFileKey::new(scope(parts), "table/file".into(), (17_u64, 2_u32));
116        let reconstructed = PuffinFileKey::new(scope(parts), "table/file".into(), (17_u64, 2_u32));
117        let keys = HashSet::from([original.clone()]);
118        assert!(keys.contains(&reconstructed));
119        assert!(keys.contains(&original.clone()));
120        assert_eq!(&(17_u64, 2_u32), original.handle());
121        assert!(!keys.contains(&PuffinFileKey::new(
122            scope(parts),
123            "table/file".into(),
124            (17_u64, 3_u32)
125        )));
126        assert!(!keys.contains(&PuffinFileKey::new(
127            scope(parts),
128            "table/file".into(),
129            (18_u64, 2_u32)
130        )));
131    }
132
133    #[test]
134    fn test_unique_scopes_require_explicit_sharing() {
135        let first = Arc::new(PuffinStorageScope::unique(
136            "s3".into(),
137            "bucket".into(),
138            "/".into(),
139        ));
140        let second = Arc::new(PuffinStorageScope::unique(
141            "s3".into(),
142            "bucket".into(),
143            "/".into(),
144        ));
145        let key = PuffinFileKey::new(first.clone(), "file".into(), 1_u64);
146        assert_eq!(key, PuffinFileKey::new(first, "file".into(), 1_u64));
147        assert_ne!(key, PuffinFileKey::new(second, "file".into(), 1_u64));
148    }
149
150    #[test]
151    fn test_storage_components_are_independent() {
152        let parts = ["s3", "bucket", "prefix/"];
153        let original = PuffinFileKey::new(scope(parts), "table/file".into(), 17_u64);
154        for index in 0..parts.len() {
155            let mut changed = parts;
156            changed[index] = "different";
157            assert_ne!(
158                original,
159                PuffinFileKey::new(scope(changed), "table/file".into(), 17_u64)
160            );
161        }
162        assert_ne!(
163            original,
164            PuffinFileKey::new(scope(parts), "other/file".into(), 17_u64)
165        );
166        assert_ne!(
167            PuffinFileKey::new(scope(["s3", "a:b", "c"]), "d".into(), 17_u64),
168            PuffinFileKey::new(scope(["s3", "a", "b:c"]), "d".into(), 17_u64),
169        );
170    }
171}