Status: draft / decision deferred. The transport contract is
shipped and stable (see runtime/pb/grpc.lua).
What's open is which concrete transports we recommend and/or ship,
and how a user picks between them.
This spec maps the protocol landscape, says where Tarantool fits, and flags what we'd build vs. recommend an external library for.
Generated M.<Service>_client(transport) and M.<Service>_server(impl)
talk to the transport in a single contract, regardless of wire protocol:
transport:unary(path, req_bytes, ctx) -> resp_bytes
transport:server_stream(path, req_bytes, ctx) -> stream
transport:client_stream(path, ctx) -> stream
transport:bidi(path, ctx) -> stream
path is /pkg.Service/Method. ctx is an opaque Lua table (headers,
deadline, metadata, …). Generated code encodes the request, hands raw
bytes to the transport, and decodes the response.
This is deliberately HTTP/2-shaped — path, byte-oriented messages,
streams — but the contract makes no commitment to a wire protocol.
Every transport in this spec is a different plug-in behind the same
four methods.
Reference transports already in runtime/pb/grpc.lua:
pb.grpc.loopback(server) — in-process; uses fiber.channel. Bridges
client → server fiber for tests and same-process apps.pb.grpc.multiplex({srv1, srv2}) — fans several servers onto one
transport. Errors on duplicate paths.For a request that crosses a process boundary, you pick a pair: a wire protocol (how bytes flow between processes) and a codec (how a message turns into bytes). The Lua-side transport bridges between the generated code and that wire/codec combo.
| Wire protocol | Body codec | Streaming | Status signaling | Browser-friendly | Off-the-shelf clients/servers | Tarantool fit |
|---|---|---|---|---|---|---|
| gRPC over HTTP/2 | proto wire | unary + all 3 | HTTP/2 trailers | no | every gRPC lib | needs an external HTTP/2 lib |
| gRPC-Web over HTTP/2 | proto wire | unary + server | trailers in body | yes (with proxy) | grpc-web JS, Envoy | same problem as gRPC + framing |
| gRPC-Web over HTTP/1.1 | proto wire | unary + server | trailers in body | yes | grpc-web JS | works with tarantool/http |
| Connect over HTTP/1.1 | proto wire | unary only | HTTP status + body | yes | connectrpc clients | works with tarantool/http |
| Connect over HTTP/2 | proto wire | unary + all 3 | HTTP status / trailers | yes | connectrpc clients | same HTTP/2 problem |
| Connect-JSON | proto3 JSON | unary only | HTTP status + body | yes | curl + connectrpc clients | drop-in for tarantool/http |
| gRPC-Gateway / transcoded REST + JSON | proto3 JSON | unary | HTTP status + body | yes | any HTTP client | drop-in for tarantool/http |
| gRPC over IProto tunnel (Tarantool-native) | proto wire | unary + all 3 | IProto error code | n/a | this project, custom clients | first-class |
| gRPC over net.box tunnel | proto wire | unary + all 3 | net.box error | n/a | this project, custom clients | first-class |
What's not in the matrix and why:
Tarantool gives us:
tarantool/http server (HTTP/1.1) — solid, idiomatic, lives in a
Lua rock. No HTTP/2, no server-pushed trailers. Good substrate for
Connect-JSON and gRPC-Gateway-style REST.http_client (libcurl-based) — HTTP/1.1 and HTTP/2 client. Has
streaming via callbacks, but trailers and gRPC framing are not
first-class. Usable for HTTP/2 unary; awkward for streaming.net.box — Tarantool's binary RPC protocol. Already gives us
request/response, streaming via long-poll, error propagation. Sane
default for in-cluster Tarantool→Tarantool calls.So the practical bands are:
tarantool/http. Both are HTTP/1.1
only, both speak JSON, both work with curl/browsers without a
proxy.http_client for HTTP/2 unary. For streaming, accept "we don't
support that yet" rather than shipping a half-baked HTTP/2 client.Names below refer to packages we'd publish; nothing here lives in this repo yet beyond the contract.
pb.grpc.transport.http_server (HTTP/1.1 server-side)Adapts an M.<Service>_server(impl) result into a tarantool/http
route handler. Wire protocol: Connect-JSON over HTTP/1.1 by default,
with content-negotiation for Connect-protobuf.
/{package.Service}/{Method} with Content-Type: application/json
→ decode body via pb.json.decode(input_desc, body), call the
generated handler, encode reply via pb.json.encode.application/proto content type → use pb.encode/pb.decode instead.application/connect+json
framed streaming over HTTP/1.1 chunked transfer is feasible later;
out of scope for v1.Why this first: it's the lowest-effort transport that gives us a real
external interface, and it works with browsers and curl. It also
covers the gRPC-Gateway use case without needing the gateway:
POST /myapp.v1.Greeter/SayHello with a JSON body is a fine REST
shape on its own.
pb.grpc.transport.netbox (in-cluster)net.box connection → speaks pb.grpc over a single user-defined
function (e.g. box.schema.func.create('grpc_dispatch')). Body is
a 2-tuple {path, req_bytes}; reply is {ok, resp_bytes} or
{err, status_code, message}.
Streaming: lean on net.box's stream/iterator support. Server runs the
handler on a fiber; messages flow through box.iproto.override /
box.session.push. Concretely tractable; out of scope for v1 but
straightforward to add.
Why second: in-cluster Tarantool clusters are a real and ready use case. The transport is small and self-contained.
pb.grpc.transport.http_client_unary (outbound, optional)Adapts http_client to speak Connect-JSON or Connect-protobuf to
external services. Unary only. Easy. Useful for calling out from
Tarantool app code to a Connect or HTTP/1.1 gRPC-Web server.
http_client's streaming
API isn't a clean fit for gRPC trailers and per-message framing.
Anyone needing this should bind to a real gRPC client (C, Go), not
reimplement in Lua. Document this limitation.Decision tree, top-down:
pb.grpc.transport.netbox.pb.grpc.transport.http_server (Connect-JSON).pb.grpc.transport.http_client_unary (Connect or HTTP/1.1
gateway). Streaming: not supported; document the Envoy/sidecar
alternative.Validate against connectrpc/conformance.
Operationally identical to the protobuf conformance suite already
running here (docker/conformance.Dockerfile, cmd/conformance-runner.lua):
ClientCompatRequest
/ writes ClientCompatResponse on stdin/stdout.--mode server (our impl is the server; Connect's
reference client drives it — fits pb.grpc.transport.http_server
validation) and --mode client (our impl is the client — fits
pb.grpc.transport.http_client_unary validation).test/grpc_conformance/known_failures.txt
(server mode) and ..._client.txt (client mode) mirroring the
proto suite's pattern at test/conformance/known_failures.txt.Canonical gRPC interop tests
(empty_unary, large_unary, ping_pong, …) are pre-Connect,
HTTP/2-only, and use a client-and-server-binary model rather than a
framed pipe. Skip them: less useful while we don't terminate HTTP/2,
and operationally distant from what we already run.
We adopt gRPC's canonical status codes (12 of them) as the cross-wire status type. Every transport translates to and from its native error representation:
| gRPC status | HTTP (Connect) | net.box / IProto error |
|---|---|---|
OK |
200 | success |
CANCELLED |
499 | ER_CANCELLED |
INVALID_ARGUMENT |
400 | ER_PROC_LUA (categorized) |
DEADLINE_EXCEEDED |
504 | ER_TIMEOUT |
NOT_FOUND |
404 | ER_NO_SUCH_PROC |
ALREADY_EXISTS |
409 | ER_TUPLE_FOUND |
PERMISSION_DENIED |
403 | ER_ACCESS_DENIED |
RESOURCE_EXHAUSTED |
429 | ER_MEMORY_ISSUE (etc.) |
FAILED_PRECONDITION |
400 | ER_* |
INTERNAL |
500 | ER_PROC_LUA |
UNAVAILABLE |
503 | ER_NO_CONNECTION |
UNAUTHENTICATED |
401 | ER_LOGIN_REQUIRED |
The mapping table belongs in runtime/pb/grpc.lua. Each transport
references it.
The ctx argument in the transport contract carries metadata between
caller and transport. We standardize three keys:
ctx.deadline — fiber-clock timestamp (seconds, double). Transport
enforces by cancelling on overrun.ctx.headers — flat {string -> string} map. Wire-side translation
is transport-specific (HTTP headers, IProto headers, …).ctx.trace_id, ctx.span_id — optional tracing hooks. Transports
inject/extract per W3C traceparent for HTTP, custom IProto field
for net.box.Per-call overrides go in ctx.options (e.g. retry policy). User code
shouldn't put anything else in ctx; we may add more standard keys.
runtime/pb/grpc.lua already exists; gains status-code
+ ctx-key constants
runtime/pb/grpc/http_server.lua new — Connect-style HTTP/1.1 server
runtime/pb/grpc/netbox.lua new — net.box tunnel (in-cluster)
runtime/pb/grpc/http_client.lua new — outbound, unary only
docs/grpc-howto.md new — user-facing recipes
Tests follow the existing pattern: each transport plugs into the loopback's test harness by replacing the in-process transport with the networked one, asserting end-to-end round-trip equality.
box.iproto.override
carry a dedicated IPROTO_GRPC request type so net.box transport
doesn't sit on top of func_call? Probably eventually; not now.Authorization headers, net.box uses
Tarantool users.IPROTO_GRPC is its own request type.When this spec gets picked back up, the load-bearing calls are:
tarantool/http as-is, browsers can call it
without a proxy, and gRPC-Gateway folks have a clean migration.
Revisit if a user has a hard dependency on grpc-web or REST shapes
that don't match Connect's URL convention.IPROTO_GRPC type later. Once net.box is in real
use, we'll know what's missing.