tensorfs 0.3.58__tar.gz → 0.3.59__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 (150) hide show
  1. {tensorfs-0.3.58 → tensorfs-0.3.59}/Cargo.lock +2 -2
  2. {tensorfs-0.3.58 → tensorfs-0.3.59}/Cargo.toml +1 -1
  3. {tensorfs-0.3.58 → tensorfs-0.3.59}/PKG-INFO +1 -1
  4. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/catalog.rs +17 -1
  5. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/http.rs +13 -2
  6. tensorfs-0.3.59/crates/tensorfs-core/src/transport/memory.rs +92 -0
  7. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/mod.rs +2 -0
  8. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/prefix.rs +1 -1
  9. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/pull.rs +1 -1
  10. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/push.rs +9 -9
  11. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_download.rs +256 -5
  12. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-py/src/lib.rs +2 -0
  13. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-py/src/store.rs +52 -15
  14. {tensorfs-0.3.58 → tensorfs-0.3.59}/pyproject.toml +1 -1
  15. {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/__init__.py +4 -0
  16. {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/_ext.pyi +10 -0
  17. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/Cargo.toml +0 -0
  18. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/build.rs +0 -0
  19. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  20. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  21. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  22. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  23. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/asset_update.rs +0 -0
  24. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/b64.rs +0 -0
  25. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  26. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  27. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  28. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  29. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  30. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  31. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  32. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  33. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  34. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  35. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/canon.rs +0 -0
  36. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/capability.rs +0 -0
  37. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/cbor.rs +0 -0
  38. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  39. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  40. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/corpus.rs +0 -0
  41. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  42. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  43. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  44. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  45. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  46. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  47. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  48. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  49. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived.rs +0 -0
  50. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/dtype.rs +0 -0
  51. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/durability.rs +0 -0
  52. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/err.rs +0 -0
  53. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/fetch.rs +0 -0
  54. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/fill.rs +0 -0
  55. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/fit.rs +0 -0
  56. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/gc.rs +0 -0
  57. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/header.rs +0 -0
  58. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/home.rs +0 -0
  59. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ids.rs +0 -0
  60. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  61. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  62. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  63. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  64. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  65. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  66. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  67. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  68. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  69. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  70. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  71. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/jcs.rs +0 -0
  72. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/lib.rs +0 -0
  73. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/limits.rs +0 -0
  74. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/machine.rs +0 -0
  75. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/manifest.rs +0 -0
  76. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/meta.rs +0 -0
  77. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/project.rs +0 -0
  78. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/providers.rs +0 -0
  79. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/read.rs +0 -0
  80. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/receipt.rs +0 -0
  81. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/reclaim.rs +0 -0
  82. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/registry.rs +0 -0
  83. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/relation.rs +0 -0
  84. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
  85. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  86. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/repository.rs +0 -0
  87. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/sha256.rs +0 -0
  88. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  89. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/spec.rs +0 -0
  90. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/staging.rs +0 -0
  91. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/stats.rs +0 -0
  92. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/storage.rs +0 -0
  93. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/store.rs +0 -0
  94. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/api.rs +0 -0
  95. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
  96. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  97. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  98. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  99. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  100. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
  101. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  102. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/vectors.rs +0 -0
  103. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  104. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  105. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  106. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  107. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fit.rs +0 -0
  108. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  109. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  110. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  111. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  112. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  113. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  114. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  115. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  116. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  117. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  118. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/home.rs +0 -0
  119. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  120. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  121. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/liveness.rs +0 -0
  122. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  123. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  124. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  125. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  126. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  127. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  128. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  129. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  130. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  131. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  132. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  133. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  134. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  135. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/staging.rs +0 -0
  136. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  137. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  138. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  139. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  140. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/version.rs +0 -0
  141. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/write_through.rs +0 -0
  142. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-py/Cargo.toml +0 -0
  143. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-py/src/docs.rs +0 -0
  144. {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-py/src/errors.rs +0 -0
  145. {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/_derived_channel.py +0 -0
  146. {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/_source_channel.py +0 -0
  147. {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/_tensors.py +0 -0
  148. {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/derived.py +0 -0
  149. {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/errors.py +0 -0
  150. {tensorfs-0.3.58 → tensorfs-0.3.59}/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.58"
442
+ version = "0.3.59"
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.58"
457
+ version = "0.3.59"
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.58"
6
+ version = "0.3.59"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.58
3
+ Version: 0.3.59
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -911,9 +911,14 @@ impl Catalog {
911
911
  object: &crate::ids::ObjectRef,
912
912
  publish: impl FnOnce() -> Result<Option<crate::store::VRecord>>,
913
913
  ) -> Result<bool> {
914
+ // One admission at a time per catalog in this process, holding the write lock from
915
+ // before publication: streams landing objects together queue here instead of in
916
+ // SQLite's busy timeout, where a lost race refused bytes already verified.
917
+ let turn = admissions(&self.path);
918
+ let _turn = turn.lock().unwrap_or_else(|error| error.into_inner());
914
919
  let mut connection = self.connection()?;
915
920
  let transaction = connection
916
- .transaction()
921
+ .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
917
922
  .map_err(|error| sql("begin blob admission", error))?;
918
923
  if let Some(operation) = operation {
919
924
  hold_on(
@@ -1079,6 +1084,17 @@ impl Catalog {
1079
1084
  }
1080
1085
  }
1081
1086
 
1087
+ fn admissions(path: &Path) -> Arc<Mutex<()>> {
1088
+ static TURNS: OnceLock<Mutex<HashMap<PathBuf, Arc<Mutex<()>>>>> = OnceLock::new();
1089
+ TURNS
1090
+ .get_or_init(Default::default)
1091
+ .lock()
1092
+ .unwrap_or_else(|error| error.into_inner())
1093
+ .entry(path.to_path_buf())
1094
+ .or_default()
1095
+ .clone()
1096
+ }
1097
+
1082
1098
  fn hold_on(connection: &Connection, operation: &str, key: &str, length: u64) -> Result<()> {
1083
1099
  let kind = if key.starts_with("blobs/") {
1084
1100
  "blob"
@@ -60,6 +60,11 @@ pub enum Judge {
60
60
  /// which is the owner's rule read literally: the lack of measured progress, never a
61
61
  /// number of seconds.
62
62
  Transfer,
63
+ /// An UPLOAD. Every wait — connect, handshake, each write, and the answer after the
64
+ /// last body byte — is judged by this exchange's own silence against the ledger's
65
+ /// patience. Nothing it moves lands here, so it neither counts as progress nor teaches
66
+ /// the pace.
67
+ Silence,
63
68
  /// Neither. The caller's deadline is the only bound, which is what a foreign-source
64
69
  /// read with its own per-part retry loop above it already had.
65
70
  Deadline,
@@ -133,7 +138,7 @@ fn transfer_verdict(
133
138
  /// progress may span arbitrarily many windows without exhausting a wall budget.
134
139
  fn stream_verdict(judge: Judge, ledger: &Ledger, advanced: Instant, phase: &str) -> Result<()> {
135
140
  ledger.check_running()?;
136
- if judge != Judge::Stream || advanced.elapsed() <= ledger.patience() {
141
+ if !judge.silent_is_stall() || advanced.elapsed() <= ledger.patience() {
137
142
  return Ok(());
138
143
  }
139
144
  refuse(
@@ -147,6 +152,12 @@ fn stream_verdict(judge: Judge, ledger: &Ledger, advanced: Instant, phase: &str)
147
152
  )
148
153
  }
149
154
 
155
+ impl Judge {
156
+ fn silent_is_stall(self) -> bool {
157
+ matches!(self, Judge::Stream | Judge::Silence)
158
+ }
159
+ }
160
+
150
161
  pub struct Client {
151
162
  tls: Arc<rustls::ClientConfig>,
152
163
  idle: IdlePool,
@@ -712,7 +723,7 @@ impl BodyReader<'_> {
712
723
  Err(e) if would_block(&e) => {
713
724
  transfer_verdict(self.judge, self.ledger, self.since, "a response body")?;
714
725
  let silent = self.advanced.elapsed();
715
- if self.judge == Judge::Stream && silent > self.ledger.patience() {
726
+ if self.judge.silent_is_stall() && silent > self.ledger.patience() {
716
727
  self.stalled = true;
717
728
  let _ = self
718
729
  .wire
@@ -0,0 +1,92 @@
1
+ //! How many transfer streams fit in the memory this process can actually use. There is no
2
+ //! assumed host: a 4 GB pod runs the same pull, source download or upload window with fewer
3
+ //! streams, never with N × buffers it does not have.
4
+ use std::fs;
5
+ use std::path::{Path, PathBuf};
6
+
7
+ /// The most memory one transfer stream holds, whatever its object's size: one 1 MiB body
8
+ /// chunk (the read, write, hash or upload buffer), the 64 KiB framing buffer, and rustls's
9
+ /// bounded record buffers, rounded up to a whole 2 MiB.
10
+ pub const STREAM_MEMORY: u64 = 2 << 20;
11
+
12
+ /// Bytes this process can still use now: the tightest of its cgroup's limit less the usage
13
+ /// that cannot be reclaimed (v2 at every ancestor, else v1) and the host's `MemAvailable`.
14
+ /// `None` when none of them is readable.
15
+ pub fn memory_available() -> Option<u64> {
16
+ [cgroup_v2(), cgroup_v1(), mem_available()]
17
+ .into_iter()
18
+ .flatten()
19
+ .min()
20
+ }
21
+
22
+ /// Concurrent transfer streams the memory available now allows: at most `wanted`, at
23
+ /// least one.
24
+ pub fn transfer_streams(wanted: usize) -> usize {
25
+ streams_within(wanted, memory_available())
26
+ }
27
+
28
+ /// `transfer_streams` against a stated budget.
29
+ pub fn streams_within(wanted: usize, available: Option<u64>) -> usize {
30
+ let fits = available.map_or(usize::MAX, |bytes| {
31
+ usize::try_from(bytes / STREAM_MEMORY).unwrap_or(usize::MAX)
32
+ });
33
+ wanted.min(fits).max(1)
34
+ }
35
+
36
+ fn number(path: &Path) -> Option<u64> {
37
+ fs::read_to_string(path).ok()?.trim().parse().ok()
38
+ }
39
+
40
+ /// `limit − (usage − page cache)`: page cache is reclaimed under pressure, so bytes a
41
+ /// download has written do not count against the next stream.
42
+ fn headroom(dir: &Path, limit: &str, usage: &str, cache: &str) -> Option<u64> {
43
+ let limit = number(&dir.join(limit))?;
44
+ let usage = number(&dir.join(usage))?;
45
+ let cached = fs::read_to_string(dir.join("memory.stat"))
46
+ .ok()
47
+ .and_then(|stat| {
48
+ stat.lines()
49
+ .find_map(|line| line.strip_prefix(cache)?.strip_prefix(' ')?.parse().ok())
50
+ })
51
+ .unwrap_or(0);
52
+ Some(limit.saturating_sub(usage.saturating_sub(cached)))
53
+ }
54
+
55
+ fn cgroup_v2() -> Option<u64> {
56
+ let root = Path::new("/sys/fs/cgroup");
57
+ let own = fs::read_to_string("/proc/self/cgroup").ok()?;
58
+ let own = own.lines().find_map(|line| line.strip_prefix("0::"))?;
59
+ let mut dir = root.join(own.trim().trim_start_matches('/'));
60
+ let mut tightest: Option<u64> = None;
61
+ loop {
62
+ if let Some(left) = headroom(&dir, "memory.max", "memory.current", "file") {
63
+ tightest = Some(tightest.map_or(left, |held| held.min(left)));
64
+ }
65
+ if dir == root || !dir.pop() || !dir.starts_with(root) {
66
+ return tightest;
67
+ }
68
+ }
69
+ }
70
+
71
+ fn cgroup_v1() -> Option<u64> {
72
+ let dir = PathBuf::from("/sys/fs/cgroup/memory");
73
+ headroom(
74
+ &dir,
75
+ "memory.limit_in_bytes",
76
+ "memory.usage_in_bytes",
77
+ "total_cache",
78
+ )
79
+ }
80
+
81
+ fn mem_available() -> Option<u64> {
82
+ let text = fs::read_to_string("/proc/meminfo").ok()?;
83
+ let kib: u64 = text
84
+ .lines()
85
+ .find_map(|line| line.strip_prefix("MemAvailable:"))?
86
+ .trim()
87
+ .strip_suffix("kB")?
88
+ .trim()
89
+ .parse()
90
+ .ok()?;
91
+ kib.checked_mul(1024)
92
+ }
@@ -35,6 +35,7 @@ mod api;
35
35
  mod http;
36
36
  mod hub;
37
37
  mod ledger;
38
+ mod memory;
38
39
  mod policy;
39
40
  pub(crate) mod prefix;
40
41
  mod pull;
@@ -55,6 +56,7 @@ pub use ledger::{
55
56
  Deadline, Ledger, PullCancellation, FETCH_ATTEMPTS, SAMPLE_SECONDS, STALL_FACTOR,
56
57
  STILL_SAMPLES, STREAMS,
57
58
  };
59
+ pub use memory::{memory_available, streams_within, transfer_streams, STREAM_MEMORY};
58
60
  pub use policy::{base_host, blocked, is_loopback_origin, resolve, CheckedUrl, SourcePolicy};
59
61
  pub use pull::{
60
62
  fetch_authorized, fetch_url, fetch_wanted, pull, FetchObserver, Fetched, ObjectSource,
@@ -479,7 +479,7 @@ pub(crate) fn fetch_sources(
479
479
  };
480
480
  download.report(0, held);
481
481
  std::thread::scope(|scope| {
482
- for _ in 0..options.streams.clamp(1, 128) {
482
+ for _ in 0..super::transfer_streams(options.streams.clamp(1, 128)) {
483
483
  scope.spawn(|| download.work());
484
484
  }
485
485
  });
@@ -980,7 +980,7 @@ pub(super) fn fetch_wanted_bounded(
980
980
  // invariant it proved — the grant set covers the wanted set exactly — is enforced per
981
981
  // mint instead: presign authorizes every digest it was asked or refuses, never a silent
982
982
  // subset, and a supplier with no URL for an object refuses in the same words.
983
- let streams = streams.clamp(1, 128);
983
+ let streams = super::memory::transfer_streams(streams.clamp(1, 128));
984
984
  let recovery = super::speculate::Recovery::new(store, plan, streams, download_budget);
985
985
  // Recovery reserves an object connection, not an authorization worker.
986
986
  let workers = streams.min(plan.wanted.len().max(1));
@@ -10,11 +10,12 @@
10
10
  //! successful end state for a content-addressed write, not a failure.
11
11
  //!
12
12
  //! The object is RE-SENT when the socket goes silent or the store answers a retryable
13
- //! status, `PUT_ATTEMPTS` times, exactly as the pull re-asks for one object. The write
14
- //! silence bound is the ledger's floor — the same derived noise floor `egress.py` used —
15
- //! never an invented constant. A retryable answer is re-sent after the wait the store
16
- //! states (`Retry-After`), which spends no attempt, or after the ledger's patience, which
17
- //! does. Authority stays where it was: `podweights` (or whoever
13
+ //! status, `PUT_ATTEMPTS` times, exactly as the pull re-asks for one object. Every wait of
14
+ //! the exchange — the answer after the last body byte included — is judged by its own
15
+ //! silence against the ledger's patience (`Judge::Silence`), and one write that moves
16
+ //! nothing by the ledger's floor; never an invented constant. A retryable answer is re-sent
17
+ //! after the wait the store states (`Retry-After`), which spends no attempt, or after the
18
+ //! ledger's patience, which does. Authority stays where it was: `podweights` (or whoever
18
19
  //! minted the grant) decided WHETHER these bytes may go; this module only moves them.
19
20
  //!
20
21
  //! **A grant is a URL AND the headers its signature covers, together.** tensorhub signs
@@ -228,10 +229,9 @@ pub fn push_object(
228
229
  Some(payload),
229
230
  deadline,
230
231
  ledger,
231
- // The push plane keeps its own derived write bound below and gains no plane
232
- // here: it serves no hub call, so it has no transfer whose stillness could
233
- // judge it.
234
- http::Judge::Deadline,
232
+ // A peer that stops answering — during the handshake, a write, or after the
233
+ // last body byte — is judged by that silence, never waited on forever.
234
+ http::Judge::Silence,
235
235
  Some(ledger.floor()),
236
236
  Code::TRANSFER_FAILED,
237
237
  );
@@ -19,8 +19,8 @@ use tensorfs_core::{
19
19
  providers::{self, Provenance, Pulled, Resolution, ResolvedMember},
20
20
  store::{Fault, Store},
21
21
  transport::{
22
- push_object, Anonymous, Deadline, Ledger, PullCancellation, SourceDownload, SourcePolicy,
23
- UploadGrant,
22
+ memory_available, push_object, streams_within, transfer_streams, Anonymous, Deadline,
23
+ Ledger, PullCancellation, SourceDownload, SourcePolicy, UploadGrant, STREAM_MEMORY,
24
24
  },
25
25
  };
26
26
 
@@ -55,6 +55,8 @@ enum Act {
55
55
  Status(u16, Vec<(String, String)>),
56
56
  /// Send the head and this many body bytes, then drop the connection.
57
57
  Cut(usize),
58
+ /// Read a PUT body, then say nothing, ever.
59
+ Silent,
58
60
  }
59
61
 
60
62
  struct Request {
@@ -177,13 +179,19 @@ impl Origin {
177
179
  let number = self.asks.fetch_add(1, Ordering::Relaxed) + 1;
178
180
  let act = (self.decide)(&request, number);
179
181
  if request.method == "PUT" {
180
- let length: usize = header("content-length").unwrap().parse().unwrap();
181
- let mut sink = vec![0u8; length];
182
- if socket.read_exact(&mut sink).is_err() {
182
+ let length: u64 = header("content-length").unwrap().parse().unwrap();
183
+ // Discarded as it arrives: this origin's memory is not the client's.
184
+ let taken = std::io::copy(&mut (&mut socket).take(length), &mut std::io::sink());
185
+ if taken.ok() != Some(length) {
183
186
  return;
184
187
  }
185
188
  self.puts.fetch_add(1, Ordering::Relaxed);
186
189
  match act {
190
+ Act::Silent => {
191
+ // Hold the socket until the client gives up on it.
192
+ let _ = socket.read(&mut [0u8; 1]);
193
+ return;
194
+ }
187
195
  Act::Status(status, extra) => {
188
196
  if !reply(&mut socket, status, &extra, b"retry") {
189
197
  return;
@@ -684,6 +692,45 @@ fn pushed_store(name: &str) -> (std::path::PathBuf, Store, ObjectRef) {
684
692
  (dir, store, object)
685
693
  }
686
694
 
695
+ #[test]
696
+ fn a_push_whose_store_goes_silent_after_the_body_is_judged_not_waited_on() {
697
+ let (dir, store, object) = pushed_store("push-silent");
698
+ let origin = Origin::start(vec![], 0, |request, _| {
699
+ if request.method == "PUT" {
700
+ Act::Silent
701
+ } else {
702
+ Act::Serve
703
+ }
704
+ });
705
+ let ledger = Ledger::with_resolution(Duration::from_millis(20));
706
+ let started = Instant::now();
707
+ let refusal = push_object(
708
+ &store,
709
+ &object.sha256,
710
+ false,
711
+ &UploadGrant::to(&format!("{}/put/object", origin.base)),
712
+ &policy(),
713
+ &Anonymous,
714
+ Deadline::none(),
715
+ &ledger,
716
+ "",
717
+ )
718
+ .unwrap_err();
719
+ assert_eq!(refusal.code, Code::TRANSFER_FAILED);
720
+ assert!(refusal.detail.contains("response head"), "{refusal}");
721
+ assert_eq!(
722
+ origin.puts.load(Ordering::Relaxed),
723
+ 4,
724
+ "each attempt sent the whole body"
725
+ );
726
+ assert!(
727
+ started.elapsed() < Duration::from_secs(30),
728
+ "{:?}",
729
+ started.elapsed()
730
+ );
731
+ fs::remove_dir_all(dir).unwrap();
732
+ }
733
+
687
734
  #[test]
688
735
  fn a_push_told_to_wait_waits_and_then_lands() {
689
736
  let (dir, store, object) = pushed_store("push-wait");
@@ -713,6 +760,210 @@ fn a_push_told_to_wait_waits_and_then_lands() {
713
760
  fs::remove_dir_all(dir).unwrap();
714
761
  }
715
762
 
763
+ #[test]
764
+ fn concurrent_admissions_queue_instead_of_refusing() {
765
+ let dir = root("admissions");
766
+ let store = Store::init(&dir).unwrap();
767
+ thread::scope(|scope| {
768
+ for worker in 0..16u64 {
769
+ let store = &store;
770
+ scope.spawn(move || {
771
+ for index in 0..4 {
772
+ let bytes = body(1000 + worker * 4 + index, 64 << 10);
773
+ let object = ObjectRef::of(&bytes);
774
+ store
775
+ .put_stream(&mut bytes.as_slice(), Some(&object), &Fault::default())
776
+ .unwrap();
777
+ assert!(store.record_valid(&object.sha256).is_ok());
778
+ }
779
+ });
780
+ }
781
+ });
782
+ fs::remove_dir_all(dir).unwrap();
783
+ }
784
+
785
+ /// Peak resident memory since `/proc/self/clear_refs` was last reset, in bytes.
786
+ fn peak_resident() -> u64 {
787
+ let status = fs::read_to_string("/proc/self/status").unwrap();
788
+ let kib: u64 = status
789
+ .lines()
790
+ .find_map(|line| line.strip_prefix("VmHWM:"))
791
+ .unwrap()
792
+ .trim()
793
+ .trim_end_matches("kB")
794
+ .trim()
795
+ .parse()
796
+ .unwrap();
797
+ kib * 1024
798
+ }
799
+
800
+ /// Run one `*_child` test of this binary in its own process, optionally inside `scope`.
801
+ fn run_child(test: &str, dir: &std::path::Path, scope: &[&str]) -> std::process::Output {
802
+ let exe = std::env::current_exe().unwrap();
803
+ let mut command = match scope.split_first() {
804
+ Some((program, args)) => {
805
+ let mut command = Command::new(program);
806
+ command.args(args).arg(&exe);
807
+ command
808
+ }
809
+ None => Command::new(&exe),
810
+ };
811
+ command
812
+ .args(["--exact", test, "--nocapture"])
813
+ .env("TFS_DOWNLOAD_CHILD", dir)
814
+ .output()
815
+ .unwrap()
816
+ }
817
+
818
+ #[test]
819
+ fn push_memory_child() {
820
+ let Some(dir) = std::env::var_os("TFS_DOWNLOAD_CHILD") else {
821
+ return;
822
+ };
823
+ let store = Store::init(std::path::Path::new(&dir)).unwrap();
824
+ let objects: Vec<ObjectRef> = (0..16u64)
825
+ .map(|seed| {
826
+ let bytes = body(300 + seed, 16 * MIB);
827
+ let object = ObjectRef::of(&bytes);
828
+ store
829
+ .put_stream(&mut bytes.as_slice(), Some(&object), &Fault::default())
830
+ .unwrap();
831
+ object
832
+ })
833
+ .collect();
834
+ let origin = Origin::start(vec![], 0, |_, _| Act::Serve);
835
+ // Forget the objects' construction; measure only the pushes.
836
+ fs::write("/proc/self/clear_refs", "5").unwrap();
837
+ let before = peak_resident();
838
+ thread::scope(|scope| {
839
+ for object in &objects {
840
+ let (store, origin) = (&store, &origin);
841
+ scope.spawn(move || {
842
+ push_object(
843
+ store,
844
+ &object.sha256,
845
+ false,
846
+ &UploadGrant::to(&format!("{}/put/{}", origin.base, object.sha256)),
847
+ &policy(),
848
+ &Anonymous,
849
+ Deadline::none(),
850
+ &Ledger::with_resolution(Duration::from_millis(200)),
851
+ "",
852
+ )
853
+ .unwrap();
854
+ });
855
+ }
856
+ });
857
+ let grew = peak_resident().saturating_sub(before);
858
+ println!("PUSHED 16 x 16 MiB, peak resident grew {grew} B");
859
+ assert_eq!(origin.puts.load(Ordering::Relaxed), 16);
860
+ // 256 MiB crossed the sockets; the pushes may hold their streams' buffers and no more.
861
+ assert!(
862
+ grew < 16 * STREAM_MEMORY + (16 << 20),
863
+ "pushes buffered {grew} B"
864
+ );
865
+ }
866
+
867
+ #[test]
868
+ fn sixteen_concurrent_pushes_hold_stream_buffers_not_objects() {
869
+ let dir = root("push-memory");
870
+ let output = run_child("push_memory_child", &dir, &[]);
871
+ let text = String::from_utf8_lossy(&output.stdout).to_string()
872
+ + &String::from_utf8_lossy(&output.stderr);
873
+ assert!(output.status.success() && text.contains("PUSHED"), "{text}");
874
+ println!(
875
+ "{}",
876
+ text.lines().find(|line| line.contains("PUSHED")).unwrap()
877
+ );
878
+ fs::remove_dir_all(dir).unwrap();
879
+ }
880
+
881
+ #[test]
882
+ fn streams_are_sized_to_the_memory_budget() {
883
+ assert_eq!(streams_within(16, None), 16);
884
+ assert_eq!(streams_within(16, Some(1 << 40)), 16);
885
+ assert_eq!(streams_within(16, Some(5 * STREAM_MEMORY)), 5);
886
+ assert_eq!(
887
+ streams_within(16, Some(0)),
888
+ 1,
889
+ "a starved host still moves one stream"
890
+ );
891
+ assert!(memory_available().is_some_and(|bytes| bytes > 0));
892
+ }
893
+
894
+ #[test]
895
+ fn bounded_memory_child() {
896
+ let Some(dir) = std::env::var_os("TFS_DOWNLOAD_CHILD") else {
897
+ return;
898
+ };
899
+ let available = memory_available().unwrap();
900
+ let streams = transfer_streams(1024);
901
+ println!("BUDGET {available} B allows {streams} streams");
902
+ // The scope's 64 MiB, not the host's gigabytes, decides.
903
+ assert!(
904
+ available < 64 << 20 && streams < 32,
905
+ "{available} B, {streams} streams"
906
+ );
907
+ let name = "model.safetensors";
908
+ let origin = Origin::start(vec![(name, body(77, 16 * MIB))], 16 << 20, |_, _| {
909
+ Act::Serve
910
+ });
911
+ let store = Store::init(std::path::Path::new(&dir)).unwrap();
912
+ materialize(
913
+ &store,
914
+ &owner("bounded"),
915
+ &origin.resolution(&[name]),
916
+ &options(MIB, 1024),
917
+ &|_, _| {},
918
+ )
919
+ .unwrap();
920
+ verify(&store, &origin, &[name]);
921
+ let live = origin.most_live.load(Ordering::SeqCst);
922
+ println!("DOWNLOADED with at most {live} live streams");
923
+ assert!(
924
+ live <= streams,
925
+ "{live} streams against a budget of {streams}"
926
+ );
927
+ }
928
+
929
+ #[test]
930
+ fn a_memory_limited_scope_downloads_with_fewer_streams() {
931
+ let scope = [
932
+ "systemd-run",
933
+ "--user",
934
+ "--scope",
935
+ "-q",
936
+ "-p",
937
+ "MemoryMax=64M",
938
+ ];
939
+ let usable = Command::new(scope[0])
940
+ .args(&scope[1..])
941
+ .arg("true")
942
+ .stdout(Stdio::null())
943
+ .stderr(Stdio::null())
944
+ .status()
945
+ .is_ok_and(|status| status.success());
946
+ if !usable {
947
+ eprintln!("SKIPPED: no systemd user manager to create a memory-limited cgroup");
948
+ return;
949
+ }
950
+ let dir = root("bounded-memory");
951
+ let output = run_child("bounded_memory_child", &dir, &scope);
952
+ let text = String::from_utf8_lossy(&output.stdout).to_string()
953
+ + &String::from_utf8_lossy(&output.stderr);
954
+ assert!(
955
+ output.status.success() && text.contains("DOWNLOADED"),
956
+ "{text}"
957
+ );
958
+ for line in text
959
+ .lines()
960
+ .filter(|line| line.contains("BUDGET") || line.contains("DOWNLOADED"))
961
+ {
962
+ println!("{line}");
963
+ }
964
+ let _ = fs::remove_dir_all(dir);
965
+ }
966
+
716
967
  /// Throughput on a paced loopback origin (each connection capped like one CDN stream).
717
968
  /// `cargo test --test source_download -- --ignored --nocapture throughput`
718
969
  #[test]
@@ -119,6 +119,8 @@ fn _ext(m: &Bound<'_, PyModule>) -> PyResult<()> {
119
119
  m.add_function(wrap_pyfunction!(store::fetch_complete_cozytensors, m)?)?;
120
120
  m.add_class::<store::PyPullCancellation>()?;
121
121
  m.add_function(wrap_pyfunction!(store::pull, m)?)?;
122
+ m.add_function(wrap_pyfunction!(store::transfer_streams, m)?)?;
123
+ m.add("STREAM_MEMORY", tensorfs_core::transport::STREAM_MEMORY)?;
122
124
  m.add_class::<store::PyRepoObjectCache>()?;
123
125
  m.add_class::<store::PyStore>()?;
124
126
  m.add_class::<store::PyOperation>()?;