tensorfs 0.3.59__tar.gz → 0.3.61__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 (153) hide show
  1. {tensorfs-0.3.59 → tensorfs-0.3.61}/Cargo.lock +2 -2
  2. {tensorfs-0.3.59 → tensorfs-0.3.61}/Cargo.toml +1 -1
  3. {tensorfs-0.3.59 → tensorfs-0.3.61}/PKG-INFO +1 -1
  4. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/fetch.rs +32 -1
  5. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/main.rs +13 -3
  6. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/catalog.rs +183 -4
  7. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/checkpoint.rs +22 -0
  8. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/resume_tests.rs +23 -16
  9. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived.rs +45 -7
  10. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/durability.rs +17 -0
  11. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/gc.rs +5 -1
  12. tensorfs-0.3.61/crates/tensorfs-core/src/ingest/custody.rs +249 -0
  13. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/fingerprint.rs +24 -1
  14. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/journal.rs +13 -0
  15. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/mod.rs +1 -0
  16. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/source.rs +481 -104
  17. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/transaction.rs +38 -7
  18. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/stats.rs +21 -0
  19. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/storage.rs +20 -10
  20. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/store.rs +72 -41
  21. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/mod.rs +4 -1
  22. tensorfs-0.3.61/crates/tensorfs-core/src/transport/pace.rs +236 -0
  23. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/pull.rs +177 -100
  24. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/speculate.rs +11 -3
  25. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/tests.rs +254 -1
  26. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/write_through.rs +15 -14
  27. tensorfs-0.3.61/crates/tensorfs-py/src/ingest.rs +223 -0
  28. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-py/src/lib.rs +8 -0
  29. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-py/src/store.rs +19 -12
  30. {tensorfs-0.3.59 → tensorfs-0.3.61}/pyproject.toml +1 -1
  31. {tensorfs-0.3.59 → tensorfs-0.3.61}/python/tensorfs/__init__.py +8 -0
  32. {tensorfs-0.3.59 → tensorfs-0.3.61}/python/tensorfs/_ext.pyi +28 -0
  33. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/Cargo.toml +0 -0
  34. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/build.rs +0 -0
  35. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  36. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  37. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  38. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  39. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/asset_update.rs +0 -0
  40. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/b64.rs +0 -0
  41. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  42. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  43. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  44. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  45. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  46. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  47. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  48. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  49. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/canon.rs +0 -0
  50. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/capability.rs +0 -0
  51. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/cbor.rs +0 -0
  52. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  53. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/corpus.rs +0 -0
  54. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  55. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  56. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  57. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  58. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  59. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  60. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  61. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/dtype.rs +0 -0
  62. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/err.rs +0 -0
  63. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/fetch.rs +0 -0
  64. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/fill.rs +0 -0
  65. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/fit.rs +0 -0
  66. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/header.rs +0 -0
  67. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/home.rs +0 -0
  68. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/ids.rs +0 -0
  69. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  70. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  71. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  72. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  73. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  74. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  75. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/jcs.rs +0 -0
  76. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/lib.rs +0 -0
  77. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/limits.rs +0 -0
  78. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/machine.rs +0 -0
  79. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/manifest.rs +0 -0
  80. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/meta.rs +0 -0
  81. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/project.rs +0 -0
  82. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/providers.rs +0 -0
  83. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/read.rs +0 -0
  84. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/receipt.rs +0 -0
  85. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/reclaim.rs +0 -0
  86. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/registry.rs +0 -0
  87. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/relation.rs +0 -0
  88. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
  89. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  90. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/repository.rs +0 -0
  91. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/sha256.rs +0 -0
  92. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  93. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/spec.rs +0 -0
  94. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/staging.rs +0 -0
  95. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/api.rs +0 -0
  96. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/http.rs +0 -0
  97. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
  98. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  99. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  100. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/memory.rs +0 -0
  101. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  102. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  103. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/push.rs +0 -0
  104. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  105. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/src/vectors.rs +0 -0
  106. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  107. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  108. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  109. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  110. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fit.rs +0 -0
  111. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  112. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  113. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  114. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  115. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  116. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  117. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  118. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  119. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  120. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  121. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/home.rs +0 -0
  122. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  123. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  124. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/liveness.rs +0 -0
  125. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  126. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  127. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  128. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  129. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  130. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  131. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  132. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  133. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  134. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  135. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/source_download.rs +0 -0
  136. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  137. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  138. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  139. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/staging.rs +0 -0
  140. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  141. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  142. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  143. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  144. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-core/tests/version.rs +0 -0
  145. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-py/Cargo.toml +0 -0
  146. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-py/src/docs.rs +0 -0
  147. {tensorfs-0.3.59 → tensorfs-0.3.61}/crates/tensorfs-py/src/errors.rs +0 -0
  148. {tensorfs-0.3.59 → tensorfs-0.3.61}/python/tensorfs/_derived_channel.py +0 -0
  149. {tensorfs-0.3.59 → tensorfs-0.3.61}/python/tensorfs/_source_channel.py +0 -0
  150. {tensorfs-0.3.59 → tensorfs-0.3.61}/python/tensorfs/_tensors.py +0 -0
  151. {tensorfs-0.3.59 → tensorfs-0.3.61}/python/tensorfs/derived.py +0 -0
  152. {tensorfs-0.3.59 → tensorfs-0.3.61}/python/tensorfs/errors.py +0 -0
  153. {tensorfs-0.3.59 → tensorfs-0.3.61}/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.59"
442
+ version = "0.3.61"
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.59"
457
+ version = "0.3.61"
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.59"
6
+ version = "0.3.61"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.59
3
+ Version: 0.3.61
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -273,7 +273,8 @@ fn pull_with_budget(
273
273
  let mut request = PullRequest::new(&store, base, refspec, &credential, &policy);
274
274
  request.lane = flag(flags, "lane").unwrap_or("");
275
275
  request.session = flag(flags, "session").unwrap_or("");
276
- request.streams = crate::flag_num(flags, "streams", transport::STREAMS);
276
+ request.streams = crate::flag_num(flags, "streams", transport::PULL_STREAMS);
277
+ request.streams_start = crate::flag_num(flags, "streams-start", transport::STREAMS);
277
278
  request.deadline = deadline_of(flags);
278
279
  request.download_budget = budget;
279
280
  // THE WALK REPORTS WHILE IT WALKS.
@@ -300,7 +301,11 @@ fn pull_with_budget(
300
301
  let observe = |_object: &ObjectRef, moved: u64, door: ObjectSource| progress.saw(moved, door);
301
302
  request.on_plan = Some(&planned);
302
303
  request.on_object = Some(&observe);
304
+ let before = tensorfs_core::stats::snapshot();
305
+ let started = std::time::Instant::now();
303
306
  let report = ok!(transport::pull(&request));
307
+ let seconds = started.elapsed().as_secs_f64();
308
+ let after = tensorfs_core::stats::snapshot();
304
309
  progress.settle();
305
310
  if !tensorfs_core::repo_cache::finish_optional_backfills() {
306
311
  eprintln!("repo cache replication incomplete; local fetch remains complete");
@@ -313,6 +318,32 @@ fn pull_with_budget(
313
318
  quoted(&report.model), quoted(&report.manifest), quoted(&report.scope),
314
319
  report.cache_writes.bytes(), report.bytes_cached, report.bytes_total,
315
320
  );
321
+ // What each stream level bought, so a slow pull says whether more streams helped
322
+ // (one GET was the limit) or not (the link, disk or origin was).
323
+ let levels: Vec<String> = report
324
+ .levels
325
+ .iter()
326
+ .map(|level| {
327
+ format!(
328
+ "[{},{}]",
329
+ level.streams,
330
+ level.bytes_per_second.round() as u64
331
+ )
332
+ })
333
+ .collect();
334
+ // And what admitting them cost: a pull whose streams stop growing while its batches
335
+ // take most of the wall time is held by the disk, not the link.
336
+ eprintln!(
337
+ "{{\"event\":\"pull.streams\",\"model\":{},\"seconds\":{:.3},\"bytes_moved\":{},\"streams\":{},\"levels\":[{}],\"admission\":{{\"objects\":{},\"batches\":{},\"seconds\":{:.3}}}}}",
338
+ quoted(&report.model),
339
+ seconds,
340
+ report.bytes_moved,
341
+ report.streams,
342
+ levels.join(","),
343
+ after.admitted_objects - before.admitted_objects,
344
+ after.admission_batches - before.admission_batches,
345
+ (after.admission_nanos - before.admission_nanos) as f64 / 1e9,
346
+ );
316
347
  println!(
317
348
  "model {}@{} lane {}",
318
349
  report.model, report.release, report.lane
@@ -42,7 +42,9 @@
42
42
  //! The ONE transport (tfs-048/tfs-049/tfs-050). The credential is `--credential-file` or
43
43
  //! TFS_CREDENTIAL (`bearer <token>`, or empty for anonymous), never bare argv; a
44
44
  //! URL of `-` arrives on stdin. Flags: --hub <base> --lane --session --streams <n>
45
- //! --timeout <s> --allow-hosts a,b,.c --allow-local --max-redirects <n>.
45
+ //! --streams-start <n> --timeout <s> --allow-hosts a,b,.c --allow-local --max-redirects <n>.
46
+ //! `--streams` is the most objects in flight (default 128, memory-bounded); the walk starts
47
+ //! at `--streams-start` (default 16) and doubles while measured goodput follows.
46
48
  //! `tfs fetch` reads and backfills the Store's bound repo cache between the local Store
47
49
  //! and the origin. It is bound by `tfs store ensure --repo-cache` and never by a flag here.
48
50
  //!
@@ -505,7 +507,7 @@ fn cmd_repo_cache_restore(store: &Path, object: &str, length: &str, flags: &Flag
505
507
  /// The chain head names links that live on the cache; each is read at its exact digest, and
506
508
  /// the chain is DISCARDED WHOLE unless every link was written under the plan `--plan` names
507
509
  /// — the plan a resuming run re-derives from the source headers alone, for zero tensor
508
- /// bytes. That is what makes a stale chain harmless rather than dangerous.
510
+ /// bytes. A discarded chain restores nothing and succeeds: the run converts afresh.
509
511
  ///
510
512
  /// The objects come back exactly as a checkpoint's closure does. The conversion journal the
511
513
  /// newest link points at is installed beside them under `--session`, so the next
@@ -548,7 +550,15 @@ fn cmd_repo_cache_resume(store: &Path, object: &str, length: &str, flags: &Flags
548
550
  };
549
551
  let set = match durability::fold(&chain, plan) {
550
552
  Ok(set) => set,
551
- Err(error) => return bail(error),
553
+ Err(error) if error.code == tensorfs_core::err::Code::IO_FAILED => return bail(error),
554
+ Err(error) => {
555
+ println!(
556
+ "chain DISCARDED {}: {} — nothing restored; the conversion starts a new chain",
557
+ error.code.as_str(),
558
+ error.detail
559
+ );
560
+ return ExitCode::SUCCESS;
561
+ }
552
562
  };
553
563
  println!(
554
564
  "chain {} link(s) over operation {} — {} B declared durable",
@@ -903,8 +903,9 @@ impl Catalog {
903
903
  hold_on(&self.connection()?, operation, key, length)
904
904
  }
905
905
 
906
- /// The object's existing hold and verification become durable together. Filesystem
907
- /// publication still precedes its verification record, with FULL synchronization.
906
+ /// A HELD admission: the hold is written before the object is published, so an
907
+ /// operation that cannot hold it publishes nothing. Unheld admissions, which is every
908
+ /// pull, take `record_grouped` instead.
908
909
  pub(crate) fn admit_blob(
909
910
  &self,
910
911
  operation: Option<&str>,
@@ -914,7 +915,7 @@ impl Catalog {
914
915
  // One admission at a time per catalog in this process, holding the write lock from
915
916
  // before publication: streams landing objects together queue here instead of in
916
917
  // SQLite's busy timeout, where a lost race refused bytes already verified.
917
- let turn = admissions(&self.path);
918
+ let turn = held_admissions(&self.path);
918
919
  let _turn = turn.lock().unwrap_or_else(|error| error.into_inner());
919
920
  let mut connection = self.connection()?;
920
921
  let transaction = connection
@@ -941,6 +942,117 @@ impl Catalog {
941
942
  Ok(verified.is_some())
942
943
  }
943
944
 
945
+ /// The object's existing hold and verification become durable together. Filesystem
946
+ /// publication has already happened, on the caller's thread, and precedes this record,
947
+ /// which is written with FULL synchronization.
948
+ ///
949
+ /// Records queue per catalog in this process, and whichever arrives while no batch is
950
+ /// committing leads the next one: one immediate transaction and one commit make every
951
+ /// waiting record durable. One object alone costs what it always did. Before this,
952
+ /// directory creation, link, directory fsync and commit ran for one object at a time
953
+ /// under one lock, several fsyncs each, and on a disk busy flushing those same objects'
954
+ /// bodies that queue, not the network, set a pull's rate.
955
+ pub(crate) fn record_grouped(&self, recording: Recording) -> Result<()> {
956
+ let group = admission_group(&self.path);
957
+ let mut state = group
958
+ .state
959
+ .lock()
960
+ .unwrap_or_else(|error| error.into_inner());
961
+ let ticket = state.next;
962
+ state.next += 1;
963
+ state.queue.push((ticket, recording));
964
+ loop {
965
+ if let Some(result) = state.done.remove(&ticket) {
966
+ return result;
967
+ }
968
+ if state.leading {
969
+ state = group
970
+ .turned
971
+ .wait(state)
972
+ .unwrap_or_else(|error| error.into_inner());
973
+ continue;
974
+ }
975
+ state.leading = true;
976
+ let batch = std::mem::take(&mut state.queue);
977
+ let mut lead = Lead {
978
+ group: &group,
979
+ tickets: batch.iter().map(|(ticket, _)| *ticket).collect(),
980
+ results: Vec::new(),
981
+ };
982
+ drop(state);
983
+ let started = std::time::Instant::now();
984
+ let objects = batch.len();
985
+ lead.results = self.record_batch(batch);
986
+ crate::stats::admission_batch(objects, started.elapsed());
987
+ drop(lead);
988
+ state = group
989
+ .state
990
+ .lock()
991
+ .unwrap_or_else(|error| error.into_inner());
992
+ }
993
+ }
994
+
995
+ fn record_batch(&self, batch: Vec<(u64, Recording)>) -> Vec<(u64, Result<()>)> {
996
+ let fail_all = |batch: Vec<(u64, Recording)>, error: Refusal| {
997
+ batch
998
+ .into_iter()
999
+ .map(|(ticket, _)| (ticket, Err(error.clone())))
1000
+ .collect::<Vec<_>>()
1001
+ };
1002
+ let mut connection = match self.connection() {
1003
+ Ok(connection) => connection,
1004
+ Err(error) => return fail_all(batch, error),
1005
+ };
1006
+ let mut transaction =
1007
+ match connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate) {
1008
+ Ok(transaction) => transaction,
1009
+ Err(error) => return fail_all(batch, sql("begin blob admission", error)),
1010
+ };
1011
+ let mut results = Vec::with_capacity(batch.len());
1012
+ let mut written = Vec::with_capacity(batch.len());
1013
+ // Each object's row sits behind its own savepoint: one refused row refuses that
1014
+ // object, as it did when every object had a transaction of its own.
1015
+ for (ticket, recording) in batch {
1016
+ let outcome = transaction
1017
+ .savepoint()
1018
+ .map_err(|error| sql("begin object admission", error))
1019
+ .and_then(|savepoint| {
1020
+ if let Some(row) = &recording.record {
1021
+ write_verification_on(&savepoint, row)?;
1022
+ }
1023
+ savepoint
1024
+ .commit()
1025
+ .map_err(|error| sql("record object admission", error))
1026
+ });
1027
+ match outcome {
1028
+ Ok(()) => written.push((ticket, recording.record)),
1029
+ Err(error) => results.push((ticket, Err(error))),
1030
+ }
1031
+ }
1032
+ match transaction
1033
+ .commit()
1034
+ .map_err(|error| sql("commit blob admission", error))
1035
+ {
1036
+ Ok(()) => {
1037
+ let trust = Trust::live(&self.path);
1038
+ for (ticket, record) in written {
1039
+ if let (Some(row), Some(trust)) = (&record, &trust) {
1040
+ trust.remember(row);
1041
+ }
1042
+ results.push((ticket, Ok(())));
1043
+ }
1044
+ }
1045
+ Err(error) => {
1046
+ results.extend(
1047
+ written
1048
+ .into_iter()
1049
+ .map(|(ticket, _)| (ticket, Err(error.clone()))),
1050
+ );
1051
+ }
1052
+ }
1053
+ results
1054
+ }
1055
+
944
1056
  pub fn held_keys(&self) -> Result<Vec<crate::storage::HeldKey>> {
945
1057
  let connection = self.connection()?;
946
1058
  let mut statement = connection
@@ -1084,7 +1196,7 @@ impl Catalog {
1084
1196
  }
1085
1197
  }
1086
1198
 
1087
- fn admissions(path: &Path) -> Arc<Mutex<()>> {
1199
+ fn held_admissions(path: &Path) -> Arc<Mutex<()>> {
1088
1200
  static TURNS: OnceLock<Mutex<HashMap<PathBuf, Arc<Mutex<()>>>>> = OnceLock::new();
1089
1201
  TURNS
1090
1202
  .get_or_init(Default::default)
@@ -1095,6 +1207,73 @@ fn admissions(path: &Path) -> Arc<Mutex<()>> {
1095
1207
  .clone()
1096
1208
  }
1097
1209
 
1210
+ /// One published object's catalog rows, handed to the admission group.
1211
+ pub(crate) struct Recording {
1212
+ /// The verification row, when this call installed the object.
1213
+ pub record: Option<crate::store::VRecord>,
1214
+ }
1215
+
1216
+ #[derive(Default)]
1217
+ struct AdmissionGroup {
1218
+ state: Mutex<GroupState>,
1219
+ turned: std::sync::Condvar,
1220
+ }
1221
+
1222
+ #[derive(Default)]
1223
+ struct GroupState {
1224
+ next: u64,
1225
+ leading: bool,
1226
+ queue: Vec<(u64, Recording)>,
1227
+ done: HashMap<u64, Result<()>>,
1228
+ }
1229
+
1230
+ fn admission_group(path: &Path) -> Arc<AdmissionGroup> {
1231
+ static GROUPS: OnceLock<Mutex<HashMap<PathBuf, Arc<AdmissionGroup>>>> = OnceLock::new();
1232
+ GROUPS
1233
+ .get_or_init(Default::default)
1234
+ .lock()
1235
+ .unwrap_or_else(|error| error.into_inner())
1236
+ .entry(path.to_path_buf())
1237
+ .or_default()
1238
+ .clone()
1239
+ }
1240
+
1241
+ /// A leader hands its batch's results back however it leaves, so a panic inside one batch
1242
+ /// cannot strand the admissions queued behind it.
1243
+ struct Lead<'a> {
1244
+ group: &'a AdmissionGroup,
1245
+ tickets: Vec<u64>,
1246
+ results: Vec<(u64, Result<()>)>,
1247
+ }
1248
+
1249
+ impl Drop for Lead<'_> {
1250
+ fn drop(&mut self) {
1251
+ let mut state = self
1252
+ .group
1253
+ .state
1254
+ .lock()
1255
+ .unwrap_or_else(|error| error.into_inner());
1256
+ state.leading = false;
1257
+ let mut answered = std::collections::HashSet::new();
1258
+ for (ticket, result) in self.results.drain(..) {
1259
+ answered.insert(ticket);
1260
+ state.done.insert(ticket, result);
1261
+ }
1262
+ for ticket in &self.tickets {
1263
+ if !answered.contains(ticket) {
1264
+ state.done.insert(
1265
+ *ticket,
1266
+ Err(Refusal {
1267
+ code: Code::IO_FAILED,
1268
+ detail: "the admission batch holding this object did not finish".into(),
1269
+ }),
1270
+ );
1271
+ }
1272
+ }
1273
+ self.group.turned.notify_all();
1274
+ }
1275
+ }
1276
+
1098
1277
  fn hold_on(connection: &Connection, operation: &str, key: &str, length: u64) -> Result<()> {
1099
1278
  let kind = if key.starts_with("blobs/") {
1100
1279
  "blob"
@@ -418,6 +418,28 @@ impl Walk {
418
418
  }
419
419
  Ok(())
420
420
  }
421
+
422
+ /// Every reached object present, or held by a live source operation's publication
423
+ /// custodian. A complete checkpoint for publication, never a readable one.
424
+ pub fn require_held(
425
+ &self,
426
+ store: &Store,
427
+ custodied: &std::collections::BTreeSet<String>,
428
+ ) -> Result<()> {
429
+ for r in self.distinct() {
430
+ if !store.contains(&r.sha256) && !custodied.contains(&r.sha256) {
431
+ return refuse(
432
+ Code::OBJECT_ABSENT,
433
+ format!(
434
+ "{} is committed by this manifest but neither resident nor held by \
435
+ a live publication custodian",
436
+ r.id()
437
+ ),
438
+ );
439
+ }
440
+ }
441
+ Ok(())
442
+ }
421
443
  }
422
444
 
423
445
  /// Manifest → CozyTensorsHeader → contained tensor rules → storage-part object runs.
@@ -351,7 +351,7 @@ fn missing_corrupt_and_changed_work_cannot_be_reused() {
351
351
  }
352
352
 
353
353
  #[test]
354
- fn recovery_refuses_old_writer_authority_and_malformed_part_geometry() {
354
+ fn recovery_discards_old_writer_authority_and_malformed_part_geometry() {
355
355
  let (store, meta, id, declaration) = fixture("authority");
356
356
  let writer = begin(&store, &meta, &id, 9, declaration.clone(), None).unwrap();
357
357
  data(&store, &meta, &id, 9, 0x31);
@@ -359,7 +359,8 @@ fn recovery_refuses_old_writer_authority_and_malformed_part_geometry() {
359
359
  let plan = progress_plan(&row);
360
360
  let mut geometry = progress_row(&row);
361
361
  geometry.added_parts[0].part.shape = vec![16, 31];
362
- for snapshot in [row, geometry] {
362
+ let mut writers = vec![writer];
363
+ for (epoch, snapshot) in [(10, row), (11, geometry)] {
363
364
  let bytes = crate::canon::write(&snapshot.to_value());
364
365
  let head = crate::durability::checkpoint_bytes(
365
366
  &store,
@@ -378,11 +379,20 @@ fn recovery_refuses_old_writer_authority_and_malformed_part_geometry() {
378
379
  .head
379
380
  .head
380
381
  .unwrap();
381
- assert!(begin(&store, &meta, &id, 10, declaration.clone(), Some(&head)).is_err());
382
- check_writer(&meta, &id, 9).unwrap();
382
+ // The checkpoint cannot be continued: it is discarded, nothing of it is imported,
383
+ // and the new writer proceeds from the work this Store already holds.
384
+ writers.push(begin(&store, &meta, &id, epoch, declaration.clone(), Some(&head)).unwrap());
385
+ check_writer(&meta, &id, epoch).unwrap();
386
+ let current = meta.derived_rows().unwrap().remove(0);
387
+ assert!(current
388
+ .added_parts
389
+ .iter()
390
+ .all(|added| added.part.shape != vec![16, 31]));
391
+ }
392
+ fence(&meta, &id, 11).unwrap();
393
+ for writer in writers {
394
+ release(&meta, writer);
383
395
  }
384
- fence(&meta, &id, 9).unwrap();
385
- release(&meta, writer);
386
396
  abandon(&store, &meta, &id).unwrap();
387
397
  std::fs::remove_dir_all(store.root()).unwrap();
388
398
  }
@@ -489,7 +499,7 @@ fn source_identity_is_locked_and_source_closure_stays_rooted_after_writer_death(
489
499
  }
490
500
 
491
501
  #[test]
492
- fn wrong_document_lengths_refuse_before_parsing_or_importing_work() {
502
+ fn wrong_document_lengths_refuse_to_validate_and_are_discarded_on_resume() {
493
503
  let (store, meta, id, declaration) = fixture("lengths");
494
504
  let writer = begin(&store, &meta, &id, 1, declaration.clone(), None).unwrap();
495
505
  data(&store, &meta, &id, 1, 0x31);
@@ -528,16 +538,13 @@ fn wrong_document_lengths_refuse_before_parsing_or_importing_work() {
528
538
  .code,
529
539
  Code::LENGTH_MISMATCH
530
540
  );
531
- assert_eq!(
532
- begin(&store, &meta, &id, 2, declaration, Some(&forged))
533
- .err()
534
- .unwrap()
535
- .code,
536
- Code::LENGTH_MISMATCH
537
- );
538
- check_writer(&meta, &id, 1).unwrap();
539
- fence(&meta, &id, 1).unwrap();
541
+ // A forged checkpoint is discarded, not imported; this begin then meets the live writer.
542
+ // Resuming from it discards it instead of refusing: the next writer proceeds.
543
+ let next = begin(&store, &meta, &id, 2, declaration, Some(&forged)).unwrap();
544
+ check_writer(&meta, &id, 2).unwrap();
545
+ fence(&meta, &id, 2).unwrap();
540
546
  release(&meta, writer);
547
+ release(&meta, next);
541
548
  abandon(&store, &meta, &id).unwrap();
542
549
  std::fs::remove_dir_all(store.root()).unwrap();
543
550
  }
@@ -1601,6 +1601,9 @@ fn require_source_roots(store: &Store, declaration: &Declaration) -> Result<()>
1601
1601
  }
1602
1602
 
1603
1603
  fn load_sources(store: &Store, declaration: &Declaration) -> Result<Vec<SourceData>> {
1604
+ // A prepared model whose output a live source operation's publication custodian holds
1605
+ // derives by reference without its bytes; the result is publishable, never readable.
1606
+ let custodied = crate::ingest::custody::live(store.root())?;
1604
1607
  let mut out = Vec::new();
1605
1608
  for declared in &declaration.sources {
1606
1609
  let manifest = checkpoint::load_manifest(store, &declared.manifest)?;
@@ -1625,11 +1628,13 @@ fn load_sources(store: &Store, declaration: &Declaration) -> Result<Vec<SourceDa
1625
1628
  let closure = checkpoint::load_closure(store, &header)?;
1626
1629
  header.validate(&closure)?;
1627
1630
  let walk = checkpoint::walk_cozytensors(store, &manifest)?;
1628
- walk.require_resident(store)?;
1631
+ walk.require_held(store, &custodied)?;
1629
1632
  // This is the zero-read/zero-hash inherit gate: the verification record proves the
1630
1633
  // exact installed inode/length/generation without opening or hashing payload bytes.
1631
1634
  for reached in &walk.objects {
1632
- if reached.kind == "part" || reached.kind == "config" {
1635
+ if (reached.kind == "part" || reached.kind == "config")
1636
+ && !custodied.contains(&reached.obj.sha256)
1637
+ {
1633
1638
  require_payload_record(store, &reached.obj, reached.kind)?;
1634
1639
  }
1635
1640
  }
@@ -2308,9 +2313,35 @@ pub fn begin(
2308
2313
  release_guards(meta, Some(writer_hold), Vec::new());
2309
2314
  return Err(error);
2310
2315
  }
2316
+ // Pin every resident source object. A custodied object has no local bytes to pin: a
2317
+ // derivation over it carries it by reference and never reads it.
2318
+ let custodied = match crate::ingest::custody::live(store.root()) {
2319
+ Ok(custodied) => custodied,
2320
+ Err(error) => {
2321
+ release_guards(meta, Some(writer_hold), Vec::new());
2322
+ return Err(error);
2323
+ }
2324
+ };
2311
2325
  let mut source_leases = Vec::new();
2312
2326
  for source in &sources {
2313
- match read::acquire_cozytensors(store, meta, &source.manifest) {
2327
+ let pinned = if custodied.is_empty() {
2328
+ read::acquire_cozytensors(store, meta, &source.manifest)
2329
+ } else {
2330
+ checkpoint::walk_cozytensors(store, &source.manifest).and_then(|walk| {
2331
+ let id = source.manifest.manifest_id();
2332
+ read::acquire(
2333
+ store,
2334
+ meta,
2335
+ id.strip_prefix("sha256:").unwrap_or(&id),
2336
+ walk.distinct()
2337
+ .into_iter()
2338
+ .filter(|object| !custodied.contains(&object.sha256))
2339
+ .cloned()
2340
+ .collect(),
2341
+ )
2342
+ })
2343
+ };
2344
+ match pinned {
2314
2345
  Ok((lease, receipts)) if receipts.0.is_empty() => {
2315
2346
  source_leases.push((source.fact.alias.clone(), lease));
2316
2347
  }
@@ -2342,15 +2373,22 @@ pub fn begin(
2342
2373
  })
2343
2374
  .collect();
2344
2375
 
2376
+ // A checkpoint this work cannot continue (another plan or work context, or no longer
2377
+ // whole) is discarded: the work re-derives what it lost instead of refusing.
2378
+ let mut checkpoint = checkpoint;
2345
2379
  let incoming = match checkpoint
2346
2380
  .map(|head| read_progress(store, head, &transaction, &declaration_text, &source_facts))
2347
- .transpose()
2348
2381
  {
2349
- Ok(incoming) => incoming,
2350
- Err(error) => {
2382
+ None => None,
2383
+ Some(Ok(row)) => Some(row),
2384
+ Some(Err(error)) if error.code == Code::IO_FAILED => {
2351
2385
  release_guards(meta, Some(writer_hold), source_leases);
2352
2386
  return Err(error);
2353
2387
  }
2388
+ Some(Err(_)) => {
2389
+ checkpoint = None;
2390
+ None
2391
+ }
2354
2392
  };
2355
2393
  // Byte verification can update the verification catalog. Keep it outside the SQL
2356
2394
  // writer transaction, then compare the observed row before assigning this epoch.
@@ -3269,7 +3307,7 @@ pub fn commit(
3269
3307
  let manifest_ref = store.put_manifest(&manifest)?.obj;
3270
3308
  store.read_manifest(&manifest_ref)?;
3271
3309
  let walk = checkpoint::walk(store, &manifest)?;
3272
- walk.require_resident(store)?;
3310
+ walk.require_held(store, &crate::ingest::custody::live(store.root())?)?;
3273
3311
  let (inherited_payload_objects, inherited_payload_bytes) =
3274
3312
  inherit_observation(&header, &row_before)?;
3275
3313
 
@@ -289,6 +289,23 @@ pub fn local_chain(store: &Store, head: &ObjectRef, plan: &str) -> Result<Set> {
289
289
  fold(&links, plan)
290
290
  }
291
291
 
292
+ /// A supplied chain this conversion can continue, or `None` when it was written under another
293
+ /// plan or slot or is no longer whole in this Store. Such a chain is discarded and the work
294
+ /// reconverts into a new chain: losing a checkpoint costs time, never a refusal.
295
+ pub fn continuable(
296
+ store: &Store,
297
+ head: &ObjectRef,
298
+ plan: &str,
299
+ chain: &str,
300
+ ) -> Result<Option<Set>> {
301
+ match local_chain(store, head, plan) {
302
+ Ok(set) if set.operation == chain => Ok(Some(set)),
303
+ Ok(_) => Ok(None),
304
+ Err(error) if error.code == Code::IO_FAILED => Err(error),
305
+ Err(_) => Ok(None),
306
+ }
307
+ }
308
+
292
309
  /// Export locally complete progress. This does not assert remote durability: the RecordOwner
293
310
  /// publishes the bounded chain and advances its durable pointer only after upload acceptance.
294
311
  /// NFS is not consulted and cannot delay this checkpoint.
@@ -108,8 +108,12 @@ pub fn session_holds(store: &Store) -> Result<(Vec<HeldKey>, Vec<String>)> {
108
108
  let mut sessions = Vec::new();
109
109
  for session in tx::sessions(store) {
110
110
  let root = tx::read_session(store.root(), &session)?;
111
- let journalled =
111
+ // An object the operation's publication custodian already holds is not in flight:
112
+ // the journal stops naming it, so the local copy is ordinary unreferenced bytes.
113
+ let custody = crate::ingest::custody::read_session(store.root(), &session)?;
114
+ let mut journalled =
112
115
  crate::ingest::journal::session_objects(&tx::candidate_dir(store.root(), &session))?;
116
+ journalled.retain(|object| !custody.contains(object));
113
117
  if root.candidates.is_empty() && journalled.is_empty() {
114
118
  continue;
115
119
  }