tensorfs 0.3.63__tar.gz → 0.3.65__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.63 → tensorfs-0.3.65}/Cargo.lock +2 -2
- {tensorfs-0.3.63 → tensorfs-0.3.65}/Cargo.toml +1 -1
- {tensorfs-0.3.63 → tensorfs-0.3.65}/PKG-INFO +1 -1
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/fetch.rs +3 -1
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/catalog.rs +10 -1
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/part_rows_tests.rs +157 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/progress.rs +10 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/resume_tests.rs +13 -3
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived.rs +166 -10
- tensorfs-0.3.65/crates/tensorfs-core/src/descriptors.rs +112 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/durability.rs +67 -27
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/lib.rs +1 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/meta.rs +11 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/providers.rs +1 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/store.rs +41 -3
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/http.rs +27 -9
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +9 -5
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/ledger.rs +15 -1
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/pace.rs +54 -8
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/policy.rs +45 -6
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/pull.rs +121 -49
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/tests.rs +5 -0
- tensorfs-0.3.65/crates/tensorfs-core/tests/fd_budget.rs +256 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_artifacts.rs +1 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_download.rs +1 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_prefixes.rs +1 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/store_boundaries.rs +32 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/transfer_memory.rs +1 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-py/src/store.rs +11 -1
- {tensorfs-0.3.63 → tensorfs-0.3.65}/pyproject.toml +1 -1
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/asset_update.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/cache_roots.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/derived/roots.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/fill.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/gc.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/source.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/repo_cache.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/source_artifact.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/stats.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/memory.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/mod.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/push.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/trust_index.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-py/src/ingest.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/crates/tensorfs-py/src/lib.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/__init__.py +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/_derived_channel.py +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/_ext.pyi +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/_source_channel.py +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/_tensors.py +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/derived.py +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/python/tensorfs/errors.py +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.65}/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.65"
|
|
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.65"
|
|
458
458
|
dependencies = [
|
|
459
459
|
"pyo3",
|
|
460
460
|
"tensorfs-core",
|
|
@@ -182,6 +182,7 @@ fn policy_of(flags: &Flags) -> SourcePolicy {
|
|
|
182
182
|
.unwrap_or_default(),
|
|
183
183
|
allow_local: crate::flag_on(flags, "allow-local"),
|
|
184
184
|
max_redirects: crate::flag_num(flags, "max-redirects", 0u32),
|
|
185
|
+
..Default::default()
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
|
|
@@ -334,12 +335,13 @@ fn pull_with_budget(
|
|
|
334
335
|
// And what admitting them cost: a pull whose streams stop growing while its batches
|
|
335
336
|
// take most of the wall time is held by the disk, not the link.
|
|
336
337
|
eprintln!(
|
|
337
|
-
"{{\"event\":\"pull.streams\",\"model\":{},\"seconds\":{:.3},\"bytes_moved\":{},\"streams\":{},\"levels\":[{}],\"admission\":{{\"objects\":{},\"batches\":{},\"seconds\":{:.3}}}}}",
|
|
338
|
+
"{{\"event\":\"pull.streams\",\"model\":{},\"seconds\":{:.3},\"bytes_moved\":{},\"streams\":{},\"levels\":[{}],\"backoffs\":{},\"admission\":{{\"objects\":{},\"batches\":{},\"seconds\":{:.3}}}}}",
|
|
338
339
|
quoted(&report.model),
|
|
339
340
|
seconds,
|
|
340
341
|
report.bytes_moved,
|
|
341
342
|
report.streams,
|
|
342
343
|
levels.join(","),
|
|
344
|
+
report.backoffs,
|
|
343
345
|
after.admitted_objects - before.admitted_objects,
|
|
344
346
|
after.admission_batches - before.admission_batches,
|
|
345
347
|
(after.admission_nanos - before.admission_nanos) as f64 / 1e9,
|
|
@@ -1473,7 +1473,16 @@ impl WriterGuard {
|
|
|
1473
1473
|
.read(true)
|
|
1474
1474
|
.write(true)
|
|
1475
1475
|
.open(&path)
|
|
1476
|
-
.map_err(|error|
|
|
1476
|
+
.map_err(|error| {
|
|
1477
|
+
if crate::descriptors::exhausted(&error) {
|
|
1478
|
+
Refusal {
|
|
1479
|
+
code: Code::FD_HEADROOM,
|
|
1480
|
+
detail: format!("create writer marker: {error}"),
|
|
1481
|
+
}
|
|
1482
|
+
} else {
|
|
1483
|
+
io("create writer marker", error)
|
|
1484
|
+
}
|
|
1485
|
+
})?;
|
|
1477
1486
|
file.try_lock_exclusive()
|
|
1478
1487
|
.map_err(|error| io("lock writer marker", error))?;
|
|
1479
1488
|
Ok(Self {
|
|
@@ -44,6 +44,7 @@ fn add(store: &Store, meta: &Meta, id: &str, writer: &Begin, key: &str, byte: u8
|
|
|
44
44
|
id,
|
|
45
45
|
1,
|
|
46
46
|
&writer.additions,
|
|
47
|
+
None,
|
|
47
48
|
("model", key, "value"),
|
|
48
49
|
&mut [byte; 512].as_slice(),
|
|
49
50
|
)
|
|
@@ -112,6 +113,7 @@ fn parts_are_rows_that_resume_refuse_conflicts_and_stay_retained() {
|
|
|
112
113
|
&id,
|
|
113
114
|
2,
|
|
114
115
|
&resumed.additions,
|
|
116
|
+
None,
|
|
115
117
|
("model", "weight_2", "value"),
|
|
116
118
|
&mut [3u8; 512].as_slice(),
|
|
117
119
|
)
|
|
@@ -167,3 +169,158 @@ fn an_older_builds_inline_row_and_a_torn_journal_line_are_read() {
|
|
|
167
169
|
assert!(!journal.exists());
|
|
168
170
|
std::fs::remove_dir_all(store.root()).unwrap();
|
|
169
171
|
}
|
|
172
|
+
|
|
173
|
+
#[test]
|
|
174
|
+
fn indexed_checkpoints_write_the_chain_full_checkpoints_write() {
|
|
175
|
+
const COUNT: usize = 40;
|
|
176
|
+
let mut stores = Vec::new();
|
|
177
|
+
for label in ["full", "indexed"] {
|
|
178
|
+
let (store, meta, id, mut declaration) = fixture(label);
|
|
179
|
+
let prototype = declaration.components[0].add[0].clone();
|
|
180
|
+
declaration.components[0].add = (0..COUNT)
|
|
181
|
+
.map(|index| {
|
|
182
|
+
let mut tensor = prototype.clone();
|
|
183
|
+
tensor.key = format!("weight_{index:03}");
|
|
184
|
+
tensor
|
|
185
|
+
})
|
|
186
|
+
.collect();
|
|
187
|
+
declaration.order = (0..COUNT)
|
|
188
|
+
.map(|index| ("model".into(), format!("weight_{index:03}")))
|
|
189
|
+
.collect();
|
|
190
|
+
declaration.configs = vec![ConfigDeclaration::Add {
|
|
191
|
+
target: "model.json".into(),
|
|
192
|
+
}];
|
|
193
|
+
declaration.max_new_bytes = (COUNT * 512 + 64) as u64;
|
|
194
|
+
let writer = begin(&store, &meta, &id, 1, declaration.clone(), None).unwrap();
|
|
195
|
+
stores.push((store, meta, id, declaration, writer));
|
|
196
|
+
}
|
|
197
|
+
let mut cursor = None;
|
|
198
|
+
let (mut full_prior, mut indexed_prior) = (None, None);
|
|
199
|
+
let mut heads: Vec<ObjectRef> = Vec::new();
|
|
200
|
+
for index in 0..COUNT {
|
|
201
|
+
let key = format!("weight_{index:03}");
|
|
202
|
+
let payload = [index as u8; 512];
|
|
203
|
+
let (store, meta, id, _, writer) = &stores[0];
|
|
204
|
+
add_indexed_part(
|
|
205
|
+
store,
|
|
206
|
+
meta,
|
|
207
|
+
id,
|
|
208
|
+
1,
|
|
209
|
+
&writer.additions,
|
|
210
|
+
None,
|
|
211
|
+
("model", &key, "value"),
|
|
212
|
+
&mut payload.as_slice(),
|
|
213
|
+
)
|
|
214
|
+
.unwrap();
|
|
215
|
+
let full = checkpoint_progress(store, meta, id, 1, "request", "model", full_prior.as_ref())
|
|
216
|
+
.unwrap();
|
|
217
|
+
let (store, meta, id, _, writer) = &stores[1];
|
|
218
|
+
add_indexed_part(
|
|
219
|
+
store,
|
|
220
|
+
meta,
|
|
221
|
+
id,
|
|
222
|
+
1,
|
|
223
|
+
&writer.additions,
|
|
224
|
+
cursor.as_mut(),
|
|
225
|
+
("model", &key, "value"),
|
|
226
|
+
&mut payload.as_slice(),
|
|
227
|
+
)
|
|
228
|
+
.unwrap();
|
|
229
|
+
// An older acknowledged head sends the writer down the full path, which re-seeds it.
|
|
230
|
+
let previous = if index == 25 {
|
|
231
|
+
heads.get(heads.len() - 3).cloned()
|
|
232
|
+
} else {
|
|
233
|
+
indexed_prior.clone()
|
|
234
|
+
};
|
|
235
|
+
let indexed = checkpoint_indexed(
|
|
236
|
+
store,
|
|
237
|
+
meta,
|
|
238
|
+
id,
|
|
239
|
+
1,
|
|
240
|
+
"request",
|
|
241
|
+
"model",
|
|
242
|
+
previous.as_ref(),
|
|
243
|
+
&mut cursor,
|
|
244
|
+
)
|
|
245
|
+
.unwrap();
|
|
246
|
+
assert_eq!(full, indexed, "checkpoint {index} diverged");
|
|
247
|
+
assert_eq!(cursor.as_ref().unwrap().tip.head, indexed.1);
|
|
248
|
+
full_prior = full.1.head;
|
|
249
|
+
indexed_prior = indexed.1.head;
|
|
250
|
+
heads.push(indexed_prior.clone().unwrap());
|
|
251
|
+
if index == 20 {
|
|
252
|
+
for (store, meta, id, _, _) in &stores {
|
|
253
|
+
add_config(meta, id, 1, "model.json", &mut b"{}".as_slice()).unwrap();
|
|
254
|
+
let _ = store;
|
|
255
|
+
}
|
|
256
|
+
cursor = None;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
let (store, meta, id, declaration, writer) = stores.pop().unwrap();
|
|
260
|
+
let head = indexed_prior.unwrap();
|
|
261
|
+
let canonical = String::from_utf8(declaration.clone().canonical_work_bytes().unwrap()).unwrap();
|
|
262
|
+
let restored = read_progress(&store, &head, &id, &canonical, &[]).unwrap();
|
|
263
|
+
assert_eq!(restored.added_parts.len(), COUNT);
|
|
264
|
+
assert_eq!(restored.added_configs.len(), 1);
|
|
265
|
+
fence(&meta, &id, 1).unwrap();
|
|
266
|
+
release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
|
|
267
|
+
let resumed = begin(&store, &meta, &id, 2, declaration, Some(&head)).unwrap();
|
|
268
|
+
assert_eq!(completed(&meta, &id, 2).unwrap().0.len(), COUNT);
|
|
269
|
+
fence(&meta, &id, 2).unwrap();
|
|
270
|
+
release_guards(&meta, Some(resumed.writer_hold), resumed.source_leases);
|
|
271
|
+
std::fs::remove_dir_all(store.root()).unwrap();
|
|
272
|
+
for (store, meta, id, _, writer) in stores {
|
|
273
|
+
fence(&meta, &id, 1).unwrap();
|
|
274
|
+
release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
|
|
275
|
+
std::fs::remove_dir_all(store.root()).unwrap();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#[test]
|
|
280
|
+
fn an_indexed_checkpoint_does_not_reread_its_chain() {
|
|
281
|
+
let (store, meta, id, declaration) = fixture("no-reread");
|
|
282
|
+
let writer = begin(&store, &meta, &id, 1, declaration, None).unwrap();
|
|
283
|
+
let mut cursor = None;
|
|
284
|
+
let mut prior: Option<ObjectRef> = None;
|
|
285
|
+
let mut first = None;
|
|
286
|
+
for (index, key) in ["weight_0", "weight_1", "weight_2"].into_iter().enumerate() {
|
|
287
|
+
add_indexed_part(
|
|
288
|
+
&store,
|
|
289
|
+
&meta,
|
|
290
|
+
&id,
|
|
291
|
+
1,
|
|
292
|
+
&writer.additions,
|
|
293
|
+
cursor.as_mut(),
|
|
294
|
+
("model", key, "value"),
|
|
295
|
+
&mut [index as u8 + 1; 512].as_slice(),
|
|
296
|
+
)
|
|
297
|
+
.unwrap();
|
|
298
|
+
let (_, head) = checkpoint_indexed(
|
|
299
|
+
&store,
|
|
300
|
+
&meta,
|
|
301
|
+
&id,
|
|
302
|
+
1,
|
|
303
|
+
"request",
|
|
304
|
+
"model",
|
|
305
|
+
prior.as_ref(),
|
|
306
|
+
&mut cursor,
|
|
307
|
+
)
|
|
308
|
+
.unwrap();
|
|
309
|
+
prior = head.head;
|
|
310
|
+
first.get_or_insert(prior.clone().unwrap());
|
|
311
|
+
if index == 0 {
|
|
312
|
+
// The chain's root link is gone: a checkpoint that walked the chain now fails.
|
|
313
|
+
let root = first.clone().unwrap();
|
|
314
|
+
std::fs::remove_file(store.object_path(root.sha256.trim_start_matches("sha256:")))
|
|
315
|
+
.unwrap();
|
|
316
|
+
assert!(
|
|
317
|
+
checkpoint_progress(&store, &meta, &id, 1, "request", "model", prior.as_ref())
|
|
318
|
+
.is_err()
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
assert_eq!(cursor.as_ref().unwrap().parts, 3);
|
|
323
|
+
fence(&meta, &id, 1).unwrap();
|
|
324
|
+
release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
|
|
325
|
+
std::fs::remove_dir_all(store.root()).unwrap();
|
|
326
|
+
}
|
|
@@ -21,6 +21,16 @@ fn part_key(part: &AddedPart) -> (&str, &str, &str) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
impl Delta {
|
|
24
|
+
/// The parts a writer accepted since its last checkpoint, sorted as a delta stores them.
|
|
25
|
+
pub(super) fn accepted(work: ObjectRef, mut parts: Vec<AddedPart>) -> Self {
|
|
26
|
+
parts.sort_by(|left, right| part_key(left).cmp(&part_key(right)));
|
|
27
|
+
Self {
|
|
28
|
+
work,
|
|
29
|
+
parts,
|
|
30
|
+
configs: Vec::new(),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
pub(super) fn between(
|
|
25
35
|
row: &TransactionRow,
|
|
26
36
|
prior: Option<&TransactionRow>,
|
|
@@ -981,6 +981,7 @@ fn profile_derived_transaction_scaling() {
|
|
|
981
981
|
let mut part_ms = 0.0;
|
|
982
982
|
let mut checkpoint_ms = 0.0;
|
|
983
983
|
let mut prior = None;
|
|
984
|
+
let mut cursor = None;
|
|
984
985
|
for index in 0..writes {
|
|
985
986
|
let key = format!("weight_{index:04}");
|
|
986
987
|
let started = Instant::now();
|
|
@@ -990,15 +991,24 @@ fn profile_derived_transaction_scaling() {
|
|
|
990
991
|
&id,
|
|
991
992
|
1,
|
|
992
993
|
&writer.additions,
|
|
994
|
+
cursor.as_mut(),
|
|
993
995
|
("model", &key, "value"),
|
|
994
996
|
&mut [index as u8; 16].as_slice(),
|
|
995
997
|
)
|
|
996
998
|
.unwrap();
|
|
997
999
|
part_ms += started.elapsed().as_secs_f64() * 1000.0;
|
|
998
1000
|
let started = Instant::now();
|
|
999
|
-
let (_, head) =
|
|
1000
|
-
|
|
1001
|
-
|
|
1001
|
+
let (_, head) = checkpoint_indexed(
|
|
1002
|
+
&store,
|
|
1003
|
+
&meta,
|
|
1004
|
+
&id,
|
|
1005
|
+
1,
|
|
1006
|
+
"profile",
|
|
1007
|
+
"model",
|
|
1008
|
+
prior.as_ref(),
|
|
1009
|
+
&mut cursor,
|
|
1010
|
+
)
|
|
1011
|
+
.unwrap();
|
|
1002
1012
|
prior = head.head;
|
|
1003
1013
|
checkpoint_ms += started.elapsed().as_secs_f64() * 1000.0;
|
|
1004
1014
|
}
|
|
@@ -2711,6 +2711,158 @@ pub fn completed(
|
|
|
2711
2711
|
))
|
|
2712
2712
|
}
|
|
2713
2713
|
|
|
2714
|
+
/// A writer's position in its checkpoint chain: the chain's tip and held blobs, its fixed
|
|
2715
|
+
/// plan and work context, and the parts it accepted since. With it a checkpoint writes and
|
|
2716
|
+
/// verifies only those parts instead of re-reading the chain it extends.
|
|
2717
|
+
#[derive(Debug, Clone)]
|
|
2718
|
+
pub struct CheckpointCursor {
|
|
2719
|
+
chain: String,
|
|
2720
|
+
plan: String,
|
|
2721
|
+
work: ObjectRef,
|
|
2722
|
+
tip: crate::durability::Tip,
|
|
2723
|
+
parts: usize,
|
|
2724
|
+
pending: Vec<AddedPart>,
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
impl CheckpointCursor {
|
|
2728
|
+
/// Seed from a checkpoint the full path just wrote or confirmed. `None` when there is
|
|
2729
|
+
/// no head yet to extend.
|
|
2730
|
+
fn at(
|
|
2731
|
+
store: &Store,
|
|
2732
|
+
meta: &Meta,
|
|
2733
|
+
transaction: &str,
|
|
2734
|
+
writer_session: u64,
|
|
2735
|
+
chain: String,
|
|
2736
|
+
plan: &str,
|
|
2737
|
+
head: &crate::durability::Head,
|
|
2738
|
+
) -> Result<Option<Self>> {
|
|
2739
|
+
let Some(tip) = head.head.as_ref() else {
|
|
2740
|
+
return Ok(None);
|
|
2741
|
+
};
|
|
2742
|
+
let mut rows: Vec<_> = meta.derived_row(transaction)?.into_iter().collect();
|
|
2743
|
+
let row = open_row(&mut rows, transaction, writer_session)?;
|
|
2744
|
+
let context = progress::context(row);
|
|
2745
|
+
let held = crate::durability::local_chain(store, tip, plan)?
|
|
2746
|
+
.blobs
|
|
2747
|
+
.into_iter()
|
|
2748
|
+
.map(|object| object.sha256)
|
|
2749
|
+
.collect();
|
|
2750
|
+
Ok(Some(Self {
|
|
2751
|
+
chain,
|
|
2752
|
+
plan: plan.to_string(),
|
|
2753
|
+
work: ObjectRef::of(&crate::canon::write(&context.to_value())),
|
|
2754
|
+
tip: crate::durability::Tip {
|
|
2755
|
+
head: head.clone(),
|
|
2756
|
+
held,
|
|
2757
|
+
},
|
|
2758
|
+
parts: row.added_parts.len(),
|
|
2759
|
+
pending: Vec::new(),
|
|
2760
|
+
}))
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
/// Extend the chain by the pending parts, or `None` when anything but this writer's own
|
|
2764
|
+
/// accepted parts moved since the cursor was taken.
|
|
2765
|
+
fn advance(
|
|
2766
|
+
&mut self,
|
|
2767
|
+
store: &Store,
|
|
2768
|
+
meta: &Meta,
|
|
2769
|
+
transaction: &str,
|
|
2770
|
+
writer_session: u64,
|
|
2771
|
+
chain: &str,
|
|
2772
|
+
previous: Option<&ObjectRef>,
|
|
2773
|
+
) -> Result<Option<(String, crate::durability::Head)>> {
|
|
2774
|
+
let tip = self.tip.head.head.clone();
|
|
2775
|
+
let root = roots::read(store, transaction)?;
|
|
2776
|
+
if self.chain != chain
|
|
2777
|
+
|| previous.is_some_and(|previous| Some(previous) != tip.as_ref())
|
|
2778
|
+
|| root.as_ref().and_then(|root| root.checkpoint.as_ref()) != tip.as_ref()
|
|
2779
|
+
{
|
|
2780
|
+
return Ok(None);
|
|
2781
|
+
}
|
|
2782
|
+
meta.check_session(transaction, writer_session)?;
|
|
2783
|
+
if meta.part_count(transaction)? != self.parts + self.pending.len() {
|
|
2784
|
+
return Ok(None);
|
|
2785
|
+
}
|
|
2786
|
+
if self.pending.is_empty() {
|
|
2787
|
+
return Ok(Some((self.plan.clone(), self.tip.head.clone())));
|
|
2788
|
+
}
|
|
2789
|
+
let parts = std::mem::take(&mut self.pending);
|
|
2790
|
+
let objects = parts
|
|
2791
|
+
.iter()
|
|
2792
|
+
.flat_map(|added| added.part.segments().iter().cloned())
|
|
2793
|
+
.collect();
|
|
2794
|
+
let count = parts.len();
|
|
2795
|
+
let delta = progress::Delta::accepted(self.work.clone(), parts);
|
|
2796
|
+
let policy = crate::durability::Policy {
|
|
2797
|
+
chain,
|
|
2798
|
+
operation: transaction,
|
|
2799
|
+
plan: &self.plan,
|
|
2800
|
+
interval: crate::durability::INTERVAL_BYTES,
|
|
2801
|
+
};
|
|
2802
|
+
let extended = crate::durability::extend(
|
|
2803
|
+
store,
|
|
2804
|
+
&policy,
|
|
2805
|
+
&self.tip,
|
|
2806
|
+
&crate::canon::write(&delta.to_value()),
|
|
2807
|
+
objects,
|
|
2808
|
+
None,
|
|
2809
|
+
)?;
|
|
2810
|
+
let Some(mut root) = root else {
|
|
2811
|
+
return Ok(None);
|
|
2812
|
+
};
|
|
2813
|
+
root.checkpoint = extended.head.head.clone();
|
|
2814
|
+
roots::write(store, &root)?;
|
|
2815
|
+
self.tip
|
|
2816
|
+
.held
|
|
2817
|
+
.extend(extended.named.into_iter().map(|object| object.sha256));
|
|
2818
|
+
self.tip.head = extended.head;
|
|
2819
|
+
self.parts += count;
|
|
2820
|
+
Ok(Some((self.plan.clone(), self.tip.head.clone())))
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
/// Checkpoint through the writer's cursor, which each full checkpoint re-seeds.
|
|
2825
|
+
#[allow(clippy::too_many_arguments)]
|
|
2826
|
+
pub fn checkpoint_indexed(
|
|
2827
|
+
store: &Store,
|
|
2828
|
+
meta: &Meta,
|
|
2829
|
+
transaction: &str,
|
|
2830
|
+
writer_session: u64,
|
|
2831
|
+
operation: &str,
|
|
2832
|
+
slot: &str,
|
|
2833
|
+
previous: Option<&ObjectRef>,
|
|
2834
|
+
cursor: &mut Option<CheckpointCursor>,
|
|
2835
|
+
) -> Result<(String, crate::durability::Head)> {
|
|
2836
|
+
let chain = format!("{operation}/{slot}");
|
|
2837
|
+
if let Some(current) = cursor.as_mut() {
|
|
2838
|
+
if let Some(advanced) =
|
|
2839
|
+
current.advance(store, meta, transaction, writer_session, &chain, previous)?
|
|
2840
|
+
{
|
|
2841
|
+
return Ok(advanced);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
*cursor = None;
|
|
2845
|
+
let (plan, head) = checkpoint_progress(
|
|
2846
|
+
store,
|
|
2847
|
+
meta,
|
|
2848
|
+
transaction,
|
|
2849
|
+
writer_session,
|
|
2850
|
+
operation,
|
|
2851
|
+
slot,
|
|
2852
|
+
previous,
|
|
2853
|
+
)?;
|
|
2854
|
+
*cursor = CheckpointCursor::at(
|
|
2855
|
+
store,
|
|
2856
|
+
meta,
|
|
2857
|
+
transaction,
|
|
2858
|
+
writer_session,
|
|
2859
|
+
chain,
|
|
2860
|
+
&plan,
|
|
2861
|
+
&head,
|
|
2862
|
+
)?;
|
|
2863
|
+
Ok((plan, head))
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2714
2866
|
pub fn checkpoint_progress(
|
|
2715
2867
|
store: &Store,
|
|
2716
2868
|
meta: &Meta,
|
|
@@ -3115,6 +3267,7 @@ pub fn add_part<R: Read>(
|
|
|
3115
3267
|
transaction,
|
|
3116
3268
|
writer_session,
|
|
3117
3269
|
&additions,
|
|
3270
|
+
None,
|
|
3118
3271
|
value,
|
|
3119
3272
|
reader,
|
|
3120
3273
|
)
|
|
@@ -3122,12 +3275,14 @@ pub fn add_part<R: Read>(
|
|
|
3122
3275
|
|
|
3123
3276
|
/// Accept one part. The cost is this part's bytes plus one indexed SQL row: the
|
|
3124
3277
|
/// declaration is not re-read and earlier accepted parts are neither read nor rewritten.
|
|
3278
|
+
#[allow(clippy::too_many_arguments)]
|
|
3125
3279
|
pub fn add_indexed_part<R: Read>(
|
|
3126
3280
|
store: &Store,
|
|
3127
3281
|
meta: &Meta,
|
|
3128
3282
|
transaction: &str,
|
|
3129
3283
|
writer_session: u64,
|
|
3130
3284
|
additions: &Additions,
|
|
3285
|
+
cursor: Option<&mut CheckpointCursor>,
|
|
3131
3286
|
value: (&str, &str, &str),
|
|
3132
3287
|
reader: &mut R,
|
|
3133
3288
|
) -> Result<crate::checkpoint::Written> {
|
|
@@ -3168,16 +3323,17 @@ pub fn add_indexed_part<R: Read>(
|
|
|
3168
3323
|
// The private root names the objects before SQL accepts the part, so GC never
|
|
3169
3324
|
// observes an accepted part whose bytes it may reclaim.
|
|
3170
3325
|
roots::append_parts(store, &transaction, written.part.segments())?;
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3326
|
+
let added = AddedPart {
|
|
3327
|
+
component: component.to_string(),
|
|
3328
|
+
key: tensor_key.to_string(),
|
|
3329
|
+
role: role.to_string(),
|
|
3330
|
+
part: written.part.clone(),
|
|
3331
|
+
};
|
|
3332
|
+
if meta.accept_part(&transaction, writer_session, &added)? {
|
|
3333
|
+
if let Some(cursor) = cursor {
|
|
3334
|
+
cursor.pending.push(added);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3181
3337
|
Ok(written)
|
|
3182
3338
|
}
|
|
3183
3339
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//! File descriptors as a budget: raised to what the process may have, measured before a
|
|
2
|
+
//! transfer sizes itself, and an exhausted table reported as backpressure.
|
|
3
|
+
//!
|
|
4
|
+
//! Run 1297 (2026-09-27) grew a pull toward 128 streams under a 1,024 soft RLIMIT_NOFILE;
|
|
5
|
+
//! each object in flight holds a socket, a temp file, writer markers and a catalog
|
|
6
|
+
//! connection, and the table ran out mid-pull (`name resolution failed (Too many open
|
|
7
|
+
//! files)`). A Go supervisor raises its own soft limit but hands its children the original.
|
|
8
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
9
|
+
use std::sync::Once;
|
|
10
|
+
|
|
11
|
+
/// Descriptors one object stream may hold at once, and what it is spent on: the object
|
|
12
|
+
/// socket and one pooled idle socket (2); the temp file and the two writer-marker files of
|
|
13
|
+
/// its admission, nested once more by the catalog record (5); the catalog connection and its
|
|
14
|
+
/// journal (2); and the transient DNS socket or contender of a retry (3).
|
|
15
|
+
pub const PER_STREAM: u64 = 12;
|
|
16
|
+
|
|
17
|
+
/// Descriptors left for everything that is not a stream: stdio, the Store's own locks, the
|
|
18
|
+
/// hub calls, the progress channel and whatever the caller opens next.
|
|
19
|
+
pub const MARGIN: u64 = 64;
|
|
20
|
+
|
|
21
|
+
static RAISE: Once = Once::new();
|
|
22
|
+
static EXHAUSTED: AtomicU64 = AtomicU64::new(0);
|
|
23
|
+
|
|
24
|
+
/// Raise the soft RLIMIT_NOFILE to the hard limit, once per process. Idempotent; a refused
|
|
25
|
+
/// raise leaves the limit where it was and the budget below measures that.
|
|
26
|
+
pub fn raise() {
|
|
27
|
+
RAISE.call_once(|| {
|
|
28
|
+
use rustix::process::{getrlimit, setrlimit, Resource, Rlimit};
|
|
29
|
+
let limit = getrlimit(Resource::Nofile);
|
|
30
|
+
if let (Some(soft), Some(hard)) = (limit.current, limit.maximum) {
|
|
31
|
+
if soft < hard {
|
|
32
|
+
let _ = setrlimit(
|
|
33
|
+
Resource::Nofile,
|
|
34
|
+
Rlimit {
|
|
35
|
+
current: Some(hard),
|
|
36
|
+
maximum: Some(hard),
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Descriptors this process can still open now: soft limit less those open and `MARGIN`.
|
|
45
|
+
/// `None` when the platform cannot say (no `/proc`, or no soft limit).
|
|
46
|
+
pub fn available() -> Option<u64> {
|
|
47
|
+
let soft = rustix::process::getrlimit(rustix::process::Resource::Nofile).current?;
|
|
48
|
+
let open = std::fs::read_dir("/proc/self/fd").ok()?.count() as u64;
|
|
49
|
+
Some(soft.saturating_sub(open).saturating_sub(MARGIN))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Object streams the descriptor table allows now: at most `wanted`, at least one.
|
|
53
|
+
pub fn streams_within(wanted: usize) -> usize {
|
|
54
|
+
streams_for(wanted, available())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// `streams_within` against a stated number of free descriptors.
|
|
58
|
+
pub fn streams_for(wanted: usize, available: Option<u64>) -> usize {
|
|
59
|
+
let fits = available.map_or(usize::MAX, |free| {
|
|
60
|
+
usize::try_from(free / PER_STREAM).unwrap_or(usize::MAX)
|
|
61
|
+
});
|
|
62
|
+
wanted.min(fits).max(1)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Whether `error` is the descriptor table being full, counting it when it is. Call it
|
|
66
|
+
/// where an open, socket or resolution fails, before the error loses its errno.
|
|
67
|
+
pub fn exhausted(error: &std::io::Error) -> bool {
|
|
68
|
+
let full = matches!(
|
|
69
|
+
error.raw_os_error(),
|
|
70
|
+
Some(code) if code == rustix::io::Errno::MFILE.raw_os_error()
|
|
71
|
+
|| code == rustix::io::Errno::NFILE.raw_os_error()
|
|
72
|
+
);
|
|
73
|
+
if full {
|
|
74
|
+
EXHAUSTED.fetch_add(1, Ordering::Relaxed);
|
|
75
|
+
}
|
|
76
|
+
full
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// How many times this process has found its descriptor table full. A caller diffs two
|
|
80
|
+
/// reads around its own work.
|
|
81
|
+
pub fn exhaustions() -> u64 {
|
|
82
|
+
EXHAUSTED.load(Ordering::Relaxed)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[cfg(test)]
|
|
86
|
+
mod tests {
|
|
87
|
+
use super::*;
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn the_budget_is_descriptors_over_per_stream_cost() {
|
|
91
|
+
assert_eq!(streams_for(128, Some(PER_STREAM * 10)), 10);
|
|
92
|
+
assert_eq!(streams_for(8, Some(PER_STREAM * 10)), 8);
|
|
93
|
+
assert_eq!(streams_for(128, Some(0)), 1, "one stream is always allowed");
|
|
94
|
+
assert_eq!(
|
|
95
|
+
streams_for(128, None),
|
|
96
|
+
128,
|
|
97
|
+
"an unmeasurable table is not a shortage"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[test]
|
|
102
|
+
fn only_a_full_table_counts_as_exhaustion() {
|
|
103
|
+
let before = exhaustions();
|
|
104
|
+
assert!(exhausted(&std::io::Error::from_raw_os_error(
|
|
105
|
+
rustix::io::Errno::MFILE.raw_os_error()
|
|
106
|
+
)));
|
|
107
|
+
assert!(!exhausted(&std::io::Error::from_raw_os_error(
|
|
108
|
+
rustix::io::Errno::CONNREFUSED.raw_os_error()
|
|
109
|
+
)));
|
|
110
|
+
assert!(exhaustions() > before);
|
|
111
|
+
}
|
|
112
|
+
}
|