dsh-ops 0.0.0-stage → 0.2.2

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 (140) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/LICENSE +30 -0
  3. package/NOTICE +106 -0
  4. package/PROVENANCE.md +435 -0
  5. package/README.en.md +126 -0
  6. package/README.md +115 -2
  7. package/README.zh.md +116 -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/release-0.2.1.md +72 -0
  12. package/docs/schema-baseline.json +64 -0
  13. package/docs/schema-current.json +84 -0
  14. package/docs/schema-measurement.md +17 -0
  15. package/dsh-plugin.json +88 -0
  16. package/icon.svg +12 -0
  17. package/lib/binary.js +409 -0
  18. package/lib/config.js +198 -0
  19. package/lib/handshake.js +252 -0
  20. package/lib/index.js +108 -0
  21. package/lib/jobs.js +42 -0
  22. package/lib/policy.js +64 -0
  23. package/lib/presentation.js +63 -0
  24. package/lib/profile-install.js +61 -0
  25. package/lib/rust.js +194 -0
  26. package/lib/session-shells.js +78 -0
  27. package/lib/shells.js +998 -0
  28. package/lib/tools.js +657 -0
  29. package/locale/en.json +6 -0
  30. package/locale/zh.json +6 -0
  31. package/package.json +114 -4
  32. package/vendor/fastctx/Cargo.lock +3210 -0
  33. package/vendor/fastctx/Cargo.toml +94 -0
  34. package/vendor/fastctx/FORK.md +119 -0
  35. package/vendor/fastctx/LICENSE-APACHE +201 -0
  36. package/vendor/fastctx/NOTICE +40 -0
  37. package/vendor/fastctx/README.md +439 -0
  38. package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
  39. package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
  40. package/vendor/fastctx/UPSTREAM.md +49 -0
  41. package/vendor/fastctx/build.rs +413 -0
  42. package/vendor/fastctx/src/background_status.rs +403 -0
  43. package/vendor/fastctx/src/binary.rs +75 -0
  44. package/vendor/fastctx/src/bounded_sort.rs +500 -0
  45. package/vendor/fastctx/src/budget.rs +781 -0
  46. package/vendor/fastctx/src/cli/mod.rs +110 -0
  47. package/vendor/fastctx/src/context_guard.rs +289 -0
  48. package/vendor/fastctx/src/control/mod.rs +6 -0
  49. package/vendor/fastctx/src/control/paths.rs +49 -0
  50. package/vendor/fastctx/src/control/settings.rs +753 -0
  51. package/vendor/fastctx/src/control/transaction.rs +531 -0
  52. package/vendor/fastctx/src/edit/document.rs +535 -0
  53. package/vendor/fastctx/src/edit/locks.rs +371 -0
  54. package/vendor/fastctx/src/edit/mod.rs +213 -0
  55. package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
  56. package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
  57. package/vendor/fastctx/src/edit/private_storage.rs +234 -0
  58. package/vendor/fastctx/src/edit/replace.rs +1030 -0
  59. package/vendor/fastctx/src/edit_server.rs +53 -0
  60. package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
  61. package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
  62. package/vendor/fastctx/src/encoding.rs +1118 -0
  63. package/vendor/fastctx/src/file_executor.rs +1151 -0
  64. package/vendor/fastctx/src/file_snapshot.rs +1491 -0
  65. package/vendor/fastctx/src/glob_filter.rs +98 -0
  66. package/vendor/fastctx/src/glob_tool.rs +653 -0
  67. package/vendor/fastctx/src/grep_sink.rs +1162 -0
  68. package/vendor/fastctx/src/grep_tool.rs +2449 -0
  69. package/vendor/fastctx/src/lib.rs +45 -0
  70. package/vendor/fastctx/src/main.rs +15 -0
  71. package/vendor/fastctx/src/model.rs +51 -0
  72. package/vendor/fastctx/src/model_guidance.rs +62 -0
  73. package/vendor/fastctx/src/operation.rs +356 -0
  74. package/vendor/fastctx/src/ordered_window.rs +1235 -0
  75. package/vendor/fastctx/src/os_environment.rs +414 -0
  76. package/vendor/fastctx/src/path_codec.rs +850 -0
  77. package/vendor/fastctx/src/paths.rs +244 -0
  78. package/vendor/fastctx/src/process_identity.rs +763 -0
  79. package/vendor/fastctx/src/process_policy.rs +74 -0
  80. package/vendor/fastctx/src/read_tool/batch.rs +496 -0
  81. package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
  82. package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
  83. package/vendor/fastctx/src/read_tool/mod.rs +245 -0
  84. package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
  85. package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
  86. package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
  87. package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
  88. package/vendor/fastctx/src/render_plan.rs +468 -0
  89. package/vendor/fastctx/src/runtime/activity.rs +159 -0
  90. package/vendor/fastctx/src/runtime/hosts.rs +99 -0
  91. package/vendor/fastctx/src/runtime/journal.rs +556 -0
  92. package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
  93. package/vendor/fastctx/src/runtime/mod.rs +746 -0
  94. package/vendor/fastctx/src/runtime/protocol.rs +296 -0
  95. package/vendor/fastctx/src/runtime/session.rs +536 -0
  96. package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
  97. package/vendor/fastctx/src/search_parallelism.rs +106 -0
  98. package/vendor/fastctx/src/search_text.rs +227 -0
  99. package/vendor/fastctx/src/server.rs +359 -0
  100. package/vendor/fastctx/src/server_manifest.rs +468 -0
  101. package/vendor/fastctx/src/server_support.rs +826 -0
  102. package/vendor/fastctx/src/session.rs +629 -0
  103. package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
  104. package/vendor/fastctx/src/shell/bash.rs +263 -0
  105. package/vendor/fastctx/src/shell/buffer.rs +108 -0
  106. package/vendor/fastctx/src/shell/encoding.rs +403 -0
  107. package/vendor/fastctx/src/shell/foreground.rs +115 -0
  108. package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
  109. package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
  110. package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
  111. package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
  112. package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
  113. package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
  114. package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
  115. package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
  116. package/vendor/fastctx/src/shell/mod.rs +345 -0
  117. package/vendor/fastctx/src/shell/normalize.rs +389 -0
  118. package/vendor/fastctx/src/shell/output.rs +406 -0
  119. package/vendor/fastctx/src/shell/process.rs +493 -0
  120. package/vendor/fastctx/src/shell_server.rs +156 -0
  121. package/vendor/fastctx/src/skip_report.rs +83 -0
  122. package/vendor/fastctx/src/stdio_transport.rs +177 -0
  123. package/vendor/fastctx/src/tool_schema.rs +204 -0
  124. package/vendor/fastctx/src/traversal.rs +846 -0
  125. package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
  126. package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
  127. package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
  128. package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
  129. package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
  130. package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
  131. package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
  132. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
  133. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
  134. package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
  135. package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
  136. package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
  137. package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
  138. package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
  139. package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
  140. package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
@@ -0,0 +1,500 @@
1
+ //! Cancelable fixed-chunk sorting on the shared grep/glob extra executor.
2
+
3
+ use crate::file_executor::{BurstEnvelope, BurstPermit, BurstUse, GrepGlobExecutor};
4
+ use crate::operation::OperationCtx;
5
+ #[cfg(test)]
6
+ use crate::operation::TestStage;
7
+ use std::cmp::Ordering;
8
+ use std::collections::VecDeque;
9
+ use std::fmt;
10
+ use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
11
+ use std::sync::Arc;
12
+ use std::sync::mpsc::{self, RecvTimeoutError};
13
+ use std::time::Duration;
14
+
15
+ pub(crate) const SORT_CHUNK_ITEMS: usize = 4_096;
16
+ const MERGE_CANCEL_ITEMS: usize = 1_024;
17
+ const LEAF_WAIT_POLL: Duration = Duration::from_millis(10);
18
+
19
+ /// A sorted collection plus test-only evidence about bounded execution.
20
+ pub(crate) struct SortOutput<T> {
21
+ pub(crate) items: Vec<T>,
22
+ #[cfg(test)]
23
+ pub(crate) metrics: SortMetrics,
24
+ }
25
+
26
+ #[cfg(test)]
27
+ #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
28
+ pub(crate) struct SortMetrics {
29
+ pub(crate) chunks: usize,
30
+ pub(crate) inline_chunks: usize,
31
+ pub(crate) spawned_chunks: usize,
32
+ pub(crate) merge_pops: usize,
33
+ }
34
+
35
+ /// Failures that must stop the request instead of returning a partial order.
36
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
37
+ pub(crate) enum SortError {
38
+ Cancelled,
39
+ WorkerPanicked,
40
+ WorkerDisconnected,
41
+ Invariant,
42
+ }
43
+
44
+ impl fmt::Display for SortError {
45
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46
+ match self {
47
+ Self::Cancelled => formatter.write_str("Request cancelled."),
48
+ Self::WorkerPanicked => formatter
49
+ .write_str("Internal file sorting failure: a bounded sort worker panicked."),
50
+ Self::WorkerDisconnected => formatter.write_str(
51
+ "Internal file sorting failure: a bounded sort worker exited without a result.",
52
+ ),
53
+ Self::Invariant => formatter.write_str(
54
+ "Internal file sorting failure: the bounded sort state was inconsistent.",
55
+ ),
56
+ }
57
+ }
58
+ }
59
+
60
+ /// Sorts fixed chunks with try-only extra leaves, then performs a deterministic
61
+ /// cancelable k-way merge on the coordinator.
62
+ pub(crate) fn sort_cancelable<T, F>(
63
+ items: Vec<T>,
64
+ compare: F,
65
+ operation: Option<&OperationCtx>,
66
+ executor: Option<&Arc<GrepGlobExecutor>>,
67
+ ) -> Result<SortOutput<T>, SortError>
68
+ where
69
+ T: Send + 'static,
70
+ F: Fn(&T, &T) -> Ordering + Send + Sync + 'static,
71
+ {
72
+ check_operation(operation)?;
73
+ if items.len() <= SORT_CHUNK_ITEMS {
74
+ let mut items = items;
75
+ sort_inline_chunk(&mut items, &compare, operation)?;
76
+ return Ok(SortOutput {
77
+ items,
78
+ #[cfg(test)]
79
+ metrics: SortMetrics {
80
+ chunks: 1,
81
+ inline_chunks: 1,
82
+ ..SortMetrics::default()
83
+ },
84
+ });
85
+ }
86
+
87
+ let mut source = items.into_iter();
88
+ let mut chunks = Vec::new();
89
+ loop {
90
+ let chunk = source.by_ref().take(SORT_CHUNK_ITEMS).collect::<Vec<_>>();
91
+ if chunk.is_empty() {
92
+ break;
93
+ }
94
+ chunks.push(chunk);
95
+ }
96
+ let chunk_count = chunks.len();
97
+ let compare = Arc::new(compare);
98
+ let (sender, receiver) = mpsc::channel::<Result<(usize, BurstEnvelope<Vec<T>>), SortError>>();
99
+ let mut inline_chunks = Vec::new();
100
+ let mut chunks = chunks.into_iter().enumerate();
101
+ let Some(base_chunk) = chunks.next() else {
102
+ return Err(SortError::Invariant);
103
+ };
104
+ inline_chunks.push(base_chunk);
105
+ let mut spawned = 0_usize;
106
+
107
+ for (chunk_index, chunk) in chunks {
108
+ check_operation(operation)?;
109
+ let Some(executor) = executor else {
110
+ inline_chunks.push((chunk_index, chunk));
111
+ continue;
112
+ };
113
+ let Some(ticket) = executor.try_runner_ticket() else {
114
+ inline_chunks.push((chunk_index, chunk));
115
+ continue;
116
+ };
117
+ let Some(permit) = executor.try_burst(BurstUse::SortExtra) else {
118
+ drop(ticket);
119
+ inline_chunks.push((chunk_index, chunk));
120
+ continue;
121
+ };
122
+
123
+ let worker_compare = Arc::clone(&compare);
124
+ let worker_operation = operation.cloned();
125
+ let worker_sender = sender.clone();
126
+ let job = move |(chunk_index, mut chunk, permit): (usize, Vec<T>, BurstPermit)| {
127
+ let outcome = catch_unwind(AssertUnwindSafe(|| {
128
+ stage_sort_chunk(worker_operation.as_ref());
129
+ check_operation(worker_operation.as_ref())?;
130
+ chunk.sort_by(|left, right| worker_compare(left, right));
131
+ check_operation(worker_operation.as_ref())
132
+ }));
133
+ match outcome {
134
+ Ok(Ok(())) => {
135
+ let _ =
136
+ worker_sender.send(Ok((chunk_index, BurstEnvelope::new(permit, chunk))));
137
+ }
138
+ Ok(Err(error)) => {
139
+ drop(permit);
140
+ let _ = worker_sender.send(Err(error));
141
+ }
142
+ Err(panic) => {
143
+ drop(permit);
144
+ let _ = worker_sender.send(Err(SortError::WorkerPanicked));
145
+ resume_unwind(panic);
146
+ }
147
+ }
148
+ };
149
+ match executor.try_spawn_with_payload(ticket, (chunk_index, chunk, permit), job) {
150
+ Ok(()) => spawned = spawned.saturating_add(1),
151
+ Err((ticket, (chunk_index, chunk, permit), _job)) => {
152
+ drop(ticket);
153
+ drop(permit);
154
+ inline_chunks.push((chunk_index, chunk));
155
+ }
156
+ }
157
+ }
158
+ drop(sender);
159
+
160
+ for (_, chunk) in &mut inline_chunks {
161
+ sort_inline_chunk(chunk, compare.as_ref(), operation)?;
162
+ }
163
+ let inline_count = inline_chunks.len();
164
+ let mut sorted_chunks = (0..chunk_count).map(|_| None).collect::<Vec<_>>();
165
+ for (chunk_index, chunk) in inline_chunks {
166
+ let Some(slot) = sorted_chunks.get_mut(chunk_index) else {
167
+ return Err(SortError::Invariant);
168
+ };
169
+ if slot.replace(chunk).is_some() {
170
+ return Err(SortError::Invariant);
171
+ }
172
+ }
173
+ let mut received = inline_count;
174
+ while received < inline_count.saturating_add(spawned) {
175
+ check_operation(operation)?;
176
+ match receiver.recv_timeout(LEAF_WAIT_POLL) {
177
+ Ok(Ok((chunk_index, envelope))) => {
178
+ let Some(slot) = sorted_chunks.get_mut(chunk_index) else {
179
+ return Err(SortError::Invariant);
180
+ };
181
+ if slot.replace(envelope.into_value()).is_some() {
182
+ return Err(SortError::Invariant);
183
+ }
184
+ received = received.saturating_add(1);
185
+ }
186
+ Ok(Err(error)) => return Err(error),
187
+ Err(RecvTimeoutError::Timeout) => continue,
188
+ Err(RecvTimeoutError::Disconnected) => return Err(SortError::WorkerDisconnected),
189
+ }
190
+ }
191
+ check_operation(operation)?;
192
+ let sorted_chunks = sorted_chunks
193
+ .into_iter()
194
+ .collect::<Option<Vec<Vec<T>>>>()
195
+ .ok_or(SortError::Invariant)?;
196
+
197
+ let (items, merge_pops) = match catch_unwind(AssertUnwindSafe(|| {
198
+ merge_sorted_chunks(sorted_chunks, compare.as_ref(), operation)
199
+ })) {
200
+ Ok(result) => result?,
201
+ Err(_) => return Err(SortError::WorkerPanicked),
202
+ };
203
+ #[cfg(not(test))]
204
+ let _ = merge_pops;
205
+ Ok(SortOutput {
206
+ items,
207
+ #[cfg(test)]
208
+ metrics: SortMetrics {
209
+ chunks: chunk_count,
210
+ inline_chunks: inline_count,
211
+ spawned_chunks: spawned,
212
+ merge_pops,
213
+ },
214
+ })
215
+ }
216
+
217
+ fn sort_inline_chunk<T, F>(
218
+ chunk: &mut [T],
219
+ compare: &F,
220
+ operation: Option<&OperationCtx>,
221
+ ) -> Result<(), SortError>
222
+ where
223
+ F: Fn(&T, &T) -> Ordering,
224
+ {
225
+ stage_sort_chunk(operation);
226
+ check_operation(operation)?;
227
+ catch_unwind(AssertUnwindSafe(|| chunk.sort_by(compare)))
228
+ .map_err(|_| SortError::WorkerPanicked)?;
229
+ check_operation(operation)
230
+ }
231
+
232
+ fn merge_sorted_chunks<T, F>(
233
+ chunks: Vec<Vec<T>>,
234
+ compare: &F,
235
+ operation: Option<&OperationCtx>,
236
+ ) -> Result<(Vec<T>, usize), SortError>
237
+ where
238
+ F: Fn(&T, &T) -> Ordering,
239
+ {
240
+ if chunks.len() == 1 {
241
+ return chunks
242
+ .into_iter()
243
+ .next()
244
+ .map(|chunk| (chunk, 0))
245
+ .ok_or(SortError::Invariant);
246
+ }
247
+ let total = chunks
248
+ .iter()
249
+ .try_fold(0_usize, |total, chunk| total.checked_add(chunk.len()))
250
+ .ok_or(SortError::Invariant)?;
251
+ stage_sort_merge(operation);
252
+ check_operation(operation)?;
253
+ let mut streams = chunks.into_iter().map(VecDeque::from).collect::<Vec<_>>();
254
+ let mut heap = Vec::with_capacity(streams.len());
255
+ for index in 0..streams.len() {
256
+ if !streams[index].is_empty() {
257
+ heap_push(&mut heap, index, &streams, compare)?;
258
+ }
259
+ }
260
+
261
+ let mut output = Vec::with_capacity(total);
262
+ while !heap.is_empty() {
263
+ if !output.is_empty() && output.len() % MERGE_CANCEL_ITEMS == 0 {
264
+ stage_sort_merge(operation);
265
+ check_operation(operation)?;
266
+ }
267
+ let stream_index = heap_pop(&mut heap, &streams, compare)?;
268
+ let item = streams
269
+ .get_mut(stream_index)
270
+ .and_then(VecDeque::pop_front)
271
+ .ok_or(SortError::Invariant)?;
272
+ output.push(item);
273
+ if !streams[stream_index].is_empty() {
274
+ heap_push(&mut heap, stream_index, &streams, compare)?;
275
+ }
276
+ }
277
+ check_operation(operation)?;
278
+ Ok((output, total))
279
+ }
280
+
281
+ fn heap_push<T, F>(
282
+ heap: &mut Vec<usize>,
283
+ value: usize,
284
+ streams: &[VecDeque<T>],
285
+ compare: &F,
286
+ ) -> Result<(), SortError>
287
+ where
288
+ F: Fn(&T, &T) -> Ordering,
289
+ {
290
+ heap.push(value);
291
+ let mut child = heap.len() - 1;
292
+ while child > 0 {
293
+ let parent = (child - 1) / 2;
294
+ if !stream_precedes(heap[child], heap[parent], streams, compare)? {
295
+ break;
296
+ }
297
+ heap.swap(child, parent);
298
+ child = parent;
299
+ }
300
+ Ok(())
301
+ }
302
+
303
+ fn heap_pop<T, F>(
304
+ heap: &mut Vec<usize>,
305
+ streams: &[VecDeque<T>],
306
+ compare: &F,
307
+ ) -> Result<usize, SortError>
308
+ where
309
+ F: Fn(&T, &T) -> Ordering,
310
+ {
311
+ if heap.is_empty() {
312
+ return Err(SortError::Invariant);
313
+ }
314
+ let root = heap.swap_remove(0);
315
+ let mut parent = 0;
316
+ while parent < heap.len() {
317
+ let left = parent.saturating_mul(2).saturating_add(1);
318
+ if left >= heap.len() {
319
+ break;
320
+ }
321
+ let right = left + 1;
322
+ let child =
323
+ if right < heap.len() && stream_precedes(heap[right], heap[left], streams, compare)? {
324
+ right
325
+ } else {
326
+ left
327
+ };
328
+ if !stream_precedes(heap[child], heap[parent], streams, compare)? {
329
+ break;
330
+ }
331
+ heap.swap(parent, child);
332
+ parent = child;
333
+ }
334
+ Ok(root)
335
+ }
336
+
337
+ fn stream_precedes<T, F>(
338
+ left: usize,
339
+ right: usize,
340
+ streams: &[VecDeque<T>],
341
+ compare: &F,
342
+ ) -> Result<bool, SortError>
343
+ where
344
+ F: Fn(&T, &T) -> Ordering,
345
+ {
346
+ let left_item = streams
347
+ .get(left)
348
+ .and_then(VecDeque::front)
349
+ .ok_or(SortError::Invariant)?;
350
+ let right_item = streams
351
+ .get(right)
352
+ .and_then(VecDeque::front)
353
+ .ok_or(SortError::Invariant)?;
354
+ let ordering = compare(left_item, right_item);
355
+ Ok(ordering == Ordering::Less || (ordering == Ordering::Equal && left < right))
356
+ }
357
+
358
+ fn check_operation(operation: Option<&OperationCtx>) -> Result<(), SortError> {
359
+ if operation.is_some_and(|operation| operation.check().is_err()) {
360
+ Err(SortError::Cancelled)
361
+ } else {
362
+ Ok(())
363
+ }
364
+ }
365
+
366
+ fn stage_sort_chunk(operation: Option<&OperationCtx>) {
367
+ #[cfg(test)]
368
+ if let Some(operation) = operation {
369
+ operation.stage(TestStage::SortChunk);
370
+ }
371
+ #[cfg(not(test))]
372
+ let _ = operation;
373
+ }
374
+
375
+ fn stage_sort_merge(operation: Option<&OperationCtx>) {
376
+ #[cfg(test)]
377
+ if let Some(operation) = operation {
378
+ operation.stage(TestStage::SortMerge);
379
+ }
380
+ #[cfg(not(test))]
381
+ let _ = operation;
382
+ }
383
+
384
+ #[cfg(test)]
385
+ mod tests {
386
+ use super::{SORT_CHUNK_ITEMS, SortError, sort_cancelable};
387
+ use crate::file_executor::GrepGlobExecutor;
388
+ use crate::operation::{RequestWorkGuard, TestStage};
389
+ use rmcp::model::RequestId;
390
+ use std::sync::Arc;
391
+ use std::sync::atomic::{AtomicBool, Ordering};
392
+ use tokio_util::sync::CancellationToken;
393
+
394
+ #[test]
395
+ fn p1_and_p4_match_the_independent_stable_sort_oracle() {
396
+ let input = (0..SORT_CHUNK_ITEMS * 3 + 91)
397
+ .map(|index| ((index * 7_919) % 997, index))
398
+ .collect::<Vec<_>>();
399
+ let mut oracle = input.clone();
400
+ oracle.sort_by_key(|item| item.0);
401
+
402
+ for parallelism in [1, 4] {
403
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(parallelism));
404
+ let output = sort_cancelable(
405
+ input.clone(),
406
+ |left, right| left.0.cmp(&right.0),
407
+ None,
408
+ Some(&executor),
409
+ )
410
+ .unwrap();
411
+ assert_eq!(output.items, oracle);
412
+ assert_eq!(output.metrics.chunks, 4);
413
+ if parallelism == 1 {
414
+ assert_eq!(output.metrics.spawned_chunks, 0);
415
+ assert_eq!(output.metrics.inline_chunks, 4);
416
+ } else {
417
+ assert!(output.metrics.spawned_chunks <= 3);
418
+ assert_eq!(
419
+ output.metrics.inline_chunks + output.metrics.spawned_chunks,
420
+ 4
421
+ );
422
+ }
423
+ assert_eq!(output.metrics.merge_pops, oracle.len());
424
+ executor.wait_for_test_quiescence();
425
+ let burst = executor.test_burst_ledger();
426
+ let tickets = executor.test_ticket_ledger();
427
+ assert_eq!(burst.allocated, burst.released);
428
+ assert_eq!(burst.live, 0);
429
+ assert_eq!(burst.duplicate_releases, 0);
430
+ assert_eq!(tickets.allocated, tickets.released);
431
+ assert_eq!(tickets.live, 0);
432
+ assert_eq!(tickets.duplicate_releases, 0);
433
+ }
434
+ }
435
+
436
+ #[test]
437
+ fn sort_chunk_and_merge_barriers_cancel_without_a_partial_order() {
438
+ for target in [TestStage::SortChunk, TestStage::SortMerge] {
439
+ let parent = CancellationToken::new();
440
+ let cancel = parent.clone();
441
+ let fired = Arc::new(AtomicBool::new(false));
442
+ let fired_hook = Arc::clone(&fired);
443
+ let (mut guard, operation) = RequestWorkGuard::new_with_hook(
444
+ RequestId::String(Arc::from(format!("sort-{target:?}"))),
445
+ parent,
446
+ Arc::new(move |stage| {
447
+ if stage == target && !fired_hook.swap(true, Ordering::AcqRel) {
448
+ cancel.cancel();
449
+ }
450
+ }),
451
+ );
452
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(4));
453
+ let input = (0..SORT_CHUNK_ITEMS * 3).rev().collect::<Vec<_>>();
454
+ let error = sort_cancelable(input, usize::cmp, Some(&operation), Some(&executor))
455
+ .err()
456
+ .expect("the requested sort stage must observe cancellation");
457
+ assert_eq!(error, SortError::Cancelled);
458
+ assert!(fired.load(Ordering::Acquire));
459
+ guard.disarm();
460
+ executor.wait_for_test_quiescence();
461
+ assert_eq!(executor.test_burst_available(), executor.extra_capacity());
462
+ assert_eq!(executor.test_ticket_available(), executor.extra_capacity());
463
+ }
464
+ }
465
+
466
+ #[test]
467
+ fn comparator_panic_during_merge_is_an_explicit_sort_failure() {
468
+ let merge_started = Arc::new(AtomicBool::new(false));
469
+ let merge_hook = Arc::clone(&merge_started);
470
+ let (mut guard, operation) = RequestWorkGuard::new_with_hook(
471
+ RequestId::String(Arc::from("sort-merge-panic")),
472
+ CancellationToken::new(),
473
+ Arc::new(move |stage| {
474
+ if stage == TestStage::SortMerge {
475
+ merge_hook.store(true, Ordering::Release);
476
+ }
477
+ }),
478
+ );
479
+ let compare_flag = Arc::clone(&merge_started);
480
+ let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(1));
481
+ let error = sort_cancelable(
482
+ (0..SORT_CHUNK_ITEMS * 2).rev().collect(),
483
+ move |left: &usize, right: &usize| {
484
+ assert!(
485
+ !compare_flag.load(Ordering::Acquire),
486
+ "injected comparator panic during merge"
487
+ );
488
+ left.cmp(right)
489
+ },
490
+ Some(&operation),
491
+ Some(&executor),
492
+ )
493
+ .err()
494
+ .expect("the merge panic must not escape the sort API");
495
+ assert_eq!(error, SortError::WorkerPanicked);
496
+ assert!(merge_started.load(Ordering::Acquire));
497
+ guard.disarm();
498
+ executor.wait_for_test_quiescence();
499
+ }
500
+ }