1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
//! Cross-process read-modify-write for a whole-file JSON document.
//!
//! Why: several trusty crates persist a small JSON document that multiple
//! independent PROCESSES mutate — `trusty-mpm`'s `projects.json` registry,
//! `trusty-gworkspace`'s `tokens.json` (issue #3502), and the dual worktree
//! registry epic #4207 will reconcile. Every one of them is a
//! load → mutate → save-the-whole-file cycle. With no cross-process
//! serialisation, two writers that interleave read/read/write/write silently
//! lose one of the two updates and BOTH callers see success; worse, if they
//! share one temp path they can publish a half-written document and corrupt the
//! file outright. An in-process `Mutex` cannot fix either failure because the
//! writers are separate processes. This module is the single implementation of
//! that critical section so each call site inherits the fix instead of
//! re-deriving it.
//! What: [`update`] takes an advisory exclusive lock on a `<path>.lock` sidecar,
//! re-reads the document from disk under that lock (never trusting a caller's
//! possibly-stale copy), applies the caller's mutation, and publishes the result
//! atomically — unique temp file, `fsync`, `rename`, `fsync` of the parent
//! directory — before releasing the lock.
//! Test: `cargo test -p trusty-common --features unconditional-only --
//! json_rmw::tests`.
//!
//! # Atomicity contract
//!
//! Guarantees a caller may rely on:
//!
//! 1. **Serialisation.** The read, the mutation and the write happen while one
//! writer holds an exclusive advisory lock, so no other [`update`] on the
//! same path can observe or overwrite the intermediate state. The lock is
//! `flock(2)`-style: it is held by the open file description, so it
//! serialises separate processes AND separate threads that each call
//! [`update`], on Unix and Windows alike.
//! 2. **All-or-nothing publish.** Readers of `path` see either the complete
//! previous document or the complete new one, never a partial write: the
//! document is built in a temp file and moved into place with `rename`.
//! That is the whole of this guarantee — the temp name (pid plus a
//! nanosecond stamp) is scratch-path hygiene, NOT a second line of defence
//! for a writer that bypasses the lock. An earlier version of this comment
//! claimed it was; #4906's review falsified that experimentally, with 16
//! threads landing on a single nanosecond value and colliding. Only
//! guarantee 1 keeps concurrent writers apart.
//! 3. **Never fail open.** Every failure — lock acquisition, read, parse,
//! serialise, write, rename — returns `Err` and leaves `path` byte-for-byte
//! unchanged. There is no path on which a failed update advances state, and
//! an unreadable-but-present file is never silently replaced with a default
//! (empty) document; only a genuinely absent file starts from
//! [`Default`].
//! 4. **Crash safety.** A writer killed at any point leaves either the previous
//! document intact or an orphaned `*.tmp` file that no reader consults.
//!
//! Explicitly NOT guaranteed:
//!
//! - **Advisory, not mandatory.** A process that writes `path` without going
//! through [`update`] is not blocked. Every writer of a given file must use
//! this entry point.
//! - **Not reentrant.** [`update`] must not be called from inside another
//! [`update`] closure on the same path: the second acquisition uses a
//! different file descriptor and will self-deadlock.
//! - **Blocking.** Lock acquisition blocks the calling thread. Async callers
//! must run [`update`] on a blocking-safe thread (e.g.
//! `tokio::task::spawn_blocking`).
use File;
use Write;
use ;
use ;
use Serialize;
use DeserializeOwned;
/// Failure modes of a locked JSON read-modify-write.
///
/// Why: callers must be able to tell a lock-contention/permission problem apart
/// from a corrupt document, because the remedies differ (retry / operator
/// intervention vs. restore the file). Every variant carries the path it
/// concerns so a multi-file caller can report which document failed.
/// What: one variant per stage of the cycle — lock, I/O, (de)serialisation.
/// `Display`/`Error` are implemented by hand rather than derived: this crate
/// keeps `thiserror` behind an optional feature (`default = []`), and `json_rmw`
/// is an unconditional module, so deriving would force `thiserror` into every
/// minimal build of `trusty-common`. Crates that own their own error types
/// should still prefer `thiserror` and convert via [`From`].
/// Test: `update_lock_path_unopenable_errors`, `update_corrupt_file_errors`.
/// Sidecar lock-file path for `path`.
///
/// #5344: re-export of [`crate::file_lock::lock_path`], which now owns the
/// lock primitive so `indexes.toml`'s TOML writers share one implementation
/// with this module's JSON ones.
pub use cratelock_path;
/// Scratch path for one publish attempt — unique per writer and per attempt.
///
/// Why: a SHARED temp name is itself a corruption bug. `trusty-mpm` used a fixed
/// `projects.json.tmp`: two processes writing it at once interleaved into one
/// file and then renamed the mangled result over the real registry, producing a
/// `projects.json` that no longer parsed. Uniqueness per attempt removes that
/// class of failure entirely, independently of the lock.
/// What: `<file_name>.<pid>.<nanos>.tmp`, alongside the target so the publish is
/// a same-filesystem `rename`.
/// Read and parse `path`, treating only genuine absence as an empty document.
///
/// Why: this is the "never fail open" hinge. If any I/O error were treated as
/// "file absent", a transient permission or hardware fault would hand the caller
/// an empty `T`, and the publish at the end of [`update`] would overwrite a
/// perfectly good document with nothing — a total data loss dressed up as
/// success.
/// What: `NotFound` yields `T::default()`; every other error propagates.
/// Publish `bytes` at `path` atomically: unique temp, fsync, rename, fsync dir.
///
/// Why: `rename(2)` within a filesystem is atomic, so a reader sees the old file
/// or the new one and never a partial write. The `fsync` of the temp file before
/// the rename is what makes that true across a power loss rather than only
/// across a process crash; the `fsync` of the directory makes the rename itself
/// durable.
/// What: writes to [`temp_path`], syncs it, renames it over `path`, then syncs
/// the parent directory. Any failure removes the temp file and returns `Err`
/// with `path` untouched.
/// Test: `update_publishes_atomically_leaving_no_temp`,
/// `update_write_failure_leaves_original_intact`.
/// Run a read-modify-write on the JSON document at `path` under an exclusive
/// cross-process lock.
///
/// Why: see the module-level rationale — this is the one place the
/// load → mutate → save cycle is made safe against concurrent writers, so
/// callers stop hand-rolling (and getting wrong) their own version of it.
/// What: acquires the exclusive advisory lock on [`lock_path`] (blocking until
/// it is available), re-reads and parses `path` under that lock (an absent file
/// starts from [`Default`]; an unreadable or malformed one is an error, never a
/// silent reset), calls `f` with the freshly-read value, and — only when `f`
/// returns `Ok` — publishes the mutated document via [`publish_atomic`]. When
/// `f` returns `Err` the document is left byte-for-byte unchanged and the error
/// is propagated, so a rejected mutation cannot advance state. The lock is
/// released by RAII on every exit path, including panics.
///
/// `f`'s error type `E` need only be constructible from [`JsonRmwError`], which
/// lets a caller keep its own domain error as the single return type.
///
/// Blocking: acquisition blocks the calling thread; async callers must wrap this
/// in `tokio::task::spawn_blocking`. Not reentrant — see the module docs.
/// Test: `update_serialises_concurrent_threads`,
/// `update_creates_file_when_absent`, `update_closure_error_does_not_write`,
/// `update_lock_path_unopenable_errors`.