use std::io::Write as _;
use std::net::TcpStream;
use std::time::Duration;
use liminal::protocol::{Frame, encode, encoded_len};
use tungstenite::Message;
use tungstenite::protocol::WebSocket;
use tungstenite::protocol::frame::coding::CloseCode;
use crate::ServerError;
use crate::server::connection::incarnation::{
AMBIGUOUS_DURABLE_WRITE_PHASE, AUTHORITY_SURRENDERED_PHASE,
};
const REFUSAL_WRITE_TIMEOUT: Duration = Duration::from_millis(250);
const SERVER_ERROR_CODE: u16 = 0xFFFF;
pub(in crate::server) const MAX_CLOSE_REASON_BYTES: usize = 123;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AdmissionRefusal {
AdmissionHeld,
AuthoritySurrendered,
ConnectionsSaturated,
ParticipantServiceFatal,
IncarnationExhausted,
AllocationFailed,
SpawnFailed,
}
impl AdmissionRefusal {
pub(in crate::server) fn classify(error: &ServerError) -> Self {
match error {
ServerError::ConnectionLimitReached { .. } => Self::ConnectionsSaturated,
ServerError::ParticipantServiceFatal { .. } => Self::ParticipantServiceFatal,
ServerError::ConnectionIncarnationExhausted { .. }
| ServerError::ServerIncarnationExhausted => Self::IncarnationExhausted,
ServerError::ParticipantIncarnation { phase, .. }
if *phase == AMBIGUOUS_DURABLE_WRITE_PHASE =>
{
Self::AdmissionHeld
}
ServerError::ParticipantIncarnation { phase, .. }
if *phase == AUTHORITY_SURRENDERED_PHASE =>
{
Self::AuthoritySurrendered
}
ServerError::ParticipantIncarnation { .. } => Self::AllocationFailed,
_ => Self::SpawnFailed,
}
}
pub(crate) const LABELS: [&'static str; 7] = [
"admission_held",
"authority_surrendered",
"connections_saturated",
"participant_service_fatal",
"incarnation_exhausted",
"allocation_failed",
"spawn_failed",
];
pub(crate) const fn slot(self) -> usize {
match self {
Self::AdmissionHeld => 0,
Self::AuthoritySurrendered => 1,
Self::ConnectionsSaturated => 2,
Self::ParticipantServiceFatal => 3,
Self::IncarnationExhausted => 4,
Self::AllocationFailed => 5,
Self::SpawnFailed => 6,
}
}
pub(crate) const fn label(self) -> &'static str {
Self::LABELS[self.slot()]
}
pub(in crate::server) const fn reason(self) -> &'static str {
match self {
Self::AdmissionHeld => {
"admission held: the server's durable connection-incarnation write had an \
ambiguous result and admission is refused until it re-reads its store"
}
Self::AuthoritySurrendered => {
"admission surrendered: another server process owns this durable \
connection-incarnation stream"
}
Self::ConnectionsSaturated => {
"admission refused: the server is at its configured max_connections bound"
}
Self::ParticipantServiceFatal => {
"admission refused: the server's participant service has latched a fatal"
}
Self::IncarnationExhausted => {
"admission refused: the server's connection-incarnation space is exhausted"
}
Self::AllocationFailed => {
"admission refused: the server could not durably allocate a connection incarnation"
}
Self::SpawnFailed => {
"admission refused: the server could not start a connection process"
}
}
}
pub(in crate::server) const fn close_reason(self) -> &'static str {
match self {
Self::AdmissionHeld => "admission_held: durable incarnation write was ambiguous",
Self::AuthoritySurrendered => {
"authority_surrendered: another process owns the incarnation stream"
}
Self::ConnectionsSaturated => {
"connections_saturated: the max_connections bound is reached"
}
Self::ParticipantServiceFatal => {
"participant_service_fatal: the participant service has latched"
}
Self::IncarnationExhausted => {
"incarnation_exhausted: connection-incarnation space is spent"
}
Self::AllocationFailed => "allocation_failed: durable incarnation allocation failed",
Self::SpawnFailed => "spawn_failed: the connection process did not start",
}
}
pub(in crate::server) const fn close_code(self) -> u16 {
match self {
Self::AdmissionHeld => 4001,
Self::AuthoritySurrendered => 4002,
Self::ConnectionsSaturated => 4003,
Self::ParticipantServiceFatal => 4004,
Self::IncarnationExhausted => 4005,
Self::AllocationFailed => 4006,
Self::SpawnFailed => 4007,
}
}
pub(in crate::server) fn connect_error_frame(self) -> Frame {
Frame::ConnectError {
flags: 0,
reason_code: SERVER_ERROR_CODE,
message: Some(self.reason().to_owned()),
}
}
pub(in crate::server) fn connect_error_bytes(self) -> Option<Vec<u8>> {
let frame = self.connect_error_frame();
let needed = encoded_len(&frame).ok()?;
let mut bytes = vec![0_u8; needed];
let written = encode(&frame, &mut bytes).ok()?;
bytes.truncate(written);
Some(bytes)
}
}
fn clamp_close_reason(reason: &str) -> &str {
if reason.len() <= MAX_CLOSE_REASON_BYTES {
return reason;
}
let boundary = reason
.char_indices()
.map(|(index, _)| index)
.take_while(|index| *index <= MAX_CLOSE_REASON_BYTES)
.last()
.unwrap_or(0);
reason.get(..boundary).unwrap_or("")
}
pub(in crate::server) fn send_websocket_refusal(
socket: &mut WebSocket<TcpStream>,
refusal: AdmissionRefusal,
) -> Result<(), tungstenite::Error> {
let stream = socket.get_ref();
stream
.set_nonblocking(false)
.map_err(tungstenite::Error::Io)?;
stream
.set_write_timeout(Some(REFUSAL_WRITE_TIMEOUT))
.map_err(tungstenite::Error::Io)?;
if let Some(bytes) = refusal.connect_error_bytes() {
socket.send(Message::Binary(bytes.into()))?;
}
socket.close(Some(tungstenite::protocol::CloseFrame {
code: CloseCode::Library(refusal.close_code()),
reason: clamp_close_reason(refusal.close_reason()).into(),
}))?;
socket.flush()
}
pub(in crate::server) fn send_tcp_refusal(
stream: &mut TcpStream,
refusal: AdmissionRefusal,
) -> std::io::Result<()> {
let Some(bytes) = refusal.connect_error_bytes() else {
return Ok(());
};
stream.set_nonblocking(false)?;
stream.set_write_timeout(Some(REFUSAL_WRITE_TIMEOUT))?;
stream.write_all(&bytes)?;
stream.flush()?;
stream.shutdown(std::net::Shutdown::Both)
}