lizining1231 opened a new issue, #3640:
URL: https://github.com/apache/dubbo-go/issues/3640
### Summary
`Server.startHttp2AndHttp3` (`protocol/triple/triple_protocol/server.go`)
starts
the HTTP/2 and HTTP/3 servers in parallel with `errgroup.Group{}` but does
NOT
hook into the errgroup cancellation mechanism: **as long as either port is
occupied (TCP or UDP)**, the failing side returns an error, while `eg.Wait()`
never returns because it is waiting for the other service that started
normally and is still listening, causing `Run` to hang permanently, and the
successfully-started side is not closed automatically (goroutine/port leak).
### Affected Locations
| Package | Test
| Test Location | Related Source
Locations
|
| --------------------------------- |
---------------------------------------------------------- |
-------------------------------------- |
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
| `protocol/triple/triple_protocol` | probe test (temporary, to be promoted
to a formal regression test) | `leak_probe_test.go` (local workspace) |
`startHttp2AndHttp3`
<https://github.com/apache/dubbo-go/blob/master/protocol/triple/triple_protocol/server.go#L288-L308>
(`eg := &errgroup.Group{}` does not wire up ctx cancellation) |
### Reproduction
Using the **occupied TCP port** as an example:
```go
// Occupy the TCP port first, then start in dual mode
tcpLn, _ := net.Listen("tcp", tcpAddr) // occupy TCP (HTTP/2 will
fail to bind)
srv.Run(constant.CallHTTP2AndHTTP3, tlsConf) // hangs, no return within
3s+
```
Observed output:
```text
leak_probe_test.go:199: Run did not return within 3s: it is hung on
errgroup.Wait
leak_probe_test.go:210: UDP port is occupied by HTTP/3 while Run is hung:
listen udp 127.0.0.1:43633: bind: address already in use
```
Symmetrically: calling `startHttp2` alone returns the error immediately when
the TCP port is occupied, and calling `startHttp3` alone does the same when
the UDP port is occupied (`listen tcp ...: bind: address already in use`),
proving the failure really happens and is simply not propagated correctly.
<br />
| TCP | UDP | Result
|
| -------- | -------- |
----------------------------------------------------- |
| free | free | runs normally
|
| free | occupied | HTTP/3 fails + HTTP/2 keeps listening → **Run
hangs** |
| occupied | free | HTTP/2 fails + HTTP/3 keeps listening → **Run
hangs** |
| occupied | occupied | both fail immediately → `Wait` returns the error
|
### Root Cause
- `errgroup.Group.Wait()` waits for **ALL** goroutines to finish before
returning the first error (golang.org/x/sync v0.21.0 errgroup.go:72-102).
- The code uses `&errgroup.Group{}` (not `WithContext`); a failure only
records `g.err` and **never notifies/cancels the other goroutine**.
- Hence after the failing side exits, the successful side is still listening
→
`Wait` never returns → `Run` hangs forever; even if the return path were
fixed later, the successful side would not close itself (resource leak).
This chain is **fully symmetric** in both directions: if HTTP/3 fails,
HTTP/2 keeps listening; if HTTP/2 fails, HTTP/3 keeps listening.
### Proposed Fix
Relative to the original implementation (an errgroup launching both
`ListenAndServe*` concurrently), this change:
1. **Add a certificate readiness check**: before startup, verify that
`tlsConf` has a server certificate configured (`Certificates` non-empty,
or
`GetCertificate`/`GetConfigForClient` set); return an error immediately if
not — preventing `ServeTLS` from failing synchronously inside a goroutine
and hanging again.
2. **Pre-bind the sockets**: turn the implicit bind into an explicit
pre-bind —
`net.Listen("tcp", s.addr)` first, then `net.ListenUDP("udp", s.addr)`;
on either failure, return immediately and close the already-bound socket
(fail-fast, no request has been served at this point).
3. **Serve concurrently**: only after both sockets are bound successfully,
run
`ServeTLS(tcpLn, "", "")` and `Serve(udpConn)` concurrently in a plain
`errgroup`, with the result propagated by `eg.Wait()`.
Benefits:
- **Zero new dependencies**: `ServeTLS(ln, "", "")` and
`http3.Server.Serve(conn)` both natively accept pre-bound sockets.
- **Symmetric coverage**: one fix covers both the TCP and UDP error paths.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]