use std::time::Duration;
use crate::OpNumber;
use ozzy_proto::RequestId;
use super::{
Channel, FlowError, Operation, PipelineLimits, Prefix, Probe, ProbeError, ProbeScheduler,
ProbeTiming, Report, Scope, Sender, VecDeque, ledger,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OpenRequest {
request_id: Option<RequestId>,
report: Report,
}
impl OpenRequest {
pub const fn report(self) -> Report {
self.report
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Repair {
pub channel: Channel,
pub after: Prefix,
pub through: Prefix,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusOutcome {
Ignored,
Observed,
Verify,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum TransmitError {
#[error(transparent)]
Flow(#[from] FlowError),
#[error(transparent)]
Probe(#[from] ProbeError),
}
#[derive(Debug)]
pub struct Transmitter {
scope: Scope,
limits: PipelineLimits,
probes: ProbeScheduler,
sender: Option<Sender>,
announced_channel: Option<Channel>,
spare: VecDeque<Operation>,
active: bool,
candidate: Option<OpenRequest>,
candidate_limit: Option<OpNumber>,
repair: Option<Repair>,
broadcast: bool,
catch_up: Option<OpNumber>,
receipt_progress: ozzy_core::live::LiveProgress,
}
impl Transmitter {
pub fn new(
scope: Scope,
limits: PipelineLimits,
first: RequestId,
timing: ProbeTiming,
now: Duration,
) -> Result<Self, TransmitError> {
let probes = ProbeScheduler::new(first, timing, now)?;
Ok(Self {
scope,
limits,
probes,
sender: None,
announced_channel: None,
spare: ledger(limits)?,
active: false,
candidate: None,
candidate_limit: None,
repair: None,
broadcast: false,
catch_up: None,
receipt_progress: ozzy_core::live::LiveProgress::new(timing.maximum),
})
}
pub fn enable_broadcast(&mut self) {
self.broadcast = true;
}
pub fn needs_catch_up(&self) -> bool {
self.catch_up.is_some_and(|target| {
self.sender()
.is_some_and(|sender| sender.sent().op < target)
})
}
pub const fn catch_up_through(&self) -> Option<OpNumber> {
self.catch_up
}
pub fn sender(&self) -> Option<&Sender> {
self.sender.as_ref().filter(|_| self.active)
}
pub const fn candidate(&self) -> Option<OpenRequest> {
self.candidate
}
pub const fn pending_probe(&self) -> Option<Probe> {
self.probes.pending()
}
pub fn replace_session(&mut self, now: Duration) -> Result<(), TransmitError> {
self.probes.invalidate(now)?;
self.announced_channel = None;
self.candidate = None;
self.candidate_limit = None;
Ok(())
}
pub fn change_scope(&mut self, scope: Scope, now: Duration) -> Result<(), TransmitError> {
if scope == self.scope {
return Ok(());
}
self.probes.invalidate(now)?;
self.scope = scope;
self.active = false;
self.announced_channel = None;
self.candidate = None;
self.candidate_limit = None;
self.repair = None;
self.catch_up = None;
self.receipt_progress.reset();
Ok(())
}
pub fn poll_probe(
&mut self,
available: Prefix,
transport_pending: bool,
now: Duration,
) -> Result<Option<Probe>, TransmitError> {
if transport_pending || self.repair.is_some() {
return Ok(None);
}
let tail = if self.broadcast {
available
} else {
self.sender().map_or(available, Sender::sent)
};
Ok(self
.probes
.poll(self.scope, tail, available.op.max(tail.op), now)?)
}
pub fn record_send(&mut self, operations: &[Operation]) -> Result<(), FlowError> {
if !self.active {
return Err(FlowError::Channel);
}
if self.repair.is_some()
|| self.broadcast
&& self.catch_up.is_some_and(|through| {
operations.last().is_some_and(|op| op.prefix.op > through)
})
{
return Err(FlowError::Capacity);
}
let sender = self.sender.as_mut().expect("active ledger");
sender.record_send(sender.channel(), operations)
}
pub fn observe(
&mut self,
report: Report,
request_id: Option<RequestId>,
now: Duration,
) -> Result<StatusOutcome, TransmitError> {
self.observe_with_repair_limit(report, request_id, None, now)
}
pub fn observe_with_repair_limit(
&mut self,
report: Report,
request_id: Option<RequestId>,
repair_limit: Option<OpNumber>,
now: Duration,
) -> Result<StatusOutcome, TransmitError> {
if report.channel.scope != self.scope {
return Ok(StatusOutcome::Ignored);
}
let correlated = if let Some(id) = request_id {
let Some(probe) = self.probes.pending().filter(|probe| probe.request_id == id) else {
return Ok(StatusOutcome::Ignored);
};
self.probes.completion_deadline(probe.scope, id, now)?;
Some(probe)
} else {
None
};
report.validate_shape()?;
if self
.sender()
.is_some_and(|sender| sender.channel() == report.channel)
{
if self.candidate.is_some_and(|old| {
old.report.channel == report.channel && old.report.revision > report.revision
}) {
return Ok(StatusOutcome::Ignored);
}
if self.broadcast && report.received.op > self.sender().expect("active").sent().op {
self.candidate = Some(OpenRequest { request_id, report });
self.candidate_limit = repair_limit;
return Ok(StatusOutcome::Verify);
}
let sender = self.sender.as_mut().expect("active ledger");
let before = sender.received();
let changed = sender.observe(report)?;
if sender.received().op > before.op {
self.receipt_progress.advanced(sender.received().op.0, now);
}
if let Some(probe) = correlated {
let target = if self.broadcast {
repair_limit.unwrap_or(probe.tail.op).min(probe.tail.op)
} else {
probe.tail.op
};
if self.broadcast {
self.catch_up = (sender.received().op < target).then_some(target);
}
let through = sender
.outstanding()
.take_while(|op| op.prefix.op <= target)
.last()
.map_or(sender.received(), |op| op.prefix);
let quiet = !self.broadcast
|| self
.receipt_progress
.repair_limit(sender.received().op.0, None, now)
.is_none();
self.repair = if quiet && sender.received().op < through.op {
Some(Repair {
channel: sender.channel(),
after: sender.received(),
through,
})
} else {
None
};
self.probes.complete(probe.scope, probe.request_id, now)?;
self.candidate = None;
self.candidate_limit = None;
}
self.trim_repair();
return Ok(if changed || correlated.is_some() {
StatusOutcome::Observed
} else {
StatusOutcome::Ignored
});
}
let Some(probe) = correlated else {
if self.sender().is_some() && self.announced_channel != Some(report.channel) {
self.probes.expedite(now)?;
self.announced_channel = Some(report.channel);
}
return Ok(StatusOutcome::Ignored);
};
if self.candidate.is_some_and(|old| {
old.report.channel == report.channel && old.report.revision > report.revision
}) {
return Ok(StatusOutcome::Ignored);
}
let candidate = OpenRequest {
request_id: Some(probe.request_id),
report,
};
self.candidate = Some(candidate);
self.candidate_limit = repair_limit;
Ok(StatusOutcome::Verify)
}
pub fn open_verified(
&mut self,
request: OpenRequest,
base: Prefix,
received: Prefix,
now: Duration,
) -> Result<bool, TransmitError> {
if self.candidate != Some(request) {
return Ok(false);
}
let probe = if let Some(id) = request.request_id {
let Some(probe) = self.probes.pending().filter(|probe| {
probe.scope == request.report.channel.scope && probe.request_id == id
}) else {
return Ok(false);
};
self.probes.completion_deadline(probe.scope, id, now)?;
Some(probe)
} else {
if !self.broadcast
|| self
.sender()
.is_none_or(|sender| sender.channel() != request.report.channel)
{
return Ok(false);
}
None
};
let report = request.report;
if report.base != base || report.received != received {
return Err(FlowError::History.into());
}
report.validate_shape()?;
let same_channel = self
.sender()
.is_some_and(|sender| sender.channel() == report.channel);
let previous = self.sender().map(|sender| sender.received().op);
if let Some(sender) = &mut self.sender {
if self.broadcast && same_channel {
if !sender.observe_publication(report, received)? {
self.candidate = None;
self.candidate_limit = None;
return Ok(false);
}
} else {
sender.reopen(report, received)?;
}
} else {
self.sender = Some(Sender::from_checked_report(
report,
self.limits,
std::mem::take(&mut self.spare),
));
}
if let Some(probe) = probe {
self.probes.complete(probe.scope, probe.request_id, now)?;
if self.broadcast {
let target = self
.candidate_limit
.unwrap_or(probe.tail.op)
.min(probe.tail.op);
self.catch_up = (report.received.op < target).then_some(target);
}
}
self.active = true;
self.candidate = None;
self.candidate_limit = None;
if !same_channel {
self.repair = None;
self.receipt_progress.reset();
}
if !same_channel || previous.is_none_or(|op| received.op > op) {
self.receipt_progress.advanced(received.op.0, now);
}
self.trim_repair();
Ok(true)
}
pub fn repair(&self, transport_pending: bool) -> Option<Repair> {
if transport_pending { None } else { self.repair }
}
pub fn record_repair(&mut self, repair: Repair, through: Prefix) -> Result<(), FlowError> {
if self.repair != Some(repair) {
return Err(FlowError::Channel);
}
if through.op <= repair.after.op
|| through.op > repair.through.op
|| self
.sender()
.is_none_or(|sender| !sender.outstanding().any(|op| op.prefix == through))
{
return Err(FlowError::History);
}
self.repair = if through == repair.through {
None
} else {
Some(Repair {
after: through,
..repair
})
};
Ok(())
}
fn trim_repair(&mut self) {
if self.catch_up.is_some_and(|target| {
self.sender()
.is_some_and(|sender| sender.received().op >= target)
}) {
self.catch_up = None;
}
let Some(repair) = self.repair else {
return;
};
let received = self
.sender()
.expect("repair has an active ledger")
.received();
self.repair = if received.op >= repair.through.op {
None
} else if received.op > repair.after.op {
Some(Repair {
after: received,
..repair
})
} else {
Some(repair)
};
}
}