Skip to content

Commit 1f0154e

Browse files
api: support iproto feature discovery
Since version 2.10.0 Tarantool supports feature discovery [1]. Client can send client protocol version and supported features and receive server protocol version and supported features information to tune its behavior. After this patch, the request will be sent on `dial`, before authentication is performed. Connector stores server info in connection internals. User can also set option RequiredProtocolInfo to fast fail on connect if server does not provide some expected feature, similar to net.box opts [2]. It is not clear how connector should behave in case if client doesn't support a protocol feature or protocol version, see [3]. For now we decided not to check requirements on the client side. Feature check iterates over lists to check if feature is enabled. It seems that iterating over a small list is way faster than building a map, see [4]. Benchmark tests show that this check is rather fast (0.5 ns for both client and server check on HP ProBook 440 G5) so it is not necessary to cache it in any way. Traces of IPROTO_FEATURE_GRACEFUL_SHUTDOWN flag and protocol version 4 could be found in Tarantool source code but they were removed in the following commits before the release and treated like they never existed. We also ignore them here too. See [5] for more info. In latest master commit new feature with code 4 and protocol version 4 were introduced [6]. 1. tarantool/tarantool#6253 2. https://www.tarantool.io/en/doc/latest/reference/reference_lua/net_box/#lua-function.net_box.new 3. tarantool/tarantool#7953 4. https://stackoverflow.com/a/52710077/11646599 5. tarantool/tarantool-python#262 6. tarantool/tarantool@948e5cd Closes #120
1 parent a20f033 commit 1f0154e

12 files changed

+856
-56
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1010

1111
### Added
1212

13+
- Support iproto feature discovery (#120).
14+
1315
### Changed
1416

1517
### Fixed

Diff for: connection.go

+128
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"math"
1515
"net"
1616
"runtime"
17+
"strings"
1718
"sync"
1819
"sync/atomic"
1920
"time"
@@ -146,6 +147,8 @@ type Connection struct {
146147
lenbuf [PacketLengthBytes]byte
147148

148149
lastStreamId uint64
150+
151+
serverProtocolInfo ProtocolInfo
149152
}
150153

151154
var _ = Connector(&Connection{}) // Check compatibility with connector interface.
@@ -269,6 +272,10 @@ type Opts struct {
269272
Transport string
270273
// SslOpts is used only if the Transport == 'ssl' is set.
271274
Ssl SslOpts
275+
// RequiredProtocolInfo contains minimal protocol version and
276+
// list of protocol features that should be supported by
277+
// Tarantool server. By default there are no restrictions
278+
RequiredProtocolInfo ProtocolInfo
272279
}
273280

274281
// SslOpts is a way to configure ssl transport.
@@ -295,8 +302,11 @@ type SslOpts struct {
295302

296303
// Copy returns the copy of an Opts object.
297304
// Beware that Notify channel, Logger and Handle are not copied.
305+
// Any changes in copy RequiredProtocolInfo will not affect the original
306+
// RequiredProtocolInfo value.
298307
func (opts Opts) Copy() Opts {
299308
optsCopy := opts
309+
optsCopy.RequiredProtocolInfo = opts.RequiredProtocolInfo.Copy()
300310

301311
return optsCopy
302312
}
@@ -510,6 +520,18 @@ func (conn *Connection) dial() (err error) {
510520
conn.Greeting.Version = bytes.NewBuffer(greeting[:64]).String()
511521
conn.Greeting.auth = bytes.NewBuffer(greeting[64:108]).String()
512522

523+
// IPROTO_ID requests can be processed without authentication.
524+
// https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/requests/#iproto-id
525+
if err = conn.identify(w, r); err != nil {
526+
connection.Close()
527+
return err
528+
}
529+
530+
if err = checkProtocolInfo(opts.RequiredProtocolInfo, conn.serverProtocolInfo); err != nil {
531+
connection.Close()
532+
return fmt.Errorf("identify: %w", err)
533+
}
534+
513535
// Auth
514536
if opts.User != "" {
515537
scr, err := scramble(conn.Greeting.auth, opts.Pass)
@@ -616,6 +638,17 @@ func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) error
616638
return nil
617639
}
618640

641+
func (conn *Connection) writeIdRequest(w *bufio.Writer, protocolInfo ProtocolInfo) error {
642+
req := NewIdRequest(protocolInfo)
643+
644+
err := conn.writeRequest(w, req)
645+
if err != nil {
646+
return fmt.Errorf("identify: %w", err)
647+
}
648+
649+
return nil
650+
}
651+
619652
func (conn *Connection) readResponse(r io.Reader) (Response, error) {
620653
respBytes, err := conn.read(r)
621654
if err != nil {
@@ -648,6 +681,15 @@ func (conn *Connection) readAuthResponse(r io.Reader) error {
648681
return nil
649682
}
650683

684+
func (conn *Connection) readIdResponse(r io.Reader) (Response, error) {
685+
resp, err := conn.readResponse(r)
686+
if err != nil {
687+
return resp, fmt.Errorf("identify: %w", err)
688+
}
689+
690+
return resp, nil
691+
}
692+
651693
func (conn *Connection) createConnection(reconnect bool) (err error) {
652694
var reconnects uint
653695
for conn.c == nil && conn.state == connDisconnected {
@@ -1191,3 +1233,89 @@ func (conn *Connection) NewStream() (*Stream, error) {
11911233
Conn: conn,
11921234
}, nil
11931235
}
1236+
1237+
// checkProtocolInfo checks that expected protocol version is
1238+
// and protocol features are supported.
1239+
func checkProtocolInfo(expected ProtocolInfo, actual ProtocolInfo) error {
1240+
var found bool
1241+
var missingFeatures []ProtocolFeature
1242+
1243+
if expected.Version > actual.Version {
1244+
return fmt.Errorf("protocol version %d is not supported", expected.Version)
1245+
}
1246+
1247+
// It seems that iterating over a small list is way faster
1248+
// than building a map: https://stackoverflow.com/a/52710077/11646599
1249+
for _, expectedFeature := range expected.Features {
1250+
found = false
1251+
for _, actualFeature := range actual.Features {
1252+
if expectedFeature == actualFeature {
1253+
found = true
1254+
}
1255+
}
1256+
if !found {
1257+
missingFeatures = append(missingFeatures, expectedFeature)
1258+
}
1259+
}
1260+
1261+
if len(missingFeatures) == 1 {
1262+
return fmt.Errorf("protocol feature %s is not supported", missingFeatures[0])
1263+
}
1264+
1265+
if len(missingFeatures) > 1 {
1266+
var sarr []string
1267+
for _, missingFeature := range missingFeatures {
1268+
sarr = append(sarr, missingFeature.String())
1269+
}
1270+
return fmt.Errorf("protocol features %s are not supported", strings.Join(sarr, ", "))
1271+
}
1272+
1273+
return nil
1274+
}
1275+
1276+
// identify sends info about client protocol, receives info
1277+
// about server protocol in response and stores it in the connection.
1278+
func (conn *Connection) identify(w *bufio.Writer, r *bufio.Reader) error {
1279+
var ok bool
1280+
1281+
werr := conn.writeIdRequest(w, clientProtocolInfo)
1282+
if werr != nil {
1283+
return werr
1284+
}
1285+
1286+
resp, rerr := conn.readIdResponse(r)
1287+
if rerr != nil {
1288+
if resp.Code == ErrUnknownRequestType {
1289+
// IPROTO_ID requests are not supported by server.
1290+
return nil
1291+
}
1292+
1293+
return rerr
1294+
}
1295+
1296+
if len(resp.Data) == 0 {
1297+
return fmt.Errorf("identify: unexpected response: no data")
1298+
}
1299+
1300+
conn.serverProtocolInfo, ok = resp.Data[0].(ProtocolInfo)
1301+
if !ok {
1302+
return fmt.Errorf("identify: unexpected response: wrong data")
1303+
}
1304+
1305+
return nil
1306+
}
1307+
1308+
// ServerProtocolVersion returns protocol version and protocol features
1309+
// supported by connected Tarantool server. Beware that values might be
1310+
// outdated if connection is in a disconnected state.
1311+
// Since 1.10.0
1312+
func (conn *Connection) ServerProtocolInfo() ProtocolInfo {
1313+
return conn.serverProtocolInfo.Copy()
1314+
}
1315+
1316+
// ClientProtocolVersion returns protocol version and protocol features
1317+
// supported by Go connection client.
1318+
// Since 1.10.0
1319+
func (conn *Connection) ClientProtocolInfo() ProtocolInfo {
1320+
return clientProtocolInfo.Copy()
1321+
}

Diff for: connection_pool/example_test.go

+48-18
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type Tuple struct {
1919

2020
var testRoles = []bool{true, true, false, true, true}
2121

22-
func examplePool(roles []bool) (*connection_pool.ConnectionPool, error) {
22+
func examplePool(roles []bool, connOpts tarantool.Opts) (*connection_pool.ConnectionPool, error) {
2323
err := test_helpers.SetClusterRO(servers, connOpts, roles)
2424
if err != nil {
2525
return nil, fmt.Errorf("ConnectionPool is not established")
@@ -33,7 +33,7 @@ func examplePool(roles []bool) (*connection_pool.ConnectionPool, error) {
3333
}
3434

3535
func ExampleConnectionPool_Select() {
36-
pool, err := examplePool(testRoles)
36+
pool, err := examplePool(testRoles, connOpts)
3737
if err != nil {
3838
fmt.Println(err)
3939
}
@@ -94,7 +94,7 @@ func ExampleConnectionPool_Select() {
9494
}
9595

9696
func ExampleConnectionPool_SelectTyped() {
97-
pool, err := examplePool(testRoles)
97+
pool, err := examplePool(testRoles, connOpts)
9898
if err != nil {
9999
fmt.Println(err)
100100
}
@@ -156,7 +156,7 @@ func ExampleConnectionPool_SelectTyped() {
156156
}
157157

158158
func ExampleConnectionPool_SelectAsync() {
159-
pool, err := examplePool(testRoles)
159+
pool, err := examplePool(testRoles, connOpts)
160160
if err != nil {
161161
fmt.Println(err)
162162
}
@@ -239,7 +239,7 @@ func ExampleConnectionPool_SelectAsync() {
239239

240240
func ExampleConnectionPool_SelectAsync_err() {
241241
roles := []bool{true, true, true, true, true}
242-
pool, err := examplePool(roles)
242+
pool, err := examplePool(roles, connOpts)
243243
if err != nil {
244244
fmt.Println(err)
245245
}
@@ -258,7 +258,7 @@ func ExampleConnectionPool_SelectAsync_err() {
258258
}
259259

260260
func ExampleConnectionPool_Ping() {
261-
pool, err := examplePool(testRoles)
261+
pool, err := examplePool(testRoles, connOpts)
262262
if err != nil {
263263
fmt.Println(err)
264264
}
@@ -276,7 +276,7 @@ func ExampleConnectionPool_Ping() {
276276
}
277277

278278
func ExampleConnectionPool_Insert() {
279-
pool, err := examplePool(testRoles)
279+
pool, err := examplePool(testRoles, connOpts)
280280
if err != nil {
281281
fmt.Println(err)
282282
}
@@ -325,7 +325,7 @@ func ExampleConnectionPool_Insert() {
325325
}
326326

327327
func ExampleConnectionPool_Delete() {
328-
pool, err := examplePool(testRoles)
328+
pool, err := examplePool(testRoles, connOpts)
329329
if err != nil {
330330
fmt.Println(err)
331331
}
@@ -377,7 +377,7 @@ func ExampleConnectionPool_Delete() {
377377
}
378378

379379
func ExampleConnectionPool_Replace() {
380-
pool, err := examplePool(testRoles)
380+
pool, err := examplePool(testRoles, connOpts)
381381
if err != nil {
382382
fmt.Println(err)
383383
}
@@ -448,7 +448,7 @@ func ExampleConnectionPool_Replace() {
448448
}
449449

450450
func ExampleConnectionPool_Update() {
451-
pool, err := examplePool(testRoles)
451+
pool, err := examplePool(testRoles, connOpts)
452452
if err != nil {
453453
fmt.Println(err)
454454
}
@@ -492,7 +492,7 @@ func ExampleConnectionPool_Update() {
492492
}
493493

494494
func ExampleConnectionPool_Call() {
495-
pool, err := examplePool(testRoles)
495+
pool, err := examplePool(testRoles, connOpts)
496496
if err != nil {
497497
fmt.Println(err)
498498
}
@@ -512,7 +512,7 @@ func ExampleConnectionPool_Call() {
512512
}
513513

514514
func ExampleConnectionPool_Eval() {
515-
pool, err := examplePool(testRoles)
515+
pool, err := examplePool(testRoles, connOpts)
516516
if err != nil {
517517
fmt.Println(err)
518518
}
@@ -532,7 +532,7 @@ func ExampleConnectionPool_Eval() {
532532
}
533533

534534
func ExampleConnectionPool_Do() {
535-
pool, err := examplePool(testRoles)
535+
pool, err := examplePool(testRoles, connOpts)
536536
if err != nil {
537537
fmt.Println(err)
538538
}
@@ -551,7 +551,7 @@ func ExampleConnectionPool_Do() {
551551
}
552552

553553
func ExampleConnectionPool_NewPrepared() {
554-
pool, err := examplePool(testRoles)
554+
pool, err := examplePool(testRoles, connOpts)
555555
if err != nil {
556556
fmt.Println(err)
557557
}
@@ -586,7 +586,17 @@ func ExampleCommitRequest() {
586586
return
587587
}
588588

589-
pool, err := examplePool(testRoles)
589+
// Assert that server supports expected features
590+
txnOpts := connOpts.Copy()
591+
txnOpts.RequiredProtocolInfo = tarantool.ProtocolInfo{
592+
Version: tarantool.ProtocolVersion(1),
593+
Features: []tarantool.ProtocolFeature{
594+
tarantool.StreamsFeature,
595+
tarantool.TransactionsFeature,
596+
},
597+
}
598+
599+
pool, err := examplePool(testRoles, txnOpts)
590600
if err != nil {
591601
fmt.Println(err)
592602
return
@@ -672,8 +682,18 @@ func ExampleRollbackRequest() {
672682
return
673683
}
674684

685+
// Assert that server supports expected features
686+
txnOpts := connOpts.Copy()
687+
txnOpts.RequiredProtocolInfo = tarantool.ProtocolInfo{
688+
Version: tarantool.ProtocolVersion(1),
689+
Features: []tarantool.ProtocolFeature{
690+
tarantool.StreamsFeature,
691+
tarantool.TransactionsFeature,
692+
},
693+
}
694+
675695
// example pool has only one rw instance
676-
pool, err := examplePool(testRoles)
696+
pool, err := examplePool(testRoles, txnOpts)
677697
if err != nil {
678698
fmt.Println(err)
679699
return
@@ -758,8 +778,18 @@ func ExampleBeginRequest_TxnIsolation() {
758778
return
759779
}
760780

781+
// Assert that server supports expected features
782+
txnOpts := connOpts.Copy()
783+
txnOpts.RequiredProtocolInfo = tarantool.ProtocolInfo{
784+
Version: tarantool.ProtocolVersion(1),
785+
Features: []tarantool.ProtocolFeature{
786+
tarantool.StreamsFeature,
787+
tarantool.TransactionsFeature,
788+
},
789+
}
790+
761791
// example pool has only one rw instance
762-
pool, err := examplePool(testRoles)
792+
pool, err := examplePool(testRoles, txnOpts)
763793
if err != nil {
764794
fmt.Println(err)
765795
return
@@ -836,7 +866,7 @@ func ExampleBeginRequest_TxnIsolation() {
836866
}
837867

838868
func ExampleConnectorAdapter() {
839-
pool, err := examplePool(testRoles)
869+
pool, err := examplePool(testRoles, connOpts)
840870
if err != nil {
841871
fmt.Println(err)
842872
}

0 commit comments

Comments
 (0)