fix(morpc): join accepted server connections on close - #26554
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
Deep review finding — P1 / merge blocker The connection tracker is admitted too late to provide the shutdown guarantee claimed by this PR. This leaves a valid shutdown interleaving:
So The new test does not cover this boundary because it manually calls Please move the lifecycle accounting to the actual goetty admission/ownership boundary:
Please also add a deterministic regression test for the accepted-but-handler-not-yet-started state. This fixes the ownership protocol rather than relying on scheduling or a scenario-specific delay. |
XuPeng-SH
left a comment
There was a problem hiding this comment.
Requesting changes for the P1 shutdown-ownership gap documented in the prior comment.
connections.begin() executes only after the goetty handler goroutine is scheduled, while connection admission happens before that goroutine is launched. Close() can therefore seal an empty tracker and return while an already accepted handler is still queued, leaving the original goroutine-leak failure possible.
Please move accounting to the goetty admission/outer-goroutine boundary and add a deterministic accepted-but-not-yet-started regression test.
|
@XuPeng-SH Thanks for the detailed interleaving. The original change was intentionally scoped to MatrixOne: after stopping the goetty application, MORPC seals a tracker and waits for active MORPC handler callbacks to return. I also moved the tracking into the MORPC handler so that a caller-provided I agree that this only accounts for callbacks after they start. It cannot cover a connection that goetty has already accepted and admitted but whose handler goroutine has not run yet, and the callback-level I investigated MatrixOne-only alternatives. A strict fix there would require either duplicating goetty's application/accept loop in MORPC or vendoring a patched goetty implementation, because the required admission and outer-goroutine ownership boundaries are currently internal to goetty. Neither looks like a good small fix. Do you think we should modify |
XuPeng-SH
left a comment
There was a problem hiding this comment.
Re-review of f5dec32bf14f after the author response: P1 / REQUEST CHANGES remains.
Yes, fixing this in matrixorigin/goetty, then bumping MatrixOne's dependency and removing the MORPC-local tracker/custom read loop, is the correct ownership boundary. The current head is unchanged, however, and still does not establish the shutdown postcondition claimed by the PR.
The complete ownership/wait chain is:
- goetty accepts a socket, constructs the
IOSession, callsaddSession, and only then launches the outer handler goroutine. - MORPC increments
connections.activeonly after that goroutine entershandleConnection. goetty.Stopcloses listeners and waits only for accept loops, removes/disconnects sessions, and returns without joining handler goroutines.- MORPC then seals the callback-level tracker.
Therefore an admitted-but-not-yet-entered handler is absent from the tracker. I reran a deterministic gated admission test on this exact head: server.Close() returned <nil> in 0.00s before the admitted handler was allowed to run. The PR's test passes under -race -count=20 because it manually calls tracker.begin() before Close, so it covers only the already-started state.
There is also an outer-cleanup closure that the upstream fix must address. On Stop, goetty deletes each session and calls only Disconnect; afterward the handler defer calls deleteSession, which returns false when running == false, so rs.Close()/IOSessionAware.Closed is skipped. A real raw accepted connection that had not sent its first MORPC message reproduced this under -race: Close returned without the matching session Closed callback (deterministic failure in 0.21s). Thus waiting only for the callback would still not provide exactly-once session destruction.
Required fix shape in goetty:
- reserve handler ownership before launch, under the same admission state/lock that prevents new reservations after Stop begins;
- release at the outermost goroutine defer, after the admitted session has reached exactly-once final
Close; - close listeners and join accept loops, disconnect sessions to release blocked reads, then join all reserved handlers;
- continue teardown and waiting even if a listener close reports an error, returning the error only after cleanup;
- add deterministic upstream regressions for both admitted-before-handler-start and an idle accepted session's final
Closedcallback.
Then bump the goetty version here and remove the MORPC-local tracker/custom loop. Existing package validation is otherwise clean: build/vet pass, the full pkg/common/morpc race suite passes, and TestStatusInRollingRestartCN passes under -race -count=5; those successes do not exercise the missing ownership states above.
What type of PR is this?
Which issue(s) this PR fixes:
issue #26550
What this PR does / why we need it:
MORPC server shutdown previously stopped the goetty application and returned after disconnecting active sessions, but goetty did not join the admitted connection handlers. Under CI load, leak detection could therefore run while
doConnection, server write-loop, and peer backend goroutines were still unwinding.This change:
Server.Closecannot return before an accepted connection completes cleanup.Validation:
mo-cgo-test -race -count=100 -run '^TestServerCloseWaitsForAcceptedConnections$' ./pkg/common/morpcmo-cgo-test -race -count=1 ./pkg/common/morpcmo-cgo-test -count=50 -run '^TestStatusInRollingRestartCN$' ./pkg/lockservicemo-cgo-test -race -count=13 -run '^TestStatusInRollingRestartCN$' ./pkg/lockservicemo-cgo-test -race -count=1 ./pkg/lockservicego build ./pkg/common/morpc/... ./pkg/lockservice/...go vet ./pkg/common/morpc/... ./pkg/lockservice/...After merging the latest
main, both focused race regressions were rerun successfully.