use std::time::Duration;
use tokio::time::Instant;
use super::super::FlushPolicy;
use crate::observability::MuxMetricsHandle;
pub(super) struct FlushGate {
policy: FlushPolicy,
metrics: Option<MuxMetricsHandle>,
staged: usize,
urgent: bool,
kicked: bool,
since: Option<Instant>,
}
impl FlushGate {
pub(super) fn new(policy: FlushPolicy, metrics: Option<MuxMetricsHandle>) -> Self {
Self {
policy,
metrics,
staged: 0,
urgent: false,
kicked: false,
since: None,
}
}
pub(super) fn stage(&mut self, count: usize) {
if count == 0 {
return;
}
if self.staged == 0 && self.linger_window().is_some() {
self.since = Some(Instant::now());
}
self.staged += count;
if let Some(metrics) = &self.metrics {
metrics.staged_records_delta(count as i64);
}
}
pub(super) fn stage_urgent(&mut self, count: usize) {
self.stage(count);
self.urgent = true;
}
pub(super) fn kick(&mut self) {
self.kicked = true;
}
pub(super) fn take_kick(&mut self) -> bool {
std::mem::take(&mut self.kicked)
}
pub(super) fn should_flush(&self) -> bool {
if self.urgent || self.policy.on_admission() {
return true;
}
match (self.since, self.linger_window()) {
(Some(since), Some(window)) => since.elapsed() >= window,
_ => false,
}
}
pub(super) fn deadline(&self) -> Option<Instant> {
match (self.since, self.linger_window()) {
(Some(since), Some(window)) => Some(since + window),
_ => None,
}
}
pub(super) fn cleared(&mut self) {
self.forget_staged();
}
pub(super) fn discarded(&mut self) {
self.forget_staged();
}
fn forget_staged(&mut self) {
if self.staged > 0
&& let Some(metrics) = &self.metrics
{
metrics.staged_records_delta(-(self.staged as i64));
}
self.staged = 0;
self.urgent = false;
self.since = None;
}
const fn linger_window(&self) -> Option<Duration> {
self.policy.max_linger()
}
}
pub(super) async fn linger_until(deadline: Option<Instant>) {
match deadline {
Some(deadline) => tokio::time::sleep_until(deadline).await,
None => std::future::pending().await,
}
}
#[cfg(test)]
mod tests {
use super::super::super::AutoFlush;
use super::*;
fn gate(policy: FlushPolicy) -> FlushGate {
FlushGate::new(policy, None)
}
fn auto(on_admission: bool, max_linger: Option<Duration>) -> FlushPolicy {
FlushPolicy::Auto(AutoFlush {
on_admission,
max_linger,
})
}
#[test]
fn auto_on_admission_writes_whatever_it_has() {
let mut gate = gate(FlushPolicy::default());
assert!(gate.should_flush(), "an empty wake still ends in a flush");
gate.stage(1);
assert!(gate.should_flush());
assert_eq!(gate.deadline(), None, "and never runs a timer");
}
#[test]
fn manual_holds_ordinary_records() {
let mut gate = gate(FlushPolicy::Manual);
gate.stage(32);
assert!(!gate.should_flush(), "manual means the application decides");
assert_eq!(gate.deadline(), None, "and there is no window to rescue it");
}
#[test]
fn manual_never_holds_a_record_that_carries_liveness() {
let mut gate = gate(FlushPolicy::Manual);
gate.stage(4);
gate.stage_urgent(1);
assert!(
gate.should_flush(),
"a close, a credit update or a terminal moves whatever the policy says"
);
}
#[test]
fn a_kick_survives_an_inline_clamp_flush() {
let mut gate = gate(FlushPolicy::Manual);
gate.stage(60);
gate.kick();
gate.cleared();
gate.stage(40);
assert!(
gate.take_kick(),
"the tail of the pass must not be stranded by a flush the app did not ask for"
);
assert!(!gate.take_kick(), "and one kick is served once");
}
#[test]
fn a_linger_window_runs_from_the_oldest_staged_record() {
let mut gate = gate(auto(false, Some(Duration::from_millis(50))));
assert_eq!(gate.deadline(), None, "nothing staged, nothing due");
gate.stage(1);
let first = gate.deadline().expect("a window is running");
gate.stage(1);
assert_eq!(
gate.deadline(),
Some(first),
"the second record does not restart the window the first started"
);
assert!(!gate.should_flush(), "and it has not elapsed yet");
}
#[test]
fn a_written_batch_starts_the_window_again() {
let mut gate = gate(auto(false, Some(Duration::from_millis(50))));
gate.stage(1);
gate.cleared();
assert_eq!(gate.deadline(), None);
assert!(!gate.should_flush(), "an empty batch is never due");
}
#[test]
fn a_discarded_batch_leaves_nothing_staged() {
let mut gate = gate(FlushPolicy::Manual);
gate.stage_urgent(8);
gate.discarded();
assert!(
!gate.should_flush(),
"an epoch death takes the urgency with the records it applied to"
);
}
}