Skip to content

Commit e8aa1ba

Browse files
userFRMclaude
andauthored
feat(streaming)!: remove flush_mode; always coalesce outbound writes on the ping heartbeat (#1195)
* feat(streaming)!: remove flush_mode; always coalesce outbound writes on the ping heartbeat flush_mode had no counterpart in the Theta Terminal: the terminal exposes no flush knob and lets the socket coalesce outbound writes, and the "immediate" mode existed only to defeat that coalescing — a client-facing way to flood the upstream with one syscall per frame. It was a server-protection mechanism turned into a footgun on the public configuration surface, so it does not belong on a terminal-parity SDK. The knob is removed from every binding (Rust StreamingConfig::flush_mode, Python Config.flush_mode, TypeScript Config.flushMode / setFlushMode, C++ set_flush_mode / get_flush_mode, C ABI thetadatadx_config_set_flush_mode / _get_flush_mode), along with the StreamingFlushMode enum, the config-file field, the cross-binding parity rows, and the docs. The I/O loop now hardwires the batched behavior: only the ping heartbeat flushes, so queued subscribe / unsubscribe frames coalesce until the next ping and a subscription burst leaves as fewer, larger packets. Received-data latency is unaffected, exactly as before. Breaking change to the configuration surface; batching is now unconditional. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test,docs: drop stale flushMode TS test + sync docs changelog Remove the leftover thetadatadx-ts __tests__/flush_mode.test.mjs that still asserted the removed cfg.flushMode / setFlushMode surface (npm test globs every __tests__/*.test.mjs), and mirror the CHANGELOG Removed entry into the docs-site changelog so the byte-identical docs-consistency gate passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(parity): drop flush_mode from the setter-property selftest fixture The .pyi setter-property degrade selftest read the real parity.toml + stub and used flush_mode as its canonical exempt-setter example; with flush_mode gone, remove setFlushMode from PYI_SETTER_PROPERTY_ROWS and the twin table and drop the flush_mode-specific getter assertion so --selftest reflects the real surface. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: regenerate llms.txt after flush_mode removal generate_docs_site emits docs-site/docs/public/llms.txt from the config surface; refresh it so the generated-docs drift check passes with flush_mode gone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: preview <noreply@anthropic.com>
1 parent 5606880 commit e8aa1ba

34 files changed

Lines changed: 47 additions & 659 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Removed
11+
12+
- **`flush_mode` streaming write-flush knob.** The `flush_mode` setting is removed from every binding (Rust `StreamingConfig::flush_mode`, Python `Config.flush_mode`, TypeScript `Config.flushMode` / `setFlushMode`, C++ `set_flush_mode` / `get_flush_mode`, C ABI `thetadatadx_config_set_flush_mode` / `_get_flush_mode`). Outbound streaming writes now always coalesce and flush on the ping heartbeat, so a subscription burst leaves as fewer, larger packets — the terminal's own behavior — with received-data latency unaffected as before. The `"immediate"` per-frame-flush mode existed only to defeat that server-friendly coalescing and is gone. This is a breaking change to the configuration surface.
13+
1014
## [0.1.1] - 2026-07-07
1115

1216
### Added

docs-site/docs/articles/configuration.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ from thetadatadx import Config
3737

3838
cfg = Config.production()
3939
cfg.retry_max_attempts = 5
40-
cfg.flush_mode = "immediate"
4140
client = Client(creds, cfg)
4241
```
4342

@@ -86,7 +85,7 @@ You can also select environments inline at the client, without building a `Confi
8685

8786
If you also set an explicit streaming or market-data host (through `THETADATA_MARKET_DATA_HOST` / `THETADATA_STREAMING_HOST`, in the environment, in the `.env` file, or in the config file), that explicit host wins over the environment's default for that channel.
8887

89-
In Rust the same fields live on `DirectConfig` struct sub-configs (`config.retry.max_attempts`, `config.streaming.flush_mode`); TypeScript uses `Config` setters (`cfg.setRetryMaxAttempts(5)`); C++ uses `thetadatadx::Config::set_retry_max_attempts(5)`.
88+
In Rust the same fields live on `DirectConfig` struct sub-configs (`config.retry.max_attempts`, `config.streaming.ring_size`); TypeScript uses `Config` setters (`cfg.setRetryMaxAttempts(5)`); C++ uses `thetadatadx::Config::set_retry_max_attempts(5)`.
9089

9190
## The knobs that matter
9291

@@ -95,7 +94,7 @@ In Rust the same fields live on `DirectConfig` struct sub-configs (`config.retry
9594
| Request deadlines | `timeout_ms` per request (builder / kwarg) | Hard per-call deadline; expiry raises a timeout error and frees the slot. |
9695
| Retries | `retry_initial_delay_ms`, `retry_max_delay_ms`, `retry_max_attempts`, `retry_jitter`, `retry_max_elapsed_secs` | Backoff schedule for transient market-data-request faults. |
9796
| Streaming reconnect | `reconnect_policy`, `reconnect_max_attempts`, `reconnect_wait_ms`, `reconnect_wait_max_ms`, `reconnect_jitter`, `reconnect_stable_window_secs`, … | Automatic streaming reconnection. See [Reconnection & Monitoring](/streaming/reliability). |
98-
| Streaming latency | `flush_mode` (`"batched"` default / `"immediate"`), `streaming_ring_size`, `streaming_timeout_ms`, keepalive fields | Write-path flush behavior and event-buffer capacity. |
97+
| Streaming latency | `streaming_ring_size`, `streaming_timeout_ms`, keepalive fields | Event-buffer capacity and I/O timeouts. |
9998
| Flat files | `flatfiles_max_attempts`, `flatfiles_initial_backoff_secs`, `flatfiles_max_backoff_secs`, `flatfiles_jitter` | Retry budget for bulk downloads. |
10099
| Observability | `metrics_port` | Optional local Prometheus exporter port (off by default). |
101100
| Runtime | `worker_threads` | Async worker-thread count for embedded bindings (0 = auto). |
@@ -114,7 +113,6 @@ host = "mdds-01.thetadata.us"
114113
port = 443
115114

116115
[streaming]
117-
flush_mode = "immediate"
118116
hosts = ["host-a.example.com:20000", "host-b.example.com:20000"]
119117
```
120118

docs-site/docs/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Removed
11+
12+
- **`flush_mode` streaming write-flush knob.** The `flush_mode` setting is removed from every binding (Rust `StreamingConfig::flush_mode`, Python `Config.flush_mode`, TypeScript `Config.flushMode` / `setFlushMode`, C++ `set_flush_mode` / `get_flush_mode`, C ABI `thetadatadx_config_set_flush_mode` / `_get_flush_mode`). Outbound streaming writes now always coalesce and flush on the ping heartbeat, so a subscription burst leaves as fewer, larger packets — the terminal's own behavior — with received-data latency unaffected as before. The `"immediate"` per-frame-flush mode existed only to defeat that server-friendly coalescing and is gone. This is a breaking change to the configuration surface.
13+
1014
## [0.1.1] - 2026-07-07
1115

1216
### Added

docs-site/docs/public/llms.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
/streaming/options/open-interest — Option Open Interest: Open-interest updates for an option contract.
9595
/streaming/options/quote — Option Quotes: Real-time NBBO quote stream for an option contract.
9696
/streaming/options/trade — Option Trades: Real-time trade stream for an option contract.
97-
/streaming/reliability — Reconnection & Monitoring: Automatic reconnect policy, flush mode, and the counters that tell you a stream is healthy.
97+
/streaming/reliability — Reconnection & Monitoring: Automatic reconnect policy and the counters that tell you a stream is healthy.
9898
/streaming/stocks/full-trade — Stock Full Trades: Every trade across all stocks in one subscription.
9999
/streaming/stocks/market-value — Stock Market Value: Real-time calculated market value for a stock.
100100
/streaming/stocks/quote — Stock Quotes: Real-time BBO quote stream for a stock.

docs-site/docs/streaming/reliability.md

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Reconnection & Monitoring
3-
description: Automatic reconnect policy, flush mode, and the counters that tell you a stream is healthy.
3+
description: Automatic reconnect policy and the counters that tell you a stream is healthy.
44
---
55

66
# Reconnection & Monitoring
@@ -46,20 +46,6 @@ Permanent failures (for example rejected credentials) never reach the callback
4646

4747
Caller-driven recovery is always available: `reconnect()` re-opens the session and restores the saved subscription set on demand.
4848

49-
## Flush mode
50-
51-
`flush_mode` trades write-path latency against syscall volume:
52-
53-
| Mode | Behavior |
54-
|---|---|
55-
| `"batched"` (default) | Outbound frames flush on the heartbeat cadence — the throughput-friendly default. |
56-
| `"immediate"` | Every frame flushes as written — lowest latency. |
57-
58-
```python
59-
cfg = Config.production()
60-
cfg.flush_mode = "immediate"
61-
```
62-
6349
## Monitoring a live stream
6450

6551
Incoming events are buffered between the connection and your callback. If your callback can't keep up, the buffer fills and the newest events are **counted and dropped** rather than stalling the feed — so these counters are the health dashboard:

parity.toml

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -671,17 +671,6 @@ python = true
671671
typescript = true
672672
cpp = true
673673

674-
# `StreamingConfig.flush_mode` is exposed on every binding:
675-
# C ABI (`thetadatadx_config_set_flush_mode`), C++ (`set_flush_mode`),
676-
# Python (`Config.flush_mode` getter + setter accepting
677-
# `"batched"` / `"immediate"`), TypeScript (`Config.flushMode`
678-
# getter + `setFlushMode` accepting the same strings).
679-
[[class]]
680-
name = "StreamingConfig.flush_mode"
681-
python = true
682-
typescript = true
683-
cpp = true
684-
685674
# `StreamingConfig.consumer_cpu` optional consumer-thread CPU pin,
686675
# exposed on every binding (Python `Config.consumer_cpu` `Optional[int]`,
687676
# TypeScript `consumerCpu` / `setConsumerCpu` `number | null`, C++
@@ -1903,34 +1892,6 @@ cpp = true
19031892
params = ["String"]
19041893
returns = "Config"
19051894

1906-
# The flush-mode knob diverges on C++: the managed bindings pass / return the
1907-
# mode as a String name, while C++ uses the integer enum code (`int`), so those
1908-
# cells are per-lang overrides.
1909-
[[method]]
1910-
class = "Config"
1911-
name = "setFlushMode"
1912-
python = true
1913-
typescript = true
1914-
cpp = true
1915-
[method.signature]
1916-
params = ["String"]
1917-
returns = "()"
1918-
cpp_params = ["i32"]
1919-
1920-
[[method]]
1921-
class = "Config"
1922-
name = "flushMode"
1923-
python = true
1924-
typescript = true
1925-
cpp = true
1926-
[method.signature]
1927-
returns = "String"
1928-
cpp_returns = "i32"
1929-
# The Python stub constrains the knob to its exact value set; pin the Literal
1930-
# so adding / removing / renaming a member fails (the bare `String` would hide
1931-
# the value-set drift). The pyo3-source `python` lane still reads `&str`.
1932-
python_pyi_returns = 'Literal["batched", "immediate"]'
1933-
19341895
# The consumer-CPU pin is an optional core index. The managed bindings carry
19351896
# it as an optional integer (`Option<usize>` / `Option<f64>` / `Option<u32>`);
19361897
# C++ sentinel-encodes the option as a bare `int64_t` (-1 = unpinned), so the

scripts/ci/check_binding_parity.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2444,7 +2444,6 @@ def _collect_cpp_class_methods(cpp_hpp: pathlib.Path) -> dict[str, set[str]]:
24442444
PYI_SETTER_PROPERTY_ROWS: frozenset[tuple[str, str]] = frozenset(
24452445
("Config", name)
24462446
for name in (
2447-
"setFlushMode",
24482447
"setConsumerCpu",
24492448
"setReconnectPolicy",
24502449
"setStreamingRingSize",
@@ -12820,7 +12819,7 @@ def _case_sig_python_pyi_setter_property_degrade() -> None:
1282012819
`.pyi` lane does NOT fail on the (correctly) absent `set_x`, while the
1282112820
matching GETTER row IS `.pyi`-checked. Uses the live stub so the real
1282212821
property annotation is exercised."""
12823-
# The 9 enrolled setters are the only pinned-python rows absent from the
12822+
# The enrolled setters are the only pinned-python rows absent from the
1282412823
# real stub — assert that membership matches reality (a NEW absent
1282512824
# pinned setter must be enrolled or it fails).
1282612825
data = tomllib.loads(PARITY_TOML.read_text(encoding="utf-8"))
@@ -12833,18 +12832,13 @@ def _case_sig_python_pyi_setter_property_degrade() -> None:
1283312832
# The setter rows still get their pyo3-source `python` / ts / cpp / ffi
1283412833
# checks; only the `.pyi` lane is exempt. So no `python_pyi` error.
1283512834
assert not any("python_pyi" in e for e in setter_errs), setter_errs
12836-
# The matching getter (`flushMode` → property `flush_mode`) IS checked.
12837-
assert _sig_extract_python_pyi(PY_PYI, "Config", "flush_mode") == (
12838-
[], 'Literal["batched", "immediate"]'
12839-
), _sig_extract_python_pyi(PY_PYI, "Config", "flush_mode")
1284012835
# FINDING-8 closure: the exemption is sound ONLY if EVERY exempt setter
1284112836
# has a getter twin whose `.pyi` property type the `python_pyi` lane
1284212837
# checks. Resolve each setter's twin from the real spec and assert (a) a
1284312838
# checked getter `[[method]]` row exists for it and (b) the stub declares
1284412839
# the property — so no exempt setter rides on an unchecked property.
1284512840
# `setStreamingRingSize` → `streamingRingSize` was the gap this closes.
1284612841
setter_to_getter = {
12847-
"setFlushMode": ("flushMode", "flush_mode"),
1284812842
"setConsumerCpu": ("consumerCpu", "consumer_cpu"),
1284912843
"setReconnectPolicy": ("reconnectPolicy", "reconnect_policy"),
1285012844
"setStreamingRingSize": ("streamingRingSize", "streaming_ring_size"),

thetadatadx-cpp/include/config_accessors.hpp.inc

Lines changed: 0 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

thetadatadx-cpp/include/thetadatadx.h

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1925,26 +1925,6 @@ int32_t thetadatadx_config_set_metrics_port(ThetaDataDxConfig* config, bool has_
19251925
*/
19261926
int32_t thetadatadx_config_get_metrics_port(const ThetaDataDxConfig* config, bool* out_has_value, uint16_t* out_port);
19271927

1928-
/**
1929-
* Set streaming flush mode on a config handle.
1930-
* mode=0: Batched (default) -- flush only on PING every 100ms.
1931-
* mode=1: Immediate -- flush after every frame write.
1932-
* @param config Config handle to mutate.
1933-
* @param mode Flush mode selector (0 = Batched, 1 = Immediate).
1934-
* @return 0 on success. -1 with thetadatadx_last_error set and thetadatadx_last_error_code =
1935-
* THETADATADX_ERR_CONFIG when mode is outside {0, 1} or config is null.
1936-
*/
1937-
int thetadatadx_config_set_flush_mode(ThetaDataDxConfig* config, int mode);
1938-
1939-
/**
1940-
* Read the current streaming flush mode. Same encoding as
1941-
* thetadatadx_config_set_flush_mode.
1942-
* @param config Config handle to read.
1943-
* @param out_mode Receives 0 (Batched) or 1 (Immediate) on success.
1944-
* @return 0 on success, -1 if either pointer is null.
1945-
*/
1946-
int32_t thetadatadx_config_get_flush_mode(const ThetaDataDxConfig* config, int32_t* out_mode);
1947-
19481928
/**
19491929
* Read the market-data environment carried by the config: "PROD"
19501930
* for the production cluster or "STAGE" for staging. The market-data and

thetadatadx-cpp/tests/error_taxonomy.cpp

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,6 @@ TEST_CASE("config enum setters reject an out-of-domain value with InvalidParamet
208208
// throw.
209209
auto cfg = thetadatadx::Config::production();
210210

211-
REQUIRE_NOTHROW(cfg.set_flush_mode(1));
212-
REQUIRE_THROWS_AS(cfg.set_flush_mode(9), thetadatadx::InvalidParameterError);
213-
REQUIRE_THROWS_AS(cfg.set_flush_mode(9), thetadatadx::ThetaDataError);
214-
215211
REQUIRE_NOTHROW(cfg.set_reconnect_jitter(2));
216212
REQUIRE_THROWS_AS(cfg.set_reconnect_jitter(9), thetadatadx::InvalidParameterError);
217213

0 commit comments

Comments
 (0)