Upstream valeriansaliou/sonic state of the touched files before 534733e716
This commit is contained in:
commit
c492bcc85f
1 file changed
+317
@@ -0,0 +1,317 @@
|
||||
// Sonic
|
||||
//
|
||||
// Fast, lightweight and schema-less search backend
|
||||
// Copyright: 2019, Valerian Saliou <valerian@valeriansaliou.name>
|
||||
// Copyright: 2026, Rémi Bardon <remi@remibardon.name>
|
||||
// License: Mozilla Public License v2.0 (MPL v2.0)
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::result::Result;
|
||||
use std::str;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use sonic::Executor;
|
||||
|
||||
use super::message::{
|
||||
ChannelMessageModeControl, ChannelMessageModeIngest, ChannelMessageModeSearch,
|
||||
ChannelMessageResult,
|
||||
};
|
||||
use super::mode::ChannelMode;
|
||||
use super::statistics::CLIENTS_CONNECTED;
|
||||
use crate::LINE_FEED;
|
||||
|
||||
pub struct ChannelHandle {
|
||||
pub app_conf: Arc<crate::Config>,
|
||||
pub executor: Executor,
|
||||
}
|
||||
|
||||
enum ChannelHandleError {
|
||||
Closed,
|
||||
InvalidMode,
|
||||
AuthenticationRequired,
|
||||
AuthenticationFailed,
|
||||
NotRecognized,
|
||||
TimedOut,
|
||||
ConnectionAborted,
|
||||
Interrupted,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
const LINE_END_GAP: usize = 1;
|
||||
const BUFFER_SIZE: usize = 20000;
|
||||
const MAX_LINE_SIZE: usize = BUFFER_SIZE + LINE_END_GAP + 1;
|
||||
const TCP_TIMEOUT_NON_ESTABLISHED: u64 = 10;
|
||||
const PROTOCOL_REVISION: u8 = 1;
|
||||
const BUFFER_LINE_SEPARATOR: u8 = b'\n';
|
||||
|
||||
const CONNECTED_BANNER: &str = concat!(
|
||||
"CONNECTED <",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
" v",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
">"
|
||||
);
|
||||
|
||||
impl ChannelHandleError {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match *self {
|
||||
ChannelHandleError::Closed => "closed",
|
||||
ChannelHandleError::InvalidMode => "invalid_mode",
|
||||
ChannelHandleError::AuthenticationRequired => "authentication_required",
|
||||
ChannelHandleError::AuthenticationFailed => "authentication_failed",
|
||||
ChannelHandleError::NotRecognized => "not_recognized",
|
||||
ChannelHandleError::TimedOut => "timed_out",
|
||||
ChannelHandleError::ConnectionAborted => "connection_aborted",
|
||||
ChannelHandleError::Interrupted => "interrupted",
|
||||
ChannelHandleError::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelHandle {
|
||||
pub fn client(&self, mut stream: TcpStream) {
|
||||
// Configure stream (non-established)
|
||||
self.configure_stream(&stream, false);
|
||||
|
||||
// Send connected banner
|
||||
write!(stream, "{}{}", CONNECTED_BANNER, LINE_FEED).expect("write failed");
|
||||
|
||||
// Increment connected clients count
|
||||
*CLIENTS_CONNECTED.write().unwrap() += 1;
|
||||
|
||||
// Ensure channel mode is set
|
||||
match self.ensure_start(&stream) {
|
||||
Ok(mode) => {
|
||||
// Configure stream (established)
|
||||
self.configure_stream(&stream, true);
|
||||
|
||||
// Send started acknowledgement (with environment variables)
|
||||
write!(
|
||||
stream,
|
||||
"STARTED {} protocol({}) buffer({}){}",
|
||||
mode.to_str(),
|
||||
PROTOCOL_REVISION,
|
||||
BUFFER_SIZE,
|
||||
LINE_FEED
|
||||
)
|
||||
.expect("write failed");
|
||||
|
||||
self.handle_stream(mode, stream);
|
||||
}
|
||||
Err(err) => match write!(stream, "ENDED {}{}", err.to_str(), LINE_FEED) {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.kind() == ErrorKind::BrokenPipe => {}
|
||||
Err(err) => panic!("write failed: {err:?}"),
|
||||
},
|
||||
}
|
||||
|
||||
// Decrement connected clients count
|
||||
*CLIENTS_CONNECTED.write().unwrap() -= 1;
|
||||
}
|
||||
|
||||
fn configure_stream(&self, stream: &TcpStream, is_established: bool) {
|
||||
let tcp_timeout = if is_established {
|
||||
self.app_conf.channel.tcp_timeout
|
||||
} else {
|
||||
TCP_TIMEOUT_NON_ESTABLISHED
|
||||
};
|
||||
|
||||
assert!(stream.set_nodelay(true).is_ok());
|
||||
|
||||
assert!(
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::new(tcp_timeout, 0)))
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::new(tcp_timeout, 0)))
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_stream(&self, mode: ChannelMode, mut stream: TcpStream) {
|
||||
// Initialize packet buffer
|
||||
let mut buffer: VecDeque<u8> = VecDeque::with_capacity(MAX_LINE_SIZE);
|
||||
|
||||
// Wait for incoming messages
|
||||
'handler: loop {
|
||||
let mut read = [0; MAX_LINE_SIZE];
|
||||
|
||||
match stream.read(&mut read) {
|
||||
Ok(n) => {
|
||||
// Should close?
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let (mut chunk, mut read) =
|
||||
read[..n].split_at(n.min(MAX_LINE_SIZE - buffer.len()));
|
||||
|
||||
// Add chunk to buffer
|
||||
buffer.extend(chunk);
|
||||
assert!(buffer.len() <= MAX_LINE_SIZE);
|
||||
|
||||
// Handle full lines from buffer (keep the last incomplete line in buffer)
|
||||
{
|
||||
let mut processed_line = Vec::with_capacity(MAX_LINE_SIZE);
|
||||
|
||||
while let Some(byte) = buffer.pop_front() {
|
||||
// Commit line and start a new one?
|
||||
if byte == BUFFER_LINE_SEPARATOR {
|
||||
if self.on_message(&mode, &stream, &processed_line)
|
||||
== ChannelMessageResult::Close
|
||||
{
|
||||
// Should close?
|
||||
break 'handler;
|
||||
}
|
||||
|
||||
// Important: clear the contents of the line, as it has just been \
|
||||
// processed.
|
||||
processed_line.clear();
|
||||
|
||||
(chunk, read) =
|
||||
read.split_at(read.len().min(MAX_LINE_SIZE - buffer.len()));
|
||||
|
||||
// Add chunk to buffer
|
||||
buffer.extend(chunk);
|
||||
assert!(buffer.len() <= MAX_LINE_SIZE);
|
||||
} else {
|
||||
// Append current byte to processed line
|
||||
processed_line.push(byte);
|
||||
}
|
||||
}
|
||||
|
||||
// Incomplete line remaining? Put it back in buffer.
|
||||
if !processed_line.is_empty() {
|
||||
buffer.extend(processed_line);
|
||||
assert!(buffer.len() <= MAX_LINE_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for buffer overflow.
|
||||
// NOTE: To avoid a needless read of `MAX_LINE_SIZE` bytes,
|
||||
// we also ensure there is enough space for the line
|
||||
// separator. If there isn’t, next loop cycle will abort
|
||||
// because the line is too long anyway.
|
||||
let separator_len = char::from(BUFFER_LINE_SEPARATOR).len_utf8();
|
||||
|
||||
if (buffer.len() + read.len()) < (MAX_LINE_SIZE - separator_len) {
|
||||
buffer.extend(read);
|
||||
} else {
|
||||
// Do not continue, as there is too much pending data
|
||||
// in the buffer. Most likely the client does not
|
||||
// implement a proper back-pressure management system,
|
||||
// thus we terminate it.
|
||||
tracing::error!("closing channel thread because of buffer overflow");
|
||||
|
||||
panic!(
|
||||
"buffer overflow ({}/{} bytes)",
|
||||
buffer.len() + read.len(),
|
||||
MAX_LINE_SIZE
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("closing channel thread with traceback: {}", err);
|
||||
|
||||
panic!("closing channel");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_start(&self, mut stream: &TcpStream) -> Result<ChannelMode, ChannelHandleError> {
|
||||
#[allow(clippy::never_loop)]
|
||||
loop {
|
||||
let mut read = [0; MAX_LINE_SIZE];
|
||||
|
||||
match stream.read(&mut read) {
|
||||
Ok(n) => {
|
||||
if n == 0 {
|
||||
return Err(ChannelHandleError::Closed);
|
||||
}
|
||||
|
||||
let mut parts = str::from_utf8(&read[0..n]).unwrap_or("").split_whitespace();
|
||||
|
||||
if parts.next().unwrap_or("").to_uppercase().as_str() == "START" {
|
||||
if let Some(res_mode) = parts.next() {
|
||||
tracing::debug!("got mode response: {}", res_mode);
|
||||
|
||||
// Extract mode
|
||||
if let Ok(mode) = ChannelMode::from_str(res_mode) {
|
||||
// Check if authenticated?
|
||||
if let Some(ref auth_password) = self.app_conf.channel.auth_password
|
||||
{
|
||||
if let Some(provided_auth) = parts.next() {
|
||||
// Compare provided password with configured password
|
||||
if provided_auth != auth_password {
|
||||
tracing::info!("password provided, but does not match");
|
||||
|
||||
return Err(ChannelHandleError::AuthenticationFailed);
|
||||
}
|
||||
} else {
|
||||
tracing::info!("no password provided, but one required");
|
||||
|
||||
// No password was provided, but we require one
|
||||
return Err(ChannelHandleError::AuthenticationRequired);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(mode);
|
||||
}
|
||||
}
|
||||
|
||||
return Err(ChannelHandleError::InvalidMode);
|
||||
}
|
||||
|
||||
return Err(ChannelHandleError::NotRecognized);
|
||||
}
|
||||
Err(err) => {
|
||||
let err_reason = match err.kind() {
|
||||
ErrorKind::TimedOut => ChannelHandleError::TimedOut,
|
||||
ErrorKind::ConnectionAborted => ChannelHandleError::ConnectionAborted,
|
||||
ErrorKind::Interrupted => ChannelHandleError::Interrupted,
|
||||
_ => ChannelHandleError::Unknown,
|
||||
};
|
||||
|
||||
return Err(err_reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_message(
|
||||
&self,
|
||||
mode: &ChannelMode,
|
||||
stream: &TcpStream,
|
||||
message_slice: &[u8],
|
||||
) -> ChannelMessageResult {
|
||||
use crate::channel::message::ChannelMessageMode as _;
|
||||
|
||||
match mode {
|
||||
ChannelMode::Search => ChannelMessageModeSearch {
|
||||
executor: &self.executor,
|
||||
search_config: &self.app_conf.sonic.search,
|
||||
normalization_config: &self.app_conf.sonic.normalization,
|
||||
tokenization_config: &self.app_conf.sonic.tokenization,
|
||||
stopwords_config: &self.app_conf.sonic.stopwords,
|
||||
}
|
||||
.on(stream, message_slice),
|
||||
ChannelMode::Ingest => ChannelMessageModeIngest {
|
||||
executor: &self.executor,
|
||||
normalization_config: &self.app_conf.sonic.normalization,
|
||||
tokenization_config: &self.app_conf.sonic.tokenization,
|
||||
stopwords_config: &self.app_conf.sonic.stopwords,
|
||||
}
|
||||
.on(stream, message_slice),
|
||||
ChannelMode::Control => ChannelMessageModeControl {
|
||||
executor: &self.executor,
|
||||
}
|
||||
.on(stream, message_slice),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user