Skip to content

Commit c925993

Browse files
committed
dial: 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. Closes #321
1 parent b482a06 commit c925993

File tree

6 files changed

+260
-0
lines changed

6 files changed

+260
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
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

Lines changed: 56 additions & 0 deletions
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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ type testDialOpts struct {
452452
isIdUnsupported bool
453453
isPapSha256Auth bool
454454
isErrAuth bool
455+
isEmptyAuth bool
455456
}
456457

457458
type dialServerActual struct {
@@ -493,6 +494,8 @@ func testDialAccept(t *testing.T, opts testDialOpts, l net.Listener) chan dialSe
493494
authRequestExpected := authRequestExpectedChapSha1
494495
if opts.isPapSha256Auth {
495496
authRequestExpected = authRequestExpectedPapSha256
497+
} else if opts.isEmptyAuth {
498+
authRequestExpected = []byte{}
496499
}
497500
authRequestActual := make([]byte, len(authRequestExpected))
498501
client.Read(authRequestActual)
@@ -535,6 +538,8 @@ func testDialer(t *testing.T, l net.Listener, dialer tarantool.Dialer,
535538
authRequestExpected := authRequestExpectedChapSha1
536539
if opts.isPapSha256Auth {
537540
authRequestExpected = authRequestExpectedPapSha256
541+
} else if opts.isEmptyAuth {
542+
authRequestExpected = []byte{}
538543
}
539544
require.Equal(t, authRequestExpected, actual.AuthRequest)
540545
conn.Close()
@@ -779,3 +784,48 @@ func TestOpenSslDialer_Dial_ctx_cancel(t *testing.T) {
779784
_, err := dialer.Dial(ctx, tarantool.DialOpts{})
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+
addr := l.Addr().String()
792+
793+
cases := []testDialOpts{
794+
{
795+
name: "all is ok",
796+
expectedProtocolInfo: idResponseTyped.Clone(),
797+
isEmptyAuth: true,
798+
},
799+
{
800+
name: "id request unsupported",
801+
expectedProtocolInfo: tarantool.ProtocolInfo{},
802+
isIdUnsupported: true,
803+
isEmptyAuth: true,
804+
},
805+
{
806+
name: "greeting response error",
807+
wantErr: true,
808+
expectedErr: "failed to read greeting",
809+
isErrGreeting: true,
810+
},
811+
{
812+
name: "id response error",
813+
wantErr: true,
814+
expectedErr: "failed to identify",
815+
isErrId: true,
816+
},
817+
}
818+
819+
for _, tc := range cases {
820+
t.Run(tc.name, func(t *testing.T) {
821+
sock, err := net.Dial("tcp", addr)
822+
require.NoError(t, err)
823+
f, err := sock.(*net.TCPConn).File()
824+
require.NoError(t, err)
825+
dialer := tarantool.FdDialer{
826+
Fd: f.Fd(),
827+
}
828+
testDialer(t, l, dialer, tc)
829+
})
830+
}
831+
}

example_test.go

Lines changed: 32 additions & 0 deletions
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

Lines changed: 84 additions & 0 deletions
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)
@@ -3924,6 +3927,87 @@ func TestConnect_context_cancel(t *testing.T) {
39243927
}
39253928
}
39263929

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

testdata/sidecar/main.go

Lines changed: 37 additions & 0 deletions
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)