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.
- {tensorfs-0.3.20 → tensorfs-0.3.22}/Cargo.lock +2 -2
- {tensorfs-0.3.20 → tensorfs-0.3.22}/Cargo.toml +1 -1
- {tensorfs-0.3.20 → tensorfs-0.3.22}/PKG-INFO +1 -1
- tensorfs-0.3.22/crates/tensorfs-core/build.rs +59 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/catalog.rs +77 -1
- tensorfs-0.3.22/crates/tensorfs-core/src/derived/adoption.rs +472 -0
- tensorfs-0.3.22/crates/tensorfs-core/src/derived/graft_tests.rs +212 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/derived/resume_tests.rs +2 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/derived.rs +166 -20
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/gc.rs +1 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/journal.rs +13 -1
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/source.rs +653 -7
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/transaction.rs +3 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/lib.rs +4 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/meta.rs +7 -3
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/providers.rs +115 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/sha256.rs +1 -0
- tensorfs-0.3.22/crates/tensorfs-core/src/source_artifact.rs +567 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/store.rs +20 -3
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/mod.rs +3 -0
- tensorfs-0.3.22/crates/tensorfs-core/src/transport/prefix.rs +505 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/liveness.rs +32 -0
- tensorfs-0.3.22/crates/tensorfs-core/tests/source_artifacts.rs +274 -0
- tensorfs-0.3.22/crates/tensorfs-core/tests/source_prefixes.rs +470 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/staging.rs +22 -3
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-py/src/store.rs +388 -6
- {tensorfs-0.3.20 → tensorfs-0.3.22}/pyproject.toml +1 -1
- {tensorfs-0.3.20 → tensorfs-0.3.22}/python/tensorfs/_ext.pyi +55 -1
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/derived/roots.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/fill.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/repo_cache.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/http.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/policy.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/pull.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/push.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/transport/tests.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/crates/tensorfs-py/src/lib.rs +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/python/tensorfs/__init__.py +0 -0
- {tensorfs-0.3.20 → tensorfs-0.3.22}/python/tensorfs/errors.py +0 -0
- {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.
|
|
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.
|
|
407
|
+
version = "0.3.22"
|
|
408
408
|
dependencies = [
|
|
409
409
|
"pyo3",
|
|
410
410
|
"tensorfs-core",
|
|
@@ -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.
|
|
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
|
+
}
|