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 23, 2022
1 parent 7592b93 commit c0d28bc
Show file tree
Hide file tree
Showing 8 changed files with 391 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
151 changes: 151 additions & 0 deletions box_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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 uint32
// Msg is the text of reason.
Msg string
// Errno is the ordinal number of the error.
Errno uint32
// Code is the number of the error as defined in `errcode.h`.
Code uint32
// 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[interface{}]interface{}
// Prev is the previous error in stack.
Prev *BoxError
}

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

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

depth = 0

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
var mapk, mapv interface{}

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.DecodeUint32(); 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.DecodeUint32(); err != nil {
return nil, err
}
case KeyErrorErrcode:
if errorStack[i].Code, err = d.DecodeUint32(); err != nil {
return nil, err
}
case KeyErrorFields:
errorStack[i].Fields = make(map[interface{}]interface{})
if l2, err = d.DecodeMapLen(); err != nil {
return nil, err
}
for ; l2 > 0; l2-- {
if mapk, err = d.DecodeInterface(); 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
}
39 changes: 39 additions & 0 deletions config.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
local json = require('json')

-- Do not set listen for now so connector won't be
-- able to send requests until everything is configured.
box.cfg{
Expand Down Expand Up @@ -130,6 +132,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 +161,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
15 changes: 13 additions & 2 deletions response.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ func (resp *Response) decodeBody() (err error) {
var stmtID, bindCount uint64
var serverProtocolInfo ProtocolInfo
var feature ProtocolFeature
var errorExtendedInfo *BoxError = nil

d := newDecoder(&resp.buf)

Expand All @@ -172,6 +173,10 @@ func (resp *Response) decodeBody() (err error) {
if resp.Data, ok = res.([]interface{}); !ok {
return fmt.Errorf("result is not array: %v", res)
}
case KeyError:
if errorExtendedInfo, err = decodeBoxError(d); err != nil {
return err
}
case KeyError24:
if resp.Error, err = d.DecodeString(); err != nil {
return err
Expand Down Expand Up @@ -236,7 +241,7 @@ func (resp *Response) decodeBody() (err error) {

if resp.Code != OkCode && resp.Code != PushCode {
resp.Code &^= ErrorCodeBit
err = Error{resp.Code, resp.Error}
err = Error{resp.Code, resp.Error, errorExtendedInfo}
}
}
return
Expand All @@ -247,6 +252,8 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) {
offset := resp.buf.Offset()
defer resp.buf.Seek(offset)

var errorExtendedInfo *BoxError = nil

var l int
d := newDecoder(&resp.buf)
if l, err = d.DecodeMapLen(); err != nil {
Expand All @@ -262,6 +269,10 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) {
if err = d.Decode(res); err != nil {
return err
}
case KeyError:
if errorExtendedInfo, err = decodeBoxError(d); err != nil {
return err
}
case KeyError24:
if resp.Error, err = d.DecodeString(); err != nil {
return err
Expand All @@ -282,7 +293,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) {
}
if resp.Code != OkCode && resp.Code != PushCode {
resp.Code &^= ErrorCodeBit
err = Error{resp.Code, resp.Error}
err = Error{resp.Code, resp.Error, errorExtendedInfo}
}
}
return
Expand Down
Loading

0 comments on commit c0d28bc

Please sign in to comment.