1use {
2 agave_banking_stage_ingress_types::{BankingPacketBatch, BankingPacketReceiver},
3 bincode::serialize_into,
4 chrono::{DateTime, Local},
5 crossbeam_channel::{Receiver, SendError, TryRecvError, TrySendError, bounded},
6 rolling_file::{RollingCondition, RollingConditionBasic, RollingFileAppender},
7 serde::{Deserialize, Serialize},
8 solana_clock::Slot,
9 solana_hash::Hash,
10 solana_streamer::{evicting_sender::EvictingSender, streamer::ChannelSend},
11 std::{
12 fs::{create_dir_all, remove_dir_all},
13 io::{self, Write},
14 path::PathBuf,
15 sync::{
16 Arc,
17 atomic::{AtomicBool, Ordering},
18 },
19 thread::{self, JoinHandle, sleep},
20 time::{Duration, SystemTime},
21 },
22 thiserror::Error,
23};
24
25const VOTE_CHANNEL_CAPACITY: usize = 1024 * 8;
28
29const NON_VOTE_CHANNEL_CAPACITY: usize = 1024 * 16;
32
33pub type BankingPacketSender = TracedSender;
34pub type TracerThreadResult = Result<(), TraceError>;
35pub type TracerThread = Option<JoinHandle<TracerThreadResult>>;
36pub type DirByteLimit = u64;
37
38#[derive(Error, Debug)]
39pub enum TraceError {
40 #[error("IO Error: {0}")]
41 IoError(#[from] std::io::Error),
42
43 #[error("Serialization Error: {0}")]
44 SerializeError(#[from] bincode::Error),
45
46 #[error("Integer Cast Error: {0}")]
47 IntegerCastError(#[from] std::num::TryFromIntError),
48
49 #[error("Trace directory's byte limit is too small (must be larger than {1}): {0}")]
50 TooSmallDirByteLimit(DirByteLimit, DirByteLimit),
51}
52
53pub(crate) const BASENAME: &str = "events";
54const TRACE_FILE_ROTATE_COUNT: u64 = 14; const TRACE_FILE_WRITE_INTERVAL_MS: u64 = 100;
56const BUF_WRITER_CAPACITY: usize = 10 * 1024 * 1024;
57pub const TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD: u64 = 1024 * 1024 * 1024;
58pub const DISABLED_BAKING_TRACE_DIR: DirByteLimit = 0;
59pub const BANKING_TRACE_DIR_DEFAULT_BYTE_LIMIT: DirByteLimit =
60 TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD * TRACE_FILE_ROTATE_COUNT;
61
62#[derive(Clone)]
63struct ActiveTracer {
64 trace_sender: EvictingSender<TimedTracedEvent>,
65 exit: Arc<AtomicBool>,
66}
67
68pub struct BankingTracer {
69 active_tracer: Option<ActiveTracer>,
70}
71
72#[cfg_attr(
73 feature = "frozen-abi",
74 derive(AbiExample),
75 frozen_abi(digest = "DY2zjwewCSNansb5xwtoxkCcNuXbVmWZe3U9nNH2kzNz")
76)]
77#[derive(Serialize, Deserialize, Debug)]
78pub struct TimedTracedEvent(pub std::time::SystemTime, pub TracedEvent);
79
80#[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))]
81#[derive(Serialize, Deserialize, Debug)]
82pub enum TracedEvent {
83 PacketBatch(ChannelLabel, BankingPacketBatch),
84 BlockAndBankHash(Slot, Hash, Hash),
85}
86
87#[cfg_attr(
88 feature = "frozen-abi",
89 derive(AbiExample, AbiEnumVisitor, StableAbi, StableAbiSample)
90)]
91#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
92pub enum ChannelLabel {
93 NonVote,
94 TpuVote,
95 GossipVote,
96 Dummy,
97}
98
99struct RollingConditionGrouped {
100 basic: RollingConditionBasic,
101 tried_rollover_after_opened: bool,
102 is_checked: bool,
103}
104
105impl RollingConditionGrouped {
106 fn new(basic: RollingConditionBasic) -> Self {
107 Self {
108 basic,
109 tried_rollover_after_opened: bool::default(),
110 is_checked: bool::default(),
111 }
112 }
113
114 fn reset(&mut self) {
115 self.is_checked = false;
116 }
117}
118
119struct GroupedWriter<'a> {
120 now: DateTime<Local>,
121 underlying: &'a mut RollingFileAppender<RollingConditionGrouped>,
122}
123
124impl<'a> GroupedWriter<'a> {
125 fn new(underlying: &'a mut RollingFileAppender<RollingConditionGrouped>) -> Self {
126 Self {
127 now: Local::now(),
128 underlying,
129 }
130 }
131}
132
133impl RollingCondition for RollingConditionGrouped {
134 fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool {
135 if !self.tried_rollover_after_opened {
136 self.tried_rollover_after_opened = true;
137
138 if current_filesize > 0 {
140 return true;
144 }
145 }
146
147 if !self.is_checked {
148 self.is_checked = true;
149 self.basic.should_rollover(now, current_filesize)
150 } else {
151 false
152 }
153 }
154}
155
156impl Write for GroupedWriter<'_> {
157 fn write(&mut self, buf: &[u8]) -> std::result::Result<usize, io::Error> {
158 self.underlying.write_with_datetime(buf, &self.now)
159 }
160 fn flush(&mut self) -> std::result::Result<(), io::Error> {
161 self.underlying.flush()
162 }
163}
164
165pub fn receiving_loop_with_minimized_sender_overhead<T, E, const SLEEP_MS: u64>(
166 exit: Arc<AtomicBool>,
167 receiver: Receiver<T>,
168 mut on_recv: impl FnMut(T) -> Result<(), E>,
169) -> Result<(), E> {
170 'outer: while !exit.load(Ordering::Relaxed) {
171 'inner: loop {
172 match receiver.try_recv() {
175 Ok(message) => on_recv(message)?,
176 Err(TryRecvError::Empty) => break 'inner,
177 Err(TryRecvError::Disconnected) => {
178 break 'outer;
179 }
180 };
181 if exit.load(Ordering::Relaxed) {
182 break 'outer;
183 }
184 }
185 sleep(Duration::from_millis(SLEEP_MS));
186 }
187
188 Ok(())
189}
190
191pub struct Channels {
194 pub non_vote_sender: BankingPacketSender,
195 pub non_vote_receiver: BankingPacketReceiver,
196 pub tpu_vote_sender: BankingPacketSender,
197 pub tpu_vote_receiver: BankingPacketReceiver,
198 pub gossip_vote_sender: BankingPacketSender,
199 pub gossip_vote_receiver: BankingPacketReceiver,
200}
201
202impl BankingTracer {
203 pub fn new(
204 maybe_config: Option<(&PathBuf, Arc<AtomicBool>, DirByteLimit)>,
205 ) -> Result<(Arc<Self>, TracerThread), TraceError> {
206 match maybe_config {
207 None => Ok((Self::new_disabled(), None)),
208 Some((path, exit, dir_byte_limit)) => {
209 let rotate_threshold_size = dir_byte_limit / TRACE_FILE_ROTATE_COUNT;
210 if rotate_threshold_size == 0 {
211 return Err(TraceError::TooSmallDirByteLimit(
212 dir_byte_limit,
213 TRACE_FILE_ROTATE_COUNT,
214 ));
215 }
216
217 const TRACING_CHANNEL_CAPACITY: usize = 50_000;
218 let (trace_sender, trace_receiver) =
219 EvictingSender::new_bounded(TRACING_CHANNEL_CAPACITY);
220
221 let file_appender = Self::create_file_appender(path, rotate_threshold_size)?;
222
223 let tracer_thread =
224 Self::spawn_background_thread(trace_receiver, file_appender, exit.clone())?;
225
226 Ok((
227 Arc::new(Self {
228 active_tracer: Some(ActiveTracer { trace_sender, exit }),
229 }),
230 Some(tracer_thread),
231 ))
232 }
233 }
234 }
235
236 pub fn new_disabled() -> Arc<Self> {
237 Arc::new(Self {
238 active_tracer: None,
239 })
240 }
241
242 pub fn is_enabled(&self) -> bool {
243 self.active_tracer.is_some()
244 }
245
246 pub fn hash_event(&self, slot: Slot, blockhash: &Hash, bank_hash: &Hash) {
247 self.trace_event(|| {
248 TimedTracedEvent(
249 SystemTime::now(),
250 TracedEvent::BlockAndBankHash(slot, *blockhash, *bank_hash),
251 )
252 })
253 }
254
255 fn trace_event(&self, on_trace: impl Fn() -> TimedTracedEvent) {
256 if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer
257 && !exit.load(Ordering::Relaxed)
258 {
259 let _ = trace_sender.try_send(on_trace());
261 }
262 }
263
264 pub fn create_channels(&self) -> Channels {
265 let (non_vote_sender, non_vote_receiver) = self.create_channel_non_vote();
266
267 let (tpu_vote_sender, tpu_vote_receiver) = Self::channel(
268 ChannelLabel::TpuVote,
269 VOTE_CHANNEL_CAPACITY,
270 self.active_tracer.as_ref().cloned(),
271 );
272 let (gossip_vote_sender, gossip_vote_receiver) = Self::channel(
273 ChannelLabel::GossipVote,
274 VOTE_CHANNEL_CAPACITY,
275 self.active_tracer.as_ref().cloned(),
276 );
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 pub fn create_channel_non_vote(&self) -> (BankingPacketSender, BankingPacketReceiver) {
289 Self::channel(
290 ChannelLabel::NonVote,
291 NON_VOTE_CHANNEL_CAPACITY,
292 self.active_tracer.as_ref().cloned(),
293 )
294 }
295
296 pub fn channel_for_test() -> (TracedSender, Receiver<BankingPacketBatch>) {
297 Self::channel(ChannelLabel::Dummy, VOTE_CHANNEL_CAPACITY, None)
298 }
299
300 fn channel(
301 label: ChannelLabel,
302 capacity: usize,
303 active_tracer: Option<ActiveTracer>,
304 ) -> (TracedSender, Receiver<BankingPacketBatch>) {
305 let (sender, receiver) = bounded(capacity);
306 let evicting = EvictingSender::new(sender, receiver.clone());
307
308 (TracedSender::new(label, evicting, active_tracer), receiver)
309 }
310
311 pub fn ensure_cleanup_path(path: &PathBuf) -> Result<(), io::Error> {
312 remove_dir_all(path).or_else(|err| {
313 if err.kind() == io::ErrorKind::NotFound {
314 Ok(())
315 } else {
316 Err(err)
317 }
318 })
319 }
320
321 fn create_file_appender(
322 path: &PathBuf,
323 rotate_threshold_size: u64,
324 ) -> Result<RollingFileAppender<RollingConditionGrouped>, TraceError> {
325 create_dir_all(path)?;
326 let grouped = RollingConditionGrouped::new(
327 RollingConditionBasic::new()
328 .daily()
329 .max_size(rotate_threshold_size),
330 );
331 let appender = RollingFileAppender::new_with_buffer_capacity(
332 path.join(BASENAME),
333 grouped,
334 (TRACE_FILE_ROTATE_COUNT - 1).try_into()?,
335 BUF_WRITER_CAPACITY,
336 )?;
337 Ok(appender)
338 }
339
340 fn spawn_background_thread(
341 trace_receiver: Receiver<TimedTracedEvent>,
342 mut file_appender: RollingFileAppender<RollingConditionGrouped>,
343 exit: Arc<AtomicBool>,
344 ) -> Result<JoinHandle<TracerThreadResult>, TraceError> {
345 let thread = thread::Builder::new().name("solBanknTracer".into()).spawn(
346 move || -> TracerThreadResult {
347 receiving_loop_with_minimized_sender_overhead::<_, _, TRACE_FILE_WRITE_INTERVAL_MS>(
348 exit,
349 trace_receiver,
350 |event| -> Result<(), TraceError> {
351 file_appender.condition_mut().reset();
352 serialize_into(&mut GroupedWriter::new(&mut file_appender), &event)?;
353 Ok(())
354 },
355 )?;
356 file_appender.flush()?;
357 Ok(())
358 },
359 )?;
360
361 Ok(thread)
362 }
363}
364
365#[derive(Clone)]
380pub struct TracedSender {
381 label: ChannelLabel,
382 sender: EvictingSender<BankingPacketBatch>,
383 active_tracer: Option<ActiveTracer>,
384}
385
386impl TracedSender {
387 fn new(
388 label: ChannelLabel,
389 sender: EvictingSender<BankingPacketBatch>,
390 active_tracer: Option<ActiveTracer>,
391 ) -> Self {
392 Self {
393 label,
394 sender,
395 active_tracer,
396 }
397 }
398
399 pub fn send(&self, batch: BankingPacketBatch) -> Result<usize, SendError<BankingPacketBatch>> {
403 if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer
404 && !exit.load(Ordering::Relaxed)
405 {
406 let _ = trace_sender.try_send(TimedTracedEvent(
408 SystemTime::now(),
409 TracedEvent::PacketBatch(self.label, BankingPacketBatch::clone(&batch)),
410 ));
411 }
412 match self.sender.try_send(batch) {
413 Ok(()) => Ok(0),
414 Err(TrySendError::Full(b)) => Ok(b.iter().map(|pb| pb.len()).sum()),
415 Err(TrySendError::Disconnected(b)) => Err(SendError(b)),
416 }
417 }
418
419 pub fn len(&self) -> usize {
420 self.sender.len()
421 }
422
423 pub fn is_empty(&self) -> bool {
424 self.len() == 0
425 }
426}
427
428#[cfg(any(test, feature = "dev-context-only-utils"))]
429pub mod for_test {
430 use {
431 super::*,
432 solana_perf::{packet::to_packet_batches, test_tx::test_tx},
433 tempfile::TempDir,
434 };
435
436 pub fn sample_packet_batch() -> BankingPacketBatch {
437 BankingPacketBatch::new(to_packet_batches(&vec![test_tx(); 4], 10))
438 }
439
440 pub fn drop_and_clean_temp_dir_unless_suppressed(temp_dir: TempDir) {
441 std::env::var("BANKING_TRACE_LEAVE_FILES").is_ok().then(|| {
442 warn!("prevented to remove {:?}", temp_dir.path());
443 drop(temp_dir.keep());
444 });
445 }
446
447 pub fn terminate_tracer(
448 tracer: Arc<BankingTracer>,
449 tracer_thread: TracerThread,
450 main_thread: JoinHandle<TracerThreadResult>,
451 sender: TracedSender,
452 exit: Option<Arc<AtomicBool>>,
453 ) {
454 if let Some(exit) = exit {
455 exit.store(true, Ordering::Relaxed);
456 }
457 drop((sender, tracer));
458 main_thread.join().unwrap().unwrap();
459 if let Some(tracer_thread) = tracer_thread {
460 tracer_thread.join().unwrap().unwrap();
461 }
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use {
468 super::*,
469 bincode::ErrorKind::Io as BincodeIoError,
470 std::{
471 fs::File,
472 io::{BufReader, ErrorKind::UnexpectedEof},
473 str::FromStr,
474 },
475 tempfile::TempDir,
476 };
477
478 #[test]
479 fn test_new_disabled() {
480 let exit = Arc::<AtomicBool>::default();
481
482 let tracer = BankingTracer::new_disabled();
483 let (non_vote_sender, non_vote_receiver) = tracer.create_channel_non_vote();
484
485 let dummy_main_thread = thread::spawn(move || {
486 receiving_loop_with_minimized_sender_overhead::<_, TraceError, 0>(
487 exit,
488 non_vote_receiver,
489 |_packet_batch| Ok(()),
490 )
491 });
492
493 non_vote_sender
494 .send(BankingPacketBatch::new(vec![]))
495 .unwrap();
496 for_test::terminate_tracer(tracer, None, dummy_main_thread, non_vote_sender, None);
497 }
498
499 #[test]
500 fn test_send_after_exited() {
501 let temp_dir = TempDir::new().unwrap();
502 let path = temp_dir.path().join("banking-trace");
503 let exit = Arc::<AtomicBool>::default();
504 let (tracer, tracer_thread) =
505 BankingTracer::new(Some((&path, exit.clone(), DirByteLimit::MAX))).unwrap();
506 let (non_vote_sender, non_vote_receiver) = tracer.create_channel_non_vote();
507
508 let exit_for_dummy_thread = Arc::<AtomicBool>::default();
509 let exit_for_dummy_thread2 = exit_for_dummy_thread.clone();
510 let dummy_main_thread = thread::spawn(move || {
511 receiving_loop_with_minimized_sender_overhead::<_, TraceError, 0>(
512 exit_for_dummy_thread,
513 non_vote_receiver,
514 |_packet_batch| Ok(()),
515 )
516 });
517
518 exit.store(true, Ordering::Relaxed);
520 tracer_thread.unwrap().join().unwrap().unwrap();
521
522 let blockhash = Hash::from_str("B1ockhash1111111111111111111111111111111111").unwrap();
524 let bank_hash = Hash::from_str("BankHash11111111111111111111111111111111111").unwrap();
525 tracer.hash_event(4, &blockhash, &bank_hash);
526
527 drop(tracer);
528
529 non_vote_sender
532 .send(for_test::sample_packet_batch())
533 .unwrap();
534
535 exit_for_dummy_thread2.store(true, Ordering::Relaxed);
537 dummy_main_thread.join().unwrap().unwrap();
538 }
539
540 #[test]
541 fn test_record_and_restore() {
542 let temp_dir = TempDir::new().unwrap();
543 let path = temp_dir.path().join("banking-trace");
544 let exit = Arc::<AtomicBool>::default();
545 let (tracer, tracer_thread) =
546 BankingTracer::new(Some((&path, exit.clone(), DirByteLimit::MAX))).unwrap();
547 let (non_vote_sender, non_vote_receiver) = tracer.create_channel_non_vote();
548
549 let dummy_main_thread = thread::spawn(move || {
550 receiving_loop_with_minimized_sender_overhead::<_, TraceError, 0>(
551 exit,
552 non_vote_receiver,
553 |_packet_batch| Ok(()),
554 )
555 });
556
557 non_vote_sender
558 .send(for_test::sample_packet_batch())
559 .unwrap();
560 let blockhash = Hash::from_str("B1ockhash1111111111111111111111111111111111").unwrap();
561 let bank_hash = Hash::from_str("BankHash11111111111111111111111111111111111").unwrap();
562 tracer.hash_event(4, &blockhash, &bank_hash);
563
564 for_test::terminate_tracer(
565 tracer,
566 tracer_thread,
567 dummy_main_thread,
568 non_vote_sender,
569 None,
570 );
571
572 let mut stream = BufReader::new(File::open(path.join(BASENAME)).unwrap());
573 let results = (0..=3)
574 .map(|_| bincode::deserialize_from::<_, TimedTracedEvent>(&mut stream))
575 .collect::<Vec<_>>();
576
577 let mut i = 0;
578 assert_matches!(
579 results[i],
580 Ok(TimedTracedEvent(
581 _,
582 TracedEvent::PacketBatch(ChannelLabel::NonVote, _)
583 ))
584 );
585 i += 1;
586 assert_matches!(
587 results[i],
588 Ok(TimedTracedEvent(
589 _,
590 TracedEvent::BlockAndBankHash(4, actual_blockhash, actual_bank_hash)
591 )) if actual_blockhash == blockhash && actual_bank_hash == bank_hash
592 );
593 i += 1;
594 assert_matches!(
595 results[i],
596 Err(ref err) if matches!(
597 **err,
598 BincodeIoError(ref error) if error.kind() == UnexpectedEof
599 )
600 );
601
602 for_test::drop_and_clean_temp_dir_unless_suppressed(temp_dir);
603 }
604
605 #[test]
606 fn test_spill_over_at_rotation() {
607 let temp_dir = TempDir::new().unwrap();
608 let path = temp_dir.path().join("banking-trace");
609 const REALLY_SMALL_ROTATION_THRESHOLD: u64 = 1;
610
611 let mut file_appender =
612 BankingTracer::create_file_appender(&path, REALLY_SMALL_ROTATION_THRESHOLD).unwrap();
613 file_appender.write_all(b"foo").unwrap();
614 file_appender.condition_mut().reset();
615 file_appender.write_all(b"bar").unwrap();
616 file_appender.condition_mut().reset();
617 file_appender.flush().unwrap();
618
619 assert_eq!(
620 [
621 std::fs::read_to_string(path.join("events")).ok(),
622 std::fs::read_to_string(path.join("events.1")).ok(),
623 std::fs::read_to_string(path.join("events.2")).ok(),
624 ],
625 [Some("bar".into()), Some("foo".into()), None]
626 );
627
628 for_test::drop_and_clean_temp_dir_unless_suppressed(temp_dir);
629 }
630
631 #[test]
632 fn test_reopen_with_blank_file() {
633 let temp_dir = TempDir::new().unwrap();
634
635 let path = temp_dir.path().join("banking-trace");
636
637 let mut file_appender =
638 BankingTracer::create_file_appender(&path, TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD)
639 .unwrap();
640 file_appender.write_all(b"f").unwrap();
642 file_appender.flush().unwrap();
643
644 let mut file_appender =
646 BankingTracer::create_file_appender(&path, TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD)
647 .unwrap();
648 assert_eq!(
650 [
651 std::fs::read_to_string(path.join("events")).ok(),
652 std::fs::read_to_string(path.join("events.1")).ok(),
653 std::fs::read_to_string(path.join("events.2")).ok(),
654 ],
655 [Some("f".into()), None, None]
656 );
657
658 file_appender.write_all(b"bar").unwrap();
660 assert_eq!(
661 [
662 std::fs::read_to_string(path.join("events")).ok(),
663 std::fs::read_to_string(path.join("events.1")).ok(),
664 std::fs::read_to_string(path.join("events.2")).ok(),
665 ],
666 [Some("".into()), Some("f".into()), None]
667 );
668
669 file_appender.flush().unwrap();
671 assert_eq!(
672 [
673 std::fs::read_to_string(path.join("events")).ok(),
674 std::fs::read_to_string(path.join("events.1")).ok(),
675 std::fs::read_to_string(path.join("events.2")).ok(),
676 ],
677 [Some("bar".into()), Some("f".into()), None]
678 );
679
680 for_test::drop_and_clean_temp_dir_unless_suppressed(temp_dir);
681 }
682}