tensorfs 0.3.64__tar.gz → 0.3.66__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 (158) hide show
  1. {tensorfs-0.3.64 → tensorfs-0.3.66}/Cargo.lock +2 -2
  2. {tensorfs-0.3.64 → tensorfs-0.3.66}/Cargo.toml +1 -1
  3. {tensorfs-0.3.64 → tensorfs-0.3.66}/PKG-INFO +1 -1
  4. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/bin/tfs/fetch.rs +7 -3
  5. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/catalog.rs +36 -11
  6. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/derived/part_rows_tests.rs +157 -0
  7. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/derived/progress.rs +10 -0
  8. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/derived/resume_tests.rs +13 -3
  9. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/derived.rs +166 -10
  10. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/durability.rs +67 -27
  11. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/ingest/preflight.rs +20 -0
  12. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/ingest/source.rs +87 -17
  13. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/meta.rs +11 -0
  14. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/store.rs +13 -11
  15. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/tests.rs +122 -89
  16. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/anima_hf_profile.rs +11 -0
  17. tensorfs-0.3.66/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +84 -0
  18. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/fd_budget.rs +32 -19
  19. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-py/src/ingest.rs +33 -9
  20. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-py/src/lib.rs +1 -0
  21. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-py/src/store.rs +7 -1
  22. {tensorfs-0.3.64 → tensorfs-0.3.66}/pyproject.toml +1 -1
  23. {tensorfs-0.3.64 → tensorfs-0.3.66}/python/tensorfs/__init__.py +2 -0
  24. {tensorfs-0.3.64 → tensorfs-0.3.66}/python/tensorfs/_ext.pyi +6 -0
  25. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/Cargo.toml +0 -0
  26. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/build.rs +0 -0
  27. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  28. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  29. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  30. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  31. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/asset_update.rs +0 -0
  32. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/b64.rs +0 -0
  33. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  34. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  35. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  36. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  37. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  38. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  39. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  40. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  41. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  42. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/canon.rs +0 -0
  43. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/capability.rs +0 -0
  44. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/cbor.rs +0 -0
  45. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  46. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  47. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/corpus.rs +0 -0
  48. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  49. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  50. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  51. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  52. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  53. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  54. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/descriptors.rs +0 -0
  55. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/dtype.rs +0 -0
  56. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/err.rs +0 -0
  57. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/fetch.rs +0 -0
  58. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/fill.rs +0 -0
  59. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/fit.rs +0 -0
  60. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/gc.rs +0 -0
  61. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/header.rs +0 -0
  62. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/home.rs +0 -0
  63. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/ids.rs +0 -0
  64. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  65. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  66. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
  67. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  68. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  69. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  70. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  71. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  72. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  73. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  74. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/jcs.rs +0 -0
  75. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/lib.rs +0 -0
  76. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/limits.rs +0 -0
  77. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/machine.rs +0 -0
  78. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/manifest.rs +0 -0
  79. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/project.rs +0 -0
  80. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/providers.rs +0 -0
  81. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/read.rs +0 -0
  82. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/receipt.rs +0 -0
  83. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/reclaim.rs +0 -0
  84. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/registry.rs +0 -0
  85. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/relation.rs +0 -0
  86. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
  87. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  88. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/repository.rs +0 -0
  89. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/sha256.rs +0 -0
  90. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  91. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/spec.rs +0 -0
  92. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/staging.rs +0 -0
  93. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/stats.rs +0 -0
  94. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/storage.rs +0 -0
  95. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/api.rs +0 -0
  96. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/http.rs +0 -0
  97. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
  98. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  99. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  100. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/memory.rs +0 -0
  101. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  102. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/pace.rs +0 -0
  103. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  104. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  105. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  106. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/push.rs +0 -0
  107. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  108. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
  109. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/src/vectors.rs +0 -0
  110. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  111. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  112. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  113. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  114. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/fit.rs +0 -0
  115. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  116. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  117. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  118. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  119. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  120. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  121. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  122. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  123. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  124. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  125. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/home.rs +0 -0
  126. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  127. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  128. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/liveness.rs +0 -0
  129. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  130. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  131. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  132. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  133. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  134. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  135. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  136. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  137. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  138. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  139. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/source_download.rs +0 -0
  140. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  141. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  142. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  143. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/staging.rs +0 -0
  144. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  145. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  146. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  147. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  148. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/version.rs +0 -0
  149. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-core/tests/write_through.rs +0 -0
  150. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-py/Cargo.toml +0 -0
  151. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-py/src/docs.rs +0 -0
  152. {tensorfs-0.3.64 → tensorfs-0.3.66}/crates/tensorfs-py/src/errors.rs +0 -0
  153. {tensorfs-0.3.64 → tensorfs-0.3.66}/python/tensorfs/_derived_channel.py +0 -0
  154. {tensorfs-0.3.64 → tensorfs-0.3.66}/python/tensorfs/_source_channel.py +0 -0
  155. {tensorfs-0.3.64 → tensorfs-0.3.66}/python/tensorfs/_tensors.py +0 -0
  156. {tensorfs-0.3.64 → tensorfs-0.3.66}/python/tensorfs/derived.py +0 -0
  157. {tensorfs-0.3.64 → tensorfs-0.3.66}/python/tensorfs/errors.py +0 -0
  158. {tensorfs-0.3.64 → tensorfs-0.3.66}/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.64"
442
+ version = "0.3.66"
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.64"
457
+ version = "0.3.66"
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.64"
6
+ version = "0.3.66"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.64
3
+ Version: 0.3.66
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -1358,6 +1358,7 @@ struct PullProgress {
1358
1358
  struct PullProgressState {
1359
1359
  total_bytes: u64,
1360
1360
  held_bytes: u64,
1361
+ wanted_objects: u64,
1361
1362
  origin_bytes: u64,
1362
1363
  cache_bytes: u64,
1363
1364
  origin_objects: u64,
@@ -1371,6 +1372,7 @@ impl PullProgress {
1371
1372
  state: Mutex::new(PullProgressState {
1372
1373
  total_bytes: 0,
1373
1374
  held_bytes: 0,
1375
+ wanted_objects: 0,
1374
1376
  origin_bytes: 0,
1375
1377
  cache_bytes: 0,
1376
1378
  origin_objects: 0,
@@ -1385,6 +1387,7 @@ impl PullProgress {
1385
1387
  let mut state = self.state.lock().unwrap();
1386
1388
  state.total_bytes = plan.declared_bytes();
1387
1389
  state.held_bytes = plan.held_bytes();
1390
+ state.wanted_objects = plan.wanted.len() as u64;
1388
1391
  state.last = Some(Instant::now());
1389
1392
  print_progress(&state);
1390
1393
  }
@@ -1447,11 +1450,12 @@ impl PullProgress {
1447
1450
  ///
1448
1451
  /// Every counter is always present, including a zero. A consumer that reads an absent key
1449
1452
  /// as zero cannot tell a cache that answered nothing from a build that never reported the
1450
- /// door at all, and those are the two states worth telling apart.
1453
+ /// door at all, and those are the two states worth telling apart. `wanted_objects` is the
1454
+ /// plan's count of objects to land; a pod host sizes this pull's storage reservation from it.
1451
1455
  fn print_progress(state: &PullProgressState) {
1452
1456
  eprintln!(
1453
- "{{\"event\":\"pull.progress\",\"total_bytes\":{},\"held_bytes\":{},\"origin_bytes\":{},\"cache_bytes\":{},\"origin_objects\":{},\"cache_objects\":{}}}",
1454
- state.total_bytes, state.held_bytes, state.origin_bytes, state.cache_bytes,
1457
+ "{{\"event\":\"pull.progress\",\"total_bytes\":{},\"held_bytes\":{},\"wanted_objects\":{},\"origin_bytes\":{},\"cache_bytes\":{},\"origin_objects\":{},\"cache_objects\":{}}}",
1458
+ state.total_bytes, state.held_bytes, state.wanted_objects, state.origin_bytes, state.cache_bytes,
1455
1459
  state.origin_objects, state.cache_objects
1456
1460
  );
1457
1461
  }
@@ -62,6 +62,11 @@ fn sql(what: impl AsRef<str>, error: rusqlite::Error) -> Refusal {
62
62
  }
63
63
  }
64
64
 
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
+
65
70
  fn io(what: impl AsRef<str>, error: std::io::Error) -> Refusal {
66
71
  crate::store::classify_io(what, error)
67
72
  }
@@ -115,7 +120,20 @@ fn connect(path: &Path) -> Result<Connection> {
115
120
  .entry(path.to_path_buf())
116
121
  .or_default() += 1;
117
122
  crate::stats::catalog_opened();
118
- let connection = Connection::open(path).map_err(|error| sql("open catalog", error))?;
123
+ let connection = Connection::open(path).map_err(|error| {
124
+ // SQLite reports a full descriptor table only as "unable to open"; the table
125
+ // itself answers whether that is what happened.
126
+ if crate::descriptors::available().is_some_and(|free| free == 0) {
127
+ crate::descriptors::exhausted(&std::io::Error::from_raw_os_error(
128
+ rustix::io::Errno::MFILE.raw_os_error(),
129
+ ));
130
+ return Refusal {
131
+ code: Code::FD_HEADROOM,
132
+ detail: format!("open catalog: no file descriptor ({error})"),
133
+ };
134
+ }
135
+ sql("open catalog", error)
136
+ })?;
119
137
  connection
120
138
  .busy_timeout(BUSY_TIMEOUT)
121
139
  .map_err(|error| sql("set catalog busy timeout", error))?;
@@ -372,6 +390,22 @@ impl Catalog {
372
390
  Ok(catalog)
373
391
  }
374
392
 
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
+
375
409
  pub fn open(root: &Path) -> Result<Self> {
376
410
  let path = Self::path(root);
377
411
  validated(&path)?;
@@ -1473,16 +1507,7 @@ impl WriterGuard {
1473
1507
  .read(true)
1474
1508
  .write(true)
1475
1509
  .open(&path)
1476
- .map_err(|error| {
1477
- if crate::descriptors::exhausted(&error) {
1478
- Refusal {
1479
- code: Code::FD_HEADROOM,
1480
- detail: format!("create writer marker: {error}"),
1481
- }
1482
- } else {
1483
- io("create writer marker", error)
1484
- }
1485
- })?;
1510
+ .map_err(|error| io("create writer marker", error))?;
1486
1511
  file.try_lock_exclusive()
1487
1512
  .map_err(|error| io("lock writer marker", error))?;
1488
1513
  Ok(Self {
@@ -44,6 +44,7 @@ fn add(store: &Store, meta: &Meta, id: &str, writer: &Begin, key: &str, byte: u8
44
44
  id,
45
45
  1,
46
46
  &writer.additions,
47
+ None,
47
48
  ("model", key, "value"),
48
49
  &mut [byte; 512].as_slice(),
49
50
  )
@@ -112,6 +113,7 @@ fn parts_are_rows_that_resume_refuse_conflicts_and_stay_retained() {
112
113
  &id,
113
114
  2,
114
115
  &resumed.additions,
116
+ None,
115
117
  ("model", "weight_2", "value"),
116
118
  &mut [3u8; 512].as_slice(),
117
119
  )
@@ -167,3 +169,158 @@ fn an_older_builds_inline_row_and_a_torn_journal_line_are_read() {
167
169
  assert!(!journal.exists());
168
170
  std::fs::remove_dir_all(store.root()).unwrap();
169
171
  }
172
+
173
+ #[test]
174
+ fn indexed_checkpoints_write_the_chain_full_checkpoints_write() {
175
+ const COUNT: usize = 40;
176
+ let mut stores = Vec::new();
177
+ for label in ["full", "indexed"] {
178
+ let (store, meta, id, mut declaration) = fixture(label);
179
+ let prototype = declaration.components[0].add[0].clone();
180
+ declaration.components[0].add = (0..COUNT)
181
+ .map(|index| {
182
+ let mut tensor = prototype.clone();
183
+ tensor.key = format!("weight_{index:03}");
184
+ tensor
185
+ })
186
+ .collect();
187
+ declaration.order = (0..COUNT)
188
+ .map(|index| ("model".into(), format!("weight_{index:03}")))
189
+ .collect();
190
+ declaration.configs = vec![ConfigDeclaration::Add {
191
+ target: "model.json".into(),
192
+ }];
193
+ declaration.max_new_bytes = (COUNT * 512 + 64) as u64;
194
+ let writer = begin(&store, &meta, &id, 1, declaration.clone(), None).unwrap();
195
+ stores.push((store, meta, id, declaration, writer));
196
+ }
197
+ let mut cursor = None;
198
+ let (mut full_prior, mut indexed_prior) = (None, None);
199
+ let mut heads: Vec<ObjectRef> = Vec::new();
200
+ for index in 0..COUNT {
201
+ let key = format!("weight_{index:03}");
202
+ let payload = [index as u8; 512];
203
+ let (store, meta, id, _, writer) = &stores[0];
204
+ add_indexed_part(
205
+ store,
206
+ meta,
207
+ id,
208
+ 1,
209
+ &writer.additions,
210
+ None,
211
+ ("model", &key, "value"),
212
+ &mut payload.as_slice(),
213
+ )
214
+ .unwrap();
215
+ let full = checkpoint_progress(store, meta, id, 1, "request", "model", full_prior.as_ref())
216
+ .unwrap();
217
+ let (store, meta, id, _, writer) = &stores[1];
218
+ add_indexed_part(
219
+ store,
220
+ meta,
221
+ id,
222
+ 1,
223
+ &writer.additions,
224
+ cursor.as_mut(),
225
+ ("model", &key, "value"),
226
+ &mut payload.as_slice(),
227
+ )
228
+ .unwrap();
229
+ // An older acknowledged head sends the writer down the full path, which re-seeds it.
230
+ let previous = if index == 25 {
231
+ heads.get(heads.len() - 3).cloned()
232
+ } else {
233
+ indexed_prior.clone()
234
+ };
235
+ let indexed = checkpoint_indexed(
236
+ store,
237
+ meta,
238
+ id,
239
+ 1,
240
+ "request",
241
+ "model",
242
+ previous.as_ref(),
243
+ &mut cursor,
244
+ )
245
+ .unwrap();
246
+ assert_eq!(full, indexed, "checkpoint {index} diverged");
247
+ assert_eq!(cursor.as_ref().unwrap().tip.head, indexed.1);
248
+ full_prior = full.1.head;
249
+ indexed_prior = indexed.1.head;
250
+ heads.push(indexed_prior.clone().unwrap());
251
+ if index == 20 {
252
+ for (store, meta, id, _, _) in &stores {
253
+ add_config(meta, id, 1, "model.json", &mut b"{}".as_slice()).unwrap();
254
+ let _ = store;
255
+ }
256
+ cursor = None;
257
+ }
258
+ }
259
+ let (store, meta, id, declaration, writer) = stores.pop().unwrap();
260
+ let head = indexed_prior.unwrap();
261
+ let canonical = String::from_utf8(declaration.clone().canonical_work_bytes().unwrap()).unwrap();
262
+ let restored = read_progress(&store, &head, &id, &canonical, &[]).unwrap();
263
+ assert_eq!(restored.added_parts.len(), COUNT);
264
+ assert_eq!(restored.added_configs.len(), 1);
265
+ fence(&meta, &id, 1).unwrap();
266
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
267
+ let resumed = begin(&store, &meta, &id, 2, declaration, Some(&head)).unwrap();
268
+ assert_eq!(completed(&meta, &id, 2).unwrap().0.len(), COUNT);
269
+ fence(&meta, &id, 2).unwrap();
270
+ release_guards(&meta, Some(resumed.writer_hold), resumed.source_leases);
271
+ std::fs::remove_dir_all(store.root()).unwrap();
272
+ for (store, meta, id, _, writer) in stores {
273
+ fence(&meta, &id, 1).unwrap();
274
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
275
+ std::fs::remove_dir_all(store.root()).unwrap();
276
+ }
277
+ }
278
+
279
+ #[test]
280
+ fn an_indexed_checkpoint_does_not_reread_its_chain() {
281
+ let (store, meta, id, declaration) = fixture("no-reread");
282
+ let writer = begin(&store, &meta, &id, 1, declaration, None).unwrap();
283
+ let mut cursor = None;
284
+ let mut prior: Option<ObjectRef> = None;
285
+ let mut first = None;
286
+ for (index, key) in ["weight_0", "weight_1", "weight_2"].into_iter().enumerate() {
287
+ add_indexed_part(
288
+ &store,
289
+ &meta,
290
+ &id,
291
+ 1,
292
+ &writer.additions,
293
+ cursor.as_mut(),
294
+ ("model", key, "value"),
295
+ &mut [index as u8 + 1; 512].as_slice(),
296
+ )
297
+ .unwrap();
298
+ let (_, head) = checkpoint_indexed(
299
+ &store,
300
+ &meta,
301
+ &id,
302
+ 1,
303
+ "request",
304
+ "model",
305
+ prior.as_ref(),
306
+ &mut cursor,
307
+ )
308
+ .unwrap();
309
+ prior = head.head;
310
+ first.get_or_insert(prior.clone().unwrap());
311
+ if index == 0 {
312
+ // The chain's root link is gone: a checkpoint that walked the chain now fails.
313
+ let root = first.clone().unwrap();
314
+ std::fs::remove_file(store.object_path(root.sha256.trim_start_matches("sha256:")))
315
+ .unwrap();
316
+ assert!(
317
+ checkpoint_progress(&store, &meta, &id, 1, "request", "model", prior.as_ref())
318
+ .is_err()
319
+ );
320
+ }
321
+ }
322
+ assert_eq!(cursor.as_ref().unwrap().parts, 3);
323
+ fence(&meta, &id, 1).unwrap();
324
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
325
+ std::fs::remove_dir_all(store.root()).unwrap();
326
+ }
@@ -21,6 +21,16 @@ fn part_key(part: &AddedPart) -> (&str, &str, &str) {
21
21
  }
22
22
 
23
23
  impl Delta {
24
+ /// The parts a writer accepted since its last checkpoint, sorted as a delta stores them.
25
+ pub(super) fn accepted(work: ObjectRef, mut parts: Vec<AddedPart>) -> Self {
26
+ parts.sort_by(|left, right| part_key(left).cmp(&part_key(right)));
27
+ Self {
28
+ work,
29
+ parts,
30
+ configs: Vec::new(),
31
+ }
32
+ }
33
+
24
34
  pub(super) fn between(
25
35
  row: &TransactionRow,
26
36
  prior: Option<&TransactionRow>,
@@ -981,6 +981,7 @@ fn profile_derived_transaction_scaling() {
981
981
  let mut part_ms = 0.0;
982
982
  let mut checkpoint_ms = 0.0;
983
983
  let mut prior = None;
984
+ let mut cursor = None;
984
985
  for index in 0..writes {
985
986
  let key = format!("weight_{index:04}");
986
987
  let started = Instant::now();
@@ -990,15 +991,24 @@ fn profile_derived_transaction_scaling() {
990
991
  &id,
991
992
  1,
992
993
  &writer.additions,
994
+ cursor.as_mut(),
993
995
  ("model", &key, "value"),
994
996
  &mut [index as u8; 16].as_slice(),
995
997
  )
996
998
  .unwrap();
997
999
  part_ms += started.elapsed().as_secs_f64() * 1000.0;
998
1000
  let started = Instant::now();
999
- let (_, head) =
1000
- checkpoint_progress(&store, &meta, &id, 1, "profile", "model", prior.as_ref())
1001
- .unwrap();
1001
+ let (_, head) = checkpoint_indexed(
1002
+ &store,
1003
+ &meta,
1004
+ &id,
1005
+ 1,
1006
+ "profile",
1007
+ "model",
1008
+ prior.as_ref(),
1009
+ &mut cursor,
1010
+ )
1011
+ .unwrap();
1002
1012
  prior = head.head;
1003
1013
  checkpoint_ms += started.elapsed().as_secs_f64() * 1000.0;
1004
1014
  }
@@ -2711,6 +2711,158 @@ pub fn completed(
2711
2711
  ))
2712
2712
  }
2713
2713
 
2714
+ /// A writer's position in its checkpoint chain: the chain's tip and held blobs, its fixed
2715
+ /// plan and work context, and the parts it accepted since. With it a checkpoint writes and
2716
+ /// verifies only those parts instead of re-reading the chain it extends.
2717
+ #[derive(Debug, Clone)]
2718
+ pub struct CheckpointCursor {
2719
+ chain: String,
2720
+ plan: String,
2721
+ work: ObjectRef,
2722
+ tip: crate::durability::Tip,
2723
+ parts: usize,
2724
+ pending: Vec<AddedPart>,
2725
+ }
2726
+
2727
+ impl CheckpointCursor {
2728
+ /// Seed from a checkpoint the full path just wrote or confirmed. `None` when there is
2729
+ /// no head yet to extend.
2730
+ fn at(
2731
+ store: &Store,
2732
+ meta: &Meta,
2733
+ transaction: &str,
2734
+ writer_session: u64,
2735
+ chain: String,
2736
+ plan: &str,
2737
+ head: &crate::durability::Head,
2738
+ ) -> Result<Option<Self>> {
2739
+ let Some(tip) = head.head.as_ref() else {
2740
+ return Ok(None);
2741
+ };
2742
+ let mut rows: Vec<_> = meta.derived_row(transaction)?.into_iter().collect();
2743
+ let row = open_row(&mut rows, transaction, writer_session)?;
2744
+ let context = progress::context(row);
2745
+ let held = crate::durability::local_chain(store, tip, plan)?
2746
+ .blobs
2747
+ .into_iter()
2748
+ .map(|object| object.sha256)
2749
+ .collect();
2750
+ Ok(Some(Self {
2751
+ chain,
2752
+ plan: plan.to_string(),
2753
+ work: ObjectRef::of(&crate::canon::write(&context.to_value())),
2754
+ tip: crate::durability::Tip {
2755
+ head: head.clone(),
2756
+ held,
2757
+ },
2758
+ parts: row.added_parts.len(),
2759
+ pending: Vec::new(),
2760
+ }))
2761
+ }
2762
+
2763
+ /// Extend the chain by the pending parts, or `None` when anything but this writer's own
2764
+ /// accepted parts moved since the cursor was taken.
2765
+ fn advance(
2766
+ &mut self,
2767
+ store: &Store,
2768
+ meta: &Meta,
2769
+ transaction: &str,
2770
+ writer_session: u64,
2771
+ chain: &str,
2772
+ previous: Option<&ObjectRef>,
2773
+ ) -> Result<Option<(String, crate::durability::Head)>> {
2774
+ let tip = self.tip.head.head.clone();
2775
+ let root = roots::read(store, transaction)?;
2776
+ if self.chain != chain
2777
+ || previous.is_some_and(|previous| Some(previous) != tip.as_ref())
2778
+ || root.as_ref().and_then(|root| root.checkpoint.as_ref()) != tip.as_ref()
2779
+ {
2780
+ return Ok(None);
2781
+ }
2782
+ meta.check_session(transaction, writer_session)?;
2783
+ if meta.part_count(transaction)? != self.parts + self.pending.len() {
2784
+ return Ok(None);
2785
+ }
2786
+ if self.pending.is_empty() {
2787
+ return Ok(Some((self.plan.clone(), self.tip.head.clone())));
2788
+ }
2789
+ let parts = std::mem::take(&mut self.pending);
2790
+ let objects = parts
2791
+ .iter()
2792
+ .flat_map(|added| added.part.segments().iter().cloned())
2793
+ .collect();
2794
+ let count = parts.len();
2795
+ let delta = progress::Delta::accepted(self.work.clone(), parts);
2796
+ let policy = crate::durability::Policy {
2797
+ chain,
2798
+ operation: transaction,
2799
+ plan: &self.plan,
2800
+ interval: crate::durability::INTERVAL_BYTES,
2801
+ };
2802
+ let extended = crate::durability::extend(
2803
+ store,
2804
+ &policy,
2805
+ &self.tip,
2806
+ &crate::canon::write(&delta.to_value()),
2807
+ objects,
2808
+ None,
2809
+ )?;
2810
+ let Some(mut root) = root else {
2811
+ return Ok(None);
2812
+ };
2813
+ root.checkpoint = extended.head.head.clone();
2814
+ roots::write(store, &root)?;
2815
+ self.tip
2816
+ .held
2817
+ .extend(extended.named.into_iter().map(|object| object.sha256));
2818
+ self.tip.head = extended.head;
2819
+ self.parts += count;
2820
+ Ok(Some((self.plan.clone(), self.tip.head.clone())))
2821
+ }
2822
+ }
2823
+
2824
+ /// Checkpoint through the writer's cursor, which each full checkpoint re-seeds.
2825
+ #[allow(clippy::too_many_arguments)]
2826
+ pub fn checkpoint_indexed(
2827
+ store: &Store,
2828
+ meta: &Meta,
2829
+ transaction: &str,
2830
+ writer_session: u64,
2831
+ operation: &str,
2832
+ slot: &str,
2833
+ previous: Option<&ObjectRef>,
2834
+ cursor: &mut Option<CheckpointCursor>,
2835
+ ) -> Result<(String, crate::durability::Head)> {
2836
+ let chain = format!("{operation}/{slot}");
2837
+ if let Some(current) = cursor.as_mut() {
2838
+ if let Some(advanced) =
2839
+ current.advance(store, meta, transaction, writer_session, &chain, previous)?
2840
+ {
2841
+ return Ok(advanced);
2842
+ }
2843
+ }
2844
+ *cursor = None;
2845
+ let (plan, head) = checkpoint_progress(
2846
+ store,
2847
+ meta,
2848
+ transaction,
2849
+ writer_session,
2850
+ operation,
2851
+ slot,
2852
+ previous,
2853
+ )?;
2854
+ *cursor = CheckpointCursor::at(
2855
+ store,
2856
+ meta,
2857
+ transaction,
2858
+ writer_session,
2859
+ chain,
2860
+ &plan,
2861
+ &head,
2862
+ )?;
2863
+ Ok((plan, head))
2864
+ }
2865
+
2714
2866
  pub fn checkpoint_progress(
2715
2867
  store: &Store,
2716
2868
  meta: &Meta,
@@ -3115,6 +3267,7 @@ pub fn add_part<R: Read>(
3115
3267
  transaction,
3116
3268
  writer_session,
3117
3269
  &additions,
3270
+ None,
3118
3271
  value,
3119
3272
  reader,
3120
3273
  )
@@ -3122,12 +3275,14 @@ pub fn add_part<R: Read>(
3122
3275
 
3123
3276
  /// Accept one part. The cost is this part's bytes plus one indexed SQL row: the
3124
3277
  /// declaration is not re-read and earlier accepted parts are neither read nor rewritten.
3278
+ #[allow(clippy::too_many_arguments)]
3125
3279
  pub fn add_indexed_part<R: Read>(
3126
3280
  store: &Store,
3127
3281
  meta: &Meta,
3128
3282
  transaction: &str,
3129
3283
  writer_session: u64,
3130
3284
  additions: &Additions,
3285
+ cursor: Option<&mut CheckpointCursor>,
3131
3286
  value: (&str, &str, &str),
3132
3287
  reader: &mut R,
3133
3288
  ) -> Result<crate::checkpoint::Written> {
@@ -3168,16 +3323,17 @@ pub fn add_indexed_part<R: Read>(
3168
3323
  // The private root names the objects before SQL accepts the part, so GC never
3169
3324
  // observes an accepted part whose bytes it may reclaim.
3170
3325
  roots::append_parts(store, &transaction, written.part.segments())?;
3171
- meta.accept_part(
3172
- &transaction,
3173
- writer_session,
3174
- &AddedPart {
3175
- component: component.to_string(),
3176
- key: tensor_key.to_string(),
3177
- role: role.to_string(),
3178
- part: written.part.clone(),
3179
- },
3180
- )?;
3326
+ let added = AddedPart {
3327
+ component: component.to_string(),
3328
+ key: tensor_key.to_string(),
3329
+ role: role.to_string(),
3330
+ part: written.part.clone(),
3331
+ };
3332
+ if meta.accept_part(&transaction, writer_session, &added)? {
3333
+ if let Some(cursor) = cursor {
3334
+ cursor.pending.push(added);
3335
+ }
3336
+ }
3181
3337
  Ok(written)
3182
3338
  }
3183
3339