Skip to content

Commit f68f147

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 f68f147

File tree

13 files changed

+569
-90
lines changed

13 files changed

+569
-90
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/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/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/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
"""

Diff for: tarantool/response.py

+53-35
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,58 @@
3232

3333
from tarantool.msgpack_ext.unpacker import ext_hook as unpacker_ext_hook
3434

35+
def build_unpacker(conn):
36+
"""
37+
Build unpacker to unpack request response.
38+
39+
:param conn: Request sender.
40+
:type conn: :class:`~tarantool.Connection`
41+
42+
:rtype: :class:`msgpack.Unpacker`
43+
"""
44+
45+
unpacker_kwargs = dict()
46+
47+
# Decode MsgPack arrays into Python lists by default (not tuples).
48+
# Can be configured in the Connection init
49+
unpacker_kwargs['use_list'] = conn.use_list
50+
51+
# Use raw=False instead of encoding='utf-8'.
52+
if msgpack.version >= (0, 5, 2) and conn.encoding == 'utf-8':
53+
# Get rid of the following warning.
54+
# > PendingDeprecationWarning: encoding is deprecated,
55+
# > Use raw=False instead.
56+
unpacker_kwargs['raw'] = False
57+
elif conn.encoding is not None:
58+
unpacker_kwargs['encoding'] = conn.encoding
59+
60+
# raw=False is default since msgpack-1.0.0.
61+
#
62+
# The option decodes mp_str to bytes, not a Unicode
63+
# string (when True).
64+
if msgpack.version >= (1, 0, 0) and conn.encoding is None:
65+
unpacker_kwargs['raw'] = True
66+
67+
# encoding option is not supported since msgpack-1.0.0,
68+
# but it is handled in the Connection constructor.
69+
assert(msgpack.version < (1, 0, 0) or conn.encoding in (None, 'utf-8'))
70+
71+
# strict_map_key=True is default since msgpack-1.0.0.
72+
#
73+
# The option forbids non-string keys in a map (when True).
74+
if msgpack.version >= (1, 0, 0):
75+
unpacker_kwargs['strict_map_key'] = False
76+
77+
# We need configured unpacker to work with error extention
78+
# type payload, but module do not provide access to self
79+
# inside extension type unpackers.
80+
unpacker_no_ext = msgpack.Unpacker(**unpacker_kwargs)
81+
ext_hook = lambda code, data: unpacker_ext_hook(code, data, unpacker_no_ext)
82+
unpacker_kwargs['ext_hook'] = ext_hook
83+
84+
return msgpack.Unpacker(**unpacker_kwargs)
85+
86+
3587
class Response(Sequence):
3688
"""
3789
Represents a single response from the server in compliance with the
@@ -56,41 +108,7 @@ def __init__(self, conn, response):
56108
# created in the __new__().
57109
# super(Response, self).__init__()
58110

59-
unpacker_kwargs = dict()
60-
61-
# Decode MsgPack arrays into Python lists by default (not tuples).
62-
# Can be configured in the Connection init
63-
unpacker_kwargs['use_list'] = conn.use_list
64-
65-
# Use raw=False instead of encoding='utf-8'.
66-
if msgpack.version >= (0, 5, 2) and conn.encoding == 'utf-8':
67-
# Get rid of the following warning.
68-
# > PendingDeprecationWarning: encoding is deprecated,
69-
# > Use raw=False instead.
70-
unpacker_kwargs['raw'] = False
71-
elif conn.encoding is not None:
72-
unpacker_kwargs['encoding'] = conn.encoding
73-
74-
# raw=False is default since msgpack-1.0.0.
75-
#
76-
# The option decodes mp_str to bytes, not a Unicode
77-
# string (when True).
78-
if msgpack.version >= (1, 0, 0) and conn.encoding is None:
79-
unpacker_kwargs['raw'] = True
80-
81-
# encoding option is not supported since msgpack-1.0.0,
82-
# but it is handled in the Connection constructor.
83-
assert(msgpack.version < (1, 0, 0) or conn.encoding in (None, 'utf-8'))
84-
85-
# strict_map_key=True is default since msgpack-1.0.0.
86-
#
87-
# The option forbids non-string keys in a map (when True).
88-
if msgpack.version >= (1, 0, 0):
89-
unpacker_kwargs['strict_map_key'] = False
90-
91-
unpacker_kwargs['ext_hook'] = unpacker_ext_hook
92-
93-
unpacker = msgpack.Unpacker(**unpacker_kwargs)
111+
unpacker = build_unpacker(conn)
94112

95113
unpacker.feed(response)
96114
header = unpacker.unpack()

0 commit comments

Comments
 (0)