From a6a4988e99b9af3abd8784781c0001d288d54514 Mon Sep 17 00:00:00 2001 From: Mergen Imeev Date: Fri, 28 Mar 2025 14:29:10 +0300 Subject: [PATCH] api: fix panic in conn.NewWatcher() Before this patch, `conn.c` was not checked for `nil` before calling its method. This could cause a panic if the connection was lost or closed. Closes #438 --- CHANGELOG.md | 3 +++ connection.go | 3 +++ tarantool_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00049a9c..c7befdb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +- Fixed panic when calling NewWatcher() during reconnection or after + connection is closed (#438). + ## [v2.3.0] - 2025-03-11 The release extends box.info responses and ConnectionPool.GetInfo return data. diff --git a/connection.go b/connection.go index 97f9dbbc..3cda4f4f 100644 --- a/connection.go +++ b/connection.go @@ -1454,6 +1454,9 @@ func isFeatureInSlice(expected iproto.Feature, actualSlice []iproto.Feature) boo // // Since 1.10.0 func (conn *Connection) NewWatcher(key string, callback WatchCallback) (Watcher, error) { + if conn.c == nil { + return nil, ClientError{ErrConnectionNotReady, "client connection is not ready"} + } // We need to check the feature because the IPROTO_WATCH request is // asynchronous. We do not expect any response from a Tarantool instance // That's why we can't just check the Tarantool response for an unsupported diff --git a/tarantool_test.go b/tarantool_test.go index 3f1e90ef..1bde0871 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3550,6 +3550,53 @@ func TestConnection_NewWatcher(t *testing.T) { } } +func TestNewWatcherDuringReconnectOrAfterClose(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const server = "127.0.0.1:3015" + testDialer := dialer + testDialer.Address = server + + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + Dialer: testDialer, + InitScript: "config.lua", + Listen: server, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 10, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(inst) + if err != nil { + t.Fatalf("Unable to start Tarantool: %s", err) + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + + reconnectOpts := opts + reconnectOpts.Reconnect = 100 * time.Millisecond + reconnectOpts.MaxReconnects = 0 + reconnectOpts.Notify = make(chan ConnEvent) + conn, err := Connect(ctx, testDialer, reconnectOpts) + if err != nil { + t.Fatalf("Connection was not established: %v", err) + } + defer conn.Close() + test_helpers.StopTarantool(inst) + + for conn.ConnectedNow() { + time.Sleep(100 * time.Millisecond) + } + _, err = conn.NewWatcher("one", func(event WatchEvent) {}) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "client connection is not ready") + + conn.Close() + _, err = conn.NewWatcher("two", func(event WatchEvent) {}) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "client connection is not ready") +} + func TestConnection_NewWatcher_noWatchersFeature(t *testing.T) { test_helpers.SkipIfWatchersSupported(t)