You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

317 lines
7.8 KiB
Go

// Copyright 2020 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package errors
import (
"fmt"
"runtime"
"strconv"
"go.uber.org/atomic"
)
// RedactLogEnabled defines whether the arguments of Error need to be redacted.
var RedactLogEnabled atomic.Bool
// ErrCode represents a specific error type in a error class.
// Same error code can be used in different error classes.
type ErrCode int
// ErrCodeText is a textual error code that represents a specific error type in a error class.
type ErrCodeText string
type ErrorID string
type RFCErrorCode string
// Error is the 'prototype' of a type of errors.
// Use DefineError to make a *Error:
// var ErrUnavailable = errors.Normalize("Region %d is unavailable", errors.RFCCodeText("Unavailable"))
//
// "throw" it at runtime:
// func Somewhat() error {
// ...
// if err != nil {
// // generate a stackful error use the message template at defining,
// // also see FastGen(it's stackless), GenWithStack(it uses custom message template).
// return ErrUnavailable.GenWithStackByArgs(region.ID)
// }
// }
//
// testing whether an error belongs to a prototype:
// if ErrUnavailable.Equal(err) {
// // handle this error.
// }
type Error struct {
code ErrCode
// codeText is the textual describe of the error code
codeText ErrCodeText
// message is a template of the description of this error.
// printf-style formatting is enabled.
message string
// redactArgsPos defines the positions of arguments in message that need to be redacted.
// And it is controlled by the global var RedactLogEnabled.
// For example, an original error is `Duplicate entry 'PRIMARY' for key 'key'`,
// when RedactLogEnabled is ON and redactArgsPos is [0, 1], the error is `Duplicate entry '?' for key '?'`.
redactArgsPos []int
// Cause is used to warp some third party error.
cause error
args []interface{}
file string
line int
}
// Code returns the numeric code of this error.
// ID() will return textual error if there it is,
// when you just want to get the purely numeric error
// (e.g., for mysql protocol transmission.), this would be useful.
func (e *Error) Code() ErrCode {
return e.code
}
// Code returns ErrorCode, by the RFC:
//
// The error code is a 3-tuple of abbreviated component name, error class and error code,
// joined by a colon like {Component}:{ErrorClass}:{InnerErrorCode}.
func (e *Error) RFCCode() RFCErrorCode {
return RFCErrorCode(e.ID())
}
// ID returns the ID of this error.
func (e *Error) ID() ErrorID {
if e.codeText != "" {
return ErrorID(e.codeText)
}
return ErrorID(strconv.Itoa(int(e.code)))
}
// Location returns the location where the error is created,
// implements juju/errors locationer interface.
func (e *Error) Location() (file string, line int) {
return e.file, e.line
}
// MessageTemplate returns the error message template of this error.
func (e *Error) MessageTemplate() string {
return e.message
}
// Error implements error interface.
func (e *Error) Error() string {
if e == nil {
return "<nil>"
}
describe := e.codeText
if len(describe) == 0 {
describe = ErrCodeText(strconv.Itoa(int(e.code)))
}
if e.cause != nil {
return fmt.Sprintf("[%s]%s: %s", e.RFCCode(), e.GetMsg(), e.cause.Error())
}
return fmt.Sprintf("[%s]%s", e.RFCCode(), e.GetMsg())
}
func (e *Error) GetMsg() string {
if len(e.args) > 0 {
return fmt.Sprintf(e.message, e.args...)
}
return e.message
}
func (e *Error) fillLineAndFile(skip int) {
// skip this
_, file, line, ok := runtime.Caller(skip + 1)
if !ok {
e.file = "<unknown>"
e.line = -1
return
}
e.file = file
e.line = line
}
// GenWithStack generates a new *Error with the same class and code, and a new formatted message.
func (e *Error) GenWithStack(format string, args ...interface{}) error {
// TODO: RedactErrorArg
err := *e
err.message = format
err.args = args
err.fillLineAndFile(1)
return AddStack(&err)
}
// GenWithStackByArgs generates a new *Error with the same class and code, and new arguments.
func (e *Error) GenWithStackByArgs(args ...interface{}) error {
RedactErrorArg(args, e.redactArgsPos)
err := *e
err.args = args
err.fillLineAndFile(1)
return AddStack(&err)
}
// FastGen generates a new *Error with the same class and code, and a new formatted message.
// This will not call runtime.Caller to get file and line.
func (e *Error) FastGen(format string, args ...interface{}) error {
// TODO: RedactErrorArg
err := *e
err.message = format
err.args = args
return SuspendStack(&err)
}
// FastGen generates a new *Error with the same class and code, and a new arguments.
// This will not call runtime.Caller to get file and line.
func (e *Error) FastGenByArgs(args ...interface{}) error {
RedactErrorArg(args, e.redactArgsPos)
err := *e
err.args = args
return SuspendStack(&err)
}
// Equal checks if err is equal to e.
func (e *Error) Equal(err error) bool {
originErr := Cause(err)
if originErr == nil {
return false
}
if error(e) == originErr {
return true
}
inErr, ok := originErr.(*Error)
if !ok {
return false
}
idEquals := e.ID() == inErr.ID()
return idEquals
}
// NotEqual checks if err is not equal to e.
func (e *Error) NotEqual(err error) bool {
return !e.Equal(err)
}
// RedactErrorArg redacts the args by position if RedactLogEnabled is enabled.
func RedactErrorArg(args []interface{}, position []int) {
if RedactLogEnabled.Load() {
for _, pos := range position {
if len(args) > pos {
args[pos] = "?"
}
}
}
}
// ErrorEqual returns a boolean indicating whether err1 is equal to err2.
func ErrorEqual(err1, err2 error) bool {
e1 := Cause(err1)
e2 := Cause(err2)
if e1 == e2 {
return true
}
if e1 == nil || e2 == nil {
return e1 == e2
}
te1, ok1 := e1.(*Error)
te2, ok2 := e2.(*Error)
if ok1 && ok2 {
return te1.Equal(te2)
}
return e1.Error() == e2.Error()
}
// ErrorNotEqual returns a boolean indicating whether err1 isn't equal to err2.
func ErrorNotEqual(err1, err2 error) bool {
return !ErrorEqual(err1, err2)
}
type jsonError struct {
// Deprecated field, please use `RFCCode` instead.
Class int `json:"class"`
Code int `json:"code"`
Msg string `json:"message"`
RFCCode string `json:"rfccode"`
}
func (e *Error) Wrap(err error) *Error {
if err != nil {
newErr := *e
newErr.cause = err
return &newErr
}
return nil
}
func (e *Error) Cause() error {
root := Unwrap(e.cause)
if root == nil {
return e.cause
}
return root
}
func (e *Error) FastGenWithCause(args ...interface{}) error {
err := *e
if e.cause != nil {
err.message = e.cause.Error()
}
err.args = args
return SuspendStack(&err)
}
func (e *Error) GenWithStackByCause(args ...interface{}) error {
err := *e
if e.cause != nil {
err.message = e.cause.Error()
}
err.args = args
err.fillLineAndFile(1)
return AddStack(&err)
}
type NormalizeOption func(*Error)
func RedactArgs(pos []int) NormalizeOption {
return func(e *Error) {
e.redactArgsPos = pos
}
}
// RFCCodeText returns a NormalizeOption to set RFC error code.
func RFCCodeText(codeText string) NormalizeOption {
return func(e *Error) {
e.codeText = ErrCodeText(codeText)
}
}
// MySQLErrorCode returns a NormalizeOption to set error code.
func MySQLErrorCode(code int) NormalizeOption {
return func(e *Error) {
e.code = ErrCode(code)
}
}
// Normalize creates a new Error object.
func Normalize(message string, opts ...NormalizeOption) *Error {
e := &Error{
message: message,
}
for _, opt := range opts {
opt(e)
}
return e
}