mirror of
https://github.com/hibiken/asynq.git
synced 2024-11-10 11:31:58 +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