Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions core/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import (
"go.uber.org/zap"
)

// ErrClientCancelled is the sentinel cause for client-initiated cancellations.
// Pass it to context.WithCancelCause so downstream code can distinguish a client
// disconnect from other cancellation sources via IsClientCancellation.
var ErrClientCancelled = stderrs.New("client cancelled")

// ErrorCode mirrors the proto ErrorCode enum and classifies a TangoError as
// a user error or an infra error (retryable or not).
type ErrorCode int
Expand Down Expand Up @@ -78,6 +83,13 @@ func newError(err error, code ErrorCode) error {
}
}

// IsClientCancellation reports whether ctx was cancelled with ErrClientCancelled
// as its cause. Use this to distinguish client-initiated disconnects from
// server-side or timeout cancellations.
func IsClientCancellation(ctx context.Context) bool {
return stderrs.Is(context.Cause(ctx), ErrClientCancelled)
}

// GetErrorCode extracts the ErrorCode from err.
// If err is context.Canceled, ErrorCancelled is returned.
// If err wraps a TangoError, its code is returned.
Expand Down
30 changes: 30 additions & 0 deletions core/errors/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,36 @@ func TestNewError_RewrappingOverwritesCode(t *testing.T) {
assert.Equal(t, ErrorInfra, te.errorCode)
}

func TestIsClientCancellation(t *testing.T) {
t.Run("context cancelled with ErrClientCancelled cause", func(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
cancel(ErrClientCancelled)
assert.True(t, IsClientCancellation(ctx))
})

t.Run("context cancelled without cause", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
assert.False(t, IsClientCancellation(ctx))
})

t.Run("context cancelled with different cause", func(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
cancel(stderrs.New("something else"))
assert.False(t, IsClientCancellation(ctx))
})

t.Run("context not cancelled", func(t *testing.T) {
assert.False(t, IsClientCancellation(context.Background()))
})

t.Run("wrapped ErrClientCancelled cause", func(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
cancel(fmt.Errorf("transport: %w", ErrClientCancelled))
assert.True(t, IsClientCancellation(ctx))
})
}

func TestGetErrorCode(t *testing.T) {
tests := []struct {
name string
Expand Down