Skip to main content

pg_proto/
pipeline.rs

1//! Bounded, payload-free orchestration for proxy request pipelines.
2//!
3//! The ledger in this module records protocol obligations, not wire messages.
4//! Applications retain ownership of decoded messages until [`FrontendAction`] or
5//! [`BackendAction`] tells them to forward, emit, retry, or discard the value.
6//! [`Pipeline::accept_frontend`] and [`Pipeline::accept_backend`] are the canonical
7//! ledger interface. Their typed counterparts additionally dispatch accepted
8//! messages through phase-specific middleware before committing them.
9//! Callers receiving [`crate::demux::SessionItem`] values should first consume
10//! any pooling or attribution evidence they need, then convert the item with
11//! [`crate::demux::SessionItem::into_backend_message`] before backend acceptance.
12
13use std::{collections::VecDeque, convert::Infallible, sync::Arc};
14
15use tokio::sync::Notify;
16
17use crate::{
18    codec::{BackendMessage, FrontendMessage},
19    demux::Demux,
20    grammar::backend,
21    middleware::{
22        AsynchronousBackendMessage, ChainError, MessageMiddleware, Middleware,
23        ReconstructableMessage as _, Then,
24    },
25};
26
27/// Stable identity of an accepted frontend operation.
28#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
29pub(crate) struct OperationId(u64);
30
31/// Pipeline policy which preserves the historical lock-step behaviour.
32#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
33pub struct NoPipeline;
34
35/// Configuration for a bounded frontend operation pipeline.
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub struct BoundedPipeline {
38    max_operations: usize,
39}
40
41impl BoundedPipeline {
42    /// Creates a pipeline with a non-zero operation-count limit.
43    ///
44    /// # Errors
45    ///
46    /// Returns an error when `max_operations` is zero.
47    pub fn new(max_operations: usize) -> Result<Self, PipelineConfigError> {
48        if max_operations == 0 {
49            return Err(PipelineConfigError);
50        }
51        Ok(Self { max_operations })
52    }
53
54    /// Returns the maximum number of incomplete operations.
55    #[must_use]
56    pub const fn max_operations(self) -> usize {
57        self.max_operations
58    }
59}
60
61/// A zero operation-count limit is not a usable pipeline configuration.
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub struct PipelineConfigError;
64
65impl std::fmt::Display for PipelineConfigError {
66    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        formatter.write_str("pipeline operation limit must be non-zero")
68    }
69}
70
71impl std::error::Error for PipelineConfigError {}
72
73mod private {
74    pub trait Sealed {}
75}
76
77/// Sealed configuration accepted by the intermediary builder.
78pub trait PipelinePolicy: private::Sealed + Copy {
79    /// Maximum number of incomplete operation records.
80    fn operation_limit(self) -> usize;
81}
82
83impl private::Sealed for NoPipeline {}
84impl PipelinePolicy for NoPipeline {
85    fn operation_limit(self) -> usize {
86        1
87    }
88}
89
90impl private::Sealed for BoundedPipeline {}
91impl PipelinePolicy for BoundedPipeline {
92    fn operation_limit(self) -> usize {
93        self.max_operations
94    }
95}
96
97/// Whether an accepted frontend operation is locally handled or forwarded.
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub(crate) enum FrontendHandling {
100    /// Send the returned message to the upstream connection.
101    Forward,
102    /// Do not send the request upstream; application code will synthesize its response.
103    Local,
104}
105
106/// Application action for one successfully projected frontend message.
107#[derive(Debug, Eq, PartialEq)]
108pub(crate) enum FrontendAction {
109    /// Forward the owned message upstream.
110    Forward {
111        /// Accepted operation identity.
112        id: OperationId,
113        /// Original, unretained frontend message.
114        message: FrontendMessage,
115    },
116    /// The operation is locally handled and the message can be discarded.
117    Discard {
118        /// Accepted operation identity.
119        id: OperationId,
120    },
121}
122
123/// Position of a successfully accepted operation.
124#[derive(Debug, Eq, PartialEq)]
125pub(crate) enum FrontendAdmission {
126    /// Nothing earlier prevents this operation's response from being emitted.
127    Immediate(FrontendAction),
128    /// The operation was accepted but an earlier response must be emitted first.
129    Waiting(FrontendAction),
130}
131
132impl FrontendAdmission {
133    /// Returns the application action, discarding only the positional annotation.
134    #[must_use]
135    pub(crate) fn into_action(self) -> FrontendAction {
136        match self {
137            Self::Immediate(action) | Self::Waiting(action) => action,
138        }
139    }
140}
141
142/// Why a frontend message could not be accepted.
143#[derive(Debug, Eq, PartialEq)]
144pub enum FrontendProjectionError {
145    /// The bounded ledger is full; the unchanged message may be retried.
146    Capacity(Box<FrontendMessage>),
147    /// The message is not legal in the projected frontend protocol state.
148    Illegal {
149        /// Projected state at rejection.
150        state: PipelineState,
151        /// Unchanged illegal message.
152        message: Box<FrontendMessage>,
153    },
154}
155
156/// Application action for a backend message.
157#[derive(Debug, Eq, PartialEq)]
158pub(crate) enum BackendAction {
159    /// Emit this owned message to the downstream client now.
160    Emit(BackendMessage),
161    /// An earlier operation must complete; retry this unchanged message later.
162    Deferred(BackendMessage),
163}
164
165/// A backend message was not legal for any outstanding operation.
166#[derive(Debug, Eq, PartialEq)]
167pub struct BackendProjectionError {
168    /// Current response-side state.
169    pub state: PipelineState,
170    /// Unchanged illegal message.
171    pub message: BackendMessage,
172}
173
174#[derive(Debug, Eq, PartialEq)]
175pub(crate) enum BackendSequenceError {
176    Cardinality { expected: usize, actual: usize },
177    Source(BackendMessage),
178    Replacement(BackendMessage),
179    DifferentSpan,
180}
181
182/// Public summary of the pipeline's projected frontend state.
183#[derive(Clone, Copy, Debug, Eq, PartialEq)]
184pub enum PipelineState {
185    /// A simple or extended cycle may begin.
186    Ready,
187    /// Extended-query messages are being accepted.
188    Extended,
189    /// An extended error discards messages through `Sync`.
190    ExtendedError,
191    /// COPY IN accepts frontend data.
192    CopyIn,
193    /// COPY OUT accepts only backend data.
194    CopyOut,
195    /// COPY BOTH accepts data in both directions.
196    CopyBoth,
197    /// The connection has terminated.
198    Terminated,
199}
200
201/// Error returned while dispatching a pipeline message through typed middleware.
202#[derive(Debug)]
203pub(crate) enum PipelineMiddlewareError<MiddlewareError, ProjectionError> {
204    /// Middleware rejected the phase-typed message.
205    Middleware(MiddlewareError),
206    /// The pipeline rejected the original or rewritten message.
207    Projection(ProjectionError),
208}
209
210macro_rules! frontend_pipeline_phases {
211    ($consumer:ident) => {
212        $consumer! {
213            Ready => frontend_ready => backend::ReadyExternalMessage,
214            Building => frontend_building => backend::BuildingExternalMessage,
215            ExtendedError => frontend_extended_error => backend::ExtendedErrorExternalMessage,
216            SimpleCopyIn => frontend_simple_copy_in => backend::SimpleCopyInExternalMessage,
217            ExtendedCopyIn => frontend_extended_copy_in => backend::ExtendedCopyInExternalMessage,
218            SimpleCopyBoth => frontend_simple_copy_both => backend::SimpleCopyBothExternalMessage,
219            ExtendedCopyBoth => frontend_extended_copy_both => backend::ExtendedCopyBothExternalMessage,
220        }
221    };
222}
223
224macro_rules! backend_pipeline_phases {
225    ($consumer:ident) => {
226        $consumer! {
227            Asynchronous => backend_asynchronous => AsynchronousBackendMessage,
228            Simple => backend_simple => backend::SimpleInternalMessage,
229            SimpleError => backend_simple_error => backend::SimpleErrorInternalMessage,
230            ParseResponse => backend_parse_response => backend::ParseResponseInternalMessage,
231            BindResponse => backend_bind_response => backend::BindResponseInternalMessage,
232            DescribeResponse => backend_describe_response => backend::DescribeResponseInternalMessage,
233            ExecuteResponse => backend_execute_response => backend::ExecuteResponseInternalMessage,
234            CloseResponse => backend_close_response => backend::CloseResponseInternalMessage,
235            SyncResponse => backend_sync_response => backend::SyncResponseInternalMessage,
236            FunctionResponse => backend_function_response => backend::FunctionResponseInternalMessage,
237            FunctionReady => backend_function_ready => backend::FunctionReadyInternalMessage,
238            SimpleCopyInDone => backend_simple_copy_in_done => backend::SimpleCopyInDoneInternalMessage,
239            SimpleCopyInFailed => backend_simple_copy_in_failed => backend::SimpleCopyInFailedInternalMessage,
240            SimpleCopyOut => backend_simple_copy_out => backend::SimpleCopyOutInternalMessage,
241            SimpleCopyOutDone => backend_simple_copy_out_done => backend::SimpleCopyOutDoneInternalMessage,
242            SimpleCopyReady => backend_simple_copy_ready => backend::SimpleCopyReadyInternalMessage,
243            ExtendedCopyInDone => backend_extended_copy_in_done => backend::ExtendedCopyInDoneInternalMessage,
244            ExtendedCopyInFailed => backend_extended_copy_in_failed => backend::ExtendedCopyInFailedInternalMessage,
245            ExtendedCopyOut => backend_extended_copy_out => backend::ExtendedCopyOutInternalMessage,
246            ExtendedCopyOutDone => backend_extended_copy_out_done => backend::ExtendedCopyOutDoneInternalMessage,
247            SimpleCopyBoth => backend_simple_copy_both => backend::SimpleCopyBothInternalMessage,
248            SimpleCopyBothClientDone => backend_simple_copy_both_client_done => backend::SimpleCopyBothClientDoneInternalMessage,
249            SimpleCopyBothDone => backend_simple_copy_both_done => backend::SimpleCopyBothDoneInternalMessage,
250            SimpleCopyBothFailed => backend_simple_copy_both_failed => backend::SimpleCopyBothFailedInternalMessage,
251            ExtendedCopyBoth => backend_extended_copy_both => backend::ExtendedCopyBothInternalMessage,
252            ExtendedCopyBothClientDone => backend_extended_copy_both_client_done => backend::ExtendedCopyBothClientDoneInternalMessage,
253            ExtendedCopyBothDone => backend_extended_copy_both_done => backend::ExtendedCopyBothDoneInternalMessage,
254            ExtendedCopyBothFailed => backend_extended_copy_both_failed => backend::ExtendedCopyBothFailedInternalMessage,
255        }
256    };
257}
258
259macro_rules! declare_pipeline_hooks {
260    ($($phase:ident => $method:ident => $message:path),+ $(,)?) => {
261        $(
262            #[doc = concat!("Intercepts backend messages in generated `", stringify!($message), "` phase.")]
263            async fn $method(
264                &mut self,
265                _state: &mut State,
266                message: $message,
267            ) -> Result<$message, Self::Error> {
268                Ok(message)
269            }
270        )+
271    };
272}
273
274/// Async middleware for frontend messages selected from the runtime ledger phase.
275#[allow(async_fn_in_trait)]
276pub(crate) trait FrontendPipelineMiddleware<State> {
277    /// An error which prevents the message from continuing through the pipeline.
278    type Error;
279    frontend_pipeline_phases!(declare_pipeline_hooks);
280}
281
282/// Async middleware for backend messages selected from the runtime ledger phase.
283#[allow(async_fn_in_trait)]
284pub(crate) trait BackendPipelineMiddleware<State> {
285    /// An error which prevents the message from continuing through the pipeline.
286    type Error;
287    backend_pipeline_phases!(declare_pipeline_hooks);
288}
289
290impl<State> FrontendPipelineMiddleware<State> for crate::middleware::Identity {
291    type Error = Infallible;
292}
293
294impl<State> BackendPipelineMiddleware<State> for crate::middleware::Identity {
295    type Error = Infallible;
296}
297
298macro_rules! chained_pipeline_hooks {
299    ($($phase:ident => $method:ident => $message:ty),+ $(,)?) => {
300        $(
301            async fn $method(
302                &mut self,
303                state: &mut State,
304                message: $message,
305            ) -> Result<$message, Self::Error> {
306                let (first, second) = self.parts_mut();
307                let message = first
308                    .$method(state, message)
309                    .await
310                    .map_err(ChainError::First)?;
311                second
312                    .$method(state, message)
313                    .await
314                    .map_err(ChainError::Second)
315            }
316        )+
317    };
318}
319
320impl<State, First, Second> FrontendPipelineMiddleware<State> for Then<First, Second>
321where
322    First: FrontendPipelineMiddleware<State>,
323    Second: FrontendPipelineMiddleware<State>,
324{
325    type Error = ChainError<First::Error, Second::Error>;
326
327    frontend_pipeline_phases!(chained_pipeline_hooks);
328}
329
330impl<State, First, Second> BackendPipelineMiddleware<State> for Then<First, Second>
331where
332    First: BackendPipelineMiddleware<State>,
333    Second: BackendPipelineMiddleware<State>,
334{
335    type Error = ChainError<First::Error, Second::Error>;
336    backend_pipeline_phases!(chained_pipeline_hooks);
337}
338
339/// Adapts direction-wide async middleware to every typed pipeline hook.
340pub(crate) struct PipelineWireAdapter<Handler> {
341    handler: Handler,
342}
343
344impl<Handler> PipelineWireAdapter<Handler> {
345    /// Wraps direction-wide middleware for runtime phase dispatch.
346    pub(crate) const fn new(handler: Handler) -> Self {
347        Self { handler }
348    }
349
350    /// Returns the wrapped direction-wide middleware.
351    pub(crate) fn into_inner(self) -> Handler {
352        self.handler
353    }
354}
355
356/// Failure from direction-wide middleware adapted to typed pipeline dispatch.
357#[derive(Debug)]
358pub(crate) enum FrontendPipelineWireAdapterError<Error> {
359    /// The wrapped middleware rejected a message.
360    Middleware(Error),
361    /// The wrapped middleware returned a frontend message illegal in the selected phase.
362    IllegalFrontend(FrontendMessage),
363}
364
365/// Failure from backend wire middleware adapted to typed pipeline dispatch.
366#[derive(Debug)]
367pub(crate) enum BackendPipelineWireAdapterError<Error> {
368    /// The wrapped middleware rejected a message.
369    Middleware(Error),
370    /// The wrapped middleware returned a message illegal in the selected phase.
371    Illegal(BackendMessage),
372}
373
374macro_rules! pipeline_adapter_frontend_hooks {
375    ($($phase:ident => $method:ident => $message:ty),+ $(,)?) => {
376        $(
377            async fn $method(
378                &mut self,
379                state: &mut State,
380                message: $message,
381            ) -> Result<$message, Self::Error> {
382                let message: FrontendMessage = message.into();
383                let message = self
384                    .handler
385                    .intercept(state, message)
386                    .await
387                    .map_err(FrontendPipelineWireAdapterError::Middleware)?;
388                <$message>::try_from(message)
389                    .map_err(FrontendPipelineWireAdapterError::IllegalFrontend)
390            }
391        )+
392    };
393}
394
395macro_rules! pipeline_adapter_backend_hooks {
396    ($ignored:ident => $async_method:ident => AsynchronousBackendMessage, $($phase:ident => $method:ident => $message:ty),+ $(,)?) => {
397        async fn $async_method(
398            &mut self,
399            state: &mut State,
400            message: AsynchronousBackendMessage,
401        ) -> Result<AsynchronousBackendMessage, Self::Error> {
402            let message = self.handler.intercept(state, message.into_wire()).await
403                .map_err(BackendPipelineWireAdapterError::Middleware)?;
404            AsynchronousBackendMessage::try_from(message)
405                .map_err(BackendPipelineWireAdapterError::Illegal)
406        }
407        $(
408            async fn $method(
409                &mut self,
410                state: &mut State,
411                message: $message,
412            ) -> Result<$message, Self::Error> {
413                let message: BackendMessage = message.into();
414                let message = self
415                    .handler
416                    .intercept(state, message)
417                    .await
418                    .map_err(BackendPipelineWireAdapterError::Middleware)?;
419                <$message>::try_from(message).map_err(BackendPipelineWireAdapterError::Illegal)
420            }
421        )+
422    };
423}
424
425impl<State, Handler> FrontendPipelineMiddleware<State> for PipelineWireAdapter<Handler>
426where
427    Handler: MessageMiddleware<FrontendMessage, State>,
428{
429    type Error = FrontendPipelineWireAdapterError<Handler::Error>;
430    frontend_pipeline_phases!(pipeline_adapter_frontend_hooks);
431}
432
433impl<State, Handler> BackendPipelineMiddleware<State> for PipelineWireAdapter<Handler>
434where
435    Handler: MessageMiddleware<BackendMessage, State>,
436{
437    type Error = BackendPipelineWireAdapterError<Handler::Error>;
438    backend_pipeline_phases!(pipeline_adapter_backend_hooks);
439}
440
441#[derive(Clone, Copy, Debug, Eq, PartialEq)]
442enum RequestState {
443    Ready,
444    Extended { bound: bool },
445    ExtendedError,
446    CopyIn,
447    CopyOut,
448    CopyBoth,
449    Terminated,
450}
451
452impl RequestState {
453    const fn public(self) -> PipelineState {
454        match self {
455            Self::Ready => PipelineState::Ready,
456            Self::Extended { .. } => PipelineState::Extended,
457            Self::ExtendedError => PipelineState::ExtendedError,
458            Self::CopyIn => PipelineState::CopyIn,
459            Self::CopyOut => PipelineState::CopyOut,
460            Self::CopyBoth => PipelineState::CopyBoth,
461            Self::Terminated => PipelineState::Terminated,
462        }
463    }
464}
465
466#[derive(Clone, Copy, Debug, Eq, PartialEq)]
467enum Origin {
468    Forwarded,
469    Local,
470}
471
472#[derive(Clone, Copy, Debug, Eq, PartialEq)]
473enum OperationKind {
474    Query,
475    FunctionCall,
476    Parse,
477    Bind,
478    Describe,
479    Execute,
480    Close,
481    Flush,
482    Sync,
483    CopyData,
484    CopyDone,
485    CopyFail,
486    Terminate,
487}
488
489#[derive(Clone, Copy, Debug, Eq, PartialEq)]
490enum PreparedResponse {
491    Asynchronous,
492    Emit {
493        head: Operation,
494        response_state: backend::RuntimeState,
495    },
496    Deferred,
497    Illegal,
498}
499
500#[derive(Clone, Copy, Debug, Eq, PartialEq)]
501enum FrontendPhase {
502    Ready,
503    Building,
504    ExtendedError,
505    SimpleCopyIn,
506    ExtendedCopyIn,
507    SimpleCopyBoth,
508    ExtendedCopyBoth,
509}
510
511#[derive(Clone, Copy, Debug)]
512struct PreparedFrontend {
513    phase: FrontendPhase,
514    request_state: RequestState,
515}
516
517#[derive(Clone, Copy, Debug, Eq, PartialEq)]
518struct Operation {
519    id: OperationId,
520    kind: OperationKind,
521    origin: Origin,
522    discarded: bool,
523    response_state: backend::RuntimeState,
524}
525
526/// A bounded ledger coordinating independently owned frontend and backend values.
527#[derive(Debug)]
528pub(crate) struct Pipeline<P = NoPipeline> {
529    policy: P,
530    operations: VecDeque<Operation>,
531    request_state: RequestState,
532    response_state: Option<PipelineState>,
533    next_id: u64,
534    changed: Arc<Notify>,
535}
536
537impl Default for Pipeline<NoPipeline> {
538    fn default() -> Self {
539        Self::new(NoPipeline)
540    }
541}
542
543impl<P: PipelinePolicy> Pipeline<P> {
544    fn snapshot(&self) -> Self {
545        Self {
546            policy: self.policy,
547            operations: self.operations.clone(),
548            request_state: self.request_state,
549            response_state: self.response_state,
550            next_id: self.next_id,
551            changed: Arc::clone(&self.changed),
552        }
553    }
554
555    pub(crate) fn prepare_backend_replacements(
556        &self,
557        sources: &[BackendMessage],
558        replacements: &[BackendMessage],
559    ) -> Result<Self, BackendSequenceError> {
560        if sources.len() != replacements.len() {
561            return Err(BackendSequenceError::Cardinality {
562                expected: sources.len(),
563                actual: replacements.len(),
564            });
565        }
566        let mut source_projection = self.snapshot();
567        for message in sources.iter().cloned() {
568            match source_projection.accept_backend(message) {
569                Ok(BackendAction::Emit(_)) => {}
570                Ok(BackendAction::Deferred(message))
571                | Err(BackendProjectionError { message, .. }) => {
572                    return Err(BackendSequenceError::Source(message));
573                }
574            }
575        }
576        let mut replacement_projection = self.snapshot();
577        for message in replacements.iter().cloned() {
578            if !message.is_reconstructable() {
579                return Err(BackendSequenceError::Replacement(message));
580            }
581            match replacement_projection.accept_backend(message) {
582                Ok(BackendAction::Emit(_)) => {}
583                Ok(BackendAction::Deferred(message))
584                | Err(BackendProjectionError { message, .. }) => {
585                    return Err(BackendSequenceError::Replacement(message));
586                }
587            }
588        }
589        if source_projection.operations != replacement_projection.operations
590            || source_projection.request_state != replacement_projection.request_state
591            || source_projection.response_state != replacement_projection.response_state
592        {
593            return Err(BackendSequenceError::DifferentSpan);
594        }
595        Ok(replacement_projection)
596    }
597    /// Creates an empty pipeline using `policy`.
598    #[must_use]
599    pub(crate) fn new(policy: P) -> Self {
600        Self {
601            policy,
602            operations: VecDeque::new(),
603            request_state: RequestState::Ready,
604            response_state: None,
605            next_id: 0,
606            changed: Arc::new(Notify::new()),
607        }
608    }
609
610    /// Returns the projected frontend protocol state.
611    #[must_use]
612    pub(crate) fn state(&self) -> PipelineState {
613        self.response_state
614            .unwrap_or_else(|| self.request_state.public())
615    }
616
617    /// Returns the number of incomplete lightweight operation records.
618    #[must_use]
619    pub(crate) fn len(&self) -> usize {
620        self.operations.len()
621    }
622
623    /// Reports whether no operations remain outstanding.
624    #[must_use]
625    pub(crate) fn is_empty(&self) -> bool {
626        self.operations.is_empty()
627    }
628
629    /// Projects and accepts one frontend message without retaining its payload.
630    ///
631    /// Capacity and legality failures return the original owned message. A
632    /// capacity failure does not mutate either projected state or the ledger.
633    ///
634    /// # Errors
635    ///
636    /// Returns the unchanged boxed message when capacity is exhausted or the
637    /// message is illegal in the projected state.
638    pub(crate) fn accept_frontend(
639        &mut self,
640        message: FrontendMessage,
641        handling: FrontendHandling,
642    ) -> Result<FrontendAdmission, FrontendProjectionError> {
643        let prepared = self.prepare_frontend(&message)?;
644        Ok(self.commit_frontend(prepared, message, handling))
645    }
646
647    fn prepare_frontend(
648        &self,
649        message: &FrontendMessage,
650    ) -> Result<PreparedFrontend, FrontendProjectionError> {
651        if self.operations.len() == self.policy.operation_limit()
652            && !matches!(
653                self.request_state,
654                RequestState::CopyIn | RequestState::CopyBoth
655            )
656        {
657            return Err(FrontendProjectionError::Capacity(Box::new(message.clone())));
658        }
659        if project_frontend(self.request_state, message).is_none() {
660            return Err(FrontendProjectionError::Illegal {
661                state: self.state(),
662                message: Box::new(message.clone()),
663            });
664        }
665        Ok(PreparedFrontend {
666            phase: frontend_phase(
667                self.request_state,
668                self.operations.front().map(|operation| operation.kind),
669            ),
670            request_state: self.request_state,
671        })
672    }
673
674    fn commit_frontend(
675        &mut self,
676        prepared: PreparedFrontend,
677        message: FrontendMessage,
678        handling: FrontendHandling,
679    ) -> FrontendAdmission {
680        let (kind, next_state) = classify_frontend(prepared.request_state, &message)
681            .expect("phase-typed frontend replacement has a ledger classification");
682        let waiting = !self.operations.is_empty();
683        let id = OperationId(self.next_id);
684        self.next_id = self.next_id.saturating_add(1);
685        self.request_state = next_state;
686        let origin = match handling {
687            FrontendHandling::Forward => Origin::Forwarded,
688            FrontendHandling::Local => Origin::Local,
689        };
690        if let Some(head) = self.operations.front_mut()
691            && let Some(event) = backend::project_external(head.response_state, &message)
692            && let Some(transition) = backend::transition(head.response_state, event)
693        {
694            head.response_state = transition.target;
695        }
696        self.operations.push_back(Operation {
697            id,
698            kind,
699            origin,
700            discarded: matches!(self.request_state, RequestState::ExtendedError)
701                && kind != OperationKind::Sync,
702            response_state: initial_response_state(kind),
703        });
704        let action = match handling {
705            FrontendHandling::Forward => FrontendAction::Forward { id, message },
706            FrontendHandling::Local => FrontendAction::Discard { id },
707        };
708        let admission = if waiting {
709            FrontendAdmission::Waiting(action)
710        } else {
711            FrontendAdmission::Immediate(action)
712        };
713        self.remove_inert_heads();
714        admission
715    }
716
717    /// Projects, asynchronously intercepts, and accepts one frontend message.
718    ///
719    /// The ledger selects the phase-specific middleware hook at runtime. The
720    /// selected hook can only return a message legal in that same phase.
721    /// Middleware is not invoked when capacity is exhausted.
722    ///
723    /// # Errors
724    ///
725    /// Returns a middleware error, an illegal original or replacement message,
726    /// or the unchanged message when capacity is exhausted.
727    pub(crate) async fn accept_frontend_typed<State, Handler>(
728        &mut self,
729        middleware: &mut Middleware<State, Handler>,
730        message: FrontendMessage,
731        handling: FrontendHandling,
732    ) -> Result<FrontendAdmission, PipelineMiddlewareError<Handler::Error, FrontendProjectionError>>
733    where
734        Handler: FrontendPipelineMiddleware<State>,
735    {
736        let prepared = self
737            .prepare_frontend(&message)
738            .map_err(PipelineMiddlewareError::Projection)?;
739
740        let message = self
741            .intercept_frontend(prepared.phase, middleware, message)
742            .await
743            .map_err(PipelineMiddlewareError::Middleware)?;
744        if !message.is_reconstructable() {
745            return Err(PipelineMiddlewareError::Projection(
746                FrontendProjectionError::Illegal {
747                    state: self.state(),
748                    message: Box::new(message),
749                },
750            ));
751        }
752        Ok(self.commit_frontend(prepared, message, handling))
753    }
754
755    /// Projects one upstream backend message and preserves response order.
756    ///
757    /// # Errors
758    ///
759    /// Returns an unchanged response which cannot belong to any outstanding operation.
760    pub(crate) fn accept_backend(
761        &mut self,
762        message: BackendMessage,
763    ) -> Result<BackendAction, BackendProjectionError> {
764        self.accept_response(None, message)
765    }
766
767    /// Intercepts an emittable backend response through its operation-typed hook.
768    ///
769    /// Responses belonging to a later operation are returned unchanged as
770    /// [`BackendAction::Deferred`] and are intercepted only when retried at the
771    /// response head. Asynchronous messages use their non-advancing hook.
772    ///
773    /// # Errors
774    ///
775    /// Returns a middleware error or an unchanged response which cannot belong
776    /// to any outstanding operation.
777    pub(crate) async fn accept_backend_typed<State, Handler>(
778        &mut self,
779        middleware: &mut Middleware<State, Handler>,
780        message: BackendMessage,
781    ) -> Result<BackendAction, PipelineMiddlewareError<Handler::Error, BackendProjectionError>>
782    where
783        Handler: BackendPipelineMiddleware<State>,
784    {
785        self.accept_response_typed(None, middleware, message).await
786    }
787
788    /// Attempts to register and emit a locally synthesized response.
789    ///
790    /// The message is returned as [`BackendAction::Deferred`] when `id` has not
791    /// reached the head. No backend payload is retained by the ledger.
792    ///
793    /// # Errors
794    ///
795    /// Returns the unchanged message when it is illegal for the named operation.
796    pub(crate) fn try_emit_local(
797        &mut self,
798        id: OperationId,
799        message: BackendMessage,
800    ) -> Result<BackendAction, BackendProjectionError> {
801        self.accept_response(Some(id), message)
802    }
803
804    /// Typed-middleware counterpart to [`Self::try_emit_local`].
805    ///
806    /// Deferred local responses are not intercepted until their operation reaches
807    /// the response head.
808    ///
809    /// # Errors
810    ///
811    /// Returns a middleware error or an illegal response for the named operation.
812    pub(crate) async fn try_emit_local_typed<State, Handler>(
813        &mut self,
814        middleware: &mut Middleware<State, Handler>,
815        id: OperationId,
816        message: BackendMessage,
817    ) -> Result<BackendAction, PipelineMiddlewareError<Handler::Error, BackendProjectionError>>
818    where
819        Handler: BackendPipelineMiddleware<State>,
820    {
821        self.accept_response_typed(Some(id), middleware, message)
822            .await
823    }
824
825    /// Waits until a local operation reaches the response head.
826    ///
827    /// Cancellation is safe: polling this future never reserves or removes a
828    /// ledger entry. The caller should then invoke [`Self::try_emit_local`].
829    pub(crate) async fn wait_until_emittable(&self, id: OperationId) {
830        loop {
831            let notified = self.changed.notified();
832            if self
833                .operations
834                .front()
835                .is_some_and(|operation| operation.id == id)
836            {
837                return;
838            }
839            notified.await;
840        }
841    }
842
843    async fn intercept_frontend<State, Handler>(
844        &self,
845        phase: FrontendPhase,
846        middleware: &mut Middleware<State, Handler>,
847        message: FrontendMessage,
848    ) -> Result<FrontendMessage, Handler::Error>
849    where
850        Handler: FrontendPipelineMiddleware<State>,
851    {
852        let (state, handler) = middleware.parts_mut();
853        macro_rules! dispatch {
854            ($message:expr, $type:path, $handler:ident, $state:ident, $method:ident) => {{
855                let Ok(typed) = <$type>::try_from($message) else {
856                    unreachable!("frontend message was prevalidated for pipeline phase")
857                };
858                $handler.$method($state, typed).await?.into()
859            }};
860        }
861
862        macro_rules! dispatch_catalogue {
863            ($($catalogue_phase:ident => $method:ident => $message_type:path),+ $(,)?) => {
864                match phase {
865                    $(
866                        FrontendPhase::$catalogue_phase =>
867                            dispatch!(message, $message_type, handler, state, $method),
868                    )+
869                }
870            };
871        }
872
873        Ok(frontend_pipeline_phases!(dispatch_catalogue))
874    }
875
876    #[allow(clippy::too_many_lines)]
877    async fn accept_response_typed<State, Handler>(
878        &mut self,
879        local_id: Option<OperationId>,
880        middleware: &mut Middleware<State, Handler>,
881        message: BackendMessage,
882    ) -> Result<BackendAction, PipelineMiddlewareError<Handler::Error, BackendProjectionError>>
883    where
884        Handler: BackendPipelineMiddleware<State>,
885    {
886        let prepared = self.prepare_response(local_id, &message);
887        if matches!(prepared, PreparedResponse::Deferred) {
888            return Ok(BackendAction::Deferred(message));
889        }
890        if matches!(prepared, PreparedResponse::Illegal) {
891            return Err(PipelineMiddlewareError::Projection(
892                BackendProjectionError {
893                    state: self.state(),
894                    message,
895                },
896            ));
897        }
898
899        let (state, handler) = middleware.parts_mut();
900        let message = match prepared {
901            PreparedResponse::Asynchronous => {
902                let Ok(typed) = AsynchronousBackendMessage::try_from(message) else {
903                    unreachable!("asynchronous response was prevalidated")
904                };
905                handler
906                    .backend_asynchronous(state, typed)
907                    .await
908                    .map_err(PipelineMiddlewareError::Middleware)?
909                    .into_wire()
910            }
911            PreparedResponse::Emit { response_state, .. } => {
912                macro_rules! dispatch {
913                    ($message:ty, $method:ident) => {{
914                        let typed = match <$message>::try_from(message) {
915                            Ok(typed) => typed,
916                            Err(message) => {
917                                return Err(PipelineMiddlewareError::Projection(
918                                    BackendProjectionError {
919                                        state: self.state(),
920                                        message,
921                                    },
922                                ));
923                            }
924                        };
925                        handler
926                            .$method(state, typed)
927                            .await
928                            .map_err(PipelineMiddlewareError::Middleware)?
929                            .into_wire()
930                    }};
931                }
932
933                macro_rules! dispatch_catalogue {
934                    ($ignored:ident => $ignored_method:ident => AsynchronousBackendMessage,
935                     $($catalogue_phase:ident => $method:ident => $message_type:path),+ $(,)?) => {
936                        match response_state {
937                            $(
938                                backend::RuntimeState::$catalogue_phase =>
939                                    dispatch!($message_type, $method),
940                            )+
941                            _ => unreachable!("response phase has no backend-selected transition"),
942                        }
943                    };
944                }
945
946                backend_pipeline_phases!(dispatch_catalogue)
947            }
948            PreparedResponse::Deferred | PreparedResponse::Illegal => unreachable!(),
949        };
950
951        if !message.is_reconstructable() {
952            return Err(PipelineMiddlewareError::Projection(
953                BackendProjectionError {
954                    state: self.state(),
955                    message,
956                },
957            ));
958        }
959        self.commit_response(prepared, message)
960            .map_err(PipelineMiddlewareError::Projection)
961    }
962
963    fn prepare_response(
964        &self,
965        local_id: Option<OperationId>,
966        message: &BackendMessage,
967    ) -> PreparedResponse {
968        if is_asynchronous(message) {
969            return PreparedResponse::Asynchronous;
970        }
971        let Some(head) = self.operations.front().copied() else {
972            return PreparedResponse::Illegal;
973        };
974        if let Some(id) = local_id {
975            if head.id != id {
976                return PreparedResponse::Deferred;
977            }
978            if head.origin != Origin::Local {
979                return PreparedResponse::Illegal;
980            }
981        } else if head.origin == Origin::Local {
982            return if self.operations.iter().skip(1).any(|operation| {
983                operation.origin == Origin::Forwarded && response_fits(*operation, message)
984            }) {
985                PreparedResponse::Deferred
986            } else {
987                PreparedResponse::Illegal
988            };
989        }
990        if head.discarded || !response_fits(head, message) {
991            return if self
992                .operations
993                .iter()
994                .skip(1)
995                .any(|operation| response_fits(*operation, message))
996            {
997                PreparedResponse::Deferred
998            } else {
999                PreparedResponse::Illegal
1000            };
1001        }
1002        PreparedResponse::Emit {
1003            head,
1004            response_state: head.response_state,
1005        }
1006    }
1007
1008    fn accept_response(
1009        &mut self,
1010        local_id: Option<OperationId>,
1011        message: BackendMessage,
1012    ) -> Result<BackendAction, BackendProjectionError> {
1013        let prepared = self.prepare_response(local_id, &message);
1014        self.commit_response(prepared, message)
1015    }
1016
1017    fn commit_response(
1018        &mut self,
1019        prepared: PreparedResponse,
1020        message: BackendMessage,
1021    ) -> Result<BackendAction, BackendProjectionError> {
1022        let PreparedResponse::Emit {
1023            head,
1024            response_state,
1025        } = prepared
1026        else {
1027            return match prepared {
1028                PreparedResponse::Asynchronous => Ok(BackendAction::Emit(message)),
1029                PreparedResponse::Deferred => Ok(BackendAction::Deferred(message)),
1030                PreparedResponse::Illegal => Err(BackendProjectionError {
1031                    state: self.state(),
1032                    message,
1033                }),
1034                PreparedResponse::Emit { .. } => unreachable!(),
1035            };
1036        };
1037        let event = backend::project_internal(response_state, &message)
1038            .expect("response was validated against its generated backend phase");
1039        let next_response_state = backend::transition(response_state, event)
1040            .expect("projected backend event has a generated transition")
1041            .target;
1042        let terminal = response_is_terminal(head.kind, &message);
1043        let error = matches!(message, BackendMessage::ErrorResponse(_));
1044        let copy_state = response_copy_state(next_response_state);
1045        if terminal {
1046            self.operations.pop_front();
1047            if error && is_extended_kind(head.kind) {
1048                self.enter_extended_error();
1049            }
1050        } else if let Some(head) = self.operations.front_mut() {
1051            head.response_state = next_response_state;
1052        }
1053        if !terminal {
1054            self.response_state = copy_state.map(RequestState::public);
1055        }
1056        if let Some(state) = copy_state
1057            && matches!(state, RequestState::CopyIn | RequestState::CopyBoth)
1058        {
1059            self.request_state = state;
1060        }
1061        if terminal {
1062            self.response_state = None;
1063            match head.kind {
1064                OperationKind::Sync | OperationKind::Query => {
1065                    self.request_state = RequestState::Ready;
1066                }
1067                OperationKind::Execute if !error => {
1068                    self.request_state = RequestState::Extended { bound: true };
1069                }
1070                _ => {}
1071            }
1072        }
1073        self.remove_inert_heads();
1074        self.changed.notify_waiters();
1075        Ok(BackendAction::Emit(message))
1076    }
1077
1078    fn enter_extended_error(&mut self) {
1079        self.request_state = RequestState::ExtendedError;
1080        self.response_state = None;
1081        for operation in &mut self.operations {
1082            if operation.kind == OperationKind::Sync {
1083                break;
1084            }
1085            operation.discarded = true;
1086        }
1087    }
1088
1089    fn remove_inert_heads(&mut self) {
1090        let previous_len = self.operations.len();
1091        while self.operations.front().is_some_and(|operation| {
1092            operation.kind == OperationKind::Flush
1093                || operation.kind == OperationKind::CopyData
1094                || operation.kind == OperationKind::CopyDone
1095                || operation.kind == OperationKind::CopyFail
1096                || operation.kind == OperationKind::Terminate
1097                || operation.discarded
1098        }) {
1099            self.operations.pop_front();
1100        }
1101        if self.operations.len() != previous_len {
1102            self.changed.notify_waiters();
1103        }
1104    }
1105}
1106
1107fn project_frontend(
1108    state: RequestState,
1109    message: &FrontendMessage,
1110) -> Option<(OperationKind, RequestState)> {
1111    use RequestState as S;
1112    let generated_state = match state {
1113        S::Ready => backend::RuntimeState::Ready,
1114        S::Extended { .. } => backend::RuntimeState::Building,
1115        S::ExtendedError => backend::RuntimeState::ExtendedError,
1116        S::CopyIn => backend::RuntimeState::ExtendedCopyIn,
1117        S::CopyOut => backend::RuntimeState::ExtendedCopyOut,
1118        S::CopyBoth => backend::RuntimeState::ExtendedCopyBoth,
1119        S::Terminated => backend::RuntimeState::Terminated,
1120    };
1121    backend::project_external(generated_state, message)?;
1122    classify_frontend(state, message)
1123}
1124
1125fn classify_frontend(
1126    state: RequestState,
1127    message: &FrontendMessage,
1128) -> Option<(OperationKind, RequestState)> {
1129    use FrontendMessage as F;
1130    use OperationKind as O;
1131    use RequestState as S;
1132
1133    match (state, message) {
1134        (S::Ready, F::Query(_)) => Some((O::Query, S::Ready)),
1135        (S::Ready, F::FunctionCall(_)) => Some((O::FunctionCall, S::Ready)),
1136        (S::Ready, F::Parse(_)) => Some((O::Parse, S::Extended { bound: false })),
1137        (S::Ready | S::Extended { .. }, F::Bind(_)) => Some((O::Bind, S::Extended { bound: true })),
1138        (S::Ready, F::Describe(_)) => Some((O::Describe, S::Extended { bound: false })),
1139        (S::Ready | S::Extended { .. }, F::Execute(_)) => {
1140            Some((O::Execute, S::Extended { bound: true }))
1141        }
1142        (S::Ready, F::Close(_)) => Some((O::Close, S::Extended { bound: false })),
1143        (S::Ready, F::Terminate) => Some((O::Terminate, S::Terminated)),
1144        (S::Extended { bound }, F::Parse(_)) => Some((O::Parse, S::Extended { bound })),
1145        (S::Extended { bound }, F::Describe(_)) => Some((O::Describe, S::Extended { bound })),
1146        (S::Extended { bound }, F::Close(_)) => Some((O::Close, S::Extended { bound })),
1147        (S::Extended { bound }, F::Flush) => Some((O::Flush, S::Extended { bound })),
1148        (S::Extended { .. } | S::ExtendedError, F::Sync) => Some((O::Sync, S::Ready)),
1149        (S::ExtendedError, _) => Some((classify_discard(message)?, S::ExtendedError)),
1150        (S::CopyIn, F::CopyData(_)) => Some((O::CopyData, S::CopyIn)),
1151        (S::CopyIn, F::CopyDone) => Some((O::CopyDone, S::Extended { bound: true })),
1152        (S::CopyIn, F::CopyFail(_)) => Some((O::CopyFail, S::ExtendedError)),
1153        (S::CopyBoth, F::CopyData(_)) => Some((O::CopyData, S::CopyBoth)),
1154        (S::CopyBoth, F::CopyDone) => Some((O::CopyDone, S::CopyBoth)),
1155        _ => None,
1156    }
1157}
1158
1159fn frontend_phase(state: RequestState, response_head: Option<OperationKind>) -> FrontendPhase {
1160    match (state, response_head) {
1161        (RequestState::Ready, _) => FrontendPhase::Ready,
1162        (RequestState::Extended { .. }, _) => FrontendPhase::Building,
1163        (RequestState::ExtendedError, _) => FrontendPhase::ExtendedError,
1164        (RequestState::CopyIn, Some(OperationKind::Query)) => FrontendPhase::SimpleCopyIn,
1165        (RequestState::CopyIn, Some(OperationKind::Execute)) => FrontendPhase::ExtendedCopyIn,
1166        (RequestState::CopyBoth, Some(OperationKind::Query)) => FrontendPhase::SimpleCopyBoth,
1167        (RequestState::CopyBoth, Some(OperationKind::Execute)) => FrontendPhase::ExtendedCopyBoth,
1168        (RequestState::CopyIn | RequestState::CopyBoth, _) => {
1169            unreachable!("COPY phase must belong to Query or Execute")
1170        }
1171        (RequestState::CopyOut | RequestState::Terminated, _) => {
1172            unreachable!("non-accepting frontend phase cannot be prepared")
1173        }
1174    }
1175}
1176
1177fn classify_discard(message: &FrontendMessage) -> Option<OperationKind> {
1178    Some(match message {
1179        FrontendMessage::Parse(_) => OperationKind::Parse,
1180        FrontendMessage::Bind(_) => OperationKind::Bind,
1181        FrontendMessage::Describe(_) => OperationKind::Describe,
1182        FrontendMessage::Execute(_) => OperationKind::Execute,
1183        FrontendMessage::Close(_) => OperationKind::Close,
1184        FrontendMessage::Flush => OperationKind::Flush,
1185        FrontendMessage::Query(_) => OperationKind::Query,
1186        FrontendMessage::FunctionCall(_) => OperationKind::FunctionCall,
1187        FrontendMessage::CopyData(_) => OperationKind::CopyData,
1188        FrontendMessage::CopyDone => OperationKind::CopyDone,
1189        FrontendMessage::CopyFail(_) => OperationKind::CopyFail,
1190        FrontendMessage::Terminate => OperationKind::Terminate,
1191        FrontendMessage::PasswordResponse(_) => return None,
1192        FrontendMessage::Sync => unreachable!("Sync is classified before discard"),
1193    })
1194}
1195
1196const fn initial_response_state(kind: OperationKind) -> backend::RuntimeState {
1197    use OperationKind as O;
1198    match kind {
1199        O::Query => backend::RuntimeState::Simple,
1200        O::FunctionCall => backend::RuntimeState::FunctionResponse,
1201        O::Parse => backend::RuntimeState::ParseResponse,
1202        O::Bind => backend::RuntimeState::BindResponse,
1203        O::Describe => backend::RuntimeState::DescribeResponse,
1204        O::Execute => backend::RuntimeState::ExecuteResponse,
1205        O::Close => backend::RuntimeState::CloseResponse,
1206        O::Sync => backend::RuntimeState::SyncResponse,
1207        O::Flush | O::CopyData | O::CopyDone | O::CopyFail | O::Terminate => {
1208            backend::RuntimeState::Terminated
1209        }
1210    }
1211}
1212
1213fn response_fits(operation: Operation, message: &BackendMessage) -> bool {
1214    backend::project_internal(operation.response_state, message).is_some()
1215}
1216
1217fn response_is_terminal(kind: OperationKind, message: &BackendMessage) -> bool {
1218    use BackendMessage as B;
1219    use OperationKind as O;
1220    match kind {
1221        O::Query | O::FunctionCall | O::Sync => matches!(message, B::ReadyForQuery(_)),
1222        O::Parse => matches!(message, B::ParseComplete | B::ErrorResponse(_)),
1223        O::Bind => matches!(message, B::BindComplete | B::ErrorResponse(_)),
1224        O::Describe => matches!(
1225            message,
1226            B::RowDescription(_) | B::NoData | B::ErrorResponse(_)
1227        ),
1228        O::Execute => matches!(
1229            message,
1230            B::CommandComplete(_) | B::PortalSuspended | B::ErrorResponse(_)
1231        ),
1232        O::Close => matches!(message, B::CloseComplete | B::ErrorResponse(_)),
1233        O::CopyDone => matches!(
1234            message,
1235            B::CopyDone | B::CommandComplete(_) | B::ErrorResponse(_)
1236        ),
1237        O::CopyFail => matches!(message, B::ErrorResponse(_)),
1238        O::Flush | O::CopyData | O::Terminate => true,
1239    }
1240}
1241
1242fn is_extended_kind(kind: OperationKind) -> bool {
1243    !matches!(
1244        kind,
1245        OperationKind::Query | OperationKind::FunctionCall | OperationKind::Terminate
1246    )
1247}
1248
1249fn response_copy_state(state: backend::RuntimeState) -> Option<RequestState> {
1250    use backend::RuntimeState as S;
1251    match state {
1252        S::SimpleCopyIn | S::ExtendedCopyIn => Some(RequestState::CopyIn),
1253        S::SimpleCopyOut | S::ExtendedCopyOut => Some(RequestState::CopyOut),
1254        S::SimpleCopyBoth
1255        | S::SimpleCopyBothClientDone
1256        | S::SimpleCopyBothServerDone
1257        | S::ExtendedCopyBoth
1258        | S::ExtendedCopyBothClientDone
1259        | S::ExtendedCopyBothServerDone => Some(RequestState::CopyBoth),
1260        _ => None,
1261    }
1262}
1263
1264fn is_asynchronous(message: &BackendMessage) -> bool {
1265    Demux::is_asynchronous(message)
1266}