|
|
… |
25 unchanged lines |
| 26 |
26 |
|
//! object (`sigs/`, `seals/`, `receipts/`), which are still read. |
| 27 |
27 |
|
//! `pack-retired` records the packs and loose objects a repack replaced, |
| 28 |
28 |
|
//! which stay for readers until a collection an hour later (oxygenhub#249). |
|
29 |
+ |
//! |
|
30 |
+ |
//! A store that is not this disk's directory, where finding an object's |
|
31 |
+ |
//! entries is a LIST request, also keeps every entry key in the |
|
32 |
+ |
//! `pack-entries` journal and, once a collection folds them, in entry |
|
33 |
+ |
//! lists (`pack-<id>.entries`), read once per open (spec §13.7, #116). |
| 29 |
34 |
|
|
| 30 |
35 |
|
use crate::attest::Attest; |
| 31 |
36 |
|
use crate::hash::{hash_object, header, ObjectRef, ObjectType}; |
|
|
… |
54 unchanged lines |
| 86 |
91 |
|
/// thousands of objects it is about to store would otherwise list the |
| 87 |
92 |
|
/// packs for each. |
| 88 |
93 |
|
const HAS_RELIST_AFTER: std::time::Duration = std::time::Duration::from_secs(1); |
|
94 |
+ |
|
|
95 |
+ |
/// The journal of signatures and index entries (spec §13.7): every entry |
|
96 |
+ |
/// key a writer on a store that is not this disk's directory wrote since |
|
97 |
+ |
/// the last fold, one line each, `<folded at, wall ms; 0 until folded> |
|
98 |
+ |
/// <key>`, as the retired record's lines are. A reference record updated |
|
99 |
+ |
/// by compare-and-swap, so concurrent writers never lose each other's |
|
100 |
+ |
/// lines; read once per open, instead of a listing per lookup. |
|
101 |
+ |
pub const JOURNAL_KEY: &str = "pack-entries"; |
|
102 |
+ |
/// A complete entry list (spec §13.7): every entry a fold found. |
|
103 |
+ |
const LIST_SUFFIX: &str = ".entries"; |
|
104 |
+ |
/// A partial entry list: journal lines a writer folded out. |
|
105 |
+ |
const PART_SUFFIX: &str = ".entries-part"; |
|
106 |
+ |
/// The first line of an entry list. |
|
107 |
+ |
const LIST_MAGIC: &str = "oxy-entries 1"; |
|
108 |
+ |
/// Written once a collection has copied the older one-list keys into |
|
109 |
+ |
/// per-object entries: readers stop looking for them. |
|
110 |
+ |
const LEGACY_FOLDED_KEY: &str = "pack-legacy-folded"; |
|
111 |
+ |
/// Unfolded journal lines a writer lets the journal hold before it folds |
|
112 |
+ |
/// them into a partial list. |
|
113 |
+ |
const JOURNAL_FOLD_AT: usize = 1000; |
|
114 |
+ |
/// The per-object entry indexes, each with the one-list name older |
|
115 |
+ |
/// stores kept it under. |
|
116 |
+ |
const ENTRY_INDEXES: [(&str, &str); 6] = [ |
|
117 |
+ |
("sig", "sigs"), |
|
118 |
+ |
("seal", "seals"), |
|
119 |
+ |
("receipt", "receipts"), |
|
120 |
+ |
("approval", "approvals"), |
|
121 |
+ |
("cert", "certs"), |
|
122 |
+ |
("accepted", "accepted-lists"), |
|
123 |
+ |
]; |
| 89 |
124 |
|
|
| 90 |
125 |
|
/// A pack listing as compared: each key with its size and time. |
| 91 |
126 |
|
type Listed = Vec<(String, u64, Option<u64>)>; |
|
|
… |
92 unchanged lines |
| 184 |
219 |
|
fn fanout_prefix(index: &str, r: &ObjectRef) -> String { |
| 185 |
220 |
|
let h = r.to_hex(); |
| 186 |
221 |
|
format!("{index}/{}/{}/", &h[..2], &h[2..]) |
|
222 |
+ |
} |
|
223 |
+ |
|
|
224 |
+ |
/// Whether a key names an entry list (spec §13.7), complete or partial. |
|
225 |
+ |
fn is_entry_list(key: &str) -> bool { |
|
226 |
+ |
key.starts_with("pack-") && (key.ends_with(LIST_SUFFIX) || key.ends_with(PART_SUFFIX)) |
|
227 |
+ |
} |
|
228 |
+ |
|
|
229 |
+ |
/// An entry list's bytes: zstd-compressed text, [`LIST_MAGIC`] and then |
|
230 |
+ |
/// one key per line, sorted. |
|
231 |
+ |
fn encode_entry_list<'a>(keys: impl Iterator<Item = &'a String>) -> Result<Vec<u8>> { |
|
232 |
+ |
let mut text = String::from(LIST_MAGIC); |
|
233 |
+ |
text.push('\n'); |
|
234 |
+ |
for k in keys { |
|
235 |
+ |
text.push_str(k); |
|
236 |
+ |
text.push('\n'); |
|
237 |
+ |
} |
|
238 |
+ |
Ok(zstd::bulk::compress(text.as_bytes(), ZSTD_LEVEL)?) |
|
239 |
+ |
} |
|
240 |
+ |
|
|
241 |
+ |
/// The keys an entry list holds. |
|
242 |
+ |
fn decode_entry_list(key: &str, bytes: &[u8]) -> Result<Vec<String>> { |
|
243 |
+ |
let bad = |why: &str| StoreError::Io(io::Error::other(format!("{key}: {why}"))); |
|
244 |
+ |
let text = zstd::stream::decode_all(bytes).map_err(|_| bad("not zstd"))?; |
|
245 |
+ |
let text = String::from_utf8(text).map_err(|_| bad("not UTF-8"))?; |
|
246 |
+ |
let mut lines = text.lines(); |
|
247 |
+ |
if lines.next() != Some(LIST_MAGIC) { |
|
248 |
+ |
return Err(bad("not an entry list")); |
|
249 |
+ |
} |
|
250 |
+ |
Ok(lines |
|
251 |
+ |
.filter(|l| !l.is_empty()) |
|
252 |
+ |
.map(str::to_string) |
|
253 |
+ |
.collect()) |
| 187 |
254 |
|
} |
| 188 |
255 |
|
|
|
256 |
+ |
/// Drop journal lines folded at least [`RETIRED_FOR`] before `now`: |
|
257 |
+ |
/// every reader that could need them has a list holding them by then. |
|
258 |
+ |
fn expire_folded(m: &mut std::collections::BTreeMap<String, u64>, now: u64) { |
|
259 |
+ |
let hour = RETIRED_FOR.as_millis() as u64; |
|
260 |
+ |
m.retain(|_, t| *t == 0 || now.saturating_sub(*t) < hour); |
|
261 |
+ |
} |
|
262 |
+ |
|
| 189 |
263 |
|
/// `<index>/<hh>/<rest>`: the one-file-per-object layout of older stores. |
| 190 |
264 |
|
fn legacy_key(index: &str, r: &ObjectRef) -> String { |
| 191 |
265 |
|
let h = r.to_hex(); |
|
|
… |
214 unchanged lines |
| 406 |
480 |
|
batch_depth: std::cell::Cell<u32>, |
| 407 |
481 |
|
/// Change metadata already read, from the `metas` cache file. |
| 408 |
482 |
|
metas: RefCell<Option<crate::fasthash::FastMap<ObjectRef, crate::state::ChangeMeta>>>, |
|
483 |
+ |
/// The live entry lists (spec §13.7) the pack listing showed: listed |
|
484 |
+ |
/// and not retired. |
|
485 |
+ |
entry_lists: RefCell<Vec<String>>, |
|
486 |
+ |
/// Whether the store is folded: a live entry list is complete. |
|
487 |
+ |
folded: std::cell::Cell<bool>, |
|
488 |
+ |
/// Whether the pack listing showed `pack-legacy-folded`. |
|
489 |
+ |
legacy_folded: std::cell::Cell<bool>, |
|
490 |
+ |
/// The entry keys of a folded store, read from its lists and journal |
|
491 |
+ |
/// on the first lookup after the packs were loaded. |
|
492 |
+ |
entries: RefCell<Option<std::collections::BTreeSet<String>>>, |
|
493 |
+ |
/// Entry keys written in an open batch, added to the journal when the |
|
494 |
+ |
/// batch is flushed. |
|
495 |
+ |
journal_queue: RefCell<Vec<String>>, |
| 409 |
496 |
|
} |
| 410 |
497 |
|
|
| 411 |
498 |
|
impl std::fmt::Debug for Store { |
|
|
… |
161 unchanged lines |
| 573 |
660 |
|
pending: RefCell::new(None), |
| 574 |
661 |
|
batch_depth: std::cell::Cell::new(0), |
| 575 |
662 |
|
metas: RefCell::new(None), |
|
663 |
+ |
entry_lists: RefCell::new(Vec::new()), |
|
664 |
+ |
folded: std::cell::Cell::new(false), |
|
665 |
+ |
legacy_folded: std::cell::Cell::new(false), |
|
666 |
+ |
entries: RefCell::new(None), |
|
667 |
+ |
journal_queue: RefCell::new(Vec::new()), |
| 576 |
668 |
|
}; |
| 577 |
669 |
|
store |
| 578 |
670 |
|
.reload_packs() |
|
|
… |
16 unchanged lines |
| 595 |
687 |
|
/// writers packed since becomes readable, and packs since retired or |
| 596 |
688 |
|
/// deleted are let go. Packs already loaded are kept, not read again. |
| 597 |
689 |
|
pub fn refresh(&self) -> Result<()> { |
|
690 |
+ |
*self.entries.borrow_mut() = None; |
| 598 |
691 |
|
self.reload_packs() |
| 599 |
692 |
|
} |
| 600 |
693 |
|
|
|
|
… |
59 unchanged lines |
| 660 |
753 |
|
(listed, sig) = self.list_packs()?; |
| 661 |
754 |
|
continue; |
| 662 |
755 |
|
} |
|
756 |
+ |
let lists: Vec<String> = listed |
|
757 |
+ |
.iter() |
|
758 |
+ |
.map(|e| e.key.as_str()) |
|
759 |
+ |
.filter(|k| is_entry_list(k) && !retired.contains_key(*k)) |
|
760 |
+ |
.map(str::to_string) |
|
761 |
+ |
.collect(); |
|
762 |
+ |
self.folded |
|
763 |
+ |
.set(lists.iter().any(|k| k.ends_with(LIST_SUFFIX))); |
|
764 |
+ |
self.legacy_folded |
|
765 |
+ |
.set(listed.iter().any(|e| e.key == LEGACY_FOLDED_KEY)); |
|
766 |
+ |
*self.entry_lists.borrow_mut() = lists; |
|
767 |
+ |
if *self.packs_listed.borrow() != sig { |
|
768 |
+ |
*self.entries.borrow_mut() = None; |
|
769 |
+ |
} |
| 663 |
770 |
|
*self.packs.borrow_mut() = packs; |
| 664 |
771 |
|
*self.packs_listed.borrow_mut() = sig; |
| 665 |
772 |
|
return Ok(()); |
|
|
… |
28 unchanged lines |
| 694 |
801 |
|
|
| 695 |
802 |
|
/// The retired record and its version (`None`: never written). |
| 696 |
803 |
|
fn read_retired( |
|
804 |
+ |
&self, |
|
805 |
+ |
) -> Result<( |
|
806 |
+ |
std::collections::BTreeMap<String, u64>, |
|
807 |
+ |
Option<strata::Version>, |
|
808 |
+ |
)> { |
|
809 |
+ |
self.read_record(RETIRED_KEY) |
|
810 |
+ |
} |
|
811 |
+ |
|
|
812 |
+ |
/// A record of `<wall ms> <key>` lines (the retired record, the |
|
813 |
+ |
/// journal) and its version (`None`: never written). |
|
814 |
+ |
fn read_record( |
| 697 |
815 |
|
&self, |
|
816 |
+ |
key: &str, |
| 698 |
817 |
|
) -> Result<( |
| 699 |
818 |
|
std::collections::BTreeMap<String, u64>, |
| 700 |
819 |
|
Option<strata::Version>, |
| 701 |
820 |
|
)> { |
| 702 |
|
- |
match self.backend.read_ref(RETIRED_KEY).map_err(backend_err)? { |
|
821 |
+ |
match self.backend.read_ref(key).map_err(backend_err)? { |
| 703 |
822 |
|
Some((bytes, v)) => Ok((parse_retired(&bytes), Some(v))), |
| 704 |
823 |
|
None => Ok((Default::default(), None)), |
| 705 |
824 |
|
} |
|
|
… |
3 unchanged lines |
| 709 |
828 |
|
/// whenever another writer changed it first. `edit` must give the |
| 710 |
829 |
|
/// same result however often it runs. |
| 711 |
830 |
|
fn update_retired( |
|
831 |
+ |
&self, |
|
832 |
+ |
edit: impl FnMut(&mut std::collections::BTreeMap<String, u64>), |
|
833 |
+ |
) -> Result<()> { |
|
834 |
+ |
self.update_record(RETIRED_KEY, "the record of retired packs", edit) |
|
835 |
+ |
} |
|
836 |
+ |
|
|
837 |
+ |
/// Change a record of `<wall ms> <key>` lines by compare-and-swap, as |
|
838 |
+ |
/// [`Store::update_retired`] does; `what` names it in the error when |
|
839 |
+ |
/// other writers kept winning. |
|
840 |
+ |
fn update_record( |
| 712 |
841 |
|
&self, |
|
842 |
+ |
key: &str, |
|
843 |
+ |
what: &str, |
| 713 |
844 |
|
mut edit: impl FnMut(&mut std::collections::BTreeMap<String, u64>), |
| 714 |
845 |
|
) -> Result<()> { |
| 715 |
846 |
|
for _ in 0..RETIRED_TRIES { |
| 716 |
|
- |
let (mut m, version) = self.read_retired()?; |
|
847 |
+ |
let (mut m, version) = self.read_record(key)?; |
| 717 |
848 |
|
let before = m.clone(); |
| 718 |
849 |
|
edit(&mut m); |
| 719 |
|
- |
if m == before && version.is_some() { |
|
850 |
+ |
// Unchanged; a record never written stays unwritten. |
|
851 |
+ |
if m == before && (version.is_some() || m.is_empty()) { |
| 720 |
852 |
|
return Ok(()); |
| 721 |
853 |
|
} |
| 722 |
854 |
|
match self |
| 723 |
855 |
|
.backend |
| 724 |
|
- |
.write_ref(RETIRED_KEY, &encode_retired(&m), version.as_ref()) |
|
856 |
+ |
.write_ref(key, &encode_retired(&m), version.as_ref()) |
| 725 |
857 |
|
{ |
| 726 |
858 |
|
Ok(_) => return Ok(()), |
| 727 |
859 |
|
Err(BackendError::Conflict(_)) => continue, |
| 728 |
860 |
|
Err(e) => return Err(backend_err(e)), |
| 729 |
861 |
|
} |
| 730 |
862 |
|
} |
| 731 |
|
- |
Err(StoreError::Io(io::Error::other( |
| 732 |
|
- |
"other writers kept changing the record of retired packs; run the command again", |
| 733 |
|
- |
))) |
|
863 |
+ |
Err(StoreError::Io(io::Error::other(format!( |
|
864 |
+ |
"other writers kept changing {what}; run the command again" |
|
865 |
+ |
)))) |
| 734 |
866 |
|
} |
| 735 |
867 |
|
|
| 736 |
868 |
|
/// Record `keys` (packs, by their `.pack` key, and loose objects) as |
|
|
… |
46 unchanged lines |
| 783 |
915 |
|
if key.ends_with(".pack") { |
| 784 |
916 |
|
self.remove_pack(key)?; |
| 785 |
917 |
|
report.packs += 1; |
|
918 |
+ |
} else if is_entry_list(key) { |
|
919 |
+ |
// An entry list a fold replaced (spec §13.7). |
|
920 |
+ |
delete(self.backend.as_ref(), key)?; |
| 786 |
921 |
|
} else if let Some(r) = loose_ref(key) { |
| 787 |
922 |
|
if self.find_packed(&r).is_some() { |
| 788 |
923 |
|
delete(self.backend.as_ref(), key)?; |
|
|
… |
152 unchanged lines |
| 941 |
1076 |
|
let old_enough = |
| 942 |
1077 |
|
|modified: Option<u64>| modified.is_none_or(|t| now.saturating_sub(t) >= grace_ms); |
| 943 |
1078 |
|
let mut report = PruneReport::default(); |
|
1079 |
+ |
let mut gone: Vec<String> = Vec::new(); |
| 944 |
1080 |
|
// Loose objects. A retired one (its object packed) whose object is |
| 945 |
1081 |
|
// unreachable goes with it, uncounted: the pack's copy is counted. |
| 946 |
1082 |
|
let retired = self.retired_loose()?; |
|
|
… |
16 unchanged lines |
| 963 |
1099 |
|
report.bytes += entry.size; |
| 964 |
1100 |
|
report.objects += 1; |
| 965 |
1101 |
|
delete(self.backend.as_ref(), &entry.key)?; |
| 966 |
|
- |
self.remove_sidecars(&r)?; |
|
1102 |
+ |
self.remove_sidecars(&r, &mut gone)?; |
| 967 |
1103 |
|
} |
| 968 |
1104 |
|
if !unretired.is_empty() { |
| 969 |
1105 |
|
self.update_retired(|m| { |
|
|
… |
44 unchanged lines |
| 1014 |
1150 |
|
}; |
| 1015 |
1151 |
|
self.write_pack(&keep_refs, &[], &old_keys, ZSTD_LEVEL, progress)?; |
| 1016 |
1152 |
|
for r in dropped { |
| 1017 |
|
- |
self.remove_sidecars(&r)?; |
|
1153 |
+ |
self.remove_sidecars(&r, &mut gone)?; |
| 1018 |
1154 |
|
} |
| 1019 |
1155 |
|
} |
|
1156 |
+ |
self.journal_forget(&gone)?; |
| 1020 |
1157 |
|
Ok(report) |
| 1021 |
1158 |
|
} |
| 1022 |
1159 |
|
|
| 1023 |
1160 |
|
/// Everything kept beside an object: its signatures and the seal and |
| 1024 |
1161 |
|
/// receipt entries under it, in both layouts. |
| 1025 |
|
- |
fn remove_sidecars(&self, r: &ObjectRef) -> Result<()> { |
|
1162 |
+ |
/// The entry keys deleted are added to `gone`, for the journal. |
|
1163 |
+ |
fn remove_sidecars(&self, r: &ObjectRef, gone: &mut Vec<String>) -> Result<()> { |
| 1026 |
1164 |
|
for index in ["sigs", "seals", "receipts"] { |
| 1027 |
1165 |
|
delete(self.backend.as_ref(), &legacy_key(index, r))?; |
| 1028 |
1166 |
|
} |
|
|
… |
4 unchanged lines |
| 1033 |
1171 |
|
.map_err(backend_err)? |
| 1034 |
1172 |
|
{ |
| 1035 |
1173 |
|
delete(self.backend.as_ref(), &e.key)?; |
|
1174 |
+ |
gone.push(e.key); |
| 1036 |
1175 |
|
} |
| 1037 |
1176 |
|
} |
| 1038 |
1177 |
|
Ok(()) |
| 1039 |
1178 |
|
} |
| 1040 |
1179 |
|
|
|
1180 |
+ |
/// Take entry keys a collection deleted out of the journal, so the |
|
1181 |
+ |
/// next complete fold leaves them out (spec §13.7). |
|
1182 |
+ |
fn journal_forget(&self, gone: &[String]) -> Result<()> { |
|
1183 |
+ |
if self.local || gone.is_empty() { |
|
1184 |
+ |
return Ok(()); |
|
1185 |
+ |
} |
|
1186 |
+ |
if let Some(set) = self.entries.borrow_mut().as_mut() { |
|
1187 |
+ |
for k in gone { |
|
1188 |
+ |
set.remove(k); |
|
1189 |
+ |
} |
|
1190 |
+ |
} |
|
1191 |
+ |
self.update_record(JOURNAL_KEY, "the journal of entries", |m| { |
|
1192 |
+ |
for k in gone { |
|
1193 |
+ |
m.remove(k); |
|
1194 |
+ |
} |
|
1195 |
+ |
}) |
|
1196 |
+ |
} |
|
1197 |
+ |
|
| 1041 |
1198 |
|
/// Put a complete, synced temporary pack file and its index into the |
| 1042 |
1199 |
|
/// store; `index` must be sorted by ObjectRef. The pack goes first: an |
| 1043 |
1200 |
|
/// index never names a pack that is not there. |
|
|
… |
293 unchanged lines |
| 1337 |
1494 |
|
return Ok(()); |
| 1338 |
1495 |
|
} |
| 1339 |
1496 |
|
let Some(pending) = self.pending.borrow_mut().take() else { |
| 1340 |
|
- |
return Ok(()); |
|
1497 |
+ |
return self.flush_journal(); |
| 1341 |
1498 |
|
}; |
| 1342 |
|
- |
self.write_pending(pending) |
|
1499 |
+ |
self.write_pending(pending)?; |
|
1500 |
+ |
self.flush_journal() |
| 1343 |
1501 |
|
} |
| 1344 |
1502 |
|
|
| 1345 |
1503 |
|
/// Make every object written so far durable, even inside an open |
|
|
… |
1 unchanged lines |
| 1347 |
1505 |
|
/// is moved (spec §13.6): the operation head, a bookmark pointer. |
| 1348 |
1506 |
|
pub fn flush(&self) -> Result<()> { |
| 1349 |
1507 |
|
let Some(pending) = self.pending.borrow_mut().take() else { |
| 1350 |
|
- |
return Ok(()); |
|
1508 |
+ |
return self.flush_journal(); |
| 1351 |
1509 |
|
}; |
| 1352 |
1510 |
|
self.write_pending(pending)?; |
|
1511 |
+ |
self.flush_journal()?; |
| 1353 |
1512 |
|
*self.pending.borrow_mut() = Some(Pending { |
| 1354 |
1513 |
|
buffered: vec![], |
| 1355 |
1514 |
|
buffered_bytes: 0, |
|
|
… |
629 unchanged lines |
| 1985 |
2144 |
|
/// Record `value` under `key` in `index`: one write-once entry, so two |
| 1986 |
2145 |
|
/// writers of a shared store never lose each other's. |
| 1987 |
2146 |
|
fn index_add(&self, index: &str, key: &ObjectRef, value: &ObjectRef) -> Result<()> { |
|
2147 |
+ |
debug_assert!(ENTRY_INDEXES.iter().any(|(i, _)| *i == index)); |
| 1988 |
2148 |
|
let entry = format!("{}{}", fanout_prefix(index, key), value.to_hex()); |
| 1989 |
|
- |
put_new_or_existing(self.backend.as_ref(), &entry, b"") |
|
2149 |
+ |
put_new_or_existing(self.backend.as_ref(), &entry, b"")?; |
|
2150 |
+ |
self.journal_add(entry) |
| 1990 |
2151 |
|
} |
| 1991 |
2152 |
|
|
| 1992 |
2153 |
|
/// The values recorded under `key` in `index`, with those an older |
| 1993 |
2154 |
|
/// store kept as one list in `legacy`, sorted. |
| 1994 |
2155 |
|
fn index_get(&self, index: &str, legacy: &str, key: &ObjectRef) -> Result<Vec<ObjectRef>> { |
| 1995 |
2156 |
|
let mut values: Vec<ObjectRef> = self |
| 1996 |
|
- |
.backend |
| 1997 |
|
- |
.list(&fanout_prefix(index, key)) |
| 1998 |
|
- |
.map_err(backend_err)? |
|
2157 |
+ |
.entry_keys(&fanout_prefix(index, key))? |
| 1999 |
2158 |
|
.into_iter() |
| 2000 |
|
- |
.filter_map(|e| e.key.rsplit('/').next().and_then(ObjectRef::from_hex)) |
|
2159 |
+ |
.filter_map(|k| k.rsplit('/').next().and_then(ObjectRef::from_hex)) |
| 2001 |
2160 |
|
.collect(); |
| 2002 |
|
- |
if let Some(bytes) = get_optional(self.backend.as_ref(), &legacy_key(legacy, key))? { |
|
2161 |
+ |
if !self.reads_legacy() { |
|
2162 |
+ |
// Older lists folded into entries (spec §13.7). |
|
2163 |
+ |
} else if let Some(bytes) = get_optional(self.backend.as_ref(), &legacy_key(legacy, key))? { |
| 2003 |
2164 |
|
values.extend( |
| 2004 |
2165 |
|
String::from_utf8_lossy(&bytes) |
| 2005 |
2166 |
|
.lines() |
|
|
… |
93 unchanged lines |
| 2099 |
2260 |
|
/// packed `many/<hh>/<rest>` lists) holds attestations for. |
| 2100 |
2261 |
|
fn indexed_states(&self, one: &str, many: &str) -> Result<Vec<ObjectRef>> { |
| 2101 |
2262 |
|
let mut out: Vec<ObjectRef> = Vec::new(); |
| 2102 |
|
- |
for e in self.backend.list(&format!("{one}/")).map_err(backend_err)? { |
|
2263 |
+ |
for key in self.entry_keys(&format!("{one}/"))? { |
| 2103 |
2264 |
|
// receipt/<hh>/<rest>/<attest> |
| 2104 |
|
- |
let mut parts = e.key.splitn(4, '/'); |
|
2265 |
+ |
let mut parts = key.splitn(4, '/'); |
| 2105 |
2266 |
|
let (Some(_), Some(hh), Some(rest), Some(_)) = |
| 2106 |
2267 |
|
(parts.next(), parts.next(), parts.next(), parts.next()) |
| 2107 |
2268 |
|
else { |
|
|
… |
1 unchanged lines |
| 2109 |
2270 |
|
}; |
| 2110 |
2271 |
|
out.extend(ObjectRef::from_hex(&format!("{hh}{rest}"))); |
| 2111 |
2272 |
|
} |
| 2112 |
|
- |
for e in self |
| 2113 |
|
- |
.backend |
|
2273 |
+ |
let legacy = if self.reads_legacy() { |
|
2274 |
+ |
self.backend |
| 2114 |
2275 |
|
.list(&format!("{many}/")) |
| 2115 |
2276 |
|
.map_err(backend_err)? |
| 2116 |
|
- |
{ |
|
2277 |
+ |
} else { |
|
2278 |
+ |
Vec::new() |
|
2279 |
+ |
}; |
|
2280 |
+ |
for e in legacy { |
| 2117 |
2281 |
|
let mut parts = e.key.splitn(3, '/'); |
| 2118 |
2282 |
|
let (Some(_), Some(hh), Some(rest)) = (parts.next(), parts.next(), parts.next()) else { |
| 2119 |
2283 |
|
continue; |
|
|
… |
17 unchanged lines |
| 2137 |
2301 |
|
/// key, sorted by key. Empty if none. |
| 2138 |
2302 |
|
pub fn signatures(&self, r: &ObjectRef) -> Result<Vec<Signature>> { |
| 2139 |
2303 |
|
let mut sigs: Vec<Signature> = Vec::new(); |
| 2140 |
|
- |
for e in self |
| 2141 |
|
- |
.backend |
| 2142 |
|
- |
.list(&fanout_prefix("sig", r)) |
| 2143 |
|
- |
.map_err(backend_err)? |
| 2144 |
|
- |
{ |
| 2145 |
|
- |
let bytes = self.backend.get(&e.key).map_err(backend_err)?; |
|
2304 |
+ |
for key in self.entry_keys(&fanout_prefix("sig", r))? { |
|
2305 |
+ |
// Gone since it was listed or journalled: a collection deleted |
|
2306 |
+ |
// it with its object, or a new signature by the same signer is |
|
2307 |
+ |
// replacing it. |
|
2308 |
+ |
let Some(bytes) = get_optional(self.backend.as_ref(), &key)? else { |
|
2309 |
+ |
continue; |
|
2310 |
+ |
}; |
| 2146 |
2311 |
|
sigs.extend(Signature::decode_list(&bytes)?); |
| 2147 |
2312 |
|
} |
| 2148 |
2313 |
|
// An older store's one list per object; a signer kept there and |
| 2149 |
2314 |
|
// here counts once, as written here. |
| 2150 |
|
- |
if let Some(bytes) = get_optional(self.backend.as_ref(), &legacy_key("sigs", r))? { |
|
2315 |
+ |
let legacy = if self.reads_legacy() { |
|
2316 |
+ |
get_optional(self.backend.as_ref(), &legacy_key("sigs", r))? |
|
2317 |
+ |
} else { |
|
2318 |
+ |
None |
|
2319 |
+ |
}; |
|
2320 |
+ |
if let Some(bytes) = legacy { |
| 2151 |
2321 |
|
for sig in Signature::decode_list(&bytes)? { |
| 2152 |
2322 |
|
if !sigs.iter().any(|s| s.key == sig.key) { |
| 2153 |
2323 |
|
sigs.push(sig); |
|
|
… |
11 unchanged lines |
| 2165 |
2335 |
|
pub fn put_signature(&self, r: &ObjectRef, sig: &Signature) -> Result<()> { |
| 2166 |
2336 |
|
let key = format!("{}{}", fanout_prefix("sig", r), sig.key.to_hex()); |
| 2167 |
2337 |
|
let bytes = Signature::encode_list(std::slice::from_ref(sig)); |
| 2168 |
|
- |
if get_optional(self.backend.as_ref(), &key)?.as_deref() == Some(bytes.as_slice()) { |
|
2338 |
+ |
if get_optional(self.backend.as_ref(), &key)?.as_deref() != Some(bytes.as_slice()) { |
|
2339 |
+ |
delete(self.backend.as_ref(), &key)?; |
|
2340 |
+ |
put_new_or_existing(self.backend.as_ref(), &key, &bytes)?; |
|
2341 |
+ |
} |
|
2342 |
+ |
// Journalled even when the key was there: an older writer's |
|
2343 |
+ |
// entry is not in the journal or any list. |
|
2344 |
+ |
self.journal_add(key) |
|
2345 |
+ |
} |
|
2346 |
+ |
|
|
2347 |
+ |
// ---- the journal and entry lists (spec §13.7, #116) |
|
2348 |
+ |
|
|
2349 |
+ |
/// Whether lookups still read the one-list keys of older stores. |
|
2350 |
+ |
fn reads_legacy(&self) -> bool { |
|
2351 |
+ |
true |
|
2352 |
+ |
} |
|
2353 |
+ |
|
|
2354 |
+ |
/// The entry keys under `prefix` (an index's `<index>/` or one |
|
2355 |
+ |
/// object's `<index>/<hh>/<rest>/`), sorted: on a folded store from |
|
2356 |
+ |
/// its lists and journal, with no request; otherwise by listing. |
|
2357 |
+ |
fn entry_keys(&self, prefix: &str) -> Result<Vec<String>> { |
|
2358 |
+ |
if let Some(keys) = self.with_entries(|set| { |
|
2359 |
+ |
set.range(prefix.to_string()..) |
|
2360 |
+ |
.take_while(|k| k.starts_with(prefix)) |
|
2361 |
+ |
.cloned() |
|
2362 |
+ |
.collect::<Vec<String>>() |
|
2363 |
+ |
})? { |
|
2364 |
+ |
return Ok(keys); |
|
2365 |
+ |
} |
|
2366 |
+ |
Ok(self |
|
2367 |
+ |
.backend |
|
2368 |
+ |
.list(prefix) |
|
2369 |
+ |
.map_err(backend_err)? |
|
2370 |
+ |
.into_iter() |
|
2371 |
+ |
.map(|e| e.key) |
|
2372 |
+ |
.collect()) |
|
2373 |
+ |
} |
|
2374 |
+ |
|
|
2375 |
+ |
/// `f` over a folded store's entry keys, read on first use; `None` |
|
2376 |
+ |
/// when the store is not folded (or is this disk's directory), and |
|
2377 |
+ |
/// lookups list. |
|
2378 |
+ |
fn with_entries<T>( |
|
2379 |
+ |
&self, |
|
2380 |
+ |
f: impl FnOnce(&std::collections::BTreeSet<String>) -> T, |
|
2381 |
+ |
) -> Result<Option<T>> { |
|
2382 |
+ |
if self.local || !self.folded.get() { |
|
2383 |
+ |
return Ok(None); |
|
2384 |
+ |
} |
|
2385 |
+ |
if self.entries.borrow().is_none() { |
|
2386 |
+ |
let Some(set) = self.read_entries()? else { |
|
2387 |
+ |
return Ok(None); |
|
2388 |
+ |
}; |
|
2389 |
+ |
*self.entries.borrow_mut() = Some(set); |
|
2390 |
+ |
} |
|
2391 |
+ |
Ok(self.entries.borrow().as_ref().map(f)) |
|
2392 |
+ |
} |
|
2393 |
+ |
|
|
2394 |
+ |
/// Every live entry list's keys and every journal line's, folded or |
|
2395 |
+ |
/// not, with what this handle has queued. A list deleted since the |
|
2396 |
+ |
/// packs were listed (retired over an hour ago) has the packs listed |
|
2397 |
+ |
/// again; `None` when one is still missing then, or the store is no |
|
2398 |
+ |
/// longer folded: lookups list. |
|
2399 |
+ |
fn read_entries(&self) -> Result<Option<std::collections::BTreeSet<String>>> { |
|
2400 |
+ |
'attempt: for attempt in 0..2 { |
|
2401 |
+ |
if !self.folded.get() { |
|
2402 |
+ |
return Ok(None); |
|
2403 |
+ |
} |
|
2404 |
+ |
let mut set = std::collections::BTreeSet::new(); |
|
2405 |
+ |
let lists = self.entry_lists.borrow().clone(); |
|
2406 |
+ |
for key in &lists { |
|
2407 |
+ |
match get_optional(self.backend.as_ref(), key)? { |
|
2408 |
+ |
Some(bytes) => set.extend(decode_entry_list(key, &bytes)?), |
|
2409 |
+ |
None if attempt == 0 => { |
|
2410 |
+ |
self.reload_packs()?; |
|
2411 |
+ |
continue 'attempt; |
|
2412 |
+ |
} |
|
2413 |
+ |
None => return Ok(None), |
|
2414 |
+ |
} |
|
2415 |
+ |
} |
|
2416 |
+ |
set.extend(self.read_record(JOURNAL_KEY)?.0.into_keys()); |
|
2417 |
+ |
set.extend(self.journal_queue.borrow().iter().cloned()); |
|
2418 |
+ |
return Ok(Some(set)); |
|
2419 |
+ |
} |
|
2420 |
+ |
Ok(None) |
|
2421 |
+ |
} |
|
2422 |
+ |
|
|
2423 |
+ |
/// Add an entry key this handle wrote to the journal: at once, or |
|
2424 |
+ |
/// when the open batch is flushed. One the handle already knows from |
|
2425 |
+ |
/// the lists or the journal is not added again. Nothing on this |
|
2426 |
+ |
/// disk's directory. |
|
2427 |
+ |
fn journal_add(&self, key: String) -> Result<()> { |
|
2428 |
+ |
if self.local { |
|
2429 |
+ |
return Ok(()); |
|
2430 |
+ |
} |
|
2431 |
+ |
if let Some(set) = self.entries.borrow_mut().as_mut() { |
|
2432 |
+ |
if !set.insert(key.clone()) { |
|
2433 |
+ |
return Ok(()); |
|
2434 |
+ |
} |
|
2435 |
+ |
} |
|
2436 |
+ |
if self.batch_depth.get() > 0 { |
|
2437 |
+ |
self.journal_queue.borrow_mut().push(key); |
|
2438 |
+ |
return Ok(()); |
|
2439 |
+ |
} |
|
2440 |
+ |
self.journal_append(std::slice::from_ref(&key)) |
|
2441 |
+ |
} |
|
2442 |
+ |
|
|
2443 |
+ |
/// Add what the open batch queued to the journal. |
|
2444 |
+ |
fn flush_journal(&self) -> Result<()> { |
|
2445 |
+ |
let queued = std::mem::take(&mut *self.journal_queue.borrow_mut()); |
|
2446 |
+ |
self.journal_append(&queued) |
|
2447 |
+ |
} |
|
2448 |
+ |
|
|
2449 |
+ |
/// Add `keys` to the journal, unfolded, in one compare-and-swap |
|
2450 |
+ |
/// update, dropping lines folded over an hour ago; past |
|
2451 |
+ |
/// [`JOURNAL_FOLD_AT`] unfolded lines, fold them into a partial list. |
|
2452 |
+ |
fn journal_append(&self, keys: &[String]) -> Result<()> { |
|
2453 |
+ |
if keys.is_empty() { |
| 2169 |
2454 |
|
return Ok(()); |
|
2455 |
+ |
} |
|
2456 |
+ |
let now = now_ms(); |
|
2457 |
+ |
let mut unfolded = 0; |
|
2458 |
+ |
self.update_record(JOURNAL_KEY, "the journal of entries", |m| { |
|
2459 |
+ |
expire_folded(m, now); |
|
2460 |
+ |
for k in keys { |
|
2461 |
+ |
m.entry(k.clone()).or_insert(0); |
|
2462 |
+ |
} |
|
2463 |
+ |
unfolded = m.values().filter(|t| **t == 0).count(); |
|
2464 |
+ |
})?; |
|
2465 |
+ |
if unfolded > JOURNAL_FOLD_AT { |
|
2466 |
+ |
self.fold_journal()?; |
|
2467 |
+ |
} |
|
2468 |
+ |
Ok(()) |
|
2469 |
+ |
} |
|
2470 |
+ |
|
|
2471 |
+ |
/// Fold the journal's unfolded lines into a partial entry list, and |
|
2472 |
+ |
/// mark them folded. Lines added meanwhile stay unfolded. |
|
2473 |
+ |
fn fold_journal(&self) -> Result<()> { |
|
2474 |
+ |
let (journal, _) = self.read_record(JOURNAL_KEY)?; |
|
2475 |
+ |
let keys: Vec<String> = journal |
|
2476 |
+ |
.into_iter() |
|
2477 |
+ |
.filter(|(_, t)| *t == 0) |
|
2478 |
+ |
.map(|(k, _)| k) |
|
2479 |
+ |
.collect(); |
|
2480 |
+ |
if keys.is_empty() { |
|
2481 |
+ |
return Ok(()); |
|
2482 |
+ |
} |
|
2483 |
+ |
self.put_entry_list(PART_SUFFIX, keys.iter())?; |
|
2484 |
+ |
self.mark_folded(&keys) |
|
2485 |
+ |
} |
|
2486 |
+ |
|
|
2487 |
+ |
/// Mark these journal lines folded now, if they are not yet; drop |
|
2488 |
+ |
/// those folded over an hour ago. |
|
2489 |
+ |
fn mark_folded(&self, keys: &[String]) -> Result<()> { |
|
2490 |
+ |
let now = now_ms(); |
|
2491 |
+ |
self.update_record(JOURNAL_KEY, "the journal of entries", |m| { |
|
2492 |
+ |
expire_folded(m, now); |
|
2493 |
+ |
for k in keys { |
|
2494 |
+ |
if let Some(t) = m.get_mut(k) { |
|
2495 |
+ |
if *t == 0 { |
|
2496 |
+ |
*t = now; |
|
2497 |
+ |
} |
|
2498 |
+ |
} |
|
2499 |
+ |
} |
|
2500 |
+ |
}) |
|
2501 |
+ |
} |
|
2502 |
+ |
|
|
2503 |
+ |
/// Write a new entry list of `keys` (sorted), complete or partial by |
|
2504 |
+ |
/// `suffix`; its key. |
|
2505 |
+ |
fn put_entry_list<'a>( |
|
2506 |
+ |
&self, |
|
2507 |
+ |
suffix: &str, |
|
2508 |
+ |
keys: impl Iterator<Item = &'a String>, |
|
2509 |
+ |
) -> Result<String> { |
|
2510 |
+ |
let key = format!("pack-{}{suffix}", fresh_id()); |
|
2511 |
+ |
let bytes = encode_entry_list(keys)?; |
|
2512 |
+ |
self.backend.put_new(&key, &bytes).map_err(backend_err)?; |
|
2513 |
+ |
Ok(key) |
|
2514 |
+ |
} |
|
2515 |
+ |
|
|
2516 |
+ |
/// Fold every signature and index entry into one complete entry list |
|
2517 |
+ |
/// (spec §13.7), what a collection does last: the journal's lines and |
|
2518 |
+ |
/// a listing of every entry prefix, written as a new list, every list |
|
2519 |
+ |
/// the packs' listing showed retired, and the journal lines read |
|
2520 |
+ |
/// marked folded. Nothing when the store is this disk's directory, or |
|
2521 |
+ |
/// when the one live list already holds exactly these entries and the |
|
2522 |
+ |
/// journal has nothing unfolded. Whether a list was written. |
|
2523 |
+ |
pub fn fold_entries(&self, progress: Progress) -> Result<bool> { |
|
2524 |
+ |
if self.local { |
|
2525 |
+ |
return Ok(false); |
| 2170 |
2526 |
|
} |
| 2171 |
|
- |
delete(self.backend.as_ref(), &key)?; |
| 2172 |
|
- |
put_new_or_existing(self.backend.as_ref(), &key, &bytes) |
|
2527 |
+ |
self.reload_packs()?; |
|
2528 |
+ |
let (journal, _) = self.read_record(JOURNAL_KEY)?; |
|
2529 |
+ |
progress("folding signatures and index entries"); |
|
2530 |
+ |
let mut all: std::collections::BTreeSet<String> = journal.keys().cloned().collect(); |
|
2531 |
+ |
for (index, _) in ENTRY_INDEXES { |
|
2532 |
+ |
for e in self |
|
2533 |
+ |
.backend |
|
2534 |
+ |
.list(&format!("{index}/")) |
|
2535 |
+ |
.map_err(backend_err)? |
|
2536 |
+ |
{ |
|
2537 |
+ |
all.insert(e.key); |
|
2538 |
+ |
} |
|
2539 |
+ |
} |
|
2540 |
+ |
let lists = self.entry_lists.borrow().clone(); |
|
2541 |
+ |
let unfolded: Vec<String> = journal |
|
2542 |
+ |
.iter() |
|
2543 |
+ |
.filter(|(_, t)| **t == 0) |
|
2544 |
+ |
.map(|(k, _)| k.clone()) |
|
2545 |
+ |
.collect(); |
|
2546 |
+ |
if unfolded.is_empty() && lists.len() == 1 && lists[0].ends_with(LIST_SUFFIX) { |
|
2547 |
+ |
if let Some(bytes) = get_optional(self.backend.as_ref(), &lists[0])? { |
|
2548 |
+ |
let held: std::collections::BTreeSet<String> = |
|
2549 |
+ |
decode_entry_list(&lists[0], &bytes)?.into_iter().collect(); |
|
2550 |
+ |
if held == all { |
|
2551 |
+ |
return Ok(false); |
|
2552 |
+ |
} |
|
2553 |
+ |
} |
|
2554 |
+ |
} |
|
2555 |
+ |
self.put_entry_list(LIST_SUFFIX, all.iter())?; |
|
2556 |
+ |
self.retire(&lists)?; |
|
2557 |
+ |
if !unfolded.is_empty() { |
|
2558 |
+ |
self.mark_folded(&unfolded)?; |
|
2559 |
+ |
} |
|
2560 |
+ |
*self.entries.borrow_mut() = None; |
|
2561 |
+ |
self.reload_packs()?; |
|
2562 |
+ |
Ok(true) |
| 2173 |
2563 |
|
} |
| 2174 |
2564 |
|
|
| 2175 |
2565 |
|
/// Finish packs a writer left unfinished: a batch writes its objects to |
|
|
… |
1375 unchanged lines |
| 3551 |
3941 |
|
let again = Store::open(dir.path()).unwrap(); |
| 3552 |
3942 |
|
for (r, b) in refs.iter().zip(&objects) { |
| 3553 |
3943 |
|
assert_eq!(again.get(r).unwrap().1, b.1); |
|
3944 |
+ |
} |
|
3945 |
+ |
} |
|
3946 |
+ |
} |
|
3947 |
+ |
|
|
3948 |
+ |
/// Signatures and index entries on a store that is not this disk's |
|
3949 |
+ |
/// directory: the journal and the entry lists (spec §13.7, #116). |
|
3950 |
+ |
#[cfg(test)] |
|
3951 |
+ |
mod entry_tests { |
|
3952 |
+ |
use super::*; |
|
3953 |
+ |
use crate::identity::{Domain, SecretKey}; |
|
3954 |
+ |
use crate::strata::Store as _; |
|
3955 |
+ |
use std::collections::HashMap; |
|
3956 |
+ |
use std::sync::atomic::{AtomicBool, Ordering}; |
|
3957 |
+ |
use std::sync::Mutex; |
|
3958 |
+ |
|
|
3959 |
+ |
/// A store in memory that counts its requests by kind, as a bucket |
|
3960 |
+ |
/// store's /health counters do; it can also slip a competing journal |
|
3961 |
+ |
/// line in just before a write of the journal, as another process |
|
3962 |
+ |
/// would. |
|
3963 |
+ |
#[derive(Default)] |
|
3964 |
+ |
struct Counted { |
|
3965 |
+ |
inner: MemoryStore, |
|
3966 |
+ |
counts: Mutex<HashMap<&'static str, usize>>, |
|
3967 |
+ |
interfere: AtomicBool, |
|
3968 |
+ |
} |
|
3969 |
+ |
|
|
3970 |
+ |
impl Counted { |
|
3971 |
+ |
fn count(&self, kind: &'static str) { |
|
3972 |
+ |
*self.counts.lock().unwrap().entry(kind).or_default() += 1; |
|
3973 |
+ |
} |
|
3974 |
+ |
fn n(&self, kind: &str) -> usize { |
|
3975 |
+ |
self.counts.lock().unwrap().get(kind).copied().unwrap_or(0) |
| 3554 |
3976 |
|
} |
|
3977 |
+ |
fn reset(&self) { |
|
3978 |
+ |
self.counts.lock().unwrap().clear(); |
|
3979 |
+ |
} |
|
3980 |
+ |
fn total(&self) -> usize { |
|
3981 |
+ |
self.counts.lock().unwrap().values().sum() |
|
3982 |
+ |
} |
|
3983 |
+ |
fn summary(&self) -> String { |
|
3984 |
+ |
let c = self.counts.lock().unwrap(); |
|
3985 |
+ |
let mut v: Vec<String> = c.iter().map(|(k, n)| format!("{k} {n}")).collect(); |
|
3986 |
+ |
v.sort(); |
|
3987 |
+ |
v.join(", ") |
|
3988 |
+ |
} |
|
3989 |
+ |
} |
|
3990 |
+ |
|
|
3991 |
+ |
impl crate::strata::Store for Counted { |
|
3992 |
+ |
fn describe(&self) -> String { |
|
3993 |
+ |
"counted".into() |
|
3994 |
+ |
} |
|
3995 |
+ |
fn has(&self, k: &str) -> crate::strata::Result<bool> { |
|
3996 |
+ |
self.count("has"); |
|
3997 |
+ |
self.inner.has(k) |
|
3998 |
+ |
} |
|
3999 |
+ |
fn get(&self, k: &str) -> crate::strata::Result<Vec<u8>> { |
|
4000 |
+ |
self.count(if is_legacy(k) { "get_legacy" } else { "get" }); |
|
4001 |
+ |
self.inner.get(k) |
|
4002 |
+ |
} |
|
4003 |
+ |
fn get_range(&self, k: &str, r: std::ops::Range<u64>) -> crate::strata::Result<Vec<u8>> { |
|
4004 |
+ |
self.count("range"); |
|
4005 |
+ |
self.inner.get_range(k, r) |
|
4006 |
+ |
} |
|
4007 |
+ |
fn put_new(&self, k: &str, b: &[u8]) -> crate::strata::Result<()> { |
|
4008 |
+ |
self.count("put"); |
|
4009 |
+ |
self.inner.put_new(k, b) |
|
4010 |
+ |
} |
|
4011 |
+ |
fn list(&self, p: &str) -> crate::strata::Result<Vec<crate::strata::Entry>> { |
|
4012 |
+ |
self.count(if p == "pack-" { |
|
4013 |
+ |
"list_packs" |
|
4014 |
+ |
} else if is_legacy(p) { |
|
4015 |
+ |
"list_legacy" |
|
4016 |
+ |
} else { |
|
4017 |
+ |
"list" |
|
4018 |
+ |
}); |
|
4019 |
+ |
self.inner.list(p) |
|
4020 |
+ |
} |
|
4021 |
+ |
fn delete(&self, k: &str) -> crate::strata::Result<()> { |
|
4022 |
+ |
self.count("delete"); |
|
4023 |
+ |
self.inner.delete(k) |
|
4024 |
+ |
} |
|
4025 |
+ |
fn read_ref( |
|
4026 |
+ |
&self, |
|
4027 |
+ |
k: &str, |
|
4028 |
+ |
) -> crate::strata::Result<Option<(Vec<u8>, crate::strata::Version)>> { |
|
4029 |
+ |
self.count(if k == JOURNAL_KEY { |
|
4030 |
+ |
"read_journal" |
|
4031 |
+ |
} else { |
|
4032 |
+ |
"read_ref" |
|
4033 |
+ |
}); |
|
4034 |
+ |
self.inner.read_ref(k) |
|
4035 |
+ |
} |
|
4036 |
+ |
fn write_ref( |
|
4037 |
+ |
&self, |
|
4038 |
+ |
k: &str, |
|
4039 |
+ |
b: &[u8], |
|
4040 |
+ |
e: Option<&crate::strata::Version>, |
|
4041 |
+ |
) -> crate::strata::Result<crate::strata::Version> { |
|
4042 |
+ |
self.count("write_ref"); |
|
4043 |
+ |
if k == JOURNAL_KEY && self.interfere.swap(false, Ordering::SeqCst) { |
|
4044 |
+ |
let (mut m, v) = match self.inner.read_ref(k)? { |
|
4045 |
+ |
Some((bytes, v)) => (parse_retired(&bytes), Some(v)), |
|
4046 |
+ |
None => (Default::default(), None), |
|
4047 |
+ |
}; |
|
4048 |
+ |
m.insert( |
|
4049 |
+ |
format!("seal/{}", "c".repeat(64)) + "/" + &"d".repeat(64), |
|
4050 |
+ |
0, |
|
4051 |
+ |
); |
|
4052 |
+ |
self.inner.write_ref(k, &encode_retired(&m), v.as_ref())?; |
|
4053 |
+ |
} |
|
4054 |
+ |
self.inner.write_ref(k, b, e) |
|
4055 |
+ |
} |
|
4056 |
+ |
} |
|
4057 |
+ |
|
|
4058 |
+ |
/// Whether a key or prefix is under an older one-list layout. |
|
4059 |
+ |
fn is_legacy(k: &str) -> bool { |
|
4060 |
+ |
ENTRY_INDEXES |
|
4061 |
+ |
.iter() |
|
4062 |
+ |
.any(|(_, legacy)| k.starts_with(&format!("{legacy}/"))) |
|
4063 |
+ |
} |
|
4064 |
+ |
|
|
4065 |
+ |
struct Fixture { |
|
4066 |
+ |
backend: Arc<Counted>, |
|
4067 |
+ |
dir: tempfile::TempDir, |
|
4068 |
+ |
handles: std::cell::Cell<usize>, |
|
4069 |
+ |
} |
|
4070 |
+ |
|
|
4071 |
+ |
impl Fixture { |
|
4072 |
+ |
fn new() -> Fixture { |
|
4073 |
+ |
Fixture { |
|
4074 |
+ |
backend: Arc::new(Counted::default()), |
|
4075 |
+ |
dir: tempfile::tempdir().unwrap(), |
|
4076 |
+ |
handles: Default::default(), |
|
4077 |
+ |
} |
|
4078 |
+ |
} |
|
4079 |
+ |
/// Another handle on the store, as another process opens it. |
|
4080 |
+ |
fn open(&self) -> Store { |
|
4081 |
+ |
let n = self.handles.get() + 1; |
|
4082 |
+ |
self.handles.set(n); |
|
4083 |
+ |
Store::open_on(self.backend.clone(), self.dir.path().join(format!("s{n}"))).unwrap() |
|
4084 |
+ |
} |
|
4085 |
+ |
} |
|
4086 |
+ |
|
|
4087 |
+ |
fn signer(n: u8) -> SecretKey { |
|
4088 |
+ |
SecretKey { |
|
4089 |
+ |
ed_seed: [n; 32], |
|
4090 |
+ |
ml_seed: [n + 100; 32], |
|
4091 |
+ |
} |
|
4092 |
+ |
} |
|
4093 |
+ |
|
|
4094 |
+ |
fn sign(s: &Store, r: &ObjectRef, n: u8) -> Signature { |
|
4095 |
+ |
let sk = signer(n); |
|
4096 |
+ |
let key = s.put_identity(&sk.identity("a", "b", 0)).unwrap(); |
|
4097 |
+ |
let sig = sk.sign(Domain::Change, &key, r); |
|
4098 |
+ |
s.put_signature(r, &sig).unwrap(); |
|
4099 |
+ |
sig |
|
4100 |
+ |
} |
|
4101 |
+ |
|
|
4102 |
+ |
fn journal(s: &Store) -> std::collections::BTreeMap<String, u64> { |
|
4103 |
+ |
s.read_record(JOURNAL_KEY).unwrap().0 |
|
4104 |
+ |
} |
|
4105 |
+ |
|
|
4106 |
+ |
fn fold(s: &Store) -> bool { |
|
4107 |
+ |
s.fold_entries(&mut silent()).unwrap() |
|
4108 |
+ |
} |
|
4109 |
+ |
|
|
4110 |
+ |
/// What 0.4.6 writes for a signature: its own key, and nothing else. |
|
4111 |
+ |
fn old_writer_signs(f: &Fixture, r: &ObjectRef, n: u8) -> Signature { |
|
4112 |
+ |
let sk = signer(n); |
|
4113 |
+ |
let sig = sk.sign(Domain::Change, &ObjectRef([n; 32]), r); |
|
4114 |
+ |
f.backend |
|
4115 |
+ |
.put_new( |
|
4116 |
+ |
&format!("{}{}", fanout_prefix("sig", r), sig.key.to_hex()), |
|
4117 |
+ |
&Signature::encode_list(std::slice::from_ref(&sig)), |
|
4118 |
+ |
) |
|
4119 |
+ |
.unwrap(); |
|
4120 |
+ |
sig |
|
4121 |
+ |
} |
|
4122 |
+ |
|
|
4123 |
+ |
/// After a fold, a signature or seal another handle writes is found |
|
4124 |
+ |
/// by a store opened later through the journal, with no listing; a |
|
4125 |
+ |
/// handle that already read the entries sees it once refreshed. |
|
4126 |
+ |
#[test] |
|
4127 |
+ |
fn an_entry_written_after_a_fold_is_found_through_the_journal() { |
|
4128 |
+ |
let f = Fixture::new(); |
|
4129 |
+ |
let w = f.open(); |
|
4130 |
+ |
let r1 = w.put(ObjectType::Chunk, b"one").unwrap(); |
|
4131 |
+ |
let s1 = sign(&w, &r1, 1); |
|
4132 |
+ |
assert!(fold(&w)); |
|
4133 |
+ |
assert!(!fold(&w), "nothing new: no second list"); |
|
4134 |
+ |
|
|
4135 |
+ |
let early = f.open(); |
|
4136 |
+ |
assert_eq!(early.signatures(&r1).unwrap(), vec![s1.clone()]); |
|
4137 |
+ |
|
|
4138 |
+ |
let w2 = f.open(); |
|
4139 |
+ |
let r2 = w2.put(ObjectType::Chunk, b"two").unwrap(); |
|
4140 |
+ |
let s2 = sign(&w2, &r2, 2); |
|
4141 |
+ |
w2.index_seal(&r2, &ObjectRef([9; 32])).unwrap(); |
|
4142 |
+ |
w2.index_receipt(&r2, &ObjectRef([8; 32])).unwrap(); |
|
4143 |
+ |
// Its own writes, at once. |
|
4144 |
+ |
assert_eq!(w2.signatures(&r2).unwrap(), vec![s2.clone()]); |
|
4145 |
+ |
|
|
4146 |
+ |
let later = f.open(); |
|
4147 |
+ |
f.backend.reset(); |
|
4148 |
+ |
assert_eq!(later.signatures(&r1).unwrap(), vec![s1]); |
|
4149 |
+ |
assert_eq!(later.signatures(&r2).unwrap(), vec![s2.clone()]); |
|
4150 |
+ |
assert_eq!(later.seals_for(&r2).unwrap(), vec![ObjectRef([9; 32])]); |
|
4151 |
+ |
assert_eq!(later.receipts_for(&r2).unwrap(), vec![ObjectRef([8; 32])]); |
|
4152 |
+ |
assert_eq!(later.receipt_states().unwrap(), vec![r2]); |
|
4153 |
+ |
assert!(later.seals_for(&r1).unwrap().is_empty()); |
|
4154 |
+ |
assert_eq!(f.backend.n("list"), 0, "{}", f.backend.summary()); |
|
4155 |
+ |
assert_eq!(f.backend.n("read_journal"), 1, "{}", f.backend.summary()); |
|
4156 |
+ |
|
|
4157 |
+ |
// The handle that read the entries before keeps its view until |
|
4158 |
+ |
// it is refreshed, as it keeps its operation head. |
|
4159 |
+ |
assert!(early.signatures(&r2).unwrap().is_empty()); |
|
4160 |
+ |
early.refresh().unwrap(); |
|
4161 |
+ |
assert_eq!(early.signatures(&r2).unwrap(), vec![s2]); |
|
4162 |
+ |
} |
|
4163 |
+ |
|
|
4164 |
+ |
/// A fold marks the journal's lines rather than dropping them, so a |
|
4165 |
+ |
/// reader whose listing predates it still finds every entry; an hour |
|
4166 |
+ |
/// on, the old list is deleted and the lines dropped, and readers |
|
4167 |
+ |
/// find everything in the new list. |
|
4168 |
+ |
#[test] |
|
4169 |
+ |
fn a_fold_keeps_the_journal_for_the_hour_readers_may_need_it() { |
|
4170 |
+ |
let f = Fixture::new(); |
|
4171 |
+ |
let w = f.open(); |
|
4172 |
+ |
let r1 = w.put(ObjectType::Chunk, b"one").unwrap(); |
|
4173 |
+ |
let s1 = sign(&w, &r1, 1); |
|
4174 |
+ |
assert!(fold(&w)); |
|
4175 |
+ |
let r2 = w.put(ObjectType::Chunk, b"two").unwrap(); |
|
4176 |
+ |
let s2 = sign(&w, &r2, 2); |
|
4177 |
+ |
assert!(journal(&w).values().any(|t| *t == 0)); |
|
4178 |
+ |
|
|
4179 |
+ |
// Opened before the second fold: lists the first list only. |
|
4180 |
+ |
let stale = f.open(); |
|
4181 |
+ |
let stale_after_expiry = f.open(); |
|
4182 |
+ |
assert!(fold(&w)); |
|
4183 |
+ |
assert!( |
|
4184 |
+ |
journal(&w).values().all(|t| *t != 0), |
|
4185 |
+ |
"every line is marked folded" |
|
4186 |
+ |
); |
|
4187 |
+ |
assert!(journal(&w).len() >= 2, "and kept"); |
|
4188 |
+ |
assert_eq!(stale.signatures(&r1).unwrap(), vec![s1.clone()]); |
|
4189 |
+ |
assert_eq!(stale.signatures(&r2).unwrap(), vec![s2.clone()]); |
|
4190 |
+ |
|
|
4191 |
+ |
// An hour later: the first list goes with the next collection, |
|
4192 |
+ |
// and the next journal update drops the folded lines. |
|
4193 |
+ |
reader_tests::age_retired(&w, RETIRED_FOR * 2); |
|
4194 |
+ |
w.update_record(JOURNAL_KEY, "test", |m| { |
|
4195 |
+ |
for t in m.values_mut() { |
|
4196 |
+ |
*t = t.saturating_sub(2 * RETIRED_FOR.as_millis() as u64); |
|
4197 |
+ |
} |
|
4198 |
+ |
}) |
|
4199 |
+ |
.unwrap(); |
|
4200 |
+ |
w.expire_retired(RETIRED_FOR, &mut silent()).unwrap(); |
|
4201 |
+ |
let lists: Vec<String> = f |
|
4202 |
+ |
.backend |
|
4203 |
+ |
.inner |
|
4204 |
+ |
.list("pack-") |
|
4205 |
+ |
.unwrap() |
|
4206 |
+ |
.into_iter() |
|
4207 |
+ |
.map(|e| e.key) |
|
4208 |
+ |
.filter(|k| is_entry_list(k)) |
|
4209 |
+ |
.collect(); |
|
4210 |
+ |
assert_eq!(lists.len(), 1, "the replaced list is deleted: {lists:?}"); |
|
4211 |
+ |
let r3 = w.put(ObjectType::Chunk, b"three").unwrap(); |
|
4212 |
+ |
let s3 = sign(&w, &r3, 3); |
|
4213 |
+ |
let j = journal(&w); |
|
4214 |
+ |
assert_eq!(j.len(), 1, "only the new line is left: {j:?}"); |
|
4215 |
+ |
assert!(j.keys().all(|k| k.starts_with(&fanout_prefix("sig", &r3)))); |
|
4216 |
+ |
|
|
4217 |
+ |
// A handle whose listing named the deleted list lists again. |
|
4218 |
+ |
assert_eq!( |
|
4219 |
+ |
stale_after_expiry.signatures(&r1).unwrap(), |
|
4220 |
+ |
vec![s1.clone()] |
|
4221 |
+ |
); |
|
4222 |
+ |
assert_eq!( |
|
4223 |
+ |
stale_after_expiry.signatures(&r3).unwrap(), |
|
4224 |
+ |
vec![s3.clone()] |
|
4225 |
+ |
); |
|
4226 |
+ |
let fresh = f.open(); |
|
4227 |
+ |
f.backend.reset(); |
|
4228 |
+ |
assert_eq!(fresh.signatures(&r1).unwrap(), vec![s1]); |
|
4229 |
+ |
assert_eq!(fresh.signatures(&r2).unwrap(), vec![s2]); |
|
4230 |
+ |
assert_eq!(fresh.signatures(&r3).unwrap(), vec![s3]); |
|
4231 |
+ |
assert_eq!(f.backend.n("list"), 0); |
|
4232 |
+ |
} |
|
4233 |
+ |
|
|
4234 |
+ |
/// A store no fold has touched (as 0.4.6 left it: per-object keys and |
|
4235 |
+ |
/// older one-list keys, no journal, no lists) reads by listing, and |
|
4236 |
+ |
/// reads the same once folded. An older writer's entry after the |
|
4237 |
+ |
/// fold is missed until the next fold: the trade-off of §13.7. |
|
4238 |
+ |
#[test] |
|
4239 |
+ |
fn an_old_layout_store_reads_before_and_after_its_first_fold() { |
|
4240 |
+ |
let f = Fixture::new(); |
|
4241 |
+ |
let w = f.open(); |
|
4242 |
+ |
let r1 = w.put(ObjectType::Chunk, b"one").unwrap(); |
|
4243 |
+ |
let r2 = w.put(ObjectType::Chunk, b"two").unwrap(); |
|
4244 |
+ |
let s1 = old_writer_signs(&f, &r1, 1); |
|
4245 |
+ |
let s2 = SecretKey { |
|
4246 |
+ |
ed_seed: [2; 32], |
|
4247 |
+ |
ml_seed: [3; 32], |
|
4248 |
+ |
} |
|
4249 |
+ |
.sign(Domain::Change, &ObjectRef([2; 32]), &r2); |
|
4250 |
+ |
f.backend |
|
4251 |
+ |
.put_new( |
|
4252 |
+ |
&legacy_key("sigs", &r2), |
|
4253 |
+ |
&Signature::encode_list(std::slice::from_ref(&s2)), |
|
4254 |
+ |
) |
|
4255 |
+ |
.unwrap(); |
|
4256 |
+ |
f.backend |
|
4257 |
+ |
.put_new( |
|
4258 |
+ |
&format!("{}{}", fanout_prefix("seal", &r1), "ab".repeat(32)), |
|
4259 |
+ |
b"", |
|
4260 |
+ |
) |
|
4261 |
+ |
.unwrap(); |
|
4262 |
+ |
f.backend |
|
4263 |
+ |
.put_new( |
|
4264 |
+ |
&legacy_key("receipts", &r2), |
|
4265 |
+ |
format!("{}\n", ObjectRef([7; 32]).to_hex()).as_bytes(), |
|
4266 |
+ |
) |
|
4267 |
+ |
.unwrap(); |
|
4268 |
+ |
let check = |s: &Store| { |
|
4269 |
+ |
assert_eq!(s.signatures(&r1).unwrap(), vec![s1.clone()]); |
|
4270 |
+ |
assert_eq!(s.signatures(&r2).unwrap(), vec![s2.clone()]); |
|
4271 |
+ |
assert_eq!(s.seals_for(&r1).unwrap(), vec![ObjectRef([0xab; 32])]); |
|
4272 |
+ |
assert_eq!(s.receipts_for(&r2).unwrap(), vec![ObjectRef([7; 32])]); |
|
4273 |
+ |
assert_eq!(s.receipt_states().unwrap(), vec![r2]); |
|
4274 |
+ |
}; |
|
4275 |
+ |
let reader = f.open(); |
|
4276 |
+ |
assert!(!reader.folded.get()); |
|
4277 |
+ |
check(&reader); |
|
4278 |
+ |
assert!(f.backend.inner.read_ref(JOURNAL_KEY).unwrap().is_none()); |
|
4279 |
+ |
|
|
4280 |
+ |
assert!(fold(&w)); |
|
4281 |
+ |
let reader = f.open(); |
|
4282 |
+ |
assert!(reader.folded.get()); |
|
4283 |
+ |
check(&reader); |
|
4284 |
+ |
|
|
4285 |
+ |
// An older writer after the fold: missed until the next one. |
|
4286 |
+ |
let r3 = w.put(ObjectType::Chunk, b"three").unwrap(); |
|
4287 |
+ |
let s3 = old_writer_signs(&f, &r3, 3); |
|
4288 |
+ |
assert!(f.open().signatures(&r3).unwrap().is_empty()); |
|
4289 |
+ |
assert!(fold(&w)); |
|
4290 |
+ |
assert_eq!(f.open().signatures(&r3).unwrap(), vec![s3]); |
|
4291 |
+ |
} |
|
4292 |
+ |
|
|
4293 |
+ |
/// Writers racing on the journal never lose a line: a lost |
|
4294 |
+ |
/// compare-and-swap reads the journal again and retries. |
|
4295 |
+ |
#[test] |
|
4296 |
+ |
fn concurrent_journal_appends_lose_nothing() { |
|
4297 |
+ |
let f = Fixture::new(); |
|
4298 |
+ |
// One race, made to happen. |
|
4299 |
+ |
let w = f.open(); |
|
4300 |
+ |
f.backend.interfere.store(true, Ordering::SeqCst); |
|
4301 |
+ |
w.index_seal(&ObjectRef([1; 32]), &ObjectRef([2; 32])) |
|
4302 |
+ |
.unwrap(); |
|
4303 |
+ |
let j = journal(&w); |
|
4304 |
+ |
assert_eq!(j.len(), 2, "the competitor's line and ours: {j:?}"); |
|
4305 |
+ |
// Many, from threads, each its own handle. |
|
4306 |
+ |
let backend: Arc<Backend> = f.backend.clone(); |
|
4307 |
+ |
std::thread::scope(|scope| { |
|
4308 |
+ |
for t in 0..8u8 { |
|
4309 |
+ |
let backend = backend.clone(); |
|
4310 |
+ |
let dir = f.dir.path().join(format!("t{t}")); |
|
4311 |
+ |
scope.spawn(move || { |
|
4312 |
+ |
let s = Store::open_on(backend, dir).unwrap(); |
|
4313 |
+ |
for i in 0..25u8 { |
|
4314 |
+ |
s.index_seal(&ObjectRef([t + 10; 32]), &ObjectRef([i; 32])) |
|
4315 |
+ |
.unwrap(); |
|
4316 |
+ |
} |
|
4317 |
+ |
}); |
|
4318 |
+ |
} |
|
4319 |
+ |
}); |
|
4320 |
+ |
assert_eq!(journal(&w).len(), 2 + 8 * 25); |
|
4321 |
+ |
assert!(fold(&w)); |
|
4322 |
+ |
let r = f.open(); |
|
4323 |
+ |
for t in 0..8u8 { |
|
4324 |
+ |
assert_eq!(r.seals_for(&ObjectRef([t + 10; 32])).unwrap().len(), 25); |
|
4325 |
+ |
} |
|
4326 |
+ |
} |
|
4327 |
+ |
|
|
4328 |
+ |
/// A batch adds its entries to the journal in one update when it is |
|
4329 |
+ |
/// flushed; past the limit the writer folds them into a partial list. |
|
4330 |
+ |
#[test] |
|
4331 |
+ |
fn a_long_journal_is_folded_into_a_partial_list() { |
|
4332 |
+ |
let f = Fixture::new(); |
|
4333 |
+ |
let w = f.open(); |
|
4334 |
+ |
assert!(fold(&w), "folded, with nothing yet"); |
|
4335 |
+ |
let key = ObjectRef([5; 32]); |
|
4336 |
+ |
f.backend.reset(); |
|
4337 |
+ |
let batch = w.begin_batch().unwrap(); |
|
4338 |
+ |
for i in 0..=JOURNAL_FOLD_AT as u32 { |
|
4339 |
+ |
let mut v = [0u8; 32]; |
|
4340 |
+ |
v[..4].copy_from_slice(&i.to_le_bytes()); |
|
4341 |
+ |
w.index_seal(&key, &ObjectRef(v)).unwrap(); |
|
4342 |
+ |
} |
|
4343 |
+ |
assert_eq!(f.backend.n("write_ref"), 0, "nothing until the flush"); |
|
4344 |
+ |
batch.finish().unwrap(); |
|
4345 |
+ |
let parts = f |
|
4346 |
+ |
.backend |
|
4347 |
+ |
.inner |
|
4348 |
+ |
.list("pack-") |
|
4349 |
+ |
.unwrap() |
|
4350 |
+ |
.into_iter() |
|
4351 |
+ |
.filter(|e| e.key.ends_with(PART_SUFFIX)) |
|
4352 |
+ |
.count(); |
|
4353 |
+ |
assert_eq!(parts, 1); |
|
4354 |
+ |
assert!(journal(&w).values().all(|t| *t != 0), "all folded"); |
|
4355 |
+ |
let r = f.open(); |
|
4356 |
+ |
assert_eq!(r.seals_for(&key).unwrap().len(), JOURNAL_FOLD_AT + 1); |
|
4357 |
+ |
// The next complete fold takes the partial list in. |
|
4358 |
+ |
assert!(fold(&w)); |
|
4359 |
+ |
let lists: Vec<String> = f.open().entry_lists.borrow().clone(); |
|
4360 |
+ |
assert_eq!(lists.len(), 1); |
|
4361 |
+ |
assert!(lists[0].ends_with(LIST_SUFFIX)); |
|
4362 |
+ |
assert_eq!(f.open().seals_for(&key).unwrap().len(), JOURNAL_FOLD_AT + 1); |
|
4363 |
+ |
} |
|
4364 |
+ |
|
|
4365 |
+ |
/// A collection that deletes an object takes its entries out of the |
|
4366 |
+ |
/// journal too, so the next fold leaves them out. |
|
4367 |
+ |
#[test] |
|
4368 |
+ |
fn pruned_objects_leave_the_journal() { |
|
4369 |
+ |
let f = Fixture::new(); |
|
4370 |
+ |
let w = f.open(); |
|
4371 |
+ |
let gone = w.put(ObjectType::Chunk, b"gone").unwrap(); |
|
4372 |
+ |
let kept = w.put(ObjectType::Chunk, b"kept").unwrap(); |
|
4373 |
+ |
sign(&w, &gone, 1); |
|
4374 |
+ |
let s = sign(&w, &kept, 2); |
|
4375 |
+ |
assert!(fold(&w)); |
|
4376 |
+ |
let keep: std::collections::HashSet<ObjectRef> = w |
|
4377 |
+ |
.list() |
|
4378 |
+ |
.unwrap() |
|
4379 |
+ |
.into_iter() |
|
4380 |
+ |
.filter(|r| *r != gone) |
|
4381 |
+ |
.collect(); |
|
4382 |
+ |
w.prune(&keep, std::time::Duration::ZERO).unwrap(); |
|
4383 |
+ |
assert!(journal(&w).keys().all(|k| !k.contains(&gone.to_hex()[2..]))); |
|
4384 |
+ |
fold(&w); |
|
4385 |
+ |
let r = f.open(); |
|
4386 |
+ |
assert!(r.signatures(&gone).unwrap().is_empty()); |
|
4387 |
+ |
assert_eq!(r.signatures(&kept).unwrap(), vec![s]); |
|
4388 |
+ |
let all = r.with_entries(|e| e.clone()).unwrap().unwrap(); |
|
4389 |
+ |
assert!( |
|
4390 |
+ |
all.iter().all(|k| !k.contains(&gone.to_hex()[2..])), |
|
4391 |
+ |
"{all:?}" |
|
4392 |
+ |
); |
|
4393 |
+ |
} |
|
4394 |
+ |
|
|
4395 |
+ |
/// Requests per lookup, before and after the fold (#116): twenty |
|
4396 |
+ |
/// objects, five of them signed, each looked up for its signatures |
|
4397 |
+ |
/// and its seals, as a fetch does. |
|
4398 |
+ |
#[test] |
|
4399 |
+ |
fn a_folded_store_looks_up_entries_without_listing() { |
|
4400 |
+ |
let f = Fixture::new(); |
|
4401 |
+ |
let w = f.open(); |
|
4402 |
+ |
let refs: Vec<ObjectRef> = (0..20) |
|
4403 |
+ |
.map(|i| { |
|
4404 |
+ |
w.put(ObjectType::Chunk, format!("o {i}").as_bytes()) |
|
4405 |
+ |
.unwrap() |
|
4406 |
+ |
}) |
|
4407 |
+ |
.collect(); |
|
4408 |
+ |
for r in &refs[..5] { |
|
4409 |
+ |
sign(&w, r, 1); |
|
4410 |
+ |
} |
|
4411 |
+ |
let lookups = |s: &Store| { |
|
4412 |
+ |
for r in &refs { |
|
4413 |
+ |
s.signatures(r).unwrap(); |
|
4414 |
+ |
s.seals_for(r).unwrap(); |
|
4415 |
+ |
} |
|
4416 |
+ |
}; |
|
4417 |
+ |
let before = f.open(); |
|
4418 |
+ |
f.backend.reset(); |
|
4419 |
+ |
lookups(&before); |
|
4420 |
+ |
let (before_total, before_list) = (f.backend.total(), f.backend.n("list")); |
|
4421 |
+ |
eprintln!("40 lookups before the fold: {}", f.backend.summary()); |
|
4422 |
+ |
|
|
4423 |
+ |
assert!(fold(&w)); |
|
4424 |
+ |
let after = f.open(); |
|
4425 |
+ |
f.backend.reset(); |
|
4426 |
+ |
lookups(&after); |
|
4427 |
+ |
eprintln!("40 lookups after the fold: {}", f.backend.summary()); |
|
4428 |
+ |
assert_eq!(before_list, 40); |
|
4429 |
+ |
assert_eq!(f.backend.n("list"), 0); |
|
4430 |
+ |
assert_eq!(f.backend.n("read_journal"), 1); |
|
4431 |
+ |
let legacy = |b: &Counted| b.n("get_legacy") + b.n("list_legacy"); |
|
4432 |
+ |
assert!( |
|
4433 |
+ |
f.backend.total() - legacy(&f.backend) < (before_total - 40) / 4, |
|
4434 |
+ |
"{}", |
|
4435 |
+ |
f.backend.summary() |
|
4436 |
+ |
); |
| 3555 |
4437 |
|
} |
| 3556 |
4438 |
|
} |