tensorfs 0.3.58__tar.gz → 0.3.59__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.58 → tensorfs-0.3.59}/Cargo.lock +2 -2
- {tensorfs-0.3.58 → tensorfs-0.3.59}/Cargo.toml +1 -1
- {tensorfs-0.3.58 → tensorfs-0.3.59}/PKG-INFO +1 -1
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/catalog.rs +17 -1
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/http.rs +13 -2
- tensorfs-0.3.59/crates/tensorfs-core/src/transport/memory.rs +92 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/mod.rs +2 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/prefix.rs +1 -1
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/pull.rs +1 -1
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/push.rs +9 -9
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_download.rs +256 -5
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-py/src/lib.rs +2 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-py/src/store.rs +52 -15
- {tensorfs-0.3.58 → tensorfs-0.3.59}/pyproject.toml +1 -1
- {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/__init__.py +4 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/_ext.pyi +10 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/asset_update.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/cache_roots.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/roots.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/fill.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/gc.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/source.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/lib.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/meta.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/providers.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/repo_cache.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/source_artifact.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/stats.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/store.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/policy.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/tests.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/trust_index.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/_derived_channel.py +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/_source_channel.py +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/_tensors.py +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/derived.py +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/python/tensorfs/errors.py +0 -0
- {tensorfs-0.3.58 → tensorfs-0.3.59}/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.59"
|
|
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.59"
|
|
458
458
|
dependencies = [
|
|
459
459
|
"pyo3",
|
|
460
460
|
"tensorfs-core",
|
|
@@ -911,9 +911,14 @@ impl Catalog {
|
|
|
911
911
|
object: &crate::ids::ObjectRef,
|
|
912
912
|
publish: impl FnOnce() -> Result<Option<crate::store::VRecord>>,
|
|
913
913
|
) -> Result<bool> {
|
|
914
|
+
// One admission at a time per catalog in this process, holding the write lock from
|
|
915
|
+
// before publication: streams landing objects together queue here instead of in
|
|
916
|
+
// SQLite's busy timeout, where a lost race refused bytes already verified.
|
|
917
|
+
let turn = admissions(&self.path);
|
|
918
|
+
let _turn = turn.lock().unwrap_or_else(|error| error.into_inner());
|
|
914
919
|
let mut connection = self.connection()?;
|
|
915
920
|
let transaction = connection
|
|
916
|
-
.
|
|
921
|
+
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
|
|
917
922
|
.map_err(|error| sql("begin blob admission", error))?;
|
|
918
923
|
if let Some(operation) = operation {
|
|
919
924
|
hold_on(
|
|
@@ -1079,6 +1084,17 @@ impl Catalog {
|
|
|
1079
1084
|
}
|
|
1080
1085
|
}
|
|
1081
1086
|
|
|
1087
|
+
fn admissions(path: &Path) -> Arc<Mutex<()>> {
|
|
1088
|
+
static TURNS: OnceLock<Mutex<HashMap<PathBuf, Arc<Mutex<()>>>>> = OnceLock::new();
|
|
1089
|
+
TURNS
|
|
1090
|
+
.get_or_init(Default::default)
|
|
1091
|
+
.lock()
|
|
1092
|
+
.unwrap_or_else(|error| error.into_inner())
|
|
1093
|
+
.entry(path.to_path_buf())
|
|
1094
|
+
.or_default()
|
|
1095
|
+
.clone()
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1082
1098
|
fn hold_on(connection: &Connection, operation: &str, key: &str, length: u64) -> Result<()> {
|
|
1083
1099
|
let kind = if key.starts_with("blobs/") {
|
|
1084
1100
|
"blob"
|
|
@@ -60,6 +60,11 @@ pub enum Judge {
|
|
|
60
60
|
/// which is the owner's rule read literally: the lack of measured progress, never a
|
|
61
61
|
/// number of seconds.
|
|
62
62
|
Transfer,
|
|
63
|
+
/// An UPLOAD. Every wait — connect, handshake, each write, and the answer after the
|
|
64
|
+
/// last body byte — is judged by this exchange's own silence against the ledger's
|
|
65
|
+
/// patience. Nothing it moves lands here, so it neither counts as progress nor teaches
|
|
66
|
+
/// the pace.
|
|
67
|
+
Silence,
|
|
63
68
|
/// Neither. The caller's deadline is the only bound, which is what a foreign-source
|
|
64
69
|
/// read with its own per-part retry loop above it already had.
|
|
65
70
|
Deadline,
|
|
@@ -133,7 +138,7 @@ fn transfer_verdict(
|
|
|
133
138
|
/// progress may span arbitrarily many windows without exhausting a wall budget.
|
|
134
139
|
fn stream_verdict(judge: Judge, ledger: &Ledger, advanced: Instant, phase: &str) -> Result<()> {
|
|
135
140
|
ledger.check_running()?;
|
|
136
|
-
if judge
|
|
141
|
+
if !judge.silent_is_stall() || advanced.elapsed() <= ledger.patience() {
|
|
137
142
|
return Ok(());
|
|
138
143
|
}
|
|
139
144
|
refuse(
|
|
@@ -147,6 +152,12 @@ fn stream_verdict(judge: Judge, ledger: &Ledger, advanced: Instant, phase: &str)
|
|
|
147
152
|
)
|
|
148
153
|
}
|
|
149
154
|
|
|
155
|
+
impl Judge {
|
|
156
|
+
fn silent_is_stall(self) -> bool {
|
|
157
|
+
matches!(self, Judge::Stream | Judge::Silence)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
150
161
|
pub struct Client {
|
|
151
162
|
tls: Arc<rustls::ClientConfig>,
|
|
152
163
|
idle: IdlePool,
|
|
@@ -712,7 +723,7 @@ impl BodyReader<'_> {
|
|
|
712
723
|
Err(e) if would_block(&e) => {
|
|
713
724
|
transfer_verdict(self.judge, self.ledger, self.since, "a response body")?;
|
|
714
725
|
let silent = self.advanced.elapsed();
|
|
715
|
-
if self.judge
|
|
726
|
+
if self.judge.silent_is_stall() && silent > self.ledger.patience() {
|
|
716
727
|
self.stalled = true;
|
|
717
728
|
let _ = self
|
|
718
729
|
.wire
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//! How many transfer streams fit in the memory this process can actually use. There is no
|
|
2
|
+
//! assumed host: a 4 GB pod runs the same pull, source download or upload window with fewer
|
|
3
|
+
//! streams, never with N × buffers it does not have.
|
|
4
|
+
use std::fs;
|
|
5
|
+
use std::path::{Path, PathBuf};
|
|
6
|
+
|
|
7
|
+
/// The most memory one transfer stream holds, whatever its object's size: one 1 MiB body
|
|
8
|
+
/// chunk (the read, write, hash or upload buffer), the 64 KiB framing buffer, and rustls's
|
|
9
|
+
/// bounded record buffers, rounded up to a whole 2 MiB.
|
|
10
|
+
pub const STREAM_MEMORY: u64 = 2 << 20;
|
|
11
|
+
|
|
12
|
+
/// Bytes this process can still use now: the tightest of its cgroup's limit less the usage
|
|
13
|
+
/// that cannot be reclaimed (v2 at every ancestor, else v1) and the host's `MemAvailable`.
|
|
14
|
+
/// `None` when none of them is readable.
|
|
15
|
+
pub fn memory_available() -> Option<u64> {
|
|
16
|
+
[cgroup_v2(), cgroup_v1(), mem_available()]
|
|
17
|
+
.into_iter()
|
|
18
|
+
.flatten()
|
|
19
|
+
.min()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Concurrent transfer streams the memory available now allows: at most `wanted`, at
|
|
23
|
+
/// least one.
|
|
24
|
+
pub fn transfer_streams(wanted: usize) -> usize {
|
|
25
|
+
streams_within(wanted, memory_available())
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// `transfer_streams` against a stated budget.
|
|
29
|
+
pub fn streams_within(wanted: usize, available: Option<u64>) -> usize {
|
|
30
|
+
let fits = available.map_or(usize::MAX, |bytes| {
|
|
31
|
+
usize::try_from(bytes / STREAM_MEMORY).unwrap_or(usize::MAX)
|
|
32
|
+
});
|
|
33
|
+
wanted.min(fits).max(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn number(path: &Path) -> Option<u64> {
|
|
37
|
+
fs::read_to_string(path).ok()?.trim().parse().ok()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// `limit − (usage − page cache)`: page cache is reclaimed under pressure, so bytes a
|
|
41
|
+
/// download has written do not count against the next stream.
|
|
42
|
+
fn headroom(dir: &Path, limit: &str, usage: &str, cache: &str) -> Option<u64> {
|
|
43
|
+
let limit = number(&dir.join(limit))?;
|
|
44
|
+
let usage = number(&dir.join(usage))?;
|
|
45
|
+
let cached = fs::read_to_string(dir.join("memory.stat"))
|
|
46
|
+
.ok()
|
|
47
|
+
.and_then(|stat| {
|
|
48
|
+
stat.lines()
|
|
49
|
+
.find_map(|line| line.strip_prefix(cache)?.strip_prefix(' ')?.parse().ok())
|
|
50
|
+
})
|
|
51
|
+
.unwrap_or(0);
|
|
52
|
+
Some(limit.saturating_sub(usage.saturating_sub(cached)))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn cgroup_v2() -> Option<u64> {
|
|
56
|
+
let root = Path::new("/sys/fs/cgroup");
|
|
57
|
+
let own = fs::read_to_string("/proc/self/cgroup").ok()?;
|
|
58
|
+
let own = own.lines().find_map(|line| line.strip_prefix("0::"))?;
|
|
59
|
+
let mut dir = root.join(own.trim().trim_start_matches('/'));
|
|
60
|
+
let mut tightest: Option<u64> = None;
|
|
61
|
+
loop {
|
|
62
|
+
if let Some(left) = headroom(&dir, "memory.max", "memory.current", "file") {
|
|
63
|
+
tightest = Some(tightest.map_or(left, |held| held.min(left)));
|
|
64
|
+
}
|
|
65
|
+
if dir == root || !dir.pop() || !dir.starts_with(root) {
|
|
66
|
+
return tightest;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn cgroup_v1() -> Option<u64> {
|
|
72
|
+
let dir = PathBuf::from("/sys/fs/cgroup/memory");
|
|
73
|
+
headroom(
|
|
74
|
+
&dir,
|
|
75
|
+
"memory.limit_in_bytes",
|
|
76
|
+
"memory.usage_in_bytes",
|
|
77
|
+
"total_cache",
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn mem_available() -> Option<u64> {
|
|
82
|
+
let text = fs::read_to_string("/proc/meminfo").ok()?;
|
|
83
|
+
let kib: u64 = text
|
|
84
|
+
.lines()
|
|
85
|
+
.find_map(|line| line.strip_prefix("MemAvailable:"))?
|
|
86
|
+
.trim()
|
|
87
|
+
.strip_suffix("kB")?
|
|
88
|
+
.trim()
|
|
89
|
+
.parse()
|
|
90
|
+
.ok()?;
|
|
91
|
+
kib.checked_mul(1024)
|
|
92
|
+
}
|
|
@@ -35,6 +35,7 @@ mod api;
|
|
|
35
35
|
mod http;
|
|
36
36
|
mod hub;
|
|
37
37
|
mod ledger;
|
|
38
|
+
mod memory;
|
|
38
39
|
mod policy;
|
|
39
40
|
pub(crate) mod prefix;
|
|
40
41
|
mod pull;
|
|
@@ -55,6 +56,7 @@ pub use ledger::{
|
|
|
55
56
|
Deadline, Ledger, PullCancellation, FETCH_ATTEMPTS, SAMPLE_SECONDS, STALL_FACTOR,
|
|
56
57
|
STILL_SAMPLES, STREAMS,
|
|
57
58
|
};
|
|
59
|
+
pub use memory::{memory_available, streams_within, transfer_streams, STREAM_MEMORY};
|
|
58
60
|
pub use policy::{base_host, blocked, is_loopback_origin, resolve, CheckedUrl, SourcePolicy};
|
|
59
61
|
pub use pull::{
|
|
60
62
|
fetch_authorized, fetch_url, fetch_wanted, pull, FetchObserver, Fetched, ObjectSource,
|
|
@@ -479,7 +479,7 @@ pub(crate) fn fetch_sources(
|
|
|
479
479
|
};
|
|
480
480
|
download.report(0, held);
|
|
481
481
|
std::thread::scope(|scope| {
|
|
482
|
-
for _ in 0..options.streams.clamp(1, 128) {
|
|
482
|
+
for _ in 0..super::transfer_streams(options.streams.clamp(1, 128)) {
|
|
483
483
|
scope.spawn(|| download.work());
|
|
484
484
|
}
|
|
485
485
|
});
|
|
@@ -980,7 +980,7 @@ pub(super) fn fetch_wanted_bounded(
|
|
|
980
980
|
// invariant it proved — the grant set covers the wanted set exactly — is enforced per
|
|
981
981
|
// mint instead: presign authorizes every digest it was asked or refuses, never a silent
|
|
982
982
|
// subset, and a supplier with no URL for an object refuses in the same words.
|
|
983
|
-
let streams = streams.clamp(1, 128);
|
|
983
|
+
let streams = super::memory::transfer_streams(streams.clamp(1, 128));
|
|
984
984
|
let recovery = super::speculate::Recovery::new(store, plan, streams, download_budget);
|
|
985
985
|
// Recovery reserves an object connection, not an authorization worker.
|
|
986
986
|
let workers = streams.min(plan.wanted.len().max(1));
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
//! successful end state for a content-addressed write, not a failure.
|
|
11
11
|
//!
|
|
12
12
|
//! The object is RE-SENT when the socket goes silent or the store answers a retryable
|
|
13
|
-
//! status, `PUT_ATTEMPTS` times, exactly as the pull re-asks for one object.
|
|
14
|
-
//!
|
|
15
|
-
//!
|
|
16
|
-
//!
|
|
17
|
-
//!
|
|
13
|
+
//! status, `PUT_ATTEMPTS` times, exactly as the pull re-asks for one object. Every wait of
|
|
14
|
+
//! the exchange — the answer after the last body byte included — is judged by its own
|
|
15
|
+
//! silence against the ledger's patience (`Judge::Silence`), and one write that moves
|
|
16
|
+
//! nothing by the ledger's floor; never an invented constant. A retryable answer is re-sent
|
|
17
|
+
//! after the wait the store states (`Retry-After`), which spends no attempt, or after the
|
|
18
|
+
//! ledger's patience, which does. Authority stays where it was: `podweights` (or whoever
|
|
18
19
|
//! minted the grant) decided WHETHER these bytes may go; this module only moves them.
|
|
19
20
|
//!
|
|
20
21
|
//! **A grant is a URL AND the headers its signature covers, together.** tensorhub signs
|
|
@@ -228,10 +229,9 @@ pub fn push_object(
|
|
|
228
229
|
Some(payload),
|
|
229
230
|
deadline,
|
|
230
231
|
ledger,
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
http::Judge::Deadline,
|
|
232
|
+
// A peer that stops answering — during the handshake, a write, or after the
|
|
233
|
+
// last body byte — is judged by that silence, never waited on forever.
|
|
234
|
+
http::Judge::Silence,
|
|
235
235
|
Some(ledger.floor()),
|
|
236
236
|
Code::TRANSFER_FAILED,
|
|
237
237
|
);
|
|
@@ -19,8 +19,8 @@ use tensorfs_core::{
|
|
|
19
19
|
providers::{self, Provenance, Pulled, Resolution, ResolvedMember},
|
|
20
20
|
store::{Fault, Store},
|
|
21
21
|
transport::{
|
|
22
|
-
|
|
23
|
-
UploadGrant,
|
|
22
|
+
memory_available, push_object, streams_within, transfer_streams, Anonymous, Deadline,
|
|
23
|
+
Ledger, PullCancellation, SourceDownload, SourcePolicy, UploadGrant, STREAM_MEMORY,
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
26
|
|
|
@@ -55,6 +55,8 @@ enum Act {
|
|
|
55
55
|
Status(u16, Vec<(String, String)>),
|
|
56
56
|
/// Send the head and this many body bytes, then drop the connection.
|
|
57
57
|
Cut(usize),
|
|
58
|
+
/// Read a PUT body, then say nothing, ever.
|
|
59
|
+
Silent,
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
struct Request {
|
|
@@ -177,13 +179,19 @@ impl Origin {
|
|
|
177
179
|
let number = self.asks.fetch_add(1, Ordering::Relaxed) + 1;
|
|
178
180
|
let act = (self.decide)(&request, number);
|
|
179
181
|
if request.method == "PUT" {
|
|
180
|
-
let length:
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
let length: u64 = header("content-length").unwrap().parse().unwrap();
|
|
183
|
+
// Discarded as it arrives: this origin's memory is not the client's.
|
|
184
|
+
let taken = std::io::copy(&mut (&mut socket).take(length), &mut std::io::sink());
|
|
185
|
+
if taken.ok() != Some(length) {
|
|
183
186
|
return;
|
|
184
187
|
}
|
|
185
188
|
self.puts.fetch_add(1, Ordering::Relaxed);
|
|
186
189
|
match act {
|
|
190
|
+
Act::Silent => {
|
|
191
|
+
// Hold the socket until the client gives up on it.
|
|
192
|
+
let _ = socket.read(&mut [0u8; 1]);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
187
195
|
Act::Status(status, extra) => {
|
|
188
196
|
if !reply(&mut socket, status, &extra, b"retry") {
|
|
189
197
|
return;
|
|
@@ -684,6 +692,45 @@ fn pushed_store(name: &str) -> (std::path::PathBuf, Store, ObjectRef) {
|
|
|
684
692
|
(dir, store, object)
|
|
685
693
|
}
|
|
686
694
|
|
|
695
|
+
#[test]
|
|
696
|
+
fn a_push_whose_store_goes_silent_after_the_body_is_judged_not_waited_on() {
|
|
697
|
+
let (dir, store, object) = pushed_store("push-silent");
|
|
698
|
+
let origin = Origin::start(vec![], 0, |request, _| {
|
|
699
|
+
if request.method == "PUT" {
|
|
700
|
+
Act::Silent
|
|
701
|
+
} else {
|
|
702
|
+
Act::Serve
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
let ledger = Ledger::with_resolution(Duration::from_millis(20));
|
|
706
|
+
let started = Instant::now();
|
|
707
|
+
let refusal = push_object(
|
|
708
|
+
&store,
|
|
709
|
+
&object.sha256,
|
|
710
|
+
false,
|
|
711
|
+
&UploadGrant::to(&format!("{}/put/object", origin.base)),
|
|
712
|
+
&policy(),
|
|
713
|
+
&Anonymous,
|
|
714
|
+
Deadline::none(),
|
|
715
|
+
&ledger,
|
|
716
|
+
"",
|
|
717
|
+
)
|
|
718
|
+
.unwrap_err();
|
|
719
|
+
assert_eq!(refusal.code, Code::TRANSFER_FAILED);
|
|
720
|
+
assert!(refusal.detail.contains("response head"), "{refusal}");
|
|
721
|
+
assert_eq!(
|
|
722
|
+
origin.puts.load(Ordering::Relaxed),
|
|
723
|
+
4,
|
|
724
|
+
"each attempt sent the whole body"
|
|
725
|
+
);
|
|
726
|
+
assert!(
|
|
727
|
+
started.elapsed() < Duration::from_secs(30),
|
|
728
|
+
"{:?}",
|
|
729
|
+
started.elapsed()
|
|
730
|
+
);
|
|
731
|
+
fs::remove_dir_all(dir).unwrap();
|
|
732
|
+
}
|
|
733
|
+
|
|
687
734
|
#[test]
|
|
688
735
|
fn a_push_told_to_wait_waits_and_then_lands() {
|
|
689
736
|
let (dir, store, object) = pushed_store("push-wait");
|
|
@@ -713,6 +760,210 @@ fn a_push_told_to_wait_waits_and_then_lands() {
|
|
|
713
760
|
fs::remove_dir_all(dir).unwrap();
|
|
714
761
|
}
|
|
715
762
|
|
|
763
|
+
#[test]
|
|
764
|
+
fn concurrent_admissions_queue_instead_of_refusing() {
|
|
765
|
+
let dir = root("admissions");
|
|
766
|
+
let store = Store::init(&dir).unwrap();
|
|
767
|
+
thread::scope(|scope| {
|
|
768
|
+
for worker in 0..16u64 {
|
|
769
|
+
let store = &store;
|
|
770
|
+
scope.spawn(move || {
|
|
771
|
+
for index in 0..4 {
|
|
772
|
+
let bytes = body(1000 + worker * 4 + index, 64 << 10);
|
|
773
|
+
let object = ObjectRef::of(&bytes);
|
|
774
|
+
store
|
|
775
|
+
.put_stream(&mut bytes.as_slice(), Some(&object), &Fault::default())
|
|
776
|
+
.unwrap();
|
|
777
|
+
assert!(store.record_valid(&object.sha256).is_ok());
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
fs::remove_dir_all(dir).unwrap();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/// Peak resident memory since `/proc/self/clear_refs` was last reset, in bytes.
|
|
786
|
+
fn peak_resident() -> u64 {
|
|
787
|
+
let status = fs::read_to_string("/proc/self/status").unwrap();
|
|
788
|
+
let kib: u64 = status
|
|
789
|
+
.lines()
|
|
790
|
+
.find_map(|line| line.strip_prefix("VmHWM:"))
|
|
791
|
+
.unwrap()
|
|
792
|
+
.trim()
|
|
793
|
+
.trim_end_matches("kB")
|
|
794
|
+
.trim()
|
|
795
|
+
.parse()
|
|
796
|
+
.unwrap();
|
|
797
|
+
kib * 1024
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/// Run one `*_child` test of this binary in its own process, optionally inside `scope`.
|
|
801
|
+
fn run_child(test: &str, dir: &std::path::Path, scope: &[&str]) -> std::process::Output {
|
|
802
|
+
let exe = std::env::current_exe().unwrap();
|
|
803
|
+
let mut command = match scope.split_first() {
|
|
804
|
+
Some((program, args)) => {
|
|
805
|
+
let mut command = Command::new(program);
|
|
806
|
+
command.args(args).arg(&exe);
|
|
807
|
+
command
|
|
808
|
+
}
|
|
809
|
+
None => Command::new(&exe),
|
|
810
|
+
};
|
|
811
|
+
command
|
|
812
|
+
.args(["--exact", test, "--nocapture"])
|
|
813
|
+
.env("TFS_DOWNLOAD_CHILD", dir)
|
|
814
|
+
.output()
|
|
815
|
+
.unwrap()
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
#[test]
|
|
819
|
+
fn push_memory_child() {
|
|
820
|
+
let Some(dir) = std::env::var_os("TFS_DOWNLOAD_CHILD") else {
|
|
821
|
+
return;
|
|
822
|
+
};
|
|
823
|
+
let store = Store::init(std::path::Path::new(&dir)).unwrap();
|
|
824
|
+
let objects: Vec<ObjectRef> = (0..16u64)
|
|
825
|
+
.map(|seed| {
|
|
826
|
+
let bytes = body(300 + seed, 16 * MIB);
|
|
827
|
+
let object = ObjectRef::of(&bytes);
|
|
828
|
+
store
|
|
829
|
+
.put_stream(&mut bytes.as_slice(), Some(&object), &Fault::default())
|
|
830
|
+
.unwrap();
|
|
831
|
+
object
|
|
832
|
+
})
|
|
833
|
+
.collect();
|
|
834
|
+
let origin = Origin::start(vec![], 0, |_, _| Act::Serve);
|
|
835
|
+
// Forget the objects' construction; measure only the pushes.
|
|
836
|
+
fs::write("/proc/self/clear_refs", "5").unwrap();
|
|
837
|
+
let before = peak_resident();
|
|
838
|
+
thread::scope(|scope| {
|
|
839
|
+
for object in &objects {
|
|
840
|
+
let (store, origin) = (&store, &origin);
|
|
841
|
+
scope.spawn(move || {
|
|
842
|
+
push_object(
|
|
843
|
+
store,
|
|
844
|
+
&object.sha256,
|
|
845
|
+
false,
|
|
846
|
+
&UploadGrant::to(&format!("{}/put/{}", origin.base, object.sha256)),
|
|
847
|
+
&policy(),
|
|
848
|
+
&Anonymous,
|
|
849
|
+
Deadline::none(),
|
|
850
|
+
&Ledger::with_resolution(Duration::from_millis(200)),
|
|
851
|
+
"",
|
|
852
|
+
)
|
|
853
|
+
.unwrap();
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
let grew = peak_resident().saturating_sub(before);
|
|
858
|
+
println!("PUSHED 16 x 16 MiB, peak resident grew {grew} B");
|
|
859
|
+
assert_eq!(origin.puts.load(Ordering::Relaxed), 16);
|
|
860
|
+
// 256 MiB crossed the sockets; the pushes may hold their streams' buffers and no more.
|
|
861
|
+
assert!(
|
|
862
|
+
grew < 16 * STREAM_MEMORY + (16 << 20),
|
|
863
|
+
"pushes buffered {grew} B"
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
#[test]
|
|
868
|
+
fn sixteen_concurrent_pushes_hold_stream_buffers_not_objects() {
|
|
869
|
+
let dir = root("push-memory");
|
|
870
|
+
let output = run_child("push_memory_child", &dir, &[]);
|
|
871
|
+
let text = String::from_utf8_lossy(&output.stdout).to_string()
|
|
872
|
+
+ &String::from_utf8_lossy(&output.stderr);
|
|
873
|
+
assert!(output.status.success() && text.contains("PUSHED"), "{text}");
|
|
874
|
+
println!(
|
|
875
|
+
"{}",
|
|
876
|
+
text.lines().find(|line| line.contains("PUSHED")).unwrap()
|
|
877
|
+
);
|
|
878
|
+
fs::remove_dir_all(dir).unwrap();
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
#[test]
|
|
882
|
+
fn streams_are_sized_to_the_memory_budget() {
|
|
883
|
+
assert_eq!(streams_within(16, None), 16);
|
|
884
|
+
assert_eq!(streams_within(16, Some(1 << 40)), 16);
|
|
885
|
+
assert_eq!(streams_within(16, Some(5 * STREAM_MEMORY)), 5);
|
|
886
|
+
assert_eq!(
|
|
887
|
+
streams_within(16, Some(0)),
|
|
888
|
+
1,
|
|
889
|
+
"a starved host still moves one stream"
|
|
890
|
+
);
|
|
891
|
+
assert!(memory_available().is_some_and(|bytes| bytes > 0));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
#[test]
|
|
895
|
+
fn bounded_memory_child() {
|
|
896
|
+
let Some(dir) = std::env::var_os("TFS_DOWNLOAD_CHILD") else {
|
|
897
|
+
return;
|
|
898
|
+
};
|
|
899
|
+
let available = memory_available().unwrap();
|
|
900
|
+
let streams = transfer_streams(1024);
|
|
901
|
+
println!("BUDGET {available} B allows {streams} streams");
|
|
902
|
+
// The scope's 64 MiB, not the host's gigabytes, decides.
|
|
903
|
+
assert!(
|
|
904
|
+
available < 64 << 20 && streams < 32,
|
|
905
|
+
"{available} B, {streams} streams"
|
|
906
|
+
);
|
|
907
|
+
let name = "model.safetensors";
|
|
908
|
+
let origin = Origin::start(vec![(name, body(77, 16 * MIB))], 16 << 20, |_, _| {
|
|
909
|
+
Act::Serve
|
|
910
|
+
});
|
|
911
|
+
let store = Store::init(std::path::Path::new(&dir)).unwrap();
|
|
912
|
+
materialize(
|
|
913
|
+
&store,
|
|
914
|
+
&owner("bounded"),
|
|
915
|
+
&origin.resolution(&[name]),
|
|
916
|
+
&options(MIB, 1024),
|
|
917
|
+
&|_, _| {},
|
|
918
|
+
)
|
|
919
|
+
.unwrap();
|
|
920
|
+
verify(&store, &origin, &[name]);
|
|
921
|
+
let live = origin.most_live.load(Ordering::SeqCst);
|
|
922
|
+
println!("DOWNLOADED with at most {live} live streams");
|
|
923
|
+
assert!(
|
|
924
|
+
live <= streams,
|
|
925
|
+
"{live} streams against a budget of {streams}"
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
#[test]
|
|
930
|
+
fn a_memory_limited_scope_downloads_with_fewer_streams() {
|
|
931
|
+
let scope = [
|
|
932
|
+
"systemd-run",
|
|
933
|
+
"--user",
|
|
934
|
+
"--scope",
|
|
935
|
+
"-q",
|
|
936
|
+
"-p",
|
|
937
|
+
"MemoryMax=64M",
|
|
938
|
+
];
|
|
939
|
+
let usable = Command::new(scope[0])
|
|
940
|
+
.args(&scope[1..])
|
|
941
|
+
.arg("true")
|
|
942
|
+
.stdout(Stdio::null())
|
|
943
|
+
.stderr(Stdio::null())
|
|
944
|
+
.status()
|
|
945
|
+
.is_ok_and(|status| status.success());
|
|
946
|
+
if !usable {
|
|
947
|
+
eprintln!("SKIPPED: no systemd user manager to create a memory-limited cgroup");
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
let dir = root("bounded-memory");
|
|
951
|
+
let output = run_child("bounded_memory_child", &dir, &scope);
|
|
952
|
+
let text = String::from_utf8_lossy(&output.stdout).to_string()
|
|
953
|
+
+ &String::from_utf8_lossy(&output.stderr);
|
|
954
|
+
assert!(
|
|
955
|
+
output.status.success() && text.contains("DOWNLOADED"),
|
|
956
|
+
"{text}"
|
|
957
|
+
);
|
|
958
|
+
for line in text
|
|
959
|
+
.lines()
|
|
960
|
+
.filter(|line| line.contains("BUDGET") || line.contains("DOWNLOADED"))
|
|
961
|
+
{
|
|
962
|
+
println!("{line}");
|
|
963
|
+
}
|
|
964
|
+
let _ = fs::remove_dir_all(dir);
|
|
965
|
+
}
|
|
966
|
+
|
|
716
967
|
/// Throughput on a paced loopback origin (each connection capped like one CDN stream).
|
|
717
968
|
/// `cargo test --test source_download -- --ignored --nocapture throughput`
|
|
718
969
|
#[test]
|
|
@@ -119,6 +119,8 @@ fn _ext(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
119
119
|
m.add_function(wrap_pyfunction!(store::fetch_complete_cozytensors, m)?)?;
|
|
120
120
|
m.add_class::<store::PyPullCancellation>()?;
|
|
121
121
|
m.add_function(wrap_pyfunction!(store::pull, m)?)?;
|
|
122
|
+
m.add_function(wrap_pyfunction!(store::transfer_streams, m)?)?;
|
|
123
|
+
m.add("STREAM_MEMORY", tensorfs_core::transport::STREAM_MEMORY)?;
|
|
122
124
|
m.add_class::<store::PyRepoObjectCache>()?;
|
|
123
125
|
m.add_class::<store::PyStore>()?;
|
|
124
126
|
m.add_class::<store::PyOperation>()?;
|