use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, MutexGuard, PoisonError};
use std::time::Duration;
use polyc_state::query_audit::{
ErrorClass, QueryCompletion, QueryOutcome, RowCount, SourceSnapshot, Truncation,
};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct DeliveredCounts {
rows: u64,
result_bytes: u64,
truncated: bool,
}
impl DeliveredCounts {
pub(crate) const fn rows(self) -> u64 {
self.rows
}
#[cfg(test)]
pub(crate) const fn result_bytes(self) -> u64 {
self.result_bytes
}
pub(crate) const fn truncated(self) -> bool {
self.truncated
}
pub(crate) const fn truncation(self) -> Truncation {
if self.truncated {
Truncation::TruncatedAt(self.rows)
} else {
Truncation::Complete
}
}
}
#[derive(Default)]
enum LatchState {
#[default]
Pending,
Reported(Box<QueryCompletion>),
ConsumerBound(Box<QueryCompletion>),
Cancelled,
Dispatched,
}
pub(crate) struct TerminalLatch {
settlement: Mutex<Settlement>,
poisoned: AtomicBool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BatchRelease {
Admitted,
CountedByConsumer,
Refused,
}
const fn admit_locked(settlement: &mut Settlement, rows: u64, result_bytes: u64) -> bool {
if !matches!(settlement.state, LatchState::Pending) {
return false;
}
let counted = DeliveredCounts {
rows: settlement.delivered.rows.saturating_add(rows),
result_bytes: settlement
.delivered
.result_bytes
.saturating_add(result_bytes),
truncated: settlement.delivered.truncated,
};
settlement.delivered = counted;
true
}
#[derive(Default)]
struct Settlement {
state: LatchState,
delivered: DeliveredCounts,
consumer_accounted: bool,
}
impl Default for TerminalLatch {
fn default() -> Self {
Self {
settlement: Mutex::new(Settlement::default()),
poisoned: AtomicBool::new(false),
}
}
}
impl TerminalLatch {
fn lock(&self) -> MutexGuard<'_, Settlement> {
self.settlement.lock().unwrap_or_else(|poisoned| {
self.poisoned.store(true, Ordering::SeqCst);
PoisonError::into_inner(poisoned)
})
}
pub(crate) fn poisoned(&self) -> bool {
self.poisoned.load(Ordering::SeqCst) || self.settlement.is_poisoned()
}
pub(crate) fn admit_release(&self, rows: u64, result_bytes: u64) -> bool {
let mut settlement = self.lock();
admit_locked(&mut settlement, rows, result_bytes)
}
pub(crate) fn account_at_consumer(&self) {
self.lock().consumer_accounted = true;
}
pub(crate) fn record_batch_delivery(&self, rows: u64, result_bytes: u64) -> BatchRelease {
let mut settlement = self.lock();
let release = if settlement.consumer_accounted {
BatchRelease::CountedByConsumer
} else if admit_locked(&mut settlement, rows, result_bytes) {
BatchRelease::Admitted
} else {
BatchRelease::Refused
};
drop(settlement);
release
}
pub(crate) fn record_truncation(&self) {
let mut settlement = self.lock();
if matches!(settlement.state, LatchState::Pending) {
settlement.delivered.truncated = true;
}
}
pub(crate) fn delivered(&self) -> DeliveredCounts {
self.lock().delivered
}
pub(crate) fn report(
&self,
outcome: QueryOutcome,
duration: Duration,
source: &SourceSnapshot,
) -> bool {
let mut settlement = self.lock();
let completion = QueryCompletion::new(
outcome,
duration,
RowCount::new(settlement.delivered.rows),
settlement.delivered.truncation(),
source.clone(),
);
let keep = match &settlement.state {
LatchState::Pending => true,
LatchState::Cancelled => completion.outcome() != QueryOutcome::Succeeded,
LatchState::ConsumerBound(_) => !matches!(
completion.outcome(),
QueryOutcome::Succeeded | QueryOutcome::Failed(ErrorClass::Cancelled)
),
LatchState::Reported(_) | LatchState::Dispatched => false,
};
if keep {
settlement.state = LatchState::Reported(Box::new(completion));
}
keep
}
pub(crate) fn settle_consumer_bound(
&self,
source: &SourceSnapshot,
duration: Duration,
) -> Option<DeliveredCounts> {
let mut settlement = self.lock();
let keep = match &settlement.state {
LatchState::Pending => true,
LatchState::Reported(reported) => reported.outcome() == QueryOutcome::Succeeded,
LatchState::ConsumerBound(_) | LatchState::Cancelled | LatchState::Dispatched => false,
};
let settled = keep.then(|| {
let delivered = DeliveredCounts {
rows: settlement.delivered.rows,
result_bytes: settlement.delivered.result_bytes,
truncated: true,
};
let completion = Box::new(QueryCompletion::new(
QueryOutcome::Succeeded,
duration,
RowCount::new(delivered.rows),
delivered.truncation(),
source.clone(),
));
settlement.delivered = delivered;
settlement.state = LatchState::ConsumerBound(completion);
delivered
});
drop(settlement);
settled
}
pub(crate) fn cancel(&self) {
let mut settlement = self.lock();
let withdraw = match &settlement.state {
LatchState::Pending => true,
LatchState::Reported(completion) => completion.outcome() == QueryOutcome::Succeeded,
LatchState::ConsumerBound(_) | LatchState::Cancelled | LatchState::Dispatched => false,
};
if withdraw {
settlement.state = LatchState::Cancelled;
}
}
pub(crate) fn is_settled(&self) -> bool {
!matches!(self.lock().state, LatchState::Pending)
}
pub(crate) fn dispatch(&self, source: &SourceSnapshot, duration: Duration) -> QueryCompletion {
let mut guard = self.lock();
let delivered = guard.delivered;
let state = std::mem::replace(&mut guard.state, LatchState::Dispatched);
drop(guard);
if self.poisoned() {
return QueryCompletion::new(
QueryOutcome::Failed(ErrorClass::Internal),
duration,
RowCount::new(delivered.rows()),
delivered.truncation(),
source.clone(),
);
}
match state {
LatchState::Reported(completion) | LatchState::ConsumerBound(completion) => *completion,
LatchState::Pending | LatchState::Cancelled | LatchState::Dispatched => {
cancelled(source, duration, delivered)
}
}
}
}
fn cancelled(
source: &SourceSnapshot,
duration: Duration,
delivered: DeliveredCounts,
) -> QueryCompletion {
QueryCompletion::new(
QueryOutcome::Failed(ErrorClass::Cancelled),
duration,
RowCount::new(delivered.rows()),
delivered.truncation(),
source.clone(),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn source() -> SourceSnapshot {
SourceSnapshot::from_canonical(vec![]).expect("an empty source vector is canonical")
}
const TICK: Duration = Duration::from_millis(1);
fn report(latch: &TerminalLatch, outcome: QueryOutcome) {
let _ = latch.report(outcome, TICK, &source());
}
fn settle_bound(latch: &TerminalLatch) -> Option<DeliveredCounts> {
latch.settle_consumer_bound(&source(), TICK)
}
#[test]
fn a_withdrawal_without_any_report_still_reports_delivered_rows() {
let latch = TerminalLatch::default();
assert!(latch.admit_release(600, 4800));
latch.cancel();
let settled = latch.dispatch(&source(), Duration::from_millis(4));
assert_eq!(
settled.outcome(),
QueryOutcome::Failed(ErrorClass::Cancelled)
);
assert_eq!(
settled.rows(),
RowCount::new(600),
"a query that released rows must never settle as zero rows"
);
assert_eq!(
latch.delivered().result_bytes(),
4800,
"the released byte count survives a withdrawal with no report"
);
}
#[test]
fn a_consumer_bound_survives_the_withdrawal_that_stops_the_producer() {
let latch = TerminalLatch::default();
assert!(latch.admit_release(4, 32));
settle_bound(&latch);
latch.cancel();
let settled = latch.dispatch(&source(), Duration::from_millis(1));
assert_eq!(
settled.outcome(),
QueryOutcome::Succeeded,
"a bounded read is a success, and the drop that ends it is not a withdrawal"
);
assert_eq!(settled.rows(), RowCount::new(4));
}
#[test]
fn a_consumer_bound_outranks_a_producer_success_and_yields_to_a_failure() {
let over_success = TerminalLatch::default();
assert!(over_success.admit_release(4, 32));
report(&over_success, QueryOutcome::Succeeded);
settle_bound(&over_success);
assert_eq!(
over_success
.dispatch(&source(), Duration::from_millis(1))
.rows(),
RowCount::new(4),
"the rows the caller received, not the rows the producer made"
);
let failure_first = TerminalLatch::default();
report(&failure_first, QueryOutcome::Failed(ErrorClass::Deadline));
settle_bound(&failure_first);
let bound_first = TerminalLatch::default();
settle_bound(&bound_first);
report(&bound_first, QueryOutcome::Failed(ErrorClass::Deadline));
for (latch, order) in [
(failure_first, "failure first"),
(bound_first, "bound first"),
] {
assert_eq!(
latch
.dispatch(&source(), Duration::from_millis(1))
.outcome(),
QueryOutcome::Failed(ErrorClass::Deadline),
"a query that failed did not succeed for anyone, {order}"
);
}
}
#[test]
fn a_producer_cancellation_after_a_consumer_bound_is_refused() {
let latch = TerminalLatch::default();
assert!(latch.admit_release(4, 32));
settle_bound(&latch);
latch.cancel();
report(&latch, QueryOutcome::Failed(ErrorClass::Cancelled));
let settled = latch.dispatch(&source(), Duration::from_millis(1));
assert_eq!(
settled.outcome(),
QueryOutcome::Succeeded,
"the producer noticing the stop is not a measurement of failure"
);
assert_eq!(settled.rows(), RowCount::new(4));
}
#[test]
fn a_consumer_bound_after_a_withdrawal_is_refused() {
let latch = TerminalLatch::default();
latch.cancel();
settle_bound(&latch);
assert_eq!(
latch
.dispatch(&source(), Duration::from_millis(1))
.outcome(),
QueryOutcome::Failed(ErrorClass::Cancelled)
);
}
#[test]
fn consumer_accounting_replaces_batch_accounting_rather_than_adding_to_it() {
let latch = TerminalLatch::default();
latch.record_batch_delivery(13, 104);
assert_eq!(
latch.delivered().rows(),
13,
"the batch count is live first"
);
let framed = TerminalLatch::default();
framed.account_at_consumer();
framed.record_batch_delivery(13, 104);
let _ = framed.admit_release(4, 32);
assert_eq!(
framed.delivered().rows(),
4,
"only what the consumer released is counted"
);
}
#[test]
fn a_withdrawal_keeps_a_measured_failure_and_drops_a_measured_success() {
let failed = TerminalLatch::default();
report(&failed, QueryOutcome::Failed(ErrorClass::Deadline));
failed.cancel();
assert_eq!(
failed
.dispatch(&source(), Duration::from_millis(1))
.outcome(),
QueryOutcome::Failed(ErrorClass::Deadline),
"a measured deadline is the exact truth and is not relabelled"
);
let succeeded = TerminalLatch::default();
let _ = succeeded.admit_release(3, 24);
report(&succeeded, QueryOutcome::Succeeded);
succeeded.cancel();
let settled = succeeded.dispatch(&source(), Duration::from_millis(1));
assert_eq!(
settled.outcome(),
QueryOutcome::Failed(ErrorClass::Cancelled)
);
assert_eq!(settled.rows(), RowCount::new(3));
}
#[test]
fn a_late_report_after_a_withdrawal_keeps_its_measured_failure() {
let latch = TerminalLatch::default();
let _ = latch.admit_release(9, 72);
latch.cancel();
report(&latch, QueryOutcome::Failed(ErrorClass::Bounds));
assert_eq!(
latch
.dispatch(&source(), Duration::from_millis(1))
.outcome(),
QueryOutcome::Failed(ErrorClass::Bounds),
);
}
#[test]
fn a_late_success_after_a_withdrawal_is_refused() {
let latch = TerminalLatch::default();
latch.cancel();
report(&latch, QueryOutcome::Succeeded);
assert_eq!(
latch
.dispatch(&source(), Duration::from_millis(1))
.outcome(),
QueryOutcome::Failed(ErrorClass::Cancelled),
);
}
#[test]
fn dispatch_and_cancellation_are_mutually_exclusive() {
let latch = TerminalLatch::default();
assert!(latch.admit_release(4, 32));
report(&latch, QueryOutcome::Succeeded);
let settled = latch.dispatch(&source(), Duration::from_millis(1));
assert_eq!(settled.outcome(), QueryOutcome::Succeeded);
latch.cancel();
let again = latch.dispatch(&source(), Duration::from_millis(1));
assert_eq!(
again.outcome(),
QueryOutcome::Failed(ErrorClass::Cancelled),
"a second dispatch never re-issues the first command's content"
);
}
#[test]
fn truncation_reports_the_delivered_row_count() {
let latch = TerminalLatch::default();
let _ = latch.admit_release(2, 16);
latch.record_truncation();
latch.cancel();
assert_eq!(
latch
.dispatch(&source(), Duration::from_millis(1))
.truncation(),
Truncation::TruncatedAt(2),
);
}
#[test]
fn no_transition_can_overwrite_or_discard_a_delivered_count() {
let latch = TerminalLatch::default();
assert!(latch.admit_release(4, 32));
report(&latch, QueryOutcome::Succeeded);
latch.cancel();
report(&latch, QueryOutcome::Failed(ErrorClass::Deadline));
assert!(
!latch.admit_release(2, 16),
"a settled query admits no further release"
);
latch.cancel();
assert_eq!(latch.delivered().rows(), 4);
assert_eq!(latch.delivered().result_bytes(), 32);
let settled = latch.dispatch(&source(), Duration::from_millis(1));
assert_eq!(
settled.outcome(),
QueryOutcome::Failed(ErrorClass::Deadline)
);
assert_eq!(settled.rows(), RowCount::new(4));
assert_eq!(latch.delivered().rows(), 4);
assert_eq!(latch.delivered().result_bytes(), 32);
}
#[test]
fn a_release_and_a_terminal_cannot_interleave() {
let release_first = TerminalLatch::default();
assert!(
release_first.admit_release(4, 32),
"nothing has ended the query yet"
);
report(&release_first, QueryOutcome::Failed(ErrorClass::Deadline));
let settled = release_first.dispatch(&source(), TICK);
assert_eq!(
settled.outcome(),
QueryOutcome::Failed(ErrorClass::Deadline)
);
assert_eq!(
settled.rows(),
RowCount::new(4),
"a release admitted before the terminal is part of it"
);
let terminal_first = TerminalLatch::default();
report(&terminal_first, QueryOutcome::Failed(ErrorClass::Deadline));
assert!(
!terminal_first.admit_release(4, 32),
"a query that has already ended admits no release"
);
let settled = terminal_first.dispatch(&source(), TICK);
assert_eq!(
settled.outcome(),
QueryOutcome::Failed(ErrorClass::Deadline)
);
assert_eq!(
settled.rows(),
RowCount::new(0),
"a refused release is in neither the record nor the caller's hands"
);
}
#[test]
fn a_report_says_whether_its_terminal_became_the_record() {
let consumer_first = TerminalLatch::default();
assert!(consumer_first.report(QueryOutcome::Failed(ErrorClass::Bounds), TICK, &source(),));
assert!(!consumer_first.report(
QueryOutcome::Failed(ErrorClass::Deadline),
TICK,
&source(),
));
assert_eq!(
consumer_first.dispatch(&source(), TICK).outcome(),
QueryOutcome::Failed(ErrorClass::Bounds),
);
let producer_first = TerminalLatch::default();
assert!(
producer_first.report(QueryOutcome::Failed(ErrorClass::Deadline), TICK, &source(),)
);
assert!(!producer_first.report(QueryOutcome::Failed(ErrorClass::Bounds), TICK, &source(),));
assert_eq!(
producer_first.dispatch(&source(), TICK).outcome(),
QueryOutcome::Failed(ErrorClass::Deadline),
);
}
#[test]
fn a_batch_offered_after_a_terminal_is_refused() {
let latch = TerminalLatch::default();
assert_eq!(
latch.record_batch_delivery(4, 32),
BatchRelease::Admitted,
"nothing has ended the query yet"
);
report(&latch, QueryOutcome::Failed(ErrorClass::Deadline));
assert_eq!(
latch.record_batch_delivery(9, 72),
BatchRelease::Refused,
"the record is chosen; this batch is not in it"
);
assert_eq!(latch.dispatch(&source(), TICK).rows(), RowCount::new(4));
}
#[test]
fn a_batch_is_not_counted_twice_when_a_consumer_owns_the_count() {
let latch = TerminalLatch::default();
latch.account_at_consumer();
assert_eq!(
latch.record_batch_delivery(9, 72),
BatchRelease::CountedByConsumer,
"the frames this batch becomes are what get admitted"
);
assert!(latch.admit_release(4, 32));
assert_eq!(
latch.delivered().rows(),
4,
"only the released frames are counted"
);
}
#[test]
fn a_release_after_dispatch_is_refused() {
let latch = TerminalLatch::default();
assert!(latch.admit_release(4, 32));
let settled = latch.dispatch(&source(), TICK);
assert_eq!(settled.rows(), RowCount::new(4));
assert!(
!latch.admit_release(1, 8),
"the permit is already a command; its content cannot change"
);
assert_eq!(
latch.delivered().rows(),
4,
"and the counts behind it are untouched"
);
}
#[test]
fn a_terminal_reports_the_counts_admitted_when_it_was_recorded() {
let latch = TerminalLatch::default();
assert!(latch.admit_release(3, 24));
report(&latch, QueryOutcome::Succeeded);
assert!(
!latch.admit_release(5, 40),
"the terminal has been chosen; nothing more is admitted"
);
let settled = latch.dispatch(&source(), TICK);
assert_eq!(settled.rows(), RowCount::new(3));
assert_eq!(settled.truncation(), Truncation::Complete);
}
#[test]
fn a_poisoned_latch_fails_closed_and_keeps_its_counts() {
let latch = std::sync::Arc::new(TerminalLatch::default());
let _ = latch.admit_release(5, 40);
report(&latch, QueryOutcome::Succeeded);
let poisoner = std::sync::Arc::clone(&latch);
let _ = std::thread::spawn(move || {
let _guard = poisoner
.settlement
.lock()
.expect("the lock is not yet poisoned");
panic!("tear the lifecycle state");
})
.join();
assert!(latch.poisoned(), "the panic marks the latch untrustworthy");
let settled = latch.dispatch(&source(), Duration::from_millis(1));
assert_eq!(
settled.outcome(),
QueryOutcome::Failed(ErrorClass::Internal),
"a torn latch never reports the success it can no longer vouch for"
);
assert_eq!(
settled.rows(),
RowCount::new(5),
"a torn transition never makes released rows inexact"
);
assert_eq!(
settled.truncation(),
Truncation::Complete,
"truncation survives the poisoning with the counts it belongs to"
);
assert_eq!(
latch.delivered().result_bytes(),
40,
"a torn transition never makes released bytes inexact"
);
}
#[test]
fn a_poisoned_latch_reports_the_exact_counts_it_already_released() {
let latch = std::sync::Arc::new(TerminalLatch::default());
let _ = latch.admit_release(600, 4096);
latch.record_truncation();
let poisoner = std::sync::Arc::clone(&latch);
let _ = std::thread::spawn(move || {
let _guard = poisoner
.settlement
.lock()
.expect("the lock is not yet poisoned");
panic!("tear the transition");
})
.join();
let _ = latch.admit_release(7, 64);
assert!(latch.poisoned());
let settled = latch.dispatch(&source(), Duration::from_millis(2));
assert_eq!(
settled.outcome(),
QueryOutcome::Failed(ErrorClass::Internal)
);
assert_eq!(settled.rows(), RowCount::new(607));
assert_eq!(settled.truncation(), Truncation::TruncatedAt(607));
assert_eq!(latch.delivered().result_bytes(), 4160);
}
}