gRPC for MoonBit.
Moved on mooncakes from Lfan-ke/moonrpc to moonbitstack/moonrpc.
let server = @net.GrpcServer::new()
// unary: one request, one reply.
server.register("/greet.Greeter/SayHello", req => handle(req))
// server-streaming: one request, an ordered run of replies.
server.register_server_streaming("/count.C/Up", (ctx, req) => [
first(req), second(req), third(req),
])
// client-streaming: many requests collected, one reply at half-close.
server.register_client_streaming("/sum.S/Add", (ctx, msgs) => fold(msgs))
// bidi: each request answered as it arrives, a farewell at half-close.
server.register_bidi("/chat.C/Echo", ctx => @moonrpc.BidiHandler::{
on_message: m => [reply_to(m)],
on_end: () => [b"bye"],
})
server.serve() // listens on 12000| Package | What it holds | Depends on |
|---|---|---|
| moonrpc/wire | message framing, Status, Method, metadata rules, keepalive, HTTP/2 defaults | moonhttp, moonvar, moonbase |
| moonrpc/proto | PbWriter/PbReader, Message, the descriptor model and its codec | moonvar |
| moonrpc/stream | the stream state machine, RpcContext, the four handler kinds, Registrar | moonhttp |
| moonrpc/balance | retry, hedging and throttling after gRPC A6, connectivity states, balancers, DNS, the channel pool | wire, moonhttp, moonward, moondate, moonvar |
| moonrpc/client | H2Client, CallReply | wire, moonhttp, moonzip |
| moonrpc/server | H2Server, interceptors | wire, proto, stream, moonhttp, moonzip, moonbase |
| moonrpc/service | health checking, server reflection, and unary/server_streaming/client_streaming over message types | server, stream, proto, wire, moonvar |
| moonrpc/cmd/rpc | the generator behind the #rpc annotations | moonast |
| moonrpc/net | GrpcServer, Channel, MuxChannel, ManagedChannel over TCP (native) | the root, moonbitlang/async |
let chan = @net.Channel::connect("127.0.0.1", @net.port)
// unary.
let reply = chan.unary("/greet.Greeter/SayHello", request)
reply.messages[0] // the reply message; reply.grpc_status is 0 on success
// server-streaming: one request, every framed reply reassembled in order.
let out = chan.server_streaming("/count.C/Up", request) // out.messages
// client-streaming: many requests, one reply.
let sum = chan.client_streaming("/sum.S/Add", [a, b, c])
// a deadline: sent as grpc-timeout and enforced locally — if it elapses the
// stream is reset and grpc_status comes back DEADLINE_EXCEEDED (4).
let bounded = chan.unary("/slow.S/Wait", request, timeout_millis=200)let health = @moonrpc.HealthService::new()
health.set_status("greet.Greeter", Serving)
server.register_health(health) // Check + Watch on /grpc.health.v1.Health/*
engine.add_unary_interceptor((ctx, req, next) => {
// inspect, then proceed — or return without calling next to short-circuit.
next(ctx, req)
})let w = @moonrpc.PbWriter::new()
w.string_(1, "testing") // field 1, a length-delimited string
w.int32(2, -1) // field 2, a varint (negative -> ten octets)
let r = @moonrpc.PbReader::new(w.to_bytes())
let (field, wire) = r.read_tag() // (1, LengthDelim)
r.read_string() // "testing"let greeter : @moonrpc.ServiceDescriptor = {
name: "Greeter",
methods: [
{ name: "SayHello", input_type: ".greet.HelloRequest",
output_type: ".greet.HelloReply",
client_streaming: false, server_streaming: false },
],
}
let refl = @moonrpc.ReflectionService::new()
refl.add_file(@moonrpc.FileDescriptor::new("greet.proto", "greet", services=[greeter]))
server.register_reflection(refl)let engine = @moonrpc.H2Server::new()
engine.register("/echo.Echo/Say", req => req)
let out = engine.feed(frame) // -> [HEADERS(:status 200), DATA(reply), HEADERS(grpc-status:0)]let frame = @moonrpc.encode_message(b"hello grpc")
// [0x00][0x00 0x00 0x00 0x0A]["hello grpc"] — 1-byte flag + 4-byte big-endian length + payload
let (compressed, payload) = @moonrpc.decode_message(frame).unwrap()
// (false, b"hello grpc")
@moonrpc.Status::code(NotFound) // 5
@moonrpc.Status::name(Unauthenticated) // "UNAUTHENTICATED", one of the 17 codes
let m : @moonrpc.Method = { service: "greet.Greeter", name: "SayHello" }
m.path() // "/greet.Greeter/SayHello"let chan = @net.Channel::connect(
"127.0.0.1",
@net.port,
throttling=@moonrpc.Throttling::new(tokens=10, ratio=0.1),
)
let reply = chan.unary(
"/greet.Greeter/SayHello",
b"Ada",
timeout_millis=2000,
retry=@moonrpc.retry,
)| Rule | What happens |
|---|---|
| Backoff | min(base × factor^(n−1), cap) times random(0.8, 1.2); the preset is A6's example, four attempts from 100 ms doubling to 1 s |
| Status | only a status in codes is retried; OK and anything else go back to the caller |
| Committed | once response headers arrive, the call is never retried |
| Pushback | grpc-retry-pushback-ms sets the next wait exactly and restarts the backoff; a negative or unreadable value stops |
| Throttle | a retryable failure takes a token, a success gives back ratio; at half the tokens or fewer nothing is retried or hedged |
| Transparent | a stream the server refused, or one a GOAWAY shows it never handled, goes again at once, without a policy and without counting |
| Limit | at most five attempts whatever a policy asks, the client-side limit A6 sets |
| Metadata | each retry carries grpc-previous-rpc-attempts |
| Deadline | one deadline for the whole call; each attempt's grpc-timeout is what is left of it |
#rpc.message
pub struct HelloRequest {
name : String // 1
#rpc.field(number="4")
tags : Array[String] // 4
nickname : String? // 5, proto3 optional
}
#rpc.service(name="greet.Greeter")
pub struct Greeter { greeting : String }
#rpc.method
fn Greeter::say_hello(self : Greeter, req : HelloRequest) -> HelloReply raise { ... }
#rpc.method
fn Greeter::count_to(self : Greeter, req : Upto) -> Array[Number] { ... }Greeter::{ greeting: "hello" }.register(server) // an H2Server or a GrpcServer
reflection.add_file(descriptor())| MoonBit | protobuf |
|---|---|
| String, Bytes, Bool | string, bytes, bool |
| Int, Int64, UInt, UInt64 | int32, int64, uint32, uint64 |
| Double, Float | double, float |
| a #rpc.message | that message |
| Array[T] | repeated T, packed for the integers and bool |
| T? | optional T |
rule(name: "rpc", command: "moonx moonbitstack/moonrpc/cmd/rpc@0.21.1 -C \"$mod_dir\" \"$input\" -o \"$output\"")dev_build(rule: "rpc", input: "service.mbt", output: "service.g.mbt")
formatter(ignore: [ "service.g.mbt" ])fn decode_health_request(msg : Bytes) -> Bytesfn decode_message(data : Bytes) -> (Bool, Bytes)?let default_max_frame_size : Intlet default_max_streams : Intlet default_window_size : Intfn dns_encode_query(id : Int, name : String, qtype : Int) -> Bytesfn encode_grpc_timeout(millis : Int) -> Bytesfn encode_health_request(service : Bytes) -> Bytesfn encode_message(payload : Bytes, compressed? : Bool) -> Bytesfn is_binary_metadata(name : Bytes) -> Boollet limit : Intlet max_header_list_size : Intlet max_message_size : Intfn message_len(data : BytesView, at? : Int) -> Int?fn metadata_value_from_wire(name : Bytes, value : Bytes) -> Bytesfn metadata_value_to_wire(name : Bytes, value : Bytes) -> Bytesfn parse_grpc_timeout(v : Bytes) -> Int?fn percent_decode(v : Bytes) -> Stringlet reflection_v1_path : Stringlet reflection_v1alpha_path : Stringfn stream_id_valid_for_initiator(id : Int, by_client~ : Bool) -> Boolfn stream_is_client_initiated(id : Int) -> Boolfn stream_is_server_initiated(id : Int) -> Bool