tensorfs 0.3.30__tar.gz → 0.3.31__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.

Potentially problematic release.


This version of tensorfs might be problematic. Click here for more details.

Files changed (126) hide show
  1. {tensorfs-0.3.30 → tensorfs-0.3.31}/Cargo.lock +2 -2
  2. {tensorfs-0.3.30 → tensorfs-0.3.31}/Cargo.toml +1 -1
  3. {tensorfs-0.3.30 → tensorfs-0.3.31}/PKG-INFO +1 -1
  4. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/bin/tfs/fetch.rs +3 -0
  5. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/repo_cache.rs +244 -16
  6. {tensorfs-0.3.30 → tensorfs-0.3.31}/pyproject.toml +1 -1
  7. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/Cargo.toml +0 -0
  8. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/build.rs +0 -0
  9. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  10. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  11. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  12. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  13. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/b64.rs +0 -0
  14. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  15. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  16. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  17. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  18. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  19. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  20. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  21. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  22. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/canon.rs +0 -0
  23. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/capability.rs +0 -0
  24. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/catalog.rs +0 -0
  25. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/cbor.rs +0 -0
  26. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  27. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/corpus.rs +0 -0
  28. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  29. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  30. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  31. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  32. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  33. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  34. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/derived.rs +0 -0
  35. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/dtype.rs +0 -0
  36. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/durability.rs +0 -0
  37. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/err.rs +0 -0
  38. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/fetch.rs +0 -0
  39. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/fill.rs +0 -0
  40. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/fit.rs +0 -0
  41. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/gc.rs +0 -0
  42. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/header.rs +0 -0
  43. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/home.rs +0 -0
  44. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/ids.rs +0 -0
  45. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  46. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  47. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  48. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  49. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  50. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  51. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  52. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  53. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  54. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  55. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/jcs.rs +0 -0
  56. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/lib.rs +0 -0
  57. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/limits.rs +0 -0
  58. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/machine.rs +0 -0
  59. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/manifest.rs +0 -0
  60. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/meta.rs +0 -0
  61. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/project.rs +0 -0
  62. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/providers.rs +0 -0
  63. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/read.rs +0 -0
  64. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/receipt.rs +0 -0
  65. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/reclaim.rs +0 -0
  66. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/registry.rs +0 -0
  67. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/relation.rs +0 -0
  68. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/repository.rs +0 -0
  69. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/sha256.rs +0 -0
  70. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  71. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/spec.rs +0 -0
  72. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/staging.rs +0 -0
  73. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/storage.rs +0 -0
  74. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/store.rs +0 -0
  75. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/transport/api.rs +0 -0
  76. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/transport/http.rs +0 -0
  77. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  78. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  79. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  80. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  81. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  82. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  83. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/transport/push.rs +0 -0
  84. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  85. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  86. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/src/vectors.rs +0 -0
  87. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  88. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/fit.rs +0 -0
  89. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  90. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  91. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  92. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  93. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  94. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  95. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  96. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  97. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/home.rs +0 -0
  98. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  99. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  100. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/liveness.rs +0 -0
  101. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  102. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  103. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  104. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  105. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  106. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  107. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  108. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  109. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  110. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  111. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  112. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  113. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/staging.rs +0 -0
  114. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  115. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  116. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/version.rs +0 -0
  117. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-core/tests/write_through.rs +0 -0
  118. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-py/Cargo.toml +0 -0
  119. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-py/src/docs.rs +0 -0
  120. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-py/src/errors.rs +0 -0
  121. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-py/src/lib.rs +0 -0
  122. {tensorfs-0.3.30 → tensorfs-0.3.31}/crates/tensorfs-py/src/store.rs +0 -0
  123. {tensorfs-0.3.30 → tensorfs-0.3.31}/python/tensorfs/__init__.py +0 -0
  124. {tensorfs-0.3.30 → tensorfs-0.3.31}/python/tensorfs/_ext.pyi +0 -0
  125. {tensorfs-0.3.30 → tensorfs-0.3.31}/python/tensorfs/errors.py +0 -0
  126. {tensorfs-0.3.30 → tensorfs-0.3.31}/python/tensorfs/py.typed +0 -0
@@ -392,7 +392,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
392
392
 
393
393
  [[package]]
394
394
  name = "tensorfs-core"
395
- version = "0.3.30"
395
+ version = "0.3.31"
396
396
  dependencies = [
397
397
  "fs2",
398
398
  "minicbor",
@@ -404,7 +404,7 @@ dependencies = [
404
404
 
405
405
  [[package]]
406
406
  name = "tensorfs-py"
407
- version = "0.3.30"
407
+ version = "0.3.31"
408
408
  dependencies = [
409
409
  "pyo3",
410
410
  "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.30"
6
+ version = "0.3.31"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.30
3
+ Version: 0.3.31
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -302,6 +302,9 @@ fn pull_with_budget(
302
302
  request.on_object = Some(&observe);
303
303
  let report = ok!(transport::pull(&request));
304
304
  progress.settle();
305
+ if !tensorfs_core::repo_cache::finish_optional_backfills() {
306
+ eprintln!("repo cache backfill stopped advancing; local fetch remains complete");
307
+ }
305
308
  println!(
306
309
  "model {}@{} lane {}",
307
310
  report.model, report.release, report.lane
@@ -16,6 +16,7 @@ use crate::limits;
16
16
  use crate::manifest::Manifest;
17
17
  use crate::sha256::{self, Sha256};
18
18
  use crate::store::{self, Fault, Store};
19
+ use crate::transport::Ledger;
19
20
 
20
21
  /// The two immutable repository object namespaces a cache may contain.
21
22
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -101,16 +102,21 @@ impl RepoObjectCache {
101
102
  kind: CacheKind,
102
103
  object: &ObjectRef,
103
104
  ) -> bool {
104
- static BACKFILLS: std::sync::OnceLock<Backfills> = std::sync::OnceLock::new();
105
105
  let pool = BACKFILLS.get_or_init(Backfills::start);
106
- pool.sender
106
+ pool.activity.offered();
107
+ let sent = pool
108
+ .sender
107
109
  .try_send(Backfill {
108
110
  cache: self.clone(),
109
111
  source: source.root().to_path_buf(),
110
112
  kind,
111
113
  object: object.clone(),
112
114
  })
113
- .is_ok()
115
+ .is_ok();
116
+ if !sent {
117
+ pool.activity.finished();
118
+ }
119
+ sent
114
120
  }
115
121
 
116
122
  /// The one cross-language layout contract. `digest` is bare lowercase SHA-256.
@@ -218,10 +224,20 @@ impl RepoObjectCache {
218
224
  source: &Store,
219
225
  kind: CacheKind,
220
226
  want: &ObjectRef,
227
+ ) -> Result<CacheWrite> {
228
+ self.backfill_observed(source, kind, want, None)
229
+ }
230
+
231
+ fn backfill_observed(
232
+ &self,
233
+ source: &Store,
234
+ kind: CacheKind,
235
+ want: &ObjectRef,
236
+ progress: Option<&dyn store::Progress>,
221
237
  ) -> Result<CacheWrite> {
222
238
  let destination = self.path(kind, &want.sha256)?;
223
239
  if destination.exists() {
224
- return Ok(match verify_path(&destination, kind, want) {
240
+ return Ok(match verify_path(&destination, kind, want, progress) {
225
241
  Ok(()) => CacheWrite::Present,
226
242
  Err(_) => CacheWrite::Unavailable,
227
243
  });
@@ -229,18 +245,18 @@ impl RepoObjectCache {
229
245
  let result = match kind {
230
246
  CacheKind::Blob => match source.open_verified(&want.sha256) {
231
247
  Ok(mut file) if file.len() == want.length => {
232
- publish_stream(&mut file, &destination, want)
248
+ publish_stream(&mut file, &destination, want, progress)
233
249
  }
234
250
  Ok(_) | Err(_) => Err(SoftFailure::Unavailable),
235
251
  },
236
252
  CacheKind::Manifest => match source.read_manifest(want) {
237
- Ok(manifest) => publish_bytes(&manifest.canonical_bytes(), &destination),
253
+ Ok(manifest) => publish_bytes(&manifest.canonical_bytes(), &destination, progress),
238
254
  Err(_) => Err(SoftFailure::Unavailable),
239
255
  },
240
256
  };
241
257
  Ok(match result {
242
258
  Ok(true) => CacheWrite::Stored,
243
- Ok(false) => match verify_path(&destination, kind, want) {
259
+ Ok(false) => match verify_path(&destination, kind, want, progress) {
244
260
  Ok(()) => CacheWrite::Present,
245
261
  Err(_) => CacheWrite::Unavailable,
246
262
  },
@@ -260,19 +276,99 @@ struct Backfill {
260
276
  object: ObjectRef,
261
277
  }
262
278
 
279
+ static BACKFILLS: std::sync::OnceLock<Backfills> = std::sync::OnceLock::new();
280
+
263
281
  struct Backfills {
264
282
  sender: std::sync::mpsc::SyncSender<Backfill>,
283
+ activity: std::sync::Arc<BackfillActivity>,
284
+ }
285
+
286
+ struct BackfillActivity {
287
+ pending: std::sync::Mutex<(usize, Ledger)>,
288
+ changed: std::sync::Condvar,
289
+ }
290
+
291
+ impl BackfillActivity {
292
+ fn new(ledger: Ledger) -> Self {
293
+ Self {
294
+ pending: std::sync::Mutex::new((0, ledger)),
295
+ changed: std::sync::Condvar::new(),
296
+ }
297
+ }
298
+
299
+ fn offered(&self) {
300
+ let mut state = self
301
+ .pending
302
+ .lock()
303
+ .unwrap_or_else(|error| error.into_inner());
304
+ if state.0 == 0 {
305
+ state.1 = Ledger::with_resolution(state.1.sample());
306
+ }
307
+ state.0 += 1;
308
+ }
309
+
310
+ fn finished(&self) {
311
+ let mut state = self
312
+ .pending
313
+ .lock()
314
+ .unwrap_or_else(|error| error.into_inner());
315
+ state.0 -= 1;
316
+ state.1.answered();
317
+ self.changed.notify_all();
318
+ }
319
+
320
+ fn wait(&self) -> bool {
321
+ let mut state = self
322
+ .pending
323
+ .lock()
324
+ .unwrap_or_else(|error| error.into_inner());
325
+ while state.0 != 0 {
326
+ let patience = state.1.stillness().saturating_sub(state.1.still().silent);
327
+ if patience.is_zero() {
328
+ return false;
329
+ }
330
+ state = self
331
+ .changed
332
+ .wait_timeout(state, patience)
333
+ .unwrap_or_else(|error| error.into_inner())
334
+ .0;
335
+ }
336
+ true
337
+ }
338
+ }
339
+
340
+ impl store::Progress for BackfillActivity {
341
+ fn admitted(&self) {}
342
+
343
+ fn moved(&self, bytes: u64) {
344
+ self.pending
345
+ .lock()
346
+ .unwrap_or_else(|error| error.into_inner())
347
+ .1
348
+ .moved(bytes);
349
+ self.changed.notify_all();
350
+ }
351
+ }
352
+
353
+ /// Before a short-lived CLI exits, give its existing optional write pool time to
354
+ /// finish while real copy/verification progress continues. A stalled cache only
355
+ /// ends this wait; it never changes the completed local fetch or joins its worker.
356
+ /// Long-lived Store callers keep the original asynchronous behavior.
357
+ pub fn finish_optional_backfills() -> bool {
358
+ BACKFILLS.get().is_none_or(|pool| pool.activity.wait())
265
359
  }
266
360
 
267
361
  impl Backfills {
268
362
  fn start() -> Self {
269
363
  let (sender, receiver) = std::sync::mpsc::sync_channel::<Backfill>(BACKFILL_QUEUE);
270
364
  let receiver = std::sync::Arc::new(std::sync::Mutex::new(receiver));
365
+ let activity = std::sync::Arc::new(BackfillActivity::new(Ledger::new()));
271
366
  // One pool for the process, never a new blocked thread set for every pull.
272
367
  // Workers are detached: process exit may leave cache temps, never canonical
273
368
  // partial objects. Dropping a pull must not join a stalled FUSE syscall.
274
369
  for _ in 0..STREAMS {
275
370
  let receiver = receiver.clone();
371
+ let activity = activity.clone();
276
372
  let _ = std::thread::Builder::new()
277
373
  .name("tfs-cache-write".into())
278
374
  .spawn(move || loop {
@@ -287,11 +383,18 @@ impl Backfills {
287
383
  // hold: a source reclaimed before it opens is simply a missed cache fill.
288
384
  // Once opened, the verified descriptor pins that inode through the copy.
289
385
  if let Ok(store) = Store::open(&work.source) {
290
- let _ = work.cache.backfill_store(&store, work.kind, &work.object);
386
+ let store = store.observed(activity.clone());
387
+ let _ = work.cache.backfill_observed(
388
+ &store,
389
+ work.kind,
390
+ &work.object,
391
+ Some(activity.as_ref()),
392
+ );
291
393
  }
394
+ activity.finished();
292
395
  });
293
396
  }
294
- Self { sender }
397
+ Self { sender, activity }
295
398
  }
296
399
  }
297
400
 
@@ -344,27 +447,33 @@ fn verify_path(
344
447
  path: &Path,
345
448
  kind: CacheKind,
346
449
  want: &ObjectRef,
450
+ progress: Option<&dyn store::Progress>,
347
451
  ) -> std::result::Result<(), SoftFailure> {
348
452
  match kind {
349
453
  CacheKind::Blob => {
350
454
  let mut file = open_exact(path, want.length)?;
351
455
  let mut sink = std::io::sink();
352
- stream_exact(&mut file, &mut sink, want)
456
+ stream_exact(&mut file, &mut sink, want, progress)
353
457
  }
354
458
  CacheKind::Manifest => read_manifest(path, want).map(|_| ()),
355
459
  }
356
460
  }
357
461
 
358
- fn publish_bytes(bytes: &[u8], destination: &Path) -> std::result::Result<bool, SoftFailure> {
462
+ fn publish_bytes(
463
+ bytes: &[u8],
464
+ destination: &Path,
465
+ progress: Option<&dyn store::Progress>,
466
+ ) -> std::result::Result<bool, SoftFailure> {
359
467
  let mut source = bytes;
360
468
  let want = ObjectRef::of(bytes);
361
- publish_stream(&mut source, destination, &want)
469
+ publish_stream(&mut source, destination, &want, progress)
362
470
  }
363
471
 
364
472
  fn publish_stream(
365
473
  source: &mut impl Read,
366
474
  destination: &Path,
367
475
  want: &ObjectRef,
476
+ progress: Option<&dyn store::Progress>,
368
477
  ) -> std::result::Result<bool, SoftFailure> {
369
478
  let Some(directory) = destination.parent() else {
370
479
  return Err(SoftFailure::Unavailable);
@@ -385,7 +494,7 @@ fn publish_stream(
385
494
  .mode(0o600)
386
495
  .open(&temporary)
387
496
  .map_err(|_| SoftFailure::Unavailable)?;
388
- if let Err(error) = stream_exact(source, &mut output, want) {
497
+ if let Err(error) = stream_exact(source, &mut output, want, progress) {
389
498
  let _ = fs::remove_file(&temporary);
390
499
  return Err(error);
391
500
  }
@@ -414,6 +523,7 @@ fn stream_exact(
414
523
  source: &mut impl Read,
415
524
  destination: &mut impl Write,
416
525
  want: &ObjectRef,
526
+ progress: Option<&dyn store::Progress>,
417
527
  ) -> std::result::Result<(), SoftFailure> {
418
528
  let mut digest = Sha256::new();
419
529
  let mut length = 0u64;
@@ -429,6 +539,9 @@ fn stream_exact(
429
539
  destination
430
540
  .write_all(&buffer[..read])
431
541
  .map_err(|_| SoftFailure::Unavailable)?;
542
+ if let Some(progress) = progress {
543
+ progress.moved(read as u64);
544
+ }
432
545
  length = length
433
546
  .checked_add(read as u64)
434
547
  .ok_or(SoftFailure::Corrupt)?;
@@ -918,10 +1031,16 @@ mod backfill_tests {
918
1031
  first: true,
919
1032
  length: object.length,
920
1033
  };
921
- assert_eq!(publish_stream(&mut input, &destination, &object), Ok(true));
1034
+ assert_eq!(
1035
+ publish_stream(&mut input, &destination, &object, None),
1036
+ Ok(true)
1037
+ );
922
1038
  assert!(!source.contains(&object.sha256));
923
1039
  assert_eq!(fs::read(&destination).unwrap(), bytes);
924
- assert_eq!(verify_path(&destination, CacheKind::Blob, &object), Ok(()));
1040
+ assert_eq!(
1041
+ verify_path(&destination, CacheKind::Blob, &object, None),
1042
+ Ok(())
1043
+ );
925
1044
  assert_eq!(
926
1045
  fs::read_dir(destination.parent().unwrap()).unwrap().count(),
927
1046
  1
@@ -950,7 +1069,7 @@ mod backfill_tests {
950
1069
  }
951
1070
  }
952
1071
  assert_eq!(
953
- publish_stream(&mut Fails(false), &destination, &object),
1072
+ publish_stream(&mut Fails(false), &destination, &object, None),
954
1073
  Err(SoftFailure::Unavailable)
955
1074
  );
956
1075
  assert!(!destination.exists());
@@ -962,3 +1081,112 @@ mod backfill_tests {
962
1081
  fs::remove_dir_all(root).unwrap();
963
1082
  }
964
1083
  }
1084
+
1085
+ #[cfg(test)]
1086
+ mod exit_wait_tests {
1087
+ use super::*;
1088
+ use std::sync::Arc;
1089
+ use std::time::{Duration, Instant};
1090
+
1091
+ #[test]
1092
+ fn advancing_copy_outlives_the_initial_silence_allowance() {
1093
+ struct Paced<'a>(&'a [u8]);
1094
+ impl Read for Paced<'_> {
1095
+ fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
1096
+ std::thread::sleep(Duration::from_millis(5));
1097
+ let take = out.len().min(4096);
1098
+ self.0.read(&mut out[..take])
1099
+ }
1100
+ }
1101
+ let root = std::env::temp_dir().join(format!(
1102
+ "tensorfs-exit-moving-{}-{}",
1103
+ std::process::id(),
1104
+ store::now_nanos_unique()
1105
+ ));
1106
+ let data = vec![7; 256 << 10];
1107
+ let object = ObjectRef::of(&data);
1108
+ let destination = root.join(&object.sha256);
1109
+ let activity = Arc::new(BackfillActivity::new(Ledger::with_resolution(
1110
+ Duration::from_millis(10),
1111
+ )));
1112
+ activity.offered();
1113
+ let observed = activity.clone();
1114
+ let target = destination.clone();
1115
+ let wanted = object.clone();
1116
+ let started = Instant::now();
1117
+ let copy = std::thread::spawn(move || {
1118
+ let result =
1119
+ publish_stream(&mut Paced(&data), &target, &wanted, Some(observed.as_ref()));
1120
+ observed.finished();
1121
+ result
1122
+ });
1123
+ assert!(activity.wait(), "a moving copy was abandoned");
1124
+ assert_eq!(copy.join().unwrap(), Ok(true));
1125
+ assert!(
1126
+ started.elapsed() > Duration::from_millis(60),
1127
+ "copy never outlived the initial silence floor"
1128
+ );
1129
+ assert_eq!(
1130
+ verify_path(&destination, CacheKind::Blob, &object, None),
1131
+ Ok(())
1132
+ );
1133
+ fs::remove_dir_all(root).unwrap();
1134
+ }
1135
+
1136
+ #[test]
1137
+ fn stalled_optional_copy_stops_waiting_without_joining_its_worker() {
1138
+ let root = std::env::temp_dir().join(format!(
1139
+ "tensorfs-exit-stalled-{}-{}",
1140
+ std::process::id(),
1141
+ store::now_nanos_unique()
1142
+ ));
1143
+ let source = Store::init(&root.join("source")).unwrap();
1144
+ let object = source
1145
+ .put_stream(&mut b"still local".as_slice(), None, &Fault::default())
1146
+ .unwrap()
1147
+ .obj;
1148
+ let cache = RepoObjectCache::new(root.join("cache"));
1149
+ let fifo = cache.path(CacheKind::Blob, &object.sha256).unwrap();
1150
+ fs::create_dir_all(fifo.parent().unwrap()).unwrap();
1151
+ assert!(std::process::Command::new("mkfifo")
1152
+ .arg(&fifo)
1153
+ .status()
1154
+ .unwrap()
1155
+ .success());
1156
+ let activity = Arc::new(BackfillActivity::new(Ledger::with_resolution(
1157
+ Duration::from_millis(10),
1158
+ )));
1159
+ activity.offered();
1160
+ let observed = activity.clone();
1161
+ let wanted = object.clone();
1162
+ let store = source.clone();
1163
+ let worker = std::thread::spawn(move || {
1164
+ let result =
1165
+ cache.backfill_observed(&store, CacheKind::Blob, &wanted, Some(observed.as_ref()));
1166
+ observed.finished();
1167
+ result
1168
+ });
1169
+ assert!(
1170
+ !activity.wait(),
1171
+ "blocked optional cache never produced a no-progress verdict"
1172
+ );
1173
+ assert!(
1174
+ !worker.is_finished(),
1175
+ "the wait returned by joining the stalled cache worker"
1176
+ );
1177
+ assert!(
1178
+ source.record_valid(&object.sha256).is_ok(),
1179
+ "cache stall invalidated completed local bytes"
1180
+ );
1181
+ let unblock = OpenOptions::new()
1182
+ .read(true)
1183
+ .write(true)
1184
+ .custom_flags(0x800)
1185
+ .open(&fifo)
1186
+ .unwrap();
1187
+ assert_eq!(worker.join().unwrap().unwrap(), CacheWrite::Unavailable);
1188
+ drop(unblock);
1189
+ assert!(activity.wait());
1190
+ fs::remove_dir_all(root).unwrap();
1191
+ }
1192
+ }
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "tensorfs"
7
- version = "0.3.30"
7
+ version = "0.3.31"
8
8
  description = "TensorFS — canonical tensor artifact substrate"
9
9
  requires-python = ">=3.12"
10
10
  classifiers = [