tensorfs 0.3.30__tar.gz → 0.3.32__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.30 → tensorfs-0.3.32}/Cargo.lock +2 -2
- {tensorfs-0.3.30 → tensorfs-0.3.32}/Cargo.toml +1 -1
- {tensorfs-0.3.30 → tensorfs-0.3.32}/PKG-INFO +1 -1
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/fetch.rs +3 -0
- tensorfs-0.3.32/crates/tensorfs-core/src/cache_roots.rs +382 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived/graft_tests.rs +47 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/gc.rs +75 -3
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/lib.rs +1 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/reclaim.rs +10 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/repo_cache.rs +244 -16
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/store.rs +57 -1
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/pull.rs +2 -2
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/tests.rs +40 -6
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-py/src/store.rs +20 -3
- {tensorfs-0.3.30 → tensorfs-0.3.32}/pyproject.toml +1 -1
- {tensorfs-0.3.30 → tensorfs-0.3.32}/python/tensorfs/_ext.pyi +1 -1
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/Cargo.toml +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/build.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/b64.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/canon.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/capability.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/catalog.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/cbor.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/checkpoint.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/corpus.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived/progress.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived/retention.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived/roots.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/dtype.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/durability.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/err.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/fetch.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/fill.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/fit.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/header.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/home.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ids.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/source.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/jcs.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/limits.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/machine.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/manifest.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/meta.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/project.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/providers.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/read.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/receipt.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/registry.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/relation.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/repository.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/sha256.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/source_artifact.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/spec.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/staging.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/storage.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/api.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/http.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/hub.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/mod.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/policy.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/push.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/vectors.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fit.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/home.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/import_tree.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/liveness.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/read_plan.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/reclaim.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/repo_store.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/source_plan.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/staging.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/version.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/write_through.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-py/Cargo.toml +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-py/src/docs.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-py/src/errors.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-py/src/lib.rs +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/python/tensorfs/__init__.py +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/python/tensorfs/errors.py +0 -0
- {tensorfs-0.3.30 → tensorfs-0.3.32}/python/tensorfs/py.typed +0 -0
|
@@ -392,7 +392,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
|
392
392
|
|
|
393
393
|
[[package]]
|
|
394
394
|
name = "tensorfs-core"
|
|
395
|
-
version = "0.3.
|
|
395
|
+
version = "0.3.32"
|
|
396
396
|
dependencies = [
|
|
397
397
|
"fs2",
|
|
398
398
|
"minicbor",
|
|
@@ -404,7 +404,7 @@ dependencies = [
|
|
|
404
404
|
|
|
405
405
|
[[package]]
|
|
406
406
|
name = "tensorfs-py"
|
|
407
|
-
version = "0.3.
|
|
407
|
+
version = "0.3.32"
|
|
408
408
|
dependencies = [
|
|
409
409
|
"pyo3",
|
|
410
410
|
"tensorfs-core",
|
|
@@ -302,6 +302,9 @@ fn pull_with_budget(
|
|
|
302
302
|
request.on_object = Some(&observe);
|
|
303
303
|
let report = ok!(transport::pull(&request));
|
|
304
304
|
progress.settle();
|
|
305
|
+
if !tensorfs_core::repo_cache::finish_optional_backfills() {
|
|
306
|
+
eprintln!("repo cache backfill stopped advancing; local fetch remains complete");
|
|
307
|
+
}
|
|
305
308
|
println!(
|
|
306
309
|
"model {}@{} lane {}",
|
|
307
310
|
report.model, report.release, report.lane
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
//! Local provenance, not reachability: an exact repository body written by Hub transport.
|
|
2
|
+
//! Missing/mismatched receipts preserve roots. GC still owns every object decision.
|
|
3
|
+
|
|
4
|
+
use std::fs::{self, OpenOptions};
|
|
5
|
+
use std::io::Write;
|
|
6
|
+
use std::path::PathBuf;
|
|
7
|
+
|
|
8
|
+
use crate::err::{Code, Refusal, Result};
|
|
9
|
+
use crate::repository::RepositoryName;
|
|
10
|
+
use crate::store::Store;
|
|
11
|
+
|
|
12
|
+
fn io(error: std::io::Error) -> Refusal {
|
|
13
|
+
Refusal {
|
|
14
|
+
code: Code::IO_FAILED,
|
|
15
|
+
detail: format!("cache root provenance: {error}"),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fn path(store: &Store, repo: &RepositoryName) -> PathBuf {
|
|
20
|
+
store
|
|
21
|
+
.root()
|
|
22
|
+
.join("tmp/cache-roots")
|
|
23
|
+
.join(&repo.org)
|
|
24
|
+
.join(&repo.name)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub(crate) fn matches(store: &Store, repo: &RepositoryName, body: &[u8]) -> Result<bool> {
|
|
28
|
+
let path = path(store, repo);
|
|
29
|
+
match fs::symlink_metadata(&path) {
|
|
30
|
+
Ok(meta) if meta.file_type().is_file() && meta.len() == 64 => {}
|
|
31
|
+
Ok(_) => return Ok(false),
|
|
32
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
|
|
33
|
+
Err(error) => return Err(io(error)),
|
|
34
|
+
}
|
|
35
|
+
Ok(fs::read(path).map_err(io)? == crate::sha256::hex_digest(body).as_bytes())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Both writers are called inside the existing repository CAS and native writer fence.
|
|
39
|
+
pub(crate) fn clear(store: &Store, repo: &RepositoryName) -> Result<()> {
|
|
40
|
+
let path = path(store, repo);
|
|
41
|
+
match fs::remove_file(&path) {
|
|
42
|
+
Ok(()) => crate::store::fsync_dir(path.parent().unwrap()),
|
|
43
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
|
44
|
+
Err(error) => Err(io(error)),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub(crate) fn record(store: &Store, repo: &RepositoryName, body: &[u8]) -> Result<()> {
|
|
49
|
+
let path = path(store, repo);
|
|
50
|
+
let parent = path.parent().unwrap();
|
|
51
|
+
fs::create_dir_all(parent).map_err(io)?;
|
|
52
|
+
let temporary = parent.join(format!(
|
|
53
|
+
".cache-root-{}-{}",
|
|
54
|
+
repo.name,
|
|
55
|
+
crate::meta::now_nanos_unique()
|
|
56
|
+
));
|
|
57
|
+
let result = (|| {
|
|
58
|
+
let mut file = OpenOptions::new()
|
|
59
|
+
.write(true)
|
|
60
|
+
.create_new(true)
|
|
61
|
+
.open(&temporary)
|
|
62
|
+
.map_err(io)?;
|
|
63
|
+
file.write_all(crate::sha256::hex_digest(body).as_bytes())
|
|
64
|
+
.map_err(io)?;
|
|
65
|
+
file.sync_all().map_err(io)?;
|
|
66
|
+
fs::rename(&temporary, &path).map_err(io)?;
|
|
67
|
+
crate::store::fsync_dir(parent)
|
|
68
|
+
})();
|
|
69
|
+
let _ = fs::remove_file(temporary);
|
|
70
|
+
result
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Called only under the native exclusive recovery lock. These small provenance facts
|
|
74
|
+
// have no lifetime beyond their exact repository; unknown repo bytes are not modified.
|
|
75
|
+
pub(crate) fn reap(store: &Store, limit: usize) -> Result<u64> {
|
|
76
|
+
let base = store.root().join("tmp/cache-roots");
|
|
77
|
+
let organizations = match fs::read_dir(&base) {
|
|
78
|
+
Ok(entries) => entries,
|
|
79
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(0),
|
|
80
|
+
Err(error) => return Err(io(error)),
|
|
81
|
+
};
|
|
82
|
+
let (mut visited, mut removed) = (0, 0);
|
|
83
|
+
for organization in organizations.take(limit) {
|
|
84
|
+
let organization = organization.map_err(io)?;
|
|
85
|
+
if !organization.file_type().map_err(io)?.is_dir() {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
let directory = organization.path();
|
|
89
|
+
for entry in fs::read_dir(&directory).map_err(io)? {
|
|
90
|
+
if visited >= limit {
|
|
91
|
+
return Ok(removed);
|
|
92
|
+
}
|
|
93
|
+
visited += 1;
|
|
94
|
+
let entry = entry.map_err(io)?;
|
|
95
|
+
if !entry.file_type().map_err(io)?.is_file() {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
let name = entry.file_name();
|
|
99
|
+
let name = name.to_string_lossy();
|
|
100
|
+
// Our interrupted atomic-write temp; no native writer is live at this point.
|
|
101
|
+
let stale = if name.starts_with(".cache-root-") {
|
|
102
|
+
true
|
|
103
|
+
} else if let Ok(repo) =
|
|
104
|
+
RepositoryName::new(organization.file_name().to_string_lossy(), name.as_ref())
|
|
105
|
+
{
|
|
106
|
+
match fs::read(store.repository_path(&repo)) {
|
|
107
|
+
Ok(body) => !matches(store, &repo, &body)?,
|
|
108
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => true,
|
|
109
|
+
Err(error) => return Err(io(error)),
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
false
|
|
113
|
+
};
|
|
114
|
+
if stale {
|
|
115
|
+
fs::remove_file(entry.path()).map_err(io)?;
|
|
116
|
+
removed += 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
crate::store::fsync_dir(&directory)?;
|
|
120
|
+
if fs::remove_dir(&directory).is_ok() {
|
|
121
|
+
crate::store::fsync_dir(&base)?;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
Ok(removed)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[cfg(test)]
|
|
128
|
+
mod tests {
|
|
129
|
+
use super::*;
|
|
130
|
+
use crate::catalog::WriterGuard;
|
|
131
|
+
use crate::ids::{Doc, ObjectRef};
|
|
132
|
+
use crate::manifest::{Draft, Entry};
|
|
133
|
+
use crate::repository::{Mutation, Repository};
|
|
134
|
+
use crate::store::Fault;
|
|
135
|
+
|
|
136
|
+
struct Fixture {
|
|
137
|
+
store: Store,
|
|
138
|
+
}
|
|
139
|
+
impl Fixture {
|
|
140
|
+
fn new() -> Self {
|
|
141
|
+
let path = std::env::temp_dir().join(format!(
|
|
142
|
+
"tensorfs-cache-root-{}-{}",
|
|
143
|
+
std::process::id(),
|
|
144
|
+
crate::meta::now_nanos_unique()
|
|
145
|
+
));
|
|
146
|
+
Self {
|
|
147
|
+
store: Store::init(&path).unwrap(),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
fn model(&self, name: &str, data: &[u8], cached: bool) -> (RepositoryName, ObjectRef) {
|
|
151
|
+
let blob = self
|
|
152
|
+
.store
|
|
153
|
+
.put_stream(&mut &data[..], None, &Fault::default())
|
|
154
|
+
.unwrap()
|
|
155
|
+
.obj;
|
|
156
|
+
let manifest = Draft {
|
|
157
|
+
entries: vec![("weights".into(), Entry::File(blob))],
|
|
158
|
+
}
|
|
159
|
+
.seal()
|
|
160
|
+
.unwrap();
|
|
161
|
+
let manifest = self.store.put_manifest(&manifest).unwrap().obj;
|
|
162
|
+
let repo = RepositoryName::new("org", name).unwrap();
|
|
163
|
+
let mutation = Mutation::PutCheckpoint {
|
|
164
|
+
repo: repo.clone(),
|
|
165
|
+
manifest: manifest.clone(),
|
|
166
|
+
};
|
|
167
|
+
if cached {
|
|
168
|
+
self.store
|
|
169
|
+
.apply_cached_repository(None, &mutation, &Fault::default())
|
|
170
|
+
.unwrap();
|
|
171
|
+
} else {
|
|
172
|
+
self.store
|
|
173
|
+
.apply_repository(None, &mutation, &Fault::default())
|
|
174
|
+
.unwrap();
|
|
175
|
+
}
|
|
176
|
+
(repo, manifest)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
impl Drop for Fixture {
|
|
180
|
+
fn drop(&mut self) {
|
|
181
|
+
let _ = fs::remove_dir_all(self.store.root());
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[test]
|
|
186
|
+
fn cached_pressure_keeps_owned_shared_and_protected_roots() {
|
|
187
|
+
let f = Fixture::new();
|
|
188
|
+
let (user, user_manifest) = f.model("user", b"shared", false);
|
|
189
|
+
let (cached_shared, _) = f.model("shared", b"shared", true);
|
|
190
|
+
let (active, active_manifest) = f.model("active", b"keep active", true);
|
|
191
|
+
let (victim, victim_manifest) = f.model("victim", b"discard me", true);
|
|
192
|
+
let report = crate::gc::collect_cached(f.store.root(), &[active_manifest.sha256]).unwrap();
|
|
193
|
+
assert!(report.reclaimed_bytes > 0);
|
|
194
|
+
assert!(f.store.repository_path(&user).exists());
|
|
195
|
+
assert!(f.store.repository_path(&cached_shared).exists());
|
|
196
|
+
assert!(f.store.repository_path(&active).exists());
|
|
197
|
+
assert!(!f.store.repository_path(&victim).exists());
|
|
198
|
+
assert!(f.store.read_manifest(&user_manifest).is_ok());
|
|
199
|
+
assert!(f.store.read_manifest(&victim_manifest).is_err());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[test]
|
|
203
|
+
fn ordinary_and_mixed_repository_mutations_revoke_cache_ownership() {
|
|
204
|
+
let f = Fixture::new();
|
|
205
|
+
for cached in [true, false] {
|
|
206
|
+
let (repo, manifest) = f.model(if cached { "cached" } else { "user" }, b"keep", cached);
|
|
207
|
+
let body = fs::read(f.store.repository_path(&repo)).unwrap();
|
|
208
|
+
let mutation = Mutation::PutCheckpoint {
|
|
209
|
+
repo: repo.clone(),
|
|
210
|
+
manifest,
|
|
211
|
+
};
|
|
212
|
+
// Even an ordinary no-op explicitly adopts that local root.
|
|
213
|
+
f.store
|
|
214
|
+
.apply_repository(Some(&body), &mutation, &Fault::default())
|
|
215
|
+
.unwrap();
|
|
216
|
+
f.store
|
|
217
|
+
.apply_cached_repository(Some(&body), &mutation, &Fault::default())
|
|
218
|
+
.unwrap();
|
|
219
|
+
assert!(!matches(&f.store, &repo, &body).unwrap());
|
|
220
|
+
}
|
|
221
|
+
assert_eq!(
|
|
222
|
+
crate::gc::collect_cached(f.store.root(), &[])
|
|
223
|
+
.unwrap()
|
|
224
|
+
.reclaimed_bytes,
|
|
225
|
+
0
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#[test]
|
|
230
|
+
fn missing_mismatched_and_interrupted_receipts_never_authorize_eviction() {
|
|
231
|
+
let f = Fixture::new();
|
|
232
|
+
for name in ["missing", "mismatch", "interrupted"] {
|
|
233
|
+
let (repo, _) = f.model(name, name.as_bytes(), true);
|
|
234
|
+
if name == "missing" {
|
|
235
|
+
clear(&f.store, &repo).unwrap();
|
|
236
|
+
} else {
|
|
237
|
+
fs::write(
|
|
238
|
+
path(&f.store, &repo),
|
|
239
|
+
if name == "mismatch" {
|
|
240
|
+
vec![b'0'; 64]
|
|
241
|
+
} else {
|
|
242
|
+
vec![b'0'; 3]
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
.unwrap();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
assert_eq!(
|
|
249
|
+
crate::gc::collect_cached(f.store.root(), &[])
|
|
250
|
+
.unwrap()
|
|
251
|
+
.reclaimed_bytes,
|
|
252
|
+
0
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[test]
|
|
257
|
+
fn live_writer_refuses_before_root_removal_and_later_reclaims() {
|
|
258
|
+
let f = Fixture::new();
|
|
259
|
+
let (repo, _) = f.model("writer", b"writer held", true);
|
|
260
|
+
let guard = WriterGuard::acquire(f.store.root()).unwrap();
|
|
261
|
+
assert_eq!(
|
|
262
|
+
crate::gc::collect_cached(f.store.root(), &[])
|
|
263
|
+
.unwrap_err()
|
|
264
|
+
.code,
|
|
265
|
+
Code::STORE_BUSY
|
|
266
|
+
);
|
|
267
|
+
assert!(f.store.repository_path(&repo).exists());
|
|
268
|
+
drop(guard);
|
|
269
|
+
assert!(
|
|
270
|
+
crate::gc::collect_cached(f.store.root(), &[])
|
|
271
|
+
.unwrap()
|
|
272
|
+
.reclaimed_bytes
|
|
273
|
+
> 0
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#[test]
|
|
278
|
+
fn native_exclusive_fence_blocks_activation_and_preserves_generation() {
|
|
279
|
+
let f = Fixture::new();
|
|
280
|
+
let (repo, _) = f.model("race", b"race", true);
|
|
281
|
+
let body = fs::read(f.store.repository_path(&repo)).unwrap();
|
|
282
|
+
let exclusive = WriterGuard::lock_rebuild(f.store.root()).unwrap();
|
|
283
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
284
|
+
let root = f.store.root().to_path_buf();
|
|
285
|
+
let reader = std::thread::spawn(move || {
|
|
286
|
+
let guard = WriterGuard::acquire(&root).unwrap();
|
|
287
|
+
tx.send(()).unwrap();
|
|
288
|
+
drop(guard);
|
|
289
|
+
});
|
|
290
|
+
assert!(rx
|
|
291
|
+
.recv_timeout(std::time::Duration::from_millis(50))
|
|
292
|
+
.is_err());
|
|
293
|
+
f.store
|
|
294
|
+
.remove_cached_repository(&body, &repo, &exclusive)
|
|
295
|
+
.unwrap();
|
|
296
|
+
crate::gc::collect_locked(&f.store, false, &exclusive).unwrap();
|
|
297
|
+
drop(exclusive);
|
|
298
|
+
rx.recv_timeout(std::time::Duration::from_secs(5)).unwrap();
|
|
299
|
+
reader.join().unwrap();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[test]
|
|
303
|
+
fn transport_replacement_cannot_mark_a_preexisting_mixed_repository() {
|
|
304
|
+
let f = Fixture::new();
|
|
305
|
+
let (repo, manifest) = f.model("mixed", b"author bytes", false);
|
|
306
|
+
let body = fs::read(f.store.repository_path(&repo)).unwrap();
|
|
307
|
+
let parsed = Repository::parse(&body).unwrap();
|
|
308
|
+
assert_eq!(parsed.checkpoints[0].manifest, manifest);
|
|
309
|
+
f.store
|
|
310
|
+
.apply_cached_repository(
|
|
311
|
+
Some(&body),
|
|
312
|
+
&Mutation::UpdateRelease {
|
|
313
|
+
repo: repo.clone(),
|
|
314
|
+
version: "1".into(),
|
|
315
|
+
expected_revision: 0,
|
|
316
|
+
remove: vec![],
|
|
317
|
+
set: vec![crate::repository::ReleaseLane {
|
|
318
|
+
lane: "bf16".into(),
|
|
319
|
+
manifest,
|
|
320
|
+
}],
|
|
321
|
+
},
|
|
322
|
+
&Fault::default(),
|
|
323
|
+
)
|
|
324
|
+
.unwrap();
|
|
325
|
+
let body = fs::read(f.store.repository_path(&repo)).unwrap();
|
|
326
|
+
assert!(!matches(&f.store, &repo, &body).unwrap());
|
|
327
|
+
assert_eq!(
|
|
328
|
+
crate::gc::collect_cached(f.store.root(), &[])
|
|
329
|
+
.unwrap()
|
|
330
|
+
.reclaimed_bytes,
|
|
331
|
+
0
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
#[test]
|
|
335
|
+
fn retained_ingest_candidate_and_live_native_reader_keep_the_root() {
|
|
336
|
+
let f = Fixture::new();
|
|
337
|
+
let (repo, manifest) = f.model("retained", b"sole retained bytes", true);
|
|
338
|
+
let meta = crate::meta::Meta::open(&f.store).unwrap();
|
|
339
|
+
let lease = meta.acquire_hold("read").unwrap();
|
|
340
|
+
assert_eq!(
|
|
341
|
+
crate::gc::collect_cached(f.store.root(), &[])
|
|
342
|
+
.unwrap_err()
|
|
343
|
+
.code,
|
|
344
|
+
Code::STORE_BUSY
|
|
345
|
+
);
|
|
346
|
+
assert!(f.store.repository_path(&repo).exists());
|
|
347
|
+
lease.release(&meta).unwrap();
|
|
348
|
+
let (mut session, writer) =
|
|
349
|
+
crate::ingest::transaction::open_root(&f.store, "retained-source", "owner").unwrap();
|
|
350
|
+
session.candidates.push(manifest.clone());
|
|
351
|
+
crate::ingest::transaction::write_session(f.store.root(), &session).unwrap();
|
|
352
|
+
drop(writer); // Paused work remains retained even after its process's live lock ends.
|
|
353
|
+
assert_eq!(
|
|
354
|
+
crate::gc::collect_cached(f.store.root(), &[])
|
|
355
|
+
.unwrap()
|
|
356
|
+
.reclaimed_bytes,
|
|
357
|
+
0
|
|
358
|
+
);
|
|
359
|
+
assert!(f.store.repository_path(&repo).exists());
|
|
360
|
+
assert!(f.store.read_manifest(&manifest).is_ok());
|
|
361
|
+
}
|
|
362
|
+
#[test]
|
|
363
|
+
fn orphan_provenance_cleanup_is_bounded_and_never_removes_a_repository() {
|
|
364
|
+
let f = Fixture::new();
|
|
365
|
+
let mut stale = Vec::new();
|
|
366
|
+
for number in 0..5 {
|
|
367
|
+
let (repo, _) = f.model(&format!("old-{number}"), &[number], true);
|
|
368
|
+
fs::remove_file(f.store.repository_path(&repo)).unwrap();
|
|
369
|
+
stale.push(path(&f.store, &repo));
|
|
370
|
+
}
|
|
371
|
+
let guard = WriterGuard::lock_rebuild(f.store.root()).unwrap();
|
|
372
|
+
assert_eq!(reap(&f.store, 2).unwrap(), 2);
|
|
373
|
+
assert_eq!(stale.iter().filter(|path| path.exists()).count(), 3);
|
|
374
|
+
drop(guard);
|
|
375
|
+
let (live, _) = f.model("live", b"live", true);
|
|
376
|
+
let guard = WriterGuard::lock_rebuild(f.store.root()).unwrap();
|
|
377
|
+
assert_eq!(reap(&f.store, 128).unwrap(), 3);
|
|
378
|
+
assert!(f.store.repository_path(&live).exists());
|
|
379
|
+
assert!(path(&f.store, &live).exists());
|
|
380
|
+
drop(guard);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -210,3 +210,50 @@ fn source_projection_inherits_only_selected_tensors_without_new_bytes() {
|
|
|
210
210
|
release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
|
|
211
211
|
std::fs::remove_dir_all(store.root()).unwrap();
|
|
212
212
|
}
|
|
213
|
+
|
|
214
|
+
#[test]
|
|
215
|
+
fn pressure_keeps_cached_inputs_of_a_paused_derive_until_explicit_abandon() {
|
|
216
|
+
use crate::ids::Doc;
|
|
217
|
+
use crate::repository::{Mutation, RepositoryName};
|
|
218
|
+
let (store, meta, body, bank, declaration) = fixture();
|
|
219
|
+
let mut repos = Vec::new();
|
|
220
|
+
for (name, receipt) in [("body", &body), ("bank", &bank)] {
|
|
221
|
+
let repo = RepositoryName::new("downloaded", name).unwrap();
|
|
222
|
+
let recorded = store
|
|
223
|
+
.apply_cached_repository(
|
|
224
|
+
None,
|
|
225
|
+
&Mutation::PutCheckpoint {
|
|
226
|
+
repo: repo.clone(),
|
|
227
|
+
manifest: receipt.manifest.clone(),
|
|
228
|
+
},
|
|
229
|
+
&Default::default(),
|
|
230
|
+
)
|
|
231
|
+
.unwrap()
|
|
232
|
+
.unwrap();
|
|
233
|
+
assert!(crate::cache_roots::matches(&store, &repo, &recorded.canonical_bytes()).unwrap());
|
|
234
|
+
repos.push(repo);
|
|
235
|
+
}
|
|
236
|
+
let id = format!("sha256:{}", "a4".repeat(32));
|
|
237
|
+
let writer = begin(&store, &meta, &id, 1, declaration, None).unwrap();
|
|
238
|
+
fence(&meta, &id, 1).unwrap();
|
|
239
|
+
release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
|
|
240
|
+
// The producer's original obligations end; only the paused derive still needs inputs.
|
|
241
|
+
dispose(&store, &meta, &body.transaction).unwrap();
|
|
242
|
+
dispose(&store, &meta, &bank.transaction).unwrap();
|
|
243
|
+
let held = crate::gc::collect_cached(store.root(), &[]).unwrap();
|
|
244
|
+
assert_eq!(held.reclaimed_bytes, 0);
|
|
245
|
+
assert!(repos
|
|
246
|
+
.iter()
|
|
247
|
+
.all(|repo| store.repository_path(repo).exists()));
|
|
248
|
+
abandon(&store, &meta, &id).unwrap();
|
|
249
|
+
assert!(
|
|
250
|
+
crate::gc::collect_cached(store.root(), &[])
|
|
251
|
+
.unwrap()
|
|
252
|
+
.reclaimed_bytes
|
|
253
|
+
> 0
|
|
254
|
+
);
|
|
255
|
+
assert!(repos
|
|
256
|
+
.iter()
|
|
257
|
+
.any(|repo| !store.repository_path(repo).exists()));
|
|
258
|
+
std::fs::remove_dir_all(store.root()).unwrap();
|
|
259
|
+
}
|
|
@@ -177,7 +177,16 @@ pub(crate) fn retained_manifests(store: &Store) -> Result<Vec<crate::ids::Object
|
|
|
177
177
|
/// One complete pass: exclusivity, orphan temps, census, plan, delete, catalog repair.
|
|
178
178
|
pub fn collect(root: &Path, dry_run: bool) -> Result<Report> {
|
|
179
179
|
let store = Store::open(root)?;
|
|
180
|
-
let
|
|
180
|
+
let exclusive = WriterGuard::lock_rebuild(root)?;
|
|
181
|
+
collect_locked(&store, dry_run, &exclusive)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
pub(crate) fn collect_locked(
|
|
185
|
+
store: &Store,
|
|
186
|
+
dry_run: bool,
|
|
187
|
+
_exclusive: &crate::catalog::RebuildGuard,
|
|
188
|
+
) -> Result<Report> {
|
|
189
|
+
let root = store.root();
|
|
181
190
|
let mut report = Report {
|
|
182
191
|
dry_run,
|
|
183
192
|
..Report::default()
|
|
@@ -186,11 +195,12 @@ pub fn collect(root: &Path, dry_run: bool) -> Result<Report> {
|
|
|
186
195
|
// Under exclusivity every remaining put- temp and lease marker belongs to a dead
|
|
187
196
|
// process: the kernel released its lock with it.
|
|
188
197
|
report.scratch_reaped = store.reap()?.0 as u64;
|
|
189
|
-
|
|
198
|
+
report.scratch_reaped += crate::cache_roots::reap(store, 128)?;
|
|
199
|
+
if let Ok(meta) = crate::meta::Meta::open(store) {
|
|
190
200
|
report.scratch_reaped += meta.reap_holds()?.len() as u64;
|
|
191
201
|
}
|
|
192
202
|
}
|
|
193
|
-
let (holds, sessions) = session_holds(
|
|
203
|
+
let (holds, sessions) = session_holds(store)?;
|
|
194
204
|
report.sessions = sessions;
|
|
195
205
|
let census = Census::open(root)?;
|
|
196
206
|
let plan = census.gc_plan(&holds)?;
|
|
@@ -258,3 +268,65 @@ fn remove(root: &Path, row: &GcDelete) -> Result<()> {
|
|
|
258
268
|
Err(error) => Err(io(format!("remove {}", row.key), error)),
|
|
259
269
|
}
|
|
260
270
|
}
|
|
271
|
+
|
|
272
|
+
/// One pressure pass on an explicitly managed store: first ordinary garbage, then the
|
|
273
|
+
/// smallest useful cache-root drop chosen by the existing dedup-aware planner. The owner
|
|
274
|
+
/// keeps its placement generation stable through this call. The native lock covers the
|
|
275
|
+
/// entire selection/removal/collection transition, including new readers and downloads.
|
|
276
|
+
/// Old roots without transport provenance are deliberately not migrated or inferred.
|
|
277
|
+
pub fn collect_cached(root: &Path, protected_manifests: &[String]) -> Result<Report> {
|
|
278
|
+
let protected_manifests = protected_manifests
|
|
279
|
+
.iter()
|
|
280
|
+
.map(|digest| {
|
|
281
|
+
crate::ids::hex64(
|
|
282
|
+
"protected manifest",
|
|
283
|
+
digest.strip_prefix("sha256:").unwrap_or(digest),
|
|
284
|
+
)
|
|
285
|
+
})
|
|
286
|
+
.collect::<Result<Vec<_>>>()?;
|
|
287
|
+
let store = Store::open(root)?;
|
|
288
|
+
let exclusive = WriterGuard::lock_rebuild(root)?;
|
|
289
|
+
let collected = collect_locked(&store, false, &exclusive)?;
|
|
290
|
+
if collected.reclaimed_bytes > 0 {
|
|
291
|
+
return Ok(collected);
|
|
292
|
+
}
|
|
293
|
+
let (holds, _) = session_holds(&store)?;
|
|
294
|
+
let mut protected: BTreeSet<&str> = protected_manifests.iter().map(String::as_str).collect();
|
|
295
|
+
protected.extend(
|
|
296
|
+
holds
|
|
297
|
+
.iter()
|
|
298
|
+
.filter(|hold| hold.kind == "manifest")
|
|
299
|
+
.map(|hold| hold.sha256.as_str()),
|
|
300
|
+
);
|
|
301
|
+
let repositories = storage::read_repositories(root)?;
|
|
302
|
+
let mut keep = Vec::new();
|
|
303
|
+
for (repository, body) in &repositories {
|
|
304
|
+
if repository
|
|
305
|
+
.checkpoints
|
|
306
|
+
.iter()
|
|
307
|
+
.any(|checkpoint| protected.contains(checkpoint.manifest.sha256.as_str()))
|
|
308
|
+
|| !crate::cache_roots::matches(&store, &repository.repo, body)?
|
|
309
|
+
{
|
|
310
|
+
keep.push(repository.repo.clone());
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
let plan = match crate::reclaim::plan_managed(&store, 1, &keep, &holds) {
|
|
314
|
+
Ok(plan) => plan,
|
|
315
|
+
Err(error) if error.code == Code::RECLAIM_INSUFFICIENT => return Ok(collected),
|
|
316
|
+
Err(error) => return Err(error),
|
|
317
|
+
};
|
|
318
|
+
// A bounded pass never removes an arbitrarily large jointly-deduplicated set.
|
|
319
|
+
if plan.victims.len() > 16 {
|
|
320
|
+
return Ok(collected);
|
|
321
|
+
}
|
|
322
|
+
for victim in plan.victims {
|
|
323
|
+
let (repository, body) = repositories
|
|
324
|
+
.iter()
|
|
325
|
+
.find(|(repository, _)| {
|
|
326
|
+
repository.repo.org == victim.org && repository.repo.name == victim.name
|
|
327
|
+
})
|
|
328
|
+
.expect("planner only names census repositories");
|
|
329
|
+
store.remove_cached_repository(body, &repository.repo, &exclusive)?;
|
|
330
|
+
}
|
|
331
|
+
collect_locked(&store, false, &exclusive)
|
|
332
|
+
}
|
|
@@ -339,6 +339,16 @@ pub fn usage(
|
|
|
339
339
|
/// rather than returning a plan that empties the Store and still cannot admit the fetch.
|
|
340
340
|
pub fn plan(store: &Store, need: u64, keep: &[RepositoryName], holds: &[HeldKey]) -> Result<Plan> {
|
|
341
341
|
permitted(store)?;
|
|
342
|
+
plan_managed(store, need, keep, holds)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Managed pressure already has an explicit owner opt-in; it shares the same planner.
|
|
346
|
+
pub(crate) fn plan_managed(
|
|
347
|
+
store: &Store,
|
|
348
|
+
need: u64,
|
|
349
|
+
keep: &[RepositoryName],
|
|
350
|
+
holds: &[HeldKey],
|
|
351
|
+
) -> Result<Plan> {
|
|
342
352
|
let census = Census::open(store.root())?;
|
|
343
353
|
let index = census.holder_index(holds)?;
|
|
344
354
|
|