1use crate::rlc::RepairSchedule;
4use crate::{
5 ContinuousEncoder, CuStreamTx, CuStreamTxError, Error, FiniteObjectEncoder,
6 LogStreamSenderConfig, RecordKind, RecoveryPoint, Result, decode_record, encode_record,
7 encode_recovery_point,
8};
9use alloc::{boxed::Box, vec, vec::Vec};
10use cu29_clock::{CuDuration, CuTime};
11
12pub const RECOVERY_REPEAT_INTERVAL: CuDuration = CuDuration(250_000_000);
15const MAX_SENDS_PER_POLL: usize = 64;
16const PENDING_BOUNDARIES: usize = 4;
17const PENDING_KEYFRAMES: usize = 2;
18pub(crate) const STRUCTURED_RECORD_BYTES: usize = 4096;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct PacingConfig {
23 pub bitrate_bps: u64,
24 pub burst_packets: u32,
25 pub max_latency: CuDuration,
26 pub memory_budget_bytes: usize,
29}
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33pub struct SenderStats {
34 pub packets_sent: u64,
35 pub bytes_sent: u64,
36 pub queue_drops: u64,
37 pub expired_packets: u64,
38 pub transport_drops: u64,
39 pub recovery_rounds: u64,
40 pub recovery_superseded: u64,
41 pub shutdown_drops: u64,
42 pub queue_peak: usize,
43}
44
45struct Packets {
47 bytes: Vec<u8>,
48 lengths: Vec<usize>,
49 times: Vec<CuTime>,
50 ids: Vec<u64>,
51 mtu: usize,
52 head: usize,
53 len: usize,
54}
55
56impl Packets {
57 fn new(capacity: usize, mtu: usize) -> Self {
58 Self {
59 bytes: vec![0; capacity * mtu],
60 lengths: vec![0; capacity],
61 times: vec![CuTime::default(); capacity],
62 ids: vec![0; capacity],
63 mtu,
64 head: 0,
65 len: 0,
66 }
67 }
68
69 fn clear(&mut self) {
70 self.head = 0;
71 self.len = 0;
72 }
73
74 fn push(&mut self, bytes: &[u8], now: CuTime, id: u64) -> bool {
75 if self.len == self.lengths.len() || bytes.len() > self.mtu {
76 return false;
77 }
78 let slot = (self.head + self.len) % self.lengths.len();
79 self.bytes[slot * self.mtu..slot * self.mtu + bytes.len()].copy_from_slice(bytes);
80 self.lengths[slot] = bytes.len();
81 self.times[slot] = now;
82 self.ids[slot] = id;
83 self.len += 1;
84 true
85 }
86
87 fn get(&self, index: usize) -> &[u8] {
88 let slot = (self.head + index) % self.lengths.len();
89 &self.bytes[slot * self.mtu..slot * self.mtu + self.lengths[slot]]
90 }
91
92 fn pop(&mut self) {
93 self.head = (self.head + 1) % self.lengths.len();
94 self.len -= 1;
95 }
96}
97
98struct PendingPackets {
99 id: Option<u64>,
100 packets: Packets,
101}
102
103struct RecoveryPackets {
104 control: Packets,
105 boundary: Packets,
106}
107
108pub struct SenderCore {
115 config: LogStreamSenderConfig,
116 continuous: Box<ContinuousEncoder<1128, 64>>,
117 finite: FiniteObjectEncoder,
118 structured_encoder: FiniteObjectEncoder,
119 repairs: RepairSchedule,
120 scratch: Vec<u8>,
121 data: Packets,
122 structured: Packets,
123 manifest: Packets,
124 latest: RecoveryPackets,
125 pending_cl: Vec<PendingPackets>,
126 pending_kf: Vec<PendingPackets>,
127 latest_id: Option<u64>,
128 manifest_cursor: usize,
129 recovery_cursor: usize,
130 next_repeat: CuTime,
131 last_now: CuTime,
132 credit: u128,
133 capacity: u128,
134 deficit: [usize; 3],
136 lane: usize,
137 stopping: bool,
138 transport_pending: bool,
139 stats: SenderStats,
140}
141
142impl SenderCore {
143 pub fn set_repair_interval(&mut self, every: u16) -> Result<()> {
145 if every == 0 {
146 return Err(Error::InvalidConfig("repair interval must be nonzero"));
147 }
148 self.repairs.set_interval(usize::from(every));
149 Ok(())
150 }
151
152 pub fn new(config: LogStreamSenderConfig, now: CuTime, reserved_bytes: usize) -> Result<Self> {
155 config.validate()?;
156 let policy = config.pacing;
157 if policy.bitrate_bps == 0
158 || policy.burst_packets == 0
159 || policy.max_latency.as_nanos() == 0
160 {
161 return Err(Error::InvalidConfig("pacing bounds must be nonzero"));
162 }
163 let mtu = crate::PACKET_HEADER_LEN + config.continuous.fec.symbol_size();
164 let object_bytes = usize::try_from(config.recovery.finite.max_object_bytes)
165 .map_err(|_| Error::InvalidConfig("object limit exceeds usize"))?;
166 let symbol = usize::from(config.recovery.finite.symbol_size);
167 if symbol != config.continuous.fec.symbol_size()
168 || symbol <= crate::rlc::FRAGMENT_HEADER_LEN
169 || config.continuous.repair_every_source_symbols == 0
170 || object_bytes < 256
171 {
172 return Err(Error::InvalidConfig(
173 "scheduled lanes require matching valid symbols and object bounds",
174 ));
175 }
176 if object_bytes.div_ceil(symbol) > 56_403
178 || raptorq::ObjectTransmissionInformation::with_defaults(
179 object_bytes as u64,
180 symbol as u16,
181 )
182 .source_blocks()
183 != 1
184 {
185 return Err(Error::InvalidConfig(
186 "scheduled objects must fit one RaptorQ source block",
187 ));
188 }
189 let repairs_per_object = config.recovery.finite.repair_symbols_per_block as usize;
190 let finite_capacity = |bytes: usize| bytes.div_ceil(symbol).checked_add(repairs_per_object);
191 let manifest_capacity = finite_capacity(config.recovery.manifest_record.len())
192 .ok_or(Error::InvalidConfig("manifest packet bound overflow"))?;
193 let control_capacity = finite_capacity(object_bytes)
194 .and_then(|v| finite_capacity(256).and_then(|a| v.checked_add(a)))
195 .ok_or(Error::InvalidConfig("recovery packet bound overflow"))?;
196 let source_capacity = config
197 .continuous
198 .max_record_bytes
199 .div_ceil(symbol - crate::rlc::FRAGMENT_HEADER_LEN);
200 let boundary_capacity = source_capacity
201 .checked_mul(2)
202 .ok_or(Error::InvalidConfig("boundary packet bound overflow"))?;
203 let packet_bytes = mtu + size_of::<usize>() + size_of::<CuTime>() + size_of::<u64>();
204 let retained_packets = control_capacity
205 .checked_mul(PENDING_KEYFRAMES + 1)
206 .and_then(|v| {
207 boundary_capacity
208 .checked_mul(PENDING_BOUNDARIES + 1)
209 .and_then(|b| v.checked_add(b))
210 })
211 .and_then(|v| v.checked_add(manifest_capacity))
212 .ok_or(Error::InvalidConfig("retained packet bound overflow"))?;
213 let fixed = retained_packets
214 .checked_mul(packet_bytes)
215 .and_then(|v| v.checked_add(size_of::<ContinuousEncoder<1128, 64>>()))
216 .and_then(|v| v.checked_add(size_of::<Self>()))
217 .and_then(|v| {
218 v.checked_add(
219 (PENDING_BOUNDARIES + PENDING_KEYFRAMES) * size_of::<PendingPackets>(),
220 )
221 })
222 .and_then(|v| v.checked_add(mtu))
223 .and_then(|v| v.checked_add(config.recovery.manifest_record.len()))
224 .and_then(|v| v.checked_add(reserved_bytes))
225 .ok_or(Error::InvalidConfig("sender storage bound overflow"))?;
226 let queue_capacity = policy
227 .memory_budget_bytes
228 .checked_sub(fixed)
229 .map(|remaining| remaining / packet_bytes)
230 .filter(|capacity| *capacity >= boundary_capacity.max(1))
231 .ok_or(Error::InvalidConfig(
232 "sender buffers exceed destination memory budget",
233 ))?;
234 let structured_capacity = (queue_capacity / 8).max(
235 finite_capacity(object_bytes.min(STRUCTURED_RECORD_BYTES))
236 .ok_or(Error::InvalidConfig("structured packet bound overflow"))?,
237 );
238 let data_capacity = queue_capacity.saturating_sub(structured_capacity);
239 if data_capacity < boundary_capacity.max(1) {
240 return Err(Error::InvalidConfig(
241 "sender packet queues exceed memory budget",
242 ));
243 }
244 let structured_encoder = FiniteObjectEncoder::new(crate::FiniteObjectSenderConfig {
245 lane: crate::Lane::StructuredLog,
246 ..config.recovery.finite
247 })?;
248 let continuous = Box::new(ContinuousEncoder::new(
249 config.continuous.identity,
250 config.continuous.lane,
251 config.continuous.fec,
252 config.continuous.max_record_bytes,
253 config.continuous.initial_esi,
254 )?);
255 let mut finite = FiniteObjectEncoder::new(config.recovery.finite)?;
256 let mut manifest = Packets::new(manifest_capacity, mtu);
257 let manifest_id = decode_record(&config.recovery.manifest_record)?.object_id;
258 finite.push_record_with(&config.recovery.manifest_record, |packet| {
259 if !manifest.push(packet, now, manifest_id) {
260 return Err(Error::InvalidConfig("manifest packet capacity"));
261 }
262 Ok(())
263 })?;
264 let recovery = || RecoveryPackets {
265 control: Packets::new(control_capacity, mtu),
266 boundary: Packets::new(boundary_capacity, mtu),
267 };
268 let capacity = u128::from(policy.burst_packets) * mtu as u128 * 8 * 1_000_000_000;
269 Ok(Self {
270 repairs: RepairSchedule::new(
271 config.continuous.repair_every_source_symbols,
272 config.continuous.first_repair_key,
273 config.continuous.repair_density,
274 ),
275 config,
276 continuous,
277 finite,
278 structured_encoder,
279 scratch: vec![0; mtu],
280 data: Packets::new(data_capacity, mtu),
281 structured: Packets::new(structured_capacity, mtu),
282 manifest,
283 latest: recovery(),
284 pending_cl: (0..PENDING_BOUNDARIES)
285 .map(|_| PendingPackets {
286 id: None,
287 packets: Packets::new(boundary_capacity, mtu),
288 })
289 .collect(),
290 pending_kf: (0..PENDING_KEYFRAMES)
291 .map(|_| PendingPackets {
292 id: None,
293 packets: Packets::new(control_capacity, mtu),
294 })
295 .collect(),
296 latest_id: None,
297 manifest_cursor: 0,
298 recovery_cursor: 0,
299 next_repeat: now + RECOVERY_REPEAT_INTERVAL,
300 last_now: now,
301 credit: capacity,
302 capacity,
303 deficit: [0; 3],
304 lane: 0,
305 stopping: false,
306 transport_pending: false,
307 stats: SenderStats::default(),
308 })
309 }
310
311 pub const fn stats(&self) -> SenderStats {
312 self.stats
313 }
314
315 pub fn accept_record(&mut self, record: &[u8], now: CuTime) -> Result<()> {
319 let decoded = decode_record(record)?;
320 match decoded.kind {
321 RecordKind::CopperList => {
322 let retain = decoded
323 .object_id
324 .is_multiple_of(u64::from(self.config.recovery.recovery_interval));
325 let slot = if retain {
326 Some(Self::reserve(
327 &mut self.pending_cl,
328 decoded.object_id,
329 &mut self.stats,
330 ))
331 } else {
332 None
333 };
334 let stats = &mut self.stats;
335 let pending = &mut self.pending_cl;
336 let data = &mut self.data;
337 self.continuous.push_record_with_repairs(
338 record,
339 &mut self.scratch,
340 &mut |packet| {
341 if let Some(slot) = slot
342 && !pending[slot].packets.push(packet, now, decoded.object_id)
343 {
344 return Err(Error::InvalidConfig("boundary packet capacity"));
345 }
346 if !data.push(packet, now, decoded.object_id) {
347 stats.queue_drops = stats.queue_drops.saturating_add(1);
348 }
349 stats.queue_peak = stats.queue_peak.max(data.len);
350 Ok(())
351 },
352 &mut self.repairs,
353 )?;
354 }
355 RecordKind::StructuredLog => {
356 self.structured_encoder.push_record_with(record, |packet| {
358 if !self.structured.push(packet, now, decoded.object_id) {
359 self.stats.queue_drops = self.stats.queue_drops.saturating_add(1);
360 }
361 self.stats.queue_peak = self
362 .stats
363 .queue_peak
364 .max(self.data.len + self.structured.len);
365 Ok(())
366 })?;
367 }
368 RecordKind::KeyFrame => {
369 if !decoded
370 .object_id
371 .is_multiple_of(u64::from(self.config.recovery.recovery_interval))
372 {
373 return Ok(());
374 }
375 let manifest = decode_record(&self.config.recovery.manifest_record)?;
376 let recovery_point = encode_record(
377 RecordKind::RecoveryPoint,
378 decoded.object_id,
379 &encode_recovery_point(&RecoveryPoint {
380 manifest_object_id: manifest.object_id,
381 manifest_record_digest: manifest.digest,
382 copperlist_id: decoded.object_id,
383 keyframe_object_id: decoded.object_id,
384 keyframe_record_digest: decoded.digest,
385 })?,
386 )?;
387 let slot = Self::reserve(&mut self.pending_kf, decoded.object_id, &mut self.stats);
388 for bytes in [record, recovery_point.as_slice()] {
389 self.finite.push_record_with(bytes, |packet| {
390 if !self.pending_kf[slot]
391 .packets
392 .push(packet, now, decoded.object_id)
393 {
394 return Err(Error::InvalidConfig("recovery packet capacity"));
395 }
396 Ok(())
397 })?;
398 }
399 }
400 _ => {
401 return Err(Error::InvalidConfig(
402 "sender inbox accepts CLs, keyframes and structured logs",
403 ));
404 }
405 }
406 self.promote_recovery();
407 Ok(())
408 }
409
410 fn reserve(slots: &mut [PendingPackets], id: u64, stats: &mut SenderStats) -> usize {
411 let index = slots
412 .iter()
413 .position(|s| s.id.is_none() || s.id == Some(id))
414 .unwrap_or_else(|| {
415 slots
416 .iter()
417 .enumerate()
418 .min_by_key(|(_, s)| s.id)
419 .unwrap()
420 .0
421 });
422 if slots[index].id.is_some() {
423 stats.recovery_superseded += 1;
424 }
425 slots[index].id = Some(id);
426 slots[index].packets.clear();
427 index
428 }
429
430 fn promote_recovery(&mut self) {
431 if self.recovery_cursor < self.recovery_len() {
434 return;
435 }
436 let pair = self
437 .pending_cl
438 .iter()
439 .enumerate()
440 .filter_map(|(cl, entry)| {
441 let id = entry.id?;
442 if self.data.len > 0 && self.data.ids[self.data.head] < id {
445 return None;
446 }
447 if self.latest_id.is_some_and(|latest| id <= latest) {
448 return None;
449 }
450 self.pending_kf
451 .iter()
452 .position(|kf| kf.id == Some(id))
453 .map(|kf| (id, cl, kf))
454 })
455 .min_by_key(|(id, _, _)| *id);
456 if let Some((id, cl, kf)) = pair {
457 core::mem::swap(&mut self.latest.boundary, &mut self.pending_cl[cl].packets);
458 core::mem::swap(&mut self.latest.control, &mut self.pending_kf[kf].packets);
459 self.pending_cl[cl].id = None;
460 self.pending_kf[kf].id = None;
461 self.latest_id = Some(id);
462 self.recovery_cursor = 0;
463 self.request_recovery();
464 }
465 }
466
467 pub fn request_recovery(&mut self) {
470 if self.stopping || self.control_packet().is_some() {
471 return;
472 }
473 if self.manifest_cursor >= self.manifest.len {
474 self.manifest_cursor = 0;
475 }
476 if self.recovery_cursor >= self.recovery_len() {
477 self.recovery_cursor = 0;
478 }
479 }
480
481 fn recovery_len(&self) -> usize {
482 self.latest.control.len + self.latest.boundary.len
483 }
484
485 fn control_packet(&self) -> Option<&[u8]> {
486 if self.manifest_cursor < self.manifest.len {
487 return Some(self.manifest.get(self.manifest_cursor));
488 }
489 if self.recovery_cursor < self.latest.control.len {
490 return Some(self.latest.control.get(self.recovery_cursor));
491 }
492 let boundary = self.recovery_cursor - self.latest.control.len;
493 (boundary < self.latest.boundary.len).then(|| self.latest.boundary.get(boundary))
494 }
495
496 pub fn begin_shutdown(&mut self) {
498 self.stopping = true;
499 }
500
501 pub fn discard_pending(&mut self) {
503 self.stats.shutdown_drops += (self.data.len
504 + self.structured.len
505 + self.manifest.len.saturating_sub(self.manifest_cursor)
506 + self.recovery_len().saturating_sub(self.recovery_cursor))
507 as u64;
508 self.data.clear();
509 self.structured.clear();
510 self.manifest_cursor = self.manifest.len;
511 self.recovery_cursor = self.recovery_len();
512 }
513
514 pub fn is_idle(&self) -> bool {
515 self.data.len == 0
516 && self.structured.len == 0
517 && self.control_packet().is_none()
518 && !self.transport_pending
519 }
520
521 pub fn poll<T: CuStreamTx>(
524 &mut self,
525 now: CuTime,
526 transport: &mut T,
527 ) -> Result<Option<CuTime>> {
528 let next = self.poll_packets(now, transport)?;
529 self.transport_pending = transport.poll_pending().map_err(|error| match error {
530 CuStreamTxError::WouldBlock => {
531 Error::Transport("Transport poll must report pending instead of WouldBlock")
532 }
533 CuStreamTxError::Failed(message) => Error::Transport(message),
534 })?;
535 Ok(if self.transport_pending {
536 let retry = now + CuDuration::from_millis(1);
537 Some(next.map_or(retry, |deadline| deadline.min(retry)))
538 } else {
539 next
540 })
541 }
542
543 fn poll_packets<T: CuStreamTx>(
544 &mut self,
545 now: CuTime,
546 transport: &mut T,
547 ) -> Result<Option<CuTime>> {
548 if now < self.last_now {
549 return Err(Error::InvalidConfig("sender clock moved backwards"));
550 }
551 let elapsed = (now - self.last_now).as_nanos();
552 self.credit = self.capacity.min(
553 self.credit
554 .saturating_add(u128::from(elapsed) * u128::from(self.config.pacing.bitrate_bps)),
555 );
556 self.last_now = now;
557 self.promote_recovery();
558 if !self.stopping && now >= self.next_repeat {
559 self.request_recovery();
560 self.stats.recovery_rounds = self.stats.recovery_rounds.saturating_add(1);
561 self.next_repeat = now + RECOVERY_REPEAT_INTERVAL;
562 }
563 let mut work = 0;
564 for queue in [&mut self.data, &mut self.structured] {
565 while queue.len > 0
566 && now >= queue.times[queue.head] + self.config.pacing.max_latency
567 && work < MAX_SENDS_PER_POLL
568 {
569 queue.pop();
570 self.stats.expired_packets += 1;
571 work += 1;
572 }
573 }
574 while work < MAX_SENDS_PER_POLL {
575 let available = [
576 self.data.len > 0,
577 self.control_packet().is_some(),
578 self.structured.len > 0,
579 ];
580 if !available.iter().any(|ready| *ready) {
581 break;
582 }
583 if !available[self.lane] {
584 self.deficit[self.lane] = 0;
585 self.lane = (self.lane + 1) % 3;
586 continue;
587 }
588 let packet = match self.lane {
589 0 => self.data.get(0),
590 1 => self.control_packet().unwrap(),
591 _ => self.structured.get(0),
592 };
593 let len = packet.len();
594 if self.deficit[self.lane] < len {
595 self.deficit[self.lane] += self.scratch.len() * if self.lane == 0 { 3 } else { 1 };
596 self.lane = (self.lane + 1) % 3;
597 continue;
598 }
599 let cost = len as u128 * 8 * 1_000_000_000;
600 if self.credit < cost {
601 let wait =
602 (cost - self.credit).div_ceil(u128::from(self.config.pacing.bitrate_bps));
603 let mut deadline = now + CuDuration(wait.min(u128::from(u64::MAX)) as u64);
604 for queue in [&self.data, &self.structured] {
605 if queue.len > 0 {
606 deadline =
607 deadline.min(queue.times[queue.head] + self.config.pacing.max_latency);
608 }
609 }
610 return Ok(Some(if self.stopping {
611 deadline
612 } else {
613 deadline.min(self.next_repeat)
614 }));
615 }
616 match transport.try_send(packet) {
617 Ok(()) => {
618 self.stats.packets_sent += 1;
619 self.stats.bytes_sent += len as u64;
620 }
621 Err(CuStreamTxError::WouldBlock) => self.stats.transport_drops += 1,
622 Err(CuStreamTxError::Failed(message)) => return Err(Error::Transport(message)),
623 }
624 self.credit -= cost;
625 self.deficit[self.lane] -= len;
626 if self.lane == 0 {
627 self.data.pop();
628 } else if self.lane == 2 {
629 self.structured.pop();
630 } else if self.manifest_cursor < self.manifest.len {
631 self.manifest_cursor += 1;
632 } else {
633 self.recovery_cursor += 1;
634 }
635 work += 1;
636 self.promote_recovery();
637 }
638 Ok(
639 if self.data.len != 0 || self.structured.len != 0 || self.control_packet().is_some() {
640 Some(now)
641 } else if self.stopping {
642 None
643 } else {
644 Some(self.next_repeat)
645 },
646 )
647 }
648}