tensorfs 0.3.63__tar.gz → 0.3.64__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.64}/Cargo.lock +2 -2
- {tensorfs-0.3.63 → tensorfs-0.3.64}/Cargo.toml +1 -1
- {tensorfs-0.3.63 → tensorfs-0.3.64}/PKG-INFO +1 -1
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/fetch.rs +3 -1
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/catalog.rs +10 -1
- tensorfs-0.3.64/crates/tensorfs-core/src/descriptors.rs +112 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/lib.rs +1 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/providers.rs +1 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/store.rs +41 -3
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/http.rs +27 -9
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +9 -5
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/ledger.rs +15 -1
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/pace.rs +54 -8
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/policy.rs +45 -6
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/pull.rs +121 -49
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/tests.rs +5 -0
- tensorfs-0.3.64/crates/tensorfs-core/tests/fd_budget.rs +256 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_artifacts.rs +1 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_download.rs +1 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_prefixes.rs +1 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/store_boundaries.rs +32 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/transfer_memory.rs +1 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-py/src/store.rs +4 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/pyproject.toml +1 -1
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/asset_update.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/cache_roots.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/part_rows_tests.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/roots.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/fill.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/gc.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/source.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/meta.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/repo_cache.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/source_artifact.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/stats.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/memory.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/mod.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/push.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/trust_index.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-py/src/ingest.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/crates/tensorfs-py/src/lib.rs +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/python/tensorfs/__init__.py +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/python/tensorfs/_derived_channel.py +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/python/tensorfs/_ext.pyi +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/python/tensorfs/_source_channel.py +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/python/tensorfs/_tensors.py +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/python/tensorfs/derived.py +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/python/tensorfs/errors.py +0 -0
- {tensorfs-0.3.63 → tensorfs-0.3.64}/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.64"
|
|
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.64"
|
|
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 {
|
|
@@ -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
|
+
}
|
|
@@ -415,9 +415,14 @@ impl Store {
|
|
|
415
415
|
///
|
|
416
416
|
/// A symlink, non-directory, partial Store, or directory containing foreign state refuses.
|
|
417
417
|
/// There is no old-layout lookup or fallback root.
|
|
418
|
+
/// Open the Store at `root`, creating it when absent or empty. Concurrent calls on one
|
|
419
|
+
/// new root serialize on the root directory: exactly one creates it, the rest open it.
|
|
418
420
|
pub fn ensure(root: &Path) -> Result<Store> {
|
|
419
421
|
match fs::symlink_metadata(root) {
|
|
420
|
-
Err(error) if error.kind() == std::io::ErrorKind::NotFound =>
|
|
422
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
|
423
|
+
fs::create_dir_all(root)
|
|
424
|
+
.map_err(|error| io(format!("create Store root {}", root.display()), error))?;
|
|
425
|
+
}
|
|
421
426
|
Err(error) => return Err(io(format!("stat Store root {}", root.display()), error)),
|
|
422
427
|
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => {
|
|
423
428
|
return refuse(
|
|
@@ -427,6 +432,7 @@ impl Store {
|
|
|
427
432
|
}
|
|
428
433
|
Ok(_) => {}
|
|
429
434
|
}
|
|
435
|
+
let _creation = creation_lock(root)?;
|
|
430
436
|
let empty = fs::read_dir(root)
|
|
431
437
|
.map_err(|error| io(format!("read Store root {}", root.display()), error))?
|
|
432
438
|
.next()
|
|
@@ -434,7 +440,7 @@ impl Store {
|
|
|
434
440
|
.map_err(|error| io(format!("read Store root {}", root.display()), error))?
|
|
435
441
|
.is_none();
|
|
436
442
|
if empty {
|
|
437
|
-
Store::
|
|
443
|
+
Store::init_locked(root)
|
|
438
444
|
} else {
|
|
439
445
|
validate_store_root(root)?;
|
|
440
446
|
let catalog_path = root.join("tensorfs.sqlite");
|
|
@@ -456,6 +462,15 @@ impl Store {
|
|
|
456
462
|
}
|
|
457
463
|
|
|
458
464
|
pub fn init(root: &Path) -> Result<Store> {
|
|
465
|
+
if matches!(fs::symlink_metadata(root), Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink())
|
|
466
|
+
{
|
|
467
|
+
let _creation = creation_lock(root)?;
|
|
468
|
+
return Store::init_locked(root);
|
|
469
|
+
}
|
|
470
|
+
Store::init_locked(root)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
fn init_locked(root: &Path) -> Result<Store> {
|
|
459
474
|
match fs::symlink_metadata(root) {
|
|
460
475
|
Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {}
|
|
461
476
|
Ok(_) => {
|
|
@@ -1347,7 +1362,16 @@ impl Store {
|
|
|
1347
1362
|
.create_new(true)
|
|
1348
1363
|
.mode(0o600)
|
|
1349
1364
|
.open(&tmp)
|
|
1350
|
-
.map_err(|e|
|
|
1365
|
+
.map_err(|e| {
|
|
1366
|
+
if crate::descriptors::exhausted(&e) {
|
|
1367
|
+
Refusal {
|
|
1368
|
+
code: Code::FD_HEADROOM,
|
|
1369
|
+
detail: format!("create temp {}: {e}", tmp.display()),
|
|
1370
|
+
}
|
|
1371
|
+
} else {
|
|
1372
|
+
io(format!("create temp {}", tmp.display()), e)
|
|
1373
|
+
}
|
|
1374
|
+
})?;
|
|
1351
1375
|
match f.try_lock_exclusive() {
|
|
1352
1376
|
Ok(()) => break (tmp, f),
|
|
1353
1377
|
// Only a reaper can hold the lock on a name this process just minted: it
|
|
@@ -2103,6 +2127,20 @@ fn read_repo_cache_binding(root: &Path) -> Option<crate::repo_cache::RepoObjectC
|
|
|
2103
2127
|
Some(crate::repo_cache::RepoObjectCache::new(cache))
|
|
2104
2128
|
}
|
|
2105
2129
|
|
|
2130
|
+
/// Serializes Store creation on one root across threads and processes. A lock on the root
|
|
2131
|
+
/// directory itself leaves nothing behind in it.
|
|
2132
|
+
fn creation_lock(root: &Path) -> Result<File> {
|
|
2133
|
+
let directory = OpenOptions::new()
|
|
2134
|
+
.read(true)
|
|
2135
|
+
.custom_flags(O_NOFOLLOW)
|
|
2136
|
+
.open(root)
|
|
2137
|
+
.map_err(|error| io(format!("open Store root {}", root.display()), error))?;
|
|
2138
|
+
directory
|
|
2139
|
+
.lock_exclusive()
|
|
2140
|
+
.map_err(|error| io(format!("lock Store root {}", root.display()), error))?;
|
|
2141
|
+
Ok(directory)
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2106
2144
|
fn open_validated(root: &Path) -> Result<Store> {
|
|
2107
2145
|
fs::create_dir_all(root.join("tmp")).map_err(|error| io("mkdir tmp", error))?;
|
|
2108
2146
|
Ok(Store {
|
|
@@ -166,7 +166,8 @@ pub struct Client {
|
|
|
166
166
|
type IdlePool = Arc<Mutex<Vec<Idle>>>;
|
|
167
167
|
|
|
168
168
|
// This bounds retained descriptors, not in-flight transfers (the caller owns those).
|
|
169
|
-
|
|
169
|
+
// `descriptors::PER_STREAM` counts one idle socket per stream.
|
|
170
|
+
const MAX_IDLE: usize = 32;
|
|
170
171
|
|
|
171
172
|
/// A connection belongs to its origin and the credentials sent on it, never to one
|
|
172
173
|
/// request target: a signed CDN URL changes per resolve, and keying on it meant the
|
|
@@ -414,7 +415,15 @@ fn connect(
|
|
|
414
415
|
let socket = match nonblocking_socket(family) {
|
|
415
416
|
Ok(socket) => socket,
|
|
416
417
|
Err(error) => {
|
|
417
|
-
|
|
418
|
+
let error: std::io::Error = error.into();
|
|
419
|
+
if crate::descriptors::exhausted(&error) {
|
|
420
|
+
// Another address needs another descriptor it will not get either.
|
|
421
|
+
return refuse(
|
|
422
|
+
Code::FD_HEADROOM,
|
|
423
|
+
format!("{}: no descriptor for a socket ({error})", checked.host),
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
last = Some(error);
|
|
418
427
|
continue;
|
|
419
428
|
}
|
|
420
429
|
};
|
|
@@ -653,6 +662,15 @@ pub struct BodyReader<'l> {
|
|
|
653
662
|
|
|
654
663
|
impl Drop for BodyReader<'_> {
|
|
655
664
|
fn drop(&mut self) {
|
|
665
|
+
self.recycle_if_consumed();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
impl BodyReader<'_> {
|
|
670
|
+
/// Hand the connection back the moment its last body byte is read, not when the
|
|
671
|
+
/// reader is dropped: the consumer may still spend seconds hashing, fsyncing and
|
|
672
|
+
/// committing what arrived, and the next object's GET should find this socket idle.
|
|
673
|
+
fn recycle_if_consumed(&mut self) {
|
|
656
674
|
// Only a fully consumed, unambiguous Content-Length response is reusable.
|
|
657
675
|
// Dropped/failed bodies, chunked bodies and EOF-delimited replies close.
|
|
658
676
|
if !matches!(self.framing, Framing::Length { remaining: 0 }) || self.raw_len() != 0 {
|
|
@@ -722,6 +740,7 @@ impl BodyReader<'_> {
|
|
|
722
740
|
let silent = self.advanced.elapsed();
|
|
723
741
|
if self.judge.silent_is_stall() && silent > self.ledger.patience() {
|
|
724
742
|
self.stalled = true;
|
|
743
|
+
self.ledger.stalled_stream();
|
|
725
744
|
let _ = self
|
|
726
745
|
.wire
|
|
727
746
|
.as_ref()
|
|
@@ -846,6 +865,7 @@ impl BodyReader<'_> {
|
|
|
846
865
|
self.framing = Framing::Length {
|
|
847
866
|
remaining: remaining - n as u64,
|
|
848
867
|
};
|
|
868
|
+
self.recycle_if_consumed();
|
|
849
869
|
Ok(n)
|
|
850
870
|
}
|
|
851
871
|
Framing::Chunked { in_chunk, done } => {
|
|
@@ -946,13 +966,11 @@ pub fn request<'l>(
|
|
|
946
966
|
write_silence: Option<Duration>,
|
|
947
967
|
fail: Code,
|
|
948
968
|
) -> Result<Response<'l>> {
|
|
949
|
-
//
|
|
950
|
-
//
|
|
951
|
-
|
|
969
|
+
// Every bodiless GET may reuse a connection to its origin: a pull's objects are many
|
|
970
|
+
// GETs to one host, and a fresh TCP+TLS connection per object spent a descriptor, a
|
|
971
|
+
// handshake and a slow start on each. Metadata writes keep one connection each.
|
|
972
|
+
let pooled = method == "GET"
|
|
952
973
|
&& body.is_none()
|
|
953
|
-
&& headers
|
|
954
|
-
.iter()
|
|
955
|
-
.any(|(name, _)| name.eq_ignore_ascii_case("range"))
|
|
956
974
|
&& !headers
|
|
957
975
|
.iter()
|
|
958
976
|
.any(|(name, _)| name.eq_ignore_ascii_case("connection"));
|
|
@@ -967,7 +985,7 @@ pub fn request<'l>(
|
|
|
967
985
|
method,
|
|
968
986
|
headers,
|
|
969
987
|
body.map(Payload::Bytes),
|
|
970
|
-
|
|
988
|
+
pooled.then_some(scoped.as_slice()),
|
|
971
989
|
deadline,
|
|
972
990
|
ledger,
|
|
973
991
|
judge,
|
|
@@ -197,6 +197,7 @@ impl Origin {
|
|
|
197
197
|
allowed_hosts: vec!["localhost".into()],
|
|
198
198
|
allow_local: true,
|
|
199
199
|
max_redirects: 2,
|
|
200
|
+
..Default::default()
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
203
|
fn count(&self) -> usize {
|
|
@@ -364,20 +365,23 @@ fn only_complete_unambiguous_http11_length_bodies_enter_the_pool() {
|
|
|
364
365
|
}
|
|
365
366
|
|
|
366
367
|
#[test]
|
|
367
|
-
fn
|
|
368
|
+
fn idle_descriptor_retention_is_bounded_and_plain_gets_pool_too() {
|
|
368
369
|
let origin = Origin::new(false, Duration::ZERO, |_, _| Reply::body(b"bytes"));
|
|
369
370
|
let client = origin.client();
|
|
370
|
-
|
|
371
|
+
let scopes = MAX_IDLE + 8;
|
|
372
|
+
for index in 0..scopes {
|
|
371
373
|
let mut headers = ranged();
|
|
372
374
|
headers.push(("authorization".into(), format!("Bearer {index}")));
|
|
373
375
|
ask(&client, &origin.checked("/object"), &headers).unwrap();
|
|
374
376
|
}
|
|
375
377
|
assert_eq!(client.idle.lock().unwrap().len(), MAX_IDLE);
|
|
376
|
-
assert_eq!(origin.count(),
|
|
378
|
+
assert_eq!(origin.count(), scopes);
|
|
379
|
+
// A whole-object GET is a pull's common case: the second rides the first's socket.
|
|
377
380
|
for _ in 0..2 {
|
|
378
|
-
ask(&client, &origin.checked("/
|
|
381
|
+
ask(&client, &origin.checked("/object"), &[]).unwrap();
|
|
379
382
|
}
|
|
380
|
-
assert_eq!(origin.count(),
|
|
383
|
+
assert_eq!(origin.count(), scopes + 1);
|
|
384
|
+
assert_eq!(client.idle.lock().unwrap().len(), MAX_IDLE);
|
|
381
385
|
}
|
|
382
386
|
|
|
383
387
|
#[test]
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
//! paid H100s sat at exactly zero for fifteen minutes with neither plane complaining.
|
|
32
32
|
|
|
33
33
|
use crate::err::{refuse, Code, Result};
|
|
34
|
-
use std::sync::atomic::{AtomicBool, Ordering};
|
|
34
|
+
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
35
35
|
use std::sync::{Arc, Mutex};
|
|
36
36
|
use std::time::{Duration, Instant};
|
|
37
37
|
|
|
@@ -151,6 +151,8 @@ impl PullCancellation {
|
|
|
151
151
|
pub struct Ledger {
|
|
152
152
|
caller_cancellation: Option<PullCancellation>,
|
|
153
153
|
cancelled: Arc<AtomicBool>,
|
|
154
|
+
/// Streams this pull has judged stalled, a backpressure signal for its pace.
|
|
155
|
+
stalls: Arc<AtomicU64>,
|
|
154
156
|
sample: Duration,
|
|
155
157
|
worst_gap: Arc<Mutex<Duration>>,
|
|
156
158
|
progress: Arc<Mutex<Progress>>,
|
|
@@ -170,6 +172,7 @@ impl Ledger {
|
|
|
170
172
|
Ledger {
|
|
171
173
|
caller_cancellation: None,
|
|
172
174
|
cancelled: Arc::new(AtomicBool::new(false)),
|
|
175
|
+
stalls: Arc::new(AtomicU64::new(0)),
|
|
173
176
|
sample: sample.max(Duration::from_millis(1)),
|
|
174
177
|
worst_gap: Arc::new(Mutex::new(Duration::ZERO)),
|
|
175
178
|
progress: Arc::new(Mutex::new(Progress {
|
|
@@ -243,6 +246,16 @@ impl Ledger {
|
|
|
243
246
|
self.sample
|
|
244
247
|
}
|
|
245
248
|
|
|
249
|
+
/// A stream of this pull was judged stalled.
|
|
250
|
+
pub(super) fn stalled_stream(&self) {
|
|
251
|
+
self.stalls.fetch_add(1, Ordering::Relaxed);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Streams this pull has judged stalled so far.
|
|
255
|
+
pub(super) fn stalls(&self) -> u64 {
|
|
256
|
+
self.stalls.load(Ordering::Relaxed)
|
|
257
|
+
}
|
|
258
|
+
|
|
246
259
|
/// The smallest patience ever granted: `STILL_SAMPLES` intervals of the resolution.
|
|
247
260
|
pub fn floor(&self) -> Duration {
|
|
248
261
|
self.sample * STILL_SAMPLES
|
|
@@ -280,6 +293,7 @@ impl Ledger {
|
|
|
280
293
|
Self {
|
|
281
294
|
caller_cancellation: self.caller_cancellation.clone(),
|
|
282
295
|
cancelled: Arc::clone(&self.cancelled),
|
|
296
|
+
stalls: Arc::clone(&self.stalls),
|
|
283
297
|
sample: self.sample,
|
|
284
298
|
worst_gap: Arc::clone(&self.worst_gap),
|
|
285
299
|
progress: Arc::clone(&self.progress),
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
//! admits while doing so bought at least `GAIN` more aggregate goodput. A doubling that
|
|
10
10
|
//! bought less settles the walk; one that bought less than nothing returns to the level
|
|
11
11
|
//! before it. Nothing is ended or failed here: this only admits workers to the next object.
|
|
12
|
+
//!
|
|
13
|
+
//! Two signals halve the level and stop its growth for the rest of the walk: a stream the
|
|
14
|
+
//! ledger judged stalled while the level stood, and a full descriptor table (`shrink`).
|
|
15
|
+
//! Both say the pull is asking more of the link or the process than either will give.
|
|
12
16
|
use super::ledger::Ledger;
|
|
13
17
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
14
18
|
use std::sync::{Condvar, Mutex};
|
|
@@ -32,6 +36,9 @@ struct State {
|
|
|
32
36
|
limit: usize,
|
|
33
37
|
active: usize,
|
|
34
38
|
settled: bool,
|
|
39
|
+
/// Times the level was halved on backpressure.
|
|
40
|
+
backoffs: u32,
|
|
41
|
+
stalls_seen: u64,
|
|
35
42
|
window_started: Instant,
|
|
36
43
|
moved_at_window: u64,
|
|
37
44
|
history: Vec<Level>,
|
|
@@ -67,6 +74,8 @@ impl Pace {
|
|
|
67
74
|
limit,
|
|
68
75
|
active: 0,
|
|
69
76
|
settled: limit == ceiling,
|
|
77
|
+
backoffs: 0,
|
|
78
|
+
stalls_seen: ledger.stalls(),
|
|
70
79
|
window_started: Instant::now(),
|
|
71
80
|
moved_at_window: ledger.still().moved,
|
|
72
81
|
history: Vec::new(),
|
|
@@ -82,10 +91,38 @@ impl Pace {
|
|
|
82
91
|
self.state.lock().unwrap().limit
|
|
83
92
|
}
|
|
84
93
|
|
|
85
|
-
/// The level the walk ended at
|
|
86
|
-
|
|
94
|
+
/// The level the walk ended at, every level it measured on the way, and how many
|
|
95
|
+
/// times backpressure halved it.
|
|
96
|
+
pub fn outcome(&self) -> (usize, Vec<Level>, u32) {
|
|
87
97
|
let state = self.state.lock().unwrap();
|
|
88
|
-
(state.limit, state.history.clone())
|
|
98
|
+
(state.limit, state.history.clone(), state.backoffs)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Times backpressure has halved the level so far. A caller reads it before an attempt
|
|
102
|
+
/// and hands it to `back_off`, so one episode that fails many streams at once halves
|
|
103
|
+
/// the level once, not once per stream.
|
|
104
|
+
pub fn backoffs(&self) -> u32 {
|
|
105
|
+
self.state.lock().unwrap().backoffs
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Halve the level unless it was already halved since `seen`.
|
|
109
|
+
pub fn back_off(&self, seen: u32) {
|
|
110
|
+
if self.state.lock().unwrap().backoffs == seen {
|
|
111
|
+
self.shrink();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Halve the level and stop growing it. `false` when it is already one stream: there is
|
|
116
|
+
/// nothing left to give back.
|
|
117
|
+
pub fn shrink(&self) -> bool {
|
|
118
|
+
let mut state = self.state.lock().unwrap();
|
|
119
|
+
state.settled = true;
|
|
120
|
+
if state.limit <= 1 {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
state.limit /= 2;
|
|
124
|
+
state.backoffs += 1;
|
|
125
|
+
true
|
|
89
126
|
}
|
|
90
127
|
|
|
91
128
|
pub fn worker_finished(&self) {
|
|
@@ -126,10 +163,7 @@ impl Pace {
|
|
|
126
163
|
loop {
|
|
127
164
|
let mut state = self.state.lock().unwrap();
|
|
128
165
|
loop {
|
|
129
|
-
if
|
|
130
|
-
|| stop.load(Ordering::Relaxed)
|
|
131
|
-
|| self.live.load(Ordering::Acquire) == 0
|
|
132
|
-
{
|
|
166
|
+
if stop.load(Ordering::Relaxed) || self.live.load(Ordering::Acquire) == 0 {
|
|
133
167
|
return;
|
|
134
168
|
}
|
|
135
169
|
let waited = state.window_started.elapsed();
|
|
@@ -138,7 +172,19 @@ impl Pace {
|
|
|
138
172
|
}
|
|
139
173
|
state = self.wake.wait_timeout(state, tick - waited).unwrap().0;
|
|
140
174
|
}
|
|
175
|
+
let stalls = ledger.stalls();
|
|
176
|
+
let stalled = stalls > state.stalls_seen;
|
|
177
|
+
state.stalls_seen = stalls;
|
|
178
|
+
if state.settled && !stalled {
|
|
179
|
+
state.window_started = Instant::now();
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
141
182
|
drop(state);
|
|
183
|
+
if stalled {
|
|
184
|
+
self.shrink();
|
|
185
|
+
self.state.lock().unwrap().window_started = Instant::now();
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
142
188
|
self.observe(ledger.still().moved, Instant::now(), unclaimed());
|
|
143
189
|
}
|
|
144
190
|
}
|
|
@@ -195,7 +241,7 @@ mod tests {
|
|
|
195
241
|
pace.observe(530, t0 + second * 3, 1000);
|
|
196
242
|
assert_eq!(pace.limit(), 64, "1.15x does not");
|
|
197
243
|
pace.observe(10_000, t0 + second * 4, 1000);
|
|
198
|
-
let (level, history) = pace.outcome();
|
|
244
|
+
let (level, history, _) = pace.outcome();
|
|
199
245
|
assert_eq!(level, 64, "a settled walk stays settled");
|
|
200
246
|
assert_eq!(history.len(), 3);
|
|
201
247
|
}
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
//! metadata read.
|
|
27
27
|
|
|
28
28
|
use crate::err::{refuse, Code, Result};
|
|
29
|
+
use std::collections::HashMap;
|
|
29
30
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs};
|
|
31
|
+
use std::sync::{Arc, Mutex};
|
|
30
32
|
|
|
31
33
|
/// The address classes no downloader in this tree connects to — the port of
|
|
32
34
|
/// `egress._CLASSES`, same names, same order. Returns why `address` is not fetchable, or
|
|
@@ -173,6 +175,36 @@ pub struct SourcePolicy {
|
|
|
173
175
|
/// Redirect hop budget. 0 refuses every redirect — a presigned GET has no business
|
|
174
176
|
/// redirecting; a provider handler (tfs-052) that needs hops declares them.
|
|
175
177
|
pub max_redirects: u32,
|
|
178
|
+
/// Answers this policy has already resolved, shared by its clones. One pull asks the
|
|
179
|
+
/// resolver once per host instead of once per object; every answer is still checked.
|
|
180
|
+
pub resolved: Resolved,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Checked answers by host and port.
|
|
184
|
+
type Answers = HashMap<(String, u16), Vec<SocketAddr>>;
|
|
185
|
+
|
|
186
|
+
/// One resolution per host and port for as long as the policy that made it lives.
|
|
187
|
+
#[derive(Clone, Default)]
|
|
188
|
+
pub struct Resolved(Arc<Mutex<Answers>>);
|
|
189
|
+
|
|
190
|
+
impl std::fmt::Debug for Resolved {
|
|
191
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
192
|
+
f.write_str("Resolved")
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
impl Resolved {
|
|
197
|
+
fn lookup(&self, host: &str, port: u16) -> std::io::Result<Vec<SocketAddr>> {
|
|
198
|
+
let key = (host.to_string(), port);
|
|
199
|
+
if let Some(addrs) = self.0.lock().unwrap().get(&key) {
|
|
200
|
+
return Ok(addrs.clone());
|
|
201
|
+
}
|
|
202
|
+
let addrs = resolve(host, port)?;
|
|
203
|
+
if !addrs.is_empty() {
|
|
204
|
+
self.0.lock().unwrap().insert(key, addrs.clone());
|
|
205
|
+
}
|
|
206
|
+
Ok(addrs)
|
|
207
|
+
}
|
|
176
208
|
}
|
|
177
209
|
|
|
178
210
|
impl SourcePolicy {
|
|
@@ -219,13 +251,20 @@ impl SourcePolicy {
|
|
|
219
251
|
);
|
|
220
252
|
}
|
|
221
253
|
let port = port.unwrap_or(if https { 443 } else { 80 });
|
|
222
|
-
let addrs =
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
254
|
+
let addrs = self
|
|
255
|
+
.resolved
|
|
256
|
+
.lookup(&host, port)
|
|
257
|
+
.map_err(|error| crate::err::Refusal {
|
|
258
|
+
code: if crate::descriptors::exhausted(&error) {
|
|
259
|
+
Code::FD_HEADROOM
|
|
260
|
+
} else {
|
|
261
|
+
Code::HUB_UNREACHABLE
|
|
262
|
+
},
|
|
263
|
+
detail: format!(
|
|
264
|
+
"{host}: name resolution failed ({error}); an address this module cannot \
|
|
226
265
|
check is not an address it will connect to"
|
|
227
|
-
|
|
228
|
-
|
|
266
|
+
),
|
|
267
|
+
})?;
|
|
229
268
|
if addrs.is_empty() {
|
|
230
269
|
return refuse(Code::HUB_UNREACHABLE, format!("{host} resolved to nothing"));
|
|
231
270
|
}
|