Created
July 20, 2026 19:34
-
-
Save vietj/75d37c1033ac584b45162ce91b8e5c3f to your computer and use it in GitHub Desktop.
Claude generated RFC for gRPC over Vert.x Event Bus
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| gRPC / Vert.x Event Bus Vert.x gRPC Project | |
| Internet-Draft Eclipse Vert.x | |
| Intended status: Experimental 20 July 2026 | |
| Expires: 21 January 2027 | |
| gRPC Transport over the Vert.x Event Bus | |
| draft-vertx-grpc-eventbus-transport-00 | |
| Abstract | |
| This document specifies an experimental transport that carries gRPC | |
| calls over the Eclipse Vert.x event bus in place of HTTP/2. When a | |
| gRPC client and server run within the same Vert.x application or | |
| cluster, this transport lets calls be made with the generated stubs | |
| over the event bus that is already present, without standing up an | |
| HTTP server, and lets those calls travel across a clustered event bus | |
| alongside other verticles. Unary calls reuse the existing Vert.x | |
| service-proxy request/reply shape; streaming calls add a small | |
| framing protocol, multiplexed over one long-lived consumer per | |
| endpoint, with message-counted flow control and an explicit liveness | |
| mechanism. | |
| Status of This Memo | |
| This Internet-Draft is submitted in full conformance with the | |
| provisions of BCP 78 and BCP 79. | |
| Internet-Drafts are working documents of the Internet Engineering | |
| Task Force (IETF). Note that other groups may also distribute working | |
| documents as Internet-Drafts. The list of current Internet-Drafts is | |
| at https://datatracker.ietf.org/drafts/current/. | |
| Internet-Drafts are draft documents valid for a maximum of six months | |
| and may be updated, replaced, or obsoleted by other documents at any | |
| time. It is inappropriate to use Internet-Drafts as reference | |
| material or to cite them other than as "work in progress." | |
| This Internet-Draft will expire on 21 January 2027. | |
| Copyright Notice | |
| Copyright (c) 2026 IETF Trust and the persons identified as the | |
| document authors. All rights reserved. | |
| This document is subject to BCP 78 and the IETF Trust's Legal | |
| Provisions Relating to IETF Documents | |
| (https://trustee.ietf.org/license-info) in effect on the date of | |
| publication of this document. Please review these documents | |
| Vert.x gRPC Expires 21 January 2027 [Page 1] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| carefully, as they describe your rights and restrictions with respect | |
| to this document. | |
| Table of Contents | |
| 1. Introduction ...................................................2 | |
| 1.1. Goals .....................................................3 | |
| 1.2. Relationship to the HTTP/2 Transport ......................3 | |
| 2. Conventions and Terminology ....................................3 | |
| 2.1. Requirements Language .....................................4 | |
| 2.2. Terminology ...............................................4 | |
| 3. Transport Model ................................................4 | |
| 4. Unary Calls ....................................................5 | |
| 5. Streaming Calls ................................................5 | |
| 5.1. Stream Establishment ......................................5 | |
| 5.2. Private Addresses and Multiplexing ........................6 | |
| 5.3. Full-Duplex Frame Exchange ................................6 | |
| 5.4. Ordering Guarantees .......................................7 | |
| 5.5. Stream Termination ........................................8 | |
| 5.6. Behaviour on a Clustered Event Bus ........................8 | |
| 6. Transport Frames ...............................................8 | |
| 6.1. Frame Envelope ............................................8 | |
| 6.2. Frame Variants ............................................9 | |
| 6.3. Message Encoding ..........................................9 | |
| 6.4. Frame Schema .............................................10 | |
| 7. Delivery Header Fields ........................................11 | |
| 8. Flow Control ..................................................12 | |
| 9. Liveness ......................................................12 | |
| 9.1. Active Detection (Delivery Failure) ......................13 | |
| 9.2. Passive Detection (Idle Timeout) .........................13 | |
| 9.3. Heartbeats ...............................................13 | |
| 9.4. Multiplexing and Head-of-Line Blocking ...................13 | |
| 10. Configuration ................................................14 | |
| 11. Security Considerations ......................................15 | |
| 12. IANA Considerations ..........................................15 | |
| 13. Open Questions and Future Work ...............................16 | |
| 14. References ...................................................16 | |
| 14.1. Normative References ....................................17 | |
| 14.2. Informative References ..................................17 | |
| Author's Address ..................................................18 | |
| 1. Introduction | |
| This document describes a transport that carries gRPC calls over the | |
| Eclipse Vert.x event bus rather than over HTTP/2. The event bus is a | |
| Vert.x gRPC Expires 21 January 2027 [Page 2] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| message bus rather than a byte stream, so gRPC streaming requires a | |
| small protocol on top of it to bridge the gap. Unary calls do not, | |
| and are mapped directly onto the event bus request/reply primitive. | |
| This transport is experimental. The wire format described here is a | |
| proposal under active development, and a given prototype MAY diverge | |
| from it in detail. This document is a design in progress rather than | |
| a finished specification, and its intended status is Experimental. | |
| 1.1. Goals | |
| A primary goal is that application code need not change between the | |
| HTTP/2 transport and this transport. The generated stubs and the | |
| GrpcServerRequest, GrpcServerResponse, and GrpcClientRequest types | |
| are shared with the HTTP/2 transport; ideally only the factory | |
| differs. | |
| // server | |
| EventBusGrpcServer.server(vertx).onSuccess(server -> { | |
| server.addService(GreeterGrpcService.of(new GreeterService() { | |
| ... | |
| })); | |
| }); | |
| // client | |
| EventBusGrpcClient.client(vertx).onSuccess(client -> { | |
| GreeterClient greeter = GreeterGrpcClient.create(client); | |
| greeter.sayHello( | |
| HelloRequest.newBuilder().setName("World").build()); | |
| }); | |
| 1.2. Relationship to the HTTP/2 Transport | |
| This transport mirrors the gRPC wire protocol [GRPC-HTTP2] at the | |
| call level: method names, metadata, leading and trailing metadata, | |
| status, and the unary and streaming call types all retain their gRPC | |
| meaning. It replaces only the framing layer. Flow control is modelled | |
| on the HTTP/2 WINDOW_UPDATE mechanism [RFC9113], applied at the | |
| message level rather than the octet level (Section 8). | |
| 2. Conventions and Terminology | |
| Vert.x gRPC Expires 21 January 2027 [Page 3] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| 2.1. Requirements Language | |
| The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", | |
| "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and | |
| "OPTIONAL" in this document are to be interpreted as described in BCP | |
| 14 [RFC2119] [RFC8174] when, and only when, they appear in all | |
| capitals, as shown here. | |
| 2.2. Terminology | |
| Endpoint: a gRPC client or server participating in this transport. | |
| Each endpoint owns exactly one private address. | |
| Service address: the event bus address at which a service is | |
| registered. On a clustered bus it is registered by | |
| every server node that hosts the service. | |
| Private address: a unique event bus address minted by an endpoint at | |
| startup, on which it registers a single long-lived | |
| consumer and through which it multiplexes all of its | |
| streams. | |
| Stream: a single streaming call, identified on a given private | |
| address by a stream_id assigned by the endpoint that consumes | |
| on that address. | |
| Frame: a TransportFrame (Section 6), the unit of exchange after a | |
| streaming call has been established. | |
| Wire format: the encoding of gRPC messages and of the frame envelope; | |
| either PROTOBUF (protobuf binary, the default) or JSON. | |
| 3. Transport Model | |
| The transport splits into two shapes, selected by the method's call | |
| type. A unary method uses a plain event bus request/reply (Section | |
| 4). A server-streaming, client-streaming, or bidirectional method | |
| uses an establishment handshake followed by full-duplex frame | |
| exchange (Section 5). | |
| The call type is known to both peers in advance and MUST NOT be | |
| negotiated on the wire. The ServiceMethod carries the call type(), | |
| filled in by the generated stub on the client and read by the server, | |
| so each side opens the call in the correct shape without guessing. | |
| Vert.x gRPC Expires 21 January 2027 [Page 4] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| There is no negotiation or marker on the wire indicating call type. | |
| 4. Unary Calls | |
| A unary call MUST be a single event bus request() and its reply. The | |
| request carries: | |
| * the method name in the "action" delivery header; | |
| * the wire format in the "grpc-wire-format" delivery header; | |
| * request metadata as delivery headers prefixed with "__header__."; | |
| * the encoded request message as the event bus body. | |
| The reply carries the encoded response message as its body, response | |
| metadata as "__header__." prefixed delivery headers, and trailing | |
| metadata as "__trailer__." prefixed delivery headers. | |
| This is deliberately identical to the shape a Vert.x service proxy | |
| uses, so that a unary call and a service-proxy call remain | |
| interchangeable on the bus. A unary method MUST NOT use the streaming | |
| establishment handshake. | |
| 5. Streaming Calls | |
| 5.1. Stream Establishment | |
| Because the event bus has no notion of a stream, a streaming call is | |
| opened with an HTTP-upgrade-style request/reply carried in delivery | |
| headers, in the same manner as a unary call, with an empty body on | |
| each side. Triggering establishment on the request headers, rather | |
| than on the first message, allows a call that only sends headers, or | |
| one that wishes to receive before it sends, to open. | |
| The client MUST send the opening request() to the service address. | |
| The request headers MUST carry: | |
| * "action", the method; | |
| * "grpc-wire-format", the wire format; | |
| * "grpc-client-address", the client's private address; | |
| Vert.x gRPC Expires 21 January 2027 [Page 5] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| * "grpc-client-stream-id", the stream_id the client has assigned to | |
| this call for its own inbound direction; | |
| * any request metadata, as "__header__." prefixed headers. | |
| The server looks the method up, sets the call up, registers it in its | |
| stream map, and MUST reply with: | |
| * "grpc-server-address", the server's private address; | |
| * "grpc-server-stream-id", the stream_id the server has assigned for | |
| its own inbound direction; | |
| * "grpc-initial-window", the initial flow-control window (Section 8). | |
| The reply is the "go" signal and nothing more. It MUST be sent before | |
| the handler runs and therefore MUST NOT carry response metadata. | |
| Response leading metadata is instead delivered later as a Headers | |
| frame (Section 6.2) ahead of the first response message, so that it | |
| reflects whatever the handler set. | |
| 5.2. Private Addresses and Multiplexing | |
| Every endpoint MUST mint exactly one private address at startup, | |
| register a single long-lived consumer on it, and multiplex all of its | |
| streams through that one consumer. Each frame carries the destination | |
| endpoint's stream_id, and the receiver demultiplexes it back to the | |
| correct call through a map keyed by stream_id. | |
| Each endpoint assigns the stream_ids for its own inbound direction, | |
| so two endpoints sharing a private-address relationship can never | |
| choose colliding ids. The establishment handshake is where the two | |
| peers exchange the ids they have each chosen. | |
| Because the consumer is long-lived and per-endpoint, opening or | |
| closing a stream is a map insertion or removal only. There is no per- | |
| call consumer registration or unregistration, which on a cluster | |
| would otherwise broadcast registration churn to every node. | |
| 5.3. Full-Duplex Frame Exchange | |
| Once established, the call is full duplex. Every subsequent unit of | |
| the call MUST be a TransportFrame (Section 6) sent to the peer's | |
| Vert.x gRPC Expires 21 January 2027 [Page 6] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| private address with fire-and-forget send(). The first request | |
| message follows as an ordinary Message frame, identical to every | |
| later message. Both directions number their messages independently. | |
| The exchange for a bidirectional call proceeds as follows. | |
| Client Server | |
| | | | |
| | request: action, grpc-wire-format, grpc-client-address, | | |
| | grpc-client-stream-id | | |
| |----------------------------------------------------------->| | |
| | (type is streaming; assign | | |
| | grpc-server-stream-id; register | | |
| | in the stream map) | | |
| | reply: grpc-server-address, grpc-server-stream-id, | | |
| | grpc-initial-window | | |
| |<-----------------------------------------------------------| | |
| | | | |
| | ===== full duplex from here; TransportFrames ===== | | |
| | | | |
| | WindowUpdate (grant the server its window) | | |
| |----------------------------------------------------------->| | |
| | Message (stream_sequence 1, first request message) | | |
| |----------------------------------------------------------->| | |
| | Headers (response leading metadata) | | |
| |<-----------------------------------------------------------| | |
| | Message (stream_sequence 1) | | |
| |<-----------------------------------------------------------| | |
| | Message (stream_sequence 2) | | |
| |----------------------------------------------------------->| | |
| | HalfClose (client done sending) | | |
| |----------------------------------------------------------->| | |
| | Message (stream_sequence 2) | | |
| |<-----------------------------------------------------------| | |
| | Trailers (status) | | |
| |<-----------------------------------------------------------| | |
| | (both ends drop the stream; the call is done) | | |
| | | | |
| 5.4. Ordering Guarantees | |
| The handshake is careful about ordering. A peer MUST NOT begin | |
| sending frames until the receiving consumer is actually registered; | |
| the establishing side therefore waits for that registration to | |
| complete first. In addition, the server's send window MUST start at | |
| Vert.x gRPC Expires 21 January 2027 [Page 7] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| zero, so the server cannot send a response before the client has | |
| registered its consumer and granted a window. | |
| Together these two rules close a race in which, on a local event bus, | |
| the whole round trip could otherwise run synchronously before the | |
| application has attached its response handler. | |
| 5.5. Stream Termination | |
| A stream is removed from the map when it ends by Trailers or by | |
| Cancel. A stream is also given up locally when a frame to the peer | |
| cannot be delivered, or when its idle timeout elapses (Section 9), so | |
| that a peer that has left the cluster does not leak the registration. | |
| A frame that arrives for a stream_id no longer present in the map | |
| (one still in flight when the stream was torn down) MUST be dropped. | |
| When an endpoint is closed, it MUST terminate its remaining streams | |
| rather than dropping them silently: each remaining stream is sent a | |
| Cancel frame so the peer is notified, and is then removed from the | |
| map. | |
| 5.6. Behaviour on a Clustered Event Bus | |
| On a clustered bus, the service address is registered by every server | |
| node, so the opening request() round-robins and lands on one node. | |
| The private address in that node's reply is unique to it, so every | |
| subsequent frame for the stream pins to that node. Because the | |
| consumer is long-lived and per-endpoint, and because ids are assigned | |
| per inbound direction, no per-call registration is broadcast to the | |
| cluster. A future stream timeout would likewise be a plain map | |
| removal. | |
| 6. Transport Frames | |
| 6.1. Frame Envelope | |
| Every unit of a streaming call after establishment is a | |
| TransportFrame. The envelope MUST be encoded in the call's wire | |
| format: protobuf binary by default, or JSON when the call runs in | |
| JSON mode. The envelope travels as the event bus body, and the format | |
| is named in the frame's "grpc-wire-format" delivery header so the | |
| Vert.x gRPC Expires 21 January 2027 [Page 8] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| receiver knows how to read it. | |
| A frame consists of a small header, stream_id and stream_sequence, | |
| plus exactly one variant. The stream_sequence is per-stream, | |
| monotonic, and MUST advance on Message frames only. | |
| 6.2. Frame Variants | |
| The variant is exactly one of the following. | |
| Message: a message payload, in either direction. | |
| WindowUpdate: a flow-control credit, in either direction (Section 8). | |
| HalfClose: sent by the client to end the request stream. | |
| Headers: sent by the server, response leading metadata, ordered ahead | |
| of the first response message. | |
| Trailers: sent by the server to terminate the call; also carries the | |
| gRPC status, since the response stream MUST end with one. | |
| Cancel: sent by either side for abnormal termination. | |
| Heartbeat: sent by either side as a liveness signal on an otherwise | |
| idle stream (Section 9). | |
| The Headers and Trailers frames are thin: the metadata itself rides | |
| as "__header__." and "__trailer__." prefixed delivery headers, and | |
| these frames only mark where in the stream that metadata belongs. | |
| 6.3. Message Encoding | |
| The transport MUST NOT re-encode messages. The gRPC encoder has | |
| already produced the message bytes, so Message.payload carries them | |
| verbatim and the receiver hands them straight back to the decoder; | |
| there is no second codec in the middle. | |
| The envelope, however, follows the call's wire format. In protobuf | |
| mode it is binary. In JSON mode the frame is a JSON object, so its | |
| shape is readable on the bus (useful with an event bus interceptor, | |
| for example to observe frames or drop one to test how a call reacts). | |
| Within a JSON frame the payload remains the opaque message bytes, | |
| carried as base64 rather than re-encoded. | |
| Vert.x gRPC Expires 21 January 2027 [Page 9] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| 6.4. Frame Schema | |
| The normative schema is as follows. | |
| syntax = "proto3"; | |
| package io.vertx.grpc.eventbus.transport.v1alpha; | |
| option java_package = | |
| "io.vertx.grpc.eventbus.transport.v1alpha"; | |
| option java_multiple_files = true; | |
| message TransportFrame { | |
| // Destination endpoint's id for this call; demuxes on the | |
| // shared address. | |
| uint64 stream_id = 1; | |
| // Per-stream, monotonic; advances on Message frames only. | |
| uint64 stream_sequence = 2; | |
| oneof frame { | |
| Message message = 3; // payload, either direction | |
| WindowUpdate window_update = 4; // flow-control credit | |
| HalfClose half_close = 5; // client->server, req. end | |
| Trailers trailers = 6; // server->client, ends call | |
| Cancel cancel = 7; // either dir., abnormal end | |
| Headers headers = 8; // server->client, resp. meta | |
| Heartbeat heartbeat = 9; // either dir., liveness | |
| } | |
| } | |
| // Server to client, response initial metadata, ordered ahead of | |
| // the first response message. The metadata itself rides as | |
| // "__header__." prefixed delivery headers. | |
| message Headers { | |
| } | |
| // A message payload, either direction. The serialized message in | |
| // the call's wire format, carried verbatim. | |
| message Message { | |
| bytes payload = 1; | |
| } | |
| // Flow-control credit, either direction: grants the peer "delta" | |
| // more messages to send. Counted in messages, after HTTP/2's | |
| Vert.x gRPC Expires 21 January 2027 [Page 10] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| // WINDOW_UPDATE. | |
| message WindowUpdate { | |
| uint32 delta = 1; | |
| } | |
| // Client to server, end of the request stream (half close). | |
| message HalfClose { | |
| } | |
| // Server to client, terminates the call. Trailing metadata rides | |
| // as "__trailer__." prefixed delivery headers. | |
| message Trailers { | |
| uint32 status = 1; // gRPC status code | |
| string status_message = 2; | |
| } | |
| // Either direction, abnormal termination. | |
| message Cancel { | |
| uint32 status = 1; // typically CANCELLED or DEADLINE_EXCEEDED | |
| string reason = 2; | |
| } | |
| // Either direction, a liveness signal keeping an otherwise idle | |
| // stream alive so the peer's idle timeout does not fire. Carries | |
| // no payload. | |
| message Heartbeat { | |
| } | |
| 7. Delivery Header Fields | |
| gRPC semantics that the encoder and decoder do not own ride as event | |
| bus delivery headers, mapped the same way for unary and streaming | |
| calls. The frame schema of Section 6 carries only what streaming | |
| genuinely adds on top. The delivery header fields are: | |
| action: the method name. | |
| grpc-wire-format: the wire format of the body (PROTOBUF or JSON). | |
| grpc-client-address: the client's private address (establishment). | |
| grpc-client-stream-id: the client's inbound stream_id | |
| (establishment). | |
| grpc-server-address: the server's private address (establishment). | |
| Vert.x gRPC Expires 21 January 2027 [Page 11] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| grpc-server-stream-id: the server's inbound stream_id | |
| (establishment). | |
| grpc-initial-window: the initial flow-control window (establishment). | |
| __header__.<name>: a request or response metadata entry. | |
| __trailer__.<name>: a trailing metadata entry. | |
| 8. Flow Control | |
| The event bus send() primitive is fire-and-forget: it returns | |
| immediately whether or not the peer is keeping up, so there is no | |
| built-in backpressure. This transport therefore carries a window of | |
| its own, counted in messages rather than octets, modelled on HTTP/2's | |
| WINDOW_UPDATE [RFC9113], Section 6.9. | |
| Each side starts with the window its peer granted during | |
| establishment and MUST spend one credit per Message frame it sends, | |
| stopping at zero. As the receiving application consumes messages, the | |
| receiver sends a WindowUpdate frame carrying a delta, and the sender | |
| adds that delta to its window. | |
| This is expressed through the Vert.x WriteStream contract. A zero | |
| window makes writeQueueFull() return true, and an arriving | |
| WindowUpdate fires the drainHandler, so a generated Pipe or any well- | |
| behaved producer behaves as it does over HTTP/2. | |
| A producer that ignores writeQueueFull(), such as a tight loop of | |
| response.write(...), MUST nonetheless remain safe. The stream buffers | |
| the excess messages once the window is spent and holds back the | |
| terminating frame until they drain, so that nothing is lost or | |
| reordered. | |
| On a local event bus this provides real backpressure, since a paused | |
| consumer would otherwise buffer and eventually drop. On a clustered | |
| event bus the window is the only mechanism pushing back at all, | |
| because a send() is already on the wire the moment it is issued. | |
| 9. Liveness | |
| HTTP/2 relies on TCP to notice a dead peer. The event bus has no such | |
| connection, so this transport detects an unreachable peer in two ways | |
| Vert.x gRPC Expires 21 January 2027 [Page 12] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| and, in either case, gives the stream up rather than retrying, since | |
| a retry would race the teardown. | |
| 9.1. Active Detection (Delivery Failure) | |
| Every frame is sent to the peer's private address. On a point-to- | |
| point send(), a missing consumer fails the write with NO_HANDLERS. | |
| When a frame cannot be delivered, the sending side MUST give the | |
| stream up: it fails the read side and any pending writes so the | |
| application is notified, and unbinds the stream from its map so | |
| nothing leaks. | |
| 9.2. Passive Detection (Idle Timeout) | |
| To cover a stream on which the peer stops sending without a delivery | |
| failure (for instance a node that vanished while this side was only | |
| receiving), each side that consumes a stream arms an idle timer and | |
| resets it on every received frame. If nothing arrives within the idle | |
| timeout, that side MUST give the stream up in the same way. | |
| 9.3. Heartbeats | |
| To keep a genuinely idle but healthy stream from tripping the idle | |
| timer, the producing side sends Heartbeat frames while its outbound | |
| half is open. A heartbeat is itself a send(), so it doubles as the | |
| producer's own liveness check: if the consumer is gone, the heartbeat | |
| fails to deliver and the producer gives up. | |
| Heartbeats follow the call direction, so a heartbeat flows only where | |
| messages would. On a server-streaming call the server heartbeats and | |
| the client times out; on a client-streaming call the client | |
| heartbeats and the server times out; a bidirectional call does both; | |
| a unary call uses neither, as it is a plain request/reply. Both | |
| timers are off by default and are enabled per endpoint (Section 10). | |
| The idle timeout SHOULD be a small multiple of the peer's heartbeat | |
| interval, so that an occasional late heartbeat does not fail a live | |
| stream. | |
| 9.4. Multiplexing and Head-of-Line Blocking | |
| Because every stream on an endpoint shares one consumer, that | |
| consumer MUST NOT be paused: pausing it would stall every stream | |
| Vert.x gRPC Expires 21 January 2027 [Page 13] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| behind a slow one, which is head-of-line blocking. The per-stream | |
| window is therefore the only backpressure available. A slow reader | |
| pushes back only by withholding WindowUpdate credit on its own | |
| stream, while the shared consumer keeps draining the others. | |
| 10. Configuration | |
| Both endpoints take an options object. All options are OPTIONAL and | |
| default to the values below. | |
| EventBusGrpcServer.server(vertx, | |
| new EventBusGrpcServerOptions() | |
| .setSupportedWireFormats(EnumSet.of(WireFormat.PROTOBUF)) | |
| .setHeartbeatInterval(30_000) | |
| .setIdleTimeout(90_000)); | |
| EventBusGrpcClient.client(vertx, | |
| new EventBusGrpcClientOptions() | |
| .setWireFormat(WireFormat.JSON) | |
| .setHeartbeatInterval(30_000) | |
| .setIdleTimeout(90_000)); | |
| maxConcurrentStreams (server, default 1000): caps the number of | |
| streams one server multiplexes at once. An open | |
| beyond the cap MUST be rejected with | |
| RESOURCE_EXHAUSTED, so that a flood of opens | |
| cannot grow the demux map without bound. | |
| supportedWireFormats (server, default [PROTOBUF, JSON]): limits which | |
| wire formats the server accepts. A request in an | |
| unsupported format MUST be rejected with | |
| UNIMPLEMENTED. | |
| wireFormat (client, default PROTOBUF): the default wire format for | |
| the client's requests. A call MAY override it with | |
| request.format(...), and the generated stubs' | |
| create(client, WireFormat.JSON) does exactly that. The | |
| override wins. | |
| heartbeatInterval (client and server, milliseconds, default 0, | |
| disabled): how often the endpoint sends Heartbeat | |
| frames on streams where it produces messages | |
| (Section 9). | |
| idleTimeout (client and server, milliseconds, default 0, disabled): | |
| Vert.x gRPC Expires 21 January 2027 [Page 14] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| how long the endpoint waits without receiving any frame | |
| on a stream where it consumes messages before giving the | |
| stream up. It SHOULD be a small multiple of the peer's | |
| heartbeatInterval. | |
| 11. Security Considerations | |
| This transport inherits the security properties of the underlying | |
| Vert.x event bus and adds no message-level authentication, integrity, | |
| or confidentiality of its own. Deployments that carry sensitive data, | |
| in particular clustered deployments whose bus traverses a network, | |
| SHOULD secure the event bus itself (for example with TLS and cluster | |
| authentication). | |
| An endpoint's private address functions as a capability. Any party | |
| able to send to that address, and able to guess or observe a live | |
| stream_id on it, can inject Message, Cancel, or other frames into a | |
| call, or forge a WindowUpdate. A frame for an unknown stream_id is | |
| dropped, which limits blind injection, but this is not a substitute | |
| for bus-level access control. Private addresses SHOULD be treated as | |
| secrets and MUST NOT be exposed to untrusted parties. | |
| In JSON mode the frame envelope is human-readable on the bus, which | |
| is convenient for interceptor-based debugging but widens the exposure | |
| of call structure to anyone with bus visibility. Operators SHOULD | |
| weigh this when choosing a wire format for sensitive traffic. | |
| Several resource-exhaustion vectors are bounded by design: | |
| maxConcurrentStreams caps the demux map and rejects excess opens with | |
| RESOURCE_EXHAUSTED; the per-stream flow-control window bounds | |
| receiver buffering. Note, however, that a local producer which | |
| ignores writeQueueFull() buffers excess messages on its own side | |
| (Section 8); an application that does this with untrusted input can | |
| exhaust its own memory. Misconfigured liveness timers (an idleTimeout | |
| that is not a sufficient multiple of the peer's heartbeatInterval) | |
| can cause healthy streams to be torn down, a self-inflicted denial of | |
| service. | |
| 12. IANA Considerations | |
| This document has no IANA actions. The delivery header field names | |
| defined in Section 7 are namespaced within the Vert.x event bus and | |
| are not registered in any IANA registry. Were this transport to be | |
| standardized, coordination of those field names would be revisited at | |
| Vert.x gRPC Expires 21 January 2027 [Page 15] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| that time. | |
| 13. Open Questions and Future Work | |
| This is an experiment, and several pieces are deliberately out of | |
| scope. The directions they would take are noted here. | |
| Session identity and resumption. Streams are already multiplexed over | |
| one long-lived private consumer per | |
| endpoint and told apart by | |
| stream_id, so there is no per-call | |
| registration churn. The private | |
| address doubles as the endpoint's | |
| session token; it is minted per | |
| process and dies with it, so a | |
| restarted endpoint has a new address | |
| and stale frames hit a dead one. A | |
| distinct, stable session_id would be | |
| needed only to tie streams across a | |
| reconnect, or for session-level flow | |
| control, and is deferred until then. | |
| Session-level flow control. HTTP/2 has a second, connection-wide | |
| window above the per-stream one, capping | |
| total buffering across a connection's | |
| streams. The analog would be a session- | |
| wide window across all streams sharing an | |
| endpoint's private address, paired with | |
| the session_id above. | |
| Resumption. The stream_sequence on each Message leaves room for a | |
| dropped client to reconnect and ask the server to replay | |
| everything after the last sequence it saw, in the spirit | |
| of the Last-Event-ID resumption of [MCP]. The reconnect | |
| handshake and a bounded replay buffer are the missing | |
| pieces. Surviving a node failure, rather than just a | |
| dropped connection, would additionally require the | |
| session state in a shared or durable store, which could | |
| be an SPI with a local default and, for example, a Redis | |
| backend. | |
| 14. References | |
| Vert.x gRPC Expires 21 January 2027 [Page 16] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| 14.1. Normative References | |
| [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate | |
| Requirement Levels", BCP 14, RFC 2119, | |
| DOI 10.17487/RFC2119, March 1997, | |
| <https://www.rfc-editor.org/info/rfc2119>. | |
| [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC | |
| 2119 Key Words", BCP 14, RFC 8174, | |
| DOI 10.17487/RFC8174, May 2017, | |
| <https://www.rfc-editor.org/info/rfc8174>. | |
| 14.2. Informative References | |
| [RFC7540] Belshe, M., Peon, R., and M. Thomson, Ed., "Hypertext | |
| Transfer Protocol Version 2 (HTTP/2)", RFC 7540, | |
| DOI 10.17487/RFC7540, May 2015, | |
| <https://www.rfc-editor.org/info/rfc7540>. Obsoleted | |
| by RFC 9113; sections 5.2 and 6.9 are the model for the | |
| message-counted window used here. | |
| [RFC9113] Thomson, M., Ed. and C. Benfield, Ed., "HTTP/2", | |
| RFC 9113, DOI 10.17487/RFC9113, June 2022, | |
| <https://www.rfc-editor.org/info/rfc9113>. | |
| [GRPC-HTTP2] | |
| "gRPC over HTTP2", the gRPC wire protocol this design | |
| mirrors at the call level, | |
| <https://github.com/grpc/grpc/blob/master/doc/ | |
| PROTOCOL-HTTP2.md>. | |
| [MCP] "Model Context Protocol, Streamable HTTP transport", the | |
| source of the session and Last-Event-ID resumption ideas | |
| in Section 13, <https://modelcontextprotocol.io/ | |
| specification>. | |
| [REACTIVE-STREAMS] | |
| "Reactive Streams", the demand-signalling model | |
| (request(n)) that Vert.x's ReadStream.fetch and this | |
| window scheme both follow, | |
| <https://www.reactive-streams.org/>. | |
| [RSOCKET] "RSocket Protocol", a message-oriented protocol whose | |
| REQUEST_N frame is close prior art for message-counted | |
| flow control, <https://rsocket.io/about/protocol>. | |
| Vert.x gRPC Expires 21 January 2027 [Page 17] | |
| Internet-Draft gRPC over the Vert.x Event Bus July 2026 | |
| Author's Address | |
| Eclipse Vert.x gRPC Contributors (editor) | |
| Eclipse Foundation | |
| Email: [email protected] | |
| Vert.x gRPC Expires 21 January 2027 [Page 18] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment