tensorfs 0.3.77__tar.gz → 0.3.79__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 (181) hide show
  1. {tensorfs-0.3.77 → tensorfs-0.3.79}/Cargo.lock +2 -2
  2. {tensorfs-0.3.77 → tensorfs-0.3.79}/Cargo.toml +1 -1
  3. {tensorfs-0.3.77 → tensorfs-0.3.79}/PKG-INFO +1 -1
  4. tensorfs-0.3.79/crates/tensorfs-core/src/bin/tfs/ensure.rs +160 -0
  5. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/fetch.rs +1 -1
  6. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/ingest.rs +1 -0
  7. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/main.rs +88 -2
  8. tensorfs-0.3.79/crates/tensorfs-core/src/catalog/lock.rs +206 -0
  9. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/catalog.rs +28 -1
  10. tensorfs-0.3.79/crates/tensorfs-core/src/ensure/admission.rs +333 -0
  11. tensorfs-0.3.79/crates/tensorfs-core/src/ensure/flight.rs +216 -0
  12. tensorfs-0.3.79/crates/tensorfs-core/src/ensure.rs +364 -0
  13. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/err.rs +3 -0
  14. tensorfs-0.3.77/crates/tensorfs-core/src/repo_cache/identity.rs → tensorfs-0.3.79/crates/tensorfs-core/src/filesystem_identity.rs +30 -24
  15. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/fill.rs +3 -3
  16. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/gc.rs +18 -3
  17. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/convert.rs +10 -12
  18. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/fingerprint.rs +22 -3
  19. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/h3_lora.rs +12 -5
  20. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/preflight.rs +0 -3
  21. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/source.rs +19 -0
  22. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/lib.rs +6 -0
  23. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/meta.rs +67 -14
  24. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/repo_cache.rs +5 -6
  25. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/source_artifact.rs +46 -0
  26. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/http.rs +72 -30
  27. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +4 -1
  28. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/mod.rs +2 -2
  29. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/pull.rs +75 -30
  30. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/tests.rs +31 -20
  31. tensorfs-0.3.79/crates/tensorfs-core/tests/catalog_owner_probe.c +29 -0
  32. tensorfs-0.3.79/crates/tensorfs-core/tests/ensure.rs +781 -0
  33. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/h3_lora.rs +153 -7
  34. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/source_prefixes.rs +3 -1
  35. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/store_boundaries.rs +42 -16
  36. tensorfs-0.3.79/crates/tensorfs-py/src/ensure.rs +145 -0
  37. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-py/src/lib.rs +4 -0
  38. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-py/src/store.rs +8 -1
  39. {tensorfs-0.3.77 → tensorfs-0.3.79}/pyproject.toml +1 -1
  40. {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/__init__.py +116 -1
  41. {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/_ext.pyi +25 -0
  42. {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/errors.py +2 -0
  43. tensorfs-0.3.77/crates/tensorfs-core/src/catalog/lock.rs +0 -121
  44. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/Cargo.toml +0 -0
  45. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/build.rs +0 -0
  46. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  47. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  48. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  49. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  50. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/asset_update.rs +0 -0
  51. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/b64.rs +0 -0
  52. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  53. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  54. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  55. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  56. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  57. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  58. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  59. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/canon.rs +0 -0
  60. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/capability.rs +0 -0
  61. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/cbor.rs +0 -0
  62. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  63. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  64. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/corpus.rs +0 -0
  65. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  66. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  67. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  68. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  69. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/group_commit_tests.rs +0 -0
  70. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/history_cost_tests.rs +0 -0
  71. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/part_rows_tests.rs +0 -0
  72. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  73. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  74. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  75. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  76. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived.rs +0 -0
  77. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/descriptors.rs +0 -0
  78. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/disk.rs +0 -0
  79. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/dtype.rs +0 -0
  80. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/durability.rs +0 -0
  81. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/fetch.rs +0 -0
  82. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/fit.rs +0 -0
  83. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/header.rs +0 -0
  84. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/home.rs +0 -0
  85. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ids.rs +0 -0
  86. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  87. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
  88. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  89. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  90. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  91. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/narrow.rs +0 -0
  92. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/routes/anima.routes.json +0 -0
  93. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/routes/anima.spec.json +0 -0
  94. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/routes/sdxl.routes.json +0 -0
  95. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/routes/sdxl.spec.json +0 -0
  96. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/routes.rs +0 -0
  97. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  98. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  99. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/jcs.rs +0 -0
  100. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/limits.rs +0 -0
  101. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/machine.rs +0 -0
  102. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/manifest.rs +0 -0
  103. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/project.rs +0 -0
  104. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/providers.rs +0 -0
  105. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/read.rs +0 -0
  106. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/receipt.rs +0 -0
  107. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/reclaim.rs +0 -0
  108. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/registry.rs +0 -0
  109. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/relation.rs +0 -0
  110. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/repository.rs +0 -0
  111. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/sha256.rs +0 -0
  112. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/spec.rs +0 -0
  113. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/staging.rs +0 -0
  114. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/stats.rs +0 -0
  115. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/storage.rs +0 -0
  116. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/store.rs +0 -0
  117. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/api.rs +0 -0
  118. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  119. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  120. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/memory.rs +0 -0
  121. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/pace.rs +0 -0
  122. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  123. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  124. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/push.rs +0 -0
  125. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  126. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
  127. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/unsynced.rs +0 -0
  128. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/vectors.rs +0 -0
  129. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  130. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
  131. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/as_is.rs +0 -0
  132. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  133. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/civitai_anima.rs +0 -0
  134. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +0 -0
  135. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
  136. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  137. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  138. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fit.rs +0 -0
  139. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  140. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  141. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  142. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  143. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  144. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  145. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  146. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  147. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  148. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/home.rs +0 -0
  149. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/hub_tls.rs +0 -0
  150. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  151. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  152. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/liveness.rs +0 -0
  153. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  154. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  155. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  156. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  157. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  158. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  159. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  160. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/routed_single_file.rs +0 -0
  161. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  162. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  163. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  164. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/source_download.rs +0 -0
  165. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  166. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  167. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/staging.rs +0 -0
  168. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  169. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  170. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  171. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/version.rs +0 -0
  172. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/write_through.rs +0 -0
  173. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-py/Cargo.toml +0 -0
  174. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-py/src/docs.rs +0 -0
  175. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-py/src/errors.rs +0 -0
  176. {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-py/src/ingest.rs +0 -0
  177. {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/_derived_channel.py +0 -0
  178. {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/_source_channel.py +0 -0
  179. {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/_tensors.py +0 -0
  180. {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/derived.py +0 -0
  181. {tensorfs-0.3.77 → tensorfs-0.3.79}/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.77"
442
+ version = "0.3.79"
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.77"
457
+ version = "0.3.79"
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.77"
6
+ version = "0.3.79"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.77
3
+ Version: 0.3.79
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| {
@@ -574,6 +574,7 @@ pub fn cmd_bank(dialect: &str, converter: &str, f: &Flags) -> ExitCode {
574
574
  return ExitCode::FAILURE;
575
575
  }
576
576
  let profile = SourceProfile {
577
+ auto_select: true,
577
578
  name: name.to_string(),
578
579
  components,
579
580
  };
@@ -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,206 @@
1
+ //! Waiting for another process's catalog lock by whether its holder can still release it.
2
+ //!
3
+ //! SQLite's locks are POSIX locks: they die with the process that holds them. A holder
4
+ //! that is alive is committing or about to, so a writer waits for it for as long as it
5
+ //! takes; a flat deadline refused a busy, progressing writer as if it were dead. The only
6
+ //! holder that cannot release is one that is stopped (a debugger or SIGSTOP), and only
7
+ //! then does a statement give up. Dead processes have already released their locks.
8
+ //!
9
+ //! Holders come from `/proc/locks`, matched by the catalog's inode (and its WAL `-shm`).
10
+ //! The catalog file itself is never opened here: closing a second descriptor on it would
11
+ //! drop every POSIX lock this process holds on it.
12
+
13
+ use std::cell::RefCell;
14
+ use std::path::{Path, PathBuf};
15
+ use std::time::Duration;
16
+
17
+ use rusqlite::Connection;
18
+
19
+ use crate::err::Result;
20
+
21
+ thread_local! {
22
+ /// The catalog this thread's statements wait on. A connection is opened for each
23
+ /// operation just before it is used, on the thread that uses it, so the one opened last
24
+ /// here is the one a busy statement belongs to.
25
+ static WAITING_ON: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
26
+ }
27
+
28
+ /// Install the waiting handler on one connection.
29
+ pub(crate) fn wait_for_live_holders(connection: &Connection, path: &Path) -> Result<()> {
30
+ WAITING_ON.with(|waiting| *waiting.borrow_mut() = Some(path.to_path_buf()));
31
+ connection
32
+ .busy_handler(Some(busy))
33
+ .map_err(|error| super::sql("install catalog lock wait", error))
34
+ }
35
+
36
+ fn busy(attempt: i32) -> bool {
37
+ let path = WAITING_ON.with(|waiting| waiting.borrow().clone());
38
+ if path.is_some_and(|path| holders_cannot_release(&path)) {
39
+ return false;
40
+ }
41
+ std::thread::sleep(Duration::from_millis(attempt.clamp(1, 64) as u64));
42
+ true
43
+ }
44
+
45
+ /// Every currently held write lock belongs to a stopped process. Without a current
46
+ /// holder to name (the lock just went, or no `/proc`), keep asking.
47
+ fn holders_cannot_release(path: &Path) -> bool {
48
+ let files: Vec<(u64, u64)> = [path.to_path_buf(), shm(path)]
49
+ .iter()
50
+ .filter_map(|file| std::fs::metadata(file).ok())
51
+ .map(|metadata| {
52
+ use std::os::unix::fs::MetadataExt;
53
+ (metadata.dev(), metadata.ino())
54
+ })
55
+ .collect();
56
+ let Ok(locks) = std::fs::read_to_string("/proc/locks") else {
57
+ return false;
58
+ };
59
+ snapshots_cannot_release(&locks, &files, std::process::id(), stopped, || {
60
+ std::fs::read_to_string("/proc/locks").ok()
61
+ })
62
+ }
63
+
64
+ fn snapshots_cannot_release(
65
+ observed: &str,
66
+ files: &[(u64, u64)],
67
+ own: u32,
68
+ is_stopped: impl Fn(u32) -> bool,
69
+ current: impl FnOnce() -> Option<String>,
70
+ ) -> bool {
71
+ let holders = write_holders(observed, files, own);
72
+ if holders.is_empty() || !holders.iter().all(|pid| is_stopped(*pid)) {
73
+ return false;
74
+ }
75
+ // A holder may unlock or exit between the lock snapshot and its process-state
76
+ // read. Refuse only if those stopped processes still hold the actual locks.
77
+ let Some(current) = current() else {
78
+ return false;
79
+ };
80
+ let current = write_holders(&current, files, own);
81
+ !current.is_empty()
82
+ && current
83
+ .iter()
84
+ .all(|pid| holders.contains(pid) && is_stopped(*pid))
85
+ }
86
+
87
+ fn shm(path: &Path) -> PathBuf {
88
+ let mut name = path.as_os_str().to_owned();
89
+ name.push("-shm");
90
+ PathBuf::from(name)
91
+ }
92
+
93
+ /// Other processes' POSIX write locks on these inodes, from `/proc/locks` lines such as
94
+ /// `3: POSIX ADVISORY WRITE 167671 103:02:204736108 1073741824 1073742335`.
95
+ fn write_holders(locks: &str, files: &[(u64, u64)], own: u32) -> Vec<u32> {
96
+ locks
97
+ .lines()
98
+ .filter_map(|line| {
99
+ let fields: Vec<&str> = line.split_whitespace().collect();
100
+ let at = fields.iter().position(|field| *field == "POSIX")?;
101
+ if fields[..at].contains(&"->") || fields.get(at + 2) != Some(&"WRITE") {
102
+ return None;
103
+ }
104
+ let pid: u32 = fields.get(at + 3)?.parse().ok()?;
105
+ let mut file = fields.get(at + 4)?.split(':');
106
+ let major = u32::from_str_radix(file.next()?, 16).ok()?;
107
+ let minor = u32::from_str_radix(file.next()?, 16).ok()?;
108
+ let inode: u64 = file.next()?.parse().ok()?;
109
+ let device = rustix::fs::makedev(major, minor) as u64;
110
+ (file.next().is_none() && pid != own && files.contains(&(device, inode))).then_some(pid)
111
+ })
112
+ .collect()
113
+ }
114
+
115
+ /// Only stopped and traced-stopped processes keep locks they cannot release.
116
+ fn stopped(pid: u32) -> bool {
117
+ std::fs::read_to_string(format!("/proc/{pid}/stat"))
118
+ .ok()
119
+ .is_some_and(|stat| process_is_stopped(&stat))
120
+ }
121
+
122
+ fn process_is_stopped(stat: &str) -> bool {
123
+ stat.rsplit_once(')')
124
+ .and_then(|(_, fields)| fields.split_whitespace().next())
125
+ .is_some_and(|state| matches!(state, "T" | "t"))
126
+ }
127
+
128
+ #[cfg(test)]
129
+ mod tests {
130
+ use super::*;
131
+
132
+ #[test]
133
+ fn only_other_processes_write_locks_on_the_catalog_are_holders() {
134
+ let locks = "\
135
+ 1: POSIX ADVISORY WRITE 4242 103:02:77 1073741824 1073742335
136
+ 2: POSIX ADVISORY READ 5151 103:02:77 1073741826 1073742335
137
+ 3: POSIX ADVISORY WRITE 6161 103:02:99 120 120
138
+ 4: FLOCK ADVISORY WRITE 7171 103:02:77 0 EOF
139
+ 5: POSIX ADVISORY WRITE 1000 103:02:77 0 EOF
140
+ 6: -> POSIX ADVISORY WRITE 8181 103:02:77 0 EOF
141
+ 7: POSIX ADVISORY WRITE 9191 104:02:77 0 EOF
142
+ ";
143
+ let device = rustix::fs::makedev(0x103, 2) as u64;
144
+ assert_eq!(write_holders(locks, &[(device, 77)], 1000), vec![4242]);
145
+ assert_eq!(
146
+ write_holders(locks, &[(device, 77), (device, 99)], 1000),
147
+ vec![4242, 6161]
148
+ );
149
+ assert!(write_holders(locks, &[(device, 5)], 1000).is_empty());
150
+ }
151
+
152
+ #[test]
153
+ fn exited_holder_is_progress_and_never_a_stopped_writer() {
154
+ for state in ["Z", "X", "R", "S", "D"] {
155
+ assert!(!process_is_stopped(&format!("4242 (writer) {state} 1 2 3")));
156
+ }
157
+ for state in ["T", "t"] {
158
+ assert!(process_is_stopped(&format!("4242 (writer) {state} 1 2 3")));
159
+ }
160
+ assert!(!process_is_stopped("unavailable"));
161
+ }
162
+
163
+ #[test]
164
+ fn stopped_holder_must_still_own_the_lock_before_refusal() {
165
+ let held = "1: POSIX ADVISORY WRITE 4242 103:02:77 0 EOF\n";
166
+ let files = [(rustix::fs::makedev(0x103, 2) as u64, 77)];
167
+ // Stable SIGSTOP remains a typed refusal, while snapshot -> unlock ->
168
+ // SIGSTOP must let SQLite retry rather than refuse the old snapshot.
169
+ assert!(snapshots_cannot_release(
170
+ held,
171
+ &files,
172
+ 1000,
173
+ |_| true,
174
+ || Some(held.into())
175
+ ));
176
+ assert!(!snapshots_cannot_release(
177
+ held,
178
+ &files,
179
+ 1000,
180
+ |_| true,
181
+ || Some(String::new())
182
+ ));
183
+ assert!(!snapshots_cannot_release(
184
+ held,
185
+ &files,
186
+ 1000,
187
+ |_| true,
188
+ || None
189
+ ));
190
+ let replacement = held.replace("4242", "6161");
191
+ assert!(!snapshots_cannot_release(
192
+ held,
193
+ &files,
194
+ 1000,
195
+ |_| true,
196
+ || Some(replacement)
197
+ ));
198
+ assert!(!snapshots_cannot_release(
199
+ held,
200
+ &files,
201
+ 1000,
202
+ |_| false,
203
+ || panic!("a live holder must retry")
204
+ ));
205
+ }
206
+ }
@@ -104,7 +104,14 @@ pub fn connections_opened(root: &Path) -> u64 {
104
104
  .unwrap_or(0)
105
105
  }
106
106
 
107
- fn connect(path: &Path) -> Result<Connection> {
107
+ pub(crate) fn connect(path: &Path) -> Result<Connection> {
108
+ // SQLite creates WAL/SHM files before assigning their database owner. Opening
109
+ // them under root exposes a transient root-owned inode to concurrent owner
110
+ // connections, which can permanently fall back to read-only WAL access.
111
+ let _identity = crate::filesystem_identity::FilesystemIdentity::for_store_root(
112
+ path.parent().unwrap_or(path),
113
+ )
114
+ .map_err(|error| io("enter catalog filesystem identity", error))?;
108
115
  *opened()
109
116
  .lock()
110
117
  .unwrap_or_else(|error| error.into_inner())
@@ -253,6 +260,26 @@ impl Trust {
253
260
  self.load(&mut kept).map(|_| ())
254
261
  }
255
262
 
263
+ /// Finish setup on the already-validated reader connection. The callback is
264
+ /// synchronous under its mutex; no second setup connection or credential
265
+ /// lifetime escapes into the returned value.
266
+ pub(crate) fn with_connection<T>(
267
+ &self,
268
+ use_connection: impl FnOnce(&mut Connection) -> Result<T>,
269
+ ) -> Result<T> {
270
+ let mut kept = self.kept.lock().unwrap_or_else(|error| error.into_inner());
271
+ self.load(&mut kept)?;
272
+ let connection = &mut kept.as_mut().expect("validated connection").connection;
273
+ // A kept connection can be used by another Runtime thread later. Install
274
+ // the lock context on the thread actually executing this operation.
275
+ lock::wait_for_live_holders(connection, &self.path)?;
276
+ let _identity = crate::filesystem_identity::FilesystemIdentity::for_store_root(
277
+ self.path.parent().unwrap_or(&self.path),
278
+ )
279
+ .map_err(|error| io("enter catalog filesystem identity", error))?;
280
+ use_connection(connection)
281
+ }
282
+
256
283
  fn miss(&self, sha256: &str) -> Option<crate::store::VRecord> {
257
284
  let mut kept = self.kept.lock().unwrap_or_else(|error| error.into_inner());
258
285
  if self.load(&mut kept).ok()? {