1use std::fmt;
55use std::io::{self, BufRead, Write};
56use std::time::Duration;
57
58use brokapi_core::{ENVELOPE_SCHEMA, Envelope};
59use serde::{Deserialize, Serialize};
60use tokio::sync::mpsc;
61
62pub const LOG_FORMAT: u32 = 1;
64
65#[derive(Serialize, Deserialize)]
66struct Header {
67 brokapi_log: u32,
68 envelope_schema: u16,
69}
70
71#[derive(Debug)]
82pub enum LogError {
83 Io(io::Error),
85 Format {
88 line: u64,
90 message: String,
92 },
93 Schema {
95 found: u32,
97 supported: u32,
99 },
100}
101
102impl fmt::Display for LogError {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 match self {
105 Self::Io(e) => write!(f, "log I/O error: {e}"),
106 Self::Format { line, message } => write!(f, "log line {line}: {message}"),
107 Self::Schema { found, supported } => write!(
108 f,
109 "log has format or schema {found}, this build reads up to {supported}"
110 ),
111 }
112 }
113}
114
115impl std::error::Error for LogError {
116 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
117 match self {
118 Self::Io(e) => Some(e),
119 _ => None,
120 }
121 }
122}
123
124impl From<io::Error> for LogError {
125 fn from(e: io::Error) -> Self {
126 Self::Io(e)
127 }
128}
129
130pub struct EnvelopeWriter<W: Write> {
140 out: W,
141 written: u64,
142}
143
144impl<W: Write> EnvelopeWriter<W> {
145 pub fn new(mut out: W) -> Result<Self, LogError> {
151 let header = Header {
152 brokapi_log: LOG_FORMAT,
153 envelope_schema: ENVELOPE_SCHEMA,
154 };
155 serde_json::to_writer(&mut out, &header).map_err(json_io)?;
156 out.write_all(b"\n")?;
157 Ok(Self { out, written: 0 })
158 }
159
160 pub fn write(&mut self, envelope: &Envelope) -> Result<(), LogError> {
167 serde_json::to_writer(&mut self.out, envelope).map_err(json_io)?;
168 self.out.write_all(b"\n")?;
169 self.written += 1;
170 Ok(())
171 }
172
173 pub fn written(&self) -> u64 {
175 self.written
176 }
177
178 pub fn flush(&mut self) -> Result<(), LogError> {
184 Ok(self.out.flush()?)
185 }
186
187 pub fn finish(mut self) -> Result<W, LogError> {
193 self.out.flush()?;
194 Ok(self.out)
195 }
196}
197
198fn json_io(e: serde_json::Error) -> LogError {
199 LogError::Io(io::Error::other(e))
200}
201
202pub struct EnvelopeReader<R: BufRead> {
211 input: R,
212 line: u64,
213 failed: bool,
214 buf: String,
215}
216
217impl<R: BufRead> EnvelopeReader<R> {
218 pub fn new(mut input: R) -> Result<Self, LogError> {
226 let mut first = String::new();
227 if input.read_line(&mut first)? == 0 {
228 return Err(LogError::Format {
229 line: 1,
230 message: "the file is empty, a log starts with a header line".into(),
231 });
232 }
233 let header: Header =
234 serde_json::from_str(first.trim_end()).map_err(|e| LogError::Format {
235 line: 1,
236 message: format!("not a log header: {e}"),
237 })?;
238 if header.brokapi_log > LOG_FORMAT {
239 return Err(LogError::Schema {
240 found: header.brokapi_log,
241 supported: LOG_FORMAT,
242 });
243 }
244 if header.envelope_schema > ENVELOPE_SCHEMA {
245 return Err(LogError::Schema {
246 found: u32::from(header.envelope_schema),
247 supported: u32::from(ENVELOPE_SCHEMA),
248 });
249 }
250 Ok(Self {
251 input,
252 line: 1,
253 failed: false,
254 buf: String::new(),
255 })
256 }
257}
258
259impl<R: BufRead> Iterator for EnvelopeReader<R> {
260 type Item = Result<Envelope, LogError>;
261
262 fn next(&mut self) -> Option<Self::Item> {
263 if self.failed {
264 return None;
265 }
266 loop {
267 self.buf.clear();
268 match self.input.read_line(&mut self.buf) {
269 Ok(0) => return None,
270 Ok(_) => {}
271 Err(e) => {
272 self.failed = true;
273 return Some(Err(e.into()));
274 }
275 }
276 self.line += 1;
277 let text = self.buf.trim_end();
278 if text.is_empty() {
279 continue;
280 }
281 return Some(match serde_json::from_str::<Envelope>(text) {
282 Ok(env) if env.schema > ENVELOPE_SCHEMA => {
283 self.failed = true;
284 Err(LogError::Schema {
285 found: u32::from(env.schema),
286 supported: u32::from(ENVELOPE_SCHEMA),
287 })
288 }
289 Ok(env) => Ok(env),
290 Err(e) => {
291 self.failed = true;
292 Err(LogError::Format {
293 line: self.line,
294 message: e.to_string(),
295 })
296 }
297 });
298 }
299 }
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Default)]
312pub enum ReplaySpeed {
313 #[default]
315 Instant,
316 Paced {
320 factor: f64,
322 max_gap: Duration,
324 },
325}
326
327#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
338pub struct ReplayReport {
339 pub sent: u64,
341 pub stopped_by_consumer: bool,
343}
344
345pub async fn replay<I>(
380 envelopes: I,
381 tx: &mpsc::Sender<Envelope>,
382 speed: ReplaySpeed,
383) -> Result<ReplayReport, LogError>
384where
385 I: IntoIterator<Item = Result<Envelope, LogError>>,
386{
387 let mut report = ReplayReport::default();
388 let mut previous: Option<i64> = None;
389 for item in envelopes {
390 let env = item?;
391 if let ReplaySpeed::Paced { factor, max_gap } = speed {
392 let now = env.ts.rx_ts.as_nanos();
393 if let Some(prev) = previous {
394 let gap = now.saturating_sub(prev).max(0) as u64;
395 if factor > 0.0 && gap > 0 {
396 let pause = Duration::from_nanos((gap as f64 / factor) as u64).min(max_gap);
397 tokio::time::sleep(pause).await;
398 }
399 }
400 previous = Some(now);
401 }
402 if tx.send(env).await.is_err() {
403 report.stopped_by_consumer = true;
404 return Ok(report);
405 }
406 report.sent += 1;
407 }
408 Ok(report)
409}
410
411#[cfg(test)]
412mod tests {
413 use std::sync::Arc;
414 use std::time::Instant;
415
416 use brokapi_core::{
417 BookLevel, Event, InstrumentId, InstrumentKey, NativeId, OrderBook, Price, Qty,
418 SubscriptionId, Timestamps, UnixNanos,
419 };
420
421 use super::*;
422
423 fn instrument() -> InstrumentId {
424 InstrumentId::new(
425 InstrumentKey::new("SBER", "TQBR"),
426 NativeId::TickerClass {
427 ticker: "SBER".into(),
428 class_code: "TQBR".into(),
429 },
430 )
431 }
432
433 fn book_env(n: i64, price: &str) -> Envelope {
434 let ts = Timestamps::new(UnixNanos::from_nanos(n), Instant::now())
435 .with_broker_ts(UnixNanos::from_nanos(n - 1));
436 let level = BookLevel {
437 price: price.parse::<Price>().unwrap(),
438 qty: Qty::from(10),
439 };
440 Envelope::new(
441 Arc::from("bcs-md"),
442 ts,
443 Event::OrderBook(OrderBook::Snapshot {
444 instrument: instrument(),
445 bids: vec![level.clone()],
446 asks: vec![],
447 ts,
448 }),
449 )
450 .with_sub(SubscriptionId::new(3))
451 }
452
453 fn write_all(envs: &[Envelope]) -> Vec<u8> {
454 let mut w = EnvelopeWriter::new(Vec::new()).unwrap();
455 for e in envs {
456 w.write(e).unwrap();
457 }
458 assert_eq!(w.written(), envs.len() as u64);
459 w.finish().unwrap()
460 }
461
462 #[test]
463 fn a_round_trip_keeps_every_field_and_the_exact_decimals() {
464 let envs = vec![book_env(10, "244.30"), book_env(20, "0.0001")];
465 let bytes = write_all(&envs);
466 let back: Vec<Envelope> = EnvelopeReader::new(bytes.as_slice())
467 .unwrap()
468 .collect::<Result<_, _>>()
469 .unwrap();
470 assert_eq!(back, envs);
471 let text = String::from_utf8(bytes).unwrap();
473 assert!(text.contains("\"244.30\""), "{text}");
474 }
475
476 #[test]
477 fn a_log_starts_with_a_header() {
478 let bytes = write_all(&[]);
479 let first = String::from_utf8(bytes).unwrap();
480 assert_eq!(first.trim_end(), r#"{"brokapi_log":1,"envelope_schema":1}"#);
481 }
482
483 #[test]
484 fn an_empty_file_and_a_foreign_file_are_refused() {
485 let empty = EnvelopeReader::new(&b""[..]).err().unwrap();
486 assert!(matches!(empty, LogError::Format { line: 1, .. }));
487 let foreign = EnvelopeReader::new(&b"{\"a\":1}\n"[..]).err().unwrap();
488 assert!(matches!(foreign, LogError::Format { line: 1, .. }));
489 }
490
491 #[test]
492 fn a_newer_format_or_schema_is_refused() {
493 let newer_log = br#"{"brokapi_log":2,"envelope_schema":1}"#;
494 let err = EnvelopeReader::new(&newer_log[..]).err().unwrap();
495 assert!(matches!(
496 err,
497 LogError::Schema {
498 found: 2,
499 supported: 1
500 }
501 ));
502 let newer_env = br#"{"brokapi_log":1,"envelope_schema":9}"#;
503 assert!(matches!(
504 EnvelopeReader::new(&newer_env[..]).err().unwrap(),
505 LogError::Schema { found: 9, .. }
506 ));
507 }
508
509 #[test]
510 fn a_bad_line_stops_the_read_and_names_the_line() {
511 let mut bytes = write_all(&[book_env(1, "1")]);
512 bytes.extend_from_slice(b"{broken\n");
513 bytes.extend_from_slice(&write_all(&[book_env(2, "2")])[39..]);
514 let mut reader = EnvelopeReader::new(bytes.as_slice()).unwrap();
515 assert!(reader.next().unwrap().is_ok());
516 let err = reader.next().unwrap().unwrap_err();
517 assert!(matches!(err, LogError::Format { line: 3, .. }), "{err}");
518 assert!(reader.next().is_none(), "nothing after a bad line");
519 }
520
521 #[test]
522 fn blank_lines_are_skipped() {
523 let mut bytes = write_all(&[book_env(1, "1")]);
524 bytes.extend_from_slice(b"\n\n");
525 let n = EnvelopeReader::new(bytes.as_slice()).unwrap().count();
526 assert_eq!(n, 1);
527 }
528
529 #[tokio::test]
530 async fn replay_sends_in_order_and_waits_for_a_slow_consumer() {
531 let envs: Vec<Envelope> = (1..=50).map(|n| book_env(n, "1")).collect();
532 let bytes = write_all(&envs);
533 let (tx, mut rx) = mpsc::channel(2); let reader = EnvelopeReader::new(bytes.as_slice()).unwrap();
535 let sender = async { replay(reader, &tx, ReplaySpeed::Instant).await };
536 let receiver = async {
537 let mut got = Vec::new();
538 while got.len() < 50 {
539 got.push(rx.recv().await.unwrap());
540 tokio::task::yield_now().await;
541 }
542 got
543 };
544 let (report, got) = tokio::join!(sender, receiver);
545 assert_eq!(report.unwrap().sent, 50);
546 assert_eq!(got, envs, "nothing lost, order kept");
547 }
548
549 #[tokio::test]
550 async fn replay_stops_quietly_when_the_consumer_goes_away() {
551 let envs: Vec<Envelope> = (1..=5).map(|n| book_env(n, "1")).collect();
552 let (tx, rx) = mpsc::channel(1);
553 drop(rx);
554 let report = replay(envs.into_iter().map(Ok), &tx, ReplaySpeed::Instant)
555 .await
556 .unwrap();
557 assert!(report.stopped_by_consumer);
558 assert_eq!(report.sent, 0);
559 }
560
561 #[tokio::test]
562 async fn replay_passes_a_read_error_on() {
563 let items: Vec<Result<Envelope, LogError>> = vec![
564 Ok(book_env(1, "1")),
565 Err(LogError::Format {
566 line: 3,
567 message: "x".into(),
568 }),
569 ];
570 let (tx, mut rx) = mpsc::channel(8);
571 let err = replay(items, &tx, ReplaySpeed::Instant).await.unwrap_err();
572 assert!(matches!(err, LogError::Format { line: 3, .. }));
573 assert!(rx.recv().await.is_some(), "what came before is sent");
574 }
575
576 #[tokio::test]
577 async fn paced_replay_waits_and_cuts_long_gaps() {
578 let envs = vec![
580 book_env(0, "1"),
581 book_env(200_000_000, "1"),
582 book_env(3_600_000_000_000, "1"),
583 ];
584 let (tx, mut rx) = mpsc::channel(8);
585 let started = Instant::now();
586 let speed = ReplaySpeed::Paced {
587 factor: 100.0,
588 max_gap: Duration::from_millis(30),
589 };
590 replay(envs.into_iter().map(Ok), &tx, speed).await.unwrap();
591 let took = started.elapsed();
592 assert!(
593 took >= Duration::from_millis(30),
594 "the long gap waits: {took:?}"
595 );
596 assert!(took < Duration::from_secs(2), "but is cut: {took:?}");
597 let mut n = 0;
598 while rx.try_recv().is_ok() {
599 n += 1;
600 }
601 assert_eq!(n, 3);
602 }
603}