tensorfs 0.3.69__tar.gz → 0.3.70__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 (162) hide show
  1. {tensorfs-0.3.69 → tensorfs-0.3.70}/Cargo.lock +2 -2
  2. {tensorfs-0.3.69 → tensorfs-0.3.70}/Cargo.toml +1 -1
  3. {tensorfs-0.3.69 → tensorfs-0.3.70}/PKG-INFO +1 -1
  4. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/watch.rs +4 -3
  5. tensorfs-0.3.70/crates/tensorfs-core/src/catalog/lock.rs +121 -0
  6. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/catalog.rs +108 -31
  7. tensorfs-0.3.70/crates/tensorfs-core/src/derived/group_commit_tests.rs +114 -0
  8. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/roots.rs +44 -26
  9. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived.rs +20 -0
  10. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/meta.rs +1 -6
  11. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/store.rs +18 -1
  12. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/pull.rs +7 -33
  13. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/unsynced.rs +17 -0
  14. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-py/src/store.rs +8 -3
  15. {tensorfs-0.3.69 → tensorfs-0.3.70}/pyproject.toml +1 -1
  16. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/Cargo.toml +0 -0
  17. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/build.rs +0 -0
  18. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  19. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  20. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  21. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  22. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/asset_update.rs +0 -0
  23. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/b64.rs +0 -0
  24. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  25. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  26. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  27. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  28. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  29. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  30. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  31. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  32. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  33. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/canon.rs +0 -0
  34. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/capability.rs +0 -0
  35. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/cbor.rs +0 -0
  36. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  37. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  38. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/corpus.rs +0 -0
  39. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  40. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/companion_tests.rs +0 -0
  41. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  42. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  43. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/part_rows_tests.rs +0 -0
  44. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  45. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  46. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  47. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/descriptors.rs +0 -0
  48. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/disk.rs +0 -0
  49. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/dtype.rs +0 -0
  50. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/durability.rs +0 -0
  51. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/err.rs +0 -0
  52. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/fetch.rs +0 -0
  53. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/fill.rs +0 -0
  54. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/fit.rs +0 -0
  55. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/gc.rs +0 -0
  56. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/header.rs +0 -0
  57. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/home.rs +0 -0
  58. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ids.rs +0 -0
  59. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  60. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  61. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/custody.rs +0 -0
  62. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  63. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  64. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  65. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  66. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  67. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  68. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  69. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  70. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  71. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/jcs.rs +0 -0
  72. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/lib.rs +0 -0
  73. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/limits.rs +0 -0
  74. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/machine.rs +0 -0
  75. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/manifest.rs +0 -0
  76. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/project.rs +0 -0
  77. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/providers.rs +0 -0
  78. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/read.rs +0 -0
  79. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/receipt.rs +0 -0
  80. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/reclaim.rs +0 -0
  81. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/registry.rs +0 -0
  82. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/relation.rs +0 -0
  83. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/repo_cache/identity.rs +0 -0
  84. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  85. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/repository.rs +0 -0
  86. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/sha256.rs +0 -0
  87. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  88. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/spec.rs +0 -0
  89. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/staging.rs +0 -0
  90. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/stats.rs +0 -0
  91. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/storage.rs +0 -0
  92. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/api.rs +0 -0
  93. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/http.rs +0 -0
  94. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/http_reuse_tests.rs +0 -0
  95. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  96. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  97. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/memory.rs +0 -0
  98. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  99. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/pace.rs +0 -0
  100. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  101. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  102. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/push.rs +0 -0
  103. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  104. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/speculate.rs +0 -0
  105. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  106. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/src/vectors.rs +0 -0
  107. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/additive_fields.rs +0 -0
  108. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/anima_hf_profile.rs +0 -0
  109. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  110. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/civitai_sdxl_profile.rs +0 -0
  111. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fd_budget.rs +0 -0
  112. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fd_headroom.rs +0 -0
  113. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  114. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fit.rs +0 -0
  115. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  116. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  117. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  118. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  119. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  120. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  121. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  122. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/foreign_json.rs +0 -0
  123. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  124. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  125. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/home.rs +0 -0
  126. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  127. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  128. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/liveness.rs +0 -0
  129. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/qwen_image21_profile.rs +0 -0
  130. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  131. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  132. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  133. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  134. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  135. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  136. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  137. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  138. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  139. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/source_download.rs +0 -0
  140. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  141. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  142. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  143. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/staging.rs +0 -0
  144. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  145. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  146. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/transfer_memory.rs +0 -0
  147. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/trust_index.rs +0 -0
  148. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/version.rs +0 -0
  149. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-core/tests/write_through.rs +0 -0
  150. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-py/Cargo.toml +0 -0
  151. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-py/src/docs.rs +0 -0
  152. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-py/src/errors.rs +0 -0
  153. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-py/src/ingest.rs +0 -0
  154. {tensorfs-0.3.69 → tensorfs-0.3.70}/crates/tensorfs-py/src/lib.rs +0 -0
  155. {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/__init__.py +0 -0
  156. {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/_derived_channel.py +0 -0
  157. {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/_ext.pyi +0 -0
  158. {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/_source_channel.py +0 -0
  159. {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/_tensors.py +0 -0
  160. {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/derived.py +0 -0
  161. {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/errors.py +0 -0
  162. {tensorfs-0.3.69 → tensorfs-0.3.70}/python/tensorfs/py.typed +0 -0
@@ -439,7 +439,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
439
439
 
440
440
  [[package]]
441
441
  name = "tensorfs-core"
442
- version = "0.3.69"
442
+ version = "0.3.70"
443
443
  dependencies = [
444
444
  "fs2",
445
445
  "libc",
@@ -454,7 +454,7 @@ dependencies = [
454
454
 
455
455
  [[package]]
456
456
  name = "tensorfs-py"
457
- version = "0.3.69"
457
+ version = "0.3.70"
458
458
  dependencies = [
459
459
  "pyo3",
460
460
  "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.69"
6
+ version = "0.3.70"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.69
3
+ Version: 0.3.70
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -29,8 +29,6 @@ use std::process::{Child, ChildStdout, ExitStatus};
29
29
  use std::sync::mpsc::{self, RecvTimeoutError};
30
30
  use std::time::{Duration, Instant};
31
31
 
32
- use tensorfs_core::catalog::BUSY_TIMEOUT;
33
-
34
32
  /// How many times its own worst silence a worker is granted before the parent looks for
35
33
  /// inactivity, and how many times its own projected run it may spin without progress.
36
34
  /// Three doublings past the demonstrated maximum: the worst of n observed gaps
@@ -101,8 +99,11 @@ fn parse_progress(line: &str) -> Option<(u64, u64)> {
101
99
  Some((moved.parse().ok()?, admitted.parse().ok()?))
102
100
  }
103
101
 
102
+ /// Patience before a worker has shown any gap to measure against.
103
+ const FIRST_PATIENCE: Duration = Duration::from_secs(5);
104
+
104
105
  fn patience(worst_gap: Duration) -> Duration {
105
- (worst_gap * PATIENCE_FACTOR).max(BUSY_TIMEOUT)
106
+ (worst_gap * PATIENCE_FACTOR).max(FIRST_PATIENCE)
106
107
  }
107
108
 
108
109
  /// What a silent-but-active worker is allowed: the whole declared input at the rate this
@@ -0,0 +1,121 @@
1
+ //! Waiting for another process's catalog lock by whether its holder can still release it.
2
+ //!
3
+ //! SQLite's locks are POSIX locks: they die with the process that holds them. A holder
4
+ //! that is alive is committing or about to, so a writer waits for it for as long as it
5
+ //! takes; a flat deadline refused a busy, progressing writer as if it were dead. The only
6
+ //! holder that cannot release is one that is stopped (a debugger, SIGSTOP) or a zombie,
7
+ //! and only then does a statement give up.
8
+ //!
9
+ //! Holders come from `/proc/locks`, matched by the catalog's inode (and its WAL `-shm`).
10
+ //! The catalog file itself is never opened here: closing a second descriptor on it would
11
+ //! drop every POSIX lock this process holds on it.
12
+
13
+ use std::cell::RefCell;
14
+ use std::path::{Path, PathBuf};
15
+ use std::time::Duration;
16
+
17
+ use rusqlite::Connection;
18
+
19
+ use crate::err::Result;
20
+
21
+ thread_local! {
22
+ /// The catalog this thread's statements wait on. A connection is opened for each
23
+ /// operation just before it is used, on the thread that uses it, so the one opened last
24
+ /// here is the one a busy statement belongs to.
25
+ static WAITING_ON: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
26
+ }
27
+
28
+ /// Install the waiting handler on one connection.
29
+ pub(crate) fn wait_for_live_holders(connection: &Connection, path: &Path) -> Result<()> {
30
+ WAITING_ON.with(|waiting| *waiting.borrow_mut() = Some(path.to_path_buf()));
31
+ connection
32
+ .busy_handler(Some(busy))
33
+ .map_err(|error| super::sql("install catalog lock wait", error))
34
+ }
35
+
36
+ fn busy(attempt: i32) -> bool {
37
+ let path = WAITING_ON.with(|waiting| waiting.borrow().clone());
38
+ if path.is_some_and(|path| holders_cannot_release(&path)) {
39
+ return false;
40
+ }
41
+ std::thread::sleep(Duration::from_millis(attempt.clamp(1, 64) as u64));
42
+ true
43
+ }
44
+
45
+ /// Every process holding a write lock on the catalog is stopped or a zombie. Without a
46
+ /// holder to name (the lock just went, or no `/proc`), keep asking.
47
+ fn holders_cannot_release(path: &Path) -> bool {
48
+ let inodes: Vec<u64> = [path.to_path_buf(), shm(path)]
49
+ .iter()
50
+ .filter_map(|file| std::fs::metadata(file).ok())
51
+ .map(|metadata| std::os::unix::fs::MetadataExt::ino(&metadata))
52
+ .collect();
53
+ let Ok(locks) = std::fs::read_to_string("/proc/locks") else {
54
+ return false;
55
+ };
56
+ let holders = write_holders(&locks, &inodes, std::process::id());
57
+ !holders.is_empty() && holders.iter().all(|pid| stopped(*pid))
58
+ }
59
+
60
+ fn shm(path: &Path) -> PathBuf {
61
+ let mut name = path.as_os_str().to_owned();
62
+ name.push("-shm");
63
+ PathBuf::from(name)
64
+ }
65
+
66
+ /// Other processes' POSIX write locks on these inodes, from `/proc/locks` lines such as
67
+ /// `3: POSIX ADVISORY WRITE 167671 103:02:204736108 1073741824 1073742335`.
68
+ fn write_holders(locks: &str, inodes: &[u64], own: u32) -> Vec<u32> {
69
+ locks
70
+ .lines()
71
+ .filter_map(|line| {
72
+ let fields: Vec<&str> = line.split_whitespace().collect();
73
+ let at = fields.iter().position(|field| *field == "POSIX")?;
74
+ if fields.get(at + 2) != Some(&"WRITE") {
75
+ return None;
76
+ }
77
+ let pid: u32 = fields.get(at + 3)?.parse().ok()?;
78
+ let inode: u64 = fields.get(at + 4)?.rsplit(':').next()?.parse().ok()?;
79
+ (pid != own && inodes.contains(&inode)).then_some(pid)
80
+ })
81
+ .collect()
82
+ }
83
+
84
+ /// Stopped, traced-stopped, zombie or dead: a process that cannot release its lock.
85
+ fn stopped(pid: u32) -> bool {
86
+ std::fs::read_to_string(format!("/proc/{pid}/stat"))
87
+ .ok()
88
+ .and_then(|stat| {
89
+ let state = stat
90
+ .rsplit_once(')')?
91
+ .1
92
+ .split_whitespace()
93
+ .next()?
94
+ .to_string();
95
+ Some(matches!(state.as_str(), "T" | "t" | "Z" | "X"))
96
+ })
97
+ .unwrap_or(false)
98
+ }
99
+
100
+ #[cfg(test)]
101
+ mod tests {
102
+ use super::*;
103
+
104
+ #[test]
105
+ fn only_other_processes_write_locks_on_the_catalog_are_holders() {
106
+ let locks = "\
107
+ 1: POSIX ADVISORY WRITE 4242 103:02:77 1073741824 1073742335
108
+ 2: POSIX ADVISORY READ 5151 103:02:77 1073741826 1073742335
109
+ 3: POSIX ADVISORY WRITE 6161 103:02:99 120 120
110
+ 4: FLOCK ADVISORY WRITE 7171 103:02:77 0 EOF
111
+ 5: POSIX ADVISORY WRITE 1000 103:02:77 0 EOF
112
+ 6: -> POSIX ADVISORY WRITE 8181 103:02:77 0 EOF
113
+ ";
114
+ assert_eq!(write_holders(locks, &[77], 1000), vec![4242, 8181]);
115
+ assert_eq!(
116
+ write_holders(locks, &[77, 99], 1000),
117
+ vec![4242, 6161, 8181]
118
+ );
119
+ assert!(write_holders(locks, &[5], 1000).is_empty());
120
+ }
121
+ }
@@ -6,7 +6,6 @@ use std::fs::{self, File};
6
6
  use std::os::unix::fs::MetadataExt;
7
7
  use std::path::{Path, PathBuf};
8
8
  use std::sync::{Arc, Mutex, OnceLock, RwLock, Weak};
9
- use std::time::Duration;
10
9
 
11
10
  use fs2::FileExt;
12
11
  use rusqlite::{params, Connection};
@@ -14,10 +13,7 @@ use rusqlite::{params, Connection};
14
13
  use crate::err::{refuse, Code, Refusal, Result};
15
14
  use crate::storage::Projection;
16
15
 
17
- /// The longest one catalog call may SLEEP on another writer's lock before it refuses. It is
18
- /// the only wait a store operation can legitimately spend blocked and idle, so it is also
19
- /// the floor under any patience a supervisor grants a worker that has gone quiet.
20
- pub const BUSY_TIMEOUT: Duration = Duration::from_secs(5);
16
+ mod lock;
21
17
 
22
18
  /// The catalog this build writes. A newer build may only ADD tables and columns (an
23
19
  /// incompatible change renames the table), so any catalog at this version or later that
@@ -62,11 +58,6 @@ fn sql(what: impl AsRef<str>, error: rusqlite::Error) -> Refusal {
62
58
  }
63
59
  }
64
60
 
65
- /// Whether a catalog refusal is SQLite's busy timeout expiring behind another writer.
66
- fn busy(refusal: &Refusal) -> bool {
67
- refusal.code == Code::IO_FAILED && refusal.detail.ends_with("database is locked")
68
- }
69
-
70
61
  fn io(what: impl AsRef<str>, error: std::io::Error) -> Refusal {
71
62
  crate::store::classify_io(what, error)
72
63
  }
@@ -134,13 +125,32 @@ fn connect(path: &Path) -> Result<Connection> {
134
125
  }
135
126
  sql("open catalog", error)
136
127
  })?;
128
+ configure(&connection, path)?;
129
+ Ok(connection)
130
+ }
131
+
132
+ /// Journal and sync by what a crash can take from the disk the catalog lives on
133
+ /// (`disk::DiskClass`): an ephemeral disk dies with the machine, so no sync buys anything;
134
+ /// a persistent one keeps a WAL synced at checkpoints; a network filesystem keeps the
135
+ /// rollback journal and a full sync, because WAL's shared memory is not coherent across
136
+ /// hosts. The catalog is only ever opened by processes on one host otherwise.
137
+ pub(crate) fn configure(connection: &Connection, path: &Path) -> Result<()> {
138
+ lock::wait_for_live_holders(connection, path)?;
139
+ let (journal, synchronous) = match crate::disk::DiskClass::of(path.parent().unwrap_or(path)) {
140
+ crate::disk::DiskClass::Ephemeral => ("wal", "OFF"),
141
+ crate::disk::DiskClass::Persistent => ("wal", "NORMAL"),
142
+ crate::disk::DiskClass::Network => ("delete", "FULL"),
143
+ };
137
144
  connection
138
- .busy_timeout(BUSY_TIMEOUT)
139
- .map_err(|error| sql("set catalog busy timeout", error))?;
145
+ .query_row(&format!("PRAGMA journal_mode={journal}"), [], |row| {
146
+ row.get::<_, String>(0)
147
+ })
148
+ .map_err(|error| sql("set catalog journal", error))?;
140
149
  connection
141
- .execute_batch("PRAGMA foreign_keys=ON; PRAGMA synchronous=FULL;")
142
- .map_err(|error| sql("configure catalog", error))?;
143
- Ok(connection)
150
+ .execute_batch(&format!(
151
+ "PRAGMA foreign_keys=ON; PRAGMA synchronous={synchronous};"
152
+ ))
153
+ .map_err(|error| sql("configure catalog", error))
144
154
  }
145
155
 
146
156
  /// A connection to an existing catalog of exactly this schema.
@@ -390,22 +400,6 @@ impl Catalog {
390
400
  Ok(catalog)
391
401
  }
392
402
 
393
- /// `open` for an object admission. A catalog busy past SQLite's timeout while this
394
- /// process's own admission batches keep committing is a queue, not a failure: the
395
- /// verified bytes wait for their turn instead of being thrown away and fetched again.
396
- /// Busy with no batch committed in the meantime is another writer's lock, and refuses.
397
- pub(crate) fn open_admitting(root: &Path) -> Result<Self> {
398
- loop {
399
- let committed = crate::stats::snapshot().admission_batches;
400
- match Self::open(root) {
401
- Err(refusal)
402
- if busy(&refusal) && crate::stats::snapshot().admission_batches > committed => {
403
- }
404
- opened => return opened,
405
- }
406
- }
407
- }
408
-
409
403
  pub fn open(root: &Path) -> Result<Self> {
410
404
  let path = Self::path(root);
411
405
  validated(&path)?;
@@ -1626,6 +1620,89 @@ mod tests {
1626
1620
  ))
1627
1621
  }
1628
1622
 
1623
+ /// Another process holding the catalog's write lock: this test binary, re-run as
1624
+ /// `lock_holder_child`, until it has taken the lock.
1625
+ fn holder(catalog: &Path, millis: u64) -> std::process::Child {
1626
+ use std::io::BufRead;
1627
+ let mut child = std::process::Command::new(std::env::current_exe().unwrap())
1628
+ .args(["--exact", "catalog::tests::lock_holder_child", "--ignored"])
1629
+ .args(["--nocapture", "--test-threads=1"])
1630
+ .env("TENSORFS_TEST_HOLD", catalog)
1631
+ .env("TENSORFS_TEST_HOLD_MS", millis.to_string())
1632
+ .stdout(std::process::Stdio::piped())
1633
+ .spawn()
1634
+ .unwrap();
1635
+ let mut lines = std::io::BufReader::new(child.stdout.take().unwrap()).lines();
1636
+ let held = lines
1637
+ .by_ref()
1638
+ .map_while(std::result::Result::ok)
1639
+ .any(|line| line.ends_with("held"));
1640
+ assert!(held, "the holder never took the catalog lock");
1641
+ std::thread::spawn(move || lines.for_each(drop));
1642
+ child
1643
+ }
1644
+
1645
+ #[test]
1646
+ #[ignore = "run by holder() in a child process"]
1647
+ fn lock_holder_child() {
1648
+ let path = std::env::var("TENSORFS_TEST_HOLD").unwrap();
1649
+ let millis: u64 = std::env::var("TENSORFS_TEST_HOLD_MS")
1650
+ .unwrap()
1651
+ .parse()
1652
+ .unwrap();
1653
+ let connection = Connection::open(path).unwrap();
1654
+ connection.execute_batch("BEGIN IMMEDIATE").unwrap();
1655
+ connection
1656
+ .execute(
1657
+ "INSERT INTO tensorfs_released_manifests VALUES ('held','by',?1)",
1658
+ [std::process::id().to_string()],
1659
+ )
1660
+ .unwrap();
1661
+ println!("held");
1662
+ std::thread::sleep(std::time::Duration::from_millis(millis));
1663
+ connection.execute_batch("COMMIT").unwrap();
1664
+ }
1665
+
1666
+ fn write(catalog: &Catalog, name: &str) -> Result<usize> {
1667
+ catalog
1668
+ .connection()?
1669
+ .execute(
1670
+ "INSERT INTO tensorfs_released_manifests VALUES ('org',?1,'digest')",
1671
+ [name],
1672
+ )
1673
+ .map_err(|error| sql("test write", error))
1674
+ }
1675
+
1676
+ #[test]
1677
+ fn a_live_lock_holder_is_waited_for_past_any_fixed_deadline() {
1678
+ let root = temporary("live-holder");
1679
+ fs::create_dir_all(&root).unwrap();
1680
+ let catalog = Catalog::initialize(&root).unwrap();
1681
+ let mut child = holder(&catalog.path, 7_000);
1682
+ let started = std::time::Instant::now();
1683
+ assert_eq!(write(&catalog, "after-holder").unwrap(), 1);
1684
+ assert!(started.elapsed() >= std::time::Duration::from_secs(6));
1685
+ assert!(child.wait().unwrap().success());
1686
+ let _ = fs::remove_dir_all(root);
1687
+ }
1688
+
1689
+ #[test]
1690
+ fn a_stopped_lock_holder_is_refused() {
1691
+ use rustix::process::{kill_process, Pid, Signal};
1692
+ let root = temporary("stopped-holder");
1693
+ fs::create_dir_all(&root).unwrap();
1694
+ let catalog = Catalog::initialize(&root).unwrap();
1695
+ let mut child = holder(&catalog.path, 60_000);
1696
+ let pid = Pid::from_raw(child.id() as i32).unwrap();
1697
+ kill_process(pid, Signal::STOP).unwrap();
1698
+ let refused = write(&catalog, "behind-a-stopped-holder").unwrap_err();
1699
+ assert!(refused.detail.contains("locked"), "{refused:?}");
1700
+ kill_process(pid, Signal::KILL).unwrap();
1701
+ child.wait().unwrap();
1702
+ assert_eq!(write(&catalog, "after-the-holder-died").unwrap(), 1);
1703
+ let _ = fs::remove_dir_all(root);
1704
+ }
1705
+
1629
1706
  #[test]
1630
1707
  fn catalog_has_seven_tables_and_opens_additive_future_versions() {
1631
1708
  let root = temporary("schema");
@@ -0,0 +1,114 @@
1
+ //! A live writer on a persistent disk syncs at its checkpoints, not per part, and still
2
+ //! resumes from its last checkpoint after the machine loses its page cache.
3
+
4
+ use super::*;
5
+ use crate::disk::DiskClass;
6
+ use crate::unsynced::testing::{from_an_earlier_boot, markers, persistent_root, tear};
7
+
8
+ const TENSORS: usize = 6;
9
+
10
+ fn declaration() -> Declaration {
11
+ let mut declaration = super::tests::created_declaration();
12
+ let prototype = declaration.components[0].add[0].clone();
13
+ declaration.components[0].add = (0..TENSORS)
14
+ .map(|index| {
15
+ let mut tensor = prototype.clone();
16
+ tensor.key = format!("weight_{index}");
17
+ tensor.shape = vec![128];
18
+ tensor.parts[0].shape = vec![128];
19
+ tensor
20
+ })
21
+ .collect();
22
+ declaration.order = (0..TENSORS)
23
+ .map(|index| ("model".into(), format!("weight_{index}")))
24
+ .collect();
25
+ declaration.max_new_bytes = (TENSORS * 512) as u64;
26
+ declaration
27
+ }
28
+
29
+ fn store(name: &str) -> (Store, Meta) {
30
+ let store = Store::init(&persistent_root(name)).unwrap();
31
+ assert_eq!(store.disk_class(), DiskClass::Persistent);
32
+ let meta = Meta::open(&store).unwrap();
33
+ (store, meta)
34
+ }
35
+
36
+ fn add(store: &Store, meta: &Meta, id: &str, session: u64, writer: &Begin, index: usize) -> Part {
37
+ add_indexed_part(
38
+ store,
39
+ meta,
40
+ id,
41
+ session,
42
+ &writer.additions,
43
+ None,
44
+ ("model", &format!("weight_{index}"), "value"),
45
+ &mut [index as u8 + 1; 512].as_slice(),
46
+ )
47
+ .unwrap()
48
+ .part
49
+ }
50
+
51
+ #[test]
52
+ fn a_power_loss_after_a_checkpoint_resumes_from_it_to_the_same_manifest() {
53
+ let id = format!("sha256:{}", "6c".repeat(32));
54
+
55
+ // The reference: one uninterrupted writer.
56
+ let (clean, clean_meta) = store("group-commit-clean");
57
+ let writer = begin(&clean, &clean_meta, &id, 1, declaration(), None).unwrap();
58
+ let live = writer_store(&clean).unwrap();
59
+ for index in 0..TENSORS {
60
+ add(&live, &clean_meta, &id, 1, &writer, index);
61
+ }
62
+ let expected = commit(&live, &clean_meta, &id, 1).unwrap();
63
+ finish_writer(&live).unwrap();
64
+ release_guards(&clean_meta, Some(writer.writer_hold), writer.source_leases);
65
+ assert!(
66
+ markers(clean.root()).is_empty(),
67
+ "a committed writer retires its epoch"
68
+ );
69
+
70
+ // A checkpoint after three parts; three more admitted unsynced; then the machine goes
71
+ // down and the page cache holding those three is lost.
72
+ let (crashed, meta) = store("group-commit-crash");
73
+ let root = crashed.root().to_path_buf();
74
+ let writer = begin(&crashed, &meta, &id, 1, declaration(), None).unwrap();
75
+ let live = writer_store(&crashed).unwrap();
76
+ assert_eq!(
77
+ markers(&root).len(),
78
+ 1,
79
+ "a persistent writer admits under one epoch"
80
+ );
81
+ for index in 0..3 {
82
+ add(&live, &meta, &id, 1, &writer, index);
83
+ }
84
+ let (_, head) = checkpoint_progress(&live, &meta, &id, 1, "request", "model", None).unwrap();
85
+ let lost: Vec<Part> = (3..TENSORS)
86
+ .map(|index| add(&live, &meta, &id, 1, &writer, index))
87
+ .collect();
88
+ fence(&meta, &id, 1).unwrap();
89
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
90
+ drop(live);
91
+ from_an_earlier_boot(&markers(&root).pop().unwrap());
92
+ for segment in lost.iter().flat_map(Part::segments) {
93
+ tear(&crashed.blob_path(&segment.sha256));
94
+ }
95
+
96
+ // Opening repairs the torn objects; the resumed writer keeps what its checkpoint made
97
+ // durable and recomputes the rest.
98
+ let reopened = Store::open(&root).unwrap();
99
+ let meta = Meta::open(&reopened).unwrap();
100
+ let head = head.head.expect("the checkpoint names a head");
101
+ let writer = begin(&reopened, &meta, &id, 2, declaration(), Some(&head)).unwrap();
102
+ assert_eq!(completed(&meta, &id, 2).unwrap().0.len(), 3);
103
+ let live = writer_store(&reopened).unwrap();
104
+ for index in 3..TENSORS {
105
+ add(&live, &meta, &id, 2, &writer, index);
106
+ }
107
+ let resumed = commit(&live, &meta, &id, 2).unwrap();
108
+ finish_writer(&live).unwrap();
109
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
110
+ assert_eq!(resumed.manifest, expected.manifest);
111
+ assert!(markers(&root).is_empty());
112
+ std::fs::remove_dir_all(clean.root()).unwrap();
113
+ std::fs::remove_dir_all(&root).unwrap();
114
+ }
@@ -123,6 +123,20 @@ fn io(error: std::io::Error) -> Refusal {
123
123
  fn directory(store: &Store) -> PathBuf {
124
124
  store.root().join("roots/derived")
125
125
  }
126
+
127
+ /// A root must survive the machine on every disk but an ephemeral one, whose Store dies with
128
+ /// its container. A parts journal follows the Store's admissions instead ([`Store::flushes`]):
129
+ /// under a writer's epoch its lines become durable with that writer's next checkpoint.
130
+ fn durable(store: &Store) -> bool {
131
+ store.disk_class() != crate::disk::DiskClass::Ephemeral
132
+ }
133
+
134
+ /// Create the root directory; true when this call created it (so its parents need a sync).
135
+ fn create_directory(store: &Store) -> Result<bool> {
136
+ let created = !directory(store).is_dir();
137
+ fs::create_dir_all(directory(store)).map_err(io)?;
138
+ Ok(created)
139
+ }
126
140
  fn path(store: &Store, transaction: &str) -> Result<PathBuf> {
127
141
  let transaction = super::transaction_id(transaction)?;
128
142
  Ok(directory(store).join(format!(
@@ -150,7 +164,7 @@ pub(super) fn append_parts(store: &Store, transaction: &str, objects: &[ObjectRe
150
164
  return Ok(());
151
165
  }
152
166
  let path = parts_path(store, transaction)?;
153
- fs::create_dir_all(directory(store)).map_err(io)?;
167
+ let parents = create_directory(store)?;
154
168
  let created = !path.exists();
155
169
  let mut file = OpenOptions::new()
156
170
  .read(true)
@@ -170,11 +184,12 @@ pub(super) fn append_parts(store: &Store, transaction: &str, objects: &[ObjectRe
170
184
  vec![b'\n']
171
185
  };
172
186
  bytes.extend(parts_line(objects));
173
- file.write_all(&bytes)
174
- .and_then(|()| file.sync_data())
175
- .map_err(io)?;
176
- if created {
177
- sync_directories(store)?;
187
+ file.write_all(&bytes).map_err(io)?;
188
+ if store.flushes() {
189
+ file.sync_data().map_err(io)?;
190
+ if created {
191
+ sync_directories(store, parents)?;
192
+ }
178
193
  }
179
194
  Ok(())
180
195
  }
@@ -182,7 +197,7 @@ pub(super) fn append_parts(store: &Store, transaction: &str, objects: &[ObjectRe
182
197
  /// Replace the journal with exactly these objects (a writer's merged progress).
183
198
  pub(super) fn rewrite_parts(store: &Store, transaction: &str, objects: &[ObjectRef]) -> Result<()> {
184
199
  let path = parts_path(store, transaction)?;
185
- fs::create_dir_all(directory(store)).map_err(io)?;
200
+ let parents = create_directory(store)?;
186
201
  let temporary = path.with_extension(format!("parts-tmp-{}", crate::meta::now_nanos_unique()));
187
202
  let mut file = OpenOptions::new()
188
203
  .write(true)
@@ -194,11 +209,15 @@ pub(super) fn rewrite_parts(store: &Store, transaction: &str, objects: &[ObjectR
194
209
  } else {
195
210
  parts_line(objects)
196
211
  };
197
- file.write_all(&bytes)
198
- .and_then(|()| file.sync_all())
199
- .map_err(io)?;
212
+ file.write_all(&bytes).map_err(io)?;
213
+ if store.flushes() {
214
+ file.sync_all().map_err(io)?;
215
+ }
200
216
  fs::rename(&temporary, &path).map_err(io)?;
201
- sync_directories(store)
217
+ if store.flushes() {
218
+ sync_directories(store, parents)?;
219
+ }
220
+ Ok(())
202
221
  }
203
222
 
204
223
  /// Every object named by any transaction's parts journal.
@@ -235,12 +254,15 @@ pub(super) fn journaled(store: &Store) -> Result<Vec<ObjectRef>> {
235
254
  Ok(objects)
236
255
  }
237
256
 
238
- fn sync_directories(store: &Store) -> Result<()> {
239
- for directory in [
257
+ /// A rename needs its own directory synced; the parents only when this write created it.
258
+ fn sync_directories(store: &Store, parents: bool) -> Result<()> {
259
+ let own = [directory(store)];
260
+ let all = [
240
261
  directory(store),
241
262
  store.root().join("roots"),
242
263
  store.root().to_path_buf(),
243
- ] {
264
+ ];
265
+ for directory in if parents { &all[..] } else { &own[..] } {
244
266
  File::open(directory)
245
267
  .and_then(|file| file.sync_all())
246
268
  .map_err(io)?;
@@ -301,25 +323,20 @@ pub(super) fn write(store: &Store, root: &Root) -> Result<()> {
301
323
  return refuse(Code::COUNT_CAP, "private derived root exceeds its byte cap");
302
324
  }
303
325
  let path = path(store, &root.transaction)?;
304
- fs::create_dir_all(directory(store)).map_err(io)?;
326
+ let parents = create_directory(store)?;
305
327
  let temporary = path.with_extension(format!("tmp-{}", crate::meta::now_nanos_unique()));
306
328
  let mut file = OpenOptions::new()
307
329
  .write(true)
308
330
  .create_new(true)
309
331
  .open(&temporary)
310
332
  .map_err(io)?;
311
- file.write_all(&bytes)
312
- .and_then(|()| file.sync_all())
313
- .map_err(io)?;
333
+ file.write_all(&bytes).map_err(io)?;
334
+ if durable(store) {
335
+ file.sync_all().map_err(io)?;
336
+ }
314
337
  fs::rename(&temporary, &path).map_err(io)?;
315
- for directory in [
316
- directory(store),
317
- store.root().join("roots"),
318
- store.root().to_path_buf(),
319
- ] {
320
- File::open(directory)
321
- .and_then(|file| file.sync_all())
322
- .map_err(io)?;
338
+ if durable(store) {
339
+ sync_directories(store, parents)?;
323
340
  }
324
341
  Ok(())
325
342
  }
@@ -396,6 +413,7 @@ pub(super) fn remove(store: &Store, transaction: &str) -> Result<()> {
396
413
  Err(error) => return Err(io(error)),
397
414
  }
398
415
  match fs::remove_file(path(store, transaction)?) {
416
+ Ok(()) if !durable(store) => Ok(()),
399
417
  Ok(()) => File::open(directory(store))
400
418
  .and_then(|file| file.sync_all())
401
419
  .map_err(io),
@@ -2346,6 +2346,19 @@ fn planned_row(
2346
2346
  })
2347
2347
  }
2348
2348
 
2349
+ /// The Store a live writer admits through. On a persistent disk its parts and progress
2350
+ /// documents are not synced one by one: each checkpoint (and the commit) makes everything
2351
+ /// admitted since the previous one durable at once, before the root names it, so a crash
2352
+ /// loses at most the work after the last checkpoint. An ephemeral disk needs no sync at all.
2353
+ pub fn writer_store(store: &Store) -> Result<Store> {
2354
+ Ok(store.syncing(crate::unsynced::Epoch::begin_every(store, u64::MAX)?))
2355
+ }
2356
+
2357
+ /// The writer is done: sync anything admitted since its last checkpoint and retire its epoch.
2358
+ pub fn finish_writer(store: &Store) -> Result<()> {
2359
+ store.finish_admitted()
2360
+ }
2361
+
2349
2362
  pub fn begin(
2350
2363
  store: &Store,
2351
2364
  meta: &Meta,
@@ -2810,6 +2823,8 @@ impl CheckpointCursor {
2810
2823
  let Some(mut root) = root else {
2811
2824
  return Ok(None);
2812
2825
  };
2826
+ // Everything the new head names is durable before the root publishes it.
2827
+ store.sync_admitted()?;
2813
2828
  root.checkpoint = extended.head.head.clone();
2814
2829
  roots::write(store, &root)?;
2815
2830
  self.tip
@@ -2935,6 +2950,7 @@ pub fn checkpoint_progress(
2935
2950
  objects,
2936
2951
  None,
2937
2952
  )?;
2953
+ store.sync_admitted()?;
2938
2954
  let mut txn = meta.txn_for(transaction)?;
2939
2955
  let row = open_row(&mut txn.rows, transaction, writer_session)?;
2940
2956
  retain_progress(store, row, exported.head.head.as_ref())?;
@@ -3609,6 +3625,7 @@ pub fn commit(
3609
3625
  walk.require_held(store, &crate::ingest::custody::live(store.root())?)?;
3610
3626
  let (inherited_payload_objects, inherited_payload_bytes) =
3611
3627
  inherit_observation(&header, &row_before)?;
3628
+ store.sync_admitted()?;
3612
3629
 
3613
3630
  let mut txn = meta.txn_for(&transaction)?;
3614
3631
  let row = open_row(&mut txn.rows, &transaction, writer_session)?;
@@ -4598,3 +4615,6 @@ mod companion_tests;
4598
4615
 
4599
4616
  #[cfg(test)]
4600
4617
  mod part_rows_tests;
4618
+
4619
+ #[cfg(test)]
4620
+ mod group_commit_tests;
@@ -222,12 +222,7 @@ impl Meta {
222
222
 
223
223
  fn connection(&self) -> Result<Connection> {
224
224
  let connection = Connection::open(&self.db).map_err(|error| sql("open catalog", error))?;
225
- connection
226
- .busy_timeout(std::time::Duration::from_secs(5))
227
- .map_err(|error| sql("set catalog busy timeout", error))?;
228
- connection
229
- .execute_batch("PRAGMA foreign_keys=ON; PRAGMA synchronous=FULL;")
230
- .map_err(|error| sql("configure catalog", error))?;
225
+ crate::catalog::configure(&connection, &self.db)?;
231
226
  Ok(connection)
232
227
  }
233
228