Skip to content

Commit 8bec34b

Browse files
committed
conn: create from socket fd
This patch adds the ability to create Tarantool connection using an existing socket fd. To achieve this, several changes have been made to work with non-blocking sockets, as `socket.socketpair` creates such [1]. The authentication [2] might have already occured when we establish such a connection. If that's the case, there is no need to pass 'user' argument. On success, connect takes ownership of the `fd`. 1. tarantool/tarantool#8944 2. https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/authentication/ Closes #304
1 parent 0ce3d7c commit 8bec34b

File tree

8 files changed

+290
-19
lines changed

8 files changed

+290
-19
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## Unreleased
8+
9+
### Added
10+
- The ability to connect to the Tarantool using an existing socket fd (#304).
11+
712
## 1.1.2 - 2023-09-20
813

914
### Fixed

tarantool/connection.py

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""
22
This module provides API for interaction with a Tarantool server.
33
"""
4+
import fcntl
45
# pylint: disable=too-many-lines,duplicate-code
56

67
import os
8+
import select
79
import time
810
import errno
911
from enum import Enum
@@ -623,7 +625,7 @@ def __init__(self, host, port,
623625
Unix sockets.
624626
:type host: :obj:`str` or :obj:`None`
625627
626-
:param port: Server port or Unix socket path.
628+
:param port: Server port, socket fd or Unix socket path.
627629
:type port: :obj:`int` or :obj:`str`
628630
629631
:param user: User name for authentication on the Tarantool
@@ -897,10 +899,34 @@ def connect_basic(self):
897899
:meta private:
898900
"""
899901

900-
if self.host is None:
901-
self.connect_unix()
902-
else:
902+
if self.host is not None:
903903
self.connect_tcp()
904+
elif isinstance(self.port, int):
905+
self.connect_socket_fd()
906+
else:
907+
self.connect_unix()
908+
909+
def connect_socket_fd(self):
910+
"""
911+
Establish a connection using an existing socket fd.
912+
913+
:raise: :exc:`OSError`
914+
915+
:meta private:
916+
"""
917+
918+
# If old socket already exists - close it and re-create.
919+
self.connected = True
920+
if self._socket:
921+
self._socket.close()
922+
923+
socket_fd = self.port
924+
self._socket = socket.socket(fileno=socket_fd)
925+
926+
is_non_blocking = fcntl.fcntl(socket_fd, fcntl.F_GETFL) & os.O_NONBLOCK != 0
927+
if is_non_blocking:
928+
# Explicitly specify, because otherwise it will not be initialized correctly.
929+
self._socket.settimeout(0)
904930

905931
def connect_tcp(self):
906932
"""
@@ -1120,9 +1146,12 @@ def _recv(self, to_read):
11201146
:meta private:
11211147
"""
11221148

1149+
is_non_blocking = self._socket.gettimeout() == 0
11231150
buf = b""
11241151
while to_read > 0:
11251152
try:
1153+
if is_non_blocking:
1154+
select.select([self._socket.fileno()], [], [])
11261155
tmp = self._socket.recv(to_read)
11271156
except OverflowError as exc:
11281157
self._socket.close()
@@ -1163,6 +1192,41 @@ def _read_response(self):
11631192
# Read the packet
11641193
return self._recv(length)
11651194

1195+
def _sendall(self, bytes):
1196+
"""
1197+
Sends bytes to the transport (socket).
1198+
1199+
:param bytes: message to send.
1200+
:type bytes: :obj:`bytes`
1201+
1202+
:raise: :exc:`~tarantool.error.NetworkError`
1203+
1204+
:meta: private:
1205+
"""
1206+
is_non_blocking = self._socket.gettimeout() == 0
1207+
total_sent = 0
1208+
while total_sent < len(bytes):
1209+
try:
1210+
if is_non_blocking:
1211+
select.select([], [self._socket.fileno()], [])
1212+
sent = self._socket.send(bytes[total_sent:])
1213+
if sent == 0:
1214+
err = socket.error(
1215+
errno.ECONNRESET,
1216+
"Lost connection to server during query"
1217+
)
1218+
raise NetworkError(err)
1219+
total_sent += sent
1220+
except BlockingIOError as exc:
1221+
total_sent += exc.characters_written
1222+
continue
1223+
except socket.error as exc:
1224+
err = socket.error(
1225+
errno.ECONNRESET,
1226+
"Lost connection to server during query"
1227+
)
1228+
raise NetworkError(err) from exc
1229+
11661230
def _send_request_wo_reconnect(self, request, on_push=None, on_push_ctx=None):
11671231
"""
11681232
Send request without trying to reconnect.
@@ -1191,7 +1255,7 @@ def _send_request_wo_reconnect(self, request, on_push=None, on_push_ctx=None):
11911255
response = None
11921256
while True:
11931257
try:
1194-
self._socket.sendall(bytes(request))
1258+
self._sendall(bytes(request))
11951259
response = request.response_class(self, self._read_response())
11961260
break
11971261
except SchemaReloadException as exc:

tarantool/mesh_connection.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -139,23 +139,23 @@ def format_error(address, err):
139139
result[key] = val
140140

141141
if isinstance(result['port'], int):
142-
# Looks like an inet address.
142+
# Looks like an inet address or socket fd.
143143

144144
# Validate host.
145-
if 'host' not in result or result['host'] is None:
146-
return format_error(result,
147-
'host is mandatory for an inet result')
148-
if not isinstance(result['host'], str):
149-
return format_error(result,
150-
'host must be a string for an inet result')
145+
if ('host' in result and result['host'] is not None
146+
and not isinstance(result['host'], str)):
147+
return format_error(result, 'host must be a string for an inet result, '
148+
'or None for a socket fd result')
151149

152150
# Validate port.
153151
if not isinstance(result['port'], int):
154152
return format_error(result,
155-
'port must be an int for an inet result')
156-
if result['port'] < 1 or result['port'] > 65535:
157-
return format_error(result, 'port must be in range [1, 65535] '
158-
'for an inet result')
153+
'port must be an int for an inet/socket fd result')
154+
if 'host' in result and isinstance(result['host'], str):
155+
# Check that the port is correct.
156+
if result['port'] < 1 or result['port'] > 65535:
157+
return format_error(result, 'port must be in range [1, 65535] '
158+
'for an inet result')
159159

160160
# Looks okay.
161161
return result, None
@@ -447,7 +447,8 @@ def __init__(self, host=None, port=None,
447447
# Don't change user provided arguments.
448448
addrs = addrs[:]
449449

450-
if host and port:
450+
if port:
451+
# host can be None in the case of socket fd or Unix socket.
451452
addrs.insert(0, {'host': host,
452453
'port': port,
453454
'transport': transport,

test/suites/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .test_execute import TestSuiteExecute
1616
from .test_dbapi import TestSuiteDBAPI
1717
from .test_encoding import TestSuiteEncoding
18+
from .test_socket_fd import TestSuiteSocketFD
1819
from .test_ssl import TestSuiteSsl
1920
from .test_decimal import TestSuiteDecimal
2021
from .test_uuid import TestSuiteUUID
@@ -33,7 +34,7 @@
3334
TestSuiteEncoding, TestSuitePool, TestSuiteSsl,
3435
TestSuiteDecimal, TestSuiteUUID, TestSuiteDatetime,
3536
TestSuiteInterval, TestSuitePackage, TestSuiteErrorExt,
36-
TestSuitePush, TestSuiteConnection, TestSuiteCrud,)
37+
TestSuitePush, TestSuiteConnection, TestSuiteCrud, TestSuiteSocketFD)
3738

3839

3940
def load_tests(loader, tests, pattern):

test/suites/lib/skip.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,14 @@ def skip_or_run_iproto_basic_features_test(func):
306306

307307
return skip_or_run_test_tarantool(func, '2.10.0',
308308
'does not support iproto ID and iproto basic features')
309+
310+
311+
def skip_or_run_box_session_new_tests(func):
312+
"""
313+
Decorator to skip or run tests that use box.session.new.
314+
315+
Tarantool supports box.session.new only in current master since
316+
commit 324872a.
317+
See https://github.com/tarantool/tarantool/issues/8801.
318+
"""
319+
return skip_or_run_test_tarantool(func, '3.0.0', 'does not support box.session.new')

test/suites/sidecar.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# pylint: disable=missing-module-docstring
2+
import os
3+
4+
import tarantool
5+
6+
socket_fd = int(os.environ["SOCKET_FD"])
7+
8+
conn = tarantool.connect(host=None, port=socket_fd)
9+
10+
# Check user.
11+
assert conn.eval("return box.session.user()").data[0] == "test"
12+
13+
# Check db operations.
14+
conn.insert("test", [1])
15+
conn.insert("test", [2])
16+
assert conn.select("test").data == [[1], [2]]

test/suites/test_mesh.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ def test_01_contructor(self):
135135
# Verify that a bad address given at initialization leads
136136
# to an error.
137137
bad_addrs = [
138-
{"port": 1234}, # no host
139138
{"host": "localhost"}, # no port
140139
{"host": "localhost", "port": "1234"}, # port is str
141140
]

0 commit comments

Comments
 (0)