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 let head = super::Captured {
227 branch: target.to_owned(),
228 version: version(record),
229 shards: record.shards.clone(),
230 };
231 let versions_released = if super::lineage::is_line(&self.reader, &head)? {
232 Some(record.seq.checked_add(1).ok_or_else(|| {
233 Failure::new("sequence_exhausted", "a line's versions cannot be counted")
234 })?)
235 } else {
236 None
237 };
238 self.rows_before_discard(target, record)?;
241 #[cfg(test)]
242 super::history::after_rows::reached()?;
243 let intent = Intent {
244 branch: target.to_owned(),
245 request_id: request.request_id.clone(),
246 version: request.expected,
247 };
248 let name = intent.name()?;
249 let result = create_branch_with_kind(
250 &name,
251 record
252 .shards
253 .iter()
254 .map(|shard| (shard.shard_id, shard.head)),
255 BranchKind::Namespace,
256 &mut self.refs,
257 &self.registry,
258 current_timestamp(),
259 );
260 if self.refs.get(&name).is_some() {
261 self.pending_discards.insert(record.name.clone());
262 self.visible.remove(&record.name)?;
263 }
264 result.map_err(|error| {
265 Failure::new(
266 if self.refs.get(&name).is_some() {
267 "durability_unknown"
268 } else {
269 "write_refused"
270 },
271 error.to_string(),
272 )
273 })?;
274 let receipt = Receipt {
275 retention: None,
276 request_id: request.request_id.clone(),
277 fingerprint: fingerprint.to_owned(),
278 branch: target.to_owned(),
279 version: request.expected,
280 versions_released,
281 };
282 self.commit_and_finish_discard(&name, &intent, &receipt)?;
283 self.release_after_discard(target, record.created)?;
286 Ok(receipt)
287 }
288
289 fn commit_and_finish_discard(
290 &mut self,
291 name: &str,
292 intent: &Intent,
293 receipt: &Receipt,
294 ) -> Result<(), Failure> {
295 if let Err(refused) = self.commit_discard_row(receipt) {
296 if refused.code == "write_refused" {
297 self.finish_intent(name, intent, None).map_err(|abort| {
298 Failure::new(
299 "discard_abort_failed",
300 format!("{refused}; failed to abort discard intent: {abort}"),
301 )
302 })?;
303 }
304 return Err(refused);
305 }
306 self.finish_intent(name, intent, Some(receipt))
307 }
308
309 fn commit_discard_row(&mut self, receipt: &Receipt) -> Result<(), Failure> {
310 #[cfg(test)]
311 let fault = ROW_COMMIT_FAULT.with(std::cell::Cell::take);
312 #[cfg(test)]
313 if matches!(fault, Some(CommitFault::RefuseBeforeCommit)) {
314 return Err(Failure::new(
315 "write_refused",
316 "injected refusal before discard row commit",
317 ));
318 }
319 let key = key(&receipt.branch, &receipt.request_id);
320 let bytes = serde_json::to_vec(receipt).map_err(Failure::storage)?;
321 let now = current_timestamp();
322 let dead_key = dead_key(&receipt.branch, receipt.version.generation);
326 let dead = serde_json::to_vec(&Dead {
327 branch: receipt.branch.clone(),
328 last: receipt.version,
329 discarded_at: now,
330 })
331 .map_err(Failure::storage)?;
332 let at = version(service_record(&self.reader, &self.refs)?);
333 let service =
334 open_branch(SERVICE, &self.refs, &self.registry).map_err(Failure::branch_commit)?;
335 service
336 .put(self.writer.shard_for(&key), key, bytes)
337 .map_err(Failure::branch)?;
338 service
339 .put(self.writer.shard_for(&dead_key), dead_key, dead)
340 .map_err(Failure::branch)?;
341 self.writer
342 .commit(
343 &service,
344 &self.registry,
345 CommitRequest {
346 durability: CommitDurability::Durable {
347 refs: &mut self.refs,
348 },
349 extra_parents: &[],
350 timestamp: now,
351 note: None,
352 },
353 )
354 .map_err(|error| {
355 let installed = self.refs.get(SERVICE).is_some_and(|now| version(now) != at);
356 Failure::new(
357 if installed {
358 "durability_unknown"
359 } else {
360 "write_refused"
361 },
362 error.to_string(),
363 )
364 })?;
365 #[cfg(test)]
366 if matches!(fault, Some(CommitFault::LoseCommittedAnswer)) {
367 return Err(Failure::new(
368 "durability_unknown",
369 "injected lost answer after discard row commit",
370 ));
371 }
372 self.publish_record(SERVICE)
373 }
374
375 pub(super) fn discard_receipt(
376 &mut self,
377 query: &ReceiptQuery,
378 ) -> Result<Option<Receipt>, Failure> {
379 let receipt = read_row(
380 &self.reader,
381 &self.refs,
382 &key(&query.branch, &query.request_id),
383 )?;
384 if let Some(receipt) = &receipt {
385 if receipt.branch != query.branch || receipt.request_id != query.request_id {
386 return Err(Failure::new(
387 "discard_record_corrupt",
388 "receipt differs from requested identity",
389 ));
390 }
391 let at = version(service_record(&self.reader, &self.refs)?);
392 self.refs
393 .confirm_version(SERVICE, at.generation, at.sequence)
394 .map_err(|error| Failure::new("durability_unknown", error.to_string()))?;
395 self.publish_record(SERVICE)?;
396 }
397 let target = branch_name(&query.branch);
400 if self.pending_discards.contains(&target) {
401 let expected = receipt
402 .as_ref()
403 .map(|receipt| receipt.version)
404 .or_else(|| self.refs.get(&target).map(version))
405 .ok_or_else(|| {
406 Failure::new(
407 "discard_pending",
408 "pending removal has no receipt or target",
409 )
410 })?;
411 let intent = Intent {
412 branch: query.branch.clone(),
413 request_id: query.request_id.clone(),
414 version: expected,
415 };
416 let name = intent.name()?;
417 if let Some(record) = self.refs.get(&name) {
418 let intent = Intent::decode(record, self.reader.shard_count())?;
419 self.finish_intent(&name, &intent, receipt.as_ref())?;
420 }
421 }
422 Ok(receipt)
423 }
424
425 pub(super) fn reconcile_discards(&mut self) -> Result<(), Failure> {
426 let mut targets = std::collections::BTreeSet::new();
429 let mut intents = Vec::new();
430 for record in self.refs.list_prefix(INTENT_PREFIX) {
431 let intent = Intent::decode(record, self.reader.shard_count())?;
432 if !targets.insert(intent.branch.clone()) {
433 return Err(Failure::new(
434 "discard_intent_corrupt",
435 "multiple intents name the same branch",
436 ));
437 }
438 intents.push((record.name.clone(), intent));
439 }
440 for (name, intent) in intents {
441 let receipt = read_row(
442 &self.reader,
443 &self.refs,
444 &key(&intent.branch, &intent.request_id),
445 )?;
446 self.finish_intent(&name, &intent, receipt.as_ref())?;
447 }
448 Ok(())
449 }
450
451 fn finish_intent(
452 &mut self,
453 name: &str,
454 intent: &Intent,
455 receipt: Option<&Receipt>,
456 ) -> Result<(), Failure> {
457 let target = branch_name(&intent.branch);
458 self.pending_discards.insert(target.clone());
459 let mut removed = false;
460 if let Some(receipt) = receipt {
461 if receipt.branch != intent.branch
462 || receipt.request_id != intent.request_id
463 || receipt.version != intent.version
464 {
465 return Err(Failure::new(
466 "discard_intent_corrupt",
467 "intent differs from its receipt row",
468 ));
469 }
470 if let Some(current) = self.refs.get(&target)
471 && current.created == intent.version.generation
472 {
473 if version(current) != intent.version {
474 return Err(Failure::new(
475 "discard_conflict",
476 "branch advanced after its removal was recorded",
477 ));
478 }
479 self.visible.remove(&target)?;
480 remove_branch(&target, &mut self.refs)
481 .map_err(|error| Failure::new("durability_unknown", error.to_string()))?;
482 removed = true;
483 }
484 }
485 remove_branch(name, &mut self.refs)
488 .map_err(|error| Failure::new("durability_unknown", error.to_string()))?;
489 self.visible.remove(name)?;
490 if self.refs.get(&target).is_some() {
491 self.publish_record(&target)?;
492 } else {
493 self.visible.remove(&target)?;
494 }
495 self.pending_discards.remove(&target);
496 if removed {
503 self.ask_pass(intent);
504 }
505 Ok(())
506 }
507
508 fn ask_pass(&self, intent: &Intent) {
512 let asked = self.writer.request_reclaim();
513 #[cfg(test)]
514 let asked = PASS_REFUSED.with(std::cell::Cell::take).map_or(asked, Err);
515 self.discard_passes.record(LastPass {
516 branch: intent.branch.clone(),
517 request_id: intent.request_id.clone(),
518 asked: asked.is_ok(),
519 refused: asked.err().map(|error| error.to_string()),
520 });
521 }
522}
523
524#[derive(Debug, Clone, Serialize)]
526struct LastPass {
527 branch: String,
528 request_id: String,
529 asked: bool,
530 refused: Option<String>,
531}
532
533#[derive(Debug, Clone, Default)]
536pub struct DiscardPasses(std::sync::Arc<std::sync::Mutex<Option<LastPass>>>);
537
538impl DiscardPasses {
539 fn record(&self, pass: LastPass) {
540 *self
541 .0
542 .lock()
543 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(pass);
544 }
545
546 #[must_use]
550 pub fn status(&self) -> Value {
551 self.0
552 .lock()
553 .unwrap_or_else(std::sync::PoisonError::into_inner)
554 .as_ref()
555 .map_or(Value::Null, |pass| json!(pass))
556 }
557}
558
559#[cfg(test)]
560#[derive(Clone, Copy)]
561enum CommitFault {
562 RefuseBeforeCommit,
563 LoseCommittedAnswer,
564}
565
566#[cfg(test)]
567thread_local! {
568 static ROW_COMMIT_FAULT: std::cell::Cell<Option<CommitFault>> =
569 const { std::cell::Cell::new(None) };
570 static PASS_REFUSED: std::cell::Cell<Option<haematite::db::DatabaseError>> =
573 const { std::cell::Cell::new(None) };
574}
575
576#[cfg(test)]
577#[path = "discards_tests.rs"]
578mod tests;