OXYGENHUB
# ef7d4ea6f33b oxygen / crates / oxy-core / src / store.rs
168.8 KiB · Rust · last changed Signatures and index entries from a journal and entry lists, not a LIST per lookup 17 days ago source blame history raw
64818938 Andy McCall ↶ 1
//! Object store (spec §13.1, §13.3, §13.4): loose objects, one per key
2
//! under `loose/<hh>/<rest>`, and packs, `pack-<id>.pack` with a sorted
3
//! `pack-<id>.idx`, holding zstd-compressed objects. Reads check loose
4
//! objects first, then every pack; every read re-hashes.
d02e545d Andy McCall ↶ 5
//!
64818938 Andy McCall ↶ 6
//! Everything is kept on a [`crate::strata::Store`] (docs/stores.md): a
7
//! directory, which is what `.oxy/objects` is, or a bucket, which is how a
8
//! repository lives in its owner's region with the hub as one client of
9
//! it. The keys are the same wherever the store is, so a bucket holds
10
//! exactly what a directory would, and a directory store is as fast as
11
//! the files were before there was a trait: a loose object is one file, a
12
//! packed one is one ranged read of the pack. What must be local, the
13
//! pack being streamed out and the derived `metas` cache, lives in a
14
//! scratch directory.
15
//!
d02e545d Andy McCall ↶ 16
//! Pack file: `OXYPACK1` then entries of
17
//! `type u8 | flags u8 (bit 0: zstd) | stored_len u32 | raw_len u32 | ref 32 | body`.
18
//! Index file: `OXYIDX01` then `count u32` then sorted entries of
19
//! `ref 32 | offset u64 | stored_len u32 | raw_len u32 | type u8 | flags u8`.
64818938 Andy McCall ↶ 20
//!
21
//! Beside the objects: `nocompress/<hex>` marks a chunk packs keep raw;
22
//! `sig/<hh>/<rest>/<key>` holds one signature over an object, written
23
//! once per signer; `seal/<hh>/<rest>/<attest>` and
24
//! `receipt/<hh>/<rest>/<attest>` index the seals covering a change and
25
//! the receipts on a state. Older stores hold these as one list per
26
//! object (`sigs/`, `seals/`, `receipts/`), which are still read.
767ac6cd Andy McCall 27
//! `pack-retired` records the packs and loose objects a repack replaced,
28
//! which stay for readers until a collection an hour later (oxygenhub#249).
ef7d4ea6 Andy McCall ↶ 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).
ff0af1ee unknown 34
5b9c6ad4 Andy McCall ↶ 35
use crate::attest::Attest;
ff0af1ee unknown 36
use crate::hash::{hash_object, header, ObjectRef, ObjectType};
1983686f Andy McCall ↶ 37
use crate::identity::{Identity, Signature};
6f8aeb99 Andy McCall ↶ 38
use crate::objects::{Blob, Change, ObjectError, StateObject};
fc1a466a Andy McCall ↶ 39
use crate::receipt::Receipt;
64818938 Andy McCall ↶ 40
use crate::strata::{self, Error as BackendError, LocalStore, MemoryStore};
ff0af1ee unknown 41
use rand::RngCore;
d02e545d Andy McCall ↶ 42
use std::cell::RefCell;
ff0af1ee unknown 43
use std::fs;
64818938 Andy McCall ↶ 44
use std::io::{self, Write};
ff0af1ee unknown 45
use std::path::{Path, PathBuf};
64818938 Andy McCall ↶ 46
use std::sync::Arc;
ff0af1ee unknown 47
d02e545d Andy McCall ↶ 48
const PACK_MAGIC: &[u8; 8] = b"OXYPACK1";
49
const IDX_MAGIC: &[u8; 8] = b"OXYIDX01";
50
const IDX_ENTRY: usize = 32 + 8 + 4 + 4 + 1 + 1;
51
const FLAG_ZSTD: u8 = 1;
a17fc622 Andy McCall ↶ 52
/// Compressed with the pack's trained dictionary (`pack-ID.dict`), which a
53
/// full repack makes from a sample of the objects it packs: small files
54
/// share most of their vocabulary, which each one alone cannot use.
55
const FLAG_DICT: u8 = 2;
56
/// The size of a pack's dictionary, and the least a repack must pack for
57
/// one to pay.
58
const DICT_BYTES: usize = 110 << 10;
59
const DICT_MIN_OBJECTS: usize = 1000;
60
/// Objects up to this size are compressed with the dictionary.
61
const DICT_OBJECT_MAX: usize = 64 << 10;
d02e545d Andy McCall ↶ 62
const ZSTD_LEVEL: i32 = 3;
da652d63 Andy McCall ↶ 63
/// The level of a full repack (`oxy gc`): slower, once, for a smaller store.
64
pub const REPACK_ZSTD_LEVEL: i32 = 9;
2b872d07 Andy McCall ↶ 65
/// Largest object a pack entry may claim; anything bigger is corruption,
66
/// and refusing it keeps a bad index from driving huge allocations.
67
const MAX_OBJECT_LEN: u32 = 1 << 30;
767ac6cd Andy McCall 68
/// What reading an entry of a pack whose dictionary is gone says. A pack
69
/// being deleted can lose its dictionary first, so a read that meets this
70
/// looks at the packs again before it is believed.
71
const NO_DICT: &str = "packed with a dictionary the pack lacks";
d02e545d Andy McCall ↶ 72
767ac6cd Andy McCall 73
/// The record of what repacks replaced (oxygenhub#249): packs and loose
74
/// objects whose contents a newer pack holds, kept for readers that
75
/// still use them until garbage collection deletes them, at least
76
/// [`RETIRED_FOR`] later. One line per key, `<retired at, wall ms> <key>`.
77
/// It is a reference record (updated by compare-and-swap, so concurrent
78
/// repacks and collections never lose each other's lines), named under
79
/// `pack-` so the pack listing every open makes shows whether it holds
80
/// anything.
81
const RETIRED_KEY: &str = "pack-retired";
82
/// How long a retired pack or loose object stays in the store: a reader
83
/// that loaded its list of packs before a repack reads on from the old
84
/// packs this long, and after that finds the objects in the new pack by
85
/// looking again (see [`Store::get`]).
86
pub const RETIRED_FOR: std::time::Duration = std::time::Duration::from_secs(60 * 60);
87
/// Compare-and-swap rounds for the retired record before giving up.
88
const RETIRED_TRIES: usize = 32;
89
/// After a look at the pack listing found nothing new, how long a missed
90
/// [`Store::has`] answers without looking again: a writer probing for
91
/// thousands of objects it is about to store would otherwise list the
92
/// packs for each.
93
const HAS_RELIST_AFTER: std::time::Duration = std::time::Duration::from_secs(1);
ef7d4ea6 Andy McCall ↶ 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
];
767ac6cd Andy McCall 124
125
/// A pack listing as compared: each key with its size and time.
126
type Listed = Vec<(String, u64, Option<u64>)>;
127
128
/// The retired record's lines: each key with the time it was retired.
129
fn parse_retired(bytes: &[u8]) -> std::collections::BTreeMap<String, u64> {
130
    String::from_utf8_lossy(bytes)
131
        .lines()
132
        .filter_map(|l| {
133
            let (t, k) = l.split_once(' ')?;
134
            Some((k.to_string(), t.parse().ok()?))
135
        })
136
        .collect()
137
}
138
139
fn encode_retired(m: &std::collections::BTreeMap<String, u64>) -> Vec<u8> {
140
    m.iter()
141
        .map(|(k, t)| format!("{t} {k}\n"))
142
        .collect::<String>()
143
        .into_bytes()
144
}
145
146
/// What deleting retired packs and loose objects did.
147
#[derive(Debug, Clone, Default, PartialEq, Eq)]
148
pub struct RetiredReport {
149
    /// Retired packs deleted, each with its index and dictionary.
150
    pub packs: usize,
151
    /// Retired loose objects deleted.
152
    pub loose: usize,
153
    /// Retired entries younger than [`RETIRED_FOR`], left for the next
154
    /// collection.
155
    pub kept: usize,
156
}
157
afaba5f1 Andy McCall ↶ 158
/// An object read from the store: its type and body.
159
pub type Object = (ObjectType, Vec<u8>);
160
9ae4d15a Andy McCall ↶ 161
/// Where a long operation reports what it is doing (oxygen#66): a phase,
162
/// or a count within one, as a line for a spinner or a log.
163
pub type Progress<'a> = &'a mut dyn FnMut(&str);
64818938 Andy McCall ↶ 164
165
/// The storage under a store: objects by key, see [`crate::strata`].
166
pub type Backend = dyn strata::Store;
9ae4d15a Andy McCall ↶ 167
168
fn silent() -> impl FnMut(&str) {
169
    |_| {}
64818938 Andy McCall ↶ 170
}
171
172
/// A backend error as a store error: what is not found or already there
173
/// is handled where it matters; the rest is an I/O failure.
174
fn backend_err(e: BackendError) -> StoreError {
175
    match e {
176
        BackendError::Io(e) => StoreError::Io(e),
177
        other => StoreError::Io(io::Error::other(other.to_string())),
178
    }
179
}
180
181
/// `put_new` where an object already there is what we wanted.
182
fn put_new_or_existing(backend: &Backend, key: &str, bytes: &[u8]) -> Result<()> {
183
    match backend.put_new(key, bytes) {
184
        Ok(()) | Err(BackendError::Exists(_)) => Ok(()),
185
        Err(e) => Err(backend_err(e)),
186
    }
187
}
188
189
/// `get` as an option: `None` when there is no such key.
190
fn get_optional(backend: &Backend, key: &str) -> Result<Option<Vec<u8>>> {
191
    match backend.get(key) {
192
        Ok(b) => Ok(Some(b)),
193
        Err(BackendError::NotFound(_)) => Ok(None),
194
        Err(e) => Err(backend_err(e)),
195
    }
196
}
197
198
fn delete(backend: &Backend, key: &str) -> Result<()> {
199
    backend.delete(key).map_err(backend_err)
200
}
201
202
/// `loose/<hh>/<rest>`.
203
fn loose_key(r: &ObjectRef) -> String {
204
    let h = r.to_hex();
205
    format!("loose/{}/{}", &h[..2], &h[2..])
206
}
207
208
/// The object a loose key names, if it is one.
209
fn loose_ref(key: &str) -> Option<ObjectRef> {
210
    let rest = key.strip_prefix("loose/")?;
211
    let (hh, tail) = rest.split_once('/')?;
212
    if hh.len() != 2 || tail.contains('/') {
213
        return None;
214
    }
215
    ObjectRef::from_hex(&format!("{hh}{tail}"))
216
}
217
218
/// `<index>/<hh>/<rest>/` for the entries kept under an object.
219
fn fanout_prefix(index: &str, r: &ObjectRef) -> String {
220
    let h = r.to_hex();
221
    format!("{index}/{}/{}/", &h[..2], &h[2..])
ef7d4ea6 Andy McCall ↶ 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())
64818938 Andy McCall ↶ 254
}
255
ef7d4ea6 Andy McCall ↶ 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
64818938 Andy McCall ↶ 263
/// `<index>/<hh>/<rest>`: the one-file-per-object layout of older stores.
264
fn legacy_key(index: &str, r: &ObjectRef) -> String {
265
    let h = r.to_hex();
266
    format!("{index}/{}/{}", &h[..2], &h[2..])
9ae4d15a Andy McCall ↶ 267
}
268
64818938 Andy McCall ↶ 269
fn now_ms() -> u64 {
270
    std::time::SystemTime::now()
271
        .duration_since(std::time::UNIX_EPOCH)
272
        .map(|d| d.as_millis() as u64)
273
        .unwrap_or(0)
274
}
275
d02e545d Andy McCall ↶ 276
#[derive(Clone, Copy, Debug)]
277
struct PackEntry {
278
    offset: u64,
279
    stored_len: u32,
280
    raw_len: u32,
281
    ty: ObjectType,
282
    flags: u8,
283
}
284
285
/// One pack's index, fully in memory and sorted by ObjectRef.
286
struct PackIndex {
64818938 Andy McCall ↶ 287
    /// The pack's key, `pack-<id>.pack`.
288
    key: String,
d02e545d Andy McCall ↶ 289
    refs: Vec<ObjectRef>,
290
    entries: Vec<PackEntry>,
a17fc622 Andy McCall ↶ 291
    /// The pack's dictionary, prepared once, when it has one.
64818938 Andy McCall ↶ 292
    dict: Option<Arc<zstd::dict::DecoderDictionary<'static>>>,
d02e545d Andy McCall ↶ 293
}
294
a17fc622 Andy McCall ↶ 295
impl std::fmt::Debug for PackIndex {
296
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297
        f.debug_struct("PackIndex")
64818938 Andy McCall ↶ 298
            .field("key", &self.key)
a17fc622 Andy McCall ↶ 299
            .field("objects", &self.refs.len())
300
            .field("dict", &self.dict.is_some())
301
            .finish()
64818938 Andy McCall ↶ 302
    }
303
}
304
305
/// Decode a stored pack entry and check it against its reference.
306
fn decode_entry(
307
    r: &ObjectRef,
308
    e: PackEntry,
309
    stored: Vec<u8>,
310
    dict: Option<&Arc<zstd::dict::DecoderDictionary<'static>>>,
311
) -> Result<Object> {
312
    let body = if e.flags & FLAG_DICT != 0 {
767ac6cd Andy McCall 313
        let dict = dict.ok_or_else(|| StoreError::Corrupt(*r, NO_DICT.into()))?;
64818938 Andy McCall ↶ 314
        zstd::bulk::Decompressor::with_prepared_dictionary(dict)
315
            .and_then(|mut d| d.decompress(&stored, e.raw_len as usize))
316
            .map_err(|err| StoreError::Corrupt(*r, format!("zstd: {err}")))?
317
    } else if e.flags & FLAG_ZSTD != 0 {
318
        zstd::bulk::decompress(&stored, e.raw_len as usize)
319
            .map_err(|err| StoreError::Corrupt(*r, format!("zstd: {err}")))?
320
    } else {
321
        stored
322
    };
323
    if body.len() != e.raw_len as usize || hash_object(e.ty, &body) != *r {
324
        return Err(StoreError::Corrupt(
325
            *r,
326
            "packed object does not match its reference".into(),
327
        ));
a17fc622 Andy McCall ↶ 328
    }
64818938 Andy McCall ↶ 329
    Ok((e.ty, body))
a17fc622 Andy McCall ↶ 330
}
331
d02e545d Andy McCall ↶ 332
impl PackIndex {
7e0e8e5a Andy McCall ↶ 333
    /// Load a pack's index; `pack_len` is the pack's size, from the listing
767ac6cd Andy McCall 334
    /// that found it (a stat of it would be a request of its own). `None`
335
    /// when the index is gone since the listing: the pack was deleted.
336
    fn load(
337
        backend: &Backend,
338
        idx_key: &str,
339
        pack_key: &str,
340
        pack_len: u64,
341
    ) -> Result<Option<PackIndex>> {
342
        let data = match backend.get(idx_key) {
343
            Ok(d) => d,
344
            Err(BackendError::NotFound(_)) => return Ok(None),
345
            Err(e) => return Err(backend_err(e)),
346
        };
64818938 Andy McCall ↶ 347
        let bad = |m: &str| StoreError::Corrupt(ObjectRef::ZERO, format!("{idx_key}: {m}"));
d02e545d Andy McCall ↶ 348
        if data.len() < 12 || &data[..8] != IDX_MAGIC {
349
            return Err(bad("bad index header"));
350
        }
351
        let count = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize;
352
        if data.len() != 12 + count * IDX_ENTRY {
353
            return Err(bad("index length mismatch"));
354
        }
355
        let mut refs = Vec::with_capacity(count);
356
        let mut entries = Vec::with_capacity(count);
357
        for i in 0..count {
358
            let e = &data[12 + i * IDX_ENTRY..12 + (i + 1) * IDX_ENTRY];
359
            let r = ObjectRef::from_slice(&e[..32]).unwrap();
360
            if let Some(prev) = refs.last() {
361
                if r <= *prev {
362
                    return Err(bad("index not sorted"));
363
                }
364
            }
365
            refs.push(r);
2b872d07 Andy McCall ↶ 366
            let entry = PackEntry {
d02e545d Andy McCall ↶ 367
                offset: u64::from_le_bytes(e[32..40].try_into().unwrap()),
368
                stored_len: u32::from_le_bytes(e[40..44].try_into().unwrap()),
369
                raw_len: u32::from_le_bytes(e[44..48].try_into().unwrap()),
75167672 Andy McCall 370
                ty: ObjectType::ALL
371
                    .get(e[48] as usize)
372
                    .copied()
373
                    .ok_or_else(|| bad("unknown object type"))?,
d02e545d Andy McCall ↶ 374
                flags: e[49],
2b872d07 Andy McCall ↶ 375
            };
a9f686ed Andy McCall ↶ 376
            if entry.stored_len > MAX_OBJECT_LEN || entry.raw_len > MAX_OBJECT_LEN {
377
                return Err(StoreError::Corrupt(
378
                    r,
379
                    format!(
64818938 Andy McCall ↶ 380
                        "{idx_key}: the index claims an object of {} bytes ({} stored), more than the {MAX_OBJECT_LEN} an object may be",
381
                        entry.raw_len, entry.stored_len
a9f686ed Andy McCall ↶ 382
                    ),
383
                ));
384
            }
385
            if entry.offset.saturating_add(entry.stored_len as u64) > pack_len {
2b872d07 Andy McCall ↶ 386
                return Err(bad("index entry points outside the pack"));
387
            }
388
            entries.push(entry);
d02e545d Andy McCall ↶ 389
        }
64818938 Andy McCall ↶ 390
        let dict_key = format!("{}.dict", pack_key.trim_end_matches(".pack"));
391
        let dict = get_optional(backend, &dict_key)?
392
            .map(|d| Arc::new(zstd::dict::DecoderDictionary::copy(&d)));
767ac6cd Andy McCall 393
        Ok(Some(PackIndex {
64818938 Andy McCall ↶ 394
            key: pack_key.to_string(),
75167672 Andy McCall 395
            refs,
396
            entries,
a17fc622 Andy McCall ↶ 397
            dict,
767ac6cd Andy McCall 398
        }))
d02e545d Andy McCall ↶ 399
    }
400
401
    fn find(&self, r: &ObjectRef) -> Option<PackEntry> {
402
        self.refs.binary_search(r).ok().map(|i| self.entries[i])
403
    }
404
64818938 Andy McCall ↶ 405
    /// Read one entry from the pack: one ranged read, then decode.
406
    fn fetch(&self, backend: &Backend, r: &ObjectRef, e: PackEntry) -> Result<Object> {
407
        let stored = backend
408
            .get_range(&self.key, e.offset..e.offset + e.stored_len as u64)
409
            .map_err(|err| match err {
410
                BackendError::NotFound(_) => StoreError::NotFound(*r),
411
                other => backend_err(other),
a17fc622 Andy McCall ↶ 412
            })?;
64818938 Andy McCall ↶ 413
        decode_entry(r, e, stored, self.dict.as_ref())
d02e545d Andy McCall ↶ 414
    }
415
}
416
417
fn type_index(ty: ObjectType) -> u8 {
418
    ObjectType::ALL.iter().position(|t| *t == ty).unwrap() as u8
419
}
420
421
/// What `pack_loose` did.
422
#[derive(Debug, Clone, Default, PartialEq, Eq)]
423
pub struct PackReport {
424
    pub objects: usize,
425
    pub raw_bytes: u64,
426
    pub packed_bytes: u64,
427
}
428
135212de Andy McCall ↶ 429
/// Packs tolerated before `merge_packs` folds them into one (spec §13.4).
430
pub const MAX_PACKS: usize = 16;
431
ba160f60 Andy McCall ↶ 432
/// What `prune` did.
433
#[derive(Debug, Clone, Default, PartialEq, Eq)]
434
pub struct PruneReport {
435
    pub objects: usize,
436
    pub bytes: u64,
437
    /// Unreachable objects left alone because they are younger than the grace period.
438
    pub deferred: usize,
439
}
440
ff0af1ee unknown 441
#[derive(Debug, thiserror::Error)]
442
pub enum StoreError {
443
    #[error("io: {0}")]
444
    Io(#[from] io::Error),
445
    #[error("corrupt object {0}: {1}")]
446
    Corrupt(ObjectRef, String),
447
    #[error("object {0}")]
448
    Object(#[from] ObjectError),
449
    #[error("object {0} is a {1}, expected {2}")]
450
    WrongType(ObjectRef, &'static str, &'static str),
451
    #[error("object {0} not found")]
452
    NotFound(ObjectRef),
6f8aeb99 Andy McCall ↶ 453
    #[error("a {kind} object of {size} bytes is larger than the {limit} bytes an object may be; nothing was stored. A change this large adds or touches too many files at once: commit it in parts (a directory at a time)")]
a9f686ed Andy McCall ↶ 454
    TooLarge {
455
        kind: &'static str,
456
        size: usize,
457
        limit: u32,
458
    },
ff0af1ee unknown 459
}
460
461
pub type Result<T> = std::result::Result<T, StoreError>;
462
463
pub struct Store {
64818938 Andy McCall ↶ 464
    backend: Arc<Backend>,
465
    /// Temporary files (a pack being streamed) and derived caches (`metas`).
466
    /// For a store in a directory it is that directory.
467
    scratch: PathBuf,
468
    /// The backend is that directory, so the old file paths still hold.
469
    local: bool,
767ac6cd Andy McCall 470
    packs: RefCell<Vec<Arc<PackIndex>>>,
471
    /// The pack listing `packs` was loaded from (key, size, time), to tell
472
    /// whether a look at the store would find anything new.
473
    packs_listed: RefCell<Listed>,
474
    /// When a look at the pack listing for a missed [`Store::has`] last
475
    /// found nothing new.
476
    has_relisted: std::cell::Cell<Option<std::time::Instant>>,
8f10a07a Andy McCall ↶ 477
    /// An open write batch (spec §13.4): objects go here instead of to loose
478
    /// files, and are written out with one `fsync` when the batch finishes.
479
    pending: RefCell<Option<Pending>>,
480
    batch_depth: std::cell::Cell<u32>,
ce14f0d6 Andy McCall ↶ 481
    /// Change metadata already read, from the `metas` cache file.
482
    metas: RefCell<Option<crate::fasthash::FastMap<ObjectRef, crate::state::ChangeMeta>>>,
ef7d4ea6 Andy McCall ↶ 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>>,
8f10a07a Andy McCall ↶ 496
}
497
64818938 Andy McCall ↶ 498
impl std::fmt::Debug for Store {
499
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
500
        f.debug_struct("Store")
501
            .field("backend", &self.backend.describe())
502
            .field("scratch", &self.scratch)
503
            .field("packs", &self.packs.borrow().len())
504
            .finish()
505
    }
506
}
507
8f10a07a Andy McCall ↶ 508
/// Objects written since `begin_batch`. Small batches stay in memory and
509
/// become loose objects; once `SPILL_OBJECTS` or `SPILL_BYTES` is exceeded
510
/// the batch streams into a pack file instead.
511
#[derive(Debug)]
512
struct Pending {
513
    buffered: Vec<(ObjectType, Vec<u8>)>,
514
    buffered_bytes: usize,
515
    lookup: std::collections::HashMap<ObjectRef, PendingEntry>,
516
    pack: Option<PendingPack>,
517
    report: PackReport,
ff0af1ee unknown 518
}
519
8f10a07a Andy McCall ↶ 520
#[derive(Debug, Clone, Copy)]
521
enum PendingEntry {
522
    Buffered(usize),
523
    Packed(PackEntry),
524
}
525
526
#[derive(Debug)]
527
struct PendingPack {
528
    id: String,
529
    tmp_path: PathBuf,
530
    writer: io::BufWriter<fs::File>,
531
    offset: u64,
532
    index: Vec<(ObjectRef, PackEntry)>,
533
}
534
535
const SPILL_OBJECTS: usize = 256;
536
const SPILL_BYTES: usize = 8 << 20;
537
538
/// Finishes the enclosing batch when dropped; call `finish` to see errors.
539
pub struct Batch<'a> {
540
    store: &'a Store,
541
    done: bool,
542
}
543
544
impl Batch<'_> {
545
    pub fn finish(mut self) -> Result<()> {
546
        self.done = true;
547
        self.store.finish_batch()
548
    }
549
}
550
551
impl Drop for Batch<'_> {
552
    fn drop(&mut self) {
553
        if !self.done {
554
            let _ = self.store.finish_batch();
555
        }
556
    }
557
}
558
192fad1e Andy McCall ↶ 559
/// Append one stored object to a pending pack and index it.
560
fn write_pack_entry(
561
    pack: &mut PendingPack,
562
    r: &ObjectRef,
563
    ty: ObjectType,
564
    raw_len: usize,
565
    flags: u8,
566
    stored: &[u8],
567
    report: &mut PackReport,
568
) -> Result<PackEntry> {
569
    let header_len = 1 + 1 + 4 + 4 + 32;
570
    pack.writer.write_all(&[type_index(ty), flags])?;
571
    pack.writer
572
        .write_all(&(stored.len() as u32).to_le_bytes())?;
573
    pack.writer.write_all(&(raw_len as u32).to_le_bytes())?;
574
    pack.writer.write_all(&r.0)?;
575
    pack.writer.write_all(stored)?;
576
    let entry = PackEntry {
577
        offset: pack.offset + header_len,
578
        stored_len: stored.len() as u32,
579
        raw_len: raw_len as u32,
580
        ty,
581
        flags,
582
    };
583
    pack.offset += header_len + stored.len() as u64;
584
    pack.index.push((*r, entry));
585
    report.objects += 1;
586
    report.raw_bytes += raw_len as u64;
587
    report.packed_bytes += stored.len() as u64;
588
    Ok(entry)
64818938 Andy McCall ↶ 589
}
590
591
/// A random name for a temporary file or a pack.
592
fn fresh_id() -> String {
593
    let mut nonce = [0u8; 8];
594
    rand::rngs::OsRng.fill_bytes(&mut nonce);
595
    hex::encode(nonce)
596
}
597
598
/// Under `OXY_STORE_BACKEND=memory` every store opens on a store in
599
/// memory shared by directory, so the whole test suite can run without a
600
/// directory store, as it would over a bucket. Objects live for the
601
/// process.
602
fn memory_backend_for(dir: &Path) -> Option<Arc<Backend>> {
603
    use std::collections::HashMap;
604
    use std::sync::{Mutex, OnceLock};
605
    if std::env::var_os("OXY_STORE_BACKEND").as_deref() != Some(std::ffi::OsStr::new("memory")) {
606
        return None;
607
    }
608
    static STORES: OnceLock<Mutex<HashMap<PathBuf, Arc<MemoryStore>>>> = OnceLock::new();
609
    let key = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
610
    let mut map = STORES
611
        .get_or_init(Default::default)
612
        .lock()
613
        .unwrap_or_else(|p| p.into_inner());
614
    let store = map
615
        .entry(key)
616
        .or_insert_with(|| Arc::new(MemoryStore::new()))
617
        .clone();
618
    Some(store)
192fad1e Andy McCall ↶ 619
}
620
dd21fd82 Andy McCall ↶ 621
#[cfg(test)]
622
thread_local! {
623
    /// A backend every store opened on this thread uses instead of its
624
    /// directory: tests of a repository kept on a store that is not this
625
    /// disk's directory, with several handles on one store.
626
    pub(crate) static TEST_BACKEND: RefCell<Option<Arc<Backend>>> = const { RefCell::new(None) };
627
}
628
ff0af1ee unknown 629
impl Store {
64818938 Andy McCall ↶ 630
    /// Open the store in `dir`, which is also its scratch directory.
ff0af1ee unknown 631
    pub fn open(dir: impl Into<PathBuf>) -> io::Result<Store> {
632
        let dir = dir.into();
633
        fs::create_dir_all(dir.join("loose"))?;
dd21fd82 Andy McCall ↶ 634
        #[cfg(test)]
635
        if let Some(backend) = TEST_BACKEND.with(|t| t.borrow().clone()) {
636
            return Store::open_on(backend, dir);
637
        }
64818938 Andy McCall ↶ 638
        if let Some(backend) = memory_backend_for(&dir) {
639
            return Store::open_on(backend, dir);
640
        }
641
        let backend: Arc<Backend> =
642
            Arc::new(LocalStore::open(&dir).map_err(|e| io::Error::other(e.to_string()))?);
643
        let mut store = Store::open_on(backend, dir)?;
644
        store.local = true;
645
        Ok(store)
646
    }
647
648
    /// Open the store kept on `backend`, with `scratch` for what must be
649
    /// on this disk: a pack while it is being written, and caches.
650
    pub fn open_on(backend: Arc<Backend>, scratch: impl Into<PathBuf>) -> io::Result<Store> {
651
        let scratch = scratch.into();
652
        fs::create_dir_all(&scratch)?;
75167672 Andy McCall 653
        let store = Store {
64818938 Andy McCall ↶ 654
            backend,
655
            scratch,
656
            local: false,
75167672 Andy McCall 657
            packs: RefCell::new(Vec::new()),
767ac6cd Andy McCall 658
            packs_listed: RefCell::new(Vec::new()),
659
            has_relisted: std::cell::Cell::new(None),
75167672 Andy McCall 660
            pending: RefCell::new(None),
661
            batch_depth: std::cell::Cell::new(0),
ce14f0d6 Andy McCall ↶ 662
            metas: RefCell::new(None),
ef7d4ea6 Andy McCall ↶ 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()),
75167672 Andy McCall 668
        };
669
        store
670
            .reload_packs()
671
            .map_err(|e| io::Error::other(e.to_string()))?;
d02e545d Andy McCall ↶ 672
        Ok(store)
64818938 Andy McCall ↶ 673
    }
674
675
    /// The storage under this store.
676
    pub fn backend(&self) -> &Arc<Backend> {
677
        &self.backend
d02e545d Andy McCall ↶ 678
    }
679
64818938 Andy McCall ↶ 680
    /// Whether the store is the directory `dir()` names, where every key
681
    /// is a file.
682
    pub fn is_local(&self) -> bool {
683
        self.local
767ac6cd Andy McCall 684
    }
685
686
    /// Load the list of packs again, from a new listing: what other
687
    /// writers packed since becomes readable, and packs since retired or
688
    /// deleted are let go. Packs already loaded are kept, not read again.
689
    pub fn refresh(&self) -> Result<()> {
ef7d4ea6 Andy McCall ↶ 690
        *self.entries.borrow_mut() = None;
767ac6cd Andy McCall 691
        self.reload_packs()
692
    }
693
694
    /// The pack listing, with how `packs_listed` records it.
695
    fn list_packs(&self) -> Result<(Vec<strata::Entry>, Listed)> {
696
        let listed = self.backend.list("pack-").map_err(backend_err)?;
697
        let sig = listed
698
            .iter()
699
            .map(|e| (e.key.clone(), e.size, e.modified_ms))
700
            .collect();
701
        Ok((listed, sig))
64818938 Andy McCall ↶ 702
    }
703
767ac6cd Andy McCall 704
    /// Load every pack the listing names that is not retired. An index
705
    /// that is gone by the time it is read (its pack deleted after the
706
    /// listing) is passed over, and the store listed once more, since
707
    /// what replaced it may be new since the first listing.
d02e545d Andy McCall ↶ 708
    fn reload_packs(&self) -> Result<()> {
767ac6cd Andy McCall 709
        let (listed, sig) = self.list_packs()?;
710
        self.load_listed(listed, sig)
711
    }
712
713
    fn load_listed(&self, mut listed: Vec<strata::Entry>, mut sig: Listed) -> Result<()> {
714
        let had: std::collections::HashMap<String, Arc<PackIndex>> = self
715
            .packs
716
            .borrow()
717
            .iter()
718
            .map(|p| (p.key.clone(), p.clone()))
719
            .collect();
720
        let mut loaded: std::collections::HashMap<String, Arc<PackIndex>> = Default::default();
721
        for attempt in 0..2 {
722
            let retired = self.retired_in(&listed)?;
723
            let mut idx_keys: Vec<&str> = listed
724
                .iter()
725
                .map(|e| e.key.as_str())
726
                .filter(|k| k.ends_with(".idx"))
727
                .collect();
728
            idx_keys.sort();
729
            let mut packs = Vec::new();
730
            let mut vanished = false;
731
            for idx in idx_keys {
732
                let pack = format!("{}.pack", idx.trim_end_matches(".idx"));
733
                if retired.contains_key(&pack) {
734
                    continue;
735
                }
736
                let Some(p) = listed.iter().find(|e| e.key == pack) else {
737
                    continue;
738
                };
739
                if let Some(kept) = had.get(&pack).or_else(|| loaded.get(&pack)) {
740
                    packs.push(kept.clone());
741
                    continue;
742
                }
743
                match PackIndex::load(self.backend.as_ref(), idx, &pack, p.size)? {
744
                    Some(index) => {
745
                        let index = Arc::new(index);
746
                        loaded.insert(pack, index.clone());
747
                        packs.push(index);
748
                    }
749
                    None => vanished = true,
750
                }
751
            }
752
            if vanished && attempt == 0 {
753
                (listed, sig) = self.list_packs()?;
754
                continue;
755
            }
ef7d4ea6 Andy McCall ↶ 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
            }
767ac6cd Andy McCall 770
            *self.packs.borrow_mut() = packs;
771
            *self.packs_listed.borrow_mut() = sig;
772
            return Ok(());
773
        }
774
        unreachable!("the second round always returns")
775
    }
776
777
    /// Load the packs again when the pack listing differs from the one
778
    /// they were loaded from; whether it did. What a miss calls before it
779
    /// is believed (oxygenhub#249): another writer may have packed the
780
    /// object, or a repack moved it, since this store loaded its packs.
781
    fn reload_if_changed(&self) -> Result<bool> {
782
        let (listed, sig) = self.list_packs()?;
783
        if *self.packs_listed.borrow() == sig {
784
            return Ok(false);
785
        }
786
        self.load_listed(listed, sig)?;
787
        Ok(true)
788
    }
789
790
    /// The retired record, read when `listed` (a pack listing) shows it
791
    /// holds anything.
792
    fn retired_in(
793
        &self,
794
        listed: &[strata::Entry],
795
    ) -> Result<std::collections::BTreeMap<String, u64>> {
796
        match listed.iter().find(|e| e.key == RETIRED_KEY) {
797
            Some(e) if e.size > 0 => Ok(self.read_retired()?.0),
798
            _ => Ok(Default::default()),
799
        }
800
    }
801
802
    /// The retired record and its version (`None`: never written).
803
    fn read_retired(
ef7d4ea6 Andy McCall ↶ 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(
767ac6cd Andy McCall 815
        &self,
ef7d4ea6 Andy McCall ↶ 816
        key: &str,
767ac6cd Andy McCall 817
    ) -> Result<(
818
        std::collections::BTreeMap<String, u64>,
819
        Option<strata::Version>,
820
    )> {
ef7d4ea6 Andy McCall ↶ 821
        match self.backend.read_ref(key).map_err(backend_err)? {
767ac6cd Andy McCall 822
            Some((bytes, v)) => Ok((parse_retired(&bytes), Some(v))),
823
            None => Ok((Default::default(), None)),
824
        }
825
    }
826
827
    /// Change the retired record by compare-and-swap, reading it again
828
    /// whenever another writer changed it first. `edit` must give the
829
    /// same result however often it runs.
830
    fn update_retired(
ef7d4ea6 Andy McCall ↶ 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(
767ac6cd Andy McCall 841
        &self,
ef7d4ea6 Andy McCall ↶ 842
        key: &str,
843
        what: &str,
767ac6cd Andy McCall 844
        mut edit: impl FnMut(&mut std::collections::BTreeMap<String, u64>),
845
    ) -> Result<()> {
846
        for _ in 0..RETIRED_TRIES {
ef7d4ea6 Andy McCall ↶ 847
            let (mut m, version) = self.read_record(key)?;
767ac6cd Andy McCall 848
            let before = m.clone();
849
            edit(&mut m);
ef7d4ea6 Andy McCall ↶ 850
            // Unchanged; a record never written stays unwritten.
851
            if m == before && (version.is_some() || m.is_empty()) {
767ac6cd Andy McCall 852
                return Ok(());
853
            }
854
            match self
855
                .backend
ef7d4ea6 Andy McCall ↶ 856
                .write_ref(key, &encode_retired(&m), version.as_ref())
767ac6cd Andy McCall 857
            {
858
                Ok(_) => return Ok(()),
859
                Err(BackendError::Conflict(_)) => continue,
860
                Err(e) => return Err(backend_err(e)),
861
            }
862
        }
ef7d4ea6 Andy McCall ↶ 863
        Err(StoreError::Io(io::Error::other(format!(
864
            "other writers kept changing {what}; run the command again"
865
        ))))
767ac6cd Andy McCall 866
    }
867
868
    /// Record `keys` (packs, by their `.pack` key, and loose objects) as
869
    /// retired now: kept for the readers still using them, and deleted by
870
    /// a collection at least [`RETIRED_FOR`] later. A key already retired
871
    /// keeps its first time.
872
    fn retire(&self, keys: &[String]) -> Result<()> {
873
        if keys.is_empty() {
874
            return Ok(());
875
        }
876
        let now = now_ms();
877
        self.update_retired(|m| {
878
            for k in keys {
879
                m.entry(k.clone()).or_insert(now);
880
            }
881
        })
882
    }
883
884
    /// Delete what was retired at least `older_than` ago (a collection
885
    /// does, with [`RETIRED_FOR`]): each pack with its index and
886
    /// dictionary, and each loose object a pack now holds. A retired loose
887
    /// object no pack holds any more (the pack that had it was pruned, and
888
    /// the object stored again since) is kept, as an ordinary loose object.
889
    pub fn expire_retired(
890
        &self,
891
        older_than: std::time::Duration,
892
        progress: Progress,
893
    ) -> Result<RetiredReport> {
894
        let (listed, _) = self.list_packs()?;
895
        let retired = self.retired_in(&listed)?;
896
        let mut report = RetiredReport::default();
897
        if retired.is_empty() {
898
            return Ok(report);
899
        }
900
        let now = now_ms();
901
        let older_than = older_than.as_millis() as u64;
902
        let due: Vec<(String, u64)> = retired
64818938 Andy McCall ↶ 903
            .iter()
767ac6cd Andy McCall 904
            .filter(|(_, t)| now.saturating_sub(**t) >= older_than)
905
            .map(|(k, t)| (k.clone(), *t))
d02e545d Andy McCall ↶ 906
            .collect();
767ac6cd Andy McCall 907
        report.kept = retired.len() - due.len();
908
        if due.is_empty() {
909
            return Ok(report);
910
        }
911
        progress(&format!("deleting {} retired files", due.len()));
912
        // Which objects the live packs hold.
913
        self.reload_packs()?;
914
        for (key, _) in &due {
915
            if key.ends_with(".pack") {
916
                self.remove_pack(key)?;
917
                report.packs += 1;
ef7d4ea6 Andy McCall ↶ 918
            } else if is_entry_list(key) {
919
                // An entry list a fold replaced (spec §13.7).
920
                delete(self.backend.as_ref(), key)?;
767ac6cd Andy McCall 921
            } else if let Some(r) = loose_ref(key) {
922
                if self.find_packed(&r).is_some() {
923
                    delete(self.backend.as_ref(), key)?;
924
                    report.loose += 1;
925
                }
d02e545d Andy McCall ↶ 926
            }
927
        }
767ac6cd Andy McCall 928
        self.update_retired(|m| {
929
            for (k, t) in &due {
930
                if m.get(k) == Some(t) {
931
                    m.remove(k);
932
                }
933
            }
934
        })?;
935
        self.reload_packs()?;
936
        Ok(report)
d02e545d Andy McCall ↶ 937
    }
938
767ac6cd Andy McCall 939
    /// The loose objects retired and not yet deleted, by key.
940
    fn retired_loose(&self) -> Result<std::collections::HashSet<String>> {
941
        let (listed, _) = self.list_packs()?;
942
        Ok(self
943
            .retired_in(&listed)?
944
            .into_keys()
945
            .filter(|k| k.starts_with("loose/"))
946
            .collect())
947
    }
948
d02e545d Andy McCall ↶ 949
    pub fn pack_count(&self) -> usize {
950
        self.packs.borrow().len()
951
    }
952
953
    fn find_packed(&self, r: &ObjectRef) -> Option<(usize, PackEntry)> {
75167672 Andy McCall 954
        self.packs
955
            .borrow()
956
            .iter()
957
            .enumerate()
958
            .find_map(|(i, p)| p.find(r).map(|e| (i, e)))
d02e545d Andy McCall ↶ 959
    }
960
767ac6cd Andy McCall 961
    /// Loose objects, with their keys and ages; not those retired, whose
962
    /// objects a pack holds.
64818938 Andy McCall ↶ 963
    fn loose_objects(&self) -> Result<Vec<(ObjectRef, strata::Entry)>> {
767ac6cd Andy McCall 964
        let retired = self.retired_loose()?;
965
        let mut out = self.loose_objects_all()?;
966
        out.retain(|(_, e)| !retired.contains(&e.key));
967
        Ok(out)
968
    }
969
970
    /// Every loose object, retired or not.
971
    fn loose_objects_all(&self) -> Result<Vec<(ObjectRef, strata::Entry)>> {
64818938 Andy McCall ↶ 972
        let mut out: Vec<(ObjectRef, strata::Entry)> = self
973
            .backend
974
            .list("loose/")
975
            .map_err(backend_err)?
976
            .into_iter()
977
            .filter_map(|e| loose_ref(&e.key).map(|r| (r, e)))
978
            .collect();
979
        out.sort_by_key(|(r, _)| *r);
d02e545d Andy McCall ↶ 980
        Ok(out)
981
    }
982
983
    pub fn loose_count(&self) -> io::Result<usize> {
64818938 Andy McCall ↶ 984
        self.loose_objects()
985
            .map(|l| l.len())
986
            .map_err(|e| io::Error::other(e.to_string()))
d02e545d Andy McCall ↶ 987
    }
988
64818938 Andy McCall ↶ 989
    /// Move every loose object into a new pack (spec §13.4). Loose objects
990
    /// are deleted only after the pack and its index are durable.
d02e545d Andy McCall ↶ 991
    pub fn pack_loose(&self) -> Result<PackReport> {
992
        let loose = self.loose_objects()?;
75167672 Andy McCall 993
        self.write_pack(
994
            &loose.iter().map(|(r, _)| *r).collect::<Vec<_>>(),
64818938 Andy McCall ↶ 995
            &loose.iter().map(|(_, e)| e.key.clone()).collect::<Vec<_>>(),
75167672 Andy McCall 996
            &[],
da652d63 Andy McCall ↶ 997
            ZSTD_LEVEL,
9ae4d15a Andy McCall ↶ 998
            &mut silent(),
75167672 Andy McCall 999
        )
da652d63 Andy McCall ↶ 1000
    }
1001
1002
    /// Fold every pack and loose object into one pack compressed at
1003
    /// `level` (spec §13.4): what an explicit `oxy gc` does, as `git gc`
1004
    /// repacks. Returns the report and how many packs were folded in.
1005
    pub fn repack_all(&self, level: i32) -> Result<(PackReport, usize)> {
9ae4d15a Andy McCall ↶ 1006
        self.repack_all_with(level, &mut silent())
1007
    }
1008
1009
    /// [`Store::repack_all`], reporting progress.
1010
    pub fn repack_all_with(&self, level: i32, progress: Progress) -> Result<(PackReport, usize)> {
da652d63 Andy McCall ↶ 1011
        let loose = self.loose_objects()?;
a67914bc Andy McCall ↶ 1012
        let (mut refs, keys, trained): (Vec<ObjectRef>, Vec<String>, bool) = {
da652d63 Andy McCall ↶ 1013
            let packs = self.packs.borrow();
1014
            (
1015
                packs.iter().flat_map(|p| p.refs.iter().copied()).collect(),
64818938 Andy McCall ↶ 1016
                packs.iter().map(|p| p.key.clone()).collect(),
a67914bc Andy McCall ↶ 1017
                packs.first().is_some_and(|p| p.dict.is_some()),
da652d63 Andy McCall ↶ 1018
            )
1019
        };
a67914bc Andy McCall ↶ 1020
        // Nothing loose and one pack already made by a full repack (only
1021
        // such a pack has a trained dictionary): rewriting it would give
1022
        // the same pack. A hub's nightly gc rewrote andymccall/oxygen's
1023
        // 48 000 objects (37 s under its write lock) every time.
1024
        if loose.is_empty() && keys.len() <= 1 && (level == ZSTD_LEVEL || trained) {
da652d63 Andy McCall ↶ 1025
            return Ok((PackReport::default(), 0));
1026
        }
1027
        refs.extend(loose.iter().map(|(r, _)| *r));
64818938 Andy McCall ↶ 1028
        let loose_keys: Vec<String> = loose.into_iter().map(|(_, e)| e.key).collect();
1029
        let n = keys.len();
1030
        let report = self.write_pack(&refs, &loose_keys, &keys, level, progress)?;
da652d63 Andy McCall ↶ 1031
        Ok((report, n))
ba160f60 Andy McCall ↶ 1032
    }
1033
135212de Andy McCall ↶ 1034
    /// Fold every pack into one when there are more than `MAX_PACKS`
1035
    /// (spec §13.4). Returns how many packs were merged.
1036
    pub fn merge_packs(&self) -> Result<usize> {
64818938 Andy McCall ↶ 1037
        let (refs, keys): (Vec<ObjectRef>, Vec<String>) = {
135212de Andy McCall ↶ 1038
            let packs = self.packs.borrow();
1039
            if packs.len() <= MAX_PACKS {
1040
                return Ok(0);
1041
            }
75167672 Andy McCall 1042
            (
1043
                packs.iter().flat_map(|p| p.refs.iter().copied()).collect(),
64818938 Andy McCall ↶ 1044
                packs.iter().map(|p| p.key.clone()).collect(),
75167672 Andy McCall 1045
            )
135212de Andy McCall ↶ 1046
        };
64818938 Andy McCall ↶ 1047
        let n = keys.len();
1048
        self.write_pack(&refs, &[], &keys, ZSTD_LEVEL, &mut silent())?;
135212de Andy McCall ↶ 1049
        Ok(n)
1050
    }
1051
ba160f60 Andy McCall ↶ 1052
    /// Delete every object not in `keep` (spec §21). Loose objects and packs
1053
    /// younger than `grace` are left alone, since a concurrent writer may not
1054
    /// have recorded its references yet. Packs holding unreachable objects
64818938 Andy McCall ↶ 1055
    /// are rewritten without them. Signatures, seal and receipt entries of
1056
    /// deleted objects go too.
75167672 Andy McCall 1057
    pub fn prune(
9ae4d15a Andy McCall ↶ 1058
        &self,
1059
        keep: &std::collections::HashSet<ObjectRef>,
1060
        grace: std::time::Duration,
1061
    ) -> Result<PruneReport> {
1062
        self.prune_with(keep, grace, &mut silent())
1063
    }
1064
1065
    /// [`Store::prune`], reporting progress.
1066
    pub fn prune_with(
75167672 Andy McCall 1067
        &self,
1068
        keep: &std::collections::HashSet<ObjectRef>,
1069
        grace: std::time::Duration,
9ae4d15a Andy McCall ↶ 1070
        progress: Progress,
75167672 Andy McCall 1071
    ) -> Result<PruneReport> {
9ae4d15a Andy McCall ↶ 1072
        progress("pruning loose objects");
64818938 Andy McCall ↶ 1073
        let now = now_ms();
1074
        let grace_ms = grace.as_millis() as u64;
1075
        // An object whose age the store does not know is old enough.
1076
        let old_enough =
1077
            |modified: Option<u64>| modified.is_none_or(|t| now.saturating_sub(t) >= grace_ms);
ba160f60 Andy McCall ↶ 1078
        let mut report = PruneReport::default();
ef7d4ea6 Andy McCall ↶ 1079
        let mut gone: Vec<String> = Vec::new();
767ac6cd Andy McCall 1080
        // Loose objects. A retired one (its object packed) whose object is
1081
        // unreachable goes with it, uncounted: the pack's copy is counted.
1082
        let retired = self.retired_loose()?;
1083
        let mut unretired: Vec<String> = Vec::new();
1084
        for (r, entry) in self.loose_objects_all()? {
ba160f60 Andy McCall ↶ 1085
            if keep.contains(&r) {
1086
                continue;
1087
            }
767ac6cd Andy McCall 1088
            if retired.contains(&entry.key) {
1089
                if old_enough(entry.modified_ms) {
1090
                    delete(self.backend.as_ref(), &entry.key)?;
1091
                    unretired.push(entry.key);
1092
                }
1093
                continue;
1094
            }
64818938 Andy McCall ↶ 1095
            if !old_enough(entry.modified_ms) {
ba160f60 Andy McCall ↶ 1096
                report.deferred += 1;
1097
                continue;
1098
            }
64818938 Andy McCall ↶ 1099
            report.bytes += entry.size;
ba160f60 Andy McCall ↶ 1100
            report.objects += 1;
64818938 Andy McCall ↶ 1101
            delete(self.backend.as_ref(), &entry.key)?;
ef7d4ea6 Andy McCall ↶ 1102
            self.remove_sidecars(&r, &mut gone)?;
767ac6cd Andy McCall 1103
        }
1104
        if !unretired.is_empty() {
1105
            self.update_retired(|m| {
1106
                for k in &unretired {
1107
                    m.remove(k);
1108
                }
1109
            })?;
ba160f60 Andy McCall ↶ 1110
        }
1111
        // Packs: rewrite those that hold unreachable objects.
1112
        let mut keep_refs: Vec<ObjectRef> = Vec::new();
64818938 Andy McCall ↶ 1113
        let mut old_keys: Vec<String> = Vec::new();
ba160f60 Andy McCall ↶ 1114
        let mut rewrite = false;
9ae4d15a Andy McCall ↶ 1115
        progress("pruning packs");
ba160f60 Andy McCall ↶ 1116
        for p in self.packs.borrow().iter() {
1117
            let unreachable = p.refs.iter().filter(|r| !keep.contains(r)).count();
1118
            if unreachable == 0 {
1119
                continue;
1120
            }
64818938 Andy McCall ↶ 1121
            let modified = self
1122
                .backend
1123
                .stat(&p.key)
1124
                .map_err(backend_err)?
1125
                .and_then(|e| e.modified_ms);
1126
            if !old_enough(modified) {
ba160f60 Andy McCall ↶ 1127
                report.deferred += unreachable;
1128
                continue;
1129
            }
1130
            rewrite = true;
64818938 Andy McCall ↶ 1131
            old_keys.push(p.key.clone());
ba160f60 Andy McCall ↶ 1132
            for (r, e) in p.refs.iter().zip(p.entries.iter()) {
1133
                if keep.contains(r) {
1134
                    keep_refs.push(*r);
1135
                } else {
1136
                    report.objects += 1;
1137
                    report.bytes += e.stored_len as u64;
1138
                }
1139
            }
1140
        }
1141
        if rewrite {
1142
            let dropped: Vec<ObjectRef> = {
1143
                let packs = self.packs.borrow();
75167672 Andy McCall 1144
                packs
1145
                    .iter()
64818938 Andy McCall ↶ 1146
                    .filter(|p| old_keys.contains(&p.key))
75167672 Andy McCall 1147
                    .flat_map(|p| p.refs.iter().copied())
1148
                    .filter(|r| !keep.contains(r))
1149
                    .collect()
ba160f60 Andy McCall ↶ 1150
            };
64818938 Andy McCall ↶ 1151
            self.write_pack(&keep_refs, &[], &old_keys, ZSTD_LEVEL, progress)?;
ba160f60 Andy McCall ↶ 1152
            for r in dropped {
ef7d4ea6 Andy McCall ↶ 1153
                self.remove_sidecars(&r, &mut gone)?;
ba160f60 Andy McCall ↶ 1154
            }
1155
        }
ef7d4ea6 Andy McCall ↶ 1156
        self.journal_forget(&gone)?;
ba160f60 Andy McCall ↶ 1157
        Ok(report)
1158
    }
1159
64818938 Andy McCall ↶ 1160
    /// Everything kept beside an object: its signatures and the seal and
1161
    /// receipt entries under it, in both layouts.
ef7d4ea6 Andy McCall ↶ 1162
    /// The entry keys deleted are added to `gone`, for the journal.
1163
    fn remove_sidecars(&self, r: &ObjectRef, gone: &mut Vec<String>) -> Result<()> {
64818938 Andy McCall ↶ 1164
        for index in ["sigs", "seals", "receipts"] {
1165
            delete(self.backend.as_ref(), &legacy_key(index, r))?;
1166
        }
1167
        for index in ["sig", "seal", "receipt"] {
1168
            for e in self
1169
                .backend
1170
                .list(&fanout_prefix(index, r))
1171
                .map_err(backend_err)?
1172
            {
1173
                delete(self.backend.as_ref(), &e.key)?;
ef7d4ea6 Andy McCall ↶ 1174
                gone.push(e.key);
ba160f60 Andy McCall ↶ 1175
            }
1176
        }
1177
        Ok(())
1178
    }
1179
ef7d4ea6 Andy McCall ↶ 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
64818938 Andy McCall ↶ 1198
    /// Put a complete, synced temporary pack file and its index into the
1199
    /// store; `index` must be sorted by ObjectRef. The pack goes first: an
1200
    /// index never names a pack that is not there.
75167672 Andy McCall 1201
    fn finish_pack_files(
1202
        &self,
1203
        id: &str,
1204
        tmp_pack: &Path,
1205
        index: &[(ObjectRef, PackEntry)],
1206
    ) -> Result<()> {
64818938 Andy McCall ↶ 1207
        let mut idx = Vec::with_capacity(12 + index.len() * IDX_ENTRY);
1208
        idx.extend_from_slice(IDX_MAGIC);
1209
        idx.extend_from_slice(&(index.len() as u32).to_le_bytes());
1210
        for (r, e) in index {
1211
            idx.extend_from_slice(&r.0);
1212
            idx.extend_from_slice(&e.offset.to_le_bytes());
1213
            idx.extend_from_slice(&e.stored_len.to_le_bytes());
1214
            idx.extend_from_slice(&e.raw_len.to_le_bytes());
1215
            idx.extend_from_slice(&[type_index(e.ty), e.flags]);
8f10a07a Andy McCall ↶ 1216
        }
64818938 Andy McCall ↶ 1217
        let put = self
1218
            .backend
1219
            .put_new_file(&format!("pack-{id}.pack"), tmp_pack)
1220
            .map_err(backend_err);
1221
        // A store on another disk copied it; the one in this directory
1222
        // moved it and this finds nothing.
1223
        let _ = fs::remove_file(tmp_pack);
1224
        put?;
1225
        self.backend
1226
            .put_new(&format!("pack-{id}.idx"), &idx)
1227
            .map_err(backend_err)?;
8f10a07a Andy McCall ↶ 1228
        self.reload_packs()
64818938 Andy McCall ↶ 1229
    }
1230
767ac6cd Andy McCall 1231
    /// Remove a pack with its index and dictionary: the index first, so
1232
    /// a store opened meanwhile does not load a pack that is going.
64818938 Andy McCall ↶ 1233
    fn remove_pack(&self, pack_key: &str) -> Result<()> {
1234
        let stem = pack_key.trim_end_matches(".pack");
767ac6cd Andy McCall 1235
        delete(self.backend.as_ref(), &format!("{stem}.idx"))?;
64818938 Andy McCall ↶ 1236
        delete(self.backend.as_ref(), pack_key)?;
1237
        delete(self.backend.as_ref(), &format!("{stem}.dict"))
8f10a07a Andy McCall ↶ 1238
    }
1239
767ac6cd Andy McCall 1240
    /// Write `refs` into a new pack, then retire `loose_keys` and
1241
    /// `old_packs`: they stay in the store, unused by stores opened from
1242
    /// now on, until a collection [`RETIRED_FOR`] later deletes them, so a
1243
    /// reader that loaded its packs before goes on reading (oxygenhub#249).
75167672 Andy McCall 1244
    fn write_pack(
1245
        &self,
1246
        refs: &[ObjectRef],
64818938 Andy McCall ↶ 1247
        loose_keys: &[String],
1248
        old_packs: &[String],
da652d63 Andy McCall ↶ 1249
        level: i32,
9ae4d15a Andy McCall ↶ 1250
        progress: Progress,
75167672 Andy McCall 1251
    ) -> Result<PackReport> {
ba160f60 Andy McCall ↶ 1252
        if refs.is_empty() {
767ac6cd Andy McCall 1253
            self.retire(old_packs)?;
ba160f60 Andy McCall ↶ 1254
            self.reload_packs()?;
d02e545d Andy McCall ↶ 1255
            return Ok(PackReport::default());
1256
        }
ba160f60 Andy McCall ↶ 1257
        let mut refs: Vec<ObjectRef> = refs.to_vec();
1258
        refs.sort();
1259
        refs.dedup();
64818938 Andy McCall ↶ 1260
        let id = fresh_id();
1261
        let tmp_pack = self.scratch.join(format!(".tmp-{id}.pack"));
d02e545d Andy McCall ↶ 1262
        let mut report = PackReport::default();
ba160f60 Andy McCall ↶ 1263
        let mut index: Vec<(ObjectRef, PackEntry)> = Vec::with_capacity(refs.len());
a17fc622 Andy McCall ↶ 1264
        // A full repack of enough objects trains a dictionary for them.
1265
        let dict = if level == REPACK_ZSTD_LEVEL && refs.len() >= DICT_MIN_OBJECTS {
1266
            let _t = crate::timing::phase("pack: train a dictionary");
9ae4d15a Andy McCall ↶ 1267
            progress(&format!("training a dictionary on {} objects", refs.len()));
a17fc622 Andy McCall ↶ 1268
            self.train_dictionary(&refs)
1269
        } else {
1270
            None
1271
        };
1272
        let encoder = dict
1273
            .as_ref()
1274
            .map(|d| zstd::dict::EncoderDictionary::copy(d, level));
64818938 Andy McCall ↶ 1275
        let nocompress = self.nocompress_set()?;
d02e545d Andy McCall ↶ 1276
        {
1277
            let mut f = io::BufWriter::new(fs::File::create(&tmp_pack)?);
1278
            f.write_all(PACK_MAGIC)?;
1279
            let mut offset = PACK_MAGIC.len() as u64;
da652d63 Andy McCall ↶ 1280
            // Read a batch (the store is not shared between threads),
1281
            // compress it on every core, write it in order.
1282
            let mut batches = refs.chunks(1024);
1283
            let mut compressed_batch = Vec::new();
9ae4d15a Andy McCall ↶ 1284
            let total = refs.len();
1285
            let mut reported = usize::MAX;
da652d63 Andy McCall ↶ 1286
            loop {
1287
                if compressed_batch.is_empty() {
1288
                    let Some(batch) = batches.next() else { break };
9ae4d15a Andy McCall ↶ 1289
                    // Every tenth, and the first batch.
1290
                    let tenth = report.objects * 10 / total.max(1);
1291
                    if tenth != reported {
1292
                        reported = tenth;
1293
                        progress(&format!("packing {} of {total} objects", report.objects));
1294
                    }
da652d63 Andy McCall ↶ 1295
                    let mut read: Vec<(ObjectRef, ObjectType, Vec<u8>, bool)> =
1296
                        Vec::with_capacity(batch.len());
1297
                    for r in batch {
1298
                        let (ty, body) = self.get(r)?;
64818938 Andy McCall ↶ 1299
                        read.push((*r, ty, body, nocompress.contains(r)));
75167672 Andy McCall 1300
                    }
da652d63 Andy McCall ↶ 1301
                    use rayon::prelude::*;
1302
                    compressed_batch = read
1303
                        .into_par_iter()
a17fc622 Andy McCall ↶ 1304
                        .map_init(
1305
                            || {
1306
                                encoder.as_ref().and_then(|e| {
1307
                                    zstd::bulk::Compressor::with_prepared_dictionary(e).ok()
1308
                                })
1309
                            },
1310
                            |with_dict, (r, ty, body, raw)| {
1311
                                let packed = if raw {
1312
                                    None
1313
                                } else {
1314
                                    // Small objects with the dictionary;
1315
                                    // a large one has context enough.
1316
                                    let dicted = with_dict
1317
                                        .as_mut()
1318
                                        .filter(|_| body.len() <= DICT_OBJECT_MAX)
1319
                                        .and_then(|c| c.compress(&body).ok())
1320
                                        .map(|c| (FLAG_ZSTD | FLAG_DICT, c));
1321
                                    dicted
1322
                                        .or_else(|| {
1323
                                            zstd::bulk::compress(&body, level)
1324
                                                .ok()
1325
                                                .map(|c| (FLAG_ZSTD, c))
1326
                                        })
1327
                                        .filter(|(_, c)| c.len() < body.len())
1328
                                };
1329
                                (r, ty, body, packed)
1330
                            },
1331
                        )
da652d63 Andy McCall ↶ 1332
                        .collect::<Vec<_>>();
1333
                    compressed_batch.reverse();
1334
                }
1335
                let Some((r, ty, body, packed)) = compressed_batch.pop() else {
1336
                    continue;
1337
                };
1338
                let r = &r;
1339
                let (flags, stored) = match packed {
a17fc622 Andy McCall ↶ 1340
                    Some(c) => c,
da652d63 Andy McCall ↶ 1341
                    None => (0, body.clone()),
cdb5beba Andy McCall ↶ 1342
                };
d02e545d Andy McCall ↶ 1343
                let header_len = 1 + 1 + 4 + 4 + 32;
1344
                f.write_all(&[type_index(ty), flags])?;
1345
                f.write_all(&(stored.len() as u32).to_le_bytes())?;
1346
                f.write_all(&(body.len() as u32).to_le_bytes())?;
1347
                f.write_all(&r.0)?;
1348
                f.write_all(&stored)?;
75167672 Andy McCall 1349
                index.push((
1350
                    *r,
1351
                    PackEntry {
1352
                        offset: offset + header_len,
1353
                        stored_len: stored.len() as u32,
1354
                        raw_len: body.len() as u32,
1355
                        ty,
1356
                        flags,
1357
                    },
1358
                ));
d02e545d Andy McCall ↶ 1359
                offset += header_len + stored.len() as u64;
1360
                report.objects += 1;
1361
                report.raw_bytes += body.len() as u64;
1362
                report.packed_bytes += stored.len() as u64;
1363
            }
1364
            f.flush()?;
1365
            f.get_ref().sync_all()?;
a17fc622 Andy McCall ↶ 1366
        }
1367
        // The dictionary before the pack that needs it becomes visible.
1368
        if let Some(d) = &dict {
64818938 Andy McCall ↶ 1369
            self.backend
1370
                .put_new(&format!("pack-{id}.dict"), d)
1371
                .map_err(backend_err)?;
d02e545d Andy McCall ↶ 1372
        }
9ae4d15a Andy McCall ↶ 1373
        progress(&format!("writing the index of {} objects", index.len()));
8f10a07a Andy McCall ↶ 1374
        self.finish_pack_files(&id, &tmp_pack, &index)?;
767ac6cd Andy McCall 1375
        let mut retired = old_packs.to_vec();
1376
        retired.extend_from_slice(loose_keys);
1377
        self.retire(&retired)?;
d02e545d Andy McCall ↶ 1378
        self.reload_packs()?;
1379
        Ok(report)
ff0af1ee unknown 1380
    }
1381
a17fc622 Andy McCall ↶ 1382
    /// A dictionary for these objects, trained on a sample of them (at
1383
    /// most 10 000, 32 MB); `None` when training fails.
1384
    fn train_dictionary(&self, refs: &[ObjectRef]) -> Option<Vec<u8>> {
1385
        let step = (refs.len() / 10_000).max(1);
1386
        let mut samples: Vec<Vec<u8>> = Vec::new();
1387
        let mut total = 0usize;
1388
        for r in refs.iter().step_by(step) {
1389
            if total > 32 << 20 {
1390
                break;
1391
            }
1392
            let Ok((ty, body)) = self.get(r) else {
1393
                continue;
1394
            };
1395
            if matches!(
1396
                ty,
1397
                ObjectType::Chunk | ObjectType::Blob | ObjectType::Change
1398
            ) && !body.is_empty()
1399
                && body.len() < 1 << 20
1400
            {
1401
                total += body.len();
1402
                samples.push(body);
1403
            }
1404
        }
1405
        zstd::dict::from_samples(&samples, DICT_BYTES).ok()
1406
    }
1407
64818938 Andy McCall ↶ 1408
    /// The scratch directory: for a store in a directory, that directory.
ff0af1ee unknown 1409
    pub fn dir(&self) -> &Path {
64818938 Andy McCall ↶ 1410
        &self.scratch
ff0af1ee unknown 1411
    }
1412
64818938 Andy McCall ↶ 1413
    /// Where a loose object is kept, for a store in a directory (tests
1414
    /// tamper with and remove objects there).
1415
    #[cfg(test)]
387335ff Andy McCall ↶ 1416
    pub(crate) fn path_for(&self, r: &ObjectRef) -> PathBuf {
64818938 Andy McCall ↶ 1417
        self.scratch.join(loose_key(r))
1418
    }
1419
1420
    /// Remove a loose object, whatever the store is on (tests make an
1421
    /// object go missing).
1422
    #[cfg(test)]
1423
    pub(crate) fn remove_loose_for_test(&self, r: &ObjectRef) -> Result<()> {
1424
        delete(self.backend.as_ref(), &loose_key(r))
ff0af1ee unknown 1425
    }
1426
767ac6cd Andy McCall 1427
    /// Whether the store holds `r`. A miss looks at the pack listing
1428
    /// again before it is believed, as [`Store::get`]'s does; a writer
1429
    /// asking about many objects it is about to store looks at most once
1430
    /// in [`HAS_RELIST_AFTER`] while nothing new turns up.
ff0af1ee unknown 1431
    pub fn has(&self, r: &ObjectRef) -> bool {
767ac6cd Andy McCall 1432
        if self.has_now(r) {
1433
            return true;
1434
        }
1435
        let recent = self
1436
            .has_relisted
1437
            .get()
1438
            .is_some_and(|t| t.elapsed() < HAS_RELIST_AFTER);
1439
        if recent {
1440
            return false;
1441
        }
1442
        match self.reload_if_changed() {
1443
            Ok(true) => {
1444
                self.has_relisted.set(None);
1445
                self.has_now(r)
1446
            }
1447
            _ => {
1448
                self.has_relisted.set(Some(std::time::Instant::now()));
1449
                false
1450
            }
1451
        }
1452
    }
1453
1454
    /// [`Store::has`] against the packs as loaded.
1455
    fn has_now(&self, r: &ObjectRef) -> bool {
1456
        self.find_packed(r).is_some()
75167672 Andy McCall 1457
            || self
1458
                .pending
1459
                .borrow()
1460
                .as_ref()
1461
                .is_some_and(|p| p.lookup.contains_key(r))
767ac6cd Andy McCall 1462
            || self.backend.has(&loose_key(r)).unwrap_or(false)
ff0af1ee unknown 1463
    }
1464
8f10a07a Andy McCall ↶ 1465
    /// Start a write batch: until the matching `finish`, `put` collects
1466
    /// objects and writes them out together, with one `fsync` for a large
1467
    /// batch (a pack) instead of one per object. Batches nest; only the
1468
    /// outermost does the work. Objects are readable while pending.
1469
    pub fn begin_batch(&self) -> Result<Batch<'_>> {
1470
        let depth = self.batch_depth.get();
1471
        if depth == 0 {
75167672 Andy McCall 1472
            *self.pending.borrow_mut() = Some(Pending {
1473
                buffered: vec![],
1474
                buffered_bytes: 0,
1475
                lookup: Default::default(),
1476
                pack: None,
1477
                report: PackReport::default(),
1478
            });
8f10a07a Andy McCall ↶ 1479
        }
1480
        self.batch_depth.set(depth + 1);
75167672 Andy McCall 1481
        Ok(Batch {
1482
            store: self,
1483
            done: false,
1484
        })
8f10a07a Andy McCall ↶ 1485
    }
1486
1487
    fn finish_batch(&self) -> Result<()> {
1488
        let depth = self.batch_depth.get();
1489
        if depth == 0 {
1490
            return Ok(());
1491
        }
1492
        self.batch_depth.set(depth - 1);
1493
        if depth > 1 {
1494
            return Ok(());
1495
        }
75167672 Andy McCall 1496
        let Some(pending) = self.pending.borrow_mut().take() else {
ef7d4ea6 Andy McCall ↶ 1497
            return self.flush_journal();
75167672 Andy McCall 1498
        };
ef7d4ea6 Andy McCall ↶ 1499
        self.write_pending(pending)?;
1500
        self.flush_journal()
4dcb8c09 Andy McCall ↶ 1501
    }
1502
1503
    /// Make every object written so far durable, even inside an open
1504
    /// batch, which stays open. Call before a reference that names them
1505
    /// is moved (spec §13.6): the operation head, a bookmark pointer.
1506
    pub fn flush(&self) -> Result<()> {
1507
        let Some(pending) = self.pending.borrow_mut().take() else {
ef7d4ea6 Andy McCall ↶ 1508
            return self.flush_journal();
4dcb8c09 Andy McCall ↶ 1509
        };
1510
        self.write_pending(pending)?;
ef7d4ea6 Andy McCall ↶ 1511
        self.flush_journal()?;
4dcb8c09 Andy McCall ↶ 1512
        *self.pending.borrow_mut() = Some(Pending {
1513
            buffered: vec![],
1514
            buffered_bytes: 0,
1515
            lookup: Default::default(),
1516
            pack: None,
1517
            report: PackReport::default(),
1518
        });
1519
        Ok(())
1520
    }
1521
1522
    fn write_pending(&self, pending: Pending) -> Result<()> {
8f10a07a Andy McCall ↶ 1523
        match pending.pack {
1524
            None => {
b30a880f Andy McCall ↶ 1525
                let objects: Vec<(ObjectType, &[u8])> = pending
1526
                    .buffered
1527
                    .iter()
1528
                    .map(|(ty, body)| (*ty, body.as_slice()))
1529
                    .collect();
1530
                self.put_loose_many(&objects)
8f10a07a Andy McCall ↶ 1531
            }
1532
            Some(mut pack) => {
1533
                pack.writer.flush()?;
1534
                pack.writer.get_ref().sync_all()?;
1535
                drop(pack.writer);
1536
                pack.index.sort_by_key(|(r, _)| *r);
1537
                self.finish_pack_files(&pack.id, &pack.tmp_path, &pack.index)?;
1538
                Ok(())
1539
            }
1540
        }
1541
    }
1542
1543
    /// Whether a batch is open and holds `r`.
64818938 Andy McCall ↶ 1544
    fn pending_get(&self, r: &ObjectRef) -> Option<Result<Object>> {
8f10a07a Andy McCall ↶ 1545
        let mut guard = self.pending.borrow_mut();
1546
        let pending = guard.as_mut()?;
1547
        let entry = *pending.lookup.get(r)?;
1548
        Some(match entry {
1549
            PendingEntry::Buffered(i) => {
1550
                let (ty, body) = &pending.buffered[i];
1551
                Ok((*ty, body.clone()))
1552
            }
1553
            PendingEntry::Packed(e) => {
75167672 Andy McCall 1554
                let Some(pack) = pending.pack.as_mut() else {
1555
                    return Some(Err(StoreError::NotFound(*r)));
1556
                };
64818938 Andy McCall ↶ 1557
                // Read from the pack file being written, on this disk.
1558
                (|| {
1559
                    pack.writer.flush()?;
1560
                    use std::io::{Read, Seek, SeekFrom};
1561
                    let mut f = fs::File::open(&pack.tmp_path)?;
1562
                    f.seek(SeekFrom::Start(e.offset))?;
1563
                    let mut stored = vec![0u8; e.stored_len as usize];
1564
                    f.read_exact(&mut stored)?;
1565
                    decode_entry(r, e, stored, None)
1566
                })()
8f10a07a Andy McCall ↶ 1567
            }
1568
        })
1569
    }
1570
1571
    /// Append one object to the pending pack file.
75167672 Andy McCall 1572
    fn pending_pack_write(
1573
        &self,
1574
        pack: &mut PendingPack,
1575
        r: &ObjectRef,
1576
        ty: ObjectType,
1577
        body: &[u8],
1578
        report: &mut PackReport,
1579
    ) -> Result<PackEntry> {
8f10a07a Andy McCall ↶ 1580
        let (flags, stored) = if self.is_nocompress(r) {
1581
            (0, body.to_vec())
1582
        } else {
75167672 Andy McCall 1583
            let compressed = zstd::bulk::compress(body, ZSTD_LEVEL)
1584
                .map_err(|e| StoreError::Corrupt(*r, format!("zstd: {e}")))?;
1585
            if compressed.len() < body.len() {
1586
                (FLAG_ZSTD, compressed)
1587
            } else {
1588
                (0, body.to_vec())
1589
            }
8f10a07a Andy McCall ↶ 1590
        };
192fad1e Andy McCall ↶ 1591
        write_pack_entry(pack, r, ty, body.len(), flags, &stored, report)
8f10a07a Andy McCall ↶ 1592
    }
1593
1594
    fn put_pending(&self, r: ObjectRef, ty: ObjectType, body: &[u8]) -> Result<()> {
1595
        let mut guard = self.pending.borrow_mut();
1596
        let pending = guard.as_mut().expect("batch open");
1597
        if pending.lookup.contains_key(&r) {
1598
            return Ok(());
1599
        }
75167672 Andy McCall 1600
        if pending.pack.is_none()
1601
            && pending.buffered.len() < SPILL_OBJECTS
1602
            && pending.buffered_bytes + body.len() <= SPILL_BYTES
1603
        {
8f10a07a Andy McCall ↶ 1604
            pending.buffered.push((ty, body.to_vec()));
1605
            pending.buffered_bytes += body.len();
75167672 Andy McCall 1606
            pending
1607
                .lookup
1608
                .insert(r, PendingEntry::Buffered(pending.buffered.len() - 1));
8f10a07a Andy McCall ↶ 1609
            return Ok(());
1610
        }
192fad1e Andy McCall ↶ 1611
        self.ensure_pack(pending)?;
1612
        let mut report = std::mem::take(&mut pending.report);
1613
        let pack = pending.pack.as_mut().unwrap();
1614
        let e = self.pending_pack_write(pack, &r, ty, body, &mut report)?;
1615
        pending.report = report;
1616
        pending.lookup.insert(r, PendingEntry::Packed(e));
1617
        Ok(())
1618
    }
1619
1620
    /// Open the pending batch's pack, moving the objects buffered so far
1621
    /// into it (a spill).
1622
    fn ensure_pack(&self, pending: &mut Pending) -> Result<()> {
8f10a07a Andy McCall ↶ 1623
        if pending.pack.is_none() {
1624
            // Spill: open the pack and move the buffered objects into it.
64818938 Andy McCall ↶ 1625
            let id = fresh_id();
1626
            let tmp_path = self.scratch.join(format!(".tmp-{id}.pack"));
8f10a07a Andy McCall ↶ 1627
            let mut writer = io::BufWriter::new(fs::File::create(&tmp_path)?);
1628
            writer.write_all(PACK_MAGIC)?;
75167672 Andy McCall 1629
            let mut pack = PendingPack {
1630
                id,
1631
                tmp_path,
1632
                writer,
1633
                offset: PACK_MAGIC.len() as u64,
1634
                index: vec![],
1635
            };
8f10a07a Andy McCall ↶ 1636
            let buffered = std::mem::take(&mut pending.buffered);
1637
            pending.buffered_bytes = 0;
1638
            let mut report = std::mem::take(&mut pending.report);
1639
            for (bty, bbody) in &buffered {
1640
                let br = hash_object(*bty, bbody);
1641
                let e = self.pending_pack_write(&mut pack, &br, *bty, bbody, &mut report)?;
1642
                pending.lookup.insert(br, PendingEntry::Packed(e));
1643
            }
1644
            pending.report = report;
1645
            pending.pack = Some(pack);
192fad1e Andy McCall ↶ 1646
        }
1647
        Ok(())
1648
    }
1649
1650
    /// Store many objects at once. Inside a large write batch they are
1651
    /// compressed on every core and appended in order; otherwise each is
1652
    /// stored as [`Store::put`] does.
1653
    pub fn put_many(&self, objects: &[(ObjectType, &[u8])]) -> Result<Vec<ObjectRef>> {
1654
        use rayon::prelude::*;
1655
        let total: usize = objects.iter().map(|(_, b)| b.len()).sum();
1656
        if self.pending.borrow().is_none() || total < SPILL_BYTES {
1657
            return objects.iter().map(|(ty, b)| self.put(*ty, b)).collect();
1658
        }
1659
        for (ty, b) in objects {
1660
            if b.len() > MAX_OBJECT_LEN as usize {
1661
                return Err(StoreError::TooLarge {
1662
                    kind: ty.name(),
1663
                    size: b.len(),
1664
                    limit: MAX_OBJECT_LEN,
1665
                });
1666
            }
8f10a07a Andy McCall ↶ 1667
        }
192fad1e Andy McCall ↶ 1668
        let refs: Vec<ObjectRef> = objects
1669
            .par_iter()
1670
            .map(|(ty, b)| hash_object(*ty, b))
1671
            .collect();
1672
        // What is new: not stored already, not earlier in this call.
1673
        let mut seen: std::collections::HashSet<ObjectRef> = std::collections::HashSet::new();
1674
        let mut todo: Vec<usize> = Vec::new();
1675
        for (i, r) in refs.iter().enumerate() {
1676
            let known = self
1677
                .pending
1678
                .borrow()
1679
                .as_ref()
1680
                .is_some_and(|p| p.lookup.contains_key(r));
1681
            if seen.insert(*r)
1682
                && !known
1683
                && self.find_packed(r).is_none()
64818938 Andy McCall ↶ 1684
                && !self.backend.has(&loose_key(r)).unwrap_or(false)
192fad1e Andy McCall ↶ 1685
            {
1686
                todo.push(i);
1687
            }
1688
        }
64818938 Andy McCall ↶ 1689
        let raw = self.nocompress_set()?;
192fad1e Andy McCall ↶ 1690
        let packed: Vec<(u8, Vec<u8>)> = todo
1691
            .par_iter()
1692
            .map(|i| {
1693
                let body = objects[*i].1;
1694
                if raw.contains(&refs[*i]) {
1695
                    return (0, body.to_vec());
1696
                }
1697
                match zstd::bulk::compress(body, ZSTD_LEVEL) {
1698
                    Ok(c) if c.len() < body.len() => (FLAG_ZSTD, c),
1699
                    _ => (0, body.to_vec()),
1700
                }
1701
            })
1702
            .collect();
1703
        let mut guard = self.pending.borrow_mut();
1704
        let pending = guard.as_mut().expect("batch open");
1705
        self.ensure_pack(pending)?;
8f10a07a Andy McCall ↶ 1706
        let mut report = std::mem::take(&mut pending.report);
1707
        let pack = pending.pack.as_mut().unwrap();
192fad1e Andy McCall ↶ 1708
        for (i, (flags, stored)) in todo.iter().zip(packed) {
1709
            let (ty, body) = objects[*i];
1710
            let e = write_pack_entry(pack, &refs[*i], ty, body.len(), flags, &stored, &mut report)?;
1711
            pending.lookup.insert(refs[*i], PendingEntry::Packed(e));
1712
        }
8f10a07a Andy McCall ↶ 1713
        pending.report = report;
192fad1e Andy McCall ↶ 1714
        Ok(refs)
8f10a07a Andy McCall ↶ 1715
    }
1716
ff0af1ee unknown 1717
    /// Store `body` as an object of type `ty`; idempotent.
1718
    pub fn put(&self, ty: ObjectType, body: &[u8]) -> Result<ObjectRef> {
a9f686ed Andy McCall ↶ 1719
        // Refuse what the store could not read back (spec §13.2), before
1720
        // anything is written: a pack must never hold an entry its index
1721
        // would reject.
1722
        if body.len() > MAX_OBJECT_LEN as usize {
1723
            return Err(StoreError::TooLarge {
1724
                kind: ty.name(),
1725
                size: body.len(),
1726
                limit: MAX_OBJECT_LEN,
1727
            });
1728
        }
ff0af1ee unknown 1729
        let r = hash_object(ty, body);
64818938 Andy McCall ↶ 1730
        if self.find_packed(&r).is_some() || self.backend.has(&loose_key(&r)).unwrap_or(false) {
ff0af1ee unknown 1731
            return Ok(r);
1732
        }
8f10a07a Andy McCall ↶ 1733
        if self.pending.borrow().is_some() {
1734
            self.put_pending(r, ty, body)?;
1735
            return Ok(r);
1736
        }
1737
        self.put_loose(ty, body)?;
1738
        Ok(r)
1739
    }
1740
64818938 Andy McCall ↶ 1741
    /// Write one loose object, durable when this returns.
8f10a07a Andy McCall ↶ 1742
    fn put_loose(&self, ty: ObjectType, body: &[u8]) -> Result<ObjectRef> {
1743
        let r = hash_object(ty, body);
64818938 Andy McCall ↶ 1744
        let mut bytes = header(ty, body.len());
1745
        bytes.extend_from_slice(body);
4dcb8c09 Andy McCall ↶ 1746
        crate::durable::step("write loose object")?;
64818938 Andy McCall ↶ 1747
        put_new_or_existing(self.backend.as_ref(), &loose_key(&r), &bytes)?;
ff0af1ee unknown 1748
        Ok(r)
b30a880f Andy McCall ↶ 1749
    }
1750
1751
    /// Write several loose objects with as few waits on the disk as the
64818938 Andy McCall ↶ 1752
    /// guarantees allow (a directory store writes them all, then syncs
1753
    /// each directory once). Durable when this returns, as `put_loose`
1754
    /// one at a time would be (spec §13.6).
b30a880f Andy McCall ↶ 1755
    fn put_loose_many(&self, objects: &[(ObjectType, &[u8])]) -> Result<()> {
1756
        if objects.len() <= 1 {
1757
            for (ty, body) in objects {
1758
                self.put_loose(*ty, body)?;
1759
            }
1760
            return Ok(());
1761
        }
1762
        let mut seen = std::collections::HashSet::new();
64818938 Andy McCall ↶ 1763
        let items: Vec<(String, Vec<u8>)> = objects
b30a880f Andy McCall ↶ 1764
            .iter()
1765
            .filter_map(|(ty, body)| {
1766
                let r = hash_object(*ty, body);
64818938 Andy McCall ↶ 1767
                seen.insert(r).then(|| {
1768
                    let mut bytes = header(*ty, body.len());
1769
                    bytes.extend_from_slice(body);
1770
                    (loose_key(&r), bytes)
b30a880f Andy McCall ↶ 1771
                })
1772
            })
1773
            .collect();
64818938 Andy McCall ↶ 1774
        self.backend.put_many_new(&items).map_err(backend_err)
ff0af1ee unknown 1775
    }
1776
09245702 Andy McCall ↶ 1777
    /// Read and verify an object, packed, loose, or pending in a batch.
1778
    /// The packs' indexes are in memory, so they are asked first: asking
1779
    /// for a loose copy of a packed object costs a request that finds
1780
    /// nothing on a store in a bucket, every time (oxygenhub#228).
767ac6cd Andy McCall 1781
    ///
1782
    /// A miss is not believed at once (oxygenhub#249): when the pack it
1783
    /// was in is gone (deleted after a repack retired it) the packs are
1784
    /// loaded again, and when the object is nowhere the pack listing is
1785
    /// looked at, and the packs loaded again if it changed (another
1786
    /// writer packed it, or a repack moved it); then it is read once more.
64818938 Andy McCall ↶ 1787
    pub fn get(&self, r: &ObjectRef) -> Result<Object> {
767ac6cd Andy McCall 1788
        match self.get_now(r) {
1789
            Err(e) if is_miss(&e) => {
1790
                // Read from a pack this store loaded, which is gone.
1791
                let packed_miss =
1792
                    !matches!(e, StoreError::NotFound(_)) || self.find_packed(r).is_some();
1793
                let changed = if packed_miss {
1794
                    self.reload_packs()?;
1795
                    true
1796
                } else {
1797
                    self.reload_if_changed()?
1798
                };
1799
                if changed {
1800
                    self.get_now(r)
1801
                } else {
1802
                    Err(e)
1803
                }
1804
            }
1805
            other => other,
1806
        }
1807
    }
1808
1809
    /// [`Store::get`] against the packs as loaded.
1810
    fn get_now(&self, r: &ObjectRef) -> Result<Object> {
09245702 Andy McCall ↶ 1811
        if let Some((i, e)) = self.find_packed(r) {
767ac6cd Andy McCall 1812
            let pack = self.packs.borrow()[i].clone();
1813
            return pack.fetch(self.backend.as_ref(), r, e);
09245702 Andy McCall ↶ 1814
        }
64818938 Andy McCall ↶ 1815
        match get_optional(self.backend.as_ref(), &loose_key(r))? {
1816
            Some(bytes) => parse_loose(&bytes, r),
09245702 Andy McCall ↶ 1817
            None => self.pending_get(r).unwrap_or(Err(StoreError::NotFound(*r))),
afaba5f1 Andy McCall ↶ 1818
        }
1819
    }
1820
09245702 Andy McCall ↶ 1821
    /// Many objects at once, read and verified in parallel: packed ones
1822
    /// from their packs, the others loose (as [`Store::get`], packs first),
1823
    /// then what is pending in a batch. Results are in the order of `refs`.
767ac6cd Andy McCall 1824
    ///
1825
    /// Misses are looked for again as [`Store::get`]'s are, together.
afaba5f1 Andy McCall ↶ 1826
    pub fn get_many(&self, refs: &[ObjectRef]) -> Vec<Result<Object>> {
767ac6cd Andy McCall 1827
        let mut out = self.get_many_now(refs);
1828
        let missed: Vec<usize> = out
1829
            .iter()
1830
            .enumerate()
1831
            .filter(|(_, g)| g.as_ref().is_err_and(is_miss))
1832
            .map(|(i, _)| i)
1833
            .collect();
1834
        if missed.is_empty() {
1835
            return out;
1836
        }
1837
        let packed_miss = missed.iter().any(|i| {
1838
            !matches!(out[*i], Err(StoreError::NotFound(_)))
1839
                || self.find_packed(&refs[*i]).is_some()
1840
        });
1841
        let changed = if packed_miss {
1842
            self.reload_packs().map(|_| true)
1843
        } else {
1844
            self.reload_if_changed()
1845
        };
1846
        match changed {
1847
            Ok(true) => {
1848
                let again: Vec<ObjectRef> = missed.iter().map(|i| refs[*i]).collect();
1849
                for (i, g) in missed.into_iter().zip(self.get_many_now(&again)) {
1850
                    out[i] = g;
1851
                }
1852
            }
1853
            Ok(false) => {}
1854
            Err(e) => {
1855
                let why = e.to_string();
1856
                for i in missed {
1857
                    out[i] = Err(StoreError::Io(io::Error::other(why.clone())));
1858
                }
1859
            }
1860
        }
1861
        out
1862
    }
1863
1864
    /// [`Store::get_many`] against the packs as loaded.
1865
    fn get_many_now(&self, refs: &[ObjectRef]) -> Vec<Result<Object>> {
afaba5f1 Andy McCall ↶ 1866
        use rayon::prelude::*;
64818938 Andy McCall ↶ 1867
        let backend = self.backend.as_ref();
afaba5f1 Andy McCall ↶ 1868
        let packs = self.packs.borrow();
09245702 Andy McCall ↶ 1869
        let place: Vec<Option<(usize, PackEntry)>> = refs
75167672 Andy McCall 1870
            .iter()
09245702 Andy McCall ↶ 1871
            .map(|r| {
afaba5f1 Andy McCall ↶ 1872
                packs
1873
                    .iter()
1874
                    .enumerate()
09245702 Andy McCall ↶ 1875
                    .find_map(|(p, pack)| pack.find(r).map(|e| (p, e)))
afaba5f1 Andy McCall ↶ 1876
            })
1877
            .collect();
767ac6cd Andy McCall 1878
        let indexes: &[Arc<PackIndex>] = &packs;
09245702 Andy McCall ↶ 1879
        let found: Vec<Option<Result<Object>>> = refs
afaba5f1 Andy McCall ↶ 1880
            .par_iter()
09245702 Andy McCall ↶ 1881
            .zip(place.par_iter())
1882
            .map(|(r, at)| match at {
1883
                Some((p, e)) => Some(indexes[*p].fetch(backend, r, *e)),
1884
                None => match get_optional(backend, &loose_key(r)) {
1885
                    Ok(Some(bytes)) => Some(parse_loose(&bytes, r)),
1886
                    Ok(None) => None,
1887
                    Err(e) => Some(Err(e)),
1888
                },
1889
            })
afaba5f1 Andy McCall ↶ 1890
            .collect();
1891
        drop(packs);
1892
        found
1893
            .into_iter()
1894
            .zip(refs)
09245702 Andy McCall ↶ 1895
            .map(|(f, r)| {
1896
                f.unwrap_or_else(|| self.pending_get(r).unwrap_or(Err(StoreError::NotFound(*r))))
afaba5f1 Andy McCall ↶ 1897
            })
1898
            .collect()
ff0af1ee unknown 1899
    }
1900
1901
    pub fn get_typed(&self, r: &ObjectRef, want: ObjectType) -> Result<Vec<u8>> {
1902
        let (ty, body) = self.get(r)?;
1903
        if ty != want {
1904
            return Err(StoreError::WrongType(*r, ty.name(), want.name()));
1905
        }
1906
        Ok(body)
1907
    }
1908
6f8aeb99 Andy McCall ↶ 1909
    /// Store a change and the text blobs (and their chunks) its large
1910
    /// inserts are stored as (spec §6.1).
ff0af1ee unknown 1911
    pub fn put_change(&self, c: &Change) -> Result<ObjectRef> {
5b4c5130 Andy McCall ↶ 1912
        let encoding = crate::timing::phase("store: encode the change and its texts");
17f94b13 Andy McCall ↶ 1913
        let bytes = c.encode_streaming(|batch| self.put_texts(&batch))?;
5b4c5130 Andy McCall ↶ 1914
        drop(encoding);
1915
        let _t = crate::timing::phase("store: put the encoding");
17f94b13 Andy McCall ↶ 1916
        self.put(ObjectType::Change, &bytes)
6f8aeb99 Andy McCall ↶ 1917
    }
1918
17f94b13 Andy McCall ↶ 1919
    /// Store text blobs and their chunks: every chunk at once (compressed
1920
    /// on every core in a large batch), then the blobs.
1921
    pub fn put_texts(&self, texts: &[crate::objects::StoredText]) -> Result<()> {
192fad1e Andy McCall ↶ 1922
        let mut chunks: Vec<(ObjectType, &[u8])> = Vec::new();
1923
        for t in texts {
1924
            if !self.has(&t.blob.hash()) {
1925
                chunks.extend(
1926
                    crate::chunk::chunks(&t.content)
1927
                        .into_iter()
1928
                        .map(|c| (ObjectType::Chunk, c)),
1929
                );
1930
            }
6f8aeb99 Andy McCall ↶ 1931
        }
192fad1e Andy McCall ↶ 1932
        self.put_many(&chunks)?;
1933
        for t in texts {
1934
            self.put(ObjectType::Blob, &t.blob.encode())?;
6f8aeb99 Andy McCall ↶ 1935
        }
17f94b13 Andy McCall ↶ 1936
        Ok(())
ff0af1ee unknown 1937
    }
1938
1939
    pub fn get_change(&self, r: &ObjectRef) -> Result<Change> {
6f8aeb99 Andy McCall ↶ 1940
        Ok(self.get_change_parts(r)?.0)
1941
    }
1942
1943
    /// A change and the objects its stored inserts are made of: each text
1944
    /// blob and its chunks, which travel and are kept with it.
1945
    pub fn get_change_parts(&self, r: &ObjectRef) -> Result<(Change, Vec<ObjectRef>)> {
1946
        let bytes = self.get_typed(r, ObjectType::Change)?;
1947
        let mut parts = Vec::new();
1948
        let mut failed = None;
0f93f49c Andy McCall ↶ 1949
        let decoded = Change::decode_with(&bytes, &mut |text, count, form| match self
1950
            .read_text(text)
1951
        {
6f8aeb99 Andy McCall ↶ 1952
            Ok((blob, content)) => {
1953
                parts.push(*text);
1954
                parts.extend(blob.chunks);
0f93f49c Andy McCall ↶ 1955
                crate::objects::text_lines(&content, count, form)
6f8aeb99 Andy McCall ↶ 1956
            }
1957
            Err(e) => {
1958
                let why = e.to_string();
1959
                failed = Some(e);
1960
                Err(ObjectError::Schema(format!(
1961
                    "text blob {}: {why}",
1962
                    text.short(12)
1963
                )))
1964
            }
1965
        });
1966
        match (decoded, failed) {
1967
            (Ok(c), _) => Ok((c, parts)),
1968
            (Err(_), Some(StoreError::NotFound(t))) => Err(StoreError::Corrupt(
1969
                *r,
1970
                format!("its text blob {} is missing", t.short(12)),
1971
            )),
1972
            (Err(e), _) => Err(StoreError::Corrupt(*r, e.to_string())),
1973
        }
1974
    }
1975
a4fad5b0 Andy McCall ↶ 1976
    /// A change with every insert left without its lines (empty
1977
    /// placeholders), and where each one's lines are, in order: for
1978
    /// history-only states, which read text only for the graphs they build
1979
    /// (see [`Change::decode_skeleton`]).
1980
    pub fn get_change_lazy(&self, r: &ObjectRef) -> Result<(Change, Vec<crate::objects::TextRef>)> {
86e1824f Andy McCall ↶ 1981
        let bytes = self.get_typed(r, ObjectType::Change)?;
a4fad5b0 Andy McCall ↶ 1982
        Change::decode_skeleton(&bytes, *r).map_err(|e| StoreError::Corrupt(*r, e.to_string()))
86e1824f Andy McCall ↶ 1983
    }
1984
69816d68 Andy McCall ↶ 1985
    /// A change's metadata (id, dependencies, author, clock, message, kind)
1986
    /// without its contents: read from its skeleton, so neither its text
1987
    /// blobs nor its inline lines are read (spec §11).
ce14f0d6 Andy McCall ↶ 1988
    ///
1989
    /// Served from the `metas` cache when this store read it before (a
1990
    /// change object can be as large as the tree it adds), as long as the
1991
    /// store still has the change.
69816d68 Andy McCall ↶ 1992
    pub fn get_change_meta(&self, r: &ObjectRef) -> Result<crate::state::ChangeMeta> {
ce14f0d6 Andy McCall ↶ 1993
        if self.metas.borrow().is_none() {
1994
            *self.metas.borrow_mut() = Some(self.read_metas_cache());
1995
        }
1996
        let cached = self.metas.borrow().as_ref().and_then(|m| m.get(r).cloned());
1997
        if let Some(meta) = cached {
1998
            if self.has(r) {
1999
                #[cfg(test)]
2000
                assert_eq!(
2001
                    meta,
2002
                    Change::decode_meta(&self.get_typed(r, ObjectType::Change)?).unwrap(),
2003
                    "the metadata cache holds a change's metadata"
2004
                );
2005
                return Ok(meta);
2006
            }
2007
        }
4d6c9c1b Andy McCall ↶ 2008
        let bytes = self.get_typed(r, ObjectType::Change)?;
ce14f0d6 Andy McCall ↶ 2009
        let meta =
2010
            Change::decode_meta(&bytes).map_err(|e| StoreError::Corrupt(*r, e.to_string()))?;
2011
        self.remember_meta(r, &meta);
2012
        Ok(meta)
2013
    }
2014
2015
    /// The metadata cache: records of a change hash, its metadata in the
2016
    /// flat layout, and the first 8 bytes of BLAKE3 over both, each
2017
    /// appended in one write. Derived only: a record that does not check
64818938 Andy McCall ↶ 2018
    /// ends the reading (a torn write), and the cache can be deleted. It
2019
    /// lives in the scratch directory, on this disk.
ce14f0d6 Andy McCall ↶ 2020
    fn read_metas_cache(&self) -> crate::fasthash::FastMap<ObjectRef, crate::state::ChangeMeta> {
2021
        let mut out = crate::fasthash::FastMap::default();
64818938 Andy McCall ↶ 2022
        let Ok(bytes) = fs::read(self.scratch.join("metas")) else {
ce14f0d6 Andy McCall ↶ 2023
            return out;
2024
        };
2025
        let mut r = crate::flat::Reader::new(&bytes);
2026
        let mut good = 0;
2027
        while !r.at_end() {
2028
            let mut record = || -> std::result::Result<_, ObjectError> {
2029
                let h = r.object()?;
2030
                let body = r.bytes()?;
2031
                let check = r.fixed::<8>()?;
2032
                Ok((h, body, check))
2033
            };
2034
            let Ok((h, body, check)) = record() else {
2035
                break;
2036
            };
2037
            let mut sum = blake3::Hasher::new();
2038
            sum.update(&h.0).update(body);
2039
            if sum.finalize().as_bytes()[..8] != check {
2040
                break;
2041
            }
2042
            match crate::state::ChangeMeta::read_flat(&mut crate::flat::Reader::new(body)) {
2043
                Ok(m) => out.insert(h, m),
2044
                Err(_) => break,
2045
            };
2046
            good += 32 + 4 + body.len() + 8;
2047
        }
2048
        // Cut a bad tail, so records appended later are read.
2049
        if good < bytes.len() {
2050
            let _ = fs::OpenOptions::new()
2051
                .write(true)
64818938 Andy McCall ↶ 2052
                .open(self.scratch.join("metas"))
ce14f0d6 Andy McCall ↶ 2053
                .and_then(|f| f.set_len(good as u64));
2054
        }
2055
        out
2056
    }
2057
2058
    /// Add a change's metadata to the cache; quietly nothing when it
2059
    /// cannot be written (it is only a cache).
2060
    fn remember_meta(&self, r: &ObjectRef, meta: &crate::state::ChangeMeta) {
2061
        let mut body = crate::flat::Writer::default();
2062
        meta.write_flat(&mut body);
2063
        let mut w = crate::flat::Writer::default();
2064
        w.object(r);
2065
        w.bytes(&body.0);
2066
        let mut sum = blake3::Hasher::new();
2067
        sum.update(&r.0).update(&body.0);
2068
        w.fixed(&sum.finalize().as_bytes()[..8]);
2069
        let _ = fs::OpenOptions::new()
2070
            .create(true)
2071
            .append(true)
64818938 Andy McCall ↶ 2072
            .open(self.scratch.join("metas"))
ce14f0d6 Andy McCall ↶ 2073
            .and_then(|mut f| f.write_all(&w.0));
2074
        if let Some(m) = self.metas.borrow_mut().as_mut() {
2075
            m.insert(*r, meta.clone());
2076
        }
69816d68 Andy McCall ↶ 2077
    }
2078
6f8aeb99 Andy McCall ↶ 2079
    fn read_text(&self, r: &ObjectRef) -> Result<(Blob, Vec<u8>)> {
2080
        let blob = Blob::decode(&self.get_typed(r, ObjectType::Blob)?)?;
2081
        let mut content = Vec::with_capacity(blob.length as usize);
2082
        for c in &blob.chunks {
2083
            content.extend(self.get_typed(c, ObjectType::Chunk)?);
2084
        }
2085
        Ok((blob, content))
ff0af1ee unknown 2086
    }
2087
2088
    pub fn put_chunk(&self, bytes: &[u8]) -> Result<ObjectRef> {
2089
        self.put(ObjectType::Chunk, bytes)
2090
    }
2091
cdb5beba Andy McCall ↶ 2092
    /// Store a chunk that packs will keep uncompressed (`nocompress`, spec §13.3).
2093
    pub fn put_chunk_nocompress(&self, bytes: &[u8]) -> Result<ObjectRef> {
8f10a07a Andy McCall ↶ 2094
        let r = hash_object(ObjectType::Chunk, bytes);
64818938 Andy McCall ↶ 2095
        put_new_or_existing(
2096
            self.backend.as_ref(),
2097
            &format!("nocompress/{}", r.to_hex()),
2098
            b"",
2099
        )?;
8f10a07a Andy McCall ↶ 2100
        self.put(ObjectType::Chunk, bytes)
cdb5beba Andy McCall ↶ 2101
    }
2102
2103
    fn is_nocompress(&self, r: &ObjectRef) -> bool {
64818938 Andy McCall ↶ 2104
        self.backend
2105
            .has(&format!("nocompress/{}", r.to_hex()))
2106
            .unwrap_or(false)
2107
    }
2108
2109
    /// Every chunk marked to be kept raw.
2110
    fn nocompress_set(&self) -> Result<std::collections::HashSet<ObjectRef>> {
2111
        Ok(self
2112
            .backend
2113
            .list("nocompress/")
2114
            .map_err(backend_err)?
2115
            .into_iter()
2116
            .filter_map(|e| ObjectRef::from_hex(e.key.trim_start_matches("nocompress/")))
2117
            .collect())
cdb5beba Andy McCall ↶ 2118
    }
2119
86e682c2 Andy McCall ↶ 2120
    pub fn put_state(&self, st: &StateObject) -> Result<ObjectRef> {
2121
        self.put(ObjectType::State, &st.encode())
2122
    }
2123
2124
    pub fn get_state(&self, r: &ObjectRef) -> Result<StateObject> {
2125
        Ok(StateObject::decode(&self.get_typed(r, ObjectType::State)?)?)
2126
    }
2127
5b9c6ad4 Andy McCall ↶ 2128
    pub fn put_attest(&self, a: &Attest) -> Result<ObjectRef> {
2129
        self.put(ObjectType::Attest, &a.encode())
2130
    }
2131
2132
    pub fn get_attest(&self, r: &ObjectRef) -> Result<Attest> {
2133
        Ok(Attest::decode(&self.get_typed(r, ObjectType::Attest)?)?)
fc1a466a Andy McCall ↶ 2134
    }
2135
2136
    pub fn put_receipt(&self, r: &Receipt) -> Result<ObjectRef> {
2137
        self.put(ObjectType::Receipt, &r.encode())
5b9c6ad4 Andy McCall ↶ 2138
    }
2139
fc1a466a Andy McCall ↶ 2140
    pub fn get_receipt(&self, r: &ObjectRef) -> Result<Receipt> {
2141
        Ok(Receipt::decode(&self.get_typed(r, ObjectType::Receipt)?)?)
5b9c6ad4 Andy McCall ↶ 2142
    }
2143
64818938 Andy McCall ↶ 2144
    /// Record `value` under `key` in `index`: one write-once entry, so two
2145
    /// writers of a shared store never lose each other's.
2146
    fn index_add(&self, index: &str, key: &ObjectRef, value: &ObjectRef) -> Result<()> {
ef7d4ea6 Andy McCall ↶ 2147
        debug_assert!(ENTRY_INDEXES.iter().any(|(i, _)| *i == index));
64818938 Andy McCall ↶ 2148
        let entry = format!("{}{}", fanout_prefix(index, key), value.to_hex());
ef7d4ea6 Andy McCall ↶ 2149
        put_new_or_existing(self.backend.as_ref(), &entry, b"")?;
2150
        self.journal_add(entry)
fc1a466a Andy McCall ↶ 2151
    }
2152
64818938 Andy McCall ↶ 2153
    /// The values recorded under `key` in `index`, with those an older
2154
    /// store kept as one list in `legacy`, sorted.
2155
    fn index_get(&self, index: &str, legacy: &str, key: &ObjectRef) -> Result<Vec<ObjectRef>> {
2156
        let mut values: Vec<ObjectRef> = self
ef7d4ea6 Andy McCall ↶ 2157
            .entry_keys(&fanout_prefix(index, key))?
64818938 Andy McCall ↶ 2158
            .into_iter()
ef7d4ea6 Andy McCall ↶ 2159
            .filter_map(|k| k.rsplit('/').next().and_then(ObjectRef::from_hex))
64818938 Andy McCall ↶ 2160
            .collect();
ef7d4ea6 Andy McCall ↶ 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))? {
64818938 Andy McCall ↶ 2164
            values.extend(
2165
                String::from_utf8_lossy(&bytes)
2166
                    .lines()
2167
                    .filter_map(ObjectRef::from_hex),
2168
            );
5b9c6ad4 Andy McCall ↶ 2169
        }
fc1a466a Andy McCall ↶ 2170
        values.sort();
64818938 Andy McCall ↶ 2171
        values.dedup();
2172
        Ok(values)
5b9c6ad4 Andy McCall ↶ 2173
    }
2174
fc1a466a Andy McCall ↶ 2175
    /// Record that seal attestation `attest` covers `change`.
2176
    pub fn index_seal(&self, change: &ObjectRef, attest: &ObjectRef) -> Result<()> {
64818938 Andy McCall ↶ 2177
        self.index_add("seal", change, attest)
fc1a466a Andy McCall ↶ 2178
    }
2179
2180
    /// Seal attestations known to cover `change`.
2181
    pub fn seals_for(&self, change: &ObjectRef) -> Result<Vec<ObjectRef>> {
64818938 Andy McCall ↶ 2182
        self.index_get("seal", "seals", change)
fc1a466a Andy McCall ↶ 2183
    }
2184
2185
    /// Record receipt attestation `attest` for `state` (spec §14.6).
c63fed2e Andy McCall ↶ 2186
    /// Keep a short-lived key's certificate (spec §14.7), with its issuer's
2187
    /// signature, indexed by the key it certifies.
2188
    pub fn put_certificate(&self, c: &crate::cert::SignedCertificate) -> Result<ObjectRef> {
2189
        let r = self.put(ObjectType::Certificate, &c.cert.encode())?;
2190
        self.put_signature(&r, &c.sig)?;
2191
        self.index_add("cert", &c.cert.key_id(), &r)?;
2192
        Ok(r)
2193
    }
2194
2195
    /// A kept certificate with its issuer's signature.
2196
    pub fn get_certificate(&self, r: &ObjectRef) -> Result<crate::cert::SignedCertificate> {
2197
        let cert = crate::cert::Certificate::decode(&self.get_typed(r, ObjectType::Certificate)?)?;
2198
        let sig = self
2199
            .signatures(r)?
2200
            .into_iter()
2201
            .find(|s| s.key == cert.issuer)
2202
            .ok_or(StoreError::NotFound(*r))?;
2203
        Ok(crate::cert::SignedCertificate { cert, sig })
2204
    }
2205
2206
    /// The certificate kept for short-lived key `key`, if any.
2207
    pub fn certificate_of(&self, key: &ObjectRef) -> Result<Option<ObjectRef>> {
2208
        Ok(self
2209
            .index_get("cert", "certs", key)?
2210
            .into_iter()
2211
            .find(|r| self.has(r)))
2212
    }
2213
f53cbe7a Andy McCall ↶ 2214
    /// Record `value` under `key` in index `index` (for repository code).
2215
    pub(crate) fn index_add_pub(
2216
        &self,
2217
        index: &str,
2218
        key: &ObjectRef,
2219
        value: &ObjectRef,
2220
    ) -> Result<()> {
2221
        self.index_add(index, key, value)
2222
    }
2223
2224
    /// The values recorded under `key` in index `index` (which has no
2225
    /// older one-list form: the legacy name only has to differ from it).
2226
    pub(crate) fn index_get_pub(&self, index: &str, key: &ObjectRef) -> Result<Vec<ObjectRef>> {
2227
        self.index_get(index, &format!("{index}-lists"), key)
2228
    }
2229
fc1a466a Andy McCall ↶ 2230
    pub fn index_receipt(&self, state: &ObjectRef, attest: &ObjectRef) -> Result<()> {
64818938 Andy McCall ↶ 2231
        self.index_add("receipt", state, attest)
fc1a466a Andy McCall ↶ 2232
    }
2233
2234
    /// Receipt attestations recorded for `state`.
2235
    pub fn receipts_for(&self, state: &ObjectRef) -> Result<Vec<ObjectRef>> {
64818938 Andy McCall ↶ 2236
        self.index_get("receipt", "receipts", state)
fc1a466a Andy McCall ↶ 2237
    }
2238
3a4b3e01 Andy McCall ↶ 2239
    pub fn index_approval(&self, state: &ObjectRef, attest: &ObjectRef) -> Result<()> {
2240
        self.index_add("approval", state, attest)
2241
    }
2242
2243
    /// Approval attestations recorded for `state` (OxygenFlow approval
2244
    /// gates, oxygenhub#235).
2245
    pub fn approvals_for(&self, state: &ObjectRef) -> Result<Vec<ObjectRef>> {
2246
        self.index_get("approval", "approvals", state)
2247
    }
2248
2249
    /// Every state with an approval, for garbage collection.
2250
    pub fn approval_states(&self) -> Result<Vec<ObjectRef>> {
2251
        self.indexed_states("approval", "approvals")
2252
    }
2253
fc1a466a Andy McCall ↶ 2254
    /// Every state with a receipt, for garbage collection.
2255
    pub fn receipt_states(&self) -> Result<Vec<ObjectRef>> {
3a4b3e01 Andy McCall ↶ 2256
        self.indexed_states("receipt", "receipts")
2257
    }
2258
2259
    /// The states an index (`one/<hh>/<rest>/<attest>` entries, and the
2260
    /// packed `many/<hh>/<rest>` lists) holds attestations for.
2261
    fn indexed_states(&self, one: &str, many: &str) -> Result<Vec<ObjectRef>> {
64818938 Andy McCall ↶ 2262
        let mut out: Vec<ObjectRef> = Vec::new();
ef7d4ea6 Andy McCall ↶ 2263
        for key in self.entry_keys(&format!("{one}/"))? {
64818938 Andy McCall ↶ 2264
            // receipt/<hh>/<rest>/<attest>
ef7d4ea6 Andy McCall ↶ 2265
            let mut parts = key.splitn(4, '/');
64818938 Andy McCall ↶ 2266
            let (Some(_), Some(hh), Some(rest), Some(_)) =
2267
                (parts.next(), parts.next(), parts.next(), parts.next())
2268
            else {
2269
                continue;
2270
            };
2271
            out.extend(ObjectRef::from_hex(&format!("{hh}{rest}")));
fc1a466a Andy McCall ↶ 2272
        }
ef7d4ea6 Andy McCall ↶ 2273
        let legacy = if self.reads_legacy() {
2274
            self.backend
2275
                .list(&format!("{many}/"))
2276
                .map_err(backend_err)?
2277
        } else {
2278
            Vec::new()
2279
        };
2280
        for e in legacy {
64818938 Andy McCall ↶ 2281
            let mut parts = e.key.splitn(3, '/');
2282
            let (Some(_), Some(hh), Some(rest)) = (parts.next(), parts.next(), parts.next()) else {
2283
                continue;
2284
            };
2285
            out.extend(ObjectRef::from_hex(&format!("{hh}{rest}")));
2286
        }
fc1a466a Andy McCall ↶ 2287
        out.sort();
64818938 Andy McCall ↶ 2288
        out.dedup();
fc1a466a Andy McCall ↶ 2289
        Ok(out)
5b9c6ad4 Andy McCall ↶ 2290
    }
2291
1983686f Andy McCall ↶ 2292
    pub fn put_identity(&self, id: &Identity) -> Result<ObjectRef> {
2293
        self.put(ObjectType::Identity, &id.encode())
2294
    }
2295
2296
    pub fn get_identity(&self, r: &ObjectRef) -> Result<Identity> {
2297
        Ok(Identity::decode(&self.get_typed(r, ObjectType::Identity)?)?)
2298
    }
2299
64818938 Andy McCall ↶ 2300
    /// Signatures stored alongside object `r` (spec §5.4), one per signing
2301
    /// key, sorted by key. Empty if none.
1983686f Andy McCall ↶ 2302
    pub fn signatures(&self, r: &ObjectRef) -> Result<Vec<Signature>> {
64818938 Andy McCall ↶ 2303
        let mut sigs: Vec<Signature> = Vec::new();
ef7d4ea6 Andy McCall ↶ 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
            };
64818938 Andy McCall ↶ 2311
            sigs.extend(Signature::decode_list(&bytes)?);
1983686f Andy McCall ↶ 2312
        }
64818938 Andy McCall ↶ 2313
        // An older store's one list per object; a signer kept there and
2314
        // here counts once, as written here.
ef7d4ea6 Andy McCall ↶ 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 {
64818938 Andy McCall ↶ 2321
            for sig in Signature::decode_list(&bytes)? {
2322
                if !sigs.iter().any(|s| s.key == sig.key) {
2323
                    sigs.push(sig);
2324
                }
2325
            }
2326
        }
2327
        sigs.sort_by_key(|s| s.key);
2328
        sigs.dedup_by_key(|s| s.key);
2329
        Ok(sigs)
1983686f Andy McCall ↶ 2330
    }
2331
64818938 Andy McCall ↶ 2332
    /// Attach a signature to object `r`, replacing any earlier one by the
2333
    /// same key: one entry per signer, so the latest by a key is the one
2334
    /// kept, and two signers never touch each other's.
1983686f Andy McCall ↶ 2335
    pub fn put_signature(&self, r: &ObjectRef, sig: &Signature) -> Result<()> {
64818938 Andy McCall ↶ 2336
        let key = format!("{}{}", fanout_prefix("sig", r), sig.key.to_hex());
2337
        let bytes = Signature::encode_list(std::slice::from_ref(sig));
ef7d4ea6 Andy McCall ↶ 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() {
64818938 Andy McCall ↶ 2454
            return Ok(());
ef7d4ea6 Andy McCall ↶ 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);
1983686f Andy McCall ↶ 2526
        }
ef7d4ea6 Andy McCall ↶ 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)
1983686f Andy McCall ↶ 2563
    }
2564
54e2e630 Andy McCall ↶ 2565
    /// Finish packs a writer left unfinished: a batch writes its objects to
2566
    /// `.tmp-ID.pack` and only indexes it at the end, so a process that
2567
    /// stopped part way (an import killed after hours) leaves every object
2568
    /// in a pack nothing can read. Each such pack untouched for `min_age`
2569
    /// is read through: every whole entry checked against its reference,
2570
    /// a torn last one cut off, then indexed as a pack of the store.
2571
    /// Returns the packs and objects adopted.
2572
    pub fn adopt_unfinished_packs(
2573
        &self,
2574
        min_age: std::time::Duration,
2575
        progress: &dyn Fn(&str),
2576
    ) -> Result<(usize, usize)> {
2577
        use std::io::{BufReader, Read};
2578
        let mut packs = 0;
2579
        let mut objects = 0;
2580
        let Ok(dir) = fs::read_dir(&self.scratch) else {
2581
            return Ok((0, 0));
2582
        };
2583
        for entry in dir.flatten() {
2584
            let name = entry.file_name().to_string_lossy().to_string();
2585
            let Some(id) = name
2586
                .strip_prefix(".tmp-")
2587
                .and_then(|n| n.strip_suffix(".pack"))
2588
            else {
2589
                continue;
2590
            };
2591
            let path = entry.path();
2592
            let meta = fs::metadata(&path)?;
2593
            let age = meta
2594
                .modified()
2595
                .ok()
2596
                .and_then(|m| m.elapsed().ok())
2597
                .unwrap_or_default();
2598
            if age < min_age {
2599
                progress(&format!(
2600
                    "{name}: written to {}s ago, left alone",
2601
                    age.as_secs()
2602
                ));
2603
                continue;
2604
            }
2605
            let mut f = BufReader::with_capacity(1 << 20, fs::File::open(&path)?);
2606
            let mut magic = [0u8; 8];
2607
            if f.read_exact(&mut magic).is_err() || &magic != PACK_MAGIC {
2608
                progress(&format!("{name}: not a pack, left alone"));
2609
                continue;
2610
            }
2611
            let mut offset = PACK_MAGIC.len() as u64;
2612
            let mut index: Vec<(ObjectRef, PackEntry)> = Vec::new();
2613
            loop {
2614
                let mut head = [0u8; 42];
2615
                if f.read_exact(&mut head).is_err() {
2616
                    break;
2617
                }
2618
                let Some(ty) = ObjectType::ALL.get(head[0] as usize).copied() else {
2619
                    break;
2620
                };
2621
                let flags = head[1];
2622
                let stored_len = u32::from_le_bytes(head[2..6].try_into().unwrap());
2623
                let raw_len = u32::from_le_bytes(head[6..10].try_into().unwrap());
2624
                let r = ObjectRef(head[10..42].try_into().unwrap());
2625
                let mut stored = vec![0u8; stored_len as usize];
2626
                if flags & FLAG_DICT != 0 || f.read_exact(&mut stored).is_err() {
2627
                    break;
2628
                }
2629
                let e = PackEntry {
2630
                    offset: offset + 42,
2631
                    stored_len,
2632
                    raw_len,
2633
                    ty,
2634
                    flags,
2635
                };
2636
                if decode_entry(&r, e, stored, None).is_err() {
2637
                    break;
2638
                }
2639
                index.push((r, e));
2640
                offset += 42 + stored_len as u64;
2641
                if index.len().is_multiple_of(200_000) {
2642
                    progress(&format!(
2643
                        "{name}: {} objects checked, {} MiB",
2644
                        index.len(),
2645
                        offset >> 20
2646
                    ));
2647
                }
2648
            }
2649
            if index.is_empty() {
2650
                progress(&format!("{name}: no whole object, left alone"));
2651
                continue;
2652
            }
2653
            if offset < meta.len() {
2654
                progress(&format!(
2655
                    "{name}: {} torn bytes at the end cut off",
2656
                    meta.len() - offset
2657
                ));
2658
                fs::OpenOptions::new()
2659
                    .write(true)
2660
                    .open(&path)?
2661
                    .set_len(offset)?;
2662
            }
2663
            index.sort_by_key(|(r, _)| *r);
2664
            index.dedup_by_key(|(r, _)| *r);
2665
            objects += index.len();
2666
            packs += 1;
2667
            progress(&format!("{name}: {} objects indexed", index.len()));
2668
            self.finish_pack_files(id, &path, &index)?;
2669
        }
2670
        Ok((packs, objects))
2671
    }
2672
d02e545d Andy McCall ↶ 2673
    /// Every object reference in the store, loose and packed (slow; for gc and tests).
ff0af1ee unknown 2674
    pub fn list(&self) -> io::Result<Vec<ObjectRef>> {
64818938 Andy McCall ↶ 2675
        let mut out: Vec<ObjectRef> = self
2676
            .loose_objects()
2677
            .map_err(|e| io::Error::other(e.to_string()))?
2678
            .into_iter()
2679
            .map(|(r, _)| r)
2680
            .collect();
d02e545d Andy McCall ↶ 2681
        for p in self.packs.borrow().iter() {
2682
            out.extend(p.refs.iter().copied());
ff0af1ee unknown 2683
        }
2684
        out.sort();
d02e545d Andy McCall ↶ 2685
        out.dedup();
ff0af1ee unknown 2686
        Ok(out)
767ac6cd Andy McCall 2687
    }
2688
}
2689
2690
/// Whether a read failed in a way a newer list of packs may cure: the
2691
/// object was not found, or its pack's dictionary is gone.
2692
fn is_miss(e: &StoreError) -> bool {
2693
    match e {
2694
        StoreError::NotFound(_) => true,
2695
        StoreError::Corrupt(_, why) => why == NO_DICT,
2696
        _ => false,
ff0af1ee unknown 2697
    }
afaba5f1 Andy McCall ↶ 2698
}
2699
64818938 Andy McCall ↶ 2700
/// A loose object's bytes, verified against `r`.
2701
fn parse_loose(data: &[u8], r: &ObjectRef) -> Result<Object> {
2702
    let nul = data
2703
        .iter()
2704
        .position(|&b| b == 0)
2705
        .ok_or_else(|| StoreError::Corrupt(*r, "no header".into()))?;
2706
    let head = std::str::from_utf8(&data[..nul])
2707
        .map_err(|_| StoreError::Corrupt(*r, "header not UTF-8".into()))?;
2708
    let mut parts = head.split(' ');
2709
    let (Some("oxy1"), Some(ty), Some(len), None) =
2710
        (parts.next(), parts.next(), parts.next(), parts.next())
2711
    else {
2712
        return Err(StoreError::Corrupt(*r, format!("bad header {head:?}")));
afaba5f1 Andy McCall ↶ 2713
    };
64818938 Andy McCall ↶ 2714
    let ty = ObjectType::from_name(ty)
2715
        .ok_or_else(|| StoreError::Corrupt(*r, format!("unknown type {ty}")))?;
2716
    let len: usize = len
2717
        .parse()
2718
        .map_err(|_| StoreError::Corrupt(*r, "bad length".into()))?;
2719
    let body = &data[nul + 1..];
2720
    if body.len() != len {
2721
        return Err(StoreError::Corrupt(
2722
            *r,
2723
            format!("length {} != header {len}", body.len()),
2724
        ));
2725
    }
2726
    if hash_object(ty, body) != *r {
2727
        return Err(StoreError::Corrupt(*r, "hash mismatch".into()));
2728
    }
2729
    Ok((ty, body.to_vec()))
2730
}
2731
2732
/// Whether the objects are files that a test may tamper with: not under
2733
/// `OXY_STORE_BACKEND=memory`.
2734
#[cfg(test)]
2735
fn files_on_disk() -> bool {
2736
    std::env::var_os("OXY_STORE_BACKEND").is_none()
ff0af1ee unknown 2737
}
2738
2739
#[cfg(test)]
2740
mod tests {
2741
    use super::*;
64818938 Andy McCall ↶ 2742
2743
    /// The store under test twice: in a directory, and on a store in
2744
    /// memory with a scratch directory, as over a bucket.
2745
    fn each_store(dir: &Path, f: impl Fn(&str, Store)) {
2746
        f("directory", Store::open(dir.join("objects")).unwrap());
2747
        f(
2748
            "memory",
2749
            Store::open_on(Arc::new(MemoryStore::new()), dir.join("scratch")).unwrap(),
2750
        );
2751
    }
ff0af1ee unknown 2752
2753
    #[test]
2754
    fn put_get_verify() {
64818938 Andy McCall ↶ 2755
        if !super::files_on_disk() {
2756
            return;
2757
        }
ff0af1ee unknown 2758
        let dir = tempfile::tempdir().unwrap();
2759
        let s = Store::open(dir.path().join("objects")).unwrap();
2760
        let r = s.put(ObjectType::Chunk, b"hello").unwrap();
2761
        assert!(s.has(&r));
2762
        assert_eq!(s.get(&r).unwrap(), (ObjectType::Chunk, b"hello".to_vec()));
2763
        assert_eq!(s.put(ObjectType::Chunk, b"hello").unwrap(), r);
2764
        assert_eq!(s.list().unwrap(), vec![r]);
75167672 Andy McCall 2765
        assert!(matches!(
2766
            s.get_typed(&r, ObjectType::Blob),
2767
            Err(StoreError::WrongType(..))
2768
        ));
2769
        assert!(matches!(
2770
            s.get(&ObjectRef([9; 32])),
2771
            Err(StoreError::NotFound(_))
2772
        ));
ff0af1ee unknown 2773
2774
        // Tamper: flip a byte, read must fail.
2775
        let path = s.path_for(&r);
2776
        let mut data = fs::read(&path).unwrap();
2777
        let last = data.len() - 1;
2778
        data[last] ^= 1;
2779
        fs::write(&path, data).unwrap();
2780
        assert!(matches!(s.get(&r), Err(StoreError::Corrupt(..))));
2781
    }
2782
2783
    #[test]
d02e545d Andy McCall ↶ 2784
    fn packing_keeps_every_object_readable() {
64818938 Andy McCall ↶ 2785
        if !super::files_on_disk() {
2786
            return;
2787
        }
d02e545d Andy McCall ↶ 2788
        let dir = tempfile::tempdir().unwrap();
2789
        let s = Store::open(dir.path()).unwrap();
2790
        let mut refs = Vec::new();
2791
        for i in 0..300u32 {
2792
            let body = format!("object {i} ").repeat(1 + (i % 7) as usize);
2793
            refs.push((s.put(ObjectType::Chunk, body.as_bytes()).unwrap(), body));
2794
        }
2795
        let c = crate::objects::tests::sample_change();
2796
        let cr = s.put_change(&c).unwrap();
2797
        assert_eq!(s.loose_count().unwrap(), 301);
2798
        let report = s.pack_loose().unwrap();
2799
        assert_eq!(report.objects, 301);
2800
        assert!(report.packed_bytes < report.raw_bytes, "{report:?}");
2801
        assert_eq!(s.loose_count().unwrap(), 0);
2802
        assert_eq!(s.pack_count(), 1);
2803
        for (r, body) in &refs {
2804
            assert!(s.has(r));
75167672 Andy McCall 2805
            assert_eq!(
2806
                s.get(r).unwrap(),
2807
                (ObjectType::Chunk, body.as_bytes().to_vec())
2808
            );
d02e545d Andy McCall ↶ 2809
        }
2810
        assert_eq!(s.get_change(&cr).unwrap(), c);
2811
        assert_eq!(s.list().unwrap().len(), 301);
2812
        // Re-putting a packed object is a no-op; new objects go loose and a
2813
        // second pack coexists with the first.
75167672 Andy McCall 2814
        assert_eq!(
2815
            s.put(ObjectType::Chunk, refs[0].1.as_bytes()).unwrap(),
2816
            refs[0].0
2817
        );
d02e545d Andy McCall ↶ 2818
        assert_eq!(s.loose_count().unwrap(), 0);
2819
        let extra = s.put(ObjectType::Chunk, b"after packing").unwrap();
2820
        s.pack_loose().unwrap();
2821
        assert_eq!(s.pack_count(), 2);
2822
        assert_eq!(s.get(&extra).unwrap().1, b"after packing");
2823
        // A reopened store sees the packs.
2824
        let s2 = Store::open(dir.path()).unwrap();
2825
        assert_eq!(s2.pack_count(), 2);
2826
        assert_eq!(s2.get(&refs[5].0).unwrap().1, refs[5].1.as_bytes());
75167672 Andy McCall 2827
        assert!(matches!(
2828
            s2.get(&ObjectRef([9; 32])),
2829
            Err(StoreError::NotFound(_))
2830
        ));
d02e545d Andy McCall ↶ 2831
        // Corrupting an object's body inside the pack is detected on read.
2832
        let target = refs[5].0;
2833
        let (pack_path, offset) = {
2834
            let packs = s2.packs.borrow();
2835
            let (i, e) = s2.find_packed(&target).unwrap();
64818938 Andy McCall ↶ 2836
            (dir.path().join(&packs[i].key), e.offset as usize)
d02e545d Andy McCall ↶ 2837
        };
2838
        let mut data = fs::read(&pack_path).unwrap();
2839
        data[offset] ^= 0xff;
2840
        fs::write(&pack_path, data).unwrap();
2841
        let s3 = Store::open(dir.path()).unwrap();
2842
        assert!(matches!(s3.get(&target), Err(StoreError::Corrupt(..))));
75167672 Andy McCall 2843
        assert_eq!(
2844
            s3.get(&refs[6].0).unwrap().1,
2845
            refs[6].1.as_bytes(),
2846
            "other objects are unaffected"
2847
        );
a17fc622 Andy McCall ↶ 2848
    }
2849
2850
    #[test]
2851
    fn a_full_repack_trains_a_dictionary_and_reads_back() {
64818938 Andy McCall ↶ 2852
        if !super::files_on_disk() {
2853
            return;
2854
        }
a17fc622 Andy McCall ↶ 2855
        let dir = tempfile::tempdir().unwrap();
2856
        let s = Store::open(dir.path()).unwrap();
2857
        // Many small, alike files: what a dictionary is for.
2858
        let bodies: Vec<Vec<u8>> = (0..1500)
2859
            .map(|i| {
2860
                format!(
2861
                    "// SPDX-License-Identifier: GPL-2.0\n#include <linux/module.h>\n\
2862
                     static int probe_{i}(struct device *dev)\n{{\n\treturn {i};\n}}\n"
2863
                )
2864
                .into_bytes()
2865
            })
2866
            .collect();
2867
        let refs: Vec<ObjectRef> = bodies
2868
            .iter()
2869
            .map(|b| s.put(ObjectType::Chunk, b).unwrap())
2870
            .collect();
2871
        s.repack_all(REPACK_ZSTD_LEVEL).unwrap();
2872
        let dicts = fs::read_dir(dir.path())
2873
            .unwrap()
2874
            .flatten()
2875
            .filter(|e| e.path().extension().is_some_and(|x| x == "dict"))
2876
            .count();
2877
        assert_eq!(dicts, 1, "the pack has a dictionary");
2878
        let with_dict = refs
2879
            .iter()
2880
            .filter(|r| s.find_packed(r).unwrap().1.flags & FLAG_DICT != 0)
2881
            .count();
2882
        assert!(with_dict > 1000, "most use it: {with_dict}");
2883
        let again = Store::open(dir.path()).unwrap();
2884
        for (r, b) in refs.iter().zip(&bodies) {
2885
            assert_eq!(&again.get(r).unwrap().1, b);
2886
        }
a67914bc Andy McCall ↶ 2887
        let packs = |dir: &Path| {
2888
            let mut v: Vec<String> = fs::read_dir(dir)
2889
                .unwrap()
2890
                .flatten()
2891
                .map(|e| e.file_name().to_string_lossy().into_owned())
2892
                .filter(|n| n.starts_with("pack-"))
2893
                .collect();
2894
            v.sort();
2895
            v
2896
        };
2897
        // With nothing new, a repack leaves that pack as it is: it would
2898
        // only write the same again.
2899
        let before = packs(dir.path());
2900
        let (report, merged) = again.repack_all(REPACK_ZSTD_LEVEL).unwrap();
2901
        assert_eq!((report.objects, merged), (0, 0));
2902
        assert_eq!(packs(dir.path()), before);
767ac6cd Andy McCall 2903
        // One new object: a repack replaces the pack and its dictionary,
2904
        // retiring the old ones, which a collection an hour on deletes.
a67914bc Andy McCall ↶ 2905
        let extra = again.put(ObjectType::Chunk, b"one more\n").unwrap();
a17fc622 Andy McCall ↶ 2906
        again.repack_all(REPACK_ZSTD_LEVEL).unwrap();
a67914bc Andy McCall ↶ 2907
        let files = packs(dir.path());
767ac6cd Andy McCall 2908
        assert_eq!(
2909
            files.len(),
2910
            7,
2911
            "two of pack, index and dictionary, and the retired record: {files:?}"
2912
        );
2913
        assert_eq!(again.pack_count(), 1, "the old pack is not read");
a17fc622 Andy McCall ↶ 2914
        assert_eq!(&again.get(&refs[7]).unwrap().1, &bodies[7]);
a67914bc Andy McCall ↶ 2915
        assert_eq!(again.get(&extra).unwrap().1, b"one more\n");
767ac6cd Andy McCall 2916
        again
2917
            .expire_retired(std::time::Duration::ZERO, &mut silent())
2918
            .unwrap();
2919
        let files = packs(dir.path());
2920
        assert_eq!(files.len(), 4, "one pack, index, dictionary: {files:?}");
2921
        assert!(before
2922
            .iter()
2923
            .filter(|f| *f != RETIRED_KEY)
2924
            .all(|f| !files.contains(f)));
2925
        assert_eq!(&again.get(&refs[8]).unwrap().1, &bodies[8]);
d02e545d Andy McCall ↶ 2926
    }
2927
2928
    #[test]
cdb5beba Andy McCall ↶ 2929
    fn nocompress_chunks_pack_raw() {
2930
        let dir = tempfile::tempdir().unwrap();
2931
        let s = Store::open(dir.path()).unwrap();
2932
        let body = vec![b'a'; 10_000];
2933
        let raw = s.put_chunk_nocompress(&body).unwrap();
2934
        let compressible = s.put(ObjectType::Chunk, &vec![b'b'; 10_000]).unwrap();
2935
        s.pack_loose().unwrap();
2936
        let packs = s.packs.borrow();
2937
        let (i, e) = s.find_packed(&raw).unwrap();
2938
        assert_eq!((e.flags, e.stored_len as usize), (0, 10_000));
2939
        let (_, c) = s.find_packed(&compressible).unwrap();
2940
        assert_eq!(c.flags, FLAG_ZSTD);
2941
        assert!(c.stored_len < 10_000);
64818938 Andy McCall ↶ 2942
        assert_eq!(packs[i].fetch(s.backend.as_ref(), &raw, e).unwrap().1, body);
cdb5beba Andy McCall ↶ 2943
    }
2944
2945
    #[test]
135212de Andy McCall ↶ 2946
    fn many_packs_merge_into_one() {
2947
        let dir = tempfile::tempdir().unwrap();
64818938 Andy McCall ↶ 2948
        each_store(dir.path(), |which, s| {
2949
            let mut refs = Vec::new();
2950
            for i in 0..(MAX_PACKS + 2) {
2951
                refs.push(
2952
                    s.put(ObjectType::Chunk, format!("pack {i}").as_bytes())
2953
                        .unwrap(),
2954
                );
2955
                s.pack_loose().unwrap();
2956
            }
2957
            assert_eq!(s.pack_count(), MAX_PACKS + 2, "{which}");
2958
            assert_eq!(s.merge_packs().unwrap(), MAX_PACKS + 2);
2959
            assert_eq!(s.pack_count(), 1);
2960
            assert_eq!(s.merge_packs().unwrap(), 0);
2961
            for (i, r) in refs.iter().enumerate() {
2962
                assert_eq!(s.get(r).unwrap().1, format!("pack {i}").as_bytes());
2963
            }
2964
            let again = Store::open_on(s.backend().clone(), s.dir()).unwrap();
2965
            assert_eq!(again.pack_count(), 1, "{which}: reopened");
2966
        });
135212de Andy McCall ↶ 2967
    }
2968
2969
    #[test]
ba160f60 Andy McCall ↶ 2970
    fn prune_removes_unreachable_loose_and_packed_objects() {
2971
        use std::collections::HashSet;
2972
        let dir = tempfile::tempdir().unwrap();
64818938 Andy McCall ↶ 2973
        each_store(dir.path(), |which, s| {
2974
            let keep_a = s.put(ObjectType::Chunk, b"keep a").unwrap();
2975
            let drop_a = s.put(ObjectType::Chunk, b"drop a").unwrap();
2976
            s.pack_loose().unwrap();
2977
            let keep_b = s.put(ObjectType::Chunk, b"keep b").unwrap();
2978
            let drop_b = s.put(ObjectType::Chunk, b"drop b").unwrap();
2979
            let sk = crate::identity::SecretKey {
2980
                ed_seed: [1; 32],
2981
                ml_seed: [2; 32],
2982
            };
2983
            let id = sk.identity("a", "b", 0);
2984
            let key = s.put_identity(&id).unwrap();
2985
            s.put_signature(
2986
                &drop_b,
2987
                &sk.sign(crate::identity::Domain::Change, &key, &drop_b),
2988
            )
75167672 Andy McCall 2989
            .unwrap();
64818938 Andy McCall ↶ 2990
            s.index_seal(&drop_a, &ObjectRef([3; 32])).unwrap();
2991
            let keep: HashSet<ObjectRef> = [keep_a, keep_b, key].into_iter().collect();
2992
            // Everything is too young under a grace period.
2993
            let r = s
2994
                .prune(&keep, std::time::Duration::from_secs(3600))
2995
                .unwrap();
2996
            assert_eq!((r.objects, r.deferred), (0, 2), "{which}");
2997
            let r = s.prune(&keep, std::time::Duration::ZERO).unwrap();
2998
            assert_eq!(r.objects, 2, "{which}: {r:?}");
2999
            assert!(s.has(&keep_a) && s.has(&keep_b) && s.has(&key));
3000
            assert!(!s.has(&drop_a) && !s.has(&drop_b));
3001
            assert!(s.signatures(&drop_b).unwrap().is_empty());
3002
            assert!(s.seals_for(&drop_a).unwrap().is_empty());
3003
            assert_eq!(s.pack_count(), 1);
3004
            assert_eq!(s.get(&keep_a).unwrap().1, b"keep a");
3005
            assert_eq!(s.list().unwrap().len(), 3);
3006
        });
ba160f60 Andy McCall ↶ 3007
    }
3008
3009
    #[test]
1983686f Andy McCall ↶ 3010
    fn signatures_are_stored_beside_objects() {
64818938 Andy McCall ↶ 3011
        use crate::identity::{Domain, SecretKey};
3012
        let dir = tempfile::tempdir().unwrap();
3013
        each_store(dir.path(), |which, s| {
3014
            let r = s.put(ObjectType::Chunk, b"x").unwrap();
3015
            assert!(s.signatures(&r).unwrap().is_empty());
3016
            let sk = SecretKey {
3017
                ed_seed: [1; 32],
3018
                ml_seed: [2; 32],
3019
            };
3020
            let id = sk.identity("a", "b", 0);
3021
            let key = s.put_identity(&id).unwrap();
3022
            assert_eq!(s.get_identity(&key).unwrap(), id);
3023
            let sig = sk.sign(Domain::Change, &key, &r);
3024
            s.put_signature(&r, &sig).unwrap();
3025
            s.put_signature(&r, &sig).unwrap();
3026
            assert_eq!(s.signatures(&r).unwrap(), vec![sig.clone()], "{which}");
3027
            // A second signer is kept beside the first, sorted by key.
3028
            let sk2 = SecretKey {
3029
                ed_seed: [3; 32],
3030
                ml_seed: [4; 32],
3031
            };
3032
            let key2 = s.put_identity(&sk2.identity("c", "d", 0)).unwrap();
3033
            let sig2 = sk2.sign(Domain::Change, &key2, &r);
3034
            s.put_signature(&r, &sig2).unwrap();
3035
            let mut both = vec![sig.clone(), sig2.clone()];
3036
            both.sort_by_key(|s| s.key);
3037
            assert_eq!(s.signatures(&r).unwrap(), both);
3038
            // The object itself is untouched by its signatures.
3039
            assert_eq!(s.get(&r).unwrap().1, b"x");
3040
            // Seal and receipt entries, and the states with receipts.
3041
            s.index_seal(&r, &ObjectRef([5; 32])).unwrap();
3042
            s.index_seal(&r, &ObjectRef([4; 32])).unwrap();
3043
            s.index_seal(&r, &ObjectRef([5; 32])).unwrap();
3044
            assert_eq!(
3045
                s.seals_for(&r).unwrap(),
3046
                vec![ObjectRef([4; 32]), ObjectRef([5; 32])]
3047
            );
3048
            let st = ObjectRef([6; 32]);
3049
            s.index_receipt(&st, &ObjectRef([7; 32])).unwrap();
3050
            assert_eq!(s.receipts_for(&st).unwrap(), vec![ObjectRef([7; 32])]);
3051
            assert_eq!(s.receipt_states().unwrap(), vec![st]);
3052
        });
3053
    }
3054
3055
    /// A store written before signatures, seals and receipts were one
3056
    /// entry each (`sigs/`, `seals/`, `receipts/` lists) still reads, and
3057
    /// what is written now sits beside what was there.
3058
    #[test]
3059
    fn an_older_store_s_sidecar_lists_are_still_read() {
3060
        if !super::files_on_disk() {
3061
            return;
3062
        }
1983686f Andy McCall ↶ 3063
        use crate::identity::{Domain, SecretKey};
3064
        let dir = tempfile::tempdir().unwrap();
3065
        let s = Store::open(dir.path()).unwrap();
64818938 Andy McCall ↶ 3066
        let r = s.put(ObjectType::Chunk, b"old").unwrap();
3067
        let h = r.to_hex();
75167672 Andy McCall 3068
        let sk = SecretKey {
3069
            ed_seed: [1; 32],
3070
            ml_seed: [2; 32],
3071
        };
64818938 Andy McCall ↶ 3072
        let key = s.put_identity(&sk.identity("a", "b", 0)).unwrap();
1983686f Andy McCall ↶ 3073
        let sig = sk.sign(Domain::Change, &key, &r);
64818938 Andy McCall ↶ 3074
        let legacy = dir.path().join("sigs").join(&h[..2]);
3075
        fs::create_dir_all(&legacy).unwrap();
3076
        fs::write(
3077
            legacy.join(&h[2..]),
3078
            Signature::encode_list(std::slice::from_ref(&sig)),
3079
        )
3080
        .unwrap();
3081
        let seals = dir.path().join("seals").join(&h[..2]);
3082
        fs::create_dir_all(&seals).unwrap();
3083
        fs::write(
3084
            seals.join(&h[2..]),
3085
            format!("{}\n", ObjectRef([9; 32]).to_hex()),
3086
        )
3087
        .unwrap();
3088
        let receipts = dir.path().join("receipts").join(&h[..2]);
3089
        fs::create_dir_all(&receipts).unwrap();
3090
        fs::write(
3091
            receipts.join(&h[2..]),
3092
            format!("{}\n", ObjectRef([8; 32]).to_hex()),
3093
        )
3094
        .unwrap();
3095
3096
        assert_eq!(s.signatures(&r).unwrap(), vec![sig.clone()]);
3097
        assert_eq!(s.seals_for(&r).unwrap(), vec![ObjectRef([9; 32])]);
3098
        assert_eq!(s.receipts_for(&r).unwrap(), vec![ObjectRef([8; 32])]);
3099
        assert_eq!(s.receipt_states().unwrap(), vec![r]);
3100
        // New entries join the old ones; the same signer is not doubled.
1983686f Andy McCall ↶ 3101
        s.put_signature(&r, &sig).unwrap();
64818938 Andy McCall ↶ 3102
        s.index_seal(&r, &ObjectRef([1; 32])).unwrap();
3103
        assert_eq!(s.signatures(&r).unwrap().len(), 1);
3104
        assert_eq!(
3105
            s.seals_for(&r).unwrap(),
3106
            vec![ObjectRef([1; 32]), ObjectRef([9; 32])]
3107
        );
3108
        // Pruning the object takes both layouts with it.
3109
        let r2 = s.put(ObjectType::Chunk, b"kept").unwrap();
3110
        let keep = [r2, key].into_iter().collect();
3111
        s.prune(&keep, std::time::Duration::ZERO).unwrap();
3112
        assert!(s.signatures(&r).unwrap().is_empty());
3113
        assert!(s.seals_for(&r).unwrap().is_empty());
3114
        assert!(s.receipt_states().unwrap().is_empty());
1983686f Andy McCall ↶ 3115
    }
3116
3117
    #[test]
ff0af1ee unknown 3118
    fn change_round_trip_through_store() {
3119
        let dir = tempfile::tempdir().unwrap();
64818938 Andy McCall ↶ 3120
        each_store(dir.path(), |which, s| {
3121
            let c = crate::objects::tests::sample_change();
3122
            let r = s.put_change(&c).unwrap();
3123
            assert_eq!(r, c.hash(), "{which}");
3124
            assert_eq!(s.get_change(&r).unwrap(), c);
3125
            assert_eq!(s.get_change_meta(&r).unwrap().message, c.message);
3126
        });
ff0af1ee unknown 3127
    }
2b872d07 Andy McCall ↶ 3128
3129
    fn xorshift(state: &mut u64) -> u64 {
3130
        *state ^= *state << 13;
3131
        *state ^= *state >> 7;
3132
        *state ^= *state << 17;
3133
        *state
3134
    }
3135
3136
    /// Random corruption of pack and index files never panics: reads fail
3137
    /// with an error or the store refuses to open, and untouched objects
3138
    /// stay readable when the index survives.
3139
    #[test]
3140
    fn corrupted_packs_and_indexes_fail_cleanly() {
64818938 Andy McCall ↶ 3141
        if !super::files_on_disk() {
3142
            return;
3143
        }
2b872d07 Andy McCall ↶ 3144
        let dir = tempfile::tempdir().unwrap();
3145
        let objects = dir.path().join("objects");
3146
        let s = Store::open(&objects).unwrap();
3147
        let mut refs = Vec::new();
3148
        let mut seed = 0x9e3779b97f4a7c15u64;
3149
        for i in 0..40u32 {
3150
            let len = (xorshift(&mut seed) % 5000) as usize;
3151
            let mut body = vec![0u8; len];
3152
            for b in body.iter_mut() {
75167672 Andy McCall 3153
                *b = if i % 2 == 0 {
3154
                    (xorshift(&mut seed) & 0xff) as u8
3155
                } else {
3156
                    b'a' + (i % 26) as u8
3157
                };
2b872d07 Andy McCall ↶ 3158
            }
3159
            refs.push(s.put(ObjectType::Chunk, &body).unwrap());
3160
        }
3161
        s.pack_loose().unwrap();
75167672 Andy McCall 3162
        let pack = fs::read_dir(&objects)
3163
            .unwrap()
3164
            .filter_map(|e| e.ok().map(|e| e.path()))
3165
            .find(|p| p.extension().is_some_and(|x| x == "pack"))
3166
            .unwrap();
2b872d07 Andy McCall ↶ 3167
        let idx = pack.with_extension("idx");
3168
        let pristine_pack = fs::read(&pack).unwrap();
3169
        let pristine_idx = fs::read(&idx).unwrap();
3170
3171
        for round in 0..300u32 {
75167672 Andy McCall 3172
            let (path, pristine) = if round % 2 == 0 {
3173
                (&pack, &pristine_pack)
3174
            } else {
3175
                (&idx, &pristine_idx)
3176
            };
2b872d07 Andy McCall ↶ 3177
            let mut data = pristine.clone();
3178
            match xorshift(&mut seed) % 4 {
3179
                0 => {
3180
                    let at = (xorshift(&mut seed) as usize) % data.len();
3181
                    data[at] ^= 1 << (xorshift(&mut seed) % 8);
3182
                }
3183
                1 => {
3184
                    let at = (xorshift(&mut seed) as usize) % data.len();
3185
                    data[at] = (xorshift(&mut seed) & 0xff) as u8;
3186
                }
3187
                2 => {
3188
                    let keep = (xorshift(&mut seed) as usize) % data.len();
3189
                    data.truncate(keep);
3190
                }
3191
                _ => {
3192
                    let at = (xorshift(&mut seed) as usize) % data.len();
3193
                    let n = ((xorshift(&mut seed) % 8) + 1) as usize;
3194
                    for k in 0..n.min(data.len() - at) {
3195
                        data[at + k] = 0xff;
3196
                    }
3197
                }
3198
            }
3199
            fs::write(path, &data).unwrap();
3200
            // Opening may refuse a broken index; a store that opens must
3201
            // either return each object intact or report it corrupt/missing.
3202
            if let Ok(s2) = Store::open(&objects) {
3203
                for r in &refs {
3204
                    match s2.get(r) {
75167672 Andy McCall 3205
                        Ok((ty, body)) => assert_eq!(
3206
                            hash_object(ty, &body),
3207
                            *r,
3208
                            "a returned object always matches its reference"
3209
                        ),
3210
                        Err(StoreError::Corrupt(..))
3211
                        | Err(StoreError::NotFound(_))
3212
                        | Err(StoreError::Io(_)) => {}
2b872d07 Andy McCall ↶ 3213
                        Err(e) => panic!("unexpected error {e}"),
3214
                    }
3215
                }
3216
            }
3217
            fs::write(path, pristine).unwrap();
3218
        }
3219
        // Restored files read back in full.
3220
        let s3 = Store::open(&objects).unwrap();
3221
        for r in &refs {
3222
            let (ty, body) = s3.get(r).unwrap();
3223
            assert_eq!(hash_object(ty, &body), *r);
3224
        }
3225
    }
8f10a07a Andy McCall ↶ 3226
3227
    /// A small batch becomes loose objects; a large one becomes a single
3228
    /// pack. Objects are readable while pending, and batches nest.
3229
    #[test]
3230
    fn batches_write_loose_or_one_pack() {
3231
        let dir = tempfile::tempdir().unwrap();
64818938 Andy McCall ↶ 3232
        each_store(dir.path(), |which, s| {
3233
            let outer = s.begin_batch().unwrap();
3234
            let a = s.put(ObjectType::Chunk, b"small a").unwrap();
3235
            {
3236
                let inner = s.begin_batch().unwrap();
3237
                let b = s.put(ObjectType::Chunk, b"small b").unwrap();
3238
                assert!(s.has(&b));
3239
                assert_eq!(s.get(&b).unwrap().1, b"small b");
3240
                inner.finish().unwrap();
3241
                assert_eq!(
3242
                    s.loose_count().unwrap(),
3243
                    0,
3244
                    "{which}: the inner batch defers to the outer one"
3245
                );
3246
            }
3247
            assert_eq!(s.get(&a).unwrap().1, b"small a");
75167672 Andy McCall 3248
            assert_eq!(
64818938 Andy McCall ↶ 3249
                s.put(ObjectType::Chunk, b"small a").unwrap(),
3250
                a,
3251
                "idempotent while pending"
75167672 Andy McCall 3252
            );
64818938 Andy McCall ↶ 3253
            outer.finish().unwrap();
3254
            assert_eq!(s.loose_count().unwrap(), 2);
3255
            assert_eq!(s.pack_count(), 0);
3256
            assert_eq!(s.get(&a).unwrap().1, b"small a");
8f10a07a Andy McCall ↶ 3257
64818938 Andy McCall ↶ 3258
            // Large: spills into one pack, readable during and after.
3259
            let big = s.begin_batch().unwrap();
3260
            let mut refs = Vec::new();
3261
            let mut seed = 7u64;
3262
            for i in 0..300u32 {
3263
                let mut body = vec![b'z'; 40_000];
3264
                if i % 3 == 0 {
3265
                    for b in body.iter_mut() {
3266
                        *b = (xorshift(&mut seed) & 0xff) as u8;
3267
                    }
8f10a07a Andy McCall ↶ 3268
                }
64818938 Andy McCall ↶ 3269
                body[..4].copy_from_slice(&i.to_le_bytes());
3270
                let r = if i % 5 == 0 {
3271
                    s.put_chunk_nocompress(&body).unwrap()
3272
                } else {
3273
                    s.put(ObjectType::Chunk, &body).unwrap()
3274
                };
3275
                assert_eq!(
3276
                    s.get(&r).unwrap().1,
3277
                    body,
3278
                    "readable while pending (object {i})"
3279
                );
3280
                refs.push((r, body));
8f10a07a Andy McCall ↶ 3281
            }
64818938 Andy McCall ↶ 3282
            assert!(s.has(&refs[0].0));
3283
            big.finish().unwrap();
3284
            assert_eq!(s.pack_count(), 1);
75167672 Andy McCall 3285
            assert_eq!(
64818938 Andy McCall ↶ 3286
                s.loose_count().unwrap(),
3287
                2,
3288
                "nothing from the big batch is loose"
75167672 Andy McCall 3289
            );
64818938 Andy McCall ↶ 3290
            assert!(fs::read_dir(s.dir())
75167672 Andy McCall 3291
                .unwrap()
64818938 Andy McCall ↶ 3292
                .flatten()
3293
                .all(|e| !e.file_name().to_string_lossy().contains("tmp-")));
3294
            let s2 = Store::open_on(s.backend().clone(), s.dir()).unwrap();
3295
            for (r, body) in &refs {
3296
                assert_eq!(s2.get(r).unwrap().1, *body);
3297
            }
3298
            // A dropped batch still writes its objects.
3299
            {
3300
                let _b = s2.begin_batch().unwrap();
3301
                s2.put(ObjectType::Chunk, b"dropped").unwrap();
3302
            }
3303
            assert_eq!(
3304
                s2.get(&hash_object(ObjectType::Chunk, b"dropped"))
3305
                    .unwrap()
3306
                    .1,
3307
                b"dropped"
3308
            );
3309
        });
afaba5f1 Andy McCall ↶ 3310
    }
3311
3312
    #[test]
3313
    fn get_many_reads_loose_and_packed_objects_in_order() {
3314
        let dir = tempfile::tempdir().unwrap();
64818938 Andy McCall ↶ 3315
        each_store(dir.path(), |which, s| {
3316
            let packed: Vec<ObjectRef> = (0..20)
3317
                .map(|i| {
3318
                    s.put(ObjectType::Chunk, format!("packed {i}").as_bytes())
3319
                        .unwrap()
3320
                })
3321
                .collect();
3322
            s.pack_loose().unwrap();
3323
            let loose: Vec<ObjectRef> = (0..20)
3324
                .map(|i| {
3325
                    s.put(ObjectType::Chunk, format!("loose {i}").as_bytes())
3326
                        .unwrap()
3327
                })
3328
                .collect();
3329
            let missing = hash_object(ObjectType::Chunk, b"never stored");
3330
            let mut refs: Vec<ObjectRef> = Vec::new();
3331
            for i in 0..20 {
3332
                refs.push(loose[i]);
3333
                refs.push(packed[i]);
3334
            }
3335
            refs.push(missing);
3336
            let got = s.get_many(&refs);
3337
            assert_eq!(got.len(), refs.len());
3338
            for (i, r) in refs.iter().enumerate().take(40) {
3339
                let want = if i % 2 == 0 {
3340
                    format!("loose {}", i / 2)
3341
                } else {
3342
                    format!("packed {}", i / 2)
3343
                };
3344
                let (ty, body) = got[i].as_ref().unwrap();
3345
                assert_eq!(
3346
                    (*ty, body.as_slice()),
3347
                    (ObjectType::Chunk, want.as_bytes()),
3348
                    "{which}: {r:?}"
3349
                );
3350
            }
3351
            assert!(matches!(got[40], Err(StoreError::NotFound(_))));
3352
        });
09245702 Andy McCall ↶ 3353
    }
3354
3355
    /// A packed object is read from its pack without first asking the
3356
    /// backend for a loose copy (oxygenhub#228: on a bucket each such ask
3357
    /// was a request that found nothing, every time).
3358
    #[test]
3359
    fn packed_objects_cost_no_loose_lookups() {
3360
        use std::sync::atomic::{AtomicUsize, Ordering};
3361
        struct Counting(MemoryStore, AtomicUsize);
3362
        impl crate::strata::Store for Counting {
3363
            fn describe(&self) -> String {
3364
                "counting".into()
3365
            }
3366
            fn has(&self, k: &str) -> crate::strata::Result<bool> {
3367
                self.0.has(k)
3368
            }
3369
            fn get(&self, k: &str) -> crate::strata::Result<Vec<u8>> {
3370
                if k.starts_with("loose/") {
3371
                    self.1.fetch_add(1, Ordering::Relaxed);
3372
                }
3373
                self.0.get(k)
3374
            }
3375
            fn get_range(
3376
                &self,
3377
                k: &str,
3378
                r: std::ops::Range<u64>,
3379
            ) -> crate::strata::Result<Vec<u8>> {
3380
                self.0.get_range(k, r)
3381
            }
3382
            fn put_new(&self, k: &str, b: &[u8]) -> crate::strata::Result<()> {
3383
                self.0.put_new(k, b)
3384
            }
3385
            fn list(&self, p: &str) -> crate::strata::Result<Vec<crate::strata::Entry>> {
3386
                self.0.list(p)
3387
            }
3388
            fn delete(&self, k: &str) -> crate::strata::Result<()> {
3389
                self.0.delete(k)
3390
            }
3391
            fn read_ref(
3392
                &self,
3393
                k: &str,
3394
            ) -> crate::strata::Result<Option<(Vec<u8>, crate::strata::Version)>> {
3395
                self.0.read_ref(k)
3396
            }
3397
            fn write_ref(
3398
                &self,
3399
                k: &str,
3400
                b: &[u8],
3401
                e: Option<&crate::strata::Version>,
3402
            ) -> crate::strata::Result<crate::strata::Version> {
3403
                self.0.write_ref(k, b, e)
3404
            }
3405
        }
3406
        let dir = tempfile::tempdir().unwrap();
3407
        let backend = Arc::new(Counting(MemoryStore::new(), AtomicUsize::new(0)));
3408
        let s = Store::open_on(backend.clone(), dir.path().join("scratch")).unwrap();
3409
        let refs: Vec<ObjectRef> = (0..10)
3410
            .map(|i| {
3411
                s.put(ObjectType::Chunk, format!("p {i}").as_bytes())
3412
                    .unwrap()
3413
            })
3414
            .collect();
3415
        s.pack_loose().unwrap();
3416
        let s = Store::open_on(backend.clone(), dir.path().join("scratch")).unwrap();
3417
        backend.1.store(0, Ordering::Relaxed);
3418
        for r in &refs {
3419
            assert!(s.get(r).is_ok());
3420
        }
3421
        assert!(s.get_many(&refs).iter().all(|g| g.is_ok()));
3422
        assert_eq!(backend.1.load(Ordering::Relaxed), 0, "no loose lookups");
3423
        // An object no pack holds is still looked for loose.
3424
        let missing = hash_object(ObjectType::Chunk, b"never stored");
3425
        assert!(matches!(s.get(&missing), Err(StoreError::NotFound(_))));
3426
        assert!(backend.1.load(Ordering::Relaxed) > 0);
767ac6cd Andy McCall 3427
    }
3428
}
3429
3430
/// Readers never wait (oxygenhub#249): a store keeps reading while other
3431
/// handles repack, collect and push into the store under it.
3432
#[cfg(test)]
3433
pub(crate) mod reader_tests {
3434
    use super::*;
3435
3436
    fn chunk(i: usize) -> Vec<u8> {
3437
        format!("object {i} ").repeat(1 + i % 5).into_bytes()
3438
    }
3439
3440
    /// Two handles on one store: a writer, and a reader opened at once.
3441
    fn each_pair(f: impl Fn(&str, Store, Store)) {
3442
        let dir = tempfile::tempdir().unwrap();
3443
        let s = Store::open(dir.path().join("objects")).unwrap();
3444
        let r = Store::open(dir.path().join("objects")).unwrap();
3445
        f("directory", s, r);
3446
        let m: Arc<Backend> = Arc::new(MemoryStore::new());
3447
        let s = Store::open_on(m.clone(), dir.path().join("scratch")).unwrap();
3448
        let r = Store::open_on(m, dir.path().join("scratch-r")).unwrap();
3449
        f("memory", s, r);
3450
    }
3451
3452
    /// Set every retired entry's time back by `by`, as if retired then.
3453
    pub(crate) fn age_retired(s: &Store, by: std::time::Duration) {
3454
        s.update_retired(|m| {
3455
            for t in m.values_mut() {
3456
                *t = t.saturating_sub(by.as_millis() as u64);
3457
            }
3458
        })
3459
        .unwrap();
3460
    }
3461
3462
    fn retired_keys(s: &Store) -> Vec<String> {
3463
        s.read_retired().unwrap().0.into_keys().collect()
3464
    }
3465
3466
    #[test]
3467
    fn a_reader_opened_before_a_repack_reads_every_object_after_it() {
3468
        each_pair(|which, writer, _| {
3469
            let mut refs = Vec::new();
3470
            for i in 0..40 {
3471
                refs.push(writer.put(ObjectType::Chunk, &chunk(i)).unwrap());
3472
            }
3473
            writer.pack_loose().unwrap();
3474
            for i in 40..80 {
3475
                refs.push(writer.put(ObjectType::Chunk, &chunk(i)).unwrap());
3476
            }
3477
            // Opened now: one pack and forty loose objects.
3478
            let reader = Store::open_on(writer.backend().clone(), writer.dir()).unwrap();
3479
            assert_eq!(reader.pack_count(), 1, "{which}");
3480
            let (_, merged) = writer.repack_all(REPACK_ZSTD_LEVEL).unwrap();
3481
            assert_eq!(merged, 1);
3482
            // The old pack and the loose objects are retired, not deleted.
3483
            let retired = retired_keys(&writer);
3484
            assert_eq!(retired.len(), 81, "{which}: {retired:?}");
3485
            assert_eq!(writer.loose_count().unwrap(), 0, "retired is not loose");
3486
            // The reader reads on from what it loaded.
3487
            for (i, r) in refs.iter().enumerate() {
3488
                assert_eq!(reader.get(r).unwrap().1, chunk(i), "{which}: {i}");
3489
            }
3490
            assert_eq!(reader.pack_count(), 1);
3491
            // A store opened now reads the new pack only.
3492
            let fresh = Store::open_on(writer.backend().clone(), writer.dir()).unwrap();
3493
            assert_eq!(fresh.pack_count(), 1);
3494
            assert_ne!(fresh.packs.borrow()[0].key, reader.packs.borrow()[0].key);
3495
            // Once the retired files are deleted, the reader finds every
3496
            // object in the new pack, by get, get_many and has alike.
3497
            let report = writer
3498
                .expire_retired(std::time::Duration::ZERO, &mut silent())
3499
                .unwrap();
3500
            assert_eq!((report.packs, report.loose), (1, 80), "{which}");
3501
            assert!(retired_keys(&writer).is_empty());
3502
            for (i, r) in refs.iter().enumerate() {
3503
                assert_eq!(reader.get(r).unwrap().1, chunk(i), "{which}: {i}");
3504
            }
3505
            let other = Store::open_on(writer.backend().clone(), writer.dir()).unwrap();
3506
            let before = other.packs.borrow()[0].key.clone();
3507
            writer.put(ObjectType::Chunk, b"new").unwrap();
3508
            writer.repack_all(REPACK_ZSTD_LEVEL).unwrap();
3509
            writer
3510
                .expire_retired(std::time::Duration::ZERO, &mut silent())
3511
                .unwrap();
3512
            assert!(other.get_many(&refs).iter().all(|g| g.is_ok()), "{which}");
3513
            assert_ne!(other.packs.borrow()[0].key, before);
3514
            let third = Store::open_on(writer.backend().clone(), writer.dir()).unwrap();
3515
            writer.put(ObjectType::Chunk, b"newer").unwrap();
3516
            writer.repack_all(REPACK_ZSTD_LEVEL).unwrap();
3517
            writer
3518
                .expire_retired(std::time::Duration::ZERO, &mut silent())
3519
                .unwrap();
3520
            assert!(refs.iter().all(|r| third.has(r)), "{which}");
3521
            assert!(!third.has(&hash_object(ObjectType::Chunk, b"never")));
3522
        });
3523
    }
3524
3525
    #[test]
3526
    fn a_reader_opened_before_a_spilled_batch_reads_its_objects() {
3527
        each_pair(|which, writer, reader| {
3528
            assert_eq!(reader.pack_count(), 0);
3529
            let mut refs = Vec::new();
3530
            {
3531
                let batch = writer.begin_batch().unwrap();
3532
                for i in 0..(SPILL_OBJECTS + 50) {
3533
                    refs.push(writer.put(ObjectType::Chunk, &chunk(i)).unwrap());
3534
                }
3535
                batch.finish().unwrap();
3536
            }
3537
            assert_eq!(writer.pack_count(), 1, "{which}: the batch spilled");
3538
            assert_eq!(reader.get(&refs[0]).unwrap().1, chunk(0), "{which}");
3539
            assert_eq!(reader.pack_count(), 1, "the miss loaded the new pack");
3540
            let got = reader.get_many(&refs);
3541
            assert!(got.iter().all(|g| g.is_ok()), "{which}");
3542
            // has, too, from another reader.
3543
            let late = Store::open_on(writer.backend().clone(), writer.dir()).unwrap();
3544
            {
3545
                let batch = writer.begin_batch().unwrap();
3546
                for i in 1000..(1000 + SPILL_OBJECTS + 1) {
3547
                    writer.put(ObjectType::Chunk, &chunk(i)).unwrap();
3548
                }
3549
                batch.finish().unwrap();
3550
            }
3551
            assert!(late.has(&hash_object(ObjectType::Chunk, &chunk(1000))));
3552
        });
3553
    }
3554
3555
    #[test]
3556
    fn retired_entries_expire_after_the_hour_and_are_never_loaded() {
3557
        each_pair(|which, s, _| {
3558
            for i in 0..10 {
3559
                s.put(ObjectType::Chunk, &chunk(i)).unwrap();
3560
            }
3561
            s.pack_loose().unwrap();
3562
            s.put(ObjectType::Chunk, &chunk(10)).unwrap();
3563
            s.repack_all(ZSTD_LEVEL).unwrap();
3564
            // Retired over an hour ago: the first pack and 10 loose objects.
3565
            age_retired(&s, RETIRED_FOR + std::time::Duration::from_secs(60));
3566
            let old = retired_keys(&s);
3567
            assert_eq!(old.len(), 12, "{which}: {old:?}");
3568
            // Retired just now: the second pack.
3569
            s.put(ObjectType::Chunk, &chunk(11)).unwrap();
3570
            s.repack_all(ZSTD_LEVEL).unwrap();
3571
            assert_eq!(retired_keys(&s).len(), 14);
3572
            let listed = s
3573
                .backend()
3574
                .list("pack-")
3575
                .unwrap()
3576
                .into_iter()
3577
                .filter(|e| e.key.ends_with(".idx"))
3578
                .count();
3579
            assert_eq!(listed, 3, "{which}");
3580
            let fresh = Store::open_on(s.backend().clone(), s.dir()).unwrap();
3581
            assert_eq!(
3582
                fresh.pack_count(),
3583
                1,
3584
                "{which}: retired packs are not loaded"
3585
            );
3586
            let report = s.expire_retired(RETIRED_FOR, &mut silent()).unwrap();
3587
            assert_eq!(
3588
                report,
3589
                RetiredReport {
3590
                    packs: 1,
3591
                    loose: 11,
3592
                    kept: 2
3593
                },
3594
                "{which}"
3595
            );
3596
            let young = retired_keys(&s);
3597
            assert_eq!(young.len(), 2, "{which}: {young:?}");
3598
            assert!(young.iter().all(|k| !old.contains(k)));
3599
            for k in old.iter().filter(|k| k.ends_with(".pack")) {
3600
                assert!(!s.backend().has(k).unwrap(), "{which}: {k}");
3601
            }
3602
            for k in &young {
3603
                assert!(s.backend().has(k).unwrap(), "{which}: {k} kept");
3604
            }
3605
            for i in 0..12 {
3606
                let r = hash_object(ObjectType::Chunk, &chunk(i));
3607
                assert_eq!(fresh.get(&r).unwrap().1, chunk(i), "{which}");
3608
            }
3609
        });
3610
    }
3611
3612
    /// A retired loose object whose pack copy was pruned since is not
3613
    /// lost when the retired files go: it is loose again.
3614
    #[test]
3615
    fn a_retired_loose_object_no_pack_holds_is_kept() {
3616
        each_pair(|which, s, _| {
3617
            let a = s.put(ObjectType::Chunk, b"a").unwrap();
3618
            let b = s.put(ObjectType::Chunk, b"b").unwrap();
3619
            s.pack_loose().unwrap();
3620
            assert_eq!(s.loose_count().unwrap(), 0);
3621
            // The pack rewritten without b, as pruning does.
3622
            let first = s.packs.borrow()[0].key.clone();
3623
            s.write_pack(&[a], &[], &[first], ZSTD_LEVEL, &mut silent())
3624
                .unwrap();
3625
            let report = s
3626
                .expire_retired(std::time::Duration::ZERO, &mut silent())
3627
                .unwrap();
3628
            assert_eq!((report.packs, report.loose), (1, 1), "{which}: a's copy");
3629
            assert!(retired_keys(&s).is_empty());
3630
            assert_eq!(s.get(&b).unwrap().1, b"b", "{which}");
3631
            assert_eq!(s.get(&a).unwrap().1, b"a");
3632
            assert_eq!(s.loose_count().unwrap(), 1, "b is loose again");
3633
            // And pruning it, unreachable, deletes it.
3634
            let keep = [a].into_iter().collect();
3635
            let r = s.prune(&keep, std::time::Duration::ZERO).unwrap();
3636
            assert_eq!(r.objects, 1);
3637
            assert!(!s.has(&b));
3638
        });
3639
    }
3640
3641
    /// A backend whose first pack listing names an index that is deleted
3642
    /// before it is read, as a collection running meanwhile would.
3643
    pub(crate) struct Racing {
3644
        inner: MemoryStore,
3645
        lists: std::sync::atomic::AtomicUsize,
3646
        /// Deleted right after the first listing returns.
3647
        pub(crate) doomed: std::sync::Mutex<Option<String>>,
3648
        /// Named in every listing, never stored.
3649
        ghost: bool,
3650
    }
3651
3652
    impl crate::strata::Store for Racing {
3653
        fn describe(&self) -> String {
3654
            "racing".into()
3655
        }
3656
        fn has(&self, k: &str) -> crate::strata::Result<bool> {
3657
            self.inner.has(k)
3658
        }
3659
        fn get(&self, k: &str) -> crate::strata::Result<Vec<u8>> {
3660
            self.inner.get(k)
3661
        }
3662
        fn get_range(&self, k: &str, r: std::ops::Range<u64>) -> crate::strata::Result<Vec<u8>> {
3663
            self.inner.get_range(k, r)
3664
        }
3665
        fn put_new(&self, k: &str, b: &[u8]) -> crate::strata::Result<()> {
3666
            self.inner.put_new(k, b)
3667
        }
3668
        fn list(&self, p: &str) -> crate::strata::Result<Vec<crate::strata::Entry>> {
3669
            let mut out = self.inner.list(p)?;
3670
            if p == "pack-" {
3671
                self.lists
3672
                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3673
                if let Some(k) = self.doomed.lock().unwrap().take() {
3674
                    self.inner.delete(&k)?;
3675
                }
3676
                if self.ghost {
3677
                    for k in ["pack-0000.idx", "pack-0000.pack"] {
3678
                        out.push(crate::strata::Entry {
3679
                            key: k.into(),
3680
                            size: 100,
3681
                            modified_ms: None,
3682
                        });
3683
                    }
3684
                    out.sort_by(|a, b| a.key.cmp(&b.key));
3685
                }
3686
            }
3687
            Ok(out)
3688
        }
3689
        fn delete(&self, k: &str) -> crate::strata::Result<()> {
3690
            self.inner.delete(k)
3691
        }
3692
        fn read_ref(
3693
            &self,
3694
            k: &str,
3695
        ) -> crate::strata::Result<Option<(Vec<u8>, crate::strata::Version)>> {
3696
            self.inner.read_ref(k)
3697
        }
3698
        fn write_ref(
3699
            &self,
3700
            k: &str,
3701
            b: &[u8],
3702
            e: Option<&crate::strata::Version>,
3703
        ) -> crate::strata::Result<crate::strata::Version> {
3704
            self.inner.write_ref(k, b, e)
3705
        }
3706
    }
3707
3708
    pub(crate) fn racing(ghost: bool) -> Arc<Racing> {
3709
        Arc::new(Racing {
3710
            inner: MemoryStore::new(),
3711
            lists: Default::default(),
3712
            doomed: Default::default(),
3713
            ghost,
3714
        })
3715
    }
3716
3717
    #[test]
3718
    fn an_index_deleted_between_listing_and_loading_is_passed_over() {
3719
        let dir = tempfile::tempdir().unwrap();
3720
        let backend = racing(false);
3721
        let s = Store::open_on(backend.clone(), dir.path()).unwrap();
3722
        let a = s.put(ObjectType::Chunk, b"a").unwrap();
3723
        s.pack_loose().unwrap();
3724
        let b = s.put(ObjectType::Chunk, b"b").unwrap();
3725
        s.pack_loose().unwrap();
3726
        let idx = {
3727
            let packs = s.packs.borrow();
3728
            let (i, _) = s.find_packed(&a).unwrap();
3729
            format!("{}.idx", packs[i].key.trim_end_matches(".pack"))
3730
        };
3731
        *backend.doomed.lock().unwrap() = Some(idx);
3732
        let before = backend.lists.load(std::sync::atomic::Ordering::Relaxed);
3733
        let again = Store::open_on(backend.clone(), dir.path()).unwrap();
3734
        assert_eq!(again.pack_count(), 1, "the deleted one is passed over");
3735
        assert_eq!(
3736
            backend.lists.load(std::sync::atomic::Ordering::Relaxed) - before,
3737
            2,
3738
            "and the store listed again"
3739
        );
3740
        assert_eq!(again.get(&b).unwrap().1, b"b");
3741
        // A listing that names an index never there opens too.
3742
        let ghosted = racing(true);
3743
        let s = Store::open_on(ghosted.clone(), dir.path().join("g")).unwrap();
3744
        let c = s.put(ObjectType::Chunk, b"c").unwrap();
3745
        s.pack_loose().unwrap();
3746
        let again = Store::open_on(ghosted, dir.path().join("g")).unwrap();
3747
        assert_eq!(again.pack_count(), 1);
3748
        assert_eq!(again.get(&c).unwrap().1, b"c");
a9f686ed Andy McCall ↶ 3749
    }
3750
}
3751
3752
#[cfg(test)]
3753
mod batch_tests {
3754
    use super::*;
3755
3756
    #[test]
3757
    fn a_large_batch_packs_every_object_and_indexes_them_all() {
3758
        let dir = tempfile::tempdir().unwrap();
3759
        let store = Store::open(dir.path()).unwrap();
3760
        let mut refs = Vec::new();
3761
        {
3762
            let batch = store.begin_batch().unwrap();
3763
            for i in 0..1000u32 {
3764
                let body = format!("object {i} {}", "x".repeat(20_000 + i as usize)).into_bytes();
3765
                refs.push(store.put(ObjectType::Blob, &body).unwrap());
3766
            }
3767
            batch.finish().unwrap();
3768
        }
3769
        let again = Store::open(dir.path()).unwrap();
3770
        for r in &refs {
3771
            again
54e2e630 Andy McCall ↶ 3772
                .get(r)
3773
                .unwrap_or_else(|e| panic!("{}: {e}", r.short(12)));
3774
        }
3775
    }
3776
3777
    #[test]
3778
    fn a_pack_a_stopped_batch_left_is_adopted_and_its_torn_end_cut() {
3779
        // oxygen#79: a batch never finished (a process killed part way)
3780
        // leaves its objects in an unindexed .tmp pack.
3781
        let dir = tempfile::tempdir().unwrap();
3782
        let mut refs = Vec::new();
3783
        {
3784
            let store = Store::open(dir.path()).unwrap();
3785
            let batch = store.begin_batch().unwrap();
3786
            for i in 0..300u32 {
3787
                let body = format!("object {i} {}", "y".repeat(20_000 + i as usize)).into_bytes();
3788
                refs.push(store.put(ObjectType::Blob, &body).unwrap());
3789
            }
3790
            std::mem::forget(batch);
3791
        }
3792
        let tmp: Vec<_> = fs::read_dir(dir.path())
3793
            .unwrap()
3794
            .flatten()
3795
            .filter(|e| e.file_name().to_string_lossy().starts_with(".tmp-"))
3796
            .map(|e| e.path())
3797
            .collect();
3798
        assert_eq!(tmp.len(), 1, "one unfinished pack");
3799
        // A torn last entry: a header and part of its body.
3800
        let mut f = fs::OpenOptions::new().append(true).open(&tmp[0]).unwrap();
3801
        f.write_all(&[0, 1, 0xff, 0, 0, 0, 9, 9, 0, 0]).unwrap();
3802
        f.write_all(&[7; 40]).unwrap();
3803
        drop(f);
3804
        let store = Store::open(dir.path()).unwrap();
3805
        assert!(store.get(&refs[0]).is_err(), "unreadable before");
3806
        let young = store
3807
            .adopt_unfinished_packs(std::time::Duration::from_secs(3600), &|_| {})
3808
            .unwrap();
3809
        assert_eq!(young, (0, 0), "a pack written to lately is left alone");
3810
        let (packs, objects) = store
3811
            .adopt_unfinished_packs(std::time::Duration::ZERO, &|_| {})
3812
            .unwrap();
3813
        assert_eq!((packs, objects), (1, 300));
3814
        for r in &refs {
3815
            store
a9f686ed Andy McCall ↶ 3816
                .get(r)
3817
                .unwrap_or_else(|e| panic!("{}: {e}", r.short(12)));
3818
        }
54e2e630 Andy McCall ↶ 3819
        let again = Store::open(dir.path()).unwrap();
3820
        assert!(again.get(&refs[299]).is_ok(), "indexed for good");
a9f686ed Andy McCall ↶ 3821
    }
3822
}
3823
3824
#[cfg(test)]
3825
mod size_tests {
3826
    use super::*;
3827
3828
    #[test]
3829
    fn an_object_over_the_limit_is_refused_and_nothing_is_written() {
3830
        let dir = tempfile::tempdir().unwrap();
3831
        let store = Store::open(dir.path()).unwrap();
3832
        let big = vec![b'x'; MAX_OBJECT_LEN as usize + 1];
3833
        {
3834
            let batch = store.begin_batch().unwrap();
3835
            store.put(ObjectType::Blob, b"small").unwrap();
3836
            let e = store.put(ObjectType::Change, &big).unwrap_err();
3837
            assert!(
3838
                matches!(e, StoreError::TooLarge { kind: "change", .. }),
3839
                "{e}"
3840
            );
3841
            assert!(e.to_string().contains("commit it in parts"), "{e}");
3842
            batch.finish().unwrap();
3843
        }
3844
        // The store still opens and reads what was written.
3845
        let again = Store::open(dir.path()).unwrap();
3846
        again.get(&hash_object(ObjectType::Blob, b"small")).unwrap();
6f8aeb99 Andy McCall ↶ 3847
    }
3848
}
3849
3850
#[cfg(test)]
3851
mod text_tests {
3852
    use super::*;
3853
    use crate::objects::{AtomRef, Edit, CHANGE_VERSION};
3854
3855
    fn large_change() -> Change {
3856
        let mut c = crate::objects::tests::sample_change();
3857
        assert_eq!(c.version, CHANGE_VERSION);
3858
        c.edits[0] = Edit::Insert {
3859
            node: crate::ids::NodeId([7; 16]),
3860
            after: AtomRef::Start,
3861
            before: AtomRef::End,
3862
            lines: (0..2000)
3863
                .map(|i| format!("line {i}\n").into_bytes())
3864
                .collect(),
3865
        };
3866
        c
3867
    }
3868
3869
    #[test]
3870
    fn a_large_insert_is_stored_beside_its_change_and_read_back() {
3871
        let dir = tempfile::tempdir().unwrap();
3872
        let s = Store::open(dir.path()).unwrap();
3873
        let c = large_change();
3874
        let r = s.put_change(&c).unwrap();
3875
        assert_eq!(r, c.hash());
3876
        let (_, body) = s.get(&r).unwrap();
3877
        assert!(
3878
            body.len() < 1024,
3879
            "the change holds a reference, not {} bytes of lines",
3880
            body.len()
3881
        );
3882
        let (back, parts) = s.get_change_parts(&r).unwrap();
3883
        assert_eq!(back, c);
3884
        // The text blob and its chunk.
3885
        assert_eq!(parts.len(), 2);
3886
        for p in &parts {
3887
            assert!(s.has(p));
3888
        }
3889
        // Storing it again writes nothing new.
3890
        let n = s.list().unwrap().len();
3891
        s.put_change(&c).unwrap();
3892
        assert_eq!(s.list().unwrap().len(), n);
3893
    }
3894
3895
    #[test]
3896
    fn a_change_whose_text_blob_is_missing_is_reported_not_misread() {
64818938 Andy McCall ↶ 3897
        if !super::files_on_disk() {
3898
            return;
3899
        }
6f8aeb99 Andy McCall ↶ 3900
        let dir = tempfile::tempdir().unwrap();
3901
        let s = Store::open(dir.path()).unwrap();
3902
        let c = large_change();
3903
        let r = s.put_change(&c).unwrap();
3904
        let (_, parts) = s.get_change_parts(&r).unwrap();
3905
        fs::remove_file(s.path_for(&parts[0])).unwrap();
3906
        let e = s.get_change(&r).unwrap_err();
3907
        assert!(
3908
            matches!(&e, StoreError::Corrupt(h, why) if *h == r && why.contains("missing")),
3909
            "{e}"
3910
        );
192fad1e Andy McCall ↶ 3911
    }
3912
}
3913
3914
#[cfg(test)]
3915
mod put_many_tests {
3916
    use super::*;
3917
3918
    #[test]
3919
    fn many_objects_in_a_batch_are_compressed_together_and_all_read_back() {
3920
        let dir = tempfile::tempdir().unwrap();
3921
        let s = Store::open(dir.path()).unwrap();
3922
        let before = s.put(ObjectType::Chunk, b"already here").unwrap();
3923
        let bodies: Vec<Vec<u8>> = (0..200u32)
3924
            .map(|i| format!("chunk {i} ").repeat(6000).into_bytes())
3925
            .collect();
3926
        let mut objects: Vec<(ObjectType, &[u8])> = bodies
3927
            .iter()
3928
            .map(|b| (ObjectType::Chunk, b.as_slice()))
3929
            .collect();
3930
        objects.push((ObjectType::Chunk, bodies[0].as_slice()));
3931
        objects.push((ObjectType::Chunk, b"already here"));
3932
        let refs = {
3933
            let batch = s.begin_batch().unwrap();
3934
            let refs = s.put_many(&objects).unwrap();
3935
            batch.finish().unwrap();
3936
            refs
3937
        };
3938
        assert_eq!(refs.len(), objects.len());
3939
        assert_eq!(refs[200], refs[0], "a repeat is the same object");
3940
        assert_eq!(refs[201], before);
3941
        let again = Store::open(dir.path()).unwrap();
3942
        for (r, b) in refs.iter().zip(&objects) {
3943
            assert_eq!(again.get(r).unwrap().1, b.1);
ef7d4ea6 Andy McCall ↶ 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)
192fad1e Andy McCall ↶ 3976
        }
ef7d4ea6 Andy McCall ↶ 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
        );
8f10a07a Andy McCall ↶ 4437
    }
ff0af1ee unknown 4438
}