tensorfs 0.3.76__tar.gz → 0.3.78__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.76 → tensorfs-0.3.78}/Cargo.lock +2 -2
- {tensorfs-0.3.76 → tensorfs-0.3.78}/Cargo.toml +1 -1
- {tensorfs-0.3.76 → tensorfs-0.3.78}/PKG-INFO +1 -1
- tensorfs-0.3.78/crates/tensorfs-core/src/bin/tfs/ensure.rs +160 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/fetch.rs +1 -1
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/ingest.rs +1 -1
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/main.rs +88 -2
- tensorfs-0.3.78/crates/tensorfs-core/src/ensure/admission.rs +333 -0
- tensorfs-0.3.78/crates/tensorfs-core/src/ensure/flight.rs +216 -0
- tensorfs-0.3.78/crates/tensorfs-core/src/ensure.rs +364 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/err.rs +3 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/gc.rs +18 -3
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/convert.rs +53 -16
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/narrow.rs +5 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/preflight.rs +62 -1
- tensorfs-0.3.78/crates/tensorfs-core/src/ingest/routes/anima.routes.json +1 -0
- tensorfs-0.3.78/crates/tensorfs-core/src/ingest/routes/anima.spec.json +52 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/routes.rs +39 -6
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/source.rs +27 -12
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/transaction.rs +1 -1
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/lib.rs +5 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/providers.rs +42 -17
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/source_artifact.rs +46 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/http.rs +72 -30
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +1 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/mod.rs +2 -2
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/pull.rs +75 -30
- tensorfs-0.3.78/crates/tensorfs-core/tests/civitai_anima.rs +458 -0
- tensorfs-0.3.78/crates/tensorfs-core/tests/ensure.rs +781 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/source_artifacts.rs +1 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/source_download.rs +2 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/source_prefixes.rs +1 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/source_preflight.rs +1 -0
- tensorfs-0.3.78/crates/tensorfs-py/src/ensure.rs +145 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-py/src/ingest.rs +1 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-py/src/lib.rs +4 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-py/src/store.rs +32 -47
- {tensorfs-0.3.76 → tensorfs-0.3.78}/pyproject.toml +1 -1
- {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/__init__.py +116 -1
- {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/_ext.pyi +25 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/errors.py +2 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/asset_update.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/cache_roots.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/catalog/lock.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/catalog.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/group_commit_tests.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/history_cost_tests.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/part_rows_tests.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived/roots.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/derived.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/descriptors.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/disk.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/fill.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/routes/sdxl.routes.json +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/routes/sdxl.spec.json +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/meta.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/repo_cache.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/stats.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/store.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/memory.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/pace.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/policy.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/push.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/transport/tests.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/unsynced.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/as_is.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/hub_tls.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/routed_single_file.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/trust_index.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/_derived_channel.py +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/_source_channel.py +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/_tensors.py +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/python/tensorfs/derived.py +0 -0
- {tensorfs-0.3.76 → tensorfs-0.3.78}/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.78"
|
|
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.78"
|
|
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| {
|
|
@@ -446,7 +446,7 @@ pub fn cmd_bank(dialect: &str, converter: &str, f: &Flags) -> ExitCode {
|
|
|
446
446
|
for (component, path) in &srcs {
|
|
447
447
|
let (full, raw) = ok!(source_core::read_carrier_at(path));
|
|
448
448
|
let h = if conv.dialect == "safetensors.single_file" {
|
|
449
|
-
ok!(convert::single_file_component(component, &full))
|
|
449
|
+
ok!(convert::single_file_component(conv, component, &full))
|
|
450
450
|
} else {
|
|
451
451
|
full
|
|
452
452
|
};
|
|
@@ -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,333 @@
|
|
|
1
|
+
//! Disk admission for one flight, and the GC policy that makes room. Disk only: nothing here
|
|
2
|
+
//! looks at RAM (owner rule: no RAM floor; the Runtime pages to disk).
|
|
3
|
+
//!
|
|
4
|
+
//! The numbers are the Host's storage-pressure policy (pod-supervisor `pressure.go`,
|
|
5
|
+
//! `fetch.go`): a reserve of max(1 GiB, 1/50 of the filesystem) and 32 inodes is never
|
|
6
|
+
//! admitted; a download needs its wanted bytes and 16 + 3 inodes per wanted object; live
|
|
7
|
+
//! flights' unwritten remainders are already spoken for. When a flight does not fit, the GC
|
|
8
|
+
//! policy runs in order: unreferenced bytes, delivered products (oldest delivery first),
|
|
9
|
+
//! then cached model lanes (the dedup-aware reclaim planner); the answer after that is the
|
|
10
|
+
//! answer.
|
|
11
|
+
|
|
12
|
+
use std::ops::Add;
|
|
13
|
+
use std::path::Path;
|
|
14
|
+
|
|
15
|
+
use crate::err::{refuse, Code, Refusal, Result};
|
|
16
|
+
use crate::gc;
|
|
17
|
+
use crate::source_artifact;
|
|
18
|
+
use crate::store::Store;
|
|
19
|
+
|
|
20
|
+
use super::flight::{self, Flight, Record};
|
|
21
|
+
|
|
22
|
+
pub(super) const RESERVE_BYTES: u64 = 1 << 30;
|
|
23
|
+
pub(super) const RESERVE_DIVISOR: u64 = 50;
|
|
24
|
+
pub(super) const RESERVE_INODES: u64 = 32;
|
|
25
|
+
pub(super) const METADATA_INODES: u64 = 16;
|
|
26
|
+
pub(super) const OBJECT_INODES: u64 = 3;
|
|
27
|
+
/// Pressure begins at 1/10 of the filesystem free and ends above 1/5.
|
|
28
|
+
pub(super) const ENTER_DIVISOR: u64 = 10;
|
|
29
|
+
pub(super) const LEAVE_DIVISOR: u64 = 5;
|
|
30
|
+
|
|
31
|
+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
32
|
+
pub(super) struct Space {
|
|
33
|
+
pub bytes: u64,
|
|
34
|
+
pub inodes: u64,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl Add for Space {
|
|
38
|
+
type Output = Space;
|
|
39
|
+
fn add(self, other: Space) -> Space {
|
|
40
|
+
Space {
|
|
41
|
+
bytes: self.bytes.saturating_add(other.bytes),
|
|
42
|
+
inodes: self.inodes.saturating_add(other.inodes),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
impl Space {
|
|
48
|
+
/// What `objects` new objects of `bytes` in total cost the disk.
|
|
49
|
+
pub fn of(bytes: u64, objects: u64) -> Space {
|
|
50
|
+
Space {
|
|
51
|
+
bytes,
|
|
52
|
+
inodes: if objects == 0 {
|
|
53
|
+
0
|
|
54
|
+
} else {
|
|
55
|
+
METADATA_INODES + OBJECT_INODES * objects
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn within(self, free: Space) -> bool {
|
|
61
|
+
self.bytes <= free.bytes && self.inodes <= free.inodes
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
66
|
+
pub(super) struct Disk {
|
|
67
|
+
pub capacity: u64,
|
|
68
|
+
pub available: u64,
|
|
69
|
+
/// Zero when the filesystem has no inode limit.
|
|
70
|
+
pub inodes: u64,
|
|
71
|
+
pub available_inodes: u64,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pub(super) fn measure(root: &Path) -> Result<Disk> {
|
|
75
|
+
let stat = rustix::fs::statvfs(root).map_err(|e| Refusal {
|
|
76
|
+
code: Code::CAPACITY_EXHAUSTED,
|
|
77
|
+
detail: format!(
|
|
78
|
+
"cannot measure the filesystem under {}: {e}; admission fails closed",
|
|
79
|
+
root.display()
|
|
80
|
+
),
|
|
81
|
+
})?;
|
|
82
|
+
let unit = stat.f_frsize.max(1);
|
|
83
|
+
Ok(Disk {
|
|
84
|
+
capacity: stat.f_blocks.saturating_mul(unit),
|
|
85
|
+
available: stat.f_bavail.saturating_mul(unit),
|
|
86
|
+
inodes: stat.f_files,
|
|
87
|
+
available_inodes: stat.f_favail,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// What `disk` admits once its reserve and `others` are set aside.
|
|
92
|
+
pub(super) fn free(disk: &Disk, others: Space) -> Space {
|
|
93
|
+
let reserve = RESERVE_BYTES.max(disk.capacity / RESERVE_DIVISOR);
|
|
94
|
+
Space {
|
|
95
|
+
bytes: disk
|
|
96
|
+
.available
|
|
97
|
+
.saturating_sub(reserve)
|
|
98
|
+
.saturating_sub(others.bytes),
|
|
99
|
+
inodes: if disk.inodes == 0 {
|
|
100
|
+
u64::MAX
|
|
101
|
+
} else {
|
|
102
|
+
disk.available_inodes
|
|
103
|
+
.saturating_sub(RESERVE_INODES)
|
|
104
|
+
.saturating_sub(others.inodes)
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// The GC policy's outcome: what it freed, why a tier could not run, and the disk after.
|
|
110
|
+
struct Collected {
|
|
111
|
+
bytes: u64,
|
|
112
|
+
unable: Option<Refusal>,
|
|
113
|
+
disk: Disk,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Run the GC policy's tiers until `short(disk)` says nothing is short. `short` answers
|
|
117
|
+
/// the bytes still missing, or `None` once the disk is enough.
|
|
118
|
+
fn collect_until(
|
|
119
|
+
store: &Store,
|
|
120
|
+
protected: &[String],
|
|
121
|
+
short: &dyn Fn(&Disk) -> Option<u64>,
|
|
122
|
+
collecting: &dyn Fn(),
|
|
123
|
+
) -> Result<Collected> {
|
|
124
|
+
let root = store.root();
|
|
125
|
+
let mut collected = Collected {
|
|
126
|
+
bytes: 0,
|
|
127
|
+
unable: None,
|
|
128
|
+
disk: measure(root)?,
|
|
129
|
+
};
|
|
130
|
+
for tier in 0..3 {
|
|
131
|
+
let Some(short) = short(&collected.disk) else {
|
|
132
|
+
break;
|
|
133
|
+
};
|
|
134
|
+
if tier == 0 {
|
|
135
|
+
collecting();
|
|
136
|
+
}
|
|
137
|
+
let freed = match tier {
|
|
138
|
+
0 => gc::collect(root, false).map(|report| report.reclaimed_bytes),
|
|
139
|
+
1 => release_delivered(store, short.max(1)),
|
|
140
|
+
_ => gc::collect_cached_for(root, protected, short.max(1))
|
|
141
|
+
.map(|report| report.reclaimed_bytes),
|
|
142
|
+
};
|
|
143
|
+
match freed {
|
|
144
|
+
Ok(bytes) => collected.bytes += bytes,
|
|
145
|
+
Err(refusal) => collected.unable = Some(refusal),
|
|
146
|
+
}
|
|
147
|
+
collected.disk = measure(root)?;
|
|
148
|
+
}
|
|
149
|
+
Ok(collected)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Live flights under the admission gate: what they still owe the disk, and their
|
|
153
|
+
/// manifests, which no tier may evict.
|
|
154
|
+
fn flights(store: &Store, mine: &str, keep: &[String]) -> Result<(Space, Vec<String>)> {
|
|
155
|
+
let live = flight::live(store, mine)?;
|
|
156
|
+
let owed = live
|
|
157
|
+
.iter()
|
|
158
|
+
.filter_map(|(_, record)| record.as_ref().map(Record::remaining))
|
|
159
|
+
.fold(Space::default(), Add::add);
|
|
160
|
+
let mut protected = keep.to_vec();
|
|
161
|
+
protected.extend(live.into_iter().map(|(manifest, _)| manifest));
|
|
162
|
+
protected.extend((!mine.is_empty()).then(|| mine.to_string()));
|
|
163
|
+
Ok((owed, protected))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Admit `record`'s flight, collecting under the GC policy when it does not fit. Returns
|
|
167
|
+
/// the bytes collected. `keep` names manifests no tier may evict.
|
|
168
|
+
pub(super) fn admit(
|
|
169
|
+
store: &Store,
|
|
170
|
+
flight: &Flight,
|
|
171
|
+
record: &Record,
|
|
172
|
+
keep: &[String],
|
|
173
|
+
collecting: &dyn Fn(),
|
|
174
|
+
) -> Result<u64> {
|
|
175
|
+
let need = Space::of(record.wanted_bytes, record.wanted_objects);
|
|
176
|
+
let _gate = flight::gate(store)?;
|
|
177
|
+
let (others, protected) = flights(store, &flight.manifest, keep)?;
|
|
178
|
+
let short = |disk: &Disk| {
|
|
179
|
+
let free = free(disk, others);
|
|
180
|
+
(!need.within(free)).then(|| need.bytes.saturating_sub(free.bytes))
|
|
181
|
+
};
|
|
182
|
+
let collected = collect_until(store, &protected, &short, collecting)?;
|
|
183
|
+
if short(&collected.disk).is_none() {
|
|
184
|
+
flight.record(record)?;
|
|
185
|
+
return Ok(collected.bytes);
|
|
186
|
+
}
|
|
187
|
+
let free = free(&collected.disk, others);
|
|
188
|
+
let inodes = match collected.disk.inodes {
|
|
189
|
+
0 => String::new(),
|
|
190
|
+
_ => format!(" ({} inodes, {} free)", need.inodes, free.inodes),
|
|
191
|
+
};
|
|
192
|
+
let why = collected
|
|
193
|
+
.unable
|
|
194
|
+
.map_or(String::new(), |r| format!("; GC could not run: {r}"));
|
|
195
|
+
refuse(
|
|
196
|
+
Code::CAPACITY_EXHAUSTED,
|
|
197
|
+
format!(
|
|
198
|
+
"this download needs {} B{inodes}; the disk under {} admits {} B after its \
|
|
199
|
+
reserve and {} B still owed to other downloads; GC freed {} B{why}",
|
|
200
|
+
need.bytes,
|
|
201
|
+
store.root().display(),
|
|
202
|
+
free.bytes,
|
|
203
|
+
others.bytes,
|
|
204
|
+
collected.bytes,
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// What one pressure pass found and did.
|
|
210
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
211
|
+
pub struct Relief {
|
|
212
|
+
/// The disk was under pressure when the pass began.
|
|
213
|
+
pub pressure: bool,
|
|
214
|
+
pub collected_bytes: u64,
|
|
215
|
+
pub capacity_bytes: u64,
|
|
216
|
+
pub available_bytes: u64,
|
|
217
|
+
/// Why a tier could not run, when one could not.
|
|
218
|
+
pub unable: Option<String>,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// The Host's pressure hysteresis in one pass: a disk with at most 1/10 free (or under its
|
|
222
|
+
/// reserve) is collected under the GC policy until more than 1/5 is free. Live flights'
|
|
223
|
+
/// manifests and `keep` are never evicted. A disk not under pressure is left alone.
|
|
224
|
+
pub fn relieve(store: &Store, keep: &[String]) -> Result<Relief> {
|
|
225
|
+
let _gate = flight::gate(store)?;
|
|
226
|
+
let (_, protected) = flights(store, "", keep)?;
|
|
227
|
+
let disk = measure(store.root())?;
|
|
228
|
+
let reserve = RESERVE_BYTES.max(disk.capacity / RESERVE_DIVISOR);
|
|
229
|
+
let low = |disk: &Disk, divisor: u64| {
|
|
230
|
+
disk.available <= reserve.max(disk.capacity / divisor)
|
|
231
|
+
|| (disk.inodes > 0
|
|
232
|
+
&& disk.available_inodes <= RESERVE_INODES.max(disk.inodes / divisor))
|
|
233
|
+
};
|
|
234
|
+
let pressure = low(&disk, ENTER_DIVISOR);
|
|
235
|
+
let short = |disk: &Disk| {
|
|
236
|
+
low(disk, LEAVE_DIVISOR).then(|| {
|
|
237
|
+
(reserve.max(disk.capacity / LEAVE_DIVISOR) + 1).saturating_sub(disk.available)
|
|
238
|
+
})
|
|
239
|
+
};
|
|
240
|
+
let collected = if pressure {
|
|
241
|
+
collect_until(store, &protected, &short, &|| {})?
|
|
242
|
+
} else {
|
|
243
|
+
Collected {
|
|
244
|
+
bytes: 0,
|
|
245
|
+
unable: None,
|
|
246
|
+
disk,
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
Ok(Relief {
|
|
250
|
+
pressure,
|
|
251
|
+
collected_bytes: collected.bytes,
|
|
252
|
+
capacity_bytes: collected.disk.capacity,
|
|
253
|
+
available_bytes: collected.disk.available,
|
|
254
|
+
unable: collected.unable.map(|r| r.to_string()),
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Release delivered products oldest delivery first, in doubling batches, collecting after
|
|
259
|
+
/// each batch, until `short` bytes are freed or none are left.
|
|
260
|
+
fn release_delivered(store: &Store, short: u64) -> Result<u64> {
|
|
261
|
+
let owners = source_artifact::delivered(store)?;
|
|
262
|
+
let (mut freed, mut at, mut batch) = (0u64, 0usize, 1usize);
|
|
263
|
+
while at < owners.len() && freed < short {
|
|
264
|
+
for owner in &owners[at..(at + batch).min(owners.len())] {
|
|
265
|
+
match source_artifact::release(store, owner) {
|
|
266
|
+
Err(refusal) if refusal.code == Code::STORE_BUSY => {}
|
|
267
|
+
other => other?,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
at += batch;
|
|
271
|
+
batch *= 2;
|
|
272
|
+
freed += gc::collect(store.root(), false)?.reclaimed_bytes;
|
|
273
|
+
}
|
|
274
|
+
Ok(freed)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#[cfg(test)]
|
|
278
|
+
mod tests {
|
|
279
|
+
use super::*;
|
|
280
|
+
|
|
281
|
+
const GIB: u64 = 1 << 30;
|
|
282
|
+
|
|
283
|
+
fn disk(capacity: u64, available: u64) -> Disk {
|
|
284
|
+
Disk {
|
|
285
|
+
capacity,
|
|
286
|
+
available,
|
|
287
|
+
inodes: 1000,
|
|
288
|
+
available_inodes: 500,
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#[test]
|
|
293
|
+
fn the_reserve_is_a_gib_or_a_fiftieth_and_others_are_spoken_for() {
|
|
294
|
+
// 10 GiB disk: the reserve is the 1 GiB floor.
|
|
295
|
+
assert_eq!(
|
|
296
|
+
free(&disk(10 * GIB, 5 * GIB), Space::default()).bytes,
|
|
297
|
+
4 * GIB
|
|
298
|
+
);
|
|
299
|
+
// 1000 GiB disk: the reserve is 20 GiB.
|
|
300
|
+
assert_eq!(
|
|
301
|
+
free(&disk(1000 * GIB, 100 * GIB), Space::default()).bytes,
|
|
302
|
+
80 * GIB
|
|
303
|
+
);
|
|
304
|
+
let others = Space::of(GIB, 10);
|
|
305
|
+
let left = free(&disk(10 * GIB, 5 * GIB), others);
|
|
306
|
+
assert_eq!(
|
|
307
|
+
left,
|
|
308
|
+
Space {
|
|
309
|
+
bytes: 3 * GIB,
|
|
310
|
+
inodes: 500 - 32 - 46
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
// Nothing left is zero, never an underflow.
|
|
314
|
+
assert_eq!(free(&disk(10 * GIB, GIB / 2), others).bytes, 0);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#[test]
|
|
318
|
+
fn a_warm_flight_needs_nothing_and_inodes_can_refuse_alone() {
|
|
319
|
+
assert!(Space::of(0, 0).within(Space::default()));
|
|
320
|
+
let tight = Disk {
|
|
321
|
+
inodes: 1000,
|
|
322
|
+
available_inodes: 40,
|
|
323
|
+
..disk(10 * GIB, 5 * GIB)
|
|
324
|
+
};
|
|
325
|
+
assert!(!Space::of(1, 1).within(free(&tight, Space::default())));
|
|
326
|
+
let unlimited = Disk {
|
|
327
|
+
inodes: 0,
|
|
328
|
+
available_inodes: 0,
|
|
329
|
+
..tight
|
|
330
|
+
};
|
|
331
|
+
assert!(Space::of(1, 1000).within(free(&unlimited, Space::default())));
|
|
332
|
+
}
|
|
333
|
+
}
|