tensorfs 0.3.46__tar.gz → 0.3.48__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 (138) hide show
  1. {tensorfs-0.3.46 → tensorfs-0.3.48}/Cargo.lock +2 -2
  2. {tensorfs-0.3.46 → tensorfs-0.3.48}/Cargo.toml +1 -1
  3. {tensorfs-0.3.46 → tensorfs-0.3.48}/PKG-INFO +1 -1
  4. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/Cargo.toml +1 -1
  5. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/fetch.rs +1 -1
  6. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/repo_cache.rs +371 -41
  7. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/http.rs +100 -17
  8. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/ledger.rs +94 -8
  9. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/mod.rs +1 -0
  10. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/pull.rs +271 -30
  11. tensorfs-0.3.48/crates/tensorfs-core/src/transport/speculate.rs +395 -0
  12. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/tests.rs +1119 -2
  13. {tensorfs-0.3.46 → tensorfs-0.3.48}/pyproject.toml +1 -1
  14. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/build.rs +0 -0
  15. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  16. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  17. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  18. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  19. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/asset_update.rs +0 -0
  20. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/b64.rs +0 -0
  21. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  22. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  23. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  24. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  25. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  26. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  27. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  28. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  29. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  30. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/canon.rs +0 -0
  31. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/capability.rs +0 -0
  32. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/catalog.rs +0 -0
  33. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/cbor.rs +0 -0
  34. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  35. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  36. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/corpus.rs +0 -0
  37. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  38. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  39. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  40. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  41. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  42. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  43. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  44. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived.rs +0 -0
  45. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/dtype.rs +0 -0
  46. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/durability.rs +0 -0
  47. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/err.rs +0 -0
  48. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/fetch.rs +0 -0
  49. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/fill.rs +0 -0
  50. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/fit.rs +0 -0
  51. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/gc.rs +0 -0
  52. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/header.rs +0 -0
  53. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/home.rs +0 -0
  54. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ids.rs +0 -0
  55. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  56. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  57. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  58. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  59. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  60. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  61. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  62. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  63. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  64. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  65. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  66. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/jcs.rs +0 -0
  67. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/lib.rs +0 -0
  68. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/limits.rs +0 -0
  69. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/machine.rs +0 -0
  70. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/manifest.rs +0 -0
  71. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/meta.rs +0 -0
  72. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/project.rs +0 -0
  73. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/providers.rs +0 -0
  74. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/read.rs +0 -0
  75. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/receipt.rs +0 -0
  76. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/reclaim.rs +0 -0
  77. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/registry.rs +0 -0
  78. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/relation.rs +0 -0
  79. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/repository.rs +0 -0
  80. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/sha256.rs +0 -0
  81. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  82. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/spec.rs +0 -0
  83. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/staging.rs +0 -0
  84. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/storage.rs +0 -0
  85. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/store.rs +0 -0
  86. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/api.rs +0 -0
  87. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  88. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  89. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  90. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/push.rs +0 -0
  91. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  92. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/vectors.rs +0 -0
  93. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  94. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  95. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fit.rs +0 -0
  96. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  97. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  98. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  99. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  100. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  101. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  102. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  103. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  104. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  105. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/home.rs +0 -0
  106. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  107. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  108. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/liveness.rs +0 -0
  109. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  110. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  111. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  112. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  113. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  114. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  115. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  116. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  117. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  118. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  119. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  120. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  121. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/staging.rs +0 -0
  122. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  123. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  124. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/version.rs +0 -0
  125. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/write_through.rs +0 -0
  126. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-py/Cargo.toml +0 -0
  127. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-py/src/docs.rs +0 -0
  128. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-py/src/errors.rs +0 -0
  129. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-py/src/lib.rs +0 -0
  130. {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-py/src/store.rs +0 -0
  131. {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/__init__.py +0 -0
  132. {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/_derived_channel.py +0 -0
  133. {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/_ext.pyi +0 -0
  134. {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/_source_channel.py +0 -0
  135. {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/_tensors.py +0 -0
  136. {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/derived.py +0 -0
  137. {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/errors.py +0 -0
  138. {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/py.typed +0 -0
@@ -421,7 +421,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
421
421
 
422
422
  [[package]]
423
423
  name = "tensorfs-core"
424
- version = "0.3.46"
424
+ version = "0.3.48"
425
425
  dependencies = [
426
426
  "fs2",
427
427
  "libc",
@@ -435,7 +435,7 @@ dependencies = [
435
435
 
436
436
  [[package]]
437
437
  name = "tensorfs-py"
438
- version = "0.3.46"
438
+ version = "0.3.48"
439
439
  dependencies = [
440
440
  "pyo3",
441
441
  "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.46"
6
+ version = "0.3.48"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.46
3
+ Version: 0.3.48
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -11,7 +11,7 @@ path = "src/bin/tfs/main.rs"
11
11
  fs2 = "0.4"
12
12
  minicbor = { version = "2.3.0", default-features = false, features = ["alloc"] }
13
13
  rusqlite = { version = "0.32", features = ["bundled"] }
14
- rustix = { version = "1.1.4", default-features = false, features = ["fs", "std"] }
14
+ rustix = { version = "1.1.4", default-features = false, features = ["fs", "std", "net", "event"] }
15
15
  # tfs-049: the ONE transport (src/transport/). rustls is the one TLS stack, used only
16
16
  # there; scripts/fence.py refuses it anywhere else so a second mover cannot grow.
17
17
  rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
@@ -303,7 +303,7 @@ fn pull_with_budget(
303
303
  let report = ok!(transport::pull(&request));
304
304
  progress.settle();
305
305
  if !tensorfs_core::repo_cache::finish_optional_backfills() {
306
- eprintln!("repo cache backfill stopped advancing; local fetch remains complete");
306
+ eprintln!("repo cache replication incomplete; local fetch remains complete");
307
307
  }
308
308
  println!(
309
309
  "model {}@{} lane {}",
@@ -102,32 +102,6 @@ impl RepoObjectCache {
102
102
  &self.root
103
103
  }
104
104
 
105
- /// Offer optional replication after local acquisition. This reports no durability
106
- /// and never waits for filesystem I/O or queue space. Ingest's explicit Mirror
107
- /// retains its separate completion/watermark contract.
108
- pub(crate) fn offer_backfill(
109
- &self,
110
- source: &Store,
111
- kind: CacheKind,
112
- object: &ObjectRef,
113
- ) -> bool {
114
- let pool = BACKFILLS.get_or_init(Backfills::start);
115
- pool.activity.offered();
116
- let sent = pool
117
- .sender
118
- .try_send(Backfill {
119
- cache: self.clone(),
120
- source: source.root().to_path_buf(),
121
- kind,
122
- object: object.clone(),
123
- })
124
- .is_ok();
125
- if !sent {
126
- pool.activity.finished();
127
- }
128
- sent
129
- }
130
-
131
105
  /// The one cross-language layout contract. `digest` is bare lowercase SHA-256.
132
106
  pub fn path(&self, kind: CacheKind, digest: &str) -> Result<PathBuf> {
133
107
  let digest = hex64("repo cache object", digest)?;
@@ -296,6 +270,94 @@ impl RepoObjectCache {
296
270
  }
297
271
  }
298
272
 
273
+ /// One pull retains only references rejected by the bounded optional write queue.
274
+ /// Its maximum is the already-declared closure; no payload buffer or persistent
275
+ /// queue is added, and origin workers never wait for cache I/O or queue space.
276
+ pub(crate) struct BackfillBatch<'a> {
277
+ cache: RepoObjectCache,
278
+ source: PathBuf,
279
+ limit: usize,
280
+ missed: std::sync::Mutex<Vec<(CacheKind, ObjectRef)>>,
281
+ pool: &'a Backfills,
282
+ }
283
+
284
+ impl BackfillBatch<'static> {
285
+ pub(crate) fn new(cache: RepoObjectCache, source: &Store, limit: usize) -> Self {
286
+ Self::with_pool(
287
+ cache,
288
+ source,
289
+ limit,
290
+ BACKFILLS.get_or_init(Backfills::start),
291
+ )
292
+ }
293
+ }
294
+
295
+ impl<'a> BackfillBatch<'a> {
296
+ fn with_pool(
297
+ cache: RepoObjectCache,
298
+ source: &Store,
299
+ limit: usize,
300
+ pool: &'a Backfills,
301
+ ) -> Self {
302
+ Self {
303
+ cache,
304
+ source: source.root().to_path_buf(),
305
+ limit,
306
+ missed: std::sync::Mutex::new(Vec::new()),
307
+ pool,
308
+ }
309
+ }
310
+
311
+ fn work(&self, kind: CacheKind, object: &ObjectRef) -> Backfill {
312
+ Backfill {
313
+ cache: self.cache.clone(),
314
+ source: self.source.clone(),
315
+ kind,
316
+ object: object.clone(),
317
+ }
318
+ }
319
+
320
+ pub(crate) fn offer(&self, kind: CacheKind, object: &ObjectRef) {
321
+ if !self.pool.enqueue(self.work(kind, object), false) {
322
+ let mut missed = self.missed.lock().unwrap();
323
+ if missed.len() < self.limit {
324
+ missed.push((kind, object.clone()));
325
+ } else {
326
+ self.pool
327
+ .activity
328
+ .incomplete
329
+ .store(true, std::sync::atomic::Ordering::Release);
330
+ }
331
+ }
332
+ }
333
+
334
+ /// After local acquisition, feed deferred references while cache copies make
335
+ /// real progress. A stalled cache abandons replication only, never the local
336
+ /// result or its origin workers. The CLI's existing drain reports completion.
337
+ pub(crate) fn finish_offers(&self) -> bool {
338
+ let missed = std::mem::take(&mut *self.missed.lock().unwrap());
339
+ for (kind, object) in missed {
340
+ if !self.pool.enqueue(self.work(kind, &object), true) {
341
+ return false;
342
+ }
343
+ }
344
+ true
345
+ }
346
+ }
347
+
348
+ impl Drop for BackfillBatch<'_> {
349
+ fn drop(&mut self) {
350
+ // A failed pull returns without waiting for optional cache I/O. Its
351
+ // rejected references must still make the later drain report incomplete.
352
+ if !self.missed.get_mut().unwrap().is_empty() {
353
+ self.pool
354
+ .activity
355
+ .incomplete
356
+ .store(true, std::sync::atomic::Ordering::Release);
357
+ }
358
+ }
359
+ }
360
+
299
361
  // Reference-only memory policy for optional acquisition backfill, not a protocol bound.
300
362
  // Typical jobs are a few hundred bytes; even two 4 KiB paths per job stay near 1 MiB.
301
363
  const BACKFILL_QUEUE: usize = 128;
@@ -317,6 +379,7 @@ struct Backfills {
317
379
  struct BackfillActivity {
318
380
  pending: std::sync::Mutex<(usize, Ledger)>,
319
381
  changed: std::sync::Condvar,
382
+ incomplete: std::sync::atomic::AtomicBool,
320
383
  }
321
384
 
322
385
  impl BackfillActivity {
@@ -324,9 +387,11 @@ impl BackfillActivity {
324
387
  Self {
325
388
  pending: std::sync::Mutex::new((0, ledger)),
326
389
  changed: std::sync::Condvar::new(),
390
+ incomplete: std::sync::atomic::AtomicBool::new(false),
327
391
  }
328
392
  }
329
393
 
394
+ #[cfg(test)]
330
395
  fn offered(&self) {
331
396
  let mut state = self
332
397
  .pending
@@ -338,6 +403,14 @@ impl BackfillActivity {
338
403
  state.0 += 1;
339
404
  }
340
405
 
406
+ fn space_available(&self) {
407
+ let _state = self
408
+ .pending
409
+ .lock()
410
+ .unwrap_or_else(|error| error.into_inner());
411
+ self.changed.notify_all();
412
+ }
413
+
341
414
  fn finished(&self) {
342
415
  let mut state = self
343
416
  .pending
@@ -384,12 +457,65 @@ impl store::Progress for BackfillActivity {
384
457
  /// Before a short-lived CLI exits, give its existing optional write pool time to
385
458
  /// finish while real copy/verification progress continues. A stalled cache only
386
459
  /// ends this wait; it never changes the completed local fetch or joins its worker.
387
- /// Long-lived Store callers keep the original asynchronous behavior.
460
+ /// Write failures and abandoned deferred work also return false, so a drained
461
+ /// queue is not mistaken for successful replication. Long-lived Store callers
462
+ /// keep the original asynchronous behavior.
388
463
  pub fn finish_optional_backfills() -> bool {
389
- BACKFILLS.get().is_none_or(|pool| pool.activity.wait())
464
+ BACKFILLS.get().is_none_or(|pool| {
465
+ let drained = pool.activity.wait();
466
+ let incomplete = pool
467
+ .activity
468
+ .incomplete
469
+ .swap(false, std::sync::atomic::Ordering::AcqRel);
470
+ drained && !incomplete
471
+ })
390
472
  }
391
473
 
392
474
  impl Backfills {
475
+ fn enqueue(&self, mut work: Backfill, wait: bool) -> bool {
476
+ let mut state = self
477
+ .activity
478
+ .pending
479
+ .lock()
480
+ .unwrap_or_else(|error| error.into_inner());
481
+ loop {
482
+ if state.0 == 0 {
483
+ state.1 = Ledger::with_resolution(state.1.sample());
484
+ }
485
+ state.0 += 1;
486
+ match self.sender.try_send(work) {
487
+ Ok(()) => return true,
488
+ Err(std::sync::mpsc::TrySendError::Full(returned)) => {
489
+ state.0 -= 1; // rejected offers are not progress
490
+ work = returned;
491
+ if !wait {
492
+ return false;
493
+ }
494
+ let patience = state.1.stillness().saturating_sub(state.1.still().silent);
495
+ if patience.is_zero() {
496
+ self.activity
497
+ .incomplete
498
+ .store(true, std::sync::atomic::Ordering::Release);
499
+ return false;
500
+ }
501
+ state = self
502
+ .activity
503
+ .changed
504
+ .wait_timeout(state, patience)
505
+ .unwrap_or_else(|error| error.into_inner())
506
+ .0;
507
+ }
508
+ Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {
509
+ state.0 -= 1;
510
+ self.activity
511
+ .incomplete
512
+ .store(true, std::sync::atomic::Ordering::Release);
513
+ return false;
514
+ }
515
+ }
516
+ }
517
+ }
518
+
393
519
  fn start() -> Self {
394
520
  let (sender, receiver) = std::sync::mpsc::sync_channel::<Backfill>(BACKFILL_QUEUE);
395
521
  let receiver = std::sync::Arc::new(std::sync::Mutex::new(receiver));
@@ -410,25 +536,37 @@ impl Backfills {
410
536
  Err(_) => return,
411
537
  }
412
538
  };
413
- // Reopen without a caller's progress observer. Queued work owns no GC
414
- // hold: a source reclaimed before it opens is simply a missed cache fill.
415
- // Once opened, the verified descriptor pins that inode through the copy.
416
- if let Ok(store) = Store::open(&work.source) {
417
- let store = store.observed(activity.clone());
418
- let _ = work.cache.backfill_observed(
419
- &store,
420
- work.kind,
421
- &work.object,
422
- Some(activity.as_ref()),
423
- );
424
- }
425
- activity.finished();
539
+ activity.space_available();
540
+ work.copy(&activity);
426
541
  });
427
542
  }
428
543
  Self { sender, activity }
429
544
  }
430
545
  }
431
546
 
547
+ impl Backfill {
548
+ fn copy(self, activity: &std::sync::Arc<BackfillActivity>) {
549
+ // Reopening drops the caller's progress observer. The verified source
550
+ // descriptor pins its inode if GC unlinks it during the copy.
551
+ let copied = Store::open(&self.source).ok().and_then(|store| {
552
+ self.cache
553
+ .backfill_observed(
554
+ &store.observed(activity.clone()),
555
+ self.kind,
556
+ &self.object,
557
+ Some(activity.as_ref()),
558
+ )
559
+ .ok()
560
+ });
561
+ if !matches!(copied, Some(CacheWrite::Stored | CacheWrite::Present)) {
562
+ activity
563
+ .incomplete
564
+ .store(true, std::sync::atomic::Ordering::Release);
565
+ }
566
+ activity.finished();
567
+ }
568
+ }
569
+
432
570
  fn read_status(error: SoftFailure) -> CacheRead {
433
571
  match error {
434
572
  SoftFailure::Missing => CacheRead::Missing,
@@ -1470,3 +1608,195 @@ mod repair_tests {
1470
1608
  fs::remove_dir_all(root).unwrap();
1471
1609
  }
1472
1610
  }
1611
+
1612
+ #[cfg(test)]
1613
+ mod acquisition_backfill_tests {
1614
+ use super::*;
1615
+ use std::sync::{atomic::Ordering, Arc, Mutex};
1616
+
1617
+ #[test]
1618
+ fn an_abandoned_saturated_batch_reports_incomplete_after_accepted_work_drains() {
1619
+ let root = std::env::temp_dir().join(format!(
1620
+ "tfs-backfill-abandoned-{}",
1621
+ store::now_nanos_unique()
1622
+ ));
1623
+ let source = Store::init(&root.join("source")).unwrap();
1624
+ let bytes = b"local success survives another object's refusal";
1625
+ let object = ObjectRef::of(bytes);
1626
+ source
1627
+ .put_stream(&mut bytes.as_slice(), Some(&object), &Fault::default())
1628
+ .unwrap();
1629
+ let other_bytes = b"verified bytes whose cache offer is rejected";
1630
+ let other = ObjectRef::of(other_bytes);
1631
+ source
1632
+ .put_stream(&mut other_bytes.as_slice(), Some(&other), &Fault::default())
1633
+ .unwrap();
1634
+ let (sender, receiver) = std::sync::mpsc::sync_channel(1);
1635
+ let activity = Arc::new(BackfillActivity::new(Ledger::new()));
1636
+ let pool = Backfills {
1637
+ sender,
1638
+ activity: activity.clone(),
1639
+ };
1640
+ {
1641
+ let batch = BackfillBatch::with_pool(
1642
+ RepoObjectCache::new(root.join("cache")),
1643
+ &source,
1644
+ 2,
1645
+ &pool,
1646
+ );
1647
+ batch.offer(CacheKind::Blob, &object);
1648
+ batch.offer(CacheKind::Blob, &other);
1649
+ assert_eq!(batch.missed.lock().unwrap().len(), 1);
1650
+ // This is the early-refusal path: no cache wait or finish_offers.
1651
+ }
1652
+ receiver.recv().unwrap().copy(&activity);
1653
+ assert!(activity.wait());
1654
+ assert!(activity.incomplete.load(Ordering::Acquire));
1655
+ assert!(source.open_verified(&object.sha256).is_ok());
1656
+ assert!(source.open_verified(&other.sha256).is_ok());
1657
+ assert!(!RepoObjectCache::new(root.join("cache"))
1658
+ .path(CacheKind::Blob, &other.sha256)
1659
+ .unwrap()
1660
+ .exists());
1661
+ fs::remove_dir_all(root).unwrap();
1662
+ }
1663
+
1664
+ #[test]
1665
+ fn saturated_acquisition_backfill_reoffers_every_declared_object() {
1666
+ for repair in [false, true] {
1667
+ let root = std::env::temp_dir().join(format!(
1668
+ "tfs-backfill-saturation-{repair}-{}",
1669
+ store::now_nanos_unique()
1670
+ ));
1671
+ let source = Store::init(&root.join("source")).unwrap();
1672
+ let cache = RepoObjectCache::new(root.join("cache"));
1673
+ let objects: Vec<ObjectRef> = (0..BACKFILL_QUEUE + 8)
1674
+ .map(|index| {
1675
+ let bytes = vec![index as u8; 1024];
1676
+ let object = ObjectRef::of(&bytes);
1677
+ source
1678
+ .put_stream(&mut bytes.as_slice(), Some(&object), &Fault::default())
1679
+ .unwrap();
1680
+ object
1681
+ })
1682
+ .collect();
1683
+ let (sender, receiver) = std::sync::mpsc::sync_channel(BACKFILL_QUEUE);
1684
+ let activity = Arc::new(BackfillActivity::new(Ledger::new()));
1685
+ let pool = Backfills {
1686
+ sender,
1687
+ activity: activity.clone(),
1688
+ };
1689
+ let batch = BackfillBatch::with_pool(cache.clone(), &source, objects.len(), &pool);
1690
+ // No cache worker starts yet: queue saturation is causal, independent
1691
+ // of filesystem throughput or how the test runner schedules threads.
1692
+ for object in &objects {
1693
+ batch.offer(CacheKind::Blob, object);
1694
+ }
1695
+ assert_eq!(batch.missed.lock().unwrap().len(), 8);
1696
+ let receiver = Arc::new(Mutex::new(receiver));
1697
+ let workers: Vec<_> = (0..STREAMS)
1698
+ .map(|_| {
1699
+ let receiver = receiver.clone();
1700
+ let activity = activity.clone();
1701
+ std::thread::spawn(move || loop {
1702
+ let work = { receiver.lock().unwrap().recv() };
1703
+ let Ok(work) = work else { return };
1704
+ activity.space_available();
1705
+ work.copy(&activity);
1706
+ })
1707
+ })
1708
+ .collect();
1709
+ if repair {
1710
+ assert!(batch.finish_offers());
1711
+ }
1712
+ assert!(activity.wait());
1713
+ assert!(!activity.incomplete.load(Ordering::Acquire));
1714
+ drop(batch);
1715
+ drop(pool);
1716
+ for worker in workers {
1717
+ worker.join().unwrap();
1718
+ }
1719
+ // Destroy the first Store: replacement admission has only the cache.
1720
+ drop(source);
1721
+ fs::remove_dir_all(root.join("source")).unwrap();
1722
+ let replacement = Store::init(&root.join("replacement")).unwrap();
1723
+ let hits = objects
1724
+ .iter()
1725
+ .filter(|object| {
1726
+ cache.admit(&replacement, CacheKind::Blob, object).unwrap() == CacheRead::Hit
1727
+ })
1728
+ .count();
1729
+ assert_eq!(
1730
+ hits,
1731
+ if repair {
1732
+ objects.len()
1733
+ } else {
1734
+ BACKFILL_QUEUE
1735
+ }
1736
+ );
1737
+ if repair {
1738
+ for object in &objects {
1739
+ assert_eq!(
1740
+ replacement.open_verified(&object.sha256).unwrap().len(),
1741
+ object.length
1742
+ );
1743
+ }
1744
+ }
1745
+ fs::remove_dir_all(root).unwrap();
1746
+ }
1747
+ }
1748
+
1749
+ #[test]
1750
+ fn cache_failure_and_nonadvancing_full_queue_are_explicitly_incomplete() {
1751
+ let root = std::env::temp_dir().join(format!(
1752
+ "tfs-backfill-incomplete-{}",
1753
+ store::now_nanos_unique()
1754
+ ));
1755
+ let source = Store::init(&root.join("source")).unwrap();
1756
+ let bytes = b"verified local bytes remain usable";
1757
+ let object = ObjectRef::of(bytes);
1758
+ source
1759
+ .put_stream(&mut bytes.as_slice(), Some(&object), &Fault::default())
1760
+ .unwrap();
1761
+ fs::write(root.join("not-a-directory"), b"unavailable cache").unwrap();
1762
+ let activity = Arc::new(BackfillActivity::new(Ledger::new()));
1763
+ activity.offered();
1764
+ Backfill {
1765
+ cache: RepoObjectCache::new(root.join("not-a-directory")),
1766
+ source: source.root().to_owned(),
1767
+ kind: CacheKind::Blob,
1768
+ object: object.clone(),
1769
+ }
1770
+ .copy(&activity);
1771
+ assert!(activity.wait());
1772
+ assert!(activity.incomplete.load(Ordering::Acquire));
1773
+ assert!(source.open_verified(&object.sha256).is_ok());
1774
+
1775
+ let (sender, _blocked_receiver) = std::sync::mpsc::sync_channel(1);
1776
+ let activity = Arc::new(BackfillActivity::new(Ledger::new()));
1777
+ let pool = Backfills {
1778
+ sender,
1779
+ activity: activity.clone(),
1780
+ };
1781
+ let batch =
1782
+ BackfillBatch::with_pool(RepoObjectCache::new(root.join("cache")), &source, 2, &pool);
1783
+ batch.offer(CacheKind::Blob, &object);
1784
+ batch.offer(CacheKind::Blob, &object);
1785
+ let start = std::time::Instant::now();
1786
+ let elapsed = Arc::new(std::sync::atomic::AtomicU64::new(0));
1787
+ let clock = elapsed.clone();
1788
+ activity.pending.lock().unwrap().1 =
1789
+ Ledger::with_clock(std::time::Duration::from_millis(10), move || {
1790
+ start + std::time::Duration::from_millis(clock.load(Ordering::Acquire))
1791
+ });
1792
+ elapsed.store(1000, Ordering::Release);
1793
+ assert!(
1794
+ !batch.finish_offers(),
1795
+ "retrying a full queue must not manufacture cache progress"
1796
+ );
1797
+ assert!(activity.incomplete.load(Ordering::Acquire));
1798
+ assert_eq!(activity.pending.lock().unwrap().0, 1);
1799
+ assert!(source.open_verified(&object.sha256).is_ok());
1800
+ fs::remove_dir_all(root).unwrap();
1801
+ }
1802
+ }