// Graceful shutdown with GOAWAY (RFC 7540 ยง6.8): a draining peer announces the highest
// stream id it will still process, so no new stream races the shutdown, and streams above
// that id were never seen and are safe to re-issue. This completes one call, then has the
// server send GOAWAY(last=1); the client marks the higher stream unprocessed and
// refuses to open new ones. It also feeds a GOAWAY into the server to flip its drain flag.
//
// moon run src/examples/25-goaway-shutdown
///|
fn drive(
server : @moonrpc.H2Server,
client : @moonrpc.H2Client,
id : Int32,
initial : Array[@http2.Frame],
) -> Unit raise {
let to_server : Array[@http2.Frame] = []
let to_client : Array[@http2.Frame] = []
for f in client.preface() {
to_server.push(f)
}
for f in server.preface() {
to_client.push(f)
}
for f in initial {
to_server.push(f)
}
let mut si = 0
let mut ci = 0
let mut steps = 0
while !client.is_done(id) && steps < 100000 {
steps = steps + 1
if si < to_server.length() {
let f = to_server[si]
si = si + 1
for r in server.feed(f) {
to_client.push(r)
}
} else if ci < to_client.length() {
let f = to_client[ci]
ci = ci + 1
for r in client.feed(f) {
to_server.push(r)
}
} else {
break
}
}
}
///|
fn run() -> Unit raise {
let server = @moonrpc.H2Server::new()
server.register("/echo.Echo/Say", req => req)
let client = @moonrpc.H2Client::new()
// A first call completes normally on stream 1.
let (id1, init1) = client.unary("/echo.Echo/Say", b"heke1228")
drive(server, client, id1, init1)
println(
"stream \{id1} completed, grpc-status \{client.reply(id1).grpc_status}",
)
// A second call is opened (stream 3) but the server starts draining before processing it.
let id2 = client.open("/echo.Echo/Say")
println("opened stream \{id2}, not yet processed")
// The server sends GOAWAY announcing stream 1 as the last it will handle.
for f in server.goaway(id1) {
let _ = client.feed(f)
}
println("client saw GOAWAY: \{client.goaway_received()}")
println(" last processed stream id: \{client.goaway_last_stream_id()}")
println(
" stream \{id1} unprocessed: \{client.unprocessed(id1)} (at or below last -> maybe processed)",
)
println(
" stream \{id2} unprocessed: \{client.unprocessed(id2)} (above last -> never seen, safe to re-issue)",
)
// A draining client refuses to open new streams: `open` returns the reserved id 0.
let refused = client.open("/echo.Echo/Say")
println("open after GOAWAY returns reserved id: \{refused}")
// The mirror direction: a GOAWAY from the client flips the server's drain flag.
println("server draining before: \{server.goaway_received()}")
let _ = server.feed(
GoAway(last_stream_id=id1, error_code=@http2.error_no_error, debug=b""),
)
println("server draining after client GOAWAY: \{server.goaway_received()}")
}
///|
#coverage.skip
fn main {
run() catch {
e => println("error: \{e}")
}
}