use std::time::{Duration, Instant};
use beamr::timer::TimerRef;
use liminal::protocol::{CONVERSATION_REPLY_REQUESTED_FLAG, Frame, MessageEnvelope};
use super::service_tap::CallTrace;
use crate::ServerError;
use crate::auth_membership::ReleaseCheck;
#[path = "pending_reply/tap.rs"]
mod tap;
#[path = "pending_reply/timers.rs"]
mod timers;
const SERVER_ERROR_CODE: u16 = 0xFFFF;
pub(super) const DEFAULT_REPLY_TIMEOUT: Duration =
Duration::from_millis(crate::config::types::LimitsConfig::DEFAULT_REPLY_TIMEOUT_MS);
#[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>,
release: Option<crate::auth_membership::ReleaseCheck>,
tap: Option<CallTrace>,
dispatched_at: Option<Instant>,
}
#[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,
release: None,
tap: None,
dispatched_at: None,
});
Ok(op_id)
}
pub(super) fn bind_release(
&mut self,
op_id: u64,
release: crate::auth_membership::ReleaseCheck,
) {
if let Some(entry) = self.entries.iter_mut().find(|entry| entry.op_id == op_id) {
entry.release = Some(release);
}
}
pub(super) fn is_pending(&self, op_id: u64) -> bool {
self.entries
.iter()
.any(|entry| entry.op_id == op_id && entry.state == EntryState::Pending)
}
pub(super) fn cancel(&mut self, op_id: u64) {
if let Some(index) = self.entries.iter().position(|entry| entry.op_id == op_id) {
let mut entry = self.entries.remove(index);
self.retire_timer(entry.timer);
if let Some(tap) = entry.tap.take() {
tap.finish(
crate::tap::TapOutcome::Refused,
Some("call_cancelled"),
Instant::now(),
);
}
}
}
pub(super) fn match_reply(
&mut self,
conversation_id: u64,
op_id: u64,
reply: MessageEnvelope,
) -> Option<Frame> {
let index = self
.entries
.iter()
.position(|entry| entry.op_id == op_id && entry.conversation_id == conversation_id)?;
let mut entry = self.entries.remove(index);
self.retire_timer(entry.timer);
let frame = match entry.state {
EntryState::Pending => match entry.release.as_ref().map(ReleaseCheck::check) {
Some(Err(refusal)) => Some(Frame::ConversationError {
flags: 0,
stream_id: entry.stream_id,
conversation_id: entry.conversation_id,
reason_code: SERVER_ERROR_CODE,
message: Some(refusal.to_string()),
}),
Some(Ok(_)) | None => Some(Frame::ConversationMessage {
flags: CONVERSATION_REPLY_REQUESTED_FLAG,
stream_id: entry.stream_id,
conversation_id: entry.conversation_id,
envelope: reply,
}),
},
EntryState::Tombstone => None,
};
if let Some(tap) = entry.tap.take()
&& let Some(frame) = frame.as_ref()
{
tap.frame(frame);
}
frame
}
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;
if let Some(tap) = entry.tap.take() {
tap.finish(crate::tap::TapOutcome::Unanswered, None, now);
}
entry.timer = None;
frames.push(reply_timeout_frame(entry.stream_id, entry.conversation_id));
}
}
frames
}
#[cfg(test)]
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 conversations_with_entries(&self) -> Vec<u64> {
let mut ids: Vec<u64> = self
.entries
.iter()
.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 mut entry in entries {
if entry.conversation_id == conversation_id {
self.retire_timer(entry.timer);
if let Some(tap) = entry.tap.take() {
tap.finish(
crate::tap::TapOutcome::Refused,
Some("conversation_closed"),
Instant::now(),
);
}
} else {
retained.push(entry);
}
}
self.entries = retained;
}
pub(super) fn cancel_all(&mut self) {
let entries = std::mem::take(&mut self.entries);
for mut entry in entries {
self.retire_timer(entry.timer);
if let Some(tap) = entry.tap.take() {
tap.finish(
crate::tap::TapOutcome::Refused,
Some("connection_closed"),
Instant::now(),
);
}
}
}
#[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 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;