From a3ffc9a24a9c925778a256e9e4574a1638f90a73 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Fri, 10 Apr 2026 14:54:11 +0100 Subject: [PATCH 1/2] fix(test): prevent Mercury v02 goroutine leak in timeout test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestV02_DoMercuryRequestV02_Timeout panicked with "Log in goroutine after Test has completed" because: 1. The client's Close() uses StopOnce which requires Start() — but the test never started the service, so Close() was a no-op 2. The mock used time.Sleep(15s) which ignored context cancellation, keeping goroutines alive after the test completed Fix by calling StartOnce in setupClient so Close() properly waits for goroutines, and replacing the mock's time.Sleep with a context-aware select that exits promptly on cancellation. Fixes: CORE-2372 --- .../v21/mercury/v02/v02_request_test.go | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/mercury/v02/v02_request_test.go b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/mercury/v02/v02_request_test.go index e971102afea..6268e1d729b 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/mercury/v02/v02_request_test.go +++ b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/mercury/v02/v02_request_test.go @@ -2,6 +2,7 @@ package v02 import ( "bytes" + "context" "encoding/json" "errors" "io" @@ -26,6 +27,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -91,13 +93,14 @@ func setupClient(t *testing.T) *client { mercuryConfig := NewMockMercuryConfigProvider() threadCtl := utils.NewThreadControl() - client := NewClient( + c := NewClient( mercuryConfig, mockHttpClient, threadCtl, lggr, ) - return client + require.NoError(t, c.StartOnce("v02_request", func() error { return nil })) + return c } func TestV02_SingleFeedRequest(t *testing.T) { @@ -690,15 +693,17 @@ func TestV02_DoMercuryRequestV02_Timeout(t *testing.T) { } hc.On("Do", mock.Anything).Return(resp, nil).Once() // First request sends result normally - resp2 := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte{})), - } - // Second request has timeout - serverTimeout := 15 * time.Second // Server has delay of 15s, higher than mercury.RequestTimeout = 10s + // Second request simulates a server that takes longer than mercury.RequestTimeout. + // Block until the request context is cancelled rather than using time.Sleep, + // so the goroutine exits promptly during cleanup. + serverTimeout := 15 * time.Second hc.On("Do", mock.Anything).Run(func(args mock.Arguments) { - time.Sleep(serverTimeout) - }).Return(resp2, nil).Once() + req := args.Get(0).(*http.Request) + select { + case <-req.Context().Done(): + case <-time.After(serverTimeout): + } + }).Return((*http.Response)(nil), context.DeadlineExceeded).Once() c.httpClient = hc From f558103fe2e8885d42e7cfa7f8bd0d49f15c46cc Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Mon, 13 Apr 2026 13:24:24 +0100 Subject: [PATCH 2/2] fix: add Start() method to v02 mercury client for proper lifecycle management The test was calling StartOnce directly on the StateMachine, which is an internal detail. Add a proper Start() method to the production client so Close() works correctly (StopOnce requires a prior StartOnce), and update the test to use it. --- .../plugins/ocr2keeper/evmregistry/v21/mercury/v02/request.go | 4 ++++ .../evmregistry/v21/mercury/v02/v02_request_test.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/mercury/v02/request.go b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/mercury/v02/request.go index 0f2fedbd827..dcc5c282b73 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/mercury/v02/request.go +++ b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/mercury/v02/request.go @@ -56,6 +56,10 @@ func NewClient(mercuryConfig mercury.MercuryConfigProvider, httpClient mercury.H } } +func (c *client) Start() error { + return c.StartOnce("v02_request", func() error { return nil }) +} + func (c *client) DoRequest(ctx context.Context, streamsLookup *mercury.StreamsLookup, upkeepType automationTypes.UpkeepType, pluginRetryKey string) (encoding.PipelineExecutionState, [][]byte, encoding.ErrCode, bool, time.Duration, error) { if len(streamsLookup.Feeds) == 0 { return encoding.NoPipelineError, nil, encoding.ErrCodeStreamsBadRequest, false, 0 * time.Second, nil diff --git a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/mercury/v02/v02_request_test.go b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/mercury/v02/v02_request_test.go index 6268e1d729b..2e02cccb127 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/mercury/v02/v02_request_test.go +++ b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/mercury/v02/v02_request_test.go @@ -99,7 +99,7 @@ func setupClient(t *testing.T) *client { threadCtl, lggr, ) - require.NoError(t, c.StartOnce("v02_request", func() error { return nil })) + require.NoError(t, c.Start()) return c }