Skip to content

Commit 5ecccf5

Browse files
iproto: support errors extra information
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 6b3a761 commit 5ecccf5

File tree

9 files changed

+166
-6
lines changed

9 files changed

+166
-6
lines changed

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).

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

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

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

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):

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:

tarantool/types.py

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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(err_map):
78+
"""
79+
Decode MessagePack map received from Tarantool to `box.error`_
80+
object representation.
81+
82+
:param err_map: Error MessagePack map received from Tarantool.
83+
:type err_map: :obj:`dict`
84+
85+
:rtype: :class:`~tarantool.BoxError`
86+
87+
:raises: :exc:`KeyError`
88+
"""
89+
90+
encoded_stack = err_map[MP_ERROR_STACK]
91+
stack = []
92+
93+
for item in encoded_stack:
94+
stack.append(BoxErrorStackUnit(
95+
item[MP_ERROR_TYPE],
96+
item[MP_ERROR_FILE],
97+
item[MP_ERROR_LINE],
98+
item[MP_ERROR_MESSAGE],
99+
item[MP_ERROR_ERRNO],
100+
item[MP_ERROR_ERRCODE],
101+
item.get(MP_ERROR_FIELDS), # omitted if empty
102+
))
103+
104+
return BoxError(stack)

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')

test/suites/test_dml.py

+18
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,22 @@ 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(info.file, r'/tarantool')
323+
self.assertTrue(info.line > 0)
324+
self.assertEqual(info.errno, 0)
325+
self.assertEqual(info.errcode, 32)
326+
self.assertEqual(info.fields, None)
327+
else:
328+
self.fail('Expected error')
329+
312330
@classmethod
313331
def tearDownClass(self):
314332
self.con.close()

0 commit comments

Comments
 (0)