Skip to content

Commit 4b35e4c

Browse files
api: extra information for iproto errors
Since Tarantool 2.4.1, iproto error responses contain extra info with backtrace. After this patch, DatabaseError would contain `extra_info` property, if it was provided. 1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/#responses-for-errors Part of #323
1 parent bd42b92 commit 4b35e4c

File tree

9 files changed

+166
-6
lines changed

9 files changed

+166
-6
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
136136

137137
- Support iproto feature discovery (#206).
138138
- Backport ConnectionPool support for Python 3.6 (#232).
139+
- Support extra information for iproto errors (#232).
139140

140141
### Changed
141142
- Bump msgpack requirement to 1.0.4 (PR #223).

Diff for: doc/api/submodule-types.rst

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module :py:mod:`tarantool.types`
2+
================================
3+
4+
.. automodule:: tarantool.types

Diff for: doc/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ API Reference
4343
api/submodule-response.rst
4444
api/submodule-schema.rst
4545
api/submodule-space.rst
46+
api/submodule-types.rst
4647
api/submodule-utils.rst
4748

4849
.. Indices and tables

Diff for: tarantool/const.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
IPROTO_OPS = 0x28
2828
#
2929
IPROTO_DATA = 0x30
30-
IPROTO_ERROR = 0x31
30+
IPROTO_ERROR_24 = 0x31
3131
#
3232
IPROTO_METADATA = 0x32
3333
IPROTO_SQL_TEXT = 0x40
@@ -36,6 +36,8 @@
3636
IPROTO_SQL_INFO_ROW_COUNT = 0x00
3737
IPROTO_SQL_INFO_AUTOINCREMENT_IDS = 0x01
3838
#
39+
IPROTO_ERROR = 0x52
40+
#
3941
IPROTO_VERSION = 0x54
4042
IPROTO_FEATURES = 0x55
4143

Diff for: tarantool/error.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,17 @@ class DatabaseError(Error):
4141
Exception raised for errors that are related to the database.
4242
"""
4343

44-
def __init__(self, *args):
44+
def __init__(self, *args, extra_info=None):
4545
"""
4646
:param args: ``(code, message)`` or ``(message,)``.
4747
:type args: :obj:`tuple`
48+
49+
:param extra_info: Additional `box.error`_ information
50+
with backtrace.
51+
:type extra_info: :class:`~tarantool.types.BoxError` or
52+
:obj:`None`, optional
53+
54+
.. _box.error: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_error/error/
4855
"""
4956

5057
super().__init__(*args)
@@ -59,6 +66,8 @@ def __init__(self, *args):
5966
self.code = 0
6067
self.message = ''
6168

69+
self.extra_info = extra_info
70+
6271

6372
class DataError(DatabaseError):
6473
"""
@@ -235,7 +244,7 @@ class NetworkError(DatabaseError):
235244
Error related to network.
236245
"""
237246

238-
def __init__(self, orig_exception=None, *args):
247+
def __init__(self, orig_exception=None, *args, **kwargs):
239248
"""
240249
:param orig_exception: Exception to wrap.
241250
:type orig_exception: optional
@@ -256,7 +265,7 @@ def __init__(self, orig_exception=None, *args):
256265
super(NetworkError, self).__init__(
257266
orig_exception.errno, self.message)
258267
else:
259-
super(NetworkError, self).__init__(orig_exception, *args)
268+
super(NetworkError, self).__init__(orig_exception, *args, **kwargs)
260269

261270

262271
class NetworkWarning(UserWarning):

Diff for: tarantool/response.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from tarantool.const import (
1212
IPROTO_REQUEST_TYPE,
1313
IPROTO_DATA,
14+
IPROTO_ERROR_24,
1415
IPROTO_ERROR,
1516
IPROTO_SYNC,
1617
IPROTO_SCHEMA_ID,
@@ -21,6 +22,7 @@
2122
IPROTO_VERSION,
2223
IPROTO_FEATURES,
2324
)
25+
from tarantool.types import decode_box_error
2426
from tarantool.error import (
2527
DatabaseError,
2628
InterfaceError,
@@ -117,14 +119,22 @@ def __init__(self, conn, response):
117119
# self.append(self._data)
118120
else:
119121
# Separate return_code and completion_code
120-
self._return_message = self._body.get(IPROTO_ERROR, "")
122+
self._return_message = self._body.get(IPROTO_ERROR_24, "")
121123
self._return_code = self._code & (REQUEST_TYPE_ERROR - 1)
124+
125+
self._return_error = None
126+
return_error_map = self._body.get(IPROTO_ERROR)
127+
if return_error_map is not None:
128+
self._return_error = decode_box_error(return_error_map)
129+
122130
self._data = []
123131
if self._return_code == 109:
124132
raise SchemaReloadException(self._return_message,
125133
self._schema_version)
126134
if self.conn.error:
127-
raise DatabaseError(self._return_code, self._return_message)
135+
raise DatabaseError(self._return_code,
136+
self._return_message,
137+
extra_info=self._return_error)
128138

129139
def __getitem__(self, idx):
130140
if self._data is None:

Diff for: tarantool/types.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
Additional Tarantool type definitions.
3+
"""
4+
5+
import typing
6+
from dataclasses import dataclass
7+
8+
@dataclass
9+
class BoxErrorStackUnit():
10+
"""
11+
Type representing Tarantool `box.error`_ single MP_ERROR_STACK
12+
object.
13+
14+
.. _box.error: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_error/error/
15+
"""
16+
17+
type: str
18+
"""
19+
Type that implies source, for example ``"ClientError"``.
20+
"""
21+
22+
file: str
23+
"""
24+
Source code file where error was caught.
25+
"""
26+
27+
line: int
28+
"""
29+
Line number in source code file.
30+
"""
31+
32+
message: str
33+
"""
34+
Text of reason.
35+
"""
36+
37+
errno: int
38+
"""
39+
Ordinal number of the error.
40+
"""
41+
42+
errcode: int
43+
"""
44+
Number of the error as defined in ``errcode.h``.
45+
"""
46+
47+
fields: typing.Optional[dict] = None
48+
"""
49+
Additional fields depending on error type. For example, if
50+
:attr:`~tarantool.types.BoxErrorStackComponent.type`
51+
is ``"AccessDeniedError"``, then it will include ``"object_type"``,
52+
``"object_name"``, ``"access_type"``.
53+
"""
54+
55+
@dataclass
56+
class BoxError():
57+
"""
58+
Type representing Tarantool `box.error`_ object.
59+
60+
.. _box.error: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_error/error/
61+
"""
62+
63+
stack: list
64+
"""
65+
Array of :class:`~tarantool.types.BoxErrorStackUnit` objects.
66+
"""
67+
68+
MP_ERROR_STACK = 0x00
69+
MP_ERROR_TYPE = 0x00
70+
MP_ERROR_FILE = 0x01
71+
MP_ERROR_LINE = 0x02
72+
MP_ERROR_MESSAGE = 0x03
73+
MP_ERROR_ERRNO = 0x04
74+
MP_ERROR_ERRCODE = 0x05
75+
MP_ERROR_FIELDS = 0x06
76+
77+
def decode_box_error(mp_map):
78+
"""
79+
Decode MessagePack map received from Tarantool to `box.error`_
80+
object representation.
81+
82+
:param mp_map: Error MessagePack map received from Tarantool.
83+
:type mp_map: :obj:`dict`
84+
85+
:rtype: :class:`~tarantool.types.BoxError`
86+
"""
87+
88+
encoded_stack = mp_map.get(MP_ERROR_STACK, [])
89+
stack = []
90+
91+
for item in encoded_stack:
92+
stack.append(BoxErrorStackUnit(
93+
item.get(MP_ERROR_TYPE),
94+
item.get(MP_ERROR_FILE),
95+
item.get(MP_ERROR_LINE),
96+
item.get(MP_ERROR_MESSAGE),
97+
item.get(MP_ERROR_ERRNO),
98+
item.get(MP_ERROR_ERRCODE),
99+
item.get(MP_ERROR_FIELDS),
100+
))
101+
102+
return BoxError(stack)

Diff for: test/suites/lib/skip.py

+11
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,14 @@ def skip_or_run_datetime_test(func):
154154

155155
return skip_or_run_test_pcall_require(func, 'datetime',
156156
'does not support datetime type')
157+
158+
def skip_or_run_error_extra_info_test(func):
159+
"""Decorator to skip or run tests related to extra error info
160+
provided over iproto depending on the tarantool version.
161+
162+
Tarantool provides extra error info only since 2.4.1 version.
163+
See https://github.com/tarantool/tarantool/issues/4398
164+
"""
165+
166+
return skip_or_run_test_tarantool(func, '2.4.1',
167+
'does not provide extra error info')

Diff for: test/suites/test_dml.py

+20
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import sys
22
import unittest
33
import tarantool
4+
from tarantool.error import DatabaseError
45

56
from .lib.tarantool_server import TarantoolServer
7+
from .lib.skip import skip_or_run_error_extra_info_test
68

79
class TestSuite_Request(unittest.TestCase):
810
@classmethod
@@ -309,6 +311,24 @@ def test_13_unix_socket_connect(self):
309311
self.sock_con = tarantool.connect(self.sock_srv.host, self.sock_srv.args['primary'])
310312
self.assertEqual(self.sock_con.ping(notime=True), "Success")
311313

314+
@skip_or_run_error_extra_info_test
315+
def test_14_extra_error_info(self):
316+
try:
317+
self.con.eval("not a Lua code")
318+
except DatabaseError as exc:
319+
self.assertEqual(len(exc.extra_info.stack), 1)
320+
info = exc.extra_info.stack[0]
321+
self.assertEqual(info.type, 'LuajitError')
322+
self.assertRegex(
323+
info.file,
324+
r'/__w/sdk/sdk/tarantool-(?:\d+\.\d+)/tarantool/src/box/lua/call\.c')
325+
self.assertTrue(info.line > 0)
326+
self.assertEqual(info.errno, 0)
327+
self.assertEqual(info.errcode, 32)
328+
self.assertEqual(info.fields, None)
329+
else:
330+
self.fail('Expected error')
331+
312332
@classmethod
313333
def tearDownClass(self):
314334
self.con.close()

0 commit comments

Comments
 (0)