1use std::collections::BTreeMap;
7use std::net::SocketAddr;
8use std::sync::{Arc, Mutex};
9use std::time::Instant;
10
11use hatchery_core::{
12 grow_read_buf, ChannelKind, DrainEnvelope, Frame, FrameError, FrameType, JournalReplyBody,
13 OpenPayload, MAX_DATA_PAYLOAD,
14};
15use tokio::io::{AsyncReadExt, AsyncWriteExt};
16use tokio::net::{TcpListener, TcpStream};
17use tokio::sync::watch;
18
19use super::buffer::{EnqueueOutcome, ReplayBuffer, StreamKey};
20use super::crypto::{c2_strip_outer, RestreamerKeys};
21use super::journal::{recover_gap, NodeJournal};
22
23#[derive(Clone, Debug)]
24pub struct RestreamerConfig {
25 pub node_listen: SocketAddr,
27 pub drain_listen: SocketAddr,
29 pub buffer: super::buffer::BufferLimits,
30 pub core_token: String,
33}
34
35#[derive(Debug)]
36pub enum RestreamerError {
37 Io(std::io::Error),
38 Config(&'static str),
39 Frame(FrameError),
40}
41
42impl std::fmt::Display for RestreamerError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::Io(e) => write!(f, "{e}"),
46 Self::Config(m) => write!(f, "{m}"),
47 Self::Frame(e) => write!(f, "{e}"),
48 }
49 }
50}
51
52impl std::error::Error for RestreamerError {}
53
54impl From<std::io::Error> for RestreamerError {
55 fn from(value: std::io::Error) -> Self {
56 Self::Io(value)
57 }
58}
59
60#[derive(Clone, Default)]
62pub struct UplinkPeers {
63 inner: Arc<Mutex<BTreeMap<String, PeerSlot>>>,
64}
65
66struct PeerSlot {
67 out: tokio::sync::mpsc::Sender<Frame>,
68 pending: Option<tokio::sync::oneshot::Sender<JournalReplyBody>>,
69}
70
71impl UplinkPeers {
72 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn register(&self, node_id: String, tx: tokio::sync::mpsc::Sender<Frame>) {
77 self.inner.lock().expect("peers").insert(
78 node_id,
79 PeerSlot {
80 out: tx,
81 pending: None,
82 },
83 );
84 }
85
86 pub fn unregister(&self, node_id: &str) {
87 self.inner.lock().expect("peers").remove(node_id);
88 }
89
90 pub fn unregister_if(&self, node_id: &str, tx: &tokio::sync::mpsc::Sender<Frame>) {
93 let mut guard = self.inner.lock().expect("peers");
94 if guard.get(node_id).is_some_and(|slot| slot.out.same_channel(tx)) {
95 guard.remove(node_id);
96 }
97 }
98
99 pub fn deliver_reply(&self, node_id: &str, body: JournalReplyBody) {
100 let mut guard = self.inner.lock().expect("peers");
101 if let Some(slot) = guard.get_mut(node_id) {
102 if let Some(tx) = slot.pending.take() {
103 let _ = tx.send(body);
104 }
105 }
106 }
107
108 pub async fn send_to(&self, node_id: &str, frame: Frame) -> Result<(), RestreamerError> {
110 let out = {
111 let guard = self.inner.lock().expect("peers");
112 guard.get(node_id).map(|slot| slot.out.clone())
113 };
114 let Some(out) = out else {
115 return Err(RestreamerError::Frame(FrameError::Io("node uplink absent".into())));
116 };
117 out.send(frame)
118 .await
119 .map_err(|_| RestreamerError::Frame(FrameError::Io("node uplink closed".into())))
120 }
121
122 pub fn is_registered(&self, node_id: &str) -> bool {
123 self.inner.lock().expect("peers").contains_key(node_id)
124 }
125
126 pub async fn request_journal(
127 &self,
128 node_id: &str,
129 channel_id: u32,
130 after: u64,
131 limit: u32,
132 timeout: std::time::Duration,
133 ) -> Result<JournalReplyBody, RestreamerError> {
134 let (tx, rx) = tokio::sync::oneshot::channel();
135 let out = {
136 let mut guard = self.inner.lock().expect("peers");
137 let Some(slot) = guard.get_mut(node_id) else {
138 return Err(RestreamerError::Frame(FrameError::Io(
139 "node uplink absent".into(),
140 )));
141 };
142 slot.pending = Some(tx);
143 slot.out.clone()
144 };
145 out.send(Frame::journal_req(channel_id, after, limit))
146 .await
147 .map_err(|_| RestreamerError::Frame(FrameError::Io("node uplink closed".into())))?;
148 match tokio::time::timeout(timeout, rx).await {
149 Ok(Ok(body)) => Ok(body),
150 Ok(Err(_)) => Err(RestreamerError::Frame(FrameError::Io(
151 "journal reply dropped".into(),
152 ))),
153 Err(_) => Err(RestreamerError::Frame(FrameError::Io(
154 "journal reply timeout".into(),
155 ))),
156 }
157 }
158}
159
160struct Inner {
161 buffer: ReplayBuffer,
162 channels: BTreeMap<(String, u32), ChannelKind>,
164 keys: Option<RestreamerKeys>,
165 pushed_to_core: bool,
167}
168
169#[derive(Clone)]
171pub struct Restreamer {
172 uplink_ready: Arc<tokio::sync::Notify>,
174 inner: Arc<Mutex<Inner>>,
175 journal: Arc<dyn NodeJournal>,
176 peers: UplinkPeers,
177}
178
179impl Restreamer {
180 pub fn new(
181 buffer_limits: super::buffer::BufferLimits,
182 journal: Arc<dyn NodeJournal>,
183 core_token: impl Into<String>,
184 keys: Option<RestreamerKeys>,
185 ) -> Self {
186 let _ = core_token; Self {
188 uplink_ready: Arc::new(tokio::sync::Notify::new()),
189 inner: Arc::new(Mutex::new(Inner {
190 buffer: ReplayBuffer::new(buffer_limits).with_default_credit(1024),
195 channels: BTreeMap::new(),
196 keys,
197 pushed_to_core: false,
198 })),
199 journal,
200 peers: UplinkPeers::new(),
201 }
202 }
203
204 pub fn set_keys(&self, keys: RestreamerKeys) {
205 self.inner.lock().expect("lock").keys = Some(keys);
206 }
207
208 pub fn uplink_notify(&self) -> Arc<tokio::sync::Notify> {
209 self.uplink_ready.clone()
210 }
211
212 pub fn peers(&self) -> &UplinkPeers {
213 &self.peers
214 }
215
216 pub fn upstream_public_keys(&self) -> Option<(super::crypto::PublicKeyBytes, super::crypto::PublicKeyBytes)> {
217 self.inner
218 .lock()
219 .expect("lock")
220 .keys
221 .as_ref()
222 .map(|k| k.upstream_public_keys())
223 }
224
225 pub fn ever_pushed_to_core(&self) -> bool {
227 self.inner.lock().expect("lock").pushed_to_core
228 }
229
230 pub fn admit_from_node(
233 &self,
234 node_id: &str,
235 channel_id: u32,
236 payload: Vec<u8>,
237 ) -> Result<EnqueueOutcome, RestreamerError> {
238 if payload.len() > MAX_DATA_PAYLOAD {
239 return Err(RestreamerError::Frame(FrameError::PayloadTooLarge(payload.len())));
240 }
241 let mut guard = self.inner.lock().expect("lock");
242 let opaque = if let Some(keys) = guard.keys.clone() {
243 c2_strip_outer(&keys, &payload).map_err(|e| {
244 RestreamerError::Frame(FrameError::Io(format!("outer layer: {e}")))
245 })?
246 } else {
247 payload
248 };
249 if matches!(
253 hatchery_core::inner::decode_inner(&opaque),
254 Some((hatchery_core::inner::InnerKind::Handshake1, _))
255 ) {
256 guard.buffer.ensure_handshake_credit(node_id, channel_id);
257 }
258 let outcome = guard
259 .buffer
260 .enqueue(node_id, channel_id, opaque.clone(), Instant::now());
261 if let EnqueueOutcome::Accepted { .. } = outcome {
262 let seq = guard.buffer.next_seq(node_id, channel_id) - 1;
263 drop(guard);
264 let _ = (seq, opaque);
267 self.uplink_ready.notify_waiters();
268 }
269 Ok(outcome)
270 }
271
272 pub fn open_channel(&self, node_id: &str, channel_id: u32, kind: ChannelKind) {
273 let mut guard = self.inner.lock().expect("lock");
274 guard.channels.insert((node_id.to_owned(), channel_id), kind);
275 guard.buffer.reset_channel_credit(node_id, channel_id);
279 }
280
281 pub fn ack(&self, node_id: &str, channel_id: u32, seq: u64) {
282 self.inner
283 .lock()
284 .expect("lock")
285 .buffer
286 .ack(node_id, channel_id, seq);
287 }
288
289 pub fn window_credit(&self, node_id: &str, channel_id: u32) -> u32 {
290 let key = StreamKey {
291 node_id: node_id.to_owned(),
292 channel_id,
293 };
294 self.inner.lock().expect("lock").buffer.credit(&key)
295 }
296
297 pub fn resume(
299 &self,
300 node_id: &str,
301 channel_id: u32,
302 after: u64,
303 ) -> Result<Vec<DrainEnvelope>, RestreamerError> {
304 let (gap, oldest, buffered) = {
305 let guard = self.inner.lock().expect("lock");
306 let gap = guard.buffer.gap_before_buffer(node_id, channel_id, after);
307 let oldest = guard.buffer.oldest_seq(node_id, channel_id);
308 let buffered = guard.buffer.events_after(node_id, channel_id, after);
309 (gap, oldest, buffered)
310 };
311 let mut out = Vec::new();
312 if gap {
313 let until = oldest;
314 let recovered = recover_gap(
315 self.journal.as_ref(),
316 node_id,
317 channel_id,
318 after,
319 until,
320 1024,
321 )
322 .map_err(|e| RestreamerError::Frame(FrameError::Io(e.to_string())))?;
323 for ev in recovered {
324 out.push(DrainEnvelope {
325 node_id: ev.node_id.clone(),
326 frame: Frame::data(ev.channel_id, ev.seq, ev.payload)
327 .map_err(RestreamerError::Frame)?,
328 });
329 }
330 }
331 for ev in buffered {
332 if out.iter().any(|e| e.frame.seq == ev.seq) {
334 continue;
335 }
336 out.push(DrainEnvelope {
337 node_id: ev.node_id.clone(),
338 frame: Frame::data(ev.channel_id, ev.seq, ev.payload)
339 .map_err(RestreamerError::Frame)?,
340 });
341 }
342 Ok(out)
343 }
344
345 pub async fn resume_with_uplink(
347 &self,
348 node_id: &str,
349 channel_id: u32,
350 after: u64,
351 ) -> Result<Vec<DrainEnvelope>, RestreamerError> {
352 let (gap, oldest, buffered) = {
353 let guard = self.inner.lock().expect("lock");
354 let gap = guard.buffer.gap_before_buffer(node_id, channel_id, after);
355 let oldest = guard.buffer.oldest_seq(node_id, channel_id);
356 let buffered = guard.buffer.events_after(node_id, channel_id, after);
357 (gap, oldest, buffered)
358 };
359 let mut out = Vec::new();
360 if gap {
361 let until = oldest;
362 let recovered = if self.peers.is_registered(node_id) {
363 let body = self
364 .peers
365 .request_journal(
366 node_id,
367 channel_id,
368 after,
369 1024,
370 std::time::Duration::from_secs(5),
371 )
372 .await;
373 let body = match body {
374 Ok(b) => b,
375 Err(err) => {
376 tracing::warn!(%node_id, channel_id, after, error = %err, "journal gap not recoverable; resuming from buffer");
378 JournalReplyBody { after, entries: Vec::new() }
379 }
380 };
381 body.entries
382 .into_iter()
383 .filter(|e| until.map(|u| e.seq < u).unwrap_or(true))
384 .map(|e| super::buffer::BufferedEvent {
385 seq: e.seq,
386 node_id: node_id.to_owned(),
387 channel_id,
388 payload: e.payload,
389 enqueued_at: Instant::now(),
390 })
391 .collect()
392 } else {
393 recover_gap(
394 self.journal.as_ref(),
395 node_id,
396 channel_id,
397 after,
398 until,
399 1024,
400 )
401 .unwrap_or_else(|err| {
402 tracing::warn!(%node_id, channel_id, after, error = %err, "journal gap not recoverable; resuming from buffer");
403 Vec::new()
404 })
405 };
406 for ev in recovered {
407 out.push(DrainEnvelope {
408 node_id: ev.node_id.clone(),
409 frame: Frame::data(ev.channel_id, ev.seq, ev.payload)
410 .map_err(RestreamerError::Frame)?,
411 });
412 }
413 }
414 for ev in buffered {
415 if out.iter().any(|e| e.frame.seq == ev.seq) {
416 continue;
417 }
418 out.push(DrainEnvelope {
419 node_id: ev.node_id.clone(),
420 frame: Frame::data(ev.channel_id, ev.seq, ev.payload)
421 .map_err(RestreamerError::Frame)?,
422 });
423 }
424 Ok(out)
425 }
426
427 pub fn buffered_after(&self, node_id: &str, channel_id: u32, after: u64) -> Vec<DrainEnvelope> {
429 let guard = self.inner.lock().expect("lock");
430 guard
431 .buffer
432 .events_after(node_id, channel_id, after)
433 .into_iter()
434 .filter_map(|ev| {
435 Frame::data(ev.channel_id, ev.seq, ev.payload)
436 .ok()
437 .map(|frame| DrainEnvelope {
438 node_id: ev.node_id,
439 frame,
440 })
441 })
442 .collect()
443 }
444}
445
446pub async fn serve(
448 config: RestreamerConfig,
449 restreamer: Restreamer,
450 mut shutdown: watch::Receiver<bool>,
451) -> Result<(), RestreamerError> {
452 if config.node_listen == config.drain_listen {
453 return Err(RestreamerError::Config(
454 "node_listen and drain_listen must differ",
455 ));
456 }
457 if config.core_token.len() < 16 {
458 return Err(RestreamerError::Config("core_token too short"));
459 }
460 let node_listener = TcpListener::bind(config.node_listen).await?;
461 let drain_listener = TcpListener::bind(config.drain_listen).await?;
462 loop {
463 tokio::select! {
464 _ = shutdown.changed() => {
465 if *shutdown.borrow() { break; }
466 }
467 accepted = node_listener.accept() => {
468 let (stream, _) = accepted?;
469 let _ = stream.set_nodelay(true);
470 let rs = restreamer.clone();
471 tokio::spawn(async move {
472 if let Err(err) = handle_node_uplink(stream, rs).await {
473 tracing::debug!(error = %err, "node uplink closed");
474 }
475 });
476 }
477 accepted = drain_listener.accept() => {
478 let (stream, _) = accepted?;
479 let _ = stream.set_nodelay(true);
480 let rs = restreamer.clone();
481 let token = config.core_token.clone();
482 tokio::spawn(async move {
483 if let Err(err) = handle_core_drain(stream, rs, &token).await {
484 tracing::debug!(error = %err, "core drain closed");
485 }
486 });
487 }
488 }
489 }
490 Ok(())
491}
492
493async fn handle_node_uplink(mut stream: TcpStream, rs: Restreamer) -> Result<(), RestreamerError> {
494 let mut buf = vec![0u8; 64 * 1024];
495 let mut filled = 0usize;
496 let mut node_id: Option<String> = None;
497 let (out_tx, mut out_rx) = tokio::sync::mpsc::channel::<Frame>(16);
498 loop {
499 tokio::select! {
500 biased;
501 maybe_out = out_rx.recv() => {
502 match maybe_out {
503 Some(frame) => {
504 stream
505 .write_all(&frame.encode().map_err(RestreamerError::Frame)?)
506 .await?;
507 }
508 None => break,
509 }
510 }
511 read = stream.read(&mut buf[filled..]) => {
512 let n = read?;
513 if n == 0 {
514 break;
515 }
516 filled += n;
517 while filled > 0 {
518 match Frame::decode(&buf[..filled]) {
519 Ok((frame, consumed)) => {
520 buf.copy_within(consumed..filled, 0);
521 filled -= consumed;
522 match frame.frame_type {
523 FrameType::Open => {
524 let open = frame.parse_open().map_err(RestreamerError::Frame)?;
525 if let Some(prev) = node_id.replace(open.node_id.clone()) {
526 rs.peers().unregister_if(&prev, &out_tx);
527 }
528 rs.peers().register(open.node_id.clone(), out_tx.clone());
529 rs.open_channel(&open.node_id, frame.channel_id, open.channel_kind);
530 let credit = rs.window_credit(&open.node_id, frame.channel_id);
531 let win = Frame::window(frame.channel_id, credit);
532 stream
533 .write_all(&win.encode().map_err(RestreamerError::Frame)?)
534 .await?;
535 }
536 FrameType::Data => {
537 let Some(ref nid) = node_id else {
538 let go = Frame::goaway(1);
539 stream
540 .write_all(&go.encode().map_err(RestreamerError::Frame)?)
541 .await?;
542 return Ok(());
543 };
544 match rs.admit_from_node(nid, frame.channel_id, frame.payload)? {
545 EnqueueOutcome::Accepted { window_credit } => {
546 let win = Frame::window(frame.channel_id, window_credit);
547 stream
548 .write_all(
549 &win.encode().map_err(RestreamerError::Frame)?,
550 )
551 .await?;
552 }
553 EnqueueOutcome::Backpressure => {
554 tracing::warn!(
559 target: "restreamer",
560 node = %nid,
561 channel = frame.channel_id,
562 "restreamer credit exhausted; dropping node→core frame (Noise may desync)"
563 );
564 let win = Frame::window(frame.channel_id, 0);
565 stream
566 .write_all(
567 &win.encode().map_err(RestreamerError::Frame)?,
568 )
569 .await?;
570 }
571 }
572 }
573 FrameType::JournalReply => {
574 if let Some(ref nid) = node_id {
575 let body = JournalReplyBody::decode(&frame.payload)
576 .map_err(RestreamerError::Frame)?;
577 rs.peers().deliver_reply(nid, body);
578 }
579 }
580 FrameType::Ping => {
581 let pong = Frame::pong(frame.seq);
582 stream
583 .write_all(&pong.encode().map_err(RestreamerError::Frame)?)
584 .await?;
585 }
586 FrameType::Close | FrameType::GoAway => {
587 if let Some(ref nid) = node_id {
588 rs.peers().unregister_if(nid, &out_tx);
589 }
590 return Ok(());
591 }
592 _ => {}
593 }
594 }
595 Err(FrameError::Truncated) => break,
596 Err(err) => return Err(RestreamerError::Frame(err)),
597 }
598 }
599 if filled == buf.len() {
600 grow_read_buf(&mut buf, filled).map_err(RestreamerError::Frame)?;
605 }
606 }
607 }
608 }
609 if let Some(ref nid) = node_id {
610 rs.peers().unregister_if(nid, &out_tx);
611 }
612 Ok(())
613}
614
615async fn handle_core_drain(
616 mut stream: TcpStream,
617 rs: Restreamer,
618 expected_token: &str,
619) -> Result<(), RestreamerError> {
620 let mut auth = Vec::with_capacity(128);
622 loop {
623 let mut byte = [0u8; 1];
624 let n = stream.read(&mut byte).await?;
625 if n == 0 {
626 return Err(RestreamerError::Config("drain auth closed"));
627 }
628 if byte[0] == b'\n' {
629 break;
630 }
631 auth.push(byte[0]);
632 if auth.len() > 512 {
633 return Err(RestreamerError::Config("drain auth too long"));
634 }
635 }
636 if !tokens_equal(&auth, expected_token.as_bytes()) {
637 let go = Frame::goaway(403);
638 let _ = stream.write_all(&go.encode().unwrap_or_default()).await;
639 return Err(RestreamerError::Config("drain auth refused"));
640 }
641
642 let mut buf = vec![0u8; 64 * 1024];
643 let mut filled = 0usize;
644 let mut selected: Option<(String, u32)> = None;
646 let mut sent_through: u64 = 0;
648 let mut resumed = false;
650 let uplink_ready = rs.uplink_notify();
651 loop {
652 let woken = uplink_ready.notified();
654 tokio::pin!(woken);
655 woken.as_mut().enable();
656 if resumed {
658 if let Some((ref nid, ch)) = selected {
659 let envelopes = rs.buffered_after(nid, ch, sent_through);
660 for env in envelopes {
661 stream
662 .write_all(&env.encode().map_err(RestreamerError::Frame)?)
663 .await?;
664 sent_through = sent_through.max(env.frame.seq);
665 }
666 }
667 }
668
669 let read = tokio::select! {
672 r = stream.read(&mut buf[filled..]) => Ok(r),
673 _ = &mut woken => continue,
674 _ = tokio::time::sleep(std::time::Duration::from_millis(50)) => Err(()),
675 };
676 match read {
677 Ok(Ok(0)) => break,
678 Ok(Ok(n)) => {
679 filled += n;
680 while filled > 0 {
681 match Frame::decode(&buf[..filled]) {
682 Ok((frame, consumed)) => {
683 buf.copy_within(consumed..filled, 0);
684 filled -= consumed;
685 match frame.frame_type {
686 FrameType::Open => {
687 let open =
688 frame.parse_open().map_err(RestreamerError::Frame)?;
689 selected = Some((open.node_id, frame.channel_id));
690 sent_through = 0;
691 resumed = false;
692 }
693 FrameType::Ack => {
694 if let Some((ref nid, ch)) = selected {
695 rs.ack(nid, ch, frame.seq);
696 }
697 }
698 FrameType::Resume => {
699 if let Some((ref nid, ch)) = selected {
700 let after = frame.seq;
701 let envelopes = rs.resume_with_uplink(nid, ch, after).await?;
702 for env in envelopes {
703 stream
704 .write_all(
705 &env.encode().map_err(RestreamerError::Frame)?,
706 )
707 .await?;
708 sent_through = sent_through.max(env.frame.seq);
709 }
710 sent_through = sent_through.max(after);
712 resumed = true;
713 }
714 }
715 FrameType::Data => {
716 if let Some((ref nid, ch)) = selected {
718 let down = Frame::data(ch, frame.seq, frame.payload)
719 .map_err(RestreamerError::Frame)?;
720 if let Err(err) = rs.peers().send_to(nid, down).await {
721 tracing::debug!(error = %err, "downlink dropped");
722 }
723 }
724 }
725 FrameType::Ping => {
726 let pong = Frame::pong(frame.seq);
727 stream
728 .write_all(
729 &pong.encode().map_err(RestreamerError::Frame)?,
730 )
731 .await?;
732 }
733 FrameType::Close | FrameType::GoAway => return Ok(()),
734 _ => {}
735 }
736 }
737 Err(FrameError::Truncated) => break,
738 Err(err) => return Err(RestreamerError::Frame(err)),
739 }
740 }
741 if filled == buf.len() {
742 grow_read_buf(&mut buf, filled).map_err(RestreamerError::Frame)?;
743 }
744 }
745 Ok(Err(e)) => return Err(e.into()),
746 Err(_) => {
747 }
749 }
750 }
751 Ok(())
752}
753
754fn tokens_equal(a: &[u8], b: &[u8]) -> bool {
755 if a.len() != b.len() {
756 return false;
757 }
758 let mut diff = 0u8;
759 for (x, y) in a.iter().zip(b.iter()) {
760 diff |= x ^ y;
761 }
762 diff == 0
763}
764
765pub struct DrainClient {
767 addr: SocketAddr,
768 token: String,
769}
770
771impl DrainClient {
772 pub fn new(addr: SocketAddr, token: impl Into<String>) -> Self {
773 Self {
774 addr,
775 token: token.into(),
776 }
777 }
778
779 pub async fn connect(&self) -> Result<DrainSession, RestreamerError> {
780 let mut stream = TcpStream::connect(self.addr).await?;
781 let _ = stream.set_nodelay(true);
782 stream.write_all(self.token.as_bytes()).await?;
783 stream.write_all(b"\n").await?;
784 Ok(DrainSession { stream })
785 }
786}
787
788pub struct DrainSession {
789 stream: TcpStream,
790}
791
792impl DrainSession {
793 pub async fn open(&mut self, node_id: &str, channel_id: u32, kind: ChannelKind) -> Result<(), RestreamerError> {
794 let frame = Frame::open(
795 channel_id,
796 &OpenPayload {
797 channel_kind: kind,
798 node_id: node_id.to_owned(),
799 },
800 )
801 .map_err(RestreamerError::Frame)?;
802 self.stream
803 .write_all(&frame.encode().map_err(RestreamerError::Frame)?)
804 .await?;
805 Ok(())
806 }
807
808 pub async fn resume(&mut self, after: u64) -> Result<(), RestreamerError> {
809 let frame = Frame::resume(after);
810 self.stream
811 .write_all(&frame.encode().map_err(RestreamerError::Frame)?)
812 .await?;
813 Ok(())
814 }
815
816 pub async fn ack(&mut self, seq: u64) -> Result<(), RestreamerError> {
817 let frame = Frame::ack(seq);
818 self.stream
819 .write_all(&frame.encode().map_err(RestreamerError::Frame)?)
820 .await?;
821 Ok(())
822 }
823
824 pub async fn recv_envelope(
825 &mut self,
826 deadline: std::time::Duration,
827 ) -> Result<Option<DrainEnvelope>, RestreamerError> {
828 let mut buf = vec![0u8; 64 * 1024];
829 let n = match tokio::time::timeout(deadline, self.stream.read(&mut buf)).await {
830 Ok(Ok(0)) => return Ok(None),
831 Ok(Ok(n)) => n,
832 Ok(Err(e)) => return Err(e.into()),
833 Err(_) => return Ok(None),
834 };
835 let (env, _) = DrainEnvelope::decode(&buf[..n]).map_err(RestreamerError::Frame)?;
836 Ok(Some(env))
837 }
838}
839
840#[cfg(test)]
841mod tests {
842 use super::*;
843 use crate::restreamer::crypto::{core_open_inner, noise_session_seal, HqRecipientSecret};
844 use crate::restreamer::journal::MemoryJournal;
845 use hatchery_core::ChannelKind;
846 use std::time::Duration;
847
848 fn token() -> String {
849 "test-core-token-32bytes-minimum!".to_owned()
850 }
851
852 #[tokio::test]
853 async fn core_drains_and_acks_without_c2_push() {
854 let journal = Arc::new(MemoryJournal::new());
855 let rs = Restreamer::new(
856 super::super::buffer::BufferLimits {
857 max_events: 100,
858 max_bytes: 1_000_000,
859 max_age: Duration::from_secs(60),
860 },
861 journal,
862 token(),
863 None,
864 );
865 rs.open_channel("node-a", 1, ChannelKind::M4a);
866 assert!(matches!(
867 rs.admit_from_node("node-a", 1, b"cipher-1".to_vec()).unwrap(),
868 EnqueueOutcome::Accepted { .. }
869 ));
870 assert!(matches!(
871 rs.admit_from_node("node-a", 1, b"cipher-2".to_vec()).unwrap(),
872 EnqueueOutcome::Accepted { .. }
873 ));
874 let batch = rs.buffered_after("node-a", 1, 0);
875 assert_eq!(batch.len(), 2);
876 assert_eq!(batch[0].frame.payload, b"cipher-1");
877 rs.ack("node-a", 1, 1);
878 let batch = rs.buffered_after("node-a", 1, 0);
879 assert_eq!(batch.len(), 1);
880 assert_eq!(batch[0].frame.seq, 2);
881 assert!(!rs.ever_pushed_to_core());
882 }
883
884 #[tokio::test]
885 async fn live_drain_session_resume_and_ack() {
886 let journal = Arc::new(MemoryJournal::new());
887 for seq in 1..=5u64 {
889 journal.append("node-a", 1, seq, format!("j{seq}").into_bytes());
890 }
891 let rs = Restreamer::new(
892 super::super::buffer::BufferLimits {
893 max_events: 2,
894 max_bytes: 1024,
895 max_age: Duration::from_secs(60),
896 },
897 journal.clone(),
898 token(),
899 None,
900 );
901 rs.admit_from_node("node-a", 1, b"b1".to_vec()).unwrap();
904 rs.admit_from_node("node-a", 1, b"b2".to_vec()).unwrap();
905 rs.ack("node-a", 1, 2);
907 rs.admit_from_node("node-a", 1, b"b3".to_vec()).unwrap();
909 rs.admit_from_node("node-a", 1, b"b4".to_vec()).unwrap();
910
911 let journal2 = Arc::new(MemoryJournal::new());
915 for seq in 1..=4u64 {
916 journal2.append("node-b", 1, seq, format!("evt-{seq}").into_bytes());
917 }
918 let rs2 = Restreamer::new(
919 super::super::buffer::BufferLimits {
920 max_events: 10,
921 max_bytes: 10_000,
922 max_age: Duration::from_secs(60),
923 },
924 journal2,
925 token(),
926 None,
927 );
928 for i in 1..=4u8 {
930 rs2.admit_from_node("node-b", 1, vec![i]).unwrap();
931 }
932 rs2.ack("node-b", 1, 4);
933 assert!(rs2.buffered_after("node-b", 1, 0).is_empty());
934 let recovered = rs2.resume("node-b", 1, 0).unwrap();
935 assert_eq!(recovered.len(), 4);
936 assert_eq!(recovered[0].frame.payload, b"evt-1");
937 assert_eq!(recovered[3].frame.seq, 4);
938 assert!(!rs2.ever_pushed_to_core());
939 }
940
941 #[tokio::test]
942 async fn tcp_drain_end_to_end() {
943 let journal = Arc::new(MemoryJournal::new());
944 let rs = Restreamer::new(
945 super::super::buffer::BufferLimits::default(),
946 journal,
947 token(),
948 None,
949 );
950 let node_l = TcpListener::bind("127.0.0.1:0").await.unwrap();
951 let drain_l = TcpListener::bind("127.0.0.1:0").await.unwrap();
952 let node_addr = node_l.local_addr().unwrap();
953 let drain_addr = drain_l.local_addr().unwrap();
954 drop(node_l);
955 drop(drain_l);
956 let (shutdown_tx, shutdown_rx) = watch::channel(false);
957 let cfg = RestreamerConfig {
958 node_listen: node_addr,
959 drain_listen: drain_addr,
960 buffer: super::super::buffer::BufferLimits::default(),
961 core_token: token(),
962 };
963 let serve_rs = rs.clone();
964 let handle = tokio::spawn(async move { serve(cfg, serve_rs, shutdown_rx).await });
965
966 tokio::time::sleep(Duration::from_millis(20)).await;
968
969 let mut node = TcpStream::connect(node_addr).await.unwrap();
971 let open = Frame::open(
972 1,
973 &OpenPayload {
974 channel_kind: ChannelKind::G4aEvents,
975 node_id: "node-a".into(),
976 },
977 )
978 .unwrap();
979 node.write_all(&open.encode().unwrap()).await.unwrap();
980 let mut rb = [0u8; 64];
982 let _ = node.read(&mut rb).await.unwrap();
983 let data = Frame::data(1, 0, b"opaque-event".to_vec()).unwrap();
984 node.write_all(&data.encode().unwrap()).await.unwrap();
986 let _ = node.read(&mut rb).await.unwrap();
987
988 let client = DrainClient::new(drain_addr, token());
990 let mut session = client.connect().await.unwrap();
991 session
992 .open("node-a", 1, ChannelKind::G4aEvents)
993 .await
994 .unwrap();
995 session.resume(0).await.unwrap();
996 let env = session
997 .recv_envelope(Duration::from_secs(2))
998 .await
999 .unwrap()
1000 .expect("envelope");
1001 assert_eq!(env.node_id, "node-a");
1002 assert_eq!(env.frame.payload, b"opaque-event");
1003 session.ack(env.frame.seq).await.unwrap();
1004
1005 let _ = shutdown_tx.send(true);
1006 let _ = handle.await;
1007 assert!(!rs.ever_pushed_to_core());
1008 }
1009
1010 #[test]
1011 fn nested_crypto_on_admit_path() {
1012 let hq = HqRecipientSecret::from_seed([11u8; 32]);
1013 let keys = RestreamerKeys::from_seeds([13u8; 32], hq.public_bytes().0);
1014 let journal = Arc::new(MemoryJournal::new());
1015 let rs = Restreamer::new(
1016 super::super::buffer::BufferLimits::default(),
1017 journal,
1018 token(),
1019 Some(keys.clone()),
1020 );
1021 let sealed = noise_session_seal(&keys, &hq, b"visible-only-to-core").unwrap();
1022 rs.admit_from_node("node-a", 1, sealed.outer).unwrap();
1023 let env = &rs.buffered_after("node-a", 1, 0)[0];
1024 assert!(!env
1025 .frame
1026 .payload
1027 .windows(b"visible-only-to-core".len())
1028 .any(|w| w == b"visible-only-to-core"));
1029 let mut hq_tx = sealed.hq_transport;
1030 let plain = core_open_inner(&mut hq_tx, &env.frame.payload).unwrap();
1031 assert_eq!(plain, b"visible-only-to-core");
1032 }
1033
1034 #[test]
1035 fn open_resets_credit_so_reopen_gets_positive_window() {
1036 let rs = Restreamer::new(
1039 super::super::buffer::BufferLimits::default(),
1040 Arc::new(MemoryJournal::new()),
1041 token(),
1042 None,
1043 );
1044 {
1046 let mut g = rs.inner.lock().expect("lock");
1047 g.buffer = ReplayBuffer::new(super::super::buffer::BufferLimits::default())
1048 .with_default_credit(2);
1049 }
1050 let now = Instant::now();
1051 for _ in 0..2 {
1052 let _ = rs.admit_from_node("box", 3, b"x".to_vec()).unwrap();
1053 let _ = now;
1054 }
1055 loop {
1057 match rs.admit_from_node("box", 3, b"y".to_vec()).unwrap() {
1058 EnqueueOutcome::Backpressure => break,
1059 EnqueueOutcome::Accepted { window_credit } if window_credit == 0 => break,
1060 _ => {}
1061 }
1062 }
1063 assert_eq!(rs.window_credit("box", 3), 0);
1064 rs.open_channel("box", 3, ChannelKind::G4aControl);
1065 assert!(rs.window_credit("box", 3) > 0, "re-OPEN must restore WINDOW credit");
1066 }
1067
1068 #[test]
1070 fn parallel_channel_opens_each_get_fresh_credit() {
1071 let rs = Restreamer::new(
1072 super::super::buffer::BufferLimits::default(),
1073 Arc::new(MemoryJournal::new()),
1074 token(),
1075 None,
1076 );
1077 {
1078 let mut g = rs.inner.lock().expect("lock");
1079 g.buffer = ReplayBuffer::new(super::super::buffer::BufferLimits::default())
1080 .with_default_credit(3);
1081 }
1082 for ch in 1u32..=20 {
1083 for _ in 0..3 {
1084 let _ = rs.admit_from_node("box", ch, b"z".to_vec());
1085 }
1086 assert_eq!(rs.window_credit("box", ch), 0);
1087 }
1088 for ch in 1u32..=20 {
1089 rs.open_channel("box", ch, ChannelKind::G4aPty);
1090 assert!(rs.window_credit("box", ch) > 0, "ch {ch}");
1091 }
1092 }
1093
1094 #[tokio::test]
1095 async fn uplink_journal_req_served_from_file_journal() {
1096 use crate::restreamer::FileJournal;
1097 use hatchery_core::FrameType;
1098
1099 let dir = std::env::temp_dir().join(format!("upj-{}", std::process::id()));
1100 let _ = std::fs::remove_dir_all(&dir);
1101 let fj = FileJournal::open(dir.join("n.journal")).unwrap();
1102 for i in 0..5u8 {
1103 fj.append(1, &[i]).unwrap();
1104 }
1105 let journal = Arc::new(MemoryJournal::new());
1106 let rs = Restreamer::new(
1107 super::super::buffer::BufferLimits::default(),
1108 journal,
1109 token(),
1110 None,
1111 );
1112 let node_l = TcpListener::bind("127.0.0.1:0").await.unwrap();
1113 let drain_l = TcpListener::bind("127.0.0.1:0").await.unwrap();
1114 let node_addr = node_l.local_addr().unwrap();
1115 let drain_addr = drain_l.local_addr().unwrap();
1116 drop(node_l);
1117 drop(drain_l);
1118 let (shutdown_tx, shutdown_rx) = watch::channel(false);
1119 let cfg = RestreamerConfig {
1120 node_listen: node_addr,
1121 drain_listen: drain_addr,
1122 buffer: super::super::buffer::BufferLimits::default(),
1123 core_token: token(),
1124 };
1125 let serve_rs = rs.clone();
1126 let handle = tokio::spawn(async move { serve(cfg, serve_rs, shutdown_rx).await });
1127 tokio::time::sleep(Duration::from_millis(20)).await;
1128
1129 let fj_node = fj.clone();
1130 let node_task = tokio::spawn(async move {
1131 let mut node = TcpStream::connect(node_addr).await.unwrap();
1132 let open = Frame::open(
1133 1,
1134 &OpenPayload {
1135 channel_kind: ChannelKind::G4aEvents,
1136 node_id: "node-a".into(),
1137 },
1138 )
1139 .unwrap();
1140 node.write_all(&open.encode().unwrap()).await.unwrap();
1141 let mut rb = vec![0u8; 64 * 1024];
1142 let mut filled = 0usize;
1143 loop {
1144 let n = node.read(&mut rb[filled..]).await.unwrap();
1145 if n == 0 {
1146 break;
1147 }
1148 filled += n;
1149 while filled > 0 {
1150 match Frame::decode(&rb[..filled]) {
1151 Ok((frame, consumed)) => {
1152 rb.copy_within(consumed..filled, 0);
1153 filled -= consumed;
1154 if frame.frame_type == FrameType::JournalReq {
1155 let limit = frame.journal_req_limit().unwrap_or(64) as usize;
1156 let body = fj_node
1157 .reply_body(frame.channel_id, frame.seq, limit)
1158 .unwrap();
1159 let reply = Frame::journal_reply(frame.channel_id, &body).unwrap();
1160 node.write_all(&reply.encode().unwrap()).await.unwrap();
1161 }
1162 }
1163 Err(FrameError::Truncated) => break,
1164 Err(e) => panic!("{e}"),
1165 }
1166 }
1167 }
1168 });
1169
1170 for _ in 0..50 {
1171 if rs.peers().is_registered("node-a") {
1172 break;
1173 }
1174 tokio::time::sleep(Duration::from_millis(10)).await;
1175 }
1176 assert!(rs.peers().is_registered("node-a"));
1177 let body = rs
1178 .peers()
1179 .request_journal("node-a", 1, 2, 10, Duration::from_secs(2))
1180 .await
1181 .unwrap();
1182 assert_eq!(
1183 body.entries.iter().map(|e| e.seq).collect::<Vec<_>>(),
1184 vec![3, 4, 5]
1185 );
1186 assert_eq!(body.entries[0].payload, &[2]);
1187
1188 let _ = shutdown_tx.send(true);
1189 node_task.abort();
1190 let _ = handle.await;
1191 let _ = std::fs::remove_dir_all(&dir);
1192 }
1193}