use std::time::{Duration, Instant};
use tracing::debug;
use melin_app::auth::{ClientRole, RoleId};
use melin_app::decoder::{Decoded, ErasedDecoder};
use melin_app::{AppEvent, Application};
use melin_pipeline::ring;
use melin_transport_core::health::{IngressMetrics, IngressTally};
use melin_transport_core::pipeline::{ClientOrigin, InputSlot, RequestId};
use melin_transport_core::trace::MonoTraceInstant;
#[cfg(any(feature = "latency-trace", feature = "tick-to-trade"))]
use melin_transport_core::trace::mono_trace_ns;
use melin_wire_protocol::control::RefusalKind;
use melin_wire_protocol::framing::{split_frame_limited, split_request};
use crate::early_reply::{EarlyKind, EarlyReply, EarlyReplySender};
use crate::halt::{HaltGate, Verdict};
pub(crate) const MAX_FRAME_SIZE: usize = melin_wire_protocol::blocking::MAX_FRAME_SIZE;
pub const MAX_REQUEST_BODY: usize = melin_wire_protocol::framing::MAX_REQUEST_BODY;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FrameAction {
Continue,
Disconnect,
Park,
}
pub(crate) const LONG_PARK: Duration = Duration::from_secs(1);
#[derive(Debug, Default)]
pub(crate) struct LongParkReport {
last: Option<Instant>,
}
impl LongParkReport {
pub(crate) fn parked_long(since: Instant, now: Instant) -> bool {
now.saturating_duration_since(since) > LONG_PARK
}
fn due(&mut self, long_parked: usize, now: Instant) -> bool {
if long_parked == 0 {
return false;
}
if self
.last
.is_some_and(|last| now.duration_since(last) < LONG_PARK)
{
return false;
}
self.last = Some(now);
true
}
pub(crate) fn report(&mut self, long_parked: usize, now: Instant) {
if self.due(long_parked, now) {
tracing::warn!(
connections = long_parked,
"client connections parked for over a second: the response stage is not taking \
early replies, and these clients are not read"
);
}
}
}
#[derive(Debug)]
pub(crate) struct ClientIngress {
parse_buf: Vec<u8>,
shed_remaining: usize,
}
impl ClientIngress {
pub(crate) fn new(parse_buf: Vec<u8>) -> Self {
debug_assert!(parse_buf.is_empty(), "a connection starts with no input");
Self {
parse_buf,
shed_remaining: 0,
}
}
#[cfg(feature = "dpdk")]
pub(crate) fn len(&self) -> usize {
self.parse_buf.len()
}
pub(crate) fn extend_from_slice(&mut self, bytes: &[u8]) {
self.parse_buf.extend_from_slice(bytes);
}
#[cfg(feature = "dpdk")]
pub(crate) fn buf_mut(&mut self) -> &mut Vec<u8> {
&mut self.parse_buf
}
#[cfg(feature = "dpdk")]
pub(crate) fn into_buf(mut self) -> Vec<u8> {
self.parse_buf.clear();
self.parse_buf
}
#[cfg(test)]
pub(crate) fn bytes(&self) -> &[u8] {
&self.parse_buf
}
#[cfg(test)]
pub(crate) fn shed_remaining(&self) -> usize {
self.shed_remaining
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn process_client_frames<A: Application>(
ingress: &mut ClientIngress,
connection_id: u64,
key_hash: u64,
role: ClientRole<RoleId>,
producer: &mut ring::Producer<InputSlot<A::Event>>,
decoder: &dyn ErasedDecoder<A::Event>,
halt: &HaltGate,
early_replies: &mut EarlyReplySender<A::Report>,
metrics: &IngressMetrics,
batch_wall_ns: u64,
recv_ts: MonoTraceInstant,
#[cfg(feature = "latency-trace")] publish_rec: &mut melin_transport_core::trace::StageRecorder,
#[cfg(feature = "tick-to-trade")] ingest_rec: &mut melin_transport_core::trace::StageRecorder,
) -> FrameAction {
let action = process_frames_unflushed::<A>(
ingress,
connection_id,
key_hash,
role,
producer,
decoder,
halt,
early_replies,
metrics,
batch_wall_ns,
recv_ts,
#[cfg(feature = "latency-trace")]
publish_rec,
#[cfg(feature = "tick-to-trade")]
ingest_rec,
);
early_replies.flush();
action
}
#[allow(clippy::too_many_arguments)]
#[inline]
fn process_frames_unflushed<A: Application>(
ingress: &mut ClientIngress,
connection_id: u64,
key_hash: u64,
role: ClientRole<RoleId>,
producer: &mut ring::Producer<InputSlot<A::Event>>,
decoder: &dyn ErasedDecoder<A::Event>,
halt: &HaltGate,
early_replies: &mut EarlyReplySender<A::Report>,
metrics: &IngressMetrics,
batch_wall_ns: u64,
recv_ts: MonoTraceInstant,
#[cfg(feature = "latency-trace")] publish_rec: &mut melin_transport_core::trace::StageRecorder,
#[cfg(feature = "tick-to-trade")] ingest_rec: &mut melin_transport_core::trace::StageRecorder,
) -> FrameAction {
let ClientIngress {
parse_buf,
shed_remaining,
} = ingress;
debug_assert!(*shed_remaining <= parse_buf.len());
let mut cursor = 0;
let mut shed_end = *shed_remaining;
let mut result = FrameAction::Continue;
let mut tally = IngressTally::default();
const COMMIT_EVERY: u64 = 16;
let refusal_reason = match halt.verdict() {
Verdict::Take => None,
Verdict::Refuse(reason) => Some(reason),
Verdict::Close => {
debug!(connection_id, "node superseded, closing connection");
return FrameAction::Disconnect;
}
};
let mut batch = producer.batch();
let mut refused: u64 = 0;
loop {
let frame_start = cursor;
let frame = match split_frame_limited(&parse_buf[cursor..], MAX_FRAME_SIZE) {
Ok(Some(frame)) => frame,
Ok(None) => break,
Err(too_large) => {
debug!(
connection_id,
frame_len = too_large.declared,
"frame too large, dropping connection"
);
result = FrameAction::Disconnect;
break;
}
};
cursor += frame.consumed();
let (request_id, body) = match split_request(frame.payload) {
Ok(request) => request,
Err(not_a_request) => {
debug!(
connection_id,
%not_a_request,
"not a request, dropping connection"
);
result = FrameAction::Disconnect;
break;
}
};
if frame_start < shed_end {
if !queue_early(early_replies, connection_id, request_id, EarlyKind::Busy) {
cursor = frame_start;
result = FrameAction::Park;
break;
}
tally.shed += 1;
continue;
}
let early = match decoder.decode_erased(body, role) {
Decoded::Filter => EarlyKind::Filtered,
Decoded::PermissionDenied(reason) => EarlyKind::Refused {
kind: RefusalKind::PermissionDenied,
reason,
},
Decoded::DecodeError(reason) => EarlyKind::Refused {
kind: RefusalKind::Malformed,
reason,
},
Decoded::Permitted(event) => match refusal_reason {
Some(reason) if !event.is_query() => {
EarlyKind::Rejected(A::build_reject(&event, reason))
}
_ => {
if publish::<A>(
&mut batch,
ClientOrigin {
connection_id,
request_id,
key_hash,
},
event,
batch_wall_ns,
recv_ts,
#[cfg(feature = "latency-trace")]
publish_rec,
#[cfg(feature = "tick-to-trade")]
ingest_rec,
) {
if batch.len() >= COMMIT_EVERY {
batch.commit();
batch = producer.batch();
}
continue;
}
shed_end = frame_start + complete_frames_len(&parse_buf[frame_start..]);
if !queue_early(early_replies, connection_id, request_id, EarlyKind::Busy) {
cursor = frame_start;
result = FrameAction::Park;
break;
}
tally.shed += 1;
continue;
}
},
};
if !queue_early(early_replies, connection_id, request_id, early) {
cursor = frame_start;
result = FrameAction::Park;
break;
}
match early {
EarlyKind::Rejected(_) => refused += 1,
EarlyKind::Refused { kind, reason } => {
tally.refused(kind);
match kind {
RefusalKind::PermissionDenied => {
debug!(connection_id, reason, "permission denied, refusing request")
}
RefusalKind::Malformed => {
debug!(connection_id, reason, "decode error, refusing request")
}
}
}
EarlyKind::Filtered => tally.filtered += 1,
EarlyKind::Busy => {}
}
}
batch.commit();
if refused > 0 {
halt.record_refused(refused);
}
metrics.record(&tally);
if tally.shed > 0 {
debug!(
connection_id,
shed = tally.shed,
"input ring full, requests shed"
);
}
*shed_remaining = shed_end.saturating_sub(cursor);
if cursor > 0 {
let remaining = parse_buf.len() - cursor;
parse_buf.copy_within(cursor.., 0);
parse_buf.truncate(remaining);
}
result
}
fn complete_frames_len(bytes: &[u8]) -> usize {
let mut len = 0;
while let Ok(Some(frame)) = split_frame_limited(&bytes[len..], MAX_FRAME_SIZE) {
len += frame.consumed();
}
len
}
fn queue_early<R: Copy>(
early_replies: &mut EarlyReplySender<R>,
connection_id: u64,
request_id: RequestId,
kind: EarlyKind<R>,
) -> bool {
early_replies
.try_send(EarlyReply {
connection_id,
request_id,
kind,
})
.is_ok()
}
#[allow(clippy::too_many_arguments)]
#[inline]
fn publish<A: Application>(
batch: &mut ring::Batch<'_, InputSlot<A::Event>>,
origin: ClientOrigin,
event: A::Event,
batch_wall_ns: u64,
recv_ts: MonoTraceInstant,
#[cfg(feature = "latency-trace")] publish_rec: &mut melin_transport_core::trace::StageRecorder,
#[cfg(feature = "tick-to-trade")] ingest_rec: &mut melin_transport_core::trace::StageRecorder,
) -> bool {
let ts = if event.is_query() { 0 } else { batch_wall_ns };
#[cfg(feature = "latency-trace")]
let pre_publish = mono_trace_ns();
if batch
.try_push_with(|slot| {
*slot = InputSlot::client(origin, event, ts, recv_ts);
})
.is_err()
{
return false;
}
#[cfg(feature = "latency-trace")]
{
let publish_done = mono_trace_ns();
publish_rec.record_elapsed(pre_publish, publish_done);
}
#[cfg(feature = "tick-to-trade")]
ingest_rec.record_elapsed(recv_ts, mono_trace_ns());
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_long_park_is_reported_at_most_once_per_interval() {
let mut report = LongParkReport::default();
let start = Instant::now();
assert!(!report.due(0, start), "nothing parked long");
assert!(report.due(2, start), "the first report");
assert!(!report.due(2, start + LONG_PARK / 2), "within the interval");
assert!(report.due(1, start + LONG_PARK), "the next interval");
assert!(!report.due(0, start + LONG_PARK * 3), "none any more");
assert!(report.due(1, start + LONG_PARK * 3), "a new one, later");
}
#[test]
fn a_park_is_long_past_the_interval_only() {
let since = Instant::now();
assert!(!LongParkReport::parked_long(since, since));
assert!(!LongParkReport::parked_long(since, since + LONG_PARK));
assert!(LongParkReport::parked_long(
since,
since + LONG_PARK + Duration::from_millis(1)
));
assert!(!LongParkReport::parked_long(
since + Duration::from_secs(5),
since
));
}
#[test]
fn a_shed_tail_covers_complete_frames_only() {
let frame = |len: u8| {
let mut f = u32::from(len).to_le_bytes().to_vec();
f.extend(std::iter::repeat_n(0xAB, usize::from(len)));
f
};
let two = [frame(3), frame(5)].concat();
assert_eq!(complete_frames_len(&[]), 0);
assert_eq!(complete_frames_len(&two), two.len());
assert_eq!(
complete_frames_len(&[two.as_slice(), &[9, 0]].concat()),
two.len()
);
let oversized = (MAX_FRAME_SIZE as u32 + 1).to_le_bytes();
assert_eq!(
complete_frames_len(&[two.as_slice(), &oversized, &frame(1)].concat()),
two.len()
);
}
}