diff --git a/CHANGELOG.md b/CHANGELOG.md index e00020ad..a71c0e80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [13.0.0-rc.13] - 2026-07-02 +### Changed + +- The default streaming read timeout is now 10s (was 3s), matching the terminal's streaming socket read timeout so a client rides through the brief silence right after a full-market subscribe instead of tripping the deadline and forcing an unnecessary reconnect. Still configurable via `streaming.timeout_ms`. + ### Fixed - **Windows server archive attaches to the release.** The Windows `thetadatadx-server` archive was built, but the upload step received a path the artifact action could not resolve on Windows, so the GitHub Release carried no Windows binary. The archive is now referenced by its native workspace path and attaches correctly. diff --git a/docs-site/docs/changelog.md b/docs-site/docs/changelog.md index e00020ad..a71c0e80 100644 --- a/docs-site/docs/changelog.md +++ b/docs-site/docs/changelog.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [13.0.0-rc.13] - 2026-07-02 +### Changed + +- The default streaming read timeout is now 10s (was 3s), matching the terminal's streaming socket read timeout so a client rides through the brief silence right after a full-market subscribe instead of tripping the deadline and forcing an unnecessary reconnect. Still configurable via `streaming.timeout_ms`. + ### Fixed - **Windows server archive attaches to the release.** The Windows `thetadatadx-server` archive was built, but the upload step received a path the artifact action could not resolve on Windows, so the GitHub Release carried no Windows binary. The archive is now referenced by its native workspace path and attaches correctly. diff --git a/thetadatadx-cpp/include/config_accessors.hpp.inc b/thetadatadx-cpp/include/config_accessors.hpp.inc index 18b20e0e..55179e1e 100644 --- a/thetadatadx-cpp/include/config_accessors.hpp.inc +++ b/thetadatadx-cpp/include/config_accessors.hpp.inc @@ -81,13 +81,13 @@ } /// Set the streaming read timeout (ms): the no-frames deadline after - /// which the streaming I/O loop reconnects. Default 3_000; + /// which the streaming I/O loop reconnects. Default 10_000; /// validated to [100, 60_000] at connect. void set_streaming_timeout_ms(std::uint64_t v) { thetadatadx_config_set_streaming_timeout_ms(handle_.get(), v); } - /// Current streaming timeout_ms (default 3_000). + /// Current streaming timeout_ms (default 10_000). std::uint64_t get_streaming_timeout_ms() const { std::uint64_t out{}; thetadatadx_config_get_streaming_timeout_ms(handle_.get(), &out); diff --git a/thetadatadx-cpp/include/thetadatadx.h b/thetadatadx-cpp/include/thetadatadx.h index 0763b16c..6bcbac72 100644 --- a/thetadatadx-cpp/include/thetadatadx.h +++ b/thetadatadx-cpp/include/thetadatadx.h @@ -1452,7 +1452,7 @@ int32_t thetadatadx_config_set_reconnect_callback(ThetaDataDxConfig* config, The /** * Set the streaming read timeout (ms): the no-frames deadline after which - * the streaming session is declared dead and reconnects. Default 3_000; + * the streaming session is declared dead and reconnects. Default 10_000; * validated to [100, 60_000] at connect. * @param config Config handle to mutate; no-op when NULL. * @param v Read timeout in milliseconds. @@ -1460,7 +1460,7 @@ int32_t thetadatadx_config_set_reconnect_callback(ThetaDataDxConfig* config, The void thetadatadx_config_set_streaming_timeout_ms(ThetaDataDxConfig* config, uint64_t v); /** - * Read the current streaming timeout_ms setting (default 3_000). + * Read the current streaming timeout_ms setting (default 10_000). * @param config Config handle to read. * @param out Receives the timeout in milliseconds on success. * @return 0 on success, -1 if either pointer is null. diff --git a/thetadatadx-ffi/src/auth.rs b/thetadatadx-ffi/src/auth.rs index 0678f396..1f8e8a1d 100644 --- a/thetadatadx-ffi/src/auth.rs +++ b/thetadatadx-ffi/src/auth.rs @@ -2189,7 +2189,7 @@ mod resilience_knob_tests { super::thetadatadx_config_get_streaming_timeout_ms(cfg, &mut got), 0 ); - assert_eq!(got, 3_000); + assert_eq!(got, 10_000); super::thetadatadx_config_set_streaming_timeout_ms(cfg, 9_000); assert_eq!( super::thetadatadx_config_get_streaming_timeout_ms(cfg, &mut got), diff --git a/thetadatadx-ffi/src/config_accessors.rs b/thetadatadx-ffi/src/config_accessors.rs index 6dffc019..2cf61a60 100644 --- a/thetadatadx-ffi/src/config_accessors.rs +++ b/thetadatadx-ffi/src/config_accessors.rs @@ -230,7 +230,7 @@ pub unsafe extern "C" fn thetadatadx_config_get_reconnect_replay_pace_ms( }) } -/// Set the streaming read timeout (ms): the no-frames deadline after which the streaming I/O loop declares the session dead and reconnects. Default `3_000`; validated to `[100, 60_000]` at connect. +/// Set the streaming read timeout (ms): the no-frames deadline after which the streaming I/O loop declares the session dead and reconnects. Default `10_000`; validated to `[100, 60_000]` at connect. #[no_mangle] pub unsafe extern "C" fn thetadatadx_config_set_streaming_timeout_ms( config: *mut ThetaDataDxConfig, @@ -242,7 +242,7 @@ pub unsafe extern "C" fn thetadatadx_config_set_streaming_timeout_ms( }) } -/// Read the current streaming `timeout_ms` setting (default `3_000`). +/// Read the current streaming `timeout_ms` setting (default `10_000`). /// /// Writes the configured value into `*out`. Returns `0` on success, /// `-1` if either pointer is null. diff --git a/thetadatadx-py/python/thetadatadx/__init__.pyi b/thetadatadx-py/python/thetadatadx/__init__.pyi index d40a9f4d..d0582a75 100644 --- a/thetadatadx-py/python/thetadatadx/__init__.pyi +++ b/thetadatadx-py/python/thetadatadx/__init__.pyi @@ -264,7 +264,7 @@ class Config: metrics_port: Optional[int] """Prometheus exporter port. ``None`` (the default) leaves the exporter disabled even when the metrics feature is compiled in; an ``int`` binds an HTTP listener on ``0.0.0.0:``. The setter raises ``ValueError`` for values outside ``0..=65535``.""" streaming_timeout_ms: int - """No-frames deadline, in milliseconds, for the streaming connection (default 3_000).""" + """No-frames deadline, in milliseconds, for the streaming connection (default 10_000).""" streaming_connect_timeout_ms: int """Connect timeout, in milliseconds, for opening a streaming connection.""" streaming_ping_interval_ms: int diff --git a/thetadatadx-py/src/_generated/config_accessors.rs b/thetadatadx-py/src/_generated/config_accessors.rs index c86deaff..097285c2 100644 --- a/thetadatadx-py/src/_generated/config_accessors.rs +++ b/thetadatadx-py/src/_generated/config_accessors.rs @@ -104,14 +104,14 @@ impl Config { /// Set the streaming read timeout (ms): the no-frames deadline after /// which the streaming I/O loop declares the session dead and - /// reconnects. Default ``3_000``; validated to ``[100, 60_000]``. + /// reconnects. Default ``10_000``; validated to ``[100, 60_000]``. #[setter] fn set_streaming_timeout_ms(&self, ms: u64) { let mut guard = self.inner.lock().unwrap_or_else(|e| e.into_inner()); guard.streaming.timeout_ms = ms; } - /// Current ``streaming.timeout_ms`` value (default ``3_000``). + /// Current ``streaming.timeout_ms`` value (default ``10_000``). #[getter] fn get_streaming_timeout_ms(&self) -> u64 { let guard = self.inner.lock().unwrap_or_else(|e| e.into_inner()); diff --git a/thetadatadx-py/tests/test_config_resilience.py b/thetadatadx-py/tests/test_config_resilience.py index 92045946..be2d29dc 100644 --- a/thetadatadx-py/tests/test_config_resilience.py +++ b/thetadatadx-py/tests/test_config_resilience.py @@ -97,7 +97,7 @@ def test_reconnect_callback_registration_switches_policy(): def test_streaming_transport_defaults_and_round_trip(): mod = _import_module() cfg = mod.Config.production() - assert cfg.streaming_timeout_ms == 3_000 + assert cfg.streaming_timeout_ms == 10_000 assert cfg.streaming_connect_timeout_ms == 2_000 assert cfg.streaming_ping_interval_ms == 250 assert cfg.streaming_ring_size == 131_072 @@ -105,7 +105,7 @@ def test_streaming_transport_defaults_and_round_trip(): assert cfg.streaming_keepalive_idle_secs == 5 assert cfg.streaming_keepalive_interval_secs == 2 assert cfg.streaming_keepalive_retries == 2 - cfg.streaming_timeout_ms = 10_000 + cfg.streaming_timeout_ms = 15_000 cfg.streaming_connect_timeout_ms = 5_000 cfg.streaming_ping_interval_ms = 1_000 cfg.streaming_ring_size = 8_192 @@ -113,7 +113,7 @@ def test_streaming_transport_defaults_and_round_trip(): cfg.streaming_keepalive_idle_secs = 10 cfg.streaming_keepalive_interval_secs = 5 cfg.streaming_keepalive_retries = 4 - assert cfg.streaming_timeout_ms == 10_000 + assert cfg.streaming_timeout_ms == 15_000 assert cfg.streaming_connect_timeout_ms == 5_000 assert cfg.streaming_ping_interval_ms == 1_000 assert cfg.streaming_ring_size == 8_192 diff --git a/thetadatadx-rs/config.default.toml b/thetadatadx-rs/config.default.toml index b685aa43..efab8ed5 100644 --- a/thetadatadx-rs/config.default.toml +++ b/thetadatadx-rs/config.default.toml @@ -36,13 +36,16 @@ hosts = [ connect_timeout = 2000 # Read timeout — how long to wait for data before the link is considered -# dead (ms). The server heartbeats every ~100 ms on a quiet session, so this -# default is ~30 missed heartbeats: a dead link is declared quickly. -read_timeout = 3000 +# dead (ms). Matches the terminal's streaming socket read timeout and rides +# through the brief silence right after a full-market subscribe while the +# server sets the subscription up. The ~100 ms cadence is the client's ping +# to the server, not an inbound heartbeat; inbound frames arrive ~250 ms. +read_timeout = 10000 -# Client heartbeat interval (ms). The server's own ~100 ms heartbeat is the -# primary liveness signal; this client ping proves write-side health at a +# Client outbound ping interval (ms). This ping proves write-side health at a # 4 Hz cadence without adding inbound-frame pressure on a recovering upstream. +# Reverse-direction liveness is the inbound frame and ping stream (~250 ms), +# which read_timeout guards. ping_interval = 250 # Initial reconnect wait after an involuntary disconnect (ms). The auto- diff --git a/thetadatadx-rs/config_surface.toml b/thetadatadx-rs/config_surface.toml index 63416700..b3ebba26 100644 --- a/thetadatadx-rs/config_surface.toml +++ b/thetadatadx-rs/config_surface.toml @@ -324,20 +324,20 @@ param = "v" abi_type = "u64" path = "streaming.timeout_ms" doc = """ -Set the streaming read timeout (ms): the no-frames deadline after which the streaming I/O loop declares the session dead and reconnects. Default `3_000`; validated to `[100, 60_000]` at connect. +Set the streaming read timeout (ms): the no-frames deadline after which the streaming I/O loop declares the session dead and reconnects. Default `10_000`; validated to `[100, 60_000]` at connect. """ cpp_doc = """ Set the streaming read timeout (ms): the no-frames deadline after -which the streaming I/O loop reconnects. Default 3_000; +which the streaming I/O loop reconnects. Default 10_000; validated to [100, 60_000] at connect. """ py_doc = """ Set the streaming read timeout (ms): the no-frames deadline after which the streaming I/O loop declares the session dead and -reconnects. Default ``3_000``; validated to ``[100, 60_000]``. +reconnects. Default ``10_000``; validated to ``[100, 60_000]``. """ ts_doc = """ -Set the streaming read timeout (ms): the no-frames deadline after which the streaming I/O loop declares the session dead and reconnects. Default `3_000n`; validated to `[100, 60_000]` at connect. +Set the streaming read timeout (ms): the no-frames deadline after which the streaming I/O loop declares the session dead and reconnects. Default `10_000n`; validated to `[100, 60_000]` at connect. """ py_param = "ms" ts_param = "ms" @@ -349,19 +349,19 @@ param = "out" abi_type = "u64" path = "streaming.timeout_ms" doc = """ -Read the current streaming `timeout_ms` setting (default `3_000`). +Read the current streaming `timeout_ms` setting (default `10_000`). Writes the configured value into `*out`. Returns `0` on success, `-1` if either pointer is null. """ cpp_doc = """ -Current streaming timeout_ms (default 3_000). +Current streaming timeout_ms (default 10_000). """ py_doc = """ -Current ``streaming.timeout_ms`` value (default ``3_000``). +Current ``streaming.timeout_ms`` value (default ``10_000``). """ ts_doc = """ -Current `streaming.timeout_ms` value (default `3_000n`). +Current `streaming.timeout_ms` value (default `10_000n`). """ [[accessor]] diff --git a/thetadatadx-rs/src/config/fpss.rs b/thetadatadx-rs/src/config/fpss.rs index 324a100e..5dfcfeda 100644 --- a/thetadatadx-rs/src/config/fpss.rs +++ b/thetadatadx-rs/src/config/fpss.rs @@ -134,11 +134,16 @@ pub struct StreamingConfig { /// Drives the per-connection initial socket read timeout, the framing /// layer's mid-frame stall budget, and the I/O loop's overall /// "no frames received" deadline that triggers - /// [`crate::RemoveReason::TimedOut`]. Default `3_000` - /// — the server heartbeats every ~100 ms on a quiet session, so - /// three seconds of total silence is ~30 missed heartbeats and a - /// dead link is declared quickly instead of after the previous - /// 10 s default. Validated to the range `[100, 60_000]` ms. + /// [`crate::RemoveReason::TimedOut`]. Default `10_000`, matching the + /// terminal's streaming socket read timeout. Right after a + /// full-market subscribe the server can fall fully silent (no + /// frames, no pings) for a few seconds while it sets the + /// subscription up; a 10 s deadline rides through that gap where a + /// shorter one trips inside it and forces an unnecessary reconnect. + /// The ~100 ms cadence is the client's ping to the server, not an + /// inbound heartbeat; inbound frames and pings arrive roughly every + /// ~250 ms on an active session. Validated to the range + /// `[100, 60_000]` ms. pub timeout_ms: u64, /// Streaming event ring buffer size (slots). @@ -154,14 +159,14 @@ pub struct StreamingConfig { /// Streaming heartbeat ping interval in milliseconds. /// - /// The streaming server expects a heartbeat at this cadence and may - /// disconnect if it falls silent. Default `250` — the server's own - /// ~100 ms heartbeat is the primary liveness signal in the - /// reverse direction; the client ping mainly proves write-side - /// health, and a 4 Hz cadence does that without contributing to - /// inbound-frame pressure on a recovering upstream. Validated to - /// the range `[100, 300_000]` ms — sub-100 ms values are rejected - /// so a misconfiguration does not flood the upstream. + /// This is the client's outbound ping cadence to the server, and the + /// server may disconnect if it falls silent. Default `250` — the ping + /// mainly proves write-side health at a 4 Hz cadence without adding + /// inbound-frame pressure on a recovering upstream. Reverse-direction + /// liveness is the inbound frame and ping stream (~250 ms on an active + /// session), which [`Self::timeout_ms`] guards. Validated to the range + /// `[100, 300_000]` ms — sub-100 ms values are rejected so a + /// misconfiguration does not flood the upstream. pub ping_interval_ms: u64, /// Per-server TCP connect timeout in milliseconds. Default `2000`. @@ -255,7 +260,7 @@ impl StreamingConfig { ], host_selection: HostSelectionPolicy::Shuffled, host_shuffle_seed: None, - timeout_ms: 3_000, + timeout_ms: 10_000, ring_size: 131_072, ping_interval_ms: 250, connect_timeout_ms: 2_000, @@ -301,7 +306,7 @@ mod tests { #[test] fn production_defaults_resilience_shape() { let cfg = StreamingConfig::production_defaults(); - assert_eq!(cfg.timeout_ms, 3_000); + assert_eq!(cfg.timeout_ms, 10_000); assert_eq!(cfg.ping_interval_ms, 250); assert_eq!(cfg.io_read_slice_ms, 25); assert_eq!(cfg.keepalive_idle_secs, 5); diff --git a/thetadatadx-rs/src/config/mod.rs b/thetadatadx-rs/src/config/mod.rs index 3e7a71a0..c5b39418 100644 --- a/thetadatadx-rs/src/config/mod.rs +++ b/thetadatadx-rs/src/config/mod.rs @@ -2013,7 +2013,7 @@ mod tests { let config = DirectConfig::production_defaults(); let validated = config.validate().expect("production defaults validate"); assert_eq!(validated.historical.window_size_kb, 64); - assert_eq!(validated.streaming.timeout_ms, 3_000); + assert_eq!(validated.streaming.timeout_ms, 10_000); assert_eq!(validated.streaming.ping_interval_ms, 250); assert_eq!(validated.streaming.connect_timeout_ms, 2_000); assert_eq!(validated.streaming.io_read_slice_ms, 25); diff --git a/thetadatadx-rs/src/fpss/connection.rs b/thetadatadx-rs/src/fpss/connection.rs index cbe25466..34f294f0 100644 --- a/thetadatadx-rs/src/fpss/connection.rs +++ b/thetadatadx-rs/src/fpss/connection.rs @@ -9,7 +9,7 @@ //! detection) so a peer that vanishes without a FIN/RST is detected //! by the transport long before the platform default of 2+ hours //! - Connect timeout: 2 seconds (configurable) -//! - Read timeout: configurable (default 3 seconds) +//! - Read timeout: configurable (default 10 seconds) //! - Host order: fault-domain-aware per-client shuffle by default (see //! [`order_hosts`]), `FixedOrder` escape hatch preserves declaration //! order diff --git a/thetadatadx-ts/__tests__/config_resilience.test.mjs b/thetadatadx-ts/__tests__/config_resilience.test.mjs index 4f5d008c..026d1fd3 100644 --- a/thetadatadx-ts/__tests__/config_resilience.test.mjs +++ b/thetadatadx-ts/__tests__/config_resilience.test.mjs @@ -67,7 +67,7 @@ test("reconnect callback registration switches policy", () => { test("streaming transport defaults and round-trip", () => { const cfg = Config.production(); - assert.equal(cfg.streamingTimeoutMs, 3_000n); + assert.equal(cfg.streamingTimeoutMs, 10_000n); assert.equal(cfg.streamingConnectTimeoutMs, 2_000n); assert.equal(cfg.streamingPingIntervalMs, 250n); assert.equal(cfg.streamingRingSize, 131_072n); @@ -75,9 +75,9 @@ test("streaming transport defaults and round-trip", () => { assert.equal(cfg.streamingKeepaliveIdleSecs, 5n); assert.equal(cfg.streamingKeepaliveIntervalSecs, 2n); assert.equal(cfg.streamingKeepaliveRetries, 2); - cfg.setStreamingTimeoutMs(10_000n); + cfg.setStreamingTimeoutMs(15_000n); cfg.setStreamingKeepaliveIdleSecs(10n); - assert.equal(cfg.streamingTimeoutMs, 10_000n); + assert.equal(cfg.streamingTimeoutMs, 15_000n); assert.equal(cfg.streamingKeepaliveIdleSecs, 10n); }); diff --git a/thetadatadx-ts/index.d.ts b/thetadatadx-ts/index.d.ts index d6199465..e8c48e2e 100644 --- a/thetadatadx-ts/index.d.ts +++ b/thetadatadx-ts/index.d.ts @@ -294,9 +294,9 @@ export declare class Config { setReconnectReplayPaceMs(ms: bigint): void /** Current `replay_pace_ms` value (default `5n`). */ get reconnectReplayPaceMs(): bigint - /** Set the streaming read timeout (ms): the no-frames deadline after which the streaming I/O loop declares the session dead and reconnects. Default `3_000n`; validated to `[100, 60_000]` at connect. */ + /** Set the streaming read timeout (ms): the no-frames deadline after which the streaming I/O loop declares the session dead and reconnects. Default `10_000n`; validated to `[100, 60_000]` at connect. */ setStreamingTimeoutMs(ms: bigint): void - /** Current `streaming.timeout_ms` value (default `3_000n`). */ + /** Current `streaming.timeout_ms` value (default `10_000n`). */ get streamingTimeoutMs(): bigint /** Set the per-server connect timeout (ms) for the streaming connection. Default `2_000n`; validated to `[1_000, 60_000]` at connect. */ setStreamingConnectTimeoutMs(ms: bigint): void diff --git a/thetadatadx-ts/src/_generated/config_accessors.rs b/thetadatadx-ts/src/_generated/config_accessors.rs index 88c853ae..b68d4807 100644 --- a/thetadatadx-ts/src/_generated/config_accessors.rs +++ b/thetadatadx-ts/src/_generated/config_accessors.rs @@ -152,7 +152,7 @@ impl Config { Ok(napi::bindgen_prelude::BigInt::from(guard.reconnect.replay_pace_ms)) } - /// Set the streaming read timeout (ms): the no-frames deadline after which the streaming I/O loop declares the session dead and reconnects. Default `3_000n`; validated to `[100, 60_000]` at connect. + /// Set the streaming read timeout (ms): the no-frames deadline after which the streaming I/O loop declares the session dead and reconnects. Default `10_000n`; validated to `[100, 60_000]` at connect. #[napi(js_name = "setStreamingTimeoutMs")] pub fn set_streaming_timeout_ms(&self, ms: napi::bindgen_prelude::BigInt) -> napi::Result<()> { let value = bigint_to_u64("setStreamingTimeoutMs", &ms)?; @@ -164,7 +164,7 @@ impl Config { Ok(()) } - /// Current `streaming.timeout_ms` value (default `3_000n`). + /// Current `streaming.timeout_ms` value (default `10_000n`). #[napi(getter, js_name = "streamingTimeoutMs")] pub fn streaming_timeout_ms(&self) -> napi::Result { let guard = self