Skip to content

net: concurrent Accept-Close with no timeout on TCPListener defers closing underlying socket #10527

@ggriffiniii

Description

@ggriffiniii

Go version: go1.4.2
OS: Linux 3.13 x86_64

The following test demonstrates what I consider a bug:

package foo

import (
    "log"
    "net"
    "testing"
)

const addr = "127.0.0.1:12345"

func TestXxx(t *testing.T) {
    for i := 0; i < 100000; i++ {
        log.Printf("Iteration %d", i+1)
        l, err := net.Listen("tcp4", addr)
        if err != nil {
            t.Fatalf("Failed to listen on %s: %v", addr, err)
        }
        go func(l net.Listener) {
            for {
                _, err := l.Accept()
                if err != nil {
                    return
                }
            }
        }(l)
        if err := l.Close(); err != nil {
            t.Fatalf("Failed to close server: %v", err)
        }
    }
}

Briefly, within a loop it Listens on tcp port 12345, invokes a goroutine that calls Accept on the listener, and then Closes the listener. My expectation was that this would always pass regardless of scheduler behavior or number of loop iterations as I expected Close() would only return once the underlying socket is no longer in use.

Instead what I see is that it will sometimes pass, but often fails on the net.Listen call of subsequent loop iterations with an error that the address is already in use. I believe I understand the cause of the failure. netFD underlies the TCPListener and it's a reference counted object. A goroutine blocked on Accept will increment the reference count. Close() flips a flag on netFD to indicate the socket is considered closed which will make other netFD operations like Accept return errors indicating the socket is closed. The underlying OS socket is only closed once the reference count for the netFD has reached zero.

The end result is that an OS socket is only closed after TCPListener.Close() is invoked AND all goroutines that are blocked in TCPListener.Accept() are scheduled and return the expected error condition. It seems like more intuitive behavior would be for TCPListener.Close() to immediately close the underlying OS socket, and goroutines blocked in TCPListener.Accept() to return errors whenever they were next scheduled/run.

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions