bp_core/storage/
tier.rs

1//! Storage tier system for BillPouch Pouch services.
2//!
3//! Instead of arbitrary byte quotas, Pouches declare one of five fixed tiers.
4//! Each tier defines:
5//! - An exact on-disk quota that the Pouch reserves.
6//! - Which tier-buckets the Pouch participates in (a T3 Pouch participates in
7//!   T1, T2, and T3 fragment placement).
8//!
9//! ## Tiers
10//!
11//! | Tier | Name     | Quota  |
12//! |------|----------|--------|
13//! | T1   | Pebble   | 10 GB  |
14//! | T2   | Stone    | 100 GB |
15//! | T3   | Boulder  | 500 GB |
16//! | T4   | Rock     | 1 TB   |
17//! | T5   | Monolith | 5 TB   |
18//!
19//! ## One Pouch per node per network
20//!
21//! A daemon rejects a second `hatch pouch --network X` if a Pouch is already
22//! active on that network for this identity.  Two correlated Pouches on the
23//! same machine defeat the purpose of distributed redundancy.
24
25use crate::error::{BpError, BpResult};
26use serde::{Deserialize, Serialize};
27use std::fmt;
28
29// ── Tier constants ────────────────────────────────────────────────────────────
30
31/// 10 GiB in bytes.
32pub const TIER_T1_BYTES: u64 = 10 * 1024 * 1024 * 1024;
33/// 100 GiB in bytes.
34pub const TIER_T2_BYTES: u64 = 100 * 1024 * 1024 * 1024;
35/// 500 GiB in bytes.
36pub const TIER_T3_BYTES: u64 = 500 * 1024 * 1024 * 1024;
37/// 1 TiB in bytes.
38pub const TIER_T4_BYTES: u64 = 1024 * 1024 * 1024 * 1024;
39/// 5 TiB in bytes.
40pub const TIER_T5_BYTES: u64 = 5 * 1024 * 1024 * 1024 * 1024;
41
42// ── StorageTier enum ──────────────────────────────────────────────────────────
43
44/// A fixed storage tier for a Pouch service.
45///
46/// Tiers are ordered: `T1 < T2 < T3 < T4 < T5`.  A Pouch at tier T participates
47/// in fragment placement for all tiers ≤ T (e.g. a T3 Pouch stores fragments
48/// from T1, T2, and T3 files).
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
50#[serde(rename_all = "UPPERCASE")]
51pub enum StorageTier {
52    /// 10 GiB — "Pebble"
53    T1,
54    /// 100 GiB — "Stone"
55    T2,
56    /// 500 GiB — "Boulder"
57    T3,
58    /// 1 TiB — "Rock"
59    T4,
60    /// 5 TiB — "Monolith"
61    T5,
62}
63
64impl StorageTier {
65    /// Return the exact quota in bytes for this tier.
66    pub fn quota_bytes(self) -> u64 {
67        match self {
68            StorageTier::T1 => TIER_T1_BYTES,
69            StorageTier::T2 => TIER_T2_BYTES,
70            StorageTier::T3 => TIER_T3_BYTES,
71            StorageTier::T4 => TIER_T4_BYTES,
72            StorageTier::T5 => TIER_T5_BYTES,
73        }
74    }
75
76    /// Human-readable name for this tier.
77    pub fn name(self) -> &'static str {
78        match self {
79            StorageTier::T1 => "Pebble",
80            StorageTier::T2 => "Stone",
81            StorageTier::T3 => "Boulder",
82            StorageTier::T4 => "Rock",
83            StorageTier::T5 => "Monolith",
84        }
85    }
86
87    /// Return all tiers from T1 up to and including `self`.
88    ///
89    /// Used to determine which tier-buckets a Pouch participates in.
90    pub fn participating_tiers(self) -> &'static [StorageTier] {
91        match self {
92            StorageTier::T1 => &[StorageTier::T1],
93            StorageTier::T2 => &[StorageTier::T1, StorageTier::T2],
94            StorageTier::T3 => &[StorageTier::T1, StorageTier::T2, StorageTier::T3],
95            StorageTier::T4 => &[
96                StorageTier::T1,
97                StorageTier::T2,
98                StorageTier::T3,
99                StorageTier::T4,
100            ],
101            StorageTier::T5 => &[
102                StorageTier::T1,
103                StorageTier::T2,
104                StorageTier::T3,
105                StorageTier::T4,
106                StorageTier::T5,
107            ],
108        }
109    }
110
111    /// Return all defined tiers in ascending order.
112    pub fn all() -> &'static [StorageTier] {
113        &[
114            StorageTier::T1,
115            StorageTier::T2,
116            StorageTier::T3,
117            StorageTier::T4,
118            StorageTier::T5,
119        ]
120    }
121
122    /// Parse a tier from a string like `"T1"`, `"t2"`, `"T3"`, etc.
123    pub fn parse(s: &str) -> BpResult<Self> {
124        match s.to_uppercase().as_str() {
125            "T1" => Ok(StorageTier::T1),
126            "T2" => Ok(StorageTier::T2),
127            "T3" => Ok(StorageTier::T3),
128            "T4" => Ok(StorageTier::T4),
129            "T5" => Ok(StorageTier::T5),
130            other => Err(BpError::InvalidInput(format!(
131                "unknown storage tier '{}'; valid values are T1, T2, T3, T4, T5",
132                other
133            ))),
134        }
135    }
136
137    /// Return the minimum tier that can hold a file of the given byte size.
138    ///
139    /// Returns `None` if the file is too large even for T5.
140    pub fn for_file_size(bytes: u64) -> Option<Self> {
141        if bytes <= TIER_T1_BYTES {
142            Some(StorageTier::T1)
143        } else if bytes <= TIER_T2_BYTES {
144            Some(StorageTier::T2)
145        } else if bytes <= TIER_T3_BYTES {
146            Some(StorageTier::T3)
147        } else if bytes <= TIER_T4_BYTES {
148            Some(StorageTier::T4)
149        } else if bytes <= TIER_T5_BYTES {
150            Some(StorageTier::T5)
151        } else {
152            None
153        }
154    }
155}
156
157impl fmt::Display for StorageTier {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        let (label, name) = match self {
160            StorageTier::T1 => ("T1", "Pebble  — 10 GB"),
161            StorageTier::T2 => ("T2", "Stone   — 100 GB"),
162            StorageTier::T3 => ("T3", "Boulder — 500 GB"),
163            StorageTier::T4 => ("T4", "Rock    — 1 TB"),
164            StorageTier::T5 => ("T5", "Monolith — 5 TB"),
165        };
166        write!(f, "{} ({})", label, name)
167    }
168}
169
170impl std::str::FromStr for StorageTier {
171    type Err = BpError;
172
173    fn from_str(s: &str) -> Result<Self, Self::Err> {
174        StorageTier::parse(s)
175    }
176}
177
178// ── Unit tests ────────────────────────────────────────────────────────────────
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn tier_quota_bytes_correct() {
186        assert_eq!(StorageTier::T1.quota_bytes(), 10 * 1024 * 1024 * 1024);
187        assert_eq!(StorageTier::T2.quota_bytes(), 100 * 1024 * 1024 * 1024);
188        assert_eq!(StorageTier::T3.quota_bytes(), 500 * 1024 * 1024 * 1024);
189        assert_eq!(StorageTier::T4.quota_bytes(), 1024 * 1024 * 1024 * 1024);
190        assert_eq!(StorageTier::T5.quota_bytes(), 5 * 1024 * 1024 * 1024 * 1024);
191    }
192
193    #[test]
194    fn tier_ordering() {
195        assert!(StorageTier::T1 < StorageTier::T2);
196        assert!(StorageTier::T2 < StorageTier::T3);
197        assert!(StorageTier::T3 < StorageTier::T4);
198        assert!(StorageTier::T4 < StorageTier::T5);
199    }
200
201    #[test]
202    fn participating_tiers_correct() {
203        assert_eq!(StorageTier::T1.participating_tiers(), &[StorageTier::T1]);
204        assert_eq!(
205            StorageTier::T3.participating_tiers(),
206            &[StorageTier::T1, StorageTier::T2, StorageTier::T3]
207        );
208        assert_eq!(StorageTier::T5.participating_tiers().len(), 5);
209    }
210
211    #[test]
212    fn parse_case_insensitive() {
213        assert_eq!(StorageTier::parse("t1").unwrap(), StorageTier::T1);
214        assert_eq!(StorageTier::parse("T3").unwrap(), StorageTier::T3);
215        assert_eq!(StorageTier::parse("T5").unwrap(), StorageTier::T5);
216        assert!(StorageTier::parse("T6").is_err());
217        assert!(StorageTier::parse("pebble").is_err());
218    }
219
220    #[test]
221    fn for_file_size() {
222        assert_eq!(StorageTier::for_file_size(1024), Some(StorageTier::T1));
223        assert_eq!(
224            StorageTier::for_file_size(TIER_T1_BYTES),
225            Some(StorageTier::T1)
226        );
227        assert_eq!(
228            StorageTier::for_file_size(TIER_T1_BYTES + 1),
229            Some(StorageTier::T2)
230        );
231        assert_eq!(
232            StorageTier::for_file_size(TIER_T5_BYTES),
233            Some(StorageTier::T5)
234        );
235        assert_eq!(StorageTier::for_file_size(TIER_T5_BYTES + 1), None);
236    }
237
238    #[test]
239    fn serde_roundtrip() {
240        let tier = StorageTier::T3;
241        let json = serde_json::to_string(&tier).unwrap();
242        assert_eq!(json, "\"T3\"");
243        let back: StorageTier = serde_json::from_str(&json).unwrap();
244        assert_eq!(back, StorageTier::T3);
245    }
246
247    #[test]
248    fn from_str_trait() {
249        use std::str::FromStr;
250        assert_eq!(StorageTier::from_str("T2").unwrap(), StorageTier::T2);
251    }
252}