use std::{collections::VecDeque, sync::Arc};
use tokio::sync::Notify;
use crate::{
codec::{BackendMessage, FrontendMessage},
demux::{Demux, SessionItem},
grammar::backend,
};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct OperationId(u64);
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct NoPipeline;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BoundedPipeline {
max_operations: usize,
}
impl BoundedPipeline {
pub fn new(max_operations: usize) -> Result<Self, PipelineConfigError> {
if max_operations == 0 {
return Err(PipelineConfigError);
}
Ok(Self { max_operations })
}
#[must_use]
pub const fn max_operations(self) -> usize {
self.max_operations
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PipelineConfigError;
impl std::fmt::Display for PipelineConfigError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("pipeline operation limit must be non-zero")
}
}
impl std::error::Error for PipelineConfigError {}
mod private {
pub trait Sealed {}
}
pub trait PipelinePolicy: private::Sealed + Copy {
fn operation_limit(self) -> usize;
}
impl private::Sealed for NoPipeline {}
impl PipelinePolicy for NoPipeline {
fn operation_limit(self) -> usize {
1
}
}
impl private::Sealed for BoundedPipeline {}
impl PipelinePolicy for BoundedPipeline {
fn operation_limit(self) -> usize {
self.max_operations
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FrontendHandling {
Forward,
Local,
}
#[derive(Debug, Eq, PartialEq)]
pub enum FrontendAction {
Forward {
id: OperationId,
message: FrontendMessage,
},
Discard {
id: OperationId,
},
Backpressure(FrontendMessage),
}
#[derive(Debug, Eq, PartialEq)]
pub enum FrontendAdmission {
Immediate(FrontendAction),
Waiting(FrontendAction),
}
impl FrontendAdmission {
#[must_use]
pub fn into_action(self) -> FrontendAction {
match self {
Self::Immediate(action) | Self::Waiting(action) => action,
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum FrontendProjectionError {
Capacity(Box<FrontendMessage>),
Illegal {
state: PipelineState,
message: Box<FrontendMessage>,
},
}
#[derive(Debug, Eq, PartialEq)]
pub enum BackendAction {
Emit(BackendMessage),
Deferred(BackendMessage),
}
#[derive(Debug, Eq, PartialEq)]
pub struct BackendProjectionError {
pub state: PipelineState,
pub message: BackendMessage,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PipelineState {
Ready,
Extended,
ExtendedError,
CopyIn,
CopyOut,
CopyBoth,
Terminated,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum RequestState {
Ready,
Extended { bound: bool },
ExtendedError,
CopyIn,
CopyOut,
CopyBoth,
Terminated,
}
impl RequestState {
const fn public(self) -> PipelineState {
match self {
Self::Ready => PipelineState::Ready,
Self::Extended { .. } => PipelineState::Extended,
Self::ExtendedError => PipelineState::ExtendedError,
Self::CopyIn => PipelineState::CopyIn,
Self::CopyOut => PipelineState::CopyOut,
Self::CopyBoth => PipelineState::CopyBoth,
Self::Terminated => PipelineState::Terminated,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Origin {
Forwarded,
Local,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum OperationKind {
Query,
FunctionCall,
Parse,
Bind,
Describe,
Execute,
Close,
Flush,
Sync,
CopyData,
CopyDone,
CopyFail,
Terminate,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Operation {
id: OperationId,
kind: OperationKind,
origin: Origin,
discarded: bool,
}
#[derive(Debug)]
pub struct Pipeline<P = NoPipeline> {
policy: P,
operations: VecDeque<Operation>,
request_state: RequestState,
response_state: Option<PipelineState>,
next_id: u64,
changed: Arc<Notify>,
}
impl Default for Pipeline<NoPipeline> {
fn default() -> Self {
Self::new(NoPipeline)
}
}
impl<P: PipelinePolicy> Pipeline<P> {
#[must_use]
pub fn new(policy: P) -> Self {
Self {
policy,
operations: VecDeque::new(),
request_state: RequestState::Ready,
response_state: None,
next_id: 0,
changed: Arc::new(Notify::new()),
}
}
#[must_use]
pub fn state(&self) -> PipelineState {
self.response_state
.unwrap_or_else(|| self.request_state.public())
}
#[must_use]
pub fn len(&self) -> usize {
self.operations.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.operations.is_empty()
}
pub fn accept_frontend(
&mut self,
message: FrontendMessage,
handling: FrontendHandling,
) -> Result<FrontendAdmission, FrontendProjectionError> {
self.remove_inert_heads();
if self.operations.len() == self.policy.operation_limit() {
return Err(FrontendProjectionError::Capacity(Box::new(message)));
}
let Some((kind, next_state)) = project_frontend(self.request_state, &message) else {
return Err(FrontendProjectionError::Illegal {
state: self.state(),
message: Box::new(message),
});
};
let waiting = !self.operations.is_empty();
let id = OperationId(self.next_id);
self.next_id = self.next_id.saturating_add(1);
self.request_state = next_state;
let origin = match handling {
FrontendHandling::Forward => Origin::Forwarded,
FrontendHandling::Local => Origin::Local,
};
self.operations.push_back(Operation {
id,
kind,
origin,
discarded: matches!(self.request_state, RequestState::ExtendedError)
&& kind != OperationKind::Sync,
});
let action = match handling {
FrontendHandling::Forward => FrontendAction::Forward { id, message },
FrontendHandling::Local => FrontendAction::Discard { id },
};
Ok(if waiting {
FrontendAdmission::Waiting(action)
} else {
FrontendAdmission::Immediate(action)
})
}
pub fn project_frontend(
&mut self,
message: FrontendMessage,
handling: FrontendHandling,
) -> Result<FrontendAdmission, FrontendProjectionError> {
self.accept_frontend(message, handling)
}
pub fn frontend_action(
&mut self,
message: FrontendMessage,
handling: FrontendHandling,
) -> Result<FrontendAction, FrontendProjectionError> {
match self.accept_frontend(message, handling) {
Ok(admission) => Ok(admission.into_action()),
Err(FrontendProjectionError::Capacity(message)) => {
Ok(FrontendAction::Backpressure(*message))
}
Err(error @ FrontendProjectionError::Illegal { .. }) => Err(error),
}
}
pub fn accept_backend(
&mut self,
message: BackendMessage,
) -> Result<BackendAction, BackendProjectionError> {
self.accept_response(None, message)
}
pub fn accept_session_item(
&mut self,
item: SessionItem,
) -> Result<BackendAction, BackendProjectionError> {
let message = match item {
SessionItem::Message(message) => message,
SessionItem::ReadyForQuery { status, .. } => BackendMessage::ReadyForQuery(status),
SessionItem::CommandComplete { tag, .. } => BackendMessage::CommandComplete(tag),
};
self.accept_backend(message)
}
pub fn try_emit_local(
&mut self,
id: OperationId,
message: BackendMessage,
) -> Result<BackendAction, BackendProjectionError> {
self.accept_response(Some(id), message)
}
pub async fn wait_until_emittable(&self, id: OperationId) {
loop {
let notified = self.changed.notified();
if self
.operations
.front()
.is_some_and(|operation| operation.id == id)
{
return;
}
notified.await;
}
}
fn accept_response(
&mut self,
local_id: Option<OperationId>,
message: BackendMessage,
) -> Result<BackendAction, BackendProjectionError> {
if is_asynchronous(&message) {
return Ok(BackendAction::Emit(message));
}
self.remove_inert_heads();
let Some(head) = self.operations.front().copied() else {
return Err(BackendProjectionError {
state: self.state(),
message,
});
};
if let Some(id) = local_id {
if head.id != id {
return Ok(BackendAction::Deferred(message));
}
if head.origin != Origin::Local {
return Err(BackendProjectionError {
state: self.state(),
message,
});
}
} else if head.origin == Origin::Local {
if self.operations.iter().skip(1).any(|operation| {
operation.origin == Origin::Forwarded && response_fits(operation.kind, &message)
}) {
return Ok(BackendAction::Deferred(message));
}
return Err(BackendProjectionError {
state: self.state(),
message,
});
}
if head.discarded || !response_fits(head.kind, &message) {
if self
.operations
.iter()
.skip(1)
.any(|operation| response_fits(operation.kind, &message))
{
return Ok(BackendAction::Deferred(message));
}
return Err(BackendProjectionError {
state: self.state(),
message,
});
}
let terminal = response_is_terminal(head.kind, &message);
let error = matches!(message, BackendMessage::ErrorResponse(_));
let copy_state = copy_state(&message);
if terminal {
self.operations.pop_front();
if error && is_extended_kind(head.kind) {
self.enter_extended_error();
}
}
if let Some(state) = copy_state {
self.response_state = Some(state.public());
if matches!(state, RequestState::CopyIn | RequestState::CopyBoth) {
self.request_state = state;
}
}
if terminal {
self.response_state = None;
match head.kind {
OperationKind::Sync | OperationKind::Query => {
self.request_state = RequestState::Ready;
}
OperationKind::Execute if !error => {
self.request_state = RequestState::Extended { bound: true };
}
_ => {}
}
}
self.remove_inert_heads();
self.changed.notify_waiters();
Ok(BackendAction::Emit(message))
}
fn enter_extended_error(&mut self) {
self.request_state = RequestState::ExtendedError;
self.response_state = None;
for operation in &mut self.operations {
if operation.kind == OperationKind::Sync {
break;
}
operation.discarded = true;
}
}
fn remove_inert_heads(&mut self) {
let previous_len = self.operations.len();
while self.operations.front().is_some_and(|operation| {
operation.kind == OperationKind::Flush
|| operation.kind == OperationKind::CopyData
|| operation.kind == OperationKind::CopyDone
|| operation.kind == OperationKind::CopyFail
|| operation.kind == OperationKind::Terminate
|| operation.discarded
}) {
self.operations.pop_front();
}
if self.operations.len() != previous_len {
self.changed.notify_waiters();
}
}
}
fn project_frontend(
state: RequestState,
message: &FrontendMessage,
) -> Option<(OperationKind, RequestState)> {
use FrontendMessage as F;
use OperationKind as O;
use RequestState as S;
let generated_state = match state {
S::Ready => backend::RuntimeState::Ready,
S::Extended { .. } => backend::RuntimeState::Building,
S::ExtendedError => backend::RuntimeState::ExtendedError,
S::CopyIn => backend::RuntimeState::ExtendedCopyIn,
S::CopyOut => backend::RuntimeState::ExtendedCopyOut,
S::CopyBoth => backend::RuntimeState::ExtendedCopyBoth,
S::Terminated => backend::RuntimeState::Terminated,
};
backend::project_external(generated_state, message)?;
match (state, message) {
(S::Ready, F::Query(_)) => Some((O::Query, S::Ready)),
(S::Ready, F::FunctionCall(_)) => Some((O::FunctionCall, S::Ready)),
(S::Ready, F::Parse(_)) => Some((O::Parse, S::Extended { bound: false })),
(S::Ready | S::Extended { .. }, F::Bind(_)) => Some((O::Bind, S::Extended { bound: true })),
(S::Ready, F::Describe(_)) => Some((O::Describe, S::Extended { bound: false })),
(S::Ready | S::Extended { .. }, F::Execute(_)) => {
Some((O::Execute, S::Extended { bound: true }))
}
(S::Ready, F::Close(_)) => Some((O::Close, S::Extended { bound: false })),
(S::Ready, F::Terminate) => Some((O::Terminate, S::Terminated)),
(S::Extended { bound }, F::Parse(_)) => Some((O::Parse, S::Extended { bound })),
(S::Extended { bound }, F::Describe(_)) => Some((O::Describe, S::Extended { bound })),
(S::Extended { bound }, F::Close(_)) => Some((O::Close, S::Extended { bound })),
(S::Extended { bound }, F::Flush) => Some((O::Flush, S::Extended { bound })),
(S::Extended { .. } | S::ExtendedError, F::Sync) => Some((O::Sync, S::Ready)),
(S::ExtendedError, _) => Some((classify_discard(message)?, S::ExtendedError)),
(S::CopyIn, F::CopyData(_)) => Some((O::CopyData, S::CopyIn)),
(S::CopyIn, F::CopyDone) => Some((O::CopyDone, S::Extended { bound: true })),
(S::CopyIn, F::CopyFail(_)) => Some((O::CopyFail, S::ExtendedError)),
(S::CopyBoth, F::CopyData(_)) => Some((O::CopyData, S::CopyBoth)),
(S::CopyBoth, F::CopyDone) => Some((O::CopyDone, S::CopyBoth)),
_ => None,
}
}
fn classify_discard(message: &FrontendMessage) -> Option<OperationKind> {
Some(match message {
FrontendMessage::Parse(_) => OperationKind::Parse,
FrontendMessage::Bind(_) => OperationKind::Bind,
FrontendMessage::Describe(_) => OperationKind::Describe,
FrontendMessage::Execute(_) => OperationKind::Execute,
FrontendMessage::Close(_) => OperationKind::Close,
FrontendMessage::Flush => OperationKind::Flush,
FrontendMessage::Query(_) => OperationKind::Query,
FrontendMessage::FunctionCall(_) => OperationKind::FunctionCall,
FrontendMessage::CopyData(_) => OperationKind::CopyData,
FrontendMessage::CopyDone => OperationKind::CopyDone,
FrontendMessage::CopyFail(_) => OperationKind::CopyFail,
FrontendMessage::Terminate => OperationKind::Terminate,
FrontendMessage::PasswordResponse(_) => return None,
FrontendMessage::Sync => unreachable!("Sync is classified before discard"),
})
}
fn response_fits(kind: OperationKind, message: &BackendMessage) -> bool {
use BackendMessage as B;
use OperationKind as O;
match kind {
O::Query => matches!(
message,
B::RowDescription(_)
| B::DataRow(_)
| B::CommandComplete(_)
| B::EmptyQueryResponse
| B::CopyInResponse(_)
| B::CopyOutResponse(_)
| B::CopyBothResponse(_)
| B::CopyData(_)
| B::CopyDone
| B::ErrorResponse(_)
| B::ReadyForQuery(_)
),
O::FunctionCall => matches!(
message,
B::FunctionCallResponse(_) | B::ErrorResponse(_) | B::ReadyForQuery(_)
),
O::Parse => matches!(message, B::ParseComplete | B::ErrorResponse(_)),
O::Bind => matches!(message, B::BindComplete | B::ErrorResponse(_)),
O::Describe => matches!(
message,
B::ParameterDescription(_) | B::RowDescription(_) | B::NoData | B::ErrorResponse(_)
),
O::Execute => matches!(
message,
B::RowDescription(_)
| B::DataRow(_)
| B::EmptyQueryResponse
| B::CommandComplete(_)
| B::PortalSuspended
| B::CopyInResponse(_)
| B::CopyOutResponse(_)
| B::CopyBothResponse(_)
| B::CopyData(_)
| B::CopyDone
| B::ErrorResponse(_)
),
O::Close => matches!(message, B::CloseComplete | B::ErrorResponse(_)),
O::Sync => matches!(message, B::ReadyForQuery(_)),
O::CopyDone => matches!(
message,
B::CopyDone | B::CommandComplete(_) | B::ErrorResponse(_)
),
O::CopyFail => matches!(message, B::ErrorResponse(_)),
O::Flush | O::CopyData | O::Terminate => false,
}
}
fn response_is_terminal(kind: OperationKind, message: &BackendMessage) -> bool {
use BackendMessage as B;
use OperationKind as O;
match kind {
O::Query | O::FunctionCall | O::Sync => matches!(message, B::ReadyForQuery(_)),
O::Parse => matches!(message, B::ParseComplete | B::ErrorResponse(_)),
O::Bind => matches!(message, B::BindComplete | B::ErrorResponse(_)),
O::Describe => matches!(
message,
B::RowDescription(_) | B::NoData | B::ErrorResponse(_)
),
O::Execute => matches!(
message,
B::CommandComplete(_) | B::PortalSuspended | B::ErrorResponse(_)
),
O::Close => matches!(message, B::CloseComplete | B::ErrorResponse(_)),
O::CopyDone => matches!(
message,
B::CopyDone | B::CommandComplete(_) | B::ErrorResponse(_)
),
O::CopyFail => matches!(message, B::ErrorResponse(_)),
O::Flush | O::CopyData | O::Terminate => true,
}
}
fn is_extended_kind(kind: OperationKind) -> bool {
!matches!(
kind,
OperationKind::Query | OperationKind::FunctionCall | OperationKind::Terminate
)
}
fn copy_state(message: &BackendMessage) -> Option<RequestState> {
match message {
BackendMessage::CopyInResponse(_) => Some(RequestState::CopyIn),
BackendMessage::CopyOutResponse(_) => Some(RequestState::CopyOut),
BackendMessage::CopyBothResponse(_) => Some(RequestState::CopyBoth),
_ => None,
}
}
fn is_asynchronous(message: &BackendMessage) -> bool {
Demux::is_asynchronous(message)
}