1use crate::feedback::{
4 FEEDBACK_BUFFER_BYTES, FeedbackController, FeedbackSnapshot, ReceiverReport,
5};
6use crate::{
7 CuFeedbackRx, CuStreamRxError, CuStreamTx, LogStreamSenderConfig, OneWay, SenderCore,
8 SenderStats,
9};
10use cu29_clock::{CuDuration, CuTime, RobotClock};
11#[allow(unused_imports)]
12use cu29_log::{ANONYMOUS, CuLogEntry, CuLogLevel};
13#[allow(unused_imports)]
14use cu29_log_runtime::log;
15#[cfg(debug_assertions)]
16#[allow(unused_imports)]
17use cu29_log_runtime::log_debug_mode;
18use cu29_runtime::{copperlist::CopperList, curuntime::KeyFrame};
19use cu29_traits::{CopperListTuple, CuError, CuResult, WriteStream};
20#[allow(unused_imports)]
21use cu29_value::to_value;
22use std::{
23 fmt::{Debug, Formatter},
24 marker::PhantomData,
25 sync::{
26 Arc, Mutex,
27 atomic::{AtomicBool, AtomicU64, Ordering},
28 mpsc::{self, Receiver, SyncSender, TryRecvError},
29 },
30 thread::{JoinHandle, Thread},
31};
32
33const CL_BUFFERS: usize = 4;
34const KF_BUFFERS: usize = 2;
35const STRUCTURED_BUFFERS: usize = 4;
36use crate::pacing::STRUCTURED_RECORD_BYTES;
37
38struct StructuredRecord {
39 bytes: Box<[u8]>,
40 len: usize,
41 queued_at: CuTime,
42}
43
44struct StructuredInbox {
45 free: crossbeam_queue::ArrayQueue<StructuredRecord>,
46 pending: crossbeam_queue::ArrayQueue<StructuredRecord>,
47}
48
49pub struct ScheduledStructuredLogSink {
53 inbox: Arc<StructuredInbox>,
54 status: Arc<Shared>,
55 thread: Thread,
56 clock: RobotClock,
57 current: Option<StructuredRecord>,
58 oversized: bool,
59}
60
61impl Debug for ScheduledStructuredLogSink {
62 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
63 f.debug_struct("ScheduledStructuredLogSink")
64 .finish_non_exhaustive()
65 }
66}
67
68impl crate::structured::StructuredLogOutput for ScheduledStructuredLogSink {
69 fn begin(&mut self) {
70 if self.current.is_none()
71 && !self.status.stop.load(Ordering::Acquire)
72 && !self.status.failed.load(Ordering::Acquire)
73 {
74 self.current = self.inbox.free.pop();
75 }
76 if let Some(record) = &mut self.current {
77 record.len = crate::record::RECORD_HEADER_LEN;
78 }
79 self.oversized = false;
80 }
81
82 fn write(&mut self, bytes: &[u8]) {
83 if let Some(record) = &mut self.current
84 && !self.oversized
85 {
86 if bytes.len() > record.bytes.len() - record.len {
87 self.oversized = true;
88 } else {
89 record.bytes[record.len..record.len + bytes.len()].copy_from_slice(bytes);
90 record.len += bytes.len();
91 }
92 }
93 }
94
95 fn finish(&mut self, success: bool) {
96 let record = self.current.take();
97 if !success
98 || self.oversized
99 || self.status.stop.load(Ordering::Acquire)
100 || self.status.failed.load(Ordering::Acquire)
101 {
102 if let Some(record) = record {
103 let _ = self.inbox.free.push(record);
104 }
105 if success {
106 self.status.inbox_drops.fetch_add(1, Ordering::Relaxed);
107 }
108 } else if let Some(mut record) = record {
109 record.queued_at = self.clock.now();
110 if let Err(record) = self.inbox.pending.push(record) {
112 let _ = self.inbox.free.push(record);
113 self.status.inbox_drops.fetch_add(1, Ordering::Relaxed);
114 } else {
115 self.thread.unpark();
116 }
117 } else {
118 self.status.inbox_drops.fetch_add(1, Ordering::Relaxed);
119 }
120 }
121}
122
123struct Record {
124 bytes: Box<[u8]>,
125 len: usize,
126 keyframe: bool,
127 queued_at: CuTime,
128}
129
130#[derive(Clone, Copy, Debug, Default)]
132pub struct SenderSnapshot {
133 pub sampled_at: CuTime,
134 pub stats: SenderStats,
135 pub inbox_drops: u64,
136 pub stopped: bool,
137 pub failed: bool,
138 pub feedback_failed: bool,
139 pub feedback: Option<FeedbackSnapshot>,
140}
141
142#[derive(Clone, Debug)]
144pub struct SenderMonitor(Arc<Shared>);
145
146#[derive(Debug, Default)]
147struct Shared {
148 stop: AtomicBool,
149 failed: AtomicBool,
150 inbox_drops: AtomicU64,
151 final_stats: Mutex<Option<SenderStats>>,
152 snapshot: Mutex<SenderSnapshot>,
153}
154
155impl SenderMonitor {
156 pub fn snapshot(&self) -> SenderSnapshot {
157 let mut snapshot = *self.0.snapshot.lock().expect("sender snapshot poisoned");
158 snapshot.failed |= self.failed();
159 snapshot
160 }
161
162 pub fn inbox_drops(&self) -> u64 {
163 self.0.inbox_drops.load(Ordering::Relaxed)
164 }
165 pub fn failed(&self) -> bool {
166 self.0.failed.load(Ordering::Acquire)
167 }
168 pub fn final_stats(&self) -> Option<SenderStats> {
169 *self.0.final_stats.lock().expect("sender stats poisoned")
170 }
171}
172
173#[derive(Debug)]
174struct Worker {
175 shared: Arc<Shared>,
176 thread: Thread,
177 handle: Option<JoinHandle<()>>,
178}
179
180impl Drop for Worker {
181 fn drop(&mut self) {
182 self.shared.stop.store(true, Ordering::Release);
183 self.thread.unpark();
184 if self
185 .handle
186 .take()
187 .is_some_and(|handle| handle.join().is_err())
188 {
189 self.shared.failed.store(true, Ordering::Release);
190 }
191 }
192}
193
194#[derive(Debug)]
195struct Inbox {
196 clock: RobotClock,
197 worker: Arc<Worker>,
198 pending: SyncSender<Record>,
199 free: Mutex<Receiver<Box<[u8]>>>,
201 recycled: SyncSender<Box<[u8]>>,
202}
203
204impl Inbox {
205 fn submit(
206 &mut self,
207 keyframe: bool,
208 encode: impl FnOnce(&mut [u8]) -> CuResult<usize>,
209 ) -> CuResult<()> {
210 if self.worker.shared.failed.load(Ordering::Acquire) {
211 return Err(CuError::from("Logstream sender worker failed"));
212 }
213 let mut bytes = match self.free.get_mut().expect("exclusive inbox").try_recv() {
214 Ok(bytes) => bytes,
215 Err(TryRecvError::Empty) => {
216 self.worker
217 .shared
218 .inbox_drops
219 .fetch_add(1, Ordering::Relaxed);
220 return Ok(());
221 }
222 Err(TryRecvError::Disconnected) => {
223 return Err(CuError::from("Logstream sender worker stopped"));
224 }
225 };
226 let queued_at = self.clock.now();
227 let len = match encode(&mut bytes) {
228 Ok(len) => len,
229 Err(error) => {
230 let _ = self.recycled.try_send(bytes);
231 return Err(error);
232 }
233 };
234 self.pending
237 .try_send(Record {
238 bytes,
239 len,
240 keyframe,
241 queued_at,
242 })
243 .map_err(|_| CuError::from("Logstream sender inbox disconnected"))?;
244 self.worker.thread.unpark();
245 Ok(())
246 }
247}
248
249pub struct ScheduledCopperListSink<P: CopperListTuple> {
251 inbox: Inbox,
252 structured: Option<ScheduledStructuredLogSink>,
253 encoder: fn(&CopperList<P>, &mut [u8]) -> crate::Result<usize>,
254 _payload: PhantomData<fn() -> P>,
255}
256impl<P: CopperListTuple> ScheduledCopperListSink<P> {
257 pub fn take_structured_log_sink(&mut self) -> Option<ScheduledStructuredLogSink> {
259 self.structured.take()
260 }
261
262 pub fn with_encoder(
264 mut self,
265 encoder: fn(&CopperList<P>, &mut [u8]) -> crate::Result<usize>,
266 ) -> Self
267 where
268 P: CopperListTuple,
269 {
270 self.encoder = encoder;
271 self
272 }
273}
274impl<P: CopperListTuple> Debug for ScheduledCopperListSink<P> {
275 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
276 f.debug_struct("ScheduledCopperListSink")
277 .finish_non_exhaustive()
278 }
279}
280impl<P: CopperListTuple + Send + Sync> WriteStream<CopperList<P>> for ScheduledCopperListSink<P> {
281 fn log(&mut self, record: &CopperList<P>) -> CuResult<()> {
282 self.inbox.submit(false, |bytes| {
283 (self.encoder)(record, bytes).map_err(|error| CuError::from(error.to_string()))
284 })
285 }
286}
287
288#[derive(Debug)]
290pub struct ScheduledKeyFrameSink {
291 inbox: Inbox,
292 interval: u32,
293}
294impl WriteStream<KeyFrame> for ScheduledKeyFrameSink {
295 fn log(&mut self, keyframe: &KeyFrame) -> CuResult<()> {
296 if !keyframe.culistid.is_multiple_of(u64::from(self.interval)) {
297 return Ok(());
298 }
299 self.inbox.submit(true, |bytes| {
300 if bytes.len() < crate::record::RECORD_HEADER_LEN {
301 return Err(CuError::from("Keyframe buffer too small"));
302 }
303 let (header, payload) = bytes.split_at_mut(crate::record::RECORD_HEADER_LEN);
304 let len = bincode::encode_into_slice(keyframe, payload, bincode::config::standard())
305 .map_err(|error| CuError::from(error.to_string()))?;
306 crate::record::encode_record_header(
307 crate::RecordKind::KeyFrame,
308 keyframe.culistid,
309 &payload[..len],
310 header,
311 )
312 .map_err(|error| CuError::from(error.to_string()))?;
313 Ok(header.len() + len)
314 })
315 }
316}
317
318pub fn scheduled_sinks<P, T>(
323 transport: T,
324 config: LogStreamSenderConfig,
325 clock: RobotClock,
326) -> CuResult<(
327 ScheduledCopperListSink<P>,
328 ScheduledKeyFrameSink,
329 SenderMonitor,
330)>
331where
332 P: CopperListTuple + Send + Sync,
333 T: CuStreamTx + 'static,
334{
335 scheduled_worker(OneWay::new(transport), config, clock, false)
336}
337
338pub fn scheduled_feedback_sinks<P, T>(
340 transport: T,
341 config: LogStreamSenderConfig,
342 clock: RobotClock,
343) -> CuResult<(
344 ScheduledCopperListSink<P>,
345 ScheduledKeyFrameSink,
346 SenderMonitor,
347)>
348where
349 P: CopperListTuple + Send + Sync,
350 T: CuStreamTx + CuFeedbackRx + 'static,
351{
352 scheduled_worker(transport, config, clock, true)
353}
354
355fn scheduled_worker<P, T>(
356 transport: T,
357 config: LogStreamSenderConfig,
358 clock: RobotClock,
359 feedback_enabled: bool,
360) -> CuResult<(
361 ScheduledCopperListSink<P>,
362 ScheduledKeyFrameSink,
363 SenderMonitor,
364)>
365where
366 P: CopperListTuple + Send + Sync,
367 T: CuStreamTx + CuFeedbackRx + 'static,
368{
369 let manifest = crate::SessionManifest::decode_record(&config.recovery.manifest_record);
370 let policy = config.feedback;
371 let advertised = manifest
372 .as_ref()
373 .ok()
374 .and_then(|manifest| manifest.requirements.feedback);
375 if policy.is_some() != feedback_enabled || advertised.is_some() != feedback_enabled {
376 return Err(CuError::from(
377 "Feedback transport wiring must match manifest capability",
378 ));
379 }
380 if let (Some(policy), Some(advertised)) = (policy, advertised)
381 && (manifest
382 .as_ref()
383 .expect("validated feedback manifest")
384 .identity
385 != config.continuous.identity
386 || advertised.report_interval_ms != policy.report_interval_ms)
387 {
388 return Err(CuError::from(
389 "Feedback manifest identity and cadence must match the sender",
390 ));
391 }
392 let mut feedback = policy
393 .map(|policy| {
394 let baseline = u16::try_from(config.continuous.repair_every_source_symbols)
395 .map_err(|_| crate::Error::InvalidConfig("feedback baseline exceeds u16"))?;
396 FeedbackController::new(
397 policy,
398 config.continuous.identity,
399 advertised
400 .expect("validated feedback capability")
401 .destination,
402 baseline,
403 )
404 })
405 .transpose()
406 .map_err(|error| CuError::from(error.to_string()))?;
407 let cl_bytes = config.continuous.max_record_bytes;
408 let kf_bytes = usize::try_from(config.recovery.finite.max_object_bytes)
409 .map_err(|_| CuError::from("Keyframe limit exceeds usize"))?;
410 let structured_bytes = kf_bytes.min(STRUCTURED_RECORD_BYTES);
411 let reserved = cl_bytes
412 .checked_mul(CL_BUFFERS)
413 .and_then(|v| {
414 kf_bytes
415 .checked_mul(KF_BUFFERS)
416 .and_then(|k| v.checked_add(k))
417 })
418 .and_then(|v| {
419 v.checked_add(
420 STRUCTURED_BUFFERS * (structured_bytes + 2 * size_of::<StructuredRecord>()),
421 )
422 })
423 .ok_or_else(|| CuError::from("Sender pool size overflow"))?;
424 let shutdown_clock = if clock.is_mock() {
425 RobotClock::new()
426 } else {
427 clock.clone()
428 };
429 let interval = config.recovery.recovery_interval;
430 let drain_time = config.pacing.max_latency;
431 let mut core = SenderCore::new(config, clock.now(), reserved)
432 .map_err(|error| CuError::from(error.to_string()))?;
433 let (pending, records) = mpsc::sync_channel::<Record>(CL_BUFFERS + KF_BUFFERS);
434 let (cl_return, cl_free) = mpsc::sync_channel(CL_BUFFERS);
435 let (kf_return, kf_free) = mpsc::sync_channel(KF_BUFFERS);
436 for _ in 0..CL_BUFFERS {
437 cl_return
438 .try_send(vec![0; cl_bytes].into_boxed_slice())
439 .expect("new pool");
440 }
441 for _ in 0..KF_BUFFERS {
442 kf_return
443 .try_send(vec![0; kf_bytes].into_boxed_slice())
444 .expect("new pool");
445 }
446 let structured = Arc::new(StructuredInbox {
447 free: crossbeam_queue::ArrayQueue::new(STRUCTURED_BUFFERS),
448 pending: crossbeam_queue::ArrayQueue::new(STRUCTURED_BUFFERS),
449 });
450 for _ in 0..STRUCTURED_BUFFERS {
451 let _ = structured.free.push(StructuredRecord {
452 bytes: vec![0; structured_bytes].into_boxed_slice(),
453 len: 0,
454 queued_at: CuTime::default(),
455 });
456 }
457 let structured_worker = structured.clone();
458 let structured_clock = clock.clone();
459 let cl_clock = clock.clone();
460 let kf_clock = clock.clone();
461 let shared = Arc::new(Shared::default());
462 *shared.snapshot.lock().expect("new snapshot") = SenderSnapshot {
463 sampled_at: clock.now(),
464 feedback: feedback.as_ref().map(FeedbackController::snapshot),
465 ..Default::default()
466 };
467 let status = shared.clone();
468 let cl_recycled = cl_return.clone();
469 let kf_recycled = kf_return.clone();
470 let handle = std::thread::Builder::new()
471 .name("cu-logstream".into())
472 .spawn(move || {
473 let mut transport = transport;
474 let mut feedback_packet = [0; FEEDBACK_BUFFER_BYTES];
475 let mut feedback_failed = false;
476 let mut next_snapshot = clock.now();
477 let mut stop_at = None;
478 let mut structured_id = 0u64;
479 let result = (|| -> crate::Result<()> {
481 loop {
482 if status.stop.load(Ordering::Acquire) && stop_at.is_none() {
483 core.begin_shutdown();
484 stop_at = Some(shutdown_clock.now() + drain_time);
485 }
486 if let Some(controller) = &mut feedback {
487 for _ in 0..8 {
488 if feedback_failed { break; }
489 match transport.try_recv_feedback(&mut feedback_packet) {
490 Ok(Some(len)) => match feedback_packet.get(..len).and_then(|p| ReceiverReport::decode(p).ok()) {
491 Some(report) => { if controller.receive(report, clock.now()) { core.request_recovery(); } }
492 None => controller.invalid_report(),
493 },
494 Ok(None) => break,
495 Err(CuStreamRxError::BufferTooSmall { .. }) => controller.invalid_report(),
496 Err(CuStreamRxError::Failed(_)) => { feedback_failed = true; }
497 }
498 }
499 controller.tick(clock.now());
500 core.set_repair_interval(controller.snapshot().effective_repair_every_source_symbols)?;
501 }
502 let mut received = 0;
503 for _ in 0..CL_BUFFERS + KF_BUFFERS {
504 let Ok(record) = records.try_recv() else {
505 break;
506 };
507 let result =
508 core.accept_record(&record.bytes[..record.len], record.queued_at);
509 let returned = if record.keyframe {
510 &kf_return
511 } else {
512 &cl_return
513 };
514 let _ = returned.try_send(record.bytes);
515 result?;
516 received += 1;
517 }
518 for _ in 0..STRUCTURED_BUFFERS {
519 let Some(mut record) = structured_worker.pending.pop() else { break; };
520 let (header, payload) = record.bytes[..record.len].split_at_mut(crate::record::RECORD_HEADER_LEN);
521 let result = crate::record::encode_record_header(
522 crate::RecordKind::StructuredLog, structured_id, payload, header,
523 ).and_then(|()| core.accept_record(&record.bytes[..record.len], record.queued_at));
524 let _ = structured_worker.free.push(record);
525 result?;
526 structured_id = structured_id.checked_add(1)
527 .ok_or(crate::Error::InvalidConfig("structured log sequence exhausted"))?;
528 received += 1;
529 }
530 let next = core.poll(clock.now(), &mut transport)?;
531 let now = clock.now();
532 if now >= next_snapshot {
533 if let Ok(mut snapshot) = status.snapshot.try_lock() {
534 *snapshot = SenderSnapshot { sampled_at: now, stats: core.stats(),
535 inbox_drops: status.inbox_drops.load(Ordering::Relaxed),
536 feedback: feedback.as_ref().map(FeedbackController::snapshot), feedback_failed,
537 ..Default::default() };
538 }
539 next_snapshot = now + CuDuration::from_millis(100);
540 }
541 if stop_at.is_some_and(|deadline| shutdown_clock.now() >= deadline)
542 || (stop_at.is_some() && received == 0 && core.is_idle())
543 {
544 break;
545 }
546 if received > 0 {
547 continue;
548 }
549 if let Some(deadline) = next {
550 let now = clock.now();
551 if deadline > now {
552 let duration = (deadline - now).min(CuDuration::from_millis(50));
553 std::thread::park_timeout(std::time::Duration::from_nanos(
554 duration.as_nanos(),
555 ));
556 }
557 } else {
558 std::thread::park_timeout(std::time::Duration::from_millis(50));
559 }
560 }
561 Ok(())
562 })();
563 status.failed.store(result.is_err(), Ordering::Release);
564 core.discard_pending();
565 while let Some(record) = structured_worker.pending.pop() {
566 status.inbox_drops.fetch_add(1, Ordering::Relaxed);
567 let _ = structured_worker.free.push(record);
568 }
569 let stats = core.stats();
570 *status.final_stats.lock().expect("sender stats") = Some(stats);
571 *status.snapshot.lock().expect("sender snapshot") = SenderSnapshot {
572 sampled_at: clock.now(), stats, inbox_drops: status.inbox_drops.load(Ordering::Relaxed),
573 stopped: true, failed: result.is_err(), feedback_failed,
574 feedback: feedback.as_ref().map(FeedbackController::snapshot),
575 };
576 cu29_log_derive::info!("Logstream sender stopped: sent={} bytes={} queue_drops={} expired={} transport_drops={} inbox_drops={} shutdown_drops={}",
577 stats.packets_sent, stats.bytes_sent, stats.queue_drops, stats.expired_packets, stats.transport_drops,
578 status.inbox_drops.load(Ordering::Relaxed), stats.shutdown_drops);
579 if let Err(error) = result {
580 status.failed.store(true, Ordering::Release);
581 cu29_log_derive::error!("Logstream sender failed: {}", error.to_string());
582 }
583 })
584 .map_err(|error| CuError::new_with_cause("Start logstream sender", error))?;
585 let worker = Arc::new(Worker {
586 shared: shared.clone(),
587 thread: handle.thread().clone(),
588 handle: Some(handle),
589 });
590 Ok((
591 ScheduledCopperListSink {
592 structured: Some(ScheduledStructuredLogSink {
593 inbox: structured,
594 status: shared.clone(),
595 thread: worker.thread.clone(),
596 clock: structured_clock,
597 current: None,
598 oversized: false,
599 }),
600 encoder: crate::encode_copperlist_record_into,
601 inbox: Inbox {
602 clock: cl_clock,
603 worker: worker.clone(),
604 pending: pending.clone(),
605 free: Mutex::new(cl_free),
606 recycled: cl_recycled,
607 },
608 _payload: PhantomData,
609 },
610 ScheduledKeyFrameSink {
611 inbox: Inbox {
612 clock: kf_clock,
613 worker,
614 pending,
615 free: Mutex::new(kf_free),
616 recycled: kf_recycled,
617 },
618 interval,
619 },
620 SenderMonitor(shared),
621 ))
622}
623
624impl SenderMonitor {
625 pub fn into_runtime_monitor(
627 self,
628 destination: &str,
629 bitrate_bps: u64,
630 baseline: usize,
631 ) -> cu29_runtime::monitoring::LogStreamMonitor {
632 cu29_runtime::monitoring::LogStreamMonitor::new(destination, bitrate_bps, baseline, self)
633 }
634}
635impl cu29_runtime::monitoring::LogStreamStatsSource for SenderMonitor {
636 fn snapshot(&self) -> cu29_runtime::monitoring::LogStreamStats {
637 use cu29_runtime::monitoring::{
638 LogStreamFeedbackState, LogStreamFeedbackStats, LogStreamStats,
639 };
640 let snapshot = SenderMonitor::snapshot(self);
641 let stats = snapshot.stats;
642 LogStreamStats {
643 sampled_at: snapshot.sampled_at,
644 packets_sent: stats.packets_sent,
645 bytes_sent: stats.bytes_sent,
646 queue_drops: stats.queue_drops,
647 expired_packets: stats.expired_packets,
648 transport_drops: stats.transport_drops,
649 inbox_drops: snapshot.inbox_drops,
650 shutdown_drops: stats.shutdown_drops,
651 recovery_rounds: stats.recovery_rounds,
652 recovery_superseded: stats.recovery_superseded,
653 queue_peak: stats.queue_peak,
654 stopped: snapshot.stopped,
655 failed: snapshot.failed,
656 feedback: snapshot.feedback.map(|feedback| {
657 let report = feedback.report.unwrap_or_default();
658 LogStreamFeedbackStats {
659 state: match feedback.state {
660 crate::feedback::FeedbackState::Waiting => LogStreamFeedbackState::Waiting,
661 crate::feedback::FeedbackState::Active => LogStreamFeedbackState::Active,
662 crate::feedback::FeedbackState::Stale => LogStreamFeedbackState::Stale,
663 },
664 age: feedback.last_received.map(|last| {
665 CuDuration::from_nanos(
666 snapshot
667 .sampled_at
668 .as_nanos()
669 .saturating_sub(last.as_nanos()),
670 )
671 }),
672 failed: snapshot.feedback_failed,
673 reports: feedback.accepted_reports,
674 rejected_reports: feedback.rejected_reports,
675 invalid_reports: feedback.invalid_reports,
676 rates_available: feedback.receiver_rates_available,
677 source_metrics_available: feedback.source_metrics_available,
678 bytes_per_second: feedback.receiver_bytes_per_second,
679 packets_per_second: feedback.receiver_packets_per_second,
680 finalized_symbols: report.sources.finalized,
681 loss_basis_points: feedback.source_loss_basis_points,
682 recovery_basis_points: feedback.source_recovery_basis_points,
683 buffered_records: report.buffered_records,
684 record_capacity: report.record_capacity,
685 latest_copperlist: report.latest_copperlist,
686 effective_repair_every_source_symbols: feedback
687 .effective_repair_every_source_symbols,
688 invalid_packets: report.invalid_packets,
689 duplicate_packets: report.duplicate_packets,
690 expired_records: report.expired_records,
691 }
692 }),
693 }
694 }
695}