dsh-ops 0.0.0-stage → 0.2.1

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 (139) hide show
  1. package/CHANGELOG.md +189 -0
  2. package/LICENSE +30 -0
  3. package/NOTICE +106 -0
  4. package/PROVENANCE.md +417 -0
  5. package/README.en.md +121 -0
  6. package/README.md +113 -2
  7. package/README.zh.md +114 -0
  8. package/bin/dsh-ops.mjs +1216 -0
  9. package/cordis.patch.yml +160 -0
  10. package/docs/manual-validation.md +53 -0
  11. package/docs/schema-baseline.json +64 -0
  12. package/docs/schema-current.json +84 -0
  13. package/docs/schema-measurement.md +17 -0
  14. package/dsh-plugin.json +88 -0
  15. package/icon.svg +12 -0
  16. package/lib/binary.js +409 -0
  17. package/lib/config.js +198 -0
  18. package/lib/handshake.js +252 -0
  19. package/lib/index.js +108 -0
  20. package/lib/jobs.js +42 -0
  21. package/lib/policy.js +64 -0
  22. package/lib/presentation.js +63 -0
  23. package/lib/profile-install.js +61 -0
  24. package/lib/rust.js +194 -0
  25. package/lib/session-shells.js +78 -0
  26. package/lib/shells.js +993 -0
  27. package/lib/tools.js +657 -0
  28. package/locale/en.json +6 -0
  29. package/locale/zh.json +6 -0
  30. package/package.json +114 -4
  31. package/vendor/fastctx/Cargo.lock +3210 -0
  32. package/vendor/fastctx/Cargo.toml +94 -0
  33. package/vendor/fastctx/FORK.md +119 -0
  34. package/vendor/fastctx/LICENSE-APACHE +201 -0
  35. package/vendor/fastctx/NOTICE +40 -0
  36. package/vendor/fastctx/README.md +439 -0
  37. package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
  38. package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
  39. package/vendor/fastctx/UPSTREAM.md +49 -0
  40. package/vendor/fastctx/build.rs +413 -0
  41. package/vendor/fastctx/src/background_status.rs +403 -0
  42. package/vendor/fastctx/src/binary.rs +75 -0
  43. package/vendor/fastctx/src/bounded_sort.rs +500 -0
  44. package/vendor/fastctx/src/budget.rs +781 -0
  45. package/vendor/fastctx/src/cli/mod.rs +110 -0
  46. package/vendor/fastctx/src/context_guard.rs +289 -0
  47. package/vendor/fastctx/src/control/mod.rs +6 -0
  48. package/vendor/fastctx/src/control/paths.rs +49 -0
  49. package/vendor/fastctx/src/control/settings.rs +753 -0
  50. package/vendor/fastctx/src/control/transaction.rs +531 -0
  51. package/vendor/fastctx/src/edit/document.rs +535 -0
  52. package/vendor/fastctx/src/edit/locks.rs +371 -0
  53. package/vendor/fastctx/src/edit/mod.rs +213 -0
  54. package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
  55. package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
  56. package/vendor/fastctx/src/edit/private_storage.rs +234 -0
  57. package/vendor/fastctx/src/edit/replace.rs +1030 -0
  58. package/vendor/fastctx/src/edit_server.rs +53 -0
  59. package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
  60. package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
  61. package/vendor/fastctx/src/encoding.rs +1118 -0
  62. package/vendor/fastctx/src/file_executor.rs +1151 -0
  63. package/vendor/fastctx/src/file_snapshot.rs +1491 -0
  64. package/vendor/fastctx/src/glob_filter.rs +98 -0
  65. package/vendor/fastctx/src/glob_tool.rs +653 -0
  66. package/vendor/fastctx/src/grep_sink.rs +1162 -0
  67. package/vendor/fastctx/src/grep_tool.rs +2449 -0
  68. package/vendor/fastctx/src/lib.rs +45 -0
  69. package/vendor/fastctx/src/main.rs +15 -0
  70. package/vendor/fastctx/src/model.rs +51 -0
  71. package/vendor/fastctx/src/model_guidance.rs +62 -0
  72. package/vendor/fastctx/src/operation.rs +356 -0
  73. package/vendor/fastctx/src/ordered_window.rs +1235 -0
  74. package/vendor/fastctx/src/os_environment.rs +414 -0
  75. package/vendor/fastctx/src/path_codec.rs +850 -0
  76. package/vendor/fastctx/src/paths.rs +244 -0
  77. package/vendor/fastctx/src/process_identity.rs +763 -0
  78. package/vendor/fastctx/src/process_policy.rs +74 -0
  79. package/vendor/fastctx/src/read_tool/batch.rs +496 -0
  80. package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
  81. package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
  82. package/vendor/fastctx/src/read_tool/mod.rs +245 -0
  83. package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
  84. package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
  85. package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
  86. package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
  87. package/vendor/fastctx/src/render_plan.rs +468 -0
  88. package/vendor/fastctx/src/runtime/activity.rs +159 -0
  89. package/vendor/fastctx/src/runtime/hosts.rs +99 -0
  90. package/vendor/fastctx/src/runtime/journal.rs +556 -0
  91. package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
  92. package/vendor/fastctx/src/runtime/mod.rs +746 -0
  93. package/vendor/fastctx/src/runtime/protocol.rs +296 -0
  94. package/vendor/fastctx/src/runtime/session.rs +536 -0
  95. package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
  96. package/vendor/fastctx/src/search_parallelism.rs +106 -0
  97. package/vendor/fastctx/src/search_text.rs +227 -0
  98. package/vendor/fastctx/src/server.rs +359 -0
  99. package/vendor/fastctx/src/server_manifest.rs +468 -0
  100. package/vendor/fastctx/src/server_support.rs +826 -0
  101. package/vendor/fastctx/src/session.rs +629 -0
  102. package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
  103. package/vendor/fastctx/src/shell/bash.rs +263 -0
  104. package/vendor/fastctx/src/shell/buffer.rs +108 -0
  105. package/vendor/fastctx/src/shell/encoding.rs +403 -0
  106. package/vendor/fastctx/src/shell/foreground.rs +115 -0
  107. package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
  108. package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
  109. package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
  110. package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
  111. package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
  112. package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
  113. package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
  114. package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
  115. package/vendor/fastctx/src/shell/mod.rs +345 -0
  116. package/vendor/fastctx/src/shell/normalize.rs +389 -0
  117. package/vendor/fastctx/src/shell/output.rs +406 -0
  118. package/vendor/fastctx/src/shell/process.rs +493 -0
  119. package/vendor/fastctx/src/shell_server.rs +156 -0
  120. package/vendor/fastctx/src/skip_report.rs +83 -0
  121. package/vendor/fastctx/src/stdio_transport.rs +177 -0
  122. package/vendor/fastctx/src/tool_schema.rs +204 -0
  123. package/vendor/fastctx/src/traversal.rs +846 -0
  124. package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
  125. package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
  126. package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
  127. package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
  128. package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
  129. package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
  130. package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
  131. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
  132. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
  133. package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
  134. package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
  135. package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
  136. package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
  137. package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
  138. package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
  139. package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
@@ -0,0 +1,1235 @@
1
+ //! Bounded, runner-based ordered execution for grep candidates.
2
+
3
+ use crate::file_executor::{BurstEnvelope, BurstPermit, BurstUse, GrepGlobExecutor};
4
+ use crate::operation::{EpochGuard, OpError, OperationCtx, WorkCtx, WorkStop};
5
+ use parking_lot::{Condvar, Mutex};
6
+ use std::any::Any;
7
+ #[cfg(test)]
8
+ use std::collections::BTreeSet;
9
+ use std::ops::ControlFlow;
10
+ use std::panic::{AssertUnwindSafe, catch_unwind};
11
+ use std::sync::Arc;
12
+ #[cfg(test)]
13
+ use std::sync::atomic::AtomicUsize;
14
+ use std::sync::atomic::{AtomicU64, Ordering};
15
+ use std::time::Duration;
16
+
17
+ const FRONTIER_WAIT_SLICE: Duration = Duration::from_millis(10);
18
+
19
+ /// Failures owned by the ordered execution state rather than by one candidate.
20
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
21
+ pub(crate) enum OrderedError {
22
+ Cancelled,
23
+ GenerationOverflow,
24
+ }
25
+
26
+ impl From<OpError> for OrderedError {
27
+ fn from(_: OpError) -> Self {
28
+ Self::Cancelled
29
+ }
30
+ }
31
+
32
+ /// Reducer-side control over the generation whose speculative results are valid.
33
+ pub(crate) struct OrderedReducer<R> {
34
+ shared: Arc<RequestSpecShared<R>>,
35
+ }
36
+
37
+ impl<R> OrderedReducer<R> {
38
+ /// Retires queued/running speculation and immediately drops every ready envelope.
39
+ pub(crate) fn retire_generation(&self) -> Result<(), OrderedError> {
40
+ self.shared.retire_generation(true)
41
+ }
42
+ }
43
+
44
+ /// Runs independent work speculatively while delivering each reachable result in order.
45
+ pub(crate) fn for_each_ordered<T, R, W, P, C>(
46
+ items: Arc<[T]>,
47
+ operation: OperationCtx,
48
+ executor: Arc<GrepGlobExecutor>,
49
+ worker: W,
50
+ panic_value: P,
51
+ mut consume: C,
52
+ ) -> Result<(), OrderedError>
53
+ where
54
+ T: Send + Sync + 'static,
55
+ R: Send + 'static,
56
+ W: Fn(usize, &T, &WorkCtx) -> Result<R, WorkStop> + Send + Sync + 'static,
57
+ P: Fn(usize, Box<dyn Any + Send>) -> R + Send + Sync + 'static,
58
+ C: FnMut(usize, R, &OrderedReducer<R>) -> ControlFlow<()>,
59
+ {
60
+ let shared = Arc::new(RequestSpecShared::new(executor.parallelism(), items.len()));
61
+ let _retire_on_exit = RetireOnDrop {
62
+ shared: Arc::clone(&shared),
63
+ };
64
+ let reducer = OrderedReducer {
65
+ shared: Arc::clone(&shared),
66
+ };
67
+ let worker = Arc::new(worker);
68
+ let panic_value = Arc::new(panic_value);
69
+
70
+ while shared.next_reduce() < items.len() {
71
+ operation.check()?;
72
+ fill_epoch(
73
+ &shared,
74
+ &items,
75
+ &operation,
76
+ &executor,
77
+ &worker,
78
+ &panic_value,
79
+ );
80
+
81
+ let index = shared.next_reduce();
82
+ let value = match shared.take_frontier(index, &operation)? {
83
+ Frontier::Ready(envelope) => envelope.into_value(),
84
+ Frontier::Inline => {
85
+ let work = operation.inline_work();
86
+ match catch_unwind(AssertUnwindSafe(|| worker(index, &items[index], &work))) {
87
+ Ok(Ok(value)) => value,
88
+ Ok(Err(WorkStop::RequestCancelled)) => return Err(OrderedError::Cancelled),
89
+ Ok(Err(WorkStop::EpochRetired)) => {
90
+ unreachable!("coordinator work never carries an epoch")
91
+ }
92
+ Err(payload) => panic_value(index, payload),
93
+ }
94
+ }
95
+ };
96
+
97
+ #[cfg(test)]
98
+ operation.stage(crate::operation::TestStage::OrderedReduce);
99
+ operation.check()?;
100
+ if consume(index, value, &reducer).is_break() {
101
+ return Ok(());
102
+ }
103
+ shared.advance(index);
104
+ }
105
+ Ok(())
106
+ }
107
+
108
+ fn fill_epoch<T, R, W, P>(
109
+ shared: &Arc<RequestSpecShared<R>>,
110
+ items: &Arc<[T]>,
111
+ operation: &OperationCtx,
112
+ executor: &Arc<GrepGlobExecutor>,
113
+ worker: &Arc<W>,
114
+ panic_value: &Arc<P>,
115
+ ) where
116
+ T: Send + Sync + 'static,
117
+ R: Send + 'static,
118
+ W: Fn(usize, &T, &WorkCtx) -> Result<R, WorkStop> + Send + Sync + 'static,
119
+ P: Fn(usize, Box<dyn Any + Send>) -> R + Send + Sync + 'static,
120
+ {
121
+ let Some((generation, vacancies)) = shared.fill_snapshot() else {
122
+ return;
123
+ };
124
+ let maximum = vacancies.min(executor.extra_capacity());
125
+ let tickets = executor.try_runner_tickets(maximum);
126
+ for ticket in tickets {
127
+ #[cfg(test)]
128
+ operation.stage(crate::operation::TestStage::RunnerQueued);
129
+ if operation.check().is_err() {
130
+ drop(ticket);
131
+ break;
132
+ }
133
+ let shared_for_job = Arc::clone(shared);
134
+ let items_for_job = Arc::clone(items);
135
+ let operation_for_job = operation.clone();
136
+ let executor_for_job = Arc::clone(executor);
137
+ let worker_for_job = Arc::clone(worker);
138
+ let panic_value_for_job = Arc::clone(panic_value);
139
+ let job = move || {
140
+ #[cfg(test)]
141
+ operation_for_job.stage(crate::operation::TestStage::RunnerStarted);
142
+ let work =
143
+ WorkCtx::speculative(operation_for_job, shared_for_job.epoch_guard(generation));
144
+ if work.check().is_err() {
145
+ return;
146
+ }
147
+ #[cfg(test)]
148
+ work.stage(crate::operation::TestStage::BeforeBurstAcquire);
149
+ if work.check().is_err() {
150
+ return;
151
+ }
152
+ let Some(permit) = executor_for_job.try_burst(BurstUse::SearchSpeculation) else {
153
+ return;
154
+ };
155
+ if work.check().is_err() {
156
+ return;
157
+ }
158
+ #[cfg(test)]
159
+ work.stage(crate::operation::TestStage::BeforeCandidateClaim);
160
+ if work.check().is_err() {
161
+ return;
162
+ }
163
+ let Some(lease) = shared_for_job.try_claim(generation, permit) else {
164
+ return;
165
+ };
166
+ let index = lease.index();
167
+ #[cfg(test)]
168
+ work.stage(crate::operation::TestStage::AfterCandidateClaim);
169
+ if work.check().is_err() {
170
+ return;
171
+ }
172
+
173
+ let value = match catch_unwind(AssertUnwindSafe(|| {
174
+ worker_for_job(index, &items_for_job[index], &work)
175
+ })) {
176
+ Ok(Ok(value)) => value,
177
+ Ok(Err(WorkStop::RequestCancelled | WorkStop::EpochRetired)) => return,
178
+ Err(payload) => panic_value_for_job(index, payload),
179
+ };
180
+ if work.check().is_err() {
181
+ return;
182
+ }
183
+ #[cfg(test)]
184
+ work.stage(crate::operation::TestStage::BeforeReadyPublish);
185
+ if work.check().is_err() {
186
+ return;
187
+ }
188
+ lease.publish(value);
189
+ };
190
+ if let Err((ticket, job)) = executor.try_spawn(ticket, job) {
191
+ drop(ticket);
192
+ drop(job);
193
+ break;
194
+ }
195
+ }
196
+ }
197
+
198
+ struct RetireOnDrop<R> {
199
+ shared: Arc<RequestSpecShared<R>>,
200
+ }
201
+
202
+ impl<R> Drop for RetireOnDrop<R> {
203
+ fn drop(&mut self) {
204
+ let _ = self.shared.retire_generation(false);
205
+ }
206
+ }
207
+
208
+ enum Frontier<R> {
209
+ Ready(BurstEnvelope<R>),
210
+ Inline,
211
+ }
212
+
213
+ struct RequestSpecShared<R> {
214
+ width: usize,
215
+ candidate_count: usize,
216
+ generation: Arc<AtomicU64>,
217
+ next_runner_id: AtomicU64,
218
+ state: Mutex<RequestSpecState<R>>,
219
+ changed: Condvar,
220
+ #[cfg(test)]
221
+ lease_ledger: LeaseLedger,
222
+ #[cfg(test)]
223
+ publish_fault: AtomicUsize,
224
+ }
225
+
226
+ struct RequestSpecState<R> {
227
+ generation: u64,
228
+ next_reduce: usize,
229
+ accepting: bool,
230
+ live_slots: usize,
231
+ #[cfg(test)]
232
+ peak_slots: usize,
233
+ slots: Box<[Option<SpecSlot<R>>]>,
234
+ }
235
+
236
+ enum SpecSlot<R> {
237
+ RunningExtra {
238
+ index: usize,
239
+ generation: u64,
240
+ runner_id: u64,
241
+ },
242
+ Ready {
243
+ index: usize,
244
+ generation: u64,
245
+ envelope: BurstEnvelope<R>,
246
+ },
247
+ }
248
+
249
+ impl<R> RequestSpecShared<R> {
250
+ fn new(width: usize, candidate_count: usize) -> Self {
251
+ let width = width.max(1);
252
+ Self {
253
+ width,
254
+ candidate_count,
255
+ generation: Arc::new(AtomicU64::new(0)),
256
+ next_runner_id: AtomicU64::new(0),
257
+ state: Mutex::new(RequestSpecState {
258
+ generation: 0,
259
+ next_reduce: 0,
260
+ accepting: true,
261
+ live_slots: 0,
262
+ #[cfg(test)]
263
+ peak_slots: 0,
264
+ slots: (0..width)
265
+ .map(|_| None)
266
+ .collect::<Vec<_>>()
267
+ .into_boxed_slice(),
268
+ }),
269
+ changed: Condvar::new(),
270
+ #[cfg(test)]
271
+ lease_ledger: LeaseLedger::default(),
272
+ #[cfg(test)]
273
+ publish_fault: AtomicUsize::new(0),
274
+ }
275
+ }
276
+
277
+ fn next_reduce(&self) -> usize {
278
+ self.state.lock().next_reduce
279
+ }
280
+
281
+ fn epoch_guard(&self, expected: u64) -> EpochGuard {
282
+ EpochGuard::new(expected, Arc::clone(&self.generation))
283
+ }
284
+
285
+ fn fill_snapshot(&self) -> Option<(u64, usize)> {
286
+ let state = self.state.lock();
287
+ if !state.accepting || state.next_reduce >= self.candidate_count {
288
+ return None;
289
+ }
290
+ let end = state
291
+ .next_reduce
292
+ .saturating_add(self.width)
293
+ .min(self.candidate_count);
294
+ let vacancies = (state.next_reduce.saturating_add(1)..end)
295
+ .filter(|index| Self::slot(&state, self.width, *index).is_none())
296
+ .count();
297
+ (vacancies > 0).then_some((state.generation, vacancies))
298
+ }
299
+
300
+ fn try_claim(
301
+ self: &Arc<Self>,
302
+ generation: u64,
303
+ permit: BurstPermit,
304
+ ) -> Option<RunningLease<R>> {
305
+ let runner_id = self
306
+ .next_runner_id
307
+ .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
308
+ current.checked_add(1)
309
+ })
310
+ .ok()?
311
+ .checked_add(1)?;
312
+ let mut lease = RunningLease::new(Arc::clone(self), generation, runner_id, permit);
313
+ let mut state = self.state.lock();
314
+ if !state.accepting || state.generation != generation {
315
+ return None;
316
+ }
317
+ let end = state
318
+ .next_reduce
319
+ .saturating_add(self.width)
320
+ .min(self.candidate_count);
321
+ let index = (state.next_reduce.saturating_add(1)..end)
322
+ .find(|index| Self::slot(&state, self.width, *index).is_none())?;
323
+ lease.bind(index);
324
+ *Self::slot_mut(&mut state, self.width, index) = Some(SpecSlot::RunningExtra {
325
+ index,
326
+ generation,
327
+ runner_id,
328
+ });
329
+ state.live_slots += 1;
330
+ #[cfg(test)]
331
+ {
332
+ state.peak_slots = state.peak_slots.max(state.live_slots);
333
+ }
334
+ debug_assert!(state.live_slots < self.width);
335
+ drop(state);
336
+ self.changed.notify_all();
337
+ Some(lease)
338
+ }
339
+
340
+ fn take_frontier(
341
+ &self,
342
+ index: usize,
343
+ operation: &OperationCtx,
344
+ ) -> Result<Frontier<R>, OrderedError> {
345
+ loop {
346
+ operation.check()?;
347
+ let mut state = self.state.lock();
348
+ debug_assert_eq!(state.next_reduce, index);
349
+ let current_generation = state.generation;
350
+ let slot = Self::slot_mut(&mut state, self.width, index);
351
+ match slot.as_ref() {
352
+ Some(SpecSlot::Ready {
353
+ index: slot_index,
354
+ generation: slot_generation,
355
+ ..
356
+ }) if *slot_index == index && *slot_generation == current_generation => {
357
+ let Some(SpecSlot::Ready { envelope, .. }) = slot.take() else {
358
+ unreachable!("the checked frontier slot remains ready")
359
+ };
360
+ state.live_slots -= 1;
361
+ return Ok(Frontier::Ready(envelope));
362
+ }
363
+ Some(SpecSlot::RunningExtra {
364
+ index: slot_index,
365
+ generation: slot_generation,
366
+ ..
367
+ }) if *slot_index == index && *slot_generation == current_generation => {
368
+ self.changed.wait_for(&mut state, FRONTIER_WAIT_SLICE);
369
+ }
370
+ _ => return Ok(Frontier::Inline),
371
+ }
372
+ }
373
+ }
374
+
375
+ fn advance(&self, index: usize) {
376
+ let mut state = self.state.lock();
377
+ debug_assert_eq!(state.next_reduce, index);
378
+ debug_assert!(Self::slot(&state, self.width, index).is_none());
379
+ state.next_reduce = index.saturating_add(1);
380
+ drop(state);
381
+ self.changed.notify_all();
382
+ }
383
+
384
+ fn retire_generation(&self, reactivate: bool) -> Result<(), OrderedError> {
385
+ let mut state = self.state.lock();
386
+ state.accepting = false;
387
+ for slot in &mut state.slots {
388
+ *slot = None;
389
+ }
390
+ state.live_slots = 0;
391
+ let Some(next) = state.generation.checked_add(1) else {
392
+ drop(state);
393
+ self.changed.notify_all();
394
+ return Err(OrderedError::GenerationOverflow);
395
+ };
396
+ state.generation = next;
397
+ self.generation.store(next, Ordering::Release);
398
+ state.accepting = reactivate;
399
+ drop(state);
400
+ self.changed.notify_all();
401
+ Ok(())
402
+ }
403
+
404
+ fn slot(state: &RequestSpecState<R>, width: usize, index: usize) -> Option<&SpecSlot<R>> {
405
+ state.slots[index % width]
406
+ .as_ref()
407
+ .filter(|slot| match slot {
408
+ SpecSlot::RunningExtra {
409
+ index: slot_index, ..
410
+ }
411
+ | SpecSlot::Ready {
412
+ index: slot_index, ..
413
+ } => *slot_index == index,
414
+ })
415
+ }
416
+
417
+ fn slot_mut(
418
+ state: &mut RequestSpecState<R>,
419
+ width: usize,
420
+ index: usize,
421
+ ) -> &mut Option<SpecSlot<R>> {
422
+ &mut state.slots[index % width]
423
+ }
424
+
425
+ #[cfg(test)]
426
+ fn inject_publish_fault(&self, fault: PublishFault) {
427
+ self.publish_fault.store(fault as usize, Ordering::Release);
428
+ }
429
+
430
+ #[cfg(test)]
431
+ fn maybe_fault(&self, fault: PublishFault) {
432
+ if self
433
+ .publish_fault
434
+ .compare_exchange(
435
+ fault as usize,
436
+ PublishFault::None as usize,
437
+ Ordering::AcqRel,
438
+ Ordering::Acquire,
439
+ )
440
+ .is_ok()
441
+ {
442
+ panic!("injected ordered-window publish fault: {fault:?}");
443
+ }
444
+ }
445
+ }
446
+
447
+ #[must_use = "a running lease must either publish or clear its exact running slot"]
448
+ struct RunningLease<R> {
449
+ shared: Arc<RequestSpecShared<R>>,
450
+ index: Option<usize>,
451
+ generation: u64,
452
+ runner_id: u64,
453
+ permit: Option<BurstPermit>,
454
+ armed: bool,
455
+ #[cfg(test)]
456
+ lease_id: u64,
457
+ }
458
+
459
+ impl<R> RunningLease<R> {
460
+ fn new(
461
+ shared: Arc<RequestSpecShared<R>>,
462
+ generation: u64,
463
+ runner_id: u64,
464
+ permit: BurstPermit,
465
+ ) -> Self {
466
+ #[cfg(test)]
467
+ let lease_id = shared.lease_ledger.allocate();
468
+ Self {
469
+ shared,
470
+ index: None,
471
+ generation,
472
+ runner_id,
473
+ permit: Some(permit),
474
+ armed: true,
475
+ #[cfg(test)]
476
+ lease_id,
477
+ }
478
+ }
479
+
480
+ fn bind(&mut self, index: usize) {
481
+ debug_assert!(self.index.is_none());
482
+ self.index = Some(index);
483
+ }
484
+
485
+ fn index(&self) -> usize {
486
+ self.index
487
+ .expect("only a claimed running lease reaches work")
488
+ }
489
+
490
+ fn publish(mut self, value: R) {
491
+ let Some(permit) = self.permit.take() else {
492
+ return;
493
+ };
494
+ let mut envelope = Some(BurstEnvelope::new(permit, value));
495
+ #[cfg(test)]
496
+ self.shared.maybe_fault(PublishFault::AfterPermitTaken);
497
+ let Some(index) = self.index else {
498
+ return;
499
+ };
500
+ let mut state = self.shared.state.lock();
501
+ let matches = state.accepting
502
+ && state.generation == self.generation
503
+ && matches!(
504
+ RequestSpecShared::slot(&state, self.shared.width, index),
505
+ Some(SpecSlot::RunningExtra {
506
+ index: slot_index,
507
+ generation,
508
+ runner_id,
509
+ }) if *slot_index == index
510
+ && *generation == self.generation
511
+ && *runner_id == self.runner_id
512
+ );
513
+ if matches && let Some(envelope) = envelope.take() {
514
+ *RequestSpecShared::slot_mut(&mut state, self.shared.width, index) =
515
+ Some(SpecSlot::Ready {
516
+ index,
517
+ generation: self.generation,
518
+ envelope,
519
+ });
520
+ drop(state);
521
+ self.shared.changed.notify_all();
522
+ #[cfg(test)]
523
+ self.shared.maybe_fault(PublishFault::AfterReadyStored);
524
+ self.armed = false;
525
+ }
526
+ }
527
+ }
528
+
529
+ impl<R> Drop for RunningLease<R> {
530
+ fn drop(&mut self) {
531
+ if self.armed
532
+ && let Some(index) = self.index
533
+ {
534
+ let mut state = self.shared.state.lock();
535
+ let matches = matches!(
536
+ RequestSpecShared::slot(&state, self.shared.width, index),
537
+ Some(SpecSlot::RunningExtra {
538
+ index: slot_index,
539
+ generation,
540
+ runner_id,
541
+ }) if *slot_index == index
542
+ && *generation == self.generation
543
+ && *runner_id == self.runner_id
544
+ );
545
+ if matches {
546
+ *RequestSpecShared::slot_mut(&mut state, self.shared.width, index) = None;
547
+ state.live_slots -= 1;
548
+ drop(state);
549
+ self.shared.changed.notify_all();
550
+ }
551
+ }
552
+ #[cfg(test)]
553
+ self.shared.lease_ledger.release(self.lease_id);
554
+ }
555
+ }
556
+
557
+ #[cfg(test)]
558
+ #[repr(usize)]
559
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
560
+ enum PublishFault {
561
+ None = 0,
562
+ AfterPermitTaken = 1,
563
+ AfterReadyStored = 2,
564
+ }
565
+
566
+ #[cfg(test)]
567
+ #[derive(Default)]
568
+ struct LeaseLedger {
569
+ state: Mutex<LeaseLedgerState>,
570
+ }
571
+
572
+ #[cfg(test)]
573
+ #[derive(Default)]
574
+ struct LeaseLedgerState {
575
+ next_id: u64,
576
+ allocated: usize,
577
+ released: usize,
578
+ live: BTreeSet<u64>,
579
+ duplicate_releases: usize,
580
+ }
581
+
582
+ #[cfg(test)]
583
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
584
+ struct LeaseLedgerSnapshot {
585
+ allocated: usize,
586
+ released: usize,
587
+ live: usize,
588
+ duplicate_releases: usize,
589
+ }
590
+
591
+ #[cfg(test)]
592
+ impl LeaseLedger {
593
+ fn allocate(&self) -> u64 {
594
+ let mut state = self.state.lock();
595
+ state.next_id = state.next_id.checked_add(1).expect("lease id overflow");
596
+ let id = state.next_id;
597
+ state.allocated += 1;
598
+ assert!(state.live.insert(id));
599
+ id
600
+ }
601
+
602
+ fn release(&self, id: u64) {
603
+ let mut state = self.state.lock();
604
+ if state.live.remove(&id) {
605
+ state.released += 1;
606
+ } else {
607
+ state.duplicate_releases += 1;
608
+ }
609
+ }
610
+
611
+ fn snapshot(&self) -> LeaseLedgerSnapshot {
612
+ let state = self.state.lock();
613
+ LeaseLedgerSnapshot {
614
+ allocated: state.allocated,
615
+ released: state.released,
616
+ live: state.live.len(),
617
+ duplicate_releases: state.duplicate_releases,
618
+ }
619
+ }
620
+ }
621
+
622
+ #[cfg(test)]
623
+ mod tests {
624
+ use super::{
625
+ LeaseLedgerSnapshot, OrderedError, PublishFault, RequestSpecShared, for_each_ordered,
626
+ };
627
+ use crate::file_executor::{BurstUse, GrepGlobExecutor};
628
+ use crate::operation::{RequestWorkGuard, TestStage};
629
+ use rmcp::model::RequestId;
630
+ use std::collections::BTreeSet;
631
+ use std::ops::ControlFlow;
632
+ use std::panic::AssertUnwindSafe;
633
+ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
634
+ use std::sync::{Arc, Barrier, Mutex, mpsc};
635
+ use std::time::Duration;
636
+ use tokio_util::sync::CancellationToken;
637
+
638
+ fn operation(id: i64) -> (RequestWorkGuard, crate::operation::OperationCtx) {
639
+ RequestWorkGuard::new(RequestId::Number(id), CancellationToken::new())
640
+ }
641
+
642
+ fn released(allocated: usize) -> LeaseLedgerSnapshot {
643
+ LeaseLedgerSnapshot {
644
+ allocated,
645
+ released: allocated,
646
+ live: 0,
647
+ duplicate_releases: 0,
648
+ }
649
+ }
650
+
651
+ fn assert_executor_ownership_released(executor: &GrepGlobExecutor) {
652
+ for snapshot in [executor.test_burst_ledger(), executor.test_ticket_ledger()] {
653
+ assert_eq!(snapshot.released, snapshot.allocated);
654
+ assert_eq!(snapshot.live, 0);
655
+ assert_eq!(snapshot.duplicate_releases, 0);
656
+ }
657
+ assert_eq!(executor.test_burst_available(), executor.extra_capacity());
658
+ assert_eq!(executor.test_ticket_available(), executor.extra_capacity());
659
+ }
660
+
661
+ #[test]
662
+ fn p1_runs_every_frontier_inline_and_in_order() {
663
+ let (mut guard, operation) = operation(1);
664
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(1));
665
+ let items: Arc<[usize]> = (0..64).collect::<Vec<_>>().into();
666
+ let mut delivered = Vec::new();
667
+ for_each_ordered(
668
+ items,
669
+ operation,
670
+ executor,
671
+ |_, item, work| {
672
+ work.check()?;
673
+ Ok(*item * 2)
674
+ },
675
+ |_, _| unreachable!("inline arithmetic does not panic"),
676
+ |index, value, _| {
677
+ delivered.push((index, value));
678
+ ControlFlow::Continue(())
679
+ },
680
+ )
681
+ .unwrap();
682
+ guard.disarm();
683
+ assert_eq!(
684
+ delivered,
685
+ (0..64).map(|index| (index, index * 2)).collect::<Vec<_>>()
686
+ );
687
+ }
688
+
689
+ #[test]
690
+ fn lease_moves_its_permit_to_ready_then_consume_releases_both_once() {
691
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(2));
692
+ let shared = Arc::new(RequestSpecShared::<usize>::new(2, 2));
693
+ let permit = executor.try_burst(BurstUse::SearchSpeculation).unwrap();
694
+ let lease = shared.try_claim(0, permit).unwrap();
695
+ assert_eq!(lease.index(), 1);
696
+ lease.publish(41);
697
+ assert_eq!(shared.lease_ledger.snapshot(), released(1));
698
+
699
+ let (_guard, operation) = operation(2);
700
+ assert!(matches!(
701
+ shared.take_frontier(0, &operation).unwrap(),
702
+ super::Frontier::Inline
703
+ ));
704
+ shared.advance(0);
705
+ let super::Frontier::Ready(envelope) = shared.take_frontier(1, &operation).unwrap() else {
706
+ panic!("published lease must become the next ready frontier")
707
+ };
708
+ assert_eq!(envelope.into_value(), 41);
709
+ assert_executor_ownership_released(&executor);
710
+ }
711
+
712
+ #[test]
713
+ fn abandoning_a_claimed_lease_clears_its_exact_slot_and_releases_once() {
714
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(2));
715
+ let shared = Arc::new(RequestSpecShared::<usize>::new(2, 2));
716
+ let permit = executor.try_burst(BurstUse::SearchSpeculation).unwrap();
717
+ let lease = shared.try_claim(0, permit).unwrap();
718
+ assert_eq!(lease.index(), 1);
719
+ assert_eq!(shared.state.lock().live_slots, 1);
720
+
721
+ drop(lease);
722
+
723
+ let state = shared.state.lock();
724
+ assert_eq!(state.live_slots, 0);
725
+ assert!(RequestSpecShared::slot(&state, shared.width, 1).is_none());
726
+ drop(state);
727
+ assert_eq!(shared.lease_ledger.snapshot(), released(1));
728
+ assert_executor_ownership_released(&executor);
729
+ }
730
+
731
+ #[test]
732
+ fn publish_faults_never_orphan_a_running_slot_or_double_release_a_lease() {
733
+ for fault in [
734
+ PublishFault::AfterPermitTaken,
735
+ PublishFault::AfterReadyStored,
736
+ ] {
737
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(2));
738
+ let shared = Arc::new(RequestSpecShared::<usize>::new(2, 2));
739
+ let permit = executor.try_burst(BurstUse::SearchSpeculation).unwrap();
740
+ let lease = shared.try_claim(0, permit).unwrap();
741
+ shared.inject_publish_fault(fault);
742
+ assert!(std::panic::catch_unwind(AssertUnwindSafe(|| lease.publish(7))).is_err());
743
+ shared.retire_generation(false).unwrap();
744
+ assert_eq!(shared.state.lock().live_slots, 0);
745
+ assert_eq!(shared.lease_ledger.snapshot(), released(1));
746
+ assert_executor_ownership_released(&executor);
747
+ }
748
+ }
749
+
750
+ #[test]
751
+ fn generation_retirement_rejects_old_publish_and_allows_the_new_generation() {
752
+ let (_guard, operation) = operation(21);
753
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(2));
754
+ let shared = Arc::new(RequestSpecShared::<usize>::new(2, 3));
755
+ let old_generation = shared.fill_snapshot().unwrap().0;
756
+ let old_work = crate::operation::WorkCtx::speculative(
757
+ operation.clone(),
758
+ shared.epoch_guard(old_generation),
759
+ );
760
+ let old_permit = executor.try_burst(BurstUse::SearchSpeculation).unwrap();
761
+ let old_lease = shared.try_claim(old_generation, old_permit).unwrap();
762
+
763
+ shared.retire_generation(true).unwrap();
764
+ assert_eq!(
765
+ old_work.check(),
766
+ Err(crate::operation::WorkStop::EpochRetired)
767
+ );
768
+ old_lease.publish(999);
769
+ assert_eq!(shared.state.lock().live_slots, 0);
770
+
771
+ let new_generation = shared.fill_snapshot().unwrap().0;
772
+ assert_ne!(new_generation, old_generation);
773
+ let new_permit = executor.try_burst(BurstUse::SearchSpeculation).unwrap();
774
+ let new_lease = shared.try_claim(new_generation, new_permit).unwrap();
775
+ new_lease.publish(7);
776
+
777
+ assert!(matches!(
778
+ shared.take_frontier(0, &operation).unwrap(),
779
+ super::Frontier::Inline
780
+ ));
781
+ shared.advance(0);
782
+ let super::Frontier::Ready(envelope) = shared.take_frontier(1, &operation).unwrap() else {
783
+ panic!("the replacement generation must publish normally")
784
+ };
785
+ assert_eq!(envelope.into_value(), 7);
786
+ assert_eq!(shared.lease_ledger.snapshot(), released(2));
787
+ assert_executor_ownership_released(&executor);
788
+ }
789
+
790
+ #[test]
791
+ fn reducer_retirement_makes_the_exact_inline_retry_win_over_old_work() {
792
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
793
+ enum Outcome {
794
+ Overflow,
795
+ Value(usize),
796
+ }
797
+
798
+ let (mut guard, operation) = operation(22);
799
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(2));
800
+ let first_index_one = Arc::new(AtomicBool::new(true));
801
+ let old_retired = Arc::new(AtomicBool::new(false));
802
+ let (old_started_tx, old_started_rx) = mpsc::sync_channel(0);
803
+ let (release_old_tx, release_old_rx) = mpsc::sync_channel(0);
804
+ let release_old_rx = Arc::new(Mutex::new(release_old_rx));
805
+ let first_index_one_by_worker = Arc::clone(&first_index_one);
806
+ let old_retired_by_worker = Arc::clone(&old_retired);
807
+ let release_old_rx_by_worker = Arc::clone(&release_old_rx);
808
+ let mut delivered = Vec::new();
809
+
810
+ for_each_ordered(
811
+ Arc::<[usize]>::from(vec![0, 1, 2]),
812
+ operation,
813
+ Arc::clone(&executor),
814
+ move |index, item, work| {
815
+ if index == 0 {
816
+ return Ok(Outcome::Overflow);
817
+ }
818
+ if index == 1 && first_index_one_by_worker.swap(false, Ordering::AcqRel) {
819
+ old_started_tx.send(()).unwrap();
820
+ release_old_rx_by_worker
821
+ .lock()
822
+ .unwrap()
823
+ .recv_timeout(Duration::from_secs(5))
824
+ .expect("retirement must release the old runner");
825
+ let stopped = work.check();
826
+ if stopped == Err(crate::operation::WorkStop::EpochRetired) {
827
+ old_retired_by_worker.store(true, Ordering::Release);
828
+ }
829
+ return stopped.map(|()| Outcome::Value(*item));
830
+ }
831
+ work.check()?;
832
+ Ok(Outcome::Value(*item))
833
+ },
834
+ |index, _| Outcome::Value(index),
835
+ |index, outcome, reducer| {
836
+ if outcome == Outcome::Overflow {
837
+ old_started_rx
838
+ .recv_timeout(Duration::from_secs(5))
839
+ .expect("the old speculative runner must claim before retirement");
840
+ reducer.retire_generation().unwrap();
841
+ delivered.push((index, Outcome::Value(100)));
842
+ release_old_tx.send(()).unwrap();
843
+ } else {
844
+ delivered.push((index, outcome));
845
+ }
846
+ ControlFlow::Continue(())
847
+ },
848
+ )
849
+ .unwrap();
850
+ guard.disarm();
851
+ executor.wait_for_test_quiescence();
852
+ assert_eq!(
853
+ delivered,
854
+ vec![
855
+ (0, Outcome::Value(100)),
856
+ (1, Outcome::Value(1)),
857
+ (2, Outcome::Value(2)),
858
+ ]
859
+ );
860
+ assert!(old_retired.load(Ordering::Acquire));
861
+ assert_executor_ownership_released(&executor);
862
+ }
863
+
864
+ #[test]
865
+ fn speculative_candidate_panic_is_published_in_order_without_an_orphan() {
866
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
867
+ enum Outcome {
868
+ Value(usize),
869
+ Panicked(usize),
870
+ }
871
+
872
+ let (mut guard, operation) = operation(23);
873
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(2));
874
+ let (panic_started_tx, panic_started_rx) = mpsc::sync_channel(0);
875
+ let panic_started_rx = Arc::new(Mutex::new(panic_started_rx));
876
+ let panic_started_rx_by_worker = Arc::clone(&panic_started_rx);
877
+ let mut delivered = Vec::new();
878
+ for_each_ordered(
879
+ Arc::<[usize]>::from(vec![0, 1, 2]),
880
+ operation,
881
+ Arc::clone(&executor),
882
+ move |index, item, work| {
883
+ if index == 0 {
884
+ panic_started_rx_by_worker
885
+ .lock()
886
+ .unwrap()
887
+ .recv_timeout(Duration::from_secs(5))
888
+ .expect("index 1 must run speculatively");
889
+ } else if index == 1 {
890
+ panic_started_tx.send(()).unwrap();
891
+ panic!("injected candidate panic");
892
+ }
893
+ work.check()?;
894
+ Ok(Outcome::Value(*item))
895
+ },
896
+ |index, _| Outcome::Panicked(index),
897
+ |index, outcome, _| {
898
+ delivered.push((index, outcome));
899
+ ControlFlow::Continue(())
900
+ },
901
+ )
902
+ .unwrap();
903
+ guard.disarm();
904
+ executor.wait_for_test_quiescence();
905
+ assert_eq!(
906
+ delivered,
907
+ vec![
908
+ (0, Outcome::Value(0)),
909
+ (1, Outcome::Panicked(1)),
910
+ (2, Outcome::Value(2)),
911
+ ]
912
+ );
913
+ assert_executor_ownership_released(&executor);
914
+ }
915
+
916
+ #[test]
917
+ fn million_candidate_state_has_only_p_minus_one_live_heavy_slots() {
918
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(4));
919
+ let shared = Arc::new(RequestSpecShared::<usize>::new(4, 1_000_000));
920
+ let generation = shared.fill_snapshot().unwrap().0;
921
+ let mut leases = Vec::new();
922
+ for expected_index in 1..4 {
923
+ let permit = executor.try_burst(BurstUse::SearchSpeculation).unwrap();
924
+ let lease = shared.try_claim(generation, permit).unwrap();
925
+ assert_eq!(lease.index(), expected_index);
926
+ leases.push(lease);
927
+ }
928
+ assert!(executor.try_burst(BurstUse::SearchSpeculation).is_none());
929
+ {
930
+ let state = shared.state.lock();
931
+ assert_eq!(state.slots.len(), 4);
932
+ assert_eq!(state.live_slots, 3);
933
+ assert_eq!(state.peak_slots, 3);
934
+ }
935
+ drop(leases);
936
+ assert_eq!(shared.state.lock().live_slots, 0);
937
+ assert_eq!(shared.lease_ledger.snapshot(), released(3));
938
+ assert_executor_ownership_released(&executor);
939
+ }
940
+
941
+ #[test]
942
+ fn concurrent_claimers_get_unique_indices_and_never_exceed_the_window() {
943
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(8));
944
+ let shared = Arc::new(RequestSpecShared::<usize>::new(8, 1_000_000));
945
+ let generation = shared.fill_snapshot().unwrap().0;
946
+ let start = Arc::new(Barrier::new(8));
947
+ let mut threads = Vec::new();
948
+ for _ in 0..7 {
949
+ let permit = executor.try_burst(BurstUse::SearchSpeculation).unwrap();
950
+ let shared_for_thread = Arc::clone(&shared);
951
+ let start_for_thread = Arc::clone(&start);
952
+ threads.push(std::thread::spawn(move || {
953
+ start_for_thread.wait();
954
+ shared_for_thread.try_claim(generation, permit).unwrap()
955
+ }));
956
+ }
957
+ start.wait();
958
+ let leases = threads
959
+ .into_iter()
960
+ .map(|thread| thread.join().unwrap())
961
+ .collect::<Vec<_>>();
962
+ let indices = leases
963
+ .iter()
964
+ .map(super::RunningLease::index)
965
+ .collect::<BTreeSet<_>>();
966
+ assert_eq!(indices, (1..8).collect());
967
+ {
968
+ let state = shared.state.lock();
969
+ assert_eq!(state.slots.len(), 8);
970
+ assert_eq!(state.live_slots, 7);
971
+ assert_eq!(state.peak_slots, 7);
972
+ }
973
+ drop(leases);
974
+ assert_eq!(shared.state.lock().live_slots, 0);
975
+ assert_eq!(shared.lease_ledger.snapshot(), released(7));
976
+ assert_executor_ownership_released(&executor);
977
+ }
978
+
979
+ #[test]
980
+ fn delayed_runner_cancelled_before_burst_never_claims_a_candidate() {
981
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(2));
982
+ let executor_for_thread = Arc::clone(&executor);
983
+ let (runner_started_tx, runner_started_rx) = mpsc::sync_channel(0);
984
+ let (release_runner_tx, release_runner_rx) = mpsc::sync_channel(0);
985
+ let release_runner_rx = Arc::new(Mutex::new(release_runner_rx));
986
+ let release_runner_rx_by_hook = Arc::clone(&release_runner_rx);
987
+ let hook = Arc::new(move |stage| {
988
+ if stage == TestStage::RunnerStarted {
989
+ runner_started_tx.send(()).unwrap();
990
+ release_runner_rx_by_hook
991
+ .lock()
992
+ .unwrap()
993
+ .recv_timeout(Duration::from_secs(5))
994
+ .expect("the cancelled delayed runner must be released");
995
+ }
996
+ });
997
+ let (guard, operation) =
998
+ RequestWorkGuard::new_with_hook(RequestId::Number(24), CancellationToken::new(), hook);
999
+ let (inline_started_tx, inline_started_rx) = mpsc::sync_channel(0);
1000
+ let (release_inline_tx, release_inline_rx) = mpsc::sync_channel(0);
1001
+ let release_inline_rx = Arc::new(Mutex::new(release_inline_rx));
1002
+ let release_inline_rx_by_worker = Arc::clone(&release_inline_rx);
1003
+ let thread = std::thread::spawn(move || {
1004
+ for_each_ordered(
1005
+ Arc::<[usize]>::from(vec![0, 1, 2]),
1006
+ operation,
1007
+ executor_for_thread,
1008
+ move |index, item, work| {
1009
+ if index == 0 {
1010
+ inline_started_tx.send(()).unwrap();
1011
+ release_inline_rx_by_worker
1012
+ .lock()
1013
+ .unwrap()
1014
+ .recv_timeout(Duration::from_secs(5))
1015
+ .expect("the cancelled inline frontier must be released");
1016
+ }
1017
+ work.check()?;
1018
+ Ok(*item)
1019
+ },
1020
+ |index, _| index,
1021
+ |_, _, _| ControlFlow::Continue(()),
1022
+ )
1023
+ });
1024
+
1025
+ runner_started_rx
1026
+ .recv_timeout(Duration::from_secs(5))
1027
+ .expect("the private-pool runner must actually start");
1028
+ inline_started_rx
1029
+ .recv_timeout(Duration::from_secs(5))
1030
+ .expect("the coordinator base lane must actually start");
1031
+ drop(guard);
1032
+ release_runner_tx.send(()).unwrap();
1033
+ release_inline_tx.send(()).unwrap();
1034
+ assert_eq!(thread.join().unwrap(), Err(OrderedError::Cancelled));
1035
+ executor.wait_for_test_quiescence();
1036
+ assert_eq!(executor.test_burst_ledger().allocated, 0);
1037
+ let ticket_ledger = executor.test_ticket_ledger();
1038
+ assert_eq!(ticket_ledger.allocated, 1);
1039
+ assert_eq!(ticket_ledger.released, 1);
1040
+ assert_executor_ownership_released(&executor);
1041
+ }
1042
+
1043
+ #[test]
1044
+ fn cancellation_at_every_runner_boundary_stops_before_later_work_or_publication() {
1045
+ for (request_id, cancelled_stage) in [
1046
+ (30, TestStage::RunnerQueued),
1047
+ (31, TestStage::RunnerStarted),
1048
+ (32, TestStage::BeforeBurstAcquire),
1049
+ (33, TestStage::BeforeCandidateClaim),
1050
+ (34, TestStage::AfterCandidateClaim),
1051
+ (35, TestStage::BeforeReadyPublish),
1052
+ ] {
1053
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(2));
1054
+ let cancellation = CancellationToken::new();
1055
+ let cancellation_for_hook = cancellation.clone();
1056
+ let stage_hits = Arc::new(AtomicUsize::new(0));
1057
+ let stage_hits_for_hook = Arc::clone(&stage_hits);
1058
+ let (stage_reached_tx, stage_reached_rx) = mpsc::channel();
1059
+ let hook = Arc::new(move |stage| {
1060
+ if stage == cancelled_stage
1061
+ && stage_hits_for_hook.fetch_add(1, Ordering::AcqRel) == 0
1062
+ {
1063
+ cancellation_for_hook.cancel();
1064
+ stage_reached_tx.send(()).unwrap();
1065
+ }
1066
+ });
1067
+ let (guard, operation) =
1068
+ RequestWorkGuard::new_with_hook(RequestId::Number(request_id), cancellation, hook);
1069
+ let stage_reached_rx = Arc::new(Mutex::new(stage_reached_rx));
1070
+ let stage_reached_for_worker = Arc::clone(&stage_reached_rx);
1071
+ let worker_calls = Arc::new(AtomicUsize::new(0));
1072
+ let worker_calls_for_worker = Arc::clone(&worker_calls);
1073
+ let consume_calls = Arc::new(AtomicUsize::new(0));
1074
+ let consume_calls_for_consumer = Arc::clone(&consume_calls);
1075
+
1076
+ let result = for_each_ordered(
1077
+ Arc::<[usize]>::from(vec![0, 1, 2]),
1078
+ operation,
1079
+ Arc::clone(&executor),
1080
+ move |index, item, work| {
1081
+ if index == 0 {
1082
+ stage_reached_for_worker
1083
+ .lock()
1084
+ .unwrap()
1085
+ .recv_timeout(Duration::from_secs(5))
1086
+ .expect("the targeted runner stage must be reached");
1087
+ } else {
1088
+ worker_calls_for_worker.fetch_add(1, Ordering::AcqRel);
1089
+ }
1090
+ work.check()?;
1091
+ Ok(*item)
1092
+ },
1093
+ |index, _| index,
1094
+ move |_, _, _| {
1095
+ consume_calls_for_consumer.fetch_add(1, Ordering::AcqRel);
1096
+ ControlFlow::Continue(())
1097
+ },
1098
+ );
1099
+ drop(guard);
1100
+ assert_eq!(result, Err(OrderedError::Cancelled), "{cancelled_stage:?}");
1101
+ assert_eq!(stage_hits.load(Ordering::Acquire), 1, "{cancelled_stage:?}");
1102
+ assert_eq!(
1103
+ consume_calls.load(Ordering::Acquire),
1104
+ 0,
1105
+ "{cancelled_stage:?}"
1106
+ );
1107
+ let expected_worker_calls =
1108
+ usize::from(cancelled_stage == TestStage::BeforeReadyPublish);
1109
+ assert_eq!(
1110
+ worker_calls.load(Ordering::Acquire),
1111
+ expected_worker_calls,
1112
+ "{cancelled_stage:?}"
1113
+ );
1114
+ executor.wait_for_test_quiescence();
1115
+ assert_executor_ownership_released(&executor);
1116
+ }
1117
+ }
1118
+
1119
+ #[test]
1120
+ fn cancellation_at_ordered_reduce_never_calls_the_consumer() {
1121
+ let cancellation = CancellationToken::new();
1122
+ let cancellation_for_hook = cancellation.clone();
1123
+ let reduce_hits = Arc::new(AtomicUsize::new(0));
1124
+ let reduce_hits_for_hook = Arc::clone(&reduce_hits);
1125
+ let hook = Arc::new(move |stage| {
1126
+ if stage == TestStage::OrderedReduce
1127
+ && reduce_hits_for_hook.fetch_add(1, Ordering::AcqRel) == 0
1128
+ {
1129
+ cancellation_for_hook.cancel();
1130
+ }
1131
+ });
1132
+ let (guard, operation) =
1133
+ RequestWorkGuard::new_with_hook(RequestId::Number(36), cancellation, hook);
1134
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(1));
1135
+ let consume_calls = Arc::new(AtomicUsize::new(0));
1136
+ let consume_calls_for_consumer = Arc::clone(&consume_calls);
1137
+
1138
+ let result = for_each_ordered(
1139
+ Arc::<[usize]>::from(vec![0]),
1140
+ operation,
1141
+ Arc::clone(&executor),
1142
+ |_, item, work| {
1143
+ work.check()?;
1144
+ Ok(*item)
1145
+ },
1146
+ |index, _| index,
1147
+ move |_, _, _| {
1148
+ consume_calls_for_consumer.fetch_add(1, Ordering::AcqRel);
1149
+ ControlFlow::Continue(())
1150
+ },
1151
+ );
1152
+ drop(guard);
1153
+ assert_eq!(result, Err(OrderedError::Cancelled));
1154
+ assert_eq!(reduce_hits.load(Ordering::Acquire), 1);
1155
+ assert_eq!(consume_calls.load(Ordering::Acquire), 0);
1156
+ assert_executor_ownership_released(&executor);
1157
+ }
1158
+
1159
+ #[test]
1160
+ fn early_break_retires_speculation_and_keeps_the_window_bounded() {
1161
+ let (mut guard, operation) = operation(3);
1162
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(4));
1163
+ let items: Arc<[usize]> = (0..10_000).collect::<Vec<_>>().into();
1164
+ let examined = Arc::new(AtomicUsize::new(0));
1165
+ let examined_by_worker = Arc::clone(&examined);
1166
+ let mut delivered = Vec::new();
1167
+ for_each_ordered(
1168
+ items,
1169
+ operation,
1170
+ Arc::clone(&executor),
1171
+ move |_, item, work| {
1172
+ work.check()?;
1173
+ examined_by_worker.fetch_add(1, Ordering::AcqRel);
1174
+ Ok(*item)
1175
+ },
1176
+ |_, _| usize::MAX,
1177
+ |index, value, _| {
1178
+ assert_eq!(index, value);
1179
+ delivered.push(index);
1180
+ if index == 9 {
1181
+ ControlFlow::Break(())
1182
+ } else {
1183
+ ControlFlow::Continue(())
1184
+ }
1185
+ },
1186
+ )
1187
+ .unwrap();
1188
+ guard.disarm();
1189
+ assert_eq!(delivered, (0..10).collect::<Vec<_>>());
1190
+ assert!(examined.load(Ordering::Acquire) < 10_000);
1191
+ executor.wait_for_test_quiescence();
1192
+ assert_executor_ownership_released(&executor);
1193
+ }
1194
+
1195
+ #[test]
1196
+ fn dropping_the_request_guard_cancels_a_waiting_frontier() {
1197
+ let (guard, operation) = operation(4);
1198
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(2));
1199
+ let executor_for_thread = Arc::clone(&executor);
1200
+ let entered = Arc::new(AtomicBool::new(false));
1201
+ let release = Arc::new(AtomicBool::new(false));
1202
+ let entered_by_worker = Arc::clone(&entered);
1203
+ let release_by_worker = Arc::clone(&release);
1204
+ let thread = std::thread::spawn(move || {
1205
+ let items: Arc<[usize]> = vec![0, 1, 2].into();
1206
+ for_each_ordered(
1207
+ items,
1208
+ operation,
1209
+ executor_for_thread,
1210
+ move |index, item, work| {
1211
+ if index == 1 {
1212
+ entered_by_worker.store(true, Ordering::Release);
1213
+ while !release_by_worker.load(Ordering::Acquire) {
1214
+ work.check()?;
1215
+ std::thread::yield_now();
1216
+ }
1217
+ }
1218
+ Ok(*item)
1219
+ },
1220
+ |_, _| usize::MAX,
1221
+ |_, _, _| ControlFlow::Continue(()),
1222
+ )
1223
+ });
1224
+ let deadline = std::time::Instant::now() + Duration::from_secs(5);
1225
+ while !entered.load(Ordering::Acquire) {
1226
+ assert!(std::time::Instant::now() < deadline, "runner never started");
1227
+ std::thread::yield_now();
1228
+ }
1229
+ drop(guard);
1230
+ release.store(true, Ordering::Release);
1231
+ assert_eq!(thread.join().unwrap(), Err(OrderedError::Cancelled));
1232
+ executor.wait_for_test_quiescence();
1233
+ assert_executor_ownership_released(&executor);
1234
+ }
1235
+ }