Skip to main content

fxfs/object_store/
project_id.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::errors::FxfsError;
6use crate::lsm_tree::Query;
7use crate::lsm_tree::types::{ItemRef, LayerIterator};
8use crate::object_store::transaction::{LockKey, Mutation, Options, lock_keys};
9use crate::object_store::{
10    BytesAndNodes, ObjectKey, ObjectKeyData, ObjectKind, ObjectStore, ObjectValue, ProjectProperty,
11};
12use anyhow::{Error, anyhow};
13use fprint::TypeFingerprint;
14use fxfs_macros::SerializeKey;
15use serde::{Deserialize, Serialize};
16use std::num::NonZeroU64;
17
18impl ObjectStore {
19    /// Adds a mutation to set the project limit as an attribute with `bytes` and `nodes` to root
20    /// node.
21    pub async fn set_project_limit(
22        &self,
23        project_id: ProjectId,
24        bytes: u64,
25        nodes: u64,
26    ) -> Result<(), Error> {
27        let root_id = self.root_directory_object_id();
28        let mut transaction = self
29            .new_transaction(
30                lock_keys![LockKey::ProjectId {
31                    store_object_id: self.store_object_id,
32                    project_id
33                }],
34                Options::default(),
35            )
36            .await?;
37        transaction.add(
38            self.store_object_id,
39            Mutation::replace_or_insert_object(
40                ObjectKey::project_limit(root_id, project_id),
41                ObjectValue::BytesAndNodes {
42                    bytes: bytes.try_into().map_err(|_| FxfsError::TooBig)?,
43                    nodes: nodes.try_into().map_err(|_| FxfsError::TooBig)?,
44                },
45            ),
46        );
47        transaction.commit().await?;
48        Ok(())
49    }
50
51    /// Clear the limit for a project by tombstoning the limits and usage attributes for the
52    /// given `project_id`. Fails if the project is still in use by one or more nodes.
53    pub async fn clear_project_limit(&self, project_id: ProjectId) -> Result<(), Error> {
54        let root_id = self.root_directory_object_id();
55        let mut transaction = self
56            .new_transaction(
57                lock_keys![LockKey::ProjectId {
58                    store_object_id: self.store_object_id,
59                    project_id
60                }],
61                Options::default(),
62            )
63            .await?;
64        transaction.add(
65            self.store_object_id,
66            Mutation::replace_or_insert_object(
67                ObjectKey::project_limit(root_id, project_id),
68                ObjectValue::None,
69            ),
70        );
71        transaction.commit().await?;
72        Ok(())
73    }
74
75    /// Apply a `project_id` to a given node. Fails if node is not found or target project is zero.
76    pub async fn set_project_for_node(
77        &self,
78        node_id: u64,
79        project_id: ProjectId,
80    ) -> Result<(), Error> {
81        let root_id = self.root_directory_object_id();
82        let mut transaction = self
83            .new_transaction(
84                lock_keys![LockKey::object(self.store_object_id, node_id)],
85                Options::default(),
86            )
87            .await?;
88
89        let object_key = ObjectKey::object(node_id);
90        let (kind, mut attributes) =
91            match self.tree().find_value(&object_key).await?.ok_or(FxfsError::NotFound)? {
92                ObjectValue::Object { kind, attributes } => (kind, attributes),
93                _ => return Err(FxfsError::Inconsistent.into()),
94            };
95        // Make sure the object kind makes sense.
96        match kind {
97            ObjectKind::File { .. } | ObjectKind::Directory { .. } => (),
98            // For now, we don't support attributes on symlink objects, so setting a project id
99            // doesn't make sense.
100            ObjectKind::Symlink { .. } | ObjectKind::EncryptedSymlink { .. } => {
101                return Err(FxfsError::NotSupported.into());
102            }
103            ObjectKind::Graveyard => return Err(FxfsError::Inconsistent.into()),
104        }
105        let storage_size = attributes.allocated_size.try_into().map_err(|_| FxfsError::TooBig)?;
106        let old_project_id = attributes.project_id;
107        if old_project_id == Some(project_id) {
108            return Ok(());
109        }
110        attributes.project_id = Some(project_id);
111
112        transaction.add(
113            self.store_object_id,
114            Mutation::replace_or_insert_object(
115                object_key,
116                ObjectValue::Object { kind, attributes },
117            ),
118        );
119        transaction.merge_bytes_and_nodes(
120            self.store_object_id,
121            ObjectKey::project_usage(root_id, project_id),
122            BytesAndNodes { bytes: storage_size, nodes: 1 },
123        );
124        if let Some(old_project_id) = old_project_id {
125            transaction.merge_bytes_and_nodes(
126                self.store_object_id,
127                ObjectKey::project_usage(root_id, old_project_id),
128                BytesAndNodes { bytes: -storage_size, nodes: -1 },
129            );
130        }
131        transaction.commit().await?;
132        Ok(())
133    }
134
135    /// Return the project_id associated with the given `node_id`.
136    pub async fn get_project_for_node(&self, node_id: u64) -> Result<Option<ProjectId>, Error> {
137        let project_id = self
138            .tree()
139            .find_map(&ObjectKey::object(node_id), |item| match item.value {
140                ObjectValue::Object { attributes, .. } => Ok(attributes.project_id),
141                _ => Err(anyhow!(FxfsError::Inconsistent)),
142            })
143            .await?
144            .ok_or(FxfsError::NotFound)??;
145        Ok(project_id)
146    }
147
148    /// Remove the project id for a given `node_id`. The call will do nothing and return success
149    /// if the node is found to not be associated with any project.
150    pub async fn clear_project_for_node(&self, node_id: u64) -> Result<(), Error> {
151        let root_id = self.root_directory_object_id();
152        let mut transaction = self
153            .new_transaction(
154                lock_keys![LockKey::object(self.store_object_id, node_id)],
155                Options::default(),
156            )
157            .await?;
158
159        let object_key = ObjectKey::object(node_id);
160        let (kind, mut attributes) =
161            match self.tree().find_value(&object_key).await?.ok_or(FxfsError::NotFound)? {
162                ObjectValue::Object { kind, attributes } => (kind, attributes),
163                _ => return Err(FxfsError::Inconsistent.into()),
164            };
165        let Some(old_project_id) = attributes.project_id else {
166            return Ok(());
167        };
168        // Make sure the object kind makes sense.
169        match kind {
170            ObjectKind::File { .. } | ObjectKind::Directory { .. } => (),
171            // For now, we don't support attributes on symlink objects, so setting a project id
172            // doesn't make sense.
173            ObjectKind::Symlink { .. } | ObjectKind::EncryptedSymlink { .. } => {
174                return Err(FxfsError::NotSupported.into());
175            }
176            ObjectKind::Graveyard => return Err(FxfsError::Inconsistent.into()),
177        }
178        attributes.project_id = None;
179        let storage_size = attributes.allocated_size;
180        transaction.add(
181            self.store_object_id,
182            Mutation::replace_or_insert_object(
183                object_key,
184                ObjectValue::Object { kind, attributes },
185            ),
186        );
187        // Not safe to convert storage_size to i64, as space usage can exceed i64 in size. Not
188        // going to deal with handling such enormous files, fail the request.
189        transaction.merge_bytes_and_nodes(
190            self.store_object_id,
191            ObjectKey::project_usage(root_id, old_project_id),
192            BytesAndNodes {
193                bytes: -(storage_size.try_into().map_err(|_| FxfsError::TooBig)?),
194                nodes: -1,
195            },
196        );
197        transaction.commit().await?;
198        Ok(())
199    }
200
201    /// Returns a list of project ids currently tracked with project limits or usage in ascending
202    /// order, beginning after `last_id` and providing up to `max_entries`. If `max_entries` would
203    /// be exceeded then it also returns the final id in the list, for use in the following call to
204    /// resume the listing.
205    pub async fn list_projects(
206        &self,
207        start_id: Option<ProjectId>,
208        max_entries: usize,
209    ) -> Result<(Vec<ProjectId>, Option<ProjectId>), Error> {
210        let start_id = start_id.unwrap_or(ProjectId::SORTED_START);
211        let root_dir_id = self.root_directory_object_id();
212        let layer_set = self.tree().layer_set();
213        let mut merger = layer_set.merger();
214        let mut iter = merger
215            .query(Query::FullRange(&ObjectKey::project_limit(root_dir_id, start_id)))
216            .await?;
217        let mut entries = Vec::new();
218        let mut prev_entry: Option<ProjectId> = None;
219        let mut next_entry = None;
220        while let Some(ItemRef { key: ObjectKey { object_id, data: key_data }, value, .. }) =
221            iter.get()
222        {
223            // We've moved outside the target object id.
224            if *object_id != root_dir_id {
225                break;
226            }
227            match key_data {
228                ObjectKeyData::Project { project_id, .. } => {
229                    // Bypass deleted or repeated entries.
230                    if *value != ObjectValue::None && prev_entry < Some(*project_id) {
231                        if entries.len() == max_entries {
232                            next_entry = Some(*project_id);
233                            break;
234                        }
235                        prev_entry = Some(*project_id);
236                        entries.push(*project_id);
237                    }
238                }
239                // We've moved outside the list of Project limits and usages.
240                _ => {
241                    break;
242                }
243            }
244            iter.advance().await?;
245        }
246        // Skip deleted entries
247        Ok((entries, next_entry))
248    }
249
250    /// Looks up the limit and usage of `project_id` as a pair of bytes and notes. Any of the two
251    /// fields not found will return None for them.
252    pub async fn project_info(
253        &self,
254        project_id: ProjectId,
255    ) -> Result<(Option<(u64, u64)>, Option<(u64, u64)>), Error> {
256        let root_id = self.root_directory_object_id();
257        let layer_set = self.tree().layer_set();
258        let mut merger = layer_set.merger();
259        let mut iter =
260            merger.query(Query::FullRange(&ObjectKey::project_limit(root_id, project_id))).await?;
261        let mut limit = None;
262        let mut usage = None;
263        // The limit should be immediately followed by the usage if both exist.
264        while let Some(ItemRef { key: ObjectKey { object_id, data: key_data }, value, .. }) =
265            iter.get()
266        {
267            // Should be within the bounds of the root dir id.
268            if *object_id != root_id {
269                break;
270            }
271            if let (
272                ObjectKeyData::Project { project_id: found_project_id, property },
273                ObjectValue::BytesAndNodes { bytes, nodes },
274            ) = (key_data, value)
275            {
276                // Outside the range for target project information.
277                if *found_project_id != project_id {
278                    break;
279                }
280                let raw_value: (u64, u64) = (
281                    // Should succeed in conversions since they shouldn't be negative.
282                    (*bytes).try_into().map_err(|_| FxfsError::Inconsistent)?,
283                    (*nodes).try_into().map_err(|_| FxfsError::Inconsistent)?,
284                );
285                match property {
286                    ProjectProperty::Limit => limit = Some(raw_value),
287                    ProjectProperty::Usage => usage = Some(raw_value),
288                }
289            } else {
290                break;
291            }
292            iter.advance().await?;
293        }
294        Ok((limit, usage))
295    }
296}
297
298#[derive(
299    Clone,
300    Copy,
301    PartialEq,
302    Eq,
303    PartialOrd,
304    Ord,
305    Debug,
306    Serialize,
307    Deserialize,
308    Hash,
309    TypeFingerprint,
310    SerializeKey,
311)]
312#[cfg_attr(fuzz, derive(arbitrary::Arbitrary))]
313#[repr(transparent)]
314pub struct ProjectId(NonZeroU64);
315
316impl ProjectId {
317    pub const SORTED_START: Self = Self::new(1).unwrap();
318
319    pub const fn new(project_id: u64) -> Option<Self> {
320        match NonZeroU64::new(project_id) {
321            None => None,
322            Some(non_zero) => Some(Self(non_zero)),
323        }
324    }
325
326    /// Returns the underlying `u64`.
327    pub const fn raw(self) -> u64 {
328        self.0.get()
329    }
330}
331
332impl log::kv::ToValue for ProjectId {
333    fn to_value(&self) -> log::kv::Value<'_> {
334        log::kv::Value::from(self.0)
335    }
336}
337
338impl std::fmt::Display for ProjectId {
339    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340        std::fmt::Display::fmt(&self.0, f)
341    }
342}
343
344/// An extension trait for project ids that makes working with `Option<ProjectId>` simpler.
345pub trait ProjectIdExt {
346    /// Returns the underlying `u64`.
347    fn raw(self) -> u64;
348}
349
350impl ProjectIdExt for Option<ProjectId> {
351    fn raw(self) -> u64 {
352        match self {
353            None => 0,
354            Some(project_id) => project_id.raw(),
355        }
356    }
357}
358
359pub mod optional_project_id {
360    use super::{ProjectId, ProjectIdExt};
361    use serde::{Deserializer, Serializer};
362
363    /// Serialize `Option<ProjectId>` as a u64.
364    pub fn serialize<S>(value: &Option<ProjectId>, serializer: S) -> Result<S::Ok, S::Error>
365    where
366        S: Serializer,
367    {
368        serializer.serialize_u64(value.raw())
369    }
370
371    /// Deserialize `Option<ProjectId>` from a u64.
372    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<ProjectId>, D::Error>
373    where
374        D: Deserializer<'de>,
375    {
376        deserializer.deserialize_u64(Visitor)
377    }
378
379    struct Visitor;
380    impl<'de> serde::de::Visitor<'de> for Visitor {
381        type Value = Option<ProjectId>;
382
383        fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384            write!(formatter, "a u64",)
385        }
386
387        fn visit_u64<E>(self, raw: u64) -> Result<Self::Value, E>
388        where
389            E: serde::de::Error,
390        {
391            Ok(ProjectId::new(raw))
392        }
393    }
394
395    pub fn fingerprint<T>() -> String {
396        "u64".to_string()
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    // ObjectStore project id tests are done end to end from the Fuchsia endpoint and so are in
403    // platform/fuchsia/volume.rs
404
405    use super::{ProjectId, ProjectIdExt};
406    use crate::serialized_types::{LATEST_VERSION, Versioned};
407    use serde::{Deserialize, Serialize};
408
409    // Versioned is used here to get the same bincode settings as would be used when the ProjectId
410    // is contained inside of a Versioned struct.
411    impl Versioned for ProjectId {}
412
413    #[test]
414    fn test_project_id_serialization_matches_u64() {
415        fn verify_matches(x: u64) {
416            // 1. Serialize x as u64
417            let mut u64_buf = Vec::new();
418            x.serialize_into(&mut u64_buf).unwrap();
419
420            // 2. Serialize ProjectId
421            let project_id = ProjectId::new(x).unwrap();
422            let mut project_id_buf = Vec::new();
423            project_id.serialize_into(&mut project_id_buf).unwrap();
424
425            // 3. Verify binary match
426            assert_eq!(u64_buf, project_id_buf);
427
428            // 4. Deserialize u64 bytes as ProjectId
429            let deserialized_project_id =
430                ProjectId::deserialize_from(&mut u64_buf.as_slice(), LATEST_VERSION).unwrap();
431            assert_eq!(deserialized_project_id, project_id);
432
433            // 5. Deserialize ProjectId bytes as u64
434            let deserialized_u64 =
435                u64::deserialize_from(&mut project_id_buf.as_slice(), LATEST_VERSION).unwrap();
436            assert_eq!(deserialized_u64, x);
437        }
438
439        verify_matches(1);
440        verify_matches(2);
441        verify_matches(u16::MAX as u64 - 1);
442        verify_matches(u16::MAX as u64);
443        verify_matches(u16::MAX as u64 + 1);
444        verify_matches(u32::MAX as u64 - 1);
445        verify_matches(u32::MAX as u64);
446        verify_matches(u32::MAX as u64 + 1);
447        verify_matches(u64::MAX - 1);
448        verify_matches(u64::MAX);
449    }
450
451    #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Versioned)]
452    struct OptionWrapper {
453        #[serde(with = "super::optional_project_id")]
454        project_id: Option<ProjectId>,
455    }
456
457    #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Versioned)]
458    struct U64Wrapper {
459        project_id: u64,
460    }
461
462    #[test]
463    fn test_optional_project_id_serialization_matches_u64() {
464        fn verify_matches(x: u64) {
465            // 1. Serialize x as u64 inside wrapper
466            let u64_wrapper = U64Wrapper { project_id: x };
467            let mut u64_buf = Vec::new();
468            u64_wrapper.serialize_into(&mut u64_buf).unwrap();
469
470            // 2. Serialize Option<ProjectId> inside wrapper
471            let opt_project_id = OptionWrapper { project_id: ProjectId::new(x) };
472            let mut opt_buf = Vec::new();
473            opt_project_id.serialize_into(&mut opt_buf).unwrap();
474
475            // 3. Verify their binary representations match exactly
476            assert_eq!(u64_buf, opt_buf);
477
478            // 4. Deserialize the serialized u64 bytes as Option<ProjectId>
479            let deserialized_opt =
480                OptionWrapper::deserialize_from(&mut u64_buf.as_slice(), LATEST_VERSION).unwrap();
481            assert_eq!(deserialized_opt, opt_project_id);
482
483            // 5. Deserialize the serialized Option<ProjectId> bytes as u64
484            let deserialized_u64 =
485                U64Wrapper::deserialize_from(&mut opt_buf.as_slice(), LATEST_VERSION).unwrap();
486            assert_eq!(deserialized_u64, u64_wrapper);
487        }
488
489        verify_matches(0);
490        verify_matches(1);
491        verify_matches(2);
492        verify_matches(u16::MAX as u64 - 1);
493        verify_matches(u16::MAX as u64);
494        verify_matches(u16::MAX as u64 + 1);
495        verify_matches(u32::MAX as u64 - 1);
496        verify_matches(u32::MAX as u64);
497        verify_matches(u32::MAX as u64 + 1);
498        verify_matches(u64::MAX - 1);
499        verify_matches(u64::MAX);
500    }
501
502    #[test]
503    fn test_option_project_id_sorting() {
504        let none = ProjectId::new(0);
505        assert!(none.is_none());
506
507        let one = ProjectId::new(1);
508        let max = ProjectId::new(u64::MAX);
509
510        // None should be sorted before all project ids.
511        assert!(none < one);
512        assert!(none < max);
513        assert!(one < max);
514    }
515
516    #[test]
517    fn test_project_id_raw() {
518        assert_eq!(ProjectId::new(0).raw(), 0);
519        assert_eq!(ProjectId::new(1).raw(), 1);
520        assert_eq!(ProjectId::new(u64::MAX).raw(), u64::MAX);
521    }
522}