Skip to content

Commit a7f084e

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 RequiredProtocolVersion and RequiredProtocolFeatures 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 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 21c68eb commit a7f084e

File tree

10 files changed

+676
-46
lines changed

10 files changed

+676
-46
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

+145
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ type Connection struct {
146146
lenbuf [PacketLengthBytes]byte
147147

148148
lastStreamId uint64
149+
150+
serverProtocolInfo ProtocolInfo
149151
}
150152

151153
var _ = Connector(&Connection{}) // Check compatibility with connector interface.
@@ -269,6 +271,14 @@ type Opts struct {
269271
Transport string
270272
// SslOpts is used only if the Transport == 'ssl' is set.
271273
Ssl SslOpts
274+
// Minimal protocol version that should be supported by
275+
// Go connection client and Tarantool server. By default
276+
// it is equal to 0 (no restrictions)
277+
RequiredProtocolVersion ProtocolVersion
278+
// List of protocol features that should be supported by
279+
// Go connection client and Tarantool server. By default
280+
// it is a nil array (no restrictions)
281+
RequiredProtocolFeatures []ProtocolFeature
272282
}
273283

274284
// SslOpts is a way to configure ssl transport.
@@ -296,6 +306,9 @@ type SslOpts struct {
296306
func (opts Opts) Copy() (optsCopy Opts) {
297307
optsCopy = opts
298308

309+
optsCopy.RequiredProtocolFeatures = make([]ProtocolFeature, len(opts.RequiredProtocolFeatures))
310+
copy(optsCopy.RequiredProtocolFeatures, opts.RequiredProtocolFeatures)
311+
299312
return optsCopy
300313
}
301314

@@ -508,6 +521,23 @@ func (conn *Connection) dial() (err error) {
508521
conn.Greeting.Version = bytes.NewBuffer(greeting[:64]).String()
509522
conn.Greeting.auth = bytes.NewBuffer(greeting[64:108]).String()
510523

524+
// IPROTO_ID requests can be processed without authentication.
525+
// https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/requests/#iproto-id
526+
if err = conn.identify(w, r); err != nil {
527+
connection.Close()
528+
return err
529+
}
530+
531+
if err = checkProtocolVersion(opts.RequiredProtocolVersion, conn.ServerProtocolVersion()); err != nil {
532+
connection.Close()
533+
return fmt.Errorf("id: %w", err)
534+
}
535+
536+
if err = checkProtocolFeatures(opts.RequiredProtocolFeatures, conn.ServerProtocolFeatures()); err != nil {
537+
connection.Close()
538+
return fmt.Errorf("id: %w", err)
539+
}
540+
511541
// Auth
512542
if opts.User != "" {
513543
scr, err := scramble(conn.Greeting.auth, opts.Pass)
@@ -614,6 +644,18 @@ func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err
614644
return nil
615645
}
616646

647+
func (conn *Connection) writeIdRequest(w *bufio.Writer, version ProtocolVersion,
648+
features []ProtocolFeature) (err error) {
649+
req := NewIdRequest(version, features)
650+
651+
err = conn.writeRequest(w, req)
652+
if err != nil {
653+
return fmt.Errorf("id: %w", err)
654+
}
655+
656+
return nil
657+
}
658+
617659
func (conn *Connection) readResponse(r io.Reader) (resp Response, err error) {
618660
respBytes, err := conn.read(r)
619661
if err != nil {
@@ -645,6 +687,15 @@ func (conn *Connection) readAuthResponse(r io.Reader) (err error) {
645687
return nil
646688
}
647689

690+
func (conn *Connection) readIdResponse(r io.Reader) (resp Response, err error) {
691+
resp, err = conn.readResponse(r)
692+
if err != nil {
693+
return resp, fmt.Errorf("id: %w", err)
694+
}
695+
696+
return resp, nil
697+
}
698+
648699
func (conn *Connection) createConnection(reconnect bool) (err error) {
649700
var reconnects uint
650701
for conn.c == nil && conn.state == connDisconnected {
@@ -1188,3 +1239,97 @@ func (conn *Connection) NewStream() (*Stream, error) {
11881239
Conn: conn,
11891240
}, nil
11901241
}
1242+
1243+
// checkProtocolVersion checks that expected protocol version is supported.
1244+
func checkProtocolVersion(expected ProtocolVersion, actual ProtocolVersion) error {
1245+
if expected > actual {
1246+
return fmt.Errorf("protocol version %d is not supported", expected)
1247+
}
1248+
1249+
return nil
1250+
}
1251+
1252+
// checkProtocolFeatures checks that expected protocol features are supported.
1253+
func checkProtocolFeatures(expectedList []ProtocolFeature, actualList []ProtocolFeature) error {
1254+
var found bool
1255+
// It seems that iterating over a small list is way faster
1256+
// than building a map: https://stackoverflow.com/a/52710077/11646599
1257+
for _, expected := range expectedList {
1258+
found = false
1259+
for _, actual := range actualList {
1260+
if expected == actual {
1261+
found = true
1262+
}
1263+
}
1264+
if !found {
1265+
// "Feature" already is a part of the String().
1266+
return fmt.Errorf("%s is not supported", expected)
1267+
}
1268+
}
1269+
1270+
return nil
1271+
}
1272+
1273+
// identify sends info about client protocol, receives info
1274+
// about server protocol in response and store it in the connection.
1275+
func (conn *Connection) identify(w *bufio.Writer, r *bufio.Reader) error {
1276+
var ok bool
1277+
1278+
werr := conn.writeIdRequest(w, conn.ClientProtocolVersion(), conn.ClientProtocolFeatures())
1279+
if werr != nil {
1280+
return werr
1281+
}
1282+
1283+
resp, rerr := conn.readIdResponse(r)
1284+
if rerr != nil {
1285+
if resp.Code == ErrUnknownRequestType {
1286+
// IPROTO_ID requests are not supported by server.
1287+
return nil
1288+
}
1289+
1290+
return rerr
1291+
}
1292+
1293+
if len(resp.Data) == 0 {
1294+
return fmt.Errorf("identify: unexpected response: no data")
1295+
}
1296+
1297+
conn.serverProtocolInfo, ok = resp.Data[0].(ProtocolInfo)
1298+
if !ok {
1299+
return fmt.Errorf("identify: unexpected response: wrong data")
1300+
}
1301+
1302+
return nil
1303+
}
1304+
1305+
// ServerProtocolVersion returns protocol version supported by
1306+
// connected Tarantool server. Beware that values might be outdated
1307+
// if connection is in a disconnected state.
1308+
// Since 1.10.0
1309+
func (conn *Connection) ServerProtocolVersion() ProtocolVersion {
1310+
return conn.serverProtocolInfo.Version
1311+
}
1312+
1313+
// ClientProtocolVersion returns protocol version supported by
1314+
// Go connection client.
1315+
// Since 1.10.0
1316+
func (conn *Connection) ClientProtocolVersion() ProtocolVersion {
1317+
return сlientProtocolVersion
1318+
}
1319+
1320+
// ServerFeatures returns protocol features supported by connected Tarantool server.
1321+
// Beware that values might be outdated if connection is in a disconnected state.
1322+
// Since 1.10.0
1323+
func (conn *Connection) ServerProtocolFeatures() []ProtocolFeature {
1324+
res := make([]ProtocolFeature, len(conn.serverProtocolInfo.Features))
1325+
copy(res, conn.serverProtocolInfo.Features)
1326+
return res
1327+
}
1328+
1329+
// ClientFeatures returns protocol features supported by Go connection client.
1330+
// Since 1.10.0
1331+
func (conn *Connection) ClientProtocolFeatures() []ProtocolFeature {
1332+
res := make([]ProtocolFeature, len(сlientProtocolFeatures))
1333+
copy(res, сlientProtocolFeatures)
1334+
return res
1335+
}

Diff for: connection_pool/example_test.go

+38-17
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,14 @@ func ExampleCommitRequest() {
586586
return
587587
}
588588

589-
pool, err := examplePool(testRoles)
589+
// Assert that server supports expected features
590+
txnOpts := connOpts.Copy()
591+
txnOpts.RequiredFeatures = []tarantool.Feature{
592+
tarantool.StreamsFeature,
593+
tarantool.TransactionsFeature,
594+
}
595+
596+
pool, err := examplePool(testRoles, txnOpts)
590597
if err != nil {
591598
fmt.Println(err)
592599
return
@@ -672,8 +679,15 @@ func ExampleRollbackRequest() {
672679
return
673680
}
674681

682+
// Assert that server supports expected features
683+
txnOpts := connOpts.Copy()
684+
txnOpts.RequiredFeatures = []tarantool.Feature{
685+
tarantool.StreamsFeature,
686+
tarantool.TransactionsFeature,
687+
}
688+
675689
// example pool has only one rw instance
676-
pool, err := examplePool(testRoles)
690+
pool, err := examplePool(testRoles, txnOpts)
677691
if err != nil {
678692
fmt.Println(err)
679693
return
@@ -758,8 +772,15 @@ func ExampleBeginRequest_TxnIsolation() {
758772
return
759773
}
760774

775+
// Assert that server supports expected features
776+
txnOpts := connOpts.Copy()
777+
txnOpts.RequiredFeatures = []tarantool.Feature{
778+
tarantool.StreamsFeature,
779+
tarantool.TransactionsFeature,
780+
}
781+
761782
// example pool has only one rw instance
762-
pool, err := examplePool(testRoles)
783+
pool, err := examplePool(testRoles, txnOpts)
763784
if err != nil {
764785
fmt.Println(err)
765786
return

Diff for: const.go

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
RollbackRequestCode = 16
1919
PingRequestCode = 64
2020
SubscribeRequestCode = 66
21+
IdRequestCode = 73
2122

2223
KeyCode = 0x00
2324
KeySync = 0x01
@@ -41,6 +42,8 @@ const (
4142
KeySQLBind = 0x41
4243
KeySQLInfo = 0x42
4344
KeyStmtID = 0x43
45+
KeyVersion = 0x54
46+
KeyFeatures = 0x55
4447
KeyTimeout = 0x56
4548
KeyTxnIsolation = 0x59
4649

0 commit comments

Comments
 (0)