tensorfs 0.3.63__tar.gz → 0.3.65__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 (157) hide show
  1. {tensorfs-0.3.63 → tensorfs-0.3.65}/Cargo.lock +2 -2
  2. {tensorfs-0.3.63 → tensorfs-0.3.65}/Cargo.toml +1 -1
  3. {tensorfs-0.3.63 → tensorfs-0.3.65}/PKG-INFO +1 -1
  4. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/fetch.rs +3 -1
  5. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/catalog.rs +10 -1
  6. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/part_rows_tests.rs +157 -0
  7. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/progress.rs +10 -0
  8. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/resume_tests.rs +13 -3
  9. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived.rs +166 -10
  10. tensorfs-0.3.65/crates/tensorfs-core/src/descriptors.rs +112 -0
  11. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/durability.rs +67 -27
  12. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/lib.rs +1 -0
  13. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/meta.rs +11 -0
  14. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/providers.rs +1 -0
  15. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/store.rs +41 -3
  16. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/http.rs +27 -9
  17. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +9 -5
  18. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/ledger.rs +15 -1
  19. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/pace.rs +54 -8
  20. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/policy.rs +45 -6
  21. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/pull.rs +121 -49
  22. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/tests.rs +5 -0
  23. tensorfs-0.3.65/crates/tensorfs-core/tests/fd_budget.rs +256 -0
  24. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_artifacts.rs +1 -0
  25. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_download.rs +1 -0
  26. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_prefixes.rs +1 -0
  27. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/store_boundaries.rs +32 -0
  28. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/transfer_memory.rs +1 -0
  29. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-py/src/store.rs +11 -1
  30. {tensorfs-0.3.63 → tensorfs-0.3.65}/pyproject.toml +1 -1
  31. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/Cargo.toml +0 -0
  32. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/build.rs +0 -0
  33. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  34. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  35. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  36. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  37. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/asset_update.rs +0 -0
  38. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/b64.rs +0 -0
  39. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  40. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  41. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  42. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  43. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  44. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  45. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  46. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  47. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  48. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/canon.rs +0 -0
  49. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/capability.rs +0 -0
  50. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/cbor.rs +0 -0
  51. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  52. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  53. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/corpus.rs +0 -0
  54. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  55. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  56. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  57. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  58. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  59. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  60. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/dtype.rs +0 -0
  61. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/err.rs +0 -0
  62. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/fetch.rs +0 -0
  63. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/fill.rs +0 -0
  64. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/fit.rs +0 -0
  65. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/gc.rs +0 -0
  66. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/header.rs +0 -0
  67. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/home.rs +0 -0
  68. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ids.rs +0 -0
  69. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  70. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  71. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
  72. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  73. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  74. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  75. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  76. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  77. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  78. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  79. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  80. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  81. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/jcs.rs +0 -0
  82. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/limits.rs +0 -0
  83. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/machine.rs +0 -0
  84. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/manifest.rs +0 -0
  85. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/project.rs +0 -0
  86. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/read.rs +0 -0
  87. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/receipt.rs +0 -0
  88. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/reclaim.rs +0 -0
  89. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/registry.rs +0 -0
  90. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/relation.rs +0 -0
  91. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
  92. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  93. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/repository.rs +0 -0
  94. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/sha256.rs +0 -0
  95. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  96. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/spec.rs +0 -0
  97. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/staging.rs +0 -0
  98. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/stats.rs +0 -0
  99. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/storage.rs +0 -0
  100. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/api.rs +0 -0
  101. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  102. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/memory.rs +0 -0
  103. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  104. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  105. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/push.rs +0 -0
  106. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  107. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
  108. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/vectors.rs +0 -0
  109. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  110. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
  111. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  112. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  113. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  114. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fit.rs +0 -0
  115. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  116. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  117. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  118. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  119. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  120. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  121. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  122. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  123. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  124. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  125. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/home.rs +0 -0
  126. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  127. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  128. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/liveness.rs +0 -0
  129. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  130. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  131. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  132. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  133. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  134. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  135. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  136. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  137. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  138. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  139. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  140. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/staging.rs +0 -0
  141. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  142. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  143. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/version.rs +0 -0
  144. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/write_through.rs +0 -0
  145. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-py/Cargo.toml +0 -0
  146. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-py/src/docs.rs +0 -0
  147. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-py/src/errors.rs +0 -0
  148. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-py/src/ingest.rs +0 -0
  149. {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-py/src/lib.rs +0 -0
  150. {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/__init__.py +0 -0
  151. {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/_derived_channel.py +0 -0
  152. {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/_ext.pyi +0 -0
  153. {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/_source_channel.py +0 -0
  154. {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/_tensors.py +0 -0
  155. {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/derived.py +0 -0
  156. {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/errors.py +0 -0
  157. {tensorfs-0.3.63 → tensorfs-0.3.65}/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.63"
442
+ version = "0.3.65"
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.63"
457
+ version = "0.3.65"
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.63"
6
+ version = "0.3.65"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.63
3
+ Version: 0.3.65
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -182,6 +182,7 @@ fn policy_of(flags: &Flags) -> SourcePolicy {
182
182
  .unwrap_or_default(),
183
183
  allow_local: crate::flag_on(flags, "allow-local"),
184
184
  max_redirects: crate::flag_num(flags, "max-redirects", 0u32),
185
+ ..Default::default()
185
186
  }
186
187
  }
187
188
 
@@ -334,12 +335,13 @@ fn pull_with_budget(
334
335
  // And what admitting them cost: a pull whose streams stop growing while its batches
335
336
  // take most of the wall time is held by the disk, not the link.
336
337
  eprintln!(
337
- "{{\"event\":\"pull.streams\",\"model\":{},\"seconds\":{:.3},\"bytes_moved\":{},\"streams\":{},\"levels\":[{}],\"admission\":{{\"objects\":{},\"batches\":{},\"seconds\":{:.3}}}}}",
338
+ "{{\"event\":\"pull.streams\",\"model\":{},\"seconds\":{:.3},\"bytes_moved\":{},\"streams\":{},\"levels\":[{}],\"backoffs\":{},\"admission\":{{\"objects\":{},\"batches\":{},\"seconds\":{:.3}}}}}",
338
339
  quoted(&report.model),
339
340
  seconds,
340
341
  report.bytes_moved,
341
342
  report.streams,
342
343
  levels.join(","),
344
+ report.backoffs,
343
345
  after.admitted_objects - before.admitted_objects,
344
346
  after.admission_batches - before.admission_batches,
345
347
  (after.admission_nanos - before.admission_nanos) as f64 / 1e9,
@@ -1473,7 +1473,16 @@ impl WriterGuard {
1473
1473
  .read(true)
1474
1474
  .write(true)
1475
1475
  .open(&path)
1476
- .map_err(|error| io("create writer marker", error))?;
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
+ })?;
1477
1486
  file.try_lock_exclusive()
1478
1487
  .map_err(|error| io("lock writer marker", error))?;
1479
1488
  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
 
@@ -0,0 +1,112 @@
1
+ //! File descriptors as a budget: raised to what the process may have, measured before a
2
+ //! transfer sizes itself, and an exhausted table reported as backpressure.
3
+ //!
4
+ //! Run 1297 (2026-09-27) grew a pull toward 128 streams under a 1,024 soft RLIMIT_NOFILE;
5
+ //! each object in flight holds a socket, a temp file, writer markers and a catalog
6
+ //! connection, and the table ran out mid-pull (`name resolution failed (Too many open
7
+ //! files)`). A Go supervisor raises its own soft limit but hands its children the original.
8
+ use std::sync::atomic::{AtomicU64, Ordering};
9
+ use std::sync::Once;
10
+
11
+ /// Descriptors one object stream may hold at once, and what it is spent on: the object
12
+ /// socket and one pooled idle socket (2); the temp file and the two writer-marker files of
13
+ /// its admission, nested once more by the catalog record (5); the catalog connection and its
14
+ /// journal (2); and the transient DNS socket or contender of a retry (3).
15
+ pub const PER_STREAM: u64 = 12;
16
+
17
+ /// Descriptors left for everything that is not a stream: stdio, the Store's own locks, the
18
+ /// hub calls, the progress channel and whatever the caller opens next.
19
+ pub const MARGIN: u64 = 64;
20
+
21
+ static RAISE: Once = Once::new();
22
+ static EXHAUSTED: AtomicU64 = AtomicU64::new(0);
23
+
24
+ /// Raise the soft RLIMIT_NOFILE to the hard limit, once per process. Idempotent; a refused
25
+ /// raise leaves the limit where it was and the budget below measures that.
26
+ pub fn raise() {
27
+ RAISE.call_once(|| {
28
+ use rustix::process::{getrlimit, setrlimit, Resource, Rlimit};
29
+ let limit = getrlimit(Resource::Nofile);
30
+ if let (Some(soft), Some(hard)) = (limit.current, limit.maximum) {
31
+ if soft < hard {
32
+ let _ = setrlimit(
33
+ Resource::Nofile,
34
+ Rlimit {
35
+ current: Some(hard),
36
+ maximum: Some(hard),
37
+ },
38
+ );
39
+ }
40
+ }
41
+ });
42
+ }
43
+
44
+ /// Descriptors this process can still open now: soft limit less those open and `MARGIN`.
45
+ /// `None` when the platform cannot say (no `/proc`, or no soft limit).
46
+ pub fn available() -> Option<u64> {
47
+ let soft = rustix::process::getrlimit(rustix::process::Resource::Nofile).current?;
48
+ let open = std::fs::read_dir("/proc/self/fd").ok()?.count() as u64;
49
+ Some(soft.saturating_sub(open).saturating_sub(MARGIN))
50
+ }
51
+
52
+ /// Object streams the descriptor table allows now: at most `wanted`, at least one.
53
+ pub fn streams_within(wanted: usize) -> usize {
54
+ streams_for(wanted, available())
55
+ }
56
+
57
+ /// `streams_within` against a stated number of free descriptors.
58
+ pub fn streams_for(wanted: usize, available: Option<u64>) -> usize {
59
+ let fits = available.map_or(usize::MAX, |free| {
60
+ usize::try_from(free / PER_STREAM).unwrap_or(usize::MAX)
61
+ });
62
+ wanted.min(fits).max(1)
63
+ }
64
+
65
+ /// Whether `error` is the descriptor table being full, counting it when it is. Call it
66
+ /// where an open, socket or resolution fails, before the error loses its errno.
67
+ pub fn exhausted(error: &std::io::Error) -> bool {
68
+ let full = matches!(
69
+ error.raw_os_error(),
70
+ Some(code) if code == rustix::io::Errno::MFILE.raw_os_error()
71
+ || code == rustix::io::Errno::NFILE.raw_os_error()
72
+ );
73
+ if full {
74
+ EXHAUSTED.fetch_add(1, Ordering::Relaxed);
75
+ }
76
+ full
77
+ }
78
+
79
+ /// How many times this process has found its descriptor table full. A caller diffs two
80
+ /// reads around its own work.
81
+ pub fn exhaustions() -> u64 {
82
+ EXHAUSTED.load(Ordering::Relaxed)
83
+ }
84
+
85
+ #[cfg(test)]
86
+ mod tests {
87
+ use super::*;
88
+
89
+ #[test]
90
+ fn the_budget_is_descriptors_over_per_stream_cost() {
91
+ assert_eq!(streams_for(128, Some(PER_STREAM * 10)), 10);
92
+ assert_eq!(streams_for(8, Some(PER_STREAM * 10)), 8);
93
+ assert_eq!(streams_for(128, Some(0)), 1, "one stream is always allowed");
94
+ assert_eq!(
95
+ streams_for(128, None),
96
+ 128,
97
+ "an unmeasurable table is not a shortage"
98
+ );
99
+ }
100
+
101
+ #[test]
102
+ fn only_a_full_table_counts_as_exhaustion() {
103
+ let before = exhaustions();
104
+ assert!(exhausted(&std::io::Error::from_raw_os_error(
105
+ rustix::io::Errno::MFILE.raw_os_error()
106
+ )));
107
+ assert!(!exhausted(&std::io::Error::from_raw_os_error(
108
+ rustix::io::Errno::CONNREFUSED.raw_os_error()
109
+ )));
110
+ assert!(exhaustions() > before);
111
+ }
112
+ }