tensorfs 0.3.57__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.57 → tensorfs-0.3.59}/Cargo.lock +2 -2
- {tensorfs-0.3.57 → tensorfs-0.3.59}/Cargo.toml +1 -1
- {tensorfs-0.3.57 → tensorfs-0.3.59}/PKG-INFO +1 -1
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/catalog.rs +17 -1
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/providers.rs +50 -30
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/http.rs +76 -5
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +14 -12
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/ledger.rs +28 -1
- tensorfs-0.3.59/crates/tensorfs-core/src/transport/memory.rs +92 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/mod.rs +3 -1
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/policy.rs +13 -0
- tensorfs-0.3.59/crates/tensorfs-core/src/transport/prefix.rs +1094 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/pull.rs +17 -5
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/push.rs +25 -9
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/ranged.rs +40 -8
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/tests.rs +9 -4
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_artifacts.rs +2 -2
- tensorfs-0.3.59/crates/tensorfs-core/tests/source_download.rs +1006 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_prefixes.rs +4 -1
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-py/src/lib.rs +2 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-py/src/store.rs +52 -15
- {tensorfs-0.3.57 → tensorfs-0.3.59}/pyproject.toml +1 -1
- {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/__init__.py +4 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/_ext.pyi +10 -0
- tensorfs-0.3.57/crates/tensorfs-core/src/transport/prefix.rs +0 -505
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/asset_update.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/cache_roots.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived/roots.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/derived.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/fill.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/gc.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/source.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/lib.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/meta.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/repo_cache.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/source_artifact.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/stats.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/store.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/trust_index.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/_derived_channel.py +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/_source_channel.py +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/_tensors.py +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/derived.py +0 -0
- {tensorfs-0.3.57 → tensorfs-0.3.59}/python/tensorfs/errors.py +0 -0
- {tensorfs-0.3.57 → 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"
|
|
@@ -793,6 +793,7 @@ pub fn content_manifest(resolution: &Resolution) -> Result<crate::manifest::Mani
|
|
|
793
793
|
}
|
|
794
794
|
|
|
795
795
|
/// Materialize precisely an accepted selection; never re-list or broaden its roster.
|
|
796
|
+
/// `progress` hears (present bytes, total bytes) of the selection's distinct objects.
|
|
796
797
|
#[allow(clippy::too_many_arguments)]
|
|
797
798
|
pub fn materialize_selected(
|
|
798
799
|
store: &Store,
|
|
@@ -801,7 +802,7 @@ pub fn materialize_selected(
|
|
|
801
802
|
policy: &SourcePolicy,
|
|
802
803
|
credentials: &dyn CredentialProvider,
|
|
803
804
|
deadline: Deadline,
|
|
804
|
-
progress:
|
|
805
|
+
progress: crate::transport::SourceProgress<'_>,
|
|
805
806
|
) -> Result<(crate::source_artifact::TreeRoot, Vec<Pulled>)> {
|
|
806
807
|
materialize_selected_with_options(
|
|
807
808
|
store,
|
|
@@ -815,7 +816,7 @@ pub fn materialize_selected(
|
|
|
815
816
|
)
|
|
816
817
|
}
|
|
817
818
|
|
|
818
|
-
///
|
|
819
|
+
/// Stream count, checkpoint sizing, cancellation and development fault points.
|
|
819
820
|
#[allow(clippy::too_many_arguments)]
|
|
820
821
|
pub fn materialize_selected_with_options(
|
|
821
822
|
store: &Store,
|
|
@@ -824,7 +825,7 @@ pub fn materialize_selected_with_options(
|
|
|
824
825
|
policy: &SourcePolicy,
|
|
825
826
|
credentials: &dyn CredentialProvider,
|
|
826
827
|
deadline: Deadline,
|
|
827
|
-
progress:
|
|
828
|
+
progress: crate::transport::SourceProgress<'_>,
|
|
828
829
|
options: &crate::transport::SourceDownload,
|
|
829
830
|
) -> Result<(crate::source_artifact::TreeRoot, Vec<Pulled>)> {
|
|
830
831
|
use crate::ids::Doc;
|
|
@@ -833,35 +834,54 @@ pub fn materialize_selected_with_options(
|
|
|
833
834
|
let mut writer = crate::source_artifact::Writer::open(store, owner, tree.object_ref())?;
|
|
834
835
|
let objects = resolution.objects();
|
|
835
836
|
let (plan, _) = FetchPlan::of_objects(store, "selected-source", &objects)?;
|
|
836
|
-
let ledger = Ledger::new();
|
|
837
|
-
let
|
|
837
|
+
let mut ledger = Ledger::new();
|
|
838
|
+
if let Some(cancellation) = &options.cancellation {
|
|
839
|
+
ledger = ledger.with_cancellation(cancellation.clone());
|
|
840
|
+
}
|
|
841
|
+
// Every member's liveness is published before any of their bytes move.
|
|
842
|
+
let mut jobs: Vec<crate::transport::prefix::SourceJob> = vec![];
|
|
843
|
+
let (mut seen, mut held_bytes) = (std::collections::BTreeSet::new(), 0);
|
|
838
844
|
for member in &resolution.members {
|
|
839
845
|
writer.hold_expected(&member.object)?;
|
|
840
|
-
let
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
846
|
+
let object = &member.object;
|
|
847
|
+
if !seen.insert(&object.sha256) {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if matches!(store.record_valid(&object.sha256), Ok(row) if row.length == object.length) {
|
|
851
|
+
held_bytes += object.length;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
jobs.push(crate::transport::prefix::SourceJob {
|
|
855
|
+
grant: DeliveryGrant::mint(&plan, object)?,
|
|
856
|
+
url: member.url.clone(),
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
let transferred = crate::transport::prefix::fetch_sources(
|
|
860
|
+
store,
|
|
861
|
+
owner,
|
|
862
|
+
&jobs,
|
|
863
|
+
policy,
|
|
864
|
+
credentials,
|
|
865
|
+
deadline,
|
|
866
|
+
&ledger,
|
|
867
|
+
options,
|
|
868
|
+
held_bytes,
|
|
869
|
+
progress,
|
|
870
|
+
)?;
|
|
871
|
+
let mut pulled = vec![];
|
|
872
|
+
let mut counted = std::collections::BTreeSet::new();
|
|
873
|
+
for member in &resolution.members {
|
|
859
874
|
writer.landed(&member.object)?;
|
|
875
|
+
let first = counted.insert(&member.object.sha256);
|
|
876
|
+
let fetched = jobs
|
|
877
|
+
.iter()
|
|
878
|
+
.position(|job| job.grant.object == member.object)
|
|
879
|
+
.filter(|_| first);
|
|
860
880
|
pulled.push(Pulled {
|
|
861
881
|
member: member.member.clone(),
|
|
862
882
|
object: member.object.clone(),
|
|
863
|
-
transferred,
|
|
864
|
-
held,
|
|
883
|
+
transferred: fetched.map_or(0, |at| transferred[at]),
|
|
884
|
+
held: fetched.is_none(),
|
|
865
885
|
});
|
|
866
886
|
}
|
|
867
887
|
plan.complete(store)?;
|
|
@@ -2475,7 +2495,7 @@ mod tests {
|
|
|
2475
2495
|
&endpoints.policy(&uri),
|
|
2476
2496
|
&Anonymous,
|
|
2477
2497
|
Deadline::none(),
|
|
2478
|
-
|
|
2498
|
+
&|_, _| {},
|
|
2479
2499
|
)
|
|
2480
2500
|
.unwrap();
|
|
2481
2501
|
assert_eq!(
|
|
@@ -2492,7 +2512,7 @@ mod tests {
|
|
|
2492
2512
|
&endpoints.policy(&uri),
|
|
2493
2513
|
&Anonymous,
|
|
2494
2514
|
Deadline::none(),
|
|
2495
|
-
|
|
2515
|
+
&|_, _| {},
|
|
2496
2516
|
)
|
|
2497
2517
|
.unwrap();
|
|
2498
2518
|
assert_eq!(warm, cold);
|
|
@@ -2646,7 +2666,7 @@ mod tests {
|
|
|
2646
2666
|
&endpoints.policy(&uri),
|
|
2647
2667
|
&Anonymous,
|
|
2648
2668
|
Deadline::none(),
|
|
2649
|
-
|
|
2669
|
+
&|_, _| {},
|
|
2650
2670
|
)
|
|
2651
2671
|
.unwrap_err();
|
|
2652
2672
|
assert_eq!(error.code, Code::OBJECT_ID_MISMATCH);
|
|
@@ -2698,7 +2718,7 @@ mod tests {
|
|
|
2698
2718
|
&endpoints.policy(&uri),
|
|
2699
2719
|
&Anonymous,
|
|
2700
2720
|
Deadline::none(),
|
|
2701
|
-
|
|
2721
|
+
&|_, _| {},
|
|
2702
2722
|
)
|
|
2703
2723
|
.unwrap();
|
|
2704
2724
|
assert!(retained.complete && !retained.released);
|
|
@@ -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,
|
|
@@ -157,20 +168,21 @@ type IdlePool = Arc<Mutex<Vec<Idle>>>;
|
|
|
157
168
|
// This bounds retained descriptors, not in-flight transfers (the caller owns those).
|
|
158
169
|
const MAX_IDLE: usize = 8;
|
|
159
170
|
|
|
171
|
+
/// A connection belongs to its origin and the credentials sent on it, never to one
|
|
172
|
+
/// request target: a signed CDN URL changes per resolve, and keying on it meant the
|
|
173
|
+
/// delivery hop never reused a socket.
|
|
160
174
|
#[derive(PartialEq, Eq)]
|
|
161
175
|
struct Scope {
|
|
162
176
|
https: bool,
|
|
163
177
|
host: String,
|
|
164
178
|
port: u16,
|
|
165
|
-
// Do not retain
|
|
179
|
+
// Do not retain credentials in an idle connection's key.
|
|
166
180
|
request: [u8; 32],
|
|
167
181
|
}
|
|
168
182
|
|
|
169
183
|
impl Scope {
|
|
170
184
|
fn ranged(checked: &CheckedUrl, headers: &[(String, String)]) -> Self {
|
|
171
185
|
let mut hash = crate::sha256::Sha256::new();
|
|
172
|
-
hash.update(&(checked.target.len() as u64).to_le_bytes());
|
|
173
|
-
hash.update(checked.target.as_bytes());
|
|
174
186
|
for (name, value) in headers
|
|
175
187
|
.iter()
|
|
176
188
|
.filter(|(name, _)| !name.eq_ignore_ascii_case("range"))
|
|
@@ -532,6 +544,34 @@ impl<'l> Response<'l> {
|
|
|
532
544
|
self.body
|
|
533
545
|
}
|
|
534
546
|
|
|
547
|
+
/// Drop a small non-artifact body (a redirect's, a refusal's) by reading it to its end,
|
|
548
|
+
/// so the connection can serve the next ask. Nothing read here is counted; a body
|
|
549
|
+
/// larger than a response head is simply left, and its connection closes.
|
|
550
|
+
pub fn discard(self) {
|
|
551
|
+
let mut body = self.body;
|
|
552
|
+
let mut buf = [0u8; 4096];
|
|
553
|
+
let mut left = HEAD_MAX_BYTES;
|
|
554
|
+
while left > 0 {
|
|
555
|
+
match body.read_framed(&mut buf[..left.min(4096)]) {
|
|
556
|
+
Ok(0) | Err(_) => return,
|
|
557
|
+
Ok(n) => left -= n,
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/// The wait this answer states (`Retry-After`), read in the origin's own clock:
|
|
563
|
+
/// delta-seconds, or an HTTP-date against the answer's own `Date`. `None` when it
|
|
564
|
+
/// states nothing readable.
|
|
565
|
+
pub fn stated_wait(&self) -> Option<Duration> {
|
|
566
|
+
let value = self.header("retry-after")?.trim();
|
|
567
|
+
if let Ok(seconds) = value.parse::<u64>() {
|
|
568
|
+
return Some(Duration::from_secs(seconds));
|
|
569
|
+
}
|
|
570
|
+
let at = http_date(value)?;
|
|
571
|
+
let now = http_date(self.header("date")?)?;
|
|
572
|
+
Some(Duration::from_secs(at.saturating_sub(now)))
|
|
573
|
+
}
|
|
574
|
+
|
|
535
575
|
/// The whole body, refused above `cap` rather than truncated.
|
|
536
576
|
pub fn read_capped(mut self, cap: usize) -> Result<Vec<u8>> {
|
|
537
577
|
let mut out = Vec::new();
|
|
@@ -552,6 +592,37 @@ impl<'l> Response<'l> {
|
|
|
552
592
|
}
|
|
553
593
|
}
|
|
554
594
|
|
|
595
|
+
/// RFC 9110 IMF-fixdate (`Sun, 06 Nov 1994 08:49:37 GMT`) as Unix seconds.
|
|
596
|
+
fn http_date(text: &str) -> Option<u64> {
|
|
597
|
+
const MONTHS: [&str; 12] = [
|
|
598
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
599
|
+
];
|
|
600
|
+
let mut parts = text.split_whitespace().skip(1);
|
|
601
|
+
let day: i64 = parts.next()?.parse().ok()?;
|
|
602
|
+
let month = parts.next()?;
|
|
603
|
+
let month = MONTHS.iter().position(|name| *name == month)? as i64 + 1;
|
|
604
|
+
let year: i64 = parts.next()?.parse().ok()?;
|
|
605
|
+
let clock: Vec<i64> = parts
|
|
606
|
+
.next()?
|
|
607
|
+
.split(':')
|
|
608
|
+
.map(|part| part.parse().ok())
|
|
609
|
+
.collect::<Option<_>>()?;
|
|
610
|
+
if parts.next()? != "GMT" || clock.len() != 3 {
|
|
611
|
+
return None;
|
|
612
|
+
}
|
|
613
|
+
// Days from the civil calendar (Hinnant).
|
|
614
|
+
let (y, m) = if month <= 2 {
|
|
615
|
+
(year - 1, month + 9)
|
|
616
|
+
} else {
|
|
617
|
+
(year, month - 3)
|
|
618
|
+
};
|
|
619
|
+
let era = y.div_euclid(400);
|
|
620
|
+
let yoe = y - era * 400;
|
|
621
|
+
let doe = yoe * 365 + yoe / 4 - yoe / 100 + (153 * m + 2) / 5 + day - 1;
|
|
622
|
+
let days = era * 146_097 + doe - 719_468;
|
|
623
|
+
u64::try_from(days * 86_400 + clock[0] * 3600 + clock[1] * 60 + clock[2]).ok()
|
|
624
|
+
}
|
|
625
|
+
|
|
555
626
|
#[derive(Clone, Copy)]
|
|
556
627
|
enum Framing {
|
|
557
628
|
Length { remaining: u64 },
|
|
@@ -652,7 +723,7 @@ impl BodyReader<'_> {
|
|
|
652
723
|
Err(e) if would_block(&e) => {
|
|
653
724
|
transfer_verdict(self.judge, self.ledger, self.since, "a response body")?;
|
|
654
725
|
let silent = self.advanced.elapsed();
|
|
655
|
-
if self.judge
|
|
726
|
+
if self.judge.silent_is_stall() && silent > self.ledger.patience() {
|
|
656
727
|
self.stalled = true;
|
|
657
728
|
let _ = self
|
|
658
729
|
.wire
|
|
@@ -243,7 +243,7 @@ fn complete_ranged_http_and_tls_bodies_reuse_one_connection() {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
#[test]
|
|
246
|
-
fn
|
|
246
|
+
fn cached_peer_must_remain_checked_and_credentials_stay_scoped_but_signed_targets_share() {
|
|
247
247
|
let origin = Origin::new(false, Duration::ZERO, |_, _| Reply::body(b"checked"));
|
|
248
248
|
let client = origin.client();
|
|
249
249
|
let checked = origin.checked("/object?signature=one");
|
|
@@ -260,8 +260,9 @@ fn cached_peer_must_remain_checked_and_credentials_and_signed_target_stay_scoped
|
|
|
260
260
|
ask(&client, &checked, &headers).unwrap();
|
|
261
261
|
headers[1].1 = "Bearer second".into();
|
|
262
262
|
ask(&client, &checked, &headers).unwrap();
|
|
263
|
+
// A re-signed delivery URL on the same origin and credentials rides the same socket.
|
|
263
264
|
ask(&client, &origin.checked("/object?signature=two"), &headers).unwrap();
|
|
264
|
-
assert_eq!(origin.count(),
|
|
265
|
+
assert_eq!(origin.count(), 3);
|
|
265
266
|
let calls = origin.asks.lock().unwrap();
|
|
266
267
|
assert_eq!(
|
|
267
268
|
calls
|
|
@@ -357,12 +358,9 @@ fn idle_descriptor_retention_is_bounded_and_non_range_gets_do_not_pool() {
|
|
|
357
358
|
let origin = Origin::new(false, Duration::ZERO, |_, _| Reply::body(b"bytes"));
|
|
358
359
|
let client = origin.client();
|
|
359
360
|
for index in 0..32 {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
&ranged(),
|
|
364
|
-
)
|
|
365
|
-
.unwrap();
|
|
361
|
+
let mut headers = ranged();
|
|
362
|
+
headers.push(("authorization".into(), format!("Bearer {index}")));
|
|
363
|
+
ask(&client, &origin.checked("/object"), &headers).unwrap();
|
|
366
364
|
}
|
|
367
365
|
assert_eq!(client.idle.lock().unwrap().len(), MAX_IDLE);
|
|
368
366
|
assert_eq!(origin.count(), 32);
|
|
@@ -583,21 +581,25 @@ fn retained_source_redirect_ranges_reduce_connections_without_changing_asks_or_b
|
|
|
583
581
|
&Origin::policy(),
|
|
584
582
|
credentials,
|
|
585
583
|
Deadline::none(),
|
|
586
|
-
|
|
584
|
+
&|_, _| {},
|
|
587
585
|
&SourceDownload {
|
|
588
586
|
checkpoint_bytes: 1 << 20,
|
|
587
|
+
streams: 1,
|
|
589
588
|
fault: Fault::default(),
|
|
589
|
+
..Default::default()
|
|
590
590
|
},
|
|
591
591
|
)
|
|
592
592
|
.unwrap();
|
|
593
593
|
let elapsed = start.elapsed();
|
|
594
594
|
assert_eq!(rows[0].transferred, bytes.len() as u64);
|
|
595
595
|
assert!(store.record_valid(&object.sha256).is_ok());
|
|
596
|
-
|
|
597
|
-
|
|
596
|
+
// One ask walks the redirect, one GET carries all eight checkpoints of the run,
|
|
597
|
+
// and the drained redirect's socket serves the delivery hop.
|
|
598
|
+
assert_eq!(origin.asks.lock().unwrap().len(), 2);
|
|
599
|
+
assert_eq!(origin.count(), if close { 2 } else { 1 });
|
|
598
600
|
timings.push((close, elapsed.as_secs_f64(), origin.count()));
|
|
599
601
|
drop(store);
|
|
600
602
|
std::fs::remove_dir_all(root).unwrap();
|
|
601
603
|
}
|
|
602
|
-
println!("retained source 8MiB,
|
|
604
|
+
println!("retained source 8MiB, one redirected ask, eight 1MiB checkpoints, 30ms connection setup: {timings:?}");
|
|
603
605
|
}
|
|
@@ -135,7 +135,7 @@ pub struct Still {
|
|
|
135
135
|
|
|
136
136
|
/// One caller's cooperative stop signal. Clones name the same pull intent;
|
|
137
137
|
/// a resumed operation uses a new token and retains existing Store bytes.
|
|
138
|
-
#[derive(Clone, Default)]
|
|
138
|
+
#[derive(Clone, Debug, Default)]
|
|
139
139
|
pub struct PullCancellation(Arc<AtomicBool>);
|
|
140
140
|
|
|
141
141
|
impl PullCancellation {
|
|
@@ -357,6 +357,33 @@ impl Ledger {
|
|
|
357
357
|
worst.mul_f64(STALL_FACTOR).max(self.patience())
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
/// What a retryable answer costs before the re-ask: the origin's own stated wait,
|
|
361
|
+
/// which is not a failed attempt, or — when it states none — this pull's patience,
|
|
362
|
+
/// which is (`true`). The hub's rule, applied to every origin.
|
|
363
|
+
pub(crate) fn retry_wait(&self, stated: Option<Duration>) -> (Duration, bool) {
|
|
364
|
+
match stated.filter(|wait| !wait.is_zero()) {
|
|
365
|
+
Some(wait) => (wait, false),
|
|
366
|
+
None => (self.patience(), true),
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/// Sleep `wait` in sampling ticks; cancellation and the caller's deadline end it.
|
|
371
|
+
pub(crate) fn pause(&self, wait: Duration, deadline: Deadline) -> Result<()> {
|
|
372
|
+
let until = Instant::now() + wait;
|
|
373
|
+
loop {
|
|
374
|
+
self.check_running()?;
|
|
375
|
+
let now = Instant::now();
|
|
376
|
+
if now >= until {
|
|
377
|
+
return Ok(());
|
|
378
|
+
}
|
|
379
|
+
let mut tick = (until - now).min(self.sample);
|
|
380
|
+
if let Some(left) = deadline.remaining()? {
|
|
381
|
+
tick = tick.min(left);
|
|
382
|
+
}
|
|
383
|
+
std::thread::sleep(tick);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
360
387
|
/// What this pull has moved and how long it has been still.
|
|
361
388
|
pub fn still(&self) -> Still {
|
|
362
389
|
let progress = self.progress.lock().unwrap();
|
|
@@ -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,
|
|
@@ -63,4 +65,4 @@ pub use pull::{
|
|
|
63
65
|
pub use push::{push_object, Pushed, UploadGrant, MAX_GRANT_BYTES, PUT_ATTEMPTS};
|
|
64
66
|
pub use ranged::{fetch_ranged, Ranged, PART_BYTES, RANGED_MIN_BYTES, SOURCE_STREAMS};
|
|
65
67
|
|
|
66
|
-
pub use prefix::SourceDownload;
|
|
68
|
+
pub use prefix::{SourceDownload, SourceProgress};
|
|
@@ -145,6 +145,19 @@ pub struct CheckedUrl {
|
|
|
145
145
|
pub addrs: Vec<SocketAddr>,
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
impl CheckedUrl {
|
|
149
|
+
/// The URL this check admitted, as a string a later ask can start from.
|
|
150
|
+
pub fn url(&self) -> String {
|
|
151
|
+
let scheme = if self.https { "https" } else { "http" };
|
|
152
|
+
let host = if self.host.contains(':') {
|
|
153
|
+
format!("[{}]", self.host)
|
|
154
|
+
} else {
|
|
155
|
+
self.host.clone()
|
|
156
|
+
};
|
|
157
|
+
format!("{scheme}://{host}:{}{}", self.port, self.target)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
148
161
|
/// The deployment's declared egress bound. Carried by every caller that hands this module
|
|
149
162
|
/// a URL; enforced on the first URL and on every redirect hop, because the hops are only
|
|
150
163
|
/// visible here.
|