From 35dd8100f7b4f3041201834221a7834569345db7 Mon Sep 17 00:00:00 2001 From: Ermachkov Yaroslav Date: Fri, 24 Oct 2025 13:25:00 +0300 Subject: [PATCH] connection: returing ctx.Cause error in canceled case Returing wrapped context.Cause(ctx) error in <-ctx.Done() case. Allows compare errors using errors.Is/As. Add 3 tests errors checking comparabily. Fixes #457 --- CHANGELOG.md | 2 ++ connection.go | 6 ++++-- example_test.go | 2 +- tarantool_test.go | 43 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2dc4fc19..eb283b863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. * Required Go version is `1.24` now (#456). * `box.New` returns an error instead of panic (#448). +* Now cases of `<-ctx.Done()` returns wrapped error provided by `ctx.Cause()`. + Allows you compare it using `errors.Is/As` (#457). ### Fixed diff --git a/connection.go b/connection.go index 5f976fbf8..f8f04d014 100644 --- a/connection.go +++ b/connection.go @@ -984,7 +984,8 @@ func (conn *Connection) newFuture(req Request) (fut *Future) { if ctx != nil { select { case <-ctx.Done(): - fut.SetError(fmt.Errorf("context is done (request ID %d)", fut.requestId)) + fut.SetError(fmt.Errorf("context is done (request ID %d): %w", + fut.requestId, context.Cause(ctx))) shard.rmut.Unlock() return default: @@ -1026,7 +1027,8 @@ func (conn *Connection) contextWatchdog(fut *Future, ctx context.Context) { case <-fut.done: return default: - conn.cancelFuture(fut, fmt.Errorf("context is done (request ID %d)", fut.requestId)) + conn.cancelFuture(fut, fmt.Errorf("context is done (request ID %d): %w", + fut.requestId, context.Cause(ctx))) } } diff --git a/example_test.go b/example_test.go index e1b9d5766..b4411fb69 100644 --- a/example_test.go +++ b/example_test.go @@ -167,7 +167,7 @@ func ExamplePingRequest_Context() { fmt.Println("Ping Error", regexp.MustCompile("[0-9]+").ReplaceAllString(err.Error(), "N")) // Output: // Ping Resp data [] - // Ping Error context is done (request ID N) + // Ping Error context is done (request ID N): context deadline exceeded } func ExampleSelectRequest() { diff --git a/tarantool_test.go b/tarantool_test.go index 437f7f96e..4b8873eb7 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3,6 +3,7 @@ package tarantool_test import ( "context" "encoding/binary" + "errors" "fmt" "io" "log" @@ -48,7 +49,8 @@ type Member struct { Val uint } -var contextDoneErrRegexp = regexp.MustCompile(`^context is done \(request ID [0-9]+\)$`) +var contextDoneErrRegexp = regexp.MustCompile( + `^context is done \(request ID [0-9]+\): context canceled$`) func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeArrayLen(2); err != nil { @@ -2742,6 +2744,45 @@ func TestClientRequestObjectsWithPassedCanceledContext(t *testing.T) { } } +// Checking comparable with simple context.WithCancel. +func TestComparableErrorsCanceledContext(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, dialer, opts) + defer conn.Close() + + ctx, cancel := context.WithCancel(context.Background()) + req := NewPingRequest().Context(ctx) + cancel() + _, err := conn.Do(req).Get() + require.True(t, errors.Is(err, context.Canceled), err.Error()) +} + +// Checking comparable with simple context.WithTimeout. +func TestComparableErrorsTimeoutContext(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, dialer, opts) + defer conn.Close() + + timeout := time.Nanosecond + ctx, cancel := context.WithTimeout(context.Background(), timeout) + req := NewPingRequest().Context(ctx) + defer cancel() + _, err := conn.Do(req).Get() + require.True(t, errors.Is(err, context.DeadlineExceeded), err.Error()) +} + +// Checking comparable with context.WithCancelCause. +// Shows ability to compare with custom errors (also with ClientError). +func TestComparableErrorsCancelCauseContext(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, dialer, opts) + defer conn.Close() + + ctxCause, cancelCause := context.WithCancelCause(context.Background()) + req := NewPingRequest().Context(ctxCause) + cancelCause(ClientError{ErrConnectionClosed, "something went wrong"}) + _, err := conn.Do(req).Get() + var tmpErr ClientError + require.True(t, errors.As(err, &tmpErr), tmpErr.Error()) +} + // waitCtxRequest waits for the WaitGroup in Body() call and returns // the context from Ctx() call. The request helps us to make sure that // the context's cancel() call is called before a response received.