tensorfs 0.3.69__tar.gz → 0.3.71__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 (168) hide show
  1. {tensorfs-0.3.69 → tensorfs-0.3.71}/Cargo.lock +2 -2
  2. {tensorfs-0.3.69 → tensorfs-0.3.71}/Cargo.toml +1 -1
  3. {tensorfs-0.3.69 → tensorfs-0.3.71}/PKG-INFO +1 -1
  4. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/ingest.rs +15 -7
  5. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/watch.rs +4 -3
  6. tensorfs-0.3.71/crates/tensorfs-core/src/catalog/lock.rs +121 -0
  7. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/catalog.rs +108 -31
  8. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/companion_tests.rs +44 -0
  9. tensorfs-0.3.71/crates/tensorfs-core/src/derived/group_commit_tests.rs +114 -0
  10. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/roots.rs +44 -26
  11. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived.rs +68 -0
  12. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/fill.rs +95 -0
  13. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/convert.rs +148 -5
  14. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/fingerprint.rs +5 -2
  15. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/journal.rs +10 -0
  16. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/mod.rs +2 -0
  17. tensorfs-0.3.71/crates/tensorfs-core/src/ingest/narrow.rs +178 -0
  18. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/preflight.rs +3 -3
  19. tensorfs-0.3.71/crates/tensorfs-core/src/ingest/routes/sdxl.routes.json +1 -0
  20. tensorfs-0.3.71/crates/tensorfs-core/src/ingest/routes/sdxl.spec.json +51 -0
  21. tensorfs-0.3.71/crates/tensorfs-core/src/ingest/routes.rs +382 -0
  22. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/source.rs +184 -16
  23. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/transaction.rs +76 -21
  24. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/meta.rs +1 -6
  25. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/providers.rs +93 -4
  26. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/store.rs +18 -1
  27. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/pull.rs +7 -33
  28. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/unsynced.rs +17 -0
  29. tensorfs-0.3.71/crates/tensorfs-core/tests/as_is.rs +334 -0
  30. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +18 -2
  31. tensorfs-0.3.71/crates/tensorfs-core/tests/routed_single_file.rs +297 -0
  32. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/source_plan.rs +35 -14
  33. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-py/src/ingest.rs +27 -0
  34. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-py/src/lib.rs +2 -0
  35. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-py/src/store.rs +21 -10
  36. {tensorfs-0.3.69 → tensorfs-0.3.71}/pyproject.toml +1 -1
  37. {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/__init__.py +4 -0
  38. {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/_ext.pyi +8 -0
  39. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/Cargo.toml +0 -0
  40. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/build.rs +0 -0
  41. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  42. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  43. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  44. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  45. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/asset_update.rs +0 -0
  46. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/b64.rs +0 -0
  47. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  48. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  49. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  50. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  51. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  52. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  53. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  54. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  55. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/canon.rs +0 -0
  56. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/capability.rs +0 -0
  57. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/cbor.rs +0 -0
  58. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  59. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  60. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/corpus.rs +0 -0
  61. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  62. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  63. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  64. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/part_rows_tests.rs +0 -0
  65. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  66. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  67. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  68. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/descriptors.rs +0 -0
  69. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/disk.rs +0 -0
  70. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/dtype.rs +0 -0
  71. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/durability.rs +0 -0
  72. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/err.rs +0 -0
  73. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/fetch.rs +0 -0
  74. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/fit.rs +0 -0
  75. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/gc.rs +0 -0
  76. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/header.rs +0 -0
  77. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/home.rs +0 -0
  78. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ids.rs +0 -0
  79. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  80. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
  81. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  82. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  83. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  84. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/jcs.rs +0 -0
  85. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/lib.rs +0 -0
  86. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/limits.rs +0 -0
  87. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/machine.rs +0 -0
  88. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/manifest.rs +0 -0
  89. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/project.rs +0 -0
  90. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/read.rs +0 -0
  91. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/receipt.rs +0 -0
  92. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/reclaim.rs +0 -0
  93. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/registry.rs +0 -0
  94. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/relation.rs +0 -0
  95. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
  96. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  97. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/repository.rs +0 -0
  98. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/sha256.rs +0 -0
  99. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  100. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/spec.rs +0 -0
  101. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/staging.rs +0 -0
  102. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/stats.rs +0 -0
  103. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/storage.rs +0 -0
  104. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/api.rs +0 -0
  105. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/http.rs +0 -0
  106. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
  107. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  108. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  109. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/memory.rs +0 -0
  110. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  111. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/pace.rs +0 -0
  112. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  113. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  114. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/push.rs +0 -0
  115. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  116. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
  117. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  118. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/vectors.rs +0 -0
  119. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  120. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
  121. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  122. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
  123. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  124. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  125. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fit.rs +0 -0
  126. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  127. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  128. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  129. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  130. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  131. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  132. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  133. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  134. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  135. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  136. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/home.rs +0 -0
  137. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  138. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  139. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/liveness.rs +0 -0
  140. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  141. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  142. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  143. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  144. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  145. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  146. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  147. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  148. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  149. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  150. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/source_download.rs +0 -0
  151. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  152. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  153. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/staging.rs +0 -0
  154. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  155. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  156. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  157. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  158. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/version.rs +0 -0
  159. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/write_through.rs +0 -0
  160. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-py/Cargo.toml +0 -0
  161. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-py/src/docs.rs +0 -0
  162. {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-py/src/errors.rs +0 -0
  163. {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/_derived_channel.py +0 -0
  164. {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/_source_channel.py +0 -0
  165. {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/_tensors.py +0 -0
  166. {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/derived.py +0 -0
  167. {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/errors.py +0 -0
  168. {tensorfs-0.3.69 → tensorfs-0.3.71}/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.69"
442
+ version = "0.3.71"
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.69"
457
+ version = "0.3.71"
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.69"
6
+ version = "0.3.71"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.69
3
+ Version: 0.3.71
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -445,7 +445,7 @@ pub fn cmd_bank(dialect: &str, converter: &str, f: &Flags) -> ExitCode {
445
445
  let mut reviewed = Vec::new();
446
446
  for (component, path) in &srcs {
447
447
  let (full, raw) = ok!(source_core::read_carrier_at(path));
448
- let h = if conv.name == "single_file.identity/1" {
448
+ let h = if conv.dialect == "safetensors.single_file" {
449
449
  ok!(convert::single_file_component(component, &full))
450
450
  } else {
451
451
  full
@@ -584,11 +584,11 @@ pub fn cmd_bank(dialect: &str, converter: &str, f: &Flags) -> ExitCode {
584
584
  .collect::<Vec<_>>()
585
585
  .join(",");
586
586
  let variant = flag(f, "spec-variant").and_then(|value| value.parse().ok());
587
- let mut prepared = match source_core::prepare_with_registry(
587
+ let mut prepared = match source_core::prepare(
588
588
  &target,
589
589
  &unnamed(&srcs),
590
590
  &CarrierSet::default(),
591
- &reg,
591
+ Some(&reg),
592
592
  variant,
593
593
  ) {
594
594
  Ok(prepared) => prepared,
@@ -849,17 +849,22 @@ fn prepare_closed_source_plan(
849
849
  if closed.registry != BUILTIN_REGISTRY {
850
850
  flags.push(("registry".to_string(), closed.registry.clone()));
851
851
  }
852
+ let as_is = closed.converter == convert::IDENTITY;
853
+ if as_is {
854
+ flags.push(("source-profile".to_string(), source_core::AS_IS.to_string()));
855
+ }
852
856
  for (component, source) in &closed.sources {
853
857
  flags.push((
854
858
  "source".to_string(),
855
859
  format!("{component}={}", source.path.to_string_lossy()),
856
860
  ));
857
861
  }
858
- let mut prepared = source_core::prepare_with_registry(
862
+ // The plan was re-derived above, so its converter is the planner's: as-is has no registry.
863
+ let mut prepared = source_core::prepare(
859
864
  &closed.target,
860
865
  &closed.sources,
861
866
  &closed.set,
862
- &registry,
867
+ (!as_is).then_some(&registry),
863
868
  None,
864
869
  )
865
870
  .map_err(bail)?;
@@ -989,15 +994,17 @@ pub(crate) fn construction_order(f: &Flags) -> Result<Vec<(String, String)>, Exi
989
994
 
990
995
  /// Headers in, plan out. The one function `plan`, `run` and the worker all go through, so
991
996
  /// a preview and a run cannot disagree about what was going to happen.
997
+ /// `--source-profile as-is/1` stores the sources as they are, unclassified.
992
998
  fn prepare(f: &Flags, target: &str) -> Result<Prepared, ExitCode> {
993
999
  let sources = sources(f)?;
994
1000
  let registry = load_registry(f)?;
995
1001
  let variant = flag(f, "spec-variant").and_then(|value| value.parse().ok());
996
- source_core::prepare_with_registry(
1002
+ let as_is = flag(f, "source-profile") == Some(source_core::AS_IS);
1003
+ source_core::prepare(
997
1004
  target,
998
1005
  &unnamed(&sources),
999
1006
  &CarrierSet::default(),
1000
- &registry,
1007
+ (!as_is).then_some(&registry),
1001
1008
  variant,
1002
1009
  )
1003
1010
  .map_err(bail)
@@ -1206,6 +1213,7 @@ pub fn cmd_run(root: &Path, target: &str, f: &Flags) -> ExitCode {
1206
1213
  | "tenant"
1207
1214
  | "isolation-tier"
1208
1215
  | "checkpoint-bytes"
1216
+ | "source-profile"
1209
1217
  ) {
1210
1218
  argv.push(format!("--{k}"));
1211
1219
  argv.push(v.clone());
@@ -29,8 +29,6 @@ use std::process::{Child, ChildStdout, ExitStatus};
29
29
  use std::sync::mpsc::{self, RecvTimeoutError};
30
30
  use std::time::{Duration, Instant};
31
31
 
32
- use tensorfs_core::catalog::BUSY_TIMEOUT;
33
-
34
32
  /// How many times its own worst silence a worker is granted before the parent looks for
35
33
  /// inactivity, and how many times its own projected run it may spin without progress.
36
34
  /// Three doublings past the demonstrated maximum: the worst of n observed gaps
@@ -101,8 +99,11 @@ fn parse_progress(line: &str) -> Option<(u64, u64)> {
101
99
  Some((moved.parse().ok()?, admitted.parse().ok()?))
102
100
  }
103
101
 
102
+ /// Patience before a worker has shown any gap to measure against.
103
+ const FIRST_PATIENCE: Duration = Duration::from_secs(5);
104
+
104
105
  fn patience(worst_gap: Duration) -> Duration {
105
- (worst_gap * PATIENCE_FACTOR).max(BUSY_TIMEOUT)
106
+ (worst_gap * PATIENCE_FACTOR).max(FIRST_PATIENCE)
106
107
  }
107
108
 
108
109
  /// What a silent-but-active worker is allowed: the whole declared input at the rate this
@@ -0,0 +1,121 @@
1
+ //! Waiting for another process's catalog lock by whether its holder can still release it.
2
+ //!
3
+ //! SQLite's locks are POSIX locks: they die with the process that holds them. A holder
4
+ //! that is alive is committing or about to, so a writer waits for it for as long as it
5
+ //! takes; a flat deadline refused a busy, progressing writer as if it were dead. The only
6
+ //! holder that cannot release is one that is stopped (a debugger, SIGSTOP) or a zombie,
7
+ //! and only then does a statement give up.
8
+ //!
9
+ //! Holders come from `/proc/locks`, matched by the catalog's inode (and its WAL `-shm`).
10
+ //! The catalog file itself is never opened here: closing a second descriptor on it would
11
+ //! drop every POSIX lock this process holds on it.
12
+
13
+ use std::cell::RefCell;
14
+ use std::path::{Path, PathBuf};
15
+ use std::time::Duration;
16
+
17
+ use rusqlite::Connection;
18
+
19
+ use crate::err::Result;
20
+
21
+ thread_local! {
22
+ /// The catalog this thread's statements wait on. A connection is opened for each
23
+ /// operation just before it is used, on the thread that uses it, so the one opened last
24
+ /// here is the one a busy statement belongs to.
25
+ static WAITING_ON: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
26
+ }
27
+
28
+ /// Install the waiting handler on one connection.
29
+ pub(crate) fn wait_for_live_holders(connection: &Connection, path: &Path) -> Result<()> {
30
+ WAITING_ON.with(|waiting| *waiting.borrow_mut() = Some(path.to_path_buf()));
31
+ connection
32
+ .busy_handler(Some(busy))
33
+ .map_err(|error| super::sql("install catalog lock wait", error))
34
+ }
35
+
36
+ fn busy(attempt: i32) -> bool {
37
+ let path = WAITING_ON.with(|waiting| waiting.borrow().clone());
38
+ if path.is_some_and(|path| holders_cannot_release(&path)) {
39
+ return false;
40
+ }
41
+ std::thread::sleep(Duration::from_millis(attempt.clamp(1, 64) as u64));
42
+ true
43
+ }
44
+
45
+ /// Every process holding a write lock on the catalog is stopped or a zombie. Without a
46
+ /// holder to name (the lock just went, or no `/proc`), keep asking.
47
+ fn holders_cannot_release(path: &Path) -> bool {
48
+ let inodes: Vec<u64> = [path.to_path_buf(), shm(path)]
49
+ .iter()
50
+ .filter_map(|file| std::fs::metadata(file).ok())
51
+ .map(|metadata| std::os::unix::fs::MetadataExt::ino(&metadata))
52
+ .collect();
53
+ let Ok(locks) = std::fs::read_to_string("/proc/locks") else {
54
+ return false;
55
+ };
56
+ let holders = write_holders(&locks, &inodes, std::process::id());
57
+ !holders.is_empty() && holders.iter().all(|pid| stopped(*pid))
58
+ }
59
+
60
+ fn shm(path: &Path) -> PathBuf {
61
+ let mut name = path.as_os_str().to_owned();
62
+ name.push("-shm");
63
+ PathBuf::from(name)
64
+ }
65
+
66
+ /// Other processes' POSIX write locks on these inodes, from `/proc/locks` lines such as
67
+ /// `3: POSIX ADVISORY WRITE 167671 103:02:204736108 1073741824 1073742335`.
68
+ fn write_holders(locks: &str, inodes: &[u64], own: u32) -> Vec<u32> {
69
+ locks
70
+ .lines()
71
+ .filter_map(|line| {
72
+ let fields: Vec<&str> = line.split_whitespace().collect();
73
+ let at = fields.iter().position(|field| *field == "POSIX")?;
74
+ if fields.get(at + 2) != Some(&"WRITE") {
75
+ return None;
76
+ }
77
+ let pid: u32 = fields.get(at + 3)?.parse().ok()?;
78
+ let inode: u64 = fields.get(at + 4)?.rsplit(':').next()?.parse().ok()?;
79
+ (pid != own && inodes.contains(&inode)).then_some(pid)
80
+ })
81
+ .collect()
82
+ }
83
+
84
+ /// Stopped, traced-stopped, zombie or dead: a process that cannot release its lock.
85
+ fn stopped(pid: u32) -> bool {
86
+ std::fs::read_to_string(format!("/proc/{pid}/stat"))
87
+ .ok()
88
+ .and_then(|stat| {
89
+ let state = stat
90
+ .rsplit_once(')')?
91
+ .1
92
+ .split_whitespace()
93
+ .next()?
94
+ .to_string();
95
+ Some(matches!(state.as_str(), "T" | "t" | "Z" | "X"))
96
+ })
97
+ .unwrap_or(false)
98
+ }
99
+
100
+ #[cfg(test)]
101
+ mod tests {
102
+ use super::*;
103
+
104
+ #[test]
105
+ fn only_other_processes_write_locks_on_the_catalog_are_holders() {
106
+ let locks = "\
107
+ 1: POSIX ADVISORY WRITE 4242 103:02:77 1073741824 1073742335
108
+ 2: POSIX ADVISORY READ 5151 103:02:77 1073741826 1073742335
109
+ 3: POSIX ADVISORY WRITE 6161 103:02:99 120 120
110
+ 4: FLOCK ADVISORY WRITE 7171 103:02:77 0 EOF
111
+ 5: POSIX ADVISORY WRITE 1000 103:02:77 0 EOF
112
+ 6: -> POSIX ADVISORY WRITE 8181 103:02:77 0 EOF
113
+ ";
114
+ assert_eq!(write_holders(locks, &[77], 1000), vec![4242, 8181]);
115
+ assert_eq!(
116
+ write_holders(locks, &[77, 99], 1000),
117
+ vec![4242, 6161, 8181]
118
+ );
119
+ assert!(write_holders(locks, &[5], 1000).is_empty());
120
+ }
121
+ }
@@ -6,7 +6,6 @@ use std::fs::{self, File};
6
6
  use std::os::unix::fs::MetadataExt;
7
7
  use std::path::{Path, PathBuf};
8
8
  use std::sync::{Arc, Mutex, OnceLock, RwLock, Weak};
9
- use std::time::Duration;
10
9
 
11
10
  use fs2::FileExt;
12
11
  use rusqlite::{params, Connection};
@@ -14,10 +13,7 @@ use rusqlite::{params, Connection};
14
13
  use crate::err::{refuse, Code, Refusal, Result};
15
14
  use crate::storage::Projection;
16
15
 
17
- /// The longest one catalog call may SLEEP on another writer's lock before it refuses. It is
18
- /// the only wait a store operation can legitimately spend blocked and idle, so it is also
19
- /// the floor under any patience a supervisor grants a worker that has gone quiet.
20
- pub const BUSY_TIMEOUT: Duration = Duration::from_secs(5);
16
+ mod lock;
21
17
 
22
18
  /// The catalog this build writes. A newer build may only ADD tables and columns (an
23
19
  /// incompatible change renames the table), so any catalog at this version or later that
@@ -62,11 +58,6 @@ fn sql(what: impl AsRef<str>, error: rusqlite::Error) -> Refusal {
62
58
  }
63
59
  }
64
60
 
65
- /// Whether a catalog refusal is SQLite's busy timeout expiring behind another writer.
66
- fn busy(refusal: &Refusal) -> bool {
67
- refusal.code == Code::IO_FAILED && refusal.detail.ends_with("database is locked")
68
- }
69
-
70
61
  fn io(what: impl AsRef<str>, error: std::io::Error) -> Refusal {
71
62
  crate::store::classify_io(what, error)
72
63
  }
@@ -134,13 +125,32 @@ fn connect(path: &Path) -> Result<Connection> {
134
125
  }
135
126
  sql("open catalog", error)
136
127
  })?;
128
+ configure(&connection, path)?;
129
+ Ok(connection)
130
+ }
131
+
132
+ /// Journal and sync by what a crash can take from the disk the catalog lives on
133
+ /// (`disk::DiskClass`): an ephemeral disk dies with the machine, so no sync buys anything;
134
+ /// a persistent one keeps a WAL synced at checkpoints; a network filesystem keeps the
135
+ /// rollback journal and a full sync, because WAL's shared memory is not coherent across
136
+ /// hosts. The catalog is only ever opened by processes on one host otherwise.
137
+ pub(crate) fn configure(connection: &Connection, path: &Path) -> Result<()> {
138
+ lock::wait_for_live_holders(connection, path)?;
139
+ let (journal, synchronous) = match crate::disk::DiskClass::of(path.parent().unwrap_or(path)) {
140
+ crate::disk::DiskClass::Ephemeral => ("wal", "OFF"),
141
+ crate::disk::DiskClass::Persistent => ("wal", "NORMAL"),
142
+ crate::disk::DiskClass::Network => ("delete", "FULL"),
143
+ };
137
144
  connection
138
- .busy_timeout(BUSY_TIMEOUT)
139
- .map_err(|error| sql("set catalog busy timeout", error))?;
145
+ .query_row(&format!("PRAGMA journal_mode={journal}"), [], |row| {
146
+ row.get::<_, String>(0)
147
+ })
148
+ .map_err(|error| sql("set catalog journal", error))?;
140
149
  connection
141
- .execute_batch("PRAGMA foreign_keys=ON; PRAGMA synchronous=FULL;")
142
- .map_err(|error| sql("configure catalog", error))?;
143
- Ok(connection)
150
+ .execute_batch(&format!(
151
+ "PRAGMA foreign_keys=ON; PRAGMA synchronous={synchronous};"
152
+ ))
153
+ .map_err(|error| sql("configure catalog", error))
144
154
  }
145
155
 
146
156
  /// A connection to an existing catalog of exactly this schema.
@@ -390,22 +400,6 @@ impl Catalog {
390
400
  Ok(catalog)
391
401
  }
392
402
 
393
- /// `open` for an object admission. A catalog busy past SQLite's timeout while this
394
- /// process's own admission batches keep committing is a queue, not a failure: the
395
- /// verified bytes wait for their turn instead of being thrown away and fetched again.
396
- /// Busy with no batch committed in the meantime is another writer's lock, and refuses.
397
- pub(crate) fn open_admitting(root: &Path) -> Result<Self> {
398
- loop {
399
- let committed = crate::stats::snapshot().admission_batches;
400
- match Self::open(root) {
401
- Err(refusal)
402
- if busy(&refusal) && crate::stats::snapshot().admission_batches > committed => {
403
- }
404
- opened => return opened,
405
- }
406
- }
407
- }
408
-
409
403
  pub fn open(root: &Path) -> Result<Self> {
410
404
  let path = Self::path(root);
411
405
  validated(&path)?;
@@ -1626,6 +1620,89 @@ mod tests {
1626
1620
  ))
1627
1621
  }
1628
1622
 
1623
+ /// Another process holding the catalog's write lock: this test binary, re-run as
1624
+ /// `lock_holder_child`, until it has taken the lock.
1625
+ fn holder(catalog: &Path, millis: u64) -> std::process::Child {
1626
+ use std::io::BufRead;
1627
+ let mut child = std::process::Command::new(std::env::current_exe().unwrap())
1628
+ .args(["--exact", "catalog::tests::lock_holder_child", "--ignored"])
1629
+ .args(["--nocapture", "--test-threads=1"])
1630
+ .env("TENSORFS_TEST_HOLD", catalog)
1631
+ .env("TENSORFS_TEST_HOLD_MS", millis.to_string())
1632
+ .stdout(std::process::Stdio::piped())
1633
+ .spawn()
1634
+ .unwrap();
1635
+ let mut lines = std::io::BufReader::new(child.stdout.take().unwrap()).lines();
1636
+ let held = lines
1637
+ .by_ref()
1638
+ .map_while(std::result::Result::ok)
1639
+ .any(|line| line.ends_with("held"));
1640
+ assert!(held, "the holder never took the catalog lock");
1641
+ std::thread::spawn(move || lines.for_each(drop));
1642
+ child
1643
+ }
1644
+
1645
+ #[test]
1646
+ #[ignore = "run by holder() in a child process"]
1647
+ fn lock_holder_child() {
1648
+ let path = std::env::var("TENSORFS_TEST_HOLD").unwrap();
1649
+ let millis: u64 = std::env::var("TENSORFS_TEST_HOLD_MS")
1650
+ .unwrap()
1651
+ .parse()
1652
+ .unwrap();
1653
+ let connection = Connection::open(path).unwrap();
1654
+ connection.execute_batch("BEGIN IMMEDIATE").unwrap();
1655
+ connection
1656
+ .execute(
1657
+ "INSERT INTO tensorfs_released_manifests VALUES ('held','by',?1)",
1658
+ [std::process::id().to_string()],
1659
+ )
1660
+ .unwrap();
1661
+ println!("held");
1662
+ std::thread::sleep(std::time::Duration::from_millis(millis));
1663
+ connection.execute_batch("COMMIT").unwrap();
1664
+ }
1665
+
1666
+ fn write(catalog: &Catalog, name: &str) -> Result<usize> {
1667
+ catalog
1668
+ .connection()?
1669
+ .execute(
1670
+ "INSERT INTO tensorfs_released_manifests VALUES ('org',?1,'digest')",
1671
+ [name],
1672
+ )
1673
+ .map_err(|error| sql("test write", error))
1674
+ }
1675
+
1676
+ #[test]
1677
+ fn a_live_lock_holder_is_waited_for_past_any_fixed_deadline() {
1678
+ let root = temporary("live-holder");
1679
+ fs::create_dir_all(&root).unwrap();
1680
+ let catalog = Catalog::initialize(&root).unwrap();
1681
+ let mut child = holder(&catalog.path, 7_000);
1682
+ let started = std::time::Instant::now();
1683
+ assert_eq!(write(&catalog, "after-holder").unwrap(), 1);
1684
+ assert!(started.elapsed() >= std::time::Duration::from_secs(6));
1685
+ assert!(child.wait().unwrap().success());
1686
+ let _ = fs::remove_dir_all(root);
1687
+ }
1688
+
1689
+ #[test]
1690
+ fn a_stopped_lock_holder_is_refused() {
1691
+ use rustix::process::{kill_process, Pid, Signal};
1692
+ let root = temporary("stopped-holder");
1693
+ fs::create_dir_all(&root).unwrap();
1694
+ let catalog = Catalog::initialize(&root).unwrap();
1695
+ let mut child = holder(&catalog.path, 60_000);
1696
+ let pid = Pid::from_raw(child.id() as i32).unwrap();
1697
+ kill_process(pid, Signal::STOP).unwrap();
1698
+ let refused = write(&catalog, "behind-a-stopped-holder").unwrap_err();
1699
+ assert!(refused.detail.contains("locked"), "{refused:?}");
1700
+ kill_process(pid, Signal::KILL).unwrap();
1701
+ child.wait().unwrap();
1702
+ assert_eq!(write(&catalog, "after-the-holder-died").unwrap(), 1);
1703
+ let _ = fs::remove_dir_all(root);
1704
+ }
1705
+
1629
1706
  #[test]
1630
1707
  fn catalog_has_seven_tables_and_opens_additive_future_versions() {
1631
1708
  let root = temporary("schema");
@@ -196,6 +196,50 @@ fn ordinary_companions_survive_derivation_without_copying_tensor_backing_files()
196
196
  std::fs::remove_dir_all(store.root()).unwrap();
197
197
  }
198
198
 
199
+ #[test]
200
+ fn a_verified_store_object_is_a_companion_by_reference_and_never_a_new_byte() {
201
+ let (store, meta, id) = fixture();
202
+ let vocab = vec![0x7b; 256 << 10]; // over the inline bound: references have none
203
+ let object = store
204
+ .put_stream(
205
+ &mut vocab.as_slice(),
206
+ Some(&ObjectRef::of(&vocab)),
207
+ &Fault::default(),
208
+ )
209
+ .unwrap()
210
+ .obj;
211
+ let mut d = super::tests::created_declaration();
212
+ d.objects = vec![("tokenizer/vocab.json".into(), object.clone())];
213
+ let encoded = d.canonical_work_bytes().unwrap();
214
+ assert_eq!(
215
+ Declaration::parse_text(std::str::from_utf8(&encoded).unwrap()).unwrap(),
216
+ d
217
+ );
218
+ let writer = begin(&store, &meta, &id, 1, d.clone(), None).unwrap();
219
+ write_weight(&store, &meta, &id);
220
+ let receipt = commit(&store, &meta, &id, 1).unwrap();
221
+ release(&meta, writer);
222
+ assert!(!receipt.added_objects.contains(&object));
223
+ let header = checkpoint::load_header(&store, &receipt.header).unwrap();
224
+ let (name, asset) = &header.assets[0];
225
+ assert_eq!(
226
+ (name.as_str(), &asset.segments),
227
+ ("tokenizer/vocab.json", &vec![object])
228
+ );
229
+ let mut read = Vec::new();
230
+ checkpoint::read_asset(&store, name, asset, &mut read).unwrap();
231
+ assert_eq!(read, vocab);
232
+
233
+ // An object this Store does not hold is refused at commit, never published.
234
+ let next = format!("sha256:{}", "c3".repeat(32));
235
+ d.objects = vec![("tokenizer/vocab.json".into(), ObjectRef::of(b"absent"))];
236
+ let writer = begin(&store, &meta, &next, 1, d, None).unwrap();
237
+ write_weight(&store, &meta, &next);
238
+ assert!(commit(&store, &meta, &next, 1).is_err());
239
+ release(&meta, writer);
240
+ std::fs::remove_dir_all(store.root()).unwrap();
241
+ }
242
+
199
243
  #[test]
200
244
  fn companion_paths_size_count_and_quotas_are_native_refusals() {
201
245
  for path in [
@@ -0,0 +1,114 @@
1
+ //! A live writer on a persistent disk syncs at its checkpoints, not per part, and still
2
+ //! resumes from its last checkpoint after the machine loses its page cache.
3
+
4
+ use super::*;
5
+ use crate::disk::DiskClass;
6
+ use crate::unsynced::testing::{from_an_earlier_boot, markers, persistent_root, tear};
7
+
8
+ const TENSORS: usize = 6;
9
+
10
+ fn declaration() -> Declaration {
11
+ let mut declaration = super::tests::created_declaration();
12
+ let prototype = declaration.components[0].add[0].clone();
13
+ declaration.components[0].add = (0..TENSORS)
14
+ .map(|index| {
15
+ let mut tensor = prototype.clone();
16
+ tensor.key = format!("weight_{index}");
17
+ tensor.shape = vec![128];
18
+ tensor.parts[0].shape = vec![128];
19
+ tensor
20
+ })
21
+ .collect();
22
+ declaration.order = (0..TENSORS)
23
+ .map(|index| ("model".into(), format!("weight_{index}")))
24
+ .collect();
25
+ declaration.max_new_bytes = (TENSORS * 512) as u64;
26
+ declaration
27
+ }
28
+
29
+ fn store(name: &str) -> (Store, Meta) {
30
+ let store = Store::init(&persistent_root(name)).unwrap();
31
+ assert_eq!(store.disk_class(), DiskClass::Persistent);
32
+ let meta = Meta::open(&store).unwrap();
33
+ (store, meta)
34
+ }
35
+
36
+ fn add(store: &Store, meta: &Meta, id: &str, session: u64, writer: &Begin, index: usize) -> Part {
37
+ add_indexed_part(
38
+ store,
39
+ meta,
40
+ id,
41
+ session,
42
+ &writer.additions,
43
+ None,
44
+ ("model", &format!("weight_{index}"), "value"),
45
+ &mut [index as u8 + 1; 512].as_slice(),
46
+ )
47
+ .unwrap()
48
+ .part
49
+ }
50
+
51
+ #[test]
52
+ fn a_power_loss_after_a_checkpoint_resumes_from_it_to_the_same_manifest() {
53
+ let id = format!("sha256:{}", "6c".repeat(32));
54
+
55
+ // The reference: one uninterrupted writer.
56
+ let (clean, clean_meta) = store("group-commit-clean");
57
+ let writer = begin(&clean, &clean_meta, &id, 1, declaration(), None).unwrap();
58
+ let live = writer_store(&clean).unwrap();
59
+ for index in 0..TENSORS {
60
+ add(&live, &clean_meta, &id, 1, &writer, index);
61
+ }
62
+ let expected = commit(&live, &clean_meta, &id, 1).unwrap();
63
+ finish_writer(&live).unwrap();
64
+ release_guards(&clean_meta, Some(writer.writer_hold), writer.source_leases);
65
+ assert!(
66
+ markers(clean.root()).is_empty(),
67
+ "a committed writer retires its epoch"
68
+ );
69
+
70
+ // A checkpoint after three parts; three more admitted unsynced; then the machine goes
71
+ // down and the page cache holding those three is lost.
72
+ let (crashed, meta) = store("group-commit-crash");
73
+ let root = crashed.root().to_path_buf();
74
+ let writer = begin(&crashed, &meta, &id, 1, declaration(), None).unwrap();
75
+ let live = writer_store(&crashed).unwrap();
76
+ assert_eq!(
77
+ markers(&root).len(),
78
+ 1,
79
+ "a persistent writer admits under one epoch"
80
+ );
81
+ for index in 0..3 {
82
+ add(&live, &meta, &id, 1, &writer, index);
83
+ }
84
+ let (_, head) = checkpoint_progress(&live, &meta, &id, 1, "request", "model", None).unwrap();
85
+ let lost: Vec<Part> = (3..TENSORS)
86
+ .map(|index| add(&live, &meta, &id, 1, &writer, index))
87
+ .collect();
88
+ fence(&meta, &id, 1).unwrap();
89
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
90
+ drop(live);
91
+ from_an_earlier_boot(&markers(&root).pop().unwrap());
92
+ for segment in lost.iter().flat_map(Part::segments) {
93
+ tear(&crashed.blob_path(&segment.sha256));
94
+ }
95
+
96
+ // Opening repairs the torn objects; the resumed writer keeps what its checkpoint made
97
+ // durable and recomputes the rest.
98
+ let reopened = Store::open(&root).unwrap();
99
+ let meta = Meta::open(&reopened).unwrap();
100
+ let head = head.head.expect("the checkpoint names a head");
101
+ let writer = begin(&reopened, &meta, &id, 2, declaration(), Some(&head)).unwrap();
102
+ assert_eq!(completed(&meta, &id, 2).unwrap().0.len(), 3);
103
+ let live = writer_store(&reopened).unwrap();
104
+ for index in 3..TENSORS {
105
+ add(&live, &meta, &id, 2, &writer, index);
106
+ }
107
+ let resumed = commit(&live, &meta, &id, 2).unwrap();
108
+ finish_writer(&live).unwrap();
109
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
110
+ assert_eq!(resumed.manifest, expected.manifest);
111
+ assert!(markers(&root).is_empty());
112
+ std::fs::remove_dir_all(clean.root()).unwrap();
113
+ std::fs::remove_dir_all(&root).unwrap();
114
+ }