Skip to content

Commit e9a8cab

Browse files
committed
api: add the ability to connect via socket fd
This patch introduces `FdDialer`, which connects to Tarantool using an existing socket file descriptor. `FdDialer` is not authenticated when creating a connection. Part of #321
1 parent ab4e64d commit e9a8cab

File tree

6 files changed

+289
-0
lines changed

6 files changed

+289
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
work_dir*
55
.rocks
66
bench*
7+
testdata/sidecar/main

dial.go

+56
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"io"
1010
"net"
11+
"os"
1112
"strings"
1213
"time"
1314

@@ -252,6 +253,61 @@ func (d OpenSslDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) {
252253
return conn, nil
253254
}
254255

256+
// FdDialer allows to use an existing socket fd for connection.
257+
type FdDialer struct {
258+
// Fd is a socket file descrpitor.
259+
Fd uintptr
260+
// RequiredProtocol contains minimal protocol version and
261+
// list of protocol features that should be supported by
262+
// Tarantool server. By default, there are no restrictions.
263+
RequiredProtocolInfo ProtocolInfo
264+
}
265+
266+
type fdAddr struct {
267+
Fd uintptr
268+
}
269+
270+
func (a fdAddr) Network() string {
271+
return "fd"
272+
}
273+
274+
func (a fdAddr) String() string {
275+
return fmt.Sprintf("fd://%d", a.Fd)
276+
}
277+
278+
type fdConn struct {
279+
net.Conn
280+
Addr fdAddr
281+
}
282+
283+
func (c *fdConn) RemoteAddr() net.Addr {
284+
return c.Addr
285+
}
286+
287+
// Dial makes FdDialer satisfy the Dialer interface.
288+
func (d FdDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) {
289+
file := os.NewFile(d.Fd, "")
290+
c, err := net.FileConn(file)
291+
if err != nil {
292+
return nil, fmt.Errorf("failed to dial: %w", err)
293+
}
294+
295+
conn := new(tntConn)
296+
conn.net = &fdConn{Conn: c, Addr: fdAddr{Fd: d.Fd}}
297+
298+
dc := &deadlineIO{to: opts.IoTimeout, c: conn.net}
299+
conn.reader = bufio.NewReaderSize(dc, bufSize)
300+
conn.writer = bufio.NewWriterSize(dc, bufSize)
301+
302+
_, err = rawDial(conn, d.RequiredProtocolInfo)
303+
if err != nil {
304+
conn.net.Close()
305+
return nil, err
306+
}
307+
308+
return conn, nil
309+
}
310+
255311
// Addr makes tntConn satisfy the Conn interface.
256312
func (c *tntConn) Addr() net.Addr {
257313
return c.net.RemoteAddr()

dial_test.go

+79
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ type testDialOpts struct {
442442
isIdUnsupported bool
443443
isPapSha256Auth bool
444444
isErrAuth bool
445+
isEmptyAuth bool
445446
}
446447

447448
type dialServerActual struct {
@@ -484,6 +485,8 @@ func testDialAccept(opts testDialOpts, l net.Listener) chan dialServerActual {
484485
authRequestExpected := authRequestExpectedChapSha1
485486
if opts.isPapSha256Auth {
486487
authRequestExpected = authRequestExpectedPapSha256
488+
} else if opts.isEmptyAuth {
489+
authRequestExpected = []byte{}
487490
}
488491
authRequestActual := make([]byte, len(authRequestExpected))
489492
client.Read(authRequestActual)
@@ -526,6 +529,8 @@ func testDialer(t *testing.T, l net.Listener, dialer tarantool.Dialer,
526529
authRequestExpected := authRequestExpectedChapSha1
527530
if opts.isPapSha256Auth {
528531
authRequestExpected = authRequestExpectedPapSha256
532+
} else if opts.isEmptyAuth {
533+
authRequestExpected = []byte{}
529534
}
530535
require.Equal(t, authRequestExpected, actual.AuthRequest)
531536
conn.Close()
@@ -779,3 +784,77 @@ func TestOpenSslDialer_Dial_ctx_cancel(t *testing.T) {
779784
}
780785
require.Error(t, err)
781786
}
787+
788+
func TestFdDialer_Dial(t *testing.T) {
789+
l, err := net.Listen("tcp", "127.0.0.1:0")
790+
require.NoError(t, err)
791+
defer l.Close()
792+
addr := l.Addr().String()
793+
794+
cases := []testDialOpts{
795+
{
796+
name: "all is ok",
797+
expectedProtocolInfo: idResponseTyped.Clone(),
798+
isEmptyAuth: true,
799+
},
800+
{
801+
name: "id request unsupported",
802+
expectedProtocolInfo: tarantool.ProtocolInfo{},
803+
isIdUnsupported: true,
804+
isEmptyAuth: true,
805+
},
806+
{
807+
name: "greeting response error",
808+
wantErr: true,
809+
expectedErr: "failed to read greeting",
810+
isErrGreeting: true,
811+
},
812+
{
813+
name: "id response error",
814+
wantErr: true,
815+
expectedErr: "failed to identify",
816+
isErrId: true,
817+
},
818+
}
819+
820+
for _, tc := range cases {
821+
t.Run(tc.name, func(t *testing.T) {
822+
sock, err := net.Dial("tcp", addr)
823+
require.NoError(t, err)
824+
f, err := sock.(*net.TCPConn).File()
825+
require.NoError(t, err)
826+
dialer := tarantool.FdDialer{
827+
Fd: f.Fd(),
828+
}
829+
testDialer(t, l, dialer, tc)
830+
})
831+
}
832+
}
833+
834+
func TestFdDialer_Dial_requirements(t *testing.T) {
835+
l, err := net.Listen("tcp", "127.0.0.1:0")
836+
require.NoError(t, err)
837+
defer l.Close()
838+
addr := l.Addr().String()
839+
840+
sock, err := net.Dial("tcp", addr)
841+
require.NoError(t, err)
842+
f, err := sock.(*net.TCPConn).File()
843+
require.NoError(t, err)
844+
dialer := tarantool.FdDialer{
845+
Fd: f.Fd(),
846+
RequiredProtocolInfo: tarantool.ProtocolInfo{
847+
Features: []iproto.Feature{42},
848+
},
849+
}
850+
851+
testDialAccept(t, testDialOpts{}, l)
852+
ctx, cancel := test_helpers.GetConnectContext()
853+
defer cancel()
854+
conn, err := dialer.Dial(ctx, tarantool.DialOpts{})
855+
if err == nil {
856+
conn.Close()
857+
}
858+
require.Error(t, err)
859+
require.Contains(t, err.Error(), "invalid server protocol")
860+
}

example_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package tarantool_test
33
import (
44
"context"
55
"fmt"
6+
"net"
67
"time"
78

89
"github.com/tarantool/go-iproto"
@@ -1330,3 +1331,34 @@ func ExampleWatchOnceRequest() {
13301331
fmt.Println(resp.Data)
13311332
}
13321333
}
1334+
1335+
// This example demonstrates how to use an existing socket file descriptor
1336+
// to establish a connection with Tarantool. This can be useful if the socket fd
1337+
// was inherited from the Tarantool process itself.
1338+
// For details, please see TestFdDialer in tarantool_test.go.
1339+
func ExampleFdDialer() {
1340+
addr := dialer.Address
1341+
c, err := net.Dial("tcp", addr)
1342+
if err != nil {
1343+
fmt.Printf("can't establish connection: %v\n", err)
1344+
return
1345+
}
1346+
f, err := c.(*net.TCPConn).File()
1347+
if err != nil {
1348+
fmt.Printf("unexpected error: %v\n", err)
1349+
return
1350+
}
1351+
dialer := tarantool.FdDialer{
1352+
Fd: f.Fd(),
1353+
}
1354+
// Use an existing socket fd to create connection with Tarantool.
1355+
conn, err := tarantool.Connect(context.Background(), dialer, opts)
1356+
if err != nil {
1357+
fmt.Printf("connect error: %v\n", err)
1358+
return
1359+
}
1360+
resp, err := conn.Do(tarantool.NewPingRequest()).Get()
1361+
fmt.Println(resp.Code, err)
1362+
// Output:
1363+
// 0 <nil>
1364+
}

tarantool_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"log"
99
"math"
1010
"os"
11+
"os/exec"
12+
"path/filepath"
1113
"reflect"
1214
"runtime"
1315
"strings"
@@ -77,6 +79,7 @@ func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error {
7779
}
7880

7981
var server = "127.0.0.1:3013"
82+
var fdDialerTestServer = "127.0.0.1:3014"
8083
var spaceNo = uint32(617)
8184
var spaceName = "test"
8285
var indexNo = uint32(0)
@@ -3927,6 +3930,87 @@ func TestConnect_context_cancel(t *testing.T) {
39273930
}
39283931
}
39293932

3933+
func buildSidecar(dir string) error {
3934+
goPath, err := exec.LookPath("go")
3935+
if err != nil {
3936+
return err
3937+
}
3938+
cmd := exec.Command(goPath, "build", "main.go")
3939+
cmd.Dir = filepath.Join(dir, "testdata", "sidecar")
3940+
return cmd.Run()
3941+
}
3942+
3943+
func TestFdDialer(t *testing.T) {
3944+
isLess, err := test_helpers.IsTarantoolVersionLess(3, 0, 0)
3945+
if err != nil || isLess {
3946+
t.Skip("box.session.new present in Tarantool since version 3.0")
3947+
}
3948+
3949+
wd, err := os.Getwd()
3950+
require.NoError(t, err)
3951+
3952+
err = buildSidecar(wd)
3953+
require.NoErrorf(t, err, "failed to build sidecar: %v", err)
3954+
3955+
instOpts := startOpts
3956+
instOpts.Listen = fdDialerTestServer
3957+
instOpts.Dialer = NetDialer{
3958+
Address: fdDialerTestServer,
3959+
User: "test",
3960+
Password: "test",
3961+
}
3962+
3963+
inst, err := test_helpers.StartTarantool(instOpts)
3964+
require.NoError(t, err)
3965+
defer test_helpers.StopTarantoolWithCleanup(inst)
3966+
3967+
conn := test_helpers.ConnectWithValidation(t, dialer, opts)
3968+
defer conn.Close()
3969+
3970+
sidecarExe := filepath.Join(wd, "testdata", "sidecar", "main")
3971+
3972+
evalBody := fmt.Sprintf(`
3973+
local socket = require('socket')
3974+
local popen = require('popen')
3975+
local os = require('os')
3976+
local s1, s2 = socket.socketpair('AF_UNIX', 'SOCK_STREAM', 0)
3977+
3978+
--[[ Tell sidecar which fd use to connect. --]]
3979+
os.setenv('SOCKET_FD', tostring(s2:fd()))
3980+
3981+
box.session.new({
3982+
type = 'binary',
3983+
fd = s1:fd(),
3984+
user = 'test',
3985+
})
3986+
s1:detach()
3987+
3988+
local ph, err = popen.new({'%s'}, {
3989+
stdout = popen.opts.PIPE,
3990+
stderr = popen.opts.PIPE,
3991+
inherit_fds = {s2:fd()},
3992+
})
3993+
3994+
if err ~= nil then
3995+
return 1, err
3996+
end
3997+
3998+
ph:wait()
3999+
4000+
local status_code = ph:info().status.exit_code
4001+
local stderr = ph:read({stderr=true}):rstrip()
4002+
local stdout = ph:read({stdout=true}):rstrip()
4003+
return status_code, stderr, stdout
4004+
`, sidecarExe)
4005+
4006+
var resp []interface{}
4007+
err = conn.EvalTyped(evalBody, []interface{}{}, &resp)
4008+
require.NoError(t, err)
4009+
require.Equal(t, "", resp[1], resp[1])
4010+
require.Equal(t, "", resp[2], resp[2])
4011+
require.Equal(t, int8(0), resp[0])
4012+
}
4013+
39304014
// runTestMain is a body of TestMain function
39314015
// (see https://pkg.go.dev/testing#hdr-Main).
39324016
// Using defer + os.Exit is not works so TestMain body

testdata/sidecar/main.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"os"
6+
"strconv"
7+
8+
"github.com/tarantool/go-tarantool/v2"
9+
)
10+
11+
func main() {
12+
fd, err := strconv.Atoi(os.Getenv("SOCKET_FD"))
13+
if err != nil {
14+
panic(err)
15+
}
16+
dialer := tarantool.FdDialer{
17+
Fd: uintptr(fd),
18+
}
19+
conn, err := tarantool.Connect(context.Background(), dialer, tarantool.Opts{})
20+
if err != nil {
21+
panic(err)
22+
}
23+
if _, err := conn.Do(tarantool.NewPingRequest()).Get(); err != nil {
24+
panic(err)
25+
}
26+
// Insert new tuple.
27+
if _, err := conn.Do(tarantool.NewInsertRequest("test").
28+
Tuple([]interface{}{239})).Get(); err != nil {
29+
panic(err)
30+
}
31+
// Delete inserted tuple.
32+
if _, err := conn.Do(tarantool.NewDeleteRequest("test").
33+
Index("primary").
34+
Key([]interface{}{239})).Get(); err != nil {
35+
panic(err)
36+
}
37+
}

0 commit comments

Comments
 (0)