use std::io::Write;
use std::net::TcpStream;
use std::str::{self, SplitWhitespace};
use std::time::Instant;
use sonic::Executor;
use sonic::config::{ConfigNormalization, ConfigSearch};
use super::command::{
COMMANDS_MODE_CONTROL, COMMANDS_MODE_INGEST, COMMANDS_MODE_SEARCH, ChannelCommandBase,
ChannelCommandControl, ChannelCommandError, ChannelCommandIngest, ChannelCommandResponse,
ChannelCommandResponseArgs, ChannelCommandSearch,
};
use super::listen::CHANNEL_AVAILABLE;
use super::statistics::{COMMAND_LATENCY_BEST, COMMAND_LATENCY_WORST, COMMANDS_TOTAL};
use crate::LINE_FEED;
pub struct ChannelMessage;
pub struct ChannelMessageModeSearch<'this> {
pub executor: &'this Executor,
pub search_config: &'this ConfigSearch,
pub normalization_config: &'this ConfigNormalization,
}
pub struct ChannelMessageModeIngest<'this> {
pub executor: &'this Executor,
pub normalization_config: &'this ConfigNormalization,
}
pub struct ChannelMessageModeControl<'this> {
pub executor: &'this Executor,
}
const COMMAND_ELAPSED_MILLIS_SLOW_WARN: u128 = 50;
#[derive(PartialEq)]
pub enum ChannelMessageResult {
Continue,
Close,
}
pub trait ChannelMessageMode {
fn handle(&self, message: &str) -> Result<Vec<ChannelCommandResponse>, ChannelCommandError>;
fn on(&self, mut stream: &TcpStream, message_slice: &[u8]) -> ChannelMessageResult {
let message = str::from_utf8(message_slice).unwrap_or("");
tracing::trace!("got channel message: {}", message);
let command_start = Instant::now();
let mut result = ChannelMessageResult::Continue;
#[allow(clippy::needless_late_init)]
let response_args_groups: Vec<ChannelCommandResponseArgs>;
if !(*CHANNEL_AVAILABLE.read().unwrap()) {
response_args_groups =
vec![ChannelCommandResponse::Err(ChannelCommandError::ShuttingDown).to_args()];
} else {
response_args_groups = match self.handle(message) {
Ok(resp_groups) => resp_groups
.iter()
.map(|resp| match resp {
ChannelCommandResponse::Ok
| ChannelCommandResponse::Pong
| ChannelCommandResponse::Pending(_)
| ChannelCommandResponse::Result(_)
| ChannelCommandResponse::Event(_, _, _)
| ChannelCommandResponse::Void
| ChannelCommandResponse::Err(_) => resp.to_args(),
ChannelCommandResponse::Ended(_) => {
result = ChannelMessageResult::Close;
resp.to_args()
}
})
.collect(),
Err(reason) => vec![ChannelCommandResponse::Err(reason).to_args()],
};
}
for response_args in response_args_groups {
if !response_args.0.is_empty() {
if let Some(ref values) = response_args.1 {
let values_string = values.join(" ");
write!(stream, "{} {}{}", response_args.0, values_string, LINE_FEED)
.expect("write failed");
tracing::trace!(
"wrote response with values: {} ({})",
response_args.0,
values_string
);
} else {
write!(stream, "{}{}", response_args.0, LINE_FEED).expect("write failed");
tracing::trace!("wrote response with no values: {}", response_args.0);
}
}
}
let command_took = command_start.elapsed();
if command_took.as_millis() >= COMMAND_ELAPSED_MILLIS_SLOW_WARN {
tracing::warn!(
"took a lot of time: {}ms to process channel message",
command_took.as_millis(),
);
} else {
tracing::trace!(
"took {}ms/{}us/{}ns to process channel message",
command_took.as_millis(),
command_took.as_micros(),
command_took.as_nanos(),
);
}
{
let command_took_millis = command_took.as_millis() as u32;
{
let mut worst = COMMAND_LATENCY_WORST.write().unwrap();
if command_took_millis > *worst {
*worst = command_took_millis;
}
}
{
let mut best = COMMAND_LATENCY_BEST.write().unwrap();
if command_took_millis > 0 && (*best == 0 || command_took_millis < *best) {
*best = command_took_millis;
}
}
*COMMANDS_TOTAL.write().unwrap() += 1;
}
result
}
}
impl ChannelMessage {
fn extract(message: &str) -> (String, SplitWhitespace<'_>) {
let mut parts = message.split_whitespace();
let command = parts.next().unwrap_or("").to_uppercase();
tracing::debug!("will dispatch search command: {}", command);
(command, parts)
}
}
impl<'this> ChannelMessageMode for ChannelMessageModeSearch<'this> {
fn handle(&self, message: &str) -> Result<Vec<ChannelCommandResponse>, ChannelCommandError> {
gen_channel_message_mode_handle!(message, COMMANDS_MODE_SEARCH, self, {
"QUERY" => ChannelCommandSearch::dispatch_query,
"SUGGEST" => ChannelCommandSearch::dispatch_suggest,
"LIST" => ChannelCommandSearch::dispatch_list,
"HELP" => ChannelCommandSearch::dispatch_help,
})
}
}
impl<'this> ChannelMessageMode for ChannelMessageModeIngest<'this> {
fn handle(&self, message: &str) -> Result<Vec<ChannelCommandResponse>, ChannelCommandError> {
gen_channel_message_mode_handle!(message, COMMANDS_MODE_INGEST, self, {
"PUSH" => ChannelCommandIngest::dispatch_push,
"POP" => ChannelCommandIngest::dispatch_pop,
"COUNT" => ChannelCommandIngest::dispatch_count,
"FLUSHC" => ChannelCommandIngest::dispatch_flushc,
"FLUSHB" => ChannelCommandIngest::dispatch_flushb,
"FLUSHO" => ChannelCommandIngest::dispatch_flusho,
"HELP" => ChannelCommandIngest::dispatch_help,
})
}
}
impl<'this> ChannelMessageMode for ChannelMessageModeControl<'this> {
fn handle(&self, message: &str) -> Result<Vec<ChannelCommandResponse>, ChannelCommandError> {
gen_channel_message_mode_handle!(message, COMMANDS_MODE_CONTROL, self, {
"TRIGGER" => ChannelCommandControl::dispatch_trigger,
"INFO" => ChannelCommandControl::dispatch_info,
"HELP" => ChannelCommandControl::dispatch_help,
})
}
}
macro_rules! gen_channel_message_mode_handle {
($message:ident, $commands:ident, $ctx:ident, { $($external:expr => $internal:expr),+, }) => {{
let (command, parts) = ChannelMessage::extract($message);
if command.is_empty() == true || $commands.contains(&command.as_str()) == true {
match command.as_str() {
"" => Ok(vec![ChannelCommandResponse::Void]),
$(
$external => $internal(parts, $ctx),
)+
"PING" => ChannelCommandBase::dispatch_ping(parts),
"QUIT" => ChannelCommandBase::dispatch_quit(parts),
_ => Ok(vec![ChannelCommandResponse::Err(
ChannelCommandError::InternalError,
)]),
}
} else {
tracing::warn!("Unknown command: {command:?}");
Ok(vec![ChannelCommandResponse::Err(
ChannelCommandError::UnknownCommand,
)])
}
}};
}
use gen_channel_message_mode_handle;