A gRPC server over non-shaded Netty, from a service value and plain functions.
(-> (server {:services [{:service greeter/Greeter
:handlers {:say-hello (fn [req] ...)}}]
:port 8080})
start)
Handler shapes, by method type — requests and responses are protobuf Messages; compose with the generated proto->X / X->proto at the edges:
:unary (fn [request] response) :server-streaming (fn [request send!]) — call send! per message, return to complete :client-streaming (fn [respond!]) -> {:on-next (fn [msg]) ... :on-complete (fn [])} — call respond! once with the response, usually from :on-complete :bidi (fn [send! close!]) -> {:on-next ... :on-complete ... :on-ready ...} — send! per message, close! to finish
Outbound flow control, for the streaming-out shapes. send! returns whether
the transport wants more: false means grpc-java is now buffering this call's
messages in memory because the client is not reading them fast enough.
.onNext never blocks and never refuses, so a producer faster than its
consumer will otherwise grow that buffer without bound — a channel or queue
in front of it bounds the producer, not the wire. Most handlers can ignore
the value: a reply per request cannot outrun anything. A handler that
generates messages on its own schedule should not.
To wait rather than poll, a :bidi handler declares :on-ready in the map it
returns; grpc calls it when a full call has drained. THE HANDLER'S OWN
THREAD MUST NOT WAIT FOR IT. grpc serializes every callback for a call —
:on-next, :on-ready, :on-complete — so a thread that is running one of them
and blocks for another deadlocks the call. The shape that works is a
producer on its own thread (a virtual thread is the cheap way), parked on
something :on-ready delivers to; :on-ready itself only hands over the
signal. examples/src/example/echo/async.clj is that, over core.async.
:server-streaming has the return value but no :on-ready, and the same rule is why: its handler IS the callback, so nothing could deliver the signal while it waited. A server-streaming handler that must respect backpressure belongs in the :bidi shape.
A thrown exception in any handler becomes Status/INTERNAL with the message attached; throw an io.grpc.StatusRuntimeException to control the status.
opts: :services [{:service Service :handlers {kebab-key fn}} ...] :address SocketAddress | port | {:unix path} | "unix:///path" :port used when :address is absent; default $PORT, else 8080 — the Knative convention :transport :auto (default) | :epoll | :nio — UDS requires epoll :health true (default) — grpc health service, wired for probes :reflection false (default) — server reflection (v1) :executor java.util.concurrent.Executor for handlers, or :direct to run them ON the Netty event loop — measured ~29% off unary latency, and a sharp edge: a handler that blocks on a direct executor stalls the transport for every connection sharing that loop. Opt in only for handlers that provably never block. :interceptors [io.grpc.ServerInterceptor ...] :permit-keepalive {:time-ms n :without-calls bool} — the pings this server ACCEPTS. gRPC's default permit is 5 minutes and calls-only; a client pinging faster gets GOAWAY too_many_pings, so a server whose clients keep connections warm (Knative, LBs) must lower this to match. clj-grpc.knative pairs the two. :max-inbound-message-size bytes :worker-threads n — Netty event loops for the connections; default 0 = Netty's 2 × cores, which assumes the loops run the handlers. Under the virtual-thread default they only do I/O and compete with the carriers: measured on a 4-core host, 2 loops instead of 8 were +14–21% streamed messages per second for an eight-connection server, nil for one connection. About half the cores is a good value for a virtual-thread server; leave the default for :direct. :initial-flow-control-window bytes — where grpc-netty's HTTP/2 window starts (its default 1 MiB; BDP auto-tuning stays on). :inbound-credits n — for :client-streaming and :bidi handlers, ask the transport for n messages at a time instead of grpc-java's one per delivered message. Each request is a hop from the handler's thread back to the event loop; measured on a 4-core host, batching them is +27–40% streamed messages per second on the virtual-thread executor at −23–35% CPU per message, and the batch size does not matter above a handful (8, 32 and 128 measure the same). Backpressure is unchanged in kind — the server still bounds what it has asked for — but up to n messages may be in flight to a handler at once, so a handler that must see exactly one at a time keeps the default. Default: nil (grpc-java's auto-request). :tls {:cert-chain File/path :private-key File/path}; absent means h2c (plaintext HTTP/2), which is what Knative speaks
Handlers run on virtual threads by default (:executor overrides): Clojure handlers block — that is the model — and grpc's default shared pool is sized for handlers that never do.
A gRPC server over non-shaded Netty, from a service value and plain
functions.
(-> (server {:services [{:service greeter/Greeter
:handlers {:say-hello (fn [req] ...)}}]
:port 8080})
start)
Handler shapes, by method type — requests and responses are protobuf
Messages; compose with the generated proto->X / X->proto at the edges:
:unary (fn [request] response)
:server-streaming (fn [request send!]) — call send! per message, return to
complete
:client-streaming (fn [respond!]) -> {:on-next (fn [msg]) ...
:on-complete (fn [])}
— call respond! once with the response, usually from
:on-complete
:bidi (fn [send! close!]) -> {:on-next ... :on-complete ...
:on-ready ...}
— send! per message, close! to finish
Outbound flow control, for the streaming-out shapes. `send!` returns whether
the transport wants more: false means grpc-java is now buffering this call's
messages in memory because the client is not reading them fast enough.
`.onNext` never blocks and never refuses, so a producer faster than its
consumer will otherwise grow that buffer without bound — a channel or queue
in front of it bounds the producer, not the wire. Most handlers can ignore
the value: a reply per request cannot outrun anything. A handler that
generates messages on its own schedule should not.
To wait rather than poll, a :bidi handler declares :on-ready in the map it
returns; grpc calls it when a full call has drained. THE HANDLER'S OWN
THREAD MUST NOT WAIT FOR IT. grpc serializes every callback for a call —
:on-next, :on-ready, :on-complete — so a thread that is running one of them
and blocks for another deadlocks the call. The shape that works is a
producer on its own thread (a virtual thread is the cheap way), parked on
something :on-ready delivers to; :on-ready itself only hands over the
signal. `examples/src/example/echo/async.clj` is that, over core.async.
:server-streaming has the return value but no :on-ready, and the same rule
is why: its handler IS the callback, so nothing could deliver the signal
while it waited. A server-streaming handler that must respect backpressure
belongs in the :bidi shape.
A thrown exception in any handler becomes Status/INTERNAL with the message
attached; throw an io.grpc.StatusRuntimeException to control the status.
opts:
:services [{:service Service :handlers {kebab-key fn}} ...]
:address SocketAddress | port | {:unix path} | "unix:///path"
:port used when :address is absent; default $PORT, else 8080 —
the Knative convention
:transport :auto (default) | :epoll | :nio — UDS requires epoll
:health true (default) — grpc health service, wired for probes
:reflection false (default) — server reflection (v1)
:executor java.util.concurrent.Executor for handlers, or :direct to
run them ON the Netty event loop — measured ~29% off unary
latency, and a sharp edge: a handler that blocks on a direct
executor stalls the transport for every connection sharing
that loop. Opt in only for handlers that provably never
block.
:interceptors [io.grpc.ServerInterceptor ...]
:permit-keepalive {:time-ms n :without-calls bool} — the pings this server
ACCEPTS. gRPC's default permit is 5 minutes and calls-only;
a client pinging faster gets GOAWAY too_many_pings, so a
server whose clients keep connections warm (Knative, LBs)
must lower this to match. clj-grpc.knative pairs the two.
:max-inbound-message-size bytes
:worker-threads n — Netty event loops for the connections; default 0 =
Netty's 2 × cores, which assumes the loops run the
handlers. Under the virtual-thread default they only do
I/O and compete with the carriers: measured on a 4-core
host, 2 loops instead of 8 were +14–21% streamed messages
per second for an eight-connection server, nil for one
connection. About half the cores is a good value for a
virtual-thread server; leave the default for :direct.
:initial-flow-control-window bytes — where grpc-netty's HTTP/2 window
starts (its default 1 MiB; BDP auto-tuning stays on).
:inbound-credits n — for :client-streaming and :bidi handlers, ask the
transport for n messages at a time instead of grpc-java's
one per delivered message. Each request is a hop from the
handler's thread back to the event loop; measured on a
4-core host, batching them is +27–40% streamed messages per
second on the virtual-thread executor at −23–35% CPU per
message, and the batch size does not matter above a handful
(8, 32 and 128 measure the same). Backpressure is unchanged
in kind — the server still bounds what it has asked for — but
up to n messages may be in flight to a handler at once, so
a handler that must see exactly one at a time keeps the
default. Default: nil (grpc-java's auto-request).
:tls {:cert-chain File/path :private-key File/path}; absent means
h2c (plaintext HTTP/2), which is what Knative speaks
Handlers run on virtual threads by default (:executor overrides): Clojure
handlers block — that is the model — and grpc's default shared pool is sized
for handlers that never do.(server {:keys [services address port transport health reflection executor
interceptors tls permit-keepalive max-inbound-message-size
inbound-credits worker-threads initial-flow-control-window]
:or {health true}})Build (without starting) a server. Returns {:server io.grpc.Server :health HealthStatusManager-or-nil :address SocketAddress}.
Build (without starting) a server. Returns {:server io.grpc.Server
:health HealthStatusManager-or-nil :address SocketAddress}.(service-definition svc)(service-definition {:keys [service handlers]} opts)A dynamic ServerServiceDefinition from a service value and a handlers map.
Methods without a handler are omitted and answer UNIMPLEMENTED, which is
gRPC's own semantics for them. opts: :inbound-credits, as for server.
A dynamic ServerServiceDefinition from a service value and a handlers map. Methods without a handler are omitted and answer UNIMPLEMENTED, which is gRPC's own semantics for them. opts: :inbound-credits, as for `server`.
(shutdown s)(shutdown {:keys [server health owned-executor] :as s} {:keys [grace-ms]})Graceful by default; :grace-ms bounds the drain, then forces. The health service (when present) enters its terminal NOT_SERVING state first, so load balancers stop routing before the listener closes — the drain order Kubernetes rollouts assume.
Graceful by default; :grace-ms bounds the drain, then forces. The health service (when present) enters its terminal NOT_SERVING state first, so load balancers stop routing before the listener closes — the drain order Kubernetes rollouts assume.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |