use tracing::debug;
use melin_app::auth::{ClientRole, RoleId};
use melin_app::decoder::{Decoded, ErasedDecoder};
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;
use melin_wire_protocol::framing::split_frame_limited;
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 = melin_wire_protocol::framing::MAX_REQUEST_BODY;
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,
role: ClientRole<RoleId>,
producer: &mut ring::Producer<InputSlot<A::Event>>,
decoder: &dyn ErasedDecoder<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;
loop {
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 frame = frame.payload;
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_erased(body, role) {
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
}