tensorfs 0.3.62__tar.gz → 0.3.64__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.62 → tensorfs-0.3.64}/Cargo.lock +2 -2
  2. {tensorfs-0.3.62 → tensorfs-0.3.64}/Cargo.toml +1 -1
  3. {tensorfs-0.3.62 → tensorfs-0.3.64}/PKG-INFO +1 -1
  4. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/fetch.rs +3 -1
  5. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/catalog.rs +10 -1
  6. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/part_rows_tests.rs +24 -6
  7. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/roots.rs +17 -5
  8. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived.rs +29 -5
  9. tensorfs-0.3.64/crates/tensorfs-core/src/descriptors.rs +112 -0
  10. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/lib.rs +1 -0
  11. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/meta.rs +5 -3
  12. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/providers.rs +1 -0
  13. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/store.rs +41 -3
  14. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/http.rs +27 -9
  15. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +9 -5
  16. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/ledger.rs +15 -1
  17. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/pace.rs +54 -8
  18. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/policy.rs +45 -6
  19. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/pull.rs +121 -49
  20. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/tests.rs +5 -0
  21. tensorfs-0.3.64/crates/tensorfs-core/tests/anima_hf_profile.rs +166 -0
  22. tensorfs-0.3.64/crates/tensorfs-core/tests/fd_budget.rs +256 -0
  23. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/qwen_image21_profile.rs +5 -4
  24. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_artifacts.rs +1 -0
  25. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_download.rs +1 -0
  26. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_prefixes.rs +1 -0
  27. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/store_boundaries.rs +32 -0
  28. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/transfer_memory.rs +1 -0
  29. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-py/src/store.rs +4 -0
  30. {tensorfs-0.3.62 → tensorfs-0.3.64}/pyproject.toml +1 -1
  31. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/Cargo.toml +0 -0
  32. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/build.rs +0 -0
  33. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  34. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  35. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  36. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  37. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/asset_update.rs +0 -0
  38. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/b64.rs +0 -0
  39. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  40. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  41. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  42. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  43. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  44. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  45. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  46. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  47. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  48. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/canon.rs +0 -0
  49. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/capability.rs +0 -0
  50. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/cbor.rs +0 -0
  51. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  52. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  53. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/corpus.rs +0 -0
  54. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  55. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  56. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  57. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  58. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  59. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  60. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  61. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/dtype.rs +0 -0
  62. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/durability.rs +0 -0
  63. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/err.rs +0 -0
  64. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/fetch.rs +0 -0
  65. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/fill.rs +0 -0
  66. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/fit.rs +0 -0
  67. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/gc.rs +0 -0
  68. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/header.rs +0 -0
  69. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/home.rs +0 -0
  70. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ids.rs +0 -0
  71. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  72. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  73. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
  74. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  75. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  76. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  77. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  78. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  79. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  80. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  81. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  82. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  83. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/jcs.rs +0 -0
  84. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/limits.rs +0 -0
  85. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/machine.rs +0 -0
  86. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/manifest.rs +0 -0
  87. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/project.rs +0 -0
  88. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/read.rs +0 -0
  89. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/receipt.rs +0 -0
  90. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/reclaim.rs +0 -0
  91. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/registry.rs +0 -0
  92. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/relation.rs +0 -0
  93. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
  94. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  95. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/repository.rs +0 -0
  96. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/sha256.rs +0 -0
  97. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  98. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/spec.rs +0 -0
  99. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/staging.rs +0 -0
  100. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/stats.rs +0 -0
  101. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/storage.rs +0 -0
  102. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/api.rs +0 -0
  103. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  104. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/memory.rs +0 -0
  105. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  106. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  107. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/push.rs +0 -0
  108. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  109. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
  110. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/vectors.rs +0 -0
  111. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  112. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  113. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  114. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  115. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fit.rs +0 -0
  116. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  117. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  118. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  119. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  120. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  121. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  122. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  123. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  124. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  125. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  126. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/home.rs +0 -0
  127. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  128. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  129. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/liveness.rs +0 -0
  130. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  131. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  132. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  133. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  134. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  135. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  136. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  137. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  138. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  139. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  140. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/staging.rs +0 -0
  141. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  142. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  143. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/version.rs +0 -0
  144. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/write_through.rs +0 -0
  145. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-py/Cargo.toml +0 -0
  146. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-py/src/docs.rs +0 -0
  147. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-py/src/errors.rs +0 -0
  148. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-py/src/ingest.rs +0 -0
  149. {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-py/src/lib.rs +0 -0
  150. {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/__init__.py +0 -0
  151. {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/_derived_channel.py +0 -0
  152. {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/_ext.pyi +0 -0
  153. {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/_source_channel.py +0 -0
  154. {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/_tensors.py +0 -0
  155. {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/derived.py +0 -0
  156. {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/errors.py +0 -0
  157. {tensorfs-0.3.62 → tensorfs-0.3.64}/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.62"
442
+ version = "0.3.64"
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.62"
457
+ version = "0.3.64"
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.62"
6
+ version = "0.3.64"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.62
3
+ Version: 0.3.64
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 {
@@ -25,7 +25,12 @@ fn fixture(label: &str) -> (Store, Meta, String, Declaration) {
25
25
  .map(|index| ("model".into(), format!("weight_{index}")))
26
26
  .collect();
27
27
  declaration.max_new_bytes = 3 * 512;
28
- (store, meta, format!("sha256:{}", "7a".repeat(32)), declaration)
28
+ (
29
+ store,
30
+ meta,
31
+ format!("sha256:{}", "7a".repeat(32)),
32
+ declaration,
33
+ )
29
34
  }
30
35
 
31
36
  fn catalog(store: &Store) -> rusqlite::Connection {
@@ -55,9 +60,16 @@ fn stored_rows(store: &Store, id: &str) -> (usize, Option<i64>, i64) {
55
60
  )
56
61
  .unwrap();
57
62
  let value = crate::canon::parse_canonical(&bytes, limits::DOC_MAX_BYTES).unwrap();
58
- let inline = TransactionRow::from_value(&value).unwrap().added_parts.len();
63
+ let inline = TransactionRow::from_value(&value)
64
+ .unwrap()
65
+ .added_parts
66
+ .len();
59
67
  let parts = connection
60
- .query_row("SELECT count(*) FROM tensorfs_derived_parts WHERE id=?1", [id], |row| row.get(0))
68
+ .query_row(
69
+ "SELECT count(*) FROM tensorfs_derived_parts WHERE id=?1",
70
+ [id],
71
+ |row| row.get(0),
72
+ )
61
73
  .unwrap();
62
74
  (inline, open, parts)
63
75
  }
@@ -74,14 +86,18 @@ fn parts_are_rows_that_resume_refuse_conflicts_and_stay_retained() {
74
86
  add(&store, &meta, &id, &writer, "weight_0", 1).unwrap();
75
87
  assert_eq!(stored_rows(&store, &id).2, 2);
76
88
  assert_eq!(
77
- add(&store, &meta, &id, &writer, "weight_0", 9).unwrap_err().code,
89
+ add(&store, &meta, &id, &writer, "weight_0", 9)
90
+ .unwrap_err()
91
+ .code,
78
92
  Code::TRANSACTION_CONFLICT
79
93
  );
80
94
  // A fenced writer cannot add. Once it is gone, GC keeps its accepted bytes through
81
95
  // the append-only journal alone, and its successor resumes with both parts.
82
96
  fence(&meta, &id, 1).unwrap();
83
97
  assert_eq!(
84
- add(&store, &meta, &id, &writer, "weight_2", 3).unwrap_err().code,
98
+ add(&store, &meta, &id, &writer, "weight_2", 3)
99
+ .unwrap_err()
100
+ .code,
85
101
  Code::WRITER_FENCED
86
102
  );
87
103
  release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
@@ -135,7 +151,9 @@ fn an_older_builds_inline_row_and_a_torn_journal_line_are_read() {
135
151
  std::fs::write(&journal, torn).unwrap();
136
152
 
137
153
  assert_eq!(
138
- add(&store, &meta, &id, &writer, "weight_0", 9).unwrap_err().code,
154
+ add(&store, &meta, &id, &writer, "weight_0", 9)
155
+ .unwrap_err()
156
+ .code,
139
157
  Code::TRANSACTION_CONFLICT
140
158
  );
141
159
  add(&store, &meta, &id, &writer, "weight_1", 2).unwrap();
@@ -2,8 +2,8 @@
2
2
  //! metadata journals lifecycle; GC never needs that database to discover retained bytes.
3
3
 
4
4
  use std::fs::{self, File, OpenOptions};
5
- use std::os::unix::fs::FileExt;
6
5
  use std::io::{Read, Write};
6
+ use std::os::unix::fs::FileExt;
7
7
  use std::path::PathBuf;
8
8
 
9
9
  use crate::canon::{Fields, Value};
@@ -164,7 +164,11 @@ pub(super) fn append_parts(store: &Store, transaction: &str, objects: &[ObjectRe
164
164
  file.read_exact_at(&mut last, length - 1).map_err(io)?;
165
165
  }
166
166
  // Terminate a line torn by a dead writer so this one parses on its own.
167
- let mut bytes = if last[0] == b'\n' { Vec::new() } else { vec![b'\n'] };
167
+ let mut bytes = if last[0] == b'\n' {
168
+ Vec::new()
169
+ } else {
170
+ vec![b'\n']
171
+ };
168
172
  bytes.extend(parts_line(objects));
169
173
  file.write_all(&bytes)
170
174
  .and_then(|()| file.sync_data())
@@ -185,7 +189,11 @@ pub(super) fn rewrite_parts(store: &Store, transaction: &str, objects: &[ObjectR
185
189
  .create_new(true)
186
190
  .open(&temporary)
187
191
  .map_err(io)?;
188
- let bytes = if objects.is_empty() { Vec::new() } else { parts_line(objects) };
192
+ let bytes = if objects.is_empty() {
193
+ Vec::new()
194
+ } else {
195
+ parts_line(objects)
196
+ };
189
197
  file.write_all(&bytes)
190
198
  .and_then(|()| file.sync_all())
191
199
  .map_err(io)?;
@@ -211,8 +219,12 @@ pub(super) fn journaled(store: &Store) -> Result<Vec<ObjectRef>> {
211
219
  Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
212
220
  Err(error) => return Err(io(error)),
213
221
  };
214
- for line in bytes.split(|byte| *byte == b'\n').filter(|line| !line.is_empty()) {
215
- let Ok(value) = crate::canon::parse_canonical(line, crate::limits::DOC_MAX_BYTES) else {
222
+ for line in bytes
223
+ .split(|byte| *byte == b'\n')
224
+ .filter(|line| !line.is_empty())
225
+ {
226
+ let Ok(value) = crate::canon::parse_canonical(line, crate::limits::DOC_MAX_BYTES)
227
+ else {
216
228
  continue;
217
229
  };
218
230
  for value in crate::canon::as_arr("PrivateDerivedParts", "objects", &value)? {
@@ -1168,11 +1168,19 @@ pub(crate) fn part_bytes(part: &Part) -> Vec<u8> {
1168
1168
  }
1169
1169
 
1170
1170
  impl AddedPart {
1171
- pub(crate) fn stored(component: String, key: String, role: String, bytes: &[u8]) -> Result<Self> {
1171
+ pub(crate) fn stored(
1172
+ component: String,
1173
+ key: String,
1174
+ role: String,
1175
+ bytes: &[u8],
1176
+ ) -> Result<Self> {
1172
1177
  name("added part component", &component)?;
1173
1178
  self::key("added part tensor", &key)?;
1174
1179
  name("added part role", &role)?;
1175
- let part = part_from_value(&crate::canon::parse_canonical(bytes, limits::DOC_MAX_BYTES)?)?;
1180
+ let part = part_from_value(&crate::canon::parse_canonical(
1181
+ bytes,
1182
+ limits::DOC_MAX_BYTES,
1183
+ )?)?;
1176
1184
  Ok(Self {
1177
1185
  component,
1178
1186
  key,
@@ -3005,7 +3013,11 @@ impl Additions {
3005
3013
  for tensor in &component.add {
3006
3014
  for part in &tensor.parts {
3007
3015
  parts.insert(
3008
- (component.target.clone(), tensor.key.clone(), part.role.clone()),
3016
+ (
3017
+ component.target.clone(),
3018
+ tensor.key.clone(),
3019
+ part.role.clone(),
3020
+ ),
3009
3021
  part.clone(),
3010
3022
  );
3011
3023
  }
@@ -3025,7 +3037,11 @@ impl Additions {
3025
3037
  fn get(&self, (component, tensor_key, role): (&str, &str, &str)) -> Result<&PartDeclaration> {
3026
3038
  let part = self
3027
3039
  .parts
3028
- .get(&(component.to_string(), tensor_key.to_string(), role.to_string()))
3040
+ .get(&(
3041
+ component.to_string(),
3042
+ tensor_key.to_string(),
3043
+ role.to_string(),
3044
+ ))
3029
3045
  .ok_or_else(|| Refusal {
3030
3046
  code: Code::MISSING_FIELD,
3031
3047
  detail: format!("{component}/{tensor_key}#{role} is not a declared addition"),
@@ -3093,7 +3109,15 @@ pub fn add_part<R: Read>(
3093
3109
  reader: &mut R,
3094
3110
  ) -> Result<crate::checkpoint::Written> {
3095
3111
  let additions = Additions::load(meta, transaction, writer_session)?;
3096
- add_indexed_part(store, meta, transaction, writer_session, &additions, value, reader)
3112
+ add_indexed_part(
3113
+ store,
3114
+ meta,
3115
+ transaction,
3116
+ writer_session,
3117
+ &additions,
3118
+ value,
3119
+ reader,
3120
+ )
3097
3121
  }
3098
3122
 
3099
3123
  /// Accept one part. The cost is this part's bytes plus one indexed SQL row: the
@@ -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
+ }
@@ -19,6 +19,7 @@ pub mod checkpoint;
19
19
  pub mod checkpoint_root;
20
20
  pub mod corpus;
21
21
  pub mod derived;
22
+ pub mod descriptors;
22
23
  pub mod dtype;
23
24
  pub mod durability;
24
25
  pub mod err;
@@ -172,9 +172,11 @@ impl Meta {
172
172
  .optional()
173
173
  .map_err(|error| sql("read accepted part", error))?
174
174
  .or_else(|| {
175
- legacy.iter().flatten().find(|old| old.same_value(added)).map(|old| {
176
- crate::derived::part_bytes(&old.part)
177
- })
175
+ legacy
176
+ .iter()
177
+ .flatten()
178
+ .find(|old| old.same_value(added))
179
+ .map(|old| crate::derived::part_bytes(&old.part))
178
180
  });
179
181
  match existing {
180
182
  Some(existing) if existing == bytes => {
@@ -176,6 +176,7 @@ impl Endpoints {
176
176
  allowed_hosts,
177
177
  allow_local: self.allow_local,
178
178
  max_redirects: 5,
179
+ ..Default::default()
179
180
  }
180
181
  }
181
182
 
@@ -415,9 +415,14 @@ impl Store {
415
415
  ///
416
416
  /// A symlink, non-directory, partial Store, or directory containing foreign state refuses.
417
417
  /// There is no old-layout lookup or fallback root.
418
+ /// Open the Store at `root`, creating it when absent or empty. Concurrent calls on one
419
+ /// new root serialize on the root directory: exactly one creates it, the rest open it.
418
420
  pub fn ensure(root: &Path) -> Result<Store> {
419
421
  match fs::symlink_metadata(root) {
420
- Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Store::init(root),
422
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
423
+ fs::create_dir_all(root)
424
+ .map_err(|error| io(format!("create Store root {}", root.display()), error))?;
425
+ }
421
426
  Err(error) => return Err(io(format!("stat Store root {}", root.display()), error)),
422
427
  Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => {
423
428
  return refuse(
@@ -427,6 +432,7 @@ impl Store {
427
432
  }
428
433
  Ok(_) => {}
429
434
  }
435
+ let _creation = creation_lock(root)?;
430
436
  let empty = fs::read_dir(root)
431
437
  .map_err(|error| io(format!("read Store root {}", root.display()), error))?
432
438
  .next()
@@ -434,7 +440,7 @@ impl Store {
434
440
  .map_err(|error| io(format!("read Store root {}", root.display()), error))?
435
441
  .is_none();
436
442
  if empty {
437
- Store::init(root)
443
+ Store::init_locked(root)
438
444
  } else {
439
445
  validate_store_root(root)?;
440
446
  let catalog_path = root.join("tensorfs.sqlite");
@@ -456,6 +462,15 @@ impl Store {
456
462
  }
457
463
 
458
464
  pub fn init(root: &Path) -> Result<Store> {
465
+ if matches!(fs::symlink_metadata(root), Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink())
466
+ {
467
+ let _creation = creation_lock(root)?;
468
+ return Store::init_locked(root);
469
+ }
470
+ Store::init_locked(root)
471
+ }
472
+
473
+ fn init_locked(root: &Path) -> Result<Store> {
459
474
  match fs::symlink_metadata(root) {
460
475
  Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {}
461
476
  Ok(_) => {
@@ -1347,7 +1362,16 @@ impl Store {
1347
1362
  .create_new(true)
1348
1363
  .mode(0o600)
1349
1364
  .open(&tmp)
1350
- .map_err(|e| io(format!("create temp {}", tmp.display()), e))?;
1365
+ .map_err(|e| {
1366
+ if crate::descriptors::exhausted(&e) {
1367
+ Refusal {
1368
+ code: Code::FD_HEADROOM,
1369
+ detail: format!("create temp {}: {e}", tmp.display()),
1370
+ }
1371
+ } else {
1372
+ io(format!("create temp {}", tmp.display()), e)
1373
+ }
1374
+ })?;
1351
1375
  match f.try_lock_exclusive() {
1352
1376
  Ok(()) => break (tmp, f),
1353
1377
  // Only a reaper can hold the lock on a name this process just minted: it
@@ -2103,6 +2127,20 @@ fn read_repo_cache_binding(root: &Path) -> Option<crate::repo_cache::RepoObjectC
2103
2127
  Some(crate::repo_cache::RepoObjectCache::new(cache))
2104
2128
  }
2105
2129
 
2130
+ /// Serializes Store creation on one root across threads and processes. A lock on the root
2131
+ /// directory itself leaves nothing behind in it.
2132
+ fn creation_lock(root: &Path) -> Result<File> {
2133
+ let directory = OpenOptions::new()
2134
+ .read(true)
2135
+ .custom_flags(O_NOFOLLOW)
2136
+ .open(root)
2137
+ .map_err(|error| io(format!("open Store root {}", root.display()), error))?;
2138
+ directory
2139
+ .lock_exclusive()
2140
+ .map_err(|error| io(format!("lock Store root {}", root.display()), error))?;
2141
+ Ok(directory)
2142
+ }
2143
+
2106
2144
  fn open_validated(root: &Path) -> Result<Store> {
2107
2145
  fs::create_dir_all(root.join("tmp")).map_err(|error| io("mkdir tmp", error))?;
2108
2146
  Ok(Store {
@@ -166,7 +166,8 @@ pub struct Client {
166
166
  type IdlePool = Arc<Mutex<Vec<Idle>>>;
167
167
 
168
168
  // This bounds retained descriptors, not in-flight transfers (the caller owns those).
169
- const MAX_IDLE: usize = 8;
169
+ // `descriptors::PER_STREAM` counts one idle socket per stream.
170
+ const MAX_IDLE: usize = 32;
170
171
 
171
172
  /// A connection belongs to its origin and the credentials sent on it, never to one
172
173
  /// request target: a signed CDN URL changes per resolve, and keying on it meant the
@@ -414,7 +415,15 @@ fn connect(
414
415
  let socket = match nonblocking_socket(family) {
415
416
  Ok(socket) => socket,
416
417
  Err(error) => {
417
- last = Some(error.into());
418
+ let error: std::io::Error = error.into();
419
+ if crate::descriptors::exhausted(&error) {
420
+ // Another address needs another descriptor it will not get either.
421
+ return refuse(
422
+ Code::FD_HEADROOM,
423
+ format!("{}: no descriptor for a socket ({error})", checked.host),
424
+ );
425
+ }
426
+ last = Some(error);
418
427
  continue;
419
428
  }
420
429
  };
@@ -653,6 +662,15 @@ pub struct BodyReader<'l> {
653
662
 
654
663
  impl Drop for BodyReader<'_> {
655
664
  fn drop(&mut self) {
665
+ self.recycle_if_consumed();
666
+ }
667
+ }
668
+
669
+ impl BodyReader<'_> {
670
+ /// Hand the connection back the moment its last body byte is read, not when the
671
+ /// reader is dropped: the consumer may still spend seconds hashing, fsyncing and
672
+ /// committing what arrived, and the next object's GET should find this socket idle.
673
+ fn recycle_if_consumed(&mut self) {
656
674
  // Only a fully consumed, unambiguous Content-Length response is reusable.
657
675
  // Dropped/failed bodies, chunked bodies and EOF-delimited replies close.
658
676
  if !matches!(self.framing, Framing::Length { remaining: 0 }) || self.raw_len() != 0 {
@@ -722,6 +740,7 @@ impl BodyReader<'_> {
722
740
  let silent = self.advanced.elapsed();
723
741
  if self.judge.silent_is_stall() && silent > self.ledger.patience() {
724
742
  self.stalled = true;
743
+ self.ledger.stalled_stream();
725
744
  let _ = self
726
745
  .wire
727
746
  .as_ref()
@@ -846,6 +865,7 @@ impl BodyReader<'_> {
846
865
  self.framing = Framing::Length {
847
866
  remaining: remaining - n as u64,
848
867
  };
868
+ self.recycle_if_consumed();
849
869
  Ok(n)
850
870
  }
851
871
  Framing::Chunked { in_chunk, done } => {
@@ -946,13 +966,11 @@ pub fn request<'l>(
946
966
  write_silence: Option<Duration>,
947
967
  fail: Code,
948
968
  ) -> Result<Response<'l>> {
949
- // Retained-source ranges already own their retry/resume budget. Other GETs and
950
- // metadata writes keep their one-connection behavior.
951
- let ranged = method == "GET"
969
+ // Every bodiless GET may reuse a connection to its origin: a pull's objects are many
970
+ // GETs to one host, and a fresh TCP+TLS connection per object spent a descriptor, a
971
+ // handshake and a slow start on each. Metadata writes keep one connection each.
972
+ let pooled = method == "GET"
952
973
  && body.is_none()
953
- && headers
954
- .iter()
955
- .any(|(name, _)| name.eq_ignore_ascii_case("range"))
956
974
  && !headers
957
975
  .iter()
958
976
  .any(|(name, _)| name.eq_ignore_ascii_case("connection"));
@@ -967,7 +985,7 @@ pub fn request<'l>(
967
985
  method,
968
986
  headers,
969
987
  body.map(Payload::Bytes),
970
- ranged.then_some(scoped.as_slice()),
988
+ pooled.then_some(scoped.as_slice()),
971
989
  deadline,
972
990
  ledger,
973
991
  judge,
@@ -197,6 +197,7 @@ impl Origin {
197
197
  allowed_hosts: vec!["localhost".into()],
198
198
  allow_local: true,
199
199
  max_redirects: 2,
200
+ ..Default::default()
200
201
  }
201
202
  }
202
203
  fn count(&self) -> usize {
@@ -364,20 +365,23 @@ fn only_complete_unambiguous_http11_length_bodies_enter_the_pool() {
364
365
  }
365
366
 
366
367
  #[test]
367
- fn idle_descriptor_retention_is_bounded_and_non_range_gets_do_not_pool() {
368
+ fn idle_descriptor_retention_is_bounded_and_plain_gets_pool_too() {
368
369
  let origin = Origin::new(false, Duration::ZERO, |_, _| Reply::body(b"bytes"));
369
370
  let client = origin.client();
370
- for index in 0..32 {
371
+ let scopes = MAX_IDLE + 8;
372
+ for index in 0..scopes {
371
373
  let mut headers = ranged();
372
374
  headers.push(("authorization".into(), format!("Bearer {index}")));
373
375
  ask(&client, &origin.checked("/object"), &headers).unwrap();
374
376
  }
375
377
  assert_eq!(client.idle.lock().unwrap().len(), MAX_IDLE);
376
- assert_eq!(origin.count(), 32);
378
+ assert_eq!(origin.count(), scopes);
379
+ // A whole-object GET is a pull's common case: the second rides the first's socket.
377
380
  for _ in 0..2 {
378
- ask(&client, &origin.checked("/metadata"), &[]).unwrap();
381
+ ask(&client, &origin.checked("/object"), &[]).unwrap();
379
382
  }
380
- assert_eq!(origin.count(), 34);
383
+ assert_eq!(origin.count(), scopes + 1);
384
+ assert_eq!(client.idle.lock().unwrap().len(), MAX_IDLE);
381
385
  }
382
386
 
383
387
  #[test]
@@ -31,7 +31,7 @@
31
31
  //! paid H100s sat at exactly zero for fifteen minutes with neither plane complaining.
32
32
 
33
33
  use crate::err::{refuse, Code, Result};
34
- use std::sync::atomic::{AtomicBool, Ordering};
34
+ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
35
35
  use std::sync::{Arc, Mutex};
36
36
  use std::time::{Duration, Instant};
37
37
 
@@ -151,6 +151,8 @@ impl PullCancellation {
151
151
  pub struct Ledger {
152
152
  caller_cancellation: Option<PullCancellation>,
153
153
  cancelled: Arc<AtomicBool>,
154
+ /// Streams this pull has judged stalled, a backpressure signal for its pace.
155
+ stalls: Arc<AtomicU64>,
154
156
  sample: Duration,
155
157
  worst_gap: Arc<Mutex<Duration>>,
156
158
  progress: Arc<Mutex<Progress>>,
@@ -170,6 +172,7 @@ impl Ledger {
170
172
  Ledger {
171
173
  caller_cancellation: None,
172
174
  cancelled: Arc::new(AtomicBool::new(false)),
175
+ stalls: Arc::new(AtomicU64::new(0)),
173
176
  sample: sample.max(Duration::from_millis(1)),
174
177
  worst_gap: Arc::new(Mutex::new(Duration::ZERO)),
175
178
  progress: Arc::new(Mutex::new(Progress {
@@ -243,6 +246,16 @@ impl Ledger {
243
246
  self.sample
244
247
  }
245
248
 
249
+ /// A stream of this pull was judged stalled.
250
+ pub(super) fn stalled_stream(&self) {
251
+ self.stalls.fetch_add(1, Ordering::Relaxed);
252
+ }
253
+
254
+ /// Streams this pull has judged stalled so far.
255
+ pub(super) fn stalls(&self) -> u64 {
256
+ self.stalls.load(Ordering::Relaxed)
257
+ }
258
+
246
259
  /// The smallest patience ever granted: `STILL_SAMPLES` intervals of the resolution.
247
260
  pub fn floor(&self) -> Duration {
248
261
  self.sample * STILL_SAMPLES
@@ -280,6 +293,7 @@ impl Ledger {
280
293
  Self {
281
294
  caller_cancellation: self.caller_cancellation.clone(),
282
295
  cancelled: Arc::clone(&self.cancelled),
296
+ stalls: Arc::clone(&self.stalls),
283
297
  sample: self.sample,
284
298
  worst_gap: Arc::clone(&self.worst_gap),
285
299
  progress: Arc::clone(&self.progress),