-
Notifications
You must be signed in to change notification settings - Fork 359
fix: suppress net.ErrClosed on concurrent Close #564
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,11 +99,7 @@ func CloseStatus(err error) StatusCode { | |
| func (c *Conn) Close(code StatusCode, reason string) (err error) { | ||
| defer errd.Wrap(&err, "failed to close WebSocket") | ||
|
|
||
| if c.casClosing() { | ||
| err = c.waitGoroutines() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if c.userClosed.Swap(true) && c.isClosed() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note [CRF-9] The
|
||
| return net.ErrClosed | ||
| } | ||
| defer func() { | ||
|
|
@@ -112,6 +108,10 @@ func (c *Conn) Close(code StatusCode, reason string) (err error) { | |
| } | ||
| }() | ||
|
|
||
| if c.casClosing() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit [CRF-5] The PR description claims concurrent user calls "return instantly without blocking." In practice, a concurrent caller reaches
|
||
| return c.waitGoroutines() | ||
| } | ||
|
|
||
| err = c.closeHandshake(code, reason) | ||
|
|
||
| err2 := c.close() | ||
|
|
@@ -132,11 +132,7 @@ func (c *Conn) Close(code StatusCode, reason string) (err error) { | |
| func (c *Conn) CloseNow() (err error) { | ||
| defer errd.Wrap(&err, "failed to immediately close WebSocket") | ||
|
|
||
| if c.casClosing() { | ||
| err = c.waitGoroutines() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if c.userClosed.Swap(true) && c.isClosed() { | ||
| return net.ErrClosed | ||
| } | ||
| defer func() { | ||
|
|
@@ -145,6 +141,10 @@ func (c *Conn) CloseNow() (err error) { | |
| } | ||
| }() | ||
|
|
||
| if c.casClosing() { | ||
| return c.waitGoroutines() | ||
| } | ||
|
|
||
| err = c.close() | ||
|
|
||
| err2 := c.waitGoroutines() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,9 +77,10 @@ type Conn struct { | |
| closeReadCtx context.Context | ||
| closeReadDone chan struct{} | ||
|
|
||
| closing atomic.Bool | ||
| closeMu sync.Mutex // Protects following. | ||
| closed chan struct{} | ||
| userClosed atomic.Bool // Set by Close/CloseNow on first user call. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3 [CRF-6]
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit [CRF-7] Comment "on first user call" restates what the name and type already show. The
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note [CRF-8] Close state is now tracked across three independent boolean-ish variables (
|
||
| closing atomic.Bool | ||
| closeMu sync.Mutex // Protects following. | ||
| closed chan struct{} | ||
|
|
||
| pingCounter atomic.Int64 | ||
| activePingsMu sync.Mutex | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2 [CRF-3] No regression test for the race condition this PR fixes. The bug: when the internal read loop (
read.go:361) or write path (write.go:365) winscasClosing()before the user calls Close, the old code returned a wrappednet.ErrClosed. No existing test exercises this interleaving.A test that reproduces this: set up a connection with
CloseRead, trigger the peer to send a close frame so the local read loop winscasClosing, then call Close from user code and assert nil return. (Bisky P1, Hisoka P2, Mafu-san P2, Mafuuu P2, Meruem P2, Chopper P2, Pariston P3, Leorio P3, Takumi P3)