tensorfs 0.3.35__tar.gz → 0.3.37__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 (127) hide show
  1. {tensorfs-0.3.35 → tensorfs-0.3.37}/Cargo.lock +2 -2
  2. {tensorfs-0.3.35 → tensorfs-0.3.37}/Cargo.toml +1 -1
  3. {tensorfs-0.3.35 → tensorfs-0.3.37}/PKG-INFO +1 -1
  4. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/derived/adoption.rs +140 -0
  5. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/reclaim.rs +34 -106
  6. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/repo_cache.rs +9 -58
  7. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/transport/tests.rs +64 -194
  8. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/reclaim_run.rs +65 -28
  9. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/repo_cache.rs +67 -0
  10. {tensorfs-0.3.35 → tensorfs-0.3.37}/pyproject.toml +1 -1
  11. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/Cargo.toml +0 -0
  12. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/build.rs +0 -0
  13. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  14. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  15. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  16. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  17. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/b64.rs +0 -0
  18. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  19. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  20. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  21. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  22. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  23. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  24. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  25. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  26. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  27. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  28. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/canon.rs +0 -0
  29. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/capability.rs +0 -0
  30. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/catalog.rs +0 -0
  31. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/cbor.rs +0 -0
  32. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  33. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/corpus.rs +0 -0
  34. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  35. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  36. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  37. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  38. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  39. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/derived.rs +0 -0
  40. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/dtype.rs +0 -0
  41. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/durability.rs +0 -0
  42. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/err.rs +0 -0
  43. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/fetch.rs +0 -0
  44. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/fill.rs +0 -0
  45. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/fit.rs +0 -0
  46. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/gc.rs +0 -0
  47. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/header.rs +0 -0
  48. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/home.rs +0 -0
  49. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/ids.rs +0 -0
  50. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  51. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  52. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  53. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  54. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  55. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  56. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  57. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  58. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  59. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  60. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/jcs.rs +0 -0
  61. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/lib.rs +0 -0
  62. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/limits.rs +0 -0
  63. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/machine.rs +0 -0
  64. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/manifest.rs +0 -0
  65. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/meta.rs +0 -0
  66. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/project.rs +0 -0
  67. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/providers.rs +0 -0
  68. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/read.rs +0 -0
  69. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/receipt.rs +0 -0
  70. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/registry.rs +0 -0
  71. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/relation.rs +0 -0
  72. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/repository.rs +0 -0
  73. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/sha256.rs +0 -0
  74. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  75. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/spec.rs +0 -0
  76. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/staging.rs +0 -0
  77. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/storage.rs +0 -0
  78. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/store.rs +0 -0
  79. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/transport/api.rs +0 -0
  80. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/transport/http.rs +0 -0
  81. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  82. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  83. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  84. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  85. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  86. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  87. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/transport/push.rs +0 -0
  88. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  89. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/src/vectors.rs +0 -0
  90. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  91. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/fit.rs +0 -0
  92. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  93. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  94. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  95. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  96. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  97. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  98. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  99. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  100. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/home.rs +0 -0
  101. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  102. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  103. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/liveness.rs +0 -0
  104. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  105. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  106. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  107. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  108. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  109. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  110. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  111. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  112. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  113. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  114. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/staging.rs +0 -0
  115. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  116. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  117. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/version.rs +0 -0
  118. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-core/tests/write_through.rs +0 -0
  119. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-py/Cargo.toml +0 -0
  120. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-py/src/docs.rs +0 -0
  121. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-py/src/errors.rs +0 -0
  122. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-py/src/lib.rs +0 -0
  123. {tensorfs-0.3.35 → tensorfs-0.3.37}/crates/tensorfs-py/src/store.rs +0 -0
  124. {tensorfs-0.3.35 → tensorfs-0.3.37}/python/tensorfs/__init__.py +0 -0
  125. {tensorfs-0.3.35 → tensorfs-0.3.37}/python/tensorfs/_ext.pyi +0 -0
  126. {tensorfs-0.3.35 → tensorfs-0.3.37}/python/tensorfs/errors.py +0 -0
  127. {tensorfs-0.3.35 → tensorfs-0.3.37}/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.35"
395
+ version = "0.3.37"
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.35"
407
+ version = "0.3.37"
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.35"
6
+ version = "0.3.37"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.35
3
+ Version: 0.3.37
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -250,6 +250,146 @@ fn export(
250
250
  #[cfg(test)]
251
251
  mod tests {
252
252
  use super::*;
253
+ use std::collections::BTreeSet;
254
+ use std::sync::atomic::{AtomicU64, Ordering};
255
+ use std::sync::Arc;
256
+
257
+ #[derive(Default)]
258
+ struct MeasuredIO {
259
+ moved: AtomicU64,
260
+ admitted: AtomicU64,
261
+ }
262
+
263
+ impl crate::store::Progress for MeasuredIO {
264
+ fn moved(&self, bytes: u64) {
265
+ self.moved.fetch_add(bytes, Ordering::Relaxed);
266
+ }
267
+ fn admitted(&self) {
268
+ self.admitted.fetch_add(1, Ordering::Relaxed);
269
+ }
270
+ }
271
+
272
+ impl MeasuredIO {
273
+ fn take(&self) -> (u64, u64) {
274
+ (
275
+ self.moved.swap(0, Ordering::Relaxed),
276
+ self.admitted.swap(0, Ordering::Relaxed),
277
+ )
278
+ }
279
+ }
280
+
281
+ #[test]
282
+ fn multiple_checkpoint_groups_separate_rehash_from_adoption_metadata_io() {
283
+ let (store, meta, mut declaration, source, target) = fixture();
284
+ let mut third = declaration.components[0].add[0].clone();
285
+ third.key = "third".into();
286
+ declaration.components[0].add.push(third);
287
+ declaration.order.push(("model".into(), "third".into()));
288
+ declaration.max_new_bytes += 2048;
289
+ let raw = declaration.canonical_work_bytes().unwrap();
290
+ let writer = begin(&store, &meta, &source, 1, declaration.clone(), None).unwrap();
291
+ let first = write(&store, &meta, &source, "weight", 3);
292
+ checkpoint_progress(&store, &meta, &source, 1, "source", "model", None).unwrap();
293
+ let second = write(&store, &meta, &source, "next", 4);
294
+ let (_, checkpoint) =
295
+ checkpoint_progress(&store, &meta, &source, 1, "source", "model", None).unwrap();
296
+ fence(&meta, &source, 1).unwrap();
297
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
298
+
299
+ let progress = Arc::new(MeasuredIO::default());
300
+ let store = store.observed(progress.clone());
301
+ eprintln!("CR108_IO_ROOT {}", store.root().display());
302
+ eprintln!("CR108_IO_PAYLOAD {} {}", first.sha256, second.sha256);
303
+ // Lose only the verification receipts in this fixture, not the actual
304
+ // bytes. The real verifier must rehash them before the warm adoption arm.
305
+ let catalog = crate::catalog::Catalog::open(store.root()).unwrap();
306
+ catalog.drop_verification(&first.sha256);
307
+ catalog.drop_verification(&second.sha256);
308
+ drop(catalog);
309
+ eprintln!("CR108_IO_PHASE verification");
310
+ assert!(matches!(
311
+ store.verify(&first.sha256).unwrap(),
312
+ crate::store::Verdict::Invalidated { .. }
313
+ ));
314
+ assert!(matches!(
315
+ store.verify(&second.sha256).unwrap(),
316
+ crate::store::Verdict::Invalidated { .. }
317
+ ));
318
+ let verification = progress.take();
319
+ assert_eq!(verification, (4096, 0));
320
+
321
+ eprintln!("CR108_IO_PHASE inspect_before");
322
+ let before: BTreeSet<_> = store.objects().unwrap().into_iter().collect();
323
+ let before_payloads: Vec<_> = [&first, &second]
324
+ .iter()
325
+ .map(|object| {
326
+ let path = store.blob_path(&object.sha256);
327
+ (
328
+ std::fs::read(&path).unwrap(),
329
+ std::fs::metadata(path).unwrap().modified().unwrap(),
330
+ )
331
+ })
332
+ .collect();
333
+ eprintln!("CR108_IO_PHASE adoption");
334
+ let (_, adopted) = adopt_checkpoint(
335
+ &store,
336
+ &meta,
337
+ &target,
338
+ &source,
339
+ &raw,
340
+ checkpoint.head.as_ref().unwrap(),
341
+ "target",
342
+ "model",
343
+ )
344
+ .unwrap();
345
+ let adoption = progress.take();
346
+ eprintln!("CR108_IO_PHASE inspect_after");
347
+ let fresh: Vec<_> = store
348
+ .objects()
349
+ .unwrap()
350
+ .into_iter()
351
+ .filter(|id| !before.contains(id))
352
+ .collect();
353
+ let metadata_bytes: u64 = fresh
354
+ .iter()
355
+ .map(|id| std::fs::metadata(store.blob_path(id)).unwrap().len())
356
+ .sum();
357
+ assert!(metadata_bytes > 0);
358
+ assert_eq!(adoption, (metadata_bytes, fresh.len() as u64));
359
+ for (object, prior) in [&first, &second].iter().zip(before_payloads) {
360
+ let path = store.blob_path(&object.sha256);
361
+ assert_eq!(std::fs::read(&path).unwrap(), prior.0);
362
+ assert_eq!(
363
+ std::fs::metadata(path).unwrap().modified().unwrap(),
364
+ prior.1
365
+ );
366
+ }
367
+ eprintln!("CR108_IO_PHASE restore");
368
+ let next = begin(
369
+ &store,
370
+ &meta,
371
+ &target,
372
+ 1,
373
+ declaration,
374
+ adopted.head.as_ref(),
375
+ )
376
+ .unwrap();
377
+ assert_eq!(
378
+ completed(&meta, &target, 1).unwrap().0,
379
+ vec![
380
+ ("model".into(), "next".into(), "value".into()),
381
+ ("model".into(), "weight".into(), "value".into())
382
+ ]
383
+ );
384
+ let restore = progress.take();
385
+ assert_eq!(restore, (0, 0));
386
+ eprintln!("CR108_IO_PHASE finish");
387
+ write(&store, &meta, &target, "third", 5);
388
+ commit(&store, &meta, &target, 1).unwrap();
389
+ release_guards(&meta, Some(next.writer_hold), next.source_leases);
390
+ eprintln!("CR108_IO_PHASE done");
391
+ eprintln!("CR108_IO_COUNTS verification={verification:?} adoption={adoption:?} restore={restore:?} adoption_metadata_bytes={metadata_bytes}");
392
+ }
253
393
 
254
394
  fn fixture() -> (Store, Meta, Declaration, String, String) {
255
395
  let path = std::env::temp_dir().join(format!(
@@ -498,22 +498,11 @@ pub(crate) fn plan_managed(
498
498
 
499
499
  // ------------------------------------------------------------------ the act (tfs-066)
500
500
 
501
- /// How a plan's bytes are proven to be gettable again before they are let go.
502
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
503
- pub enum Recovery {
504
- /// The object is already on the bound repo cache at its exact length.
505
- Cache,
506
- /// This run put it there.
507
- Demoted,
508
- /// The caller declared the roots recoverable from the hub. Their word, like free space.
509
- Declared,
510
- }
511
-
512
501
  /// What `run` was asked to do beyond the plan itself.
513
502
  #[derive(Debug, Clone, Copy, Default)]
514
503
  pub struct RunOptions {
515
- /// Publish each object to the bound repo cache before dropping it, so the refetch is a
516
- /// volume read rather than an R2 pull.
504
+ /// Best-effort cache warming before reclaim. This never authorizes deletion;
505
+ /// assume_upstream is still required because the cache is disposable.
517
506
  pub demote: bool,
518
507
  /// The caller declares these roots recoverable from the hub. Same class of caller-input
519
508
  /// as free space (decisions.md 241): TensorFS cannot see the hub, the caller can.
@@ -573,87 +562,23 @@ impl RunReport {
573
562
  }
574
563
  }
575
564
 
576
- /// Is this object gettable again without the pod's own history?
577
- ///
578
- /// decisions.md 842 set the DURABLE-UPSTREAM INVARIANT — *"pressure eviction may target
579
- /// only objects with a recorded durable upstream; local-only artifacts are never
580
- /// pressure-evicted"* — with the falsifier *"a pressure sweep evicting a local-only
581
- /// artifact"*. tfs-020 then deleted the `hub_published` boolean along with the pressure
582
- /// plane it gated, so the invariant has had no mechanism since.
583
- ///
584
- /// It comes back CONSTRUCTIVELY rather than as a flag someone must remember to set: the
585
- /// cache is checked, or this run puts the bytes there, or the caller says so. Demotion is
586
- /// the interesting one, because it does not FIND the invariant true — it MAKES it true, and
587
- /// it makes it true against the `/workspace` volume, which measurably survives the
588
- /// container-disk reset that destroys the Store (th-152, 2026-09-04).
589
- fn recovery(
590
- store: &Store,
591
- row: &crate::storage::GcDelete,
592
- options: RunOptions,
593
- report: &mut RunReport,
594
- ) -> Option<Recovery> {
595
- if options.assume_upstream {
596
- return Some(Recovery::Declared);
597
- }
598
- let cache = store.repo_cache()?;
599
- let kind = match row.kind {
600
- "manifest" => CacheKind::Manifest,
601
- _ => CacheKind::Blob,
602
- };
603
- let want = ObjectRef {
604
- sha256: row.sha256.clone(),
605
- length: row.length,
606
- };
607
- // Present at the exact length is the cheap check and the honest one: the cache's own
608
- // admission path re-verifies the digest on the way back in, so a byte-level check here
609
- // would pay for a promise something else already keeps.
610
- if let Ok(path) = cache.path(kind, &row.sha256) {
611
- if std::fs::metadata(&path).is_ok_and(|meta| meta.is_file() && meta.len() == row.length) {
612
- report.already_cached_objects += 1;
613
- return Some(Recovery::Cache);
614
- }
615
- }
616
- if !options.demote {
617
- return None;
618
- }
619
- match cache.backfill_store(store, kind, &want) {
620
- Ok(CacheWrite::Stored) => {
621
- report.demoted_objects += 1;
622
- report.demoted_bytes = report.demoted_bytes.saturating_add(row.length);
623
- Some(Recovery::Demoted)
624
- }
625
- Ok(CacheWrite::Present) => {
626
- report.already_cached_objects += 1;
627
- Some(Recovery::Cache)
628
- }
629
- // A cache that cannot take the bytes is weather everywhere else in this crate and
630
- // stays weather here — but for an EVICTION the consequence is different. On a fetch
631
- // the authority is still R2; on an eviction this pod may hold the last copy. So a
632
- // failed demote does not become a silent delete, it becomes a refusal.
633
- Ok(CacheWrite::Unavailable) | Err(_) => None,
634
- }
635
- }
636
-
637
- /// Demote, tombstone, collect — in that order, and the order is the argument.
638
- ///
639
- /// 1. **Demote** while the objects are still live and still reachable. Nothing is deleted.
640
- /// 2. **Tombstone** — `DeleteRepository` per victim, through `apply_repository`'s
641
- /// compare-and-set. This is the *explicit act* decisions.md 1135 requires: bytes become
642
- /// collectable because somebody removed the thing that named them, never because a sweep
643
- /// inferred they were garbage.
644
- /// 3. **Collect** — the existing `gc::collect`, unchanged. It recomputes reachability from
645
- /// `repos/`, `manifests/` and `blobs/` alone.
646
- ///
647
- /// **Step 3 is why a wrong plan cannot corrupt the Store.** The plan chooses which ROOTS
648
- /// stop being named; the census decides which OBJECTS that actually frees. A caller that
649
- /// names every root still cannot delete an object a hold protects or a retained manifest
650
- /// needs.
565
+ /// Reclaim named roots only after the caller confirms durable upstream custody.
566
+ /// Optional cache warming precedes repository tombstones and ordinary GC; cache
567
+ /// presence never authorizes either step. Retained roots and active holds still
568
+ /// protect their objects through the existing reachability census.
651
569
  pub fn run(store: &Store, drop: &[RepositoryName], options: RunOptions) -> Result<RunReport> {
652
570
  permitted(store)?;
653
571
  let mut report = RunReport::default();
654
572
  if drop.is_empty() {
655
573
  return Ok(report);
656
574
  }
575
+ if !options.assume_upstream {
576
+ return refuse(
577
+ Code::DURABILITY_UNPROVEN,
578
+ "named local roots require --assume-upstream after confirming durable upstream \
579
+ custody; an optional cache or --demote cannot preserve the only copy",
580
+ );
581
+ }
657
582
 
658
583
  // EXCLUSIVITY IS PROVED BEFORE ANYTHING IS DONE, not discovered at the end.
659
584
  //
@@ -698,26 +623,29 @@ pub fn run(store: &Store, drop: &[RepositoryName], options: RunOptions) -> Resul
698
623
  .filter(|row| !already.contains(&row.key))
699
624
  .collect();
700
625
 
701
- let mut unrecoverable = Vec::new();
702
- for row in &condemned {
703
- if recovery(store, row, options, &mut report).is_none() {
704
- unrecoverable.push(format!("{}:{}", row.kind, row.sha256));
626
+ if options.demote {
627
+ if let Some(cache) = store.repo_cache() {
628
+ for row in &condemned {
629
+ let kind = if row.kind == "manifest" {
630
+ CacheKind::Manifest
631
+ } else {
632
+ CacheKind::Blob
633
+ };
634
+ let object = ObjectRef {
635
+ sha256: row.sha256.clone(),
636
+ length: row.length,
637
+ };
638
+ match cache.backfill_store(store, kind, &object) {
639
+ Ok(CacheWrite::Stored) => {
640
+ report.demoted_objects += 1;
641
+ report.demoted_bytes = report.demoted_bytes.saturating_add(row.length);
642
+ }
643
+ Ok(CacheWrite::Present) => report.already_cached_objects += 1,
644
+ Ok(CacheWrite::Unavailable) | Err(_) => {}
645
+ }
646
+ }
705
647
  }
706
648
  }
707
- if !unrecoverable.is_empty() {
708
- let shown: Vec<&str> = unrecoverable.iter().take(3).map(String::as_str).collect();
709
- return refuse(
710
- Code::DURABILITY_UNPROVEN,
711
- format!(
712
- "{} of {} objects cannot be got back if they go — {} — so nothing was \
713
- dropped; bind a repo cache and pass --demote, or declare the roots \
714
- recoverable with --assume-upstream",
715
- unrecoverable.len(),
716
- condemned.len(),
717
- shown.join(", "),
718
- ),
719
- );
720
- }
721
649
 
722
650
  let fault = crate::store::Fault::default();
723
651
  for repo in drop {
@@ -83,6 +83,11 @@ enum SoftFailure {
83
83
  Unavailable,
84
84
  }
85
85
 
86
+ #[cfg(target_os = "linux")]
87
+ const O_NONBLOCK: i32 = 0o4000;
88
+ #[cfg(target_os = "macos")]
89
+ const O_NONBLOCK: i32 = 0x0004;
90
+
86
91
  impl RepoObjectCache {
87
92
  /// Name an optional cache. Construction performs no I/O and creates no metadata.
88
93
  pub fn new(root: impl Into<PathBuf>) -> Self {
@@ -409,7 +414,10 @@ fn read_status(error: SoftFailure) -> CacheRead {
409
414
  fn open_exact(path: &Path, length: u64) -> std::result::Result<File, SoftFailure> {
410
415
  let file = OpenOptions::new()
411
416
  .read(true)
412
- .custom_flags(store::O_NOFOLLOW)
417
+ // A FIFO open waits for a writer before metadata can reject it. Nonblocking
418
+ // open lets every nonregular entry reach the check below; regular files are
419
+ // unaffected. This does not impose a deadline on hard-mounted NFS I/O.
420
+ .custom_flags(store::O_NOFOLLOW | O_NONBLOCK)
413
421
  .open(path)
414
422
  .map_err(|error| {
415
423
  if error.kind() == std::io::ErrorKind::NotFound {
@@ -1176,61 +1184,4 @@ mod exit_wait_tests {
1176
1184
  );
1177
1185
  fs::remove_dir_all(root).unwrap();
1178
1186
  }
1179
-
1180
- #[test]
1181
- fn stalled_optional_copy_stops_waiting_without_joining_its_worker() {
1182
- let root = std::env::temp_dir().join(format!(
1183
- "tensorfs-exit-stalled-{}-{}",
1184
- std::process::id(),
1185
- store::now_nanos_unique()
1186
- ));
1187
- let source = Store::init(&root.join("source")).unwrap();
1188
- let object = source
1189
- .put_stream(&mut b"still local".as_slice(), None, &Fault::default())
1190
- .unwrap()
1191
- .obj;
1192
- let cache = RepoObjectCache::new(root.join("cache"));
1193
- let fifo = cache.path(CacheKind::Blob, &object.sha256).unwrap();
1194
- fs::create_dir_all(fifo.parent().unwrap()).unwrap();
1195
- assert!(std::process::Command::new("mkfifo")
1196
- .arg(&fifo)
1197
- .status()
1198
- .unwrap()
1199
- .success());
1200
- let activity = Arc::new(BackfillActivity::new(Ledger::with_resolution(
1201
- Duration::from_millis(10),
1202
- )));
1203
- activity.offered();
1204
- let observed = activity.clone();
1205
- let wanted = object.clone();
1206
- let store = source.clone();
1207
- let worker = std::thread::spawn(move || {
1208
- let result =
1209
- cache.backfill_observed(&store, CacheKind::Blob, &wanted, Some(observed.as_ref()));
1210
- observed.finished();
1211
- result
1212
- });
1213
- assert!(
1214
- !activity.wait(),
1215
- "blocked optional cache never produced a no-progress verdict"
1216
- );
1217
- assert!(
1218
- !worker.is_finished(),
1219
- "the wait returned by joining the stalled cache worker"
1220
- );
1221
- assert!(
1222
- source.record_valid(&object.sha256).is_ok(),
1223
- "cache stall invalidated completed local bytes"
1224
- );
1225
- let unblock = OpenOptions::new()
1226
- .read(true)
1227
- .write(true)
1228
- .custom_flags(0x800)
1229
- .open(&fifo)
1230
- .unwrap();
1231
- assert_eq!(worker.join().unwrap().unwrap(), CacheWrite::Unavailable);
1232
- drop(unblock);
1233
- assert!(activity.wait());
1234
- fs::remove_dir_all(root).unwrap();
1235
- }
1236
1187
  }