solana_core/
banking_trace.rs

1use {
2    agave_banking_stage_ingress_types::{BankingPacketBatch, BankingPacketReceiver},
3    bincode::serialize_into,
4    chrono::{DateTime, Local},
5    crossbeam_channel::{unbounded, Receiver, SendError, Sender, TryRecvError},
6    rolling_file::{RollingCondition, RollingConditionBasic, RollingFileAppender},
7    solana_clock::Slot,
8    solana_hash::Hash,
9    std::{
10        fs::{create_dir_all, remove_dir_all},
11        io::{self, Write},
12        path::PathBuf,
13        sync::{
14            atomic::{AtomicBool, Ordering},
15            Arc,
16        },
17        thread::{self, sleep, JoinHandle},
18        time::{Duration, SystemTime},
19    },
20    thiserror::Error,
21};
22
23pub type BankingPacketSender = TracedSender;
24pub type TracerThreadResult = Result<(), TraceError>;
25pub type TracerThread = Option<JoinHandle<TracerThreadResult>>;
26pub type DirByteLimit = u64;
27
28#[derive(Error, Debug)]
29pub enum TraceError {
30    #[error("IO Error: {0}")]
31    IoError(#[from] std::io::Error),
32
33    #[error("Serialization Error: {0}")]
34    SerializeError(#[from] bincode::Error),
35
36    #[error("Integer Cast Error: {0}")]
37    IntegerCastError(#[from] std::num::TryFromIntError),
38
39    #[error("Trace directory's byte limit is too small (must be larger than {1}): {0}")]
40    TooSmallDirByteLimit(DirByteLimit, DirByteLimit),
41}
42
43pub(crate) const BASENAME: &str = "events";
44const TRACE_FILE_ROTATE_COUNT: u64 = 14; // target 2 weeks retention under normal load
45const TRACE_FILE_WRITE_INTERVAL_MS: u64 = 100;
46const BUF_WRITER_CAPACITY: usize = 10 * 1024 * 1024;
47pub const TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD: u64 = 1024 * 1024 * 1024;
48pub const DISABLED_BAKING_TRACE_DIR: DirByteLimit = 0;
49pub const BANKING_TRACE_DIR_DEFAULT_BYTE_LIMIT: DirByteLimit =
50    TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD * TRACE_FILE_ROTATE_COUNT;
51
52#[derive(Clone, Debug)]
53struct ActiveTracer {
54    trace_sender: Sender<TimedTracedEvent>,
55    exit: Arc<AtomicBool>,
56}
57
58#[derive(Debug)]
59pub struct BankingTracer {
60    active_tracer: Option<ActiveTracer>,
61}
62
63#[cfg_attr(
64    feature = "frozen-abi",
65    derive(AbiExample),
66    frozen_abi(digest = "91baCBT3aY2nXSAuzY3S5dnMhWabVsHowgWqYPLjfyg7")
67)]
68#[derive(Serialize, Deserialize, Debug)]
69pub struct TimedTracedEvent(pub std::time::SystemTime, pub TracedEvent);
70
71#[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))]
72#[derive(Serialize, Deserialize, Debug)]
73pub enum TracedEvent {
74    PacketBatch(ChannelLabel, BankingPacketBatch),
75    BlockAndBankHash(Slot, Hash, Hash),
76}
77
78#[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))]
79#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
80pub enum ChannelLabel {
81    NonVote,
82    TpuVote,
83    GossipVote,
84    Dummy,
85}
86
87struct RollingConditionGrouped {
88    basic: RollingConditionBasic,
89    tried_rollover_after_opened: bool,
90    is_checked: bool,
91}
92
93impl RollingConditionGrouped {
94    fn new(basic: RollingConditionBasic) -> Self {
95        Self {
96            basic,
97            tried_rollover_after_opened: bool::default(),
98            is_checked: bool::default(),
99        }
100    }
101
102    fn reset(&mut self) {
103        self.is_checked = false;
104    }
105}
106
107struct GroupedWriter<'a> {
108    now: DateTime<Local>,
109    underlying: &'a mut RollingFileAppender<RollingConditionGrouped>,
110}
111
112impl<'a> GroupedWriter<'a> {
113    fn new(underlying: &'a mut RollingFileAppender<RollingConditionGrouped>) -> Self {
114        Self {
115            now: Local::now(),
116            underlying,
117        }
118    }
119}
120
121impl RollingCondition for RollingConditionGrouped {
122    fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool {
123        if !self.tried_rollover_after_opened {
124            self.tried_rollover_after_opened = true;
125
126            // rollover normally if empty to reuse it if possible
127            if current_filesize > 0 {
128                // forcibly rollover anew, so that we always avoid to append
129                // to a possibly-damaged tracing file even after unclean
130                // restarts
131                return true;
132            }
133        }
134
135        if !self.is_checked {
136            self.is_checked = true;
137            self.basic.should_rollover(now, current_filesize)
138        } else {
139            false
140        }
141    }
142}
143
144impl Write for GroupedWriter<'_> {
145    fn write(&mut self, buf: &[u8]) -> std::result::Result<usize, io::Error> {
146        self.underlying.write_with_datetime(buf, &self.now)
147    }
148    fn flush(&mut self) -> std::result::Result<(), io::Error> {
149        self.underlying.flush()
150    }
151}
152
153pub fn receiving_loop_with_minimized_sender_overhead<T, E, const SLEEP_MS: u64>(
154    exit: Arc<AtomicBool>,
155    receiver: Receiver<T>,
156    mut on_recv: impl FnMut(T) -> Result<(), E>,
157) -> Result<(), E> {
158    'outer: while !exit.load(Ordering::Relaxed) {
159        'inner: loop {
160            // avoid futex-based blocking here, otherwise a sender would have to
161            // wake me up at a syscall cost...
162            match receiver.try_recv() {
163                Ok(message) => on_recv(message)?,
164                Err(TryRecvError::Empty) => break 'inner,
165                Err(TryRecvError::Disconnected) => {
166                    break 'outer;
167                }
168            };
169            if exit.load(Ordering::Relaxed) {
170                break 'outer;
171            }
172        }
173        sleep(Duration::from_millis(SLEEP_MS));
174    }
175
176    Ok(())
177}
178
179pub struct Channels {
180    pub non_vote_sender: BankingPacketSender,
181    pub non_vote_receiver: BankingPacketReceiver,
182    pub tpu_vote_sender: BankingPacketSender,
183    pub tpu_vote_receiver: BankingPacketReceiver,
184    pub gossip_vote_sender: BankingPacketSender,
185    pub gossip_vote_receiver: BankingPacketReceiver,
186}
187
188#[allow(dead_code)]
189impl Channels {
190    #[cfg(feature = "dev-context-only-utils")]
191    pub fn unified_sender(&self) -> &BankingPacketSender {
192        let unified_sender = &self.non_vote_sender;
193        assert!(unified_sender
194            .sender
195            .same_channel(&self.tpu_vote_sender.sender));
196        assert!(unified_sender
197            .sender
198            .same_channel(&self.gossip_vote_sender.sender));
199        unified_sender
200    }
201
202    pub(crate) fn unified_receiver(&self) -> &BankingPacketReceiver {
203        let unified_receiver = &self.non_vote_receiver;
204        assert!(unified_receiver.same_channel(&self.tpu_vote_receiver));
205        assert!(unified_receiver.same_channel(&self.gossip_vote_receiver));
206        unified_receiver
207    }
208}
209
210impl BankingTracer {
211    pub fn new(
212        maybe_config: Option<(&PathBuf, Arc<AtomicBool>, DirByteLimit)>,
213    ) -> Result<(Arc<Self>, TracerThread), TraceError> {
214        match maybe_config {
215            None => Ok((Self::new_disabled(), None)),
216            Some((path, exit, dir_byte_limit)) => {
217                let rotate_threshold_size = dir_byte_limit / TRACE_FILE_ROTATE_COUNT;
218                if rotate_threshold_size == 0 {
219                    return Err(TraceError::TooSmallDirByteLimit(
220                        dir_byte_limit,
221                        TRACE_FILE_ROTATE_COUNT,
222                    ));
223                }
224
225                let (trace_sender, trace_receiver) = unbounded();
226
227                let file_appender = Self::create_file_appender(path, rotate_threshold_size)?;
228
229                let tracer_thread =
230                    Self::spawn_background_thread(trace_receiver, file_appender, exit.clone())?;
231
232                Ok((
233                    Arc::new(Self {
234                        active_tracer: Some(ActiveTracer { trace_sender, exit }),
235                    }),
236                    Some(tracer_thread),
237                ))
238            }
239        }
240    }
241
242    pub fn new_disabled() -> Arc<Self> {
243        Arc::new(Self {
244            active_tracer: None,
245        })
246    }
247
248    pub fn is_enabled(&self) -> bool {
249        self.active_tracer.is_some()
250    }
251
252    pub fn create_channels(&self, unify_channels: bool) -> Channels {
253        if unify_channels {
254            // Returning the same channel is needed when unified scheduler supports block
255            // production because unified scheduler doesn't distinguish them and treats them as
256            // unified as the single source of incoming transactions. This is to reduce the number
257            // of recv operation per loop and load balance evenly as much as possible there.
258            let (non_vote_sender, non_vote_receiver) = self.create_channel_non_vote();
259            // Tap into some private helper fns so that banking trace labelling works as before.
260            let (tpu_vote_sender, tpu_vote_receiver) =
261                self.create_unified_channel_tpu_vote(&non_vote_sender, &non_vote_receiver);
262            let (gossip_vote_sender, gossip_vote_receiver) =
263                self.create_unified_channel_gossip_vote(&non_vote_sender, &non_vote_receiver);
264
265            Channels {
266                non_vote_sender,
267                non_vote_receiver,
268                tpu_vote_sender,
269                tpu_vote_receiver,
270                gossip_vote_sender,
271                gossip_vote_receiver,
272            }
273        } else {
274            let (non_vote_sender, non_vote_receiver) = self.create_channel_non_vote();
275            let (tpu_vote_sender, tpu_vote_receiver) = self.create_channel_tpu_vote();
276            let (gossip_vote_sender, gossip_vote_receiver) = self.create_channel_gossip_vote();
277
278            Channels {
279                non_vote_sender,
280                non_vote_receiver,
281                tpu_vote_sender,
282                tpu_vote_receiver,
283                gossip_vote_sender,
284                gossip_vote_receiver,
285            }
286        }
287    }
288
289    fn create_channel(&self, label: ChannelLabel) -> (BankingPacketSender, BankingPacketReceiver) {
290        Self::channel(label, self.active_tracer.as_ref().cloned())
291    }
292
293    pub fn create_channel_non_vote(&self) -> (BankingPacketSender, BankingPacketReceiver) {
294        self.create_channel(ChannelLabel::NonVote)
295    }
296
297    fn create_channel_tpu_vote(&self) -> (BankingPacketSender, BankingPacketReceiver) {
298        self.create_channel(ChannelLabel::TpuVote)
299    }
300
301    fn create_channel_gossip_vote(&self) -> (BankingPacketSender, BankingPacketReceiver) {
302        self.create_channel(ChannelLabel::GossipVote)
303    }
304
305    fn create_unified_channel_tpu_vote(
306        &self,
307        sender: &TracedSender,
308        receiver: &BankingPacketReceiver,
309    ) -> (BankingPacketSender, BankingPacketReceiver) {
310        Self::channel_inner(
311            ChannelLabel::TpuVote,
312            self.active_tracer.as_ref().cloned(),
313            sender.sender.clone(),
314            receiver.clone(),
315        )
316    }
317
318    fn create_unified_channel_gossip_vote(
319        &self,
320        sender: &TracedSender,
321        receiver: &BankingPacketReceiver,
322    ) -> (BankingPacketSender, BankingPacketReceiver) {
323        Self::channel_inner(
324            ChannelLabel::GossipVote,
325            self.active_tracer.as_ref().cloned(),
326            sender.sender.clone(),
327            receiver.clone(),
328        )
329    }
330
331    pub fn hash_event(&self, slot: Slot, blockhash: &Hash, bank_hash: &Hash) {
332        self.trace_event(|| {
333            TimedTracedEvent(
334                SystemTime::now(),
335                TracedEvent::BlockAndBankHash(slot, *blockhash, *bank_hash),
336            )
337        })
338    }
339
340    fn trace_event(&self, on_trace: impl Fn() -> TimedTracedEvent) {
341        if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer {
342            if !exit.load(Ordering::Relaxed) {
343                trace_sender
344                    .send(on_trace())
345                    .expect("active tracer thread unless exited");
346            }
347        }
348    }
349
350    pub fn channel_for_test() -> (TracedSender, Receiver<BankingPacketBatch>) {
351        Self::channel(ChannelLabel::Dummy, None)
352    }
353
354    fn channel(
355        label: ChannelLabel,
356        active_tracer: Option<ActiveTracer>,
357    ) -> (TracedSender, Receiver<BankingPacketBatch>) {
358        let (sender, receiver) = unbounded();
359        Self::channel_inner(label, active_tracer, sender, receiver)
360    }
361
362    fn channel_inner(
363        label: ChannelLabel,
364        active_tracer: Option<ActiveTracer>,
365        sender: Sender<BankingPacketBatch>,
366        receiver: BankingPacketReceiver,
367    ) -> (TracedSender, Receiver<BankingPacketBatch>) {
368        (TracedSender::new(label, sender, active_tracer), receiver)
369    }
370
371    pub fn ensure_cleanup_path(path: &PathBuf) -> Result<(), io::Error> {
372        remove_dir_all(path).or_else(|err| {
373            if err.kind() == io::ErrorKind::NotFound {
374                Ok(())
375            } else {
376                Err(err)
377            }
378        })
379    }
380
381    fn create_file_appender(
382        path: &PathBuf,
383        rotate_threshold_size: u64,
384    ) -> Result<RollingFileAppender<RollingConditionGrouped>, TraceError> {
385        create_dir_all(path)?;
386        let grouped = RollingConditionGrouped::new(
387            RollingConditionBasic::new()
388                .daily()
389                .max_size(rotate_threshold_size),
390        );
391        let appender = RollingFileAppender::new_with_buffer_capacity(
392            path.join(BASENAME),
393            grouped,
394            (TRACE_FILE_ROTATE_COUNT - 1).try_into()?,
395            BUF_WRITER_CAPACITY,
396        )?;
397        Ok(appender)
398    }
399
400    fn spawn_background_thread(
401        trace_receiver: Receiver<TimedTracedEvent>,
402        mut file_appender: RollingFileAppender<RollingConditionGrouped>,
403        exit: Arc<AtomicBool>,
404    ) -> Result<JoinHandle<TracerThreadResult>, TraceError> {
405        let thread = thread::Builder::new().name("solBanknTracer".into()).spawn(
406            move || -> TracerThreadResult {
407                receiving_loop_with_minimized_sender_overhead::<_, _, TRACE_FILE_WRITE_INTERVAL_MS>(
408                    exit,
409                    trace_receiver,
410                    |event| -> Result<(), TraceError> {
411                        file_appender.condition_mut().reset();
412                        serialize_into(&mut GroupedWriter::new(&mut file_appender), &event)?;
413                        Ok(())
414                    },
415                )?;
416                file_appender.flush()?;
417                Ok(())
418            },
419        )?;
420
421        Ok(thread)
422    }
423}
424
425pub struct TracedSender {
426    label: ChannelLabel,
427    sender: Sender<BankingPacketBatch>,
428    active_tracer: Option<ActiveTracer>,
429}
430
431impl TracedSender {
432    fn new(
433        label: ChannelLabel,
434        sender: Sender<BankingPacketBatch>,
435        active_tracer: Option<ActiveTracer>,
436    ) -> Self {
437        Self {
438            label,
439            sender,
440            active_tracer,
441        }
442    }
443
444    pub fn send(&self, batch: BankingPacketBatch) -> Result<(), SendError<BankingPacketBatch>> {
445        if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer {
446            if !exit.load(Ordering::Relaxed) {
447                trace_sender
448                    .send(TimedTracedEvent(
449                        SystemTime::now(),
450                        TracedEvent::PacketBatch(self.label, BankingPacketBatch::clone(&batch)),
451                    ))
452                    .map_err(|err| {
453                        error!("unexpected error when tracing a banking event...: {err:?}");
454                        SendError(BankingPacketBatch::clone(&batch))
455                    })?;
456            }
457        }
458        self.sender.send(batch)
459    }
460
461    pub fn len(&self) -> usize {
462        self.sender.len()
463    }
464
465    pub fn is_empty(&self) -> bool {
466        self.len() == 0
467    }
468}
469
470#[cfg(any(test, feature = "dev-context-only-utils"))]
471pub mod for_test {
472    use {
473        super::*,
474        solana_perf::{packet::to_packet_batches, test_tx::test_tx},
475        tempfile::TempDir,
476    };
477
478    pub fn sample_packet_batch() -> BankingPacketBatch {
479        BankingPacketBatch::new(to_packet_batches(&vec![test_tx(); 4], 10))
480    }
481
482    pub fn drop_and_clean_temp_dir_unless_suppressed(temp_dir: TempDir) {
483        std::env::var("BANKING_TRACE_LEAVE_FILES").is_ok().then(|| {
484            warn!("prevented to remove {:?}", temp_dir.path());
485            drop(temp_dir.keep());
486        });
487    }
488
489    pub fn terminate_tracer(
490        tracer: Arc<BankingTracer>,
491        tracer_thread: TracerThread,
492        main_thread: JoinHandle<TracerThreadResult>,
493        sender: TracedSender,
494        exit: Option<Arc<AtomicBool>>,
495    ) {
496        if let Some(exit) = exit {
497            exit.store(true, Ordering::Relaxed);
498        }
499        drop((sender, tracer));
500        main_thread.join().unwrap().unwrap();
501        if let Some(tracer_thread) = tracer_thread {
502            tracer_thread.join().unwrap().unwrap();
503        }
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use {
510        super::*,
511        bincode::ErrorKind::Io as BincodeIoError,
512        std::{
513            fs::File,
514            io::{BufReader, ErrorKind::UnexpectedEof},
515            str::FromStr,
516        },
517        tempfile::TempDir,
518    };
519
520    #[test]
521    fn test_new_disabled() {
522        let exit = Arc::<AtomicBool>::default();
523
524        let tracer = BankingTracer::new_disabled();
525        let (non_vote_sender, non_vote_receiver) = tracer.create_channel_non_vote();
526
527        let dummy_main_thread = thread::spawn(move || {
528            receiving_loop_with_minimized_sender_overhead::<_, TraceError, 0>(
529                exit,
530                non_vote_receiver,
531                |_packet_batch| Ok(()),
532            )
533        });
534
535        non_vote_sender
536            .send(BankingPacketBatch::new(vec![]))
537            .unwrap();
538        for_test::terminate_tracer(tracer, None, dummy_main_thread, non_vote_sender, None);
539    }
540
541    #[test]
542    fn test_send_after_exited() {
543        let temp_dir = TempDir::new().unwrap();
544        let path = temp_dir.path().join("banking-trace");
545        let exit = Arc::<AtomicBool>::default();
546        let (tracer, tracer_thread) =
547            BankingTracer::new(Some((&path, exit.clone(), DirByteLimit::MAX))).unwrap();
548        let (non_vote_sender, non_vote_receiver) = tracer.create_channel_non_vote();
549
550        let exit_for_dummy_thread = Arc::<AtomicBool>::default();
551        let exit_for_dummy_thread2 = exit_for_dummy_thread.clone();
552        let dummy_main_thread = thread::spawn(move || {
553            receiving_loop_with_minimized_sender_overhead::<_, TraceError, 0>(
554                exit_for_dummy_thread,
555                non_vote_receiver,
556                |_packet_batch| Ok(()),
557            )
558        });
559
560        // kill and join the tracer thread
561        exit.store(true, Ordering::Relaxed);
562        tracer_thread.unwrap().join().unwrap().unwrap();
563
564        // .hash_event() must succeed even after exit is already set to true
565        let blockhash = Hash::from_str("B1ockhash1111111111111111111111111111111111").unwrap();
566        let bank_hash = Hash::from_str("BankHash11111111111111111111111111111111111").unwrap();
567        tracer.hash_event(4, &blockhash, &bank_hash);
568
569        drop(tracer);
570
571        // .send() must succeed even after exit is already set to true and further tracer is
572        // already dropped
573        non_vote_sender
574            .send(for_test::sample_packet_batch())
575            .unwrap();
576
577        // finally terminate and join the main thread
578        exit_for_dummy_thread2.store(true, Ordering::Relaxed);
579        dummy_main_thread.join().unwrap().unwrap();
580    }
581
582    #[test]
583    fn test_record_and_restore() {
584        let temp_dir = TempDir::new().unwrap();
585        let path = temp_dir.path().join("banking-trace");
586        let exit = Arc::<AtomicBool>::default();
587        let (tracer, tracer_thread) =
588            BankingTracer::new(Some((&path, exit.clone(), DirByteLimit::MAX))).unwrap();
589        let (non_vote_sender, non_vote_receiver) = tracer.create_channel_non_vote();
590
591        let dummy_main_thread = thread::spawn(move || {
592            receiving_loop_with_minimized_sender_overhead::<_, TraceError, 0>(
593                exit,
594                non_vote_receiver,
595                |_packet_batch| Ok(()),
596            )
597        });
598
599        non_vote_sender
600            .send(for_test::sample_packet_batch())
601            .unwrap();
602        let blockhash = Hash::from_str("B1ockhash1111111111111111111111111111111111").unwrap();
603        let bank_hash = Hash::from_str("BankHash11111111111111111111111111111111111").unwrap();
604        tracer.hash_event(4, &blockhash, &bank_hash);
605
606        for_test::terminate_tracer(
607            tracer,
608            tracer_thread,
609            dummy_main_thread,
610            non_vote_sender,
611            None,
612        );
613
614        let mut stream = BufReader::new(File::open(path.join(BASENAME)).unwrap());
615        let results = (0..=3)
616            .map(|_| bincode::deserialize_from::<_, TimedTracedEvent>(&mut stream))
617            .collect::<Vec<_>>();
618
619        let mut i = 0;
620        assert_matches!(
621            results[i],
622            Ok(TimedTracedEvent(
623                _,
624                TracedEvent::PacketBatch(ChannelLabel::NonVote, _)
625            ))
626        );
627        i += 1;
628        assert_matches!(
629            results[i],
630            Ok(TimedTracedEvent(
631                _,
632                TracedEvent::BlockAndBankHash(4, actual_blockhash, actual_bank_hash)
633            )) if actual_blockhash == blockhash && actual_bank_hash == bank_hash
634        );
635        i += 1;
636        assert_matches!(
637            results[i],
638            Err(ref err) if matches!(
639                **err,
640                BincodeIoError(ref error) if error.kind() == UnexpectedEof
641            )
642        );
643
644        for_test::drop_and_clean_temp_dir_unless_suppressed(temp_dir);
645    }
646
647    #[test]
648    fn test_spill_over_at_rotation() {
649        let temp_dir = TempDir::new().unwrap();
650        let path = temp_dir.path().join("banking-trace");
651        const REALLY_SMALL_ROTATION_THRESHOLD: u64 = 1;
652
653        let mut file_appender =
654            BankingTracer::create_file_appender(&path, REALLY_SMALL_ROTATION_THRESHOLD).unwrap();
655        file_appender.write_all(b"foo").unwrap();
656        file_appender.condition_mut().reset();
657        file_appender.write_all(b"bar").unwrap();
658        file_appender.condition_mut().reset();
659        file_appender.flush().unwrap();
660
661        assert_eq!(
662            [
663                std::fs::read_to_string(path.join("events")).ok(),
664                std::fs::read_to_string(path.join("events.1")).ok(),
665                std::fs::read_to_string(path.join("events.2")).ok(),
666            ],
667            [Some("bar".into()), Some("foo".into()), None]
668        );
669
670        for_test::drop_and_clean_temp_dir_unless_suppressed(temp_dir);
671    }
672
673    #[test]
674    fn test_reopen_with_blank_file() {
675        let temp_dir = TempDir::new().unwrap();
676
677        let path = temp_dir.path().join("banking-trace");
678
679        let mut file_appender =
680            BankingTracer::create_file_appender(&path, TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD)
681                .unwrap();
682        // assume this is unclean write
683        file_appender.write_all(b"f").unwrap();
684        file_appender.flush().unwrap();
685
686        // reopen while shadow-dropping the old tracer
687        let mut file_appender =
688            BankingTracer::create_file_appender(&path, TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD)
689                .unwrap();
690        // new file won't be created as appender is lazy
691        assert_eq!(
692            [
693                std::fs::read_to_string(path.join("events")).ok(),
694                std::fs::read_to_string(path.join("events.1")).ok(),
695                std::fs::read_to_string(path.join("events.2")).ok(),
696            ],
697            [Some("f".into()), None, None]
698        );
699
700        // initial write actually creates the new blank file
701        file_appender.write_all(b"bar").unwrap();
702        assert_eq!(
703            [
704                std::fs::read_to_string(path.join("events")).ok(),
705                std::fs::read_to_string(path.join("events.1")).ok(),
706                std::fs::read_to_string(path.join("events.2")).ok(),
707            ],
708            [Some("".into()), Some("f".into()), None]
709        );
710
711        // flush actually write the actual data
712        file_appender.flush().unwrap();
713        assert_eq!(
714            [
715                std::fs::read_to_string(path.join("events")).ok(),
716                std::fs::read_to_string(path.join("events.1")).ok(),
717                std::fs::read_to_string(path.join("events.2")).ok(),
718            ],
719            [Some("bar".into()), Some("f".into()), None]
720        );
721
722        for_test::drop_and_clean_temp_dir_unless_suppressed(temp_dir);
723    }
724}