|
|
… |
146 unchanged lines |
| 147 |
147 |
|
pub struct Store { |
| 148 |
148 |
|
dir: PathBuf, |
| 149 |
149 |
|
packs: RefCell<Vec<PackIndex>>, |
|
150 |
+ |
/// An open write batch (spec §13.4): objects go here instead of to loose |
|
151 |
+ |
/// files, and are written out with one `fsync` when the batch finishes. |
|
152 |
+ |
pending: RefCell<Option<Pending>>, |
|
153 |
+ |
batch_depth: std::cell::Cell<u32>, |
|
154 |
+ |
} |
|
155 |
+ |
|
|
156 |
+ |
/// Objects written since `begin_batch`. Small batches stay in memory and |
|
157 |
+ |
/// become loose objects; once `SPILL_OBJECTS` or `SPILL_BYTES` is exceeded |
|
158 |
+ |
/// the batch streams into a pack file instead. |
|
159 |
+ |
#[derive(Debug)] |
|
160 |
+ |
struct Pending { |
|
161 |
+ |
buffered: Vec<(ObjectType, Vec<u8>)>, |
|
162 |
+ |
buffered_bytes: usize, |
|
163 |
+ |
lookup: std::collections::HashMap<ObjectRef, PendingEntry>, |
|
164 |
+ |
pack: Option<PendingPack>, |
|
165 |
+ |
report: PackReport, |
| 150 |
166 |
|
} |
| 151 |
167 |
|
|
|
168 |
+ |
#[derive(Debug, Clone, Copy)] |
|
169 |
+ |
enum PendingEntry { |
|
170 |
+ |
Buffered(usize), |
|
171 |
+ |
Packed(PackEntry), |
|
172 |
+ |
} |
|
173 |
+ |
|
|
174 |
+ |
#[derive(Debug)] |
|
175 |
+ |
struct PendingPack { |
|
176 |
+ |
id: String, |
|
177 |
+ |
tmp_path: PathBuf, |
|
178 |
+ |
writer: io::BufWriter<fs::File>, |
|
179 |
+ |
offset: u64, |
|
180 |
+ |
index: Vec<(ObjectRef, PackEntry)>, |
|
181 |
+ |
} |
|
182 |
+ |
|
|
183 |
+ |
const SPILL_OBJECTS: usize = 256; |
|
184 |
+ |
const SPILL_BYTES: usize = 8 << 20; |
|
185 |
+ |
|
|
186 |
+ |
/// Finishes the enclosing batch when dropped; call `finish` to see errors. |
|
187 |
+ |
pub struct Batch<'a> { |
|
188 |
+ |
store: &'a Store, |
|
189 |
+ |
done: bool, |
|
190 |
+ |
} |
|
191 |
+ |
|
|
192 |
+ |
impl Batch<'_> { |
|
193 |
+ |
pub fn finish(mut self) -> Result<()> { |
|
194 |
+ |
self.done = true; |
|
195 |
+ |
self.store.finish_batch() |
|
196 |
+ |
} |
|
197 |
+ |
} |
|
198 |
+ |
|
|
199 |
+ |
impl Drop for Batch<'_> { |
|
200 |
+ |
fn drop(&mut self) { |
|
201 |
+ |
if !self.done { |
|
202 |
+ |
let _ = self.store.finish_batch(); |
|
203 |
+ |
} |
|
204 |
+ |
} |
|
205 |
+ |
} |
|
206 |
+ |
|
| 152 |
207 |
|
impl Store { |
| 153 |
208 |
|
pub fn open(dir: impl Into<PathBuf>) -> io::Result<Store> { |
| 154 |
209 |
|
let dir = dir.into(); |
| 155 |
210 |
|
fs::create_dir_all(dir.join("loose"))?; |
| 156 |
|
- |
let store = Store { dir, packs: RefCell::new(Vec::new()) }; |
|
211 |
+ |
let store = Store { dir, packs: RefCell::new(Vec::new()), pending: RefCell::new(None), batch_depth: std::cell::Cell::new(0) }; |
| 157 |
212 |
|
store.reload_packs().map_err(|e| io::Error::other(e.to_string()))?; |
| 158 |
213 |
|
Ok(store) |
| 159 |
214 |
|
} |
|
|
… |
148 unchanged lines |
| 308 |
363 |
|
} |
| 309 |
364 |
|
|
| 310 |
365 |
|
/// Write `refs` into a new pack, then delete `loose_paths` and `old_packs`. |
|
366 |
+ |
/// Write the index for a complete, synced temporary pack file and move |
|
367 |
+ |
/// both into place; `index` must be sorted by ObjectRef. |
|
368 |
+ |
fn finish_pack_files(&self, id: &str, tmp_pack: &Path, index: &[(ObjectRef, PackEntry)]) -> Result<()> { |
|
369 |
+ |
let pack_path = self.dir.join(format!("pack-{id}.pack")); |
|
370 |
+ |
let idx_path = self.dir.join(format!("pack-{id}.idx")); |
|
371 |
+ |
let tmp_idx = self.dir.join(format!("tmp-{id}.idx")); |
|
372 |
+ |
{ |
|
373 |
+ |
let mut f = io::BufWriter::new(fs::File::create(&tmp_idx)?); |
|
374 |
+ |
f.write_all(IDX_MAGIC)?; |
|
375 |
+ |
f.write_all(&(index.len() as u32).to_le_bytes())?; |
|
376 |
+ |
for (r, e) in index { |
|
377 |
+ |
f.write_all(&r.0)?; |
|
378 |
+ |
f.write_all(&e.offset.to_le_bytes())?; |
|
379 |
+ |
f.write_all(&e.stored_len.to_le_bytes())?; |
|
380 |
+ |
f.write_all(&e.raw_len.to_le_bytes())?; |
|
381 |
+ |
f.write_all(&[type_index(e.ty), e.flags])?; |
|
382 |
+ |
} |
|
383 |
+ |
f.flush()?; |
|
384 |
+ |
f.get_ref().sync_all()?; |
|
385 |
+ |
} |
|
386 |
+ |
fs::rename(tmp_pack, &pack_path)?; |
|
387 |
+ |
fs::rename(&tmp_idx, &idx_path)?; |
|
388 |
+ |
self.reload_packs() |
|
389 |
+ |
} |
|
390 |
+ |
|
| 311 |
391 |
|
fn write_pack(&self, refs: &[ObjectRef], loose_paths: &[PathBuf], old_packs: &[PathBuf]) -> Result<PackReport> { |
| 312 |
392 |
|
if refs.is_empty() { |
| 313 |
393 |
|
for p in old_packs { |
|
|
… |
9 unchanged lines |
| 323 |
403 |
|
let mut nonce = [0u8; 8]; |
| 324 |
404 |
|
rand::rngs::OsRng.fill_bytes(&mut nonce); |
| 325 |
405 |
|
let id = hex::encode(nonce); |
| 326 |
|
- |
let pack_path = self.dir.join(format!("pack-{id}.pack")); |
| 327 |
|
- |
let idx_path = self.dir.join(format!("pack-{id}.idx")); |
| 328 |
406 |
|
let tmp_pack = self.dir.join(format!("tmp-{id}.pack")); |
| 329 |
|
- |
let tmp_idx = self.dir.join(format!("tmp-{id}.idx")); |
| 330 |
407 |
|
let mut report = PackReport::default(); |
| 331 |
408 |
|
let mut index: Vec<(ObjectRef, PackEntry)> = Vec::with_capacity(refs.len()); |
| 332 |
409 |
|
{ |
|
|
… |
23 unchanged lines |
| 356 |
433 |
|
f.flush()?; |
| 357 |
434 |
|
f.get_ref().sync_all()?; |
| 358 |
435 |
|
} |
| 359 |
|
- |
{ |
| 360 |
|
- |
let mut f = io::BufWriter::new(fs::File::create(&tmp_idx)?); |
| 361 |
|
- |
f.write_all(IDX_MAGIC)?; |
| 362 |
|
- |
f.write_all(&(index.len() as u32).to_le_bytes())?; |
| 363 |
|
- |
for (r, e) in &index { |
| 364 |
|
- |
f.write_all(&r.0)?; |
| 365 |
|
- |
f.write_all(&e.offset.to_le_bytes())?; |
| 366 |
|
- |
f.write_all(&e.stored_len.to_le_bytes())?; |
| 367 |
|
- |
f.write_all(&e.raw_len.to_le_bytes())?; |
| 368 |
|
- |
f.write_all(&[type_index(e.ty), e.flags])?; |
| 369 |
|
- |
} |
| 370 |
|
- |
f.flush()?; |
| 371 |
|
- |
f.get_ref().sync_all()?; |
| 372 |
|
- |
} |
| 373 |
|
- |
fs::rename(&tmp_pack, &pack_path)?; |
| 374 |
|
- |
fs::rename(&tmp_idx, &idx_path)?; |
|
436 |
+ |
self.finish_pack_files(&id, &tmp_pack, &index)?; |
| 375 |
437 |
|
for p in old_packs { |
| 376 |
438 |
|
fs::remove_file(p)?; |
| 377 |
439 |
|
fs::remove_file(p.with_extension("idx"))?; |
|
|
… |
18 unchanged lines |
| 396 |
458 |
|
} |
| 397 |
459 |
|
|
| 398 |
460 |
|
pub fn has(&self, r: &ObjectRef) -> bool { |
| 399 |
|
- |
self.path_for(r).is_file() || self.find_packed(r).is_some() |
|
461 |
+ |
self.path_for(r).is_file() || self.find_packed(r).is_some() || self.pending.borrow().as_ref().is_some_and(|p| p.lookup.contains_key(r)) |
| 400 |
462 |
|
} |
| 401 |
463 |
|
|
|
464 |
+ |
/// Start a write batch: until the matching `finish`, `put` collects |
|
465 |
+ |
/// objects and writes them out together, with one `fsync` for a large |
|
466 |
+ |
/// batch (a pack) instead of one per object. Batches nest; only the |
|
467 |
+ |
/// outermost does the work. Objects are readable while pending. |
|
468 |
+ |
pub fn begin_batch(&self) -> Result<Batch<'_>> { |
|
469 |
+ |
let depth = self.batch_depth.get(); |
|
470 |
+ |
if depth == 0 { |
|
471 |
+ |
*self.pending.borrow_mut() = Some(Pending { buffered: vec![], buffered_bytes: 0, lookup: Default::default(), pack: None, report: PackReport::default() }); |
|
472 |
+ |
} |
|
473 |
+ |
self.batch_depth.set(depth + 1); |
|
474 |
+ |
Ok(Batch { store: self, done: false }) |
|
475 |
+ |
} |
|
476 |
+ |
|
|
477 |
+ |
fn finish_batch(&self) -> Result<()> { |
|
478 |
+ |
let depth = self.batch_depth.get(); |
|
479 |
+ |
if depth == 0 { |
|
480 |
+ |
return Ok(()); |
|
481 |
+ |
} |
|
482 |
+ |
self.batch_depth.set(depth - 1); |
|
483 |
+ |
if depth > 1 { |
|
484 |
+ |
return Ok(()); |
|
485 |
+ |
} |
|
486 |
+ |
let Some(pending) = self.pending.borrow_mut().take() else { return Ok(()) }; |
|
487 |
+ |
match pending.pack { |
|
488 |
+ |
None => { |
|
489 |
+ |
for (ty, body) in &pending.buffered { |
|
490 |
+ |
self.put_loose(*ty, body)?; |
|
491 |
+ |
} |
|
492 |
+ |
Ok(()) |
|
493 |
+ |
} |
|
494 |
+ |
Some(mut pack) => { |
|
495 |
+ |
pack.writer.flush()?; |
|
496 |
+ |
pack.writer.get_ref().sync_all()?; |
|
497 |
+ |
drop(pack.writer); |
|
498 |
+ |
pack.index.sort_by_key(|(r, _)| *r); |
|
499 |
+ |
self.finish_pack_files(&pack.id, &pack.tmp_path, &pack.index)?; |
|
500 |
+ |
Ok(()) |
|
501 |
+ |
} |
|
502 |
+ |
} |
|
503 |
+ |
} |
|
504 |
+ |
|
|
505 |
+ |
/// Whether a batch is open and holds `r`. |
|
506 |
+ |
fn pending_get(&self, r: &ObjectRef) -> Option<Result<(ObjectType, Vec<u8>)>> { |
|
507 |
+ |
let mut guard = self.pending.borrow_mut(); |
|
508 |
+ |
let pending = guard.as_mut()?; |
|
509 |
+ |
let entry = *pending.lookup.get(r)?; |
|
510 |
+ |
Some(match entry { |
|
511 |
+ |
PendingEntry::Buffered(i) => { |
|
512 |
+ |
let (ty, body) = &pending.buffered[i]; |
|
513 |
+ |
Ok((*ty, body.clone())) |
|
514 |
+ |
} |
|
515 |
+ |
PendingEntry::Packed(e) => { |
|
516 |
+ |
let Some(pack) = pending.pack.as_mut() else { return Some(Err(StoreError::NotFound(*r))) }; |
|
517 |
+ |
match pack.writer.flush() { |
|
518 |
+ |
Ok(()) => PackIndex { path: pack.tmp_path.clone(), refs: vec![], entries: vec![] }.read(r, e), |
|
519 |
+ |
Err(err) => Err(err.into()), |
|
520 |
+ |
} |
|
521 |
+ |
} |
|
522 |
+ |
}) |
|
523 |
+ |
} |
|
524 |
+ |
|
|
525 |
+ |
/// Append one object to the pending pack file. |
|
526 |
+ |
fn pending_pack_write(&self, pack: &mut PendingPack, r: &ObjectRef, ty: ObjectType, body: &[u8], report: &mut PackReport) -> Result<PackEntry> { |
|
527 |
+ |
let (flags, stored) = if self.is_nocompress(r) { |
|
528 |
+ |
(0, body.to_vec()) |
|
529 |
+ |
} else { |
|
530 |
+ |
let compressed = zstd::bulk::compress(body, ZSTD_LEVEL).map_err(|e| StoreError::Corrupt(*r, format!("zstd: {e}")))?; |
|
531 |
+ |
if compressed.len() < body.len() { (FLAG_ZSTD, compressed) } else { (0, body.to_vec()) } |
|
532 |
+ |
}; |
|
533 |
+ |
let header_len = 1 + 1 + 4 + 4 + 32; |
|
534 |
+ |
pack.writer.write_all(&[type_index(ty), flags])?; |
|
535 |
+ |
pack.writer.write_all(&(stored.len() as u32).to_le_bytes())?; |
|
536 |
+ |
pack.writer.write_all(&(body.len() as u32).to_le_bytes())?; |
|
537 |
+ |
pack.writer.write_all(&r.0)?; |
|
538 |
+ |
pack.writer.write_all(&stored)?; |
|
539 |
+ |
let entry = PackEntry { offset: pack.offset + header_len, stored_len: stored.len() as u32, raw_len: body.len() as u32, ty, flags }; |
|
540 |
+ |
pack.offset += header_len + stored.len() as u64; |
|
541 |
+ |
pack.index.push((*r, entry)); |
|
542 |
+ |
report.objects += 1; |
|
543 |
+ |
report.raw_bytes += body.len() as u64; |
|
544 |
+ |
report.packed_bytes += stored.len() as u64; |
|
545 |
+ |
Ok(entry) |
|
546 |
+ |
} |
|
547 |
+ |
|
|
548 |
+ |
fn put_pending(&self, r: ObjectRef, ty: ObjectType, body: &[u8]) -> Result<()> { |
|
549 |
+ |
let mut guard = self.pending.borrow_mut(); |
|
550 |
+ |
let pending = guard.as_mut().expect("batch open"); |
|
551 |
+ |
if pending.lookup.contains_key(&r) { |
|
552 |
+ |
return Ok(()); |
|
553 |
+ |
} |
|
554 |
+ |
if pending.pack.is_none() && pending.buffered.len() < SPILL_OBJECTS && pending.buffered_bytes + body.len() <= SPILL_BYTES { |
|
555 |
+ |
pending.buffered.push((ty, body.to_vec())); |
|
556 |
+ |
pending.buffered_bytes += body.len(); |
|
557 |
+ |
pending.lookup.insert(r, PendingEntry::Buffered(pending.buffered.len() - 1)); |
|
558 |
+ |
return Ok(()); |
|
559 |
+ |
} |
|
560 |
+ |
if pending.pack.is_none() { |
|
561 |
+ |
// Spill: open the pack and move the buffered objects into it. |
|
562 |
+ |
let mut nonce = [0u8; 8]; |
|
563 |
+ |
rand::rngs::OsRng.fill_bytes(&mut nonce); |
|
564 |
+ |
let id = hex::encode(nonce); |
|
565 |
+ |
let tmp_path = self.dir.join(format!("tmp-{id}.pack")); |
|
566 |
+ |
let mut writer = io::BufWriter::new(fs::File::create(&tmp_path)?); |
|
567 |
+ |
writer.write_all(PACK_MAGIC)?; |
|
568 |
+ |
let mut pack = PendingPack { id, tmp_path, writer, offset: PACK_MAGIC.len() as u64, index: vec![] }; |
|
569 |
+ |
let buffered = std::mem::take(&mut pending.buffered); |
|
570 |
+ |
pending.buffered_bytes = 0; |
|
571 |
+ |
let mut report = std::mem::take(&mut pending.report); |
|
572 |
+ |
for (bty, bbody) in &buffered { |
|
573 |
+ |
let br = hash_object(*bty, bbody); |
|
574 |
+ |
let e = self.pending_pack_write(&mut pack, &br, *bty, bbody, &mut report)?; |
|
575 |
+ |
pending.lookup.insert(br, PendingEntry::Packed(e)); |
|
576 |
+ |
} |
|
577 |
+ |
pending.report = report; |
|
578 |
+ |
pending.pack = Some(pack); |
|
579 |
+ |
} |
|
580 |
+ |
let mut report = std::mem::take(&mut pending.report); |
|
581 |
+ |
let pack = pending.pack.as_mut().unwrap(); |
|
582 |
+ |
let e = self.pending_pack_write(pack, &r, ty, body, &mut report)?; |
|
583 |
+ |
pending.report = report; |
|
584 |
+ |
pending.lookup.insert(r, PendingEntry::Packed(e)); |
|
585 |
+ |
Ok(()) |
|
586 |
+ |
} |
|
587 |
+ |
|
| 402 |
588 |
|
/// Store `body` as an object of type `ty`; idempotent. |
| 403 |
589 |
|
pub fn put(&self, ty: ObjectType, body: &[u8]) -> Result<ObjectRef> { |
| 404 |
590 |
|
let r = hash_object(ty, body); |
|
|
… |
1 unchanged lines |
| 406 |
592 |
|
if path.is_file() || self.find_packed(&r).is_some() { |
| 407 |
593 |
|
return Ok(r); |
| 408 |
594 |
|
} |
|
595 |
+ |
if self.pending.borrow().is_some() { |
|
596 |
+ |
self.put_pending(r, ty, body)?; |
|
597 |
+ |
return Ok(r); |
|
598 |
+ |
} |
|
599 |
+ |
self.put_loose(ty, body)?; |
|
600 |
+ |
Ok(r) |
|
601 |
+ |
} |
|
602 |
+ |
|
|
603 |
+ |
/// Write one loose object atomically with its own `fsync`. |
|
604 |
+ |
fn put_loose(&self, ty: ObjectType, body: &[u8]) -> Result<ObjectRef> { |
|
605 |
+ |
let r = hash_object(ty, body); |
|
606 |
+ |
let path = self.path_for(&r); |
|
607 |
+ |
if path.is_file() { |
|
608 |
+ |
return Ok(r); |
|
609 |
+ |
} |
| 409 |
610 |
|
fs::create_dir_all(path.parent().unwrap())?; |
| 410 |
611 |
|
let mut nonce = [0u8; 8]; |
| 411 |
612 |
|
rand::rngs::OsRng.fill_bytes(&mut nonce); |
|
|
… |
8 unchanged lines |
| 420 |
621 |
|
Ok(r) |
| 421 |
622 |
|
} |
| 422 |
623 |
|
|
| 423 |
|
- |
/// Read and verify an object, loose or packed. |
|
624 |
+ |
/// Read and verify an object, loose, packed, or pending in a batch. |
| 424 |
625 |
|
pub fn get(&self, r: &ObjectRef) -> Result<(ObjectType, Vec<u8>)> { |
| 425 |
626 |
|
let path = self.path_for(r); |
| 426 |
627 |
|
let data = match fs::read(&path) { |
|
|
… |
1 unchanged lines |
| 428 |
629 |
|
Err(e) if e.kind() == io::ErrorKind::NotFound => { |
| 429 |
630 |
|
return match self.find_packed(r) { |
| 430 |
631 |
|
Some((i, e)) => self.packs.borrow()[i].read(r, e), |
| 431 |
|
- |
None => Err(StoreError::NotFound(*r)), |
|
632 |
+ |
None => self.pending_get(r).unwrap_or(Err(StoreError::NotFound(*r))), |
| 432 |
633 |
|
}; |
| 433 |
634 |
|
} |
| 434 |
635 |
|
Err(e) => return Err(e.into()), |
|
|
… |
38 unchanged lines |
| 473 |
674 |
|
|
| 474 |
675 |
|
/// Store a chunk that packs will keep uncompressed (`nocompress`, spec §13.3). |
| 475 |
676 |
|
pub fn put_chunk_nocompress(&self, bytes: &[u8]) -> Result<ObjectRef> { |
| 476 |
|
- |
let r = self.put(ObjectType::Chunk, bytes)?; |
|
677 |
+ |
let r = hash_object(ObjectType::Chunk, bytes); |
| 477 |
678 |
|
let dir = self.dir.join("nocompress"); |
| 478 |
679 |
|
fs::create_dir_all(&dir)?; |
| 479 |
680 |
|
let _ = fs::File::create(dir.join(r.to_hex())); |
| 480 |
|
- |
Ok(r) |
|
681 |
+ |
self.put(ObjectType::Chunk, bytes) |
| 481 |
682 |
|
} |
| 482 |
683 |
|
|
| 483 |
684 |
|
fn is_nocompress(&self, r: &ObjectRef) -> bool { |
|
|
… |
348 unchanged lines |
| 832 |
1033 |
|
assert_eq!(hash_object(ty, &body), *r); |
| 833 |
1034 |
|
} |
| 834 |
1035 |
|
} |
|
1036 |
+ |
|
|
1037 |
+ |
/// A small batch becomes loose objects; a large one becomes a single |
|
1038 |
+ |
/// pack. Objects are readable while pending, and batches nest. |
|
1039 |
+ |
#[test] |
|
1040 |
+ |
fn batches_write_loose_or_one_pack() { |
|
1041 |
+ |
let dir = tempfile::tempdir().unwrap(); |
|
1042 |
+ |
let s = Store::open(dir.path().join("objects")).unwrap(); |
|
1043 |
+ |
let outer = s.begin_batch().unwrap(); |
|
1044 |
+ |
let a = s.put(ObjectType::Chunk, b"small a").unwrap(); |
|
1045 |
+ |
{ |
|
1046 |
+ |
let inner = s.begin_batch().unwrap(); |
|
1047 |
+ |
let b = s.put(ObjectType::Chunk, b"small b").unwrap(); |
|
1048 |
+ |
assert!(s.has(&b)); |
|
1049 |
+ |
assert_eq!(s.get(&b).unwrap().1, b"small b"); |
|
1050 |
+ |
inner.finish().unwrap(); |
|
1051 |
+ |
assert_eq!(s.loose_count().unwrap(), 0, "the inner batch defers to the outer one"); |
|
1052 |
+ |
} |
|
1053 |
+ |
assert_eq!(s.get(&a).unwrap().1, b"small a"); |
|
1054 |
+ |
assert_eq!(s.put(ObjectType::Chunk, b"small a").unwrap(), a, "idempotent while pending"); |
|
1055 |
+ |
outer.finish().unwrap(); |
|
1056 |
+ |
assert_eq!(s.loose_count().unwrap(), 2); |
|
1057 |
+ |
assert_eq!(s.pack_count(), 0); |
|
1058 |
+ |
assert_eq!(s.get(&a).unwrap().1, b"small a"); |
|
1059 |
+ |
|
|
1060 |
+ |
// Large: spills into one pack, readable during and after. |
|
1061 |
+ |
let big = s.begin_batch().unwrap(); |
|
1062 |
+ |
let mut refs = Vec::new(); |
|
1063 |
+ |
let mut seed = 7u64; |
|
1064 |
+ |
for i in 0..300u32 { |
|
1065 |
+ |
let mut body = vec![b'z'; 40_000]; |
|
1066 |
+ |
if i % 3 == 0 { |
|
1067 |
+ |
for b in body.iter_mut() { |
|
1068 |
+ |
*b = (xorshift(&mut seed) & 0xff) as u8; |
|
1069 |
+ |
} |
|
1070 |
+ |
} |
|
1071 |
+ |
body[..4].copy_from_slice(&i.to_le_bytes()); |
|
1072 |
+ |
let r = if i % 5 == 0 { s.put_chunk_nocompress(&body).unwrap() } else { s.put(ObjectType::Chunk, &body).unwrap() }; |
|
1073 |
+ |
assert_eq!(s.get(&r).unwrap().1, body, "readable while pending (object {i})"); |
|
1074 |
+ |
refs.push((r, body)); |
|
1075 |
+ |
} |
|
1076 |
+ |
assert!(s.has(&refs[0].0)); |
|
1077 |
+ |
big.finish().unwrap(); |
|
1078 |
+ |
assert_eq!(s.pack_count(), 1); |
|
1079 |
+ |
assert_eq!(s.loose_count().unwrap(), 2, "nothing from the big batch is loose"); |
|
1080 |
+ |
assert!(fs::read_dir(dir.path().join("objects")).unwrap().flatten().all(|e| !e.file_name().to_string_lossy().starts_with("tmp-"))); |
|
1081 |
+ |
let s2 = Store::open(dir.path().join("objects")).unwrap(); |
|
1082 |
+ |
for (r, body) in &refs { |
|
1083 |
+ |
assert_eq!(s2.get(r).unwrap().1, *body); |
|
1084 |
+ |
} |
|
1085 |
+ |
// A dropped batch still writes its objects. |
|
1086 |
+ |
{ |
|
1087 |
+ |
let _b = s2.begin_batch().unwrap(); |
|
1088 |
+ |
s2.put(ObjectType::Chunk, b"dropped").unwrap(); |
|
1089 |
+ |
} |
|
1090 |
+ |
assert_eq!(s2.get(&hash_object(ObjectType::Chunk, b"dropped")).unwrap().1, b"dropped"); |
|
1091 |
+ |
} |
| 835 |
1092 |
|
} |