<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Wang Lee</title>
    <description>The latest articles on DEV Community by Wang Lee (@qianwj).</description>
    <link>https://dev.to/qianwj</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F317772%2F2129f777-227e-4884-a83f-e26df2c7d7fe.jpeg</url>
      <title>DEV Community: Wang Lee</title>
      <link>https://dev.to/qianwj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/qianwj"/>
    <language>en</language>
    <item>
      <title>Why Build gRPC Directly on Reactor Netty</title>
      <dc:creator>Wang Lee</dc:creator>
      <pubDate>Fri, 07 Aug 2026 14:23:22 +0000</pubDate>
      <link>https://dev.to/qianwj/why-build-grpc-directly-on-reactor-netty-2g96</link>
      <guid>https://dev.to/qianwj/why-build-grpc-directly-on-reactor-netty-2g96</guid>
      <description>&lt;p&gt;I recently started building &lt;a href="https://github.com/qianwj/grpc-reactor" rel="noopener noreferrer"&gt;grpc-reactor&lt;/a&gt;: an experimental gRPC implementation built directly on Reactor Netty HTTP/2. The project uses Protobuf but has zero runtime dependency on grpc-java transport, &lt;code&gt;ClientCall&lt;/code&gt;, &lt;code&gt;ServerCall&lt;/code&gt;, or &lt;code&gt;StreamObserver&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is not about proving grpc-java is bad. grpc-java is mature, stable, and covers load balancing, NameResolver, retries, and rich observability. This project answers a different question: if your programming model is already Reactor, can &lt;code&gt;Mono&lt;/code&gt; and &lt;code&gt;Flux&lt;/code&gt; flow all the way from the generated API down to the HTTP/2 stream, without adapting between two async abstractions?&lt;/p&gt;

&lt;p&gt;This post is based on JDK 25, Gradle 9.2.1, Reactor 3.8.6, Reactor Netty 1.3.6, and Netty 4.2.15.Final. The &lt;code&gt;main&lt;/code&gt; branch has completed Stage 0 through Stage 10: beyond protocol, four RPC cardinalities, production transport, DNS, codegen, standard services, and Stage 9 hardening, Stage 10 adds an optional load-balancing extensions artifact for grpclb, RLS, load reporting, and ORCA.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API Goal Is Not Wrapping StreamObserver
&lt;/h2&gt;

&lt;p&gt;Protobuf methods have four cardinalities. The target API maps them directly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Protobuf Method&lt;/th&gt;
&lt;th&gt;Reactor Signature&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;unary&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mono&amp;lt;Resp&amp;gt; method(Mono&amp;lt;Req&amp;gt;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;server streaming&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Flux&amp;lt;Resp&amp;gt; method(Mono&amp;lt;Req&amp;gt;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;client streaming&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mono&amp;lt;Resp&amp;gt; method(Flux&amp;lt;Req&amp;gt;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bidirectional streaming&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Flux&amp;lt;Resp&amp;gt; method(Flux&amp;lt;Req&amp;gt;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why does gRPC define four instead of just unary? It's essentially a 2x2 combination where request and response each independently choose "single value or stream":&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Single Response&lt;/th&gt;
&lt;th&gt;Streaming Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Single Request&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;unary&lt;/td&gt;
&lt;td&gt;server streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Streaming Request&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;client streaming&lt;/td&gt;
&lt;td&gt;bidirectional&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They solve different scenarios: unary covers classic request-response; server streaming handles server push (event subscriptions, large paginated pulls); client streaming handles bulk uploads (file chunks, batch writes); bidirectional streaming handles real-time two-way communication (chat, collaborative editing). These aren't invented patterns — HTTP/2 streams are inherently full-duplex. A single connection can multiplex hundreds of concurrent streams, with both request and response sending multiple data frames independently. gRPC elevates this transport capability to first-class API semantics: not a variant of chunked transfer encoding, but compiler-level type checking that callers and implementers match.&lt;/p&gt;

&lt;p&gt;The low-level transport retains a single unified model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flux&amp;lt;Req&amp;gt; -&amp;gt; HTTP/2 stream -&amp;gt; Flux&amp;lt;Resp&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generated code applies cardinality checks like &lt;code&gt;single()&lt;/code&gt; at the boundary. This avoids implementing four separate network logic paths for four RPC types, while explicitly rejecting empty requests, duplicate requests, or duplicate responses in unary calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compatibility Targets the Wire Protocol
&lt;/h2&gt;

&lt;p&gt;The project's compatibility target is the public &lt;a href="https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md" rel="noopener noreferrer"&gt;gRPC over HTTP/2 protocol spec&lt;/a&gt;, not grpc-java internal APIs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;generated Reactor API
        |
call dispatcher / service registry
        |
marshaller + message framer/deframer
        |
metadata + status + deadline
        |
Reactor Netty HTTP/2 stream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each RPC corresponds to one HTTP/2 stream. Requests start with a HEADERS frame containing at minimum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:method      POST
:path        /&amp;lt;package.Service&amp;gt;/&amp;lt;Method&amp;gt;
content-type application/grpc+proto
te           trailers
grpc-timeout &amp;lt;relative timeout, e.g. 100m for 100ms&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Message bodies use Length-Prefixed-Message format encapsulated in DATA frames — each Protobuf message is preceded by a five-byte envelope (1 byte compression flag + 4 bytes big-endian length). A single DATA frame may contain multiple gRPC messages, and a large message may span multiple DATA frames.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does gRPC require HTTP trailers?&lt;/strong&gt; This is one of the protocol's most counterintuitive designs. HTTP status codes are nearly useless for gRPC — the protocol requires the HTTP layer to always return 200, with the actual call result (&lt;code&gt;grpc-status&lt;/code&gt; and &lt;code&gt;grpc-message&lt;/code&gt;) placed in trailers. The reason: in streaming scenarios, the server may have already sent thousands of messages, and whether it ultimately succeeded or failed can only be determined after processing the last piece of data. HTTP headers are sent before the body and cannot carry this posterior result. Trailers are the only mechanism in HTTP/2 that can append metadata after the body.&lt;/p&gt;

&lt;p&gt;The protocol also defines &lt;strong&gt;trailers-only&lt;/strong&gt; mode: when the server can determine failure before reading the body (e.g., path not found, authentication failed), &lt;code&gt;grpc-status&lt;/code&gt; is returned directly in response headers without sending a body, saving one round-trip. Clients must check both headers and trailers to correctly extract the final status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modules Split Along Protocol Boundaries
&lt;/h2&gt;

&lt;p&gt;The project currently has nine modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grpc-reactor-protocol      → Transport-independent protocol primitives
grpc-reactor-transport     → Reactor Netty HTTP/2 mapping
grpc-reactor-codegen       → Protoc plugin, generates Reactor stubs
grpc-reactor-gradle-plugin → Gradle integration
grpc-reactor-maven-plugin  → Maven generate-sources integration
grpc-reactor-services      → Optional Health, Reflection &amp;amp; Channelz standard services
grpc-reactor-binlog        → Optional, bounded canonical binary logging
grpc-reactor-lb-extensions → Optional grpclb, RLS, load reporting &amp;amp; ORCA
grpc-reactor-interop-test  → grpc-java compatibility tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;protocol&lt;/code&gt; handles only transport-independent values and codecs: message framing, metadata, status, timeout, compression, and protobuf marshallers. It depends on Reactor Core and Netty Buffer but not Reactor Netty — meaning the protocol layer can be tested independently without real HTTP/2 connections.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;transport&lt;/code&gt; maps the protocol onto Reactor Netty HTTP/2, handling client, server, service registry, and per-call context.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;codegen&lt;/code&gt; consumes Protobuf &lt;code&gt;CodeGeneratorRequest&lt;/code&gt; and generates type-safe Reactor client and service binders.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;services&lt;/code&gt; uses the same codegen to generate canonical gRPC service bindings, implementing Health v1, Reflection v1, and Channelz v1 on top of transport's immutable descriptor/diagnostics snapshots. This module requires explicit registration — adding the dependency alone won't expose management endpoints.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;binlog&lt;/code&gt; is also an explicitly-enabled standalone module. It captures canonical binary-log v1 events via a transport interceptor, controlling information exposure and memory limits through metadata/message truncation, sensitive key redaction, and fixed-capacity sinks.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;interop-test&lt;/code&gt; introduces grpc-java in test scope. grpc-java serves as the compatibility oracle here, never entering the project runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency Selection and Version Pinning
&lt;/h2&gt;

&lt;p&gt;The runtime dependency chain is intentionally kept short:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dependency&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reactor Core&lt;/td&gt;
&lt;td&gt;3.8.6&lt;/td&gt;
&lt;td&gt;Mono/Flux programming model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reactor Netty&lt;/td&gt;
&lt;td&gt;1.3.6&lt;/td&gt;
&lt;td&gt;HTTP/2 client/server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netty&lt;/td&gt;
&lt;td&gt;4.2.15&lt;/td&gt;
&lt;td&gt;ByteBuf, HTTP/2 codec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protobuf-java&lt;/td&gt;
&lt;td&gt;4.35.1&lt;/td&gt;
&lt;td&gt;Message serialization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The project does not introduce Spring, Micrometer, or any DI framework. Tests use JUnit 6.1.2 and Reactor Test's &lt;code&gt;StepVerifier&lt;/code&gt;. grpc-java 1.82.2 appears only in &lt;code&gt;interop-test&lt;/code&gt;'s test classpath with zero runtime intrusion.&lt;/p&gt;

&lt;p&gt;Versions are centrally managed via &lt;code&gt;gradle/libs.versions.toml&lt;/code&gt; with Gradle dependency locking generating lock files, ensuring fully reproducible builds across machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Protocol Layer Must Come First
&lt;/h2&gt;

&lt;p&gt;It's tempting to start with "spin up an HTTP/2 Server" — you quickly get an echo demo, but it pushes the truly difficult problems to later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DATA may split in the middle of the five-byte gRPC header;&lt;/li&gt;
&lt;li&gt;A single DATA buffer may contain multiple messages;&lt;/li&gt;
&lt;li&gt;Metadata allows duplicate keys, and binary values require Base64;&lt;/li&gt;
&lt;li&gt;Timeout wire values are at most eight digits with a unit suffix;&lt;/li&gt;
&lt;li&gt;Final status comes from trailers;&lt;/li&gt;
&lt;li&gt;ByteBuf must be released on success, failure, and cancellation paths;&lt;/li&gt;
&lt;li&gt;Reactive Streams demand counts messages, HTTP/2 flow control counts bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Therefore the project progresses by Stage: first lock down the build and interop fixtures, then complete the protocol layer, then implement unary transport, streaming, production features, and codegen. Each Stage has executable exit criteria — "classes have been created" is not a completion standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 0: Build Baseline and Interop Fixture
&lt;/h2&gt;

&lt;p&gt;Before writing any protocol code, Stage 0 solves "how to prove code is correct":&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JDK 25 compilation&lt;/strong&gt; — The project uses &lt;code&gt;-Xlint:all -parameters -encoding UTF-8&lt;/code&gt; for strict compilation. All warnings are compilation errors; silent suppression is not allowed. JDK 25 was chosen to validate Netty and Protobuf compatibility on the latest JVM early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spotless formatting&lt;/strong&gt; — Unified Eclipse formatter config plus ktlint. &lt;code&gt;spotlessCheck&lt;/code&gt; is the first gate in CI. This eliminates all code review discussions about formatting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;interop.proto test fixture&lt;/strong&gt; — Defines a test service covering all four RPC cardinalities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="kd"&gt;service&lt;/span&gt; &lt;span class="n"&gt;InteropTestService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;Unary&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;ServerStreaming&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="n"&gt;TestResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;ClientStreaming&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="n"&gt;TestRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;BidirectionalStreaming&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="n"&gt;TestRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="n"&gt;TestResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GrpcJavaFixture&lt;/strong&gt; — In the &lt;code&gt;interop-test&lt;/code&gt; module, a test utility class starts both a grpc-java server and client, providing &lt;code&gt;start()&lt;/code&gt; / &lt;code&gt;close()&lt;/code&gt; lifecycle. Through it, bidirectional verification is possible: Reactor client calls grpc-java server, and grpc-java client calls Reactor server. Both use the same &lt;code&gt;.proto&lt;/code&gt; generated code, ensuring wire compatibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Utilities&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FreePorts&lt;/code&gt;: Allocates independent ports for each test, avoiding parallel test conflicts;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TlsTestCertificates&lt;/code&gt;: Pre-generates self-signed certificates for subsequent TLS tests;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LeakDetection&lt;/code&gt;: Integrates Netty's &lt;code&gt;ResourceLeakDetector&lt;/code&gt;, ensuring ByteBuf leaks are immediately exposed in tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stage 0 exit criteria&lt;/strong&gt;: &lt;code&gt;./gradlew clean test&lt;/code&gt; passes from fresh checkout, CI is green on Linux + Java 25, protoc generation is deterministically reproducible, grpc-java fixture communicates bidirectionally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Staged Verification Strategy
&lt;/h2&gt;

&lt;p&gt;The project progresses through 12 Stages, each with clear goals, executable exit criteria, and regression coverage:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Key Deliverable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Build baseline&lt;/td&gt;
&lt;td&gt;CI, formatting, interop fixture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Protocol foundation&lt;/td&gt;
&lt;td&gt;Frame codec, metadata, status, timeout, compression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Unary transport&lt;/td&gt;
&lt;td&gt;End-to-end h2c unary call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Server streaming&lt;/td&gt;
&lt;td&gt;Multi-message response stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Full cardinality&lt;/td&gt;
&lt;td&gt;Client streaming + bidirectional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Production transport&lt;/td&gt;
&lt;td&gt;TLS, gzip, deadline, GOAWAY, connection pool, keepalive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Name resolution&lt;/td&gt;
&lt;td&gt;DNS, subchannel, pick_first, round_robin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Codegen &amp;amp; build integration&lt;/td&gt;
&lt;td&gt;Protoc plugin, descriptor registry, Gradle/Maven plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Standard services&lt;/td&gt;
&lt;td&gt;Health v1, Reflection v1, Channelz v1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Operations &amp;amp; hardening&lt;/td&gt;
&lt;td&gt;Interceptor, observer, binlog, canonical smoke, fuzz/churn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Load-balancing extensions&lt;/td&gt;
&lt;td&gt;grpclb, RLS, load reporter, ORCA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Diagnostics (planned)&lt;/td&gt;
&lt;td&gt;Channelz v2 over the bounded diagnostics registry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each Stage completion requires: &lt;code&gt;./gradlew clean spotlessCheck test --no-daemon&lt;/code&gt; passes in full, and all prior Stage tests continue running as regression. This means when Stage 4 completes, Stage 2's unary tests are still green.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Reactor semantics differ from grpc-java
&lt;/h2&gt;

&lt;p&gt;The wire contract is shared, but the application contracts are not. The tests therefore compare the two implementations at the protocol boundary and test each runtime's lifecycle rules separately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Reactor contract&lt;/th&gt;
&lt;th&gt;grpc-java contract&lt;/th&gt;
&lt;th&gt;What the tests must prove&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Demand&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Subscription.request(n)&lt;/code&gt; counts decoded messages; transport demand must also respect HTTP/2 byte windows&lt;/td&gt;
&lt;td&gt;inbound flow is controlled through &lt;code&gt;ClientCall.request(n)&lt;/code&gt; / readiness callbacks&lt;/td&gt;
&lt;td&gt;no response DATA is delivered before downstream demand, and coalesced frames do not bypass demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cancellation&lt;/td&gt;
&lt;td&gt;disposing a &lt;code&gt;Mono&lt;/code&gt;/&lt;code&gt;Flux&lt;/code&gt; cancels both application publishers and resets an open stream&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ClientCall.cancel&lt;/code&gt; or &lt;code&gt;Context&lt;/code&gt; cancellation reaches &lt;code&gt;StreamObserver&lt;/code&gt; callbacks&lt;/td&gt;
&lt;td&gt;both sides stop, exactly one terminal signal wins, and late DATA/trailers are ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trailers&lt;/td&gt;
&lt;td&gt;success trailers are retained in the call result; non-OK trailers become &lt;code&gt;GrpcException.trailers()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;trailers are exposed through &lt;code&gt;ClientCall.Listener#onClose&lt;/code&gt; or &lt;code&gt;StatusRuntimeException#getTrailers()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;grpc-status&lt;/code&gt;, &lt;code&gt;grpc-message&lt;/code&gt;, and custom trailers survive in both directions, including trailers-only errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buffer ownership&lt;/td&gt;
&lt;td&gt;Netty &lt;code&gt;ByteBuf&lt;/code&gt; is reference-counted and must be released on success, error, and cancel; decoded protobuf values cross the API boundary&lt;/td&gt;
&lt;td&gt;generated protobuf messages hide transport buffers from application code&lt;/td&gt;
&lt;td&gt;every rejected, partial, compressed, and cancelled frame reaches &lt;code&gt;refCnt() == 0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, the Reactor demand test starts with zero demand and asks for one response at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;StepVerifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bidirectionalStreaming&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;BIDI&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expectNext&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expectNext&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;third&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expectComplete&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corresponding grpc-java test uses &lt;code&gt;StreamObserver&lt;/code&gt; callbacks and completion signals. The messages and trailers must be wire-compatible, but it would be incorrect to describe the two tests as asserting the same backpressure mechanism. This distinction is why the project keeps both cross-runtime interop and runtime-specific failure tests.&lt;/p&gt;

&lt;p&gt;The repository now also has two server-streaming cancellation interop cases in &lt;code&gt;ServerStreamingInteroperabilityTest&lt;/code&gt;: a Reactor client receives one response from a grpc-java server and cancels, while a grpc-java client cancels a Reactor server after its first response. Both sides wait for the peer's cancellation callback under paranoid leak detection. The lower-level &lt;code&gt;GrpcFrameCodecTest&lt;/code&gt; keeps the direct ownership assertion by checking that cancelled and partial inputs finish with &lt;code&gt;refCnt() == 0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The essential cancellation paths are deliberately small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Reactor client: cancel the HTTP/2 call after the first response.&lt;/span&gt;
&lt;span class="nc"&gt;TestResponse&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reactorStub&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;serverStreaming&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;just&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;single&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// grpc-java client: cancel from the response callback.&lt;/span&gt;
&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onNext&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TestResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;requestStream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cancel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cancel after first response"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the grpc-java server side, the test installs &lt;code&gt;setOnCancelHandler&lt;/code&gt;; on the Reactor server side, the response &lt;code&gt;Flux&lt;/code&gt; uses &lt;code&gt;doOnCancel&lt;/code&gt;. The complete test, including latches, peer status assertions, shutdown, and leak-detection scope, is in &lt;a href="https://github.com/qianwj/grpc-reactor/blob/db1a2fb/grpc-reactor-interop-test/src/test/java/io/github/qianwj/grpc/reactor/testing/ServerStreamingInteroperabilityTest.java#L97-L183" rel="noopener noreferrer"&gt;&lt;code&gt;ServerStreamingInteroperabilityTest.java&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current Verification Results
&lt;/h2&gt;

&lt;p&gt;The project uses this unified gate that simultaneously checks formatting, compilation, protocol tests, transport tests, and grpc-java interop tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew clean spotlessCheck &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--no-daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command passes as of this writing. The gate now includes the optional Stage 10 load-balancing extension tests in addition to protocol, transport, codegen, services, binary-log, and grpc-java interop tests. The dependency baseline is Protobuf 4.35.1, grpc-java 1.82.2, and JUnit 6.1.2. Under JDK 25, you'll still see Protobuf &lt;code&gt;Unsafe&lt;/code&gt;, Netty/Gradle native access, and Gradle deprecated feature warnings — they don't affect test results but need ongoing tracking through future JDK and Gradle upgrades.&lt;/p&gt;

&lt;p&gt;Subsequent posts will continue discussing the five-byte gRPC message envelope, ByteBuf ownership, four RPC cardinalities, production transport, DNS, code generation, standard management services, and how to integrate interceptors, observation, and binary logging without breaking Reactive Streams semantics.&lt;/p&gt;

</description>
      <category>java</category>
      <category>grpc</category>
      <category>netty</category>
      <category>reactive</category>
    </item>
  </channel>
</rss>
