tensorfs 0.3.46__tar.gz → 0.3.48__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.46 → tensorfs-0.3.48}/Cargo.lock +2 -2
- {tensorfs-0.3.46 → tensorfs-0.3.48}/Cargo.toml +1 -1
- {tensorfs-0.3.46 → tensorfs-0.3.48}/PKG-INFO +1 -1
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/Cargo.toml +1 -1
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/fetch.rs +1 -1
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/repo_cache.rs +371 -41
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/http.rs +100 -17
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/ledger.rs +94 -8
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/mod.rs +1 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/pull.rs +271 -30
- tensorfs-0.3.48/crates/tensorfs-core/src/transport/speculate.rs +395 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/tests.rs +1119 -2
- {tensorfs-0.3.46 → tensorfs-0.3.48}/pyproject.toml +1 -1
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/asset_update.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/cache_roots.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/catalog.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived/roots.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/derived.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/fill.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/gc.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/source.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/lib.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/meta.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/providers.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/reclaim.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/source_artifact.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/store.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/policy.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/push.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-py/src/lib.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/crates/tensorfs-py/src/store.rs +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/__init__.py +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/_derived_channel.py +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/_ext.pyi +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/_source_channel.py +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/_tensors.py +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/derived.py +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/errors.py +0 -0
- {tensorfs-0.3.46 → tensorfs-0.3.48}/python/tensorfs/py.typed +0 -0
|
@@ -421,7 +421,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
|
421
421
|
|
|
422
422
|
[[package]]
|
|
423
423
|
name = "tensorfs-core"
|
|
424
|
-
version = "0.3.
|
|
424
|
+
version = "0.3.48"
|
|
425
425
|
dependencies = [
|
|
426
426
|
"fs2",
|
|
427
427
|
"libc",
|
|
@@ -435,7 +435,7 @@ dependencies = [
|
|
|
435
435
|
|
|
436
436
|
[[package]]
|
|
437
437
|
name = "tensorfs-py"
|
|
438
|
-
version = "0.3.
|
|
438
|
+
version = "0.3.48"
|
|
439
439
|
dependencies = [
|
|
440
440
|
"pyo3",
|
|
441
441
|
"tensorfs-core",
|
|
@@ -11,7 +11,7 @@ path = "src/bin/tfs/main.rs"
|
|
|
11
11
|
fs2 = "0.4"
|
|
12
12
|
minicbor = { version = "2.3.0", default-features = false, features = ["alloc"] }
|
|
13
13
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
|
14
|
-
rustix = { version = "1.1.4", default-features = false, features = ["fs", "std"] }
|
|
14
|
+
rustix = { version = "1.1.4", default-features = false, features = ["fs", "std", "net", "event"] }
|
|
15
15
|
# tfs-049: the ONE transport (src/transport/). rustls is the one TLS stack, used only
|
|
16
16
|
# there; scripts/fence.py refuses it anywhere else so a second mover cannot grow.
|
|
17
17
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
|
|
@@ -303,7 +303,7 @@ fn pull_with_budget(
|
|
|
303
303
|
let report = ok!(transport::pull(&request));
|
|
304
304
|
progress.settle();
|
|
305
305
|
if !tensorfs_core::repo_cache::finish_optional_backfills() {
|
|
306
|
-
eprintln!("repo cache
|
|
306
|
+
eprintln!("repo cache replication incomplete; local fetch remains complete");
|
|
307
307
|
}
|
|
308
308
|
println!(
|
|
309
309
|
"model {}@{} lane {}",
|
|
@@ -102,32 +102,6 @@ impl RepoObjectCache {
|
|
|
102
102
|
&self.root
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/// Offer optional replication after local acquisition. This reports no durability
|
|
106
|
-
/// and never waits for filesystem I/O or queue space. Ingest's explicit Mirror
|
|
107
|
-
/// retains its separate completion/watermark contract.
|
|
108
|
-
pub(crate) fn offer_backfill(
|
|
109
|
-
&self,
|
|
110
|
-
source: &Store,
|
|
111
|
-
kind: CacheKind,
|
|
112
|
-
object: &ObjectRef,
|
|
113
|
-
) -> bool {
|
|
114
|
-
let pool = BACKFILLS.get_or_init(Backfills::start);
|
|
115
|
-
pool.activity.offered();
|
|
116
|
-
let sent = pool
|
|
117
|
-
.sender
|
|
118
|
-
.try_send(Backfill {
|
|
119
|
-
cache: self.clone(),
|
|
120
|
-
source: source.root().to_path_buf(),
|
|
121
|
-
kind,
|
|
122
|
-
object: object.clone(),
|
|
123
|
-
})
|
|
124
|
-
.is_ok();
|
|
125
|
-
if !sent {
|
|
126
|
-
pool.activity.finished();
|
|
127
|
-
}
|
|
128
|
-
sent
|
|
129
|
-
}
|
|
130
|
-
|
|
131
105
|
/// The one cross-language layout contract. `digest` is bare lowercase SHA-256.
|
|
132
106
|
pub fn path(&self, kind: CacheKind, digest: &str) -> Result<PathBuf> {
|
|
133
107
|
let digest = hex64("repo cache object", digest)?;
|
|
@@ -296,6 +270,94 @@ impl RepoObjectCache {
|
|
|
296
270
|
}
|
|
297
271
|
}
|
|
298
272
|
|
|
273
|
+
/// One pull retains only references rejected by the bounded optional write queue.
|
|
274
|
+
/// Its maximum is the already-declared closure; no payload buffer or persistent
|
|
275
|
+
/// queue is added, and origin workers never wait for cache I/O or queue space.
|
|
276
|
+
pub(crate) struct BackfillBatch<'a> {
|
|
277
|
+
cache: RepoObjectCache,
|
|
278
|
+
source: PathBuf,
|
|
279
|
+
limit: usize,
|
|
280
|
+
missed: std::sync::Mutex<Vec<(CacheKind, ObjectRef)>>,
|
|
281
|
+
pool: &'a Backfills,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
impl BackfillBatch<'static> {
|
|
285
|
+
pub(crate) fn new(cache: RepoObjectCache, source: &Store, limit: usize) -> Self {
|
|
286
|
+
Self::with_pool(
|
|
287
|
+
cache,
|
|
288
|
+
source,
|
|
289
|
+
limit,
|
|
290
|
+
BACKFILLS.get_or_init(Backfills::start),
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
impl<'a> BackfillBatch<'a> {
|
|
296
|
+
fn with_pool(
|
|
297
|
+
cache: RepoObjectCache,
|
|
298
|
+
source: &Store,
|
|
299
|
+
limit: usize,
|
|
300
|
+
pool: &'a Backfills,
|
|
301
|
+
) -> Self {
|
|
302
|
+
Self {
|
|
303
|
+
cache,
|
|
304
|
+
source: source.root().to_path_buf(),
|
|
305
|
+
limit,
|
|
306
|
+
missed: std::sync::Mutex::new(Vec::new()),
|
|
307
|
+
pool,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
fn work(&self, kind: CacheKind, object: &ObjectRef) -> Backfill {
|
|
312
|
+
Backfill {
|
|
313
|
+
cache: self.cache.clone(),
|
|
314
|
+
source: self.source.clone(),
|
|
315
|
+
kind,
|
|
316
|
+
object: object.clone(),
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
pub(crate) fn offer(&self, kind: CacheKind, object: &ObjectRef) {
|
|
321
|
+
if !self.pool.enqueue(self.work(kind, object), false) {
|
|
322
|
+
let mut missed = self.missed.lock().unwrap();
|
|
323
|
+
if missed.len() < self.limit {
|
|
324
|
+
missed.push((kind, object.clone()));
|
|
325
|
+
} else {
|
|
326
|
+
self.pool
|
|
327
|
+
.activity
|
|
328
|
+
.incomplete
|
|
329
|
+
.store(true, std::sync::atomic::Ordering::Release);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// After local acquisition, feed deferred references while cache copies make
|
|
335
|
+
/// real progress. A stalled cache abandons replication only, never the local
|
|
336
|
+
/// result or its origin workers. The CLI's existing drain reports completion.
|
|
337
|
+
pub(crate) fn finish_offers(&self) -> bool {
|
|
338
|
+
let missed = std::mem::take(&mut *self.missed.lock().unwrap());
|
|
339
|
+
for (kind, object) in missed {
|
|
340
|
+
if !self.pool.enqueue(self.work(kind, &object), true) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
true
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
impl Drop for BackfillBatch<'_> {
|
|
349
|
+
fn drop(&mut self) {
|
|
350
|
+
// A failed pull returns without waiting for optional cache I/O. Its
|
|
351
|
+
// rejected references must still make the later drain report incomplete.
|
|
352
|
+
if !self.missed.get_mut().unwrap().is_empty() {
|
|
353
|
+
self.pool
|
|
354
|
+
.activity
|
|
355
|
+
.incomplete
|
|
356
|
+
.store(true, std::sync::atomic::Ordering::Release);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
299
361
|
// Reference-only memory policy for optional acquisition backfill, not a protocol bound.
|
|
300
362
|
// Typical jobs are a few hundred bytes; even two 4 KiB paths per job stay near 1 MiB.
|
|
301
363
|
const BACKFILL_QUEUE: usize = 128;
|
|
@@ -317,6 +379,7 @@ struct Backfills {
|
|
|
317
379
|
struct BackfillActivity {
|
|
318
380
|
pending: std::sync::Mutex<(usize, Ledger)>,
|
|
319
381
|
changed: std::sync::Condvar,
|
|
382
|
+
incomplete: std::sync::atomic::AtomicBool,
|
|
320
383
|
}
|
|
321
384
|
|
|
322
385
|
impl BackfillActivity {
|
|
@@ -324,9 +387,11 @@ impl BackfillActivity {
|
|
|
324
387
|
Self {
|
|
325
388
|
pending: std::sync::Mutex::new((0, ledger)),
|
|
326
389
|
changed: std::sync::Condvar::new(),
|
|
390
|
+
incomplete: std::sync::atomic::AtomicBool::new(false),
|
|
327
391
|
}
|
|
328
392
|
}
|
|
329
393
|
|
|
394
|
+
#[cfg(test)]
|
|
330
395
|
fn offered(&self) {
|
|
331
396
|
let mut state = self
|
|
332
397
|
.pending
|
|
@@ -338,6 +403,14 @@ impl BackfillActivity {
|
|
|
338
403
|
state.0 += 1;
|
|
339
404
|
}
|
|
340
405
|
|
|
406
|
+
fn space_available(&self) {
|
|
407
|
+
let _state = self
|
|
408
|
+
.pending
|
|
409
|
+
.lock()
|
|
410
|
+
.unwrap_or_else(|error| error.into_inner());
|
|
411
|
+
self.changed.notify_all();
|
|
412
|
+
}
|
|
413
|
+
|
|
341
414
|
fn finished(&self) {
|
|
342
415
|
let mut state = self
|
|
343
416
|
.pending
|
|
@@ -384,12 +457,65 @@ impl store::Progress for BackfillActivity {
|
|
|
384
457
|
/// Before a short-lived CLI exits, give its existing optional write pool time to
|
|
385
458
|
/// finish while real copy/verification progress continues. A stalled cache only
|
|
386
459
|
/// ends this wait; it never changes the completed local fetch or joins its worker.
|
|
387
|
-
///
|
|
460
|
+
/// Write failures and abandoned deferred work also return false, so a drained
|
|
461
|
+
/// queue is not mistaken for successful replication. Long-lived Store callers
|
|
462
|
+
/// keep the original asynchronous behavior.
|
|
388
463
|
pub fn finish_optional_backfills() -> bool {
|
|
389
|
-
BACKFILLS.get().is_none_or(|pool|
|
|
464
|
+
BACKFILLS.get().is_none_or(|pool| {
|
|
465
|
+
let drained = pool.activity.wait();
|
|
466
|
+
let incomplete = pool
|
|
467
|
+
.activity
|
|
468
|
+
.incomplete
|
|
469
|
+
.swap(false, std::sync::atomic::Ordering::AcqRel);
|
|
470
|
+
drained && !incomplete
|
|
471
|
+
})
|
|
390
472
|
}
|
|
391
473
|
|
|
392
474
|
impl Backfills {
|
|
475
|
+
fn enqueue(&self, mut work: Backfill, wait: bool) -> bool {
|
|
476
|
+
let mut state = self
|
|
477
|
+
.activity
|
|
478
|
+
.pending
|
|
479
|
+
.lock()
|
|
480
|
+
.unwrap_or_else(|error| error.into_inner());
|
|
481
|
+
loop {
|
|
482
|
+
if state.0 == 0 {
|
|
483
|
+
state.1 = Ledger::with_resolution(state.1.sample());
|
|
484
|
+
}
|
|
485
|
+
state.0 += 1;
|
|
486
|
+
match self.sender.try_send(work) {
|
|
487
|
+
Ok(()) => return true,
|
|
488
|
+
Err(std::sync::mpsc::TrySendError::Full(returned)) => {
|
|
489
|
+
state.0 -= 1; // rejected offers are not progress
|
|
490
|
+
work = returned;
|
|
491
|
+
if !wait {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
let patience = state.1.stillness().saturating_sub(state.1.still().silent);
|
|
495
|
+
if patience.is_zero() {
|
|
496
|
+
self.activity
|
|
497
|
+
.incomplete
|
|
498
|
+
.store(true, std::sync::atomic::Ordering::Release);
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
state = self
|
|
502
|
+
.activity
|
|
503
|
+
.changed
|
|
504
|
+
.wait_timeout(state, patience)
|
|
505
|
+
.unwrap_or_else(|error| error.into_inner())
|
|
506
|
+
.0;
|
|
507
|
+
}
|
|
508
|
+
Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {
|
|
509
|
+
state.0 -= 1;
|
|
510
|
+
self.activity
|
|
511
|
+
.incomplete
|
|
512
|
+
.store(true, std::sync::atomic::Ordering::Release);
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
393
519
|
fn start() -> Self {
|
|
394
520
|
let (sender, receiver) = std::sync::mpsc::sync_channel::<Backfill>(BACKFILL_QUEUE);
|
|
395
521
|
let receiver = std::sync::Arc::new(std::sync::Mutex::new(receiver));
|
|
@@ -410,25 +536,37 @@ impl Backfills {
|
|
|
410
536
|
Err(_) => return,
|
|
411
537
|
}
|
|
412
538
|
};
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
// Once opened, the verified descriptor pins that inode through the copy.
|
|
416
|
-
if let Ok(store) = Store::open(&work.source) {
|
|
417
|
-
let store = store.observed(activity.clone());
|
|
418
|
-
let _ = work.cache.backfill_observed(
|
|
419
|
-
&store,
|
|
420
|
-
work.kind,
|
|
421
|
-
&work.object,
|
|
422
|
-
Some(activity.as_ref()),
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
activity.finished();
|
|
539
|
+
activity.space_available();
|
|
540
|
+
work.copy(&activity);
|
|
426
541
|
});
|
|
427
542
|
}
|
|
428
543
|
Self { sender, activity }
|
|
429
544
|
}
|
|
430
545
|
}
|
|
431
546
|
|
|
547
|
+
impl Backfill {
|
|
548
|
+
fn copy(self, activity: &std::sync::Arc<BackfillActivity>) {
|
|
549
|
+
// Reopening drops the caller's progress observer. The verified source
|
|
550
|
+
// descriptor pins its inode if GC unlinks it during the copy.
|
|
551
|
+
let copied = Store::open(&self.source).ok().and_then(|store| {
|
|
552
|
+
self.cache
|
|
553
|
+
.backfill_observed(
|
|
554
|
+
&store.observed(activity.clone()),
|
|
555
|
+
self.kind,
|
|
556
|
+
&self.object,
|
|
557
|
+
Some(activity.as_ref()),
|
|
558
|
+
)
|
|
559
|
+
.ok()
|
|
560
|
+
});
|
|
561
|
+
if !matches!(copied, Some(CacheWrite::Stored | CacheWrite::Present)) {
|
|
562
|
+
activity
|
|
563
|
+
.incomplete
|
|
564
|
+
.store(true, std::sync::atomic::Ordering::Release);
|
|
565
|
+
}
|
|
566
|
+
activity.finished();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
432
570
|
fn read_status(error: SoftFailure) -> CacheRead {
|
|
433
571
|
match error {
|
|
434
572
|
SoftFailure::Missing => CacheRead::Missing,
|
|
@@ -1470,3 +1608,195 @@ mod repair_tests {
|
|
|
1470
1608
|
fs::remove_dir_all(root).unwrap();
|
|
1471
1609
|
}
|
|
1472
1610
|
}
|
|
1611
|
+
|
|
1612
|
+
#[cfg(test)]
|
|
1613
|
+
mod acquisition_backfill_tests {
|
|
1614
|
+
use super::*;
|
|
1615
|
+
use std::sync::{atomic::Ordering, Arc, Mutex};
|
|
1616
|
+
|
|
1617
|
+
#[test]
|
|
1618
|
+
fn an_abandoned_saturated_batch_reports_incomplete_after_accepted_work_drains() {
|
|
1619
|
+
let root = std::env::temp_dir().join(format!(
|
|
1620
|
+
"tfs-backfill-abandoned-{}",
|
|
1621
|
+
store::now_nanos_unique()
|
|
1622
|
+
));
|
|
1623
|
+
let source = Store::init(&root.join("source")).unwrap();
|
|
1624
|
+
let bytes = b"local success survives another object's refusal";
|
|
1625
|
+
let object = ObjectRef::of(bytes);
|
|
1626
|
+
source
|
|
1627
|
+
.put_stream(&mut bytes.as_slice(), Some(&object), &Fault::default())
|
|
1628
|
+
.unwrap();
|
|
1629
|
+
let other_bytes = b"verified bytes whose cache offer is rejected";
|
|
1630
|
+
let other = ObjectRef::of(other_bytes);
|
|
1631
|
+
source
|
|
1632
|
+
.put_stream(&mut other_bytes.as_slice(), Some(&other), &Fault::default())
|
|
1633
|
+
.unwrap();
|
|
1634
|
+
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
|
|
1635
|
+
let activity = Arc::new(BackfillActivity::new(Ledger::new()));
|
|
1636
|
+
let pool = Backfills {
|
|
1637
|
+
sender,
|
|
1638
|
+
activity: activity.clone(),
|
|
1639
|
+
};
|
|
1640
|
+
{
|
|
1641
|
+
let batch = BackfillBatch::with_pool(
|
|
1642
|
+
RepoObjectCache::new(root.join("cache")),
|
|
1643
|
+
&source,
|
|
1644
|
+
2,
|
|
1645
|
+
&pool,
|
|
1646
|
+
);
|
|
1647
|
+
batch.offer(CacheKind::Blob, &object);
|
|
1648
|
+
batch.offer(CacheKind::Blob, &other);
|
|
1649
|
+
assert_eq!(batch.missed.lock().unwrap().len(), 1);
|
|
1650
|
+
// This is the early-refusal path: no cache wait or finish_offers.
|
|
1651
|
+
}
|
|
1652
|
+
receiver.recv().unwrap().copy(&activity);
|
|
1653
|
+
assert!(activity.wait());
|
|
1654
|
+
assert!(activity.incomplete.load(Ordering::Acquire));
|
|
1655
|
+
assert!(source.open_verified(&object.sha256).is_ok());
|
|
1656
|
+
assert!(source.open_verified(&other.sha256).is_ok());
|
|
1657
|
+
assert!(!RepoObjectCache::new(root.join("cache"))
|
|
1658
|
+
.path(CacheKind::Blob, &other.sha256)
|
|
1659
|
+
.unwrap()
|
|
1660
|
+
.exists());
|
|
1661
|
+
fs::remove_dir_all(root).unwrap();
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
#[test]
|
|
1665
|
+
fn saturated_acquisition_backfill_reoffers_every_declared_object() {
|
|
1666
|
+
for repair in [false, true] {
|
|
1667
|
+
let root = std::env::temp_dir().join(format!(
|
|
1668
|
+
"tfs-backfill-saturation-{repair}-{}",
|
|
1669
|
+
store::now_nanos_unique()
|
|
1670
|
+
));
|
|
1671
|
+
let source = Store::init(&root.join("source")).unwrap();
|
|
1672
|
+
let cache = RepoObjectCache::new(root.join("cache"));
|
|
1673
|
+
let objects: Vec<ObjectRef> = (0..BACKFILL_QUEUE + 8)
|
|
1674
|
+
.map(|index| {
|
|
1675
|
+
let bytes = vec![index as u8; 1024];
|
|
1676
|
+
let object = ObjectRef::of(&bytes);
|
|
1677
|
+
source
|
|
1678
|
+
.put_stream(&mut bytes.as_slice(), Some(&object), &Fault::default())
|
|
1679
|
+
.unwrap();
|
|
1680
|
+
object
|
|
1681
|
+
})
|
|
1682
|
+
.collect();
|
|
1683
|
+
let (sender, receiver) = std::sync::mpsc::sync_channel(BACKFILL_QUEUE);
|
|
1684
|
+
let activity = Arc::new(BackfillActivity::new(Ledger::new()));
|
|
1685
|
+
let pool = Backfills {
|
|
1686
|
+
sender,
|
|
1687
|
+
activity: activity.clone(),
|
|
1688
|
+
};
|
|
1689
|
+
let batch = BackfillBatch::with_pool(cache.clone(), &source, objects.len(), &pool);
|
|
1690
|
+
// No cache worker starts yet: queue saturation is causal, independent
|
|
1691
|
+
// of filesystem throughput or how the test runner schedules threads.
|
|
1692
|
+
for object in &objects {
|
|
1693
|
+
batch.offer(CacheKind::Blob, object);
|
|
1694
|
+
}
|
|
1695
|
+
assert_eq!(batch.missed.lock().unwrap().len(), 8);
|
|
1696
|
+
let receiver = Arc::new(Mutex::new(receiver));
|
|
1697
|
+
let workers: Vec<_> = (0..STREAMS)
|
|
1698
|
+
.map(|_| {
|
|
1699
|
+
let receiver = receiver.clone();
|
|
1700
|
+
let activity = activity.clone();
|
|
1701
|
+
std::thread::spawn(move || loop {
|
|
1702
|
+
let work = { receiver.lock().unwrap().recv() };
|
|
1703
|
+
let Ok(work) = work else { return };
|
|
1704
|
+
activity.space_available();
|
|
1705
|
+
work.copy(&activity);
|
|
1706
|
+
})
|
|
1707
|
+
})
|
|
1708
|
+
.collect();
|
|
1709
|
+
if repair {
|
|
1710
|
+
assert!(batch.finish_offers());
|
|
1711
|
+
}
|
|
1712
|
+
assert!(activity.wait());
|
|
1713
|
+
assert!(!activity.incomplete.load(Ordering::Acquire));
|
|
1714
|
+
drop(batch);
|
|
1715
|
+
drop(pool);
|
|
1716
|
+
for worker in workers {
|
|
1717
|
+
worker.join().unwrap();
|
|
1718
|
+
}
|
|
1719
|
+
// Destroy the first Store: replacement admission has only the cache.
|
|
1720
|
+
drop(source);
|
|
1721
|
+
fs::remove_dir_all(root.join("source")).unwrap();
|
|
1722
|
+
let replacement = Store::init(&root.join("replacement")).unwrap();
|
|
1723
|
+
let hits = objects
|
|
1724
|
+
.iter()
|
|
1725
|
+
.filter(|object| {
|
|
1726
|
+
cache.admit(&replacement, CacheKind::Blob, object).unwrap() == CacheRead::Hit
|
|
1727
|
+
})
|
|
1728
|
+
.count();
|
|
1729
|
+
assert_eq!(
|
|
1730
|
+
hits,
|
|
1731
|
+
if repair {
|
|
1732
|
+
objects.len()
|
|
1733
|
+
} else {
|
|
1734
|
+
BACKFILL_QUEUE
|
|
1735
|
+
}
|
|
1736
|
+
);
|
|
1737
|
+
if repair {
|
|
1738
|
+
for object in &objects {
|
|
1739
|
+
assert_eq!(
|
|
1740
|
+
replacement.open_verified(&object.sha256).unwrap().len(),
|
|
1741
|
+
object.length
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
fs::remove_dir_all(root).unwrap();
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
#[test]
|
|
1750
|
+
fn cache_failure_and_nonadvancing_full_queue_are_explicitly_incomplete() {
|
|
1751
|
+
let root = std::env::temp_dir().join(format!(
|
|
1752
|
+
"tfs-backfill-incomplete-{}",
|
|
1753
|
+
store::now_nanos_unique()
|
|
1754
|
+
));
|
|
1755
|
+
let source = Store::init(&root.join("source")).unwrap();
|
|
1756
|
+
let bytes = b"verified local bytes remain usable";
|
|
1757
|
+
let object = ObjectRef::of(bytes);
|
|
1758
|
+
source
|
|
1759
|
+
.put_stream(&mut bytes.as_slice(), Some(&object), &Fault::default())
|
|
1760
|
+
.unwrap();
|
|
1761
|
+
fs::write(root.join("not-a-directory"), b"unavailable cache").unwrap();
|
|
1762
|
+
let activity = Arc::new(BackfillActivity::new(Ledger::new()));
|
|
1763
|
+
activity.offered();
|
|
1764
|
+
Backfill {
|
|
1765
|
+
cache: RepoObjectCache::new(root.join("not-a-directory")),
|
|
1766
|
+
source: source.root().to_owned(),
|
|
1767
|
+
kind: CacheKind::Blob,
|
|
1768
|
+
object: object.clone(),
|
|
1769
|
+
}
|
|
1770
|
+
.copy(&activity);
|
|
1771
|
+
assert!(activity.wait());
|
|
1772
|
+
assert!(activity.incomplete.load(Ordering::Acquire));
|
|
1773
|
+
assert!(source.open_verified(&object.sha256).is_ok());
|
|
1774
|
+
|
|
1775
|
+
let (sender, _blocked_receiver) = std::sync::mpsc::sync_channel(1);
|
|
1776
|
+
let activity = Arc::new(BackfillActivity::new(Ledger::new()));
|
|
1777
|
+
let pool = Backfills {
|
|
1778
|
+
sender,
|
|
1779
|
+
activity: activity.clone(),
|
|
1780
|
+
};
|
|
1781
|
+
let batch =
|
|
1782
|
+
BackfillBatch::with_pool(RepoObjectCache::new(root.join("cache")), &source, 2, &pool);
|
|
1783
|
+
batch.offer(CacheKind::Blob, &object);
|
|
1784
|
+
batch.offer(CacheKind::Blob, &object);
|
|
1785
|
+
let start = std::time::Instant::now();
|
|
1786
|
+
let elapsed = Arc::new(std::sync::atomic::AtomicU64::new(0));
|
|
1787
|
+
let clock = elapsed.clone();
|
|
1788
|
+
activity.pending.lock().unwrap().1 =
|
|
1789
|
+
Ledger::with_clock(std::time::Duration::from_millis(10), move || {
|
|
1790
|
+
start + std::time::Duration::from_millis(clock.load(Ordering::Acquire))
|
|
1791
|
+
});
|
|
1792
|
+
elapsed.store(1000, Ordering::Release);
|
|
1793
|
+
assert!(
|
|
1794
|
+
!batch.finish_offers(),
|
|
1795
|
+
"retrying a full queue must not manufacture cache progress"
|
|
1796
|
+
);
|
|
1797
|
+
assert!(activity.incomplete.load(Ordering::Acquire));
|
|
1798
|
+
assert_eq!(activity.pending.lock().unwrap().0, 1);
|
|
1799
|
+
assert!(source.open_verified(&object.sha256).is_ok());
|
|
1800
|
+
fs::remove_dir_all(root).unwrap();
|
|
1801
|
+
}
|
|
1802
|
+
}
|