)]}'
{
  "commit": "18f412e45b0cb7e782fdea66f32e79e2fc6f8881",
  "tree": "a4e5eeb573b8cb2903fc36702d8ba5ecda1e4f1c",
  "parents": [
    "fe4f1457af08b9a98d518169529152dec1357345"
  ],
  "author": {
    "name": "Matej Šnuderl",
    "email": "ematej.snuderl@gmail.com",
    "time": "Fri Oct 16 15:02:21 2026 +0200"
  },
  "committer": {
    "name": "GitHub",
    "email": "noreply@github.com",
    "time": "Fri Oct 16 18:32:21 2026 +0530"
  },
  "message": "netty: drain before closing a gracefully shut down server connection (#13086)\n\nFixes #9566.\n\nAfter the second GOAWAY of a graceful shutdown (`maxConnectionAge`,\n`maxConnectionIdle`, `Server.shutdown()`), `Http2ConnectionHandler`\ncloses the socket as soon as the last stream\u0027s final frame has been\nwritten to the kernel. If the client hasn\u0027t read the response yet, the\nresponse is still in the server\u0027s send queue. The client\u0027s next frame (a\nBDP PING or WINDOW_UPDATE) then reaches a socket the server has closed,\nso the server\u0027s kernel answers with RST and drops the unsent bytes,\ntrailers included. The client fails an RPC the server completed, with\n`UNAVAILABLE: Connection closed after GOAWAY. HTTP/2 error code:\nNO_ERROR, debug data: max_age`. The response headers had already\narrived, so the call is committed and isn\u0027t retried. The full analysis\nand a standalone reproducer are in\nhttps://github.com/grpc/grpc-java/issues/9566#issuecomment-5930328142.\n\n### Change\n\nOnce the second GOAWAY has been sent and no streams are active,\n`NettyServerHandler` keeps the connection open and reading for up to 1\nsecond before closing. The client normally closes first, as soon as it\nhas read the GOAWAY and its streams have completed, so the drain usually\nends early. This is the approach Go\u0027s HTTP/2 server took\n(golang/net@cd69bc3).\n\n- It overrides `isGracefulShutdownComplete()`, the hook\n`NettyClientHandler` already uses, so the close still goes through\n`Http2ConnectionHandler` and works the same with TLS.\n- It never extends the grace time: if `maxConnectionAgeGrace` (or the\nshutdown grace) runs out first, the connection closes then.\n- The drain is cancelled when the channel becomes inactive.\n\n### Tests\n\n- `NettyServerHandlerTest`: 9 tests asserted that the channel closes\nimmediately after the second GOAWAY. They now advance the fake clock by\nthe drain time first. Two new tests:\n`maxConnectionAge_drainStartsWhenLastStreamCloses` and\n`maxConnectionAgeGrace_shorterThanDrain_closesAtGrace`.\n- New `NettyServerGracefulCloseTest`: an end-to-end regression test over\na real loopback connection, using only public APIs and the server\u0027s\ndefault transport. A unary call outlives the max age, and the client\u0027s\nevent loop stalls 300 ms while the 256 KiB response is sent. Without the\nfix it fails on Linux with the error above (3/3 runs); with the fix it\npasses (3/3, about 6 s). macOS loopback doesn\u0027t lose the data, so there\nit passes either way. Note that `maxConnectionAge` has a 1 s floor,\nwhich is why the call runs 1.5 s.\n- All 655 `grpc-netty` tests pass.\n\nThe standalone reproducer from the issue, on Linux (`master` against\nthis branch, 20 iterations each, `parkMs` \u003d client event-loop stall):\n\n| Scenario | `master` | this PR |\n|---|---|---|\n| No hooks | 5/20 fail | 0/20 |\n| 100 / 500 / 900 ms client stall | 49/60 fail | 0/60 |\n| 1500 ms client stall (longer than the drain) | 1/20 | 3/20 |\n\nA client stalled for longer than the drain can still lose the race, as\nin Go.\n\n### Relation to Netty\n\nThe root cause is in Netty\u0027s `Http2ConnectionHandler`, so I\u0027ve also\nopened netty/netty#17701 (issue netty/netty#17700). It adds an opt-in\n`gracefulShutdownDrainMillis`. Once a Netty release includes it, the\noverride here can be replaced by calling\n`gracefulShutdownDrainMillis(1000)`. This PR fixes grpc-java without\nwaiting for that release.\n\n### Not included\n\n`OkHttpServerTransport` closes the same way after the second GOAWAY and\nis probably affected too. I haven\u0027t tested it, and I\u0027m happy to follow\nup if you want both transports to behave the same.\n\n---------\n\nCo-authored-by: Claude Opus 5.5 \u003cnoreply@anthropic.com\u003e",
  "tree_diff": [
    {
      "type": "modify",
      "old_id": "4103f1fc6ade179a0ac17d6c48a6a468561ed52c",
      "old_mode": 33188,
      "old_path": "netty/src/main/java/io/grpc/netty/NettyServerHandler.java",
      "new_id": "8857060012064cada542ea0e3487f448b29e30f3",
      "new_mode": 33188,
      "new_path": "netty/src/main/java/io/grpc/netty/NettyServerHandler.java"
    },
    {
      "type": "add",
      "old_id": "0000000000000000000000000000000000000000",
      "old_mode": 0,
      "old_path": "/dev/null",
      "new_id": "85a538f1e7a49180a5e24c364f36cf1d5bffc8e6",
      "new_mode": 33188,
      "new_path": "netty/src/test/java/io/grpc/netty/NettyServerGracefulCloseTest.java"
    },
    {
      "type": "modify",
      "old_id": "848c8d59f43994668e7c2ffc2fa3b72061f4ca99",
      "old_mode": 33188,
      "old_path": "netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java",
      "new_id": "cafb41193033dd3a7dca62aa8553b366801969f4",
      "new_mode": 33188,
      "new_path": "netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java"
    }
  ]
}
