Skip to main content

orbit_stream/exchange/
pair.rs

1use std::fmt;
2use std::ops::{Deref, DerefMut};
3use std::str::FromStr;
4use std::sync::Arc;
5use std::time::Duration;
6
7use orbit_core::{Fleet, NodeId};
8
9use super::arena::Publication;
10use super::protocol::CONTROL_FRAME_BYTES;
11use super::{
12    ChunkDescriptor, ChunkGeometry, ChunkPlan, ControlEvent, ExchangeId, Flow,
13    PayloadArena, PayloadArenaSpec, PayloadChunk, ResetCode,
14};
15use crate::{
16    Error, Incarnation, ReadHalf, Result, Side, StreamSpec, Streams, Ticket,
17    WriteHalf,
18};
19
20/// How an exchange budgets payload memory for its two directions.
21///
22/// The shared form is the compact generic default. Directional arenas are for
23/// adapters whose two flows have deliberately different budgets (for example
24/// an upload and a streamed response) or must not consume each other's
25/// credit.
26#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
27pub enum ExchangePayloadSpec {
28    Shared(PayloadArenaSpec),
29    Directional { a_to_b: PayloadArenaSpec, b_to_a: PayloadArenaSpec },
30}
31
32impl ExchangePayloadSpec {
33    /// Physical payload arena used by one byte-flow direction.
34    pub const fn for_flow(
35        self,
36        flow: Flow,
37    ) -> PayloadArenaSpec {
38        match (self, flow) {
39            (Self::Shared(payload), _) => payload,
40            (Self::Directional { a_to_b, .. }, Flow::AtoB) => a_to_b,
41            (Self::Directional { b_to_a, .. }, Flow::BtoA) => b_to_a,
42        }
43    }
44}
45
46/// The physical resources used by an exchange table.
47#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
48pub struct ExchangeSpec {
49    /// One lossless, bounded control table. Its two byte directions carry
50    /// fixed-size typed events, never application payload bytes.
51    pub control: StreamSpec,
52    /// Payload memory may be shared by both directions or split into two
53    /// independently bounded arenas.
54    pub payload: ExchangePayloadSpec,
55}
56
57impl ExchangeSpec {
58    pub const fn new(
59        control: StreamSpec,
60        payload: PayloadArenaSpec,
61    ) -> Self {
62        Self { control, payload: ExchangePayloadSpec::Shared(payload) }
63    }
64
65    pub const fn directional(
66        control: StreamSpec,
67        a_to_b: PayloadArenaSpec,
68        b_to_a: PayloadArenaSpec,
69    ) -> Self {
70        Self {
71            control,
72            payload: ExchangePayloadSpec::Directional { a_to_b, b_to_a },
73        }
74    }
75
76    /// Physical payload arena used by one byte-flow direction.
77    pub const fn payload_for(
78        self,
79        flow: Flow,
80    ) -> PayloadArenaSpec {
81        self.payload.for_flow(flow)
82    }
83
84    /// Physical chunk geometry used by one byte-flow direction.
85    pub const fn chunk_geometry(
86        self,
87        flow: Flow,
88    ) -> Option<ChunkGeometry> {
89        self.payload_for(flow).chunk_geometry()
90    }
91
92    fn validate(self) -> Result<()> {
93        if self.control.buffer_bytes < CONTROL_FRAME_BYTES {
94            return Err(Error::Malformed(format!(
95                "exchange control ring is {} bytes; one control frame is {CONTROL_FRAME_BYTES}",
96                self.control.buffer_bytes
97            )));
98        }
99        match self.payload {
100            ExchangePayloadSpec::Shared(payload)
101                if self.control.kind == payload.kind =>
102            {
103                return Err(Error::Malformed(
104                    "exchange control and payload need distinct SHM kinds"
105                        .to_owned(),
106                ));
107            }
108            ExchangePayloadSpec::Directional { a_to_b, b_to_a }
109                if self.control.kind == a_to_b.kind
110                    || self.control.kind == b_to_a.kind
111                    || a_to_b.kind == b_to_a.kind =>
112            {
113                return Err(Error::Malformed(
114                    "exchange control and directional payloads need three distinct SHM kinds"
115                        .to_owned()
116                ));
117            }
118            _ => {}
119        }
120        Ok(())
121    }
122}
123
124/// The client representative's capability to open the other side of an
125/// exchange. It may travel through an Orbit offer or any setup channel.
126#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
127pub struct ExchangeTicket(Ticket);
128
129impl ExchangeTicket {
130    pub const fn from_stream_ticket(ticket: Ticket) -> Self {
131        Self(ticket)
132    }
133
134    pub const fn stream_ticket(self) -> Ticket {
135        self.0
136    }
137
138    pub const fn id(self) -> ExchangeId {
139        ExchangeId::from_stream_id(self.0.id)
140    }
141}
142
143impl fmt::Display for ExchangeTicket {
144    fn fmt(
145        &self,
146        f: &mut fmt::Formatter<'_>,
147    ) -> fmt::Result {
148        self.0.fmt(f)
149    }
150}
151
152impl FromStr for ExchangeTicket {
153    type Err = Error;
154
155    fn from_str(text: &str) -> Result<Self> {
156        text.parse().map(Self)
157    }
158}
159
160#[derive(Clone)]
161enum PayloadArenas {
162    Shared(PayloadArena),
163    Directional { a_to_b: PayloadArena, b_to_a: PayloadArena },
164}
165
166impl PayloadArenas {
167    fn for_flow(
168        &self,
169        flow: Flow,
170    ) -> PayloadArena {
171        match (self, flow) {
172            (Self::Shared(arena), _) => arena.clone(),
173            (Self::Directional { a_to_b, .. }, Flow::AtoB) => a_to_b.clone(),
174            (Self::Directional { b_to_a, .. }, Flow::BtoA) => b_to_a.clone(),
175        }
176    }
177
178    fn node_dead(
179        &self,
180        node: NodeId,
181        incarnation: Incarnation,
182    ) {
183        match self {
184            Self::Shared(arena) => arena.node_dead(node, incarnation),
185            Self::Directional { a_to_b, b_to_a } => {
186                a_to_b.node_dead(node, incarnation);
187                b_to_a.node_dead(node, incarnation);
188            }
189        }
190    }
191
192    fn reset_all(&self) {
193        match self {
194            Self::Shared(arena) => arena.reset_all(),
195            Self::Directional { a_to_b, b_to_a } => {
196                a_to_b.reset_all();
197                b_to_a.reset_all();
198            }
199        }
200    }
201
202    #[cfg(unix)]
203    fn unlink(&self) -> Result<()> {
204        match self {
205            Self::Shared(arena) => arena.unlink(),
206            Self::Directional { a_to_b, b_to_a } => {
207                a_to_b.unlink()?;
208                b_to_a.unlink()
209            }
210        }
211    }
212}
213
214/// One fleet's exchange transport: one duplex control table and either one
215/// shared payload arena or one independently bounded arena per direction.
216#[derive(Clone)]
217pub struct Exchanges {
218    control: Streams,
219    payload: PayloadArenas,
220}
221
222impl Exchanges {
223    pub fn open(
224        fleet: Arc<Fleet>,
225        incarnation: Incarnation,
226        spec: ExchangeSpec,
227    ) -> Result<Self> {
228        spec.validate()?;
229        let control =
230            Streams::with_spec(Arc::clone(&fleet), incarnation, spec.control)?;
231        let payload = match spec.payload {
232            ExchangePayloadSpec::Shared(payload) => PayloadArenas::Shared(
233                PayloadArena::open(fleet, incarnation, payload)?,
234            ),
235            ExchangePayloadSpec::Directional { a_to_b, b_to_a } => {
236                PayloadArenas::Directional {
237                    a_to_b: PayloadArena::open(
238                        Arc::clone(&fleet),
239                        incarnation,
240                        a_to_b,
241                    )?,
242                    b_to_a: PayloadArena::open(fleet, incarnation, b_to_a)?,
243                }
244            }
245        };
246        Ok(Self { control, payload })
247    }
248
249    /// Create side A. Its sender writes A-to-B and its receiver reads B-to-A.
250    /// The ticket opens side B.
251    pub fn create(&self) -> Result<(ExchangeEndpoint, ExchangeTicket)> {
252        let (endpoint, ticket) = self.control.create()?;
253        Ok((self.endpoint(endpoint), ExchangeTicket(ticket)))
254    }
255
256    /// Whether side A can currently reserve its first flow payload.
257    /// Session transports use this before reserving a remote resource so
258    /// payload backpressure does not become an abandoned resource lease.
259    pub fn create_payload_available(
260        &self,
261        payload_len: usize,
262    ) -> Result<bool> {
263        self.payload.for_flow(Flow::AtoB).is_available(payload_len)
264    }
265
266    /// Task readiness for side A's first flow payload credit.
267    pub fn poll_create_payload_available(
268        &self,
269        payload_len: usize,
270        cx: &mut std::task::Context<'_>,
271    ) -> std::task::Poll<Result<()>> {
272        self.payload.for_flow(Flow::AtoB).poll_available(payload_len, cx)
273    }
274
275    /// Open side B. Its sender writes B-to-A and its receiver reads A-to-B.
276    /// Both directions have independent lifecycle and may start in either order.
277    pub fn open_peer(
278        &self,
279        ticket: ExchangeTicket,
280    ) -> Result<ExchangeEndpoint> {
281        let endpoint = self.control.open(ticket.0)?;
282        Ok(self.endpoint(endpoint))
283    }
284
285    fn endpoint(
286        &self,
287        endpoint: crate::Endpoint,
288    ) -> ExchangeEndpoint {
289        let id = ExchangeId::from_stream_id(endpoint.id());
290        let (send_flow, receive_flow) = match endpoint.side() {
291            Side::A => (Flow::AtoB, Flow::BtoA),
292            Side::B => (Flow::BtoA, Flow::AtoB),
293        };
294        let (read, write) = endpoint.split();
295        ExchangeEndpoint {
296            sender: Sender(Producer::new(
297                id,
298                send_flow,
299                self.payload.for_flow(send_flow),
300                write,
301            )),
302            receiver: Receiver(Consumer::new(
303                id,
304                receive_flow,
305                self.payload.for_flow(receive_flow),
306                read,
307            )),
308        }
309    }
310
311    pub fn offer(
312        &self,
313        ticket: ExchangeTicket,
314        to: NodeId,
315    ) -> Result<()> {
316        self.control.offer(ticket.0, to)
317    }
318
319    pub fn take_offer(&self) -> Option<ExchangeTicket> {
320        self.control.take_offer().map(ExchangeTicket)
321    }
322
323    /// Park until an exchange is offered to this node.
324    pub fn blocking_take_offer(&self) -> Result<ExchangeTicket> {
325        self.control.blocking_take_offer().map(ExchangeTicket)
326    }
327
328    /// Wait at most `timeout` for an offered exchange.
329    #[cfg(any(
330        target_os = "linux",
331        target_os = "freebsd",
332        target_os = "macos"
333    ))]
334    pub fn take_offer_timeout(
335        &self,
336        timeout: Duration,
337    ) -> Result<Option<ExchangeTicket>> {
338        self.control
339            .take_offer_timeout(timeout)
340            .map(|ticket| ticket.map(ExchangeTicket))
341    }
342
343    /// Task readiness for the next offered exchange.
344    pub fn poll_take_offer(
345        &self,
346        cx: &mut std::task::Context<'_>,
347    ) -> std::task::Poll<Result<ExchangeTicket>> {
348        match self.control.poll_take_offer(cx) {
349            std::task::Poll::Ready(result) => {
350                std::task::Poll::Ready(result.map(ExchangeTicket))
351            }
352            std::task::Poll::Pending => std::task::Poll::Pending,
353        }
354    }
355
356    pub fn control(&self) -> &Streams {
357        &self.control
358    }
359
360    /// Apply a confirmed process-death report to control ownership and every
361    /// payload arena. This is never inferred from a replacement attaching.
362    pub fn node_dead(
363        &self,
364        node: NodeId,
365        incarnation: Incarnation,
366    ) {
367        self.control.node_dead(node, incarnation);
368        self.payload.node_dead(node, incarnation);
369    }
370
371    /// Quiescent-owner maintenance only. Attaching a replacement process
372    /// never resets an exchange table or either payload arena implicitly.
373    pub fn reset_all(&self) {
374        self.control.reset_all();
375        self.payload.reset_all();
376    }
377
378    /// Remove the control and payload SHM names. Existing mappings remain
379    /// valid.
380    #[cfg(unix)]
381    pub fn unlink(&self) -> Result<()> {
382        self.control.unlink()?;
383        self.payload.unlink()
384    }
385}
386
387/// One held exchange side. A worker may hold endpoints on either side of many
388/// exchanges at once; sender and receiver are capabilities, not worker roles.
389pub struct ExchangeEndpoint {
390    sender: Sender,
391    receiver: Receiver,
392}
393
394impl ExchangeEndpoint {
395    pub fn id(&self) -> ExchangeId {
396        self.sender.id()
397    }
398
399    pub fn sender(&mut self) -> &mut Sender {
400        &mut self.sender
401    }
402
403    pub fn receiver(&mut self) -> &mut Receiver {
404        &mut self.receiver
405    }
406
407    pub fn split(self) -> (Sender, Receiver) {
408        (self.sender, self.receiver)
409    }
410}
411
412#[derive(Clone, Copy, Debug, Eq, PartialEq)]
413enum ProducerState {
414    New,
415    Open,
416    Terminal,
417}
418
419struct Producer {
420    exchange: ExchangeId,
421    flow: Flow,
422    arena: PayloadArena,
423    control: WriteHalf,
424    next_chunk: u64,
425    state: ProducerState,
426}
427
428impl Producer {
429    fn new(
430        exchange: ExchangeId,
431        flow: Flow,
432        arena: PayloadArena,
433        control: WriteHalf,
434    ) -> Self {
435        Self {
436            exchange,
437            flow,
438            arena,
439            control,
440            next_chunk: 1,
441            state: ProducerState::New,
442        }
443    }
444
445    fn id(&self) -> ExchangeId {
446        self.exchange
447    }
448
449    fn start(
450        &mut self,
451        metadata: Option<&[u8]>,
452    ) -> Result<()> {
453        if self.state != ProducerState::New {
454            return Err(Error::Malformed(
455                "flow start was already sent".to_owned(),
456            ));
457        }
458        if let Some(bytes) = metadata.filter(|bytes| !bytes.is_empty()) {
459            let mut pending = self.reserve_start(bytes.len())?;
460            pending.copy_from_slice(bytes);
461            pending.commit()?;
462        } else {
463            self.send(ControlEvent::Start {
464                exchange: self.exchange,
465                flow: self.flow,
466                metadata: None,
467            })?;
468            self.state = ProducerState::Open;
469        }
470        Ok(())
471    }
472
473    fn data(
474        &mut self,
475        payload: &[u8],
476    ) -> Result<ChunkDescriptor> {
477        let mut pending = self.reserve_data(payload.len())?;
478        pending.copy_from_slice(payload);
479        pending.commit()
480    }
481
482    fn reserve_start(
483        &mut self,
484        payload_len: usize,
485    ) -> Result<PendingStart<'_>> {
486        if self.state != ProducerState::New {
487            return Err(Error::Malformed(
488                "flow start was already sent".to_owned(),
489            ));
490        }
491        let publication = self.arena.reserve(payload_len)?;
492        Ok(PendingStart { producer: self, publication })
493    }
494
495    fn reserve_data(
496        &mut self,
497        payload_len: usize,
498    ) -> Result<PendingData<'_>> {
499        if self.state != ProducerState::Open {
500            return Err(Error::Malformed(
501                "flow data requires an open flow".to_owned(),
502            ));
503        }
504        let publication = self.arena.reserve(payload_len)?;
505        Ok(PendingData { producer: self, publication })
506    }
507
508    fn commit_start(
509        &mut self,
510        publication: Publication,
511    ) -> Result<ChunkDescriptor> {
512        let descriptor = self.descriptor(&publication);
513        publication.make_live();
514        self.send(ControlEvent::Start {
515            exchange: self.exchange,
516            flow: self.flow,
517            metadata: Some(descriptor),
518        })?;
519        publication.mark_published();
520        self.next_chunk += 1;
521        self.state = ProducerState::Open;
522        Ok(descriptor)
523    }
524
525    fn commit_data(
526        &mut self,
527        publication: Publication,
528    ) -> Result<ChunkDescriptor> {
529        let descriptor = self.descriptor(&publication);
530        publication.make_live();
531        self.send(ControlEvent::Data(descriptor))?;
532        publication.mark_published();
533        self.next_chunk += 1;
534        Ok(descriptor)
535    }
536
537    fn finish(&mut self) -> Result<()> {
538        if self.state != ProducerState::Open {
539            return Err(Error::Malformed(
540                "flow FIN requires an open flow".to_owned(),
541            ));
542        }
543        self.send(ControlEvent::Fin {
544            exchange: self.exchange,
545            flow: self.flow,
546        })?;
547        self.control.finish()?;
548        self.state = ProducerState::Terminal;
549        Ok(())
550    }
551
552    fn reset(
553        &mut self,
554        code: ResetCode,
555    ) -> Result<()> {
556        if self.state == ProducerState::Terminal {
557            return Err(Error::Closed);
558        }
559        self.send(ControlEvent::Reset {
560            exchange: self.exchange,
561            flow: self.flow,
562            code,
563        })?;
564        self.control.finish()?;
565        self.state = ProducerState::Terminal;
566        Ok(())
567    }
568
569    fn send(
570        &self,
571        event: ControlEvent,
572    ) -> Result<()> {
573        self.control.try_write_exact(&event.encode())
574    }
575
576    fn descriptor(
577        &self,
578        publication: &Publication,
579    ) -> ChunkDescriptor {
580        let (
581            generation,
582            first_slot,
583            slot_count,
584            payload_len,
585            arena_kind,
586            owner_node,
587        ) = publication.coordinates();
588        ChunkDescriptor::new(
589            self.exchange,
590            self.flow,
591            self.next_chunk,
592            generation,
593            first_slot,
594            slot_count,
595            payload_len,
596            arena_kind,
597            owner_node,
598        )
599    }
600}
601
602/// A flow-start payload reserved directly in the shared SHM arena. The bytes
603/// become immutable and visible only when `commit` sends the matching start
604/// event. Dropping this value returns the reserved slots.
605pub struct PendingStart<'a> {
606    producer: &'a mut Producer,
607    publication: Publication,
608}
609
610impl PendingStart<'_> {
611    pub fn as_mut_slice(&mut self) -> &mut [u8] {
612        self.publication.as_mut_slice()
613    }
614
615    pub fn commit(self) -> Result<ChunkDescriptor> {
616        self.producer.commit_start(self.publication)
617    }
618
619    /// Commit only the initialized prefix of a larger reservation. Any whole
620    /// trailing slots that prefix does not need return to the arena first.
621    pub fn commit_len(
622        mut self,
623        payload_len: usize,
624    ) -> Result<ChunkDescriptor> {
625        self.publication.truncate(payload_len)?;
626        self.producer.commit_start(self.publication)
627    }
628}
629
630impl Deref for PendingStart<'_> {
631    type Target = [u8];
632
633    fn deref(&self) -> &Self::Target {
634        self.publication.as_slice()
635    }
636}
637
638impl DerefMut for PendingStart<'_> {
639    fn deref_mut(&mut self) -> &mut Self::Target {
640        self.as_mut_slice()
641    }
642}
643
644/// A data chunk reserved directly in the shared SHM arena. One commit publishes
645/// the complete variable-sized chunk and one descriptor.
646pub struct PendingData<'a> {
647    producer: &'a mut Producer,
648    publication: Publication,
649}
650
651impl PendingData<'_> {
652    pub fn as_mut_slice(&mut self) -> &mut [u8] {
653        self.publication.as_mut_slice()
654    }
655
656    pub fn commit(self) -> Result<ChunkDescriptor> {
657        self.producer.commit_data(self.publication)
658    }
659
660    /// Commit only the initialized prefix of a larger reservation. This is
661    /// the direct-read path for producers that learn the byte count from IO.
662    pub fn commit_len(
663        mut self,
664        payload_len: usize,
665    ) -> Result<ChunkDescriptor> {
666        self.publication.truncate(payload_len)?;
667        self.producer.commit_data(self.publication)
668    }
669}
670
671impl Deref for PendingData<'_> {
672    type Target = [u8];
673
674    fn deref(&self) -> &Self::Target {
675        self.publication.as_slice()
676    }
677}
678
679impl DerefMut for PendingData<'_> {
680    fn deref_mut(&mut self) -> &mut Self::Target {
681        self.as_mut_slice()
682    }
683}
684
685#[derive(Clone, Copy, Debug, Eq, PartialEq)]
686enum ConsumerState {
687    New,
688    Open,
689    Terminal,
690}
691
692struct Consumer {
693    exchange: ExchangeId,
694    flow: Flow,
695    arena: PayloadArena,
696    control: ReadHalf,
697    next_chunk: u64,
698    state: ConsumerState,
699}
700
701impl Consumer {
702    fn new(
703        exchange: ExchangeId,
704        flow: Flow,
705        arena: PayloadArena,
706        control: ReadHalf,
707    ) -> Self {
708        Self {
709            exchange,
710            flow,
711            arena,
712            control,
713            next_chunk: 1,
714            state: ConsumerState::New,
715        }
716    }
717
718    fn id(&self) -> ExchangeId {
719        self.exchange
720    }
721
722    fn next(&mut self) -> Result<FlowEvent> {
723        if self.state == ConsumerState::Terminal {
724            return Err(Error::Closed);
725        }
726        let mut frame = [0_u8; CONTROL_FRAME_BYTES];
727        self.control.try_read_exact(&mut frame)?;
728        let event = ControlEvent::decode(&frame)
729            .map_err(|reason| Error::Malformed(reason.to_owned()))?;
730        if event.exchange() != self.exchange || event.flow() != self.flow {
731            return Err(Error::Malformed(
732                "control event belongs to another exchange flow".to_owned(),
733            ));
734        }
735        match event {
736            ControlEvent::Start { metadata, .. }
737                if self.state == ConsumerState::New =>
738            {
739                let metadata = metadata
740                    .map(|descriptor| self.take_chunk(descriptor))
741                    .transpose()?;
742                self.state = ConsumerState::Open;
743                Ok(FlowEvent::Start { metadata })
744            }
745            ControlEvent::Data(descriptor)
746                if self.state == ConsumerState::Open =>
747            {
748                let chunk = self.take_chunk(descriptor)?;
749                Ok(FlowEvent::Data(chunk))
750            }
751            ControlEvent::Fin { .. } if self.state == ConsumerState::Open => {
752                self.state = ConsumerState::Terminal;
753                Ok(FlowEvent::Fin)
754            }
755            ControlEvent::Reset { code, .. } => {
756                self.state = ConsumerState::Terminal;
757                Ok(FlowEvent::Reset(code))
758            }
759            _ => Err(Error::Malformed(
760                "control event violates the flow lifecycle".to_owned(),
761            )),
762        }
763    }
764
765    fn take_chunk(
766        &mut self,
767        descriptor: ChunkDescriptor,
768    ) -> Result<PayloadChunk> {
769        if descriptor.chunk_id() != self.next_chunk {
770            return Err(Error::Malformed(format!(
771                "flow expected chunk {}, received {}",
772                self.next_chunk,
773                descriptor.chunk_id()
774            )));
775        }
776        let chunk = self.arena.read(descriptor)?;
777        self.next_chunk += 1;
778        Ok(chunk)
779    }
780}
781
782/// The event program seen by a flow consumer.
783pub enum FlowEvent {
784    Start { metadata: Option<PayloadChunk> },
785    Data(PayloadChunk),
786    Fin,
787    Reset(ResetCode),
788}
789
790/// Typed callbacks for one flow. A handler decides what its events mean; the
791/// transport does not encode application sequencing policy.
792pub trait FlowHandler {
793    type Error;
794
795    fn on_start(
796        &mut self,
797        metadata: Option<PayloadChunk>,
798    ) -> std::result::Result<(), Self::Error>;
799    fn on_data(
800        &mut self,
801        chunk: PayloadChunk,
802    ) -> std::result::Result<(), Self::Error>;
803    fn on_fin(&mut self) -> std::result::Result<(), Self::Error>;
804    fn on_reset(
805        &mut self,
806        code: ResetCode,
807    ) -> std::result::Result<(), Self::Error>;
808}
809
810#[derive(Debug)]
811pub enum DispatchError<E> {
812    Transport(Error),
813    Handler(E),
814}
815
816impl<E> From<Error> for DispatchError<E> {
817    fn from(value: Error) -> Self {
818        Self::Transport(value)
819    }
820}
821
822fn dispatch<H: FlowHandler>(
823    consumer: &mut Consumer,
824    handler: &mut H,
825) -> std::result::Result<(), DispatchError<H::Error>> {
826    match consumer.next()? {
827        FlowEvent::Start { metadata } => handler.on_start(metadata),
828        FlowEvent::Data(chunk) => handler.on_data(chunk),
829        FlowEvent::Fin => handler.on_fin(),
830        FlowEvent::Reset(code) => handler.on_reset(code),
831    }
832    .map_err(DispatchError::Handler)
833}
834
835/// The capability to publish one endpoint's outbound flow.
836pub struct Sender(Producer);
837
838impl Sender {
839    pub fn id(&self) -> ExchangeId {
840        self.0.id()
841    }
842
843    pub fn flow(&self) -> Flow {
844        self.0.flow
845    }
846
847    /// Physical geometry of this sender's actual directional payload arena.
848    pub fn chunk_geometry(&self) -> ChunkGeometry {
849        self.0.arena.chunk_geometry()
850    }
851
852    /// Plan a known payload for this direction's actual arena geometry.
853    pub fn plan_chunks(
854        &self,
855        data_bytes: usize,
856    ) -> Result<ChunkPlan> {
857        self.0.arena.plan_chunks(data_bytes)
858    }
859
860    /// Plan this direction with a runtime-selected slot count.
861    pub fn plan_chunks_with(
862        &self,
863        data_bytes: usize,
864        slots_per_chunk: usize,
865    ) -> Result<ChunkPlan> {
866        self.0.arena.plan_chunks_with(data_bytes, slots_per_chunk)
867    }
868    pub fn start(
869        &mut self,
870        metadata: Option<&[u8]>,
871    ) -> Result<()> {
872        self.0.start(metadata)
873    }
874
875    /// Reserve start metadata directly in the shared payload arena. No start
876    /// event is visible until the returned guard is committed.
877    pub fn reserve_start(
878        &mut self,
879        payload_len: usize,
880    ) -> Result<PendingStart<'_>> {
881        self.0.reserve_start(payload_len)
882    }
883
884    pub fn data(
885        &mut self,
886        payload: &[u8],
887    ) -> Result<ChunkDescriptor> {
888        self.0.data(payload)
889    }
890
891    /// Reserve one variable-sized data chunk directly in the payload arena.
892    /// One commit emits one data descriptor.
893    pub fn reserve_data(
894        &mut self,
895        payload_len: usize,
896    ) -> Result<PendingData<'_>> {
897        self.0.reserve_data(payload_len)
898    }
899
900    pub fn finish(&mut self) -> Result<()> {
901        self.0.finish()
902    }
903
904    pub fn reset(
905        &mut self,
906        code: ResetCode,
907    ) -> Result<()> {
908        self.0.reset(code)
909    }
910
911    pub fn wait_writable(&self) -> Result<()> {
912        self.0.control.wait_writable_exact(CONTROL_FRAME_BYTES)
913    }
914
915    pub fn poll_writable(
916        &self,
917        cx: &mut std::task::Context<'_>,
918    ) -> std::task::Poll<Result<()>> {
919        self.0.control.poll_writable_exact(CONTROL_FRAME_BYTES, cx)
920    }
921
922    /// Wait for both one control frame and the payload credit needed by the
923    /// next publication. Use zero for FIN, RESET, or a start without metadata.
924    pub fn wait_ready(
925        &self,
926        payload_len: usize,
927    ) -> Result<()> {
928        self.0.control.wait_writable_exact(CONTROL_FRAME_BYTES)?;
929        if payload_len != 0 {
930            self.0.arena.wait_available(payload_len)?;
931        }
932        Ok(())
933    }
934
935    /// Task readiness for the same combined control and payload capacity.
936    /// Both sources register before `Pending` is returned.
937    pub fn poll_ready(
938        &self,
939        payload_len: usize,
940        cx: &mut std::task::Context<'_>,
941    ) -> std::task::Poll<Result<()>> {
942        let control =
943            self.0.control.poll_writable_exact(CONTROL_FRAME_BYTES, cx);
944        let payload = if payload_len == 0 {
945            std::task::Poll::Ready(Ok(()))
946        } else {
947            self.0.arena.poll_available(payload_len, cx)
948        };
949        match (control, payload) {
950            (std::task::Poll::Ready(Err(error)), _)
951            | (_, std::task::Poll::Ready(Err(error))) => {
952                std::task::Poll::Ready(Err(error))
953            }
954            (
955                std::task::Poll::Ready(Ok(())),
956                std::task::Poll::Ready(Ok(())),
957            ) => std::task::Poll::Ready(Ok(())),
958            _ => std::task::Poll::Pending,
959        }
960    }
961}
962
963/// The capability to consume the peer endpoint's outbound flow.
964pub struct Receiver(Consumer);
965
966impl Receiver {
967    pub fn id(&self) -> ExchangeId {
968        self.0.id()
969    }
970
971    pub fn flow(&self) -> Flow {
972        self.0.flow
973    }
974
975    pub fn try_next(&mut self) -> Result<FlowEvent> {
976        self.0.next()
977    }
978
979    pub fn dispatch<H: FlowHandler>(
980        &mut self,
981        handler: &mut H,
982    ) -> std::result::Result<(), DispatchError<H::Error>> {
983        dispatch(&mut self.0, handler)
984    }
985
986    pub fn wait_readable(&self) -> Result<()> {
987        self.0.control.wait_readable()
988    }
989
990    pub fn poll_readable(
991        &self,
992        cx: &mut std::task::Context<'_>,
993    ) -> std::task::Poll<Result<()>> {
994        self.0.control.poll_readable(cx)
995    }
996}
997
998#[cfg(test)]
999mod tests {
1000    use super::*;
1001
1002    fn exchanges() -> Exchanges {
1003        Exchanges::open(
1004            Arc::new(Fleet::join("exchange-pair-test", 2).expect("fleet")),
1005            Incarnation::new(1),
1006            ExchangeSpec::new(
1007                StreamSpec::new(230, 8, 512),
1008                PayloadArenaSpec::new(231, 8, 64),
1009            ),
1010        )
1011        .expect("exchanges")
1012    }
1013
1014    #[test]
1015    fn peer_can_send_before_the_first_flow_finishes() {
1016        let exchanges = exchanges();
1017        let (server, ticket) = exchanges.create().expect("server");
1018        let client = exchanges.open_peer(ticket).expect("client");
1019        let (mut request_out, mut response_in) = server.split();
1020        let (mut response_out, mut request_in) = client.split();
1021
1022        request_out.start(Some(b"request-head")).expect("request start");
1023        let request_descriptor =
1024            request_out.data(&[7_u8; 192]).expect("request data");
1025        let request_metadata =
1026            match request_in.try_next().expect("request start event") {
1027                FlowEvent::Start { metadata: Some(metadata) } => metadata,
1028                _ => panic!("expected request start"),
1029            };
1030        assert_eq!(&*request_metadata, b"request-head");
1031        let request_chunk =
1032            match request_in.try_next().expect("request data event") {
1033                FlowEvent::Data(chunk) => chunk,
1034                _ => panic!("expected request data"),
1035            };
1036        assert_eq!(request_descriptor.arena_kind(), 231);
1037        assert_eq!(request_descriptor.slot_count(), 3);
1038
1039        // Both directions share one arena, but their flow lifecycles are
1040        // independent, so the peer can start before the first flow finishes.
1041        response_out
1042            .start(Some(b"response-head"))
1043            .expect("early response start");
1044        let response_descriptor =
1045            response_out.data(b"response").expect("response data");
1046        assert_eq!(response_descriptor.arena_kind(), 231);
1047        assert!(matches!(response_in.try_next(), Ok(FlowEvent::Start { .. })));
1048        let response_chunk =
1049            match response_in.try_next().expect("response data event") {
1050                FlowEvent::Data(chunk) => chunk,
1051                _ => panic!("expected response data"),
1052            };
1053        assert_eq!(&*response_chunk, b"response");
1054
1055        drop(request_chunk);
1056        request_out.finish().expect("request fin");
1057        assert!(matches!(request_in.try_next(), Ok(FlowEvent::Fin)));
1058        response_out.finish().expect("response fin");
1059        assert!(matches!(response_in.try_next(), Ok(FlowEvent::Fin)));
1060    }
1061
1062    #[test]
1063    fn directional_payloads_keep_each_flows_credit_and_kind_separate() {
1064        let exchanges = Exchanges::open(
1065            Arc::new(
1066                Fleet::join("exchange-directional-test", 2).expect("fleet"),
1067            ),
1068            Incarnation::new(1),
1069            ExchangeSpec::directional(
1070                StreamSpec::new(220, 8, 512),
1071                PayloadArenaSpec::new(221, 1, 64),
1072                PayloadArenaSpec::new(222, 1, 128),
1073            ),
1074        )
1075        .expect("directional exchanges");
1076        let (mut side_a, ticket) = exchanges.create().expect("side A");
1077        let mut side_b = exchanges.open_peer(ticket).expect("side B");
1078
1079        let request_geometry = side_a.sender().chunk_geometry();
1080        let response_geometry = side_b.sender().chunk_geometry();
1081        assert_eq!(request_geometry.slot_payload_bytes(), 64);
1082        assert_eq!(response_geometry.slot_payload_bytes(), 128);
1083        assert_eq!(request_geometry.chunk_bytes(1), Some(64));
1084        assert_eq!(response_geometry.chunk_bytes(1), Some(128));
1085
1086        side_a.sender().start(None).expect("A-to-B start");
1087        assert!(matches!(
1088            side_b.receiver().try_next(),
1089            Ok(FlowEvent::Start { .. })
1090        ));
1091        side_b.sender().start(None).expect("B-to-A start");
1092        assert!(matches!(
1093            side_a.receiver().try_next(),
1094            Ok(FlowEvent::Start { .. })
1095        ));
1096
1097        let a = side_a.sender().data(&[1; 64]).expect("A-to-B payload");
1098        let b = side_b.sender().data(&[2; 128]).expect("B-to-A payload");
1099        assert_eq!(a.arena_kind(), 221);
1100        assert_eq!(b.arena_kind(), 222);
1101
1102        let a_payload = match side_b.receiver().try_next().expect("A-to-B data")
1103        {
1104            FlowEvent::Data(chunk) => chunk,
1105            _ => panic!("expected A-to-B data"),
1106        };
1107        let b_payload = match side_a.receiver().try_next().expect("B-to-A data")
1108        {
1109            FlowEvent::Data(chunk) => chunk,
1110            _ => panic!("expected B-to-A data"),
1111        };
1112        assert_eq!(&*a_payload, &[1; 64]);
1113        assert_eq!(&*b_payload, &[2; 128]);
1114    }
1115
1116    #[test]
1117    fn a_reserved_chunk_is_one_descriptor_over_many_slots() {
1118        let exchanges = Exchanges::open(
1119            Arc::new(
1120                Fleet::join("exchange-reservation-test", 2).expect("fleet"),
1121            ),
1122            Incarnation::new(1),
1123            ExchangeSpec::new(
1124                StreamSpec::new(233, 8, 512),
1125                PayloadArenaSpec::new(234, 8, 256),
1126            ),
1127        )
1128        .expect("exchanges");
1129        let (mut server, ticket) = exchanges.create().expect("server");
1130        let mut client = exchanges.open_peer(ticket).expect("client");
1131
1132        let expected: Vec<u8> =
1133            (0..777).map(|index| (index % 251) as u8).collect();
1134        let descriptor = {
1135            let mut pending =
1136                server.sender().reserve_start(expected.len()).expect("reserve");
1137            pending.copy_from_slice(&expected);
1138            pending.commit().expect("commit")
1139        };
1140        assert_eq!(descriptor.payload_len(), 777);
1141        assert_eq!(descriptor.slot_count(), 4);
1142        let received = match client.receiver().try_next().expect("start") {
1143            FlowEvent::Start { metadata: Some(metadata) } => metadata,
1144            _ => panic!("expected start metadata"),
1145        };
1146        assert_eq!(&*received, expected);
1147    }
1148
1149    #[test]
1150    fn dropping_an_uncommitted_chunk_returns_credit_without_an_event() {
1151        let exchanges = exchanges();
1152        let (mut server, ticket) = exchanges.create().expect("server");
1153        let mut client = exchanges.open_peer(ticket).expect("client");
1154        server.sender().start(None).expect("start");
1155        assert!(matches!(
1156            client.receiver().try_next(),
1157            Ok(FlowEvent::Start { metadata: None })
1158        ));
1159
1160        let pending =
1161            server.sender().reserve_data(256).expect("reserve all slots");
1162        drop(pending);
1163        assert!(matches!(client.receiver().try_next(), Err(Error::WouldBlock)));
1164        assert!(server.sender().reserve_data(256).is_ok());
1165    }
1166
1167    #[test]
1168    fn a_failed_control_commit_returns_its_payload_credit() {
1169        let exchanges = Exchanges::open(
1170            Arc::new(
1171                Fleet::join("exchange-failed-commit-test", 2).expect("fleet"),
1172            ),
1173            Incarnation::new(1),
1174            ExchangeSpec::new(
1175                StreamSpec::new(236, 8, CONTROL_FRAME_BYTES),
1176                PayloadArenaSpec::new(237, 1, 64),
1177            ),
1178        )
1179        .expect("exchanges");
1180        let (mut server, ticket) = exchanges.create().expect("server");
1181        let mut client = exchanges.open_peer(ticket).expect("client");
1182
1183        server.sender().start(None).expect("start fills control ring");
1184        let mut pending = server
1185            .sender()
1186            .reserve_data(64)
1187            .expect("reserve only payload slot");
1188        pending.fill(7);
1189        assert!(matches!(pending.commit(), Err(Error::WouldBlock)));
1190
1191        assert!(matches!(
1192            client.receiver().try_next(),
1193            Ok(FlowEvent::Start { metadata: None })
1194        ));
1195        let mut retry = server
1196            .sender()
1197            .reserve_data(64)
1198            .expect("failed commit returned credit");
1199        retry.fill(9);
1200        retry.commit().expect("retry commit");
1201        let received = match client.receiver().try_next().expect("retry data") {
1202            FlowEvent::Data(chunk) => chunk,
1203            _ => panic!("expected retry data"),
1204        };
1205        assert_eq!(&*received, &[9; 64]);
1206    }
1207
1208    #[test]
1209    fn a_short_io_commit_returns_unused_trailing_slots() {
1210        let exchanges = exchanges();
1211        let (mut server, ticket) = exchanges.create().expect("server");
1212        let mut client = exchanges.open_peer(ticket).expect("client");
1213        server.sender().start(None).expect("start");
1214        assert!(matches!(
1215            client.receiver().try_next(),
1216            Ok(FlowEvent::Start { metadata: None })
1217        ));
1218
1219        let descriptor = {
1220            let mut pending =
1221                server.sender().reserve_data(256).expect("reserve IO capacity");
1222            pending[..37].fill(5);
1223            pending.commit_len(37).expect("commit bytes read")
1224        };
1225        assert_eq!(descriptor.payload_len(), 37);
1226        assert_eq!(descriptor.slot_count(), 1);
1227
1228        let mut following =
1229            server.sender().reserve_data(192).expect("trailing slots returned");
1230        following.fill(9);
1231        following.commit().expect("commit following chunk");
1232        let first = match client.receiver().try_next().expect("short data") {
1233            FlowEvent::Data(chunk) => chunk,
1234            _ => panic!("expected short data"),
1235        };
1236        assert_eq!(&*first, &[5; 37]);
1237        let second = match client.receiver().try_next().expect("following data")
1238        {
1239            FlowEvent::Data(chunk) => chunk,
1240            _ => panic!("expected following data"),
1241        };
1242        assert_eq!(&*second, &[9; 192]);
1243    }
1244
1245    #[test]
1246    fn reset_is_terminal_for_only_its_own_flow() {
1247        let exchanges = exchanges();
1248        let (server, ticket) = exchanges.create().expect("server");
1249        let client = exchanges.open_peer(ticket).expect("client");
1250        let (mut request_out, mut response_in) = server.split();
1251        let (mut response_out, mut request_in) = client.split();
1252
1253        request_out.reset(ResetCode::new(41)).expect("request reset");
1254        assert!(matches!(
1255            request_in.try_next(),
1256            Ok(FlowEvent::Reset(code)) if code.get() == 41
1257        ));
1258
1259        response_out.start(None).expect("response remains independent");
1260        assert!(matches!(
1261            response_in.try_next(),
1262            Ok(FlowEvent::Start { metadata: None })
1263        ));
1264    }
1265}