mirror of
https://github.com/hibiken/asynq.git
synced 2024-12-27 00:02:19 +08:00
Create internal errors package
This commit is contained in:
parent
4d65024bd7
commit
807624e7dd
192
internal/errors/errors.go
Normal file
192
internal/errors/errors.go
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
// Copyright 2020 Kentaro Hibino. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package errors defines the error type and functions used by
|
||||||
|
// asynq and its internal packages.
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Code Code
|
||||||
|
Op Op
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
var b strings.Builder
|
||||||
|
if e.Op != "" {
|
||||||
|
b.WriteString(string(e.Op))
|
||||||
|
}
|
||||||
|
if e.Code != Unspecified {
|
||||||
|
if b.Len() > 0 {
|
||||||
|
b.WriteString(": ")
|
||||||
|
}
|
||||||
|
b.WriteString(e.Code.String())
|
||||||
|
}
|
||||||
|
if e.Err != nil {
|
||||||
|
if b.Len() > 0 {
|
||||||
|
b.WriteString(": ")
|
||||||
|
}
|
||||||
|
b.WriteString(e.Err.Error())
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code defines the canonical error code.
|
||||||
|
type Code uint8
|
||||||
|
|
||||||
|
// List of canonical error codes.
|
||||||
|
const (
|
||||||
|
Unspecified Code = iota
|
||||||
|
NotFound
|
||||||
|
FailedPrecondition
|
||||||
|
Internal
|
||||||
|
AlreadyExists
|
||||||
|
Unknown
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c Code) String() string {
|
||||||
|
switch c {
|
||||||
|
case Unspecified:
|
||||||
|
return "ERROR_CODE_UNSPECIFIED"
|
||||||
|
case NotFound:
|
||||||
|
return "NOT_FOUND"
|
||||||
|
case FailedPrecondition:
|
||||||
|
return "FAILED_PRECONDITION"
|
||||||
|
case Internal:
|
||||||
|
return "INTERNAL_ERROR"
|
||||||
|
case AlreadyExists:
|
||||||
|
return "ALREADY_EXISTS"
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("unknown error code %d", c))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Op describes an operation, usually as the package and method,
|
||||||
|
// such as "rdb.Enqueue".
|
||||||
|
type Op string
|
||||||
|
|
||||||
|
func E(args ...interface{}) error {
|
||||||
|
e := &Error{}
|
||||||
|
for _, arg := range args {
|
||||||
|
switch arg := arg.(type) {
|
||||||
|
case Op:
|
||||||
|
e.Op = arg
|
||||||
|
case Code:
|
||||||
|
e.Code = arg
|
||||||
|
case error:
|
||||||
|
e.Err = arg
|
||||||
|
case string:
|
||||||
|
e.Err = errors.New(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanonicalCode returns the canonical code of the given error if one is present.
|
||||||
|
// Otherwise it returns Unspecified.
|
||||||
|
func CanonicalCode(err error) Code {
|
||||||
|
if err == nil {
|
||||||
|
return Unspecified
|
||||||
|
}
|
||||||
|
e, ok := err.(*Error)
|
||||||
|
if !ok {
|
||||||
|
return Unspecified
|
||||||
|
}
|
||||||
|
if e.Code == Unspecified {
|
||||||
|
return CanonicalCode(e.Err)
|
||||||
|
}
|
||||||
|
return e.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************
|
||||||
|
Domin Specific Error Types
|
||||||
|
*******************************************/
|
||||||
|
|
||||||
|
// TaskNotFoundError indicates that a task with the given ID does not exist
|
||||||
|
// in the given queue.
|
||||||
|
type TaskNotFoundError struct {
|
||||||
|
Queue string // queue name
|
||||||
|
ID string // task id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *TaskNotFoundError) Error() string {
|
||||||
|
return fmt.Sprintf("cannot find task with id=%s in queue %q", e.ID, e.Queue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTaskNotFound reports whether any error in err's chain is of type TaskNotFoundError.
|
||||||
|
func IsTaskNotFound(err error) bool {
|
||||||
|
var target *TaskNotFoundError
|
||||||
|
return As(err, &target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueueNotFoundError indicates that a queue with the given name does not exist.
|
||||||
|
type QueueNotFoundError struct {
|
||||||
|
Queue string // queue name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *QueueNotFoundError) Error() string {
|
||||||
|
return fmt.Sprintf("queue %q does not exist", e.Queue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsQueueNotFound reports whether any error in err's chain is of type QueueNotFoundError.
|
||||||
|
func IsQueueNotFound(err error) bool {
|
||||||
|
var target *QueueNotFoundError
|
||||||
|
return As(err, &target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskAlreadyArchivedError indicates that the task in question is already archived.
|
||||||
|
type TaskAlreadyArchivedError struct {
|
||||||
|
Queue string // queue name
|
||||||
|
ID string // task id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *TaskAlreadyArchivedError) Error() string {
|
||||||
|
return fmt.Sprintf("task is already archived: id=%s, queue=%s", e.ID, e.Queue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTaskAlreadyArchived reports whether any error in err's chain is of type TaskAlreadyArchivedError.
|
||||||
|
func IsTaskAlreadyArchived(err error) bool {
|
||||||
|
var target *TaskAlreadyArchivedError
|
||||||
|
return As(err, &target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*************************************************
|
||||||
|
Standard Library errors package functions
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
// New returns an error that formats as the given text.
|
||||||
|
// Each call to New returns a distinct error value even if the text is identical.
|
||||||
|
//
|
||||||
|
// This function is the errors.New function from the standard libarary (https://golang.org/pkg/errors/#New).
|
||||||
|
// It is exported from this package for import convinience.
|
||||||
|
func New(text string) error { return errors.New(text) }
|
||||||
|
|
||||||
|
// Is reports whether any error in err's chain matches target.
|
||||||
|
//
|
||||||
|
// This function is the errors.Is function from the standard libarary (https://golang.org/pkg/errors/#Is).
|
||||||
|
// It is exported from this package for import convinience.
|
||||||
|
func Is(err, target error) bool { return errors.Is(err, target) }
|
||||||
|
|
||||||
|
// As finds the first error in err's chain that matches target, and if so, sets target to that error value and returns true.
|
||||||
|
// Otherwise, it returns false.
|
||||||
|
//
|
||||||
|
// This function is the errors.As function from the standard libarary (https://golang.org/pkg/errors/#As).
|
||||||
|
// It is exported from this package for import convinience.
|
||||||
|
func As(err error, target interface{}) bool { return errors.As(err, target) }
|
||||||
|
|
||||||
|
// Unwrap returns the result of calling the Unwrap method on err, if err's type contains an Unwrap method returning error.
|
||||||
|
// Otherwise, Unwrap returns nil.
|
||||||
|
//
|
||||||
|
// This function is the errors.Unwrap function from the standard libarary (https://golang.org/pkg/errors/#Unwrap).
|
||||||
|
// It is exported from this package for import convinience.
|
||||||
|
func Unwrap(err error) error { return errors.Unwrap(err) }
|
147
internal/errors/errors_test.go
Normal file
147
internal/errors/errors_test.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// Copyright 2020 Kentaro Hibino. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestErrorString(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
err error
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "With Op, Code, and string",
|
||||||
|
err: E(Op("rdb.DeleteTask"), NotFound, "cannot find task with id=123"),
|
||||||
|
want: "rdb.DeleteTask: NOT_FOUND: cannot find task with id=123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "With Op, Code and error",
|
||||||
|
err: E(Op("rdb.DeleteTask"), NotFound, &TaskNotFoundError{Queue: "default", ID: "123"}),
|
||||||
|
want: `rdb.DeleteTask: NOT_FOUND: cannot find task with id=123 in queue "default"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
if got := tc.err.Error(); got != tc.want {
|
||||||
|
t.Errorf("%s: got=%q, want=%q", tc.desc, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorIs(t *testing.T) {
|
||||||
|
var ErrCustom = New("custom sentinel error")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
err error
|
||||||
|
target error
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "should unwrap one level",
|
||||||
|
err: E(Op("rdb.DeleteTask"), ErrCustom),
|
||||||
|
target: ErrCustom,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
if got := Is(tc.err, tc.target); got != tc.want {
|
||||||
|
t.Errorf("%s: got=%t, want=%t", tc.desc, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorAs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
err error
|
||||||
|
target interface{}
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "should unwrap one level",
|
||||||
|
err: E(Op("rdb.DeleteTask"), NotFound, &QueueNotFoundError{Queue: "email"}),
|
||||||
|
target: &QueueNotFoundError{},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
if got := As(tc.err, &tc.target); got != tc.want {
|
||||||
|
t.Errorf("%s: got=%t, want=%t", tc.desc, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorPredicates(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
fn func(err error) bool
|
||||||
|
err error
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "IsTaskNotFound should detect presence of TaskNotFoundError in err's chain",
|
||||||
|
fn: IsTaskNotFound,
|
||||||
|
err: E(Op("rdb.ArchiveTask"), NotFound, &TaskNotFoundError{Queue: "default", ID: "9876"}),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "IsTaskNotFound should detect absence of TaskNotFoundError in err's chain",
|
||||||
|
fn: IsTaskNotFound,
|
||||||
|
err: E(Op("rdb.ArchiveTask"), NotFound, &QueueNotFoundError{Queue: "default"}),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "IsQueueNotFound should detect presence of QueueNotFoundError in err's chain",
|
||||||
|
fn: IsQueueNotFound,
|
||||||
|
err: E(Op("rdb.ArchiveTask"), NotFound, &QueueNotFoundError{Queue: "default"}),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
if got := tc.fn(tc.err); got != tc.want {
|
||||||
|
t.Errorf("%s: got=%t, want=%t", tc.desc, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanonicalCode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
err error
|
||||||
|
want Code
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "without nesting",
|
||||||
|
err: E(Op("rdb.DeleteTask"), NotFound, &TaskNotFoundError{Queue: "default", ID: "123"}),
|
||||||
|
want: NotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "with nesting",
|
||||||
|
err: E(FailedPrecondition, E(NotFound)),
|
||||||
|
want: FailedPrecondition,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "returns Unspecified if err is not *Error",
|
||||||
|
err: New("some other error"),
|
||||||
|
want: Unspecified,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "returns Unspecified if err is nil",
|
||||||
|
err: nil,
|
||||||
|
want: Unspecified,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
if got := CanonicalCode(tc.err); got != tc.want {
|
||||||
|
t.Errorf("%s: got=%s, want=%s", tc.desc, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user