Skip to content

Commit 73b1e83

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 73b1e83

14 files changed

+849
-62
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

+131-4
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.
@@ -293,10 +300,12 @@ type SslOpts struct {
293300
Ciphers string
294301
}
295302

296-
// Copy returns the copy of an Opts object.
297-
// Beware that Notify channel, Logger and Handle are not copied.
298-
func (opts Opts) Copy() Opts {
303+
// Clone returns a copy of the Opts object.
304+
// Any changes in copy RequiredProtocolInfo will not affect the original
305+
// RequiredProtocolInfo value.
306+
func (opts Opts) Clone() Opts {
299307
optsCopy := opts
308+
optsCopy.RequiredProtocolInfo = opts.RequiredProtocolInfo.Clone()
300309

301310
return optsCopy
302311
}
@@ -327,7 +336,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) {
327336
contextRequestId: 1,
328337
Greeting: &Greeting{},
329338
control: make(chan struct{}),
330-
opts: opts.Copy(),
339+
opts: opts.Clone(),
331340
dec: newDecoder(&smallBuf{}),
332341
}
333342
maxprocs := uint32(runtime.GOMAXPROCS(-1))
@@ -510,6 +519,18 @@ func (conn *Connection) dial() (err error) {
510519
conn.Greeting.Version = bytes.NewBuffer(greeting[:64]).String()
511520
conn.Greeting.auth = bytes.NewBuffer(greeting[64:108]).String()
512521

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

640+
func (conn *Connection) writeIdRequest(w *bufio.Writer, protocolInfo ProtocolInfo) error {
641+
req := NewIdRequest(protocolInfo)
642+
643+
err := conn.writeRequest(w, req)
644+
if err != nil {
645+
return fmt.Errorf("identify: %w", err)
646+
}
647+
648+
return nil
649+
}
650+
619651
func (conn *Connection) readResponse(r io.Reader) (Response, error) {
620652
respBytes, err := conn.read(r)
621653
if err != nil {
@@ -648,6 +680,15 @@ func (conn *Connection) readAuthResponse(r io.Reader) error {
648680
return nil
649681
}
650682

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

Diff for: connection_pool/connection_pool.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co
125125

126126
connPool = &ConnectionPool{
127127
addrs: make([]string, 0, len(addrs)),
128-
connOpts: connOpts.Copy(),
128+
connOpts: connOpts.Clone(),
129129
opts: opts,
130130
state: unknownState,
131131
done: make(chan struct{}),

0 commit comments

Comments
 (0)