Skip to content

Commit 5b9f1cc

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. 1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/authentication/ Closes #304
1 parent db06064 commit 5b9f1cc

File tree

5 files changed

+158
-16
lines changed

5 files changed

+158
-16
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/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_socket_fd.py

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

0 commit comments

Comments
 (0)