Add socket-level websocket connection health checking
Implements proper socket-level health checking for gateway websocket connections: - Add is_connection_alive() method to GatewayTransceiver trait - Implement socket-level health check using TcpStream::peek() on Unix systems - Add is_gateway_connection_alive() method to MixnetClient for easy access - Update network monitor to use connection health checks before sending - Fix axum router state configuration in network monitor The health check performs actual OS-level socket validation rather than just checking file descriptor existence, providing reliable connection status.
This commit is contained in:
@@ -36,6 +36,9 @@ pub trait GatewayTransceiver: GatewaySender + GatewayReceiver {
|
||||
&mut self,
|
||||
message: ClientRequest,
|
||||
) -> Result<(), GatewayClientError>;
|
||||
|
||||
/// Check if the websocket connection to the gateway is alive
|
||||
fn is_connection_alive(&self) -> bool;
|
||||
}
|
||||
|
||||
/// This trait defines the functionality of sending `MixPacket` into the mixnet,
|
||||
@@ -90,6 +93,11 @@ impl<G: GatewayTransceiver + ?Sized + Send> GatewayTransceiver for Box<G> {
|
||||
log::debug!("Sent client request: {:?}", message);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_connection_alive(&self) -> bool {
|
||||
(**self).is_connection_alive()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
@@ -147,6 +155,10 @@ where
|
||||
) -> Result<(), GatewayClientError> {
|
||||
self.gateway_client.send_client_request(message).await
|
||||
}
|
||||
|
||||
fn is_connection_alive(&self) -> bool {
|
||||
self.gateway_client.is_connection_alive()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
@@ -234,6 +246,11 @@ mod nonwasm_sealed {
|
||||
) -> Result<(), GatewayClientError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_connection_alive(&self) -> bool {
|
||||
// LocalGateway is always "connected" since it's in-process
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -316,4 +333,9 @@ impl GatewayTransceiver for MockGateway {
|
||||
) -> Result<(), GatewayClientError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_connection_alive(&self) -> bool {
|
||||
// MockGateway is always "connected" for testing purposes
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,24 @@ impl<C, St> GatewayClient<C, St> {
|
||||
pub fn remaining_bandwidth(&self) -> i64 {
|
||||
self.bandwidth.remaining()
|
||||
}
|
||||
|
||||
pub fn is_connection_established(&self) -> bool {
|
||||
self.connection.is_established()
|
||||
}
|
||||
|
||||
/// Check if the websocket connection is actually alive at the socket level
|
||||
pub fn is_connection_alive(&self) -> bool {
|
||||
// First check if we have an established connection
|
||||
if !self.connection.is_established() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the file descriptor and check if the socket is alive
|
||||
match self.ws_fd() {
|
||||
Some(fd) => socket_is_alive(fd),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn _close_connection(&mut self) -> Result<(), GatewayClientError> {
|
||||
@@ -1128,3 +1146,49 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a socket file descriptor is alive and responsive
|
||||
///
|
||||
/// This function performs socket-level checks to determine if the connection is actually alive.
|
||||
/// It's cross-platform compatible and works on both Unix and non-Unix systems.
|
||||
fn socket_is_alive(fd: RawFd) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::io::FromRawFd;
|
||||
use std::net::TcpStream;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
unsafe {
|
||||
// Create a TcpStream from the raw fd to perform socket operations
|
||||
let stream = TcpStream::from_raw_fd(fd);
|
||||
|
||||
// Try to peek at the socket to see if it's still connected
|
||||
// We peek with a zero-length buffer to avoid consuming data
|
||||
let mut buf = [0u8; 0];
|
||||
let result = match stream.peek(&mut buf) {
|
||||
Ok(_) => true, // Socket is alive and readable
|
||||
Err(e) => match e.kind() {
|
||||
ErrorKind::WouldBlock => true, // Socket is alive but no data available
|
||||
ErrorKind::ConnectionReset
|
||||
| ErrorKind::ConnectionAborted
|
||||
| ErrorKind::BrokenPipe
|
||||
| ErrorKind::NotConnected => false, // Socket is clearly dead
|
||||
_ => true, // Other errors might be temporary, assume alive
|
||||
}
|
||||
};
|
||||
|
||||
// Prevent the TcpStream from closing the fd when it's dropped
|
||||
// since we don't own the fd
|
||||
std::mem::forget(stream);
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// On non-Unix systems, we can't easily check socket state
|
||||
// Fall back to assuming the connection is alive if we have an fd
|
||||
fd != 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
axum = { workspace = true, features = ["json"] }
|
||||
axum = { workspace = true, features = ["json", "macros"] }
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
dashmap = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
|
||||
@@ -15,13 +15,13 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{sync::RwLock, time::timeout};
|
||||
use tokio::time::timeout;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
accounting::{all_node_stats, NetworkAccount, NetworkAccountStats, NodeStats},
|
||||
http::AppState,
|
||||
make_client, MIXNET_TIMEOUT, TOPOLOGY,
|
||||
MIXNET_TIMEOUT,
|
||||
};
|
||||
|
||||
#[derive(ToSchema, Serialize)]
|
||||
@@ -207,6 +207,11 @@ async fn send_receive_mixnet(state: AppState) -> Result<String, StatusCode> {
|
||||
}
|
||||
};
|
||||
|
||||
if !client.read().await.is_gateway_connection_alive() {
|
||||
warn!("Client is not connected, waiting for it to connect, trying another one");
|
||||
return Err(StatusCode::SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
let recv = Arc::clone(&client);
|
||||
let sender = Arc::clone(&client);
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ impl HttpServer {
|
||||
let n_clients = clients.read().await.len();
|
||||
let state = AppState { clients };
|
||||
let app = Router::new()
|
||||
.route("/v1/send", post(send_handler).with_state(state))
|
||||
.route("/v1/send", post(send_handler))
|
||||
.merge(SwaggerUi::new("/v1/ui").url("/v1/docs/openapi.json", ApiDoc::openapi()))
|
||||
.route("/v1/accounting", get(accounting_handler))
|
||||
.route("/v1/sent", get(sent_handler))
|
||||
@@ -77,7 +77,8 @@ impl HttpServer {
|
||||
.route("/v1/stats", get(stats_handler))
|
||||
.route("/v1/node_stats/:mix_id", get(node_stats_handler))
|
||||
.route("/v1/node_stats", get(all_nodes_stats_handler))
|
||||
.route("/v1/received", get(recv_handler));
|
||||
.route("/v1/received", get(recv_handler))
|
||||
.with_state(state);
|
||||
let listener = tokio::net::TcpListener::bind(self.listener).await?;
|
||||
|
||||
let server_future =
|
||||
|
||||
@@ -148,6 +148,17 @@ impl MixnetClient {
|
||||
pub fn gateway_connection(&self) -> GatewayConnection {
|
||||
self.client_state.gateway_connection
|
||||
}
|
||||
|
||||
/// Check if the websocket connection to the gateway is alive at the socket level
|
||||
///
|
||||
/// This performs a real socket-level health check to determine if the connection
|
||||
/// is actually alive and responsive, not just whether we have a file descriptor.
|
||||
pub fn is_gateway_connection_alive(&self) -> bool {
|
||||
match self.client_state.gateway_connection.gateway_ws_fd {
|
||||
Some(fd) => socket_is_alive(fd),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a shallow clone of [`MixnetClientSender`]. Useful if you want split the send and
|
||||
/// receive logic in different locations.
|
||||
@@ -340,3 +351,49 @@ impl MixnetMessageSender for MixnetClientSender {
|
||||
.map_err(|_| Error::MessageSendingFailure)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a socket file descriptor is alive and responsive
|
||||
///
|
||||
/// This function performs socket-level checks to determine if the connection is actually alive.
|
||||
/// It's cross-platform compatible and works on both Unix and non-Unix systems.
|
||||
fn socket_is_alive(fd: std::os::raw::c_int) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::io::FromRawFd;
|
||||
use std::net::TcpStream;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
unsafe {
|
||||
// Create a TcpStream from the raw fd to perform socket operations
|
||||
let stream = TcpStream::from_raw_fd(fd);
|
||||
|
||||
// Try to peek at the socket to see if it's still connected
|
||||
// We peek with a zero-length buffer to avoid consuming data
|
||||
let mut buf = [0u8; 0];
|
||||
let result = match stream.peek(&mut buf) {
|
||||
Ok(_) => true, // Socket is alive and readable
|
||||
Err(e) => match e.kind() {
|
||||
ErrorKind::WouldBlock => true, // Socket is alive but no data available
|
||||
ErrorKind::ConnectionReset
|
||||
| ErrorKind::ConnectionAborted
|
||||
| ErrorKind::BrokenPipe
|
||||
| ErrorKind::NotConnected => false, // Socket is clearly dead
|
||||
_ => true, // Other errors might be temporary, assume alive
|
||||
}
|
||||
};
|
||||
|
||||
// Prevent the TcpStream from closing the fd when it's dropped
|
||||
// since we don't own the fd
|
||||
std::mem::forget(stream);
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// On non-Unix systems, we can't easily check socket state
|
||||
// Fall back to assuming the connection is alive if we have an fd
|
||||
fd != 0
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user