Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make feature discovery similar to go-tarantool #296

Merged
merged 3 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Added
- Allow to require specific server protocol version and features (#267).

## 1.0.0 - 2023-04-17

### Changed
Expand Down
91 changes: 65 additions & 26 deletions tarantool/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import ctypes.util
from ctypes import c_ssize_t
from typing import Optional, Union
from copy import copy

import msgpack

Expand Down Expand Up @@ -65,6 +66,9 @@
IPROTO_FEATURE_TRANSACTIONS,
IPROTO_FEATURE_ERROR_EXTENSION,
IPROTO_FEATURE_WATCHERS,
IPROTO_FEATURE_PAGINATION,
IPROTO_FEATURE_SPACE_AND_INDEX_NAMES,
IPROTO_FEATURE_WATCH_ONCE,
IPROTO_CHUNK,
AUTH_TYPE_CHAP_SHA1,
AUTH_TYPE_PAP_SHA256,
Expand Down Expand Up @@ -607,7 +611,9 @@ def __init__(self, host, port,
packer_factory=default_packer_factory,
unpacker_factory=default_unpacker_factory,
auth_type=None,
fetch_schema=True):
fetch_schema=True,
required_protocol_version=None,
required_features=None):
"""
:param host: Server hostname or IP address. Use ``None`` for
Unix sockets.
Expand Down Expand Up @@ -776,6 +782,14 @@ def __init__(self, host, port,
:meth:`~tarantool.Connection.space`.
:type fetch_schema: :obj:`bool`, optional

:param required_protocol_version: Minimal protocol version that
should be supported by Tarantool server.
:type required_protocol_version: :obj:`int` or :obj:`None`, optional

:param required_features: List of protocol features that
should be supported by Tarantool server.
:type required_features: :obj:`list` or :obj:`None`, optional

:raise: :exc:`~tarantool.error.ConfigurationError`,
:meth:`~tarantool.Connection.connect` exceptions

Expand All @@ -784,7 +798,7 @@ def __init__(self, host, port,
.. _mp_bin: https://github.com/msgpack/msgpack/blob/master/spec.md#bin-format-family
.. _mp_array: https://github.com/msgpack/msgpack/blob/master/spec.md#array-format-family
"""
# pylint: disable=too-many-arguments,too-many-locals
# pylint: disable=too-many-arguments,too-many-locals,too-many-statements

if msgpack.version >= (1, 0, 0) and encoding not in (None, 'utf-8'):
raise ConfigurationError("msgpack>=1.0.0 only supports None and "
Expand Down Expand Up @@ -830,6 +844,9 @@ def __init__(self, host, port,
IPROTO_FEATURE_TRANSACTIONS: False,
IPROTO_FEATURE_ERROR_EXTENSION: False,
IPROTO_FEATURE_WATCHERS: False,
IPROTO_FEATURE_PAGINATION: False,
IPROTO_FEATURE_SPACE_AND_INDEX_NAMES: False,
IPROTO_FEATURE_WATCH_ONCE: False,
}
self._packer_factory_impl = packer_factory
self._unpacker_factory_impl = unpacker_factory
Expand All @@ -838,6 +855,12 @@ def __init__(self, host, port,
self.version_id = None
self.uuid = None
self._salt = None
self._client_protocol_version = CONNECTOR_IPROTO_VERSION
self._client_features = copy(CONNECTOR_FEATURES)
self._server_protocol_version = None
self._server_features = None
self.required_protocol_version = required_protocol_version
self.required_features = copy(required_features)

if connect_now:
self.connect()
Expand Down Expand Up @@ -1044,10 +1067,11 @@ def handshake(self):
if greeting.protocol != "Binary":
raise NetworkError("Unsupported protocol: " + greeting.protocol)
self.version_id = greeting.version_id
if self.version_id >= version_id(2, 10, 0):
self._check_features()
self.uuid = greeting.uuid
self._salt = greeting.salt

self._check_features()

if self.user:
self.authenticate(self.user, self.password)

Expand Down Expand Up @@ -2057,32 +2081,47 @@ def _check_features(self):
:exc:`~tarantool.error.SslError`
"""

try:
request = RequestProtocolVersion(self,
CONNECTOR_IPROTO_VERSION,
CONNECTOR_FEATURES)
response = self._send_request(request)
server_protocol_version = response.protocol_version
server_features = response.features
server_auth_type = response.auth_type
except DatabaseError as exc:
if exc.code == ER_UNKNOWN_REQUEST_TYPE:
server_protocol_version = None
server_features = []
server_auth_type = None
if self.version_id >= version_id(2, 10, 0):
try:
request = RequestProtocolVersion(self,
self._client_protocol_version,
self._client_features)
response = self._send_request(request)
self._server_protocol_version = response.protocol_version
self._server_features = response.features
self._server_auth_type = response.auth_type
except DatabaseError as exc:
if exc.code != ER_UNKNOWN_REQUEST_TYPE:
raise exc

if self.required_protocol_version is not None:
if self._server_protocol_version is None or \
self._server_protocol_version < self.required_protocol_version:
raise ConfigurationError('Server protocol version is '
f'{self._server_protocol_version}, '
f'protocol version {self.required_protocol_version} '
'is required')

if self.required_features is not None:
if self._server_features is None:
failed_features = self.required_features
else:
raise exc
failed_features = [val for val in self.required_features
if val not in self._server_features]

if server_protocol_version is not None:
self._protocol_version = min(server_protocol_version,
CONNECTOR_IPROTO_VERSION)
if len(failed_features) > 0:
str_features = ', '.join([str(v) for v in failed_features])
raise ConfigurationError(f'Server missing protocol features with id {str_features}')

# Intercept lists of features
features_list = [val for val in CONNECTOR_FEATURES if val in server_features]
for val in features_list:
self._features[val] = True
if self._server_protocol_version is not None:
self._protocol_version = min(self._server_protocol_version,
self._client_protocol_version)

self._server_auth_type = server_auth_type
# Intercept lists of features
if self._server_features is not None:
features_list = [val for val in self._client_features if val in self._server_features]
for val in features_list:
self._features[val] = True

def _packer_factory(self):
return self._packer_factory_impl(self)
Expand Down
7 changes: 5 additions & 2 deletions tarantool/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@
IPROTO_FEATURE_TRANSACTIONS = 1
IPROTO_FEATURE_ERROR_EXTENSION = 2
IPROTO_FEATURE_WATCHERS = 3
IPROTO_FEATURE_PAGINATION = 4
IPROTO_FEATURE_SPACE_AND_INDEX_NAMES = 5
IPROTO_FEATURE_WATCH_ONCE = 6

# Default value for connection timeout (seconds)
CONNECTION_TIMEOUT = None
Expand Down Expand Up @@ -133,8 +136,8 @@
# Default delay between attempts to reconnect (seconds)
POOL_INSTANCE_RECONNECT_DELAY = 0

# Tarantool 2.10 protocol version is 3
CONNECTOR_IPROTO_VERSION = 3
# Tarantool master 970ea48 protocol version is 6
CONNECTOR_IPROTO_VERSION = 6
# List of connector-supported features
CONNECTOR_FEATURES = [IPROTO_FEATURE_ERROR_EXTENSION]

Expand Down
16 changes: 16 additions & 0 deletions test/suites/lib/skip.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,19 @@ def skip_or_run_constraints_test(func):

return skip_or_run_test_tarantool(func, '2.10.0',
'does not support schema constraints')


def skip_or_run_iproto_basic_features_test(func):
"""
Decorator to skip or run tests related to iproto ID requests,
protocol version and features.

Tarantool supports iproto ID requests only since 2.10.0 version.
Protocol version is 3 for Tarantool 2.10.0,
IPROTO_FEATURE_STREAMS, IPROTO_FEATURE_TRANSACTIONS
and IPROTO_FEATURE_ERROR_EXTENSION are supported in Tarantool 2.10.0.
See https://github.com/tarantool/tarantool/issues/6253
"""

return skip_or_run_test_tarantool(func, '2.10.0',
'does not support iproto ID and iproto basic features')
34 changes: 34 additions & 0 deletions test/suites/test_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@
IPROTO_FEATURE_TRANSACTIONS,
IPROTO_FEATURE_ERROR_EXTENSION,
IPROTO_FEATURE_WATCHERS,
IPROTO_FEATURE_PAGINATION,
IPROTO_FEATURE_SPACE_AND_INDEX_NAMES,
IPROTO_FEATURE_WATCH_ONCE,
)
from tarantool.error import NetworkError
from tarantool.utils import greeting_decode, version_id

from .lib.tarantool_server import TarantoolServer
from .lib.skip import skip_or_run_iproto_basic_features_test


class TestSuiteProtocol(unittest.TestCase):
Expand Down Expand Up @@ -91,6 +96,35 @@ def test_04_protocol(self):
self.assertEqual(self.con._features[IPROTO_FEATURE_STREAMS], False)
self.assertEqual(self.con._features[IPROTO_FEATURE_TRANSACTIONS], False)
self.assertEqual(self.con._features[IPROTO_FEATURE_WATCHERS], False)
self.assertEqual(self.con._features[IPROTO_FEATURE_PAGINATION], False)
self.assertEqual(self.con._features[IPROTO_FEATURE_SPACE_AND_INDEX_NAMES], False)
self.assertEqual(self.con._features[IPROTO_FEATURE_WATCH_ONCE], False)

@skip_or_run_iproto_basic_features_test
def test_protocol_requirement(self):
try:
con = tarantool.Connection(self.srv.host, self.srv.args['primary'],
required_protocol_version=3,
required_features=[IPROTO_FEATURE_STREAMS,
IPROTO_FEATURE_TRANSACTIONS,
IPROTO_FEATURE_ERROR_EXTENSION])
con.close()
except Exception as exc: # pylint: disable=bad-option-value,broad-exception-caught,broad-except
self.fail(f'Connection create have raised Exception: {repr(exc)}')

def test_protocol_version_requirement_fail(self):
with self.assertRaisesRegex(NetworkError, # ConfigurationError is wrapped in NetworkError
'protocol version 100500 is required'):
con = tarantool.Connection(self.srv.host, self.srv.args['primary'],
required_protocol_version=100500)
con.close()

def test_protocol_features_requirement_fail(self):
with self.assertRaisesRegex(NetworkError, # ConfigurationError is wrapped in NetworkError
'Server missing protocol features with id 100500, 500100'):
con = tarantool.Connection(self.srv.host, self.srv.args['primary'],
required_features=[100500, 500100])
con.close()

@classmethod
def tearDownClass(cls):
Expand Down