tensorfs 0.3.38__tar.gz → 0.3.40__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 (130) hide show
  1. {tensorfs-0.3.38 → tensorfs-0.3.40}/Cargo.lock +2 -2
  2. {tensorfs-0.3.38 → tensorfs-0.3.40}/Cargo.toml +1 -1
  3. {tensorfs-0.3.38 → tensorfs-0.3.40}/PKG-INFO +1 -1
  4. tensorfs-0.3.40/crates/tensorfs-core/src/derived/estimate.rs +303 -0
  5. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/derived/resume_tests.rs +150 -0
  6. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/derived.rs +189 -134
  7. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/meta.rs +74 -5
  8. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-py/src/store.rs +71 -0
  9. {tensorfs-0.3.38 → tensorfs-0.3.40}/pyproject.toml +1 -1
  10. {tensorfs-0.3.38 → tensorfs-0.3.40}/python/tensorfs/__init__.py +6 -0
  11. tensorfs-0.3.40/python/tensorfs/_derived_channel.py +458 -0
  12. {tensorfs-0.3.38 → tensorfs-0.3.40}/python/tensorfs/_ext.pyi +20 -0
  13. tensorfs-0.3.40/python/tensorfs/derived.py +208 -0
  14. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/Cargo.toml +0 -0
  15. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/build.rs +0 -0
  16. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  17. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  18. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  19. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  20. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/b64.rs +0 -0
  21. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  22. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  23. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  24. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  25. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  26. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  27. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  28. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  29. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  30. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  31. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/canon.rs +0 -0
  32. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/capability.rs +0 -0
  33. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/catalog.rs +0 -0
  34. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/cbor.rs +0 -0
  35. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  36. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/corpus.rs +0 -0
  37. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  38. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  39. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  40. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  41. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  42. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/dtype.rs +0 -0
  43. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/durability.rs +0 -0
  44. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/err.rs +0 -0
  45. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/fetch.rs +0 -0
  46. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/fill.rs +0 -0
  47. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/fit.rs +0 -0
  48. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/gc.rs +0 -0
  49. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/header.rs +0 -0
  50. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/home.rs +0 -0
  51. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/ids.rs +0 -0
  52. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  53. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  54. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  55. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  56. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  57. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  58. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  59. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  60. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  61. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  62. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/jcs.rs +0 -0
  63. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/lib.rs +0 -0
  64. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/limits.rs +0 -0
  65. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/machine.rs +0 -0
  66. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/manifest.rs +0 -0
  67. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/project.rs +0 -0
  68. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/providers.rs +0 -0
  69. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/read.rs +0 -0
  70. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/receipt.rs +0 -0
  71. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/reclaim.rs +0 -0
  72. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/registry.rs +0 -0
  73. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/relation.rs +0 -0
  74. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  75. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/repository.rs +0 -0
  76. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/sha256.rs +0 -0
  77. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  78. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/spec.rs +0 -0
  79. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/staging.rs +0 -0
  80. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/storage.rs +0 -0
  81. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/store.rs +0 -0
  82. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/transport/api.rs +0 -0
  83. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/transport/http.rs +0 -0
  84. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  85. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  86. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  87. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  88. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  89. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  90. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/transport/push.rs +0 -0
  91. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  92. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  93. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/src/vectors.rs +0 -0
  94. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  95. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/fit.rs +0 -0
  96. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  97. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  98. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  99. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  100. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  101. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  102. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  103. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  104. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/home.rs +0 -0
  105. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  106. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  107. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/liveness.rs +0 -0
  108. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  109. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  110. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  111. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  112. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  113. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  114. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  115. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  116. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  117. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  118. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  119. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  120. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/staging.rs +0 -0
  121. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  122. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  123. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/version.rs +0 -0
  124. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-core/tests/write_through.rs +0 -0
  125. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-py/Cargo.toml +0 -0
  126. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-py/src/docs.rs +0 -0
  127. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-py/src/errors.rs +0 -0
  128. {tensorfs-0.3.38 → tensorfs-0.3.40}/crates/tensorfs-py/src/lib.rs +0 -0
  129. {tensorfs-0.3.38 → tensorfs-0.3.40}/python/tensorfs/errors.py +0 -0
  130. {tensorfs-0.3.38 → tensorfs-0.3.40}/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.38"
424
+ version = "0.3.40"
425
425
  dependencies = [
426
426
  "fs2",
427
427
  "minicbor",
@@ -434,7 +434,7 @@ dependencies = [
434
434
 
435
435
  [[package]]
436
436
  name = "tensorfs-py"
437
- version = "0.3.38"
437
+ version = "0.3.40"
438
438
  dependencies = [
439
439
  "pyo3",
440
440
  "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.38"
6
+ version = "0.3.40"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.38
3
+ Version: 0.3.40
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -0,0 +1,303 @@
1
+ //! Remaining added-role geometry without opening or fencing a native writer.
2
+
3
+ use super::*;
4
+
5
+ #[derive(Debug, Default, PartialEq, Eq)]
6
+ pub struct WriteEstimate {
7
+ pub payload_bytes: u64,
8
+ pub largest_part_bytes: u64,
9
+ pub parts: u64,
10
+ }
11
+
12
+ /// Inspect exactly the progress that begin would adopt. The observation is not a
13
+ /// reservation or receipt: begin revalidates it, and per-role admission still
14
+ /// guards every actual write. All inspection holds are released before return.
15
+ pub fn estimate_write(
16
+ store: &Store,
17
+ meta: &Meta,
18
+ transaction: &str,
19
+ writer_session: u64,
20
+ mut declaration: Declaration,
21
+ checkpoint: Option<&ObjectRef>,
22
+ ) -> Result<WriteEstimate> {
23
+ let transaction = transaction_id(transaction)?;
24
+ if writer_session == 0 || writer_session > limits::INT_MAX as u64 {
25
+ return refuse(
26
+ Code::NUMBER_RANGE,
27
+ "WriterSessionId must be positive and inside the canonical integer range",
28
+ );
29
+ }
30
+ let text =
31
+ String::from_utf8(declaration.canonical_work_bytes()?).expect("canonical JSON is UTF-8");
32
+ let hold = meta.acquire_hold("derived-write-estimate")?;
33
+ let estimate = (|| {
34
+ require_source_roots(store, &declaration)?;
35
+ let sources = load_sources(store, &declaration)?;
36
+ let facts: Vec<_> = sources.iter().map(|source| source.fact.clone()).collect();
37
+ let incoming = checkpoint
38
+ .map(|head| read_progress(store, head, &transaction, &text, &facts))
39
+ .transpose()?;
40
+ let initial = meta
41
+ .derived_rows()?
42
+ .into_iter()
43
+ .find(|row| row.id == transaction);
44
+ let mut row = planned_row(
45
+ initial.as_ref(),
46
+ &transaction,
47
+ writer_session,
48
+ &text,
49
+ &facts,
50
+ )?;
51
+ merge_progress(store, &mut row, incoming.as_ref())?;
52
+ build_header(
53
+ &declaration,
54
+ &sources,
55
+ &row.added_parts,
56
+ &row.added_configs,
57
+ true,
58
+ )?;
59
+ if roots::read(store, &transaction)?
60
+ .and_then(|root| root.restore_epoch)
61
+ .is_some_and(|epoch| epoch > writer_session)
62
+ {
63
+ return refuse(
64
+ Code::WRITER_FENCED,
65
+ "writer is behind the retained restore epoch",
66
+ );
67
+ }
68
+ let completed: HashSet<_> = row
69
+ .added_parts
70
+ .iter()
71
+ .map(|part| {
72
+ (
73
+ part.component.as_str(),
74
+ part.key.as_str(),
75
+ part.role.as_str(),
76
+ )
77
+ })
78
+ .collect();
79
+ let mut result = WriteEstimate::default();
80
+ for component in &declaration.components {
81
+ for tensor in &component.add {
82
+ for part in &tensor.parts {
83
+ if part.source.is_some()
84
+ || completed.contains(&(
85
+ component.target.as_str(),
86
+ tensor.key.as_str(),
87
+ part.role.as_str(),
88
+ ))
89
+ {
90
+ continue;
91
+ }
92
+ let length = checked_bytes("remaining derived part", &part.shape, part.dtype)?;
93
+ result.payload_bytes = result
94
+ .payload_bytes
95
+ .checked_add(length)
96
+ .ok_or_else(|| arithmetic("remaining derived payload overflows"))?;
97
+ result.largest_part_bytes = result.largest_part_bytes.max(length);
98
+ result.parts += 1;
99
+ }
100
+ }
101
+ }
102
+ Ok(result)
103
+ })();
104
+ let released = hold.release(meta);
105
+ estimate.and_then(|value| released.map(|()| value))
106
+ }
107
+
108
+ #[cfg(test)]
109
+ mod tests {
110
+ use super::*;
111
+
112
+ fn fixture(label: &str) -> (Store, Meta, String, Declaration) {
113
+ let path = std::env::temp_dir().join(format!(
114
+ "tensorfs-derived-estimate-{label}-{}",
115
+ crate::meta::now_nanos_unique()
116
+ ));
117
+ let store = Store::init(&path).unwrap();
118
+ let meta = Meta::open(&store).unwrap();
119
+ (
120
+ store,
121
+ meta,
122
+ format!("sha256:{}", "78".repeat(32)),
123
+ super::super::tests::created_declaration(),
124
+ )
125
+ }
126
+
127
+ fn release(meta: &Meta, begun: Begin) {
128
+ release_guards(meta, Some(begun.writer_hold), begun.source_leases);
129
+ }
130
+
131
+ #[test]
132
+ fn fresh_estimate_does_not_claim_writer_or_block_collection() {
133
+ let (store, meta, id, declaration) = fixture("fresh");
134
+ let before = meta.derived_rows().unwrap();
135
+ assert_eq!(
136
+ estimate_write(&store, &meta, &id, 1, declaration.clone(), None).unwrap(),
137
+ WriteEstimate {
138
+ payload_bytes: 2048,
139
+ largest_part_bytes: 2048,
140
+ parts: 1
141
+ }
142
+ );
143
+ assert_eq!(meta.derived_rows().unwrap(), before);
144
+ assert!(matches!(
145
+ lookup(&store, &meta, &id).unwrap(),
146
+ Lookup::Absent
147
+ ));
148
+ crate::gc::collect(store.root(), false).unwrap();
149
+ let writer = begin(&store, &meta, &id, 1, declaration, None).unwrap();
150
+ fence(&meta, &id, 1).unwrap();
151
+ release(&meta, writer);
152
+ std::fs::remove_dir_all(store.root()).unwrap();
153
+ }
154
+
155
+ #[test]
156
+ fn interrupted_large_writer_only_charges_its_small_remaining_tail() {
157
+ let (store, meta, id, mut declaration) = fixture("large-partial");
158
+ const LARGE: u64 = 64 << 20;
159
+ let mut tail = declaration.components[0].add[0].clone();
160
+ tail.key = "tail".into();
161
+ tail.shape = vec![1];
162
+ tail.parts[0].shape = vec![1];
163
+ declaration.components[0].add[0].shape = vec![LARGE / 4];
164
+ declaration.components[0].add[0].parts[0].shape = vec![LARGE / 4];
165
+ declaration.components[0].add.push(tail);
166
+ declaration.order.push(("model".into(), "tail".into()));
167
+ declaration.max_new_bytes = LARGE + 4;
168
+ let writer = begin(&store, &meta, &id, 1, declaration.clone(), None).unwrap();
169
+ let written = add_part(
170
+ &store,
171
+ &meta,
172
+ &id,
173
+ 1,
174
+ ("model", "weight", "value"),
175
+ &mut std::io::repeat(0x31).take(LARGE),
176
+ )
177
+ .unwrap();
178
+ let (_, checkpoint) =
179
+ checkpoint_progress(&store, &meta, &id, 1, "request", "model", None).unwrap();
180
+ assert!(checkpoint.head.is_some());
181
+ fence(&meta, &id, 1).unwrap();
182
+ release(&meta, writer);
183
+ let before = meta.derived_rows().unwrap();
184
+ assert_eq!(
185
+ estimate_write(
186
+ &store,
187
+ &meta,
188
+ &id,
189
+ 1,
190
+ declaration.clone(),
191
+ checkpoint.head.as_ref()
192
+ )
193
+ .unwrap_err()
194
+ .code,
195
+ Code::WRITER_FENCED
196
+ );
197
+ assert_eq!(
198
+ estimate_write(
199
+ &store,
200
+ &meta,
201
+ &id,
202
+ 2,
203
+ declaration.clone(),
204
+ checkpoint.head.as_ref()
205
+ )
206
+ .unwrap(),
207
+ WriteEstimate {
208
+ payload_bytes: 4,
209
+ largest_part_bytes: 4,
210
+ parts: 1
211
+ }
212
+ );
213
+ assert_eq!(meta.derived_rows().unwrap(), before);
214
+ crate::gc::collect(store.root(), false).unwrap();
215
+ assert!(written
216
+ .part
217
+ .segments()
218
+ .iter()
219
+ .all(|part| store.contains(&part.sha256)));
220
+ let resumed = begin(&store, &meta, &id, 2, declaration, checkpoint.head.as_ref()).unwrap();
221
+ assert_eq!(
222
+ completed(&meta, &id, 2).unwrap().0,
223
+ vec![("model".into(), "weight".into(), "value".into())]
224
+ );
225
+ add_part(
226
+ &store,
227
+ &meta,
228
+ &id,
229
+ 2,
230
+ ("model", "tail", "value"),
231
+ &mut [0u8; 4].as_slice(),
232
+ )
233
+ .unwrap();
234
+ commit(&store, &meta, &id, 2).unwrap();
235
+ release(&meta, resumed);
236
+ std::fs::remove_dir_all(store.root()).unwrap();
237
+ }
238
+
239
+ #[test]
240
+ fn grafts_charge_no_new_payload_and_preserve_source_custody() {
241
+ let (store, meta, id, mut declaration) = fixture("graft");
242
+ let writer = begin(&store, &meta, &id, 1, declaration.clone(), None).unwrap();
243
+ add_part(
244
+ &store,
245
+ &meta,
246
+ &id,
247
+ 1,
248
+ ("model", "weight", "value"),
249
+ &mut [0x31u8; 2048].as_slice(),
250
+ )
251
+ .unwrap();
252
+ let original = commit(&store, &meta, &id, 1).unwrap();
253
+ release(&meta, writer);
254
+ declaration.sources.push(Source {
255
+ alias: "original".into(),
256
+ manifest: original.manifest.clone(),
257
+ });
258
+ declaration.components[0].add[0].parts[0].source = Some(PartSource {
259
+ source: "original".into(),
260
+ component: "model".into(),
261
+ tensor: "weight".into(),
262
+ role: "value".into(),
263
+ });
264
+ declaration.max_new_bytes = 0;
265
+ let target = format!("sha256:{}", "79".repeat(32));
266
+ let before = meta.derived_rows().unwrap();
267
+ assert_eq!(
268
+ estimate_write(&store, &meta, &target, 1, declaration.clone(), None).unwrap(),
269
+ WriteEstimate::default()
270
+ );
271
+ assert_eq!(meta.derived_rows().unwrap(), before);
272
+ crate::gc::collect(store.root(), false).unwrap();
273
+ let target_writer = begin(&store, &meta, &target, 1, declaration, None).unwrap();
274
+ assert_eq!(
275
+ commit(&store, &meta, &target, 1).unwrap().manifest,
276
+ original.manifest
277
+ );
278
+ release(&meta, target_writer);
279
+ std::fs::remove_dir_all(store.root()).unwrap();
280
+ }
281
+
282
+ #[test]
283
+ fn changed_work_and_missing_checkpoint_refuse_without_mutating_progress() {
284
+ let (store, meta, id, declaration) = fixture("refuse");
285
+ let writer = begin(&store, &meta, &id, 1, declaration.clone(), None).unwrap();
286
+ fence(&meta, &id, 1).unwrap();
287
+ release(&meta, writer);
288
+ let before = meta.derived_rows().unwrap();
289
+ let mut changed = declaration.clone();
290
+ changed.work_fingerprint = Some(format!("sha256:{}", "65".repeat(32)));
291
+ assert_eq!(
292
+ estimate_write(&store, &meta, &id, 2, changed, None)
293
+ .unwrap_err()
294
+ .code,
295
+ Code::TRANSACTION_CONFLICT
296
+ );
297
+ let absent = ObjectRef::of(b"not a checkpoint");
298
+ assert!(estimate_write(&store, &meta, &id, 2, declaration, Some(&absent)).is_err());
299
+ assert_eq!(meta.derived_rows().unwrap(), before);
300
+ crate::gc::collect(store.root(), false).unwrap();
301
+ std::fs::remove_dir_all(store.root()).unwrap();
302
+ }
303
+ }
@@ -918,3 +918,153 @@ fn source_free_restore_holds_survive_gc_until_begin_or_explicit_abandon() {
918
918
  release(&source_meta, writer);
919
919
  std::fs::remove_dir_all(source.root()).unwrap();
920
920
  }
921
+
922
+ /// Opt-in generated-data profile. Payloads are tiny so metadata scaling is visible.
923
+ /// Run in release mode; elapsed time is evidence, never a flaky unit-test threshold.
924
+ #[test]
925
+ #[ignore]
926
+ fn profile_derived_transaction_scaling() {
927
+ use std::time::Instant;
928
+ let scenarios = [
929
+ (219, 1, 1),
930
+ (219, 1, 10),
931
+ (219, 4, 10),
932
+ (219, 32, 10),
933
+ (219, 128, 10),
934
+ (3968, 1, 10),
935
+ (3968, 4, 10),
936
+ (219, 1, 219),
937
+ ];
938
+ for (keys, histories, writes) in scenarios {
939
+ let (store, meta, id, _) = fixture("row-profile");
940
+ let mut declaration = super::tests::created_declaration();
941
+ let prototype = declaration.components[0].add[0].clone();
942
+ declaration.components[0].add = (0..keys)
943
+ .map(|index| {
944
+ let mut tensor = prototype.clone();
945
+ tensor.key = format!("weight_{index:04}");
946
+ tensor.shape = vec![4];
947
+ tensor.parts[0].shape = vec![4];
948
+ tensor
949
+ })
950
+ .collect();
951
+ declaration.order = declaration.components[0]
952
+ .add
953
+ .iter()
954
+ .map(|tensor| ("model".into(), tensor.key.clone()))
955
+ .collect();
956
+ declaration.max_new_bytes = (keys * 16) as u64;
957
+ let writer = begin(&store, &meta, &id, 1, declaration, None).unwrap();
958
+ // Populate representative unrelated transactions without timing fixture creation.
959
+ let mut txn = meta.txn().unwrap();
960
+ let prototype = txn.rows[0].clone();
961
+ let row_bytes = crate::canon::write(&prototype.to_value()).len();
962
+ for index in 1..histories {
963
+ let mut row = prototype.clone();
964
+ row.id = ObjectRef::of(format!("history-{index}").as_bytes()).id();
965
+ txn.rows.push(row);
966
+ }
967
+ txn.commit().unwrap();
968
+ let started = Instant::now();
969
+ for _ in 0..20 {
970
+ check_writer(&meta, &id, 1).unwrap();
971
+ }
972
+ let fence_ms = started.elapsed().as_secs_f64() * 1000.0;
973
+ let mut part_ms = 0.0;
974
+ let mut checkpoint_ms = 0.0;
975
+ let mut prior = None;
976
+ for index in 0..writes {
977
+ let key = format!("weight_{index:04}");
978
+ let started = Instant::now();
979
+ add_part(
980
+ &store,
981
+ &meta,
982
+ &id,
983
+ 1,
984
+ ("model", &key, "value"),
985
+ &mut [index as u8; 16].as_slice(),
986
+ )
987
+ .unwrap();
988
+ part_ms += started.elapsed().as_secs_f64() * 1000.0;
989
+ let started = Instant::now();
990
+ let (_, head) =
991
+ checkpoint_progress(&store, &meta, &id, 1, "profile", "model", prior.as_ref())
992
+ .unwrap();
993
+ prior = head.head;
994
+ checkpoint_ms += started.elapsed().as_secs_f64() * 1000.0;
995
+ }
996
+ eprintln!("PROFILE {{\"keys\":{keys},\"histories\":{histories},\"writes\":{writes},\"row_bytes\":{row_bytes},\"fence20_ms\":{fence_ms:.3},\"add_ms\":{part_ms:.3},\"checkpoint_ms\":{checkpoint_ms:.3}}}");
997
+ fence(&meta, &id, 1).unwrap();
998
+ release(&meta, writer);
999
+ std::fs::remove_dir_all(store.root()).unwrap();
1000
+ }
1001
+ }
1002
+
1003
+ #[test]
1004
+ fn named_writer_operations_leave_other_rows_and_writer_lock_alone() {
1005
+ use rusqlite::{params, Connection};
1006
+ let (store, meta, first_id, declaration) = fixture("scoped-row");
1007
+ let second_id = ObjectRef::of(b"independent writer").id();
1008
+ let first = begin(&store, &meta, &first_id, 1, declaration.clone(), None).unwrap();
1009
+ let second = begin(&store, &meta, &second_id, 1, declaration.clone(), None).unwrap();
1010
+ let connection = Connection::open(crate::catalog::Catalog::path(store.root())).unwrap();
1011
+ let untouched: Vec<u8> = connection
1012
+ .query_row(
1013
+ "SELECT bytes FROM tensorfs_derived_transactions WHERE id=?1",
1014
+ [&second_id],
1015
+ |row| row.get(0),
1016
+ )
1017
+ .unwrap();
1018
+ // Corrupt unrelated history makes an accidental whole-table read fail, while
1019
+ // a scoped mutation must preserve it byte-for-byte and mutate only this writer.
1020
+ let damaged = ObjectRef::of(b"unrelated damaged history").id();
1021
+ connection
1022
+ .execute(
1023
+ "INSERT INTO tensorfs_derived_transactions(id,bytes) VALUES(?1,?2)",
1024
+ params![damaged, b"broken".as_slice()],
1025
+ )
1026
+ .unwrap();
1027
+ data(&store, &meta, &first_id, 1, 0x31);
1028
+ let after: Vec<u8> = connection
1029
+ .query_row(
1030
+ "SELECT bytes FROM tensorfs_derived_transactions WHERE id=?1",
1031
+ [&second_id],
1032
+ |row| row.get(0),
1033
+ )
1034
+ .unwrap();
1035
+ assert_eq!(untouched, after);
1036
+ // A read fence must succeed while an unrelated SQLite writer is active. The old
1037
+ // BEGIN IMMEDIATE observation instead hit the five-second busy timeout.
1038
+ connection.execute_batch("BEGIN IMMEDIATE").unwrap();
1039
+ check_writer(&meta, &second_id, 1).unwrap();
1040
+ assert!(completed(&meta, &second_id, 1).unwrap().0.is_empty());
1041
+ connection.execute_batch("ROLLBACK").unwrap();
1042
+ // A named operation does not parse unrelated history. Global inventory still
1043
+ // reports the damaged row rather than silently skipping it.
1044
+ check_writer(&meta, &first_id, 1).unwrap();
1045
+ assert!(meta.derived_rows().is_err());
1046
+ connection
1047
+ .execute(
1048
+ "DELETE FROM tensorfs_derived_transactions WHERE id=?1",
1049
+ [&damaged],
1050
+ )
1051
+ .unwrap();
1052
+ assert!(fence(&meta, &first_id, 1).unwrap());
1053
+ assert_eq!(
1054
+ check_writer(&meta, &first_id, 1).unwrap_err().code,
1055
+ Code::WRITER_FENCED
1056
+ );
1057
+ check_writer(&meta, &second_id, 1).unwrap();
1058
+ release(&meta, first);
1059
+ let resumed = begin(&store, &meta, &first_id, 2, declaration, None).unwrap();
1060
+ assert_eq!(completed(&meta, &first_id, 2).unwrap().0.len(), 1);
1061
+ assert_eq!(
1062
+ check_writer(&meta, &first_id, 1).unwrap_err().code,
1063
+ Code::WRITER_FENCED
1064
+ );
1065
+ fence(&meta, &first_id, 2).unwrap();
1066
+ fence(&meta, &second_id, 1).unwrap();
1067
+ release(&meta, resumed);
1068
+ release(&meta, second);
1069
+ std::fs::remove_dir_all(store.root()).unwrap();
1070
+ }