Skip to content

Commit 10adf37

Browse files
iproto: support feature discovery
Since version 2.10.0 Tarantool supports feature discovery [1]. Client can send the schema version and supported features and receive server-side schema version and supported features information to tune its behavior. After this patch, the request would be send on `connect`. Connector will use protocol version that is minimal of connector version (now it's 3) and server version. Feature would be enabled if both client and server supports it (for now client does not support any features from the list). Unknown request type error response is expected for pre-2.10.0 versions. In this case, protocol version would be `None` and no features would be enabled. 1. tarantool/tarantool#6253 Closes #206
1 parent 728ce4f commit 10adf37

File tree

8 files changed

+217
-11
lines changed

8 files changed

+217
-11
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
134134
datetime: Timestamp('2022-03-31 00:00:00'), tz: ""
135135
```
136136

137+
- Support iproto feature discovery (#206).
138+
137139
### Changed
138140
- Bump msgpack requirement to 1.0.4 (PR #223).
139141
The only reason of this bump is various vulnerability fixes,

Diff for: tarantool/connection.py

+61-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
RequestUpdate,
3838
RequestUpsert,
3939
RequestAuthenticate,
40-
RequestExecute
40+
RequestExecute,
41+
RequestProtocolVersion,
4142
)
4243
from tarantool.space import Space
4344
from tarantool.const import (
@@ -55,7 +56,14 @@
5556
REQUEST_TYPE_ERROR,
5657
IPROTO_GREETING_SIZE,
5758
ITERATOR_EQ,
58-
ITERATOR_ALL
59+
ITERATOR_ALL,
60+
CONNECTOR_IPROTO_VERSION,
61+
CONNECTOR_FEATURES,
62+
IPROTO_FEATURE_STREAMS,
63+
IPROTO_FEATURE_TRANSACTIONS,
64+
IPROTO_FEATURE_ERROR_EXTENSION,
65+
IPROTO_FEATURE_WATCHERS,
66+
IPROTO_FEATURE_GRACEFUL_SHUTDOWN,
5967
)
6068
from tarantool.error import (
6169
Error,
@@ -498,6 +506,15 @@ def __init__(self, host, port,
498506
self.ssl_cert_file = ssl_cert_file
499507
self.ssl_ca_file = ssl_ca_file
500508
self.ssl_ciphers = ssl_ciphers
509+
self._protocol_version = None
510+
self._features = {
511+
IPROTO_FEATURE_STREAMS: False,
512+
IPROTO_FEATURE_TRANSACTIONS: False,
513+
IPROTO_FEATURE_ERROR_EXTENSION: False,
514+
IPROTO_FEATURE_WATCHERS: False,
515+
IPROTO_FEATURE_GRACEFUL_SHUTDOWN: False,
516+
}
517+
501518
if connect_now:
502519
self.connect()
503520

@@ -686,6 +703,7 @@ def connect(self):
686703
self.wrap_socket_ssl()
687704
self.handshake()
688705
self.load_schema()
706+
self._check_features()
689707
except SslError as e:
690708
raise e
691709
except Exception as e:
@@ -1602,3 +1620,44 @@ def execute(self, query, params=None):
16021620
request = RequestExecute(self, query, params)
16031621
response = self._send_request(request)
16041622
return response
1623+
1624+
def _check_features(self):
1625+
"""
1626+
Execute an ID request: inform the server about the protocol
1627+
version and features connector support and get server-side
1628+
information about it.
1629+
1630+
After executing this request, the connector will choose a
1631+
protocol version and features supported both by connector and
1632+
server.
1633+
1634+
:raise: :exc:`~AssertionError`,
1635+
:exc:`~tarantool.error.DatabaseError`,
1636+
:exc:`~tarantool.error.SchemaError`,
1637+
:exc:`~tarantool.error.NetworkError`,
1638+
:exc:`~tarantool.error.SslError`
1639+
"""
1640+
1641+
try:
1642+
request = RequestProtocolVersion(self,
1643+
CONNECTOR_IPROTO_VERSION,
1644+
CONNECTOR_FEATURES)
1645+
response = self._send_request(request)
1646+
server_protocol_version = response.protocol_version
1647+
server_features = response.features
1648+
except DatabaseError as exc:
1649+
ER_UNKNOWN_REQUEST_TYPE = 48
1650+
if exc.code == ER_UNKNOWN_REQUEST_TYPE:
1651+
server_protocol_version = None
1652+
server_features = []
1653+
else:
1654+
raise exc
1655+
1656+
if server_protocol_version is not None:
1657+
self._protocol_version = min(server_protocol_version,
1658+
CONNECTOR_IPROTO_VERSION)
1659+
1660+
# Intercept lists of features
1661+
features_list = [val for val in CONNECTOR_FEATURES if val in server_features]
1662+
for val in features_list:
1663+
self._features[val] = True

Diff for: tarantool/const.py

+15
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
IPROTO_SQL_INFO = 0x42
3636
IPROTO_SQL_INFO_ROW_COUNT = 0x00
3737
IPROTO_SQL_INFO_AUTOINCREMENT_IDS = 0x01
38+
#
39+
IPROTO_VERSION = 0x54
40+
IPROTO_FEATURES = 0x55
3841

3942
IPROTO_GREETING_SIZE = 128
4043
IPROTO_BODY_MAX_LEN = 2147483648
@@ -54,6 +57,7 @@
5457
REQUEST_TYPE_PING = 0x40
5558
REQUEST_TYPE_JOIN = 0x41
5659
REQUEST_TYPE_SUBSCRIBE = 0x42
60+
REQUEST_TYPE_ID = 0x49
5761
REQUEST_TYPE_ERROR = 1 << 15
5862

5963
SPACE_SCHEMA = 272
@@ -85,6 +89,12 @@
8589
ITERATOR_OVERLAPS = 10
8690
ITERATOR_NEIGHBOR = 11
8791

92+
IPROTO_FEATURE_STREAMS = 0
93+
IPROTO_FEATURE_TRANSACTIONS = 1
94+
IPROTO_FEATURE_ERROR_EXTENSION = 2
95+
IPROTO_FEATURE_WATCHERS = 3
96+
IPROTO_FEATURE_GRACEFUL_SHUTDOWN = 4
97+
8898
# Default value for connection timeout (seconds)
8999
CONNECTION_TIMEOUT = None
90100
# Default value for socket timeout (seconds)
@@ -113,3 +123,8 @@
113123
POOL_INSTANCE_RECONNECT_MAX_ATTEMPTS = 0
114124
# Default delay between attempts to reconnect (seconds)
115125
POOL_INSTANCE_RECONNECT_DELAY = 0
126+
127+
# Tarantool 2.10 protocol version is 3
128+
CONNECTOR_IPROTO_VERSION = 3
129+
# List of connector-supported features
130+
CONNECTOR_FEATURES = []

Diff for: tarantool/request.py

+37-2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
IPROTO_SCHEMA_ID,
3434
IPROTO_SQL_TEXT,
3535
IPROTO_SQL_BIND,
36+
IPROTO_VERSION,
37+
IPROTO_FEATURES,
3638
REQUEST_TYPE_OK,
3739
REQUEST_TYPE_PING,
3840
REQUEST_TYPE_SELECT,
@@ -47,9 +49,14 @@
4749
REQUEST_TYPE_EVAL,
4850
REQUEST_TYPE_AUTHENTICATE,
4951
REQUEST_TYPE_JOIN,
50-
REQUEST_TYPE_SUBSCRIBE
52+
REQUEST_TYPE_SUBSCRIBE,
53+
REQUEST_TYPE_ID,
54+
)
55+
from tarantool.response import (
56+
Response,
57+
ResponseExecute,
58+
ResponseProtocolVersion,
5159
)
52-
from tarantool.response import Response, ResponseExecute
5360
from tarantool.utils import (
5461
strxor,
5562
)
@@ -656,3 +663,31 @@ def __init__(self, conn, sql, args):
656663

657664
self._body = request_body
658665
self.response_class = ResponseExecute
666+
667+
class RequestProtocolVersion(Request):
668+
"""
669+
Represents ID request: inform the server about the protocol
670+
version and features connector support.
671+
"""
672+
673+
request_type = REQUEST_TYPE_ID
674+
675+
def __init__(self, conn, protocol_version, features):
676+
"""
677+
:param conn: Request sender.
678+
:type conn: :class:`~tarantool.Connection`
679+
680+
:param protocol_version: Connector protocol version.
681+
:type protocol_version: :obj:`int`
682+
683+
:param features: List of supported features.
684+
:type features: :obj:`list`
685+
"""
686+
687+
super(RequestProtocolVersion, self).__init__(conn)
688+
689+
request_body = self._dumps({IPROTO_VERSION: protocol_version,
690+
IPROTO_FEATURES: features})
691+
692+
self._body = request_body
693+
self.response_class = ResponseProtocolVersion

Diff for: tarantool/response.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
REQUEST_TYPE_ERROR,
1818
IPROTO_SQL_INFO,
1919
IPROTO_SQL_INFO_ROW_COUNT,
20-
IPROTO_SQL_INFO_AUTOINCREMENT_IDS
20+
IPROTO_SQL_INFO_AUTOINCREMENT_IDS,
21+
IPROTO_VERSION,
22+
IPROTO_FEATURES,
2123
)
2224
from tarantool.error import (
2325
DatabaseError,
@@ -324,3 +326,35 @@ def affected_row_count(self):
324326
return None
325327

326328
return info.get(IPROTO_SQL_INFO_ROW_COUNT)
329+
330+
331+
class ResponseProtocolVersion(Response):
332+
"""
333+
Represents an ID request response: information about server protocol
334+
version and features it supports.
335+
"""
336+
337+
@property
338+
def protocol_version(self):
339+
"""
340+
Server protocol version.
341+
342+
:rtype: :obj:`int` or :obj:`None`
343+
"""
344+
345+
if self._return_code != 0:
346+
return None
347+
return self._body.get(IPROTO_VERSION)
348+
349+
@property
350+
def features(self):
351+
"""
352+
Server supported features.
353+
354+
:rtype: :obj:`list`
355+
"""
356+
357+
if self._return_code != 0:
358+
return []
359+
return self._body.get(IPROTO_FEATURES)
360+

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

+2-5
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,11 @@ def wrapper(self, *args, **kwargs):
2828

2929
assert srv is not None
3030

31-
self.__class__.tnt_version = re.match(
32-
r'[\d.]+', srv.admin('box.info.version')[0]
33-
).group()
31+
self.__class__.tnt_version = srv.admin.tnt_version
3432

35-
tnt_version = pkg_resources.parse_version(self.tnt_version)
3633
support_version = pkg_resources.parse_version(REQUIRED_TNT_VERSION)
3734

38-
if tnt_version < support_version:
35+
if self.tnt_version < support_version:
3936
self.skipTest('Tarantool %s %s' % (self.tnt_version, msg))
4037

4138
if func.__name__ != 'setUp':

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

+16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import socket
22
import yaml
3+
import re
4+
import pkg_resources
35

46

57
class TarantoolAdmin(object):
@@ -8,6 +10,7 @@ def __init__(self, host, port):
810
self.port = port
911
self.is_connected = False
1012
self.socket = None
13+
self._tnt_version = None
1114

1215
def connect(self):
1316
self.socket = socket.create_connection((self.host, self.port))
@@ -62,3 +65,16 @@ def execute(self, command):
6265
break
6366

6467
return yaml.safe_load(res)
68+
69+
@property
70+
def tnt_version(self):
71+
if self._tnt_version is not None:
72+
return self._tnt_version
73+
74+
raw_version = re.match(
75+
r'[\d.]+', self.execute('box.info.version')[0]
76+
).group()
77+
78+
self._tnt_version = pkg_resources.parse_version(raw_version)
79+
80+
return self._tnt_version

Diff for: test/suites/test_protocol.py

+49-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,36 @@
11
import sys
2+
import pkg_resources
23
import unittest
3-
from tarantool.utils import greeting_decode, version_id
44
import uuid
55

6+
import tarantool
7+
from tarantool.utils import greeting_decode, version_id
8+
9+
from .lib.tarantool_server import TarantoolServer
10+
11+
from tarantool.const import (
12+
IPROTO_FEATURE_STREAMS,
13+
IPROTO_FEATURE_TRANSACTIONS,
14+
IPROTO_FEATURE_ERROR_EXTENSION,
15+
IPROTO_FEATURE_WATCHERS,
16+
IPROTO_FEATURE_GRACEFUL_SHUTDOWN,
17+
)
18+
619
class TestSuite_Protocol(unittest.TestCase):
720
@classmethod
821
def setUpClass(self):
922
print(' PROTOCOL '.center(70, '='), file=sys.stderr)
1023
print('-' * 70, file=sys.stderr)
24+
self.srv = TarantoolServer()
25+
self.srv.script = 'test/suites/box.lua'
26+
self.srv.start()
27+
self.con = tarantool.Connection(self.srv.host, self.srv.args['primary'])
28+
self.adm = self.srv.admin
29+
30+
def setUp(self):
31+
# prevent a remote tarantool from clean our session
32+
if self.srv.is_started():
33+
self.srv.touch_lock()
1134

1235
def test_00_greeting_1_6(self):
1336
buf = "Tarantool 1.6.6 \n" + \
@@ -45,3 +68,28 @@ def test_03_greeting_1_6_7(self):
4568
self.assertEqual(greeting.uuid,
4669
uuid.UUID('52dc2837-8001-48fe-bdce-c493c04599ce'))
4770
self.assertIsNotNone(greeting.salt)
71+
72+
def test_04_protocol(self):
73+
# First Tarantool protocol version (1) was introduced between
74+
# 2.10.0-beta1 and 2.10.0-beta2. Versions 2 and 3 were also
75+
# introduced between 2.10.0-beta1 and 2.10.0-beta2. Version 4
76+
# was introduced between 2.10.0-beta2 and 2.10.0-rc1 and reverted
77+
# back to version 3 in the same version interval.
78+
# Tarantool 2.10.3 still has version 3.
79+
if self.adm.tnt_version >= pkg_resources.parse_version('2.10.0'):
80+
self.assertTrue(self.con._protocol_version >= 3)
81+
else:
82+
self.assertIsNone(self.con._protocol_version)
83+
84+
self.assertEqual(self.con._features[IPROTO_FEATURE_STREAMS], False)
85+
self.assertEqual(self.con._features[IPROTO_FEATURE_TRANSACTIONS], False)
86+
self.assertEqual(self.con._features[IPROTO_FEATURE_ERROR_EXTENSION], False)
87+
self.assertEqual(self.con._features[IPROTO_FEATURE_WATCHERS], False)
88+
self.assertEqual(self.con._features[IPROTO_FEATURE_GRACEFUL_SHUTDOWN], False)
89+
90+
@classmethod
91+
def tearDownClass(self):
92+
self.con.close()
93+
self.srv.stop()
94+
self.srv.clean()
95+

0 commit comments

Comments
 (0)