Skip to content

Commit 3576750

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. After that, user may call API handles to check if it is possible to use a feature. 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 [2]. 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 [3] for more info. 1. tarantool/tarantool#6253 2. https://stackoverflow.com/a/52710077/11646599 3. tarantool/tarantool-python#262 Closes #120
1 parent c367122 commit 3576750

File tree

8 files changed

+316
-1
lines changed

8 files changed

+316
-1
lines changed

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

connection.go

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

148148
lastStreamId uint64
149+
150+
serverProtocolVersion ProtocolVersion
151+
serverFeatures []Feature
149152
}
150153

151154
var _ = Connector(&Connection{}) // Check compatibility with connector interface.
@@ -502,6 +505,13 @@ func (conn *Connection) dial() (err error) {
502505
conn.Greeting.Version = bytes.NewBuffer(greeting[:64]).String()
503506
conn.Greeting.auth = bytes.NewBuffer(greeting[64:108]).String()
504507

508+
// IPROTO_ID requests can be processed without authentication.
509+
// https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/requests/#iproto-id
510+
if err = conn.loadProtocolInfo(w, r); err != nil {
511+
connection.Close()
512+
return err
513+
}
514+
505515
// Auth
506516
if opts.User != "" {
507517
scr, err := scramble(conn.Greeting.auth, opts.Pass)
@@ -608,6 +618,18 @@ func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err
608618
return nil
609619
}
610620

621+
func (conn *Connection) writeIdRequest(w *bufio.Writer, version ProtocolVersion,
622+
features []Feature) (err error) {
623+
req := newIdRequest(version, features)
624+
625+
err = conn.writeRequest(w, req)
626+
if err != nil {
627+
return fmt.Errorf("id: %w", err)
628+
}
629+
630+
return nil
631+
}
632+
611633
func (conn *Connection) readResponse(r io.Reader) (resp Response, err error) {
612634
respBytes, err := conn.read(r)
613635
if err != nil {
@@ -639,6 +661,15 @@ func (conn *Connection) readAuthResponse(r io.Reader) (err error) {
639661
return nil
640662
}
641663

664+
func (conn *Connection) readIdResponse(r io.Reader) (resp Response, err error) {
665+
resp, err = conn.readResponse(r)
666+
if err != nil {
667+
return resp, fmt.Errorf("id: %w", err)
668+
}
669+
670+
return resp, nil
671+
}
672+
642673
func (conn *Connection) createConnection(reconnect bool) (err error) {
643674
var reconnects uint
644675
for conn.c == nil && conn.state == connDisconnected {
@@ -1182,3 +1213,71 @@ func (conn *Connection) NewStream() (*Stream, error) {
11821213
Conn: conn,
11831214
}, nil
11841215
}
1216+
1217+
// loadProtocolInfo sends info about client protocol,
1218+
// receives info about server protocol in response
1219+
// and store in in connection serverProtocolInfo.
1220+
func (conn *Connection) loadProtocolInfo(w *bufio.Writer, r *bufio.Reader) error {
1221+
var resp Response
1222+
var err error
1223+
1224+
err = conn.writeIdRequest(w, ClientProtocolVersion, ClientFeatures)
1225+
if err != nil {
1226+
return err
1227+
}
1228+
1229+
resp, err = conn.readIdResponse(r)
1230+
if err != nil {
1231+
tarantoolError, ok := err.(Error)
1232+
if ok && tarantoolError.Code == ErrUnknownRequestType {
1233+
// IPROTO_ID requests are not supported by server.
1234+
conn.serverProtocolVersion = ProtocolVersionUnsupported
1235+
conn.serverFeatures = []Feature{}
1236+
1237+
return nil
1238+
}
1239+
1240+
return err
1241+
}
1242+
1243+
if len(resp.Data) == 0 {
1244+
return fmt.Errorf("Unexpected response on protocol info exchange: no data")
1245+
}
1246+
1247+
serverProtocolInfo, ok := resp.Data[0].(protocolInfo)
1248+
if !ok {
1249+
return fmt.Errorf("Unexpected response on protocol info exchange: wrong data")
1250+
}
1251+
conn.serverProtocolVersion = serverProtocolInfo.version
1252+
conn.serverFeatures = serverProtocolInfo.features
1253+
1254+
return nil
1255+
}
1256+
1257+
// ServerProtocolVersion returns protocol version supported by
1258+
// connected Tarantool server.
1259+
// Since 1.10.0
1260+
func (conn *Connection) ServerProtocolVersion() ProtocolVersion {
1261+
return conn.serverProtocolVersion
1262+
}
1263+
1264+
// ClientProtocolVersion returns protocol version supported by
1265+
// Go connection client.
1266+
// Since 1.10.0
1267+
func (conn *Connection) ClientProtocolVersion() ProtocolVersion {
1268+
return ClientProtocolVersion
1269+
}
1270+
1271+
// DoesServerSupportFeature checks if expected feature
1272+
// is supported by connected Tarantool server.
1273+
// Since 1.10.0
1274+
func (conn *Connection) DoesServerSupportFeature(feature Feature) bool {
1275+
return isFeatureSupported(feature, conn.serverFeatures)
1276+
}
1277+
1278+
// DoesClientSupportFeature checks if expected feature
1279+
// is supported by Go connection client.
1280+
// Since 1.10.0
1281+
func (conn *Connection) DoesClientSupportFeature(feature Feature) bool {
1282+
return isFeatureSupported(feature, ClientFeatures)
1283+
}

connection_pool/connection_pool.go

+44
Original file line numberDiff line numberDiff line change
@@ -1028,3 +1028,47 @@ func newErrorFuture(err error) *tarantool.Future {
10281028
fut.SetError(err)
10291029
return fut
10301030
}
1031+
1032+
// ServerProtocolVersion returns protocol version supported by
1033+
// Tarantool server for a connection selected by userMode from connPool.
1034+
// Since 1.10.0
1035+
func (connPool *ConnectionPool) ServerProtocolVersion(userMode Mode) (tarantool.ProtocolVersion, error) {
1036+
conn, err := connPool.getNextConnection(userMode)
1037+
if err != nil {
1038+
return tarantool.ProtocolVersionUnsupported, err
1039+
}
1040+
return conn.ServerProtocolVersion(), nil
1041+
}
1042+
1043+
// ClientProtocolVersion returns protocol version supported by
1044+
// Go connection client for a connection selected by userMode from connPool.
1045+
// Since 1.10.0
1046+
func (connPool *ConnectionPool) ClientProtocolVersion(userMode Mode) (tarantool.ProtocolVersion, error) {
1047+
conn, err := connPool.getNextConnection(userMode)
1048+
if err != nil {
1049+
return tarantool.ProtocolVersionUnsupported, err
1050+
}
1051+
return conn.ClientProtocolVersion(), nil
1052+
}
1053+
1054+
// DoesServerSupportFeature checks if expected feature is supported
1055+
// by Tarantool server for a connection selected by userMode from connPool.
1056+
// Since 1.10.0
1057+
func (connPool *ConnectionPool) DoesServerSupportFeature(feature tarantool.Feature, userMode Mode) (bool, error) {
1058+
conn, err := connPool.getNextConnection(userMode)
1059+
if err != nil {
1060+
return false, err
1061+
}
1062+
return conn.DoesServerSupportFeature(feature), nil
1063+
}
1064+
1065+
// DoesClientSupportFeature checks if expected feature is supported
1066+
// by Go connection client for a connection selected by userMode from connPool.
1067+
// Since 1.10.0
1068+
func (connPool *ConnectionPool) DoesClientSupportFeature(feature tarantool.Feature, userMode Mode) (bool, error) {
1069+
conn, err := connPool.getNextConnection(userMode)
1070+
if err != nil {
1071+
return false, err
1072+
}
1073+
return conn.DoesClientSupportFeature(feature), nil
1074+
}

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

protocol.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package tarantool
2+
3+
type ProtocolVersion uint64
4+
type Feature uint64
5+
6+
type protocolInfo struct {
7+
version ProtocolVersion
8+
features []Feature
9+
}
10+
11+
const ProtocolVersionUnsupported ProtocolVersion = 0
12+
13+
const (
14+
// Streams support.
15+
FeatureStreams Feature = 0
16+
// Interactive transactions support.
17+
FeatureTransactions Feature = 1
18+
// Support of MP_ERROR object over MessagePack.
19+
FeatureErrorExtension Feature = 2
20+
// Support of watchers.
21+
FeatureWatchers Feature = 3
22+
)
23+
24+
// Protocol version supported by connector. Version 3
25+
// was introduced in Tarantool 2.10.0 and used in latest 2.10.4.
26+
const ClientProtocolVersion ProtocolVersion = 3
27+
28+
// Protocol features supported by connector.
29+
var ClientFeatures = []Feature{
30+
FeatureStreams,
31+
FeatureTransactions,
32+
}
33+
34+
func isFeatureSupported(feature Feature, supportedFeatures []Feature) bool {
35+
// It seems that iterating over a small list is way faster
36+
// than building a map: https://stackoverflow.com/a/52710077/11646599
37+
for _, supportedFeature := range supportedFeatures {
38+
if feature == supportedFeature {
39+
return true
40+
}
41+
}
42+
43+
return false
44+
}

request.go

+56
Original file line numberDiff line numberDiff line change
@@ -1106,3 +1106,59 @@ func (req *ExecuteRequest) Context(ctx context.Context) *ExecuteRequest {
11061106
req.ctx = ctx
11071107
return req
11081108
}
1109+
1110+
// IdRequest informs the server about supported protocol
1111+
// version and features.
1112+
type IdRequest struct {
1113+
baseRequest
1114+
protocolInfo
1115+
}
1116+
1117+
// newIdRequest returns a new IdRequest.
1118+
func newIdRequest(version ProtocolVersion, features []Feature) *IdRequest {
1119+
req := new(IdRequest)
1120+
req.requestCode = IdRequestCode
1121+
req.version = version
1122+
req.features = features
1123+
return req
1124+
}
1125+
1126+
// Body fills an encoder with the id request body.
1127+
func (req *IdRequest) Body(res SchemaResolver, enc *encoder) error {
1128+
return req.fillIdRequest(enc)
1129+
}
1130+
1131+
// Context sets a passed context to the request.
1132+
//
1133+
// Pay attention that when using context with request objects,
1134+
// the timeout option for Connection does not affect the lifetime
1135+
// of the request. For those purposes use context.WithTimeout() as
1136+
// the root context.
1137+
func (req *IdRequest) Context(ctx context.Context) *IdRequest {
1138+
req.ctx = ctx
1139+
return req
1140+
}
1141+
1142+
func (req *IdRequest) fillIdRequest(enc *encoder) error {
1143+
enc.EncodeMapLen(2)
1144+
1145+
encodeUint(enc, KeyVersion)
1146+
if err := enc.Encode(req.version); err != nil {
1147+
return err
1148+
}
1149+
1150+
encodeUint(enc, KeyFeatures)
1151+
1152+
t := len(req.features)
1153+
if err := enc.EncodeArrayLen(t); err != nil {
1154+
return err
1155+
}
1156+
1157+
for _, feature := range req.features {
1158+
if err := enc.Encode(feature); err != nil {
1159+
return err
1160+
}
1161+
}
1162+
1163+
return nil
1164+
}

response.go

+30-1
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,14 @@ func (resp *Response) decodeBody() (err error) {
147147
offset := resp.buf.Offset()
148148
defer resp.buf.Seek(offset)
149149

150-
var l int
150+
var l, larr int
151151
var stmtID, bindCount uint64
152+
var serverProtocolInfo protocolInfo = protocolInfo{
153+
version: ProtocolVersionUnsupported,
154+
features: []Feature{},
155+
}
156+
var feature Feature
157+
isProtocolInfoResponse := false
152158

153159
d := newDecoder(&resp.buf)
154160

@@ -190,6 +196,24 @@ func (resp *Response) decodeBody() (err error) {
190196
if bindCount, err = d.DecodeUint64(); err != nil {
191197
return err
192198
}
199+
case KeyVersion:
200+
isProtocolInfoResponse = true
201+
if err = d.Decode(&serverProtocolInfo.version); err != nil {
202+
return err
203+
}
204+
case KeyFeatures:
205+
isProtocolInfoResponse = true
206+
if larr, err = d.DecodeArrayLen(); err != nil {
207+
return err
208+
}
209+
210+
serverProtocolInfo.features = make([]Feature, larr)
211+
for i := 0; i < larr; i++ {
212+
if err = d.Decode(&feature); err != nil {
213+
return err
214+
}
215+
serverProtocolInfo.features[i] = feature
216+
}
193217
default:
194218
if err = d.Skip(); err != nil {
195219
return err
@@ -204,6 +228,11 @@ func (resp *Response) decodeBody() (err error) {
204228
}
205229
resp.Data = []interface{}{stmt}
206230
}
231+
232+
if isProtocolInfoResponse {
233+
resp.Data = []interface{}{serverProtocolInfo}
234+
}
235+
207236
if resp.Code != OkCode && resp.Code != PushCode {
208237
resp.Code &^= ErrorCodeBit
209238
err = Error{resp.Code, resp.Error}

0 commit comments

Comments
 (0)