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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
//! TEST-ONLY fault injection for the producer boundaries that used to collapse a
//! dead producer into a clean end of input: the query row stream's two (issue
//! #3106), the k-way MERGE's shared row-forward funnel (issue #3120), and every
//! spawned task on the multi-generation streaming-scan path (issue #3124).
//!
//! # Why this exists
//!
//! The query row stream ([`crate::storage::sstable::reader::QueryRowStream`]) is
//! fed by a detached producer thread over a bounded channel, and on the full-ring
//! arm that thread is in turn fed by an INNER `tokio` task over a second channel.
//! Before issue #3106 BOTH boundaries collapsed a channel DISCONNECT into a clean
//! end of stream, so a producer that UNWOUND (a panic anywhere in the walk/decode,
//! rather than an `Err` return) dropped its sender without a terminal message and
//! the request completed SUCCESSFULLY with a silently truncated result set. The
//! fix makes completion explicit at both boundaries; this module is how a test
//! PROVES that, deterministically, without waiting for a real decode bug to
//! unwind.
//!
//! Independent seams, one per boundary:
//!
//! * [`arm_query_row_producer_panic`] → the OUTER boundary: the query-row producer
//! THREAD panics at a batch handoff ([`ProducerFault::before_batch_handoff`],
//! consulted in `query_rows::emit_rows`).
//! * [`arm_inner_scan_task_panic`] → the INNER boundary: the batched-scan `tokio`
//! TASK panics ([`inner_scan_task_checkpoint`]). ONE checkpoint site, in that
//! task's cursor-open prelude
//! (`data_access::joined_scan_stream::SSTableReader::open_batched_scan_cursor`),
//! which sits ABOVE the `requires_chunk_stitching()` branch and is therefore
//! reached whatever on-disk format the reader has — a checkpoint inside either
//! branch would fire only for that branch's formats and could silently not fire.
//! This is the arm a `do_get` with NO token filter takes, so it is the issue's
//! own repro; killing the task drops its sender with no terminator, which the
//! query-row thread used to read as "the scan finished".
//! * [`arm_scan_task_panic`] → the generalisation of the second seam to EVERY
//! spawned scan task on the ≠1-generation (query-engine full scan) path (issue
//! #3124): the fan-out k-way merge task, a per-reader per-row sub-scan, the
//! windowed forwarder, and the cross-generation RECONCILING merge task — see
//! [`ScanTaskSite`]. Each had a DISCARDED `JoinHandle`
//! and a consumer that read channel-close as end-of-scan, i.e. the #3106 defect
//! on the multi-generation path. An arm is keyed by `(site, scope)`, not scope
//! alone, because one scan traverses several of these checkpoints.
//! * [`arm_merge_producer_panic`] → the K-WAY MERGE boundary (issue #3120): a
//! merge producer THREAD panics at a row forward
//! ([`MergeProducerFault::before_row_forward`], consulted in
//! `write_engine::merge::from_readers::forward_row`). That is the `emit`
//! callback BOTH `stream_all_partitions_for_compaction` and
//! `..._for_query` invoke, and it sits in `write_engine/merge` ABOVE any
//! reader format branch — so unlike a checkpoint inside the reader there is no
//! `requires_chunk_stitching()`-style bypass that could make the fault silently
//! not fire, and ONE funnel covers BOTH producer shapes (path-based compaction
//! and shared-reader warm query).
//!
//! Its registry is SEPARATE from the query-row one ([`arm_merge_producer_panic`]
//! vs [`arm_query_row_producer_panic`]) on purpose: a shared registry would let
//! a merge producer consume an arm a query-row test registered (and vice versa),
//! which is exactly the cross-consumption class the per-reader scoping below
//! exists to eliminate.
//!
//! # Every arm is SCOPED to one reader — no test can take another's (roborev)
//!
//! An arm is registered against a `scope` STRING, and a checkpoint fires only when
//! the scanning reader's `Data.db` PATH contains that scope. This is the
//! structural replacement for the earlier "process-global, caller serializes"
//! convention, which was unsound in the `cqlite-core` lib test binary: the in-`src`
//! panic tests compile into the SAME binary as thousands of other tests and
//! libtest runs them in parallel, so a concurrent test's scan could consume the arm
//! and let a panic-injection test pass for the wrong reason (a doc comment asking
//! callers to serialize is exactly the mitigation that failed).
//!
//! Two properties make it structural rather than conventional:
//!
//! * **Scoped consumption.** A scan whose path does not match leaves the arm
//! registered, so it cannot consume a foreign arm even by racing. A test scopes
//! to its own `TempDir` path (unique per run) or to its own `keyspace/table`.
//! * **A registry, not a slot.** Arms live in a `Vec`, so two concurrently armed
//! tests coexist; neither can clobber the other's arm by arming second. Each
//! guard removes its OWN entry (by id) on drop.
//!
//! Matching is by substring so a caller can scope to a directory (`keyspace/table`)
//! without knowing the generated SSTable filename. An arm that matches nothing is
//! simply never taken — the arming test then fails LOUDLY (it sees no error), which
//! is the correct failure direction.
//!
//! # Why this is not a production knob (no-heuristics safe)
//!
//! * Everything that can arm a fault is `#[cfg(any(test, feature =
//! "producer-fault-injection"))]`. In a default build the arming API does not
//! exist, the registries do not exist, [`ProducerFault`] is a zero-sized struct
//! with no fields, and both checkpoints compile to empty functions that never
//! even evaluate their scope closure — so a production scan does not pay the
//! `PathBuf` clone. The production build is byte-identical to one without this
//! module's body, and the module itself is `pub(crate)` there.
//! * No environment variable, config field or on-disk byte pattern can arm it: the
//! only way in is a Rust call to an arming function that does not exist in
//! production builds. So it cannot influence a decoding decision (issue #28)
//! even accidentally.
//! * `cqlite-flight` enables the feature from its `[dev-dependencies]` (the same
//! convention `arrow-shape-corpus` / `test-util` already use), so the shipped
//! Flight binary never links it.
//!
//! # One seam injects an `Err`, not a panic (issue #3154)
//!
//! [`arm_merge_construction_error`] (child module `construction`) makes
//! `KWayMerger::new` REPORT a chosen error variant on the cross-generation merge
//! path, proving the narrowed fallback classification: an I/O or corruption failure
//! must propagate, while a merger-ineligible unsupported-format failure must still
//! degrade to the documented concat. See that module's doc.
/// The `Err`-reporting construction seam (issue #3154), in a child module so this
/// file stays under the ~800-line campsite target (epic #1116).
///
/// Gated to exactly where the seam it injures EXISTS —
/// `generation_merge::stream_generations_for_read` is `write-support` AND
/// `not(tombstones)` — for the same reason [`ScanTaskSite::CrossGenerationMerge`] is:
/// a symbol nothing in a configuration can reach would be a lie about that
/// configuration's coverage, and `#[allow(dead_code)]` would hide it, not state it.
pub use ;
/// Producer-fault state captured ONCE when a query row stream is opened, then
/// owned by that stream's producer thread.
///
/// A zero-sized, no-op struct in a production build (see the module doc).
pub
/// Merge-producer fault state captured ONCE when a k-way merge run's producer
/// thread starts, then owned by that thread (issue #3120).
///
/// A zero-sized, no-op struct in a production build, exactly like
/// [`ProducerFault`] (see the module doc).
///
/// Gated on `write-support` — unlike its query-row sibling [`ProducerFault`],
/// which serves a READ-path stream that exists unconditionally. The k-way merge,
/// its producer threads, and the `forward_row` funnel this checkpoint lives in are
/// ALL `#[cfg(feature = "write-support")]` (`storage::write_engine::merge`), so
/// without that feature there is no producer thread to injure and this type has no
/// possible constructor. The gate's `minimal-build`
/// (`--no-default-features --features all-compression`) proves it: an ungated
/// version is genuinely dead code there, which is the wiring telling the truth
/// rather than a warning to silence.
pub
/// Which spawned scan task a checkpoint belongs to (issue #3124).
///
/// Part of an arm's key, not just documentation: the four #3124 boundaries sit on
/// ONE code path, so a single fan-out scan runs through the merge task, each
/// per-reader sub-scan and (on a compressed reader) the windowed forwarder. Keyed by
/// scope ALONE, a test arming the boundary it means to prove would have its arm
/// consumed by whichever checkpoint the scan reached first, and would then pass
/// while the boundary under test was never exercised.
///
/// Exists in production builds too (it is a checkpoint PARAMETER), where every
/// checkpoint compiles to an empty function that never inspects it.
/// Consulted at a spawned scan task's checkpoint so an armed fault unwinds THAT
/// task — reproducing exactly the condition every one of these boundaries used to
/// read as "the scan finished": the task's sender drops with no error and no
/// terminator.
///
/// Panics ON PURPOSE when an arm for this SITE, scoped to THIS reader, is
/// registered, taking that arm so exactly one task dies. Compiles to an empty
/// function — which never calls `scope_of` — in a production build.
pub
/// The INNER batched-scan task's checkpoint (its cursor-open prelude) — the
/// boundary whose disconnect the query-row thread used to read as "the scan
/// finished" (issue #3106). A named wrapper over
/// [`scan_task_checkpoint`] so the #3106 call site reads as its own boundary.
pub
/// A checkpoint scope CAPTURED for a task that will run later, elsewhere (issue
/// #3124).
///
/// [`scan_task_checkpoint`] takes a lazy closure because its callers hold the reader
/// right there. Two #3124 sites do not: the fan-out merge task and the windowed
/// forwarder are `tokio::spawn`ed with an environment that must OWN whatever they
/// check, and the forwarder is spawned from a function with no reader in scope. This
/// type is that owned scope — and it is a ZERO-SIZED, no-op struct in a production
/// build, so a production scan clones no `PathBuf` and the spawned task's
/// environment grows by nothing.
pub
/// The `Err`-reporting construction seam's take side (issue #3154), gated to the one
/// configuration whose call site exists — the cross-generation reconciling merge's
/// construction window in `generation_merge::stream_generations_for_read` — so no
/// build carries a method nothing can reach (see the `mod construction` doc above).
/// The panic message both checkpoints raise. Exported so a test can (a) assert
/// the forwarded error carries it and (b) suppress exactly this panic in its
/// panic hook without silencing a real one.
pub const INJECTED_PANIC_MESSAGE: &str =
"cqlite test fault injection (issue #3106): producer panic";
/// Arm the next query row stream opened over a reader whose `Data.db` path
/// contains `scope` to panic in its producer THREAD just before it hands over
/// batch number `after_batches` (0-based), so `after_batches` batches reach the
/// consumer and the walk then dies MID-STREAM.
///
/// `0` kills the producer before its first handoff. Disarmed when the returned
/// guard drops, and TAKEN by the first MATCHING stream — a stream over any other
/// reader leaves it alone, so a concurrently-running test can neither consume nor
/// clobber this arm (see the module doc). Scope to something unique: a test's own
/// `TempDir` path, or `keyspace/table`.
///
/// TEST-ONLY: this symbol does not exist unless `cfg(test)` or the
/// `producer-fault-injection` feature is on.
/// Guard returned by [`arm_query_row_producer_panic`]: removes its own arm on
/// drop. Holds no lock, so it is safe to hold across an `.await`.
/// Arm the next batched-scan task over a reader whose `Data.db` path contains
/// `scope` to panic in its cursor-open prelude, so the task unwinds and drops its
/// sender with no error and no terminator.
///
/// It dies before any row and independently of which format branch the reader
/// would have taken; the join that must catch it wraps the whole task, so the
/// property this proves holds for a panic anywhere inside it. There is
/// deliberately no "die after N units" knob — the prelude is the only checkpoint,
/// so a count would be unspendable.
///
/// Disarmed when the returned guard drops, and TAKEN by the first MATCHING scan;
/// a scan over any other reader leaves it registered (see the module doc).
///
/// TEST-ONLY: this symbol does not exist unless `cfg(test)` or the
/// `producer-fault-injection` feature is on.
/// Arm the next scan task of `site`, over a reader whose `Data.db` path contains
/// `scope`, to panic at that site's checkpoint — so the task unwinds and drops its
/// sender with no error and no terminator (issue #3124).
///
/// The task dies before any row it would have produced at that site, and the join
/// that must catch it wraps the whole task, so the property proven holds for a panic
/// anywhere inside it. There is deliberately no "die after N units" knob: each site
/// has exactly ONE checkpoint, so a count would be unspendable.
///
/// Disarmed when the returned guard drops, and TAKEN by the first scan that matches
/// BOTH the site and the scope; any other scan — or the same scan at a different
/// site — leaves it registered (see the module doc).
///
/// TEST-ONLY: this symbol does not exist unless `cfg(test)` or the
/// `producer-fault-injection` feature is on.
/// Guard returned by [`arm_scan_task_panic`] / [`arm_inner_scan_task_panic`]:
/// removes its own arm on drop. Holds no lock, so it is safe to hold across an
/// `.await`.
/// Arm the next k-way MERGE run over a reader whose `Data.db` path contains
/// `scope` to panic in its producer THREAD just before it forwards row number
/// `after_rows` (0-based), so `after_rows` rows reach the merge and the run then
/// dies MID-WALK (issue #3120).
///
/// `0` kills the producer before its first row. Disarmed when the returned guard
/// drops, and TAKEN by the first MATCHING run — a run over any other input leaves
/// it alone, so in a K-input merge exactly the intended input's producer dies and
/// the rows-through count is deterministic. Scope to ONE input's `Data.db` path,
/// NOT the enclosing `TempDir` (see [`armed::take_merge`]).
///
/// TEST-ONLY: this symbol does not exist unless (`cfg(test)` or the
/// `producer-fault-injection` feature) AND `write-support` — the k-way merge it
/// injures is itself `write-support`-gated (see [`MergeProducerFault`]).
/// Guard returned by [`arm_merge_producer_panic`]: removes its own arm on drop.
/// Holds no lock, so it is safe to hold across an `.await`.
/// Panic-hook silencing, in a child module so this file stays under the ~800-line
/// campsite target (epic #1116). Re-exported here so every caller keeps the
/// `producer_fault::silence_injected_panics` path it already uses.
pub use ;
// Unit tests in a `*_tests.rs` sibling so this file stays under the ~800-line
// campsite target (epic #1116 / #1135) — see that file's header.