Skip to main content

krabka_log/log/
transaction.rs

1//! Last-stable-offset bookkeeping and the effect of one transaction end
2//! marker on the in-memory transaction state.
3//!
4//! An open transaction holds the LSO at its first offset, so every append
5//! that is not transactional data moves the LSO through the one helper
6//! here and a commit or abort marker closes the transaction through the
7//! other.
8
9use {
10    super::{
11        Log,
12        control::{
13            TransactionMarkerFlags, batch_control_marker_coordinator_epoch,
14            transaction_marker_flags,
15        },
16    },
17    crate::{error::LogError, stamp_index::OffsetRange, txn_index::AbortedTxn},
18    krabka_ids::{Offset, ProducerId},
19    krabka_protocol::records::RecordBatch,
20};
21
22impl Log {
23    /// Recompute the first unstable offset from the bounds of open transactions and
24    /// every complete transaction whose marker is not below the high
25    /// watermark yet.
26    ///
27    /// The earliest of those transactions holds the offset at its first
28    /// offset. With none, the offset advances to the exact log end. Every
29    /// append path and recovery use this same selection.
30    pub(super) fn refresh_lso(&mut self) -> Result<(), LogError> {
31        let log_end = self.log_end_offset();
32        // The ordered index tracks every mutation of the producer map. Its
33        // maximum preserves the scalar kernel's all-start corruption check;
34        // checking only the minimum would hide a later invalid start.
35        let (first, last) = self.pending.bounds().unwrap_or((log_end, log_end));
36        let completed = self
37            .unreplicated
38            .first_key_value()
39            .map_or(log_end, |(start, _)| *start);
40        let starts = [first.0, last.0, completed.0];
41        self.lso = krabka_verified::first_unstable_offset(&starts, log_end.0)
42            .map(Offset)
43            .ok_or_else(|| {
44                LogError::Corrupt(format!(
45                    "pending transaction starts beyond log end {log_end}"
46                ))
47            })?;
48        Ok(())
49    }
50
51    /// The last stable offset that a reader sees at `high_watermark`.
52    ///
53    /// The call first releases every complete transaction whose marker offset
54    /// is below `high_watermark`, as Kafka's
55    /// `ProducerStateManager.onHighWatermarkUpdated` does. It then answers
56    /// `min(first unstable offset, high_watermark)`, which is Kafka's
57    /// `UnifiedLog.lastStableOffset`. A transaction whose marker the high
58    /// watermark has not passed still holds the answer at its first offset,
59    /// because a leader change can truncate that marker away.
60    ///
61    /// Call it with the partition's current high watermark. The high watermark
62    /// only moves forward, so a released transaction never has to come back.
63    pub fn last_stable_offset(&mut self, high_watermark: Offset) -> Offset {
64        self.release_replicated_transactions(high_watermark);
65        self.lso.min(high_watermark)
66    }
67
68    /// Release every complete transaction whose marker offset is below
69    /// `high_watermark`, and move the first unstable offset forward.
70    ///
71    /// The writer calls this before it appends a transaction marker, so the
72    /// set of complete transactions stays bounded on a partition that no
73    /// reader fetches from.
74    pub fn release_replicated_transactions(&mut self, high_watermark: Offset) {
75        let before = self.unreplicated.len();
76        self.unreplicated
77            .retain(|_, marker_offset| *marker_offset >= high_watermark);
78        if self.unreplicated.len() != before
79            && let Err(error) = self.refresh_lso()
80        {
81            // Releasing a transaction removes a start offset and cannot put a
82            // start beyond the log end. Keep the lower offset if it happens.
83            tracing::warn!(%error, "last stable offset refresh failed");
84        }
85    }
86
87    /// Apply one transaction end marker to the in-memory transaction state.
88    ///
89    /// The marker closes `producer_id`'s open transaction on this partition.
90    /// An ABORT marker appends the transaction's offset range to the active
91    /// `.txnindex`. A COMMIT marker stamps the transaction's data ranges with
92    /// `transaction_stamp`, or with the next stamp from the installed source.
93    ///
94    /// This method never runs for a barrier marker.
95    pub(super) fn apply_transaction_marker(
96        &mut self,
97        batch: &RecordBatch,
98        producer_id: ProducerId,
99        last_offset: Offset,
100        transaction_stamp: Option<u64>,
101    ) -> Result<(), LogError> {
102        // Read the inner control record: key = (version: i16, type: i16) BE.
103        let TransactionMarkerFlags {
104            is_abort,
105            is_commit,
106        } = transaction_marker_flags(batch)?;
107        let closes = krabka_verified::transaction_marker_closes(
108            is_abort,
109            is_commit,
110            self.pending.contains_key(producer_id),
111        );
112        if (is_abort || is_commit)
113            && producer_id.get() >= 0
114            && let Some(epoch) = batch_control_marker_coordinator_epoch(batch)?
115        {
116            self.coordinator_epochs.insert(producer_id, epoch);
117        }
118        if closes && is_abort {
119            let pending_start = self.pending.get(producer_id).map(|offset| offset.0);
120            let Some((start, last)) = krabka_verified::aborted_transaction_interval(
121                pending_start,
122                last_offset.0,
123                producer_id.get(),
124            ) else {
125                return Err(LogError::Corrupt(format!(
126                    "invalid aborted transaction interval for producer {producer_id}"
127                )));
128            };
129            // Kafka's `ProducerStateManager.lastStableOffset(completedTxn)`:
130            // the first offset of another still-open transaction, or of an
131            // earlier completed transaction whose marker has not yet crossed
132            // the high watermark (still in `self.unreplicated`, exactly as
133            // `refresh_lso` also chains in), or one past this transaction's
134            // own last offset when none of those remain.
135            let other_starts: Vec<i64> = self
136                .pending
137                .iter()
138                .filter(|(other_pid, _)| **other_pid != producer_id)
139                .map(|(_, offset)| offset.0)
140                .chain(self.unreplicated.keys().next().map(|offset| offset.0))
141                .collect();
142            let last_stable_offset = krabka_verified::first_unstable_offset(
143                &other_starts,
144                last_offset.0 + 1,
145            )
146            .map(Offset)
147            .ok_or_else(|| {
148                LogError::Corrupt(format!(
149                    "pending transaction starts beyond aborted marker offset for producer {producer_id}"
150                ))
151            })?;
152            self.active_txn_index.append(AbortedTxn {
153                start_offset: Offset(start),
154                last_offset: Offset(last),
155                producer_id,
156                last_stable_offset,
157            })?;
158        }
159        let stamp_ranges = self
160            .pending_stamp_ranges
161            .get(&producer_id)
162            .cloned()
163            .unwrap_or_default();
164        if closes && is_commit && !stamp_ranges.is_empty() {
165            let stamp = transaction_stamp
166                .or_else(|| self.stamp_source.as_ref().map(|source| source.next_stamp()));
167            if let Some(stamp) = stamp {
168                for OffsetRange { first, last } in stamp_ranges {
169                    self.record_stamp_value(first, last, stamp)?;
170                }
171            }
172        }
173        // Keep the in-memory transaction state until all durable sidecar
174        // writes succeed. A caller can then retry a marker whose log append
175        // succeeded but whose index update failed.
176        if closes {
177            if let Some(first_offset) = self.pending.remove(producer_id) {
178                self.unreplicated.insert(first_offset, last_offset);
179            }
180            self.pending_stamp_ranges.remove(&producer_id);
181        }
182        Ok(())
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use {
189        super::*,
190        crate::{
191            log::test_support::{
192                EncodedVerbatim, TempLog, TransactionFields, abort_marker, append_transaction,
193                commit_marker, install_stamps, sample_batch, test_batch_at, test_log,
194                tiny_segments, transaction_entries, transaction_fields, transactional_batch,
195                verbatim_from,
196            },
197            name,
198            segment::Segment,
199            test_support::{aborted_txn, open_log},
200        },
201        bytes::Bytes,
202        krabka_ids::LeaderEpoch,
203        std::fs,
204        tempfile::tempdir,
205    };
206
207    // ---- transactional LSO / txnindex tests ----
208
209    #[test]
210    fn transactional_batch_holds_lso() {
211        let TempLog { dir: _dir, mut log } = test_log();
212        // First, a non-txn batch — LSO advances past it.
213        let mut b0 = sample_batch(1);
214        log.append(&mut b0).unwrap();
215        assert2::assert!(log.lso() == log.log_end_offset());
216
217        // Now an in-flight txn batch — LSO stays.
218        let mut b1 = transactional_batch(1000, 0, &["a", "b"]); // pid=1000 epoch=0
219        let old_lso = log.lso();
220        log.append(&mut b1).unwrap();
221        assert2::assert!(log.lso() == old_lso);
222
223        // Commit marker: the LSO catches up once the high watermark passes it.
224        let mut commit = commit_marker(1000, 0);
225        log.append(&mut commit).unwrap();
226        let log_end = log.log_end_offset();
227        assert2::assert!(log.last_stable_offset(log_end) == log_end);
228    }
229
230    #[test]
231    fn negative_producer_ids_never_create_transaction_state() {
232        let dir = tempdir().unwrap();
233        {
234            let mut log = open_log(dir.path());
235            install_stamps(&mut log, 40, 1);
236
237            let mut owned = sample_batch(1);
238            owned.producer_id = -2;
239            owned.producer_epoch = 0;
240            owned.base_sequence = 0;
241            owned.attributes = owned.attributes.with_transactional(true);
242            log.append(&mut owned).unwrap();
243            assert2::assert!(log.lso() == Offset(1));
244            assert2::assert!(log.stamp_for_offset(Offset(0)) == Some(40));
245
246            let mut marker = commit_marker(-2, 0);
247            log.append(&mut marker).unwrap();
248            assert2::assert!(
249                transaction_fields(&log, ProducerId(-2))
250                    == TransactionFields {
251                        coordinator_epoch: -1,
252                        first_offset: None,
253                    }
254            );
255
256            let mut verbatim = test_batch_at(0);
257            verbatim.producer_id = -3;
258            verbatim.producer_epoch = 0;
259            verbatim.base_sequence = 0;
260            verbatim.attributes = verbatim.attributes.with_transactional(true);
261            let EncodedVerbatim { batch, .. } = verbatim_from(&verbatim, LeaderEpoch(0));
262            log.append_verbatim(&batch).unwrap();
263            assert2::assert!(log.lso() == Offset(3));
264            assert2::assert!(log.stamp_for_offset(Offset(2)) == Some(41));
265            assert2::assert!(log.producer_state_snapshot().is_empty());
266        }
267
268        let reopened = open_log(dir.path());
269        assert2::assert!(reopened.lso() == reopened.log_end_offset());
270        assert2::assert!(
271            transaction_fields(&reopened, ProducerId(-2))
272                == TransactionFields {
273                    coordinator_epoch: -1,
274                    first_offset: None,
275                }
276        );
277        assert2::assert!(
278            transaction_fields(&reopened, ProducerId(-3))
279                == TransactionFields {
280                    coordinator_epoch: -1,
281                    first_offset: None,
282                }
283        );
284        assert2::assert!(reopened.producer_state_snapshot().is_empty());
285    }
286
287    #[yare::parameterized(
288        single_offset = { 0, 3, 4 },
289        two_offset_marker = { 1, 4, 5 },
290    )]
291    fn abort_marker_records_its_base_plus_delta(
292        last_offset_delta: i32,
293        expected_last_offset: i64,
294        expected_lso: i64,
295    ) {
296        let TempLog { dir, mut log } = test_log();
297        append_transaction(&mut log, 1000, 0, &["a", "b", "c"]);
298
299        let mut a = abort_marker(1000, 0);
300        a.last_offset_delta = last_offset_delta;
301        log.append(&mut a).unwrap();
302
303        let entries = transaction_entries(dir.path(), "00000000000000000000.txnindex");
304        // The data batch occupies offsets 0-2; the marker starts at 3.
305        assert2::assert!(
306            entries
307                == [AbortedTxn {
308                    start_offset: Offset(0),
309                    last_offset: Offset(expected_last_offset),
310                    producer_id: ProducerId(1000),
311                    last_stable_offset: Offset(expected_lso),
312                }]
313        );
314    }
315
316    /// An earlier completed transaction, still held in `unreplicated`
317    /// because the high watermark has not passed its marker yet, holds the
318    /// last-stable-offset written for a later transaction's abort marker --
319    /// the same way it holds `refresh_lso`'s in-memory `lso`. Before this
320    /// fix, `other_starts` only chained `self.pending` (other still-open
321    /// transactions), so this scenario wrote `last_offset + 1` into the
322    /// `.txnindex` entry instead of the earlier unreplicated start.
323    #[test]
324    fn abort_marker_lso_is_held_by_an_earlier_unreplicated_transaction() {
325        let TempLog { dir, mut log } = test_log();
326
327        // Producer 1000 commits first: its marker lands at offset 2, and it
328        // stays in `unreplicated` (this test never advances the high
329        // watermark past it).
330        let mut t1 = transactional_batch(1000, 0, &["a", "b"]);
331        log.append(&mut t1).unwrap();
332        log.append(&mut commit_marker(1000, 0)).unwrap();
333
334        // Producer 2000 opens after that and then aborts.
335        let mut t2 = transactional_batch(2000, 0, &["c"]);
336        log.append(&mut t2).unwrap();
337        log.append(&mut abort_marker(2000, 0)).unwrap();
338
339        let entries = transaction_entries(dir.path(), "00000000000000000000.txnindex");
340        // Producer 1000's transaction started at offset 0: that is the
341        // earlier unreplicated start, and it must be the recorded LSO, not
342        // producer 2000's own last_offset + 1.
343        assert2::assert!(
344            entries
345                == [aborted_txn()
346                    .producer(2000)
347                    .bounds(Offset(3)..=Offset(4))
348                    .stable(0)
349                    .call()]
350        );
351    }
352
353    #[test]
354    fn aborted_transaction_uses_cached_marker_segment_beyond_range_end() {
355        let TempLog { dir, mut log } = test_log();
356        let mut transaction = transactional_batch(1000, 0, &["a", "b", "c"]);
357        log.append(&mut transaction).unwrap();
358
359        log.set_config(tiny_segments());
360        let (marker_base, _) = log.append(&mut abort_marker(1000, 0)).unwrap();
361        log.append(&mut sample_batch(1)).unwrap();
362        fs::remove_file(name::txnindex_path(dir.path(), marker_base.0)).unwrap();
363
364        assert2::assert!(log.aborted_in_range(Offset(0), marker_base) == [aborted_txn().call()]);
365    }
366
367    // LSO tracking (owned path) keys on `is_transactional() && !pid.is_none()`.
368    // A NON-transactional batch that carries a valid producer_id (idempotent
369    // producer, pid >= 0) must NOT be treated as an open txn: LSO advances to
370    // log_end. Mutating `&&`→`||` would hold LSO at the batch base (0).
371    #[test]
372    fn non_txn_batch_with_valid_pid_advances_lso() {
373        let TempLog { dir: _dir, mut log } = test_log();
374        // Idempotent (not transactional) producer: pid >= 0, no transactional
375        // attribute bit set.
376        let mut b = sample_batch(2);
377        b.producer_id = 55;
378        b.producer_epoch = 0;
379        assert2::assert!(!b.attributes.is_transactional());
380        log.append(&mut b).unwrap();
381        // Not an open txn → LSO advances to log_end (2), not held at 0.
382        assert2::assert!(log.lso() == Offset(2));
383        assert2::assert!(log.log_end_offset() == Offset(2));
384    }
385
386    #[test]
387    fn lso_held_by_remaining_producer_after_partial_commit() {
388        let TempLog { dir: _dir, mut log } = test_log();
389
390        // Open two producers' transactions in parallel.
391        let mut t1 = transactional_batch(1000, 0, &["a", "b"]);
392        log.append(&mut t1).unwrap();
393        let mut t2 = transactional_batch(2000, 0, &["c"]);
394        log.append(&mut t2).unwrap();
395        let lso_after_open = log.lso();
396
397        // Commit producer 1000. LSO must still be held back by 2000.
398        let mut c1 = commit_marker(1000, 0);
399        log.append(&mut c1).unwrap();
400        let log_end = log.log_end_offset();
401        assert2::assert!(log.last_stable_offset(log_end) == Offset(2));
402        assert2::assert!(log.lso() > lso_after_open);
403
404        // Commit producer 2000. LSO advances to log_end_offset.
405        let mut c2 = commit_marker(2000, 0);
406        log.append(&mut c2).unwrap();
407        let log_end = log.log_end_offset();
408        assert2::assert!(log.last_stable_offset(log_end) == log_end);
409    }
410
411    #[yare::parameterized(
412        malformed = { "malformed", true },
413        different_producer = { "different producer", false },
414    )]
415    fn only_a_valid_marker_for_the_pending_producer_closes_it(case: &str, malformed: bool) {
416        let dir = tempdir().unwrap();
417        {
418            let mut log = open_log(dir.path());
419            append_transaction(&mut log, 1000, 0, &["a", "b"]);
420            let held = log.lso();
421            let mut marker = if malformed {
422                let mut marker = commit_marker(1000, 0);
423                marker.records[0].key = Some(Bytes::from_static(&[0, 0, 0]));
424                marker
425            } else {
426                commit_marker(2000, 0)
427            };
428            // Kafka's `ControlRecordType.parseTypeId` refuses a key too
429            // short to hold its type, so the malformed marker is never
430            // written.
431            let refused = log.append(&mut marker).err().map(|e| e.to_string());
432            let want = malformed.then(|| {
433                "Invalid value size found for end control record key. Must have at least 4 \
434                 bytes, but found only 3"
435                    .to_owned()
436            });
437            assert2::assert!(refused == want, "case {case}");
438            assert2::assert!(log.lso() == held, "case {case}");
439            assert2::assert!(
440                log.pending_transaction_start(ProducerId(1000)) == Some(Offset(0)),
441                "case {case}"
442            );
443        }
444        let reopened = open_log(dir.path());
445        assert2::assert!(reopened.lso() == Offset(0), "case {case}");
446        assert2::assert!(
447            reopened.pending_transaction_start(ProducerId(1000)) == Some(Offset(0)),
448            "case {case}"
449        );
450    }
451
452    /// Kafka keeps a complete transaction in the first unstable offset until
453    /// the high watermark passes its marker (`ProducerStateManager
454    /// .removeUnreplicatedTransactions` drops it only when
455    /// `lastOffset < highWatermark`), and reports
456    /// `min(first unstable offset, high watermark)`.
457    #[test]
458    fn a_complete_transaction_holds_the_lso_until_the_high_watermark_passes_its_marker() {
459        struct Case {
460            name: &'static str,
461            transaction: bool,
462            high_watermarks: &'static [i64],
463            want: Offset,
464        }
465        let cases = [
466            Case {
467                name: "high watermark inside the transaction",
468                transaction: true,
469                high_watermarks: &[12],
470                want: Offset(10),
471            },
472            Case {
473                name: "high watermark at the marker",
474                transaction: true,
475                high_watermarks: &[15],
476                want: Offset(10),
477            },
478            Case {
479                name: "high watermark past the marker",
480                transaction: true,
481                high_watermarks: &[16],
482                want: Offset(16),
483            },
484            Case {
485                name: "high watermark moves past the marker in steps",
486                transaction: true,
487                high_watermarks: &[12, 15, 16],
488                want: Offset(16),
489            },
490            Case {
491                name: "no transaction",
492                transaction: false,
493                high_watermarks: &[16],
494                want: Offset(16),
495            },
496        ];
497        for case in cases {
498            let TempLog { dir: _dir, mut log } = test_log();
499            log.append(&mut sample_batch(10)).unwrap(); // offsets 0 to 9
500            if case.transaction {
501                log.append(&mut transactional_batch(1000, 0, &[
502                    "a", "b", "c", "d", "e",
503                ]))
504                .unwrap(); // offsets 10 to 14
505                log.append(&mut commit_marker(1000, 0)).unwrap(); // offset 15
506            } else {
507                log.append(&mut sample_batch(6)).unwrap(); // offsets 10 to 15
508            }
509            let mut got = Offset(-1);
510            for &high_watermark in case.high_watermarks {
511                got = log.last_stable_offset(Offset(high_watermark));
512            }
513            assert2::assert!(got == case.want, "case {}", case.name);
514        }
515    }
516
517    /// A reopened log replays the markers after its last producer snapshot,
518    /// so a complete transaction still holds the LSO until the high watermark
519    /// passes its marker, as Kafka's log recovery puts it in
520    /// `unreplicatedTxns`. A released transaction never holds it again.
521    #[test]
522    fn a_reopened_log_holds_a_complete_transaction_until_the_high_watermark_passes() {
523        let dir = tempdir().unwrap();
524        {
525            let mut log = open_log(dir.path());
526            log.append(&mut sample_batch(10)).unwrap(); // offsets 0 to 9
527            append_transaction(&mut log, 1000, 0, &["a", "b"]); // offsets 10 and 11
528            log.append(&mut abort_marker(1000, 0)).unwrap(); // offset 12
529        }
530        let mut reopened = open_log(dir.path());
531        assert2::assert!(
532            [12, 13, 11].map(|hw| reopened.last_stable_offset(Offset(hw)))
533                == [Offset(10), Offset(13), Offset(11)]
534        );
535    }
536
537    #[test]
538    fn stale_pending_start_beyond_log_end_is_rejected() {
539        let TempLog { dir: _dir, mut log } = test_log();
540        log.pending.insert(ProducerId(1000), Offset(1));
541        let error = log.refresh_lso().unwrap_err();
542        assert2::assert!(let LogError::Corrupt(_) = error);
543        assert2::assert!(log.lso() == Offset(0));
544    }
545
546    #[test]
547    fn a_nonminimal_pending_start_beyond_log_end_is_rejected() {
548        let TempLog { dir: _dir, mut log } = test_log();
549        log.pending.insert(ProducerId(1000), Offset(0));
550        log.pending.insert(ProducerId(2000), Offset(1));
551        assert2::assert!(let Err(LogError::Corrupt(_)) = log.refresh_lso());
552        assert2::assert!(log.lso() == Offset(0));
553    }
554
555    #[yare::parameterized(
556        hundred = { 100 },
557        thousand = { 1_000 },
558        ten_thousand = { 10_000 },
559    )]
560    fn unchanged_pending_frontier_survives_many_ordinary_appends(producers: i64) {
561        let TempLog { dir: _dir, mut log } = test_log();
562        log.append(&mut sample_batch(i32::try_from(producers).unwrap()))
563            .unwrap();
564        // Populate the producer lookup and its bounds once. A fixed count of
565        // extrema reaches the scalar validator on all 1,000 appends below,
566        // regardless of the producer map's length or allocated capacity.
567        for producer in 0..producers {
568            log.pending.insert(ProducerId(producer), Offset(producer));
569        }
570        for _ in 0..1_000 {
571            log.append(&mut sample_batch(1)).unwrap();
572            assert2::assert!(log.lso() == Offset(0));
573        }
574    }
575
576    #[test]
577    fn ordinary_appends_and_marker_release_match_the_scalar_frontier() {
578        let TempLog { dir: _dir, mut log } = test_log();
579        for producer in 0..100 {
580            append_transaction(&mut log, producer, 0, &["open"]);
581        }
582        for _ in 0..100 {
583            log.append(&mut sample_batch(1)).unwrap();
584            let starts: Vec<_> = log.pending.values().map(|offset| offset.0).collect();
585            assert2::assert!(
586                Some(log.lso().0)
587                    == krabka_verified::first_unstable_offset(&starts, log.log_end_offset().0)
588            );
589        }
590        for producer in 0..100 {
591            log.append(&mut commit_marker(producer, 0)).unwrap();
592            let end = log.log_end_offset();
593            assert2::assert!(log.last_stable_offset(Offset(end.0 - 1)) <= Offset(end.0 - 1));
594            log.release_replicated_transactions(end);
595            let starts: Vec<_> = log.pending.values().map(|offset| offset.0).collect();
596            assert2::assert!(
597                Some(log.lso().0) == krabka_verified::first_unstable_offset(&starts, end.0)
598            );
599        }
600    }
601
602    /// One step of a transactional layout.
603    #[derive(Clone, Copy)]
604    enum TxnStep {
605        /// A transactional batch of this many records from this producer.
606        Data(i64, usize),
607        Abort(i64),
608        Commit(i64),
609        /// Start a new segment.
610        Roll,
611        /// The high watermark reaches the log end, releasing completed
612        /// transactions from the last-stable-offset computation.
613        Replicate,
614    }
615
616    fn txn_layout(steps: &[TxnStep]) -> TempLog {
617        let TempLog { dir, mut log } = test_log();
618        for &step in steps {
619            match step {
620                TxnStep::Data(pid, records) => {
621                    let values = vec!["v"; records];
622                    append_transaction(&mut log, pid, 0, &values);
623                }
624                TxnStep::Abort(pid) => {
625                    log.append(&mut abort_marker(pid, 0)).unwrap();
626                }
627                TxnStep::Commit(pid) => {
628                    log.append(&mut commit_marker(pid, 0)).unwrap();
629                }
630                TxnStep::Roll => {
631                    log.roll().unwrap();
632                }
633                TxnStep::Replicate => {
634                    let end = log.log_end_offset();
635                    log.release_replicated_transactions(end);
636                }
637            }
638        }
639        TempLog { dir, log }
640    }
641
642    /// The search before the early stop: every overlapping entry in every
643    /// index from the first segment that reaches `start`.
644    fn aborted_by_full_scan(log: &Log, start: Offset, end: Offset) -> Vec<AbortedTxn> {
645        let mut aborted = Vec::new();
646        if let Some(first_base) = log
647            .segments
648            .iter()
649            .find(|segment| segment.last_offset() >= start)
650            .map(Segment::base_offset)
651        {
652            for index in log
653                .sealed_txn_indexes
654                .range(first_base..)
655                .map(|(_, index)| index)
656            {
657                aborted.extend(index.aborted_in_range(start, end).copied());
658            }
659        }
660        aborted.extend(log.active_txn_index.aborted_in_range(start, end).copied());
661        aborted
662    }
663
664    /// Starting at the first segment that reaches the fetch and stopping
665    /// where an entry's last stable offset passes the range's end finds
666    /// exactly what a scan of every later index finds, for every range over
667    /// layouts that spread transactions across segments.
668    #[yare::parameterized(
669        one_abort_per_segment = { &[
670            TxnStep::Data(1, 3), TxnStep::Abort(1), TxnStep::Replicate, TxnStep::Roll,
671            TxnStep::Data(2, 2), TxnStep::Abort(2), TxnStep::Replicate, TxnStep::Roll,
672            TxnStep::Data(1, 4), TxnStep::Abort(1), TxnStep::Replicate, TxnStep::Roll,
673            TxnStep::Data(3, 1), TxnStep::Abort(3), TxnStep::Replicate,
674        ] },
675        interleaved_producers = { &[
676            TxnStep::Data(1, 2), TxnStep::Data(2, 2), TxnStep::Abort(2), TxnStep::Roll,
677            TxnStep::Data(3, 2), TxnStep::Data(1, 1), TxnStep::Abort(1), TxnStep::Replicate,
678            TxnStep::Roll, TxnStep::Abort(3), TxnStep::Data(2, 3), TxnStep::Replicate,
679            TxnStep::Roll, TxnStep::Abort(2),
680        ] },
681        a_long_transaction_spans_segments = { &[
682            TxnStep::Data(9, 2), TxnStep::Data(1, 2), TxnStep::Abort(1), TxnStep::Replicate,
683            TxnStep::Roll, TxnStep::Data(2, 2), TxnStep::Abort(2), TxnStep::Replicate,
684            TxnStep::Roll, TxnStep::Data(9, 2), TxnStep::Abort(9), TxnStep::Replicate,
685            TxnStep::Roll, TxnStep::Data(3, 2), TxnStep::Abort(3), TxnStep::Replicate,
686        ] },
687        commits_between_aborts_without_replication = { &[
688            TxnStep::Data(1, 2), TxnStep::Commit(1), TxnStep::Data(2, 2), TxnStep::Abort(2),
689            TxnStep::Roll, TxnStep::Data(1, 1), TxnStep::Abort(1), TxnStep::Roll,
690            TxnStep::Data(2, 2), TxnStep::Commit(2), TxnStep::Data(3, 2), TxnStep::Abort(3),
691        ] },
692        segments_without_aborts = { &[
693            TxnStep::Data(1, 2), TxnStep::Commit(1), TxnStep::Replicate, TxnStep::Roll,
694            TxnStep::Data(2, 2), TxnStep::Abort(2), TxnStep::Replicate, TxnStep::Roll,
695            TxnStep::Data(3, 2), TxnStep::Commit(3), TxnStep::Replicate, TxnStep::Roll,
696            TxnStep::Data(4, 2), TxnStep::Commit(4), TxnStep::Replicate, TxnStep::Roll,
697            TxnStep::Data(5, 1), TxnStep::Abort(5),
698        ] },
699    )]
700    fn aborted_search_stops_without_changing_its_result(steps: &[TxnStep]) {
701        let TempLog { dir: _dir, log } = txn_layout(steps);
702        let end = log.log_end_offset().0;
703        let mut actual = Vec::new();
704        let mut expected = Vec::new();
705        for start in 0..=end + 1 {
706            for upper in 0..=end + 2 {
707                let (start, upper) = (Offset(start), Offset(upper));
708                actual.push(log.aborted_in_range(start, upper));
709                expected.push(aborted_by_full_scan(&log, start, upper));
710            }
711        }
712        assert2::assert!(actual == expected);
713        assert2::assert!(expected.iter().any(|aborted| !aborted.is_empty()));
714    }
715}