Skip to content

Commit 1dad7d3

Browse files
committed
conn: create from socket fd
This patch adds the ability to create Tarantool connection using an existing socket fd. The authentication [1] 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` and sets the specified timeout. 1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/authentication/ Closes #304
1 parent eeb033e commit 1dad7d3

File tree

7 files changed

+167
-18
lines changed

7 files changed

+167
-18
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: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ def __init__(self, host, port,
623623
Unix sockets.
624624
:type host: :obj:`str` or :obj:`None`
625625
626-
:param port: Server port or Unix socket path.
626+
:param port: Server port, socket fd or Unix socket path.
627627
:type port: :obj:`int` or :obj:`str`
628628
629629
:param user: User name for authentication on the Tarantool
@@ -897,10 +897,33 @@ def connect_basic(self):
897897
:meta private:
898898
"""
899899

900-
if self.host is None:
901-
self.connect_unix()
902-
else:
900+
if self.host is not None:
903901
self.connect_tcp()
902+
return
903+
if isinstance(self.port, int):
904+
self.connect_socket_fd()
905+
return
906+
self.connect_unix()
907+
908+
def connect_socket_fd(self):
909+
"""
910+
Establish a connection using an existing socket fd.
911+
912+
:raise: :exc:`~tarantool.error.NetworkError`
913+
914+
:meta private:
915+
"""
916+
917+
try:
918+
# If old socket already exists - close it and re-create.
919+
self.connected = True
920+
if self._socket:
921+
self._socket.close()
922+
self._socket = socket.socket(fileno=self.port)
923+
self._socket.settimeout(self.socket_timeout)
924+
except socket.error as exc:
925+
self.connected = False
926+
raise NetworkError(exc) from exc
904927

905928
def connect_tcp(self):
906929
"""

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/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
]

test/suites/test_socket_fd.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""
2+
This module tests work with connection over socket fd.
3+
"""
4+
# pylint: disable=missing-class-docstring,missing-function-docstring
5+
6+
import socket
7+
import sys
8+
import unittest
9+
10+
import tarantool
11+
from .lib.skip import skip_or_run_box_session_new_tests
12+
from .lib.tarantool_server import TarantoolServer
13+
from .utils import assert_admin_success
14+
15+
16+
class TestSuiteSocketFD(unittest.TestCase):
17+
EVAL_USER = "return box.session.user()"
18+
19+
@classmethod
20+
def setUpClass(cls):
21+
print(' SOCKET FD '.center(70, '='), file=sys.stderr)
22+
print('-' * 70, file=sys.stderr)
23+
cls.srv = TarantoolServer()
24+
cls.srv.script = 'test/suites/box.lua'
25+
cls.srv.start()
26+
cls.tcp_port = 8111
27+
28+
# pylint:disable=C0209
29+
resp = cls.srv.admin("""
30+
local socket = require('socket')
31+
32+
box.cfg{}
33+
box.schema.user.create('test', {password = 'test', if_not_exists = true})
34+
box.schema.user.grant('test', 'read,write,execute,create', 'universe',
35+
nil, {if_not_exists = true})
36+
box.schema.user.grant('guest', 'execute', 'universe',
37+
nil, {if_not_exists = true})
38+
39+
socket.tcp_server('localhost', 8111, function(s)
40+
box.session.new({
41+
type = 'binary',
42+
fd = s:fd(),
43+
user = 'test',
44+
})
45+
s:detach()
46+
end)
47+
48+
return true
49+
""")
50+
51+
assert_admin_success(resp)
52+
test_sock = cls._get_tt_sock(cls)
53+
test_sock.close()
54+
55+
@skip_or_run_box_session_new_tests
56+
def setUp(self):
57+
# Prevent a remote tarantool from clean our session.
58+
if self.srv.is_started():
59+
self.srv.touch_lock()
60+
61+
def _get_tt_sock(self):
62+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, proto=socket.getprotobyname('tcp'))
63+
sock.connect(("localhost", self.tcp_port))
64+
return sock
65+
66+
def test_01_socket_fd_connect(self):
67+
sock = self._get_tt_sock()
68+
conn = tarantool.connect(host=None, port=sock.fileno())
69+
sock.detach()
70+
try:
71+
self.assertSequenceEqual(conn.eval(self.EVAL_USER), ["test"])
72+
finally:
73+
conn.close()
74+
75+
def test_02_socket_fd_re_auth(self):
76+
sock = self._get_tt_sock()
77+
conn = tarantool.connect(host=None, port=sock.fileno(), user="guest")
78+
sock.detach()
79+
try:
80+
self.assertSequenceEqual(conn.eval(self.EVAL_USER), ["guest"])
81+
finally:
82+
conn.close()
83+
84+
def test_03_socket_fd_mesh(self):
85+
sock = self._get_tt_sock()
86+
pool = tarantool.ConnectionPool(
87+
addrs=[{'host': None, 'port': sock.fileno()}]
88+
)
89+
sock.detach()
90+
try:
91+
self.assertSequenceEqual(pool.eval(self.EVAL_USER, mode=tarantool.Mode.ANY), ["test"])
92+
finally:
93+
pool.close()
94+
95+
def test_04_socket_fd_pool(self):
96+
sock = self._get_tt_sock()
97+
mesh = tarantool.MeshConnection(
98+
host=None, port=sock.fileno()
99+
)
100+
sock.detach()
101+
try:
102+
self.assertSequenceEqual(mesh.eval(self.EVAL_USER), ["test"])
103+
finally:
104+
mesh.close()
105+
106+
@classmethod
107+
def tearDownClass(cls):
108+
cls.srv.stop()
109+
cls.srv.clean()

0 commit comments

Comments
 (0)