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
//! An exclusive, crash-releasing claim on one inbox entry (#5192, ADR-0034 ยง5).
//!
//! Why: the drain has to answer two questions that look alike and are not. "Is
//! another drainer already working this entry?" must be answered without
//! double-processing it, and "did the drainer that was working this entry
//! die?" must be answered without stranding it. A claim marker written to disk
//! answers the first and gets the second wrong forever โ a crash leaves the
//! marker behind and the delivery is never picked up again, which is the silent
//! loss ADR-0034 exists to remove, arriving one hop later.
//!
//! What: `flock(LOCK_EX | LOCK_NB)` on the entry file itself. The kernel
//! releases it when the holding fd closes, which includes the process being
//! SIGKILLed โ so "crashed mid-processing" and "finished" are the same state to
//! everyone else, and that state is *claimable*. A busy entry answers
//! `EWOULDBLOCK` immediately rather than blocking, so one slow review never
//! stalls the rest of the drain.
//!
//! ๐ด The `nlink` check after the lock is not defensive padding. Two drainers
//! can both `open` the same path; the first locks, processes, unlinks and
//! releases; the second then acquires the lock on an inode with no name and
//! would process a delivery that has already been handled. `st_nlink == 0` is
//! how a held fd learns its file is gone, and it is what makes the claim a
//! once-only claim rather than a mutual-exclusion window.
//!
//! Test: `tests.rs` โ `claim_*`.
use File;
use Read as _;
use AsRawFd as _;
use MetadataExt as _;
use ;
use RelayDelivery;
/// Everything that can stop a claim attempt from producing an answer.
///
/// Deliberately small: "someone else holds it", "it is already gone" and "its
/// contents are not a delivery" are all valid ANSWERS ([`ClaimOutcome`]), not
/// errors. Only a failure that leaves the drain unable to say anything about
/// the entry is an error.
/// What one [`Claim::try_acquire`] found.
///
/// Why: three of these four are ordinary, expected states under concurrency and
/// after a crash, and the drain counts them differently. Collapsing them into
/// `Option<Claim>` is what makes a report say `skipped: 3` with no way to tell
/// a busy entry from a poisoned one.
/// Test: `claim_is_exclusive_between_two_holders`,
/// `claim_of_a_vanished_entry_reports_vanished`,
/// `claim_of_an_undecodable_entry_reports_undecodable`.
/// Exclusive ownership of one inbox entry, for as long as this value lives.
///
/// Why: the drain's whole safety argument is "the entry is removed only after
/// the pipeline accepted it, and until then exactly one drainer may touch it".
/// This type is the second half of that sentence, and its `Drop` is what makes
/// a panic or a SIGKILL indistinguishable from a clean release.
/// What: the decoded delivery plus the `flock`-holding fd. Not `Clone`, not
/// `Send`-hostile โ a `File` is `Send`, so a claim may be held across an await.
/// Test: `claim_is_exclusive_between_two_holders`,
/// `claim_is_released_when_the_holder_is_dropped`.
/// Does the file behind this fd still have a name?
///
/// Why: the TOCTOU check the whole once-only claim rests on. Two drainers can
/// both `open` one path; the first locks, processes, unlinks and releases, and
/// the second then wins a lock on an inode with no name. `st_nlink == 0` is how
/// the holder of an fd learns that happened.
///
/// It is a free function rather than three inline lines because the branch is
/// otherwise unreachable from a test โ the unlink has to land inside
/// [`Claim::try_acquire`]'s own open-to-stat window, which no caller can
/// schedule. Calling the predicate directly against a deliberately unlinked fd
/// proves the same condition without pretending to reproduce the race.
///
/// # Errors
///
/// When the fd cannot be stat'd.
///
/// Test: `entry_is_still_linked_is_false_for_an_unlinked_fd`,
/// `entry_is_still_linked_is_true_for_a_live_entry`.