Error codespacing (#809)

* Initial codespacing layout (ref #766)
* Add codespace to Router (ref #766)
* Implement Codespacer and update modules
* Default codespaces, testcases
* Update error formatting, codespacer tests
* Add RegisterOrPanic testcase
* Update CHANGELOG
This commit is contained in:
Christopher Goes
2018-04-17 19:16:21 -07:00
committed by Jae Kwon
parent ffd428d199
commit f8e44b5b00
41 changed files with 438 additions and 305 deletions
+35
View File
@@ -0,0 +1,35 @@
package types
// Codespacer is a simple struct to track reserved codespaces
type Codespacer struct {
reserved map[CodespaceType]bool
}
// NewCodespacer generates a new Codespacer with the starting codespace
func NewCodespacer() *Codespacer {
return &Codespacer{
reserved: make(map[CodespaceType]bool),
}
}
// RegisterNext reserves and returns the next available codespace, starting from a default, and panics if the maximum codespace is reached
func (c *Codespacer) RegisterNext(codespace CodespaceType) CodespaceType {
for {
if !c.reserved[codespace] {
c.reserved[codespace] = true
return codespace
}
codespace++
if codespace == MaximumCodespace {
panic("Maximum codespace reached!")
}
}
}
// RegisterOrPanic reserved a codespace or panics if it is unavailable
func (c *Codespacer) RegisterOrPanic(codespace CodespaceType) {
if c.reserved[codespace] {
panic("Cannot register codespace, already reserved")
}
c.reserved[codespace] = true
}
+47
View File
@@ -0,0 +1,47 @@
package types
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestRegisterNext(t *testing.T) {
codespacer := NewCodespacer()
// unregistered, allow
code1 := codespacer.RegisterNext(CodespaceType(2))
require.Equal(t, code1, CodespaceType(2))
// registered, pick next
code2 := codespacer.RegisterNext(CodespaceType(2))
require.Equal(t, code2, CodespaceType(3))
// pick next
code3 := codespacer.RegisterNext(CodespaceType(2))
require.Equal(t, code3, CodespaceType(4))
// skip 1
code4 := codespacer.RegisterNext(CodespaceType(6))
require.Equal(t, code4, CodespaceType(6))
code5 := codespacer.RegisterNext(CodespaceType(2))
require.Equal(t, code5, CodespaceType(5))
code6 := codespacer.RegisterNext(CodespaceType(2))
require.Equal(t, code6, CodespaceType(7))
// panic on maximum
defer func() {
r := recover()
require.NotNil(t, r, "Did not panic on maximum codespace")
}()
codespacer.RegisterNext(MaximumCodespace - 1)
codespacer.RegisterNext(MaximumCodespace - 1)
}
func TestRegisterOrPanic(t *testing.T) {
codespacer := NewCodespacer()
// unregistered, allow
code1 := codespacer.RegisterNext(CodespaceType(2))
require.Equal(t, code1, CodespaceType(2))
// panic on duplicate
defer func() {
r := recover()
require.NotNil(t, r, "Did not panic on duplicate codespace")
}()
codespacer.RegisterOrPanic(CodespaceType(2))
}
+102 -89
View File
@@ -2,25 +2,42 @@ package types
import (
"fmt"
"runtime"
cmn "github.com/tendermint/tmlibs/common"
abci "github.com/tendermint/abci/types"
)
// ABCI Response Code
type CodeType uint32
// ABCICodeType - combined codetype / codespace
type ABCICodeType uint32
// is everything okay?
func (code CodeType) IsOK() bool {
if code == CodeOK {
// CodeType - code identifier within codespace
type CodeType uint16
// CodespaceType - codespace identifier
type CodespaceType uint16
// IsOK - is everything okay?
func (code ABCICodeType) IsOK() bool {
if code == ABCICodeOK {
return true
}
return false
}
// ABCI Response Codes
// Base SDK reserves 0 - 99.
func ToABCICode(space CodespaceType, code CodeType) ABCICodeType {
// TODO: Make Tendermint more aware of codespaces.
if space == CodespaceRoot && code == CodeOK {
return ABCICodeOK
}
return ABCICodeType((uint32(space) << 16) | uint32(code))
}
const (
// ABCI error codes
ABCICodeOK ABCICodeType = 0
// Base error codes
CodeOK CodeType = 0
CodeInternal CodeType = 1
CodeTxDecode CodeType = 2
@@ -34,7 +51,14 @@ const (
CodeInsufficientCoins CodeType = 10
CodeInvalidCoins CodeType = 11
CodeGenesisParse CodeType = 0xdead // TODO: remove ? // why remove?
// CodespaceRoot is a codespace for error codes in this file only.
// Notice that 0 is an "unset" codespace, which can be overridden with
// Error.WithDefaultCodespace().
CodespaceUndefined CodespaceType = 0
CodespaceRoot CodespaceType = 1
// Maximum reservable codespace (2^16 - 1)
MaximumCodespace CodespaceType = 65535
)
// NOTE: Don't stringer this, we'll put better messages in later.
@@ -44,8 +68,6 @@ func CodeToDefaultMsg(code CodeType) string {
return "Internal error"
case CodeTxDecode:
return "Tx parse error"
case CodeGenesisParse:
return "Genesis parse error"
case CodeInvalidSequence:
return "Invalid sequence"
case CodeUnauthorized:
@@ -75,40 +97,37 @@ func CodeToDefaultMsg(code CodeType) string {
// nolint
func ErrInternal(msg string) Error {
return newError(CodeInternal, msg)
return newErrorWithRootCodespace(CodeInternal, msg)
}
func ErrTxDecode(msg string) Error {
return newError(CodeTxDecode, msg)
}
func ErrGenesisParse(msg string) Error {
return newError(CodeGenesisParse, msg)
return newErrorWithRootCodespace(CodeTxDecode, msg)
}
func ErrInvalidSequence(msg string) Error {
return newError(CodeInvalidSequence, msg)
return newErrorWithRootCodespace(CodeInvalidSequence, msg)
}
func ErrUnauthorized(msg string) Error {
return newError(CodeUnauthorized, msg)
return newErrorWithRootCodespace(CodeUnauthorized, msg)
}
func ErrInsufficientFunds(msg string) Error {
return newError(CodeInsufficientFunds, msg)
return newErrorWithRootCodespace(CodeInsufficientFunds, msg)
}
func ErrUnknownRequest(msg string) Error {
return newError(CodeUnknownRequest, msg)
return newErrorWithRootCodespace(CodeUnknownRequest, msg)
}
func ErrInvalidAddress(msg string) Error {
return newError(CodeInvalidAddress, msg)
return newErrorWithRootCodespace(CodeInvalidAddress, msg)
}
func ErrUnknownAddress(msg string) Error {
return newError(CodeUnknownAddress, msg)
return newErrorWithRootCodespace(CodeUnknownAddress, msg)
}
func ErrInvalidPubKey(msg string) Error {
return newError(CodeInvalidPubKey, msg)
return newErrorWithRootCodespace(CodeInvalidPubKey, msg)
}
func ErrInsufficientCoins(msg string) Error {
return newError(CodeInsufficientCoins, msg)
return newErrorWithRootCodespace(CodeInsufficientCoins, msg)
}
func ErrInvalidCoins(msg string) Error {
return newError(CodeInvalidCoins, msg)
return newErrorWithRootCodespace(CodeInvalidCoins, msg)
}
//----------------------------------------
@@ -117,104 +136,98 @@ func ErrInvalidCoins(msg string) Error {
// sdk Error type
type Error interface {
Error() string
ABCICode() CodeType
Code() CodeType
Codespace() CodespaceType
ABCILog() string
ABCICode() ABCICodeType
WithDefaultCodespace(codespace CodespaceType) Error
Trace(msg string) Error
TraceCause(cause error, msg string) Error
Cause() error
T() interface{}
Result() Result
QueryResult() abci.ResponseQuery
}
func NewError(code CodeType, msg string) Error {
return newError(code, msg)
// NewError - create an error
func NewError(codespace CodespaceType, code CodeType, msg string) Error {
return newError(codespace, code, msg)
}
type traceItem struct {
msg string
filename string
lineno int
func newErrorWithRootCodespace(code CodeType, msg string) *sdkError {
return newError(CodespaceRoot, code, msg)
}
func (ti traceItem) String() string {
return fmt.Sprintf("%v:%v %v", ti.filename, ti.lineno, ti.msg)
}
type sdkError struct {
code CodeType
msg string
cause error
traces []traceItem
}
func newError(code CodeType, msg string) *sdkError {
// TODO capture stacktrace if ENV is set.
func newError(codespace CodespaceType, code CodeType, msg string) *sdkError {
if msg == "" {
msg = CodeToDefaultMsg(code)
}
return &sdkError{
code: code,
msg: msg,
cause: nil,
traces: nil,
codespace: codespace,
code: code,
err: cmn.NewErrorWithT(code, msg),
}
}
type sdkError struct {
codespace CodespaceType
code CodeType
err cmn.Error
}
// Implements ABCIError.
func (err *sdkError) Error() string {
return fmt.Sprintf("Error{%d:%s,%v,%v}", err.code, err.msg, err.cause, len(err.traces))
return fmt.Sprintf("Error{%d:%d,%#v}", err.codespace, err.code, err.err)
}
// Implements ABCIError.
func (err *sdkError) ABCICode() CodeType {
func (err *sdkError) ABCICode() ABCICodeType {
return ToABCICode(err.codespace, err.code)
}
// Implements Error.
func (err *sdkError) Codespace() CodespaceType {
return err.codespace
}
// Implements Error.
func (err *sdkError) Code() CodeType {
return err.code
}
// Implements ABCIError.
func (err *sdkError) ABCILog() string {
traceLog := ""
for _, ti := range err.traces {
traceLog += ti.String() + "\n"
}
return fmt.Sprintf("msg: %v\ntrace:\n%v",
err.msg,
traceLog,
)
return fmt.Sprintf(`=== ABCI Log ===
Codespace: %v
Code: %v
ABCICode: %v
Error: %#v
=== /ABCI Log ===
`, err.codespace, err.code, err.ABCICode(), err.err)
}
// Add tracing information with msg.
func (err *sdkError) Trace(msg string) Error {
return err.doTrace(msg, 2)
}
// Add tracing information with cause and msg.
func (err *sdkError) TraceCause(cause error, msg string) Error {
err.cause = cause
return err.doTrace(msg, 2)
}
func (err *sdkError) doTrace(msg string, n int) Error {
_, fn, line, ok := runtime.Caller(n)
if !ok {
if fn == "" {
fn = "<unknown>"
}
if line <= 0 {
line = -1
}
return &sdkError{
codespace: err.codespace,
code: err.code,
err: err.err.Trace(msg),
}
// Include file & line number & msg.
// Do not include the whole stack trace.
err.traces = append(err.traces, traceItem{
filename: fn,
lineno: line,
msg: msg,
})
return err
}
func (err *sdkError) Cause() error {
return err.cause
// Implements Error.
func (err *sdkError) WithDefaultCodespace(cs CodespaceType) Error {
codespace := err.codespace
if codespace == CodespaceUndefined {
codespace = cs
}
return &sdkError{
codespace: codespace,
code: err.code,
err: err.err,
}
}
func (err *sdkError) T() interface{} {
return err.err.T()
}
func (err *sdkError) Result() Result {
+3 -6
View File
@@ -16,7 +16,6 @@ var codeTypes = []CodeType{
CodeUnknownRequest,
CodeUnknownAddress,
CodeInvalidPubKey,
CodeGenesisParse,
}
type errFn func(msg string) Error
@@ -30,14 +29,12 @@ var errFns = []errFn{
ErrUnknownRequest,
ErrUnknownAddress,
ErrInvalidPubKey,
ErrGenesisParse,
}
func TestCodeType(t *testing.T) {
assert.True(t, CodeOK.IsOK())
assert.True(t, ABCICodeOK.IsOK())
for _, c := range codeTypes {
assert.False(t, c.IsOK())
msg := CodeToDefaultMsg(c)
assert.False(t, strings.HasPrefix(msg, "Unknown code"))
}
@@ -47,7 +44,7 @@ func TestErrFn(t *testing.T) {
for i, errFn := range errFns {
err := errFn("")
codeType := codeTypes[i]
assert.Equal(t, err.ABCICode(), codeType)
assert.Equal(t, err.Result().Code, codeType)
assert.Equal(t, err.Code(), codeType)
assert.Equal(t, err.Result().Code, ToABCICode(CodespaceRoot, codeType))
}
}
+4 -4
View File
@@ -97,23 +97,23 @@ func NewRatFromDecimal(decimalStr string) (f Rat, err Error) {
switch len(str) {
case 1:
if len(str[0]) == 0 {
return f, NewError(CodeUnknownRequest, "not a decimal string")
return f, ErrUnknownRequest("not a decimal string")
}
numStr = str[0]
case 2:
if len(str[0]) == 0 || len(str[1]) == 0 {
return f, NewError(CodeUnknownRequest, "not a decimal string")
return f, ErrUnknownRequest("not a decimal string")
}
numStr = str[0] + str[1]
len := int64(len(str[1]))
denom = new(big.Int).Exp(big.NewInt(10), big.NewInt(len), nil).Int64()
default:
return f, NewError(CodeUnknownRequest, "not a decimal string")
return f, ErrUnknownRequest("not a decimal string")
}
num, errConv := strconv.Atoi(numStr)
if errConv != nil {
return f, NewError(CodeUnknownRequest, errConv.Error())
return f, ErrUnknownRequest(errConv.Error())
}
if neg {
+1 -1
View File
@@ -9,7 +9,7 @@ import (
type Result struct {
// Code is the response code, is stored back on the chain.
Code CodeType
Code ABCICodeType
// Data is any data returned from the app.
Data []byte