Skip to content

feat(mdds)!: shard streaming pulls; size shards from request shape#1215

Open
userFRM wants to merge 2 commits into
mainfrom
feat/generic-sharding
Open

feat(mdds)!: shard streaming pulls; size shards from request shape#1215
userFRM wants to merge 2 commits into
mainfrom
feat/generic-sharding

Conversation

@userFRM

@userFRM userFRM commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

Bulk-fetch sharding, made generic, resilient, and reachable from the streaming path. Two commits:

  1. The dedicated streaming builders (*_stream()) now shard concurrently like the buffered path — they previously always ran a single stream regardless of bulk_fetch or tier concurrency.
  2. Shard sizing no longer runs a density probe. plan_query decides shardability from the request shape and cuts the requested time/date range into equal concurrent bands. The OHLC sizing probe and its per-endpoint rows_per_event multiplier are deleted.
  3. A shard band failure no longer loses the whole pull.
  4. The two chunk-delivery implementations are unified onto one.

Sizing: the probe is gone

The old sizer fired a separate OHLC query to estimate a pull by counting trades and multiplying by a hardcoded per-endpoint constant (roughly 15 rows per trade for quote endpoints). Real NBBO density is two orders of magnitude higher, so large quote chains fell under the shard threshold and ran single-stream. Equal-time banding needs no estimate and no per-endpoint tuning; the concurrency win dominates the residual imbalance from equal-time versus equal-work cuts.

Resilience: a band failure bounds its blast radius

Previously one band's transport error cancelled the siblings and lost the entire pull.

  • Buffered (.await): a band's rows are held internally until the merge, so a failed band is re-fetched from scratch within the existing retry budget. Transient transport errors recover transparently, no duplicates.
  • Streaming (.stream): sibling bands drain to completion instead of being cancelled, and the returned Error::PartialShardFetch { failed } names the exact failed band windows, so a caller re-pulls only those slices instead of the whole result. A band that fails before delivering any chunk still retries transparently.

Delivery unification

The *_stream() builders used a codegen template that marked an empty keepalive chunk as "delivered," diverging from the macro path's deliver_chunk_slices on both retry budget and empty-query status. The template is deleted; both the sharded per-band arm and the single-stream arm now call deliver_chunk_slices, so the two public surfaces of an endpoint behave identically.

Generic and machine-enforced

No per-endpoint tuning remains. The shardable-endpoint set is now checked against the endpoint registry (intraday history = a history subcategory carrying start_time/end_time), so a newly added history endpoint cannot silently never-shard.

Breaking / API changes

  • bulk_fetch_plan is now fn(&self, endpoint, query) -> Option<ShardPlan> (was async fn -> Result<Option<ShardPlan>, Error>). It is pure computation on the request shape, so there is nothing to await and no error to surface. Rust-only; exposed in no binding.
  • New Error::PartialShardFetch variant (additive), mapped to StreamError / THETADATADX_ERR_STREAM in every binding beside the existing PartialReconnect.

Behavior notes (intentional)

  • A tick-interval pull now fans out on the window alone; a sparse one produces fast empty bands that fold to a single NotFound, so wall-clock is neutral.
  • A window under the 5-minute minimum band width stays on the single stream.
  • Merged output keeps the same canonical order; bulk_fetch = "off" bypasses sharding entirely.

Deadline

The 300s request timeout is a default. Large pulls should raise it via with_deadline / timeout_ms (Duration::ZERO disables it). Unchanged here.

Verification

  • cargo clippy --workspace --all-targets --locked -- -D warnings, cargo test --workspace + doctests, cargo fmt --check — clean
  • SDK-surface / docs-site / gRPC-snapshot regeneration — no drift; codegen deterministic
  • binding-parity, docs-consistency, lockfile-drift, and vocabulary gates — pass
  • New integration test (tests/sharded_fanout.rs, 7 cases): fan-out issues N band requests, merge/interleave order, NotFound folding, buffered re-fetch recovery, streaming partial-error naming the failed window
  • Two independent adversarial audits (band partition math; resilience/concurrency semantics) — 0 critical, 0 high

Note: check_c_abi_completeness flags 12 FFI config accessors (bulk_fetch / shard_concurrency / wait_mode / window sizes) declared in the header but unimplemented in thetadatadx-ffi/src. This is pre-existing on main (shipped in 0.2.0) and unrelated to this change.

userFRM added 2 commits July 19, 2026 12:20
The dedicated streaming builders (*_stream) now route through the bulk-fetch sharder like the buffered path; previously they always ran a single stream regardless of bulk_fetch policy or tier concurrency.

Shard sizing no longer runs a density probe. plan_query decides shardability from the request shape and cuts the requested time or date range into equal concurrent bands, removing the OHLC sizing probe and its per-endpoint rows_per_event multiplier; that multiplier mis-sized quote chains badly enough that large quote pulls never sharded.

BREAKING CHANGE: MarketDataClient::bulk_fetch_plan is now fn -> Option<ShardPlan> (was async fn -> Result<Option<ShardPlan>, Error>).
…eaming chunk delivery

A terminal band failure on a chunk-streaming sharded pull no longer cancels the sibling bands: they drain to completion, and the call returns the new Error::PartialShardFetch naming the failed band window(s) so the caller can re-pull exactly the missing slices instead of restarting the whole pull. A pull where no chunk reached the handler still fails wholesale with the underlying error, a band that fails before delivering anything still retries transparently, and the call deadline still cancels every band at once. The buffered path's per-band from-scratch re-fetch (attempt-local collection, dedup-free replay) is documented and pinned by tests. The new variant maps to StreamError / THETADATADX_ERR_STREAM across the Python, TypeScript, and C surfaces.

The four dedicated *_stream builders now drain through MarketDataClient::deliver_chunk_slices, the same primitive the generated .stream(handler) methods use, so both public surfaces of one endpoint agree on the no-replay delivered gating (marked only when rows reach the handler, keeping a recoverable transient after empty keepalives retryable) and stop at the first decode failure; the divergent for_each_chunk_body template is deleted.

The planner now logs at debug why a query did not shard (unlisted endpoint, no cut axis, width under two, malformed window, provably small grid, narrow window), and each band future runs inside a shard_band tracing span carrying its window plus a per-band completion line with rows and duration, so concurrent bands stay distinguishable in the log stream.

The shardable-endpoint set is now a const roster asserted equal to the registry's intraday-history endpoints (history* subcategory carrying start_time/end_time filters), so a new intraday history endpoint cannot silently never-shard. A new integration suite drives sharded pulls end-to-end through a per-band scripted mock, buffered and streaming: fan-out shape, band-order merge, NotFound folding, buffered band re-fetch, pre-delivery streaming retry, and the streaming partial-fetch error.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant