diff --git a/README.md b/README.md index 4dcbcf5..7b63d47 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. diff --git a/conncheck.go b/conncheck.go index 0f02e8e..310d093 100644 --- a/conncheck.go +++ b/conncheck.go @@ -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 } diff --git a/conncheck_test.go b/conncheck_test.go index 750c129..3576f13 100644 --- a/conncheck_test.go +++ b/conncheck_test.go @@ -6,7 +6,10 @@ import ( "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "errors" + "fmt" "net" + "syscall" "testing" "time" @@ -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() { diff --git a/peek_windows.go b/peek_windows.go index f1c22f7..a1954b3 100644 --- a/peek_windows.go +++ b/peek_windows.go @@ -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 } - 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) @@ -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 }