Skip to content

Commit e2ade05

Browse files
msgpack: support error extension type
Tarantool supports error extension type since version 2.4.1 [1]. This patch introduced the support of Tarantool error extension type in msgpack decoders and encoders. Tarantool error extension type objects are decoded to `tarantool.BoxError` type. `tarantool.BoxError` may be encoded to Tarantool error extension type objects. Error extension type internals are the same as errors extra information: the only difference is that extra information is encoded as a separate error dictionary field and error extension type objects is encoded as MessagePack extension type objects. The only way to receive an error extension type object from Tarantool is to receive an explicitly built `box.error` object: either from `return box.error.new(...)` or a tuple with it. All errors raised within Tarantool (including those raised with `box.error(...)`) are encoded based on the same rules as simple errors due to backward compatibility. It is possible to create error extension type objects with Python code, but it not likely to be really useful since most of their fields is computed on error initialization on server side (even for custom error types): ``` tarantool.BoxError([ tarantool.BoxErrorStackUnit( type='ClientError', file='[string " local err = box.error.ne..."]', line=1, message='Unknown error', errno=0, errcode=0, ) ]) ``` 1. tarantool/tarantool#4398 Part of #232
1 parent 1e53ee1 commit e2ade05

File tree

11 files changed

+399
-8
lines changed

11 files changed

+399
-8
lines changed

Diff for: doc/dev-guide.rst

+3
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ they are represented with in-built and custom types:
8383
+-----------------------------+----+-------------+----+-----------------------------+
8484
| :obj:`uuid.UUID` | -> | `UUID`_ | -> | :obj:`uuid.UUID` |
8585
+-----------------------------+----+-------------+----+-----------------------------+
86+
| :class:`tarantool.BoxError` | -> | `ERROR`_ | -> | :class:`tarantool.BoxError` |
87+
+-----------------------------+----+-------------+----+-----------------------------+
8688
| :class:`tarantool.Datetime` | -> | `DATETIME`_ | -> | :class:`tarantool.Datetime` |
8789
+-----------------------------+----+-------------+----+-----------------------------+
8890
| :class:`tarantool.Interval` | -> | `INTERVAL`_ | -> | :class:`tarantool.Interval` |
@@ -109,5 +111,6 @@ and iterate through it as with any other serializable object.
109111
.. _extension types: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/
110112
.. _DECIMAL: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type
111113
.. _UUID: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-uuid-type
114+
.. _ERROR: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-error-type
112115
.. _DATETIME: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type
113116
.. _INTERVAL: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-interval-type

Diff for: tarantool/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242

4343
from tarantool.connection_pool import ConnectionPool, Mode
4444

45+
from tarantool.types import BoxError, BoxErrorStackUnit
46+
4547
__version__ = "0.9.0"
4648

4749

@@ -136,4 +138,4 @@ def connectmesh(addrs=({'host': 'localhost', 'port': 3301},), user=None,
136138
__all__ = ['connect', 'Connection', 'connectmesh', 'MeshConnection', 'Schema',
137139
'Error', 'DatabaseError', 'NetworkError', 'NetworkWarning',
138140
'SchemaError', 'dbapi', 'Datetime', 'Interval', 'IntervalAdjust',
139-
'ConnectionPool', 'Mode']
141+
'ConnectionPool', 'Mode', 'BoxError', 'BoxErrorStackUnit']

Diff for: tarantool/const.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,4 @@
129129
# Tarantool 2.10 protocol version is 3
130130
CONNECTOR_IPROTO_VERSION = 3
131131
# List of connector-supported features
132-
CONNECTOR_FEATURES = []
132+
CONNECTOR_FEATURES = [IPROTO_FEATURE_ERROR_EXTENSION,]

Diff for: tarantool/msgpack_ext/error.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""
2+
Tarantool `error`_ extension type support module.
3+
4+
Refer to :mod:`~tarantool.msgpack_ext.types.error`.
5+
6+
.. _error: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-error-type
7+
"""
8+
9+
import msgpack
10+
11+
from tarantool.types import (
12+
encode_box_error,
13+
decode_box_error,
14+
)
15+
16+
EXT_ID = 3
17+
"""
18+
`error`_ type id.
19+
"""
20+
21+
def encode(obj):
22+
"""
23+
Encode an error object.
24+
25+
:param obj: Error to encode.
26+
:type obj: :class:`tarantool.BoxError`
27+
28+
:return: Encoded error.
29+
:rtype: :obj:`bytes`
30+
"""
31+
32+
err_map = encode_box_error(obj)
33+
return msgpack.packb(err_map)
34+
35+
def decode(data):
36+
"""
37+
Decode an error object.
38+
39+
:param obj: Error to decode.
40+
:type obj: :obj:`bytes`
41+
42+
:return: Decoded error.
43+
:rtype: :class:`tarantool.BoxError`
44+
"""
45+
46+
err_map = msgpack.unpackb(data, strict_map_key=False)
47+
return decode_box_error(err_map)

Diff for: tarantool/msgpack_ext/packer.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@
88
from uuid import UUID
99
from msgpack import ExtType
1010

11+
from tarantool.types import BoxError
1112
from tarantool.msgpack_ext.types.datetime import Datetime
1213
from tarantool.msgpack_ext.types.interval import Interval
1314

1415
import tarantool.msgpack_ext.decimal as ext_decimal
1516
import tarantool.msgpack_ext.uuid as ext_uuid
17+
import tarantool.msgpack_ext.error as ext_error
1618
import tarantool.msgpack_ext.datetime as ext_datetime
1719
import tarantool.msgpack_ext.interval as ext_interval
1820

1921
encoders = [
2022
{'type': Decimal, 'ext': ext_decimal },
2123
{'type': UUID, 'ext': ext_uuid },
24+
{'type': BoxError, 'ext': ext_error },
2225
{'type': Datetime, 'ext': ext_datetime},
2326
{'type': Interval, 'ext': ext_interval},
2427
]
@@ -29,7 +32,8 @@ def default(obj):
2932
3033
:param obj: Object to encode.
3134
:type obj: :class:`decimal.Decimal` or :class:`uuid.UUID` or
32-
:class:`tarantool.Datetime` or :class:`tarantool.Interval`
35+
or :class:`tarantool.BoxError` or :class:`tarantool.Datetime`
36+
or :class:`tarantool.Interval`
3337
3438
:return: Encoded value.
3539
:rtype: :class:`msgpack.ExtType`

Diff for: tarantool/msgpack_ext/unpacker.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
import tarantool.msgpack_ext.decimal as ext_decimal
88
import tarantool.msgpack_ext.uuid as ext_uuid
9+
import tarantool.msgpack_ext.error as ext_error
910
import tarantool.msgpack_ext.datetime as ext_datetime
1011
import tarantool.msgpack_ext.interval as ext_interval
1112

1213
decoders = {
1314
ext_decimal.EXT_ID : ext_decimal.decode ,
1415
ext_uuid.EXT_ID : ext_uuid.decode ,
16+
ext_error.EXT_ID : ext_error.decode ,
1517
ext_datetime.EXT_ID: ext_datetime.decode,
1618
ext_interval.EXT_ID: ext_interval.decode,
1719
}
@@ -28,7 +30,8 @@ def ext_hook(code, data):
2830
2931
:return: Decoded value.
3032
:rtype: :class:`decimal.Decimal` or :class:`uuid.UUID` or
31-
:class:`tarantool.Datetime` or :class:`tarantool.Interval`
33+
or :class:`tarantool.BoxError` or :class:`tarantool.Datetime`
34+
or :class:`tarantool.Interval`
3235
3336
:raise: :exc:`NotImplementedError`
3437
"""

Diff for: tarantool/types.py

+32-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class BoxErrorStackUnit():
4747
fields: typing.Optional[dict] = None
4848
"""
4949
Additional fields depending on error type. For example, if
50-
:attr:`~tarantool.types.BoxErrorStackComponent.type`
50+
:attr:`~tarantool.BoxErrorStackComponent.type`
5151
is ``"AccessDeniedError"``, then it will include ``"object_type"``,
5252
``"object_name"``, ``"access_type"``.
5353
"""
@@ -62,7 +62,7 @@ class BoxError():
6262

6363
stack: list
6464
"""
65-
Array of :class:`~tarantool.types.BoxErrorStackUnit` objects.
65+
Array of :class:`~tarantool.BoxErrorStackUnit` objects.
6666
"""
6767

6868
MP_ERROR_STACK = 0x00
@@ -102,3 +102,33 @@ def decode_box_error(err_map):
102102
))
103103

104104
return BoxError(stack)
105+
106+
def encode_box_error(err):
107+
"""
108+
Encode Python `box.error`_ representation to MessagePack map.
109+
110+
:param err: Error to encode
111+
:type err: :obj:`tarantool.BoxError`
112+
113+
:rtype: :obj:`dict`
114+
115+
:raises: :exc:`KeyError`
116+
"""
117+
118+
stack = []
119+
for item in err.stack:
120+
dict_item = {
121+
MP_ERROR_TYPE: item.type,
122+
MP_ERROR_FILE: item.file,
123+
MP_ERROR_LINE: item.line,
124+
MP_ERROR_MESSAGE: item.message,
125+
MP_ERROR_ERRNO: item.errno,
126+
MP_ERROR_ERRCODE: item.errcode,
127+
}
128+
129+
if item.fields is not None: # omitted if empty
130+
dict_item[MP_ERROR_FIELDS] = item.fields
131+
132+
stack.append(dict_item)
133+
134+
return {MP_ERROR_STACK: stack}

Diff for: test/suites/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
from .test_uuid import TestSuite_UUID
2020
from .test_datetime import TestSuite_Datetime
2121
from .test_interval import TestSuite_Interval
22+
from .test_error_ext import TestSuite_ErrorExt
2223

2324
test_cases = (TestSuite_Schema_UnicodeConnection,
2425
TestSuite_Schema_BinaryConnection,
2526
TestSuite_Request, TestSuite_Protocol, TestSuite_Reconnect,
2627
TestSuite_Mesh, TestSuite_Execute, TestSuite_DBAPI,
2728
TestSuite_Encoding, TestSuite_Pool, TestSuite_Ssl,
2829
TestSuite_Decimal, TestSuite_UUID, TestSuite_Datetime,
29-
TestSuite_Interval)
30+
TestSuite_Interval, TestSuite_ErrorExt,)
3031

3132
def load_tests(loader, tests, pattern):
3233
suite = unittest.TestSuite()

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

+11
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,14 @@ def skip_or_run_error_extra_info_test(func):
165165

166166
return skip_or_run_test_tarantool(func, '2.4.1',
167167
'does not provide extra error info')
168+
169+
def skip_or_run_error_ext_type_test(func):
170+
"""Decorator to skip or run tests related to error extension
171+
type depending on the tarantool version.
172+
173+
Tarantool supports error extension type only since 2.4.1 version.
174+
See https://github.com/tarantool/tarantool/issues/4398
175+
"""
176+
177+
return skip_or_run_test_tarantool(func, '2.4.1',
178+
'does not support error extension type')

0 commit comments

Comments
 (0)