Skip to content

Commit b02ea20

Browse files
msgpack: support error extension type
Tarantool supports error extension type since version 2.4.1 [1], encoding was introduced in Tarantool 2.10.0 [2]. 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 2. tarantool/tarantool#6433 Part of #232
1 parent 216cc37 commit b02ea20

17 files changed

+577
-98
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
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',]

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/datetime.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
`datetime`_ type id.
1414
"""
1515

16-
def encode(obj):
16+
def encode(obj, _):
1717
"""
1818
Encode a datetime object.
1919
@@ -28,7 +28,7 @@ def encode(obj):
2828

2929
return obj.msgpack_encode()
3030

31-
def decode(data):
31+
def decode(data, _):
3232
"""
3333
Decode a datetime object.
3434

Diff for: tarantool/msgpack_ext/decimal.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def strip_decimal_str(str_repr, scale, first_digit_ind):
225225
# Do not strips zeroes before the decimal point
226226
return str_repr
227227

228-
def encode(obj):
228+
def encode(obj, _):
229229
"""
230230
Encode a decimal object.
231231
@@ -335,7 +335,7 @@ def add_str_digit(digit, digits_reverted, scale):
335335

336336
digits_reverted.append(str(digit))
337337

338-
def decode(data):
338+
def decode(data, _):
339339
"""
340340
Decode a decimal object.
341341

Diff for: tarantool/msgpack_ext/error.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
from tarantool.types import (
10+
encode_box_error,
11+
decode_box_error,
12+
)
13+
14+
EXT_ID = 3
15+
"""
16+
`error`_ type id.
17+
"""
18+
19+
def encode(obj, packer):
20+
"""
21+
Encode an error object.
22+
23+
:param obj: Error to encode.
24+
:type obj: :class:`tarantool.BoxError`
25+
26+
:param packer: msgpack packer to encode error dictionary.
27+
:type packer: :class:`msgpack.Packer`
28+
29+
:return: Encoded error.
30+
:rtype: :obj:`bytes`
31+
"""
32+
33+
err_map = encode_box_error(obj)
34+
return packer.pack(err_map)
35+
36+
def decode(data, unpacker):
37+
"""
38+
Decode an error object.
39+
40+
:param obj: Error to decode.
41+
:type obj: :obj:`bytes`
42+
43+
:param unpacker: msgpack unpacker to decode error dictionary.
44+
:type unpacker: :class:`msgpack.Unpacker`
45+
46+
:return: Decoded error.
47+
:rtype: :class:`tarantool.BoxError`
48+
"""
49+
50+
unpacker.feed(data)
51+
err_map = unpacker.unpack()
52+
return decode_box_error(err_map)

Diff for: tarantool/msgpack_ext/interval.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
`datetime.interval`_ type id.
1414
"""
1515

16-
def encode(obj):
16+
def encode(obj, _):
1717
"""
1818
Encode an interval object.
1919
@@ -28,7 +28,7 @@ def encode(obj):
2828

2929
return obj.msgpack_encode()
3030

31-
def decode(data):
31+
def decode(data, _):
3232
"""
3333
Decode an interval object.
3434

Diff for: tarantool/msgpack_ext/packer.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,36 @@
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
]
2528

26-
def default(obj):
29+
def default(obj, packer=None):
2730
"""
2831
:class:`msgpack.Packer` encoder.
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`
37+
38+
:param packer: msgpack packer to work with common types
39+
(like dictionary in extended error payload)
40+
:type packer: :class:`msgpack.Packer`, optional
3341
3442
:return: Encoded value.
3543
:rtype: :class:`msgpack.ExtType`
@@ -39,5 +47,5 @@ def default(obj):
3947

4048
for encoder in encoders:
4149
if isinstance(obj, encoder['type']):
42-
return ExtType(encoder['ext'].EXT_ID, encoder['ext'].encode(obj))
50+
return ExtType(encoder['ext'].EXT_ID, encoder['ext'].encode(obj, packer))
4351
raise TypeError("Unknown type: %r" % (obj,))

Diff for: tarantool/msgpack_ext/unpacker.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,19 @@
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
}
1820

19-
def ext_hook(code, data):
21+
def ext_hook(code, data, unpacker=None):
2022
"""
2123
:class:`msgpack.Unpacker` decoder.
2224
@@ -26,13 +28,18 @@ def ext_hook(code, data):
2628
:param data: MessagePack extension type data.
2729
:type data: :obj:`bytes`
2830
31+
:param unpacker: msgpack unpacker to work with common types
32+
(like dictionary in extended error payload)
33+
:type unpacker: :class:`msgpack.Unpacker`, optional
34+
2935
:return: Decoded value.
3036
:rtype: :class:`decimal.Decimal` or :class:`uuid.UUID` or
31-
:class:`tarantool.Datetime` or :class:`tarantool.Interval`
37+
or :class:`tarantool.BoxError` or :class:`tarantool.Datetime`
38+
or :class:`tarantool.Interval`
3239
3340
:raise: :exc:`NotImplementedError`
3441
"""
3542

3643
if code in decoders:
37-
return decoders[code](data)
44+
return decoders[code](data, unpacker)
3845
raise NotImplementedError("Unknown msgpack extension type code %d" % (code,))

Diff for: tarantool/msgpack_ext/uuid.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
`uuid`_ type id.
2121
"""
2222

23-
def encode(obj):
23+
def encode(obj, _):
2424
"""
2525
Encode an UUID object.
2626
@@ -33,7 +33,7 @@ def encode(obj):
3333

3434
return obj.bytes
3535

36-
def decode(data):
36+
def decode(data, _):
3737
"""
3838
Decode an UUID object.
3939

Diff for: tarantool/request.py

+62-44
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,67 @@
6363

6464
from tarantool.msgpack_ext.packer import default as packer_default
6565

66+
def build_packer(conn):
67+
"""
68+
Build packer to pack request.
69+
70+
:param conn: Request sender.
71+
:type conn: :class:`~tarantool.Connection`
72+
73+
:rtype: :class:`msgpack.Packer`
74+
"""
75+
76+
packer_kwargs = dict()
77+
78+
# use_bin_type=True is default since msgpack-1.0.0.
79+
#
80+
# The option controls whether to pack binary (non-unicode)
81+
# string values as mp_bin or as mp_str.
82+
#
83+
# The default behaviour of the Python 3 connector (since
84+
# default encoding is "utf-8") is to pack bytes as mp_bin
85+
# and Unicode strings as mp_str. encoding=None mode must
86+
# be used to work with non-utf strings.
87+
#
88+
# encoding = 'utf-8'
89+
#
90+
# Python 3 -> Tarantool -> Python 3
91+
# str -> mp_str (string) -> str
92+
# bytes -> mp_bin (varbinary) -> bytes
93+
#
94+
# encoding = None
95+
#
96+
# Python 3 -> Tarantool -> Python 3
97+
# bytes -> mp_str (string) -> bytes
98+
# str -> mp_str (string) -> bytes
99+
# mp_bin (varbinary) -> bytes
100+
#
101+
# msgpack-0.5.0 (and only this version) warns when the
102+
# option is unset:
103+
#
104+
# | FutureWarning: use_bin_type option is not specified.
105+
# | Default value of the option will be changed in future
106+
# | version.
107+
#
108+
# The option is supported since msgpack-0.4.0, so we can
109+
# just always set it for all msgpack versions to get rid
110+
# of the warning on msgpack-0.5.0 and to keep our
111+
# behaviour on msgpack-1.0.0.
112+
if conn.encoding is None:
113+
packer_kwargs['use_bin_type'] = False
114+
else:
115+
packer_kwargs['use_bin_type'] = True
116+
117+
# We need configured packer to work with error extention
118+
# type payload, but module do not provide access to self
119+
# inside extension type packers.
120+
packer_no_ext = msgpack.Packer(**packer_kwargs)
121+
default = lambda obj: packer_default(obj, packer_no_ext)
122+
packer_kwargs['default'] = default
123+
124+
return msgpack.Packer(**packer_kwargs)
125+
126+
66127
class Request(object):
67128
"""
68129
Represents a single request to the server in compliance with the
@@ -87,50 +148,7 @@ def __init__(self, conn):
87148
self._body = ''
88149
self.response_class = Response
89150

90-
packer_kwargs = dict()
91-
92-
# use_bin_type=True is default since msgpack-1.0.0.
93-
#
94-
# The option controls whether to pack binary (non-unicode)
95-
# string values as mp_bin or as mp_str.
96-
#
97-
# The default behaviour of the Python 3 connector (since
98-
# default encoding is "utf-8") is to pack bytes as mp_bin
99-
# and Unicode strings as mp_str. encoding=None mode must
100-
# be used to work with non-utf strings.
101-
#
102-
# encoding = 'utf-8'
103-
#
104-
# Python 3 -> Tarantool -> Python 3
105-
# str -> mp_str (string) -> str
106-
# bytes -> mp_bin (varbinary) -> bytes
107-
#
108-
# encoding = None
109-
#
110-
# Python 3 -> Tarantool -> Python 3
111-
# bytes -> mp_str (string) -> bytes
112-
# str -> mp_str (string) -> bytes
113-
# mp_bin (varbinary) -> bytes
114-
#
115-
# msgpack-0.5.0 (and only this version) warns when the
116-
# option is unset:
117-
#
118-
# | FutureWarning: use_bin_type option is not specified.
119-
# | Default value of the option will be changed in future
120-
# | version.
121-
#
122-
# The option is supported since msgpack-0.4.0, so we can
123-
# just always set it for all msgpack versions to get rid
124-
# of the warning on msgpack-0.5.0 and to keep our
125-
# behaviour on msgpack-1.0.0.
126-
if conn.encoding is None:
127-
packer_kwargs['use_bin_type'] = False
128-
else:
129-
packer_kwargs['use_bin_type'] = True
130-
131-
packer_kwargs['default'] = packer_default
132-
133-
self.packer = msgpack.Packer(**packer_kwargs)
151+
self.packer = build_packer(conn)
134152

135153
def _dumps(self, src):
136154
"""

0 commit comments

Comments
 (0)