tensorfs 0.3.20__tar.gz → 0.3.22__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 (124) hide show
  1. {tensorfs-0.3.20 → tensorfs-0.3.22}/Cargo.lock +2 -2
  2. {tensorfs-0.3.20 → tensorfs-0.3.22}/Cargo.toml +1 -1
  3. {tensorfs-0.3.20 → tensorfs-0.3.22}/PKG-INFO +1 -1
  4. tensorfs-0.3.22/crates/tensorfs-core/build.rs +59 -0
  5. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/catalog.rs +77 -1
  6. tensorfs-0.3.22/crates/tensorfs-core/src/derived/adoption.rs +472 -0
  7. tensorfs-0.3.22/crates/tensorfs-core/src/derived/graft_tests.rs +212 -0
  8. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/derived/resume_tests.rs +2 -0
  9. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/derived.rs +166 -20
  10. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/gc.rs +1 -0
  11. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/journal.rs +13 -1
  12. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/source.rs +653 -7
  13. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/transaction.rs +3 -0
  14. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/lib.rs +4 -0
  15. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/meta.rs +7 -3
  16. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/providers.rs +115 -0
  17. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/sha256.rs +1 -0
  18. tensorfs-0.3.22/crates/tensorfs-core/src/source_artifact.rs +567 -0
  19. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/store.rs +20 -3
  20. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/mod.rs +3 -0
  21. tensorfs-0.3.22/crates/tensorfs-core/src/transport/prefix.rs +505 -0
  22. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/liveness.rs +32 -0
  23. tensorfs-0.3.22/crates/tensorfs-core/tests/source_artifacts.rs +274 -0
  24. tensorfs-0.3.22/crates/tensorfs-core/tests/source_prefixes.rs +470 -0
  25. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/staging.rs +22 -3
  26. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-py/src/store.rs +388 -6
  27. {tensorfs-0.3.20 → tensorfs-0.3.22}/pyproject.toml +1 -1
  28. {tensorfs-0.3.20 → tensorfs-0.3.22}/python/tensorfs/_ext.pyi +55 -1
  29. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/Cargo.toml +0 -0
  30. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  31. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  32. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  33. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  34. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/b64.rs +0 -0
  35. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  36. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  37. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  38. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  39. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  40. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  41. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  42. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  43. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  44. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/canon.rs +0 -0
  45. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/capability.rs +0 -0
  46. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/cbor.rs +0 -0
  47. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  48. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/corpus.rs +0 -0
  49. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  50. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  51. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  52. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/dtype.rs +0 -0
  53. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/durability.rs +0 -0
  54. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/err.rs +0 -0
  55. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/fetch.rs +0 -0
  56. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/fill.rs +0 -0
  57. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/fit.rs +0 -0
  58. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/header.rs +0 -0
  59. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/home.rs +0 -0
  60. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ids.rs +0 -0
  61. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  62. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  63. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  64. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  65. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  66. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  67. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  68. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/jcs.rs +0 -0
  69. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/limits.rs +0 -0
  70. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/machine.rs +0 -0
  71. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/manifest.rs +0 -0
  72. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/project.rs +0 -0
  73. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/read.rs +0 -0
  74. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/receipt.rs +0 -0
  75. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/reclaim.rs +0 -0
  76. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/registry.rs +0 -0
  77. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/relation.rs +0 -0
  78. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  79. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/repository.rs +0 -0
  80. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/spec.rs +0 -0
  81. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/staging.rs +0 -0
  82. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/storage.rs +0 -0
  83. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/api.rs +0 -0
  84. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/http.rs +0 -0
  85. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  86. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  87. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  88. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  89. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/push.rs +0 -0
  90. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  91. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  92. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/vectors.rs +0 -0
  93. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  94. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fit.rs +0 -0
  95. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  96. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  97. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  98. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  99. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  100. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  101. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  102. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  103. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/home.rs +0 -0
  104. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  105. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  106. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  107. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  108. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  109. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  110. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  111. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  112. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  113. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  114. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  115. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  116. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/version.rs +0 -0
  117. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/write_through.rs +0 -0
  118. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-py/Cargo.toml +0 -0
  119. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-py/src/docs.rs +0 -0
  120. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-py/src/errors.rs +0 -0
  121. {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-py/src/lib.rs +0 -0
  122. {tensorfs-0.3.20 → tensorfs-0.3.22}/python/tensorfs/__init__.py +0 -0
  123. {tensorfs-0.3.20 → tensorfs-0.3.22}/python/tensorfs/errors.py +0 -0
  124. {tensorfs-0.3.20 → tensorfs-0.3.22}/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.20"
395
+ version = "0.3.22"
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.20"
407
+ version = "0.3.22"
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.20"
6
+ version = "0.3.22"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.20
3
+ Version: 0.3.22
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -0,0 +1,59 @@
1
+ //! Capture actual native source/dependency/build inputs, including same-version edits.
2
+ #[allow(dead_code)]
3
+ #[path = "src/sha256.rs"]
4
+ mod sha256;
5
+ use std::{env, fs, path::Path};
6
+ fn collect(path: &Path, files: &mut Vec<std::path::PathBuf>) {
7
+ if path.is_dir() {
8
+ for entry in fs::read_dir(path).unwrap() {
9
+ collect(&entry.unwrap().path(), files);
10
+ }
11
+ } else {
12
+ files.push(path.to_path_buf());
13
+ }
14
+ }
15
+ fn main() {
16
+ let mut files = Vec::new();
17
+ for path in [
18
+ "src",
19
+ "../../vectors",
20
+ "Cargo.toml",
21
+ "../../Cargo.toml",
22
+ "../../Cargo.lock",
23
+ "build.rs",
24
+ ] {
25
+ println!("cargo:rerun-if-changed={path}");
26
+ collect(Path::new(path), &mut files);
27
+ }
28
+ files.sort();
29
+ let mut hash = sha256::Sha256::new();
30
+ for file in files {
31
+ let name = file.to_string_lossy();
32
+ let bytes = fs::read(&file).unwrap();
33
+ hash.update(&(name.len() as u64).to_le_bytes());
34
+ hash.update(name.as_bytes());
35
+ hash.update(&(bytes.len() as u64).to_le_bytes());
36
+ hash.update(&bytes);
37
+ }
38
+ for key in [
39
+ "TARGET",
40
+ "PROFILE",
41
+ "OPT_LEVEL",
42
+ "DEBUG",
43
+ "CARGO_ENCODED_RUSTFLAGS",
44
+ ] {
45
+ println!("cargo:rerun-if-env-changed={key}");
46
+ hash.update(key.as_bytes());
47
+ hash.update(env::var(key).unwrap_or_default().as_bytes());
48
+ hash.update(&[0]);
49
+ }
50
+ let rustc = std::process::Command::new(env::var("RUSTC").unwrap())
51
+ .arg("--version")
52
+ .output()
53
+ .unwrap();
54
+ hash.update(&rustc.stdout);
55
+ println!(
56
+ "cargo:rustc-env=TENSORFS_IMPLEMENTATION_SHA256={}",
57
+ sha256::hex(&hash.finish())
58
+ );
59
+ }
@@ -384,6 +384,26 @@ impl Catalog {
384
384
  /// after an explicit catalog rebuild. The returned advisory-lock guard must live for the
385
385
  /// whole resumed writer/install process.
386
386
  pub fn resume_operation(&self, id: &str, org: &str, name: &str) -> Result<WriterGuard> {
387
+ self.resume_operation_inner(id, org, name, true)
388
+ }
389
+
390
+ /// Borrow an existing operation without recreating a released predecessor.
391
+ pub(crate) fn resume_retained_operation(
392
+ &self,
393
+ id: &str,
394
+ org: &str,
395
+ name: &str,
396
+ ) -> Result<WriterGuard> {
397
+ self.resume_operation_inner(id, org, name, false)
398
+ }
399
+
400
+ fn resume_operation_inner(
401
+ &self,
402
+ id: &str,
403
+ org: &str,
404
+ name: &str,
405
+ recreate: bool,
406
+ ) -> Result<WriterGuard> {
387
407
  use rusqlite::OptionalExtension;
388
408
 
389
409
  let root = self.path.parent().ok_or_else(|| Refusal {
@@ -404,6 +424,12 @@ impl Catalog {
404
424
  .optional()
405
425
  .map_err(|error| sql("read operation resume", error))?;
406
426
  match existing {
427
+ None if !recreate => {
428
+ return refuse(
429
+ Code::ROOT_ABSENT,
430
+ "predecessor operation is no longer retained",
431
+ )
432
+ }
407
433
  None => {
408
434
  transaction
409
435
  .execute(
@@ -450,10 +476,32 @@ impl Catalog {
450
476
  &self,
451
477
  operation: &str,
452
478
  request_digest: &str,
479
+ ) -> Result<SourcePreparation> {
480
+ self.begin_source_preparation_inner(operation, request_digest, true)
481
+ }
482
+
483
+ pub(crate) fn begin_retained_source_preparation(
484
+ &self,
485
+ operation: &str,
486
+ request_digest: &str,
487
+ ) -> Result<SourcePreparation> {
488
+ self.begin_source_preparation_inner(operation, request_digest, false)
489
+ }
490
+
491
+ fn begin_source_preparation_inner(
492
+ &self,
493
+ operation: &str,
494
+ request_digest: &str,
495
+ recreate: bool,
453
496
  ) -> Result<SourcePreparation> {
454
497
  source_operation_id(operation)?;
455
498
  let request_digest = crate::ids::prefixed("source preparation request", request_digest)?;
456
- let guard = self.resume_operation(operation, "_tensorfs", "model-source-preparation")?;
499
+ let guard = self.resume_operation_inner(
500
+ operation,
501
+ "_tensorfs",
502
+ "model-source-preparation",
503
+ recreate,
504
+ )?;
457
505
  let connection = self.connection()?;
458
506
  let existing = connection.query_row(
459
507
  "SELECT request_digest,state FROM tensorfs_source_preparations WHERE operation_id=?1",
@@ -470,6 +518,12 @@ impl Catalog {
470
518
  }
471
519
  state
472
520
  }
521
+ Err(rusqlite::Error::QueryReturnedNoRows) if !recreate => {
522
+ return refuse(
523
+ Code::ROOT_ABSENT,
524
+ "source preparation is no longer retained",
525
+ )
526
+ }
473
527
  Err(rusqlite::Error::QueryReturnedNoRows) => {
474
528
  connection
475
529
  .execute(
@@ -1289,6 +1343,28 @@ mod tests {
1289
1343
  let _ = fs::remove_dir_all(root);
1290
1344
  }
1291
1345
 
1346
+ #[test]
1347
+ fn inherited_recovery_descriptor_keeps_gc_fenced_until_child_exit() {
1348
+ use std::process::{Command, Stdio};
1349
+ let root = temporary("inherited-recovery");
1350
+ crate::store::Store::init(&root).unwrap();
1351
+ let writer = WriterGuard::acquire(&root).unwrap();
1352
+ // An actual child retains the same open-file description. Concurrent
1353
+ // spawn/exec can briefly inherit such descriptors even with CLOEXEC.
1354
+ let mut child = Command::new("cat")
1355
+ .stdin(Stdio::piped())
1356
+ .stdout(Stdio::from(writer._recovery.try_clone().unwrap()))
1357
+ .spawn()
1358
+ .unwrap();
1359
+ drop(writer);
1360
+ let refused = WriterGuard::lock_rebuild(&root);
1361
+ drop(child.stdin.take());
1362
+ assert!(child.wait().unwrap().success());
1363
+ assert_eq!(refused.unwrap_err().code, Code::STORE_BUSY);
1364
+ WriterGuard::lock_rebuild(&root).unwrap();
1365
+ fs::remove_dir_all(root).unwrap();
1366
+ }
1367
+
1292
1368
  #[test]
1293
1369
  fn gc_recovery_lock_refuses_live_read_or_derived_hold() {
1294
1370
  let root = temporary("hold-fence");
@@ -0,0 +1,472 @@
1
+ //! Independent custody of checkpointed work for another exact derived declaration.
2
+
3
+ use super::*;
4
+
5
+ /// Adopt a stopped writer's verified progress without copying tensor payloads or
6
+ /// transferring its writer session. The caller owns authorization between requests;
7
+ /// native code binds the complete declaration and independently roots the result.
8
+ #[allow(clippy::too_many_arguments)]
9
+ pub fn adopt_checkpoint(
10
+ store: &Store,
11
+ meta: &Meta,
12
+ transaction: &str,
13
+ source_transaction: &str,
14
+ declaration_bytes: &[u8],
15
+ source_head: &ObjectRef,
16
+ operation: &str,
17
+ slot: &str,
18
+ ) -> Result<(String, crate::durability::Head)> {
19
+ let transaction = transaction_id(transaction)?;
20
+ let source_transaction = transaction_id(source_transaction)?;
21
+ if transaction == source_transaction {
22
+ return refuse(
23
+ Code::TRANSACTION_CONFLICT,
24
+ "partial adoption requires a new transaction",
25
+ );
26
+ }
27
+ let mut declaration = Declaration::from_value(&crate::canon::parse_canonical(
28
+ declaration_bytes,
29
+ limits::DOC_MAX_BYTES,
30
+ )?)?;
31
+ let canonical = declaration.canonical_work_bytes()?;
32
+ if canonical != declaration_bytes {
33
+ return refuse(
34
+ Code::TRANSACTION_CONFLICT,
35
+ "partial adoption changed its declaration",
36
+ );
37
+ }
38
+ let text = String::from_utf8(canonical).expect("canonical JSON is UTF-8");
39
+ let hold = meta.acquire_hold("derived-adoption")?;
40
+ let result = (|| {
41
+ require_source_roots(store, &declaration)?;
42
+ let sources = load_sources(store, &declaration)?;
43
+ let facts: Vec<_> = sources.iter().map(|source| source.fact.clone()).collect();
44
+ let rows = meta.derived_rows()?;
45
+ let existing = rows.iter().find(|row| row.id == transaction);
46
+ if existing.is_some_and(|row| {
47
+ row.state != TransactionState::Open
48
+ || row.declaration.as_deref() != Some(&text)
49
+ || row.source_facts != facts
50
+ }) {
51
+ return refuse(
52
+ Code::TRANSACTION_CONFLICT,
53
+ "adoption recipient changed or closed",
54
+ );
55
+ }
56
+
57
+ let recipient_root = roots::read(store, &transaction)?;
58
+ if recipient_root
59
+ .as_ref()
60
+ .is_some_and(|root| root.restore_epoch.is_some())
61
+ {
62
+ return refuse(
63
+ Code::WRITER_FENCED,
64
+ "adoption recipient has an active restore fence",
65
+ );
66
+ }
67
+ // The filesystem root is committed before the metadata row. Recover that
68
+ // boundary even when the original request has since released its custody.
69
+ if let Some(head) = recipient_root
70
+ .as_ref()
71
+ .and_then(|root| root.checkpoint.as_ref())
72
+ {
73
+ let recovered = read_progress(store, head, &transaction, &text, &facts)?;
74
+ validate_chain(store, head, &recovered, operation, slot)?;
75
+ for object in added_objects(&recovered) {
76
+ require_payload_record(store, &object, "adopted progress")?;
77
+ }
78
+ build_header(
79
+ &declaration,
80
+ &sources,
81
+ &recovered.added_parts,
82
+ &recovered.added_configs,
83
+ true,
84
+ )?;
85
+ let result = observation(store, head.clone())?;
86
+ let mut txn = meta.txn()?;
87
+ if txn.rows.iter().find(|row| row.id == transaction) != existing
88
+ || roots::read(store, &transaction)? != recipient_root
89
+ {
90
+ return refuse(
91
+ Code::TRANSACTION_CONFLICT,
92
+ "adoption recipient changed during recovery",
93
+ );
94
+ }
95
+ if existing.is_none() {
96
+ retain_progress(store, &recovered, Some(head))?;
97
+ txn.rows.push(recovered);
98
+ txn.commit()?;
99
+ }
100
+ return Ok(result);
101
+ }
102
+ if existing.is_some() || recipient_root.is_some() {
103
+ return refuse(
104
+ Code::TRANSACTION_CONFLICT,
105
+ "adoption recipient already owns different work",
106
+ );
107
+ }
108
+ let source = rows
109
+ .iter()
110
+ .find(|row| row.id == source_transaction)
111
+ .ok_or_else(|| Refusal {
112
+ code: Code::ROOT_ABSENT,
113
+ detail: "partial source has no retained transaction".into(),
114
+ })?;
115
+ if source.state != TransactionState::Open || source.current_session.is_some() {
116
+ return refuse(
117
+ Code::WRITER_FENCED,
118
+ "partial source writer must be stopped before adoption",
119
+ );
120
+ }
121
+ if source.declaration.as_deref() != Some(&text) || source.source_facts != facts {
122
+ return refuse(
123
+ Code::TRANSACTION_CONFLICT,
124
+ "partial source has different work or inputs",
125
+ );
126
+ }
127
+ let root = roots::read(store, &source_transaction)?.ok_or_else(|| Refusal {
128
+ code: Code::ROOT_ABSENT,
129
+ detail: "partial source no longer has native custody".into(),
130
+ })?;
131
+ if root.restore_epoch.is_some() {
132
+ return refuse(
133
+ Code::WRITER_FENCED,
134
+ "partial source has an active restore fence",
135
+ );
136
+ }
137
+ let current = root.checkpoint.as_ref().ok_or_else(|| Refusal {
138
+ code: Code::ROOT_ABSENT,
139
+ detail: "partial source has no completed checkpoint".into(),
140
+ })?;
141
+ let link = crate::durability::local_link(store, source_head)?;
142
+ let plan = progress_plan(source);
143
+ let selected = crate::durability::checkpoint_predecessor(
144
+ store,
145
+ &crate::durability::Policy {
146
+ chain: &link.operation,
147
+ operation: &source_transaction,
148
+ plan: &plan,
149
+ interval: crate::durability::INTERVAL_BYTES,
150
+ },
151
+ Some(current),
152
+ Some(source_head),
153
+ )?
154
+ .expect("source checkpoint supplied");
155
+ let incoming = read_progress(store, &selected, &source_transaction, &text, &facts)?;
156
+ let mut recipient = incoming.clone();
157
+ recipient.id = transaction.clone();
158
+ recipient.added_parts.clear();
159
+ recipient.added_configs.clear();
160
+ merge_progress(store, &mut recipient, Some(&incoming))?;
161
+ build_header(
162
+ &declaration,
163
+ &sources,
164
+ &recipient.added_parts,
165
+ &recipient.added_configs,
166
+ true,
167
+ )?;
168
+ let (plan, exported) = export(store, &recipient, operation, slot)?;
169
+ let mut txn = meta.txn()?;
170
+ if txn.rows.iter().find(|row| row.id == source_transaction) != Some(source)
171
+ || txn.rows.iter().any(|row| row.id == transaction)
172
+ || roots::read(store, &source_transaction)?.as_ref() != Some(&root)
173
+ || roots::read(store, &transaction)? != recipient_root
174
+ {
175
+ return refuse(
176
+ Code::TRANSACTION_CONFLICT,
177
+ "partial source or recipient changed during adoption",
178
+ );
179
+ }
180
+ retain_progress(store, &recipient, exported.head.as_ref())?;
181
+ txn.rows.push(recipient);
182
+ txn.commit()?;
183
+ Ok((plan, exported))
184
+ })();
185
+ hold.release(meta)?;
186
+ result
187
+ }
188
+
189
+ fn validate_chain(
190
+ store: &Store,
191
+ head: &ObjectRef,
192
+ row: &TransactionRow,
193
+ operation: &str,
194
+ slot: &str,
195
+ ) -> Result<()> {
196
+ let link = crate::durability::local_link(store, head)?;
197
+ if link.operation != format!("{operation}/{slot}") || link.plan != progress_plan(row) {
198
+ return refuse(
199
+ Code::TRANSACTION_CONFLICT,
200
+ "adopted checkpoint changed its recipient binding",
201
+ );
202
+ }
203
+ Ok(())
204
+ }
205
+
206
+ fn observation(store: &Store, head: ObjectRef) -> Result<(String, crate::durability::Head)> {
207
+ let link = crate::durability::local_link(store, &head)?;
208
+ let chain = crate::durability::local_chain(store, &head, &link.plan)?;
209
+ Ok((
210
+ link.plan,
211
+ crate::durability::Head {
212
+ head: Some(head),
213
+ links: chain.links,
214
+ bytes: chain.bytes,
215
+ },
216
+ ))
217
+ }
218
+
219
+ fn export(
220
+ store: &Store,
221
+ row: &TransactionRow,
222
+ operation: &str,
223
+ slot: &str,
224
+ ) -> Result<(String, crate::durability::Head)> {
225
+ let plan = progress_plan(row);
226
+ let chain = format!("{operation}/{slot}");
227
+ ascii_name("checkpoint operation", &chain, limits::MAX_NAME_BYTES)?;
228
+ let work_bytes = crate::canon::write(&progress::context(row).to_value());
229
+ let work = ObjectRef::of(&work_bytes);
230
+ store.put_stream(&mut work_bytes.as_slice(), Some(&work), &Default::default())?;
231
+ let delta = progress::Delta::between(row, None, work.clone())?;
232
+ let mut objects = added_objects(row);
233
+ objects.push(work);
234
+ let exported = crate::durability::checkpoint_bytes(
235
+ store,
236
+ &crate::durability::Policy {
237
+ chain: &chain,
238
+ operation: &row.id,
239
+ plan: &plan,
240
+ interval: crate::durability::INTERVAL_BYTES,
241
+ },
242
+ None,
243
+ &crate::canon::write(&delta.to_value()),
244
+ objects,
245
+ None,
246
+ )?;
247
+ Ok((plan, exported.head))
248
+ }
249
+
250
+ #[cfg(test)]
251
+ mod tests {
252
+ use super::*;
253
+
254
+ fn fixture() -> (Store, Meta, Declaration, String, String) {
255
+ let path = std::env::temp_dir().join(format!(
256
+ "tensorfs-partial-adopt-{}",
257
+ crate::meta::now_nanos_unique()
258
+ ));
259
+ let store = Store::init(&path).unwrap();
260
+ let meta = Meta::open(&store).unwrap();
261
+ let mut declaration = super::super::tests::created_declaration();
262
+ let mut second = declaration.components[0].add[0].clone();
263
+ second.key = "next".into();
264
+ declaration.components[0].add.push(second);
265
+ declaration.order.push(("model".into(), "next".into()));
266
+ declaration.max_new_bytes *= 2;
267
+ (
268
+ store,
269
+ meta,
270
+ declaration,
271
+ format!("sha256:{}", "a1".repeat(32)),
272
+ format!("sha256:{}", "a2".repeat(32)),
273
+ )
274
+ }
275
+
276
+ fn write(store: &Store, meta: &Meta, id: &str, key: &str, byte: u8) -> ObjectRef {
277
+ let written = add_part(
278
+ store,
279
+ meta,
280
+ id,
281
+ 1,
282
+ ("model", key, "value"),
283
+ &mut vec![byte; 2048].as_slice(),
284
+ )
285
+ .unwrap();
286
+ written.part.segments()[0].clone()
287
+ }
288
+
289
+ #[test]
290
+ fn independent_partial_adoption_survives_producer_release_and_finishes_exactly() {
291
+ let (store, meta, mut declaration, source, target) = fixture();
292
+ let bytes = declaration.canonical_work_bytes().unwrap();
293
+ let writer = begin(&store, &meta, &source, 1, declaration.clone(), None).unwrap();
294
+ let object = write(&store, &meta, &source, "weight", 3);
295
+ let (_, progress) =
296
+ checkpoint_progress(&store, &meta, &source, 1, "old", "output", None).unwrap();
297
+ // An add after the durable checkpoint is deliberately not adopted.
298
+ write(&store, &meta, &source, "next", 9);
299
+ let head = progress.head.unwrap();
300
+ assert_eq!(
301
+ adopt_checkpoint(&store, &meta, &target, &source, &bytes, &head, "new", "output")
302
+ .unwrap_err()
303
+ .code,
304
+ Code::WRITER_FENCED
305
+ );
306
+ fence(&meta, &source, 1).unwrap();
307
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
308
+ let (plan, adopted) = adopt_checkpoint(
309
+ &store, &meta, &target, &source, &bytes, &head, "new", "output",
310
+ )
311
+ .unwrap();
312
+ assert_ne!(adopted.head.as_ref(), Some(&head));
313
+ validate_checkpoint(
314
+ &store,
315
+ &meta,
316
+ &target,
317
+ &bytes,
318
+ adopted.head.as_ref().unwrap(),
319
+ "new",
320
+ "output",
321
+ &plan,
322
+ )
323
+ .unwrap();
324
+ abandon(&store, &meta, &source).unwrap();
325
+ crate::gc::collect(store.root(), false).unwrap();
326
+ assert!(store.contains(&object.sha256));
327
+ let (_, replay) = adopt_checkpoint(
328
+ &store, &meta, &target, &source, &bytes, &head, "new", "output",
329
+ )
330
+ .unwrap();
331
+ assert_eq!(replay.head, adopted.head);
332
+ let next = begin(
333
+ &store,
334
+ &meta,
335
+ &target,
336
+ 1,
337
+ declaration.clone(),
338
+ adopted.head.as_ref(),
339
+ )
340
+ .unwrap();
341
+ assert_eq!(
342
+ completed(&meta, &target, 1).unwrap().0,
343
+ vec![("model".into(), "weight".into(), "value".into())]
344
+ );
345
+ write(&store, &meta, &target, "next", 4);
346
+ let result = commit(&store, &meta, &target, 1).unwrap();
347
+ release_guards(&meta, Some(next.writer_hold), next.source_leases);
348
+ let control = format!("sha256:{}", "a3".repeat(32));
349
+ let clean = begin(&store, &meta, &control, 1, declaration, None).unwrap();
350
+ write(&store, &meta, &control, "weight", 3);
351
+ write(&store, &meta, &control, "next", 4);
352
+ assert_eq!(
353
+ commit(&store, &meta, &control, 1).unwrap().manifest,
354
+ result.manifest
355
+ );
356
+ release_guards(&meta, Some(clean.writer_hold), clean.source_leases);
357
+ }
358
+
359
+ #[test]
360
+ fn different_implementation_or_closed_recipient_cannot_adopt() {
361
+ let (store, meta, mut declaration, source, target) = fixture();
362
+ let bytes = declaration.canonical_work_bytes().unwrap();
363
+ let writer = begin(&store, &meta, &source, 1, declaration.clone(), None).unwrap();
364
+ write(&store, &meta, &source, "weight", 3);
365
+ let (_, progress) =
366
+ checkpoint_progress(&store, &meta, &source, 1, "old", "output", None).unwrap();
367
+ fence(&meta, &source, 1).unwrap();
368
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
369
+ let head = progress.head.unwrap();
370
+ declaration.work_fingerprint = Some(format!("sha256:{}", "ff".repeat(32)));
371
+ assert_eq!(
372
+ adopt_checkpoint(
373
+ &store,
374
+ &meta,
375
+ &target,
376
+ &source,
377
+ &declaration.canonical_work_bytes().unwrap(),
378
+ &head,
379
+ "new",
380
+ "output"
381
+ )
382
+ .unwrap_err()
383
+ .code,
384
+ Code::TRANSACTION_CONFLICT
385
+ );
386
+ assert!(matches!(
387
+ lookup(&store, &meta, &target).unwrap(),
388
+ Lookup::Absent
389
+ ));
390
+ abandon(&store, &meta, &target).unwrap();
391
+ assert_eq!(
392
+ adopt_checkpoint(&store, &meta, &target, &source, &bytes, &head, "new", "output")
393
+ .unwrap_err()
394
+ .code,
395
+ Code::TRANSACTION_CONFLICT
396
+ );
397
+ }
398
+
399
+ #[test]
400
+ fn root_before_metadata_commit_recovery_does_not_need_the_original_owner() {
401
+ let (store, meta, mut declaration, source, target) = fixture();
402
+ let bytes = declaration.canonical_work_bytes().unwrap();
403
+ let writer = begin(&store, &meta, &source, 1, declaration.clone(), None).unwrap();
404
+ let object = write(&store, &meta, &source, "weight", 3);
405
+ let (_, progress) =
406
+ checkpoint_progress(&store, &meta, &source, 1, "old", "output", None).unwrap();
407
+ fence(&meta, &source, 1).unwrap();
408
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
409
+ let head = progress.head.unwrap();
410
+ let (_, adopted) = adopt_checkpoint(
411
+ &store, &meta, &target, &source, &bytes, &head, "new", "output",
412
+ )
413
+ .unwrap();
414
+ // Reproduce the durable root / uncommitted SQL boundary exactly.
415
+ let db = rusqlite::Connection::open(crate::catalog::Catalog::path(store.root())).unwrap();
416
+ db.execute(
417
+ "DELETE FROM tensorfs_derived_transactions WHERE id=?1",
418
+ [&target],
419
+ )
420
+ .unwrap();
421
+ drop(db);
422
+ abandon(&store, &meta, &source).unwrap();
423
+ crate::gc::collect(store.root(), false).unwrap();
424
+ let (_, replay) = adopt_checkpoint(
425
+ &store, &meta, &target, &source, &bytes, &head, "new", "output",
426
+ )
427
+ .unwrap();
428
+ assert_eq!(replay.head, adopted.head);
429
+ assert!(store.contains(&object.sha256));
430
+ let writer = begin(&store, &meta, &target, 1, declaration, replay.head.as_ref()).unwrap();
431
+ assert_eq!(completed(&meta, &target, 1).unwrap().0.len(), 1);
432
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
433
+ }
434
+
435
+ #[test]
436
+ fn replay_refuses_missing_or_corrupt_payload_and_preserves_restore_fence() {
437
+ for mode in ["missing", "corrupt", "restore"] {
438
+ let (store, meta, mut declaration, source, target) = fixture();
439
+ let bytes = declaration.canonical_work_bytes().unwrap();
440
+ let writer = begin(&store, &meta, &source, 1, declaration, None).unwrap();
441
+ let object = write(&store, &meta, &source, "weight", 3);
442
+ let (_, progress) =
443
+ checkpoint_progress(&store, &meta, &source, 1, "old", "output", None).unwrap();
444
+ fence(&meta, &source, 1).unwrap();
445
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
446
+ let head = progress.head.unwrap();
447
+ adopt_checkpoint(
448
+ &store, &meta, &target, &source, &bytes, &head, "new", "output",
449
+ )
450
+ .unwrap();
451
+ match mode {
452
+ "missing" => std::fs::remove_file(store.blob_path(&object.sha256)).unwrap(),
453
+ "corrupt" => {
454
+ std::fs::remove_file(store.blob_path(&object.sha256)).unwrap();
455
+ std::fs::write(store.blob_path(&object.sha256), vec![7; 2048]).unwrap();
456
+ }
457
+ _ => retain_restore(&store, &meta, &target, 2, None).unwrap(),
458
+ }
459
+ let error = adopt_checkpoint(
460
+ &store, &meta, &target, &source, &bytes, &head, "new", "output",
461
+ )
462
+ .unwrap_err();
463
+ if mode == "restore" {
464
+ assert_eq!(error.code, Code::WRITER_FENCED);
465
+ assert_eq!(
466
+ roots::read(&store, &target).unwrap().unwrap().restore_epoch,
467
+ Some(2)
468
+ );
469
+ }
470
+ }
471
+ }
472
+ }