use tracing::debug;
use melin_app::auth::Permission;
use melin_app::decoder::{Decoded, RequestDecoder};
use melin_app::{AppEvent, Application};
use melin_journal::JournalEvent;
use melin_pipeline::ring;
use melin_transport_core::pipeline::InputSlot;
use melin_transport_core::trace::{MonoTraceInstant, mono_trace_ns};
use melin_wire_protocol::control_codec::{TAG_APP, TAG_LEN};
use crate::halt::{HaltGate, Refusal, RefusalSender, Verdict};
pub const MAX_FRAME_SIZE: usize = melin_wire_protocol::blocking::MAX_FRAME_SIZE;
pub const MAX_REQUEST_BODY: usize = MAX_FRAME_SIZE - TAG_LEN;
pub(crate) enum FrameAction {
Continue,
Disconnect,
PipelineFull,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn process_client_frames<A: Application>(
parse_buf: &mut Vec<u8>,
connection_id: u64,
key_hash: u64,
permission: Permission,
producer: &mut ring::Producer<InputSlot<A::Event>>,
decoder: &dyn RequestDecoder<Event = A::Event>,
halt: &HaltGate,
refusals: &mut RefusalSender<A::Report>,
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 mut cursor = 0;
let mut result = FrameAction::Continue;
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;
while cursor + 4 <= parse_buf.len() {
let len_bytes: [u8; 4] = parse_buf[cursor..cursor + 4]
.try_into()
.expect("slice is exactly 4 bytes");
let frame_len = u32::from_le_bytes(len_bytes) as usize;
if frame_len > MAX_FRAME_SIZE {
debug!(
connection_id,
frame_len, "frame too large, dropping connection"
);
result = FrameAction::Disconnect;
break;
}
if cursor + 4 + frame_len > parse_buf.len() {
break;
}
let frame = &parse_buf[cursor + 4..cursor + 4 + frame_len];
cursor += 4 + frame_len;
let body = match frame.split_first() {
Some((&TAG_APP, body)) => body,
Some((&tag, _)) => {
debug!(
connection_id,
tag, "frame is not an application request, dropping"
);
continue;
}
None => {
debug!(connection_id, "empty frame, dropping");
continue;
}
};
let event = match decoder.decode(body, permission) {
Decoded::Filter => continue,
Decoded::PermissionDenied(reason) => {
debug!(connection_id, reason, "permission denied, dropping request");
continue;
}
Decoded::DecodeError(reason) => {
debug!(connection_id, reason, "decode error");
continue;
}
Decoded::Permitted(event) => event,
};
if let Some(reason) = refusal_reason
&& !event.is_query()
{
refused += 1;
let refusal = Refusal {
connection_id,
input_seq: batch.next_sequence(),
report: A::build_reject(&event, reason),
};
if refusals.try_send(refusal).is_err() {
result = FrameAction::PipelineFull;
break;
}
continue;
}
let ts = if event.is_query() { 0 } else { batch_wall_ns };
let event = JournalEvent::App(event);
#[cfg(feature = "latency-trace")]
let pre_publish = mono_trace_ns();
#[allow(clippy::let_unit_value)]
let publish_ts = mono_trace_ns();
let push_result = batch.try_push_with(|slot| {
slot.connection_id = connection_id;
slot.key_hash = key_hash;
slot.sequence = 0;
slot.timestamp_ns = ts;
slot.event = event;
slot.publish_ts = publish_ts;
slot.recv_ts = recv_ts;
});
if push_result.is_err() {
result = FrameAction::PipelineFull;
break;
}
#[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());
if batch.len() >= COMMIT_EVERY {
refusals.flush();
batch.commit();
batch = producer.batch();
}
}
refusals.flush();
batch.commit();
if refused > 0 {
halt.record_refused(refused);
}
if cursor > 0 {
let remaining = parse_buf.len() - cursor;
parse_buf.copy_within(cursor.., 0);
parse_buf.truncate(remaining);
}
result
}