Skip to content
Open
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
20 changes: 20 additions & 0 deletions core/errors/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "errors",
srcs = ["errors.go"],
importpath = "github.com/uber/tango/core/errors",
visibility = ["//visibility:public"],
deps = ["@org_uber_go_zap//:zap"],
)

go_test(
name = "errors_test",
srcs = ["errors_test.go"],
embed = [":errors"],
deps = [
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
"@org_uber_go_zap//zapcore",
],
)
172 changes: 172 additions & 0 deletions core/errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package errors

import (
"context"
stderrs "errors"

"go.uber.org/zap"
)

// ErrorCode mirrors the proto ErrorCode enum and classifies a TangoError as
// a user error or an infra error (retryable or not).
type ErrorCode int

const (
ErrorUnknown ErrorCode = iota
ErrorCancelled
ErrorUser
ErrorInfra
ErrorInfraRetryable
)

// String returns the string form of the code: "cancelled", "user", "infra",
// "infra_retryable", or "unknown" as the default.
func (code ErrorCode) String() string {
switch code {
case ErrorCancelled:
return "cancelled"
case ErrorUser:
return "user"
case ErrorInfra:
return "infra"
case ErrorInfraRetryable:
return "infra_retryable"
default:
return "unknown"
}
}

// FailureSource represents the component where an error occurred.
type FailureSource interface {
source()
String() string
}

type source string

func (s source) source() {}
func (s source) String() string { return string(s) }

var (
// FailureSourceUnknown is used as a fallback when no other source applies.
FailureSourceUnknown FailureSource = source("unknown")
// FailureSourceGit represents failures in git operations.
FailureSourceGit FailureSource = source("git")
// FailureSourceBazel represents failures in bazel operations.
FailureSourceBazel FailureSource = source("bazel")
// FailureSourceITG represents failures in ITG cache, changeanalyzer, and graph.
FailureSourceITG FailureSource = source("itg")
// FailureSourceStorage represents failures in storage.
FailureSourceStorage FailureSource = source("storage")
// FailureSourceConfig represents failures in config parser.
FailureSourceConfig FailureSource = source("config")
// FailureSourceController represents failures in controller.
FailureSourceController FailureSource = source("controller")
// FailureSourceTargetHasher represents failures in target hasher.
FailureSourceTargetHasher FailureSource = source("targethasher")
// FailureSourceOrchestrator represents failures in orchestrator.
FailureSourceOrchestrator FailureSource = source("orchestrator")
// FailureSourceRepoManager represents failures in repo manager.
FailureSourceRepoManager FailureSource = source("repomanager")
)

// TangoError is Tango's internal error type, carrying the underlying error, its `FailureSource`, and its `ErrorCode`.
// The `mapper` package uses the error and error code to build the proto `TangoError` for the RPC response, and metrics emitters use the failure source and error code as metric tags.
type TangoError struct {
failureSource FailureSource
err error
errorCode ErrorCode
}

// Error returns the underlying error's message.
func (te *TangoError) Error() string {
return te.err.Error()
}

// Unwrap returns the underlying error, so errors.Is / errors.As can traverse a TangoError.
func (te *TangoError) Unwrap() error {
return te.err
}

// NewInfra wraps err as a TangoError classified ErrorInfra.
func NewInfra(src FailureSource, err error) error {
return newError(src, err, ErrorInfra)
}

// NewUser wraps err as a TangoError classified ErrorUser.
func NewUser(src FailureSource, err error) error {
return newError(src, err, ErrorUser)
}

// NewInfraRetryable wraps err as a TangoError classified ErrorInfraRetryable.
func NewInfraRetryable(src FailureSource, err error) error {
return newError(src, err, ErrorInfraRetryable)
}

func newError(src FailureSource, err error, code ErrorCode) error {
if err == nil {
return nil
}

if src == nil {
src = FailureSourceUnknown
}

var te *TangoError
if stderrs.As(err, &te) {
src = te.failureSource
code = te.errorCode
}

if stderrs.Is(err, context.Canceled) {
code = ErrorCancelled
}

return &TangoError{
failureSource: src,
err: err,
errorCode: code,
}
}

// GetErrorCode extracts the ErrorCode from err.
// If err is context.Canceled, ErrorCancelled is returned.
// If err wraps a TangoError, its code is returned.
// Otherwise ErrorUnknown is returned.
func GetErrorCode(err error) ErrorCode {
code := ErrorUnknown

if stderrs.Is(err, context.Canceled) {
code = ErrorCancelled
}

var te *TangoError
if stderrs.As(err, &te) {
code = te.errorCode
}

return code
}

// GetFailureSource extracts the FailureSource from err.
// If err wraps a TangoError, its source is returned.
// Otherwise FailureSourceUnknown is returned.
func GetFailureSource(err error) FailureSource {
source := FailureSourceUnknown

var te *TangoError
if stderrs.As(err, &te) {
source = te.failureSource
}

return source
}

// Fields returns zap fields describing err: the error message, its ErrorCode, and its FailureSource.
func Fields(err error) []zap.Field {
return []zap.Field{
zap.Error(err),
zap.String("error_code", GetErrorCode(err).String()),
zap.String("failure_source", GetFailureSource(err).String()),
}
}
110 changes: 110 additions & 0 deletions core/errors/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package errors

import (
stderrs "errors"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zapcore"
)

func TestNewError_Constructors(t *testing.T) {
underlying := stderrs.New("boom")

tests := []struct {
name string
newErr func(FailureSource, error) error
source FailureSource
wantCode ErrorCode
}{
{
name: "infra",
newErr: NewInfra,
source: FailureSourceGit,
wantCode: ErrorInfra,
},
{
name: "user",
newErr: NewUser,
source: FailureSourceConfig,
wantCode: ErrorUser,
},
{
name: "infra retryable",
newErr: NewInfraRetryable,
source: FailureSourceStorage,
wantCode: ErrorInfraRetryable,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.newErr(tt.source, underlying)

var te *TangoError
require.True(t, stderrs.As(err, &te))
assert.Equal(t, tt.source, te.failureSource)
assert.Equal(t, tt.wantCode, te.errorCode)
assert.Equal(t, underlying.Error(), te.Error())
})
}
}

func TestNewError_NilSourceDefaultsToUnknown(t *testing.T) {
err := NewInfra(nil, stderrs.New("boom"))

var te *TangoError
require.True(t, stderrs.As(err, &te))
assert.Equal(t, FailureSourceUnknown, te.failureSource)
}

func TestNewError_RewrappingPreservesOriginalClassification(t *testing.T) {
inner := NewUser(FailureSourceConfig, stderrs.New("original"))

outer := NewInfra(FailureSourceGit, inner)

var te *TangoError
require.True(t, stderrs.As(outer, &te))
assert.Equal(t, FailureSourceConfig, te.failureSource)
assert.Equal(t, ErrorUser, te.errorCode)
}

func TestFailureSource_String(t *testing.T) {
assert.Equal(t, "fake-source", source("fake-source").String())
}

func TestFields(t *testing.T) {
tests := []struct {
name string
err error
wantCode string
wantSource string
}{
{
name: "classified error",
err: NewUser(FailureSourceConfig, stderrs.New("boom")),
wantCode: "user",
wantSource: "config",
},
{
name: "unclassified error",
err: stderrs.New("boom"),
wantCode: "unknown",
wantSource: "unknown",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
enc := zapcore.NewMapObjectEncoder()
for _, field := range Fields(tt.err) {
field.AddTo(enc)
}

assert.Equal(t, tt.err.Error(), enc.Fields["error"])
assert.Equal(t, tt.wantCode, enc.Fields["error_code"])
assert.Equal(t, tt.wantSource, enc.Fields["failure_source"])
})
}
}
28 changes: 28 additions & 0 deletions internal/mapper/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "mapper",
srcs = ["errors.go"],
importpath = "github.com/uber/tango/internal/mapper",
visibility = ["//visibility:public"],
deps = [
"//core/errors",
"//tangopb",
"@org_uber_go_yarpc//encoding/protobuf",
"@org_uber_go_yarpc//yarpcerrors",
],
)

go_test(
name = "mapper_test",
srcs = ["errors_test.go"],
embed = [":mapper"],
deps = [
"//core/errors",
"//tangopb",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
"@org_uber_go_yarpc//encoding/protobuf",
"@org_uber_go_yarpc//yarpcerrors",
],
)
54 changes: 54 additions & 0 deletions internal/mapper/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package mapper

import (
tangoerrors "github.com/uber/tango/core/errors"
"github.com/uber/tango/tangopb"
"go.uber.org/yarpc/encoding/protobuf"
"go.uber.org/yarpc/yarpcerrors"
)

// ToProtoError converts err into a YARPC error with a TangoError detail.
func ToProtoError(err error) error {
if err == nil {
return nil
}

tangoCode := tangoerrors.GetErrorCode(err)

return protobuf.NewError(
toYARPCCode(tangoCode),
err.Error(),
protobuf.WithErrorDetails(&tangopb.TangoError{
Code: toProtoErrorCode(tangoCode),
Message: err.Error(),
}),
)
}

func toYARPCCode(code tangoerrors.ErrorCode) yarpcerrors.Code {
switch code {
case tangoerrors.ErrorCancelled:
return yarpcerrors.CodeCancelled
case tangoerrors.ErrorUser:
return yarpcerrors.CodeInvalidArgument
case tangoerrors.ErrorInfra, tangoerrors.ErrorInfraRetryable:
return yarpcerrors.CodeInternal
default:
return yarpcerrors.CodeUnknown
}
}

func toProtoErrorCode(code tangoerrors.ErrorCode) tangopb.ErrorCode {
switch code {
case tangoerrors.ErrorCancelled:
return tangopb.ERROR_CANCELLED
case tangoerrors.ErrorUser:
return tangopb.ERROR_USER
case tangoerrors.ErrorInfra:
return tangopb.ERROR_INFRA
case tangoerrors.ErrorInfraRetryable:
return tangopb.ERROR_INFRA_RETRYABLE
default:
return tangopb.ERROR_UNKNOWN
}
}
Loading
Loading