Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
conncheck implements robust, fast, and reusable network connection liveness check in Go.
Essentially, `conncheck.Do` implements the missing `net.Conn.IsOpen()` method that tells you
if the connection has been closed on the client, was interrupted by network infrastructure,
or the server asked to close it, e.g. with an RST or FIN packet in case of TCP.
or the server asked to close it e.g., with an RST or FIN packet in case of TCP.

`conncheck` is useful in any network application with long-lived, sometimes idle, connections
like SQL connection pools, Apache Thrift clients, etc.
Expand All @@ -30,9 +30,15 @@ the ping takes the time of a full round-trip and consumes precious server resour
`conncheck` peeks without blocking from the connection at OS level. If the OS kernel is aware
of the connection being closed or interrupted, by either peer, it will reject the peek.

The Github team first documented the general approach in [a blog post](https://github.blog/engineering/three-bugs-in-the-go-mysql-driver/)
and a fix in the MySQL Go SQL driver. The approach was later borrowed by other libraries that use
long-lived connections like [Apache Thrift](https://github.com/apache/thrift/pull/2153).
The Github team first documented the general approach in
[a blog post](https://github.blog/engineering/three-bugs-in-the-go-mysql-driver/)
The approach was later borrowed by other libraries that use long-lived connections
like [Apache Thrift](https://github.com/apache/thrift/pull/2153).

Unlike existing solutions, `conncheck` is reusable and supports both Unix systems and Windows.
PRs will be opened, using this code, for the OSS projects with existing limited solutions.
PRs will be opened, using this code, for the OSS projects with existing limited solutions.

## Copyright and acknowledgements

This library is licensed under the Apache 2 license. All code was written from scratch. The Unix code
used the Apache Thrift implementation as a reference, while the Windows code is completely novel.
2 changes: 1 addition & 1 deletion conncheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func Do(conn net.Conn) Status {
}

rawConn, err := sc.SyscallConn()
if err != nil {
if err != nil || rawConn == nil {
if errors.Is(err, syscall.EINVAL) || errors.Is(err, net.ErrClosed) {
return StatusNotOpen
}
Expand Down
32 changes: 32 additions & 0 deletions conncheck_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"errors"
"fmt"
"net"
"syscall"
"testing"
"time"

Expand All @@ -26,8 +29,37 @@ func TestDo(t *testing.T) {
})
t.Run("clientClose", testClientClose)
t.Run("UDP", testUDP)
t.Run("unsupported", testUnsupported)
}

func testUnsupported(t *testing.T) {
for err, res := range map[error]conncheck.Status{
syscall.EINVAL: conncheck.StatusNotOpen,
net.ErrClosed: conncheck.StatusNotOpen,
errors.New("other"): conncheck.StatusUnknown,
nil: conncheck.StatusUnknown} {
t.Run("syscall.Conn error "+fmt.Sprint(err), func(t *testing.T) {
conn := unsupportedConn{syscallConnErr: err}
require.Equal(t, res, conncheck.Do(conn))
})
}
}

type unsupportedConn struct {
net.Conn

syscallConnErr error
}

func (u unsupportedConn) SyscallConn() (syscall.RawConn, error) {
return nil, u.syscallConnErr
}

var _ interface {
net.Conn
syscall.Conn
} = unsupportedConn{}

func testClientClose(t *testing.T) {
accepted, ln := createServer(t, nil)
defer func() {
Expand Down
21 changes: 13 additions & 8 deletions peek_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,26 @@ func tryPeek(rawConn syscall.RawConn) Status {
return true // escape out of the RawConn.Read loop
})

if readErr != nil {
// The read was rejected
// The only possible reason was that the connection was just closed on this side.
return StatusNotOpen
}

if sockOptErr != nil {
// We couldn't set the timeout and do the check.
// TODO: Determine if the reason is that the socket is closed and return NotOpen in that case
return StatusUnknown
}
Comment on lines 64 to 68

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When sockOptErr is non-nil, the function currently returns StatusUnknown. However, if the error indicates that the socket is closed, not connected, or has been shut down (e.g., WSAENOTSOCK, WSAENOTCONN, or WSAESHUTDOWN), we can definitively return StatusNotOpen. This addresses the TODO on line 66 and improves the accuracy of the liveness check on Windows by reducing false positives (where a closed connection might be assumed open).

if sockOptErr != nil {
	if errors.Is(sockOptErr, windows.WSAENOTSOCK) ||
		errors.Is(sockOptErr, windows.WSAENOTCONN) ||
		errors.Is(sockOptErr, windows.WSAESHUTDOWN) {
		return StatusNotOpen
	}
	return StatusUnknown
}


if readErr != nil {
if sockOptResetErr != nil {
// The turning the timeout back didn't work.
// The only possible reason was that the connection was just closed on this side.
return StatusNotOpen
}

// readErr, sockOptErr, sockOptResetErr are nil - test finished

if n > 0 || // we peeked something,
// or there was nothing in the buffer, which is indicated by n == 0 and either:
errors.Is(recvErr, windows.WSAETIMEDOUT) || // if the connection is in blocking mode (typical)
Expand All @@ -73,14 +84,8 @@ func tryPeek(rawConn syscall.RawConn) Status {
// connection is open and there is nothing in the buffer
// recvErr may not be nil even with n > 0. Still, if we read something, the connection is open.

if sockOptResetErr != nil {
// The socket was open, but turning the timeout back didn't work.
// The only possible reason was that the connection was just closed on this side.
return StatusNotOpen
}

return StatusOpen
}

return StatusNotOpen // recvErr is not nil or n == 0 with recvErr == nil which means EOF
return StatusNotOpen // recvErr is not a timeout, or n == 0 && recvErr == nil, which means EOF
}
Loading