Skip to content

Commit

Permalink
api: support errors extended information
Browse files Browse the repository at this point in the history
Since Tarantool 2.4.1, iproto error responses contain extended info with
backtrace [1]. After this patch, Error would contain ExtendedInfo field
(BoxError object), if it was provided. Error() handle now will print
extended info, if possible.

1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/#responses-for-errors

Part of #209
  • Loading branch information
DifferentialOrange committed Nov 29, 2022
1 parent 500da07 commit 4e3bdfa
Show file tree
Hide file tree
Showing 9 changed files with 443 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
### Added

- Support iproto feature discovery (#120).
- Support errors extended information (#209).

### Changed

Expand Down
156 changes: 156 additions & 0 deletions box_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package tarantool

import (
"fmt"
)

// BoxError is a type representing Tarantool `box.error` object: a single
// MP_ERROR_STACK object with a link to the previous stack error.
// See https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_error/error/
//
// Since 1.10.0
type BoxError struct {
// Type is error type that implies its source (for example, "ClientError").
Type string
// File is a source code file where the error was caught.
File string
// Line is a number of line in the source code file where the error was caught.
Line uint64
// Msg is the text of reason.
Msg string
// Errno is the ordinal number of the error.
Errno uint64
// Code is the number of the error as defined in `errcode.h`.
Code uint64
// Fields are additional fields depending on error type. For example, if
// type is "AccessDeniedError", then it will include "object_type",
// "object_name", "access_type".
Fields map[string]interface{}
// Prev is the previous error in stack.
Prev *BoxError
}

// Error converts a BoxError to a string.
func (e *BoxError) Error() string {
s := fmt.Sprintf("%s (%s, code 0x%x), see %s line %d",
e.Msg, e.Type, e.Code, e.File, e.Line)

if e.Prev != nil {
return fmt.Sprintf("%s: %s", s, e.Prev)
}

return s
}

// Depth computes the count of errors in stack, including the current one.
func (e *BoxError) Depth() int {
depth := int(0)

cur := e
for cur != nil {
cur = cur.Prev
depth++
}

return depth
}

func decodeBoxError(d *decoder) (*BoxError, error) {
var l, larr, l1, l2 int
var errorStack []BoxError
var err error

if l, err = d.DecodeMapLen(); err != nil {
return nil, err
}

for ; l > 0; l-- {
var cd int
if cd, err = d.DecodeInt(); err != nil {
return nil, err
}
switch cd {
case KeyErrorStack:
if larr, err = d.DecodeArrayLen(); err != nil {
return nil, err
}

errorStack = make([]BoxError, larr)

for i := 0; i < larr; i++ {
if l1, err = d.DecodeMapLen(); err != nil {
return nil, err
}

for ; l1 > 0; l1-- {
var cd1 int
if cd1, err = d.DecodeInt(); err != nil {
return nil, err
}
switch cd1 {
case KeyErrorType:
if errorStack[i].Type, err = d.DecodeString(); err != nil {
return nil, err
}
case KeyErrorFile:
if errorStack[i].File, err = d.DecodeString(); err != nil {
return nil, err
}
case KeyErrorLine:
if errorStack[i].Line, err = d.DecodeUint64(); err != nil {
return nil, err
}
case KeyErrorMessage:
if errorStack[i].Msg, err = d.DecodeString(); err != nil {
return nil, err
}
case KeyErrorErrno:
if errorStack[i].Errno, err = d.DecodeUint64(); err != nil {
return nil, err
}
case KeyErrorErrcode:
if errorStack[i].Code, err = d.DecodeUint64(); err != nil {
return nil, err
}
case KeyErrorFields:
var mapk string
var mapv interface{}

errorStack[i].Fields = make(map[string]interface{})

if l2, err = d.DecodeMapLen(); err != nil {
return nil, err
}
for ; l2 > 0; l2-- {
if mapk, err = d.DecodeString(); err != nil {
return nil, err
}
if mapv, err = d.DecodeInterface(); err != nil {
return nil, err
}
errorStack[i].Fields[mapk] = mapv
}
default:
if err = d.Skip(); err != nil {
return nil, err
}
}
}

if i > 0 {
errorStack[i-1].Prev = &errorStack[i]
}
}
default:
if err = d.Skip(); err != nil {
return nil, err
}
}
}

if len(errorStack) > 0 {
return &errorStack[0], nil
}

return nil, nil
}
77 changes: 77 additions & 0 deletions box_error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package tarantool_test

import (
"testing"

"github.com/stretchr/testify/require"
. "github.com/tarantool/go-tarantool"
)

var samples = map[string]BoxError{
"SimpleError": {
Type: "ClientError",
File: "config.lua",
Line: uint64(202),
Msg: "Unknown error",
Errno: uint64(0),
Code: uint64(0),
},
"AccessDeniedError": {
Type: "AccessDeniedError",
File: "/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c",
Line: uint64(535),
Msg: "Execute access to function 'forbidden_function' is denied for user 'no_grants'",
Errno: uint64(0),
Code: uint64(42),
Fields: map[string]interface{}{
"object_type": "function",
"object_name": "forbidden_function",
"access_type": "Execute",
},
},
"ChainedError": {
Type: "ClientError",
File: "config.lua",
Line: uint64(205),
Msg: "Timeout exceeded",
Errno: uint64(0),
Code: uint64(78),
Prev: &BoxError{
Type: "ClientError",
File: "config.lua",
Line: uint64(202),
Msg: "Unknown error",
Errno: uint64(0),
Code: uint64(0),
},
},
}

var stringCases = map[string]struct {
e BoxError
s string
}{
"SimpleError": {
samples["SimpleError"],
"Unknown error (ClientError, code 0x0), see config.lua line 202",
},
"AccessDeniedError": {
samples["AccessDeniedError"],
"Execute access to function 'forbidden_function' is denied for user " +
"'no_grants' (AccessDeniedError, code 0x2a), see " +
"/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c line 535",
},
"ChainedError": {
samples["ChainedError"],
"Timeout exceeded (ClientError, code 0x4e), see config.lua line 205: " +
"Unknown error (ClientError, code 0x0), see config.lua line 202",
},
}

func TestBoxErrorStringRepr(t *testing.T) {
for name, testcase := range stringCases {
t.Run(name, func(t *testing.T) {
require.Equal(t, testcase.s, testcase.e.Error())
})
}
}
37 changes: 37 additions & 0 deletions config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ box.once("init", function()
-- grants for sql tests
box.schema.user.grant('test', 'create,read,write,drop,alter', 'space')
box.schema.user.grant('test', 'create', 'sequence')

box.schema.user.create('no_grants')
end)

local function func_name()
Expand Down Expand Up @@ -157,6 +159,41 @@ local function push_func(cnt)
end
rawset(_G, 'push_func', push_func)

local function tarantool_version_at_least(wanted_major, wanted_minor, wanted_patch)
-- https://github.com/tarantool/crud/blob/733528be02c1ffa3dacc12c034ee58c9903127fc/test/helper.lua#L316-L337
local major_minor_patch = _TARANTOOL:split('-', 1)[1]
local major_minor_patch_parts = major_minor_patch:split('.', 2)

local major = tonumber(major_minor_patch_parts[1])
local minor = tonumber(major_minor_patch_parts[2])
local patch = tonumber(major_minor_patch_parts[3])

if major < (wanted_major or 0) then return false end
if major > (wanted_major or 0) then return true end

if minor < (wanted_minor or 0) then return false end
if minor > (wanted_minor or 0) then return true end

if patch < (wanted_patch or 0) then return false end
if patch > (wanted_patch or 0) then return true end

return true
end

if tarantool_version_at_least(2, 4, 1) then
local e1 = box.error.new(box.error.UNKNOWN)
local e2 = box.error.new(box.error.TIMEOUT)
e2:set_prev(e1)
rawset(_G, 'chained_error', e2)

local user = box.session.user()
box.schema.func.create('forbidden_function', {body = 'function() end'})
box.session.su('no_grants')
local _, access_denied_error = pcall(function() box.func.forbidden_function:call() end)
box.session.su(user)
rawset(_G, 'access_denied_error', access_denied_error)
end

box.space.test:truncate()

--box.schema.user.revoke('guest', 'read,write,execute', 'universe')
Expand Down
12 changes: 11 additions & 1 deletion const.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ const (
KeyExpression = 0x27
KeyDefTuple = 0x28
KeyData = 0x30
KeyError24 = 0x31
KeyError24 = 0x31 /* Error in pre-2.4 format */
KeyMetaData = 0x32
KeyBindCount = 0x34
KeySQLText = 0x40
KeySQLBind = 0x41
KeySQLInfo = 0x42
KeyStmtID = 0x43
KeyError = 0x52 /* Extended error in >= 2.4 format. */
KeyVersion = 0x54
KeyFeatures = 0x55
KeyTimeout = 0x56
Expand All @@ -56,6 +57,15 @@ const (
KeySQLInfoRowCount = 0x00
KeySQLInfoAutoincrementIds = 0x01

KeyErrorStack = 0x00
KeyErrorType = 0x00
KeyErrorFile = 0x01
KeyErrorLine = 0x02
KeyErrorMessage = 0x03
KeyErrorErrno = 0x04
KeyErrorErrcode = 0x05
KeyErrorFields = 0x06

// https://github.com/fl00r/go-tarantool-1.6/issues/2

IterEq = uint32(0) // key == x ASC order
Expand Down
9 changes: 7 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import "fmt"

// Error is wrapper around error returned by Tarantool.
type Error struct {
Code uint32
Msg string
Code uint32
Msg string
ExtendedInfo *BoxError
}

// Error converts an Error to a string.
func (tnterr Error) Error() string {
if tnterr.ExtendedInfo != nil {
return tnterr.ExtendedInfo.Error()
}

return fmt.Sprintf("%s (0x%x)", tnterr.Msg, tnterr.Code)
}

Expand Down
Loading

0 comments on commit 4e3bdfa

Please sign in to comment.