Skip to main content

running_process/broker/backend_lib/
wire.rs

1//! Backend-side wire handling for broker handoff offers (#354, slice 6).
2//!
3//! Given a framed connection to the broker (the same v1 local-socket
4//! framing used by every other broker connection), this module:
5//!
6//! 1. reads one [`HandoffOffer`] frame
7//!    ([`read_handoff_offer`]),
8//! 2. validates and consumes the presented one-time token through the
9//!    existing [`accept_handed_off`] path, and
10//! 3. replies with a [`HandoffAck`] frame echoing the token and
11//!    correlation id ([`respond_to_handoff_offer`]).
12//!
13//! [`serve_handoff_offer`] composes all three for the common case. A
14//! rejected token still produces a well-formed `HandoffAck` with
15//! `accepted = false` and the rejection detail, so the broker can fall
16//! back to the reconnect path immediately instead of waiting out its ACK
17//! deadline.
18
19use std::io::{Read, Write};
20use std::time::Instant;
21
22use prost::Message;
23
24use crate::broker::backend_lib::accept_handed_off::{
25    accept_handed_off, HandedOffPayload, HandoffAcceptance,
26};
27use crate::broker::protocol::{
28    read_frame, write_frame, Frame, FrameKind, FramingError, HandoffAck, HandoffOffer,
29};
30use crate::broker::server::deadline_stream::with_nonblocking_deadline;
31use crate::broker::server::handoff::wire::{handoff_ack_frame, validate_handoff_frame};
32use crate::broker::server::{HandoffToken, HandoffTokenStore};
33
34/// Errors surfaced while reading or answering a handoff offer frame.
35#[derive(Debug, thiserror::Error)]
36pub enum BackendHandoffWireError {
37    /// v1 framing failed.
38    #[error(transparent)]
39    Framing(#[from] FramingError),
40    /// The offer frame could not be decoded.
41    #[error("failed to decode HandoffOffer Frame: {0}")]
42    DecodeFrame(prost::DecodeError),
43    /// The offer payload could not be decoded.
44    #[error("failed to decode HandoffOffer payload: {0}")]
45    DecodePayload(prost::DecodeError),
46    /// The frame did not match the handoff-offer contract.
47    #[error("unexpected HandoffOffer frame: {0}")]
48    UnexpectedFrame(&'static str),
49}
50
51impl From<std::io::Error> for BackendHandoffWireError {
52    fn from(error: std::io::Error) -> Self {
53        Self::Framing(FramingError::Io(error))
54    }
55}
56
57/// Read and validate one broker→backend [`HandoffOffer`] frame.
58pub fn read_handoff_offer<S: Read>(
59    stream: &mut S,
60) -> Result<HandoffOffer, BackendHandoffWireError> {
61    let bytes = read_frame(stream)?;
62    let frame = Frame::decode(bytes.as_slice()).map_err(BackendHandoffWireError::DecodeFrame)?;
63    validate_handoff_frame(&frame, FrameKind::Request)
64        .map_err(BackendHandoffWireError::UnexpectedFrame)?;
65    let offer = HandoffOffer::decode(frame.payload.as_slice())
66        .map_err(BackendHandoffWireError::DecodePayload)?;
67    if frame.request_id != offer.correlation_id {
68        return Err(BackendHandoffWireError::UnexpectedFrame(
69            "frame request_id does not match HandoffOffer correlation_id",
70        ));
71    }
72    Ok(offer)
73}
74
75/// Read one broker→backend [`HandoffOffer`] from a production local socket
76/// with an absolute deadline, restoring blocking mode before returning.
77pub fn read_handoff_offer_with_deadline(
78    stream: &mut interprocess::local_socket::Stream,
79    deadline: Instant,
80) -> Result<HandoffOffer, BackendHandoffWireError> {
81    with_nonblocking_deadline(stream, deadline, |stream| read_handoff_offer(stream))
82}
83
84/// Validate/consume one received offer and write the matching [`HandoffAck`].
85///
86/// The presented token rides the offer; `expected_token` is the token the
87/// backend was told to expect (it arrived out of band, e.g. through the
88/// spawn environment). On acceptance the one-time token is consumed from
89/// `pending_tokens` exactly once and the ACK reports `accepted = true`; on
90/// rejection the ACK carries `accepted = false` plus the rejection detail.
91/// Either way the ACK echoes the offer's token bytes and correlation id.
92pub fn respond_to_handoff_offer<S: Write>(
93    stream: &mut S,
94    pending_tokens: &mut HandoffTokenStore,
95    expected_token: HandoffToken,
96    offer: HandoffOffer,
97    now: Instant,
98) -> Result<HandoffAcceptance<HandoffOffer>, BackendHandoffWireError> {
99    let presented_token = offer.token.clone();
100    let correlation_id = offer.correlation_id;
101    let payload = HandedOffPayload::new(expected_token, presented_token.clone(), offer);
102    let acceptance = accept_handed_off(pending_tokens, payload, now);
103
104    let ack = match &acceptance {
105        HandoffAcceptance::Accepted(_) => HandoffAck {
106            token: presented_token,
107            accepted: true,
108            error_detail: String::new(),
109            correlation_id,
110        },
111        HandoffAcceptance::Rejected(rejected) => HandoffAck {
112            token: presented_token,
113            accepted: false,
114            error_detail: rejected.reason.to_string(),
115            correlation_id,
116        },
117    };
118    write_handoff_ack(stream, &ack)?;
119    Ok(acceptance)
120}
121
122/// Write one backend→broker [`HandoffAck`] frame.
123pub fn write_handoff_ack<S: Write>(
124    stream: &mut S,
125    ack: &HandoffAck,
126) -> Result<(), BackendHandoffWireError> {
127    let frame = handoff_ack_frame(ack);
128    let mut bytes = Vec::with_capacity(64);
129    frame
130        .encode(&mut bytes)
131        .expect("prost encoding Frame into Vec cannot fail because Vec writes are infallible");
132    write_frame(stream, &bytes)?;
133    Ok(())
134}
135
136/// Read one offer, validate/consume the token, and reply with the ACK.
137///
138/// Convenience composition of [`read_handoff_offer`] and
139/// [`respond_to_handoff_offer`] for backends serving one handoff per
140/// control exchange. This legacy generic entry point cannot configure bounded
141/// I/O itself; callers must pre-bound custom streams. Production local-socket
142/// users should call [`serve_handoff_offer_with_deadline`] instead.
143#[deprecated(
144    since = "4.6.2",
145    note = "use serve_handoff_offer_with_deadline for production local sockets"
146)]
147pub fn serve_handoff_offer<S: Read + Write>(
148    stream: &mut S,
149    pending_tokens: &mut HandoffTokenStore,
150    expected_token: HandoffToken,
151    now: Instant,
152) -> Result<HandoffAcceptance<HandoffOffer>, BackendHandoffWireError> {
153    serve_handoff_offer_inner(stream, pending_tokens, expected_token, now)
154}
155
156fn serve_handoff_offer_inner<S: Read + Write>(
157    stream: &mut S,
158    pending_tokens: &mut HandoffTokenStore,
159    expected_token: HandoffToken,
160    now: Instant,
161) -> Result<HandoffAcceptance<HandoffOffer>, BackendHandoffWireError> {
162    let offer = read_handoff_offer(stream)?;
163    respond_to_handoff_offer(stream, pending_tokens, expected_token, offer, now)
164}
165
166/// Serve one handoff offer over a production local socket with an absolute
167/// read deadline.
168///
169/// The stream is temporarily switched to nonblocking mode so silent, partial,
170/// and continuously trickling peers cannot extend the bound. Blocking mode is
171/// restored before the token is consumed and the ACK is written. If both the
172/// read and mode restoration fail, the read error takes precedence so a timeout
173/// remains classified as [`FramingError::Io`] with
174/// [`std::io::ErrorKind::TimedOut`].
175///
176/// This wire helper does not receive or acquire an out-of-band descriptor.
177/// A consuming transport that receives an `SCM_RIGHTS` descriptor before
178/// calling this function must retain it in an owned RAII value and drop it when
179/// this function returns an error, including a deadline expiry.
180///
181/// The deprecated [`serve_handoff_offer`] remains available for compatibility
182/// with custom `Read + Write` streams whose bounded-I/O policy is configured by
183/// the caller.
184pub fn serve_handoff_offer_with_deadline(
185    stream: &mut interprocess::local_socket::Stream,
186    pending_tokens: &mut HandoffTokenStore,
187    expected_token: HandoffToken,
188    now: Instant,
189    deadline: Instant,
190) -> Result<HandoffAcceptance<HandoffOffer>, BackendHandoffWireError> {
191    let offer = read_handoff_offer_with_deadline(stream, deadline)?;
192    respond_to_handoff_offer(stream, pending_tokens, expected_token, offer, now)
193}