tensorfs 0.3.67__tar.gz → 0.3.69__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. {tensorfs-0.3.67 → tensorfs-0.3.69}/Cargo.lock +2 -2
  2. {tensorfs-0.3.67 → tensorfs-0.3.69}/Cargo.toml +1 -1
  3. {tensorfs-0.3.67 → tensorfs-0.3.69}/PKG-INFO +1 -1
  4. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/bin/tfs/fetch.rs +14 -3
  5. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/bin/tfs/main.rs +5 -0
  6. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/derived/part_rows_tests.rs +47 -0
  7. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/derived/retention.rs +31 -2
  8. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/derived.rs +30 -0
  9. tensorfs-0.3.69/crates/tensorfs-core/src/disk.rs +153 -0
  10. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/lib.rs +2 -0
  11. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/staging.rs +3 -1
  12. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/stats.rs +33 -0
  13. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/store.rs +84 -16
  14. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/pull.rs +15 -1
  15. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/tests.rs +242 -47
  16. tensorfs-0.3.69/crates/tensorfs-core/src/unsynced.rs +674 -0
  17. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-py/src/lib.rs +5 -0
  18. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-py/src/store.rs +119 -31
  19. {tensorfs-0.3.67 → tensorfs-0.3.69}/pyproject.toml +1 -1
  20. {tensorfs-0.3.67 → tensorfs-0.3.69}/python/tensorfs/_ext.pyi +15 -3
  21. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/Cargo.toml +0 -0
  22. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/build.rs +0 -0
  23. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  24. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  25. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  26. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  27. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/asset_update.rs +0 -0
  28. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/b64.rs +0 -0
  29. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  30. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  31. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  32. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  33. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  34. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  35. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  36. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  37. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/canon.rs +0 -0
  38. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/capability.rs +0 -0
  39. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/catalog.rs +0 -0
  40. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/cbor.rs +0 -0
  41. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  42. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  43. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/corpus.rs +0 -0
  44. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  45. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  46. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  47. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  48. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  49. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  50. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  51. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/descriptors.rs +0 -0
  52. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/dtype.rs +0 -0
  53. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/durability.rs +0 -0
  54. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/err.rs +0 -0
  55. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/fetch.rs +0 -0
  56. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/fill.rs +0 -0
  57. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/fit.rs +0 -0
  58. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/gc.rs +0 -0
  59. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/header.rs +0 -0
  60. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/home.rs +0 -0
  61. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/ids.rs +0 -0
  62. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  63. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  64. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
  65. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  66. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  67. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  68. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  69. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  70. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  71. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  72. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  73. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  74. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/jcs.rs +0 -0
  75. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/limits.rs +0 -0
  76. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/machine.rs +0 -0
  77. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/manifest.rs +0 -0
  78. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/meta.rs +0 -0
  79. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/project.rs +0 -0
  80. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/providers.rs +0 -0
  81. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/read.rs +0 -0
  82. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/receipt.rs +0 -0
  83. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/reclaim.rs +0 -0
  84. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/registry.rs +0 -0
  85. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/relation.rs +0 -0
  86. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
  87. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  88. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/repository.rs +0 -0
  89. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/sha256.rs +0 -0
  90. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  91. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/spec.rs +0 -0
  92. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/storage.rs +0 -0
  93. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/api.rs +0 -0
  94. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/http.rs +0 -0
  95. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
  96. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  97. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  98. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/memory.rs +0 -0
  99. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  100. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/pace.rs +0 -0
  101. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  102. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  103. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/push.rs +0 -0
  104. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  105. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
  106. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/src/vectors.rs +0 -0
  107. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  108. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
  109. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  110. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +0 -0
  111. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
  112. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  113. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  114. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/fit.rs +0 -0
  115. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  116. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  117. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  118. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  119. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  120. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  121. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  122. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  123. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  124. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  125. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/home.rs +0 -0
  126. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  127. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  128. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/liveness.rs +0 -0
  129. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  130. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  131. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  132. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  133. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  134. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  135. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  136. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  137. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  138. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  139. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/source_download.rs +0 -0
  140. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  141. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  142. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  143. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/staging.rs +0 -0
  144. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  145. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  146. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  147. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  148. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/version.rs +0 -0
  149. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-core/tests/write_through.rs +0 -0
  150. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-py/Cargo.toml +0 -0
  151. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-py/src/docs.rs +0 -0
  152. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-py/src/errors.rs +0 -0
  153. {tensorfs-0.3.67 → tensorfs-0.3.69}/crates/tensorfs-py/src/ingest.rs +0 -0
  154. {tensorfs-0.3.67 → tensorfs-0.3.69}/python/tensorfs/__init__.py +0 -0
  155. {tensorfs-0.3.67 → tensorfs-0.3.69}/python/tensorfs/_derived_channel.py +0 -0
  156. {tensorfs-0.3.67 → tensorfs-0.3.69}/python/tensorfs/_source_channel.py +0 -0
  157. {tensorfs-0.3.67 → tensorfs-0.3.69}/python/tensorfs/_tensors.py +0 -0
  158. {tensorfs-0.3.67 → tensorfs-0.3.69}/python/tensorfs/derived.py +0 -0
  159. {tensorfs-0.3.67 → tensorfs-0.3.69}/python/tensorfs/errors.py +0 -0
  160. {tensorfs-0.3.67 → tensorfs-0.3.69}/python/tensorfs/py.typed +0 -0
@@ -439,7 +439,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
439
439
 
440
440
  [[package]]
441
441
  name = "tensorfs-core"
442
- version = "0.3.67"
442
+ version = "0.3.69"
443
443
  dependencies = [
444
444
  "fs2",
445
445
  "libc",
@@ -454,7 +454,7 @@ dependencies = [
454
454
 
455
455
  [[package]]
456
456
  name = "tensorfs-py"
457
- version = "0.3.67"
457
+ version = "0.3.69"
458
458
  dependencies = [
459
459
  "pyo3",
460
460
  "tensorfs-core",
@@ -3,5 +3,5 @@ resolver = "2"
3
3
  members = ["crates/tensorfs-core", "crates/tensorfs-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.67"
6
+ version = "0.3.69"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.67
3
+ Version: 0.3.69
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -257,6 +257,8 @@ fn pull_with_budget(
257
257
  flags: &Flags,
258
258
  budget: Option<(u64, u64)>,
259
259
  ) -> ExitCode {
260
+ // Before the open, which is where an unclean shutdown's recovery runs.
261
+ let before = tensorfs_core::stats::snapshot();
260
262
  let store = ok!(Store::open(root));
261
263
  let Some(base) = flag(flags, "hub") else {
262
264
  eprintln!("--hub <base-url> is required");
@@ -302,7 +304,6 @@ fn pull_with_budget(
302
304
  let observe = |_object: &ObjectRef, moved: u64, door: ObjectSource| progress.saw(moved, door);
303
305
  request.on_plan = Some(&planned);
304
306
  request.on_object = Some(&observe);
305
- let before = tensorfs_core::stats::snapshot();
306
307
  let started = std::time::Instant::now();
307
308
  let report = ok!(transport::pull(&request));
308
309
  let seconds = started.elapsed().as_secs_f64();
@@ -333,9 +334,12 @@ fn pull_with_budget(
333
334
  })
334
335
  .collect();
335
336
  // And what admitting them cost: a pull whose streams stop growing while its batches
336
- // take most of the wall time is held by the disk, not the link.
337
+ // take most of the wall time is held by the disk, not the link. `durability` says
338
+ // which syncs the disk the Store lives on made it pay, and what an unclean shutdown
339
+ // before this pull cost to repair.
340
+ let durability = &report.durability;
337
341
  eprintln!(
338
- "{{\"event\":\"pull.streams\",\"model\":{},\"seconds\":{:.3},\"bytes_moved\":{},\"streams\":{},\"levels\":[{}],\"backoffs\":{},\"admission\":{{\"objects\":{},\"batches\":{},\"seconds\":{:.3}}}}}",
342
+ "{{\"event\":\"pull.streams\",\"model\":{},\"seconds\":{:.3},\"bytes_moved\":{},\"streams\":{},\"levels\":[{}],\"backoffs\":{},\"admission\":{{\"objects\":{},\"batches\":{},\"seconds\":{:.3}}},\"durability\":{{\"class\":{},\"filesystem\":{},\"syncs\":{},\"sync_seconds\":{:.3},\"recovery\":{{\"rehashed\":{},\"removed\":{},\"seconds\":{:.3}}}}}}}",
339
343
  quoted(&report.model),
340
344
  seconds,
341
345
  report.bytes_moved,
@@ -345,6 +349,13 @@ fn pull_with_budget(
345
349
  after.admitted_objects - before.admitted_objects,
346
350
  after.admission_batches - before.admission_batches,
347
351
  (after.admission_nanos - before.admission_nanos) as f64 / 1e9,
352
+ quoted(durability.class.as_str()),
353
+ quoted(durability.filesystem),
354
+ durability.syncs,
355
+ durability.seconds,
356
+ after.recovery_rehashed - before.recovery_rehashed,
357
+ after.recovery_removed - before.recovery_removed,
358
+ (after.recovery_nanos - before.recovery_nanos) as f64 / 1e9,
348
359
  );
349
360
  println!(
350
361
  "model {}@{} lane {}",
@@ -904,6 +904,11 @@ fn cmd_store(root: &Path, verb: &str, flags: &Flags) -> ExitCode {
904
904
  None => "(none)".to_string(),
905
905
  }
906
906
  );
907
+ println!(
908
+ "durability: {} ({})",
909
+ s.disk_class().as_str(),
910
+ s.disk().kind
911
+ );
907
912
  ExitCode::SUCCESS
908
913
  }
909
914
 
@@ -324,3 +324,50 @@ fn an_indexed_checkpoint_does_not_reread_its_chain() {
324
324
  release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
325
325
  std::fs::remove_dir_all(store.root()).unwrap();
326
326
  }
327
+
328
+ #[test]
329
+ fn a_digest_receipt_names_the_declaration_the_intent_bound() {
330
+ let (store, meta, id, declaration) = fixture("receipt-digest");
331
+ let intent = declaration.clone().canonical_work_bytes().unwrap();
332
+ let writer = begin(&store, &meta, &id, 1, declaration, None).unwrap();
333
+ for (index, key) in ["weight_0", "weight_1", "weight_2"].into_iter().enumerate() {
334
+ add(&store, &meta, &id, &writer, key, index as u8 + 1).unwrap();
335
+ }
336
+ let receipt = commit(&store, &meta, &id, 1).unwrap();
337
+ let inline = receipt.value(ReceiptDeclaration::Inline);
338
+ let digest = receipt.value(ReceiptDeclaration::Digest);
339
+ assert_eq!(inline, receipt.to_value());
340
+ let field = |value: &Value, name: &str| {
341
+ as_obj("receipt", "facts", value)
342
+ .unwrap()
343
+ .iter()
344
+ .find(|(key, _)| key == name)
345
+ .map(|(_, value)| value.clone())
346
+ };
347
+ assert_eq!(
348
+ crate::canon::write(&field(&inline, "declaration").unwrap()),
349
+ intent
350
+ );
351
+ assert!(field(&digest, "declaration").is_none());
352
+ assert_eq!(
353
+ field(&digest, "declaration_digest"),
354
+ Some(Value::str(ObjectRef::of(&intent).id()))
355
+ );
356
+ assert_eq!(
357
+ field(&digest, "declaration_length"),
358
+ Some(Value::uint(intent.len() as u64))
359
+ );
360
+ // Every other fact is identical in both forms.
361
+ let others = |value: &Value| {
362
+ as_obj("receipt", "facts", value)
363
+ .unwrap()
364
+ .iter()
365
+ .filter(|(key, _)| !key.starts_with("declaration"))
366
+ .cloned()
367
+ .collect::<Vec<_>>()
368
+ };
369
+ assert_eq!(others(&inline), others(&digest));
370
+ let _ = fence(&meta, &id, 1);
371
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
372
+ std::fs::remove_dir_all(store.root()).unwrap();
373
+ }
@@ -5,7 +5,8 @@ use std::io::{Read, Write};
5
5
  use std::path::PathBuf;
6
6
 
7
7
  use super::{
8
- checkpoint, receipt_from, require_payload_record, roots, Disposition, TransactionState,
8
+ checkpoint, receipt_from, require_payload_record, roots, Disposition, ReceiptDeclaration,
9
+ TransactionState,
9
10
  };
10
11
  use crate::canon::{Fields, Value};
11
12
  use crate::catalog::WriterGuard;
@@ -217,7 +218,12 @@ pub fn retain_result(
217
218
  );
218
219
  }
219
220
  let facts = receipt_from(store, row)?;
220
- if object_id(&crate::canon::write(&facts.to_value())) != receipt {
221
+ // A receipt's identity is the digest of whichever form its caller was handed.
222
+ let forms = [ReceiptDeclaration::Inline, ReceiptDeclaration::Digest];
223
+ if !forms
224
+ .iter()
225
+ .any(|form| object_id(&crate::canon::write(&facts.value(*form))) == receipt)
226
+ {
221
227
  return refuse(
222
228
  Code::OBJECT_ID_MISMATCH,
223
229
  "retained result receipt differs from committed computation",
@@ -369,6 +375,29 @@ mod tests {
369
375
  }
370
376
  }
371
377
 
378
+ #[test]
379
+ fn a_result_is_retained_under_either_receipt_form_and_nothing_else() {
380
+ let f = fixture();
381
+ let facts = derived::lookup(&f.store, &f.meta, &f.transaction).unwrap();
382
+ let derived::Lookup::Committed(committed) = facts else {
383
+ panic!("committed result expected")
384
+ };
385
+ let digest_form = object_id(&crate::canon::write(
386
+ &committed.receipt().value(ReceiptDeclaration::Digest),
387
+ ));
388
+ assert_ne!(digest_form, f.receipt);
389
+ let retained =
390
+ retain_result(&f.store, &f.meta, &f.transaction, &digest_form, &id("b1")).unwrap();
391
+ assert_eq!(retained.receipt_digest, digest_form);
392
+ retain_result(&f.store, &f.meta, &f.transaction, &f.receipt, &id("b2")).unwrap();
393
+ assert_eq!(
394
+ retain_result(&f.store, &f.meta, &f.transaction, &id("99"), &id("b3"))
395
+ .unwrap_err()
396
+ .code,
397
+ Code::OBJECT_ID_MISMATCH
398
+ );
399
+ }
400
+
372
401
  #[test]
373
402
  fn independent_owners_keep_original_bytes_and_receipt_through_disposal_and_gc() {
374
403
  let f = fixture();
@@ -3421,8 +3421,38 @@ pub struct ReceiptFacts {
3421
3421
  pub inherited_payload_bytes: u64,
3422
3422
  }
3423
3423
 
3424
+ /// How a receipt names its declaration: the whole document, or its digest and length.
3425
+ /// The whole document grows with the tensors declared; a digest keeps receipts constant.
3426
+ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3427
+ pub enum ReceiptDeclaration {
3428
+ #[default]
3429
+ Inline,
3430
+ Digest,
3431
+ }
3432
+
3424
3433
  impl ReceiptFacts {
3425
3434
  pub fn to_value(&self) -> Value {
3435
+ self.value(ReceiptDeclaration::Inline)
3436
+ }
3437
+
3438
+ pub fn value(&self, form: ReceiptDeclaration) -> Value {
3439
+ let mut value = self.inline_value();
3440
+ if form == ReceiptDeclaration::Digest {
3441
+ if let Value::Obj(fields) = &mut value {
3442
+ let bytes = crate::canon::write(&self.declaration.to_value());
3443
+ fields.retain(|(key, _)| key != "declaration");
3444
+ fields.push((
3445
+ "declaration_digest".into(),
3446
+ Value::str(ObjectRef::of(&bytes).id()),
3447
+ ));
3448
+ fields.push(("declaration_length".into(), Value::uint(bytes.len() as u64)));
3449
+ fields.sort_by(|left, right| left.0.cmp(&right.0));
3450
+ }
3451
+ }
3452
+ value
3453
+ }
3454
+
3455
+ fn inline_value(&self) -> Value {
3426
3456
  Value::obj(vec![
3427
3457
  (
3428
3458
  "added_objects",
@@ -0,0 +1,153 @@
1
+ //! What a crash can take from a Store, read off the filesystem the Store actually lives on.
2
+ //!
3
+ //! An fsync protects bytes against the machine going down before its page cache is written.
4
+ //! Whether that protection is worth paying for depends on where the bytes live, and only the
5
+ //! mount can say that — never a flag, never an environment variable.
6
+ //!
7
+ //! The rule, from `statfs(2)` of the Store root (Linux):
8
+ //!
9
+ //! - **Ephemeral** — tmpfs, ramfs, overlayfs. RAM, or a container's writable layer, which the
10
+ //! platform discards with the machine or container (a RunPod container disk is erased when
11
+ //! the pod stops). Nothing a crash could leave behind is ever read again, and a process
12
+ //! crash leaves the page cache intact, so no fsync buys anything.
13
+ //! - **Network** — NFS, SMB/CIFS, FUSE, Ceph, 9p, AFS, Coda, GFS2, OCFS2, Lustre, GPFS.
14
+ //! Another machine may read it, and close-to-open is the only ordering that machine sees:
15
+ //! bytes are flushed before their name is published, as before.
16
+ //! - **Persistent** — everything else (ext4, xfs, btrfs, zfs, ...), a `statfs` that fails,
17
+ //! and every non-Linux platform. The safe side: it survives a crash of this machine.
18
+
19
+ use std::path::Path;
20
+
21
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22
+ pub enum DiskClass {
23
+ Ephemeral,
24
+ Persistent,
25
+ Network,
26
+ }
27
+
28
+ impl DiskClass {
29
+ /// The class of the filesystem holding `path`.
30
+ pub fn of(path: &Path) -> DiskClass {
31
+ filesystem(path).class
32
+ }
33
+
34
+ pub fn as_str(self) -> &'static str {
35
+ match self {
36
+ DiskClass::Ephemeral => "ephemeral",
37
+ DiskClass::Persistent => "persistent",
38
+ DiskClass::Network => "network",
39
+ }
40
+ }
41
+ }
42
+
43
+ /// The filesystem holding a path: its class and the name it was recognised by.
44
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
45
+ pub struct Filesystem {
46
+ pub class: DiskClass,
47
+ /// `ext4`, `overlayfs`, `nfs`, ...; `other` for an unrecognised magic, `unknown` when
48
+ /// `statfs` failed or the platform has no magic.
49
+ pub kind: &'static str,
50
+ }
51
+
52
+ #[cfg(target_os = "linux")]
53
+ pub fn filesystem(path: &Path) -> Filesystem {
54
+ match rustix::fs::statfs(path) {
55
+ #[allow(clippy::unnecessary_cast)]
56
+ Ok(stat) => classify((stat.f_type as u64) & 0xffff_ffff),
57
+ Err(_) => Filesystem {
58
+ class: DiskClass::Persistent,
59
+ kind: "unknown",
60
+ },
61
+ }
62
+ }
63
+
64
+ #[cfg(not(target_os = "linux"))]
65
+ pub fn filesystem(_path: &Path) -> Filesystem {
66
+ Filesystem {
67
+ class: DiskClass::Persistent,
68
+ kind: "unknown",
69
+ }
70
+ }
71
+
72
+ /// Superblock magics from `linux/magic.h` and the filesystems' own headers.
73
+ #[cfg(target_os = "linux")]
74
+ fn classify(magic: u64) -> Filesystem {
75
+ use DiskClass::*;
76
+ let (class, kind) = match magic {
77
+ 0x0102_1994 => (Ephemeral, "tmpfs"),
78
+ 0x8584_58f6 => (Ephemeral, "ramfs"),
79
+ 0x794c_7630 => (Ephemeral, "overlayfs"),
80
+ 0x6969 => (Network, "nfs"),
81
+ 0x517b => (Network, "smb"),
82
+ 0xff53_4d42 => (Network, "cifs"),
83
+ 0xfe53_4d42 => (Network, "smb2"),
84
+ 0x6573_5546 => (Network, "fuse"),
85
+ 0x00c3_6400 => (Network, "ceph"),
86
+ 0x0102_1997 => (Network, "9p"),
87
+ 0x5346_414f | 0x6b41_4653 => (Network, "afs"),
88
+ 0x7375_7245 => (Network, "coda"),
89
+ 0x0116_1970 => (Network, "gfs2"),
90
+ 0x7461_636f => (Network, "ocfs2"),
91
+ 0x0bd0_0bd0 => (Network, "lustre"),
92
+ 0x4750_4653 => (Network, "gpfs"),
93
+ 0xef53 => (Persistent, "ext4"),
94
+ 0x5846_5342 => (Persistent, "xfs"),
95
+ 0x9123_683e => (Persistent, "btrfs"),
96
+ 0x2fc1_2fc1 => (Persistent, "zfs"),
97
+ 0xf2f5_2010 => (Persistent, "f2fs"),
98
+ _ => (Persistent, "other"),
99
+ };
100
+ Filesystem { class, kind }
101
+ }
102
+
103
+ /// This kernel's boot, or `None` where there is no such identity. Page cache belongs to a
104
+ /// boot: bytes written under this boot and not yet on disk are still readable until it ends.
105
+ /// A container sees its host's boot, which is the one that owns the page cache.
106
+ pub(crate) fn boot_id() -> Option<String> {
107
+ #[cfg(target_os = "linux")]
108
+ {
109
+ let id = std::fs::read_to_string("/proc/sys/kernel/random/boot_id").ok()?;
110
+ let id = id.trim();
111
+ (id.len() == 36 && id.bytes().all(|b| b.is_ascii_hexdigit() || b == b'-'))
112
+ .then(|| id.to_string())
113
+ }
114
+ #[cfg(not(target_os = "linux"))]
115
+ {
116
+ None
117
+ }
118
+ }
119
+
120
+ #[cfg(test)]
121
+ mod tests {
122
+ use super::*;
123
+
124
+ #[cfg(target_os = "linux")]
125
+ #[test]
126
+ fn magics_name_their_class() {
127
+ assert_eq!(classify(0x794c_7630).class, DiskClass::Ephemeral);
128
+ assert_eq!(classify(0x0102_1994).class, DiskClass::Ephemeral);
129
+ assert_eq!(classify(0x6969).class, DiskClass::Network);
130
+ assert_eq!(classify(0x6573_5546).class, DiskClass::Network);
131
+ assert_eq!(classify(0xef53).class, DiskClass::Persistent);
132
+ assert_eq!(classify(0x1234_5678).class, DiskClass::Persistent);
133
+ }
134
+
135
+ #[cfg(target_os = "linux")]
136
+ #[test]
137
+ fn real_mounts_classify() {
138
+ if Path::new("/dev/shm").is_dir() {
139
+ assert_eq!(filesystem(Path::new("/dev/shm")).kind, "tmpfs");
140
+ assert_eq!(DiskClass::of(Path::new("/dev/shm")), DiskClass::Ephemeral);
141
+ }
142
+ assert_eq!(
143
+ DiskClass::of(Path::new("/proc/self")),
144
+ DiskClass::Persistent,
145
+ "an unrecognised filesystem is the safe side"
146
+ );
147
+ assert_eq!(
148
+ DiskClass::of(Path::new("/no/such/path/anywhere")),
149
+ DiskClass::Persistent
150
+ );
151
+ assert!(boot_id().is_some());
152
+ }
153
+ }
@@ -20,6 +20,7 @@ pub mod checkpoint_root;
20
20
  pub mod corpus;
21
21
  pub mod derived;
22
22
  pub mod descriptors;
23
+ pub mod disk;
23
24
  pub mod dtype;
24
25
  pub mod durability;
25
26
  pub mod err;
@@ -53,6 +54,7 @@ pub mod stats;
53
54
  pub mod storage;
54
55
  pub mod store;
55
56
  pub mod transport;
57
+ pub mod unsynced;
56
58
  pub mod vectors;
57
59
 
58
60
  /// This TensorFS's release version.
@@ -204,7 +204,9 @@ pub fn stage<R: Read>(store: &Store, r: &mut R, want: &ObjectRef) -> Result<Carr
204
204
  return Err(io(format!("link {}", destination.display()), error));
205
205
  }
206
206
  }
207
- crate::store::fsync_dir(&dir)?;
207
+ if store.flushes() {
208
+ crate::store::fsync_dir(&dir)?;
209
+ }
208
210
  let _ = fs::remove_file(&verified.tmp);
209
211
  drop(verified.file);
210
212
  Ok(Carrier {
@@ -9,6 +9,11 @@ static REPO_CACHE_VERIFIED_READS: AtomicU64 = AtomicU64::new(0);
9
9
  static ADMISSION_BATCHES: AtomicU64 = AtomicU64::new(0);
10
10
  static ADMITTED_OBJECTS: AtomicU64 = AtomicU64::new(0);
11
11
  static ADMISSION_NANOS: AtomicU64 = AtomicU64::new(0);
12
+ static SYNCS: AtomicU64 = AtomicU64::new(0);
13
+ static SYNC_NANOS: AtomicU64 = AtomicU64::new(0);
14
+ static RECOVERY_REHASHED: AtomicU64 = AtomicU64::new(0);
15
+ static RECOVERY_REMOVED: AtomicU64 = AtomicU64::new(0);
16
+ static RECOVERY_NANOS: AtomicU64 = AtomicU64::new(0);
12
17
 
13
18
  #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14
19
  pub struct Stats {
@@ -24,6 +29,14 @@ pub struct Stats {
24
29
  pub admitted_objects: u64,
25
30
  /// Wall time those batches spent linking, fsyncing and committing.
26
31
  pub admission_nanos: u64,
32
+ /// Filesystem syncs pulls ran on persistent disks, and their wall time.
33
+ pub syncs: u64,
34
+ pub sync_nanos: u64,
35
+ /// Objects rehashed after an unclean shutdown, those found torn and removed, and the
36
+ /// wall time the recovery took.
37
+ pub recovery_rehashed: u64,
38
+ pub recovery_removed: u64,
39
+ pub recovery_nanos: u64,
27
40
  }
28
41
 
29
42
  pub fn snapshot() -> Stats {
@@ -34,6 +47,11 @@ pub fn snapshot() -> Stats {
34
47
  admission_batches: ADMISSION_BATCHES.load(Ordering::Relaxed),
35
48
  admitted_objects: ADMITTED_OBJECTS.load(Ordering::Relaxed),
36
49
  admission_nanos: ADMISSION_NANOS.load(Ordering::Relaxed),
50
+ syncs: SYNCS.load(Ordering::Relaxed),
51
+ sync_nanos: SYNC_NANOS.load(Ordering::Relaxed),
52
+ recovery_rehashed: RECOVERY_REHASHED.load(Ordering::Relaxed),
53
+ recovery_removed: RECOVERY_REMOVED.load(Ordering::Relaxed),
54
+ recovery_nanos: RECOVERY_NANOS.load(Ordering::Relaxed),
37
55
  }
38
56
  }
39
57
 
@@ -69,3 +87,18 @@ pub(crate) fn admission_batch(objects: usize, spent: std::time::Duration) {
69
87
  Ordering::Relaxed,
70
88
  );
71
89
  }
90
+
91
+ fn nanos(spent: std::time::Duration) -> u64 {
92
+ u64::try_from(spent.as_nanos()).unwrap_or(u64::MAX)
93
+ }
94
+
95
+ pub(crate) fn synced(spent: std::time::Duration) {
96
+ SYNCS.fetch_add(1, Ordering::Relaxed);
97
+ SYNC_NANOS.fetch_add(nanos(spent), Ordering::Relaxed);
98
+ }
99
+
100
+ pub(crate) fn recovered(recovery: &crate::unsynced::Recovery, spent: std::time::Duration) {
101
+ RECOVERY_REHASHED.fetch_add(recovery.rehashed, Ordering::Relaxed);
102
+ RECOVERY_REMOVED.fetch_add(recovery.removed, Ordering::Relaxed);
103
+ RECOVERY_NANOS.fetch_add(nanos(spent), Ordering::Relaxed);
104
+ }
@@ -8,7 +8,10 @@
8
8
  //!
9
9
  //! Admission is `writing → verified-temp → installed`: temp in `tmp/`, hash while writing,
10
10
  //! fsync, 0444, then a no-clobber `link()` into place plus a parent-directory fsync. A
11
- //! crash at any stage leaves a temp, never a partial object under `objects/`.
11
+ //! crash at any stage leaves a temp, never a partial object under `objects/`. Which of those
12
+ //! syncs an admission pays depends on the disk the Store lives on ([`crate::disk`]): none on
13
+ //! an ephemeral one, and on a persistent one none inside a pull, whose batch sync
14
+ //! covers them ([`crate::unsynced`]).
12
15
  //!
13
16
  //! `contains` is a PRESENCE HINT and nothing more. Trusting bytes requires a durable
14
17
  //! verification record bound to object id, length, file identity (dev, ino), size and
@@ -305,6 +308,10 @@ pub struct Store {
305
308
  budget: Option<u64>,
306
309
  /// This process's verified-blob index for the catalog, shared with every other handle.
307
310
  trust: std::sync::Arc<crate::catalog::Trust>,
311
+ /// The filesystem this Store lives on, and so what a crash can take from it.
312
+ disk: crate::disk::Filesystem,
313
+ /// The pull whose batch sync covers this handle's admissions (persistent disk).
314
+ epoch: Option<std::sync::Arc<crate::unsynced::Epoch>>,
308
315
  }
309
316
 
310
317
  impl std::fmt::Debug for Store {
@@ -313,6 +320,7 @@ impl std::fmt::Debug for Store {
313
320
  .field("root", &self.root)
314
321
  .field("observed", &self.progress.is_some())
315
322
  .field("repo_cache", &self.cache.as_ref().map(|c| c.root()))
323
+ .field("disk", &self.disk)
316
324
  .finish()
317
325
  }
318
326
  }
@@ -518,6 +526,8 @@ impl Store {
518
526
  cache: read_repo_cache_binding(root),
519
527
  budget: read_disk_budget(root),
520
528
  trust: crate::catalog::Trust::of(root),
529
+ disk: crate::disk::filesystem(root),
530
+ epoch: None,
521
531
  })
522
532
  }
523
533
 
@@ -549,6 +559,45 @@ impl Store {
549
559
  &self.root
550
560
  }
551
561
 
562
+ /// The filesystem this Store lives on. See [`crate::disk`].
563
+ pub fn disk(&self) -> crate::disk::Filesystem {
564
+ self.disk
565
+ }
566
+
567
+ pub fn disk_class(&self) -> crate::disk::DiskClass {
568
+ self.disk.class
569
+ }
570
+
571
+ /// Whether an admission makes its own bytes and name durable before it reports: not on
572
+ /// an ephemeral disk, and not under a pull's epoch, whose batch sync covers them.
573
+ pub(crate) fn flushes(&self) -> bool {
574
+ match self.disk.class {
575
+ crate::disk::DiskClass::Ephemeral => false,
576
+ crate::disk::DiskClass::Network => true,
577
+ crate::disk::DiskClass::Persistent => self.epoch.is_none(),
578
+ }
579
+ }
580
+
581
+ /// The same Store, its admissions covered by `epoch`.
582
+ pub(crate) fn syncing(&self, epoch: Option<std::sync::Arc<crate::unsynced::Epoch>>) -> Store {
583
+ Store {
584
+ epoch,
585
+ ..self.clone()
586
+ }
587
+ }
588
+
589
+ /// The same Store as if it lived on a disk of `class`: benchmarks of one real disk.
590
+ #[cfg(test)]
591
+ pub(crate) fn with_disk_class(self, class: crate::disk::DiskClass) -> Store {
592
+ Store {
593
+ disk: crate::disk::Filesystem {
594
+ class,
595
+ kind: "injected",
596
+ },
597
+ ..self
598
+ }
599
+ }
600
+
552
601
  pub(crate) fn trust(&self) -> &crate::catalog::Trust {
553
602
  &self.trust
554
603
  }
@@ -867,12 +916,14 @@ impl Store {
867
916
  child
868
917
  .set_permissions(fs::Permissions::from_mode(0o755))
869
918
  .map_err(|error| io("set public Store directory mode", error))?;
870
- child
871
- .sync_all()
872
- .map_err(|error| io("sync public Store directory", error))?;
873
- parent
874
- .sync_all()
875
- .map_err(|error| io("sync public directory parent", error))?;
919
+ if self.flushes() {
920
+ child
921
+ .sync_all()
922
+ .map_err(|error| io("sync public Store directory", error))?;
923
+ parent
924
+ .sync_all()
925
+ .map_err(|error| io("sync public directory parent", error))?;
926
+ }
876
927
  } else if metadata.uid() != owner.uid() || metadata.gid() != owner.gid() {
877
928
  return refuse(
878
929
  Code::STORE_ERA,
@@ -1072,8 +1123,10 @@ impl Store {
1072
1123
  .map_err(|error| io("create manifest temp", error))?;
1073
1124
  file.write_all(&bytes)
1074
1125
  .map_err(|error| io("write manifest", error))?;
1075
- file.sync_all()
1076
- .map_err(|error| io("fsync manifest", error))?;
1126
+ if self.flushes() {
1127
+ file.sync_all()
1128
+ .map_err(|error| io("fsync manifest", error))?;
1129
+ }
1077
1130
  fs::set_permissions(&temporary, fs::Permissions::from_mode(0o444))
1078
1131
  .map_err(|error| io("chmod manifest", error))?;
1079
1132
  let admitted = match fs::hard_link(&temporary, &path) {
@@ -1082,9 +1135,12 @@ impl Store {
1082
1135
  Err(error) => return cleanup(&temporary, io("install manifest", error)),
1083
1136
  };
1084
1137
  let _ = fs::remove_file(&temporary);
1085
- if admitted {
1138
+ if admitted && self.flushes() {
1086
1139
  fsync_dir(path.parent().unwrap())?;
1087
1140
  }
1141
+ if let (true, Some(epoch)) = (admitted, &self.epoch) {
1142
+ epoch.admitted(path.clone(), object.length)?;
1143
+ }
1088
1144
  self.read_manifest(&object)?;
1089
1145
  Ok(Put {
1090
1146
  obj: object,
@@ -1404,8 +1460,10 @@ impl Store {
1404
1460
  }
1405
1461
  }
1406
1462
  fault.hit("written");
1407
- if let Err(e) = f.sync_all() {
1408
- return cleanup(&tmp, io("fsync temp", e));
1463
+ if self.flushes() {
1464
+ if let Err(e) = f.sync_all() {
1465
+ return cleanup(&tmp, io("fsync temp", e));
1466
+ }
1409
1467
  }
1410
1468
  fault.hit("fsync");
1411
1469
 
@@ -1483,13 +1541,18 @@ impl Store {
1483
1541
  };
1484
1542
  let publish = move || {
1485
1543
  let installed = self.link_verified(&verified, fault)?;
1486
- if let Some(dir) = &installed {
1544
+ if let Some(dir) = installed.as_ref().filter(|_| self.flushes()) {
1487
1545
  if let Err(error) = fsync_dir(dir) {
1488
1546
  let _ = fs::remove_file(&verified.tmp);
1489
1547
  return Err(error);
1490
1548
  }
1491
1549
  }
1492
- self.finish_linked(verified, installed.is_some())
1550
+ let length = verified.length;
1551
+ let record = self.finish_linked(verified, installed.is_some())?;
1552
+ if let (Some(epoch), Some(record)) = (&self.epoch, &record) {
1553
+ epoch.admitted(self.object_path(&record.sha256), length)?;
1554
+ }
1555
+ Ok(record)
1493
1556
  };
1494
1557
  let admitted = match operation {
1495
1558
  Some(operation) => catalog.admit_blob(Some(operation), &object, publish)?,
@@ -2143,15 +2206,20 @@ fn creation_lock(root: &Path) -> Result<File> {
2143
2206
  Ok(directory)
2144
2207
  }
2145
2208
 
2209
+ /// No handle is returned while an unclean shutdown's unsynced admissions are unverified.
2146
2210
  fn open_validated(root: &Path) -> Result<Store> {
2147
2211
  fs::create_dir_all(root.join("tmp")).map_err(|error| io("mkdir tmp", error))?;
2148
- Ok(Store {
2212
+ let store = Store {
2149
2213
  root: root.to_path_buf(),
2150
2214
  progress: None,
2151
2215
  cache: read_repo_cache_binding(root),
2152
2216
  budget: read_disk_budget(root),
2153
2217
  trust: crate::catalog::Trust::of(root),
2154
- })
2218
+ disk: crate::disk::filesystem(root),
2219
+ epoch: None,
2220
+ };
2221
+ crate::unsynced::recover(&store)?;
2222
+ Ok(store)
2155
2223
  }
2156
2224
 
2157
2225
  fn open_real_directory(path: &Path) -> Result<File> {