1use hashtree_core::store::StoreError;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use std::path::PathBuf;
5use std::str::FromStr;
6use std::time::Duration;
7use uuid::Uuid;
8
9pub(super) const DEFAULT_CATALOG_MAP_SIZE_BYTES: u64 = 16 * 1024 * 1024 * 1024;
10pub(super) const MIN_MEMBER_MAP_SIZE_BYTES: u64 = 16 * 1024 * 1024;
11
12#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
14pub struct PoolMemberId(pub(super) [u8; 16]);
15
16impl PoolMemberId {
17 pub fn new() -> Self {
18 Self(*Uuid::new_v4().as_bytes())
19 }
20
21 pub fn as_bytes(&self) -> &[u8; 16] {
22 &self.0
23 }
24}
25
26impl Default for PoolMemberId {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl fmt::Debug for PoolMemberId {
33 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34 fmt::Display::fmt(self, formatter)
35 }
36}
37
38impl fmt::Display for PoolMemberId {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 Uuid::from_bytes(self.0).fmt(formatter)
41 }
42}
43
44impl FromStr for PoolMemberId {
45 type Err = StoreError;
46
47 fn from_str(value: &str) -> Result<Self, Self::Err> {
48 let id = Uuid::parse_str(value)
49 .map_err(|error| StoreError::Other(format!("invalid pool member id: {error}")))?;
50 Ok(Self(*id.as_bytes()))
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "kebab-case")]
56pub enum PoolMemberState {
57 Active,
58 Draining,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct PoolMemberConfig {
64 pub path: PathBuf,
65 pub capacity_bytes: u64,
66 pub map_size_bytes: u64,
67 pub external_blob_dir: Option<PathBuf>,
68 pub external_blob_min_bytes: Option<u64>,
69 pub external_blob_sync: bool,
70 pub external_pack_target_bytes: Option<u64>,
71 pub max_read_concurrency: u32,
72 pub max_write_concurrency: u32,
73}
74
75impl PoolMemberConfig {
76 pub fn new(path: PathBuf, capacity_bytes: u64) -> Self {
77 Self {
78 path,
79 capacity_bytes,
80 map_size_bytes: capacity_bytes.max(MIN_MEMBER_MAP_SIZE_BYTES),
81 external_blob_dir: None,
82 external_blob_min_bytes: None,
83 external_blob_sync: true,
84 external_pack_target_bytes: None,
85 max_read_concurrency: 64,
86 max_write_concurrency: 16,
87 }
88 }
89
90 pub fn with_map_size_bytes(mut self, map_size_bytes: u64) -> Self {
91 self.map_size_bytes = map_size_bytes.max(MIN_MEMBER_MAP_SIZE_BYTES);
92 self
93 }
94
95 pub fn with_external_blobs(
96 mut self,
97 directory: PathBuf,
98 min_bytes: u64,
99 sync: bool,
100 pack_target_bytes: Option<u64>,
101 ) -> Self {
102 self.external_blob_dir = Some(directory);
103 self.external_blob_min_bytes = Some(min_bytes.max(1));
104 self.external_blob_sync = sync;
105 self.external_pack_target_bytes = pack_target_bytes.filter(|value| *value > 0);
106 self
107 }
108}
109
110#[derive(Debug, Clone)]
111pub struct PoolStoreConfig {
112 pub catalog_map_size_bytes: u64,
113 pub member_failure_cooldown: Duration,
114}
115
116impl Default for PoolStoreConfig {
117 fn default() -> Self {
118 Self {
119 catalog_map_size_bytes: DEFAULT_CATALOG_MAP_SIZE_BYTES,
120 member_failure_cooldown: Duration::from_secs(5),
121 }
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct PoolMemberStatus {
127 pub id: PoolMemberId,
128 pub state: PoolMemberState,
129 pub path: PathBuf,
130 pub capacity_bytes: u64,
131 pub map_size_bytes: u64,
132 pub external_blob_dir: Option<PathBuf>,
133 pub external_blob_min_bytes: Option<u64>,
134 pub external_blob_sync: bool,
135 pub external_pack_target_bytes: Option<u64>,
136 pub max_read_concurrency: u32,
137 pub max_write_concurrency: u32,
138 pub logical_bytes: u64,
139 pub located_blobs: u64,
140 pub available: bool,
141 pub last_error: Option<String>,
142}
143
144#[derive(Debug, Clone, Default, PartialEq, Eq)]
145pub struct PoolMaintenanceReport {
146 pub examined: usize,
147 pub moved: usize,
148 pub bytes_moved: u64,
149 pub failed: Vec<String>,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
153pub(super) struct MemberRecord {
154 pub(super) id: PoolMemberId,
155 pub(super) state: PoolMemberState,
156 pub(super) config: PoolMemberConfig,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub(super) struct PoolManifest {
161 pub(super) version: u32,
162 pub(super) generation: u64,
163 pub(super) members: Vec<MemberRecord>,
164}
165
166impl Default for PoolManifest {
167 fn default() -> Self {
168 Self {
169 version: 1,
170 generation: 0,
171 members: Vec::new(),
172 }
173 }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub(super) enum LocationRecord {
178 Pending {
179 member: PoolMemberId,
180 size: u64,
181 },
182 Stored {
183 member: PoolMemberId,
184 size: u64,
185 },
186 Moving {
187 source: PoolMemberId,
188 target: PoolMemberId,
189 size: u64,
190 },
191}
192
193impl LocationRecord {
194 pub(super) fn size(self) -> u64 {
195 match self {
196 Self::Pending { size, .. } | Self::Stored { size, .. } | Self::Moving { size, .. } => {
197 size
198 }
199 }
200 }
201
202 pub(super) fn preferred_member(self) -> PoolMemberId {
203 match self {
204 Self::Pending { member, .. } | Self::Stored { member, .. } => member,
205 Self::Moving { target, .. } => target,
206 }
207 }
208
209 pub(super) fn members(self) -> ([PoolMemberId; 2], usize) {
210 match self {
211 Self::Pending { member, .. } | Self::Stored { member, .. } => ([member, member], 1),
212 Self::Moving { source, target, .. } => ([source, target], 2),
213 }
214 }
215
216 pub(super) fn encode(self) -> Vec<u8> {
217 let mut bytes = Vec::with_capacity(41);
218 match self {
219 Self::Pending { member, size } => {
220 bytes.push(1);
221 bytes.extend_from_slice(member.as_bytes());
222 bytes.extend_from_slice(&size.to_be_bytes());
223 }
224 Self::Stored { member, size } => {
225 bytes.push(2);
226 bytes.extend_from_slice(member.as_bytes());
227 bytes.extend_from_slice(&size.to_be_bytes());
228 }
229 Self::Moving {
230 source,
231 target,
232 size,
233 } => {
234 bytes.push(3);
235 bytes.extend_from_slice(source.as_bytes());
236 bytes.extend_from_slice(target.as_bytes());
237 bytes.extend_from_slice(&size.to_be_bytes());
238 }
239 }
240 bytes
241 }
242
243 pub(super) fn decode(bytes: &[u8]) -> Result<Self, StoreError> {
244 let member = |slice: &[u8]| -> Result<PoolMemberId, StoreError> {
245 let bytes: [u8; 16] = slice
246 .try_into()
247 .map_err(|_| StoreError::Other("invalid pool member id bytes".into()))?;
248 Ok(PoolMemberId(bytes))
249 };
250 let size = |slice: &[u8]| -> Result<u64, StoreError> {
251 Ok(u64::from_be_bytes(slice.try_into().map_err(|_| {
252 StoreError::Other("invalid pool location size".into())
253 })?))
254 };
255 match bytes.first().copied() {
256 Some(1) if bytes.len() == 25 => Ok(Self::Pending {
257 member: member(&bytes[1..17])?,
258 size: size(&bytes[17..25])?,
259 }),
260 Some(2) if bytes.len() == 25 => Ok(Self::Stored {
261 member: member(&bytes[1..17])?,
262 size: size(&bytes[17..25])?,
263 }),
264 Some(3) if bytes.len() == 41 => Ok(Self::Moving {
265 source: member(&bytes[1..17])?,
266 target: member(&bytes[17..33])?,
267 size: size(&bytes[33..41])?,
268 }),
269 _ => Err(StoreError::Other("invalid pool location record".into())),
270 }
271 }
272}