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.
Files changed (127) hide show
  1. {tensorfs-0.3.30 → tensorfs-0.3.32}/Cargo.lock +2 -2
  2. {tensorfs-0.3.30 → tensorfs-0.3.32}/Cargo.toml +1 -1
  3. {tensorfs-0.3.30 → tensorfs-0.3.32}/PKG-INFO +1 -1
  4. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/fetch.rs +3 -0
  5. tensorfs-0.3.32/crates/tensorfs-core/src/cache_roots.rs +382 -0
  6. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived/graft_tests.rs +47 -0
  7. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/gc.rs +75 -3
  8. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/lib.rs +1 -0
  9. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/reclaim.rs +10 -0
  10. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/repo_cache.rs +244 -16
  11. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/store.rs +57 -1
  12. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/pull.rs +2 -2
  13. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/tests.rs +40 -6
  14. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-py/src/store.rs +20 -3
  15. {tensorfs-0.3.30 → tensorfs-0.3.32}/pyproject.toml +1 -1
  16. {tensorfs-0.3.30 → tensorfs-0.3.32}/python/tensorfs/_ext.pyi +1 -1
  17. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/Cargo.toml +0 -0
  18. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/build.rs +0 -0
  19. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  20. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  21. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  22. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  23. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/b64.rs +0 -0
  24. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  25. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  26. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  27. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  28. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  29. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  30. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  31. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  32. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/canon.rs +0 -0
  33. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/capability.rs +0 -0
  34. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/catalog.rs +0 -0
  35. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/cbor.rs +0 -0
  36. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  37. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/corpus.rs +0 -0
  38. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  39. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  40. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  41. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  42. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  43. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/derived.rs +0 -0
  44. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/dtype.rs +0 -0
  45. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/durability.rs +0 -0
  46. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/err.rs +0 -0
  47. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/fetch.rs +0 -0
  48. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/fill.rs +0 -0
  49. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/fit.rs +0 -0
  50. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/header.rs +0 -0
  51. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/home.rs +0 -0
  52. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ids.rs +0 -0
  53. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  54. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  55. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  56. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  57. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  58. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  59. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  60. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  61. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  62. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  63. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/jcs.rs +0 -0
  64. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/limits.rs +0 -0
  65. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/machine.rs +0 -0
  66. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/manifest.rs +0 -0
  67. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/meta.rs +0 -0
  68. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/project.rs +0 -0
  69. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/providers.rs +0 -0
  70. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/read.rs +0 -0
  71. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/receipt.rs +0 -0
  72. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/registry.rs +0 -0
  73. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/relation.rs +0 -0
  74. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/repository.rs +0 -0
  75. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/sha256.rs +0 -0
  76. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  77. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/spec.rs +0 -0
  78. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/staging.rs +0 -0
  79. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/storage.rs +0 -0
  80. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/api.rs +0 -0
  81. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/http.rs +0 -0
  82. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  83. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  84. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  85. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  86. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  87. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/push.rs +0 -0
  88. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  89. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/src/vectors.rs +0 -0
  90. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  91. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fit.rs +0 -0
  92. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  93. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  94. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  95. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  96. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  97. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  98. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  99. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  100. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/home.rs +0 -0
  101. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  102. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  103. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/liveness.rs +0 -0
  104. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  105. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  106. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  107. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  108. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  109. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  110. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  111. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  112. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  113. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  114. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  115. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  116. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/staging.rs +0 -0
  117. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  118. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  119. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/version.rs +0 -0
  120. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-core/tests/write_through.rs +0 -0
  121. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-py/Cargo.toml +0 -0
  122. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-py/src/docs.rs +0 -0
  123. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-py/src/errors.rs +0 -0
  124. {tensorfs-0.3.30 → tensorfs-0.3.32}/crates/tensorfs-py/src/lib.rs +0 -0
  125. {tensorfs-0.3.30 → tensorfs-0.3.32}/python/tensorfs/__init__.py +0 -0
  126. {tensorfs-0.3.30 → tensorfs-0.3.32}/python/tensorfs/errors.py +0 -0
  127. {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.30"
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.30"
407
+ version = "0.3.32"
408
408
  dependencies = [
409
409
  "pyo3",
410
410
  "tensorfs-core",
@@ -3,5 +3,5 @@ resolver = "2"
3
3
  members = ["crates/tensorfs-core", "crates/tensorfs-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.30"
6
+ version = "0.3.32"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.30
3
+ Version: 0.3.32
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -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 _exclusive = WriterGuard::lock_rebuild(root)?;
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
- if let Ok(meta) = crate::meta::Meta::open(&store) {
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(&store)?;
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
+ }
@@ -9,6 +9,7 @@
9
9
  #![deny(unsafe_code)]
10
10
 
11
11
  pub mod b64;
12
+ mod cache_roots;
12
13
  pub mod canon;
13
14
  pub mod capability;
14
15
  pub mod catalog;
@@ -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