tensorfs 0.3.76__tar.gz → 0.3.78__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 (179) hide show
  1. {tensorfs-0.3.76 → tensorfs-0.3.78}/Cargo.lock +2 -2
  2. {tensorfs-0.3.76 → tensorfs-0.3.78}/Cargo.toml +1 -1
  3. {tensorfs-0.3.76 → tensorfs-0.3.78}/PKG-INFO +1 -1
  4. tensorfs-0.3.78/crates/tensorfs-core/src/bin/tfs/ensure.rs +160 -0
  5. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/fetch.rs +1 -1
  6. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/ingest.rs +1 -1
  7. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/main.rs +88 -2
  8. tensorfs-0.3.78/crates/tensorfs-core/src/ensure/admission.rs +333 -0
  9. tensorfs-0.3.78/crates/tensorfs-core/src/ensure/flight.rs +216 -0
  10. tensorfs-0.3.78/crates/tensorfs-core/src/ensure.rs +364 -0
  11. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/err.rs +3 -0
  12. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/gc.rs +18 -3
  13. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/convert.rs +53 -16
  14. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/narrow.rs +5 -0
  15. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/preflight.rs +62 -1
  16. tensorfs-0.3.78/crates/tensorfs-core/src/ingest/routes/anima.routes.json +1 -0
  17. tensorfs-0.3.78/crates/tensorfs-core/src/ingest/routes/anima.spec.json +52 -0
  18. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/routes.rs +39 -6
  19. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/source.rs +27 -12
  20. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/transaction.rs +1 -1
  21. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/lib.rs +5 -0
  22. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/providers.rs +42 -17
  23. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/source_artifact.rs +46 -0
  24. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/http.rs +72 -30
  25. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +1 -0
  26. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/mod.rs +2 -2
  27. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/pull.rs +75 -30
  28. tensorfs-0.3.78/crates/tensorfs-core/tests/civitai_anima.rs +458 -0
  29. tensorfs-0.3.78/crates/tensorfs-core/tests/ensure.rs +781 -0
  30. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/source_artifacts.rs +1 -0
  31. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/source_download.rs +2 -0
  32. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/source_prefixes.rs +1 -0
  33. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/source_preflight.rs +1 -0
  34. tensorfs-0.3.78/crates/tensorfs-py/src/ensure.rs +145 -0
  35. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-py/src/ingest.rs +1 -0
  36. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-py/src/lib.rs +4 -0
  37. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-py/src/store.rs +32 -47
  38. {tensorfs-0.3.76 → tensorfs-0.3.78}/pyproject.toml +1 -1
  39. {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/__init__.py +116 -1
  40. {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/_ext.pyi +25 -0
  41. {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/errors.py +2 -0
  42. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/Cargo.toml +0 -0
  43. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/build.rs +0 -0
  44. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  45. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  46. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  47. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  48. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/asset_update.rs +0 -0
  49. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/b64.rs +0 -0
  50. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  51. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  52. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  53. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  54. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  55. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  56. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  57. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/canon.rs +0 -0
  58. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/capability.rs +0 -0
  59. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/catalog/lock.rs +0 -0
  60. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/catalog.rs +0 -0
  61. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/cbor.rs +0 -0
  62. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  63. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  64. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/corpus.rs +0 -0
  65. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  66. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  67. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  68. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  69. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/group_commit_tests.rs +0 -0
  70. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/history_cost_tests.rs +0 -0
  71. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/part_rows_tests.rs +0 -0
  72. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  73. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  74. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  75. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  76. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived.rs +0 -0
  77. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/descriptors.rs +0 -0
  78. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/disk.rs +0 -0
  79. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/dtype.rs +0 -0
  80. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/durability.rs +0 -0
  81. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/fetch.rs +0 -0
  82. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/fill.rs +0 -0
  83. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/fit.rs +0 -0
  84. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/header.rs +0 -0
  85. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/home.rs +0 -0
  86. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ids.rs +0 -0
  87. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  88. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
  89. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  90. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  91. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  92. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  93. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  94. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/routes/sdxl.routes.json +0 -0
  95. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/routes/sdxl.spec.json +0 -0
  96. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  97. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/jcs.rs +0 -0
  98. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/limits.rs +0 -0
  99. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/machine.rs +0 -0
  100. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/manifest.rs +0 -0
  101. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/meta.rs +0 -0
  102. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/project.rs +0 -0
  103. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/read.rs +0 -0
  104. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/receipt.rs +0 -0
  105. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/reclaim.rs +0 -0
  106. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/registry.rs +0 -0
  107. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/relation.rs +0 -0
  108. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
  109. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  110. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/repository.rs +0 -0
  111. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/sha256.rs +0 -0
  112. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/spec.rs +0 -0
  113. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/staging.rs +0 -0
  114. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/stats.rs +0 -0
  115. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/storage.rs +0 -0
  116. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/store.rs +0 -0
  117. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/api.rs +0 -0
  118. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  119. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  120. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/memory.rs +0 -0
  121. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/pace.rs +0 -0
  122. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  123. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  124. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/push.rs +0 -0
  125. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  126. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
  127. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  128. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/unsynced.rs +0 -0
  129. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/vectors.rs +0 -0
  130. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  131. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
  132. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/as_is.rs +0 -0
  133. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  134. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +0 -0
  135. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
  136. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  137. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  138. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fit.rs +0 -0
  139. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  140. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  141. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  142. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  143. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  144. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  145. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  146. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  147. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  148. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  149. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/home.rs +0 -0
  150. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/hub_tls.rs +0 -0
  151. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  152. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  153. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/liveness.rs +0 -0
  154. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  155. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  156. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  157. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  158. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  159. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  160. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  161. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/routed_single_file.rs +0 -0
  162. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  163. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  164. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  165. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/staging.rs +0 -0
  166. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  167. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  168. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  169. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  170. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/version.rs +0 -0
  171. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/write_through.rs +0 -0
  172. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-py/Cargo.toml +0 -0
  173. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-py/src/docs.rs +0 -0
  174. {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-py/src/errors.rs +0 -0
  175. {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/_derived_channel.py +0 -0
  176. {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/_source_channel.py +0 -0
  177. {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/_tensors.py +0 -0
  178. {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/derived.py +0 -0
  179. {tensorfs-0.3.76 → tensorfs-0.3.78}/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.76"
442
+ version = "0.3.78"
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.76"
457
+ version = "0.3.78"
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.76"
6
+ version = "0.3.78"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.76
3
+ Version: 0.3.78
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -0,0 +1,160 @@
1
+ //! `tfs ensure <root> <ref> --hub <base>`: make one model resident (proto-063 M2a).
2
+ //!
3
+ //! STDOUT is JSON lines and nothing else: `ensure.progress` samples, then exactly one
4
+ //! `ensure.result` (exit 0) or `ensure.refused` (exit 1). Usage errors exit 2.
5
+
6
+ use std::io::Write;
7
+ use std::path::Path;
8
+ use std::process::ExitCode;
9
+ use std::sync::Mutex;
10
+
11
+ use tensorfs_core::canon::{self, Value};
12
+ use tensorfs_core::ensure::{self, Ensured, Event, Request};
13
+ use tensorfs_core::err::Refusal;
14
+ use tensorfs_core::store::Store;
15
+ use tensorfs_core::transport;
16
+
17
+ use crate::{flag, flag_all, flag_num, Flags};
18
+
19
+ fn read(path: &str) -> Result<Vec<u8>, Refusal> {
20
+ std::fs::read(path).map_err(|e| Refusal {
21
+ code: tensorfs_core::err::Code::IO_FAILED,
22
+ detail: format!("read {path}: {e}"),
23
+ })
24
+ }
25
+
26
+ fn quoted(text: &str) -> String {
27
+ String::from_utf8(canon::write(&Value::str(text))).unwrap()
28
+ }
29
+
30
+ fn say(out: &Mutex<std::io::Stdout>, line: String) {
31
+ let mut out = out.lock().unwrap();
32
+ // A closed reader is not this download's business; the bytes are still wanted.
33
+ let _ = writeln!(out, "{line}").and_then(|()| out.flush());
34
+ }
35
+
36
+ fn progress(event: &Event) -> String {
37
+ format!(
38
+ "{{\"event\":\"ensure.progress\",\"model\":{},\"step\":{},\"phase\":\"{}\",\"bytes_done\":{},\"bytes_total\":{},\"rate\":{:.1},\"attempt\":{}}}",
39
+ quoted(&event.model),
40
+ quoted(&event.step),
41
+ event.phase.as_str(),
42
+ event.bytes_done,
43
+ event.bytes_total,
44
+ event.rate,
45
+ event.attempt,
46
+ )
47
+ }
48
+
49
+ fn result(r: &Ensured) -> String {
50
+ format!(
51
+ "{{\"event\":\"ensure.result\",\"model\":{},\"release\":{},\"lane\":{},\"scope\":{},\"manifest\":\"{}\",\"manifest_length\":{},\"bytes_total\":{},\"bytes_held\":{},\"bytes_fetched\":{},\"bytes_cached\":{},\"cache_written_bytes\":{},\"collected_bytes\":{},\"attempts\":{},\"joined\":{},\"seconds\":{:.3}}}",
52
+ quoted(&r.model),
53
+ quoted(&r.release),
54
+ quoted(&r.lane),
55
+ quoted(&r.scope),
56
+ r.manifest.id(),
57
+ r.manifest.length,
58
+ r.bytes_total,
59
+ r.bytes_held,
60
+ r.bytes_fetched,
61
+ r.bytes_cached,
62
+ r.cache_written_bytes,
63
+ r.collected_bytes,
64
+ r.attempts,
65
+ r.joined,
66
+ r.seconds,
67
+ )
68
+ }
69
+
70
+ fn refused(refusal: &Refusal, model: &str, step: &str) -> String {
71
+ format!(
72
+ "{{\"event\":\"ensure.refused\",\"code\":\"{}\",\"resumable\":{},\"detail\":{},\"model\":{},\"step\":{}}}",
73
+ refusal.code.as_str(),
74
+ ensure::resumable(refusal.code),
75
+ quoted(&refusal.detail),
76
+ quoted(model),
77
+ quoted(step),
78
+ )
79
+ }
80
+
81
+ pub fn cmd_ensure(root: &Path, refspec: &str, flags: &Flags) -> ExitCode {
82
+ let Some(hub) = flag(flags, "hub") else {
83
+ eprintln!("--hub <base-url> is required");
84
+ return ExitCode::from(2);
85
+ };
86
+ let out = Mutex::new(std::io::stdout());
87
+ let model = refspec.split('@').next().unwrap_or_default();
88
+ let step = flag(flags, "step").unwrap_or("");
89
+ let answer = |outcome: Result<Ensured, Refusal>| match outcome {
90
+ Ok(ensured) => {
91
+ say(&out, result(&ensured));
92
+ ExitCode::SUCCESS
93
+ }
94
+ Err(refusal) => {
95
+ say(&out, refused(&refusal, model, step));
96
+ ExitCode::FAILURE
97
+ }
98
+ };
99
+ let setup = || -> Result<_, Refusal> {
100
+ if let Some(path) = flag(flags, "ca-file") {
101
+ transport::trust_roots(&read(path)?)?;
102
+ }
103
+ // The credential reaches the child as a file or TFS_CREDENTIAL, never argv.
104
+ let spec = match flag(flags, "credential-file") {
105
+ Some(path) => String::from_utf8_lossy(&read(path)?).into_owned(),
106
+ None => std::env::var("TFS_CREDENTIAL").unwrap_or_default(),
107
+ };
108
+ let credential =
109
+ transport::credential_from_spec(spec.trim(), vec![transport::base_host(hub)?])?;
110
+ Ok((Store::open(root)?, credential))
111
+ };
112
+ let (store, credential) = match setup() {
113
+ Ok(setup) => setup,
114
+ Err(refusal) => return answer(Err(refusal)),
115
+ };
116
+ let policy = crate::fetch::policy_of(flags);
117
+ let keep: Vec<String> = flag_all(flags, "keep")
118
+ .into_iter()
119
+ .map(str::to_string)
120
+ .collect();
121
+ let on_event = |event: &Event| say(&out, progress(event));
122
+ let mut request = Request::new(&store, hub, refspec, &credential, &policy);
123
+ request.lane = flag(flags, "lane").unwrap_or("");
124
+ request.step = step;
125
+ request.keep = &keep;
126
+ request.streams = flag_num(flags, "streams", transport::PULL_STREAMS);
127
+ request.streams_start = flag_num(flags, "streams-start", transport::STREAMS);
128
+ request.sample_seconds = flag_num(flags, "sample-seconds", transport::SAMPLE_SECONDS);
129
+ request.on_event = Some(&on_event);
130
+ answer(ensure::ensure(&request))
131
+ }
132
+
133
+ /// `tfs pressure <root> [--keep sha256:<m>]...`: one pressure pass (see `ensure::relieve`),
134
+ /// reported as one JSON line.
135
+ pub fn cmd_pressure(root: &Path, flags: &Flags) -> ExitCode {
136
+ let keep: Vec<String> = flag_all(flags, "keep")
137
+ .into_iter()
138
+ .map(str::to_string)
139
+ .collect();
140
+ let relief = match Store::open(root).and_then(|store| ensure::relieve(&store, &keep)) {
141
+ Ok(relief) => relief,
142
+ Err(refusal) => {
143
+ println!(
144
+ "{{\"event\":\"pressure.refused\",\"code\":\"{}\",\"detail\":{}}}",
145
+ refusal.code.as_str(),
146
+ quoted(&refusal.detail)
147
+ );
148
+ return ExitCode::FAILURE;
149
+ }
150
+ };
151
+ println!(
152
+ "{{\"event\":\"pressure\",\"pressure\":{},\"collected_bytes\":{},\"capacity_bytes\":{},\"available_bytes\":{},\"unable\":{}}}",
153
+ relief.pressure,
154
+ relief.collected_bytes,
155
+ relief.capacity_bytes,
156
+ relief.available_bytes,
157
+ relief.unable.as_deref().map_or("null".to_string(), quoted),
158
+ );
159
+ ExitCode::SUCCESS
160
+ }
@@ -168,7 +168,7 @@ use tensorfs_core::transport::{
168
168
  self, credential_from_spec, Anonymous, Deadline, PullRequest, SourcePolicy,
169
169
  };
170
170
 
171
- fn policy_of(flags: &Flags) -> SourcePolicy {
171
+ pub(crate) fn policy_of(flags: &Flags) -> SourcePolicy {
172
172
  SourcePolicy {
173
173
  allowed_hosts: crate::flag(flags, "allow-hosts")
174
174
  .map(|hosts| {
@@ -446,7 +446,7 @@ pub fn cmd_bank(dialect: &str, converter: &str, f: &Flags) -> ExitCode {
446
446
  for (component, path) in &srcs {
447
447
  let (full, raw) = ok!(source_core::read_carrier_at(path));
448
448
  let h = if conv.dialect == "safetensors.single_file" {
449
- ok!(convert::single_file_component(component, &full))
449
+ ok!(convert::single_file_component(conv, component, &full))
450
450
  } else {
451
451
  full
452
452
  };
@@ -31,7 +31,8 @@
31
31
  //! [--session <s>] [--tenant <t>] [--no-journal]
32
32
  //! tfs repo-cache chain <cache> <chain-id> <length> the links, root to head
33
33
  //! tfs put <root> <file> stream one object in (any shape)
34
- //! tfs get <root> <hex> verified read, whole or ranged
34
+ //! tfs get <root> <hex> verified read, whole or ranged; with --out - the bytes
35
+ //! go to stdout under a read lease (--range <off>:<len>|<off>:)
35
36
  //! tfs contains <root> <hex> the presence HINT, and what it is worth
36
37
  //! tfs verify <root> <hex> verification record or rehash; remove corrupt bytes
37
38
  //! tfs scrub <root> one rate knob over the whole store
@@ -48,6 +49,15 @@
48
49
  //! `tfs fetch` reads and backfills the Store's bound repo cache between the local Store
49
50
  //! and the origin. It is bound by `tfs store ensure --repo-cache` and never by a flag here.
50
51
  //!
52
+ //! tfs capabilities one feature name per line (ensure/1, get-range/1, ...)
53
+ //! tfs ensure <root> <ref> --hub <base> [--lane L] [--step S] [--keep sha256:<m>]...
54
+ //! make one model resident: closure, one flight per manifest per
55
+ //! Store, disk admission + GC policy, paced resumable fetch, retry
56
+ //! only after measured progress. STDOUT is JSON lines:
57
+ //! ensure.progress samples, then ensure.result (exit 0) or
58
+ //! ensure.refused {code, resumable, detail} (exit 1)
59
+ //! tfs pressure <root> [--keep sha256:<m>]... one pressure pass: at <= 1/10 free, run
60
+ //! the GC policy until > 1/5 is free; one JSON line
51
61
  //! tfs fetch <root> <ref> closure -> plan -> N-way walk (presign at use) -> proof
52
62
  //! tfs fetch bounded <root> <ref> --max-download-bytes N --max-download-objects N
53
63
  //! same pull, refused before body transfer if new objects exceed bounds
@@ -203,7 +213,9 @@ fn accept_printed_ids(args: Vec<String>) -> Vec<String> {
203
213
 
204
214
  fn main() -> ExitCode {
205
215
  let (args, flags) = split_flags(std::env::args().skip(1).collect());
206
- if let Some(path) = flag(&flags, "ca-file") {
216
+ // `ensure` names its CA itself so a bad file is a refusal on its own channel.
217
+ let ensure = args.first().is_some_and(|verb| verb == "ensure");
218
+ if let (Some(path), false) = (flag(&flags, "ca-file"), ensure) {
207
219
  let trusted = fs::read(path)
208
220
  .map_err(|e| format!("IO_FAILED: read {path}: {e}"))
209
221
  .and_then(|pem| {
@@ -218,6 +230,14 @@ fn main() -> ExitCode {
218
230
  let a: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
219
231
  match a.as_slice() {
220
232
  ["version"] => cmd_version(),
233
+ ["capabilities"] => {
234
+ tensorfs_core::CAPABILITIES
235
+ .iter()
236
+ .for_each(|c| println!("{c}"));
237
+ ExitCode::SUCCESS
238
+ }
239
+ ["ensure", r, refspec] => ensure::cmd_ensure(Path::new(r), refspec, &flags),
240
+ ["pressure", r] => ensure::cmd_pressure(Path::new(r), &flags),
221
241
  ["home"] => cmd_home(None),
222
242
  ["home", r] => cmd_home(Some(Path::new(r))),
223
243
  ["store", "init"] => cmd_store_default("init", &flags),
@@ -815,6 +835,7 @@ macro_rules! ok {
815
835
 
816
836
  mod ckpt;
817
837
  mod conform;
838
+ mod ensure;
818
839
  mod fetch;
819
840
  mod ingest;
820
841
  mod rd;
@@ -936,6 +957,67 @@ fn fault_of(f: &Flags) -> Fault {
936
957
  }
937
958
  }
938
959
 
960
+ /// `<off>:<len>` or `<off>:` (to the end) inside an object of `length` bytes.
961
+ fn byte_range(range: &str, length: u64) -> TfsResult<(u64, u64)> {
962
+ let bad = || Refusal {
963
+ code: tensorfs_core::err::Code::RANGE_BOUNDS,
964
+ detail: format!("--range {range:?} is not <off>:<len> or <off>: inside {length} B"),
965
+ };
966
+ let (off, len) = range.split_once(':').ok_or_else(bad)?;
967
+ let off: u64 = off.parse().map_err(|_| bad())?;
968
+ let len: u64 = match len {
969
+ "" => length.checked_sub(off).ok_or_else(bad)?,
970
+ len => len.parse().map_err(|_| bad())?,
971
+ };
972
+ match off.checked_add(len) {
973
+ Some(end) if end <= length => Ok((off, len)),
974
+ _ => Err(bad()),
975
+ }
976
+ }
977
+
978
+ /// Stream a range of one verified object to stdout under a read lease, so a GC pass cannot
979
+ /// take the object mid-read.
980
+ fn get_leased(store: &Store, hex: &str, range: Option<&str>) -> TfsResult<()> {
981
+ use std::io::Write;
982
+ use tensorfs_core::read::{self, ObjectRange};
983
+ let length = store.open_verified(hex)?.len();
984
+ let (mut off, mut left) = match range {
985
+ Some(range) => byte_range(range, length)?,
986
+ None => (0, length),
987
+ };
988
+ let meta = tensorfs_core::meta::Meta::open(store)?;
989
+ let obj = ObjectRef {
990
+ sha256: hex.to_string(),
991
+ length,
992
+ };
993
+ let (lease, _) = read::acquire(store, &meta, "", vec![obj.clone()])?;
994
+ let mut out = std::io::stdout().lock();
995
+ let mut buf = vec![0u8; 1 << 20];
996
+ let streamed = (|| {
997
+ while left > 0 {
998
+ let n = left.min(buf.len() as u64);
999
+ let chunk = &mut buf[..n as usize];
1000
+ let range = ObjectRange {
1001
+ obj: obj.clone(),
1002
+ off,
1003
+ len: n,
1004
+ };
1005
+ read::read_into(&lease, &range, chunk)?;
1006
+ out.write_all(chunk).map_err(|e| Refusal {
1007
+ code: tensorfs_core::err::Code::IO_FAILED,
1008
+ detail: format!("write stdout: {e}"),
1009
+ })?;
1010
+ (off, left) = (off + n, left - n);
1011
+ }
1012
+ out.flush().map_err(|e| Refusal {
1013
+ code: tensorfs_core::err::Code::IO_FAILED,
1014
+ detail: format!("write stdout: {e}"),
1015
+ })
1016
+ })();
1017
+ lease.release(&meta)?;
1018
+ streamed
1019
+ }
1020
+
939
1021
  fn cmd_put(root: &Path, src: &Path, f: &Flags) -> ExitCode {
940
1022
  let s = ok!(Store::open(root));
941
1023
  let expect = match (flag(f, "expect"), flag(f, "expect-length")) {
@@ -1016,6 +1098,10 @@ fn cmd_verify_object(root: &Path, hex: &str, f: &Flags) -> ExitCode {
1016
1098
 
1017
1099
  fn cmd_get(root: &Path, hex: &str, f: &Flags) -> ExitCode {
1018
1100
  let s = ok!(Store::open(root));
1101
+ if flag(f, "out") == Some("-") {
1102
+ ok!(get_leased(&s, hex, flag(f, "range")));
1103
+ return ExitCode::SUCCESS;
1104
+ }
1019
1105
  let t = Instant::now();
1020
1106
  if let Some(r) = flag(f, "range") {
1021
1107
  let (o, l) = r.split_once(':').unwrap_or((r, "1"));
@@ -0,0 +1,333 @@
1
+ //! Disk admission for one flight, and the GC policy that makes room. Disk only: nothing here
2
+ //! looks at RAM (owner rule: no RAM floor; the Runtime pages to disk).
3
+ //!
4
+ //! The numbers are the Host's storage-pressure policy (pod-supervisor `pressure.go`,
5
+ //! `fetch.go`): a reserve of max(1 GiB, 1/50 of the filesystem) and 32 inodes is never
6
+ //! admitted; a download needs its wanted bytes and 16 + 3 inodes per wanted object; live
7
+ //! flights' unwritten remainders are already spoken for. When a flight does not fit, the GC
8
+ //! policy runs in order: unreferenced bytes, delivered products (oldest delivery first),
9
+ //! then cached model lanes (the dedup-aware reclaim planner); the answer after that is the
10
+ //! answer.
11
+
12
+ use std::ops::Add;
13
+ use std::path::Path;
14
+
15
+ use crate::err::{refuse, Code, Refusal, Result};
16
+ use crate::gc;
17
+ use crate::source_artifact;
18
+ use crate::store::Store;
19
+
20
+ use super::flight::{self, Flight, Record};
21
+
22
+ pub(super) const RESERVE_BYTES: u64 = 1 << 30;
23
+ pub(super) const RESERVE_DIVISOR: u64 = 50;
24
+ pub(super) const RESERVE_INODES: u64 = 32;
25
+ pub(super) const METADATA_INODES: u64 = 16;
26
+ pub(super) const OBJECT_INODES: u64 = 3;
27
+ /// Pressure begins at 1/10 of the filesystem free and ends above 1/5.
28
+ pub(super) const ENTER_DIVISOR: u64 = 10;
29
+ pub(super) const LEAVE_DIVISOR: u64 = 5;
30
+
31
+ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
32
+ pub(super) struct Space {
33
+ pub bytes: u64,
34
+ pub inodes: u64,
35
+ }
36
+
37
+ impl Add for Space {
38
+ type Output = Space;
39
+ fn add(self, other: Space) -> Space {
40
+ Space {
41
+ bytes: self.bytes.saturating_add(other.bytes),
42
+ inodes: self.inodes.saturating_add(other.inodes),
43
+ }
44
+ }
45
+ }
46
+
47
+ impl Space {
48
+ /// What `objects` new objects of `bytes` in total cost the disk.
49
+ pub fn of(bytes: u64, objects: u64) -> Space {
50
+ Space {
51
+ bytes,
52
+ inodes: if objects == 0 {
53
+ 0
54
+ } else {
55
+ METADATA_INODES + OBJECT_INODES * objects
56
+ },
57
+ }
58
+ }
59
+
60
+ fn within(self, free: Space) -> bool {
61
+ self.bytes <= free.bytes && self.inodes <= free.inodes
62
+ }
63
+ }
64
+
65
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
66
+ pub(super) struct Disk {
67
+ pub capacity: u64,
68
+ pub available: u64,
69
+ /// Zero when the filesystem has no inode limit.
70
+ pub inodes: u64,
71
+ pub available_inodes: u64,
72
+ }
73
+
74
+ pub(super) fn measure(root: &Path) -> Result<Disk> {
75
+ let stat = rustix::fs::statvfs(root).map_err(|e| Refusal {
76
+ code: Code::CAPACITY_EXHAUSTED,
77
+ detail: format!(
78
+ "cannot measure the filesystem under {}: {e}; admission fails closed",
79
+ root.display()
80
+ ),
81
+ })?;
82
+ let unit = stat.f_frsize.max(1);
83
+ Ok(Disk {
84
+ capacity: stat.f_blocks.saturating_mul(unit),
85
+ available: stat.f_bavail.saturating_mul(unit),
86
+ inodes: stat.f_files,
87
+ available_inodes: stat.f_favail,
88
+ })
89
+ }
90
+
91
+ /// What `disk` admits once its reserve and `others` are set aside.
92
+ pub(super) fn free(disk: &Disk, others: Space) -> Space {
93
+ let reserve = RESERVE_BYTES.max(disk.capacity / RESERVE_DIVISOR);
94
+ Space {
95
+ bytes: disk
96
+ .available
97
+ .saturating_sub(reserve)
98
+ .saturating_sub(others.bytes),
99
+ inodes: if disk.inodes == 0 {
100
+ u64::MAX
101
+ } else {
102
+ disk.available_inodes
103
+ .saturating_sub(RESERVE_INODES)
104
+ .saturating_sub(others.inodes)
105
+ },
106
+ }
107
+ }
108
+
109
+ /// The GC policy's outcome: what it freed, why a tier could not run, and the disk after.
110
+ struct Collected {
111
+ bytes: u64,
112
+ unable: Option<Refusal>,
113
+ disk: Disk,
114
+ }
115
+
116
+ /// Run the GC policy's tiers until `short(disk)` says nothing is short. `short` answers
117
+ /// the bytes still missing, or `None` once the disk is enough.
118
+ fn collect_until(
119
+ store: &Store,
120
+ protected: &[String],
121
+ short: &dyn Fn(&Disk) -> Option<u64>,
122
+ collecting: &dyn Fn(),
123
+ ) -> Result<Collected> {
124
+ let root = store.root();
125
+ let mut collected = Collected {
126
+ bytes: 0,
127
+ unable: None,
128
+ disk: measure(root)?,
129
+ };
130
+ for tier in 0..3 {
131
+ let Some(short) = short(&collected.disk) else {
132
+ break;
133
+ };
134
+ if tier == 0 {
135
+ collecting();
136
+ }
137
+ let freed = match tier {
138
+ 0 => gc::collect(root, false).map(|report| report.reclaimed_bytes),
139
+ 1 => release_delivered(store, short.max(1)),
140
+ _ => gc::collect_cached_for(root, protected, short.max(1))
141
+ .map(|report| report.reclaimed_bytes),
142
+ };
143
+ match freed {
144
+ Ok(bytes) => collected.bytes += bytes,
145
+ Err(refusal) => collected.unable = Some(refusal),
146
+ }
147
+ collected.disk = measure(root)?;
148
+ }
149
+ Ok(collected)
150
+ }
151
+
152
+ /// Live flights under the admission gate: what they still owe the disk, and their
153
+ /// manifests, which no tier may evict.
154
+ fn flights(store: &Store, mine: &str, keep: &[String]) -> Result<(Space, Vec<String>)> {
155
+ let live = flight::live(store, mine)?;
156
+ let owed = live
157
+ .iter()
158
+ .filter_map(|(_, record)| record.as_ref().map(Record::remaining))
159
+ .fold(Space::default(), Add::add);
160
+ let mut protected = keep.to_vec();
161
+ protected.extend(live.into_iter().map(|(manifest, _)| manifest));
162
+ protected.extend((!mine.is_empty()).then(|| mine.to_string()));
163
+ Ok((owed, protected))
164
+ }
165
+
166
+ /// Admit `record`'s flight, collecting under the GC policy when it does not fit. Returns
167
+ /// the bytes collected. `keep` names manifests no tier may evict.
168
+ pub(super) fn admit(
169
+ store: &Store,
170
+ flight: &Flight,
171
+ record: &Record,
172
+ keep: &[String],
173
+ collecting: &dyn Fn(),
174
+ ) -> Result<u64> {
175
+ let need = Space::of(record.wanted_bytes, record.wanted_objects);
176
+ let _gate = flight::gate(store)?;
177
+ let (others, protected) = flights(store, &flight.manifest, keep)?;
178
+ let short = |disk: &Disk| {
179
+ let free = free(disk, others);
180
+ (!need.within(free)).then(|| need.bytes.saturating_sub(free.bytes))
181
+ };
182
+ let collected = collect_until(store, &protected, &short, collecting)?;
183
+ if short(&collected.disk).is_none() {
184
+ flight.record(record)?;
185
+ return Ok(collected.bytes);
186
+ }
187
+ let free = free(&collected.disk, others);
188
+ let inodes = match collected.disk.inodes {
189
+ 0 => String::new(),
190
+ _ => format!(" ({} inodes, {} free)", need.inodes, free.inodes),
191
+ };
192
+ let why = collected
193
+ .unable
194
+ .map_or(String::new(), |r| format!("; GC could not run: {r}"));
195
+ refuse(
196
+ Code::CAPACITY_EXHAUSTED,
197
+ format!(
198
+ "this download needs {} B{inodes}; the disk under {} admits {} B after its \
199
+ reserve and {} B still owed to other downloads; GC freed {} B{why}",
200
+ need.bytes,
201
+ store.root().display(),
202
+ free.bytes,
203
+ others.bytes,
204
+ collected.bytes,
205
+ ),
206
+ )
207
+ }
208
+
209
+ /// What one pressure pass found and did.
210
+ #[derive(Debug, Clone, PartialEq, Eq)]
211
+ pub struct Relief {
212
+ /// The disk was under pressure when the pass began.
213
+ pub pressure: bool,
214
+ pub collected_bytes: u64,
215
+ pub capacity_bytes: u64,
216
+ pub available_bytes: u64,
217
+ /// Why a tier could not run, when one could not.
218
+ pub unable: Option<String>,
219
+ }
220
+
221
+ /// The Host's pressure hysteresis in one pass: a disk with at most 1/10 free (or under its
222
+ /// reserve) is collected under the GC policy until more than 1/5 is free. Live flights'
223
+ /// manifests and `keep` are never evicted. A disk not under pressure is left alone.
224
+ pub fn relieve(store: &Store, keep: &[String]) -> Result<Relief> {
225
+ let _gate = flight::gate(store)?;
226
+ let (_, protected) = flights(store, "", keep)?;
227
+ let disk = measure(store.root())?;
228
+ let reserve = RESERVE_BYTES.max(disk.capacity / RESERVE_DIVISOR);
229
+ let low = |disk: &Disk, divisor: u64| {
230
+ disk.available <= reserve.max(disk.capacity / divisor)
231
+ || (disk.inodes > 0
232
+ && disk.available_inodes <= RESERVE_INODES.max(disk.inodes / divisor))
233
+ };
234
+ let pressure = low(&disk, ENTER_DIVISOR);
235
+ let short = |disk: &Disk| {
236
+ low(disk, LEAVE_DIVISOR).then(|| {
237
+ (reserve.max(disk.capacity / LEAVE_DIVISOR) + 1).saturating_sub(disk.available)
238
+ })
239
+ };
240
+ let collected = if pressure {
241
+ collect_until(store, &protected, &short, &|| {})?
242
+ } else {
243
+ Collected {
244
+ bytes: 0,
245
+ unable: None,
246
+ disk,
247
+ }
248
+ };
249
+ Ok(Relief {
250
+ pressure,
251
+ collected_bytes: collected.bytes,
252
+ capacity_bytes: collected.disk.capacity,
253
+ available_bytes: collected.disk.available,
254
+ unable: collected.unable.map(|r| r.to_string()),
255
+ })
256
+ }
257
+
258
+ /// Release delivered products oldest delivery first, in doubling batches, collecting after
259
+ /// each batch, until `short` bytes are freed or none are left.
260
+ fn release_delivered(store: &Store, short: u64) -> Result<u64> {
261
+ let owners = source_artifact::delivered(store)?;
262
+ let (mut freed, mut at, mut batch) = (0u64, 0usize, 1usize);
263
+ while at < owners.len() && freed < short {
264
+ for owner in &owners[at..(at + batch).min(owners.len())] {
265
+ match source_artifact::release(store, owner) {
266
+ Err(refusal) if refusal.code == Code::STORE_BUSY => {}
267
+ other => other?,
268
+ }
269
+ }
270
+ at += batch;
271
+ batch *= 2;
272
+ freed += gc::collect(store.root(), false)?.reclaimed_bytes;
273
+ }
274
+ Ok(freed)
275
+ }
276
+
277
+ #[cfg(test)]
278
+ mod tests {
279
+ use super::*;
280
+
281
+ const GIB: u64 = 1 << 30;
282
+
283
+ fn disk(capacity: u64, available: u64) -> Disk {
284
+ Disk {
285
+ capacity,
286
+ available,
287
+ inodes: 1000,
288
+ available_inodes: 500,
289
+ }
290
+ }
291
+
292
+ #[test]
293
+ fn the_reserve_is_a_gib_or_a_fiftieth_and_others_are_spoken_for() {
294
+ // 10 GiB disk: the reserve is the 1 GiB floor.
295
+ assert_eq!(
296
+ free(&disk(10 * GIB, 5 * GIB), Space::default()).bytes,
297
+ 4 * GIB
298
+ );
299
+ // 1000 GiB disk: the reserve is 20 GiB.
300
+ assert_eq!(
301
+ free(&disk(1000 * GIB, 100 * GIB), Space::default()).bytes,
302
+ 80 * GIB
303
+ );
304
+ let others = Space::of(GIB, 10);
305
+ let left = free(&disk(10 * GIB, 5 * GIB), others);
306
+ assert_eq!(
307
+ left,
308
+ Space {
309
+ bytes: 3 * GIB,
310
+ inodes: 500 - 32 - 46
311
+ }
312
+ );
313
+ // Nothing left is zero, never an underflow.
314
+ assert_eq!(free(&disk(10 * GIB, GIB / 2), others).bytes, 0);
315
+ }
316
+
317
+ #[test]
318
+ fn a_warm_flight_needs_nothing_and_inodes_can_refuse_alone() {
319
+ assert!(Space::of(0, 0).within(Space::default()));
320
+ let tight = Disk {
321
+ inodes: 1000,
322
+ available_inodes: 40,
323
+ ..disk(10 * GIB, 5 * GIB)
324
+ };
325
+ assert!(!Space::of(1, 1).within(free(&tight, Space::default())));
326
+ let unlimited = Disk {
327
+ inodes: 0,
328
+ available_inodes: 0,
329
+ ..tight
330
+ };
331
+ assert!(Space::of(1, 1000).within(free(&unlimited, Space::default())));
332
+ }
333
+ }