tensorfs 0.3.62__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.62 → tensorfs-0.3.64}/Cargo.lock +2 -2
- {tensorfs-0.3.62 → tensorfs-0.3.64}/Cargo.toml +1 -1
- {tensorfs-0.3.62 → tensorfs-0.3.64}/PKG-INFO +1 -1
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/fetch.rs +3 -1
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/catalog.rs +10 -1
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/part_rows_tests.rs +24 -6
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/roots.rs +17 -5
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived.rs +29 -5
- tensorfs-0.3.64/crates/tensorfs-core/src/descriptors.rs +112 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/lib.rs +1 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/meta.rs +5 -3
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/providers.rs +1 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/store.rs +41 -3
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/http.rs +27 -9
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +9 -5
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/ledger.rs +15 -1
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/pace.rs +54 -8
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/policy.rs +45 -6
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/pull.rs +121 -49
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/tests.rs +5 -0
- tensorfs-0.3.64/crates/tensorfs-core/tests/anima_hf_profile.rs +166 -0
- tensorfs-0.3.64/crates/tensorfs-core/tests/fd_budget.rs +256 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/qwen_image21_profile.rs +5 -4
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_artifacts.rs +1 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_download.rs +1 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_prefixes.rs +1 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/store_boundaries.rs +32 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/transfer_memory.rs +1 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-py/src/store.rs +4 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/pyproject.toml +1 -1
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/asset_update.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/cache_roots.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/fill.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/gc.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/source.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/repo_cache.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/source_artifact.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/stats.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/memory.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/mod.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/push.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/trust_index.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-py/src/ingest.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/crates/tensorfs-py/src/lib.rs +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/__init__.py +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/_derived_channel.py +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/_ext.pyi +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/_source_channel.py +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/_tensors.py +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/derived.py +0 -0
- {tensorfs-0.3.62 → tensorfs-0.3.64}/python/tensorfs/errors.py +0 -0
- {tensorfs-0.3.62 → 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 {
|
|
@@ -25,7 +25,12 @@ fn fixture(label: &str) -> (Store, Meta, String, Declaration) {
|
|
|
25
25
|
.map(|index| ("model".into(), format!("weight_{index}")))
|
|
26
26
|
.collect();
|
|
27
27
|
declaration.max_new_bytes = 3 * 512;
|
|
28
|
-
(
|
|
28
|
+
(
|
|
29
|
+
store,
|
|
30
|
+
meta,
|
|
31
|
+
format!("sha256:{}", "7a".repeat(32)),
|
|
32
|
+
declaration,
|
|
33
|
+
)
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
fn catalog(store: &Store) -> rusqlite::Connection {
|
|
@@ -55,9 +60,16 @@ fn stored_rows(store: &Store, id: &str) -> (usize, Option<i64>, i64) {
|
|
|
55
60
|
)
|
|
56
61
|
.unwrap();
|
|
57
62
|
let value = crate::canon::parse_canonical(&bytes, limits::DOC_MAX_BYTES).unwrap();
|
|
58
|
-
let inline = TransactionRow::from_value(&value)
|
|
63
|
+
let inline = TransactionRow::from_value(&value)
|
|
64
|
+
.unwrap()
|
|
65
|
+
.added_parts
|
|
66
|
+
.len();
|
|
59
67
|
let parts = connection
|
|
60
|
-
.query_row(
|
|
68
|
+
.query_row(
|
|
69
|
+
"SELECT count(*) FROM tensorfs_derived_parts WHERE id=?1",
|
|
70
|
+
[id],
|
|
71
|
+
|row| row.get(0),
|
|
72
|
+
)
|
|
61
73
|
.unwrap();
|
|
62
74
|
(inline, open, parts)
|
|
63
75
|
}
|
|
@@ -74,14 +86,18 @@ fn parts_are_rows_that_resume_refuse_conflicts_and_stay_retained() {
|
|
|
74
86
|
add(&store, &meta, &id, &writer, "weight_0", 1).unwrap();
|
|
75
87
|
assert_eq!(stored_rows(&store, &id).2, 2);
|
|
76
88
|
assert_eq!(
|
|
77
|
-
add(&store, &meta, &id, &writer, "weight_0", 9)
|
|
89
|
+
add(&store, &meta, &id, &writer, "weight_0", 9)
|
|
90
|
+
.unwrap_err()
|
|
91
|
+
.code,
|
|
78
92
|
Code::TRANSACTION_CONFLICT
|
|
79
93
|
);
|
|
80
94
|
// A fenced writer cannot add. Once it is gone, GC keeps its accepted bytes through
|
|
81
95
|
// the append-only journal alone, and its successor resumes with both parts.
|
|
82
96
|
fence(&meta, &id, 1).unwrap();
|
|
83
97
|
assert_eq!(
|
|
84
|
-
add(&store, &meta, &id, &writer, "weight_2", 3)
|
|
98
|
+
add(&store, &meta, &id, &writer, "weight_2", 3)
|
|
99
|
+
.unwrap_err()
|
|
100
|
+
.code,
|
|
85
101
|
Code::WRITER_FENCED
|
|
86
102
|
);
|
|
87
103
|
release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
|
|
@@ -135,7 +151,9 @@ fn an_older_builds_inline_row_and_a_torn_journal_line_are_read() {
|
|
|
135
151
|
std::fs::write(&journal, torn).unwrap();
|
|
136
152
|
|
|
137
153
|
assert_eq!(
|
|
138
|
-
add(&store, &meta, &id, &writer, "weight_0", 9)
|
|
154
|
+
add(&store, &meta, &id, &writer, "weight_0", 9)
|
|
155
|
+
.unwrap_err()
|
|
156
|
+
.code,
|
|
139
157
|
Code::TRANSACTION_CONFLICT
|
|
140
158
|
);
|
|
141
159
|
add(&store, &meta, &id, &writer, "weight_1", 2).unwrap();
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
//! metadata journals lifecycle; GC never needs that database to discover retained bytes.
|
|
3
3
|
|
|
4
4
|
use std::fs::{self, File, OpenOptions};
|
|
5
|
-
use std::os::unix::fs::FileExt;
|
|
6
5
|
use std::io::{Read, Write};
|
|
6
|
+
use std::os::unix::fs::FileExt;
|
|
7
7
|
use std::path::PathBuf;
|
|
8
8
|
|
|
9
9
|
use crate::canon::{Fields, Value};
|
|
@@ -164,7 +164,11 @@ pub(super) fn append_parts(store: &Store, transaction: &str, objects: &[ObjectRe
|
|
|
164
164
|
file.read_exact_at(&mut last, length - 1).map_err(io)?;
|
|
165
165
|
}
|
|
166
166
|
// Terminate a line torn by a dead writer so this one parses on its own.
|
|
167
|
-
let mut bytes = if last[0] == b'\n' {
|
|
167
|
+
let mut bytes = if last[0] == b'\n' {
|
|
168
|
+
Vec::new()
|
|
169
|
+
} else {
|
|
170
|
+
vec![b'\n']
|
|
171
|
+
};
|
|
168
172
|
bytes.extend(parts_line(objects));
|
|
169
173
|
file.write_all(&bytes)
|
|
170
174
|
.and_then(|()| file.sync_data())
|
|
@@ -185,7 +189,11 @@ pub(super) fn rewrite_parts(store: &Store, transaction: &str, objects: &[ObjectR
|
|
|
185
189
|
.create_new(true)
|
|
186
190
|
.open(&temporary)
|
|
187
191
|
.map_err(io)?;
|
|
188
|
-
let bytes = if objects.is_empty() {
|
|
192
|
+
let bytes = if objects.is_empty() {
|
|
193
|
+
Vec::new()
|
|
194
|
+
} else {
|
|
195
|
+
parts_line(objects)
|
|
196
|
+
};
|
|
189
197
|
file.write_all(&bytes)
|
|
190
198
|
.and_then(|()| file.sync_all())
|
|
191
199
|
.map_err(io)?;
|
|
@@ -211,8 +219,12 @@ pub(super) fn journaled(store: &Store) -> Result<Vec<ObjectRef>> {
|
|
|
211
219
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
|
|
212
220
|
Err(error) => return Err(io(error)),
|
|
213
221
|
};
|
|
214
|
-
for line in bytes
|
|
215
|
-
|
|
222
|
+
for line in bytes
|
|
223
|
+
.split(|byte| *byte == b'\n')
|
|
224
|
+
.filter(|line| !line.is_empty())
|
|
225
|
+
{
|
|
226
|
+
let Ok(value) = crate::canon::parse_canonical(line, crate::limits::DOC_MAX_BYTES)
|
|
227
|
+
else {
|
|
216
228
|
continue;
|
|
217
229
|
};
|
|
218
230
|
for value in crate::canon::as_arr("PrivateDerivedParts", "objects", &value)? {
|
|
@@ -1168,11 +1168,19 @@ pub(crate) fn part_bytes(part: &Part) -> Vec<u8> {
|
|
|
1168
1168
|
}
|
|
1169
1169
|
|
|
1170
1170
|
impl AddedPart {
|
|
1171
|
-
pub(crate) fn stored(
|
|
1171
|
+
pub(crate) fn stored(
|
|
1172
|
+
component: String,
|
|
1173
|
+
key: String,
|
|
1174
|
+
role: String,
|
|
1175
|
+
bytes: &[u8],
|
|
1176
|
+
) -> Result<Self> {
|
|
1172
1177
|
name("added part component", &component)?;
|
|
1173
1178
|
self::key("added part tensor", &key)?;
|
|
1174
1179
|
name("added part role", &role)?;
|
|
1175
|
-
let part = part_from_value(&crate::canon::parse_canonical(
|
|
1180
|
+
let part = part_from_value(&crate::canon::parse_canonical(
|
|
1181
|
+
bytes,
|
|
1182
|
+
limits::DOC_MAX_BYTES,
|
|
1183
|
+
)?)?;
|
|
1176
1184
|
Ok(Self {
|
|
1177
1185
|
component,
|
|
1178
1186
|
key,
|
|
@@ -3005,7 +3013,11 @@ impl Additions {
|
|
|
3005
3013
|
for tensor in &component.add {
|
|
3006
3014
|
for part in &tensor.parts {
|
|
3007
3015
|
parts.insert(
|
|
3008
|
-
(
|
|
3016
|
+
(
|
|
3017
|
+
component.target.clone(),
|
|
3018
|
+
tensor.key.clone(),
|
|
3019
|
+
part.role.clone(),
|
|
3020
|
+
),
|
|
3009
3021
|
part.clone(),
|
|
3010
3022
|
);
|
|
3011
3023
|
}
|
|
@@ -3025,7 +3037,11 @@ impl Additions {
|
|
|
3025
3037
|
fn get(&self, (component, tensor_key, role): (&str, &str, &str)) -> Result<&PartDeclaration> {
|
|
3026
3038
|
let part = self
|
|
3027
3039
|
.parts
|
|
3028
|
-
.get(&(
|
|
3040
|
+
.get(&(
|
|
3041
|
+
component.to_string(),
|
|
3042
|
+
tensor_key.to_string(),
|
|
3043
|
+
role.to_string(),
|
|
3044
|
+
))
|
|
3029
3045
|
.ok_or_else(|| Refusal {
|
|
3030
3046
|
code: Code::MISSING_FIELD,
|
|
3031
3047
|
detail: format!("{component}/{tensor_key}#{role} is not a declared addition"),
|
|
@@ -3093,7 +3109,15 @@ pub fn add_part<R: Read>(
|
|
|
3093
3109
|
reader: &mut R,
|
|
3094
3110
|
) -> Result<crate::checkpoint::Written> {
|
|
3095
3111
|
let additions = Additions::load(meta, transaction, writer_session)?;
|
|
3096
|
-
add_indexed_part(
|
|
3112
|
+
add_indexed_part(
|
|
3113
|
+
store,
|
|
3114
|
+
meta,
|
|
3115
|
+
transaction,
|
|
3116
|
+
writer_session,
|
|
3117
|
+
&additions,
|
|
3118
|
+
value,
|
|
3119
|
+
reader,
|
|
3120
|
+
)
|
|
3097
3121
|
}
|
|
3098
3122
|
|
|
3099
3123
|
/// Accept one part. The cost is this part's bytes plus one indexed SQL row: the
|
|
@@ -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
|
+
}
|
|
@@ -172,9 +172,11 @@ impl Meta {
|
|
|
172
172
|
.optional()
|
|
173
173
|
.map_err(|error| sql("read accepted part", error))?
|
|
174
174
|
.or_else(|| {
|
|
175
|
-
legacy
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
legacy
|
|
176
|
+
.iter()
|
|
177
|
+
.flatten()
|
|
178
|
+
.find(|old| old.same_value(added))
|
|
179
|
+
.map(|old| crate::derived::part_bytes(&old.part))
|
|
178
180
|
});
|
|
179
181
|
match existing {
|
|
180
182
|
Some(existing) if existing == bytes => {
|
|
@@ -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),
|