Skip to content

Commit c0802b2

Browse files
committed
api: add support of a batch insert request
Draft changes: add support the IPROTO_INSERT_ARROW request and message pack type MP_ARROW . Closes #399
1 parent 592db69 commit c0802b2

File tree

6 files changed

+308
-6
lines changed

6 files changed

+308
-6
lines changed

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
99
## [Unreleased]
1010

1111
### Added
12-
- Add err log to `ConnectionPool.Add()` in case, when unable to establish
13-
connection and ctx is not canceled;
14-
also added logs for error case of `ConnectionPool.tryConnect()` calls in
12+
- Add err log to `ConnectionPool.Add()` in case, when unable to establish
13+
connection and ctx is not canceled;
14+
also added logs for error case of `ConnectionPool.tryConnect()` calls in
1515
`ConnectionPool.controller()` and `ConnectionPool.reconnect()`
1616
- Methods that are implemented but not included in the pooler interface (#395).
1717
- Implemented stringer methods for pool.Role (#405).
18+
- Support the IPROTO_INSERT_ARROW request (#399).
1819

1920
### Changed
2021

arrow/arrow.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package arrow
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"github.com/vmihailenco/msgpack/v5"
8+
)
9+
10+
// Arrow MessagePack extension type
11+
const arrowExtId = 8
12+
13+
// Arrow struct wraps a raw arrow data buffer.
14+
type Arrow struct {
15+
data []byte
16+
}
17+
18+
// MakeArrow returns a new arrow.Arrow object that contains
19+
// wrapped a raw arrow data buffer.
20+
func MakeArrow(arrow []byte) (Arrow, error) {
21+
if len(arrow) == 0 {
22+
return Arrow{}, fmt.Errorf("no Arrow data")
23+
}
24+
return Arrow{arrow}, nil
25+
}
26+
27+
// ToArrow returns a []byte that contains Arrow raw data.
28+
func (a *Arrow) ToArrow() []byte {
29+
return a.data
30+
}
31+
32+
func arrowDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error {
33+
arrow := Arrow{
34+
data: make([]byte, 0, extLen),
35+
}
36+
n, err := d.Buffered().Read(arrow.data)
37+
if err != nil {
38+
return fmt.Errorf("msgpack: can't read bytes on Arrow decode: %w", err)
39+
}
40+
if n < extLen || n != len(arrow.data) {
41+
return fmt.Errorf("msgpack: unexpected end of stream after %d Arrow bytes", n)
42+
}
43+
44+
v.Set(reflect.ValueOf(arrow))
45+
return nil
46+
}
47+
48+
func arrowEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) {
49+
if v.IsValid() {
50+
return v.Interface().(Arrow).data, nil
51+
}
52+
53+
return []byte{}, fmt.Errorf("msgpack: not valid Arrow value")
54+
}
55+
56+
func init() {
57+
msgpack.RegisterExtDecoder(arrowExtId, Arrow{}, arrowDecoder)
58+
msgpack.RegisterExtEncoder(arrowExtId, Arrow{}, arrowEncoder)
59+
}

arrow/arrow_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package arrow_test
2+
3+
import (
4+
"encoding/hex"
5+
"log"
6+
"os"
7+
"strconv"
8+
"testing"
9+
"time"
10+
11+
"github.com/stretchr/testify/require"
12+
"github.com/tarantool/go-tarantool/v2"
13+
"github.com/tarantool/go-tarantool/v2/arrow"
14+
"github.com/tarantool/go-tarantool/v2/test_helpers"
15+
)
16+
17+
var isArrowSupported = false
18+
19+
var server = "127.0.0.1:3013"
20+
var dialer = tarantool.NetDialer{
21+
Address: server,
22+
User: "test",
23+
Password: "test",
24+
}
25+
var space = "testArrow"
26+
27+
var opts = tarantool.Opts{
28+
//! Timeout: 5 * time.Second,
29+
}
30+
31+
func skipIfArrowUnsupported(t *testing.T) {
32+
t.Helper()
33+
if !isArrowSupported {
34+
t.Skip("Skipping test for Tarantool without Arrow support in msgpack")
35+
}
36+
}
37+
38+
// TestInsert uses Arrow sequence from Tarantool's test .
39+
// nolint:lll
40+
// See: https://github.com/tarantool/tarantool/blob/master/test/box-luatest/gh_10508_iproto_insert_arrow_test.lua
41+
func TestInsert_invalid(t *testing.T) {
42+
skipIfArrowUnsupported(t)
43+
44+
arrows := []struct {
45+
arrow string
46+
expected string
47+
}{
48+
{
49+
"",
50+
"no Arrow data",
51+
},
52+
{
53+
"00",
54+
"Failed to decode Arrow IPC data",
55+
},
56+
{
57+
"ffffffff70000000040000009effffff0400010004000000" +
58+
"b6ffffff0c00000004000000000000000100000004000000daffffff140000000202" +
59+
"000004000000f0ffffff4000000001000000610000000600080004000c0010000400" +
60+
"080009000c000c000c0000000400000008000a000c00040006000800ffffffff8800" +
61+
"0000040000008affffff0400030010000000080000000000000000000000acffffff" +
62+
"01000000000000003400000008000000000000000200000000000000000000000000" +
63+
"00000000000000000000000000000800000000000000000000000100000001000000" +
64+
"0000000000000000000000000a00140004000c0010000c0014000400060008000c00" +
65+
"00000000000000000000",
66+
"memtx does not support arrow format",
67+
},
68+
}
69+
70+
conn := test_helpers.ConnectWithValidation(t, dialer, opts)
71+
defer conn.Close()
72+
73+
for i, a := range arrows {
74+
t.Run(strconv.Itoa(i), func(t *testing.T) {
75+
data, err := hex.DecodeString(a.arrow)
76+
require.NoError(t, err)
77+
78+
arr, err := arrow.MakeArrow(data)
79+
if err != nil {
80+
require.ErrorContains(t, err, a.expected)
81+
return
82+
}
83+
req := tarantool.NewInsertArrowRequest(space).Arrow(arr)
84+
85+
_, err = conn.Do(req).Get()
86+
require.ErrorContains(t, err, a.expected)
87+
})
88+
}
89+
90+
}
91+
92+
// runTestMain is a body of TestMain function
93+
// (see https://pkg.go.dev/testing#hdr-Main).
94+
// Using defer + os.Exit is not works so TestMain body
95+
// is a separate function, see
96+
// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls
97+
func runTestMain(m *testing.M) int {
98+
isLess, err := test_helpers.IsTarantoolVersionLess(3, 3, 0)
99+
if err != nil {
100+
log.Fatalf("Failed to extract Tarantool version: %s", err)
101+
}
102+
isArrowSupported = !isLess
103+
104+
if !isArrowSupported {
105+
log.Println("Skipping insert Arrow tests...")
106+
return m.Run()
107+
}
108+
109+
instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{
110+
Dialer: dialer,
111+
InitScript: "config.lua",
112+
Listen: server,
113+
WaitStart: 100 * time.Millisecond,
114+
ConnectRetry: 10,
115+
RetryTimeout: 500 * time.Millisecond,
116+
})
117+
defer test_helpers.StopTarantoolWithCleanup(instance)
118+
119+
if err != nil {
120+
log.Printf("Failed to prepare test Tarantool: %s", err)
121+
return 1
122+
}
123+
124+
return m.Run()
125+
}
126+
127+
func TestMain(m *testing.M) {
128+
code := runTestMain(m)
129+
os.Exit(code)
130+
}

arrow/config.lua

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
--? local uuid = require('uuid')
2+
--? local msgpack = require('msgpack')
3+
4+
-- Do not set listen for now so connector won't be
5+
-- able to send requests until everything is configured.
6+
box.cfg{
7+
work_dir = os.getenv("TEST_TNT_WORK_DIR"),
8+
}
9+
10+
box.schema.user.create('test', { password = 'test' , if_not_exists = true })
11+
box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true })
12+
13+
--? local uuid_msgpack_supported = pcall(msgpack.encode, uuid.new())
14+
--? if not uuid_msgpack_supported then
15+
--? error('UUID unsupported, use Tarantool 2.4.1 or newer')
16+
--? end
17+
18+
local s = box.schema.space.create('testArrow', {
19+
id = 524,
20+
if_not_exists = true,
21+
})
22+
s:create_index('primary', {
23+
type = 'tree',
24+
parts = {{ field = 1, type = 'integer' }},
25+
if_not_exists = true
26+
})
27+
s:truncate()
28+
29+
box.schema.user.grant('test', 'read,write', 'space', 'testArrow', { if_not_exists = true })
30+
31+
-- Set listen only when every other thing is configured.
32+
box.cfg{
33+
listen = os.getenv("TEST_TNT_LISTEN"),
34+
}

request.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ import (
1313
"github.com/vmihailenco/msgpack/v5"
1414
)
1515

16+
// INSERT Arrow request.
17+
//
18+
// FIXME: replace with iproto.IPROTO_INSERT_ARROW when iproto will released.
19+
// https://github.com/tarantool/go-replica/issues/30
20+
const iprotoInsertArrowType = iproto.Type(17)
21+
22+
// The data in Arrow format.
23+
//
24+
// FIXME: replace with iproto.IPROTO_ARROW when iproto will released.
25+
// https://github.com/tarantool/go-replica/issues/30
26+
const iprotoArrowKey = iproto.Key(0x36)
27+
1628
type spaceEncoder struct {
1729
Id uint32
1830
Name string
@@ -136,6 +148,20 @@ func fillInsert(enc *msgpack.Encoder, spaceEnc spaceEncoder, tuple interface{})
136148
return enc.Encode(tuple)
137149
}
138150

151+
func fillInsertArrow(enc *msgpack.Encoder, spaceEnc spaceEncoder, arrow interface{}) error {
152+
if err := enc.EncodeMapLen(2); err != nil {
153+
return err
154+
}
155+
if err := spaceEnc.Encode(enc); err != nil {
156+
return err
157+
}
158+
159+
if err := enc.EncodeUint(uint64(iprotoArrowKey)); err != nil {
160+
return err
161+
}
162+
return enc.Encode(arrow)
163+
}
164+
139165
func fillSelect(enc *msgpack.Encoder, spaceEnc spaceEncoder, indexEnc indexEncoder,
140166
offset, limit uint32, iterator Iter, key, after interface{}, fetchPos bool) error {
141167
mapLen := 6
@@ -1156,6 +1182,50 @@ func (req *InsertRequest) Context(ctx context.Context) *InsertRequest {
11561182
return req
11571183
}
11581184

1185+
// InsertArrowRequest helps you to create an insert request object for execution
1186+
// by a Connection.
1187+
type InsertArrowRequest struct {
1188+
spaceRequest
1189+
arrow interface{}
1190+
}
1191+
1192+
// NewInsertArrowRequest returns a new empty InsertArrowRequest.
1193+
func NewInsertArrowRequest(space interface{}) *InsertArrowRequest {
1194+
req := new(InsertArrowRequest)
1195+
req.rtype = iprotoInsertArrowType
1196+
req.setSpace(space)
1197+
req.arrow = []interface{}{}
1198+
return req
1199+
}
1200+
1201+
// Arrow sets the arrow for insertion the insert arrow request.
1202+
// Note: default value is nil.
1203+
func (req *InsertArrowRequest) Arrow(arrow interface{}) *InsertArrowRequest {
1204+
req.arrow = arrow
1205+
return req
1206+
}
1207+
1208+
// Body fills an msgpack.Encoder with the insert arrow request body.
1209+
func (req *InsertArrowRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error {
1210+
spaceEnc, err := newSpaceEncoder(res, req.space)
1211+
if err != nil {
1212+
return err
1213+
}
1214+
1215+
return fillInsertArrow(enc, spaceEnc, req.arrow)
1216+
}
1217+
1218+
// Context sets a passed context to the request.
1219+
//
1220+
// Pay attention that when using context with request objects,
1221+
// the timeout option for Connection does not affect the lifetime
1222+
// of the request. For those purposes use context.WithTimeout() as
1223+
// the root context.
1224+
func (req *InsertArrowRequest) Context(ctx context.Context) *InsertArrowRequest {
1225+
req.ctx = ctx
1226+
return req
1227+
}
1228+
11591229
// ReplaceRequest helps you to create a replace request object for execution
11601230
// by a Connection.
11611231
type ReplaceRequest struct {

test_helpers/main.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,22 @@ func atoiUint64(str string) (uint64, error) {
120120
return res, nil
121121
}
122122

123+
func getTarantoolExec() string {
124+
125+
if tar_bin := os.Getenv("TARANTOOL_BIN"); tar_bin != "" {
126+
return tar_bin
127+
}
128+
129+
return "tarantool"
130+
}
131+
123132
// IsTarantoolVersionLess checks if tarantool version is less
124133
// than passed <major.minor.patch>. Returns error if failed
125134
// to extract version.
126135
func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) (bool, error) {
127136
var major, minor, patch uint64
128137

129-
out, err := exec.Command("tarantool", "--version").Output()
138+
out, err := exec.Command(getTarantoolExec(), "--version").Output()
130139

131140
if err != nil {
132141
return true, err
@@ -202,8 +211,7 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) {
202211
return inst, err
203212
}
204213
}
205-
206-
inst.Cmd = exec.Command("tarantool", startOpts.InitScript)
214+
inst.Cmd = exec.Command(getTarantoolExec(), startOpts.InitScript)
207215

208216
inst.Cmd.Env = append(
209217
os.Environ(),

0 commit comments

Comments
 (0)