1use haematite::branch::{CommitDurability, CommitRequest, RefRecord, remove as remove_branch};
4use haematite::db::ReadOnlyDatabase;
5use haematite::tree::Hash;
6use serde::{Deserialize, Serialize};
7use serde_json::{Value, json};
8
9use super::{
10 BranchKind, BranchRefStore, Failure, Receipt, SERVICE, Storage, branch_name,
11 create_branch_with_kind, current_timestamp,
12 dead::{Dead, dead_key},
13 identifier, open_branch, version,
14};
15use crate::serve::protocol::{Mutation, ReceiptQuery, Version};
16
17const FILE_PREFIX: &str = "@haem/discard/";
18const INTENT_PREFIX: &str = "@haem/discard-pending/";
19
20#[derive(Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22struct Intent {
23 branch: String,
24 request_id: String,
25 version: Version,
26}
27
28impl Intent {
29 fn name(&self) -> Result<String, Failure> {
30 Ok(format!(
31 "{INTENT_PREFIX}{}",
32 serde_json::to_string(self).map_err(Failure::storage)?
33 ))
34 }
35
36 fn decode(record: &RefRecord, shard_count: usize) -> Result<Self, Failure> {
37 let corrupt = |message| {
38 Failure::new(
39 "discard_intent_corrupt",
40 format!("{}: {message}", record.name),
41 )
42 };
43 let suffix = record
44 .name
45 .strip_prefix(INTENT_PREFIX)
46 .ok_or_else(|| corrupt("intent namespace differs".to_owned()))?;
47 let intent: Self =
48 serde_json::from_str(suffix).map_err(|error| corrupt(error.to_string()))?;
49 identifier(&intent.branch).map_err(|error| corrupt(error.to_string()))?;
50 identifier(&intent.request_id).map_err(|error| corrupt(error.to_string()))?;
51 if intent.branch == "main"
52 || record.kind != BranchKind::Namespace
53 || record.seq != 0
54 || intent.name()? != record.name
55 || record.shards.len() != shard_count
56 || record
57 .shards
58 .iter()
59 .enumerate()
60 .any(|(id, shard)| id != shard.shard_id)
61 {
62 return Err(corrupt("intent identity or kind differs".to_owned()));
63 }
64 Ok(intent)
65 }
66}
67
68impl Storage {
69 pub fn pending_discards(&self) -> Result<Vec<(String, Version)>, Failure> {
77 let shard_count = self.reader.shard_count();
78 self.refs
79 .list_prefix(INTENT_PREFIX)
80 .map(|record| {
81 Intent::decode(record, shard_count).map(|intent| (intent.branch, intent.version))
82 })
83 .collect()
84 }
85}
86
87pub(super) fn key(branch: &str, request: &str) -> Vec<u8> {
88 let mut key = Vec::with_capacity(66);
89 key.extend_from_slice(b"d\0");
90 key.extend_from_slice(Hash::of(branch.as_bytes()).as_bytes());
91 key.extend_from_slice(Hash::of(request.as_bytes()).as_bytes());
92 key
93}
94
95fn service_record<'a>(
96 reader: &ReadOnlyDatabase,
97 refs: &'a BranchRefStore,
98) -> Result<&'a RefRecord, Failure> {
99 let record = refs
100 .get(SERVICE)
101 .ok_or_else(|| Failure::new("reference_missing", "missing service namespace"))?;
102 if record.kind != BranchKind::Namespace
103 || record.shards.len() != reader.shard_count()
104 || record
105 .shards
106 .iter()
107 .enumerate()
108 .any(|(id, shard)| id != shard.shard_id)
109 {
110 return Err(Failure::new(
111 "discard_service_invalid",
112 "service must name every physical shard",
113 ));
114 }
115 Ok(record)
116}
117
118fn read_row(
119 reader: &ReadOnlyDatabase,
120 refs: &BranchRefStore,
121 key: &[u8],
122) -> Result<Option<Receipt>, Failure> {
123 let service = service_record(reader, refs)?;
124 let shard_id = reader.shard_for(key);
125 let root = service
126 .shards
127 .get(shard_id)
128 .ok_or_else(|| Failure::new("incomplete_roots", "service row shard is missing"))?
129 .head;
130 reader
131 .get_at(shard_id, root, key)
132 .map_err(Failure::observer)?
133 .map(|bytes| {
134 let corrupt = |message| Failure::new("discard_record_corrupt", message);
135 let receipt: Receipt =
136 serde_json::from_slice(&bytes).map_err(|error| corrupt(error.to_string()))?;
137 identifier(&receipt.branch).map_err(|error| corrupt(error.to_string()))?;
138 identifier(&receipt.request_id).map_err(|error| corrupt(error.to_string()))?;
139 if receipt.branch == "main"
140 || self::key(&receipt.branch, &receipt.request_id) != key
141 || receipt.fingerprint.len() != 64
142 || !receipt
143 .fingerprint
144 .bytes()
145 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
146 {
147 return Err(corrupt("receipt identity differs from its row".to_owned()));
148 }
149 Ok(receipt)
150 })
151 .transpose()
152}
153
154pub(super) fn require_file_rows(
155 reader: &ReadOnlyDatabase,
156 refs: &BranchRefStore,
157) -> Result<(), Failure> {
158 for record in refs.list_prefix(FILE_PREFIX) {
159 let suffix = record
160 .name
161 .strip_prefix(FILE_PREFIX)
162 .ok_or_else(|| Failure::new("discard_ref_corrupt", &record.name))?;
163 let (branch, request) = suffix
164 .split_once('/')
165 .ok_or_else(|| Failure::new("discard_ref_corrupt", &record.name))?;
166 let mut key = b"d\0".to_vec();
167 key.extend_from_slice(&decode_hash(branch).map_err(|error| {
168 Failure::new("discard_ref_corrupt", format!("{}: {error}", record.name))
169 })?);
170 key.extend_from_slice(&decode_hash(request).map_err(|error| {
171 Failure::new("discard_ref_corrupt", format!("{}: {error}", record.name))
172 })?);
173 if read_row(reader, refs, &key)?.is_none() {
174 return Err(Failure::new(
175 "discard_rows_incomplete",
176 format!("discard file {} has no matching row", record.name),
177 ));
178 }
179 }
180 Ok(())
181}
182
183fn decode_hash(hex: &str) -> Result<[u8; 32], Failure> {
184 let invalid = || {
185 Failure::new(
186 "discard_ref_corrupt",
187 "discard reference requires canonical hashes",
188 )
189 };
190 if hex.len() != 64
191 || !hex
192 .bytes()
193 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
194 {
195 return Err(invalid());
196 }
197 let mut hash = [0; 32];
198 for (byte, pair) in hash.iter_mut().zip(hex.as_bytes().as_chunks::<2>().0) {
199 let digits = std::str::from_utf8(pair).map_err(|_| invalid())?;
200 *byte = u8::from_str_radix(digits, 16).map_err(|_| invalid())?;
201 }
202 Ok(hash)
203}
204
205impl Storage {
206 pub(super) fn discard_request(
207 &mut self,
208 request: &Mutation,
209 fingerprint: &str,
210 target: &str,
211 record: &RefRecord,
212 ) -> Result<Receipt, Failure> {
213 identifier(target)?;
214 if target == "main" {
215 return Err(Failure::new("main_protected", "main cannot be discarded"));
216 }
217 if target != request.branch {
218 return Err(Failure::new(
219 "discard_target_mismatch",
220 "discard must name the mutation's branch",
221 ));
222 }
223 self.rows_before_discard(target, record)?;
226 #[cfg(test)]
227 super::history::after_rows::reached()?;
228 let intent = Intent {
229 branch: target.to_owned(),
230 request_id: request.request_id.clone(),
231 version: request.expected,
232 };
233 let name = intent.name()?;
234 let result = create_branch_with_kind(
235 &name,
236 record
237 .shards
238 .iter()
239 .map(|shard| (shard.shard_id, shard.head)),
240 BranchKind::Namespace,
241 &mut self.refs,
242 &self.registry,
243 current_timestamp(),
244 );
245 if self.refs.get(&name).is_some() {
246 self.pending_discards.insert(record.name.clone());
247 self.visible.remove(&record.name)?;
248 }
249 result.map_err(|error| {
250 Failure::new(
251 if self.refs.get(&name).is_some() {
252 "durability_unknown"
253 } else {
254 "write_refused"
255 },
256 error.to_string(),
257 )
258 })?;
259 let receipt = Receipt {
260 retention: None,
261 request_id: request.request_id.clone(),
262 fingerprint: fingerprint.to_owned(),
263 branch: target.to_owned(),
264 version: request.expected,
265 };
266 self.commit_and_finish_discard(&name, &intent, &receipt)?;
267 Ok(receipt)
268 }
269
270 fn commit_and_finish_discard(
271 &mut self,
272 name: &str,
273 intent: &Intent,
274 receipt: &Receipt,
275 ) -> Result<(), Failure> {
276 if let Err(refused) = self.commit_discard_row(receipt) {
277 if refused.code == "write_refused" {
278 self.finish_intent(name, intent, None).map_err(|abort| {
279 Failure::new(
280 "discard_abort_failed",
281 format!("{refused}; failed to abort discard intent: {abort}"),
282 )
283 })?;
284 }
285 return Err(refused);
286 }
287 self.finish_intent(name, intent, Some(receipt))
288 }
289
290 fn commit_discard_row(&mut self, receipt: &Receipt) -> Result<(), Failure> {
291 #[cfg(test)]
292 let fault = ROW_COMMIT_FAULT.with(std::cell::Cell::take);
293 #[cfg(test)]
294 if matches!(fault, Some(CommitFault::RefuseBeforeCommit)) {
295 return Err(Failure::new(
296 "write_refused",
297 "injected refusal before discard row commit",
298 ));
299 }
300 let key = key(&receipt.branch, &receipt.request_id);
301 let bytes = serde_json::to_vec(receipt).map_err(Failure::storage)?;
302 let now = current_timestamp();
303 let dead_key = dead_key(&receipt.branch, receipt.version.generation);
307 let dead = serde_json::to_vec(&Dead {
308 branch: receipt.branch.clone(),
309 last: receipt.version,
310 discarded_at: now,
311 })
312 .map_err(Failure::storage)?;
313 let at = version(service_record(&self.reader, &self.refs)?);
314 let service =
315 open_branch(SERVICE, &self.refs, &self.registry).map_err(Failure::branch_commit)?;
316 service
317 .put(self.writer.shard_for(&key), key, bytes)
318 .map_err(Failure::branch)?;
319 service
320 .put(self.writer.shard_for(&dead_key), dead_key, dead)
321 .map_err(Failure::branch)?;
322 self.writer
323 .commit(
324 &service,
325 &self.registry,
326 CommitRequest {
327 durability: CommitDurability::Durable {
328 refs: &mut self.refs,
329 },
330 extra_parents: &[],
331 timestamp: now,
332 },
333 )
334 .map_err(|error| {
335 let installed = self.refs.get(SERVICE).is_some_and(|now| version(now) != at);
336 Failure::new(
337 if installed {
338 "durability_unknown"
339 } else {
340 "write_refused"
341 },
342 error.to_string(),
343 )
344 })?;
345 #[cfg(test)]
346 if matches!(fault, Some(CommitFault::LoseCommittedAnswer)) {
347 return Err(Failure::new(
348 "durability_unknown",
349 "injected lost answer after discard row commit",
350 ));
351 }
352 self.publish_record(SERVICE)
353 }
354
355 pub(super) fn discard_receipt(
356 &mut self,
357 query: &ReceiptQuery,
358 ) -> Result<Option<Receipt>, Failure> {
359 let receipt = read_row(
360 &self.reader,
361 &self.refs,
362 &key(&query.branch, &query.request_id),
363 )?;
364 if let Some(receipt) = &receipt {
365 if receipt.branch != query.branch || receipt.request_id != query.request_id {
366 return Err(Failure::new(
367 "discard_record_corrupt",
368 "receipt differs from requested identity",
369 ));
370 }
371 let at = version(service_record(&self.reader, &self.refs)?);
372 self.refs
373 .confirm_version(SERVICE, at.generation, at.sequence)
374 .map_err(|error| Failure::new("durability_unknown", error.to_string()))?;
375 self.publish_record(SERVICE)?;
376 }
377 let target = branch_name(&query.branch);
380 if self.pending_discards.contains(&target) {
381 let expected = receipt
382 .as_ref()
383 .map(|receipt| receipt.version)
384 .or_else(|| self.refs.get(&target).map(version))
385 .ok_or_else(|| {
386 Failure::new(
387 "discard_pending",
388 "pending removal has no receipt or target",
389 )
390 })?;
391 let intent = Intent {
392 branch: query.branch.clone(),
393 request_id: query.request_id.clone(),
394 version: expected,
395 };
396 let name = intent.name()?;
397 if let Some(record) = self.refs.get(&name) {
398 let intent = Intent::decode(record, self.reader.shard_count())?;
399 self.finish_intent(&name, &intent, receipt.as_ref())?;
400 }
401 }
402 Ok(receipt)
403 }
404
405 pub(super) fn reconcile_discards(&mut self) -> Result<(), Failure> {
406 let mut targets = std::collections::BTreeSet::new();
409 let mut intents = Vec::new();
410 for record in self.refs.list_prefix(INTENT_PREFIX) {
411 let intent = Intent::decode(record, self.reader.shard_count())?;
412 if !targets.insert(intent.branch.clone()) {
413 return Err(Failure::new(
414 "discard_intent_corrupt",
415 "multiple intents name the same branch",
416 ));
417 }
418 intents.push((record.name.clone(), intent));
419 }
420 for (name, intent) in intents {
421 let receipt = read_row(
422 &self.reader,
423 &self.refs,
424 &key(&intent.branch, &intent.request_id),
425 )?;
426 self.finish_intent(&name, &intent, receipt.as_ref())?;
427 }
428 Ok(())
429 }
430
431 fn finish_intent(
432 &mut self,
433 name: &str,
434 intent: &Intent,
435 receipt: Option<&Receipt>,
436 ) -> Result<(), Failure> {
437 let target = branch_name(&intent.branch);
438 self.pending_discards.insert(target.clone());
439 let mut removed = false;
440 if let Some(receipt) = receipt {
441 if receipt.branch != intent.branch
442 || receipt.request_id != intent.request_id
443 || receipt.version != intent.version
444 {
445 return Err(Failure::new(
446 "discard_intent_corrupt",
447 "intent differs from its receipt row",
448 ));
449 }
450 if let Some(current) = self.refs.get(&target)
451 && current.created == intent.version.generation
452 {
453 if version(current) != intent.version {
454 return Err(Failure::new(
455 "discard_conflict",
456 "branch advanced after its removal was recorded",
457 ));
458 }
459 self.visible.remove(&target)?;
460 remove_branch(&target, &mut self.refs)
461 .map_err(|error| Failure::new("durability_unknown", error.to_string()))?;
462 removed = true;
463 }
464 }
465 remove_branch(name, &mut self.refs)
468 .map_err(|error| Failure::new("durability_unknown", error.to_string()))?;
469 self.visible.remove(name)?;
470 if self.refs.get(&target).is_some() {
471 self.publish_record(&target)?;
472 } else {
473 self.visible.remove(&target)?;
474 }
475 self.pending_discards.remove(&target);
476 if removed {
483 self.ask_pass(intent);
484 }
485 Ok(())
486 }
487
488 fn ask_pass(&self, intent: &Intent) {
492 let asked = self.writer.request_reclaim();
493 #[cfg(test)]
494 let asked = PASS_REFUSED.with(std::cell::Cell::take).map_or(asked, Err);
495 self.discard_passes.record(LastPass {
496 branch: intent.branch.clone(),
497 request_id: intent.request_id.clone(),
498 asked: asked.is_ok(),
499 refused: asked.err().map(|error| error.to_string()),
500 });
501 }
502}
503
504#[derive(Debug, Clone, Serialize)]
506struct LastPass {
507 branch: String,
508 request_id: String,
509 asked: bool,
510 refused: Option<String>,
511}
512
513#[derive(Debug, Clone, Default)]
516pub struct DiscardPasses(std::sync::Arc<std::sync::Mutex<Option<LastPass>>>);
517
518impl DiscardPasses {
519 fn record(&self, pass: LastPass) {
520 *self
521 .0
522 .lock()
523 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(pass);
524 }
525
526 #[must_use]
530 pub fn status(&self) -> Value {
531 self.0
532 .lock()
533 .unwrap_or_else(std::sync::PoisonError::into_inner)
534 .as_ref()
535 .map_or(Value::Null, |pass| json!(pass))
536 }
537}
538
539#[cfg(test)]
540#[derive(Clone, Copy)]
541enum CommitFault {
542 RefuseBeforeCommit,
543 LoseCommittedAnswer,
544}
545
546#[cfg(test)]
547thread_local! {
548 static ROW_COMMIT_FAULT: std::cell::Cell<Option<CommitFault>> =
549 const { std::cell::Cell::new(None) };
550 static PASS_REFUSED: std::cell::Cell<Option<haematite::db::DatabaseError>> =
553 const { std::cell::Cell::new(None) };
554}
555
556#[cfg(test)]
557#[path = "discards_tests.rs"]
558mod tests;