geektutu/7days-golang · error · ErrShutdown

connection is shut down

Error message

connection is shut down

What it means

ErrShutdown is the gee-rpc client's sentinel (errors.New("connection is shut down")). Close() and registerCall return it when the client has been locally closed (closing=true) or told to stop by the server (shutdown=true); all subsequent calls on that client fail.

Source

Thrown at gee-rpc/day2-client/client.go:50

// Client represents an RPC Client.
// There may be multiple outstanding Calls associated
// with a single Client, and a Client may be used by
// multiple goroutines simultaneously.
type Client struct {
	cc       codec.Codec
	opt      *Option
	sending  sync.Mutex // protect following
	header   codec.Header
	mu       sync.Mutex // protect following
	seq      uint64
	pending  map[uint64]*Call
	closing  bool // user has called Close
	shutdown bool // server has told us to stop
}

var _ io.Closer = (*Client)(nil)

var ErrShutdown = errors.New("connection is shut down")

// Close the connection
func (client *Client) Close() error {
	client.mu.Lock()
	defer client.mu.Unlock()
	if client.closing {
		return ErrShutdown
	}
	client.closing = true
	return client.cc.Close()
}

// IsAvailable return true if the client does work
func (client *Client) IsAvailable() bool {
	client.mu.Lock()
	defer client.mu.Unlock()
	return !client.shutdown && !client.closing
}

View on GitHub (pinned to cf36443821)

Solutions

  1. Create a new client (re-dial) after ErrShutdown instead of reusing the old one
  2. Order lifecycle so Close happens only after all requests complete
  3. Check client.closing/shutdown state before submitting work in concurrent code
  4. Detect the sentinel with errors.Is(err, ErrShutdown) and reconnect transparently

Example fix

// before
call := <-client.Go("Foo.Sum", args, reply, nil).Done // after Close -> ErrShutdown
// after
if err := client.Call("Foo.Sum", args, reply); errors.Is(err, rpc.ErrShutdown) {
    client = rpc.Dial("tcp", addr) // reconnect
    err = client.Call("Foo.Sum", args, reply)
}
Defensive patterns

Strategy: try-catch

Validate before calling

func clientAlive(c *rpc.Client) bool {
    c.mu.Lock(); defer c.mu.Unlock()
    return !c.closing && !c.shutdown
}

Try / catch

if err := client.Call("Svc.Method", args, reply); err != nil {
    if errors.Is(err, ErrShutdown) {
        client, err = Dial(network, addr) // reconnect and retry once
        if err == nil { err = client.Call("Svc.Method", args, reply) }
    }
}

Prevention

When it happens

Trigger: Calling a method after client.Close(); server sent a shutdown notice and the client calls registerCall for new requests; using a client shared across goroutines after another goroutine closed it.

Common situations: Closing a pooled client while requests are still being submitted; connection idle-timeout on the server marks shutdown; lifecycle bug where Close runs before in-flight work finishes.

Related errors


AI-assisted analysis of geektutu/7days-golang@cf36443821 (2026-09-03). Data as JSON: /api/errors/f051fd0005d4d222. Report an issue: GitHub.