tensorfs 0.3.77__tar.gz → 0.3.79__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.77 → tensorfs-0.3.79}/Cargo.lock +2 -2
- {tensorfs-0.3.77 → tensorfs-0.3.79}/Cargo.toml +1 -1
- {tensorfs-0.3.77 → tensorfs-0.3.79}/PKG-INFO +1 -1
- tensorfs-0.3.79/crates/tensorfs-core/src/bin/tfs/ensure.rs +160 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/fetch.rs +1 -1
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/ingest.rs +1 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/main.rs +88 -2
- tensorfs-0.3.79/crates/tensorfs-core/src/catalog/lock.rs +206 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/catalog.rs +28 -1
- tensorfs-0.3.79/crates/tensorfs-core/src/ensure/admission.rs +333 -0
- tensorfs-0.3.79/crates/tensorfs-core/src/ensure/flight.rs +216 -0
- tensorfs-0.3.79/crates/tensorfs-core/src/ensure.rs +364 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/err.rs +3 -0
- tensorfs-0.3.77/crates/tensorfs-core/src/repo_cache/identity.rs → tensorfs-0.3.79/crates/tensorfs-core/src/filesystem_identity.rs +30 -24
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/fill.rs +3 -3
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/gc.rs +18 -3
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/convert.rs +10 -12
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/fingerprint.rs +22 -3
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/h3_lora.rs +12 -5
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/preflight.rs +0 -3
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/source.rs +19 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/lib.rs +6 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/meta.rs +67 -14
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/repo_cache.rs +5 -6
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/source_artifact.rs +46 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/http.rs +72 -30
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +4 -1
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/mod.rs +2 -2
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/pull.rs +75 -30
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/tests.rs +31 -20
- tensorfs-0.3.79/crates/tensorfs-core/tests/catalog_owner_probe.c +29 -0
- tensorfs-0.3.79/crates/tensorfs-core/tests/ensure.rs +781 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/h3_lora.rs +153 -7
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/source_prefixes.rs +3 -1
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/store_boundaries.rs +42 -16
- tensorfs-0.3.79/crates/tensorfs-py/src/ensure.rs +145 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-py/src/lib.rs +4 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-py/src/store.rs +8 -1
- {tensorfs-0.3.77 → tensorfs-0.3.79}/pyproject.toml +1 -1
- {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/__init__.py +116 -1
- {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/_ext.pyi +25 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/errors.py +2 -0
- tensorfs-0.3.77/crates/tensorfs-core/src/catalog/lock.rs +0 -121
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/asset_update.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/cache_roots.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/group_commit_tests.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/history_cost_tests.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/part_rows_tests.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived/roots.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/derived.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/descriptors.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/disk.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/narrow.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/routes/anima.routes.json +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/routes/anima.spec.json +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/routes/sdxl.routes.json +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/routes/sdxl.spec.json +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/routes.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/providers.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/stats.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/store.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/memory.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/pace.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/policy.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/push.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/unsynced.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/as_is.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/civitai_anima.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/hub_tls.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/routed_single_file.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/source_download.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/trust_index.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/crates/tensorfs-py/src/ingest.rs +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/_derived_channel.py +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/_source_channel.py +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/_tensors.py +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/python/tensorfs/derived.py +0 -0
- {tensorfs-0.3.77 → tensorfs-0.3.79}/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.79"
|
|
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.79"
|
|
458
458
|
dependencies = [
|
|
459
459
|
"pyo3",
|
|
460
460
|
"tensorfs-core",
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
//! `tfs ensure <root> <ref> --hub <base>`: make one model resident (proto-063 M2a).
|
|
2
|
+
//!
|
|
3
|
+
//! STDOUT is JSON lines and nothing else: `ensure.progress` samples, then exactly one
|
|
4
|
+
//! `ensure.result` (exit 0) or `ensure.refused` (exit 1). Usage errors exit 2.
|
|
5
|
+
|
|
6
|
+
use std::io::Write;
|
|
7
|
+
use std::path::Path;
|
|
8
|
+
use std::process::ExitCode;
|
|
9
|
+
use std::sync::Mutex;
|
|
10
|
+
|
|
11
|
+
use tensorfs_core::canon::{self, Value};
|
|
12
|
+
use tensorfs_core::ensure::{self, Ensured, Event, Request};
|
|
13
|
+
use tensorfs_core::err::Refusal;
|
|
14
|
+
use tensorfs_core::store::Store;
|
|
15
|
+
use tensorfs_core::transport;
|
|
16
|
+
|
|
17
|
+
use crate::{flag, flag_all, flag_num, Flags};
|
|
18
|
+
|
|
19
|
+
fn read(path: &str) -> Result<Vec<u8>, Refusal> {
|
|
20
|
+
std::fs::read(path).map_err(|e| Refusal {
|
|
21
|
+
code: tensorfs_core::err::Code::IO_FAILED,
|
|
22
|
+
detail: format!("read {path}: {e}"),
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn quoted(text: &str) -> String {
|
|
27
|
+
String::from_utf8(canon::write(&Value::str(text))).unwrap()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn say(out: &Mutex<std::io::Stdout>, line: String) {
|
|
31
|
+
let mut out = out.lock().unwrap();
|
|
32
|
+
// A closed reader is not this download's business; the bytes are still wanted.
|
|
33
|
+
let _ = writeln!(out, "{line}").and_then(|()| out.flush());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn progress(event: &Event) -> String {
|
|
37
|
+
format!(
|
|
38
|
+
"{{\"event\":\"ensure.progress\",\"model\":{},\"step\":{},\"phase\":\"{}\",\"bytes_done\":{},\"bytes_total\":{},\"rate\":{:.1},\"attempt\":{}}}",
|
|
39
|
+
quoted(&event.model),
|
|
40
|
+
quoted(&event.step),
|
|
41
|
+
event.phase.as_str(),
|
|
42
|
+
event.bytes_done,
|
|
43
|
+
event.bytes_total,
|
|
44
|
+
event.rate,
|
|
45
|
+
event.attempt,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn result(r: &Ensured) -> String {
|
|
50
|
+
format!(
|
|
51
|
+
"{{\"event\":\"ensure.result\",\"model\":{},\"release\":{},\"lane\":{},\"scope\":{},\"manifest\":\"{}\",\"manifest_length\":{},\"bytes_total\":{},\"bytes_held\":{},\"bytes_fetched\":{},\"bytes_cached\":{},\"cache_written_bytes\":{},\"collected_bytes\":{},\"attempts\":{},\"joined\":{},\"seconds\":{:.3}}}",
|
|
52
|
+
quoted(&r.model),
|
|
53
|
+
quoted(&r.release),
|
|
54
|
+
quoted(&r.lane),
|
|
55
|
+
quoted(&r.scope),
|
|
56
|
+
r.manifest.id(),
|
|
57
|
+
r.manifest.length,
|
|
58
|
+
r.bytes_total,
|
|
59
|
+
r.bytes_held,
|
|
60
|
+
r.bytes_fetched,
|
|
61
|
+
r.bytes_cached,
|
|
62
|
+
r.cache_written_bytes,
|
|
63
|
+
r.collected_bytes,
|
|
64
|
+
r.attempts,
|
|
65
|
+
r.joined,
|
|
66
|
+
r.seconds,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn refused(refusal: &Refusal, model: &str, step: &str) -> String {
|
|
71
|
+
format!(
|
|
72
|
+
"{{\"event\":\"ensure.refused\",\"code\":\"{}\",\"resumable\":{},\"detail\":{},\"model\":{},\"step\":{}}}",
|
|
73
|
+
refusal.code.as_str(),
|
|
74
|
+
ensure::resumable(refusal.code),
|
|
75
|
+
quoted(&refusal.detail),
|
|
76
|
+
quoted(model),
|
|
77
|
+
quoted(step),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pub fn cmd_ensure(root: &Path, refspec: &str, flags: &Flags) -> ExitCode {
|
|
82
|
+
let Some(hub) = flag(flags, "hub") else {
|
|
83
|
+
eprintln!("--hub <base-url> is required");
|
|
84
|
+
return ExitCode::from(2);
|
|
85
|
+
};
|
|
86
|
+
let out = Mutex::new(std::io::stdout());
|
|
87
|
+
let model = refspec.split('@').next().unwrap_or_default();
|
|
88
|
+
let step = flag(flags, "step").unwrap_or("");
|
|
89
|
+
let answer = |outcome: Result<Ensured, Refusal>| match outcome {
|
|
90
|
+
Ok(ensured) => {
|
|
91
|
+
say(&out, result(&ensured));
|
|
92
|
+
ExitCode::SUCCESS
|
|
93
|
+
}
|
|
94
|
+
Err(refusal) => {
|
|
95
|
+
say(&out, refused(&refusal, model, step));
|
|
96
|
+
ExitCode::FAILURE
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
let setup = || -> Result<_, Refusal> {
|
|
100
|
+
if let Some(path) = flag(flags, "ca-file") {
|
|
101
|
+
transport::trust_roots(&read(path)?)?;
|
|
102
|
+
}
|
|
103
|
+
// The credential reaches the child as a file or TFS_CREDENTIAL, never argv.
|
|
104
|
+
let spec = match flag(flags, "credential-file") {
|
|
105
|
+
Some(path) => String::from_utf8_lossy(&read(path)?).into_owned(),
|
|
106
|
+
None => std::env::var("TFS_CREDENTIAL").unwrap_or_default(),
|
|
107
|
+
};
|
|
108
|
+
let credential =
|
|
109
|
+
transport::credential_from_spec(spec.trim(), vec![transport::base_host(hub)?])?;
|
|
110
|
+
Ok((Store::open(root)?, credential))
|
|
111
|
+
};
|
|
112
|
+
let (store, credential) = match setup() {
|
|
113
|
+
Ok(setup) => setup,
|
|
114
|
+
Err(refusal) => return answer(Err(refusal)),
|
|
115
|
+
};
|
|
116
|
+
let policy = crate::fetch::policy_of(flags);
|
|
117
|
+
let keep: Vec<String> = flag_all(flags, "keep")
|
|
118
|
+
.into_iter()
|
|
119
|
+
.map(str::to_string)
|
|
120
|
+
.collect();
|
|
121
|
+
let on_event = |event: &Event| say(&out, progress(event));
|
|
122
|
+
let mut request = Request::new(&store, hub, refspec, &credential, &policy);
|
|
123
|
+
request.lane = flag(flags, "lane").unwrap_or("");
|
|
124
|
+
request.step = step;
|
|
125
|
+
request.keep = &keep;
|
|
126
|
+
request.streams = flag_num(flags, "streams", transport::PULL_STREAMS);
|
|
127
|
+
request.streams_start = flag_num(flags, "streams-start", transport::STREAMS);
|
|
128
|
+
request.sample_seconds = flag_num(flags, "sample-seconds", transport::SAMPLE_SECONDS);
|
|
129
|
+
request.on_event = Some(&on_event);
|
|
130
|
+
answer(ensure::ensure(&request))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// `tfs pressure <root> [--keep sha256:<m>]...`: one pressure pass (see `ensure::relieve`),
|
|
134
|
+
/// reported as one JSON line.
|
|
135
|
+
pub fn cmd_pressure(root: &Path, flags: &Flags) -> ExitCode {
|
|
136
|
+
let keep: Vec<String> = flag_all(flags, "keep")
|
|
137
|
+
.into_iter()
|
|
138
|
+
.map(str::to_string)
|
|
139
|
+
.collect();
|
|
140
|
+
let relief = match Store::open(root).and_then(|store| ensure::relieve(&store, &keep)) {
|
|
141
|
+
Ok(relief) => relief,
|
|
142
|
+
Err(refusal) => {
|
|
143
|
+
println!(
|
|
144
|
+
"{{\"event\":\"pressure.refused\",\"code\":\"{}\",\"detail\":{}}}",
|
|
145
|
+
refusal.code.as_str(),
|
|
146
|
+
quoted(&refusal.detail)
|
|
147
|
+
);
|
|
148
|
+
return ExitCode::FAILURE;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
println!(
|
|
152
|
+
"{{\"event\":\"pressure\",\"pressure\":{},\"collected_bytes\":{},\"capacity_bytes\":{},\"available_bytes\":{},\"unable\":{}}}",
|
|
153
|
+
relief.pressure,
|
|
154
|
+
relief.collected_bytes,
|
|
155
|
+
relief.capacity_bytes,
|
|
156
|
+
relief.available_bytes,
|
|
157
|
+
relief.unable.as_deref().map_or("null".to_string(), quoted),
|
|
158
|
+
);
|
|
159
|
+
ExitCode::SUCCESS
|
|
160
|
+
}
|
|
@@ -168,7 +168,7 @@ use tensorfs_core::transport::{
|
|
|
168
168
|
self, credential_from_spec, Anonymous, Deadline, PullRequest, SourcePolicy,
|
|
169
169
|
};
|
|
170
170
|
|
|
171
|
-
fn policy_of(flags: &Flags) -> SourcePolicy {
|
|
171
|
+
pub(crate) fn policy_of(flags: &Flags) -> SourcePolicy {
|
|
172
172
|
SourcePolicy {
|
|
173
173
|
allowed_hosts: crate::flag(flags, "allow-hosts")
|
|
174
174
|
.map(|hosts| {
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
//! [--session <s>] [--tenant <t>] [--no-journal]
|
|
32
32
|
//! tfs repo-cache chain <cache> <chain-id> <length> the links, root to head
|
|
33
33
|
//! tfs put <root> <file> stream one object in (any shape)
|
|
34
|
-
//! tfs get <root> <hex> verified read, whole or ranged
|
|
34
|
+
//! tfs get <root> <hex> verified read, whole or ranged; with --out - the bytes
|
|
35
|
+
//! go to stdout under a read lease (--range <off>:<len>|<off>:)
|
|
35
36
|
//! tfs contains <root> <hex> the presence HINT, and what it is worth
|
|
36
37
|
//! tfs verify <root> <hex> verification record or rehash; remove corrupt bytes
|
|
37
38
|
//! tfs scrub <root> one rate knob over the whole store
|
|
@@ -48,6 +49,15 @@
|
|
|
48
49
|
//! `tfs fetch` reads and backfills the Store's bound repo cache between the local Store
|
|
49
50
|
//! and the origin. It is bound by `tfs store ensure --repo-cache` and never by a flag here.
|
|
50
51
|
//!
|
|
52
|
+
//! tfs capabilities one feature name per line (ensure/1, get-range/1, ...)
|
|
53
|
+
//! tfs ensure <root> <ref> --hub <base> [--lane L] [--step S] [--keep sha256:<m>]...
|
|
54
|
+
//! make one model resident: closure, one flight per manifest per
|
|
55
|
+
//! Store, disk admission + GC policy, paced resumable fetch, retry
|
|
56
|
+
//! only after measured progress. STDOUT is JSON lines:
|
|
57
|
+
//! ensure.progress samples, then ensure.result (exit 0) or
|
|
58
|
+
//! ensure.refused {code, resumable, detail} (exit 1)
|
|
59
|
+
//! tfs pressure <root> [--keep sha256:<m>]... one pressure pass: at <= 1/10 free, run
|
|
60
|
+
//! the GC policy until > 1/5 is free; one JSON line
|
|
51
61
|
//! tfs fetch <root> <ref> closure -> plan -> N-way walk (presign at use) -> proof
|
|
52
62
|
//! tfs fetch bounded <root> <ref> --max-download-bytes N --max-download-objects N
|
|
53
63
|
//! same pull, refused before body transfer if new objects exceed bounds
|
|
@@ -203,7 +213,9 @@ fn accept_printed_ids(args: Vec<String>) -> Vec<String> {
|
|
|
203
213
|
|
|
204
214
|
fn main() -> ExitCode {
|
|
205
215
|
let (args, flags) = split_flags(std::env::args().skip(1).collect());
|
|
206
|
-
|
|
216
|
+
// `ensure` names its CA itself so a bad file is a refusal on its own channel.
|
|
217
|
+
let ensure = args.first().is_some_and(|verb| verb == "ensure");
|
|
218
|
+
if let (Some(path), false) = (flag(&flags, "ca-file"), ensure) {
|
|
207
219
|
let trusted = fs::read(path)
|
|
208
220
|
.map_err(|e| format!("IO_FAILED: read {path}: {e}"))
|
|
209
221
|
.and_then(|pem| {
|
|
@@ -218,6 +230,14 @@ fn main() -> ExitCode {
|
|
|
218
230
|
let a: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
|
219
231
|
match a.as_slice() {
|
|
220
232
|
["version"] => cmd_version(),
|
|
233
|
+
["capabilities"] => {
|
|
234
|
+
tensorfs_core::CAPABILITIES
|
|
235
|
+
.iter()
|
|
236
|
+
.for_each(|c| println!("{c}"));
|
|
237
|
+
ExitCode::SUCCESS
|
|
238
|
+
}
|
|
239
|
+
["ensure", r, refspec] => ensure::cmd_ensure(Path::new(r), refspec, &flags),
|
|
240
|
+
["pressure", r] => ensure::cmd_pressure(Path::new(r), &flags),
|
|
221
241
|
["home"] => cmd_home(None),
|
|
222
242
|
["home", r] => cmd_home(Some(Path::new(r))),
|
|
223
243
|
["store", "init"] => cmd_store_default("init", &flags),
|
|
@@ -815,6 +835,7 @@ macro_rules! ok {
|
|
|
815
835
|
|
|
816
836
|
mod ckpt;
|
|
817
837
|
mod conform;
|
|
838
|
+
mod ensure;
|
|
818
839
|
mod fetch;
|
|
819
840
|
mod ingest;
|
|
820
841
|
mod rd;
|
|
@@ -936,6 +957,67 @@ fn fault_of(f: &Flags) -> Fault {
|
|
|
936
957
|
}
|
|
937
958
|
}
|
|
938
959
|
|
|
960
|
+
/// `<off>:<len>` or `<off>:` (to the end) inside an object of `length` bytes.
|
|
961
|
+
fn byte_range(range: &str, length: u64) -> TfsResult<(u64, u64)> {
|
|
962
|
+
let bad = || Refusal {
|
|
963
|
+
code: tensorfs_core::err::Code::RANGE_BOUNDS,
|
|
964
|
+
detail: format!("--range {range:?} is not <off>:<len> or <off>: inside {length} B"),
|
|
965
|
+
};
|
|
966
|
+
let (off, len) = range.split_once(':').ok_or_else(bad)?;
|
|
967
|
+
let off: u64 = off.parse().map_err(|_| bad())?;
|
|
968
|
+
let len: u64 = match len {
|
|
969
|
+
"" => length.checked_sub(off).ok_or_else(bad)?,
|
|
970
|
+
len => len.parse().map_err(|_| bad())?,
|
|
971
|
+
};
|
|
972
|
+
match off.checked_add(len) {
|
|
973
|
+
Some(end) if end <= length => Ok((off, len)),
|
|
974
|
+
_ => Err(bad()),
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/// Stream a range of one verified object to stdout under a read lease, so a GC pass cannot
|
|
979
|
+
/// take the object mid-read.
|
|
980
|
+
fn get_leased(store: &Store, hex: &str, range: Option<&str>) -> TfsResult<()> {
|
|
981
|
+
use std::io::Write;
|
|
982
|
+
use tensorfs_core::read::{self, ObjectRange};
|
|
983
|
+
let length = store.open_verified(hex)?.len();
|
|
984
|
+
let (mut off, mut left) = match range {
|
|
985
|
+
Some(range) => byte_range(range, length)?,
|
|
986
|
+
None => (0, length),
|
|
987
|
+
};
|
|
988
|
+
let meta = tensorfs_core::meta::Meta::open(store)?;
|
|
989
|
+
let obj = ObjectRef {
|
|
990
|
+
sha256: hex.to_string(),
|
|
991
|
+
length,
|
|
992
|
+
};
|
|
993
|
+
let (lease, _) = read::acquire(store, &meta, "", vec![obj.clone()])?;
|
|
994
|
+
let mut out = std::io::stdout().lock();
|
|
995
|
+
let mut buf = vec![0u8; 1 << 20];
|
|
996
|
+
let streamed = (|| {
|
|
997
|
+
while left > 0 {
|
|
998
|
+
let n = left.min(buf.len() as u64);
|
|
999
|
+
let chunk = &mut buf[..n as usize];
|
|
1000
|
+
let range = ObjectRange {
|
|
1001
|
+
obj: obj.clone(),
|
|
1002
|
+
off,
|
|
1003
|
+
len: n,
|
|
1004
|
+
};
|
|
1005
|
+
read::read_into(&lease, &range, chunk)?;
|
|
1006
|
+
out.write_all(chunk).map_err(|e| Refusal {
|
|
1007
|
+
code: tensorfs_core::err::Code::IO_FAILED,
|
|
1008
|
+
detail: format!("write stdout: {e}"),
|
|
1009
|
+
})?;
|
|
1010
|
+
(off, left) = (off + n, left - n);
|
|
1011
|
+
}
|
|
1012
|
+
out.flush().map_err(|e| Refusal {
|
|
1013
|
+
code: tensorfs_core::err::Code::IO_FAILED,
|
|
1014
|
+
detail: format!("write stdout: {e}"),
|
|
1015
|
+
})
|
|
1016
|
+
})();
|
|
1017
|
+
lease.release(&meta)?;
|
|
1018
|
+
streamed
|
|
1019
|
+
}
|
|
1020
|
+
|
|
939
1021
|
fn cmd_put(root: &Path, src: &Path, f: &Flags) -> ExitCode {
|
|
940
1022
|
let s = ok!(Store::open(root));
|
|
941
1023
|
let expect = match (flag(f, "expect"), flag(f, "expect-length")) {
|
|
@@ -1016,6 +1098,10 @@ fn cmd_verify_object(root: &Path, hex: &str, f: &Flags) -> ExitCode {
|
|
|
1016
1098
|
|
|
1017
1099
|
fn cmd_get(root: &Path, hex: &str, f: &Flags) -> ExitCode {
|
|
1018
1100
|
let s = ok!(Store::open(root));
|
|
1101
|
+
if flag(f, "out") == Some("-") {
|
|
1102
|
+
ok!(get_leased(&s, hex, flag(f, "range")));
|
|
1103
|
+
return ExitCode::SUCCESS;
|
|
1104
|
+
}
|
|
1019
1105
|
let t = Instant::now();
|
|
1020
1106
|
if let Some(r) = flag(f, "range") {
|
|
1021
1107
|
let (o, l) = r.split_once(':').unwrap_or((r, "1"));
|
|
@@ -0,0 +1,206 @@
|
|
|
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 or SIGSTOP), and only
|
|
7
|
+
//! then does a statement give up. Dead processes have already released their locks.
|
|
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 currently held write lock belongs to a stopped process. Without a current
|
|
46
|
+
/// holder to name (the lock just went, or no `/proc`), keep asking.
|
|
47
|
+
fn holders_cannot_release(path: &Path) -> bool {
|
|
48
|
+
let files: Vec<(u64, u64)> = [path.to_path_buf(), shm(path)]
|
|
49
|
+
.iter()
|
|
50
|
+
.filter_map(|file| std::fs::metadata(file).ok())
|
|
51
|
+
.map(|metadata| {
|
|
52
|
+
use std::os::unix::fs::MetadataExt;
|
|
53
|
+
(metadata.dev(), metadata.ino())
|
|
54
|
+
})
|
|
55
|
+
.collect();
|
|
56
|
+
let Ok(locks) = std::fs::read_to_string("/proc/locks") else {
|
|
57
|
+
return false;
|
|
58
|
+
};
|
|
59
|
+
snapshots_cannot_release(&locks, &files, std::process::id(), stopped, || {
|
|
60
|
+
std::fs::read_to_string("/proc/locks").ok()
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn snapshots_cannot_release(
|
|
65
|
+
observed: &str,
|
|
66
|
+
files: &[(u64, u64)],
|
|
67
|
+
own: u32,
|
|
68
|
+
is_stopped: impl Fn(u32) -> bool,
|
|
69
|
+
current: impl FnOnce() -> Option<String>,
|
|
70
|
+
) -> bool {
|
|
71
|
+
let holders = write_holders(observed, files, own);
|
|
72
|
+
if holders.is_empty() || !holders.iter().all(|pid| is_stopped(*pid)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
// A holder may unlock or exit between the lock snapshot and its process-state
|
|
76
|
+
// read. Refuse only if those stopped processes still hold the actual locks.
|
|
77
|
+
let Some(current) = current() else {
|
|
78
|
+
return false;
|
|
79
|
+
};
|
|
80
|
+
let current = write_holders(¤t, files, own);
|
|
81
|
+
!current.is_empty()
|
|
82
|
+
&& current
|
|
83
|
+
.iter()
|
|
84
|
+
.all(|pid| holders.contains(pid) && is_stopped(*pid))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn shm(path: &Path) -> PathBuf {
|
|
88
|
+
let mut name = path.as_os_str().to_owned();
|
|
89
|
+
name.push("-shm");
|
|
90
|
+
PathBuf::from(name)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Other processes' POSIX write locks on these inodes, from `/proc/locks` lines such as
|
|
94
|
+
/// `3: POSIX ADVISORY WRITE 167671 103:02:204736108 1073741824 1073742335`.
|
|
95
|
+
fn write_holders(locks: &str, files: &[(u64, u64)], own: u32) -> Vec<u32> {
|
|
96
|
+
locks
|
|
97
|
+
.lines()
|
|
98
|
+
.filter_map(|line| {
|
|
99
|
+
let fields: Vec<&str> = line.split_whitespace().collect();
|
|
100
|
+
let at = fields.iter().position(|field| *field == "POSIX")?;
|
|
101
|
+
if fields[..at].contains(&"->") || fields.get(at + 2) != Some(&"WRITE") {
|
|
102
|
+
return None;
|
|
103
|
+
}
|
|
104
|
+
let pid: u32 = fields.get(at + 3)?.parse().ok()?;
|
|
105
|
+
let mut file = fields.get(at + 4)?.split(':');
|
|
106
|
+
let major = u32::from_str_radix(file.next()?, 16).ok()?;
|
|
107
|
+
let minor = u32::from_str_radix(file.next()?, 16).ok()?;
|
|
108
|
+
let inode: u64 = file.next()?.parse().ok()?;
|
|
109
|
+
let device = rustix::fs::makedev(major, minor) as u64;
|
|
110
|
+
(file.next().is_none() && pid != own && files.contains(&(device, inode))).then_some(pid)
|
|
111
|
+
})
|
|
112
|
+
.collect()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Only stopped and traced-stopped processes keep locks they cannot release.
|
|
116
|
+
fn stopped(pid: u32) -> bool {
|
|
117
|
+
std::fs::read_to_string(format!("/proc/{pid}/stat"))
|
|
118
|
+
.ok()
|
|
119
|
+
.is_some_and(|stat| process_is_stopped(&stat))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn process_is_stopped(stat: &str) -> bool {
|
|
123
|
+
stat.rsplit_once(')')
|
|
124
|
+
.and_then(|(_, fields)| fields.split_whitespace().next())
|
|
125
|
+
.is_some_and(|state| matches!(state, "T" | "t"))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[cfg(test)]
|
|
129
|
+
mod tests {
|
|
130
|
+
use super::*;
|
|
131
|
+
|
|
132
|
+
#[test]
|
|
133
|
+
fn only_other_processes_write_locks_on_the_catalog_are_holders() {
|
|
134
|
+
let locks = "\
|
|
135
|
+
1: POSIX ADVISORY WRITE 4242 103:02:77 1073741824 1073742335
|
|
136
|
+
2: POSIX ADVISORY READ 5151 103:02:77 1073741826 1073742335
|
|
137
|
+
3: POSIX ADVISORY WRITE 6161 103:02:99 120 120
|
|
138
|
+
4: FLOCK ADVISORY WRITE 7171 103:02:77 0 EOF
|
|
139
|
+
5: POSIX ADVISORY WRITE 1000 103:02:77 0 EOF
|
|
140
|
+
6: -> POSIX ADVISORY WRITE 8181 103:02:77 0 EOF
|
|
141
|
+
7: POSIX ADVISORY WRITE 9191 104:02:77 0 EOF
|
|
142
|
+
";
|
|
143
|
+
let device = rustix::fs::makedev(0x103, 2) as u64;
|
|
144
|
+
assert_eq!(write_holders(locks, &[(device, 77)], 1000), vec![4242]);
|
|
145
|
+
assert_eq!(
|
|
146
|
+
write_holders(locks, &[(device, 77), (device, 99)], 1000),
|
|
147
|
+
vec![4242, 6161]
|
|
148
|
+
);
|
|
149
|
+
assert!(write_holders(locks, &[(device, 5)], 1000).is_empty());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn exited_holder_is_progress_and_never_a_stopped_writer() {
|
|
154
|
+
for state in ["Z", "X", "R", "S", "D"] {
|
|
155
|
+
assert!(!process_is_stopped(&format!("4242 (writer) {state} 1 2 3")));
|
|
156
|
+
}
|
|
157
|
+
for state in ["T", "t"] {
|
|
158
|
+
assert!(process_is_stopped(&format!("4242 (writer) {state} 1 2 3")));
|
|
159
|
+
}
|
|
160
|
+
assert!(!process_is_stopped("unavailable"));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[test]
|
|
164
|
+
fn stopped_holder_must_still_own_the_lock_before_refusal() {
|
|
165
|
+
let held = "1: POSIX ADVISORY WRITE 4242 103:02:77 0 EOF\n";
|
|
166
|
+
let files = [(rustix::fs::makedev(0x103, 2) as u64, 77)];
|
|
167
|
+
// Stable SIGSTOP remains a typed refusal, while snapshot -> unlock ->
|
|
168
|
+
// SIGSTOP must let SQLite retry rather than refuse the old snapshot.
|
|
169
|
+
assert!(snapshots_cannot_release(
|
|
170
|
+
held,
|
|
171
|
+
&files,
|
|
172
|
+
1000,
|
|
173
|
+
|_| true,
|
|
174
|
+
|| Some(held.into())
|
|
175
|
+
));
|
|
176
|
+
assert!(!snapshots_cannot_release(
|
|
177
|
+
held,
|
|
178
|
+
&files,
|
|
179
|
+
1000,
|
|
180
|
+
|_| true,
|
|
181
|
+
|| Some(String::new())
|
|
182
|
+
));
|
|
183
|
+
assert!(!snapshots_cannot_release(
|
|
184
|
+
held,
|
|
185
|
+
&files,
|
|
186
|
+
1000,
|
|
187
|
+
|_| true,
|
|
188
|
+
|| None
|
|
189
|
+
));
|
|
190
|
+
let replacement = held.replace("4242", "6161");
|
|
191
|
+
assert!(!snapshots_cannot_release(
|
|
192
|
+
held,
|
|
193
|
+
&files,
|
|
194
|
+
1000,
|
|
195
|
+
|_| true,
|
|
196
|
+
|| Some(replacement)
|
|
197
|
+
));
|
|
198
|
+
assert!(!snapshots_cannot_release(
|
|
199
|
+
held,
|
|
200
|
+
&files,
|
|
201
|
+
1000,
|
|
202
|
+
|_| false,
|
|
203
|
+
|| panic!("a live holder must retry")
|
|
204
|
+
));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -104,7 +104,14 @@ pub fn connections_opened(root: &Path) -> u64 {
|
|
|
104
104
|
.unwrap_or(0)
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
fn connect(path: &Path) -> Result<Connection> {
|
|
107
|
+
pub(crate) fn connect(path: &Path) -> Result<Connection> {
|
|
108
|
+
// SQLite creates WAL/SHM files before assigning their database owner. Opening
|
|
109
|
+
// them under root exposes a transient root-owned inode to concurrent owner
|
|
110
|
+
// connections, which can permanently fall back to read-only WAL access.
|
|
111
|
+
let _identity = crate::filesystem_identity::FilesystemIdentity::for_store_root(
|
|
112
|
+
path.parent().unwrap_or(path),
|
|
113
|
+
)
|
|
114
|
+
.map_err(|error| io("enter catalog filesystem identity", error))?;
|
|
108
115
|
*opened()
|
|
109
116
|
.lock()
|
|
110
117
|
.unwrap_or_else(|error| error.into_inner())
|
|
@@ -253,6 +260,26 @@ impl Trust {
|
|
|
253
260
|
self.load(&mut kept).map(|_| ())
|
|
254
261
|
}
|
|
255
262
|
|
|
263
|
+
/// Finish setup on the already-validated reader connection. The callback is
|
|
264
|
+
/// synchronous under its mutex; no second setup connection or credential
|
|
265
|
+
/// lifetime escapes into the returned value.
|
|
266
|
+
pub(crate) fn with_connection<T>(
|
|
267
|
+
&self,
|
|
268
|
+
use_connection: impl FnOnce(&mut Connection) -> Result<T>,
|
|
269
|
+
) -> Result<T> {
|
|
270
|
+
let mut kept = self.kept.lock().unwrap_or_else(|error| error.into_inner());
|
|
271
|
+
self.load(&mut kept)?;
|
|
272
|
+
let connection = &mut kept.as_mut().expect("validated connection").connection;
|
|
273
|
+
// A kept connection can be used by another Runtime thread later. Install
|
|
274
|
+
// the lock context on the thread actually executing this operation.
|
|
275
|
+
lock::wait_for_live_holders(connection, &self.path)?;
|
|
276
|
+
let _identity = crate::filesystem_identity::FilesystemIdentity::for_store_root(
|
|
277
|
+
self.path.parent().unwrap_or(&self.path),
|
|
278
|
+
)
|
|
279
|
+
.map_err(|error| io("enter catalog filesystem identity", error))?;
|
|
280
|
+
use_connection(connection)
|
|
281
|
+
}
|
|
282
|
+
|
|
256
283
|
fn miss(&self, sha256: &str) -> Option<crate::store::VRecord> {
|
|
257
284
|
let mut kept = self.kept.lock().unwrap_or_else(|error| error.into_inner());
|
|
258
285
|
if self.load(&mut kept).ok()? {
|