tensorfs 0.3.69__tar.gz → 0.3.71__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.69 → tensorfs-0.3.71}/Cargo.lock +2 -2
- {tensorfs-0.3.69 → tensorfs-0.3.71}/Cargo.toml +1 -1
- {tensorfs-0.3.69 → tensorfs-0.3.71}/PKG-INFO +1 -1
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/ingest.rs +15 -7
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/watch.rs +4 -3
- tensorfs-0.3.71/crates/tensorfs-core/src/catalog/lock.rs +121 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/catalog.rs +108 -31
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/companion_tests.rs +44 -0
- tensorfs-0.3.71/crates/tensorfs-core/src/derived/group_commit_tests.rs +114 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/roots.rs +44 -26
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived.rs +68 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/fill.rs +95 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/convert.rs +148 -5
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/fingerprint.rs +5 -2
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/journal.rs +10 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/mod.rs +2 -0
- tensorfs-0.3.71/crates/tensorfs-core/src/ingest/narrow.rs +178 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/preflight.rs +3 -3
- tensorfs-0.3.71/crates/tensorfs-core/src/ingest/routes/sdxl.routes.json +1 -0
- tensorfs-0.3.71/crates/tensorfs-core/src/ingest/routes/sdxl.spec.json +51 -0
- tensorfs-0.3.71/crates/tensorfs-core/src/ingest/routes.rs +382 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/source.rs +184 -16
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/transaction.rs +76 -21
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/meta.rs +1 -6
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/providers.rs +93 -4
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/store.rs +18 -1
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/pull.rs +7 -33
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/unsynced.rs +17 -0
- tensorfs-0.3.71/crates/tensorfs-core/tests/as_is.rs +334 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +18 -2
- tensorfs-0.3.71/crates/tensorfs-core/tests/routed_single_file.rs +297 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/source_plan.rs +35 -14
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-py/src/ingest.rs +27 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-py/src/lib.rs +2 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-py/src/store.rs +21 -10
- {tensorfs-0.3.69 → tensorfs-0.3.71}/pyproject.toml +1 -1
- {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/__init__.py +4 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/_ext.pyi +8 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/asset_update.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/cache_roots.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/part_rows_tests.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/descriptors.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/disk.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/gc.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/lib.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/repo_cache.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/source_artifact.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/stats.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/http.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/memory.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/mod.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/pace.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/policy.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/push.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/transport/tests.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/source_download.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/trust_index.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/_derived_channel.py +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/_source_channel.py +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/_tensors.py +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/derived.py +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/errors.py +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.71}/python/tensorfs/py.typed +0 -0
|
@@ -439,7 +439,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
|
439
439
|
|
|
440
440
|
[[package]]
|
|
441
441
|
name = "tensorfs-core"
|
|
442
|
-
version = "0.3.
|
|
442
|
+
version = "0.3.71"
|
|
443
443
|
dependencies = [
|
|
444
444
|
"fs2",
|
|
445
445
|
"libc",
|
|
@@ -454,7 +454,7 @@ dependencies = [
|
|
|
454
454
|
|
|
455
455
|
[[package]]
|
|
456
456
|
name = "tensorfs-py"
|
|
457
|
-
version = "0.3.
|
|
457
|
+
version = "0.3.71"
|
|
458
458
|
dependencies = [
|
|
459
459
|
"pyo3",
|
|
460
460
|
"tensorfs-core",
|
|
@@ -445,7 +445,7 @@ pub fn cmd_bank(dialect: &str, converter: &str, f: &Flags) -> ExitCode {
|
|
|
445
445
|
let mut reviewed = Vec::new();
|
|
446
446
|
for (component, path) in &srcs {
|
|
447
447
|
let (full, raw) = ok!(source_core::read_carrier_at(path));
|
|
448
|
-
let h = if conv.
|
|
448
|
+
let h = if conv.dialect == "safetensors.single_file" {
|
|
449
449
|
ok!(convert::single_file_component(component, &full))
|
|
450
450
|
} else {
|
|
451
451
|
full
|
|
@@ -584,11 +584,11 @@ pub fn cmd_bank(dialect: &str, converter: &str, f: &Flags) -> ExitCode {
|
|
|
584
584
|
.collect::<Vec<_>>()
|
|
585
585
|
.join(",");
|
|
586
586
|
let variant = flag(f, "spec-variant").and_then(|value| value.parse().ok());
|
|
587
|
-
let mut prepared = match source_core::
|
|
587
|
+
let mut prepared = match source_core::prepare(
|
|
588
588
|
&target,
|
|
589
589
|
&unnamed(&srcs),
|
|
590
590
|
&CarrierSet::default(),
|
|
591
|
-
®,
|
|
591
|
+
Some(®),
|
|
592
592
|
variant,
|
|
593
593
|
) {
|
|
594
594
|
Ok(prepared) => prepared,
|
|
@@ -849,17 +849,22 @@ fn prepare_closed_source_plan(
|
|
|
849
849
|
if closed.registry != BUILTIN_REGISTRY {
|
|
850
850
|
flags.push(("registry".to_string(), closed.registry.clone()));
|
|
851
851
|
}
|
|
852
|
+
let as_is = closed.converter == convert::IDENTITY;
|
|
853
|
+
if as_is {
|
|
854
|
+
flags.push(("source-profile".to_string(), source_core::AS_IS.to_string()));
|
|
855
|
+
}
|
|
852
856
|
for (component, source) in &closed.sources {
|
|
853
857
|
flags.push((
|
|
854
858
|
"source".to_string(),
|
|
855
859
|
format!("{component}={}", source.path.to_string_lossy()),
|
|
856
860
|
));
|
|
857
861
|
}
|
|
858
|
-
|
|
862
|
+
// The plan was re-derived above, so its converter is the planner's: as-is has no registry.
|
|
863
|
+
let mut prepared = source_core::prepare(
|
|
859
864
|
&closed.target,
|
|
860
865
|
&closed.sources,
|
|
861
866
|
&closed.set,
|
|
862
|
-
®istry,
|
|
867
|
+
(!as_is).then_some(®istry),
|
|
863
868
|
None,
|
|
864
869
|
)
|
|
865
870
|
.map_err(bail)?;
|
|
@@ -989,15 +994,17 @@ pub(crate) fn construction_order(f: &Flags) -> Result<Vec<(String, String)>, Exi
|
|
|
989
994
|
|
|
990
995
|
/// Headers in, plan out. The one function `plan`, `run` and the worker all go through, so
|
|
991
996
|
/// a preview and a run cannot disagree about what was going to happen.
|
|
997
|
+
/// `--source-profile as-is/1` stores the sources as they are, unclassified.
|
|
992
998
|
fn prepare(f: &Flags, target: &str) -> Result<Prepared, ExitCode> {
|
|
993
999
|
let sources = sources(f)?;
|
|
994
1000
|
let registry = load_registry(f)?;
|
|
995
1001
|
let variant = flag(f, "spec-variant").and_then(|value| value.parse().ok());
|
|
996
|
-
source_core::
|
|
1002
|
+
let as_is = flag(f, "source-profile") == Some(source_core::AS_IS);
|
|
1003
|
+
source_core::prepare(
|
|
997
1004
|
target,
|
|
998
1005
|
&unnamed(&sources),
|
|
999
1006
|
&CarrierSet::default(),
|
|
1000
|
-
®istry,
|
|
1007
|
+
(!as_is).then_some(®istry),
|
|
1001
1008
|
variant,
|
|
1002
1009
|
)
|
|
1003
1010
|
.map_err(bail)
|
|
@@ -1206,6 +1213,7 @@ pub fn cmd_run(root: &Path, target: &str, f: &Flags) -> ExitCode {
|
|
|
1206
1213
|
| "tenant"
|
|
1207
1214
|
| "isolation-tier"
|
|
1208
1215
|
| "checkpoint-bytes"
|
|
1216
|
+
| "source-profile"
|
|
1209
1217
|
) {
|
|
1210
1218
|
argv.push(format!("--{k}"));
|
|
1211
1219
|
argv.push(v.clone());
|
|
@@ -29,8 +29,6 @@ use std::process::{Child, ChildStdout, ExitStatus};
|
|
|
29
29
|
use std::sync::mpsc::{self, RecvTimeoutError};
|
|
30
30
|
use std::time::{Duration, Instant};
|
|
31
31
|
|
|
32
|
-
use tensorfs_core::catalog::BUSY_TIMEOUT;
|
|
33
|
-
|
|
34
32
|
/// How many times its own worst silence a worker is granted before the parent looks for
|
|
35
33
|
/// inactivity, and how many times its own projected run it may spin without progress.
|
|
36
34
|
/// Three doublings past the demonstrated maximum: the worst of n observed gaps
|
|
@@ -101,8 +99,11 @@ fn parse_progress(line: &str) -> Option<(u64, u64)> {
|
|
|
101
99
|
Some((moved.parse().ok()?, admitted.parse().ok()?))
|
|
102
100
|
}
|
|
103
101
|
|
|
102
|
+
/// Patience before a worker has shown any gap to measure against.
|
|
103
|
+
const FIRST_PATIENCE: Duration = Duration::from_secs(5);
|
|
104
|
+
|
|
104
105
|
fn patience(worst_gap: Duration) -> Duration {
|
|
105
|
-
(worst_gap * PATIENCE_FACTOR).max(
|
|
106
|
+
(worst_gap * PATIENCE_FACTOR).max(FIRST_PATIENCE)
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
/// What a silent-but-active worker is allowed: the whole declared input at the rate this
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
//! Waiting for another process's catalog lock by whether its holder can still release it.
|
|
2
|
+
//!
|
|
3
|
+
//! SQLite's locks are POSIX locks: they die with the process that holds them. A holder
|
|
4
|
+
//! that is alive is committing or about to, so a writer waits for it for as long as it
|
|
5
|
+
//! takes; a flat deadline refused a busy, progressing writer as if it were dead. The only
|
|
6
|
+
//! holder that cannot release is one that is stopped (a debugger, SIGSTOP) or a zombie,
|
|
7
|
+
//! and only then does a statement give up.
|
|
8
|
+
//!
|
|
9
|
+
//! Holders come from `/proc/locks`, matched by the catalog's inode (and its WAL `-shm`).
|
|
10
|
+
//! The catalog file itself is never opened here: closing a second descriptor on it would
|
|
11
|
+
//! drop every POSIX lock this process holds on it.
|
|
12
|
+
|
|
13
|
+
use std::cell::RefCell;
|
|
14
|
+
use std::path::{Path, PathBuf};
|
|
15
|
+
use std::time::Duration;
|
|
16
|
+
|
|
17
|
+
use rusqlite::Connection;
|
|
18
|
+
|
|
19
|
+
use crate::err::Result;
|
|
20
|
+
|
|
21
|
+
thread_local! {
|
|
22
|
+
/// The catalog this thread's statements wait on. A connection is opened for each
|
|
23
|
+
/// operation just before it is used, on the thread that uses it, so the one opened last
|
|
24
|
+
/// here is the one a busy statement belongs to.
|
|
25
|
+
static WAITING_ON: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Install the waiting handler on one connection.
|
|
29
|
+
pub(crate) fn wait_for_live_holders(connection: &Connection, path: &Path) -> Result<()> {
|
|
30
|
+
WAITING_ON.with(|waiting| *waiting.borrow_mut() = Some(path.to_path_buf()));
|
|
31
|
+
connection
|
|
32
|
+
.busy_handler(Some(busy))
|
|
33
|
+
.map_err(|error| super::sql("install catalog lock wait", error))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn busy(attempt: i32) -> bool {
|
|
37
|
+
let path = WAITING_ON.with(|waiting| waiting.borrow().clone());
|
|
38
|
+
if path.is_some_and(|path| holders_cannot_release(&path)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
std::thread::sleep(Duration::from_millis(attempt.clamp(1, 64) as u64));
|
|
42
|
+
true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Every process holding a write lock on the catalog is stopped or a zombie. Without a
|
|
46
|
+
/// holder to name (the lock just went, or no `/proc`), keep asking.
|
|
47
|
+
fn holders_cannot_release(path: &Path) -> bool {
|
|
48
|
+
let inodes: Vec<u64> = [path.to_path_buf(), shm(path)]
|
|
49
|
+
.iter()
|
|
50
|
+
.filter_map(|file| std::fs::metadata(file).ok())
|
|
51
|
+
.map(|metadata| std::os::unix::fs::MetadataExt::ino(&metadata))
|
|
52
|
+
.collect();
|
|
53
|
+
let Ok(locks) = std::fs::read_to_string("/proc/locks") else {
|
|
54
|
+
return false;
|
|
55
|
+
};
|
|
56
|
+
let holders = write_holders(&locks, &inodes, std::process::id());
|
|
57
|
+
!holders.is_empty() && holders.iter().all(|pid| stopped(*pid))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn shm(path: &Path) -> PathBuf {
|
|
61
|
+
let mut name = path.as_os_str().to_owned();
|
|
62
|
+
name.push("-shm");
|
|
63
|
+
PathBuf::from(name)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Other processes' POSIX write locks on these inodes, from `/proc/locks` lines such as
|
|
67
|
+
/// `3: POSIX ADVISORY WRITE 167671 103:02:204736108 1073741824 1073742335`.
|
|
68
|
+
fn write_holders(locks: &str, inodes: &[u64], own: u32) -> Vec<u32> {
|
|
69
|
+
locks
|
|
70
|
+
.lines()
|
|
71
|
+
.filter_map(|line| {
|
|
72
|
+
let fields: Vec<&str> = line.split_whitespace().collect();
|
|
73
|
+
let at = fields.iter().position(|field| *field == "POSIX")?;
|
|
74
|
+
if fields.get(at + 2) != Some(&"WRITE") {
|
|
75
|
+
return None;
|
|
76
|
+
}
|
|
77
|
+
let pid: u32 = fields.get(at + 3)?.parse().ok()?;
|
|
78
|
+
let inode: u64 = fields.get(at + 4)?.rsplit(':').next()?.parse().ok()?;
|
|
79
|
+
(pid != own && inodes.contains(&inode)).then_some(pid)
|
|
80
|
+
})
|
|
81
|
+
.collect()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Stopped, traced-stopped, zombie or dead: a process that cannot release its lock.
|
|
85
|
+
fn stopped(pid: u32) -> bool {
|
|
86
|
+
std::fs::read_to_string(format!("/proc/{pid}/stat"))
|
|
87
|
+
.ok()
|
|
88
|
+
.and_then(|stat| {
|
|
89
|
+
let state = stat
|
|
90
|
+
.rsplit_once(')')?
|
|
91
|
+
.1
|
|
92
|
+
.split_whitespace()
|
|
93
|
+
.next()?
|
|
94
|
+
.to_string();
|
|
95
|
+
Some(matches!(state.as_str(), "T" | "t" | "Z" | "X"))
|
|
96
|
+
})
|
|
97
|
+
.unwrap_or(false)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[cfg(test)]
|
|
101
|
+
mod tests {
|
|
102
|
+
use super::*;
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn only_other_processes_write_locks_on_the_catalog_are_holders() {
|
|
106
|
+
let locks = "\
|
|
107
|
+
1: POSIX ADVISORY WRITE 4242 103:02:77 1073741824 1073742335
|
|
108
|
+
2: POSIX ADVISORY READ 5151 103:02:77 1073741826 1073742335
|
|
109
|
+
3: POSIX ADVISORY WRITE 6161 103:02:99 120 120
|
|
110
|
+
4: FLOCK ADVISORY WRITE 7171 103:02:77 0 EOF
|
|
111
|
+
5: POSIX ADVISORY WRITE 1000 103:02:77 0 EOF
|
|
112
|
+
6: -> POSIX ADVISORY WRITE 8181 103:02:77 0 EOF
|
|
113
|
+
";
|
|
114
|
+
assert_eq!(write_holders(locks, &[77], 1000), vec![4242, 8181]);
|
|
115
|
+
assert_eq!(
|
|
116
|
+
write_holders(locks, &[77, 99], 1000),
|
|
117
|
+
vec![4242, 6161, 8181]
|
|
118
|
+
);
|
|
119
|
+
assert!(write_holders(locks, &[5], 1000).is_empty());
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -6,7 +6,6 @@ use std::fs::{self, File};
|
|
|
6
6
|
use std::os::unix::fs::MetadataExt;
|
|
7
7
|
use std::path::{Path, PathBuf};
|
|
8
8
|
use std::sync::{Arc, Mutex, OnceLock, RwLock, Weak};
|
|
9
|
-
use std::time::Duration;
|
|
10
9
|
|
|
11
10
|
use fs2::FileExt;
|
|
12
11
|
use rusqlite::{params, Connection};
|
|
@@ -14,10 +13,7 @@ use rusqlite::{params, Connection};
|
|
|
14
13
|
use crate::err::{refuse, Code, Refusal, Result};
|
|
15
14
|
use crate::storage::Projection;
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
/// the only wait a store operation can legitimately spend blocked and idle, so it is also
|
|
19
|
-
/// the floor under any patience a supervisor grants a worker that has gone quiet.
|
|
20
|
-
pub const BUSY_TIMEOUT: Duration = Duration::from_secs(5);
|
|
16
|
+
mod lock;
|
|
21
17
|
|
|
22
18
|
/// The catalog this build writes. A newer build may only ADD tables and columns (an
|
|
23
19
|
/// incompatible change renames the table), so any catalog at this version or later that
|
|
@@ -62,11 +58,6 @@ fn sql(what: impl AsRef<str>, error: rusqlite::Error) -> Refusal {
|
|
|
62
58
|
}
|
|
63
59
|
}
|
|
64
60
|
|
|
65
|
-
/// Whether a catalog refusal is SQLite's busy timeout expiring behind another writer.
|
|
66
|
-
fn busy(refusal: &Refusal) -> bool {
|
|
67
|
-
refusal.code == Code::IO_FAILED && refusal.detail.ends_with("database is locked")
|
|
68
|
-
}
|
|
69
|
-
|
|
70
61
|
fn io(what: impl AsRef<str>, error: std::io::Error) -> Refusal {
|
|
71
62
|
crate::store::classify_io(what, error)
|
|
72
63
|
}
|
|
@@ -134,13 +125,32 @@ fn connect(path: &Path) -> Result<Connection> {
|
|
|
134
125
|
}
|
|
135
126
|
sql("open catalog", error)
|
|
136
127
|
})?;
|
|
128
|
+
configure(&connection, path)?;
|
|
129
|
+
Ok(connection)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// Journal and sync by what a crash can take from the disk the catalog lives on
|
|
133
|
+
/// (`disk::DiskClass`): an ephemeral disk dies with the machine, so no sync buys anything;
|
|
134
|
+
/// a persistent one keeps a WAL synced at checkpoints; a network filesystem keeps the
|
|
135
|
+
/// rollback journal and a full sync, because WAL's shared memory is not coherent across
|
|
136
|
+
/// hosts. The catalog is only ever opened by processes on one host otherwise.
|
|
137
|
+
pub(crate) fn configure(connection: &Connection, path: &Path) -> Result<()> {
|
|
138
|
+
lock::wait_for_live_holders(connection, path)?;
|
|
139
|
+
let (journal, synchronous) = match crate::disk::DiskClass::of(path.parent().unwrap_or(path)) {
|
|
140
|
+
crate::disk::DiskClass::Ephemeral => ("wal", "OFF"),
|
|
141
|
+
crate::disk::DiskClass::Persistent => ("wal", "NORMAL"),
|
|
142
|
+
crate::disk::DiskClass::Network => ("delete", "FULL"),
|
|
143
|
+
};
|
|
137
144
|
connection
|
|
138
|
-
.
|
|
139
|
-
|
|
145
|
+
.query_row(&format!("PRAGMA journal_mode={journal}"), [], |row| {
|
|
146
|
+
row.get::<_, String>(0)
|
|
147
|
+
})
|
|
148
|
+
.map_err(|error| sql("set catalog journal", error))?;
|
|
140
149
|
connection
|
|
141
|
-
.execute_batch(
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
.execute_batch(&format!(
|
|
151
|
+
"PRAGMA foreign_keys=ON; PRAGMA synchronous={synchronous};"
|
|
152
|
+
))
|
|
153
|
+
.map_err(|error| sql("configure catalog", error))
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
/// A connection to an existing catalog of exactly this schema.
|
|
@@ -390,22 +400,6 @@ impl Catalog {
|
|
|
390
400
|
Ok(catalog)
|
|
391
401
|
}
|
|
392
402
|
|
|
393
|
-
/// `open` for an object admission. A catalog busy past SQLite's timeout while this
|
|
394
|
-
/// process's own admission batches keep committing is a queue, not a failure: the
|
|
395
|
-
/// verified bytes wait for their turn instead of being thrown away and fetched again.
|
|
396
|
-
/// Busy with no batch committed in the meantime is another writer's lock, and refuses.
|
|
397
|
-
pub(crate) fn open_admitting(root: &Path) -> Result<Self> {
|
|
398
|
-
loop {
|
|
399
|
-
let committed = crate::stats::snapshot().admission_batches;
|
|
400
|
-
match Self::open(root) {
|
|
401
|
-
Err(refusal)
|
|
402
|
-
if busy(&refusal) && crate::stats::snapshot().admission_batches > committed => {
|
|
403
|
-
}
|
|
404
|
-
opened => return opened,
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
403
|
pub fn open(root: &Path) -> Result<Self> {
|
|
410
404
|
let path = Self::path(root);
|
|
411
405
|
validated(&path)?;
|
|
@@ -1626,6 +1620,89 @@ mod tests {
|
|
|
1626
1620
|
))
|
|
1627
1621
|
}
|
|
1628
1622
|
|
|
1623
|
+
/// Another process holding the catalog's write lock: this test binary, re-run as
|
|
1624
|
+
/// `lock_holder_child`, until it has taken the lock.
|
|
1625
|
+
fn holder(catalog: &Path, millis: u64) -> std::process::Child {
|
|
1626
|
+
use std::io::BufRead;
|
|
1627
|
+
let mut child = std::process::Command::new(std::env::current_exe().unwrap())
|
|
1628
|
+
.args(["--exact", "catalog::tests::lock_holder_child", "--ignored"])
|
|
1629
|
+
.args(["--nocapture", "--test-threads=1"])
|
|
1630
|
+
.env("TENSORFS_TEST_HOLD", catalog)
|
|
1631
|
+
.env("TENSORFS_TEST_HOLD_MS", millis.to_string())
|
|
1632
|
+
.stdout(std::process::Stdio::piped())
|
|
1633
|
+
.spawn()
|
|
1634
|
+
.unwrap();
|
|
1635
|
+
let mut lines = std::io::BufReader::new(child.stdout.take().unwrap()).lines();
|
|
1636
|
+
let held = lines
|
|
1637
|
+
.by_ref()
|
|
1638
|
+
.map_while(std::result::Result::ok)
|
|
1639
|
+
.any(|line| line.ends_with("held"));
|
|
1640
|
+
assert!(held, "the holder never took the catalog lock");
|
|
1641
|
+
std::thread::spawn(move || lines.for_each(drop));
|
|
1642
|
+
child
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
#[test]
|
|
1646
|
+
#[ignore = "run by holder() in a child process"]
|
|
1647
|
+
fn lock_holder_child() {
|
|
1648
|
+
let path = std::env::var("TENSORFS_TEST_HOLD").unwrap();
|
|
1649
|
+
let millis: u64 = std::env::var("TENSORFS_TEST_HOLD_MS")
|
|
1650
|
+
.unwrap()
|
|
1651
|
+
.parse()
|
|
1652
|
+
.unwrap();
|
|
1653
|
+
let connection = Connection::open(path).unwrap();
|
|
1654
|
+
connection.execute_batch("BEGIN IMMEDIATE").unwrap();
|
|
1655
|
+
connection
|
|
1656
|
+
.execute(
|
|
1657
|
+
"INSERT INTO tensorfs_released_manifests VALUES ('held','by',?1)",
|
|
1658
|
+
[std::process::id().to_string()],
|
|
1659
|
+
)
|
|
1660
|
+
.unwrap();
|
|
1661
|
+
println!("held");
|
|
1662
|
+
std::thread::sleep(std::time::Duration::from_millis(millis));
|
|
1663
|
+
connection.execute_batch("COMMIT").unwrap();
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
fn write(catalog: &Catalog, name: &str) -> Result<usize> {
|
|
1667
|
+
catalog
|
|
1668
|
+
.connection()?
|
|
1669
|
+
.execute(
|
|
1670
|
+
"INSERT INTO tensorfs_released_manifests VALUES ('org',?1,'digest')",
|
|
1671
|
+
[name],
|
|
1672
|
+
)
|
|
1673
|
+
.map_err(|error| sql("test write", error))
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
#[test]
|
|
1677
|
+
fn a_live_lock_holder_is_waited_for_past_any_fixed_deadline() {
|
|
1678
|
+
let root = temporary("live-holder");
|
|
1679
|
+
fs::create_dir_all(&root).unwrap();
|
|
1680
|
+
let catalog = Catalog::initialize(&root).unwrap();
|
|
1681
|
+
let mut child = holder(&catalog.path, 7_000);
|
|
1682
|
+
let started = std::time::Instant::now();
|
|
1683
|
+
assert_eq!(write(&catalog, "after-holder").unwrap(), 1);
|
|
1684
|
+
assert!(started.elapsed() >= std::time::Duration::from_secs(6));
|
|
1685
|
+
assert!(child.wait().unwrap().success());
|
|
1686
|
+
let _ = fs::remove_dir_all(root);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
#[test]
|
|
1690
|
+
fn a_stopped_lock_holder_is_refused() {
|
|
1691
|
+
use rustix::process::{kill_process, Pid, Signal};
|
|
1692
|
+
let root = temporary("stopped-holder");
|
|
1693
|
+
fs::create_dir_all(&root).unwrap();
|
|
1694
|
+
let catalog = Catalog::initialize(&root).unwrap();
|
|
1695
|
+
let mut child = holder(&catalog.path, 60_000);
|
|
1696
|
+
let pid = Pid::from_raw(child.id() as i32).unwrap();
|
|
1697
|
+
kill_process(pid, Signal::STOP).unwrap();
|
|
1698
|
+
let refused = write(&catalog, "behind-a-stopped-holder").unwrap_err();
|
|
1699
|
+
assert!(refused.detail.contains("locked"), "{refused:?}");
|
|
1700
|
+
kill_process(pid, Signal::KILL).unwrap();
|
|
1701
|
+
child.wait().unwrap();
|
|
1702
|
+
assert_eq!(write(&catalog, "after-the-holder-died").unwrap(), 1);
|
|
1703
|
+
let _ = fs::remove_dir_all(root);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1629
1706
|
#[test]
|
|
1630
1707
|
fn catalog_has_seven_tables_and_opens_additive_future_versions() {
|
|
1631
1708
|
let root = temporary("schema");
|
|
@@ -196,6 +196,50 @@ fn ordinary_companions_survive_derivation_without_copying_tensor_backing_files()
|
|
|
196
196
|
std::fs::remove_dir_all(store.root()).unwrap();
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
#[test]
|
|
200
|
+
fn a_verified_store_object_is_a_companion_by_reference_and_never_a_new_byte() {
|
|
201
|
+
let (store, meta, id) = fixture();
|
|
202
|
+
let vocab = vec![0x7b; 256 << 10]; // over the inline bound: references have none
|
|
203
|
+
let object = store
|
|
204
|
+
.put_stream(
|
|
205
|
+
&mut vocab.as_slice(),
|
|
206
|
+
Some(&ObjectRef::of(&vocab)),
|
|
207
|
+
&Fault::default(),
|
|
208
|
+
)
|
|
209
|
+
.unwrap()
|
|
210
|
+
.obj;
|
|
211
|
+
let mut d = super::tests::created_declaration();
|
|
212
|
+
d.objects = vec![("tokenizer/vocab.json".into(), object.clone())];
|
|
213
|
+
let encoded = d.canonical_work_bytes().unwrap();
|
|
214
|
+
assert_eq!(
|
|
215
|
+
Declaration::parse_text(std::str::from_utf8(&encoded).unwrap()).unwrap(),
|
|
216
|
+
d
|
|
217
|
+
);
|
|
218
|
+
let writer = begin(&store, &meta, &id, 1, d.clone(), None).unwrap();
|
|
219
|
+
write_weight(&store, &meta, &id);
|
|
220
|
+
let receipt = commit(&store, &meta, &id, 1).unwrap();
|
|
221
|
+
release(&meta, writer);
|
|
222
|
+
assert!(!receipt.added_objects.contains(&object));
|
|
223
|
+
let header = checkpoint::load_header(&store, &receipt.header).unwrap();
|
|
224
|
+
let (name, asset) = &header.assets[0];
|
|
225
|
+
assert_eq!(
|
|
226
|
+
(name.as_str(), &asset.segments),
|
|
227
|
+
("tokenizer/vocab.json", &vec![object])
|
|
228
|
+
);
|
|
229
|
+
let mut read = Vec::new();
|
|
230
|
+
checkpoint::read_asset(&store, name, asset, &mut read).unwrap();
|
|
231
|
+
assert_eq!(read, vocab);
|
|
232
|
+
|
|
233
|
+
// An object this Store does not hold is refused at commit, never published.
|
|
234
|
+
let next = format!("sha256:{}", "c3".repeat(32));
|
|
235
|
+
d.objects = vec![("tokenizer/vocab.json".into(), ObjectRef::of(b"absent"))];
|
|
236
|
+
let writer = begin(&store, &meta, &next, 1, d, None).unwrap();
|
|
237
|
+
write_weight(&store, &meta, &next);
|
|
238
|
+
assert!(commit(&store, &meta, &next, 1).is_err());
|
|
239
|
+
release(&meta, writer);
|
|
240
|
+
std::fs::remove_dir_all(store.root()).unwrap();
|
|
241
|
+
}
|
|
242
|
+
|
|
199
243
|
#[test]
|
|
200
244
|
fn companion_paths_size_count_and_quotas_are_native_refusals() {
|
|
201
245
|
for path in [
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
//! A live writer on a persistent disk syncs at its checkpoints, not per part, and still
|
|
2
|
+
//! resumes from its last checkpoint after the machine loses its page cache.
|
|
3
|
+
|
|
4
|
+
use super::*;
|
|
5
|
+
use crate::disk::DiskClass;
|
|
6
|
+
use crate::unsynced::testing::{from_an_earlier_boot, markers, persistent_root, tear};
|
|
7
|
+
|
|
8
|
+
const TENSORS: usize = 6;
|
|
9
|
+
|
|
10
|
+
fn declaration() -> Declaration {
|
|
11
|
+
let mut declaration = super::tests::created_declaration();
|
|
12
|
+
let prototype = declaration.components[0].add[0].clone();
|
|
13
|
+
declaration.components[0].add = (0..TENSORS)
|
|
14
|
+
.map(|index| {
|
|
15
|
+
let mut tensor = prototype.clone();
|
|
16
|
+
tensor.key = format!("weight_{index}");
|
|
17
|
+
tensor.shape = vec![128];
|
|
18
|
+
tensor.parts[0].shape = vec![128];
|
|
19
|
+
tensor
|
|
20
|
+
})
|
|
21
|
+
.collect();
|
|
22
|
+
declaration.order = (0..TENSORS)
|
|
23
|
+
.map(|index| ("model".into(), format!("weight_{index}")))
|
|
24
|
+
.collect();
|
|
25
|
+
declaration.max_new_bytes = (TENSORS * 512) as u64;
|
|
26
|
+
declaration
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn store(name: &str) -> (Store, Meta) {
|
|
30
|
+
let store = Store::init(&persistent_root(name)).unwrap();
|
|
31
|
+
assert_eq!(store.disk_class(), DiskClass::Persistent);
|
|
32
|
+
let meta = Meta::open(&store).unwrap();
|
|
33
|
+
(store, meta)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn add(store: &Store, meta: &Meta, id: &str, session: u64, writer: &Begin, index: usize) -> Part {
|
|
37
|
+
add_indexed_part(
|
|
38
|
+
store,
|
|
39
|
+
meta,
|
|
40
|
+
id,
|
|
41
|
+
session,
|
|
42
|
+
&writer.additions,
|
|
43
|
+
None,
|
|
44
|
+
("model", &format!("weight_{index}"), "value"),
|
|
45
|
+
&mut [index as u8 + 1; 512].as_slice(),
|
|
46
|
+
)
|
|
47
|
+
.unwrap()
|
|
48
|
+
.part
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn a_power_loss_after_a_checkpoint_resumes_from_it_to_the_same_manifest() {
|
|
53
|
+
let id = format!("sha256:{}", "6c".repeat(32));
|
|
54
|
+
|
|
55
|
+
// The reference: one uninterrupted writer.
|
|
56
|
+
let (clean, clean_meta) = store("group-commit-clean");
|
|
57
|
+
let writer = begin(&clean, &clean_meta, &id, 1, declaration(), None).unwrap();
|
|
58
|
+
let live = writer_store(&clean).unwrap();
|
|
59
|
+
for index in 0..TENSORS {
|
|
60
|
+
add(&live, &clean_meta, &id, 1, &writer, index);
|
|
61
|
+
}
|
|
62
|
+
let expected = commit(&live, &clean_meta, &id, 1).unwrap();
|
|
63
|
+
finish_writer(&live).unwrap();
|
|
64
|
+
release_guards(&clean_meta, Some(writer.writer_hold), writer.source_leases);
|
|
65
|
+
assert!(
|
|
66
|
+
markers(clean.root()).is_empty(),
|
|
67
|
+
"a committed writer retires its epoch"
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// A checkpoint after three parts; three more admitted unsynced; then the machine goes
|
|
71
|
+
// down and the page cache holding those three is lost.
|
|
72
|
+
let (crashed, meta) = store("group-commit-crash");
|
|
73
|
+
let root = crashed.root().to_path_buf();
|
|
74
|
+
let writer = begin(&crashed, &meta, &id, 1, declaration(), None).unwrap();
|
|
75
|
+
let live = writer_store(&crashed).unwrap();
|
|
76
|
+
assert_eq!(
|
|
77
|
+
markers(&root).len(),
|
|
78
|
+
1,
|
|
79
|
+
"a persistent writer admits under one epoch"
|
|
80
|
+
);
|
|
81
|
+
for index in 0..3 {
|
|
82
|
+
add(&live, &meta, &id, 1, &writer, index);
|
|
83
|
+
}
|
|
84
|
+
let (_, head) = checkpoint_progress(&live, &meta, &id, 1, "request", "model", None).unwrap();
|
|
85
|
+
let lost: Vec<Part> = (3..TENSORS)
|
|
86
|
+
.map(|index| add(&live, &meta, &id, 1, &writer, index))
|
|
87
|
+
.collect();
|
|
88
|
+
fence(&meta, &id, 1).unwrap();
|
|
89
|
+
release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
|
|
90
|
+
drop(live);
|
|
91
|
+
from_an_earlier_boot(&markers(&root).pop().unwrap());
|
|
92
|
+
for segment in lost.iter().flat_map(Part::segments) {
|
|
93
|
+
tear(&crashed.blob_path(&segment.sha256));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Opening repairs the torn objects; the resumed writer keeps what its checkpoint made
|
|
97
|
+
// durable and recomputes the rest.
|
|
98
|
+
let reopened = Store::open(&root).unwrap();
|
|
99
|
+
let meta = Meta::open(&reopened).unwrap();
|
|
100
|
+
let head = head.head.expect("the checkpoint names a head");
|
|
101
|
+
let writer = begin(&reopened, &meta, &id, 2, declaration(), Some(&head)).unwrap();
|
|
102
|
+
assert_eq!(completed(&meta, &id, 2).unwrap().0.len(), 3);
|
|
103
|
+
let live = writer_store(&reopened).unwrap();
|
|
104
|
+
for index in 3..TENSORS {
|
|
105
|
+
add(&live, &meta, &id, 2, &writer, index);
|
|
106
|
+
}
|
|
107
|
+
let resumed = commit(&live, &meta, &id, 2).unwrap();
|
|
108
|
+
finish_writer(&live).unwrap();
|
|
109
|
+
release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
|
|
110
|
+
assert_eq!(resumed.manifest, expected.manifest);
|
|
111
|
+
assert!(markers(&root).is_empty());
|
|
112
|
+
std::fs::remove_dir_all(clean.root()).unwrap();
|
|
113
|
+
std::fs::remove_dir_all(&root).unwrap();
|
|
114
|
+
}
|