use std::fmt::{self, Debug, Formatter};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::{Duration, Instant};
use crate::ErrorCode;
use crate::ingress::AckLevel;
use crate::ingress::QwpWsSenderError;
use crate::ingress::buffer::{Buffer, QwpWsColumnarBuffer, QwpWsEncodeScratch, SymbolGlobalDict};
use crate::ingress::sender::qwp_ws::{
SyncQwpWsHandlerState, publish_qwp_ws_payload_background, qwp_ws_acked_fsn_background,
qwp_ws_begin_close_background, qwp_ws_check_error_background,
qwp_ws_drain_to_deadline_background, qwp_ws_is_terminal_background, qwp_ws_ok_fsn_background,
qwp_ws_poll_sender_error_background, qwp_ws_poll_sender_error_notification_background,
qwp_ws_published_fsn_background, qwp_ws_sender_errors_dropped_background,
};
use crate::ingress::sender::qwp_ws_sfa_publisher::{SfaForegroundPublisher, SfaPublishOutcome};
#[cfg(feature = "arrow-ingress")]
use crate::ingress::{ColumnName, TableName};
use crate::{Result, error};
#[cfg(feature = "arrow-ingress")]
use super::arrow_batch::{self, ArrowColumnOverride, ArrowTsSource};
use super::chunk::Chunk;
use super::conn::{ColumnConn, PublishError};
use super::encoder;
#[cfg(feature = "arrow-ingress")]
use arrow::array::RecordBatch;
fn classify_flush_error(err: crate::Error) -> crate::Error {
if err.code() == ErrorCode::SocketError {
return crate::Error::new(ErrorCode::FailoverRetry, err.msg().to_owned());
}
err
}
enum FrameOutcome {
Published,
TooLarge(crate::Error),
NoSlot(crate::Error),
}
#[cfg(feature = "arrow-ingress")]
struct ArrowFrameSpec<'a> {
table: TableName<'a>,
batch: &'a RecordBatch,
ts: ArrowTsSource,
overrides: &'a [ArrowColumnOverride<'a>],
}
fn split_mid(row_count: usize) -> Option<usize> {
if row_count <= 8 {
return None;
}
let mid = (row_count / 2) & !7;
Some(if mid == 0 { 8 } else { mid })
}
#[derive(Clone, Copy)]
struct SfaFrameCaps {
hard: usize,
soft: usize,
}
impl SfaFrameCaps {
fn for_range(self, row_count: usize) -> usize {
if split_mid(row_count).is_some() {
self.soft
} else {
self.hard
}
}
}
fn sfa_frame_size_error(encoded_len: usize, frame_cap: usize) -> crate::Error {
error::fmt!(
BatchTooLarge,
"QWP frame ({} bytes) exceeds the store-and-forward per-frame cap ({} bytes, \
the smaller of max_buf_size and the sf_max_segment_bytes segment payload capacity)",
encoded_len,
frame_cap
)
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum WaitForAck {
No,
Yes(AckLevel),
}
#[derive(Debug)]
#[doc(hidden)]
#[non_exhaustive]
pub enum FlushFailure {
NotDelivered(crate::Error),
DeliveryUnknown(crate::Error),
}
impl FlushFailure {
#[doc(hidden)]
pub fn into_error(self) -> crate::Error {
match self {
FlushFailure::NotDelivered(e) => e,
FlushFailure::DeliveryUnknown(e) => e.with_in_doubt(true),
}
}
#[doc(hidden)]
#[must_use]
pub fn is_not_delivered(&self) -> bool {
matches!(self, FlushFailure::NotDelivered(_))
}
}
fn direct_not_delivered(e: crate::Error) -> FlushFailure {
FlushFailure::NotDelivered(classify_flush_error(e))
}
fn direct_delivery_unknown(e: crate::Error) -> FlushFailure {
FlushFailure::DeliveryUnknown(classify_flush_error(e))
}
fn deny_retry_after_partial(f: FlushFailure) -> FlushFailure {
match f {
FlushFailure::NotDelivered(e) => FlushFailure::DeliveryUnknown(e),
other => other,
}
}
pub struct PooledSenderCore {
backend: Box<SfaBackend>,
}
#[doc(hidden)]
pub struct DirectSenderCore {
backend: Box<DirectColumnBackend>,
}
struct DirectColumnBackend {
conn: ColumnConn,
symbol_dict: SymbolGlobalDict,
scratch: encoder::EncodeScratch,
first_frame_sent: bool,
commit_since_sync: bool,
}
struct SfaBackend {
foreground: SfaForegroundPublisher,
state: SyncQwpWsHandlerState,
buffer_scratch: QwpWsEncodeScratch,
scratch: encoder::EncodeScratch,
max_buf_size: usize,
request_durable_ack: bool,
sync_timeout: Duration,
last_ok_sync_boundary: Option<u64>,
last_durable_sync_boundary: Option<u64>,
drop_on_return: bool,
}
impl Debug for PooledSenderCore {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("PooledSenderCore")
.field(
"must_close",
&(self.backend.drop_on_return
|| qwp_ws_is_terminal_background(&self.backend.state)),
)
.finish()
}
}
impl Debug for DirectSenderCore {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("DirectSenderCore")
.field("must_close", &self.backend.conn.must_close())
.field("in_flight", &self.backend.conn.in_flight())
.finish()
}
}
impl PooledSenderCore {
pub(crate) fn new_store_and_forward(
mut state: SyncQwpWsHandlerState,
max_buf_size: usize,
request_durable_ack: bool,
sync_timeout: Duration,
) -> Result<Self> {
let delta_dict_enabled = state.delta_dict_enabled;
let persisted_symbol_dict = state.persisted_symbol_dict.take();
let mut foreground = SfaForegroundPublisher::new(delta_dict_enabled, persisted_symbol_dict);
if delta_dict_enabled {
let recovered = std::mem::take(&mut state.recovered_dict_entries);
foreground.seed(&recovered, state.recovered_dict_count)?;
}
state.release_dormant_encoder_dict();
Ok(Self {
backend: Box::new(SfaBackend {
foreground,
state,
buffer_scratch: QwpWsEncodeScratch::new(),
scratch: encoder::EncodeScratch::new(),
max_buf_size,
request_durable_ack,
sync_timeout,
last_ok_sync_boundary: None,
last_durable_sync_boundary: None,
drop_on_return: false,
}),
})
}
pub(crate) fn rebase_lease_observation(&mut self) {
let sfa = &mut self.backend;
if let Ok(published) = qwp_ws_published_fsn_background(&sfa.state) {
sfa.last_ok_sync_boundary = published;
sfa.last_durable_sync_boundary = published;
}
while let Ok(Some(_)) = qwp_ws_poll_sender_error_background(&sfa.state) {}
while let Ok(Some(_)) = qwp_ws_poll_sender_error_notification_background(&sfa.state) {}
}
pub fn poll_error(&self) -> Result<Option<QwpWsSenderError>> {
qwp_ws_poll_sender_error_background(&self.backend.state)
}
pub fn error_events_dropped(&self) -> Result<u64> {
qwp_ws_sender_errors_dropped_background(&self.backend.state)
}
#[must_use]
pub fn must_close(&self) -> bool {
self.backend.drop_on_return || qwp_ws_is_terminal_background(&self.backend.state)
}
pub fn mark_must_close(&mut self) {
self.backend.drop_on_return = true;
}
pub fn effective_frame_cap(&self) -> (usize, bool) {
self.backend.effective_hard_frame_cap()
}
pub(crate) fn sfa_fully_delivered(&self, durable: bool) -> bool {
let sfa = &self.backend;
if qwp_ws_is_terminal_background(&sfa.state) {
return true;
}
let Ok(Some(published)) = qwp_ws_published_fsn_background(&sfa.state) else {
return true;
};
let watermark = if durable {
qwp_ws_acked_fsn_background(&sfa.state)
} else {
qwp_ws_ok_fsn_background(&sfa.state)
};
matches!(watermark, Ok(Some(w)) if w >= published)
}
pub(crate) fn begin_close(&self) {
qwp_ws_begin_close_background(&self.backend.state);
}
pub(crate) fn drain_to_deadline(&mut self, deadline: Option<Instant>) -> crate::Result<()> {
qwp_ws_drain_to_deadline_background(&mut self.backend.state, deadline)
}
pub fn flush(&mut self, chunk: &mut Chunk<'_>) -> Result<()> {
self.backend
.flush_chunk(chunk, WaitForAck::No)
.map_err(FlushFailure::into_error)
}
pub fn flush_buffer(&mut self, buffer: &mut Buffer) -> Result<()> {
self.flush_buffer_and_get_fsn(buffer).map(|_| ())
}
pub fn flush_buffer_and_keep(&mut self, buffer: &Buffer) -> Result<()> {
self.flush_buffer_and_keep_and_get_fsn(buffer).map(|_| ())
}
pub fn flush_buffer_and_get_fsn(&mut self, buffer: &mut Buffer) -> Result<Option<u64>> {
let fsn = self.publish_buffer(buffer, None)?;
buffer.clear();
Ok(fsn)
}
pub fn flush_buffer_and_keep_and_get_fsn(&mut self, buffer: &Buffer) -> Result<Option<u64>> {
self.publish_buffer(buffer, None)
}
pub fn flush_buffer_and_wait(
&mut self,
buffer: &mut Buffer,
ack_level: AckLevel,
) -> Result<()> {
let boundary = self.publish_buffer(buffer, Some(ack_level))?;
buffer.clear();
let sfa = &mut self.backend;
match boundary {
Some(fsn) => sfa
.wait_for_boundary(ack_level, fsn, sfa.sync_timeout)
.map_err(FlushFailure::DeliveryUnknown)
.map_err(FlushFailure::into_error),
None => sfa.wait(ack_level, sfa.sync_timeout),
}
}
fn publish_buffer(
&mut self,
buffer: &Buffer,
ack_level: Option<AckLevel>,
) -> Result<Option<u64>> {
let qwp = buffer.as_qwp_ws().ok_or_else(|| {
error::fmt!(
InvalidApiCall,
"Pooled QWP ingestion requires a QWP/WebSocket buffer created by `QuestDb::new_buffer()`."
)
})?;
self.backend
.publish_buffer(qwp, ack_level)
.map_err(FlushFailure::into_error)
}
pub fn flush_and_get_fsn(&mut self, chunk: &mut Chunk<'_>) -> Result<Option<u64>> {
self.backend
.flush_chunk_and_get_fsn(chunk)
.map(Some)
.map_err(FlushFailure::into_error)
}
pub fn flush_and_wait(&mut self, chunk: &mut Chunk<'_>, ack_level: AckLevel) -> Result<()> {
self.backend
.flush_chunk(chunk, WaitForAck::Yes(ack_level))
.map_err(FlushFailure::into_error)
}
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_now<'t, T>(
&mut self,
table: T,
batch: &RecordBatch,
overrides: &[ArrowColumnOverride<'_>],
) -> Result<()>
where
T: TryInto<TableName<'t>>,
crate::Error: From<T::Error>,
{
let table: TableName<'t> = table.try_into()?;
self.flush_arrow_batch_dispatch(
table,
batch,
ArrowTsSource::ServerNow,
overrides,
WaitForAck::No,
)
.map_err(FlushFailure::into_error)
}
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_now_and_get_fsn<'t, T>(
&mut self,
table: T,
batch: &RecordBatch,
overrides: &[ArrowColumnOverride<'_>],
) -> Result<Option<u64>>
where
T: TryInto<TableName<'t>>,
crate::Error: From<T::Error>,
{
let table: TableName<'t> = table.try_into()?;
self.flush_arrow_batch_dispatch_get_fsn(table, batch, ArrowTsSource::ServerNow, overrides)
.map_err(FlushFailure::into_error)
}
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_now_and_wait<'t, T>(
&mut self,
table: T,
batch: &RecordBatch,
overrides: &[ArrowColumnOverride<'_>],
ack_level: AckLevel,
) -> Result<()>
where
T: TryInto<TableName<'t>>,
crate::Error: From<T::Error>,
{
let table: TableName<'t> = table.try_into()?;
self.flush_arrow_batch_dispatch(
table,
batch,
ArrowTsSource::ServerNow,
overrides,
WaitForAck::Yes(ack_level),
)
.map_err(FlushFailure::into_error)
}
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_column<'t, T>(
&mut self,
table: T,
batch: &RecordBatch,
ts_column: ColumnName<'_>,
overrides: &[ArrowColumnOverride<'_>],
) -> Result<()>
where
T: TryInto<TableName<'t>>,
crate::Error: From<T::Error>,
{
let table: TableName<'t> = table.try_into()?;
let ts_col_idx = arrow_batch::resolve_ts_column(batch, ts_column)?;
self.flush_arrow_batch_dispatch(
table,
batch,
ArrowTsSource::Column(ts_col_idx),
overrides,
WaitForAck::No,
)
.map_err(FlushFailure::into_error)
}
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_scalar_nanos<'t, T>(
&mut self,
table: T,
batch: &RecordBatch,
nanos: i64,
overrides: &[ArrowColumnOverride<'_>],
) -> Result<()>
where
T: TryInto<TableName<'t>>,
crate::Error: From<T::Error>,
{
let table: TableName<'t> = table.try_into()?;
self.flush_arrow_batch_dispatch(
table,
batch,
ArrowTsSource::ScalarNanos(nanos),
overrides,
WaitForAck::No,
)
.map_err(FlushFailure::into_error)
}
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_column_and_get_fsn<'t, T>(
&mut self,
table: T,
batch: &RecordBatch,
ts_column: ColumnName<'_>,
overrides: &[ArrowColumnOverride<'_>],
) -> Result<Option<u64>>
where
T: TryInto<TableName<'t>>,
crate::Error: From<T::Error>,
{
let table: TableName<'t> = table.try_into()?;
let ts_col_idx = arrow_batch::resolve_ts_column(batch, ts_column)?;
self.flush_arrow_batch_dispatch_get_fsn(
table,
batch,
ArrowTsSource::Column(ts_col_idx),
overrides,
)
.map_err(FlushFailure::into_error)
}
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_column_and_wait<'t, T>(
&mut self,
table: T,
batch: &RecordBatch,
ts_column: ColumnName<'_>,
overrides: &[ArrowColumnOverride<'_>],
ack_level: AckLevel,
) -> Result<()>
where
T: TryInto<TableName<'t>>,
crate::Error: From<T::Error>,
{
let table: TableName<'t> = table.try_into()?;
let ts_col_idx = arrow_batch::resolve_ts_column(batch, ts_column)?;
self.flush_arrow_batch_dispatch(
table,
batch,
ArrowTsSource::Column(ts_col_idx),
overrides,
WaitForAck::Yes(ack_level),
)
.map_err(FlushFailure::into_error)
}
#[cfg(feature = "arrow-ingress")]
fn flush_arrow_batch_dispatch(
&mut self,
table: TableName<'_>,
batch: &RecordBatch,
ts: ArrowTsSource,
overrides: &[ArrowColumnOverride<'_>],
wait: WaitForAck,
) -> std::result::Result<(), FlushFailure> {
self.backend
.flush_arrow_batch(table, batch, ts, overrides, wait)
}
#[cfg(feature = "arrow-ingress")]
fn flush_arrow_batch_dispatch_get_fsn(
&mut self,
table: TableName<'_>,
batch: &RecordBatch,
ts: ArrowTsSource,
overrides: &[ArrowColumnOverride<'_>],
) -> std::result::Result<Option<u64>, FlushFailure> {
self.backend
.flush_arrow_batch_and_get_fsn(table, batch, ts, overrides)
.map(Some)
}
#[doc(hidden)]
pub fn validate_ack_level(&self, ack_level: AckLevel) -> Result<()> {
self.backend.validate_ack_level(ack_level)
}
#[doc(hidden)]
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_now_and_wait_ffi(
&mut self,
table: TableName<'_>,
batch: &RecordBatch,
overrides: &[ArrowColumnOverride<'_>],
ack_level: AckLevel,
) -> std::result::Result<(), FlushFailure> {
self.flush_arrow_batch_dispatch(
table,
batch,
ArrowTsSource::ServerNow,
overrides,
WaitForAck::Yes(ack_level),
)
}
#[doc(hidden)]
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_column_and_wait_ffi(
&mut self,
table: TableName<'_>,
batch: &RecordBatch,
ts_column: ColumnName<'_>,
overrides: &[ArrowColumnOverride<'_>],
ack_level: AckLevel,
) -> std::result::Result<(), FlushFailure> {
let ts_col_idx =
arrow_batch::resolve_ts_column(batch, ts_column).map_err(FlushFailure::NotDelivered)?;
self.flush_arrow_batch_dispatch(
table,
batch,
ArrowTsSource::Column(ts_col_idx),
overrides,
WaitForAck::Yes(ack_level),
)
}
pub fn sync(&mut self, ack_level: AckLevel) -> Result<()> {
self.backend.sync(ack_level)
}
pub fn wait(&mut self, ack_level: AckLevel, timeout: Duration) -> Result<()> {
self.backend.wait(ack_level, timeout)
}
pub fn published_fsn(&self) -> Result<Option<u64>> {
self.backend.published_fsn()
}
pub fn acked_fsn(&self) -> Result<Option<u64>> {
self.backend.acked_fsn()
}
}
impl DirectSenderCore {
pub(crate) fn new(
conn: ColumnConn,
symbol_dict: SymbolGlobalDict,
scratch: encoder::EncodeScratch,
first_frame_sent: bool,
) -> Self {
Self {
backend: Box::new(DirectColumnBackend {
conn,
symbol_dict,
scratch,
first_frame_sent,
commit_since_sync: false,
}),
}
}
#[must_use]
pub fn must_close(&self) -> bool {
self.backend.conn.must_close()
}
pub fn mark_must_close(&mut self) {
self.backend.conn.mark_must_close();
}
pub(crate) fn in_flight(&self) -> u32 {
self.backend.conn.in_flight()
}
pub(crate) fn transport_dead(&self) -> bool {
self.backend.conn.transport_dead()
}
pub(crate) fn can_drain_in_flight(&self) -> bool {
self.backend.conn.can_drain_in_flight()
}
pub(crate) fn endpoint_idx(&self) -> usize {
self.backend.conn.endpoint_idx()
}
pub fn flush(&mut self, chunk: &mut Chunk<'_>) -> Result<()> {
self.backend
.flush_inner(chunk, WaitForAck::No)
.map_err(FlushFailure::into_error)
}
pub fn flush_and_wait(&mut self, chunk: &mut Chunk<'_>, ack_level: AckLevel) -> Result<()> {
self.backend
.flush_inner(chunk, WaitForAck::Yes(ack_level))
.map_err(FlushFailure::into_error)
}
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_now<'t, T>(
&mut self,
table: T,
batch: &RecordBatch,
overrides: &[ArrowColumnOverride<'_>],
) -> Result<()>
where
T: TryInto<TableName<'t>>,
crate::Error: From<T::Error>,
{
let table: TableName<'t> = table.try_into()?;
self.backend
.flush_arrow_batch_inner(
table,
batch,
ArrowTsSource::ServerNow,
overrides,
WaitForAck::No,
)
.map_err(FlushFailure::into_error)
}
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_column<'t, T>(
&mut self,
table: T,
batch: &RecordBatch,
ts_column: ColumnName<'_>,
overrides: &[ArrowColumnOverride<'_>],
) -> Result<()>
where
T: TryInto<TableName<'t>>,
crate::Error: From<T::Error>,
{
let table: TableName<'t> = table.try_into()?;
let ts_col_idx = arrow_batch::resolve_ts_column(batch, ts_column)?;
self.backend
.flush_arrow_batch_inner(
table,
batch,
ArrowTsSource::Column(ts_col_idx),
overrides,
WaitForAck::No,
)
.map_err(FlushFailure::into_error)
}
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_scalar_nanos<'t, T>(
&mut self,
table: T,
batch: &RecordBatch,
nanos: i64,
overrides: &[ArrowColumnOverride<'_>],
) -> Result<()>
where
T: TryInto<TableName<'t>>,
crate::Error: From<T::Error>,
{
let table: TableName<'t> = table.try_into()?;
self.backend
.flush_arrow_batch_inner(
table,
batch,
ArrowTsSource::ScalarNanos(nanos),
overrides,
WaitForAck::No,
)
.map_err(FlushFailure::into_error)
}
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_now_and_wait<'t, T>(
&mut self,
table: T,
batch: &RecordBatch,
overrides: &[ArrowColumnOverride<'_>],
ack_level: AckLevel,
) -> Result<()>
where
T: TryInto<TableName<'t>>,
crate::Error: From<T::Error>,
{
let table: TableName<'t> = table.try_into()?;
self.backend
.flush_arrow_batch_inner(
table,
batch,
ArrowTsSource::ServerNow,
overrides,
WaitForAck::Yes(ack_level),
)
.map_err(FlushFailure::into_error)
}
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_column_and_wait<'t, T>(
&mut self,
table: T,
batch: &RecordBatch,
ts_column: ColumnName<'_>,
overrides: &[ArrowColumnOverride<'_>],
ack_level: AckLevel,
) -> Result<()>
where
T: TryInto<TableName<'t>>,
crate::Error: From<T::Error>,
{
let table: TableName<'t> = table.try_into()?;
let ts_col_idx = arrow_batch::resolve_ts_column(batch, ts_column)?;
self.backend
.flush_arrow_batch_inner(
table,
batch,
ArrowTsSource::Column(ts_col_idx),
overrides,
WaitForAck::Yes(ack_level),
)
.map_err(FlushFailure::into_error)
}
#[doc(hidden)]
pub fn validate_ack_level(&self, ack_level: AckLevel) -> Result<()> {
self.backend.conn.validate_ack_level(ack_level)
}
#[doc(hidden)]
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_now_and_wait_ffi(
&mut self,
table: TableName<'_>,
batch: &RecordBatch,
overrides: &[ArrowColumnOverride<'_>],
ack_level: AckLevel,
) -> std::result::Result<(), FlushFailure> {
self.backend.flush_arrow_batch_inner(
table,
batch,
ArrowTsSource::ServerNow,
overrides,
WaitForAck::Yes(ack_level),
)
}
#[doc(hidden)]
#[cfg(feature = "arrow-ingress")]
pub fn flush_arrow_batch_at_column_and_wait_ffi(
&mut self,
table: TableName<'_>,
batch: &RecordBatch,
ts_column: ColumnName<'_>,
overrides: &[ArrowColumnOverride<'_>],
ack_level: AckLevel,
) -> std::result::Result<(), FlushFailure> {
let ts_col_idx =
arrow_batch::resolve_ts_column(batch, ts_column).map_err(FlushFailure::NotDelivered)?;
self.backend.flush_arrow_batch_inner(
table,
batch,
ArrowTsSource::Column(ts_col_idx),
overrides,
WaitForAck::Yes(ack_level),
)
}
pub fn sync(&mut self, ack_level: AckLevel) -> Result<()> {
self.backend.sync(ack_level)
}
}
impl DirectColumnBackend {
fn sync(&mut self, ack_level: AckLevel) -> Result<()> {
let first_frame_sent = self.first_frame_sent;
let mut commit_chunk = Chunk::new("");
let mut result = self.flush_inner(&mut commit_chunk, WaitForAck::Yes(ack_level));
self.first_frame_sent = first_frame_sent;
if self.commit_since_sync {
result = result.map_err(deny_retry_after_partial);
}
result.map_err(FlushFailure::into_error)
}
fn flush_inner(
&mut self,
chunk: &mut Chunk<'_>,
wait: WaitForAck,
) -> std::result::Result<(), FlushFailure> {
let defer_commit = match wait {
WaitForAck::No => self.first_frame_sent,
WaitForAck::Yes(level) => {
self.conn
.validate_ack_level(level)
.map_err(direct_not_delivered)?;
false
}
};
self.conn.try_drain_acks().map_err(direct_not_delivered)?;
match self.publish_frame(chunk, None, defer_commit)? {
FrameOutcome::Published => {}
FrameOutcome::NoSlot(err) => return Err(FlushFailure::NotDelivered(err)),
FrameOutcome::TooLarge(err) => {
let row_count = chunk.row_count();
match split_mid(row_count) {
Some(mid) => {
let mut committed = false;
let mut result = self.publish_split(chunk, 0, mid, true, &mut committed);
if result.is_ok() {
result = self.publish_split(
chunk,
mid,
row_count - mid,
defer_commit,
&mut committed,
);
}
if let Err(e) = result {
self.conn.mark_must_close();
return Err(if committed {
deny_retry_after_partial(e)
} else {
e
});
}
}
None => return Err(direct_not_delivered(err)),
}
}
}
chunk.clear();
if let WaitForAck::Yes(level) = wait {
self.conn
.sync_all_acks(level)
.map_err(direct_delivery_unknown)?;
self.commit_since_sync = false;
}
Ok(())
}
fn publish_frame(
&mut self,
chunk: &Chunk<'_>,
range: Option<(usize, usize)>,
defer_commit: bool,
) -> std::result::Result<FrameOutcome, FlushFailure> {
if defer_commit && !self.conn.has_sync_commit_slot() {
return Ok(FrameOutcome::NoSlot(error::fmt!(
InvalidApiCall,
"column sender deferred flush capacity exhausted; call sync() \
before flushing more chunks."
)));
}
if self.conn.at_in_flight_cap() {
self.conn
.drain_one_ack_blocking()
.map_err(direct_not_delivered)?;
}
let dict_mark = self.symbol_dict.mark();
let result = self.conn.publish_qwp(|out| match range {
None => encoder::encode_chunk_into(
out,
chunk,
&mut self.symbol_dict,
&mut self.scratch,
defer_commit,
),
Some((offset, count)) => {
let view = unsafe { chunk.slice_rows(offset, count) };
encoder::encode_chunk_into(
out,
&view,
&mut self.symbol_dict,
&mut self.scratch,
defer_commit,
)
}
});
match result {
Ok(published) => {
self.conn.push_pending(published.fsn);
self.first_frame_sent = true;
Ok(FrameOutcome::Published)
}
Err(PublishError::BeforeWrite(e)) if e.code() == ErrorCode::BatchTooLarge => {
self.symbol_dict.rollback(dict_mark);
Ok(FrameOutcome::TooLarge(e))
}
Err(PublishError::BeforeWrite(e)) => {
if e.code() != ErrorCode::SocketError {
self.symbol_dict.rollback(dict_mark);
}
self.latch_if_connection_is_spent(&e);
Err(direct_not_delivered(e))
}
Err(PublishError::DuringWrite(e)) => Err(direct_delivery_unknown(e)),
}
}
fn latch_if_connection_is_spent(&mut self, err: &crate::Error) {
if err.code() == ErrorCode::SymbolDictFull {
self.conn.mark_spent();
}
}
fn publish_split(
&mut self,
chunk: &Chunk<'_>,
row_offset: usize,
row_count: usize,
defer_commit: bool,
committed: &mut bool,
) -> std::result::Result<(), FlushFailure> {
let outcome =
match self.publish_frame(chunk, Some((row_offset, row_count)), defer_commit)? {
FrameOutcome::NoSlot(_) => {
self.sync(AckLevel::Ok)
.map_err(FlushFailure::DeliveryUnknown)?;
*committed = true;
self.commit_since_sync = true;
self.publish_frame(chunk, Some((row_offset, row_count)), defer_commit)?
}
outcome => outcome,
};
match outcome {
FrameOutcome::Published => Ok(()),
FrameOutcome::NoSlot(err) => Err(FlushFailure::NotDelivered(err)),
FrameOutcome::TooLarge(err) => match split_mid(row_count) {
Some(mid) => {
self.publish_split(chunk, row_offset, mid, true, committed)?;
self.publish_split(
chunk,
row_offset + mid,
row_count - mid,
defer_commit,
committed,
)
}
None => Err(direct_not_delivered(err)),
},
}
}
#[cfg(feature = "arrow-ingress")]
#[allow(clippy::too_many_arguments)]
fn flush_arrow_batch_inner(
&mut self,
table: TableName<'_>,
batch: &RecordBatch,
ts: ArrowTsSource,
overrides: &[ArrowColumnOverride<'_>],
wait: WaitForAck,
) -> std::result::Result<(), FlushFailure> {
let defer_commit = match wait {
WaitForAck::No => self.first_frame_sent,
WaitForAck::Yes(level) => {
self.conn
.validate_ack_level(level)
.map_err(direct_not_delivered)?;
false
}
};
self.conn.try_drain_acks().map_err(direct_not_delivered)?;
let spec = ArrowFrameSpec {
table,
batch,
ts,
overrides,
};
match self.publish_arrow_frame(&spec, None, defer_commit)? {
FrameOutcome::Published => {}
FrameOutcome::NoSlot(err) => return Err(FlushFailure::NotDelivered(err)),
FrameOutcome::TooLarge(err) => {
let row_count = batch.num_rows();
match split_mid(row_count) {
Some(mid) => {
let mut committed = false;
let mut result =
self.publish_arrow_split(&spec, 0, mid, true, &mut committed);
if result.is_ok() {
result = self.publish_arrow_split(
&spec,
mid,
row_count - mid,
defer_commit,
&mut committed,
);
}
if let Err(e) = result {
self.conn.mark_must_close();
return Err(if committed {
deny_retry_after_partial(e)
} else {
e
});
}
}
None => return Err(direct_not_delivered(err)),
}
}
}
if let WaitForAck::Yes(level) = wait {
self.conn
.sync_all_acks(level)
.map_err(direct_delivery_unknown)?;
self.commit_since_sync = false;
}
Ok(())
}
#[cfg(feature = "arrow-ingress")]
fn publish_arrow_frame(
&mut self,
spec: &ArrowFrameSpec<'_>,
range: Option<(usize, usize)>,
defer_commit: bool,
) -> std::result::Result<FrameOutcome, FlushFailure> {
if defer_commit && !self.conn.has_sync_commit_slot() {
return Ok(FrameOutcome::NoSlot(error::fmt!(
InvalidApiCall,
"column sender deferred flush capacity exhausted; call sync() \
before flushing more arrow batches."
)));
}
if self.conn.at_in_flight_cap() {
self.conn
.drain_one_ack_blocking()
.map_err(direct_not_delivered)?;
}
let dict_mark = self.symbol_dict.mark();
let sliced;
let batch = match range {
None => spec.batch,
Some((offset, count)) => {
sliced = spec.batch.slice(offset, count);
&sliced
}
};
let result = self.conn.publish_qwp(|out| {
arrow_batch::encode_arrow_batch_into(
out,
spec.table,
batch,
spec.ts,
spec.overrides,
&mut self.symbol_dict,
defer_commit,
)
});
match result {
Ok(published) => {
self.conn.push_pending(published.fsn);
self.first_frame_sent = true;
Ok(FrameOutcome::Published)
}
Err(PublishError::BeforeWrite(e)) if e.code() == ErrorCode::BatchTooLarge => {
self.symbol_dict.rollback(dict_mark);
Ok(FrameOutcome::TooLarge(e))
}
Err(PublishError::BeforeWrite(e)) => {
if e.code() != ErrorCode::SocketError {
self.symbol_dict.rollback(dict_mark);
}
self.latch_if_connection_is_spent(&e);
Err(direct_not_delivered(e))
}
Err(PublishError::DuringWrite(e)) => Err(direct_delivery_unknown(e)),
}
}
#[cfg(feature = "arrow-ingress")]
fn publish_arrow_split(
&mut self,
spec: &ArrowFrameSpec<'_>,
row_offset: usize,
row_count: usize,
defer_commit: bool,
committed: &mut bool,
) -> std::result::Result<(), FlushFailure> {
let outcome =
match self.publish_arrow_frame(spec, Some((row_offset, row_count)), defer_commit)? {
FrameOutcome::NoSlot(_) => {
self.sync(AckLevel::Ok)
.map_err(FlushFailure::DeliveryUnknown)?;
*committed = true;
self.commit_since_sync = true;
self.publish_arrow_frame(spec, Some((row_offset, row_count)), defer_commit)?
}
outcome => outcome,
};
match outcome {
FrameOutcome::Published => Ok(()),
FrameOutcome::NoSlot(err) => Err(FlushFailure::NotDelivered(err)),
FrameOutcome::TooLarge(err) => match split_mid(row_count) {
Some(mid) => {
self.publish_arrow_split(spec, row_offset, mid, true, committed)?;
self.publish_arrow_split(
spec,
row_offset + mid,
row_count - mid,
defer_commit,
committed,
)
}
None => Err(direct_not_delivered(err)),
},
}
}
}
impl SfaBackend {
fn validate_ack_level(&self, ack_level: AckLevel) -> Result<()> {
if ack_level == AckLevel::Durable && !self.request_durable_ack {
return Err(error::fmt!(
InvalidApiCall,
"AckLevel::Durable requires the pool to be opened with \
`request_durable_ack=on` in the connect string."
));
}
Ok(())
}
fn latch_if_connection_is_spent(&mut self, err: &crate::Error) {
if err.code() == ErrorCode::SymbolDictFull {
self.drop_on_return = true;
}
}
fn publish_buffer(
&mut self,
buffer: &QwpWsColumnarBuffer,
ack_level: Option<AckLevel>,
) -> std::result::Result<Option<u64>, FlushFailure> {
if let Some(level) = ack_level {
self.validate_ack_level(level)
.map_err(FlushFailure::NotDelivered)?;
}
if let Err(err) = qwp_ws_check_error_background(&self.state) {
return Err(FlushFailure::NotDelivered(err));
}
if buffer.is_empty() {
return Ok(None);
}
let frame_cap = self.effective_frame_caps().hard;
let result = {
let Self {
foreground,
state,
buffer_scratch,
..
} = self;
foreground.encode_persist_publish(
frame_cap,
|payload, symbol_dict, delta_enabled| {
buffer.encode_ws_replay_message_with_defer(
payload,
buffer_scratch,
symbol_dict,
super::wire::QWP_VERSION_1,
false,
delta_enabled,
)
},
|payload| publish_qwp_ws_payload_background(state, payload, frame_cap),
)
};
if let Err(err) = &result {
self.latch_if_connection_is_spent(err);
}
match result.map_err(FlushFailure::NotDelivered)? {
SfaPublishOutcome::Published(fsn) => Ok(Some(fsn)),
SfaPublishOutcome::TooLarge {
encoded_len,
max_buf_size,
} => Err(FlushFailure::NotDelivered(sfa_frame_size_error(
encoded_len,
max_buf_size,
))),
}
}
fn flush_chunk(
&mut self,
chunk: &mut Chunk<'_>,
wait: WaitForAck,
) -> std::result::Result<(), FlushFailure> {
self.flush_chunk_boundary(chunk, wait).map(|_| ())
}
fn flush_chunk_and_get_fsn(
&mut self,
chunk: &mut Chunk<'_>,
) -> std::result::Result<u64, FlushFailure> {
self.flush_chunk_boundary(chunk, WaitForAck::No)
}
fn flush_chunk_boundary(
&mut self,
chunk: &mut Chunk<'_>,
wait: WaitForAck,
) -> std::result::Result<u64, FlushFailure> {
if let WaitForAck::Yes(level) = wait {
self.validate_ack_level(level)
.map_err(FlushFailure::NotDelivered)?;
}
if let Err(e) = qwp_ws_check_error_background(&self.state) {
return Err(FlushFailure::NotDelivered(e));
}
let caps = self.effective_frame_caps();
let boundary =
match self.publish_chunk_sfa(chunk, None, caps.for_range(chunk.row_count()))? {
SfaPublishOutcome::Published(fsn) => fsn,
SfaPublishOutcome::TooLarge {
encoded_len,
max_buf_size,
} => {
let err = sfa_frame_size_error(encoded_len, max_buf_size);
let row_count = chunk.row_count();
match split_mid(row_count) {
Some(mid) => {
self.publish_split_sfa(chunk, 0, mid, caps)?;
self.publish_split_sfa(chunk, mid, row_count - mid, caps)
.map_err(deny_retry_after_partial)?
}
None => return Err(FlushFailure::NotDelivered(err)),
}
}
};
chunk.clear();
if let WaitForAck::Yes(level) = wait {
self.wait_for_boundary(level, boundary, self.sync_timeout)
.map_err(FlushFailure::DeliveryUnknown)?;
}
Ok(boundary)
}
fn publish_chunk_sfa(
&mut self,
chunk: &Chunk<'_>,
range: Option<(usize, usize)>,
frame_cap: usize,
) -> std::result::Result<SfaPublishOutcome, FlushFailure> {
let view;
let target = match range {
None => chunk,
Some((offset, count)) => {
view = unsafe { chunk.slice_rows(offset, count) };
&view
}
};
let result = {
let Self {
state,
foreground,
scratch,
..
} = self;
foreground.encode_persist_publish(
frame_cap,
|payload, symbol_dict, delta_enabled| {
if delta_enabled {
encoder::encode_chunk_into(payload, target, symbol_dict, scratch, false)
} else {
encoder::encode_chunk_replay_into(payload, target, symbol_dict, scratch)
}
},
|encoded| publish_qwp_ws_payload_background(state, encoded, frame_cap),
)
};
if let Err(err) = &result {
self.latch_if_connection_is_spent(err);
}
result.map_err(FlushFailure::NotDelivered)
}
fn publish_split_sfa(
&mut self,
chunk: &Chunk<'_>,
row_offset: usize,
row_count: usize,
caps: SfaFrameCaps,
) -> std::result::Result<u64, FlushFailure> {
match self.publish_chunk_sfa(
chunk,
Some((row_offset, row_count)),
caps.for_range(row_count),
)? {
SfaPublishOutcome::Published(fsn) => Ok(fsn),
SfaPublishOutcome::TooLarge {
encoded_len,
max_buf_size,
} => match split_mid(row_count) {
Some(mid) => {
self.publish_split_sfa(chunk, row_offset, mid, caps)?;
self.publish_split_sfa(chunk, row_offset + mid, row_count - mid, caps)
.map_err(deny_retry_after_partial)
}
None => Err(FlushFailure::NotDelivered(sfa_frame_size_error(
encoded_len,
max_buf_size,
))),
},
}
}
#[cfg(feature = "arrow-ingress")]
fn flush_arrow_batch(
&mut self,
table: TableName<'_>,
batch: &RecordBatch,
ts: ArrowTsSource,
overrides: &[ArrowColumnOverride<'_>],
wait: WaitForAck,
) -> std::result::Result<(), FlushFailure> {
self.flush_arrow_batch_boundary(table, batch, ts, overrides, wait)
.map(|_| ())
}
#[cfg(feature = "arrow-ingress")]
fn flush_arrow_batch_and_get_fsn(
&mut self,
table: TableName<'_>,
batch: &RecordBatch,
ts: ArrowTsSource,
overrides: &[ArrowColumnOverride<'_>],
) -> std::result::Result<u64, FlushFailure> {
self.flush_arrow_batch_boundary(table, batch, ts, overrides, WaitForAck::No)
}
#[cfg(feature = "arrow-ingress")]
fn flush_arrow_batch_boundary(
&mut self,
table: TableName<'_>,
batch: &RecordBatch,
ts: ArrowTsSource,
overrides: &[ArrowColumnOverride<'_>],
wait: WaitForAck,
) -> std::result::Result<u64, FlushFailure> {
if let WaitForAck::Yes(level) = wait {
self.validate_ack_level(level)
.map_err(FlushFailure::NotDelivered)?;
}
if let Err(e) = qwp_ws_check_error_background(&self.state) {
return Err(FlushFailure::NotDelivered(e));
}
let caps = self.effective_frame_caps();
let spec = ArrowFrameSpec {
table,
batch,
ts,
overrides,
};
let boundary =
match self.publish_arrow_sfa(&spec, None, caps.for_range(batch.num_rows()))? {
SfaPublishOutcome::Published(fsn) => fsn,
SfaPublishOutcome::TooLarge {
encoded_len,
max_buf_size,
} => {
let err = sfa_frame_size_error(encoded_len, max_buf_size);
let row_count = batch.num_rows();
match split_mid(row_count) {
Some(mid) => {
self.publish_arrow_split_sfa(&spec, 0, mid, caps)?;
self.publish_arrow_split_sfa(&spec, mid, row_count - mid, caps)
.map_err(deny_retry_after_partial)?
}
None => return Err(FlushFailure::NotDelivered(err)),
}
}
};
if let WaitForAck::Yes(level) = wait {
self.wait_for_boundary(level, boundary, self.sync_timeout)
.map_err(FlushFailure::DeliveryUnknown)?;
}
Ok(boundary)
}
#[cfg(feature = "arrow-ingress")]
fn publish_arrow_sfa(
&mut self,
spec: &ArrowFrameSpec<'_>,
range: Option<(usize, usize)>,
frame_cap: usize,
) -> std::result::Result<SfaPublishOutcome, FlushFailure> {
let sliced;
let batch = match range {
None => spec.batch,
Some((offset, count)) => {
sliced = spec.batch.slice(offset, count);
&sliced
}
};
let result = {
let Self {
state, foreground, ..
} = self;
foreground.encode_persist_publish(
frame_cap,
|payload, symbol_dict, delta_enabled| {
if delta_enabled {
arrow_batch::encode_arrow_batch_into(
payload,
spec.table,
batch,
spec.ts,
spec.overrides,
symbol_dict,
false,
)
} else {
arrow_batch::encode_arrow_batch_replay_into(
payload,
spec.table,
batch,
spec.ts,
spec.overrides,
symbol_dict,
)
}
},
|payload| publish_qwp_ws_payload_background(state, payload, frame_cap),
)
};
if let Err(err) = &result {
self.latch_if_connection_is_spent(err);
}
result.map_err(FlushFailure::NotDelivered)
}
#[cfg(feature = "arrow-ingress")]
fn publish_arrow_split_sfa(
&mut self,
spec: &ArrowFrameSpec<'_>,
row_offset: usize,
row_count: usize,
caps: SfaFrameCaps,
) -> std::result::Result<u64, FlushFailure> {
match self.publish_arrow_sfa(
spec,
Some((row_offset, row_count)),
caps.for_range(row_count),
)? {
SfaPublishOutcome::Published(fsn) => Ok(fsn),
SfaPublishOutcome::TooLarge {
encoded_len,
max_buf_size,
} => match split_mid(row_count) {
Some(mid) => {
self.publish_arrow_split_sfa(spec, row_offset, mid, caps)?;
self.publish_arrow_split_sfa(spec, row_offset + mid, row_count - mid, caps)
.map_err(deny_retry_after_partial)
}
None => Err(FlushFailure::NotDelivered(sfa_frame_size_error(
encoded_len,
max_buf_size,
))),
},
}
}
fn sync(&mut self, ack_level: AckLevel) -> Result<()> {
self.wait(ack_level, self.sync_timeout)
}
fn wait(&mut self, ack_level: AckLevel, timeout: Duration) -> Result<()> {
self.validate_ack_level(ack_level)?;
let Some(boundary) = qwp_ws_published_fsn_background(&self.state)? else {
return Ok(());
};
self.wait_for_boundary(ack_level, boundary, timeout)
}
fn published_fsn(&self) -> Result<Option<u64>> {
qwp_ws_published_fsn_background(&self.state)
}
fn acked_fsn(&self) -> Result<Option<u64>> {
qwp_ws_acked_fsn_background(&self.state)
}
fn wait_for_boundary(
&mut self,
ack_level: AckLevel,
boundary: u64,
timeout: Duration,
) -> Result<()> {
let last_boundary = match ack_level {
AckLevel::Ok => self.last_ok_sync_boundary,
AckLevel::Durable => self.last_durable_sync_boundary,
};
if last_boundary.is_some_and(|last| last >= boundary) {
return Ok(());
}
let mut deadline_anchor = Instant::now();
let mut last_completed: Option<u64> = None;
loop {
let completed = match ack_level {
AckLevel::Ok => qwp_ws_ok_fsn_background(&self.state)?,
AckLevel::Durable => qwp_ws_acked_fsn_background(&self.state)?,
};
if completed.is_some_and(|fsn| fsn >= boundary) {
match ack_level {
AckLevel::Ok => self.last_ok_sync_boundary = Some(boundary),
AckLevel::Durable => self.last_durable_sync_boundary = Some(boundary),
}
return Ok(());
}
if completed != last_completed {
last_completed = completed;
deadline_anchor = Instant::now();
}
qwp_ws_check_error_background(&self.state)?;
if !timeout.is_zero() && deadline_anchor.elapsed() >= timeout {
return Err(sfa_sync_timeout(timeout, ack_level, boundary, completed));
}
thread::sleep(Duration::from_millis(10));
}
}
fn effective_hard_frame_cap(&self) -> (usize, bool) {
let server_max = self.state.server_max_batch_size.load(Ordering::Acquire);
effective_hard_frame_cap(
self.max_buf_size,
server_max,
self.state.sfa_frame_payload_cap,
)
}
fn effective_frame_caps(&self) -> SfaFrameCaps {
let (hard, _) = self.effective_hard_frame_cap();
SfaFrameCaps {
hard,
soft: hard.min(self.state.sfa_frame_split_target),
}
}
}
fn effective_hard_frame_cap(
max_buf_size: usize,
server_max_batch_size: usize,
sfa_frame_payload_cap: usize,
) -> (usize, bool) {
let configured_cap = if server_max_batch_size == 0 {
max_buf_size
} else {
max_buf_size.min(server_max_batch_size)
};
(
configured_cap.min(sfa_frame_payload_cap),
server_max_batch_size != 0,
)
}
fn sfa_sync_timeout(
sync_timeout: Duration,
ack_level: AckLevel,
boundary: u64,
completed: Option<u64>,
) -> crate::Error {
let level = match ack_level {
AckLevel::Ok => "ok",
AckLevel::Durable => "durable",
};
let progress = match completed {
Some(fsn) => format!("reached FSN {}", fsn),
None => "reached no frame".to_string(),
};
crate::Error::new(
ErrorCode::FailoverRetry,
format!(
"QWP/WebSocket store-and-forward wait({}) timed out after {:?} \
with no ack progress (target FSN {}, {}); the connection is alive \
but the server is not advancing the watermark. The frames remain \
queued and the background runner keeps delivering them: retry \
wait() to keep awaiting the ack, or close the pool to drain. Do \
not re-flush the same data, which is already accepted and would \
be delivered twice.",
level, sync_timeout, boundary, progress
),
)
}
#[cfg(test)]
mod tests {
use super::{effective_hard_frame_cap, split_mid};
#[test]
fn effective_hard_cap_reports_whether_the_server_cap_is_known() {
assert_eq!(effective_hard_frame_cap(1000, 0, 800), (800, false));
assert_eq!(effective_hard_frame_cap(1000, 400, 800), (400, true));
assert_eq!(effective_hard_frame_cap(1000, 1200, 800), (800, true));
}
#[test]
fn split_mid_floors_at_eight_rows() {
assert_eq!(split_mid(0), None);
assert_eq!(split_mid(1), None);
assert_eq!(split_mid(8), None);
}
#[test]
fn split_mid_returns_eight_aligned_point_below_count() {
for count in [9usize, 12, 15, 16, 17, 100, 10_000, 16_384] {
let mid = split_mid(count).unwrap();
assert_eq!(
mid % 8,
0,
"split point must be 8-aligned for count {count}"
);
assert!(mid >= 8, "split point must be at least 8 for count {count}");
assert!(
mid < count,
"split point must make progress for count {count}"
);
}
}
#[test]
fn sfa_frame_caps_use_split_target_only_while_range_can_split() {
use super::SfaFrameCaps;
let caps = SfaFrameCaps {
hard: 1000,
soft: 400,
};
assert_eq!(caps.for_range(1), 1000);
assert_eq!(caps.for_range(8), 1000);
assert_eq!(caps.for_range(9), 400);
assert_eq!(caps.for_range(10_000), 400);
}
#[test]
fn deny_retry_after_partial_downgrades_not_delivered_and_never_upgrades() {
use super::{FlushFailure, deny_retry_after_partial};
use crate::{Error, ErrorCode};
let nd = FlushFailure::NotDelivered(Error::new(ErrorCode::SocketError, "boom"));
assert!(nd.is_not_delivered());
let downgraded = deny_retry_after_partial(nd);
assert!(!downgraded.is_not_delivered());
assert!(
downgraded.into_error().in_doubt(),
"downgraded failure must be flagged in-doubt"
);
let du = FlushFailure::DeliveryUnknown(Error::new(ErrorCode::SocketError, "boom"));
let still = deny_retry_after_partial(du);
assert!(!still.is_not_delivered());
assert!(still.into_error().in_doubt());
}
}