From be89765d80183a97579a8cf6f44d0d6d6ecf7885 Mon Sep 17 00:00:00 2001 From: 0xcathiefish <72328723+0xcathiefish@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:03:01 +0000 Subject: [PATCH] fix: recover Hyperliquid WS from idle-disconnect and bound TradeSnap requests The monitor could hang indefinitely (last event 2026-06-01) after an idle server disconnect. Root cause: InfoClient::new disables SDK reconnect, so on disconnect the reader task emitted a single Message::NoData and exited without dropping the channel sender; the old code swallowed NoData into the catch-all arm and looped back to recv(), which then blocked forever. - hyperliquid.rs: create the client with InfoClient::with_reconnect so the SDK auto-reconnects (~1s) and resubscribes UserEvents on the same channel. Handle Message::NoData explicitly for observability instead of swallowing it. - Drop the 180s recv() timeout: the SDK does not forward Pong frames to the subscription channel, so a quiet market (no fills) is indistinguishable from a dead connection and any finite timeout caused spurious reconnects, each opening a blind window where fills (not backfilled on resubscribe) are missed. - notion.rs: give the TradeSnap HTTP client a 15s timeout. Events are consumed serially, so a hung screenshot request would otherwise stall all trade syncing. Co-Authored-By: Claude Opus 4.8 --- src/hyperliquid.rs | 14 +++++++++++++- src/notion.rs | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/hyperliquid.rs b/src/hyperliquid.rs index 21f458a..d9d5629 100644 --- a/src/hyperliquid.rs +++ b/src/hyperliquid.rs @@ -47,7 +47,7 @@ impl HyperliquidMonitor { loop { info!("Connecting to Hyperliquid InfoClient..."); - let mut info_client = match InfoClient::new(None, Some(base_url)).await { + let mut info_client = match InfoClient::with_reconnect(None, Some(base_url)).await { Ok(client) => client, Err(e) => { error!( @@ -174,6 +174,18 @@ impl HyperliquidMonitor { Some(Message::Pong) => { debug!("Received Pong from Hyperliquid WS"); } + Some(Message::NoData) => { + // Server idle-disconnect or dropped connection. Because the + // client is created with InfoClient::with_reconnect, the SDK + // auto-reconnects (~1s) and resubscribes UserEvents on the same + // channel, so we keep reading instead of tearing down here. + // NOTE: a short application-level recv() timeout was intentionally + // NOT added: the SDK does not forward Pong frames to the + // subscription channel, so a quiet market (no fills) is + // indistinguishable from a dead connection and any finite timeout + // would cause spurious reconnects and missed-fill blind windows. + warn!("Hyperliquid WS disconnected; SDK is auto-reconnecting..."); + } Some(other) => { debug!(msg = ?other, "Received other message from Hyperliquid WS"); } diff --git a/src/notion.rs b/src/notion.rs index 9eadbc3..367b182 100644 --- a/src/notion.rs +++ b/src/notion.rs @@ -231,7 +231,10 @@ impl NotionWriter { format!("BINANCE:{}USDC.P", coin.to_uppercase()) }; let tradesnap_url = url.trim_end_matches('/'); - let http_client = reqwest::Client::new(); + let http_client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(15)) + .build() + .unwrap_or_else(|_| reqwest::Client::new()); let mut children = Vec::new(); let mut timeframes = Vec::new();