Skip to content

Commit 4e3bdfa

Browse files
api: support errors extended information
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
1 parent 500da07 commit 4e3bdfa

9 files changed

+443
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1111
### Added
1212

1313
- Support iproto feature discovery (#120).
14+
- Support errors extended information (#209).
1415

1516
### Changed
1617

box_error.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package tarantool
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
// BoxError is a type representing Tarantool `box.error` object: a single
8+
// MP_ERROR_STACK object with a link to the previous stack error.
9+
// See https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_error/error/
10+
//
11+
// Since 1.10.0
12+
type BoxError struct {
13+
// Type is error type that implies its source (for example, "ClientError").
14+
Type string
15+
// File is a source code file where the error was caught.
16+
File string
17+
// Line is a number of line in the source code file where the error was caught.
18+
Line uint64
19+
// Msg is the text of reason.
20+
Msg string
21+
// Errno is the ordinal number of the error.
22+
Errno uint64
23+
// Code is the number of the error as defined in `errcode.h`.
24+
Code uint64
25+
// Fields are additional fields depending on error type. For example, if
26+
// type is "AccessDeniedError", then it will include "object_type",
27+
// "object_name", "access_type".
28+
Fields map[string]interface{}
29+
// Prev is the previous error in stack.
30+
Prev *BoxError
31+
}
32+
33+
// Error converts a BoxError to a string.
34+
func (e *BoxError) Error() string {
35+
s := fmt.Sprintf("%s (%s, code 0x%x), see %s line %d",
36+
e.Msg, e.Type, e.Code, e.File, e.Line)
37+
38+
if e.Prev != nil {
39+
return fmt.Sprintf("%s: %s", s, e.Prev)
40+
}
41+
42+
return s
43+
}
44+
45+
// Depth computes the count of errors in stack, including the current one.
46+
func (e *BoxError) Depth() int {
47+
depth := int(0)
48+
49+
cur := e
50+
for cur != nil {
51+
cur = cur.Prev
52+
depth++
53+
}
54+
55+
return depth
56+
}
57+
58+
func decodeBoxError(d *decoder) (*BoxError, error) {
59+
var l, larr, l1, l2 int
60+
var errorStack []BoxError
61+
var err error
62+
63+
if l, err = d.DecodeMapLen(); err != nil {
64+
return nil, err
65+
}
66+
67+
for ; l > 0; l-- {
68+
var cd int
69+
if cd, err = d.DecodeInt(); err != nil {
70+
return nil, err
71+
}
72+
switch cd {
73+
case KeyErrorStack:
74+
if larr, err = d.DecodeArrayLen(); err != nil {
75+
return nil, err
76+
}
77+
78+
errorStack = make([]BoxError, larr)
79+
80+
for i := 0; i < larr; i++ {
81+
if l1, err = d.DecodeMapLen(); err != nil {
82+
return nil, err
83+
}
84+
85+
for ; l1 > 0; l1-- {
86+
var cd1 int
87+
if cd1, err = d.DecodeInt(); err != nil {
88+
return nil, err
89+
}
90+
switch cd1 {
91+
case KeyErrorType:
92+
if errorStack[i].Type, err = d.DecodeString(); err != nil {
93+
return nil, err
94+
}
95+
case KeyErrorFile:
96+
if errorStack[i].File, err = d.DecodeString(); err != nil {
97+
return nil, err
98+
}
99+
case KeyErrorLine:
100+
if errorStack[i].Line, err = d.DecodeUint64(); err != nil {
101+
return nil, err
102+
}
103+
case KeyErrorMessage:
104+
if errorStack[i].Msg, err = d.DecodeString(); err != nil {
105+
return nil, err
106+
}
107+
case KeyErrorErrno:
108+
if errorStack[i].Errno, err = d.DecodeUint64(); err != nil {
109+
return nil, err
110+
}
111+
case KeyErrorErrcode:
112+
if errorStack[i].Code, err = d.DecodeUint64(); err != nil {
113+
return nil, err
114+
}
115+
case KeyErrorFields:
116+
var mapk string
117+
var mapv interface{}
118+
119+
errorStack[i].Fields = make(map[string]interface{})
120+
121+
if l2, err = d.DecodeMapLen(); err != nil {
122+
return nil, err
123+
}
124+
for ; l2 > 0; l2-- {
125+
if mapk, err = d.DecodeString(); err != nil {
126+
return nil, err
127+
}
128+
if mapv, err = d.DecodeInterface(); err != nil {
129+
return nil, err
130+
}
131+
errorStack[i].Fields[mapk] = mapv
132+
}
133+
default:
134+
if err = d.Skip(); err != nil {
135+
return nil, err
136+
}
137+
}
138+
}
139+
140+
if i > 0 {
141+
errorStack[i-1].Prev = &errorStack[i]
142+
}
143+
}
144+
default:
145+
if err = d.Skip(); err != nil {
146+
return nil, err
147+
}
148+
}
149+
}
150+
151+
if len(errorStack) > 0 {
152+
return &errorStack[0], nil
153+
}
154+
155+
return nil, nil
156+
}

box_error_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package tarantool_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
. "github.com/tarantool/go-tarantool"
8+
)
9+
10+
var samples = map[string]BoxError{
11+
"SimpleError": {
12+
Type: "ClientError",
13+
File: "config.lua",
14+
Line: uint64(202),
15+
Msg: "Unknown error",
16+
Errno: uint64(0),
17+
Code: uint64(0),
18+
},
19+
"AccessDeniedError": {
20+
Type: "AccessDeniedError",
21+
File: "/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c",
22+
Line: uint64(535),
23+
Msg: "Execute access to function 'forbidden_function' is denied for user 'no_grants'",
24+
Errno: uint64(0),
25+
Code: uint64(42),
26+
Fields: map[string]interface{}{
27+
"object_type": "function",
28+
"object_name": "forbidden_function",
29+
"access_type": "Execute",
30+
},
31+
},
32+
"ChainedError": {
33+
Type: "ClientError",
34+
File: "config.lua",
35+
Line: uint64(205),
36+
Msg: "Timeout exceeded",
37+
Errno: uint64(0),
38+
Code: uint64(78),
39+
Prev: &BoxError{
40+
Type: "ClientError",
41+
File: "config.lua",
42+
Line: uint64(202),
43+
Msg: "Unknown error",
44+
Errno: uint64(0),
45+
Code: uint64(0),
46+
},
47+
},
48+
}
49+
50+
var stringCases = map[string]struct {
51+
e BoxError
52+
s string
53+
}{
54+
"SimpleError": {
55+
samples["SimpleError"],
56+
"Unknown error (ClientError, code 0x0), see config.lua line 202",
57+
},
58+
"AccessDeniedError": {
59+
samples["AccessDeniedError"],
60+
"Execute access to function 'forbidden_function' is denied for user " +
61+
"'no_grants' (AccessDeniedError, code 0x2a), see " +
62+
"/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c line 535",
63+
},
64+
"ChainedError": {
65+
samples["ChainedError"],
66+
"Timeout exceeded (ClientError, code 0x4e), see config.lua line 205: " +
67+
"Unknown error (ClientError, code 0x0), see config.lua line 202",
68+
},
69+
}
70+
71+
func TestBoxErrorStringRepr(t *testing.T) {
72+
for name, testcase := range stringCases {
73+
t.Run(name, func(t *testing.T) {
74+
require.Equal(t, testcase.s, testcase.e.Error())
75+
})
76+
}
77+
}

config.lua

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ box.once("init", function()
130130
-- grants for sql tests
131131
box.schema.user.grant('test', 'create,read,write,drop,alter', 'space')
132132
box.schema.user.grant('test', 'create', 'sequence')
133+
134+
box.schema.user.create('no_grants')
133135
end)
134136

135137
local function func_name()
@@ -157,6 +159,41 @@ local function push_func(cnt)
157159
end
158160
rawset(_G, 'push_func', push_func)
159161

162+
local function tarantool_version_at_least(wanted_major, wanted_minor, wanted_patch)
163+
-- https://github.com/tarantool/crud/blob/733528be02c1ffa3dacc12c034ee58c9903127fc/test/helper.lua#L316-L337
164+
local major_minor_patch = _TARANTOOL:split('-', 1)[1]
165+
local major_minor_patch_parts = major_minor_patch:split('.', 2)
166+
167+
local major = tonumber(major_minor_patch_parts[1])
168+
local minor = tonumber(major_minor_patch_parts[2])
169+
local patch = tonumber(major_minor_patch_parts[3])
170+
171+
if major < (wanted_major or 0) then return false end
172+
if major > (wanted_major or 0) then return true end
173+
174+
if minor < (wanted_minor or 0) then return false end
175+
if minor > (wanted_minor or 0) then return true end
176+
177+
if patch < (wanted_patch or 0) then return false end
178+
if patch > (wanted_patch or 0) then return true end
179+
180+
return true
181+
end
182+
183+
if tarantool_version_at_least(2, 4, 1) then
184+
local e1 = box.error.new(box.error.UNKNOWN)
185+
local e2 = box.error.new(box.error.TIMEOUT)
186+
e2:set_prev(e1)
187+
rawset(_G, 'chained_error', e2)
188+
189+
local user = box.session.user()
190+
box.schema.func.create('forbidden_function', {body = 'function() end'})
191+
box.session.su('no_grants')
192+
local _, access_denied_error = pcall(function() box.func.forbidden_function:call() end)
193+
box.session.su(user)
194+
rawset(_G, 'access_denied_error', access_denied_error)
195+
end
196+
160197
box.space.test:truncate()
161198

162199
--box.schema.user.revoke('guest', 'read,write,execute', 'universe')

const.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ const (
3535
KeyExpression = 0x27
3636
KeyDefTuple = 0x28
3737
KeyData = 0x30
38-
KeyError24 = 0x31
38+
KeyError24 = 0x31 /* Error in pre-2.4 format */
3939
KeyMetaData = 0x32
4040
KeyBindCount = 0x34
4141
KeySQLText = 0x40
4242
KeySQLBind = 0x41
4343
KeySQLInfo = 0x42
4444
KeyStmtID = 0x43
45+
KeyError = 0x52 /* Extended error in >= 2.4 format. */
4546
KeyVersion = 0x54
4647
KeyFeatures = 0x55
4748
KeyTimeout = 0x56
@@ -56,6 +57,15 @@ const (
5657
KeySQLInfoRowCount = 0x00
5758
KeySQLInfoAutoincrementIds = 0x01
5859

60+
KeyErrorStack = 0x00
61+
KeyErrorType = 0x00
62+
KeyErrorFile = 0x01
63+
KeyErrorLine = 0x02
64+
KeyErrorMessage = 0x03
65+
KeyErrorErrno = 0x04
66+
KeyErrorErrcode = 0x05
67+
KeyErrorFields = 0x06
68+
5969
// https://github.com/fl00r/go-tarantool-1.6/issues/2
6070

6171
IterEq = uint32(0) // key == x ASC order

errors.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import "fmt"
44

55
// Error is wrapper around error returned by Tarantool.
66
type Error struct {
7-
Code uint32
8-
Msg string
7+
Code uint32
8+
Msg string
9+
ExtendedInfo *BoxError
910
}
1011

1112
// Error converts an Error to a string.
1213
func (tnterr Error) Error() string {
14+
if tnterr.ExtendedInfo != nil {
15+
return tnterr.ExtendedInfo.Error()
16+
}
17+
1318
return fmt.Sprintf("%s (0x%x)", tnterr.Msg, tnterr.Code)
1419
}
1520

0 commit comments

Comments
 (0)