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 28, 2022
1 parent a20f033 commit 73b1e83
Show file tree
Hide file tree
Showing 14 changed files with 849 additions and 62 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
135 changes: 131 additions & 4 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 @@ -293,10 +300,12 @@ type SslOpts struct {
Ciphers string
}

// Copy returns the copy of an Opts object.
// Beware that Notify channel, Logger and Handle are not copied.
func (opts Opts) Copy() Opts {
// 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 @@ -327,7 +336,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) {
contextRequestId: 1,
Greeting: &Greeting{},
control: make(chan struct{}),
opts: opts.Copy(),
opts: opts.Clone(),
dec: newDecoder(&smallBuf{}),
}
maxprocs := uint32(runtime.GOMAXPROCS(-1))
Expand Down Expand Up @@ -510,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 @@ -616,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 @@ -648,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 @@ -1191,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()
}
2 changes: 1 addition & 1 deletion connection_pool/connection_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co

connPool = &ConnectionPool{
addrs: make([]string, 0, len(addrs)),
connOpts: connOpts.Copy(),
connOpts: connOpts.Clone(),
opts: opts,
state: unknownState,
done: make(chan struct{}),
Expand Down
Loading

0 comments on commit 73b1e83

Please sign in to comment.