diff --git a/foreign/go/client/tcp/tcp_core.go b/foreign/go/client/tcp/tcp_core.go index b101e1b287..2988db7db7 100644 --- a/foreign/go/client/tcp/tcp_core.go +++ b/foreign/go/client/tcp/tcp_core.go @@ -433,12 +433,20 @@ func (c *IggyTcpClient) sendLocked(wirePayload []byte) ([]byte, error) { // invalidateConnLocked closes the connection and marks it as disconnected func (c *IggyTcpClient) invalidateConnLocked() { - if c.conn != nil { - _ = c.conn.Close() - } + _ = c.closeConnLocked() c.state = iggcon.StateDisconnected } +// closeConnLocked closes and drops the current connection. +func (c *IggyTcpClient) closeConnLocked() error { + if c.conn == nil { + return nil + } + err := c.conn.Close() + c.conn = nil + return err +} + func (c *IggyTcpClient) GetConnectionInfo() *iggcon.ConnectionInfo { c.mtx.Lock() defer c.mtx.Unlock() @@ -610,22 +618,17 @@ func (c *IggyTcpClient) disconnect() error { c.mtx.Lock() defer c.mtx.Unlock() - if c.state == iggcon.StateDisconnected { + if c.state == iggcon.StateDisconnected || c.state == iggcon.StateShutdown { return nil } c.logger.Info("Iggy client is disconnecting from server...", slog.String("client_address", c.clientAddress)) c.state = iggcon.StateDisconnected - - if c.conn != nil { - if err := c.conn.Close(); err != nil { - return err - } - } + err := c.closeConnLocked() c.logger.Info("Iggy client has disconnected from server.", slog.String("client_address", c.clientAddress)) // TODO event pushing logic - return nil + return err } func (c *IggyTcpClient) shutdown() error { @@ -638,16 +641,12 @@ func (c *IggyTcpClient) shutdown() error { c.logger.Info("Shutting down the Iggy TCP client...", slog.String("client_address", c.clientAddress)) - if c.conn != nil { - if err := c.conn.Close(); err != nil { - return err - } - } - + err := c.closeConnLocked() c.state = iggcon.StateShutdown + c.logger.Info("Iggy TCP client has been shutdown.", slog.String("client_address", c.clientAddress)) // TODO push shutdown event - return nil + return err } func (c *IggyTcpClient) Close() error { diff --git a/foreign/go/client/tcp/tcp_core_test.go b/foreign/go/client/tcp/tcp_core_test.go index 36f3a91de2..4f87aeaff4 100644 --- a/foreign/go/client/tcp/tcp_core_test.go +++ b/foreign/go/client/tcp/tcp_core_test.go @@ -272,3 +272,60 @@ func TestNewIggyTcpClient_StoresProvidedLogger(t *testing.T) { t.Errorf("expected logger output to contain 'source=tcp', got: %q", output) } } + +var errCloseFailed = errors.New("close failed") + +// failingCloseConn is a connection whose Close always fails, standing in for a +// socket whose teardown reports an error the client cannot act on. +type failingCloseConn struct { + net.Conn + closes int +} + +func (f *failingCloseConn) Close() error { + f.closes++ + return errCloseFailed +} + +func TestShutdown_FailedCloseStillCompletesTeardown(t *testing.T) { + c, _ := newTestClient(t) + conn := &failingCloseConn{Conn: c.conn} + c.conn = conn + + if err := c.shutdown(); !errors.Is(err, errCloseFailed) { + t.Fatalf("got %v, want %v", err, errCloseFailed) + } + if c.state != iggcon.StateShutdown { + t.Errorf("expected state %v, got %v", iggcon.StateShutdown, c.state) + } + if c.conn != nil { + t.Error("expected the closed connection to be dropped") + } + + if err := c.shutdown(); err != nil { + t.Errorf("expected the second shutdown to be a no-op, got %v", err) + } + if conn.closes != 1 { + t.Errorf("expected the connection to be closed once, got %d", conn.closes) + } +} + +func TestDisconnect_ShutdownClientIsNotResurrected(t *testing.T) { + c, _ := newTestClient(t) + + if err := c.shutdown(); err != nil { + t.Fatalf("unexpected shutdown error: %v", err) + } + if err := c.disconnect(); err != nil { + t.Fatalf("unexpected disconnect error: %v", err) + } + + if c.state != iggcon.StateShutdown { + t.Errorf("expected state to stay %v, got %v", iggcon.StateShutdown, c.state) + } + + _, err := c.sendWireAndFetchResponse(context.Background(), emptyWireReq) + if !errors.Is(err, ierror.ErrClientShutdown) { + t.Errorf("got %v, want %v", err, ierror.ErrClientShutdown) + } +}