Skip to content

Commit 1bb56c9

Browse files
authored
Add Kraken spot WebSocket idle timeout to detect dead connections (#4275)
`KrakenSpotWebSocketClient::connect` hardcoded `idle_timeout_ms: None`, so a connection that acknowledges a subscription but never attaches the data fan-out (socket stays open, no close frame, no transport error) was undetectable: the strategy received no data indefinitely with no reconnect. Add `ws_idle_timeout_ms` to `KrakenDataClientConfig` (default 10s) and wire it into the spot v2 WS config. The read loop resets its idle timer on Kraken application frames (text/binary), and Kraken sends a `heartbeat` text frame once per second while any subscription is active, so a live subscribed connection stays well within the window while a silent one trips it and runs the existing reconnect + resubscribe path. `0` disables the timeout. The default is kept short enough to rely on the 1/s heartbeats rather than the ~30s keepalive pong, which assumes the connection carries a subscription; this is documented on the field. The authenticated execution WS opts out (`0`) to preserve its current behavior, since this issue concerns the public data WS. Fixes #4255. Coded by an LLM.
1 parent 947e8ca commit 1bb56c9

5 files changed

Lines changed: 45 additions & 1 deletion

File tree

crates/adapters/kraken/src/config.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,27 @@ pub struct KrakenDataClientConfig {
5959
pub timeout_secs: u64,
6060
#[builder(default = 30)]
6161
pub heartbeat_interval_secs: u64,
62+
/// Idle timeout (milliseconds) for the spot v2 WebSocket.
63+
///
64+
/// If no application data (any text or binary frame) is received within this
65+
/// window, the connection is treated as dead and the client reconnects and
66+
/// resubscribes. This recovers from a backend that acknowledges a
67+
/// subscription but never attaches the data fan-out: the socket stays open
68+
/// with no close frame or transport error, so nothing else detects it.
69+
///
70+
/// Kraken sends a `heartbeat` text frame once per second while at least one
71+
/// subscription is active, so a live subscribed connection resets this timer
72+
/// well within the window. Note the client's keepalive `ping` is answered
73+
/// with a `pong` *text* frame, which also resets the timer roughly every
74+
/// `heartbeat_interval_secs`; the default below is therefore kept short
75+
/// enough to rely on the 1/s heartbeats rather than the keepalive, which
76+
/// assumes the connection carries at least one subscription. A connection
77+
/// held open without any subscription should disable this (`0`) or raise it
78+
/// above `heartbeat_interval_secs`.
79+
///
80+
/// `0` disables the idle timeout.
81+
#[builder(default = 10_000)]
82+
pub ws_idle_timeout_ms: u64,
6283
pub max_requests_per_second: Option<u32>,
6384
#[builder(default)]
6485
pub transport_backend: TransportBackend,
@@ -283,6 +304,19 @@ validate_l3_checksum = false
283304
assert!(!config.validate_l3_checksum);
284305
}
285306

307+
#[rstest]
308+
fn test_data_config_ws_idle_timeout_default() {
309+
let config = KrakenDataClientConfig::default();
310+
assert_eq!(config.ws_idle_timeout_ms, 10_000);
311+
}
312+
313+
#[rstest]
314+
fn test_data_config_ws_idle_timeout_override() {
315+
let config: KrakenDataClientConfig = toml::from_str("ws_idle_timeout_ms = 0").unwrap();
316+
317+
assert_eq!(config.ws_idle_timeout_ms, 0);
318+
}
319+
286320
#[rstest]
287321
fn test_exec_config_toml_empty_uses_defaults() {
288322
let config: KrakenExecClientConfig = toml::from_str("").unwrap();

crates/adapters/kraken/src/execution/spot.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ impl KrakenSpotExecutionClient {
161161
proxy_url: config.proxy_url.clone(),
162162
timeout_secs: config.timeout_secs,
163163
heartbeat_interval_secs: config.heartbeat_interval_secs,
164+
// The authenticated order-routing WS keeps its prior behavior (no
165+
// idle timeout); issue #4255 concerns the public spot data WS.
166+
ws_idle_timeout_ms: 0,
164167
max_requests_per_second: config.max_requests_per_second,
165168
transport_backend: config.transport_backend,
166169
};

crates/adapters/kraken/src/python/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ impl KrakenDataClientConfig {
4545
proxy_url = None,
4646
timeout_secs = None,
4747
heartbeat_interval_secs = None,
48+
ws_idle_timeout_ms = None,
4849
max_requests_per_second = None,
4950
))]
5051
#[expect(clippy::too_many_arguments)]
@@ -61,6 +62,7 @@ impl KrakenDataClientConfig {
6162
proxy_url: Option<String>,
6263
timeout_secs: Option<u64>,
6364
heartbeat_interval_secs: Option<u64>,
65+
ws_idle_timeout_ms: Option<u64>,
6466
max_requests_per_second: Option<u32>,
6567
) -> Self {
6668
let defaults = Self::default();
@@ -78,6 +80,7 @@ impl KrakenDataClientConfig {
7880
timeout_secs: timeout_secs.unwrap_or(defaults.timeout_secs),
7981
heartbeat_interval_secs: heartbeat_interval_secs
8082
.unwrap_or(defaults.heartbeat_interval_secs),
83+
ws_idle_timeout_ms: ws_idle_timeout_ms.unwrap_or(defaults.ws_idle_timeout_ms),
8184
max_requests_per_second,
8285
transport_backend: defaults.transport_backend,
8386
}

crates/adapters/kraken/src/websocket/spot_v2/client.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,10 @@ impl KrakenSpotWebSocketClient {
259259
reconnect_backoff_factor: Some(1.5),
260260
reconnect_jitter_ms: Some(250),
261261
reconnect_max_attempts: None,
262-
idle_timeout_ms: None,
262+
// Treat a silent connection as dead so the reconnect + resubscribe
263+
// path runs. `0` disables; see `ws_idle_timeout_ms` docs (issue #4255).
264+
idle_timeout_ms: (self.config.ws_idle_timeout_ms != 0)
265+
.then_some(self.config.ws_idle_timeout_ms),
263266
backend: self.transport_backend,
264267
proxy_url: self.proxy_url.clone(),
265268
};

python/nautilus_trader/adapters/kraken/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class KrakenDataClientConfig:
5656
proxy_url: str | None = None,
5757
timeout_secs: int | None = None,
5858
heartbeat_interval_secs: int | None = None,
59+
ws_idle_timeout_ms: int | None = None,
5960
max_requests_per_second: int | None = None,
6061
) -> None: ...
6162

0 commit comments

Comments
 (0)