use std::time::{Duration, Instant};
use beamr::timer::TimerRef;
use liminal::protocol::{CONVERSATION_REPLY_REQUESTED_FLAG, Frame, MessageEnvelope};
use crate::ServerError;
const SERVER_ERROR_CODE: u16 = 0xFFFF;
pub(super) const DEFAULT_REPLY_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EntryState {
Pending,
Tombstone,
}
#[derive(Debug)]
struct PendingReplyEntry {
op_id: u64,
conversation_id: u64,
stream_id: u32,
deadline: Instant,
state: EntryState,
timer: Option<TimerRef>,
}
#[derive(Debug)]
pub(super) struct PendingReplyTable {
entries: Vec<PendingReplyEntry>,
next_op_id: u64,
max_per_conversation: usize,
max_per_connection: usize,
reply_timeout: Duration,
retired_timers: Vec<TimerRef>,
}
impl Default for PendingReplyTable {
fn default() -> Self {
Self::new(
crate::config::types::LimitsConfig::DEFAULT_MAX_PENDING_REPLIES_PER_CONVERSATION,
crate::config::types::LimitsConfig::DEFAULT_MAX_PENDING_CONVERSATION_REPLIES_PER_CONNECTION,
DEFAULT_REPLY_TIMEOUT,
)
}
}
impl PendingReplyTable {
pub(super) const fn new(
max_per_conversation: usize,
max_per_connection: usize,
reply_timeout: Duration,
) -> Self {
Self {
entries: Vec::new(),
next_op_id: 1,
max_per_conversation,
max_per_connection,
reply_timeout,
retired_timers: Vec::new(),
}
}
pub(super) fn admit(
&mut self,
conversation_id: u64,
stream_id: u32,
now: Instant,
) -> Result<u64, ServerError> {
let per_conversation = self
.entries
.iter()
.filter(|entry| entry.conversation_id == conversation_id)
.count();
if per_conversation >= self.max_per_conversation {
return Err(ServerError::ConnectionCapReached {
operation: "conversation reply".to_owned(),
cap: "max_pending_replies_per_conversation",
limit: self.max_per_conversation,
});
}
let per_connection_pending = self
.entries
.iter()
.filter(|entry| entry.state == EntryState::Pending)
.count();
if per_connection_pending >= self.max_per_connection {
return Err(ServerError::ConnectionCapReached {
operation: "conversation reply".to_owned(),
cap: "max_pending_conversation_replies_per_connection",
limit: self.max_per_connection,
});
}
let op_id = self.next_op_id;
self.next_op_id += 1;
self.entries.push(PendingReplyEntry {
op_id,
conversation_id,
stream_id,
deadline: now + self.reply_timeout,
state: EntryState::Pending,
timer: None,
});
Ok(op_id)
}
pub(super) fn cancel(&mut self, op_id: u64) {
if let Some(index) = self.entries.iter().position(|entry| entry.op_id == op_id) {
let entry = self.entries.remove(index);
self.retire_timer(entry.timer);
}
}
pub(super) fn match_reply(
&mut self,
conversation_id: u64,
reply: MessageEnvelope,
) -> Option<Frame> {
let index = self.oldest_index_for(conversation_id)?;
let entry = self.entries.remove(index);
self.retire_timer(entry.timer);
match entry.state {
EntryState::Pending => Some(Frame::ConversationMessage {
flags: CONVERSATION_REPLY_REQUESTED_FLAG,
stream_id: entry.stream_id,
conversation_id: entry.conversation_id,
envelope: reply,
}),
EntryState::Tombstone => None,
}
}
pub(super) fn expire_due(&mut self, now: Instant) -> Vec<Frame> {
let mut frames = Vec::new();
for entry in &mut self.entries {
if entry.state == EntryState::Pending && entry.deadline <= now {
entry.state = EntryState::Tombstone;
entry.timer = None;
frames.push(reply_timeout_frame(entry.stream_id, entry.conversation_id));
}
}
frames
}
pub(super) fn conversations_awaiting_reply(&self) -> Vec<u64> {
let mut ids: Vec<u64> = self
.entries
.iter()
.filter(|entry| entry.state == EntryState::Pending)
.map(|entry| entry.conversation_id)
.collect();
ids.sort_unstable();
ids.dedup();
ids
}
pub(super) fn remove_conversation(&mut self, conversation_id: u64) {
let mut retained = Vec::with_capacity(self.entries.len());
let entries = std::mem::take(&mut self.entries);
for entry in entries {
if entry.conversation_id == conversation_id {
self.retire_timer(entry.timer);
} else {
retained.push(entry);
}
}
self.entries = retained;
}
pub(super) fn cancel_all(&mut self) {
let entries = std::mem::take(&mut self.entries);
for entry in entries {
self.retire_timer(entry.timer);
}
}
pub(super) fn timers_to_arm(&self, now: Instant) -> Vec<(u64, Duration)> {
self.entries
.iter()
.filter(|entry| entry.state == EntryState::Pending && entry.timer.is_none())
.map(|entry| (entry.op_id, entry.deadline.saturating_duration_since(now)))
.collect()
}
pub(super) fn install_timer(&mut self, op_id: u64, timer: TimerRef) -> bool {
let Some(entry) = self.entries.iter_mut().find(|entry| entry.op_id == op_id) else {
return false;
};
if entry.state != EntryState::Pending || entry.timer.is_some() {
return false;
}
entry.timer = Some(timer);
true
}
pub(super) fn take_retired_timers(&mut self) -> Vec<TimerRef> {
std::mem::take(&mut self.retired_timers)
}
pub(super) fn has_due(&self, now: Instant) -> bool {
self.entries
.iter()
.any(|entry| entry.state == EntryState::Pending && entry.deadline <= now)
}
fn retire_timer(&mut self, timer: Option<TimerRef>) {
if let Some(timer) = timer {
self.retired_timers.push(timer);
}
}
#[cfg(test)]
pub(super) fn len(&self) -> usize {
self.entries.len()
}
#[cfg(test)]
pub(super) fn pending_for(&self, conversation_id: u64) -> usize {
self.entries
.iter()
.filter(|entry| {
entry.conversation_id == conversation_id && entry.state == EntryState::Pending
})
.count()
}
#[cfg(test)]
pub(super) fn tombstones_for(&self, conversation_id: u64) -> usize {
self.entries
.iter()
.filter(|entry| {
entry.conversation_id == conversation_id && entry.state == EntryState::Tombstone
})
.count()
}
fn oldest_index_for(&self, conversation_id: u64) -> Option<usize> {
self.entries
.iter()
.enumerate()
.filter(|(_, entry)| entry.conversation_id == conversation_id)
.min_by_key(|(_, entry)| entry.op_id)
.map(|(index, _)| index)
}
}
fn reply_timeout_frame(stream_id: u32, conversation_id: u64) -> Frame {
Frame::ConversationError {
flags: 0,
stream_id,
conversation_id,
reason_code: SERVER_ERROR_CODE,
message: Some(
"conversation reply timed out: no participant reply arrived within the deadline"
.to_owned(),
),
}
}
pub(super) fn cap_refusal_frame(
stream_id: u32,
conversation_id: u64,
error: &ServerError,
) -> Frame {
Frame::ConversationError {
flags: 0,
stream_id,
conversation_id,
reason_code: SERVER_ERROR_CODE,
message: Some(error.to_string()),
}
}
#[cfg(test)]
pub(super) fn test_reply_envelope(payload: &[u8]) -> MessageEnvelope {
use liminal::protocol::{CausalContext, SchemaId as ProtocolSchemaId};
MessageEnvelope::new(
ProtocolSchemaId::new([0; ProtocolSchemaId::WIRE_LEN]),
CausalContext::independent(),
payload.to_vec(),
)
}
#[cfg(test)]
#[path = "pending_reply_tests.rs"]
mod tests;