Skip to content

feat(grpc): in-house gRPC transport replacing tonic#524

Merged
userFRM merged 1 commit into
mainfrom
feat/inhouse-grpc-transport
May 15, 2026
Merged

feat(grpc): in-house gRPC transport replacing tonic#524
userFRM merged 1 commit into
mainfrom
feat/inhouse-grpc-transport

Conversation

@userFRM
Copy link
Copy Markdown
Owner

@userFRM userFRM commented May 13, 2026

Closes #522.

Summary

In-house gRPC client stack built directly on the h2 crate. The MddsClient and every generated MDDS endpoint now route through this transport; tonic, tonic-prost, and tonic-prost-build are removed from the workspace.

Transport machinery

  • Codec<Req, Resp> — phantom-typed wrapper performing prost encode/decode plus the 5-byte gRPC frame header ([1 byte compressed flag][4 bytes BE length][payload]). Rejects any non-zero compressed flag and any oversized frame.
  • Status — HTTP/2 trailers parser (grpc-status + grpc-message). Missing or malformed trailers surface as typed errors rather than panics.
  • Channel — owns one HTTP/2 connection. connect_tls runs the TLS handshake through tokio-rustls; connect_h2c opens a plaintext connection for sidecars and mock harnesses.
  • ServerStreamingfutures_core::Stream adapter over the h2 response body. Reads DATA frames into a BytesMut accumulator, decodes each complete frame through Codec, and on body close awaits trailers to surface either stream-end (OK status) or Err(ChannelError::Rpc) (non-OK status).

Off-reactor decoder pool

  +-------------------+   LMAX ring   +-------------------+
  | h2 receive task   |-------------->| Decoder thread    |
  | (tokio worker)    |  work+reply   | (std::thread,     |
  +-------------------+               |  TLS zstd ctx +   |
                                      |  scratch buffer)  |
                                      +---------+---------+
                                                |
                                                | DataTable
                                                v
                                      +-------------------+
                                      | oneshot::Sender   |
                                      | -> async caller   |
                                      +-------------------+
  • DecoderPool runs every response chunk's zstd decompress + DataTable::decode on dedicated std::threads. Each decoder owns one LMAX Disruptor ring + the existing thread-local zstd state in mdds::decode::transport. Producers are MultiProducer clones so concurrent submissions from N tokio tasks stay lock-free in the steady state.
  • DecoderWaitStrategy (16 spins + 4 yields + spin-loop hint) tuned for MDDS burst cadence — short enough that submit-to-dequeue handoff is single-digit microseconds, cooperative enough that idle decoders do not burn whole cores between bursts.
  • MddsClient::connect wires the pool through ChannelPool::from_channels_with_decoders.
  • Sizing knobs on MddsConfig: decoder_threads (0 = auto-size to min(channels, available_parallelism / 2)) and decoder_ring_size (power of two, >= 64, validated via util::ring::check_ring_size).
  • util::ring lifted out of fpss::ring so the FPSS read loop and the gRPC decoder pool share one validator.

Hardening

  • Per-call deadlines via Channel::server_streaming_with_deadline cover both the open phase (h2 send_request + response head) and the streaming phase (DATA frames + trailers). On elapse the h2 stream is dropped (sending RST_STREAM) and the call surfaces ChannelError::DeadlineExceeded.
  • GOAWAY classification: h2::Error::is_go_away / is_remote / is_io surface as ChannelError::ConnectionClosed so consumers can recycle the channel rather than retry on a dead connection.
  • Cancellation: dropping a ServerStreaming cancels the underlying h2 stream cleanly. Cancelled decode work is elided — the decoder thread checks oneshot::Sender::is_closed before running the work, so cancelled RPCs do not burn CPU on results no one will read.
  • grpc-encoding: identity is the only accepted body encoding; zstd-compressed compressed_data payloads inside ResponseData are decompressed by the decoder pool's dedicated threads.

Pooling

  • ChannelPool round-robins over N independent Channels so workloads that exceed the per-connection MAX_CONCURRENT_STREAMS limit fan out across multiple h2 connections. The cursor is an AtomicUsize advanced with Relaxed ordering.
  • MddsClient::connect builds a 4-channel pool by default (clamped to the subscription-tier concurrent ceiling). Every endpoint call picks the next channel via MddsClient::channel() — transparent to downstream callers.
  • Channels and decoder threads are independent dimensions: 4 channels × 6 decoder threads is the typical production shape on a 12-logical-core host.

Custom codegen replacing tonic-build

  • build_support/grpc/ reads proto/mdds.proto, invokes prost-build for the message types, and installs a custom ServiceGenerator that emits one async function per RPC method into $OUT_DIR/beta_endpoints.rs. Each emitted function:

    pub async fn <method_snake_case>(
        channel: &Channel,
        req: <RequestType>,
    ) -> Result<ServerStreaming<<ResponseType>>, ChannelError>
    

    with method path /BetaEndpoints.BetaThetaTerminal/<MethodCamelCase>. The generator rejects unary or client-streaming RPCs at build time so a future proto change cannot silently mis-frame the wire.

  • build_support/grpc::check reruns the codegen and confirms the output is byte-identical to the file already on disk. Gated by the THETADATADX_GRPC_CHECK env var so CI can re-run the build with the var set and surface drift as a build failure.

Endpoint migration

  • MddsClient::stub() is gone; every generated list_endpoint! / parsed_endpoint! / streaming macro invocation routes through crate::proto::beta_theta_terminal::<method>(client.channel(), request).
  • mdds::stream::{collect_stream, for_each_chunk} accept grpc::ServerStreaming<ResponseData> instead of tonic::Streaming<ResponseData>. Each chunk's decode now routes through the stream's attached DecoderHandle.
  • error.rs swaps Error::Transport(tonic::transport::Error) for Error::Transport(String) and replaces the From<tonic::Status> impl with From<crate::grpc::Status> + From<crate::grpc::ChannelError>. GrpcStatusKind::from_code(tonic::Code) becomes GrpcStatusKind::from_u32(u32) reading the wire code directly.

Tonic removal

  • tonic, tonic-prost, and tonic-prost-build are removed from crates/thetadatadx/Cargo.toml. The inhouse-grpc Cargo feature flag is dropped (the in-house transport is now the only path).
  • cargo tree -i tonic returns "no such package"; cargo tree --workspace | grep tonic is empty.

Test plan

  • Unit tests for codec: roundtrip, partial header, partial payload, compressed-flag rejection, invalid-flag rejection, oversized-frame rejection, two-frame demultiplex, wire-format parity vs the gRPC spec, plus a proptest over arbitrary Vec<Vec<u8>> payloads.
  • Unit tests for status: OK, error-with-message, missing trailer, non-numeric status, non-UTF8 status, non-UTF8 message, Display formatting.
  • Unit tests for pool: even distribution, within-one partial cycle, thread-safety under contention, panic on empty.
  • Unit tests for decoder pool: empty-pool rejection, invalid ring size rejection, single-response decode, concurrent decode (16 in-flight per decoder), cancelled-request elision, pool clone sharing decoders.
  • Integration tests against the mock h2 server: single chunk, multi-chunk in order, non-OK status surfacing as ChannelError::Rpc, connect to closed port, DeadlineExceeded mid-stream, GOAWAY classified as ConnectionClosed, ChannelPool round-robin distribution.
  • End-to-end test for stock_list_symbols through the in-house Channel (single chunk and two-chunk merge).
  • Error-mapping tests: From<grpc::Status> carries kind + message, unauthenticated kind, deadline routes to Error::Timeout, rpc routes to Error::Grpc.
  • cargo fmt --all -- --check
  • cargo clippy --workspace --locked -- -D warnings
  • cargo clippy --workspace --locked --tests --benches -- -D warnings
  • cargo test --workspace --locked
  • cargo run -p thetadatadx --features config-file --bin generate_sdk_surfaces --locked -- --check
  • python3 scripts/check_docs_consistency.py
  • cargo tree -i tonic returns empty

Cross-binding ABI

The Rust transport changes preserve the binding-visible error contract:

  • Error::Transport(String) still formats identically — bindings parse the message, not the inner type.
  • ChannelError::Rpc { status: Status } keys off status.code(), unchanged.
  • New CodecError::Encode only surfaces via ChannelError::Codec(_) which maps to Error::Transport at the FFI boundary.
  • Channel::connect_*_with_max_message_size are additive — the original connect_h2c / connect_tls keep their signatures.
  • Channel::with_decoder is a builder-style attachment; channels constructed without it fall back to the inline decode path.

Bench numbers

grpc_channel (loopback mock, 100-sample p50)

Endpoint p50 p95 alloc/call
stock_list_symbols 107.84 µs 108.88 µs 78,127 B
stock_history_eod 58.90 µs 59.68 µs 18,958 B
option_history_quote 71.86 µs 72.70 µs 32,698 B

grpc_concurrent_burst (64-way burst on a 4-channel pool, loopback mock)

Rows p50 (baseline) p50 (after) Δ Throughput Δ Alloc/call Δ
256 3.45 ms 2.28 ms -33.9% +51.4% -1.4% (-914 B)
4096 54.86 ms 29.99 ms -45.3% +82.9% -1.2% (-11 KB)
250000 3.612 s 1.820 s -49.6% +98.8% -1.2% (-1.2 MB)

Flamegraph at 4096 rows confirms prost::Message::decode, decompress_response, and ZSTD_decompressBlock_internal frames sit under the decoder thread group (87.3% of total samples) rather than the tokio worker stack (2.5%).

Public surface impact

  • thetadatadx::grpc::{Channel, ChannelError, ChannelPool, Codec, CodecError, ServerStreaming, Status, StatusParseError} — new public surface.
  • thetadatadx::grpc::{DecoderPool, DecoderHandle, DecoderPoolError, DecoderWaitStrategy, DecodeResult, default_decoder_thread_count} — new public decoder pool surface.
  • thetadatadx::grpc::{stock_list_symbols, endpoints::bench_support} — new public functions.
  • ChannelPool::from_channels_with_decoders — production constructor attaching a decoder pool to a channel set.
  • Channel::with_decoder — builder-style decoder attachment.
  • Channel::connect_h2c_with_max_message_size / Channel::connect_tls_with_max_message_size — codec-ceiling-aware constructors; the existing connect_h2c / connect_tls keep their signatures and delegate to the new ones with DEFAULT_MAX_MESSAGE_SIZE.
  • MddsConfig::decoder_threads / MddsConfig::decoder_ring_size — new public fields with 0 / 256 production defaults.
  • MddsClient::stub is gone (was crate-private; no downstream impact).
  • Error::Transport changes from From<tonic::transport::Error> to carrying a String — callers that previously matched the inner error must update.
  • inhouse-grpc Cargo feature is removed.
  • Cross-binding ABI (Python / TypeScript / C++ / FFI) unchanged.

Allocator and bench knobs

Optional mimalloc-allocator feature

thetadatadx = { features = ["mimalloc-allocator"] } pulls mimalloc into the dependency graph and re-exports MiMalloc at thetadatadx::mimalloc::MiMalloc so the consuming binary can wire it in with one line:

#[global_allocator]
static GLOBAL: thetadatadx::mimalloc::MiMalloc = thetadatadx::mimalloc::MiMalloc;

Default-off — applications that prefer the system allocator pay no compile or link cost. Full opt-in walk-through in docs-site/docs/configuration.md under Performance tuning. Library crates cannot install a #[global_allocator] of their own, so the SDK only provides the re-export.

THETADATADX_BENCH_ROWS env override on grpc_concurrent_burst

The concurrent-burst harness honours THETADATADX_BENCH_ROWS=<n> so the same bench sweeps the small, medium, and large payload regimes without recompiling.

Replace tonic with an in-house gRPC client on top of h2, and route
MDDS response decoding through a dedicated decoder pool that keeps
CPU-bound work off the tokio reactor.

Transport:
- Channel: HTTP/2 client wrapping h2::client::SendRequest<Bytes>
- Codec: length-prefix gRPC framing with prost encode/decode and
  propagated serialization errors
- Status: parsed from HTTP/2 trailers (grpc-status, grpc-message);
  trailers-only responses surfaced from the initial HEADERS frame
- ServerStreaming: async Stream over h2 RecvStream with per-call
  deadline support and GOAWAY classification; per-stream RST_STREAM
  distinguished from connection-level errors
- ChannelPool: round-robin across N HTTP/2 connections with
  credit-aware pick on connection-level MAX_CONCURRENT_STREAMS;
  honours configured concurrent_requests
- TLS via rustls 1.3; :scheme = https on TLS-backed Channels;
  mdds.max_message_size threaded through Channel -> Codec

Codegen:
- Custom prost-build configurator replacing tonic-build
- Drift guard runs unconditionally during build
- All MDDS endpoints routed through the new Channel

Decoder pool (grpc::DecoderPool):
- Decodes zstd-compressed protobuf responses on dedicated
  std::threads, keeping CPU-bound work off the tokio reactor
- One LMAX Disruptor SPSC ring per decoder thread with
  pre-allocated slots; per-thread zstd context and reusable
  scratch buffer
- Adaptive wait strategy (16 spins + 4 yields + spin-loop hint)
  tuned for decode-burst cadence
- Auto-sized to num_cpus/2 capped to channel count; ring size
  configurable, validated as a power of two >= 64
- Message-agnostic via Box<dyn FnOnce() -> DecodeResult + Send>

Configuration:
- mdds.decoder_threads (0 = auto)
- mdds.decoder_ring_size (power of two, >= 64, default 256)
- mdds.max_message_size honoured by the in-house Codec
- Optional mimalloc-allocator feature for downstream binaries

Util:
- Ring-size validation lifted to util::ring; FPSS and gRPC share
  the validator instead of duplicating it

Bench harness:
- 64-way concurrent burst over ChannelPool with mock h2 server
- THETADATADX_BENCH_ROWS env override (256 / 4096 / 250000)
- Allocator accounting captured only over the timed iter region

Performance (vs main, 64-way concurrent burst):

  | rows    | wall time | throughput |
  |---------|-----------|------------|
  | 256     | -33.9%    | +51%       |
  | 4 096   | -45.3%    | +83%       |
  | 250 000 | -49.6%    | +99%       |

Drops: tonic, tonic-prost, tonic-prost-build.
@userFRM userFRM force-pushed the feat/inhouse-grpc-transport branch from b8fb128 to 4dd1fa1 Compare May 14, 2026 11:18
@userFRM userFRM merged commit 0dadc17 into main May 15, 2026
48 of 49 checks passed
@userFRM userFRM deleted the feat/inhouse-grpc-transport branch May 15, 2026 12:37
userFRM added a commit that referenced this pull request May 15, 2026
Closes six pre-merge audit findings against the in-house gRPC
transport at 0dadc17 on main.

1. Decoder pool panic containment + poison-aware publish loop
   (decoder_pool.rs). Consumer wraps each work closure in
   catch_unwind; on panic it flips a pool-wide Arc<AtomicBool>
   poison flag and drains still-queued ring slots with the
   transport-level poison reason rather than leaving callers
   parked on dead oneshots. The publish path itself is
   poison-aware: instead of disruptor's blocking publish() (which
   busy-waits until a slot frees and ignores the poison flag),
   submit_work() drives try_publish() in a bounded retry loop
   that re-checks the flag between every attempt — a poison flip
   propagates to every parked submitter within one back-off
   window. Submits made after the flag is set return
   DecoderSubmitError::Poisoned without touching the ring. Tests
   include panicking_work_poisons_pool,
   submit_after_poison_fails_fast,
   pending_in_flight_drains_with_poisoned_after_panic, and
   poison_flag_unblocks_publishers_on_full_ring (fills the ring
   with a barrier-parked head item, spawns extra submitters that
   park in the retry loop, flips the flag, asserts every parked
   submitter returns Poisoned within 250 ms).

2. grpc-message percent-decoding (status.rs). Parser percent-
   decodes per RFC 3986 (%HH escapes only, no +-as-space).
   Malformed escapes fall back to raw header bytes; non-UTF-8
   bytes fall back to empty. Never invalidates a parsed
   grpc-status. Removed StatusParseError::MessageNotUtf8 (added
   in PR #524, never released). Added percent-encoding v2 direct
   dependency.

3. h2 error classification on the open path (channel.rs). ready(),
   send_request(), send_data() errors now route through
   classify_h2_error so connection-level failures (GOAWAY in
   either direction, IO failure, peer shutdown, open-phase drops)
   surface as ChannelError::ConnectionClosed. Per-stream
   RST_STREAM (any reason code) continues to surface as
   ChannelError::H2Stream. The ConnectionClosed Display string
   broadens to "h2 connection closed: ..." to reflect the wider
   scope. Variant docs on the enum, the module-level docstring,
   and the CHANGELOG record the contract reshape so downstream
   callers using ChannelError for retry / recycle policy can
   remap branches.

4. Picker counts dispatched, not pending (channel.rs).
   InFlightToken::new is now incremented before ready_fut.await
   so ChannelPool::next() sees every pending open the moment it
   commits, not just the ones that have cleared the h2 ready
   barrier. Eliminates head-of-line blocking under burst
   contention; the picker uses CAS-retry to settle on the
   lightest-loaded channel even under concurrent commits, and
   the lease pre-reserves an in-flight slot at pool.next() time
   so join_all-style dispatch fans out cleanly.

5. v10 changelog (CHANGELOG.md, docs-site changelog). Added an
   Unreleased block covering the Error::Transport payload change
   (tonic::transport::Error → String), MddsConfig::decoder_threads
   / decoder_ring_size, tonic and inhouse-grpc feature removal,
   MddsClient::stub removal, GrpcStatusKind::from_u32() rename,
   ChannelError variant reshape, and migration notes for each.
   The merged feature shipped after the v10.0.0 tag, so the
   Unreleased bucket is the correct landing site; the version
   bump itself is intentionally not part of this PR.

6. Codegen drift guard (build_support/grpc). Persisted snapshot at
   crates/thetadatadx/proto/beta_endpoints.snapshot.rs (git-
   tracked, normalised to LF line endings via .gitattributes so
   CRLF-on-checkout platforms still match). check() regenerates
   into a scratch dir under OUT_DIR and byte-compares against
   the committed snapshot. Drift fails the build with the byte
   offset and line number of the first divergence and the
   regeneration command; THETADATADX_GRPC_REGEN=1 cargo build
   refreshes the snapshot in-tree. cargo:rerun-if-changed and
   cargo:rerun-if-env-changed wired so cargo invalidates on
   snapshot or env-var edits.
userFRM added a commit that referenced this pull request May 15, 2026
Closes six pre-merge audit findings against the in-house gRPC
transport at 0dadc17 on main.

1. Decoder pool panic containment + poison-aware publish loop
   (decoder_pool.rs). Consumer wraps each work closure in
   catch_unwind; on panic it flips a pool-wide Arc<AtomicBool>
   poison flag and drains still-queued ring slots with the
   transport-level poison reason rather than leaving callers
   parked on dead oneshots. The publish path itself is
   poison-aware: instead of disruptor's blocking publish() (which
   busy-waits until a slot frees and ignores the poison flag),
   submit_work() drives try_publish() in a bounded retry loop
   that re-checks the flag between every attempt — a poison flip
   propagates to every parked submitter within one back-off
   window. Submits made after the flag is set return
   DecoderSubmitError::Poisoned without touching the ring. Tests
   include panicking_work_poisons_pool,
   submit_after_poison_fails_fast,
   pending_in_flight_drains_with_poisoned_after_panic, and
   poison_flag_unblocks_publishers_on_full_ring (fills the ring
   with a barrier-parked head item, spawns extra submitters that
   park in the retry loop, flips the flag, asserts every parked
   submitter returns Poisoned within 250 ms).

2. grpc-message percent-decoding (status.rs). Parser percent-
   decodes per RFC 3986 (%HH escapes only, no +-as-space).
   Malformed escapes fall back to raw header bytes; non-UTF-8
   bytes fall back to empty. Never invalidates a parsed
   grpc-status. Removed StatusParseError::MessageNotUtf8 (added
   in PR #524, never released). Added percent-encoding v2 direct
   dependency.

3. h2 error classification on the open path (channel.rs). ready(),
   send_request(), send_data() errors now route through
   classify_h2_error so connection-level failures (GOAWAY in
   either direction, IO failure, peer shutdown, open-phase drops)
   surface as ChannelError::ConnectionClosed. Per-stream
   RST_STREAM (any reason code) continues to surface as
   ChannelError::H2Stream. The ConnectionClosed Display string
   broadens to "h2 connection closed: ..." to reflect the wider
   scope. Variant docs on the enum, the module-level docstring,
   and the CHANGELOG record the contract reshape so downstream
   callers using ChannelError for retry / recycle policy can
   remap branches.

4. Picker counts dispatched, not pending (channel.rs).
   InFlightToken::new is now incremented before ready_fut.await
   so ChannelPool::next() sees every pending open the moment it
   commits, not just the ones that have cleared the h2 ready
   barrier. Eliminates head-of-line blocking under burst
   contention; the picker uses CAS-retry to settle on the
   lightest-loaded channel even under concurrent commits, and
   the lease pre-reserves an in-flight slot at pool.next() time
   so join_all-style dispatch fans out cleanly.

5. v10 changelog (CHANGELOG.md, docs-site changelog). Added an
   Unreleased block covering the Error::Transport payload change
   (tonic::transport::Error → String), MddsConfig::decoder_threads
   / decoder_ring_size, tonic and inhouse-grpc feature removal,
   MddsClient::stub removal, GrpcStatusKind::from_u32() rename,
   ChannelError variant reshape, and migration notes for each.
   The merged feature shipped after the v10.0.0 tag, so the
   Unreleased bucket is the correct landing site; the version
   bump itself is intentionally not part of this PR.

6. Codegen drift guard (build_support/grpc). Persisted snapshot at
   crates/thetadatadx/proto/beta_endpoints.snapshot.rs (git-
   tracked, normalised to LF line endings via .gitattributes so
   CRLF-on-checkout platforms still match). check() regenerates
   into a scratch dir under OUT_DIR and byte-compares against
   the committed snapshot. Drift fails the build with the byte
   offset and line number of the first divergence and the
   regeneration command; THETADATADX_GRPC_REGEN=1 cargo build
   refreshes the snapshot in-tree. cargo:rerun-if-changed and
   cargo:rerun-if-env-changed wired so cargo invalidates on
   snapshot or env-var edits.
userFRM added a commit that referenced this pull request May 15, 2026
* fix(grpc): close pre-merge audit findings

Closes six pre-merge audit findings against the in-house gRPC
transport at 0dadc17 on main.

1. Decoder pool panic containment + poison-aware publish loop
   (decoder_pool.rs). Consumer wraps each work closure in
   catch_unwind; on panic it flips a pool-wide Arc<AtomicBool>
   poison flag and drains still-queued ring slots with the
   transport-level poison reason rather than leaving callers
   parked on dead oneshots. The publish path itself is
   poison-aware: instead of disruptor's blocking publish() (which
   busy-waits until a slot frees and ignores the poison flag),
   submit_work() drives try_publish() in a bounded retry loop
   that re-checks the flag between every attempt — a poison flip
   propagates to every parked submitter within one back-off
   window. Submits made after the flag is set return
   DecoderSubmitError::Poisoned without touching the ring. Tests
   include panicking_work_poisons_pool,
   submit_after_poison_fails_fast,
   pending_in_flight_drains_with_poisoned_after_panic, and
   poison_flag_unblocks_publishers_on_full_ring (fills the ring
   with a barrier-parked head item, spawns extra submitters that
   park in the retry loop, flips the flag, asserts every parked
   submitter returns Poisoned within 250 ms).

2. grpc-message percent-decoding (status.rs). Parser percent-
   decodes per RFC 3986 (%HH escapes only, no +-as-space).
   Malformed escapes fall back to raw header bytes; non-UTF-8
   bytes fall back to empty. Never invalidates a parsed
   grpc-status. Removed StatusParseError::MessageNotUtf8 (added
   in PR #524, never released). Added percent-encoding v2 direct
   dependency.

3. h2 error classification on the open path (channel.rs). ready(),
   send_request(), send_data() errors now route through
   classify_h2_error so connection-level failures (GOAWAY in
   either direction, IO failure, peer shutdown, open-phase drops)
   surface as ChannelError::ConnectionClosed. Per-stream
   RST_STREAM (any reason code) continues to surface as
   ChannelError::H2Stream. The ConnectionClosed Display string
   broadens to "h2 connection closed: ..." to reflect the wider
   scope. Variant docs on the enum, the module-level docstring,
   and the CHANGELOG record the contract reshape so downstream
   callers using ChannelError for retry / recycle policy can
   remap branches.

4. Picker counts dispatched, not pending (channel.rs).
   InFlightToken::new is now incremented before ready_fut.await
   so ChannelPool::next() sees every pending open the moment it
   commits, not just the ones that have cleared the h2 ready
   barrier. Eliminates head-of-line blocking under burst
   contention; the picker uses CAS-retry to settle on the
   lightest-loaded channel even under concurrent commits, and
   the lease pre-reserves an in-flight slot at pool.next() time
   so join_all-style dispatch fans out cleanly.

5. v10 changelog (CHANGELOG.md, docs-site changelog). Added an
   Unreleased block covering the Error::Transport payload change
   (tonic::transport::Error → String), MddsConfig::decoder_threads
   / decoder_ring_size, tonic and inhouse-grpc feature removal,
   MddsClient::stub removal, GrpcStatusKind::from_u32() rename,
   ChannelError variant reshape, and migration notes for each.
   The merged feature shipped after the v10.0.0 tag, so the
   Unreleased bucket is the correct landing site; the version
   bump itself is intentionally not part of this PR.

6. Codegen drift guard (build_support/grpc). Persisted snapshot at
   crates/thetadatadx/proto/beta_endpoints.snapshot.rs (git-
   tracked, normalised to LF line endings via .gitattributes so
   CRLF-on-checkout platforms still match). check() regenerates
   into a scratch dir under OUT_DIR and byte-compares against
   the committed snapshot. Drift fails the build with the byte
   offset and line number of the first divergence and the
   regeneration command; THETADATADX_GRPC_REGEN=1 cargo build
   refreshes the snapshot in-tree. cargo:rerun-if-changed and
   cargo:rerun-if-env-changed wired so cargo invalidates on
   snapshot or env-var edits.

* chore(deps): re-lock sub-project Cargo.lock for percent-encoding
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.

feat(grpc): in-house gRPC transport replacing tonic

1 participant