tensorfs 0.3.69__tar.gz → 0.3.70__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.70}/Cargo.lock +2 -2
- {tensorfs-0.3.69 → tensorfs-0.3.70}/Cargo.toml +1 -1
- {tensorfs-0.3.69 → tensorfs-0.3.70}/PKG-INFO +1 -1
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/watch.rs +4 -3
- tensorfs-0.3.70/crates/tensorfs-core/src/catalog/lock.rs +121 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/catalog.rs +108 -31
- tensorfs-0.3.70/crates/tensorfs-core/src/derived/group_commit_tests.rs +114 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/roots.rs +44 -26
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived.rs +20 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/meta.rs +1 -6
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/store.rs +18 -1
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/pull.rs +7 -33
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/unsynced.rs +17 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-py/src/store.rs +8 -3
- {tensorfs-0.3.69 → tensorfs-0.3.70}/pyproject.toml +1 -1
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/asset_update.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/cache_roots.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/part_rows_tests.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/descriptors.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/disk.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/fill.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/gc.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/source.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/lib.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/providers.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/repo_cache.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/source_artifact.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/stats.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/http.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/memory.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/mod.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/pace.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/policy.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/push.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/tests.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/source_download.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/trust_index.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-py/src/ingest.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-py/src/lib.rs +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/__init__.py +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/_derived_channel.py +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/_ext.pyi +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/_source_channel.py +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/_tensors.py +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/derived.py +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/errors.py +0 -0
- {tensorfs-0.3.69 → tensorfs-0.3.70}/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.70"
|
|
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.70"
|
|
458
458
|
dependencies = [
|
|
459
459
|
"pyo3",
|
|
460
460
|
"tensorfs-core",
|
|
@@ -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");
|
|
@@ -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
|
+
}
|
|
@@ -123,6 +123,20 @@ fn io(error: std::io::Error) -> Refusal {
|
|
|
123
123
|
fn directory(store: &Store) -> PathBuf {
|
|
124
124
|
store.root().join("roots/derived")
|
|
125
125
|
}
|
|
126
|
+
|
|
127
|
+
/// A root must survive the machine on every disk but an ephemeral one, whose Store dies with
|
|
128
|
+
/// its container. A parts journal follows the Store's admissions instead ([`Store::flushes`]):
|
|
129
|
+
/// under a writer's epoch its lines become durable with that writer's next checkpoint.
|
|
130
|
+
fn durable(store: &Store) -> bool {
|
|
131
|
+
store.disk_class() != crate::disk::DiskClass::Ephemeral
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Create the root directory; true when this call created it (so its parents need a sync).
|
|
135
|
+
fn create_directory(store: &Store) -> Result<bool> {
|
|
136
|
+
let created = !directory(store).is_dir();
|
|
137
|
+
fs::create_dir_all(directory(store)).map_err(io)?;
|
|
138
|
+
Ok(created)
|
|
139
|
+
}
|
|
126
140
|
fn path(store: &Store, transaction: &str) -> Result<PathBuf> {
|
|
127
141
|
let transaction = super::transaction_id(transaction)?;
|
|
128
142
|
Ok(directory(store).join(format!(
|
|
@@ -150,7 +164,7 @@ pub(super) fn append_parts(store: &Store, transaction: &str, objects: &[ObjectRe
|
|
|
150
164
|
return Ok(());
|
|
151
165
|
}
|
|
152
166
|
let path = parts_path(store, transaction)?;
|
|
153
|
-
|
|
167
|
+
let parents = create_directory(store)?;
|
|
154
168
|
let created = !path.exists();
|
|
155
169
|
let mut file = OpenOptions::new()
|
|
156
170
|
.read(true)
|
|
@@ -170,11 +184,12 @@ pub(super) fn append_parts(store: &Store, transaction: &str, objects: &[ObjectRe
|
|
|
170
184
|
vec![b'\n']
|
|
171
185
|
};
|
|
172
186
|
bytes.extend(parts_line(objects));
|
|
173
|
-
file.write_all(&bytes)
|
|
174
|
-
|
|
175
|
-
.map_err(io)?;
|
|
176
|
-
|
|
177
|
-
|
|
187
|
+
file.write_all(&bytes).map_err(io)?;
|
|
188
|
+
if store.flushes() {
|
|
189
|
+
file.sync_data().map_err(io)?;
|
|
190
|
+
if created {
|
|
191
|
+
sync_directories(store, parents)?;
|
|
192
|
+
}
|
|
178
193
|
}
|
|
179
194
|
Ok(())
|
|
180
195
|
}
|
|
@@ -182,7 +197,7 @@ pub(super) fn append_parts(store: &Store, transaction: &str, objects: &[ObjectRe
|
|
|
182
197
|
/// Replace the journal with exactly these objects (a writer's merged progress).
|
|
183
198
|
pub(super) fn rewrite_parts(store: &Store, transaction: &str, objects: &[ObjectRef]) -> Result<()> {
|
|
184
199
|
let path = parts_path(store, transaction)?;
|
|
185
|
-
|
|
200
|
+
let parents = create_directory(store)?;
|
|
186
201
|
let temporary = path.with_extension(format!("parts-tmp-{}", crate::meta::now_nanos_unique()));
|
|
187
202
|
let mut file = OpenOptions::new()
|
|
188
203
|
.write(true)
|
|
@@ -194,11 +209,15 @@ pub(super) fn rewrite_parts(store: &Store, transaction: &str, objects: &[ObjectR
|
|
|
194
209
|
} else {
|
|
195
210
|
parts_line(objects)
|
|
196
211
|
};
|
|
197
|
-
file.write_all(&bytes)
|
|
198
|
-
|
|
199
|
-
.map_err(io)?;
|
|
212
|
+
file.write_all(&bytes).map_err(io)?;
|
|
213
|
+
if store.flushes() {
|
|
214
|
+
file.sync_all().map_err(io)?;
|
|
215
|
+
}
|
|
200
216
|
fs::rename(&temporary, &path).map_err(io)?;
|
|
201
|
-
|
|
217
|
+
if store.flushes() {
|
|
218
|
+
sync_directories(store, parents)?;
|
|
219
|
+
}
|
|
220
|
+
Ok(())
|
|
202
221
|
}
|
|
203
222
|
|
|
204
223
|
/// Every object named by any transaction's parts journal.
|
|
@@ -235,12 +254,15 @@ pub(super) fn journaled(store: &Store) -> Result<Vec<ObjectRef>> {
|
|
|
235
254
|
Ok(objects)
|
|
236
255
|
}
|
|
237
256
|
|
|
238
|
-
|
|
239
|
-
|
|
257
|
+
/// A rename needs its own directory synced; the parents only when this write created it.
|
|
258
|
+
fn sync_directories(store: &Store, parents: bool) -> Result<()> {
|
|
259
|
+
let own = [directory(store)];
|
|
260
|
+
let all = [
|
|
240
261
|
directory(store),
|
|
241
262
|
store.root().join("roots"),
|
|
242
263
|
store.root().to_path_buf(),
|
|
243
|
-
]
|
|
264
|
+
];
|
|
265
|
+
for directory in if parents { &all[..] } else { &own[..] } {
|
|
244
266
|
File::open(directory)
|
|
245
267
|
.and_then(|file| file.sync_all())
|
|
246
268
|
.map_err(io)?;
|
|
@@ -301,25 +323,20 @@ pub(super) fn write(store: &Store, root: &Root) -> Result<()> {
|
|
|
301
323
|
return refuse(Code::COUNT_CAP, "private derived root exceeds its byte cap");
|
|
302
324
|
}
|
|
303
325
|
let path = path(store, &root.transaction)?;
|
|
304
|
-
|
|
326
|
+
let parents = create_directory(store)?;
|
|
305
327
|
let temporary = path.with_extension(format!("tmp-{}", crate::meta::now_nanos_unique()));
|
|
306
328
|
let mut file = OpenOptions::new()
|
|
307
329
|
.write(true)
|
|
308
330
|
.create_new(true)
|
|
309
331
|
.open(&temporary)
|
|
310
332
|
.map_err(io)?;
|
|
311
|
-
file.write_all(&bytes)
|
|
312
|
-
|
|
313
|
-
.map_err(io)?;
|
|
333
|
+
file.write_all(&bytes).map_err(io)?;
|
|
334
|
+
if durable(store) {
|
|
335
|
+
file.sync_all().map_err(io)?;
|
|
336
|
+
}
|
|
314
337
|
fs::rename(&temporary, &path).map_err(io)?;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
store.root().join("roots"),
|
|
318
|
-
store.root().to_path_buf(),
|
|
319
|
-
] {
|
|
320
|
-
File::open(directory)
|
|
321
|
-
.and_then(|file| file.sync_all())
|
|
322
|
-
.map_err(io)?;
|
|
338
|
+
if durable(store) {
|
|
339
|
+
sync_directories(store, parents)?;
|
|
323
340
|
}
|
|
324
341
|
Ok(())
|
|
325
342
|
}
|
|
@@ -396,6 +413,7 @@ pub(super) fn remove(store: &Store, transaction: &str) -> Result<()> {
|
|
|
396
413
|
Err(error) => return Err(io(error)),
|
|
397
414
|
}
|
|
398
415
|
match fs::remove_file(path(store, transaction)?) {
|
|
416
|
+
Ok(()) if !durable(store) => Ok(()),
|
|
399
417
|
Ok(()) => File::open(directory(store))
|
|
400
418
|
.and_then(|file| file.sync_all())
|
|
401
419
|
.map_err(io),
|
|
@@ -2346,6 +2346,19 @@ fn planned_row(
|
|
|
2346
2346
|
})
|
|
2347
2347
|
}
|
|
2348
2348
|
|
|
2349
|
+
/// The Store a live writer admits through. On a persistent disk its parts and progress
|
|
2350
|
+
/// documents are not synced one by one: each checkpoint (and the commit) makes everything
|
|
2351
|
+
/// admitted since the previous one durable at once, before the root names it, so a crash
|
|
2352
|
+
/// loses at most the work after the last checkpoint. An ephemeral disk needs no sync at all.
|
|
2353
|
+
pub fn writer_store(store: &Store) -> Result<Store> {
|
|
2354
|
+
Ok(store.syncing(crate::unsynced::Epoch::begin_every(store, u64::MAX)?))
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
/// The writer is done: sync anything admitted since its last checkpoint and retire its epoch.
|
|
2358
|
+
pub fn finish_writer(store: &Store) -> Result<()> {
|
|
2359
|
+
store.finish_admitted()
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2349
2362
|
pub fn begin(
|
|
2350
2363
|
store: &Store,
|
|
2351
2364
|
meta: &Meta,
|
|
@@ -2810,6 +2823,8 @@ impl CheckpointCursor {
|
|
|
2810
2823
|
let Some(mut root) = root else {
|
|
2811
2824
|
return Ok(None);
|
|
2812
2825
|
};
|
|
2826
|
+
// Everything the new head names is durable before the root publishes it.
|
|
2827
|
+
store.sync_admitted()?;
|
|
2813
2828
|
root.checkpoint = extended.head.head.clone();
|
|
2814
2829
|
roots::write(store, &root)?;
|
|
2815
2830
|
self.tip
|
|
@@ -2935,6 +2950,7 @@ pub fn checkpoint_progress(
|
|
|
2935
2950
|
objects,
|
|
2936
2951
|
None,
|
|
2937
2952
|
)?;
|
|
2953
|
+
store.sync_admitted()?;
|
|
2938
2954
|
let mut txn = meta.txn_for(transaction)?;
|
|
2939
2955
|
let row = open_row(&mut txn.rows, transaction, writer_session)?;
|
|
2940
2956
|
retain_progress(store, row, exported.head.head.as_ref())?;
|
|
@@ -3609,6 +3625,7 @@ pub fn commit(
|
|
|
3609
3625
|
walk.require_held(store, &crate::ingest::custody::live(store.root())?)?;
|
|
3610
3626
|
let (inherited_payload_objects, inherited_payload_bytes) =
|
|
3611
3627
|
inherit_observation(&header, &row_before)?;
|
|
3628
|
+
store.sync_admitted()?;
|
|
3612
3629
|
|
|
3613
3630
|
let mut txn = meta.txn_for(&transaction)?;
|
|
3614
3631
|
let row = open_row(&mut txn.rows, &transaction, writer_session)?;
|
|
@@ -4598,3 +4615,6 @@ mod companion_tests;
|
|
|
4598
4615
|
|
|
4599
4616
|
#[cfg(test)]
|
|
4600
4617
|
mod part_rows_tests;
|
|
4618
|
+
|
|
4619
|
+
#[cfg(test)]
|
|
4620
|
+
mod group_commit_tests;
|
|
@@ -222,12 +222,7 @@ impl Meta {
|
|
|
222
222
|
|
|
223
223
|
fn connection(&self) -> Result<Connection> {
|
|
224
224
|
let connection = Connection::open(&self.db).map_err(|error| sql("open catalog", error))?;
|
|
225
|
-
connection
|
|
226
|
-
.busy_timeout(std::time::Duration::from_secs(5))
|
|
227
|
-
.map_err(|error| sql("set catalog busy timeout", error))?;
|
|
228
|
-
connection
|
|
229
|
-
.execute_batch("PRAGMA foreign_keys=ON; PRAGMA synchronous=FULL;")
|
|
230
|
-
.map_err(|error| sql("configure catalog", error))?;
|
|
225
|
+
crate::catalog::configure(&connection, &self.db)?;
|
|
231
226
|
Ok(connection)
|
|
232
227
|
}
|
|
233
228
|
|