Skip to content

Commit

Permalink
api: support iproto feature discovery
Browse files Browse the repository at this point in the history
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
  • Loading branch information
DifferentialOrange committed Nov 29, 2022
1 parent f433cf2 commit 33d0483
Show file tree
Hide file tree
Showing 12 changed files with 844 additions and 56 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.

### Added

- Support iproto feature discovery (#120).

### Changed

### Fixed
Expand Down
128 changes: 128 additions & 0 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"math"
"net"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
Expand Down Expand Up @@ -146,6 +147,8 @@ type Connection struct {
lenbuf [PacketLengthBytes]byte

lastStreamId uint64

serverProtocolInfo ProtocolInfo
}

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

// SslOpts is a way to configure ssl transport.
Expand All @@ -294,8 +301,11 @@ type SslOpts struct {
}

// Clone returns a copy of the Opts object.
// Any changes in copy RequiredProtocolInfo will not affect the original
// RequiredProtocolInfo value.
func (opts Opts) Clone() Opts {
optsCopy := opts
optsCopy.RequiredProtocolInfo = opts.RequiredProtocolInfo.Clone()

return optsCopy
}
Expand Down Expand Up @@ -509,6 +519,18 @@ func (conn *Connection) dial() (err error) {
conn.Greeting.Version = bytes.NewBuffer(greeting[:64]).String()
conn.Greeting.auth = bytes.NewBuffer(greeting[64:108]).String()

// IPROTO_ID requests can be processed without authentication.
// https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/requests/#iproto-id
if err = conn.identify(w, r); err != nil {
connection.Close()
return err
}

if err = checkProtocolInfo(opts.RequiredProtocolInfo, conn.serverProtocolInfo); err != nil {
connection.Close()
return fmt.Errorf("identify: %w", err)
}

// Auth
if opts.User != "" {
scr, err := scramble(conn.Greeting.auth, opts.Pass)
Expand Down Expand Up @@ -615,6 +637,17 @@ func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) error
return nil
}

func (conn *Connection) writeIdRequest(w *bufio.Writer, protocolInfo ProtocolInfo) error {
req := NewIdRequest(protocolInfo)

err := conn.writeRequest(w, req)
if err != nil {
return fmt.Errorf("identify: %w", err)
}

return nil
}

func (conn *Connection) readResponse(r io.Reader) (Response, error) {
respBytes, err := conn.read(r)
if err != nil {
Expand Down Expand Up @@ -647,6 +680,15 @@ func (conn *Connection) readAuthResponse(r io.Reader) error {
return nil
}

func (conn *Connection) readIdResponse(r io.Reader) (Response, error) {
resp, err := conn.readResponse(r)
if err != nil {
return resp, fmt.Errorf("identify: %w", err)
}

return resp, nil
}

func (conn *Connection) createConnection(reconnect bool) (err error) {
var reconnects uint
for conn.c == nil && conn.state == connDisconnected {
Expand Down Expand Up @@ -1190,3 +1232,89 @@ func (conn *Connection) NewStream() (*Stream, error) {
Conn: conn,
}, nil
}

// checkProtocolInfo checks that expected protocol version is
// and protocol features are supported.
func checkProtocolInfo(expected ProtocolInfo, actual ProtocolInfo) error {
var found bool
var missingFeatures []ProtocolFeature

if expected.Version > actual.Version {
return fmt.Errorf("protocol version %d is not supported", expected.Version)
}

// It seems that iterating over a small list is way faster
// than building a map: https://stackoverflow.com/a/52710077/11646599
for _, expectedFeature := range expected.Features {
found = false
for _, actualFeature := range actual.Features {
if expectedFeature == actualFeature {
found = true
}
}
if !found {
missingFeatures = append(missingFeatures, expectedFeature)
}
}

if len(missingFeatures) == 1 {
return fmt.Errorf("protocol feature %s is not supported", missingFeatures[0])
}

if len(missingFeatures) > 1 {
var sarr []string
for _, missingFeature := range missingFeatures {
sarr = append(sarr, missingFeature.String())
}
return fmt.Errorf("protocol features %s are not supported", strings.Join(sarr, ", "))
}

return nil
}

// identify sends info about client protocol, receives info
// about server protocol in response and stores it in the connection.
func (conn *Connection) identify(w *bufio.Writer, r *bufio.Reader) error {
var ok bool

werr := conn.writeIdRequest(w, clientProtocolInfo)
if werr != nil {
return werr
}

resp, rerr := conn.readIdResponse(r)
if rerr != nil {
if resp.Code == ErrUnknownRequestType {
// IPROTO_ID requests are not supported by server.
return nil
}

return rerr
}

if len(resp.Data) == 0 {
return fmt.Errorf("identify: unexpected response: no data")
}

conn.serverProtocolInfo, ok = resp.Data[0].(ProtocolInfo)
if !ok {
return fmt.Errorf("identify: unexpected response: wrong data")
}

return nil
}

// ServerProtocolVersion returns protocol version and protocol features
// supported by connected Tarantool server. Beware that values might be
// outdated if connection is in a disconnected state.
// Since 1.10.0
func (conn *Connection) ServerProtocolInfo() ProtocolInfo {
return conn.serverProtocolInfo.Clone()
}

// ClientProtocolVersion returns protocol version and protocol features
// supported by Go connection client.
// Since 1.10.0
func (conn *Connection) ClientProtocolInfo() ProtocolInfo {
return clientProtocolInfo.Clone()
}
54 changes: 36 additions & 18 deletions connection_pool/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Tuple struct {

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

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

func ExampleConnectionPool_Select() {
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand Down Expand Up @@ -94,7 +94,7 @@ func ExampleConnectionPool_Select() {
}

func ExampleConnectionPool_SelectTyped() {
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand Down Expand Up @@ -156,7 +156,7 @@ func ExampleConnectionPool_SelectTyped() {
}

func ExampleConnectionPool_SelectAsync() {
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand Down Expand Up @@ -239,7 +239,7 @@ func ExampleConnectionPool_SelectAsync() {

func ExampleConnectionPool_SelectAsync_err() {
roles := []bool{true, true, true, true, true}
pool, err := examplePool(roles)
pool, err := examplePool(roles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand All @@ -258,7 +258,7 @@ func ExampleConnectionPool_SelectAsync_err() {
}

func ExampleConnectionPool_Ping() {
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand All @@ -276,7 +276,7 @@ func ExampleConnectionPool_Ping() {
}

func ExampleConnectionPool_Insert() {
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand Down Expand Up @@ -325,7 +325,7 @@ func ExampleConnectionPool_Insert() {
}

func ExampleConnectionPool_Delete() {
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand Down Expand Up @@ -377,7 +377,7 @@ func ExampleConnectionPool_Delete() {
}

func ExampleConnectionPool_Replace() {
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand Down Expand Up @@ -448,7 +448,7 @@ func ExampleConnectionPool_Replace() {
}

func ExampleConnectionPool_Update() {
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand Down Expand Up @@ -492,7 +492,7 @@ func ExampleConnectionPool_Update() {
}

func ExampleConnectionPool_Call() {
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand All @@ -512,7 +512,7 @@ func ExampleConnectionPool_Call() {
}

func ExampleConnectionPool_Eval() {
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand All @@ -532,7 +532,7 @@ func ExampleConnectionPool_Eval() {
}

func ExampleConnectionPool_Do() {
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand All @@ -551,7 +551,7 @@ func ExampleConnectionPool_Do() {
}

func ExampleConnectionPool_NewPrepared() {
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand All @@ -575,6 +575,21 @@ func ExampleConnectionPool_NewPrepared() {
}
}

func getTestTxnOpts() tarantool.Opts {
txnOpts := connOpts.Clone()

// Assert that server supports expected protocol features
txnOpts.RequiredProtocolInfo = tarantool.ProtocolInfo{
Version: tarantool.ProtocolVersion(1),
Features: []tarantool.ProtocolFeature{
tarantool.StreamsFeature,
tarantool.TransactionsFeature,
},
}

return txnOpts
}

func ExampleCommitRequest() {
var req tarantool.Request
var resp *tarantool.Response
Expand All @@ -586,7 +601,8 @@ func ExampleCommitRequest() {
return
}

pool, err := examplePool(testRoles)
txnOpts := getTestTxnOpts()
pool, err := examplePool(testRoles, txnOpts)
if err != nil {
fmt.Println(err)
return
Expand Down Expand Up @@ -672,8 +688,9 @@ func ExampleRollbackRequest() {
return
}

txnOpts := getTestTxnOpts()
// example pool has only one rw instance
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, txnOpts)
if err != nil {
fmt.Println(err)
return
Expand Down Expand Up @@ -758,8 +775,9 @@ func ExampleBeginRequest_TxnIsolation() {
return
}

txnOpts := getTestTxnOpts()
// example pool has only one rw instance
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, txnOpts)
if err != nil {
fmt.Println(err)
return
Expand Down Expand Up @@ -836,7 +854,7 @@ func ExampleBeginRequest_TxnIsolation() {
}

func ExampleConnectorAdapter() {
pool, err := examplePool(testRoles)
pool, err := examplePool(testRoles, connOpts)
if err != nil {
fmt.Println(err)
}
Expand Down
Loading

0 comments on commit 33d0483

Please sign in to comment.