tensorfs 0.3.57__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 (151) hide show
  1. {tensorfs-0.3.57 → tensorfs-0.3.59}/Cargo.lock +2 -2
  2. {tensorfs-0.3.57 → tensorfs-0.3.59}/Cargo.toml +1 -1
  3. {tensorfs-0.3.57 → tensorfs-0.3.59}/PKG-INFO +1 -1
  4. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/catalog.rs +17 -1
  5. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/providers.rs +50 -30
  6. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/http.rs +76 -5
  7. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +14 -12
  8. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/ledger.rs +28 -1
  9. tensorfs-0.3.59/crates/tensorfs-core/src/transport/memory.rs +92 -0
  10. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/mod.rs +3 -1
  11. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/policy.rs +13 -0
  12. tensorfs-0.3.59/crates/tensorfs-core/src/transport/prefix.rs +1094 -0
  13. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/pull.rs +17 -5
  14. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/push.rs +25 -9
  15. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/ranged.rs +40 -8
  16. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/tests.rs +9 -4
  17. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_artifacts.rs +2 -2
  18. tensorfs-0.3.59/crates/tensorfs-core/tests/source_download.rs +1006 -0
  19. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_prefixes.rs +4 -1
  20. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-py/src/lib.rs +2 -0
  21. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-py/src/store.rs +52 -15
  22. {tensorfs-0.3.57 → tensorfs-0.3.59}/pyproject.toml +1 -1
  23. {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/__init__.py +4 -0
  24. {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/_ext.pyi +10 -0
  25. tensorfs-0.3.57/crates/tensorfs-core/src/transport/prefix.rs +0 -505
  26. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/Cargo.toml +0 -0
  27. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/build.rs +0 -0
  28. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  29. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  30. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  31. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  32. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/asset_update.rs +0 -0
  33. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/b64.rs +0 -0
  34. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  35. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  36. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  37. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  38. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  39. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  40. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  41. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  42. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  43. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  44. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/canon.rs +0 -0
  45. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/capability.rs +0 -0
  46. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/cbor.rs +0 -0
  47. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  48. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  49. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/corpus.rs +0 -0
  50. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  51. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  52. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  53. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  54. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  55. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  56. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  57. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  58. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived.rs +0 -0
  59. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/dtype.rs +0 -0
  60. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/durability.rs +0 -0
  61. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/err.rs +0 -0
  62. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/fetch.rs +0 -0
  63. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/fill.rs +0 -0
  64. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/fit.rs +0 -0
  65. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/gc.rs +0 -0
  66. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/header.rs +0 -0
  67. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/home.rs +0 -0
  68. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ids.rs +0 -0
  69. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  70. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  71. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  72. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  73. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  74. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  75. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  76. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  77. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  78. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  79. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  80. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/jcs.rs +0 -0
  81. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/lib.rs +0 -0
  82. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/limits.rs +0 -0
  83. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/machine.rs +0 -0
  84. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/manifest.rs +0 -0
  85. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/meta.rs +0 -0
  86. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/project.rs +0 -0
  87. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/read.rs +0 -0
  88. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/receipt.rs +0 -0
  89. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/reclaim.rs +0 -0
  90. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/registry.rs +0 -0
  91. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/relation.rs +0 -0
  92. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
  93. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  94. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/repository.rs +0 -0
  95. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/sha256.rs +0 -0
  96. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  97. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/spec.rs +0 -0
  98. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/staging.rs +0 -0
  99. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/stats.rs +0 -0
  100. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/storage.rs +0 -0
  101. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/store.rs +0 -0
  102. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/api.rs +0 -0
  103. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  104. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
  105. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/vectors.rs +0 -0
  106. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  107. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  108. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  109. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  110. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fit.rs +0 -0
  111. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  112. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  113. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  114. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  115. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  116. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  117. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  118. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  119. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  120. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  121. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/home.rs +0 -0
  122. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  123. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  124. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/liveness.rs +0 -0
  125. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  126. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  127. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  128. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  129. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  130. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  131. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  132. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  133. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  134. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  135. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  136. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/staging.rs +0 -0
  137. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  138. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  139. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  140. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  141. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/version.rs +0 -0
  142. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/write_through.rs +0 -0
  143. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-py/Cargo.toml +0 -0
  144. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-py/src/docs.rs +0 -0
  145. {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-py/src/errors.rs +0 -0
  146. {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/_derived_channel.py +0 -0
  147. {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/_source_channel.py +0 -0
  148. {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/_tensors.py +0 -0
  149. {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/derived.py +0 -0
  150. {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/errors.py +0 -0
  151. {tensorfs-0.3.57 → 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.57"
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.57"
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.57"
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.57
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"
@@ -793,6 +793,7 @@ pub fn content_manifest(resolution: &Resolution) -> Result<crate::manifest::Mani
793
793
  }
794
794
 
795
795
  /// Materialize precisely an accepted selection; never re-list or broaden its roster.
796
+ /// `progress` hears (present bytes, total bytes) of the selection's distinct objects.
796
797
  #[allow(clippy::too_many_arguments)]
797
798
  pub fn materialize_selected(
798
799
  store: &Store,
@@ -801,7 +802,7 @@ pub fn materialize_selected(
801
802
  policy: &SourcePolicy,
802
803
  credentials: &dyn CredentialProvider,
803
804
  deadline: Deadline,
804
- progress: &mut dyn FnMut(&str, u64),
805
+ progress: crate::transport::SourceProgress<'_>,
805
806
  ) -> Result<(crate::source_artifact::TreeRoot, Vec<Pulled>)> {
806
807
  materialize_selected_with_options(
807
808
  store,
@@ -815,7 +816,7 @@ pub fn materialize_selected(
815
816
  )
816
817
  }
817
818
 
818
- /// Native development fault points and checkpoint sizing for source qualification.
819
+ /// Stream count, checkpoint sizing, cancellation and development fault points.
819
820
  #[allow(clippy::too_many_arguments)]
820
821
  pub fn materialize_selected_with_options(
821
822
  store: &Store,
@@ -824,7 +825,7 @@ pub fn materialize_selected_with_options(
824
825
  policy: &SourcePolicy,
825
826
  credentials: &dyn CredentialProvider,
826
827
  deadline: Deadline,
827
- progress: &mut dyn FnMut(&str, u64),
828
+ progress: crate::transport::SourceProgress<'_>,
828
829
  options: &crate::transport::SourceDownload,
829
830
  ) -> Result<(crate::source_artifact::TreeRoot, Vec<Pulled>)> {
830
831
  use crate::ids::Doc;
@@ -833,35 +834,54 @@ pub fn materialize_selected_with_options(
833
834
  let mut writer = crate::source_artifact::Writer::open(store, owner, tree.object_ref())?;
834
835
  let objects = resolution.objects();
835
836
  let (plan, _) = FetchPlan::of_objects(store, "selected-source", &objects)?;
836
- let ledger = Ledger::new();
837
- let mut pulled = vec![];
837
+ let mut ledger = Ledger::new();
838
+ if let Some(cancellation) = &options.cancellation {
839
+ ledger = ledger.with_cancellation(cancellation.clone());
840
+ }
841
+ // Every member's liveness is published before any of their bytes move.
842
+ let mut jobs: Vec<crate::transport::prefix::SourceJob> = vec![];
843
+ let (mut seen, mut held_bytes) = (std::collections::BTreeSet::new(), 0);
838
844
  for member in &resolution.members {
839
845
  writer.hold_expected(&member.object)?;
840
- let held = matches!(store.record_valid(&member.object.sha256), Ok(row) if row.length == member.object.length);
841
- let transferred = if held {
842
- 0
843
- } else {
844
- let grant = DeliveryGrant::mint(&plan, &member.object)?;
845
- crate::transport::prefix::fetch_source(
846
- store,
847
- owner,
848
- &grant,
849
- &member.url,
850
- policy,
851
- credentials,
852
- deadline,
853
- &ledger,
854
- options,
855
- Some(&mut |bytes| progress(&member.member, bytes)),
856
- )?
857
- .transferred
858
- };
846
+ let object = &member.object;
847
+ if !seen.insert(&object.sha256) {
848
+ continue;
849
+ }
850
+ if matches!(store.record_valid(&object.sha256), Ok(row) if row.length == object.length) {
851
+ held_bytes += object.length;
852
+ continue;
853
+ }
854
+ jobs.push(crate::transport::prefix::SourceJob {
855
+ grant: DeliveryGrant::mint(&plan, object)?,
856
+ url: member.url.clone(),
857
+ });
858
+ }
859
+ let transferred = crate::transport::prefix::fetch_sources(
860
+ store,
861
+ owner,
862
+ &jobs,
863
+ policy,
864
+ credentials,
865
+ deadline,
866
+ &ledger,
867
+ options,
868
+ held_bytes,
869
+ progress,
870
+ )?;
871
+ let mut pulled = vec![];
872
+ let mut counted = std::collections::BTreeSet::new();
873
+ for member in &resolution.members {
859
874
  writer.landed(&member.object)?;
875
+ let first = counted.insert(&member.object.sha256);
876
+ let fetched = jobs
877
+ .iter()
878
+ .position(|job| job.grant.object == member.object)
879
+ .filter(|_| first);
860
880
  pulled.push(Pulled {
861
881
  member: member.member.clone(),
862
882
  object: member.object.clone(),
863
- transferred,
864
- held,
883
+ transferred: fetched.map_or(0, |at| transferred[at]),
884
+ held: fetched.is_none(),
865
885
  });
866
886
  }
867
887
  plan.complete(store)?;
@@ -2475,7 +2495,7 @@ mod tests {
2475
2495
  &endpoints.policy(&uri),
2476
2496
  &Anonymous,
2477
2497
  Deadline::none(),
2478
- &mut |_, _| {},
2498
+ &|_, _| {},
2479
2499
  )
2480
2500
  .unwrap();
2481
2501
  assert_eq!(
@@ -2492,7 +2512,7 @@ mod tests {
2492
2512
  &endpoints.policy(&uri),
2493
2513
  &Anonymous,
2494
2514
  Deadline::none(),
2495
- &mut |_, _| {},
2515
+ &|_, _| {},
2496
2516
  )
2497
2517
  .unwrap();
2498
2518
  assert_eq!(warm, cold);
@@ -2646,7 +2666,7 @@ mod tests {
2646
2666
  &endpoints.policy(&uri),
2647
2667
  &Anonymous,
2648
2668
  Deadline::none(),
2649
- &mut |_, _| {},
2669
+ &|_, _| {},
2650
2670
  )
2651
2671
  .unwrap_err();
2652
2672
  assert_eq!(error.code, Code::OBJECT_ID_MISMATCH);
@@ -2698,7 +2718,7 @@ mod tests {
2698
2718
  &endpoints.policy(&uri),
2699
2719
  &Anonymous,
2700
2720
  Deadline::none(),
2701
- &mut |_, _| {},
2721
+ &|_, _| {},
2702
2722
  )
2703
2723
  .unwrap();
2704
2724
  assert!(retained.complete && !retained.released);
@@ -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,
@@ -157,20 +168,21 @@ type IdlePool = Arc<Mutex<Vec<Idle>>>;
157
168
  // This bounds retained descriptors, not in-flight transfers (the caller owns those).
158
169
  const MAX_IDLE: usize = 8;
159
170
 
171
+ /// A connection belongs to its origin and the credentials sent on it, never to one
172
+ /// request target: a signed CDN URL changes per resolve, and keying on it meant the
173
+ /// delivery hop never reused a socket.
160
174
  #[derive(PartialEq, Eq)]
161
175
  struct Scope {
162
176
  https: bool,
163
177
  host: String,
164
178
  port: u16,
165
- // Do not retain signed targets or credentials in an idle connection's key.
179
+ // Do not retain credentials in an idle connection's key.
166
180
  request: [u8; 32],
167
181
  }
168
182
 
169
183
  impl Scope {
170
184
  fn ranged(checked: &CheckedUrl, headers: &[(String, String)]) -> Self {
171
185
  let mut hash = crate::sha256::Sha256::new();
172
- hash.update(&(checked.target.len() as u64).to_le_bytes());
173
- hash.update(checked.target.as_bytes());
174
186
  for (name, value) in headers
175
187
  .iter()
176
188
  .filter(|(name, _)| !name.eq_ignore_ascii_case("range"))
@@ -532,6 +544,34 @@ impl<'l> Response<'l> {
532
544
  self.body
533
545
  }
534
546
 
547
+ /// Drop a small non-artifact body (a redirect's, a refusal's) by reading it to its end,
548
+ /// so the connection can serve the next ask. Nothing read here is counted; a body
549
+ /// larger than a response head is simply left, and its connection closes.
550
+ pub fn discard(self) {
551
+ let mut body = self.body;
552
+ let mut buf = [0u8; 4096];
553
+ let mut left = HEAD_MAX_BYTES;
554
+ while left > 0 {
555
+ match body.read_framed(&mut buf[..left.min(4096)]) {
556
+ Ok(0) | Err(_) => return,
557
+ Ok(n) => left -= n,
558
+ }
559
+ }
560
+ }
561
+
562
+ /// The wait this answer states (`Retry-After`), read in the origin's own clock:
563
+ /// delta-seconds, or an HTTP-date against the answer's own `Date`. `None` when it
564
+ /// states nothing readable.
565
+ pub fn stated_wait(&self) -> Option<Duration> {
566
+ let value = self.header("retry-after")?.trim();
567
+ if let Ok(seconds) = value.parse::<u64>() {
568
+ return Some(Duration::from_secs(seconds));
569
+ }
570
+ let at = http_date(value)?;
571
+ let now = http_date(self.header("date")?)?;
572
+ Some(Duration::from_secs(at.saturating_sub(now)))
573
+ }
574
+
535
575
  /// The whole body, refused above `cap` rather than truncated.
536
576
  pub fn read_capped(mut self, cap: usize) -> Result<Vec<u8>> {
537
577
  let mut out = Vec::new();
@@ -552,6 +592,37 @@ impl<'l> Response<'l> {
552
592
  }
553
593
  }
554
594
 
595
+ /// RFC 9110 IMF-fixdate (`Sun, 06 Nov 1994 08:49:37 GMT`) as Unix seconds.
596
+ fn http_date(text: &str) -> Option<u64> {
597
+ const MONTHS: [&str; 12] = [
598
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
599
+ ];
600
+ let mut parts = text.split_whitespace().skip(1);
601
+ let day: i64 = parts.next()?.parse().ok()?;
602
+ let month = parts.next()?;
603
+ let month = MONTHS.iter().position(|name| *name == month)? as i64 + 1;
604
+ let year: i64 = parts.next()?.parse().ok()?;
605
+ let clock: Vec<i64> = parts
606
+ .next()?
607
+ .split(':')
608
+ .map(|part| part.parse().ok())
609
+ .collect::<Option<_>>()?;
610
+ if parts.next()? != "GMT" || clock.len() != 3 {
611
+ return None;
612
+ }
613
+ // Days from the civil calendar (Hinnant).
614
+ let (y, m) = if month <= 2 {
615
+ (year - 1, month + 9)
616
+ } else {
617
+ (year, month - 3)
618
+ };
619
+ let era = y.div_euclid(400);
620
+ let yoe = y - era * 400;
621
+ let doe = yoe * 365 + yoe / 4 - yoe / 100 + (153 * m + 2) / 5 + day - 1;
622
+ let days = era * 146_097 + doe - 719_468;
623
+ u64::try_from(days * 86_400 + clock[0] * 3600 + clock[1] * 60 + clock[2]).ok()
624
+ }
625
+
555
626
  #[derive(Clone, Copy)]
556
627
  enum Framing {
557
628
  Length { remaining: u64 },
@@ -652,7 +723,7 @@ impl BodyReader<'_> {
652
723
  Err(e) if would_block(&e) => {
653
724
  transfer_verdict(self.judge, self.ledger, self.since, "a response body")?;
654
725
  let silent = self.advanced.elapsed();
655
- if self.judge == Judge::Stream && silent > self.ledger.patience() {
726
+ if self.judge.silent_is_stall() && silent > self.ledger.patience() {
656
727
  self.stalled = true;
657
728
  let _ = self
658
729
  .wire
@@ -243,7 +243,7 @@ fn complete_ranged_http_and_tls_bodies_reuse_one_connection() {
243
243
  }
244
244
 
245
245
  #[test]
246
- fn cached_peer_must_remain_checked_and_credentials_and_signed_target_stay_scoped() {
246
+ fn cached_peer_must_remain_checked_and_credentials_stay_scoped_but_signed_targets_share() {
247
247
  let origin = Origin::new(false, Duration::ZERO, |_, _| Reply::body(b"checked"));
248
248
  let client = origin.client();
249
249
  let checked = origin.checked("/object?signature=one");
@@ -260,8 +260,9 @@ fn cached_peer_must_remain_checked_and_credentials_and_signed_target_stay_scoped
260
260
  ask(&client, &checked, &headers).unwrap();
261
261
  headers[1].1 = "Bearer second".into();
262
262
  ask(&client, &checked, &headers).unwrap();
263
+ // A re-signed delivery URL on the same origin and credentials rides the same socket.
263
264
  ask(&client, &origin.checked("/object?signature=two"), &headers).unwrap();
264
- assert_eq!(origin.count(), 4);
265
+ assert_eq!(origin.count(), 3);
265
266
  let calls = origin.asks.lock().unwrap();
266
267
  assert_eq!(
267
268
  calls
@@ -357,12 +358,9 @@ fn idle_descriptor_retention_is_bounded_and_non_range_gets_do_not_pool() {
357
358
  let origin = Origin::new(false, Duration::ZERO, |_, _| Reply::body(b"bytes"));
358
359
  let client = origin.client();
359
360
  for index in 0..32 {
360
- ask(
361
- &client,
362
- &origin.checked(&format!("/object/{index}")),
363
- &ranged(),
364
- )
365
- .unwrap();
361
+ let mut headers = ranged();
362
+ headers.push(("authorization".into(), format!("Bearer {index}")));
363
+ ask(&client, &origin.checked("/object"), &headers).unwrap();
366
364
  }
367
365
  assert_eq!(client.idle.lock().unwrap().len(), MAX_IDLE);
368
366
  assert_eq!(origin.count(), 32);
@@ -583,21 +581,25 @@ fn retained_source_redirect_ranges_reduce_connections_without_changing_asks_or_b
583
581
  &Origin::policy(),
584
582
  credentials,
585
583
  Deadline::none(),
586
- &mut |_, _| {},
584
+ &|_, _| {},
587
585
  &SourceDownload {
588
586
  checkpoint_bytes: 1 << 20,
587
+ streams: 1,
589
588
  fault: Fault::default(),
589
+ ..Default::default()
590
590
  },
591
591
  )
592
592
  .unwrap();
593
593
  let elapsed = start.elapsed();
594
594
  assert_eq!(rows[0].transferred, bytes.len() as u64);
595
595
  assert!(store.record_valid(&object.sha256).is_ok());
596
- assert_eq!(origin.asks.lock().unwrap().len(), 16);
597
- assert_eq!(origin.count(), if close { 16 } else { 2 });
596
+ // One ask walks the redirect, one GET carries all eight checkpoints of the run,
597
+ // and the drained redirect's socket serves the delivery hop.
598
+ assert_eq!(origin.asks.lock().unwrap().len(), 2);
599
+ assert_eq!(origin.count(), if close { 2 } else { 1 });
598
600
  timings.push((close, elapsed.as_secs_f64(), origin.count()));
599
601
  drop(store);
600
602
  std::fs::remove_dir_all(root).unwrap();
601
603
  }
602
- println!("retained source 8MiB, eight 1MiB ranges, one redirect/range, 30ms connection setup: {timings:?}");
604
+ println!("retained source 8MiB, one redirected ask, eight 1MiB checkpoints, 30ms connection setup: {timings:?}");
603
605
  }
@@ -135,7 +135,7 @@ pub struct Still {
135
135
 
136
136
  /// One caller's cooperative stop signal. Clones name the same pull intent;
137
137
  /// a resumed operation uses a new token and retains existing Store bytes.
138
- #[derive(Clone, Default)]
138
+ #[derive(Clone, Debug, Default)]
139
139
  pub struct PullCancellation(Arc<AtomicBool>);
140
140
 
141
141
  impl PullCancellation {
@@ -357,6 +357,33 @@ impl Ledger {
357
357
  worst.mul_f64(STALL_FACTOR).max(self.patience())
358
358
  }
359
359
 
360
+ /// What a retryable answer costs before the re-ask: the origin's own stated wait,
361
+ /// which is not a failed attempt, or — when it states none — this pull's patience,
362
+ /// which is (`true`). The hub's rule, applied to every origin.
363
+ pub(crate) fn retry_wait(&self, stated: Option<Duration>) -> (Duration, bool) {
364
+ match stated.filter(|wait| !wait.is_zero()) {
365
+ Some(wait) => (wait, false),
366
+ None => (self.patience(), true),
367
+ }
368
+ }
369
+
370
+ /// Sleep `wait` in sampling ticks; cancellation and the caller's deadline end it.
371
+ pub(crate) fn pause(&self, wait: Duration, deadline: Deadline) -> Result<()> {
372
+ let until = Instant::now() + wait;
373
+ loop {
374
+ self.check_running()?;
375
+ let now = Instant::now();
376
+ if now >= until {
377
+ return Ok(());
378
+ }
379
+ let mut tick = (until - now).min(self.sample);
380
+ if let Some(left) = deadline.remaining()? {
381
+ tick = tick.min(left);
382
+ }
383
+ std::thread::sleep(tick);
384
+ }
385
+ }
386
+
360
387
  /// What this pull has moved and how long it has been still.
361
388
  pub fn still(&self) -> Still {
362
389
  let progress = self.progress.lock().unwrap();
@@ -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,
@@ -63,4 +65,4 @@ pub use pull::{
63
65
  pub use push::{push_object, Pushed, UploadGrant, MAX_GRANT_BYTES, PUT_ATTEMPTS};
64
66
  pub use ranged::{fetch_ranged, Ranged, PART_BYTES, RANGED_MIN_BYTES, SOURCE_STREAMS};
65
67
 
66
- pub use prefix::SourceDownload;
68
+ pub use prefix::{SourceDownload, SourceProgress};
@@ -145,6 +145,19 @@ pub struct CheckedUrl {
145
145
  pub addrs: Vec<SocketAddr>,
146
146
  }
147
147
 
148
+ impl CheckedUrl {
149
+ /// The URL this check admitted, as a string a later ask can start from.
150
+ pub fn url(&self) -> String {
151
+ let scheme = if self.https { "https" } else { "http" };
152
+ let host = if self.host.contains(':') {
153
+ format!("[{}]", self.host)
154
+ } else {
155
+ self.host.clone()
156
+ };
157
+ format!("{scheme}://{host}:{}{}", self.port, self.target)
158
+ }
159
+ }
160
+
148
161
  /// The deployment's declared egress bound. Carried by every caller that hands this module
149
162
  /// a URL; enforced on the first URL and on every redirect hop, because the hops are only
150
163
  /// visible here.