From efd431ca44c104e9f9218a5e1f679477321b1407 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Sat, 20 Dec 2014 12:20:19 +0300 Subject: [PATCH 001/605] test --- connection.go | 55 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/connection.go b/connection.go index 08d4fa6be..cebb44088 100644 --- a/connection.go +++ b/connection.go @@ -2,7 +2,6 @@ package tarantool import ( "net" - "fmt" "github.com/vmihailenco/msgpack" "sync/atomic" "bytes" @@ -16,6 +15,8 @@ type Connection struct { Greeting *Greeting requests map[uint32]chan *Response packets chan []byte + opts Opts + state uint } type Greeting struct { @@ -23,38 +24,62 @@ type Greeting struct { auth string } -func Connect(addr string) (conn *Connection, err error) { - fmt.Printf("Connecting to %s ...\n", addr) +type Opts { + pingInterval int // seconds + timeout int // microseconds + reconnect bool +} + +const ( + stConnecting = iota + stEstablished + stBroken +) + +func Connect(addr string, opts Opts) (conn *Connection, err error) { connection, err := net.Dial("tcp", addr) if err != nil { return } connection.(*net.TCPConn).SetNoDelay(true) - fmt.Println("Connected ...") - - conn = &Connection{ connection, &sync.Mutex{}, 0, &Greeting{}, make(map[uint32]chan *Response), make(chan []byte) } + conn = &Connection{ + connection: connection, + mutex: &sync.Mutex{}, + requestId: 0, + greeting: &Greeting{}, + requests: make(map[uint32]chan *Response), + packets: make(chan []byte), + opts: opts, + state: stConnecting + } err = conn.handShake() go conn.writer() go conn.reader() + //go conn.pinger() return } +func (conn *Connection) Close() (err error) { + // TODO close all pending responses + return conn.connection.Close() +} + +func (conn *Connection) getConnection() (connection net.Conn, err error) { + return conn.connection +} + func (conn *Connection) handShake() (err error) { - fmt.Printf("Greeting ... ") greeting := make([]byte, 128) _, err = conn.connection.Read(greeting) if err != nil { - fmt.Println("Error") return } conn.Greeting.version = bytes.NewBuffer(greeting[:64]).String() conn.Greeting.auth = bytes.NewBuffer(greeting[64:]).String() - fmt.Println("Success") - fmt.Println("Version:", conn.Greeting.version) return } @@ -92,6 +117,16 @@ func (conn *Connection) reader() { } } +func (conn *Connection) pinger() { + for { + resp, err := conn.Ping() + if err != nil { + conn.Close() + // TODO: reconnect if network + } + } +} + func (conn *Connection) write(data []byte) (err error) { l, err := conn.connection.Write(data) if l != len(data) { From f4e787a17b7f3e43b1218f6af49469087a7fa91f Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Sat, 20 Dec 2014 20:36:59 +0300 Subject: [PATCH 002/605] test --- connection.go | 117 ++++++++++++++++++++++++++++++-------------------- const.go | 2 + response.go | 9 +++- 3 files changed, 81 insertions(+), 47 deletions(-) diff --git a/connection.go b/connection.go index cebb44088..08c1417a0 100644 --- a/connection.go +++ b/connection.go @@ -9,6 +9,7 @@ import ( ) type Connection struct { + addr string connection net.Conn mutex *sync.Mutex requestId uint32 @@ -37,62 +38,81 @@ const ( ) func Connect(addr string, opts Opts) (conn *Connection, err error) { - connection, err := net.Dial("tcp", addr) - if err != nil { - return - } - connection.(*net.TCPConn).SetNoDelay(true) conn = &Connection{ - connection: connection, + addr: addr, + connection: nil, mutex: &sync.Mutex{}, requestId: 0, greeting: &Greeting{}, requests: make(map[uint32]chan *Response), packets: make(chan []byte), opts: opts, - state: stConnecting } - err = conn.handShake() + + err = co.dial() + if err != nil { + return + } go conn.writer() go conn.reader() - //go conn.pinger() return } -func (conn *Connection) Close() (err error) { - // TODO close all pending responses - return conn.connection.Close() -} - -func (conn *Connection) getConnection() (connection net.Conn, err error) { - return conn.connection -} - -func (conn *Connection) handShake() (err error) { +func (conn *Connection) dial() (err error) { + connection, err := net.Dial("tcp", conn.addr) + if err != nil { + return + } + connection.(*net.TCPConn).SetNoDelay(true) + conn.connection = connection greeting := make([]byte, 128) + // TODO: read all _, err = conn.connection.Read(greeting) if err != nil { return } conn.Greeting.version = bytes.NewBuffer(greeting[:64]).String() conn.Greeting.auth = bytes.NewBuffer(greeting[64:]).String() - return } -func (conn *Connection) writer(){ +func (conn *Connection) getConnection() (connection net.Conn, err error) { + conn.mutex.Lock() + defer conn.mutex.Unlock() + if conn.connection == nil { + err = conn.dial() + } + return conn.connection, err +} + +func (conn *Connection) closeConnection(err error) { + conn.mutex.Lock() + defer conn.mutex.Unlock() + conn.connection.Close() + conn.connection = nil + for requestId, respChan := range(conn.requests) { + respChan <- NewNetErrResponse(err) + delete(conn.requests, requestId) + close(respChan) + } +} + +func (conn *Connection) writer() { var ( err error packet []byte + connection net.Conn ) for { packet = <- conn.packets - err = conn.write(packet) + connetion = conn.getConnection() + err = write(connection, packet) if err != nil { - panic(err) + conn.closeConnection(err) + continue } } } @@ -101,64 +121,63 @@ func (conn *Connection) reader() { var ( err error resp_bytes []byte + connection net.Conn ) for { - resp_bytes, err = conn.read() + connection = conn.getConnection() + resp_bytes, err = read(connection) if err != nil { - panic(err) + conn.closeConnection(err) + continue } - resp := NewResponse(resp_bytes) respChan := conn.requests[resp.RequestId] conn.mutex.Lock() delete(conn.requests, resp.RequestId) conn.mutex.Unlock() - respChan <- resp - } -} - -func (conn *Connection) pinger() { - for { - resp, err := conn.Ping() - if err != nil { - conn.Close() - // TODO: reconnect if network + if respChan != nil { + respChan <- resp + } else { + log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) } } } -func (conn *Connection) write(data []byte) (err error) { - l, err := conn.connection.Write(data) +func write(connection net.Conn, data []byte) (err error) { + l, err := connection.Write(data) + if err != nil { + return + } if l != len(data) { panic("Wrong length writed") } return } -func (conn *Connection) read() (response []byte, err error){ +func read(connection net.Conn) (response []byte, err error){ var length_uint uint32 var l, tl int - length := make([]byte, PacketLengthBytes) + length := make([]byte, PacketLengthBytes) tl = 0 for tl < int(PacketLengthBytes) { - l, err = conn.connection.Read(length[tl:]) + l, err = connection.Read(length[tl:]) tl += l if err != nil { return } } - err = msgpack.Unmarshal(length, &length_uint) + err = msgpack.Unmarshal(length, &length_uint) if err != nil { return } response = make([]byte, length_uint) - if(length_uint > 0){ + if length_uint > 0 { tl = 0 for tl < int(length_uint) { - l, err = conn.connection.Read(response[tl:]) + l, err = connection.Read(response[tl:]) tl += l if err != nil { return @@ -170,6 +189,12 @@ func (conn *Connection) read() (response []byte, err error){ } func (conn *Connection) nextRequestId() (requestId uint32) { - conn.requestId = atomic.AddUint32(&conn.requestId, 1) - return conn.requestId + return atomic.AddUint32(&conn.requestId, 1) +} + +func (conn *Connection) Close() (err error) { + // TODO close all pending responses + conn.closeConnection(errors.New("connection closed")) + return conn.connection.Close() } + diff --git a/const.go b/const.go index 54eb4b4bf..04285feab 100644 --- a/const.go +++ b/const.go @@ -38,5 +38,7 @@ const ( OkCode = uint32(0) + NetErrCode = uint32(0xfffffff1) // fake code to wrap network problems into response + PacketLengthBytes = 5 ) diff --git a/response.go b/response.go index 1c19b4f57..cdc66e62c 100644 --- a/response.go +++ b/response.go @@ -30,5 +30,12 @@ func NewResponse(bytes []byte) (resp *Response) { resp.Error = body[KeyError].(string) } - return + return +} + +func NewNetErrResponse(err error) (resp *Response) { + resp = &Response{} + resp.Code = NetErrorCode + resp.Error = err + return } From 0cfcb80d1def682cf95cdd4584f14cc409baf68a Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Mon, 22 Dec 2014 12:00:29 +0300 Subject: [PATCH 003/605] timeouts implemented --- connection.go | 87 +++++++++++++++++++++++++++------------------------ const.go | 2 +- request.go | 20 +++++++++++- response.go | 15 +++++++-- 4 files changed, 78 insertions(+), 46 deletions(-) diff --git a/connection.go b/connection.go index 08c1417a0..bf36e93de 100644 --- a/connection.go +++ b/connection.go @@ -6,6 +6,9 @@ import ( "sync/atomic" "bytes" "sync" + "time" + "log" + "errors" ) type Connection struct { @@ -17,7 +20,7 @@ type Connection struct { requests map[uint32]chan *Response packets chan []byte opts Opts - state uint + closed bool } type Greeting struct { @@ -25,17 +28,11 @@ type Greeting struct { auth string } -type Opts { - pingInterval int // seconds - timeout int // microseconds - reconnect bool +type Opts struct { + Timeout time.Duration // milliseconds + Reconnect time.Duration // milliseconds } -const ( - stConnecting = iota - stEstablished - stBroken -) func Connect(addr string, opts Opts) (conn *Connection, err error) { @@ -44,20 +41,25 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { connection: nil, mutex: &sync.Mutex{}, requestId: 0, - greeting: &Greeting{}, + Greeting: &Greeting{}, requests: make(map[uint32]chan *Response), packets: make(chan []byte), opts: opts, } - err = co.dial() + err = conn.dial() if err != nil { return } go conn.writer() go conn.reader() + return +} +func (conn *Connection) Close() (err error) { + conn.closed = true; + err = conn.closeConnection(errors.New("client closed connection")) return } @@ -79,37 +81,49 @@ func (conn *Connection) dial() (err error) { return } -func (conn *Connection) getConnection() (connection net.Conn, err error) { +func (conn *Connection) getConnection() (connection net.Conn) { conn.mutex.Lock() defer conn.mutex.Unlock() - if conn.connection == nil { - err = conn.dial() + for conn.connection == nil { + if conn.closed { + return nil + } + err := conn.dial() + if err == nil { + break + } else if conn.opts.Reconnect > 0 { + time.Sleep(conn.opts.Reconnect) + } else { + return nil + } } - return conn.connection, err + return conn.connection } -func (conn *Connection) closeConnection(err error) { +func (conn *Connection) closeConnection(neterr error) (err error) { conn.mutex.Lock() defer conn.mutex.Unlock() - conn.connection.Close() + if conn.connection == nil { + return + } + err = conn.connection.Close() conn.connection = nil for requestId, respChan := range(conn.requests) { - respChan <- NewNetErrResponse(err) + respChan <- FakeResponse(NetErrCode, neterr) delete(conn.requests, requestId) close(respChan) } + return } func (conn *Connection) writer() { - var ( - err error - packet []byte - connection net.Conn - ) for { - packet = <- conn.packets - connetion = conn.getConnection() - err = write(connection, packet) + packet := <- conn.packets + connection := conn.getConnection() + if connection == nil { + return + } + err := write(connection, packet) if err != nil { conn.closeConnection(err) continue @@ -118,14 +132,12 @@ func (conn *Connection) writer() { } func (conn *Connection) reader() { - var ( - err error - resp_bytes []byte - connection net.Conn - ) for { - connection = conn.getConnection() - resp_bytes, err = read(connection) + connection := conn.getConnection() + if connection == nil { + return + } + resp_bytes, err := read(connection) if err != nil { conn.closeConnection(err) continue @@ -191,10 +203,3 @@ func read(connection net.Conn) (response []byte, err error){ func (conn *Connection) nextRequestId() (requestId uint32) { return atomic.AddUint32(&conn.requestId, 1) } - -func (conn *Connection) Close() (err error) { - // TODO close all pending responses - conn.closeConnection(errors.New("connection closed")) - return conn.connection.Close() -} - diff --git a/const.go b/const.go index 04285feab..d55fb619e 100644 --- a/const.go +++ b/const.go @@ -37,8 +37,8 @@ const ( IterBitsAllNotSet = uint32(9) // all bits are not set OkCode = uint32(0) - NetErrCode = uint32(0xfffffff1) // fake code to wrap network problems into response + TimeoutErrCode = uint32(0xfffffff2) // fake code to wrap timeout error into repsonse PacketLengthBytes = 5 ) diff --git a/request.go b/request.go index a8e721833..180e4de2f 100644 --- a/request.go +++ b/request.go @@ -3,6 +3,7 @@ package tarantool import( "github.com/vmihailenco/msgpack" "errors" + "time" ) type Request struct { @@ -109,6 +110,10 @@ func (conn *Connection) Auth(key, tuple []interface{}) (resp *Response, err erro func (req *Request) perform() (resp *Response, err error) { + if req.conn.closed { + return nil, errors.New("using closed connection") + } + packet, err := req.pack() if err != nil { return @@ -121,7 +126,20 @@ func (req *Request) perform() (resp *Response, err error) { req.conn.mutex.Unlock() req.conn.packets <- (packet) - resp = <-responseChan + + if req.conn.opts.Timeout > 0 { + select { + case resp = <-responseChan: + break + case <-time.After(req.conn.opts.Timeout): + req.conn.mutex.Lock() + delete(req.conn.requests, req.requestId) + req.conn.mutex.Unlock() + resp = FakeResponse(TimeoutErrCode, errors.New("client timeout")) + } + } else { + resp = <-responseChan + } if resp.Error != "" { err = errors.New(resp.Error) diff --git a/response.go b/response.go index cdc66e62c..78ab81818 100644 --- a/response.go +++ b/response.go @@ -1,6 +1,7 @@ package tarantool import( + "fmt" "github.com/vmihailenco/msgpack" ) @@ -33,9 +34,17 @@ func NewResponse(bytes []byte) (resp *Response) { return } -func NewNetErrResponse(err error) (resp *Response) { +func FakeResponse(code uint32, err error) (resp *Response) { resp = &Response{} - resp.Code = NetErrorCode - resp.Error = err + resp.Code = code + resp.Error = fmt.Sprintf("%s",err) + return +} + +func (resp *Response) GoString (str string) { + str = fmt.Sprintf("<%d %d '%s'>\n", resp.RequestId, resp.Code, resp.Error) + for t := range(resp.Data) { + str += fmt.Sprintf("%v\n", t) + } return } From 8c65d5ccf67eebec3cc2b50bff3f80562da2379d Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Tue, 23 Dec 2014 12:54:17 +0300 Subject: [PATCH 004/605] test --- connection.go | 10 +++++----- request.go | 13 +++++++++---- response.go | 12 +++++------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/connection.go b/connection.go index bf36e93de..8e07ce057 100644 --- a/connection.go +++ b/connection.go @@ -17,7 +17,7 @@ type Connection struct { mutex *sync.Mutex requestId uint32 Greeting *Greeting - requests map[uint32]chan *Response + requests map[uint32]chan *ResponseAndError packets chan []byte opts Opts closed bool @@ -42,7 +42,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { mutex: &sync.Mutex{}, requestId: 0, Greeting: &Greeting{}, - requests: make(map[uint32]chan *Response), + requests: make(map[uint32]chan *ResponseAndError), packets: make(chan []byte), opts: opts, } @@ -109,7 +109,7 @@ func (conn *Connection) closeConnection(neterr error) (err error) { err = conn.connection.Close() conn.connection = nil for requestId, respChan := range(conn.requests) { - respChan <- FakeResponse(NetErrCode, neterr) + respChan <- &ResponseAndError{nil, neterr} delete(conn.requests, requestId) close(respChan) } @@ -143,12 +143,12 @@ func (conn *Connection) reader() { continue } resp := NewResponse(resp_bytes) - respChan := conn.requests[resp.RequestId] conn.mutex.Lock() + respChan := conn.requests[resp.RequestId] delete(conn.requests, resp.RequestId) conn.mutex.Unlock() if respChan != nil { - respChan <- resp + respChan <- &ResponseAndError{resp, nil} } else { log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) } diff --git a/request.go b/request.go index 180e4de2f..f8a31ed38 100644 --- a/request.go +++ b/request.go @@ -129,19 +129,24 @@ func (req *Request) perform() (resp *Response, err error) { if req.conn.opts.Timeout > 0 { select { - case resp = <-responseChan: + case respAndErr := <-responseChan: + resp = respAndErr.resp + err = respAndErr.err break case <-time.After(req.conn.opts.Timeout): req.conn.mutex.Lock() delete(req.conn.requests, req.requestId) req.conn.mutex.Unlock() - resp = FakeResponse(TimeoutErrCode, errors.New("client timeout")) + resp = nil + err = errors.New("client timeout") } } else { - resp = <-responseChan + respAndError := <-responseChan + resp = respAndError.resp + err = respAndError.err } - if resp.Error != "" { + if resp != nil && resp.Error != "" { err = errors.New(resp.Error) } return diff --git a/response.go b/response.go index 78ab81818..4d2165e2f 100644 --- a/response.go +++ b/response.go @@ -12,6 +12,11 @@ type Response struct { Data []interface{} } +type ResponseAndError struct { + resp *Response + err error +} + func NewResponse(bytes []byte) (resp *Response) { var header, body map[int32]interface{} resp = &Response{} @@ -34,13 +39,6 @@ func NewResponse(bytes []byte) (resp *Response) { return } -func FakeResponse(code uint32, err error) (resp *Response) { - resp = &Response{} - resp.Code = code - resp.Error = fmt.Sprintf("%s",err) - return -} - func (resp *Response) GoString (str string) { str = fmt.Sprintf("<%d %d '%s'>\n", resp.RequestId, resp.Code, resp.Error) for t := range(resp.Data) { From bad4d0eff840e838a2ef0e182fcb76d3ab5ec19a Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Wed, 24 Dec 2014 02:39:11 +0300 Subject: [PATCH 005/605] test --- connection.go | 8 ++++---- request.go | 2 +- response.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/connection.go b/connection.go index 8e07ce057..6caff5fd1 100644 --- a/connection.go +++ b/connection.go @@ -17,7 +17,7 @@ type Connection struct { mutex *sync.Mutex requestId uint32 Greeting *Greeting - requests map[uint32]chan *ResponseAndError + requests map[uint32]chan *responseAndError packets chan []byte opts Opts closed bool @@ -42,7 +42,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { mutex: &sync.Mutex{}, requestId: 0, Greeting: &Greeting{}, - requests: make(map[uint32]chan *ResponseAndError), + requests: make(map[uint32]chan *responseAndError), packets: make(chan []byte), opts: opts, } @@ -109,7 +109,7 @@ func (conn *Connection) closeConnection(neterr error) (err error) { err = conn.connection.Close() conn.connection = nil for requestId, respChan := range(conn.requests) { - respChan <- &ResponseAndError{nil, neterr} + respChan <- &responseAndError{nil, neterr} delete(conn.requests, requestId) close(respChan) } @@ -148,7 +148,7 @@ func (conn *Connection) reader() { delete(conn.requests, resp.RequestId) conn.mutex.Unlock() if respChan != nil { - respChan <- &ResponseAndError{resp, nil} + respChan <- &responseAndError{resp, nil} } else { log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) } diff --git a/request.go b/request.go index f8a31ed38..1574a9ddf 100644 --- a/request.go +++ b/request.go @@ -119,7 +119,7 @@ func (req *Request) perform() (resp *Response, err error) { return } - responseChan := make(chan *Response) + responseChan := make(chan *responseAndError) req.conn.mutex.Lock() req.conn.requests[req.requestId] = responseChan diff --git a/response.go b/response.go index 4d2165e2f..5142d9689 100644 --- a/response.go +++ b/response.go @@ -12,7 +12,7 @@ type Response struct { Data []interface{} } -type ResponseAndError struct { +type responseAndError struct { resp *Response err error } From 4b5e0aa17af42fa49a3a4faacf9bf2e8890d4715 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Thu, 25 Dec 2014 19:03:59 +0300 Subject: [PATCH 006/605] minor --- connection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connection.go b/connection.go index 6caff5fd1..9bd0d590d 100644 --- a/connection.go +++ b/connection.go @@ -118,7 +118,7 @@ func (conn *Connection) closeConnection(neterr error) (err error) { func (conn *Connection) writer() { for { - packet := <- conn.packets + packet := <-conn.packets connection := conn.getConnection() if connection == nil { return From e56110f1d18811898331aa6887a2392af4c824ed Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 8 Jan 2015 19:21:46 +0300 Subject: [PATCH 007/605] optimize: explicitly stop request timeout Timer --- request.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/request.go b/request.go index 1574a9ddf..984a92be4 100644 --- a/request.go +++ b/request.go @@ -128,12 +128,14 @@ func (req *Request) perform() (resp *Response, err error) { req.conn.packets <- (packet) if req.conn.opts.Timeout > 0 { + timer := time.NewTimer(req.conn.opts.Timeout) select { case respAndErr := <-responseChan: + timer.Stop() resp = respAndErr.resp err = respAndErr.err break - case <-time.After(req.conn.opts.Timeout): + case <-timer.C: req.conn.mutex.Lock() delete(req.conn.requests, req.requestId) req.conn.mutex.Unlock() From bcfd9a7a7ebd1ad6d6ba5ffd079155a75e7e846d Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 8 Jan 2015 19:28:32 +0300 Subject: [PATCH 008/605] fix possible deadlock if channel is not buffered, then write to is locked until someone reads it. Combined with timeout, it could lead to deadlock. --- request.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/request.go b/request.go index 984a92be4..2f860f48e 100644 --- a/request.go +++ b/request.go @@ -119,7 +119,7 @@ func (req *Request) perform() (resp *Response, err error) { return } - responseChan := make(chan *responseAndError) + responseChan := make(chan *responseAndError, 1) req.conn.mutex.Lock() req.conn.requests[req.requestId] = responseChan From d25a39a720dcebc87e2bcbcdd4fe158d8588a726 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 8 Jan 2015 19:56:09 +0300 Subject: [PATCH 009/605] fix import msgpack --- connection.go | 2 +- request.go | 2 +- response.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/connection.go b/connection.go index 9bd0d590d..c3ae518aa 100644 --- a/connection.go +++ b/connection.go @@ -2,7 +2,7 @@ package tarantool import ( "net" - "github.com/vmihailenco/msgpack" + "gopkg.in/vmihailenco/msgpack.v2" "sync/atomic" "bytes" "sync" diff --git a/request.go b/request.go index 2f860f48e..1e952bf50 100644 --- a/request.go +++ b/request.go @@ -1,7 +1,7 @@ package tarantool import( - "github.com/vmihailenco/msgpack" + "gopkg.in/vmihailenco/msgpack.v2" "errors" "time" ) diff --git a/response.go b/response.go index 5142d9689..93b27b053 100644 --- a/response.go +++ b/response.go @@ -2,7 +2,7 @@ package tarantool import( "fmt" - "github.com/vmihailenco/msgpack" + "gopkg.in/vmihailenco/msgpack.v2" ) type Response struct { From 2b86b0b7081673fb49c5c82501ddf2b07f0feca1 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 8 Jan 2015 20:00:54 +0300 Subject: [PATCH 010/605] reduce allocation by using channel of structs --- connection.go | 8 ++++---- request.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/connection.go b/connection.go index c3ae518aa..f4238ea13 100644 --- a/connection.go +++ b/connection.go @@ -17,7 +17,7 @@ type Connection struct { mutex *sync.Mutex requestId uint32 Greeting *Greeting - requests map[uint32]chan *responseAndError + requests map[uint32]chan responseAndError packets chan []byte opts Opts closed bool @@ -42,7 +42,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { mutex: &sync.Mutex{}, requestId: 0, Greeting: &Greeting{}, - requests: make(map[uint32]chan *responseAndError), + requests: make(map[uint32]chan responseAndError), packets: make(chan []byte), opts: opts, } @@ -109,7 +109,7 @@ func (conn *Connection) closeConnection(neterr error) (err error) { err = conn.connection.Close() conn.connection = nil for requestId, respChan := range(conn.requests) { - respChan <- &responseAndError{nil, neterr} + respChan <- responseAndError{nil, neterr} delete(conn.requests, requestId) close(respChan) } @@ -148,7 +148,7 @@ func (conn *Connection) reader() { delete(conn.requests, resp.RequestId) conn.mutex.Unlock() if respChan != nil { - respChan <- &responseAndError{resp, nil} + respChan <- responseAndError{resp, nil} } else { log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) } diff --git a/request.go b/request.go index 1e952bf50..75fcef983 100644 --- a/request.go +++ b/request.go @@ -119,7 +119,7 @@ func (req *Request) perform() (resp *Response, err error) { return } - responseChan := make(chan *responseAndError, 1) + responseChan := make(chan responseAndError, 1) req.conn.mutex.Lock() req.conn.requests[req.requestId] = responseChan From e6cae4953b53699459c5e3df67123b71c34f7061 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 8 Jan 2015 20:13:01 +0300 Subject: [PATCH 011/605] defer is costly, and no need for mutex in good case --- connection.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/connection.go b/connection.go index f4238ea13..1394faf61 100644 --- a/connection.go +++ b/connection.go @@ -82,6 +82,9 @@ func (conn *Connection) dial() (err error) { } func (conn *Connection) getConnection() (connection net.Conn) { + if c := conn.connection; c != nil { + return c + } conn.mutex.Lock() defer conn.mutex.Unlock() for conn.connection == nil { From a5b1c2e0fd4a315cc578b3ff0a835c8273c2058d Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 8 Jan 2015 20:13:36 +0300 Subject: [PATCH 012/605] reduce allocations in read loop --- connection.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/connection.go b/connection.go index 1394faf61..f48e8f1a6 100644 --- a/connection.go +++ b/connection.go @@ -135,12 +135,13 @@ func (conn *Connection) writer() { } func (conn *Connection) reader() { + var length [PacketLengthBytes]byte for { connection := conn.getConnection() if connection == nil { return } - resp_bytes, err := read(connection) + resp_bytes, err := read(length[:], connection) if err != nil { conn.closeConnection(err) continue @@ -169,10 +170,9 @@ func write(connection net.Conn, data []byte) (err error) { return } -func read(connection net.Conn) (response []byte, err error){ +func read(length []byte, connection net.Conn) (response []byte, err error){ var length_uint uint32 var l, tl int - length := make([]byte, PacketLengthBytes) tl = 0 for tl < int(PacketLengthBytes) { From b18407bc5a17d72553c56c4a2f466afc3e2d7f6c Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Sat, 10 Jan 2015 20:01:05 +0300 Subject: [PATCH 013/605] special type for tarantool errors --- errors.go | 14 ++++++++++++++ request.go | 2 +- response.go | 11 +++++------ 3 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 errors.go diff --git a/errors.go b/errors.go new file mode 100644 index 000000000..2178fa4b4 --- /dev/null +++ b/errors.go @@ -0,0 +1,14 @@ +package tarantool + +import ( + "fmt" +) + +type Error struct { + Code uint32 + Msg string +} + +func (tnterr Error) Error() (string) { + return fmt.Sprintf("%s (0x%x)", tnterr.Msg, tnterr.Code) +} diff --git a/request.go b/request.go index 75fcef983..1bdf6e89e 100644 --- a/request.go +++ b/request.go @@ -149,7 +149,7 @@ func (req *Request) perform() (resp *Response, err error) { } if resp != nil && resp.Error != "" { - err = errors.New(resp.Error) + err = Error{resp.Code, resp.Error} } return } diff --git a/response.go b/response.go index 93b27b053..34fa8af5d 100644 --- a/response.go +++ b/response.go @@ -35,14 +35,13 @@ func NewResponse(bytes []byte) (resp *Response) { if resp.Code != OkCode { resp.Error = body[KeyError].(string) } - return } -func (resp *Response) GoString (str string) { - str = fmt.Sprintf("<%d %d '%s'>\n", resp.RequestId, resp.Code, resp.Error) - for t := range(resp.Data) { - str += fmt.Sprintf("%v\n", t) +func (resp *Response) String() (str string) { + if (resp.Code == OkCode) { + return fmt.Sprintf("<%d OK %v>", resp.RequestId, resp.Data) + } else { + return fmt.Sprintf("<%d ERR 0x%x %s>", resp.RequestId, resp.Code, resp.Error) } - return } From 0d93224a18d273cd8e40ed61a8ab83421462c5f2 Mon Sep 17 00:00:00 2001 From: Peter Yanovich Date: Tue, 13 Jan 2015 17:39:26 +0300 Subject: [PATCH 014/605] [ISSUE] -> diff --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6fea3477f..21d5ecae0 100644 --- a/README.md +++ b/README.md @@ -143,3 +143,5 @@ func main() { // #=> ---- ``` +## Contributors +@mialinx, не хочешь забрать клиент на суппорт? @kostja будет безумно рад From 3414af41a9d7e78e1ac0b387ab12acda5073a298 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 13 Jan 2015 14:04:06 +0300 Subject: [PATCH 015/605] make main channels buffered --- connection.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connection.go b/connection.go index f48e8f1a6..2e3973389 100644 --- a/connection.go +++ b/connection.go @@ -42,8 +42,8 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { mutex: &sync.Mutex{}, requestId: 0, Greeting: &Greeting{}, - requests: make(map[uint32]chan responseAndError), - packets: make(chan []byte), + requests: make(map[uint32]chan responseAndError, 64), + packets: make(chan []byte, 64), opts: opts, } From 981e90f450ebc49062f57da3492d1b0494b963eb Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 13 Jan 2015 16:24:04 +0300 Subject: [PATCH 016/605] buffered io for connection --- connection.go | 87 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/connection.go b/connection.go index 2e3973389..228e9e5ab 100644 --- a/connection.go +++ b/connection.go @@ -1,19 +1,23 @@ package tarantool import ( - "net" - "gopkg.in/vmihailenco/msgpack.v2" - "sync/atomic" + "bufio" "bytes" + "errors" + "gopkg.in/vmihailenco/msgpack.v2" + "io" + "log" + "net" "sync" + "sync/atomic" "time" - "log" - "errors" ) type Connection struct { addr string connection net.Conn + r io.Reader + w *bufio.Writer mutex *sync.Mutex requestId uint32 Greeting *Greeting @@ -29,22 +33,21 @@ type Greeting struct { } type Opts struct { - Timeout time.Duration // milliseconds - Reconnect time.Duration // milliseconds + Timeout time.Duration // milliseconds + Reconnect time.Duration // milliseconds } - func Connect(addr string, opts Opts) (conn *Connection, err error) { conn = &Connection{ - addr: addr, + addr: addr, connection: nil, - mutex: &sync.Mutex{}, - requestId: 0, - Greeting: &Greeting{}, - requests: make(map[uint32]chan responseAndError, 64), - packets: make(chan []byte, 64), - opts: opts, + mutex: &sync.Mutex{}, + requestId: 0, + Greeting: &Greeting{}, + requests: make(map[uint32]chan responseAndError, 64), + packets: make(chan []byte, 64), + opts: opts, } err = conn.dial() @@ -58,7 +61,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { } func (conn *Connection) Close() (err error) { - conn.closed = true; + conn.closed = true err = conn.closeConnection(errors.New("client closed connection")) return } @@ -70,6 +73,8 @@ func (conn *Connection) dial() (err error) { } connection.(*net.TCPConn).SetNoDelay(true) conn.connection = connection + conn.r = bufio.NewReaderSize(conn.connection, 128*1024) + conn.w = bufio.NewWriter(conn.connection) greeting := make([]byte, 128) // TODO: read all _, err = conn.connection.Read(greeting) @@ -81,15 +86,12 @@ func (conn *Connection) dial() (err error) { return } -func (conn *Connection) getConnection() (connection net.Conn) { - if c := conn.connection; c != nil { - return c - } +func (conn *Connection) createConnection() (r io.Reader, w *bufio.Writer) { conn.mutex.Lock() defer conn.mutex.Unlock() for conn.connection == nil { if conn.closed { - return nil + return } err := conn.dial() if err == nil { @@ -97,10 +99,10 @@ func (conn *Connection) getConnection() (connection net.Conn) { } else if conn.opts.Reconnect > 0 { time.Sleep(conn.opts.Reconnect) } else { - return nil + return } } - return conn.connection + return conn.r, conn.w } func (conn *Connection) closeConnection(neterr error) (err error) { @@ -111,7 +113,7 @@ func (conn *Connection) closeConnection(neterr error) (err error) { } err = conn.connection.Close() conn.connection = nil - for requestId, respChan := range(conn.requests) { + for requestId, respChan := range conn.requests { respChan <- responseAndError{nil, neterr} delete(conn.requests, requestId) close(respChan) @@ -120,14 +122,25 @@ func (conn *Connection) closeConnection(neterr error) (err error) { } func (conn *Connection) writer() { + var w *bufio.Writer for { - packet := <-conn.packets - connection := conn.getConnection() - if connection == nil { - return + var packet []byte + select { + case packet = <-conn.packets: + default: + if w = conn.w; w != nil { + if err := w.Flush(); err != nil { + conn.closeConnection(err) + } + } + packet = <-conn.packets } - err := write(connection, packet) - if err != nil { + if w = conn.w; w == nil { + if _, w = conn.createConnection(); w == nil { + return + } + } + if err := write(w, packet); err != nil { conn.closeConnection(err) continue } @@ -136,12 +149,14 @@ func (conn *Connection) writer() { func (conn *Connection) reader() { var length [PacketLengthBytes]byte + var r io.Reader for { - connection := conn.getConnection() - if connection == nil { - return + if r = conn.r; r == nil { + if r, _ = conn.createConnection(); r == nil { + return + } } - resp_bytes, err := read(length[:], connection) + resp_bytes, err := read(length[:], r) if err != nil { conn.closeConnection(err) continue @@ -159,7 +174,7 @@ func (conn *Connection) reader() { } } -func write(connection net.Conn, data []byte) (err error) { +func write(connection io.Writer, data []byte) (err error) { l, err := connection.Write(data) if err != nil { return @@ -170,7 +185,7 @@ func write(connection net.Conn, data []byte) (err error) { return } -func read(length []byte, connection net.Conn) (response []byte, err error){ +func read(length []byte, connection io.Reader) (response []byte, err error) { var length_uint uint32 var l, tl int From eaa58312e82a24f79b1a3c8c50e099ee9713ddd7 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 13 Jan 2015 16:38:44 +0300 Subject: [PATCH 017/605] control channel to close connection --- connection.go | 14 ++++++++++++-- request.go | 8 ++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/connection.go b/connection.go index 228e9e5ab..b4be16860 100644 --- a/connection.go +++ b/connection.go @@ -23,6 +23,7 @@ type Connection struct { Greeting *Greeting requests map[uint32]chan responseAndError packets chan []byte + control chan struct{} opts Opts closed bool } @@ -45,8 +46,9 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { mutex: &sync.Mutex{}, requestId: 0, Greeting: &Greeting{}, - requests: make(map[uint32]chan responseAndError, 64), + requests: make(map[uint32]chan responseAndError), packets: make(chan []byte, 64), + control: make(chan struct{}), opts: opts, } @@ -62,6 +64,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { func (conn *Connection) Close() (err error) { conn.closed = true + close(conn.control) err = conn.closeConnection(errors.New("client closed connection")) return } @@ -133,7 +136,14 @@ func (conn *Connection) writer() { conn.closeConnection(err) } } - packet = <-conn.packets + select { + case packet = <-conn.packets: + case <-conn.control: + return + } + } + if packet == nil { + return } if w = conn.w; w == nil { if _, w = conn.createConnection(); w == nil { diff --git a/request.go b/request.go index 1bdf6e89e..a683df6c9 100644 --- a/request.go +++ b/request.go @@ -110,10 +110,6 @@ func (conn *Connection) Auth(key, tuple []interface{}) (resp *Response, err erro func (req *Request) perform() (resp *Response, err error) { - if req.conn.closed { - return nil, errors.New("using closed connection") - } - packet, err := req.pack() if err != nil { return @@ -122,6 +118,10 @@ func (req *Request) perform() (resp *Response, err error) { responseChan := make(chan responseAndError, 1) req.conn.mutex.Lock() + if req.conn.closed { + req.conn.mutex.Unlock() + return nil, errors.New("using closed connection") + } req.conn.requests[req.requestId] = responseChan req.conn.mutex.Unlock() From 0e77d9ac3c4152d89ee9dcf9d7e62f5870a128b9 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 13 Jan 2015 18:48:55 +0300 Subject: [PATCH 018/605] future for async result --- connection.go | 19 ++--- request.go | 198 ++++++++++++++++++++++++++++++++++++-------------- response.go | 11 +-- 3 files changed, 159 insertions(+), 69 deletions(-) diff --git a/connection.go b/connection.go index b4be16860..e05bcac86 100644 --- a/connection.go +++ b/connection.go @@ -21,7 +21,7 @@ type Connection struct { mutex *sync.Mutex requestId uint32 Greeting *Greeting - requests map[uint32]chan responseAndError + requests map[uint32]*responseAndError packets chan []byte control chan struct{} opts Opts @@ -46,7 +46,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { mutex: &sync.Mutex{}, requestId: 0, Greeting: &Greeting{}, - requests: make(map[uint32]chan responseAndError), + requests: make(map[uint32]*responseAndError), packets: make(chan []byte, 64), control: make(chan struct{}), opts: opts, @@ -116,10 +116,10 @@ func (conn *Connection) closeConnection(neterr error) (err error) { } err = conn.connection.Close() conn.connection = nil - for requestId, respChan := range conn.requests { - respChan <- responseAndError{nil, neterr} - delete(conn.requests, requestId) - close(respChan) + for rid, resp := range conn.requests { + resp.e = neterr + close(resp.c) + delete(conn.requests, rid) } return } @@ -173,11 +173,12 @@ func (conn *Connection) reader() { } resp := NewResponse(resp_bytes) conn.mutex.Lock() - respChan := conn.requests[resp.RequestId] + r := conn.requests[resp.RequestId] delete(conn.requests, resp.RequestId) conn.mutex.Unlock() - if respChan != nil { - respChan <- responseAndError{resp, nil} + if r != nil { + r.r = resp + close(r.c) } else { log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) } diff --git a/request.go b/request.go index a683df6c9..4eff8c7ce 100644 --- a/request.go +++ b/request.go @@ -1,8 +1,8 @@ package tarantool -import( - "gopkg.in/vmihailenco/msgpack.v2" +import ( "errors" + "gopkg.in/vmihailenco/msgpack.v2" "time" ) @@ -13,12 +13,20 @@ type Request struct { body map[int]interface{} } +type Future struct { + conn *Connection + id uint32 + r responseAndError + t *time.Timer + tc <-chan time.Time +} + func (conn *Connection) NewRequest(requestCode int32) (req *Request) { req = &Request{} - req.conn = conn - req.requestId = conn.nextRequestId() + req.conn = conn + req.requestId = conn.nextRequestId() req.requestCode = requestCode - req.body = make(map[int]interface{}) + req.body = make(map[int]interface{}) return } @@ -29,73 +37,106 @@ func (conn *Connection) Ping() (resp *Response, err error) { return } +func (r *Request) fillSearch(spaceNo, indexNo uint32, key []interface{}) { + r.body[KeySpaceNo] = spaceNo + r.body[KeyIndexNo] = indexNo + r.body[KeyKey] = key +} +func (r *Request) fillIterator(offset, limit, iterator uint32) { + r.body[KeyIterator] = iterator + r.body[KeyOffset] = offset + r.body[KeyLimit] = limit +} + +func (r *Request) fillInsert(spaceNo uint32, tuple []interface{}) { + r.body[KeySpaceNo] = spaceNo + r.body[KeyTuple] = tuple +} + func (conn *Connection) Select(spaceNo, indexNo, offset, limit, iterator uint32, key []interface{}) (resp *Response, err error) { request := conn.NewRequest(SelectRequest) - - request.body[KeySpaceNo] = spaceNo - request.body[KeyIndexNo] = indexNo - request.body[KeyIterator] = iterator - request.body[KeyOffset] = offset - request.body[KeyLimit] = limit - request.body[KeyKey] = key - + request.fillSearch(spaceNo, indexNo, key) + request.fillIterator(offset, limit, iterator) resp, err = request.perform() return } func (conn *Connection) Insert(spaceNo uint32, tuple []interface{}) (resp *Response, err error) { request := conn.NewRequest(InsertRequest) - - request.body[KeySpaceNo] = spaceNo - request.body[KeyTuple] = tuple - + request.fillInsert(spaceNo, tuple) resp, err = request.perform() return } func (conn *Connection) Replace(spaceNo uint32, tuple []interface{}) (resp *Response, err error) { request := conn.NewRequest(ReplaceRequest) - - request.body[KeySpaceNo] = spaceNo - request.body[KeyTuple] = tuple - + request.fillInsert(spaceNo, tuple) resp, err = request.perform() return } func (conn *Connection) Delete(spaceNo, indexNo uint32, key []interface{}) (resp *Response, err error) { request := conn.NewRequest(DeleteRequest) - - request.body[KeySpaceNo] = spaceNo - request.body[KeyIndexNo] = indexNo - request.body[KeyKey] = key - + request.fillSearch(spaceNo, indexNo, key) resp, err = request.perform() return } func (conn *Connection) Update(spaceNo, indexNo uint32, key, tuple []interface{}) (resp *Response, err error) { request := conn.NewRequest(UpdateRequest) - - request.body[KeySpaceNo] = spaceNo - request.body[KeyIndexNo] = indexNo - request.body[KeyKey] = key - request.body[KeyTuple] = tuple - + request.fillSearch(spaceNo, indexNo, key) + request.body[KeyTuple] = tuple resp, err = request.perform() return } func (conn *Connection) Call(functionName string, tuple []interface{}) (resp *Response, err error) { request := conn.NewRequest(CallRequest) - request.body[KeyFunctionName] = functionName - request.body[KeyTuple] = tuple - + request.body[KeyTuple] = tuple resp, err = request.perform() return } +func (conn *Connection) SelectAsync(spaceNo, indexNo, offset, limit, iterator uint32, key []interface{}) *Future { + request := conn.NewRequest(SelectRequest) + request.fillSearch(spaceNo, indexNo, key) + request.fillIterator(offset, limit, iterator) + return request.future() +} + +func (conn *Connection) InsertAsync(spaceNo uint32, tuple []interface{}) *Future { + request := conn.NewRequest(InsertRequest) + request.fillInsert(spaceNo, tuple) + return request.future() +} + +func (conn *Connection) ReplaceAsync(spaceNo uint32, tuple []interface{}) *Future { + request := conn.NewRequest(ReplaceRequest) + request.fillInsert(spaceNo, tuple) + return request.future() +} + +func (conn *Connection) DeleteAsync(spaceNo, indexNo uint32, key []interface{}) *Future { + request := conn.NewRequest(DeleteRequest) + request.fillSearch(spaceNo, indexNo, key) + return request.future() +} + +func (conn *Connection) UpdateAsync(spaceNo, indexNo uint32, key, tuple []interface{}) *Future { + request := conn.NewRequest(UpdateRequest) + request.fillSearch(spaceNo, indexNo, key) + request.body[KeyTuple] = tuple + return request.future() +} + +func (conn *Connection) CallAsync(functionName string, tuple []interface{}) *Future { + request := conn.NewRequest(CallRequest) + request.body[KeyFunctionName] = functionName + request.body[KeyTuple] = tuple + return request.future() +} + // // To be implemented // @@ -103,26 +144,24 @@ func (conn *Connection) Auth(key, tuple []interface{}) (resp *Response, err erro return } - // // private // - func (req *Request) perform() (resp *Response, err error) { - packet, err := req.pack() - if err != nil { + var packet []byte + if packet, err = req.pack(); err != nil { return } - responseChan := make(chan responseAndError, 1) + r := responseAndError{c: make(chan struct{})} req.conn.mutex.Lock() if req.conn.closed { req.conn.mutex.Unlock() return nil, errors.New("using closed connection") } - req.conn.requests[req.requestId] = responseChan + req.conn.requests[req.requestId] = &r req.conn.mutex.Unlock() req.conn.packets <- (packet) @@ -130,22 +169,20 @@ func (req *Request) perform() (resp *Response, err error) { if req.conn.opts.Timeout > 0 { timer := time.NewTimer(req.conn.opts.Timeout) select { - case respAndErr := <-responseChan: - timer.Stop() - resp = respAndErr.resp - err = respAndErr.err - break - case <-timer.C: - req.conn.mutex.Lock() - delete(req.conn.requests, req.requestId) - req.conn.mutex.Unlock() - resp = nil - err = errors.New("client timeout") + case <-r.c: + timer.Stop() + resp, err = r.r, r.e + break + case <-timer.C: + req.conn.mutex.Lock() + delete(req.conn.requests, req.requestId) + req.conn.mutex.Unlock() + resp = nil + err = errors.New("client timeout") } } else { - respAndError := <-responseChan - resp = respAndError.resp - err = respAndError.err + <-r.c + resp, err = r.r, r.e } if resp != nil && resp.Error != "" { @@ -182,3 +219,54 @@ func (req *Request) pack() (packet []byte, err error) { packet = append(packet, body...) return } + +func (req *Request) future() (f *Future) { + f = &Future{ + conn: req.conn, + id: req.requestId, + r: responseAndError{c: make(chan struct{})}, + } + var packet []byte + if packet, f.r.e = req.pack(); f.r.e != nil { + return + } + + req.conn.mutex.Lock() + if req.conn.closed { + req.conn.mutex.Unlock() + f.r.e = errors.New("using closed connection") + return + } + req.conn.requests[req.requestId] = &f.r + req.conn.mutex.Unlock() + req.conn.packets <- (packet) + + if req.conn.opts.Timeout > 0 { + f.t = time.NewTimer(req.conn.opts.Timeout) + f.tc = f.t.C + } + return +} + +func (f *Future) Get() (*Response, error) { + select { + case <-f.r.c: + default: + select { + case <-f.r.c: + case <-f.tc: + f.conn.mutex.Lock() + delete(f.conn.requests, f.id) + f.conn.mutex.Unlock() + f.r.r = nil + f.r.e = errors.New("client timeout") + close(f.r.c) + } + } + if f.t != nil { + f.t.Stop() + f.t = nil + f.tc = nil + } + return f.r.r, f.r.e +} diff --git a/response.go b/response.go index 34fa8af5d..bcca6e95b 100644 --- a/response.go +++ b/response.go @@ -1,6 +1,6 @@ package tarantool -import( +import ( "fmt" "gopkg.in/vmihailenco/msgpack.v2" ) @@ -13,8 +13,9 @@ type Response struct { } type responseAndError struct { - resp *Response - err error + c chan struct{} + r *Response + e error } func NewResponse(bytes []byte) (resp *Response) { @@ -27,7 +28,7 @@ func NewResponse(bytes []byte) (resp *Response) { if body[KeyData] != nil { data := body[KeyData].([]interface{}) resp.Data = make([]interface{}, len(data)) - for i, v := range(data) { + for i, v := range data { resp.Data[i] = v.([]interface{}) } } @@ -39,7 +40,7 @@ func NewResponse(bytes []byte) (resp *Response) { } func (resp *Response) String() (str string) { - if (resp.Code == OkCode) { + if resp.Code == OkCode { return fmt.Sprintf("<%d OK %v>", resp.RequestId, resp.Data) } else { return fmt.Sprintf("<%d ERR 0x%x %s>", resp.RequestId, resp.Code, resp.Error) From 1c7f0617496c1b19e2a94588eb58a9fa044c92b9 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 13 Jan 2015 18:58:56 +0300 Subject: [PATCH 019/605] optimize request construction --- request.go | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/request.go b/request.go index 4eff8c7ce..8449c0c41 100644 --- a/request.go +++ b/request.go @@ -192,15 +192,15 @@ func (req *Request) perform() (resp *Response, err error) { } func (req *Request) pack() (packet []byte, err error) { - var header, body, packetLength []byte - - msg_header := make(map[int]interface{}) - msg_header[KeyCode] = req.requestCode - msg_header[KeySync] = req.requestId - - header, err = msgpack.Marshal(msg_header) - if err != nil { - return + var body []byte + rid := req.requestId + h := [...]byte{ + 0xce, 0, 0, 0, 0, // length + 0x82, // 2 element map + KeyCode, byte(req.requestCode), // request code + KeySync, 0xce, + byte(rid >> 24), byte(rid >> 16), + byte(rid >> 8), byte(rid), } body, err = msgpack.Marshal(req.body) @@ -208,15 +208,13 @@ func (req *Request) pack() (packet []byte, err error) { return } - length := uint32(len(header) + len(body)) - packetLength, err = msgpack.Marshal(length) - if err != nil { - return - } + l := uint32(len(h) - 5 + len(body)) + h[1] = byte(l >> 24) + h[2] = byte(l >> 16) + h[3] = byte(l >> 8) + h[4] = byte(l) - packet = append(packet, packetLength...) - packet = append(packet, header...) - packet = append(packet, body...) + packet = append(h[:], body...) return } From 361789e1a6ed5795d610d3bd032bdf58d1655d09 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 13 Jan 2015 20:06:01 +0300 Subject: [PATCH 020/605] optimize a bit reader --- connection.go | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/connection.go b/connection.go index e05bcac86..0f7a59064 100644 --- a/connection.go +++ b/connection.go @@ -4,7 +4,6 @@ import ( "bufio" "bytes" "errors" - "gopkg.in/vmihailenco/msgpack.v2" "io" "log" "net" @@ -196,35 +195,27 @@ func write(connection io.Writer, data []byte) (err error) { return } -func read(length []byte, connection io.Reader) (response []byte, err error) { - var length_uint uint32 - var l, tl int +func read(length []byte, r io.Reader) (response []byte, err error) { + var need int - tl = 0 - for tl < int(PacketLengthBytes) { - l, err = connection.Read(length[tl:]) - tl += l - if err != nil { - return - } + if _, err = io.ReadFull(r, length); err != nil { + return } - - err = msgpack.Unmarshal(length, &length_uint) - if err != nil { + if length[0] != 0xce { + err = errors.New("Wrong reponse header") return } + need = (int(length[1]) << 24) + + (int(length[2]) << 16) + + (int(length[3]) << 8) + + int(length[4]) - response = make([]byte, length_uint) - if length_uint > 0 { - tl = 0 - for tl < int(length_uint) { - l, err = connection.Read(response[tl:]) - tl += l - if err != nil { - return - } - } + if need == 0 { + err = errors.New("Response should not be 0 length") + return } + response = make([]byte, need) + _, err = io.ReadFull(r, response) return } From 72cae2c47826da3b2e5679e49205cbb68725a5ef Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 13 Jan 2015 21:31:07 +0300 Subject: [PATCH 021/605] more optimizations: parse in a client goroutine + typed parse --- connection.go | 10 ++- request.go | 29 +++++---- response.go | 166 +++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 170 insertions(+), 35 deletions(-) diff --git a/connection.go b/connection.go index 0f7a59064..e95ac5e49 100644 --- a/connection.go +++ b/connection.go @@ -116,7 +116,7 @@ func (conn *Connection) closeConnection(neterr error) (err error) { err = conn.connection.Close() conn.connection = nil for rid, resp := range conn.requests { - resp.e = neterr + resp.r.Error = neterr close(resp.c) delete(conn.requests, rid) } @@ -170,13 +170,19 @@ func (conn *Connection) reader() { conn.closeConnection(err) continue } - resp := NewResponse(resp_bytes) + var resp Response + resp_bytes = resp.fill(resp_bytes) + if resp.Error != nil { + conn.closeConnection(resp.Error) + continue + } conn.mutex.Lock() r := conn.requests[resp.RequestId] delete(conn.requests, resp.RequestId) conn.mutex.Unlock() if r != nil { r.r = resp + r.b = resp_bytes close(r.c) } else { log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) diff --git a/request.go b/request.go index 8449c0c41..145d90f6e 100644 --- a/request.go +++ b/request.go @@ -171,7 +171,6 @@ func (req *Request) perform() (resp *Response, err error) { select { case <-r.c: timer.Stop() - resp, err = r.r, r.e break case <-timer.C: req.conn.mutex.Lock() @@ -182,13 +181,9 @@ func (req *Request) perform() (resp *Response, err error) { } } else { <-r.c - resp, err = r.r, r.e } - if resp != nil && resp.Error != "" { - err = Error{resp.Code, resp.Error} - } - return + return r.get() } func (req *Request) pack() (packet []byte, err error) { @@ -225,14 +220,16 @@ func (req *Request) future() (f *Future) { r: responseAndError{c: make(chan struct{})}, } var packet []byte - if packet, f.r.e = req.pack(); f.r.e != nil { + if packet, f.r.r.Error = req.pack(); f.r.r.Error != nil { + close(f.r.c) return } req.conn.mutex.Lock() if req.conn.closed { req.conn.mutex.Unlock() - f.r.e = errors.New("using closed connection") + f.r.r.Error = errors.New("using closed connection") + close(f.r.c) return } req.conn.requests[req.requestId] = &f.r @@ -246,7 +243,7 @@ func (req *Request) future() (f *Future) { return } -func (f *Future) Get() (*Response, error) { +func (f *Future) wait() { select { case <-f.r.c: default: @@ -256,8 +253,7 @@ func (f *Future) Get() (*Response, error) { f.conn.mutex.Lock() delete(f.conn.requests, f.id) f.conn.mutex.Unlock() - f.r.r = nil - f.r.e = errors.New("client timeout") + f.r.r.Error = errors.New("client timeout") close(f.r.c) } } @@ -266,5 +262,14 @@ func (f *Future) Get() (*Response, error) { f.t = nil f.tc = nil } - return f.r.r, f.r.e +} + +func (f *Future) Get() (*Response, error) { + f.wait() + return f.r.get() +} + +func (f *Future) GetTyped(r interface{}) error { + f.wait() + return f.r.getTyped(r) } diff --git a/response.go b/response.go index bcca6e95b..4dabe41e7 100644 --- a/response.go +++ b/response.go @@ -1,42 +1,95 @@ package tarantool import ( + "errors" "fmt" "gopkg.in/vmihailenco/msgpack.v2" + "io" ) type Response struct { RequestId uint32 Code uint32 - Error string + Error error Data []interface{} } type responseAndError struct { c chan struct{} - r *Response - e error -} - -func NewResponse(bytes []byte) (resp *Response) { - var header, body map[int32]interface{} - resp = &Response{} - - msgpack.Unmarshal(bytes, &header, &body) - resp.RequestId = uint32(header[KeySync].(uint64)) - resp.Code = uint32(header[KeyCode].(uint64)) - if body[KeyData] != nil { - data := body[KeyData].([]interface{}) - resp.Data = make([]interface{}, len(data)) - for i, v := range data { - resp.Data[i] = v.([]interface{}) - } + b []byte + r Response +} + +type smallBuf struct { + b []byte + p int +} + +func (s *smallBuf) Read(d []byte) (l int, err error) { + l = len(s.b) - s.p + if l == 0 && len(d) > 0 { + return 0, io.EOF + } + if l > len(d) { + l = len(d) + } + copy(d, s.b[s.p:]) + s.p += l + return l, nil +} + +func (s *smallBuf) ReadByte() (b byte, err error) { + if s.p == len(s.b) { + return 0, io.EOF + } + b = s.b[s.p] + s.p++ + return b, nil +} + +func (s *smallBuf) UnreadByte() error { + if s.p == 0 { + return errors.New("Could not unread") + } + s.p-- + return nil +} + +func (s *smallBuf) Len() int { + return len(s.b) - s.p +} + +func (s *smallBuf) Bytes() []byte { + if len(s.b) > s.p { + return s.b[s.p:] } + return nil +} - if resp.Code != OkCode { - resp.Error = body[KeyError].(string) +func (r *Response) fill(b []byte) []byte { + var l int + s := smallBuf{b: b} + d := msgpack.NewDecoder(&s) + if l, r.Error = d.DecodeMapLen(); r.Error != nil { + return nil + } + for ; l > 0; l-- { + var cd int + if cd, r.Error = d.DecodeInt(); r.Error != nil { + return nil + } + switch cd { + case KeySync: + if r.RequestId, r.Error = d.DecodeUint32(); r.Error != nil { + return nil + } + case KeyCode: + if r.Code, r.Error = d.DecodeUint32(); r.Error != nil { + return nil + } + } } - return + return s.Bytes() } func (resp *Response) String() (str string) { @@ -46,3 +99,74 @@ func (resp *Response) String() (str string) { return fmt.Sprintf("<%d ERR 0x%x %s>", resp.RequestId, resp.Code, resp.Error) } } + +func (r *responseAndError) get() (*Response, error) { + if r.r.Error != nil { + return &r.r, r.r.Error + } + if len(r.b) > 0 { + var body map[int]interface{} + d := msgpack.NewDecoder(&smallBuf{b: r.b}) + + if r.r.Error = d.Decode(&body); r.r.Error != nil { + r.b = nil + return nil, r.r.Error + } + + if body[KeyData] != nil { + data := body[KeyData].([]interface{}) + r.r.Data = make([]interface{}, len(data)) + for i, v := range data { + r.r.Data[i] = v.([]interface{}) + } + } + + if r.r.Code != OkCode { + r.r.Error = Error{r.r.Code, body[KeyError].(string)} + } + r.b = nil + } + + return &r.r, r.r.Error +} + +func (r *responseAndError) getTyped(res interface{}) error { + if r.r.Error != nil { + return r.r.Error + } + if len(r.b) > 0 { + var l int + d := msgpack.NewDecoder(&smallBuf{b: r.b}) + if l, r.r.Error = d.DecodeMapLen(); r.r.Error != nil { + r.b = nil + return r.r.Error + } + + for ; l > 0; l-- { + var cd int + if cd, r.r.Error = d.DecodeInt(); r.r.Error != nil { + r.b = nil + return r.r.Error + } + switch cd { + case KeyData: + if r.r.Error = d.Decode(res); r.r.Error != nil { + r.b = nil + return r.r.Error + } + case KeyError: + var str string + if str, r.r.Error = d.DecodeString(); r.r.Error == nil { + r.r.Error = Error{ + r.r.Code, + str, + } + } + r.b = nil + return r.r.Error + } + } + } + + return r.r.Error +} From 258678aa3ac7a4caf86f27b6d941c9b2af316783 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 14 Jan 2015 04:43:35 +0300 Subject: [PATCH 022/605] a bit more of testing --- config.lua | 13 ++++++ tarantool_test.go | 110 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 103 insertions(+), 20 deletions(-) create mode 100644 config.lua diff --git a/config.lua b/config.lua new file mode 100644 index 000000000..37c9b2548 --- /dev/null +++ b/config.lua @@ -0,0 +1,13 @@ +box.cfg{ + listen = 3013, + wal_dir='xlog', + snap_dir='snap', +} +local s = box.schema.space.create('test', {if_not_exists = true}) +local i = s:create_index('primary', {type = 'hash', parts = {1, 'NUM'}, if_not_exists = true}) + +box.schema.user.grant('guest', 'read,write,execute', 'universe') + +local console = require 'console' +console.listen '0.0.0.0:33015' + diff --git a/tarantool_test.go b/tarantool_test.go index 96ddc8d3c..a6fc9fa9c 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -1,27 +1,97 @@ package tarantool -import( - "testing" +import ( "fmt" + "testing" ) +var server = "127.0.0.1:3013" +var spaceNo = uint32(512) +var indexNo = uint32(0) +var limit = uint32(10) +var offset = uint32(0) +var iterator = IterAll +var key = []interface{}{12} +var tuple1 = []interface{}{12, "Hello World", "Olga"} +var tuple2 = []interface{}{12, "Hello Mars", "Anna"} +var upd_tuple = []interface{}{[]interface{}{"=", 1, "Hello Moon"}, []interface{}{"#", 2, 1}} + +var functionName = "box.cfg()" +var functionTuple = []interface{}{"box.schema.SPACE_ID"} + +func BenchmarkClientSerial(b *testing.B) { + var err error + + client, err := Connect(server, Opts{}) + if err != nil { + b.Errorf("No connection available") + } + + _, err = client.Replace(spaceNo, tuple1) + if err != nil { + b.Errorf("No connection available") + } + + for i := 0; i < b.N; i++ { + _, err = client.Select(spaceNo, indexNo, offset, limit, iterator, key) + if err != nil { + b.Errorf("No connection available") + } + + } +} + +func BenchmarkClientFuture(b *testing.B) { + var err error + + client, err := Connect(server, Opts{}) + if err != nil { + b.Errorf("No connection available") + } + + _, err = client.Replace(spaceNo, tuple1) + if err != nil { + b.Errorf("No connection available") + } + + for i := 0; i < b.N; i += 10 { + var fs [10]*Future + for j := 0; j < 10; j++ { + fs[j] = client.SelectAsync(spaceNo, indexNo, offset, limit, iterator, key) + } + for j := 0; j < 10; j++ { + _, err = fs[j].Get() + if err != nil { + b.Errorf("No connection available") + } + } + + } +} + +func BenchmarkClientParrallel(b *testing.B) { + client, err := Connect(server, Opts{}) + if err != nil { + b.Errorf("No connection available") + } + + _, err = client.Replace(spaceNo, tuple1) + if err != nil { + b.Errorf("No connection available") + } + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err = client.Select(spaceNo, indexNo, offset, limit, iterator, key) + if err != nil { + b.Errorf("No connection available") + } + } + }) +} + func TestClient(t *testing.T) { - server := "127.0.0.1:3013" - spaceNo := uint32(512) - indexNo := uint32(0) - limit := uint32(10) - offset := uint32(0) - iterator := IterAll - key := []interface{}{ 12 } - tuple1 := []interface{}{ 12, "Hello World", "Olga" } - tuple2 := []interface{}{ 12, "Hello Mars", "Anna" } - upd_tuple := []interface{}{ []interface{}{ "=", 1, "Hello Moon" }, []interface{}{ "#", 2, 1 } } - - functionName := "box.cfg()" - functionTuple := []interface{}{ "box.schema.SPACE_ID" } - - - client, err := Connect(server) + client, err := Connect(server, Opts{}) if err != nil { t.Errorf("No connection available") } @@ -82,7 +152,7 @@ func TestClient(t *testing.T) { cnt2 := 500 for j := 0; j < cnt1; j++ { for i := 0; i < cnt2; i++ { - go func(){ + go func() { resp, err = client.Select(spaceNo, indexNo, offset, limit, iterator, key) responses <- resp }() @@ -107,4 +177,4 @@ func TestClient(t *testing.T) { fmt.Println("Data", resp.Data) fmt.Println("----") -} \ No newline at end of file +} From b3f4ed9666dad2e8846cbae6303f5adfe9d8d6e7 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 14 Jan 2015 05:18:00 +0300 Subject: [PATCH 023/605] example of typed select in a bench --- tarantool_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/tarantool_test.go b/tarantool_test.go index a6fc9fa9c..0b5c76047 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -1,7 +1,10 @@ package tarantool import ( + "errors" "fmt" + "gopkg.in/vmihailenco/msgpack.v2" + "reflect" "testing" ) @@ -44,6 +47,84 @@ func BenchmarkClientSerial(b *testing.B) { func BenchmarkClientFuture(b *testing.B) { var err error + client, err := Connect(server, Opts{}) + if err != nil { + b.Error(err) + } + + _, err = client.Replace(spaceNo, tuple1) + if err != nil { + b.Error(err) + } + + for i := 0; i < b.N; i += 10 { + var fs [10]*Future + for j := 0; j < 10; j++ { + fs[j] = client.SelectAsync(spaceNo, indexNo, offset, limit, iterator, key) + } + for j := 0; j < 10; j++ { + _, err = fs[j].Get() + if err != nil { + b.Error(err) + } + } + + } +} + +type tuple struct { + Id int + Msg string + Name string +} + +func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { + t := v.Interface().(tuple) + if err := e.EncodeSliceLen(3); err != nil { + return err + } + if err := e.EncodeInt(t.Id); err != nil { + return err + } + if err := e.EncodeString(t.Msg); err != nil { + return err + } + if err := e.EncodeString(t.Name); err != nil { + return err + } + return nil +} + +func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { + var err error + var l int + var t tuple + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 3 { + return errors.New("array len doesn't match") + } + if t.Id, err = d.DecodeInt(); err != nil { + return err + } + if t.Msg, err = d.DecodeString(); err != nil { + return err + } + if t.Name, err = d.DecodeString(); err != nil { + return err + } + v.Set(reflect.ValueOf(t)) + return nil +} + +func init() { + msgpack.Register(reflect.TypeOf(new(tuple)).Elem(), encodeTuple, decodeTuple) +} + +func BenchmarkClientFutureTyped(b *testing.B) { + var err error + client, err := Connect(server, Opts{}) if err != nil { b.Errorf("No connection available") @@ -60,9 +141,13 @@ func BenchmarkClientFuture(b *testing.B) { fs[j] = client.SelectAsync(spaceNo, indexNo, offset, limit, iterator, key) } for j := 0; j < 10; j++ { - _, err = fs[j].Get() + var r []tuple + err = fs[j].GetTyped(&r) if err != nil { - b.Errorf("No connection available") + b.Error(err) + } + if len(r) != 1 || r[0].Id != 12 { + b.Errorf("Doesn't match %v", r) } } From 01c2224e995804b4a5a16cc405b5821a491a645e Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 14 Jan 2015 05:36:16 +0300 Subject: [PATCH 024/605] SelectTyped --- request.go | 30 +++++++++++++++++++++++------- tarantool_test.go | 10 ++++++++-- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/request.go b/request.go index 145d90f6e..8962763ec 100644 --- a/request.go +++ b/request.go @@ -61,6 +61,13 @@ func (conn *Connection) Select(spaceNo, indexNo, offset, limit, iterator uint32, return } +func (conn *Connection) SelectTyped(spaceNo, indexNo, offset, limit, iterator uint32, key []interface{}, result interface{}) error { + request := conn.NewRequest(SelectRequest) + request.fillSearch(spaceNo, indexNo, key) + request.fillIterator(offset, limit, iterator) + return request.performTyped(result) +} + func (conn *Connection) Insert(spaceNo uint32, tuple []interface{}) (resp *Response, err error) { request := conn.NewRequest(InsertRequest) request.fillInsert(spaceNo, tuple) @@ -148,20 +155,20 @@ func (conn *Connection) Auth(key, tuple []interface{}) (resp *Response, err erro // private // -func (req *Request) perform() (resp *Response, err error) { +func (req *Request) wait(r *responseAndError) { + var err error var packet []byte if packet, err = req.pack(); err != nil { return } - r := responseAndError{c: make(chan struct{})} - req.conn.mutex.Lock() if req.conn.closed { req.conn.mutex.Unlock() - return nil, errors.New("using closed connection") + r.r.Error = errors.New("using closed connection") + return } - req.conn.requests[req.requestId] = &r + req.conn.requests[req.requestId] = r req.conn.mutex.Unlock() req.conn.packets <- (packet) @@ -176,16 +183,25 @@ func (req *Request) perform() (resp *Response, err error) { req.conn.mutex.Lock() delete(req.conn.requests, req.requestId) req.conn.mutex.Unlock() - resp = nil - err = errors.New("client timeout") + r.r.Error = errors.New("client timeout") } } else { <-r.c } +} +func (req *Request) perform() (resp *Response, err error) { + r := responseAndError{c: make(chan struct{})} + req.wait(&r) return r.get() } +func (req *Request) performTyped(res interface{}) (err error) { + r := responseAndError{c: make(chan struct{})} + req.wait(&r) + return r.getTyped(res) +} + func (req *Request) pack() (packet []byte, err error) { var body []byte rid := req.requestId diff --git a/tarantool_test.go b/tarantool_test.go index 0b5c76047..f84d06733 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -1,7 +1,6 @@ package tarantool import ( - "errors" "fmt" "gopkg.in/vmihailenco/msgpack.v2" "reflect" @@ -103,7 +102,7 @@ func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { return err } if l != 3 { - return errors.New("array len doesn't match") + return fmt.Errorf("array len doesn't match: %d", l) } if t.Id, err = d.DecodeInt(); err != nil { return err @@ -204,6 +203,13 @@ func TestClient(t *testing.T) { fmt.Println("Data", resp.Data) fmt.Println("----") + var tpl []tuple + err = client.SelectTyped(spaceNo, indexNo, offset, limit, iterator, key, &tpl) + fmt.Println("GetTyped") + fmt.Println("ERROR", err) + fmt.Println("Value", tpl) + fmt.Println("----") + resp, err = client.Replace(spaceNo, tuple2) fmt.Println("Replace") fmt.Println("ERROR", err) From ad9f3c3941aa980490b1da21564e9d5eab033a45 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 14 Jan 2015 11:58:02 +0300 Subject: [PATCH 025/605] better example of typed decoder --- tarantool_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tarantool_test.go b/tarantool_test.go index f84d06733..f31b5fafb 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -97,7 +97,7 @@ func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { var err error var l int - var t tuple + t := v.Addr().Interface().(*tuple) if l, err = d.DecodeSliceLen(); err != nil { return err } @@ -113,7 +113,6 @@ func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { if t.Name, err = d.DecodeString(); err != nil { return err } - v.Set(reflect.ValueOf(t)) return nil } From 7d60ca5dcd55955406be6e16f97c2076d7e74fb3 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 14 Jan 2015 11:58:32 +0300 Subject: [PATCH 026/605] more tests for Parallel and Typed --- tarantool_test.go | 98 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/tarantool_test.go b/tarantool_test.go index f31b5fafb..1dc59499c 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -5,6 +5,7 @@ import ( "gopkg.in/vmihailenco/msgpack.v2" "reflect" "testing" + "time" ) var server = "127.0.0.1:3013" @@ -20,11 +21,14 @@ var upd_tuple = []interface{}{[]interface{}{"=", 1, "Hello Moon"}, []interface{} var functionName = "box.cfg()" var functionTuple = []interface{}{"box.schema.SPACE_ID"} +var opts = Opts{Timeout: 500 * time.Millisecond} + +const N = 500 func BenchmarkClientSerial(b *testing.B) { var err error - client, err := Connect(server, Opts{}) + client, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") } @@ -46,7 +50,7 @@ func BenchmarkClientSerial(b *testing.B) { func BenchmarkClientFuture(b *testing.B) { var err error - client, err := Connect(server, Opts{}) + client, err := Connect(server, opts) if err != nil { b.Error(err) } @@ -56,12 +60,12 @@ func BenchmarkClientFuture(b *testing.B) { b.Error(err) } - for i := 0; i < b.N; i += 10 { - var fs [10]*Future - for j := 0; j < 10; j++ { + for i := 0; i < b.N; i += N { + var fs [N]*Future + for j := 0; j < N; j++ { fs[j] = client.SelectAsync(spaceNo, indexNo, offset, limit, iterator, key) } - for j := 0; j < 10; j++ { + for j := 0; j < N; j++ { _, err = fs[j].Get() if err != nil { b.Error(err) @@ -123,7 +127,7 @@ func init() { func BenchmarkClientFutureTyped(b *testing.B) { var err error - client, err := Connect(server, Opts{}) + client, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") } @@ -133,12 +137,12 @@ func BenchmarkClientFutureTyped(b *testing.B) { b.Errorf("No connection available") } - for i := 0; i < b.N; i += 10 { - var fs [10]*Future - for j := 0; j < 10; j++ { + for i := 0; i < b.N; i += N { + var fs [N]*Future + for j := 0; j < N; j++ { fs[j] = client.SelectAsync(spaceNo, indexNo, offset, limit, iterator, key) } - for j := 0; j < 10; j++ { + for j := 0; j < N; j++ { var r []tuple err = fs[j].GetTyped(&r) if err != nil { @@ -152,8 +156,78 @@ func BenchmarkClientFutureTyped(b *testing.B) { } } +func BenchmarkClientFutureParallel(b *testing.B) { + var err error + + client, err := Connect(server, opts) + if err != nil { + b.Errorf("No connection available") + } + + _, err = client.Replace(spaceNo, tuple1) + if err != nil { + b.Errorf("No connection available") + } + + b.RunParallel(func(pb *testing.PB) { + exit := false + for !exit { + var fs [N]*Future + var j int + for j = 0; j < N && pb.Next(); j++ { + fs[j] = client.SelectAsync(spaceNo, indexNo, offset, limit, iterator, key) + } + exit = j < N + for j > 0 { + j-- + _, err = fs[j].Get() + if err != nil { + b.Error(err) + } + } + } + }) +} + +func BenchmarkClientFutureParallelTyped(b *testing.B) { + var err error + + client, err := Connect(server, opts) + if err != nil { + b.Errorf("No connection available") + } + + _, err = client.Replace(spaceNo, tuple1) + if err != nil { + b.Errorf("No connection available") + } + + b.RunParallel(func(pb *testing.PB) { + exit := false + for !exit { + var fs [N]*Future + var j int + for j = 0; j < N && pb.Next(); j++ { + fs[j] = client.SelectAsync(spaceNo, indexNo, offset, limit, iterator, key) + } + exit = j < N + for j > 0 { + var r []tuple + j-- + err = fs[j].GetTyped(&r) + if err != nil { + b.Error(err) + } + if len(r) != 1 || r[0].Id != 12 { + b.Errorf("Doesn't match %v", r) + } + } + } + }) +} + func BenchmarkClientParrallel(b *testing.B) { - client, err := Connect(server, Opts{}) + client, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") } From da9c91e7f59502856fdb7f893df6a07eeeddd246 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 14 Jan 2015 21:37:08 +0300 Subject: [PATCH 027/605] simplify by using future for synchronous query --- request.go | 43 ++----------------------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/request.go b/request.go index 8962763ec..905ac095b 100644 --- a/request.go +++ b/request.go @@ -155,51 +155,12 @@ func (conn *Connection) Auth(key, tuple []interface{}) (resp *Response, err erro // private // -func (req *Request) wait(r *responseAndError) { - var err error - var packet []byte - if packet, err = req.pack(); err != nil { - return - } - - req.conn.mutex.Lock() - if req.conn.closed { - req.conn.mutex.Unlock() - r.r.Error = errors.New("using closed connection") - return - } - req.conn.requests[req.requestId] = r - req.conn.mutex.Unlock() - - req.conn.packets <- (packet) - - if req.conn.opts.Timeout > 0 { - timer := time.NewTimer(req.conn.opts.Timeout) - select { - case <-r.c: - timer.Stop() - break - case <-timer.C: - req.conn.mutex.Lock() - delete(req.conn.requests, req.requestId) - req.conn.mutex.Unlock() - r.r.Error = errors.New("client timeout") - } - } else { - <-r.c - } -} - func (req *Request) perform() (resp *Response, err error) { - r := responseAndError{c: make(chan struct{})} - req.wait(&r) - return r.get() + return req.future().Get() } func (req *Request) performTyped(res interface{}) (err error) { - r := responseAndError{c: make(chan struct{})} - req.wait(&r) - return r.getTyped(res) + return req.future().GetTyped(res) } func (req *Request) pack() (packet []byte, err error) { From 91b109248b8ef2c70317ceb488ea9a13be104a03 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 14 Jan 2015 21:37:34 +0300 Subject: [PATCH 028/605] protect from double close result channel --- connection.go | 8 ++++---- request.go | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/connection.go b/connection.go index e95ac5e49..632cf7b2f 100644 --- a/connection.go +++ b/connection.go @@ -177,14 +177,14 @@ func (conn *Connection) reader() { continue } conn.mutex.Lock() - r := conn.requests[resp.RequestId] - delete(conn.requests, resp.RequestId) - conn.mutex.Unlock() - if r != nil { + if r, ok := conn.requests[resp.RequestId]; ok { + delete(conn.requests, resp.RequestId) r.r = resp r.b = resp_bytes close(r.c) + conn.mutex.Unlock() } else { + conn.mutex.Unlock() log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) } } diff --git a/request.go b/request.go index 905ac095b..33a448981 100644 --- a/request.go +++ b/request.go @@ -228,10 +228,12 @@ func (f *Future) wait() { case <-f.r.c: case <-f.tc: f.conn.mutex.Lock() - delete(f.conn.requests, f.id) + if _, ok := f.conn.requests[f.id]; ok { + delete(f.conn.requests, f.id) + close(f.r.c) + f.r.r.Error = errors.New("client timeout") + } f.conn.mutex.Unlock() - f.r.r.Error = errors.New("client timeout") - close(f.r.c) } } if f.t != nil { From a3aca728427f55d9fb3e2ff91fe461e332941bed Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Thu, 15 Jan 2015 16:12:15 +0300 Subject: [PATCH 029/605] set reader/writer to nil in closeConnection --- connection.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/connection.go b/connection.go index 632cf7b2f..cffe0c1f0 100644 --- a/connection.go +++ b/connection.go @@ -115,6 +115,8 @@ func (conn *Connection) closeConnection(neterr error) (err error) { } err = conn.connection.Close() conn.connection = nil + conn.r = nil + conn.w = nil for rid, resp := range conn.requests { resp.r.Error = neterr close(resp.c) From 11dc92ba4d9f3dc6b0c05a420b799cdbb3326365 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Thu, 15 Jan 2015 16:40:39 +0300 Subject: [PATCH 030/605] minor renaming --- connection.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/connection.go b/connection.go index cffe0c1f0..b429905d4 100644 --- a/connection.go +++ b/connection.go @@ -159,7 +159,6 @@ func (conn *Connection) writer() { } func (conn *Connection) reader() { - var length [PacketLengthBytes]byte var r io.Reader for { if r = conn.r; r == nil { @@ -167,7 +166,7 @@ func (conn *Connection) reader() { return } } - resp_bytes, err := read(length[:], r) + resp_bytes, err := read(r) if err != nil { conn.closeConnection(err) continue @@ -203,26 +202,27 @@ func write(connection io.Writer, data []byte) (err error) { return } -func read(length []byte, r io.Reader) (response []byte, err error) { - var need int +func read(r io.Reader) (response []byte, err error) { + var lenbuf [PacketLengthBytes]byte + var length int - if _, err = io.ReadFull(r, length); err != nil { + if _, err = io.ReadFull(r, lenbuf[:]); err != nil { return } - if length[0] != 0xce { + if lenbuf[0] != 0xce { err = errors.New("Wrong reponse header") return } - need = (int(length[1]) << 24) + - (int(length[2]) << 16) + - (int(length[3]) << 8) + - int(length[4]) + length = (int(lenbuf[1]) << 24) + + (int(lenbuf[2]) << 16) + + (int(lenbuf[3]) << 8) + + int(lenbuf[4]) - if need == 0 { + if length == 0 { err = errors.New("Response should not be 0 length") return } - response = make([]byte, need) + response = make([]byte, length) _, err = io.ReadFull(r, response) return From c5e4a5decfc010261b18c728bad39c8154897467 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Thu, 15 Jan 2015 20:11:38 +0300 Subject: [PATCH 031/605] simplify data structures --- connection.go | 24 ++++---- request.go | 38 +++++++----- response.go | 162 ++++++++++++++++---------------------------------- smallbuf.go | 52 ++++++++++++++++ 4 files changed, 138 insertions(+), 138 deletions(-) create mode 100644 smallbuf.go diff --git a/connection.go b/connection.go index b429905d4..c5e6552a2 100644 --- a/connection.go +++ b/connection.go @@ -20,7 +20,7 @@ type Connection struct { mutex *sync.Mutex requestId uint32 Greeting *Greeting - requests map[uint32]*responseAndError + requests map[uint32]*Future packets chan []byte control chan struct{} opts Opts @@ -45,7 +45,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { mutex: &sync.Mutex{}, requestId: 0, Greeting: &Greeting{}, - requests: make(map[uint32]*responseAndError), + requests: make(map[uint32]*Future), packets: make(chan []byte, 64), control: make(chan struct{}), opts: opts, @@ -117,9 +117,9 @@ func (conn *Connection) closeConnection(neterr error) (err error) { conn.connection = nil conn.r = nil conn.w = nil - for rid, resp := range conn.requests { - resp.r.Error = neterr - close(resp.c) + for rid, fut := range conn.requests { + fut.err = neterr + close(fut.c) delete(conn.requests, rid) } return @@ -171,18 +171,16 @@ func (conn *Connection) reader() { conn.closeConnection(err) continue } - var resp Response - resp_bytes = resp.fill(resp_bytes) - if resp.Error != nil { - conn.closeConnection(resp.Error) + resp, err := newResponse(resp_bytes) + if err != nil { + conn.closeConnection(err) continue } conn.mutex.Lock() - if r, ok := conn.requests[resp.RequestId]; ok { + if fut, ok := conn.requests[resp.RequestId]; ok { delete(conn.requests, resp.RequestId) - r.r = resp - r.b = resp_bytes - close(r.c) + fut.resp = resp + close(rae.c) conn.mutex.Unlock() } else { conn.mutex.Unlock() diff --git a/request.go b/request.go index 33a448981..4e3eab341 100644 --- a/request.go +++ b/request.go @@ -16,7 +16,9 @@ type Request struct { type Future struct { conn *Connection id uint32 - r responseAndError + resp *Response + err error + c chan struct{} t *time.Timer tc <-chan time.Time } @@ -194,22 +196,22 @@ func (req *Request) future() (f *Future) { f = &Future{ conn: req.conn, id: req.requestId, - r: responseAndError{c: make(chan struct{})}, + c: make(chan struct{}), } var packet []byte - if packet, f.r.r.Error = req.pack(); f.r.r.Error != nil { - close(f.r.c) + if packet, f.err = req.pack(); f.err != nil { + close(f.c) return } req.conn.mutex.Lock() if req.conn.closed { req.conn.mutex.Unlock() - f.r.r.Error = errors.New("using closed connection") - close(f.r.c) + f.err = errors.New("using closed connection") + close(f.c) return } - req.conn.requests[req.requestId] = &f.r + req.conn.requests[req.requestId] = &f req.conn.mutex.Unlock() req.conn.packets <- (packet) @@ -222,16 +224,16 @@ func (req *Request) future() (f *Future) { func (f *Future) wait() { select { - case <-f.r.c: + case <-f.c: default: select { - case <-f.r.c: + case <-f.c: case <-f.tc: f.conn.mutex.Lock() if _, ok := f.conn.requests[f.id]; ok { delete(f.conn.requests, f.id) - close(f.r.c) - f.r.r.Error = errors.New("client timeout") + close(f.c) + f.err = errors.New("client timeout") } f.conn.mutex.Unlock() } @@ -245,10 +247,18 @@ func (f *Future) wait() { func (f *Future) Get() (*Response, error) { f.wait() - return f.r.get() + if f.err != nil { + return f.resp, f.err + } + f.err = f.resp.decodeBody() + return f.resp, f.err } -func (f *Future) GetTyped(r interface{}) error { +func (f *Future) GetTyped(r interface{}) (error) { f.wait() - return f.r.getTyped(r) + if f.err != nil { + return f.err + } + f.err = f.resp.decodeBody(r) + return f.err } diff --git a/response.go b/response.go index 4dabe41e7..767bad462 100644 --- a/response.go +++ b/response.go @@ -1,172 +1,112 @@ package tarantool import ( - "errors" "fmt" "gopkg.in/vmihailenco/msgpack.v2" - "io" ) type Response struct { RequestId uint32 Code uint32 - Error error + Error string Data []interface{} + buf smallBuf } -type responseAndError struct { - c chan struct{} - b []byte - r Response +func (resp *Response) fill(b []byte) { + resp.buf.b = b } -type smallBuf struct { - b []byte - p int +func newResponse(b []byte) (resp *Response, err error) { + resp = &Response{ buf: smallBuf{b: b} } + err = resp.decodeHeader() + return } -func (s *smallBuf) Read(d []byte) (l int, err error) { - l = len(s.b) - s.p - if l == 0 && len(d) > 0 { - return 0, io.EOF - } - if l > len(d) { - l = len(d) - } - copy(d, s.b[s.p:]) - s.p += l - return l, nil -} - -func (s *smallBuf) ReadByte() (b byte, err error) { - if s.p == len(s.b) { - return 0, io.EOF - } - b = s.b[s.p] - s.p++ - return b, nil -} - -func (s *smallBuf) UnreadByte() error { - if s.p == 0 { - return errors.New("Could not unread") - } - s.p-- - return nil -} - -func (s *smallBuf) Len() int { - return len(s.b) - s.p -} - -func (s *smallBuf) Bytes() []byte { - if len(s.b) > s.p { - return s.b[s.p:] - } - return nil -} - -func (r *Response) fill(b []byte) []byte { +func (resp *Response) decodeHeader() (err error) { var l int - s := smallBuf{b: b} - d := msgpack.NewDecoder(&s) - if l, r.Error = d.DecodeMapLen(); r.Error != nil { - return nil + d := msgpack.NewDecoder(&resp.buf) + if l, err = d.DecodeMapLen(); err != nil { + return } for ; l > 0; l-- { var cd int - if cd, r.Error = d.DecodeInt(); r.Error != nil { - return nil + if cd, err = d.DecodeInt(); err != nil { + return } switch cd { case KeySync: - if r.RequestId, r.Error = d.DecodeUint32(); r.Error != nil { - return nil + if resp.RequestId, err = d.DecodeUint32(); err != nil { + return } case KeyCode: - if r.Code, r.Error = d.DecodeUint32(); r.Error != nil { - return nil + if resp.Code, err = d.DecodeUint32(); err != nil { + return } } } - return s.Bytes() -} - -func (resp *Response) String() (str string) { - if resp.Code == OkCode { - return fmt.Sprintf("<%d OK %v>", resp.RequestId, resp.Data) - } else { - return fmt.Sprintf("<%d ERR 0x%x %s>", resp.RequestId, resp.Code, resp.Error) - } + return nil } -func (r *responseAndError) get() (*Response, error) { - if r.r.Error != nil { - return &r.r, r.r.Error - } - if len(r.b) > 0 { +func (resp *Response) decodeBody() (err error) { + if len(resp.buf.Len()) > 2 { var body map[int]interface{} - d := msgpack.NewDecoder(&smallBuf{b: r.b}) + d := msgpack.NewDecoder(&resp.buf) - if r.r.Error = d.Decode(&body); r.r.Error != nil { - r.b = nil - return nil, r.r.Error + if err = d.Decode(&body); err != nil { + return nil, err } if body[KeyData] != nil { data := body[KeyData].([]interface{}) - r.r.Data = make([]interface{}, len(data)) + resp.Data = make([]interface{}, len(data)) for i, v := range data { - r.r.Data[i] = v.([]interface{}) + resp.Data[i] = v.([]interface{}) } } - if r.r.Code != OkCode { - r.r.Error = Error{r.r.Code, body[KeyError].(string)} + if resp.Code != OkCode { + err = Error{resp.Code, body[KeyError].(string)} } - r.b = nil } - - return &r.r, r.r.Error } -func (r *responseAndError) getTyped(res interface{}) error { - if r.r.Error != nil { - return r.r.Error - } - if len(r.b) > 0 { +func (resp *Response) decodeBodyTyped(res interface{}) (err error) { + if len(resp.buf.Len()) > 0 { var l int - d := msgpack.NewDecoder(&smallBuf{b: r.b}) - if l, r.r.Error = d.DecodeMapLen(); r.r.Error != nil { - r.b = nil - return r.r.Error + d := msgpack.NewDecoder(&resp.buf) + if l, err = d.DecodeMapLen(); err != nil { + return err } for ; l > 0; l-- { var cd int - if cd, r.r.Error = d.DecodeInt(); r.r.Error != nil { - r.b = nil - return r.r.Error + if cd, err = d.DecodeInt(); err != nil { + return err } switch cd { case KeyData: - if r.r.Error = d.Decode(res); r.r.Error != nil { - r.b = nil - return r.r.Error + if err = d.Decode(res); err != nil { + return err } case KeyError: - var str string - if str, r.r.Error = d.DecodeString(); r.r.Error == nil { - r.r.Error = Error{ - r.r.Code, - str, - } + if resp.Error, err = d.DecodeString(); err != nil { + return err } - r.b = nil - return r.r.Error } } + + if resp.Code != OkCode { + err = Error{resp.Code, body[KeyError].(string)} + } } + return +} - return r.r.Error +func (resp *Response) String() (str string) { + if resp.Code == OkCode { + return fmt.Sprintf("<%d OK %v>", resp.RequestId, resp.Data) + } else { + return fmt.Sprintf("<%d ERR 0x%x %s>", resp.RequestId, resp.Code, resp.Error) + } } diff --git a/smallbuf.go b/smallbuf.go new file mode 100644 index 000000000..2b5a9352f --- /dev/null +++ b/smallbuf.go @@ -0,0 +1,52 @@ +package tarantool + +import ( + "io" + "errors" +) + +type smallBuf struct { + b []byte + p int +} + +func (s *smallBuf) Read(d []byte) (l int, err error) { + l = len(s.b) - s.p + if l == 0 && len(d) > 0 { + return 0, io.EOF + } + if l > len(d) { + l = len(d) + } + copy(d, s.b[s.p:]) + s.p += l + return l, nil +} + +func (s *smallBuf) ReadByte() (b byte, err error) { + if s.p == len(s.b) { + return 0, io.EOF + } + b = s.b[s.p] + s.p++ + return b, nil +} + +func (s *smallBuf) UnreadByte() error { + if s.p == 0 { + return errors.New("Could not unread") + } + s.p-- + return nil +} + +func (s *smallBuf) Len() int { + return len(s.b) - s.p +} + +func (s *smallBuf) Bytes() []byte { + if len(s.b) > s.p { + return s.b[s.p:] + } + return nil +} From e6e1b261625e6126f4458029b1e690bd9995e1d2 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Thu, 15 Jan 2015 21:10:16 +0300 Subject: [PATCH 032/605] syntax errors --- connection.go | 2 +- request.go | 4 ++-- response.go | 14 +++++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/connection.go b/connection.go index c5e6552a2..28a170b47 100644 --- a/connection.go +++ b/connection.go @@ -180,7 +180,7 @@ func (conn *Connection) reader() { if fut, ok := conn.requests[resp.RequestId]; ok { delete(conn.requests, resp.RequestId) fut.resp = resp - close(rae.c) + close(fut.c) conn.mutex.Unlock() } else { conn.mutex.Unlock() diff --git a/request.go b/request.go index 4e3eab341..c407e8e59 100644 --- a/request.go +++ b/request.go @@ -211,7 +211,7 @@ func (req *Request) future() (f *Future) { close(f.c) return } - req.conn.requests[req.requestId] = &f + req.conn.requests[req.requestId] = f req.conn.mutex.Unlock() req.conn.packets <- (packet) @@ -259,6 +259,6 @@ func (f *Future) GetTyped(r interface{}) (error) { if f.err != nil { return f.err } - f.err = f.resp.decodeBody(r) + f.err = f.resp.decodeBodyTyped(r) return f.err } diff --git a/response.go b/response.go index 767bad462..a440eb9da 100644 --- a/response.go +++ b/response.go @@ -49,12 +49,12 @@ func (resp *Response) decodeHeader() (err error) { } func (resp *Response) decodeBody() (err error) { - if len(resp.buf.Len()) > 2 { + if resp.buf.Len() > 2 { var body map[int]interface{} d := msgpack.NewDecoder(&resp.buf) if err = d.Decode(&body); err != nil { - return nil, err + return err } if body[KeyData] != nil { @@ -64,15 +64,19 @@ func (resp *Response) decodeBody() (err error) { resp.Data[i] = v.([]interface{}) } } + if body[KeyError] != nil { + resp.Error = body[KeyError].(string) + } if resp.Code != OkCode { - err = Error{resp.Code, body[KeyError].(string)} + err = Error{resp.Code, resp.Error} } } + return } func (resp *Response) decodeBodyTyped(res interface{}) (err error) { - if len(resp.buf.Len()) > 0 { + if resp.buf.Len() > 0 { var l int d := msgpack.NewDecoder(&resp.buf) if l, err = d.DecodeMapLen(); err != nil { @@ -97,7 +101,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { } if resp.Code != OkCode { - err = Error{resp.Code, body[KeyError].(string)} + err = Error{resp.Code, resp.Error} } } return From 855df82a175cb5c1c2a52778a70ca56381c5963e Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Fri, 16 Jan 2015 11:25:49 +0300 Subject: [PATCH 033/605] minor optimization --- connection.go | 4 +++- request.go | 31 ++++++++++++++++--------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/connection.go b/connection.go index 28a170b47..2b1c55a38 100644 --- a/connection.go +++ b/connection.go @@ -171,7 +171,9 @@ func (conn *Connection) reader() { conn.closeConnection(err) continue } - resp, err := newResponse(resp_bytes) + resp := Response{buf:smallBuf{b:resp_bytes}} + err = resp.decodeHeader() + //resp, err := newResponse(resp_bytes) if err != nil { conn.closeConnection(err) continue diff --git a/request.go b/request.go index c407e8e59..9822c13c7 100644 --- a/request.go +++ b/request.go @@ -16,11 +16,10 @@ type Request struct { type Future struct { conn *Connection id uint32 - resp *Response + resp Response err error c chan struct{} t *time.Timer - tc <-chan time.Time } func (conn *Connection) NewRequest(requestCode int32) (req *Request) { @@ -217,7 +216,6 @@ func (req *Request) future() (f *Future) { if req.conn.opts.Timeout > 0 { f.t = time.NewTimer(req.conn.opts.Timeout) - f.tc = f.t.C } return } @@ -226,32 +224,35 @@ func (f *Future) wait() { select { case <-f.c: default: - select { - case <-f.c: - case <-f.tc: - f.conn.mutex.Lock() - if _, ok := f.conn.requests[f.id]; ok { - delete(f.conn.requests, f.id) - close(f.c) - f.err = errors.New("client timeout") + if t := f.t; t != nil { + select { + case <-f.c: + case <-t.C: + f.conn.mutex.Lock() + if _, ok := f.conn.requests[f.id]; ok { + delete(f.conn.requests, f.id) + close(f.c) + f.err = errors.New("client timeout") + } + f.conn.mutex.Unlock() } - f.conn.mutex.Unlock() + } else { + <-f.c } } if f.t != nil { f.t.Stop() f.t = nil - f.tc = nil } } func (f *Future) Get() (*Response, error) { f.wait() if f.err != nil { - return f.resp, f.err + return &f.resp, f.err } f.err = f.resp.decodeBody() - return f.resp, f.err + return &f.resp, f.err } func (f *Future) GetTyped(r interface{}) (error) { From d3c5510d03e6a285a77745f9693caf0a39c78d28 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Tue, 4 Aug 2015 19:16:32 +0300 Subject: [PATCH 034/605] Update README.md --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 21d5ecae0..d35a25836 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Tarantool -[Tarantool 1.6](http://tarantool.org/) client on Go. +[Tarantool 1.6+](http://tarantool.org/) client on Go. ## Usage @@ -142,6 +142,3 @@ func main() { // #=> Data [] // #=> ---- ``` - -## Contributors -@mialinx, не хочешь забрать клиент на суппорт? @kostja будет безумно рад From bfae99e5895df599c79c198fc267be7978be4fa0 Mon Sep 17 00:00:00 2001 From: shilkin Date: Thu, 20 Aug 2015 16:54:12 +0300 Subject: [PATCH 035/605] Auth request added + tests --- config.lua | 12 +++++++--- connection.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++- const.go | 1 + request.go | 7 +++++- tarantool_test.go | 28 ++++++++++++++++++---- 5 files changed, 99 insertions(+), 9 deletions(-) diff --git a/config.lua b/config.lua index 37c9b2548..3f17f0e50 100644 --- a/config.lua +++ b/config.lua @@ -6,8 +6,14 @@ box.cfg{ local s = box.schema.space.create('test', {if_not_exists = true}) local i = s:create_index('primary', {type = 'hash', parts = {1, 'NUM'}, if_not_exists = true}) -box.schema.user.grant('guest', 'read,write,execute', 'universe') +-- box.schema.user.grant('guest', 'read,write,execute', 'universe') -local console = require 'console' -console.listen '0.0.0.0:33015' +-- auth testing: access control +if not box.schema.user.exists('test') then + box.schema.user.create('test', {password = 'test'}) + box.schema.user.grant('test', 'read,write,execute', 'universe') +end + +--local console = require 'console' +--console.listen '0.0.0.0:33015' diff --git a/connection.go b/connection.go index 2b1c55a38..5be0f3753 100644 --- a/connection.go +++ b/connection.go @@ -10,6 +10,8 @@ import ( "sync" "sync/atomic" "time" + "crypto/sha1" + "encoding/base64" ) type Connection struct { @@ -35,6 +37,8 @@ type Greeting struct { type Opts struct { Timeout time.Duration // milliseconds Reconnect time.Duration // milliseconds + User string + Pass string } func Connect(addr string, opts Opts) (conn *Connection, err error) { @@ -58,6 +62,15 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { go conn.writer() go conn.reader() + + // Send auth request if needed + if opts.User != "" { + err = conn.auth() + if err != nil { + return + } + } + return } @@ -84,10 +97,55 @@ func (conn *Connection) dial() (err error) { return } conn.Greeting.version = bytes.NewBuffer(greeting[:64]).String() - conn.Greeting.auth = bytes.NewBuffer(greeting[64:]).String() + conn.Greeting.auth = bytes.NewBuffer(greeting[64:108]).String() + return +} + +func (conn *Connection) auth() (err error) { + scr, err := scramble(conn.Greeting.auth, conn.opts.Pass) + if err != nil { + return + } + _, err = conn.Auth(conn.opts.User, []interface{}{string("chap-sha1"), string(scr)}) return } +func scramble(encoded_salt, pass string) (scramble []byte, err error) { + /* ================================================================== + Acording to: http://tarantool.org/doc/dev_guide/box-protocol.html + + salt = base64_decode(encoded_salt); + step_1 = sha1(password); + step_2 = sha1(step_1); + step_3 = sha1(salt, step_2); + scramble = xor(step_1, step_3); + return scramble; + + ===================================================================== */ + scrambleSize := sha1.Size // == 20 + + salt, err := base64.StdEncoding.DecodeString(encoded_salt) + if err != nil { + return + } + step_1 := sha1.Sum([]byte(pass)) + step_2 := sha1.Sum(step_1[0:]) + hash := sha1.New() // may be create it once per connection ? + hash.Write(salt[0:scrambleSize]) + hash.Write(step_2[0:]) + step_3 := hash.Sum(nil) + + return xor(step_1[0:], step_3[0:], scrambleSize), nil +} + +func xor(left, right []byte, size int) []byte { + result := make([]byte, size) + for i := 0; i < size ; i++ { + result[i] = left[i] ^ right[i] + } + return result +} + func (conn *Connection) createConnection() (r io.Reader, w *bufio.Writer) { conn.mutex.Lock() defer conn.mutex.Unlock() diff --git a/const.go b/const.go index d55fb619e..533316dcd 100644 --- a/const.go +++ b/const.go @@ -21,6 +21,7 @@ const ( KeyKey = 0x20 KeyTuple = 0x21 KeyFunctionName = 0x22 + KeyUserName = 0x23 KeyData = 0x30 KeyError = 0x31 diff --git a/request.go b/request.go index 9822c13c7..6492266f3 100644 --- a/request.go +++ b/request.go @@ -148,7 +148,11 @@ func (conn *Connection) CallAsync(functionName string, tuple []interface{}) *Fut // // To be implemented // -func (conn *Connection) Auth(key, tuple []interface{}) (resp *Response, err error) { +func (conn *Connection) Auth(user string, tuple []interface{}) (resp *Response, err error) { + request := conn.NewRequest(AuthRequest) + request.body[KeyUserName] = user + request.body[KeyTuple] = tuple + resp, err = request.perform() return } @@ -204,6 +208,7 @@ func (req *Request) future() (f *Future) { } req.conn.mutex.Lock() + if req.conn.closed { req.conn.mutex.Unlock() f.err = errors.New("using closed connection") diff --git a/tarantool_test.go b/tarantool_test.go index 1dc59499c..0b6a1cd85 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -25,6 +25,7 @@ var opts = Opts{Timeout: 500 * time.Millisecond} const N = 500 + func BenchmarkClientSerial(b *testing.B) { var err error @@ -248,9 +249,29 @@ func BenchmarkClientParrallel(b *testing.B) { } func TestClient(t *testing.T) { - client, err := Connect(server, Opts{}) + + // Invalid user + client, err := Connect(server, Opts{User: "invalid_user", Pass: "test"}) + if err == nil { + t.Errorf("Should fail with incorrect password") + } + + // Invalid pass + client, err = Connect(server, Opts{User: "test", Pass: "invalid_pass"}) + if err == nil { + t.Errorf("Should fail with incorrect user") + } + + // Invalid user/pass + client, err = Connect(server, Opts{User: "invalid_user", Pass: "invalid_pass"}) + if err == nil { + t.Errorf("Should fail with incorrect user and password") + } + + // Valid user/pass + client, err = Connect(server, Opts{User: "test", Pass: "test"}) if err != nil { - t.Errorf("No connection available") + t.Errorf("Should pass but error is [%s]", err.Error()) } var resp *Response @@ -340,5 +361,4 @@ func TestClient(t *testing.T) { fmt.Println("Code", resp.Code) fmt.Println("Data", resp.Data) fmt.Println("----") - -} +} \ No newline at end of file From ced2d7b0eaeee661074683e9552d0f9635474eef Mon Sep 17 00:00:00 2001 From: shilkin Date: Thu, 20 Aug 2015 16:56:13 +0300 Subject: [PATCH 036/605] config.lua fixed --- config.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config.lua b/config.lua index 3f17f0e50..859a656c2 100644 --- a/config.lua +++ b/config.lua @@ -6,7 +6,7 @@ box.cfg{ local s = box.schema.space.create('test', {if_not_exists = true}) local i = s:create_index('primary', {type = 'hash', parts = {1, 'NUM'}, if_not_exists = true}) --- box.schema.user.grant('guest', 'read,write,execute', 'universe') +box.schema.user.grant('guest', 'read,write,execute', 'universe') -- auth testing: access control if not box.schema.user.exists('test') then @@ -14,6 +14,6 @@ if not box.schema.user.exists('test') then box.schema.user.grant('test', 'read,write,execute', 'universe') end ---local console = require 'console' ---console.listen '0.0.0.0:33015' +local console = require 'console' +console.listen '0.0.0.0:33015' From 9c5395c938ac80063d0782fc713febd25844d87b Mon Sep 17 00:00:00 2001 From: Denis Date: Thu, 20 Aug 2015 17:04:34 +0300 Subject: [PATCH 037/605] New line at the end of file --- tarantool_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tarantool_test.go b/tarantool_test.go index 0b6a1cd85..44d8a201b 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -361,4 +361,4 @@ func TestClient(t *testing.T) { fmt.Println("Code", resp.Code) fmt.Println("Data", resp.Data) fmt.Println("----") -} \ No newline at end of file +} From f09e3cf43d887209e0a9b68ce0e5df1ee5a68c85 Mon Sep 17 00:00:00 2001 From: shilkin Date: Wed, 26 Aug 2015 17:39:01 +0300 Subject: [PATCH 038/605] Added auth during reconnect --- auth.go | 42 +++++++++++++++ connection.go | 134 +++++++++++++++++++++++++++------------------- request.go | 18 ++++++- tarantool_test.go | 31 +++++------ 4 files changed, 150 insertions(+), 75 deletions(-) create mode 100644 auth.go diff --git a/auth.go b/auth.go new file mode 100644 index 000000000..349885f84 --- /dev/null +++ b/auth.go @@ -0,0 +1,42 @@ +package tarantool + +import ( + "crypto/sha1" + "encoding/base64" +) + +func scramble(encoded_salt, pass string) (scramble []byte, err error) { + /* ================================================================== + Acording to: http://tarantool.org/doc/dev_guide/box-protocol.html + + salt = base64_decode(encoded_salt); + step_1 = sha1(password); + step_2 = sha1(step_1); + step_3 = sha1(salt, step_2); + scramble = xor(step_1, step_3); + return scramble; + + ===================================================================== */ + scrambleSize := sha1.Size // == 20 + + salt, err := base64.StdEncoding.DecodeString(encoded_salt) + if err != nil { + return + } + step_1 := sha1.Sum([]byte(pass)) + step_2 := sha1.Sum(step_1[0:]) + hash := sha1.New() // may be create it once per connection ? + hash.Write(salt[0:scrambleSize]) + hash.Write(step_2[0:]) + step_3 := hash.Sum(nil) + + return xor(step_1[0:], step_3[0:], scrambleSize), nil +} + +func xor(left, right []byte, size int) []byte { + result := make([]byte, size) + for i := 0; i < size ; i++ { + result[i] = left[i] ^ right[i] + } + return result +} \ No newline at end of file diff --git a/connection.go b/connection.go index 5be0f3753..b21c68c91 100644 --- a/connection.go +++ b/connection.go @@ -10,8 +10,6 @@ import ( "sync" "sync/atomic" "time" - "crypto/sha1" - "encoding/base64" ) type Connection struct { @@ -55,23 +53,10 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { opts: opts, } - err = conn.dial() - if err != nil { - return - } - go conn.writer() go conn.reader() - // Send auth request if needed - if opts.User != "" { - err = conn.auth() - if err != nil { - return - } - } - - return + return conn, err } func (conn *Connection) Close() (err error) { @@ -82,80 +67,110 @@ func (conn *Connection) Close() (err error) { } func (conn *Connection) dial() (err error) { + conn.mutex.Lock() + defer conn.mutex.Unlock() + + if conn.connection != nil { + log.Println("dial conn.connection != nil - return") + return + } + connection, err := net.Dial("tcp", conn.addr) if err != nil { + log.Println("dial net.Dial error - return") return } connection.(*net.TCPConn).SetNoDelay(true) - conn.connection = connection - conn.r = bufio.NewReaderSize(conn.connection, 128*1024) - conn.w = bufio.NewWriter(conn.connection) + r := bufio.NewReaderSize(connection, 128*1024) + w := bufio.NewWriter(connection) greeting := make([]byte, 128) // TODO: read all - _, err = conn.connection.Read(greeting) + _, err = connection.Read(greeting) if err != nil { return } conn.Greeting.version = bytes.NewBuffer(greeting[:64]).String() conn.Greeting.auth = bytes.NewBuffer(greeting[64:108]).String() + + // Auth + if err = conn.auth(r, w); err != nil { + return + } + + // Only if connected and authenticated + conn.connection = connection // TODO: think about atomic LoadPointer/StorePointer + conn.r = r + conn.w = w + return } -func (conn *Connection) auth() (err error) { +func (conn *Connection) auth(r io.Reader, w *bufio.Writer) (err error) { + if conn.opts.User == "" { + return // without authentication [guest session] + } + if r == nil || w == nil { + return errors.New("auth: reader/writer not ready") + } + if conn.Greeting.auth == "" { + return errors.New("auth: empty greeting") + } scr, err := scramble(conn.Greeting.auth, conn.opts.Pass) if err != nil { + return errors.New("auth: scrambling failure " + err.Error()) + } + if err = conn.writeAuthRequest(w, scr); err != nil { + return + } + if err = conn.readAuthResponse(r); err !=nil { return } - _, err = conn.Auth(conn.opts.User, []interface{}{string("chap-sha1"), string(scr)}) return } -func scramble(encoded_salt, pass string) (scramble []byte, err error) { - /* ================================================================== - Acording to: http://tarantool.org/doc/dev_guide/box-protocol.html - - salt = base64_decode(encoded_salt); - step_1 = sha1(password); - step_2 = sha1(step_1); - step_3 = sha1(salt, step_2); - scramble = xor(step_1, step_3); - return scramble; - - ===================================================================== */ - scrambleSize := sha1.Size // == 20 - - salt, err := base64.StdEncoding.DecodeString(encoded_salt) +func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { + request := conn.NewRequest(AuthRequest) + request.body[KeyUserName] = conn.opts.User + request.body[KeyTuple] = []interface{}{string("chap-sha1"), string(scramble)} + packet, err := request.pack() if err != nil { - return + return errors.New("auth: pack error " + err.Error()) } - step_1 := sha1.Sum([]byte(pass)) - step_2 := sha1.Sum(step_1[0:]) - hash := sha1.New() // may be create it once per connection ? - hash.Write(salt[0:scrambleSize]) - hash.Write(step_2[0:]) - step_3 := hash.Sum(nil) - - return xor(step_1[0:], step_3[0:], scrambleSize), nil + if err := write(w, packet); err != nil { + return errors.New("auth: write error " + err.Error()) + } + if err = w.Flush(); err != nil { + return errors.New("auth: flush error " + err.Error()) + } + return } -func xor(left, right []byte, size int) []byte { - result := make([]byte, size) - for i := 0; i < size ; i++ { - result[i] = left[i] ^ right[i] +func (conn *Connection) readAuthResponse(r io.Reader) (err error) { + resp_bytes, err := read(r) + if err != nil { + return errors.New("auth: read error " + err.Error()) + } + resp := Response{buf:smallBuf{b:resp_bytes}} + err = resp.decodeHeader() + if err != nil { + return errors.New("auth: decode response header error " + err.Error()) + } + err = resp.decodeBody() + if err != nil { + return errors.New("auth: decode response body error " + err.Error()) } - return result + return } func (conn *Connection) createConnection() (r io.Reader, w *bufio.Writer) { - conn.mutex.Lock() - defer conn.mutex.Unlock() - for conn.connection == nil { + // mutex.lock() replaced to dial() and connectoinIsNil() : finely granular locking + for conn.connectionIsNil() { if conn.closed { return } err := conn.dial() if err == nil { - break + break } else if conn.opts.Reconnect > 0 { time.Sleep(conn.opts.Reconnect) } else { @@ -165,6 +180,15 @@ func (conn *Connection) createConnection() (r io.Reader, w *bufio.Writer) { return conn.r, conn.w } + +func (conn *Connection) connectionIsNil() bool { + // TODO: think about atomic LoadPointer/StorePointer + conn.mutex.Lock() + defer conn.mutex.Unlock() + return conn.connection == nil +} + + func (conn *Connection) closeConnection(neterr error) (err error) { conn.mutex.Lock() defer conn.mutex.Unlock() diff --git a/request.go b/request.go index 6492266f3..1846106b2 100644 --- a/request.go +++ b/request.go @@ -149,6 +149,11 @@ func (conn *Connection) CallAsync(functionName string, tuple []interface{}) *Fut // To be implemented // func (conn *Connection) Auth(user string, tuple []interface{}) (resp *Response, err error) { + // This request does not make sense + // It should be a part of connecting process: + // * phisical connection - tcp + // * logical connection - authentication + // Functionality implemented in Connection.dial() request := conn.NewRequest(AuthRequest) request.body[KeyUserName] = user request.body[KeyTuple] = tuple @@ -201,20 +206,31 @@ func (req *Request) future() (f *Future) { id: req.requestId, c: make(chan struct{}), } + + // check connection ready to process packets + if f.conn.connectionIsNil() { + close(f.c) + f.err = errors.New("client connection is not ready") + return // we shouldn't perform this request + } + var packet []byte if packet, f.err = req.pack(); f.err != nil { close(f.c) return } + // TODO: conn.connectionIsClosed() with mutex inside req.conn.mutex.Lock() - if req.conn.closed { req.conn.mutex.Unlock() f.err = errors.New("using closed connection") close(f.c) return } + + // TODO: conn.addRequest() with mutex inside or think about message passing + // Gophers rule: "Share memory by communicating; don't communicate by sharing memory." req.conn.requests[req.requestId] = f req.conn.mutex.Unlock() req.conn.packets <- (packet) diff --git a/tarantool_test.go b/tarantool_test.go index 0b6a1cd85..2558ffc9a 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -250,30 +250,23 @@ func BenchmarkClientParrallel(b *testing.B) { func TestClient(t *testing.T) { - // Invalid user - client, err := Connect(server, Opts{User: "invalid_user", Pass: "test"}) - if err == nil { - t.Errorf("Should fail with incorrect password") - } - - // Invalid pass - client, err = Connect(server, Opts{User: "test", Pass: "invalid_pass"}) - if err == nil { - t.Errorf("Should fail with incorrect user") - } - - // Invalid user/pass - client, err = Connect(server, Opts{User: "invalid_user", Pass: "invalid_pass"}) - if err == nil { - t.Errorf("Should fail with incorrect user and password") - } - // Valid user/pass - client, err = Connect(server, Opts{User: "test", Pass: "test"}) + client, err := Connect(server, Opts{User: "test", Pass: "test"}) if err != nil { t.Errorf("Should pass but error is [%s]", err.Error()) } + // Wait connection + /*i := 10 + for client.connectionIsNil() { + time.Sleep(500 * time.Millisecond) + i-- + if i == 0 { + t.Errorf("Connection failed") + return + } + }*/ + var resp *Response resp, err = client.Ping() From 9999bb60e69481e65f4c019ae293a6d9c5cafa7f Mon Sep 17 00:00:00 2001 From: shilkin Date: Wed, 26 Aug 2015 17:42:12 +0300 Subject: [PATCH 039/605] Remove garbage from test --- tarantool_test.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tarantool_test.go b/tarantool_test.go index f0a6d43e5..bd3bda306 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -256,17 +256,6 @@ func TestClient(t *testing.T) { t.Errorf("Should pass but error is [%s]", err.Error()) } - // Wait connection - /*i := 10 - for client.connectionIsNil() { - time.Sleep(500 * time.Millisecond) - i-- - if i == 0 { - t.Errorf("Connection failed") - return - } - }*/ - var resp *Response resp, err = client.Ping() From d34fc099844ef6d24794b544bcae5e1c8a09ae23 Mon Sep 17 00:00:00 2001 From: shilkin Date: Wed, 26 Aug 2015 17:56:44 +0300 Subject: [PATCH 040/605] debug print removed, comment added --- connection.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/connection.go b/connection.go index b21c68c91..fb395d2fe 100644 --- a/connection.go +++ b/connection.go @@ -71,8 +71,7 @@ func (conn *Connection) dial() (err error) { defer conn.mutex.Unlock() if conn.connection != nil { - log.Println("dial conn.connection != nil - return") - return + return // in case connection was created by enother goroutine (reader or writer) } connection, err := net.Dial("tcp", conn.addr) From e70345a4c45509bc01d2a53bc20a79479d6dbc77 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Sun, 30 Aug 2015 07:09:18 +0300 Subject: [PATCH 041/605] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d35a25836..83294e424 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ package main import ( - "github.com/fl00r/go-tarantool-1.6" + "github.com/tarantool/go-tarantool" "fmt" ) From da93738e66f35c08141ff44aaf4a8445a442b00d Mon Sep 17 00:00:00 2001 From: shilkin Date: Mon, 31 Aug 2015 10:49:19 +0300 Subject: [PATCH 042/605] Mutual exclusion of call createConnection/closeConnection --- auth.go | 12 ++++++------ config.lua | 2 ++ connection.go | 25 +++++++++---------------- request.go | 5 +---- 4 files changed, 18 insertions(+), 26 deletions(-) diff --git a/auth.go b/auth.go index 349885f84..7d215e460 100644 --- a/auth.go +++ b/auth.go @@ -7,14 +7,14 @@ import ( func scramble(encoded_salt, pass string) (scramble []byte, err error) { /* ================================================================== - Acording to: http://tarantool.org/doc/dev_guide/box-protocol.html + According to: http://tarantool.org/doc/dev_guide/box-protocol.html salt = base64_decode(encoded_salt); - step_1 = sha1(password); - step_2 = sha1(step_1); - step_3 = sha1(salt, step_2); - scramble = xor(step_1, step_3); - return scramble; + step_1 = sha1(password); + step_2 = sha1(step_1); + step_3 = sha1(salt, step_2); + scramble = xor(step_1, step_3); + return scramble; ===================================================================== */ scrambleSize := sha1.Size // == 20 diff --git a/config.lua b/config.lua index 859a656c2..1813e6256 100644 --- a/config.lua +++ b/config.lua @@ -17,3 +17,5 @@ end local console = require 'console' console.listen '0.0.0.0:33015' +box.schema.user.revoke('guest', 'read,write,execute', 'universe') + diff --git a/connection.go b/connection.go index fb395d2fe..39700b8a9 100644 --- a/connection.go +++ b/connection.go @@ -67,16 +67,8 @@ func (conn *Connection) Close() (err error) { } func (conn *Connection) dial() (err error) { - conn.mutex.Lock() - defer conn.mutex.Unlock() - - if conn.connection != nil { - return // in case connection was created by enother goroutine (reader or writer) - } - connection, err := net.Dial("tcp", conn.addr) if err != nil { - log.Println("dial net.Dial error - return") return } connection.(*net.TCPConn).SetNoDelay(true) @@ -86,6 +78,7 @@ func (conn *Connection) dial() (err error) { // TODO: read all _, err = connection.Read(greeting) if err != nil { + connection.Close() return } conn.Greeting.version = bytes.NewBuffer(greeting[:64]).String() @@ -93,11 +86,12 @@ func (conn *Connection) dial() (err error) { // Auth if err = conn.auth(r, w); err != nil { + connection.Close() return } // Only if connected and authenticated - conn.connection = connection // TODO: think about atomic LoadPointer/StorePointer + conn.connection = connection conn.r = r conn.w = w @@ -162,8 +156,9 @@ func (conn *Connection) readAuthResponse(r io.Reader) (err error) { } func (conn *Connection) createConnection() (r io.Reader, w *bufio.Writer) { - // mutex.lock() replaced to dial() and connectoinIsNil() : finely granular locking - for conn.connectionIsNil() { + conn.mutex.Lock() + defer conn.mutex.Unlock() + for conn.connection == nil { if conn.closed { return } @@ -179,14 +174,12 @@ func (conn *Connection) createConnection() (r io.Reader, w *bufio.Writer) { return conn.r, conn.w } - +/* func (conn *Connection) connectionIsNil() bool { - // TODO: think about atomic LoadPointer/StorePointer - conn.mutex.Lock() - defer conn.mutex.Unlock() + // function to encapsulate architecture depentent things return conn.connection == nil } - +*/ func (conn *Connection) closeConnection(neterr error) (err error) { conn.mutex.Lock() diff --git a/request.go b/request.go index 1846106b2..7d3edaf72 100644 --- a/request.go +++ b/request.go @@ -208,7 +208,7 @@ func (req *Request) future() (f *Future) { } // check connection ready to process packets - if f.conn.connectionIsNil() { + if f.conn.connection == nil { close(f.c) f.err = errors.New("client connection is not ready") return // we shouldn't perform this request @@ -220,7 +220,6 @@ func (req *Request) future() (f *Future) { return } - // TODO: conn.connectionIsClosed() with mutex inside req.conn.mutex.Lock() if req.conn.closed { req.conn.mutex.Unlock() @@ -229,8 +228,6 @@ func (req *Request) future() (f *Future) { return } - // TODO: conn.addRequest() with mutex inside or think about message passing - // Gophers rule: "Share memory by communicating; don't communicate by sharing memory." req.conn.requests[req.requestId] = f req.conn.mutex.Unlock() req.conn.packets <- (packet) From 31fe1e15cc9b439c48fb03d7134f309c4bce23b8 Mon Sep 17 00:00:00 2001 From: shilkin Date: Mon, 31 Aug 2015 10:54:28 +0300 Subject: [PATCH 043/605] Formattign --- connection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connection.go b/connection.go index 39700b8a9..d40bdf6b3 100644 --- a/connection.go +++ b/connection.go @@ -164,7 +164,7 @@ func (conn *Connection) createConnection() (r io.Reader, w *bufio.Writer) { } err := conn.dial() if err == nil { - break + break } else if conn.opts.Reconnect > 0 { time.Sleep(conn.opts.Reconnect) } else { From 7a8e56168ef332cbe36a8c6027c23d3b564b2708 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Wed, 2 Sep 2015 14:49:04 +0300 Subject: [PATCH 044/605] auth support --- README.md | 159 +++++++++---------------------------------- connection.go | 167 +++++++++++++++++++++++----------------------- request.go | 22 +----- tarantool_test.go | 3 +- 4 files changed, 119 insertions(+), 232 deletions(-) diff --git a/README.md b/README.md index d35a25836..403e18927 100644 --- a/README.md +++ b/README.md @@ -8,137 +8,42 @@ package main import ( - "github.com/fl00r/go-tarantool-1.6" + "github.com/tarantool/go-tarantool" + "time" "fmt" ) func main() { - server := "127.0.0.1:3013" - spaceNo := uint32(514) - indexNo := uint32(0) - limit := uint32(10) - offset := uint32(0) - iterator := tarantool.IterAll - key := []interface{}{ 12 } - tuple1 := []interface{}{ 12, "Hello World", "Olga" } - tuple2 := []interface{}{ 12, "Hello Mars", "Anna" } - upd_tuple := []interface{}{ []interface{}{ "=", 1, "Hello Moon" }, []interface{}{ "#", 2, 1 } } - - functionName := "box.cfg()" - functionTuple := []interface{}{ "box.schema.SPACE_ID" } - - - client, err := tarantool.Connect(server) - - var resp *tarantool.Response - - resp, err = client.Ping() - fmt.Println("Ping") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Insert(spaceNo, tuple1) - fmt.Println("Insert") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Select(spaceNo, indexNo, offset, limit, iterator, key) - fmt.Println("Select") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Replace(spaceNo, tuple2) - fmt.Println("Replace") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Select(spaceNo, indexNo, offset, limit, iterator, key) - fmt.Println("Select") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Update(spaceNo, indexNo, key, upd_tuple) - fmt.Println("Update") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Select(spaceNo, indexNo, offset, limit, iterator, key) - fmt.Println("Select") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Delete(spaceNo, indexNo, key) - fmt.Println("Delete") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Call(functionName, functionTuple) - fmt.Println("Call") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") + spaceNo := uint32(512) + indexNo := uint32(0) + tuple1 := []interface{}{12, "Hello World", "Olga"} + + server := "127.0.0.1:3013" + opts := tarantool.Opts{ + Timeout: 500 * time.Millisecond, + Reconnect: 3 * time.Second, + MaxReconnects: 10, + User: "test", + Pass: "pass", + } + client, err := tarantool.Connect(server, opts) + if err != nil { + log.Fatalf("Failed to connect: %s", err.Error()) + } + + resp, err = client.Ping() + fmt.Println(resp.Code) + fmt.Println(resp.Data) + fmt.Println(err) + + resp, err = client.Insert(spaceNo, tuple1) + fmt.Println("Insert") + fmt.Println("ERROR", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("----") + + // TODO: complete doc } -// #=> Connecting to 127.0.0.1:3013 ... -// #=> Connected ... -// #=> Greeting ... Success -// #=> Version: Tarantool 1.6.2-34-ga53cf4a -// #=> -// #=> Insert -// #=> ERROR -// #=> Code 0 -// #=> Data [[12 Hello World Olga]] -// #=> ---- -// #=> Select -// #=> ERROR -// #=> Code 0 -// #=> Data [[12 Hello World Olga]] -// #=> ---- -// #=> Replace -// #=> ERROR -// #=> Code 0 -// #=> Data [[12 Hello Mars Anna]] -// #=> ---- -// #=> Select -// #=> ERROR -// #=> Code 0 -// #=> Data [[12 Hello Mars Anna]] -// #=> ---- -// #=> Update -// #=> ERROR -// #=> Code 0 -// #=> Data [[12 Hello Moon]] -// #=> ---- -// #=> Select -// #=> ERROR -// #=> Code 0 -// #=> Data [[12 Hello Moon]] -// #=> ---- -// #=> Delete -// #=> ERROR -// #=> Code 0 -// #=> Data [[12 Hello Moon]] -// #=> ---- -// #=> Call -// #=> ERROR Execute access denied for user 'guest' to function 'box.cfg()' -// #=> Code 13570 -// #=> Data [] -// #=> ---- ``` diff --git a/connection.go b/connection.go index d40bdf6b3..d49dd2198 100644 --- a/connection.go +++ b/connection.go @@ -13,18 +13,18 @@ import ( ) type Connection struct { - addr string - connection net.Conn - r io.Reader - w *bufio.Writer - mutex *sync.Mutex - requestId uint32 - Greeting *Greeting - requests map[uint32]*Future - packets chan []byte - control chan struct{} - opts Opts - closed bool + addr string + c *net.TCPConn + r *bufio.Reader + w *bufio.Writer + mutex *sync.Mutex + requestId uint32 + Greeting *Greeting + requests map[uint32]*Future + packets chan []byte + control chan struct{} + opts Opts + closed bool } type Greeting struct { @@ -33,30 +33,35 @@ type Greeting struct { } type Opts struct { - Timeout time.Duration // milliseconds - Reconnect time.Duration // milliseconds - User string - Pass string + Timeout time.Duration // milliseconds + Reconnect time.Duration // milliseconds + MaxReconnects uint + User string + Pass string } func Connect(addr string, opts Opts) (conn *Connection, err error) { conn = &Connection{ - addr: addr, - connection: nil, - mutex: &sync.Mutex{}, - requestId: 0, - Greeting: &Greeting{}, - requests: make(map[uint32]*Future), - packets: make(chan []byte, 64), - control: make(chan struct{}), - opts: opts, + addr: addr, + mutex: &sync.Mutex{}, + requestId: 0, + Greeting: &Greeting{}, + requests: make(map[uint32]*Future), + packets: make(chan []byte, 64), + control: make(chan struct{}), + opts: opts, + } + + _, _, err = conn.createConnection() + if err != nil { + return nil, err } go conn.writer() go conn.reader() - return conn, err + return } func (conn *Connection) Close() (err error) { @@ -71,56 +76,45 @@ func (conn *Connection) dial() (err error) { if err != nil { return } - connection.(*net.TCPConn).SetNoDelay(true) - r := bufio.NewReaderSize(connection, 128*1024) - w := bufio.NewWriter(connection) + c := connection.(*net.TCPConn) + c.SetNoDelay(true) + r := bufio.NewReaderSize(c, 128*1024) + w := bufio.NewWriter(c) greeting := make([]byte, 128) - // TODO: read all - _, err = connection.Read(greeting) + _, err = io.ReadFull(r, greeting) if err != nil { - connection.Close() + c.Close() return } conn.Greeting.version = bytes.NewBuffer(greeting[:64]).String() conn.Greeting.auth = bytes.NewBuffer(greeting[64:108]).String() // Auth - if err = conn.auth(r, w); err != nil { - connection.Close() - return + if conn.opts.User != "" { + scr, err := scramble(conn.Greeting.auth, conn.opts.Pass) + if err != nil { + err = errors.New("auth: scrambling failure " + err.Error()) + c.Close() + return err + } + if err = conn.writeAuthRequest(w, scr); err != nil { + c.Close() + return err + } + if err = conn.readAuthResponse(r); err != nil { + c.Close() + return err + } } // Only if connected and authenticated - conn.connection = connection + conn.c = c conn.r = r conn.w = w return } -func (conn *Connection) auth(r io.Reader, w *bufio.Writer) (err error) { - if conn.opts.User == "" { - return // without authentication [guest session] - } - if r == nil || w == nil { - return errors.New("auth: reader/writer not ready") - } - if conn.Greeting.auth == "" { - return errors.New("auth: empty greeting") - } - scr, err := scramble(conn.Greeting.auth, conn.opts.Pass) - if err != nil { - return errors.New("auth: scrambling failure " + err.Error()) - } - if err = conn.writeAuthRequest(w, scr); err != nil { - return - } - if err = conn.readAuthResponse(r); err !=nil { - return - } - return -} - func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { request := conn.NewRequest(AuthRequest) request.body[KeyUserName] = conn.opts.User @@ -143,7 +137,7 @@ func (conn *Connection) readAuthResponse(r io.Reader) (err error) { if err != nil { return errors.New("auth: read error " + err.Error()) } - resp := Response{buf:smallBuf{b:resp_bytes}} + resp := Response{buf: smallBuf{b: resp_bytes}} err = resp.decodeHeader() if err != nil { return errors.New("auth: decode response header error " + err.Error()) @@ -155,40 +149,43 @@ func (conn *Connection) readAuthResponse(r io.Reader) (err error) { return } -func (conn *Connection) createConnection() (r io.Reader, w *bufio.Writer) { +func (conn *Connection) createConnection() (r *bufio.Reader, w *bufio.Writer, err error) { conn.mutex.Lock() defer conn.mutex.Unlock() - for conn.connection == nil { - if conn.closed { - return - } - err := conn.dial() + if conn.closed { + err = errors.New("connection already closed") + return + } + var reconnects uint + for { + err = conn.dial() if err == nil { break } else if conn.opts.Reconnect > 0 { - time.Sleep(conn.opts.Reconnect) + if conn.opts.MaxReconnects > 0 && reconnects > conn.opts.MaxReconnects { + return + } else { + reconnects += 1 + time.Sleep(conn.opts.Reconnect) + continue + } } else { return } } - return conn.r, conn.w -} - -/* -func (conn *Connection) connectionIsNil() bool { - // function to encapsulate architecture depentent things - return conn.connection == nil + r = conn.r + w = conn.w + return } -*/ func (conn *Connection) closeConnection(neterr error) (err error) { conn.mutex.Lock() defer conn.mutex.Unlock() - if conn.connection == nil { + if conn.c == nil { return } - err = conn.connection.Close() - conn.connection = nil + err = conn.c.Close() + conn.c = nil conn.r = nil conn.w = nil for rid, fut := range conn.requests { @@ -201,6 +198,7 @@ func (conn *Connection) closeConnection(neterr error) (err error) { func (conn *Connection) writer() { var w *bufio.Writer + var err error for { var packet []byte select { @@ -221,7 +219,7 @@ func (conn *Connection) writer() { return } if w = conn.w; w == nil { - if _, w = conn.createConnection(); w == nil { + if _, w, err = conn.createConnection(); err != nil { return } } @@ -233,10 +231,11 @@ func (conn *Connection) writer() { } func (conn *Connection) reader() { - var r io.Reader + var r *bufio.Reader + var err error for { if r = conn.r; r == nil { - if r, _ = conn.createConnection(); r == nil { + if r, _, err = conn.createConnection(); err != nil { return } } @@ -245,7 +244,7 @@ func (conn *Connection) reader() { conn.closeConnection(err) continue } - resp := Response{buf:smallBuf{b:resp_bytes}} + resp := Response{buf: smallBuf{b: resp_bytes}} err = resp.decodeHeader() //resp, err := newResponse(resp_bytes) if err != nil { @@ -265,8 +264,8 @@ func (conn *Connection) reader() { } } -func write(connection io.Writer, data []byte) (err error) { - l, err := connection.Write(data) +func write(w io.Writer, data []byte) (err error) { + l, err := w.Write(data) if err != nil { return } diff --git a/request.go b/request.go index 7d3edaf72..9ee67beed 100644 --- a/request.go +++ b/request.go @@ -145,22 +145,6 @@ func (conn *Connection) CallAsync(functionName string, tuple []interface{}) *Fut return request.future() } -// -// To be implemented -// -func (conn *Connection) Auth(user string, tuple []interface{}) (resp *Response, err error) { - // This request does not make sense - // It should be a part of connecting process: - // * phisical connection - tcp - // * logical connection - authentication - // Functionality implemented in Connection.dial() - request := conn.NewRequest(AuthRequest) - request.body[KeyUserName] = user - request.body[KeyTuple] = tuple - resp, err = request.perform() - return -} - // // private // @@ -208,10 +192,10 @@ func (req *Request) future() (f *Future) { } // check connection ready to process packets - if f.conn.connection == nil { + if c := f.conn.c; c == nil { close(f.c) f.err = errors.New("client connection is not ready") - return // we shouldn't perform this request + return // we shouldn't perform this request } var packet []byte @@ -273,7 +257,7 @@ func (f *Future) Get() (*Response, error) { return &f.resp, f.err } -func (f *Future) GetTyped(r interface{}) (error) { +func (f *Future) GetTyped(r interface{}) error { f.wait() if f.err != nil { return f.err diff --git a/tarantool_test.go b/tarantool_test.go index bd3bda306..85887e028 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -19,13 +19,12 @@ var tuple1 = []interface{}{12, "Hello World", "Olga"} var tuple2 = []interface{}{12, "Hello Mars", "Anna"} var upd_tuple = []interface{}{[]interface{}{"=", 1, "Hello Moon"}, []interface{}{"#", 2, 1}} -var functionName = "box.cfg()" +var functionName = "box.info" var functionTuple = []interface{}{"box.schema.SPACE_ID"} var opts = Opts{Timeout: 500 * time.Millisecond} const N = 500 - func BenchmarkClientSerial(b *testing.B) { var err error From 2acef86e1b873d3a0f90fa07baa4a6a304787aec Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Mon, 14 Sep 2015 16:31:37 +0300 Subject: [PATCH 045/605] tarantool error codes --- const.go | 153 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 129 insertions(+), 24 deletions(-) diff --git a/const.go b/const.go index d55fb619e..a706b1a78 100644 --- a/const.go +++ b/const.go @@ -1,28 +1,28 @@ package tarantool const ( - SelectRequest = 1 - InsertRequest = 2 - ReplaceRequest = 3 - UpdateRequest = 4 - DeleteRequest = 5 - CallRequest = 6 - AuthRequest = 7 - PingRequest = 64 - SubscribeRequest = 66 + SelectRequest = 1 + InsertRequest = 2 + ReplaceRequest = 3 + UpdateRequest = 4 + DeleteRequest = 5 + CallRequest = 6 + AuthRequest = 7 + PingRequest = 64 + SubscribeRequest = 66 - KeyCode = 0x00 - KeySync = 0x01 - KeySpaceNo = 0x10 - KeyIndexNo = 0x11 - KeyLimit = 0x12 - KeyOffset = 0x13 - KeyIterator = 0x14 - KeyKey = 0x20 - KeyTuple = 0x21 - KeyFunctionName = 0x22 - KeyData = 0x30 - KeyError = 0x31 + KeyCode = 0x00 + KeySync = 0x01 + KeySpaceNo = 0x10 + KeyIndexNo = 0x11 + KeyLimit = 0x12 + KeyOffset = 0x13 + KeyIterator = 0x14 + KeyKey = 0x20 + KeyTuple = 0x21 + KeyFunctionName = 0x22 + KeyData = 0x30 + KeyError = 0x31 // https://github.com/fl00r/go-tarantool-1.6/issues/2 IterEq = uint32(0) // key == x ASC order @@ -37,8 +37,113 @@ const ( IterBitsAllNotSet = uint32(9) // all bits are not set OkCode = uint32(0) - NetErrCode = uint32(0xfffffff1) // fake code to wrap network problems into response - TimeoutErrCode = uint32(0xfffffff2) // fake code to wrap timeout error into repsonse - PacketLengthBytes = 5 ) + +// Tarantool server error codes +const ( + ErrUnknown = 0x8000 + iota // Unknown error + ErrIllegalParams = 0x8000 + iota // Illegal parameters, %s + ErrMemoryIssue = 0x8000 + iota // Failed to allocate %u bytes in %s for %s + ErrTupleFound = 0x8000 + iota // Duplicate key exists in unique index '%s' in space '%s' + ErrTupleNotFound = 0x8000 + iota // Tuple doesn't exist in index '%s' in space '%s' + ErrUnsupported = 0x8000 + iota // %s does not support %s + ErrNonmaster = 0x8000 + iota // Can't modify data on a replication slave. My master is: %s + ErrReadonly = 0x8000 + iota // Can't modify data because this server is in read-only mode. + ErrInjection = 0x8000 + iota // Error injection '%s' + ErrCreateSpace = 0x8000 + iota // Failed to create space '%s': %s + ErrSpaceExists = 0x8000 + iota // Space '%s' already exists + ErrDropSpace = 0x8000 + iota // Can't drop space '%s': %s + ErrAlterSpace = 0x8000 + iota // Can't modify space '%s': %s + ErrIndexType = 0x8000 + iota // Unsupported index type supplied for index '%s' in space '%s' + ErrModifyIndex = 0x8000 + iota // Can't create or modify index '%s' in space '%s': %s + ErrLastDrop = 0x8000 + iota // Can't drop the primary key in a system space, space '%s' + ErrTupleFormatLimit = 0x8000 + iota // Tuple format limit reached: %u + ErrDropPrimaryKey = 0x8000 + iota // Can't drop primary key in space '%s' while secondary keys exist + ErrKeyPartType = 0x8000 + iota // Supplied key type of part %u does not match index part type: expected %s + ErrExactMatch = 0x8000 + iota // Invalid key part count in an exact match (expected %u, got %u) + ErrInvalidMsgpack = 0x8000 + iota // Invalid MsgPack - %s + ErrProcRet = 0x8000 + iota // msgpack.encode: can not encode Lua type '%s' + ErrTupleNotArray = 0x8000 + iota // Tuple/Key must be MsgPack array + ErrFieldType = 0x8000 + iota // Tuple field %u type does not match one required by operation: expected %s + ErrFieldTypeMismatch = 0x8000 + iota // Ambiguous field type in index '%s', key part %u. Requested type is %s but the field has previously been defined as %s + ErrSplice = 0x8000 + iota // SPLICE error on field %u: %s + ErrArgType = 0x8000 + iota // Argument type in operation '%c' on field %u does not match field type: expected a %s + ErrTupleIsTooLong = 0x8000 + iota // Tuple is too long %u + ErrUnknownUpdateOp = 0x8000 + iota // Unknown UPDATE operation + ErrUpdateField = 0x8000 + iota // Field %u UPDATE error: %s + ErrFiberStack = 0x8000 + iota // Can not create a new fiber: recursion limit reached + ErrKeyPartCount = 0x8000 + iota // Invalid key part count (expected [0..%u], got %u) + ErrProcLua = 0x8000 + iota // %s + ErrNoSuchProc = 0x8000 + iota // Procedure '%.*s' is not defined + ErrNoSuchTrigger = 0x8000 + iota // Trigger is not found + ErrNoSuchIndex = 0x8000 + iota // No index #%u is defined in space '%s' + ErrNoSuchSpace = 0x8000 + iota // Space '%s' does not exist + ErrNoSuchField = 0x8000 + iota // Field %d was not found in the tuple + ErrSpaceFieldCount = 0x8000 + iota // Tuple field count %u does not match space '%s' field count %u + ErrIndexFieldCount = 0x8000 + iota // Tuple field count %u is less than required by a defined index (expected %u) + ErrWalIo = 0x8000 + iota // Failed to write to disk + ErrMoreThanOneTuple = 0x8000 + iota // More than one tuple found by get() + ErrAccessDenied = 0x8000 + iota // %s access denied for user '%s' + ErrCreateUser = 0x8000 + iota // Failed to create user '%s': %s + ErrDropUser = 0x8000 + iota // Failed to drop user '%s': %s + ErrNoSuchUser = 0x8000 + iota // User '%s' is not found + ErrUserExists = 0x8000 + iota // User '%s' already exists + ErrPasswordMismatch = 0x8000 + iota // Incorrect password supplied for user '%s' + ErrUnknownRequestType = 0x8000 + iota // Unknown request type %u + ErrUnknownSchemaObject = 0x8000 + iota // Unknown object type '%s' + ErrCreateFunction = 0x8000 + iota // Failed to create function '%s': %s + ErrNoSuchFunction = 0x8000 + iota // Function '%s' does not exist + ErrFunctionExists = 0x8000 + iota // Function '%s' already exists + ErrFunctionAccessDenied = 0x8000 + iota // %s access denied for user '%s' to function '%s' + ErrFunctionMax = 0x8000 + iota // A limit on the total number of functions has been reached: %u + ErrSpaceAccessDenied = 0x8000 + iota // %s access denied for user '%s' to space '%s' + ErrUserMax = 0x8000 + iota // A limit on the total number of users has been reached: %u + ErrNoSuchEngine = 0x8000 + iota // Space engine '%s' does not exist + ErrReloadCfg = 0x8000 + iota // Can't set option '%s' dynamically + ErrCfg = 0x8000 + iota // Incorrect value for option '%s': %s + ErrSophia = 0x8000 + iota // %s + ErrLocalServerIsNotActive = 0x8000 + iota // Local server is not active + ErrUnknownServer = 0x8000 + iota // Server %s is not registered with the cluster + ErrClusterIdMismatch = 0x8000 + iota // Cluster id of the replica %s doesn't match cluster id of the master %s + ErrInvalidUuid = 0x8000 + iota // Invalid UUID: %s + ErrClusterIdIsRo = 0x8000 + iota // Can't reset cluster id: it is already assigned + ErrReserved66 = 0x8000 + iota // Reserved66 + ErrServerIdIsReserved = 0x8000 + iota // Can't initialize server id with a reserved value %u + ErrInvalidOrder = 0x8000 + iota // Invalid LSN order for server %u: previous LSN = %llu, new lsn = %llu + ErrMissingRequestField = 0x8000 + iota // Missing mandatory field '%s' in request + ErrIdentifier = 0x8000 + iota // Invalid identifier '%s' (expected letters, digits or an underscore) + ErrDropFunction = 0x8000 + iota // Can't drop function %u: %s + ErrIteratorType = 0x8000 + iota // Unknown iterator type '%s' + ErrReplicaMax = 0x8000 + iota // Replica count limit reached: %u + ErrInvalidXlog = 0x8000 + iota // Failed to read xlog: %lld + ErrInvalidXlogName = 0x8000 + iota // Invalid xlog name: expected %lld got %lld + ErrInvalidXlogOrder = 0x8000 + iota // Invalid xlog order: %lld and %lld + ErrNoConnection = 0x8000 + iota // Connection is not established + ErrTimeout = 0x8000 + iota // Timeout exceeded + ErrActiveTransaction = 0x8000 + iota // Operation is not permitted when there is an active transaction + ErrNoActiveTransaction = 0x8000 + iota // Operation is not permitted when there is no active transaction + ErrCrossEngineTransaction = 0x8000 + iota // A multi-statement transaction can not use multiple storage engines + ErrNoSuchRole = 0x8000 + iota // Role '%s' is not found + ErrRoleExists = 0x8000 + iota // Role '%s' already exists + ErrCreateRole = 0x8000 + iota // Failed to create role '%s': %s + ErrIndexExists = 0x8000 + iota // Index '%s' already exists + ErrTupleRefOverflow = 0x8000 + iota // Tuple reference counter overflow + ErrRoleLoop = 0x8000 + iota // Granting role '%s' to role '%s' would create a loop + ErrGrant = 0x8000 + iota // Incorrect grant arguments: %s + ErrPrivGranted = 0x8000 + iota // User '%s' already has %s access on %s '%s' + ErrRoleGranted = 0x8000 + iota // User '%s' already has role '%s' + ErrPrivNotGranted = 0x8000 + iota // User '%s' does not have %s access on %s '%s' + ErrRoleNotGranted = 0x8000 + iota // User '%s' does not have role '%s' + ErrMissingSnapshot = 0x8000 + iota // Can't find snapshot + ErrCantUpdatePrimaryKey = 0x8000 + iota // Attempt to modify a tuple field which is part of index '%s' in space '%s' + ErrUpdateIntegerOverflow = 0x8000 + iota // Integer overflow when performing '%c' operation on field %u + ErrGuestUserPassword = 0x8000 + iota // Setting password for guest user has no effect + ErrTransactionConflict = 0x8000 + iota // Transaction has been aborted by conflict + ErrUnsupportedRolePriv = 0x8000 + iota // Unsupported role privilege '%s' + ErrLoadFunction = 0x8000 + iota // Failed to dynamically load function '%s': %s + ErrFunctionLanguage = 0x8000 + iota // Unsupported language '%s' specified for function '%s' + ErrRtreeRect = 0x8000 + iota // RTree: %s must be an array with %u (point) or %u (rectangle/box) numeric coordinates + ErrProcC = 0x8000 + iota // ??? + ErrUnknownRtreeIndexDistanceType = 0x8000 + iota //Unknown RTREE index distance type %s +) From b25414dfb060ee6ef8680099aba7042154b1ad4a Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Thu, 24 Sep 2015 13:18:29 +0300 Subject: [PATCH 046/605] eval command --- const.go | 2 ++ request.go | 15 +++++++++++++++ response.go | 8 ++------ tarantool_test.go | 7 +++++++ 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/const.go b/const.go index b80e3b43b..fbf6750f8 100644 --- a/const.go +++ b/const.go @@ -8,6 +8,7 @@ const ( DeleteRequest = 5 CallRequest = 6 AuthRequest = 7 + EvalRequest = 8 PingRequest = 64 SubscribeRequest = 66 @@ -22,6 +23,7 @@ const ( KeyTuple = 0x21 KeyFunctionName = 0x22 KeyUserName = 0x23 + KeyExpression = 0x27 KeyData = 0x30 KeyError = 0x31 diff --git a/request.go b/request.go index 9ee67beed..9705f2f15 100644 --- a/request.go +++ b/request.go @@ -106,6 +106,14 @@ func (conn *Connection) Call(functionName string, tuple []interface{}) (resp *Re return } +func (conn *Connection) Eval(expr string, tuple []interface{}) (resp *Response, err error) { + request := conn.NewRequest(EvalRequest) + request.body[KeyExpression] = expr + request.body[KeyTuple] = tuple + resp, err = request.perform() + return +} + func (conn *Connection) SelectAsync(spaceNo, indexNo, offset, limit, iterator uint32, key []interface{}) *Future { request := conn.NewRequest(SelectRequest) request.fillSearch(spaceNo, indexNo, key) @@ -145,6 +153,13 @@ func (conn *Connection) CallAsync(functionName string, tuple []interface{}) *Fut return request.future() } +func (conn *Connection) EvalAsync(expr string, tuple []interface{}) *Future { + request := conn.NewRequest(EvalRequest) + request.body[KeyExpression] = expr + request.body[KeyTuple] = tuple + return request.future() +} + // // private // diff --git a/response.go b/response.go index a440eb9da..bdeb8c73a 100644 --- a/response.go +++ b/response.go @@ -18,7 +18,7 @@ func (resp *Response) fill(b []byte) { } func newResponse(b []byte) (resp *Response, err error) { - resp = &Response{ buf: smallBuf{b: b} } + resp = &Response{buf: smallBuf{b: b}} err = resp.decodeHeader() return } @@ -58,11 +58,7 @@ func (resp *Response) decodeBody() (err error) { } if body[KeyData] != nil { - data := body[KeyData].([]interface{}) - resp.Data = make([]interface{}, len(data)) - for i, v := range data { - resp.Data[i] = v.([]interface{}) - } + resp.Data = body[KeyData].([]interface{}) } if body[KeyError] != nil { resp.Error = body[KeyError].(string) diff --git a/tarantool_test.go b/tarantool_test.go index 85887e028..f8c1ea537 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -342,4 +342,11 @@ func TestClient(t *testing.T) { fmt.Println("Code", resp.Code) fmt.Println("Data", resp.Data) fmt.Println("----") + + resp, err = client.Eval("return 5 + 6", []interface{}{}) + fmt.Println("Eval") + fmt.Println("ERROR", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("----") } From 7d3612632cb6e87877ff58e40eb8a07a17f12812 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Thu, 24 Sep 2015 15:48:59 +0300 Subject: [PATCH 047/605] Tuples() method for response always returns table --- response.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/response.go b/response.go index bdeb8c73a..4d9bbb595 100644 --- a/response.go +++ b/response.go @@ -110,3 +110,16 @@ func (resp *Response) String() (str string) { return fmt.Sprintf("<%d ERR 0x%x %s>", resp.RequestId, resp.Code, resp.Error) } } + +func (resp *Response) Tuples() (res [][]interface{}) { + res = make([][]interface{}, len(resp.Data)) + for i, t := range resp.Data { + switch t := t.(type) { + case []interface{}: + res[i] = t + default: + res[i] = []interface{}{t} + } + } + return res +} From 0cdf3a06a6beb4c8727fa0eb50e3fd69d165f8e0 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Thu, 24 Sep 2015 15:50:13 +0300 Subject: [PATCH 048/605] +test --- tarantool_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tarantool_test.go b/tarantool_test.go index f8c1ea537..fbffc9566 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -276,6 +276,7 @@ func TestClient(t *testing.T) { fmt.Println("ERROR", err) fmt.Println("Code", resp.Code) fmt.Println("Data", resp.Data) + fmt.Println("Tuples", resp.Tuples()) fmt.Println("----") var tpl []tuple @@ -348,5 +349,6 @@ func TestClient(t *testing.T) { fmt.Println("ERROR", err) fmt.Println("Code", resp.Code) fmt.Println("Data", resp.Data) + fmt.Println("Tuples", resp.Tuples()) fmt.Println("----") } From 4cb76a582fdf73049d36c8de158f6e6f923bb88c Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Wed, 14 Oct 2015 14:18:25 +0300 Subject: [PATCH 049/605] error message changed for timeouts --- request.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/request.go b/request.go index 9705f2f15..806ef5f3d 100644 --- a/request.go +++ b/request.go @@ -2,6 +2,7 @@ package tarantool import ( "errors" + "fmt" "gopkg.in/vmihailenco/msgpack.v2" "time" ) @@ -249,7 +250,7 @@ func (f *Future) wait() { if _, ok := f.conn.requests[f.id]; ok { delete(f.conn.requests, f.id) close(f.c) - f.err = errors.New("client timeout") + f.err = fmt.Errorf("client timeout for request %d", f.id) } f.conn.mutex.Unlock() } From 784df418f80c7b8ece07f66abe85ad40a963188c Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Tue, 20 Oct 2015 15:54:48 +0300 Subject: [PATCH 050/605] reconnect fix --- connection.go | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/connection.go b/connection.go index 118364eb6..aef34e5e6 100644 --- a/connection.go +++ b/connection.go @@ -53,15 +53,19 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { opts: opts, } + var reconnect time.Duration + // disable reconnecting for first connect + reconnect, conn.opts.Reconnect = conn.opts.Reconnect, 0 _, _, err = conn.createConnection() - if err != nil { + conn.opts.Reconnect = reconnect + if err != nil && reconnect == 0 { return nil, err } go conn.writer() go conn.reader() - return + return conn, err } func (conn *Connection) Close() (err error) { @@ -161,21 +165,25 @@ func (conn *Connection) createConnection() (r *bufio.Reader, w *bufio.Writer, er err = errors.New("connection already closed") return } - var reconnects uint - for { - err = conn.dial() - if err == nil { - break - } else if conn.opts.Reconnect > 0 { - if conn.opts.MaxReconnects > 0 && reconnects > conn.opts.MaxReconnects { - return + if conn.c == nil { + var reconnects uint + for { + err = conn.dial() + if err == nil { + break + } else if conn.opts.Reconnect > 0 { + if conn.opts.MaxReconnects > 0 && reconnects > conn.opts.MaxReconnects { + log.Printf("tarantool: last reconnect to %s failed: %s, giving it up.\n", conn.addr, err.Error()) + return + } else { + log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s\n", reconnects, conn.opts.MaxReconnects, conn.addr, err.Error()) + reconnects += 1 + time.Sleep(conn.opts.Reconnect) + continue + } } else { - reconnects += 1 - time.Sleep(conn.opts.Reconnect) - continue + return } - } else { - return } } r = conn.r From 432de6af407a67ec8f85a5f29eb434dd61fb520c Mon Sep 17 00:00:00 2001 From: Andrey Oleynik Date: Thu, 26 Nov 2015 16:06:41 +0100 Subject: [PATCH 051/605] added upsert --- const.go | 9 +++++++++ request.go | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/const.go b/const.go index fbf6750f8..2b798daea 100644 --- a/const.go +++ b/const.go @@ -9,6 +9,7 @@ const ( CallRequest = 6 AuthRequest = 7 EvalRequest = 8 + UpsertRequest = 9 PingRequest = 64 SubscribeRequest = 66 @@ -24,6 +25,7 @@ const ( KeyFunctionName = 0x22 KeyUserName = 0x23 KeyExpression = 0x27 + KeyDefTuple = 0x28 KeyData = 0x30 KeyError = 0x31 @@ -149,4 +151,11 @@ const ( ErrRtreeRect = 0x8000 + iota // RTree: %s must be an array with %u (point) or %u (rectangle/box) numeric coordinates ErrProcC = 0x8000 + iota // ??? ErrUnknownRtreeIndexDistanceType = 0x8000 + iota //Unknown RTREE index distance type %s + ErrProtocol = 0x8000 + iota // %s + ErrUpsertUniqueSecondaryKey = 0x8000 + iota // Space %s has a unique secondary index and does not support UPSERT + ErrWrongIndexRecord = 0x8000 + iota // Wrong record in _index space: got {%s}, expected {%s} + ErrWrongIndexParts = 0x8000 + iota // Wrong index parts (field %u): %s; expected field1 id (number), field1 type (string), ... + ErrWrongIndexOptions = 0x8000 + iota // Wrong index options (field %u): %s + ErrWrongSchemaVaersion = 0x8000 + iota // Wrong schema version, current: %d, in request: %u + ErrSlabAllocMax = 0x8000 + iota // Failed to allocate %u bytes for tuple in the slab allocator: tuple is too large. Check 'slab_alloc_maximal' configuration option. ) diff --git a/request.go b/request.go index 806ef5f3d..3f8be7e33 100644 --- a/request.go +++ b/request.go @@ -99,6 +99,15 @@ func (conn *Connection) Update(spaceNo, indexNo uint32, key, tuple []interface{} return } +func (conn *Connection) Upsert(spaceNo, indexNo uint32, key, tuple, def_tuple []interface{}) (resp *Response, err error) { + request := conn.NewRequest(UpsertRequest) + request.fillSearch(spaceNo, indexNo, key) + request.body[KeyTuple] = tuple + request.body[KeyDefTuple] = def_tuple + resp, err = request.perform() + return +} + func (conn *Connection) Call(functionName string, tuple []interface{}) (resp *Response, err error) { request := conn.NewRequest(CallRequest) request.body[KeyFunctionName] = functionName @@ -147,6 +156,14 @@ func (conn *Connection) UpdateAsync(spaceNo, indexNo uint32, key, tuple []interf return request.future() } +func (conn *Connection) UpsertAsync(spaceNo, indexNo uint32, key, tuple, def_tuple []interface{}) *Future { + request := conn.NewRequest(UpsertRequest) + request.fillSearch(spaceNo, indexNo, key) + request.body[KeyTuple] = tuple + request.body[KeyDefTuple] = def_tuple + return request.future() +} + func (conn *Connection) CallAsync(functionName string, tuple []interface{}) *Future { request := conn.NewRequest(CallRequest) request.body[KeyFunctionName] = functionName From 5410f290fbf31a1580759ae194fe39174d0b10d0 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Sun, 29 Nov 2015 00:21:14 +0300 Subject: [PATCH 052/605] response header parsing fix --- response.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/response.go b/response.go index 4d9bbb595..103afae50 100644 --- a/response.go +++ b/response.go @@ -43,6 +43,10 @@ func (resp *Response) decodeHeader() (err error) { if resp.Code, err = d.DecodeUint32(); err != nil { return } + default: + if _, err = d.DecodeInterface(); err != nil { + return + } } } return nil From 8978ac81c52b87fdb5544a0f08fd2f20932443fc Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Mon, 30 Nov 2015 00:12:37 +0300 Subject: [PATCH 053/605] bugfix: extra map fields should be skipped --- response.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/response.go b/response.go index 103afae50..d2c12d304 100644 --- a/response.go +++ b/response.go @@ -56,18 +56,15 @@ func (resp *Response) decodeBody() (err error) { if resp.buf.Len() > 2 { var body map[int]interface{} d := msgpack.NewDecoder(&resp.buf) - if err = d.Decode(&body); err != nil { return err } - if body[KeyData] != nil { resp.Data = body[KeyData].([]interface{}) } if body[KeyError] != nil { resp.Error = body[KeyError].(string) } - if resp.Code != OkCode { err = Error{resp.Code, resp.Error} } @@ -82,7 +79,6 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if l, err = d.DecodeMapLen(); err != nil { return err } - for ; l > 0; l-- { var cd int if cd, err = d.DecodeInt(); err != nil { @@ -97,9 +93,12 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if resp.Error, err = d.DecodeString(); err != nil { return err } + default: + if _, err = d.DecodeInterface(); err != nil { + return err + } } } - if resp.Code != OkCode { err = Error{resp.Code, resp.Error} } From 3619d1ff213c0af61b1dbeb3e382873e59fca32b Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Mon, 30 Nov 2015 00:13:31 +0300 Subject: [PATCH 054/605] correct upsert interface --- const.go | 14 +++++++------- request.go | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/const.go b/const.go index 2b798daea..c9ebf089f 100644 --- a/const.go +++ b/const.go @@ -151,11 +151,11 @@ const ( ErrRtreeRect = 0x8000 + iota // RTree: %s must be an array with %u (point) or %u (rectangle/box) numeric coordinates ErrProcC = 0x8000 + iota // ??? ErrUnknownRtreeIndexDistanceType = 0x8000 + iota //Unknown RTREE index distance type %s - ErrProtocol = 0x8000 + iota // %s - ErrUpsertUniqueSecondaryKey = 0x8000 + iota // Space %s has a unique secondary index and does not support UPSERT - ErrWrongIndexRecord = 0x8000 + iota // Wrong record in _index space: got {%s}, expected {%s} - ErrWrongIndexParts = 0x8000 + iota // Wrong index parts (field %u): %s; expected field1 id (number), field1 type (string), ... - ErrWrongIndexOptions = 0x8000 + iota // Wrong index options (field %u): %s - ErrWrongSchemaVaersion = 0x8000 + iota // Wrong schema version, current: %d, in request: %u - ErrSlabAllocMax = 0x8000 + iota // Failed to allocate %u bytes for tuple in the slab allocator: tuple is too large. Check 'slab_alloc_maximal' configuration option. + ErrProtocol = 0x8000 + iota // %s + ErrUpsertUniqueSecondaryKey = 0x8000 + iota // Space %s has a unique secondary index and does not support UPSERT + ErrWrongIndexRecord = 0x8000 + iota // Wrong record in _index space: got {%s}, expected {%s} + ErrWrongIndexParts = 0x8000 + iota // Wrong index parts (field %u): %s; expected field1 id (number), field1 type (string), ... + ErrWrongIndexOptions = 0x8000 + iota // Wrong index options (field %u): %s + ErrWrongSchemaVaersion = 0x8000 + iota // Wrong schema version, current: %d, in request: %u + ErrSlabAllocMax = 0x8000 + iota // Failed to allocate %u bytes for tuple in the slab allocator: tuple is too large. Check 'slab_alloc_maximal' configuration option. ) diff --git a/request.go b/request.go index 3f8be7e33..6487d7df2 100644 --- a/request.go +++ b/request.go @@ -91,19 +91,19 @@ func (conn *Connection) Delete(spaceNo, indexNo uint32, key []interface{}) (resp return } -func (conn *Connection) Update(spaceNo, indexNo uint32, key, tuple []interface{}) (resp *Response, err error) { +func (conn *Connection) Update(spaceNo, indexNo uint32, key, ops []interface{}) (resp *Response, err error) { request := conn.NewRequest(UpdateRequest) request.fillSearch(spaceNo, indexNo, key) - request.body[KeyTuple] = tuple + request.body[KeyTuple] = ops resp, err = request.perform() return } -func (conn *Connection) Upsert(spaceNo, indexNo uint32, key, tuple, def_tuple []interface{}) (resp *Response, err error) { +func (conn *Connection) Upsert(spaceNo uint32, tuple, ops []interface{}) (resp *Response, err error) { request := conn.NewRequest(UpsertRequest) - request.fillSearch(spaceNo, indexNo, key) + request.body[KeySpaceNo] = spaceNo request.body[KeyTuple] = tuple - request.body[KeyDefTuple] = def_tuple + request.body[KeyDefTuple] = ops resp, err = request.perform() return } @@ -149,18 +149,18 @@ func (conn *Connection) DeleteAsync(spaceNo, indexNo uint32, key []interface{}) return request.future() } -func (conn *Connection) UpdateAsync(spaceNo, indexNo uint32, key, tuple []interface{}) *Future { +func (conn *Connection) UpdateAsync(spaceNo, indexNo uint32, key, ops []interface{}) *Future { request := conn.NewRequest(UpdateRequest) request.fillSearch(spaceNo, indexNo, key) - request.body[KeyTuple] = tuple + request.body[KeyTuple] = ops return request.future() } -func (conn *Connection) UpsertAsync(spaceNo, indexNo uint32, key, tuple, def_tuple []interface{}) *Future { +func (conn *Connection) UpsertAsync(spaceNo uint32, tuple, ops []interface{}) *Future { request := conn.NewRequest(UpsertRequest) - request.fillSearch(spaceNo, indexNo, key) + request.body[KeySpaceNo] = spaceNo request.body[KeyTuple] = tuple - request.body[KeyDefTuple] = def_tuple + request.body[KeyDefTuple] = ops return request.future() } From 58b3c28290ed361f52f1ad3b452bb861986329f0 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Mon, 30 Nov 2015 00:14:13 +0300 Subject: [PATCH 055/605] tests --- config.lua | 5 +- tarantool_test.go | 404 +++++++++++++++++++++++++++++++--------------- 2 files changed, 276 insertions(+), 133 deletions(-) diff --git a/config.lua b/config.lua index 1813e6256..ba5385028 100644 --- a/config.lua +++ b/config.lua @@ -5,8 +5,9 @@ box.cfg{ } local s = box.schema.space.create('test', {if_not_exists = true}) local i = s:create_index('primary', {type = 'hash', parts = {1, 'NUM'}, if_not_exists = true}) +s:truncate() -box.schema.user.grant('guest', 'read,write,execute', 'universe') +--box.schema.user.grant('guest', 'read,write,execute', 'universe') -- auth testing: access control if not box.schema.user.exists('test') then @@ -17,5 +18,5 @@ end local console = require 'console' console.listen '0.0.0.0:33015' -box.schema.user.revoke('guest', 'read,write,execute', 'universe') +--box.schema.user.revoke('guest', 'read,write,execute', 'universe') diff --git a/tarantool_test.go b/tarantool_test.go index fbffc9566..129186176 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -8,19 +8,19 @@ import ( "time" ) +type tuple struct { + Id int + Msg string + Name string +} + +func init() { + msgpack.Register(reflect.TypeOf(new(tuple)).Elem(), encodeTuple, decodeTuple) +} + var server = "127.0.0.1:3013" var spaceNo = uint32(512) var indexNo = uint32(0) -var limit = uint32(10) -var offset = uint32(0) -var iterator = IterAll -var key = []interface{}{12} -var tuple1 = []interface{}{12, "Hello World", "Olga"} -var tuple2 = []interface{}{12, "Hello Mars", "Anna"} -var upd_tuple = []interface{}{[]interface{}{"=", 1, "Hello Moon"}, []interface{}{"#", 2, 1}} - -var functionName = "box.info" -var functionTuple = []interface{}{"box.schema.SPACE_ID"} var opts = Opts{Timeout: 500 * time.Millisecond} const N = 500 @@ -28,18 +28,18 @@ const N = 500 func BenchmarkClientSerial(b *testing.B) { var err error - client, err := Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") } - _, err = client.Replace(spaceNo, tuple1) + _, err = conn.Replace(spaceNo, []interface{}{1, "hello", "world"}) if err != nil { b.Errorf("No connection available") } for i := 0; i < b.N; i++ { - _, err = client.Select(spaceNo, indexNo, offset, limit, iterator, key) + _, err = conn.Select(spaceNo, indexNo, 0, 1, IterAll, []interface{}{1}) if err != nil { b.Errorf("No connection available") } @@ -50,12 +50,12 @@ func BenchmarkClientSerial(b *testing.B) { func BenchmarkClientFuture(b *testing.B) { var err error - client, err := Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { b.Error(err) } - _, err = client.Replace(spaceNo, tuple1) + _, err = conn.Replace(spaceNo, []interface{}{1, "hello", "world"}) if err != nil { b.Error(err) } @@ -63,7 +63,7 @@ func BenchmarkClientFuture(b *testing.B) { for i := 0; i < b.N; i += N { var fs [N]*Future for j := 0; j < N; j++ { - fs[j] = client.SelectAsync(spaceNo, indexNo, offset, limit, iterator, key) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{1}) } for j := 0; j < N; j++ { _, err = fs[j].Get() @@ -75,12 +75,6 @@ func BenchmarkClientFuture(b *testing.B) { } } -type tuple struct { - Id int - Msg string - Name string -} - func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { t := v.Interface().(tuple) if err := e.EncodeSliceLen(3); err != nil { @@ -120,19 +114,15 @@ func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { return nil } -func init() { - msgpack.Register(reflect.TypeOf(new(tuple)).Elem(), encodeTuple, decodeTuple) -} - func BenchmarkClientFutureTyped(b *testing.B) { var err error - client, err := Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") } - _, err = client.Replace(spaceNo, tuple1) + _, err = conn.Replace(spaceNo, []interface{}{1, "hello", "world"}) if err != nil { b.Errorf("No connection available") } @@ -140,7 +130,7 @@ func BenchmarkClientFutureTyped(b *testing.B) { for i := 0; i < b.N; i += N { var fs [N]*Future for j := 0; j < N; j++ { - fs[j] = client.SelectAsync(spaceNo, indexNo, offset, limit, iterator, key) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{1}) } for j := 0; j < N; j++ { var r []tuple @@ -159,12 +149,12 @@ func BenchmarkClientFutureTyped(b *testing.B) { func BenchmarkClientFutureParallel(b *testing.B) { var err error - client, err := Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") } - _, err = client.Replace(spaceNo, tuple1) + _, err = conn.Replace(spaceNo, []interface{}{1, "hello", "world"}) if err != nil { b.Errorf("No connection available") } @@ -175,7 +165,7 @@ func BenchmarkClientFutureParallel(b *testing.B) { var fs [N]*Future var j int for j = 0; j < N && pb.Next(); j++ { - fs[j] = client.SelectAsync(spaceNo, indexNo, offset, limit, iterator, key) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{1}) } exit = j < N for j > 0 { @@ -192,12 +182,12 @@ func BenchmarkClientFutureParallel(b *testing.B) { func BenchmarkClientFutureParallelTyped(b *testing.B) { var err error - client, err := Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") } - _, err = client.Replace(spaceNo, tuple1) + _, err = conn.Replace(spaceNo, []interface{}{1, "hello", "world"}) if err != nil { b.Errorf("No connection available") } @@ -208,7 +198,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { var fs [N]*Future var j int for j = 0; j < N && pb.Next(); j++ { - fs[j] = client.SelectAsync(spaceNo, indexNo, offset, limit, iterator, key) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{1}) } exit = j < N for j > 0 { @@ -227,19 +217,19 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { } func BenchmarkClientParrallel(b *testing.B) { - client, err := Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") } - _, err = client.Replace(spaceNo, tuple1) + _, err = conn.Replace(spaceNo, []interface{}{1, "hello", "world"}) if err != nil { b.Errorf("No connection available") } b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _, err = client.Select(spaceNo, indexNo, offset, limit, iterator, key) + _, err = conn.Select(spaceNo, indexNo, 0, 1, IterAll, []interface{}{1}) if err != nil { b.Errorf("No connection available") } @@ -247,108 +237,260 @@ func BenchmarkClientParrallel(b *testing.B) { }) } +/////////////////// + func TestClient(t *testing.T) { + var resp *Response + var err error + var conn *Connection - // Valid user/pass - client, err := Connect(server, Opts{User: "test", Pass: "test"}) + conn, err = Connect(server, Opts{User: "test", Pass: "test"}) if err != nil { - t.Errorf("Should pass but error is [%s]", err.Error()) + t.Errorf("Failed to connect: %s", err.Error()) + } + if conn == nil { + t.Errorf("conn is nil after Connect") } - var resp *Response + // Ping + resp, err = conn.Ping() + if err != nil { + t.Errorf("Failed to Ping: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Ping") + } - resp, err = client.Ping() - fmt.Println("Ping") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Insert(spaceNo, tuple1) - fmt.Println("Insert") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Select(spaceNo, indexNo, offset, limit, iterator, key) - fmt.Println("Select") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("Tuples", resp.Tuples()) - fmt.Println("----") + // Insert + resp, err = conn.Insert(spaceNo, []interface{}{1, "hello", "world"}) + if err != nil { + t.Errorf("Failed to Insert: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Insert") + } + if len(resp.Data) != 1 { + t.Errorf("Response Body len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Insert") + } else { + if len(tpl) != 3 { + t.Errorf("Unexpected body of Insert (tuple len)") + } + if id, ok := tpl[0].(int64); !ok || id != 1 { + t.Errorf("Unexpected body of Insert (0)") + } + if h, ok := tpl[1].(string); !ok || h != "hello" { + t.Errorf("Unexpected body of Insert (1)") + } + } + resp, err = conn.Insert(spaceNo, []interface{}{1, "hello", "world"}) + if tntErr, ok := err.(Error); !ok || tntErr.Code != ErrTupleFound { + t.Errorf("Expected ErrTupleFound but got: %v", err) + } + if len(resp.Data) != 0 { + t.Errorf("Response Body len != 1") + } - var tpl []tuple - err = client.SelectTyped(spaceNo, indexNo, offset, limit, iterator, key, &tpl) - fmt.Println("GetTyped") - fmt.Println("ERROR", err) - fmt.Println("Value", tpl) - fmt.Println("----") - - resp, err = client.Replace(spaceNo, tuple2) - fmt.Println("Replace") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Select(spaceNo, indexNo, offset, limit, iterator, key) - fmt.Println("Select") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Update(spaceNo, indexNo, key, upd_tuple) - fmt.Println("Update") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Select(spaceNo, indexNo, offset, limit, iterator, key) - fmt.Println("Select") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - responses := make(chan *Response) - cnt1 := 50 - cnt2 := 500 - for j := 0; j < cnt1; j++ { - for i := 0; i < cnt2; i++ { - go func() { - resp, err = client.Select(spaceNo, indexNo, offset, limit, iterator, key) - responses <- resp - }() + // Delete + resp, err = conn.Delete(spaceNo, indexNo, []interface{}{1}) + if err != nil { + t.Errorf("Failed to Delete: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Delete") + } + if len(resp.Data) != 1 { + t.Errorf("Response Body len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Delete") + } else { + if len(tpl) != 3 { + t.Errorf("Unexpected body of Delete (tuple len)") } - for i := 0; i < cnt2; i++ { - resp = <-responses - // fmt.Println(resp) + if id, ok := tpl[0].(int64); !ok || id != 1 { + t.Errorf("Unexpected body of Delete (0)") } + if h, ok := tpl[1].(string); !ok || h != "hello" { + t.Errorf("Unexpected body of Delete (1)") + } + } + resp, err = conn.Delete(spaceNo, indexNo, []interface{}{101}) + if err != nil { + t.Errorf("Failed to Replace: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Delete") + } + if len(resp.Data) != 0 { + t.Errorf("Response Data len != 0") + } + + // Replace + resp, err = conn.Replace(spaceNo, []interface{}{2, "hello", "world"}) + if err != nil { + t.Errorf("Failed to Replace: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Replace") + } + resp, err = conn.Replace(spaceNo, []interface{}{2, "hi", "planet"}) + if err != nil { + t.Errorf("Failed to Replace (duplicate): %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Replace (duplicate)") + } + if len(resp.Data) != 1 { + t.Errorf("Response Data len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Replace") + } else { + if len(tpl) != 3 { + t.Errorf("Unexpected body of Replace (tuple len)") + } + if id, ok := tpl[0].(int64); !ok || id != 2 { + t.Errorf("Unexpected body of Replace (0)") + } + if h, ok := tpl[1].(string); !ok || h != "hi" { + t.Errorf("Unexpected body of Replace (1)") + } + } + + // Update + resp, err = conn.Update(spaceNo, indexNo, []interface{}{2}, []interface{}{[]interface{}{"=", 1, "bye"}, []interface{}{"#", 2, 1}}) + if err != nil { + t.Errorf("Failed to Update: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Update") + } + if len(resp.Data) != 1 { + t.Errorf("Response Data len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Update") + } else { + if len(tpl) != 2 { + t.Errorf("Unexpected body of Update (tuple len)") + } + if id, ok := tpl[0].(int64); !ok || id != 2 { + t.Errorf("Unexpected body of Update (0)") + } + if h, ok := tpl[1].(string); !ok || h != "bye" { + t.Errorf("Unexpected body of Update (1)") + } + } + + // Upsert + resp, err = conn.Upsert(spaceNo, []interface{}{3, 1}, []interface{}{[]interface{}{"+", 1, 1}}) + if err != nil { + t.Errorf("Failed to Upsert (insert): %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Upsert (insert)") + } + resp, err = conn.Upsert(spaceNo, []interface{}{3, 1}, []interface{}{[]interface{}{"+", 1, 1}}) + if err != nil { + t.Errorf("Failed to Upsert (update): %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Upsert (update)") + } + + // Select + for i := 10; i < 20; i++ { + resp, err = conn.Replace(spaceNo, []interface{}{i, fmt.Sprintf("val %d", i), "bla"}) + if err != nil { + t.Errorf("Failed to Replace: %s", err.Error()) + } + } + resp, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{10}) + if err != nil { + t.Errorf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Select") + } + if len(resp.Data) != 1 { + t.Errorf("Response Data len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Select") + } else { + if id, ok := tpl[0].(int64); !ok || id != 10 { + t.Errorf("Unexpected body of Select (0)") + } + if h, ok := tpl[1].(string); !ok || h != "val 10" { + t.Errorf("Unexpected body of Select (1)") + } + } + + // Select empty + resp, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{30}) + if err != nil { + t.Errorf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Select") + } + if len(resp.Data) != 0 { + t.Errorf("Response Data len != 0") + } + + // Select Typed + var tpl []tuple + err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{10}, &tpl) + if err != nil { + t.Errorf("Failed to SelectTyped: %s", err.Error()) + } + if len(tpl) != 1 { + t.Errorf("Result len of SelectTyped != 1") + } else { + if tpl[0].Id != 10 { + t.Errorf("Bad value loaded from SelectTyped") + } + } + + // Select Typed Empty + var tpl2 []tuple + err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{30}, &tpl2) + if err != nil { + t.Errorf("Failed to SelectTyped: %s", err.Error()) + } + if len(tpl2) != 0 { + t.Errorf("Result len of SelectTyped != 1") + } + + // Call + resp, err = conn.Call("box.info", []interface{}{"box.schema.SPACE_ID"}) + if err != nil { + t.Errorf("Failed to Call: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Call") + } + if len(resp.Data) < 1 { + t.Errorf("Response.Data is empty after Eval") + } + + // Eval + resp, err = conn.Eval("return 5 + 6", []interface{}{}) + if err != nil { + t.Errorf("Failed to Eval: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Eval") + } + if len(resp.Data) < 1 { + t.Errorf("Response.Data is empty after Eval") + } + val := resp.Data[0].(int64) + if val != 11 { + t.Errorf("5 + 6 == 11, but got %v", val) } - resp, err = client.Delete(spaceNo, indexNo, key) - fmt.Println("Delete") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Call(functionName, functionTuple) - fmt.Println("Call") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("----") - - resp, err = client.Eval("return 5 + 6", []interface{}{}) - fmt.Println("Eval") - fmt.Println("ERROR", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("Tuples", resp.Tuples()) - fmt.Println("----") } From 113e637dd2552818fff0b0e0a64fc055c08683f5 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Mon, 30 Nov 2015 00:46:12 +0300 Subject: [PATCH 056/605] upsert documentation --- .gitignore | 2 ++ README.md | 47 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..535ca4a3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +snap +xlog diff --git a/README.md b/README.md index 854f3e4d8..14df12fc4 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,6 @@ import ( func main() { spaceNo := uint32(512) indexNo := uint32(0) - tuple1 := []interface{}{12, "Hello World", "Olga"} - tuple2 := []interface{}{13, "99 bugs in code", "Anna"} server := "127.0.0.1:3013" opts := tarantool.Opts{ @@ -37,35 +35,68 @@ func main() { log.Println(resp.Data) log.Println(err) - resp, err = client.Insert(spaceNo, tuple1) + // insert new tuple { 10, 1 } + resp, err = client.Insert(spaceNo, []interface{}{10, 1}) log.Println("Insert") log.Println("Error", err) log.Println("Code", resp.Code) log.Println("Data", resp.Data) - resp, err = client.Replace(spaceNo, tuple2) + // delete tuple with primary key { 10 } + resp, err = client.Delete(spaceNo, indexNo, []interface{}{10}) + log.Println("Delete") + log.Println("Error", err) + log.Println("Code", resp.Code) + log.Println("Data", resp.Data) + + // replace tuple with { 13, 1 } + resp, err = client.Replace(spaceNo, []interface{}{13, 1}) log.Println("Replace") log.Println("Error", err) log.Println("Code", resp.Code) log.Println("Data", resp.Data) - resp, err = client.Delete(spaceNo, indexNo, tuple1[0:1]) - log.Println("Delete") + // update tuple with primary key { 13 }, incrementing second field by 3 + resp, err = client.Update(spaceNo, indexNo, []interface{}{13}, []interface{}{[]interface{}{"+", 1, 3}}) + log.Println("Update") + log.Println("Error", err) + log.Println("Code", resp.Code) + log.Println("Data", resp.Data) + + // insert tuple {15, 1} or increment second field by 1 + resp, err = client.Upsert(spaceNo, []interface{}{15, 1}, []interface{}{[]interface{}{"+", 1, 1}}) + log.Println("Upsert") log.Println("Error", err) log.Println("Code", resp.Code) log.Println("Data", resp.Data) - resp, err = client.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, tuple2[0:1]) + // select just one tuple with primay key { 15 } + resp, err = client.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{15}) log.Println("Select") log.Println("Error", err) log.Println("Code", resp.Code) log.Println("Data", resp.Data) + // select tuples by condition ( primay key > 15 ) with offset 7 limit 5 + // BTREE index supposed + resp, err = client.Select(spaceNo, indexNo, 7, 5, tarantool.IterGt, []interface{}{15}) + log.Println("Select") + log.Println("Error", err) + log.Println("Code", resp.Code) + log.Println("Data", resp.Data) + + // call function 'func_name' with arguments resp, err = client.Call("func_name", []interface{}{1, 2, 3}) log.Println("Call") log.Println("Error", err) log.Println("Code", resp.Code) log.Println("Data", resp.Data) -} + // run raw lua code + resp, err = client.Eval("return 1 + 2", []interface{}{}) + log.Println("Eval") + log.Println("Error", err) + log.Println("Code", resp.Code) + log.Println("Data", resp.Data) +} ``` From ed05f5ac542464596856fe3bd018b4c6b83cd5fb Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Mon, 30 Nov 2015 00:59:41 +0300 Subject: [PATCH 057/605] Update README.md --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 14df12fc4..951da0355 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Tarantool -[Tarantool 1.6+](http://tarantool.org/) client on Go. +[Tarantool 1.6+](http://tarantool.org/) client in Go. ## Usage @@ -100,3 +100,10 @@ func main() { log.Println("Data", resp.Data) } ``` + +## Options +* Timeout - timeout for any particular request. If Timeout is zero request any request may block infinitely +* Reconnect - timeout for between reconnect attempts. If Reconnect is zero, no reconnects will be performed +* MaxReconnects - maximal number of reconnect failures after that we give it up +* User - user name to login tarantool +* Pass - user password to login tarantool From b0b36c95f8cd0d6f0a091036d03aabaf483b6faf Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Thu, 31 Dec 2015 00:01:20 +0300 Subject: [PATCH 058/605] docs and test fixed to workaround msgpack bug https://github.com/tarantool/go-tarantool/issues/9 --- README.md | 14 ++++++------ tarantool_test.go | 58 +++++++++++++++++++++++------------------------ 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 951da0355..971973c13 100644 --- a/README.md +++ b/README.md @@ -36,42 +36,42 @@ func main() { log.Println(err) // insert new tuple { 10, 1 } - resp, err = client.Insert(spaceNo, []interface{}{10, 1}) + resp, err = client.Insert(spaceNo, []interface{}{uint(10), 1}) log.Println("Insert") log.Println("Error", err) log.Println("Code", resp.Code) log.Println("Data", resp.Data) // delete tuple with primary key { 10 } - resp, err = client.Delete(spaceNo, indexNo, []interface{}{10}) + resp, err = client.Delete(spaceNo, indexNo, []interface{}{uint(10)}) log.Println("Delete") log.Println("Error", err) log.Println("Code", resp.Code) log.Println("Data", resp.Data) // replace tuple with { 13, 1 } - resp, err = client.Replace(spaceNo, []interface{}{13, 1}) + resp, err = client.Replace(spaceNo, []interface{}{uint(13), 1}) log.Println("Replace") log.Println("Error", err) log.Println("Code", resp.Code) log.Println("Data", resp.Data) // update tuple with primary key { 13 }, incrementing second field by 3 - resp, err = client.Update(spaceNo, indexNo, []interface{}{13}, []interface{}{[]interface{}{"+", 1, 3}}) + resp, err = client.Update(spaceNo, indexNo, []interface{}{uint(13)}, []interface{}{[]interface{}{"+", 1, 3}}) log.Println("Update") log.Println("Error", err) log.Println("Code", resp.Code) log.Println("Data", resp.Data) // insert tuple {15, 1} or increment second field by 1 - resp, err = client.Upsert(spaceNo, []interface{}{15, 1}, []interface{}{[]interface{}{"+", 1, 1}}) + resp, err = client.Upsert(spaceNo, []interface{}{uint(15), 1}, []interface{}{[]interface{}{"+", 1, 1}}) log.Println("Upsert") log.Println("Error", err) log.Println("Code", resp.Code) log.Println("Data", resp.Data) // select just one tuple with primay key { 15 } - resp, err = client.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{15}) + resp, err = client.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{uint(15)}) log.Println("Select") log.Println("Error", err) log.Println("Code", resp.Code) @@ -79,7 +79,7 @@ func main() { // select tuples by condition ( primay key > 15 ) with offset 7 limit 5 // BTREE index supposed - resp, err = client.Select(spaceNo, indexNo, 7, 5, tarantool.IterGt, []interface{}{15}) + resp, err = client.Select(spaceNo, indexNo, 7, 5, tarantool.IterGt, []interface{}{uint(15)}) log.Println("Select") log.Println("Error", err) log.Println("Code", resp.Code) diff --git a/tarantool_test.go b/tarantool_test.go index 129186176..e9efa4a84 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -9,7 +9,7 @@ import ( ) type tuple struct { - Id int + Id uint Msg string Name string } @@ -33,13 +33,13 @@ func BenchmarkClientSerial(b *testing.B) { b.Errorf("No connection available") } - _, err = conn.Replace(spaceNo, []interface{}{1, "hello", "world"}) + _, err = conn.Replace(spaceNo, []interface{}{uint(1), "hello", "world"}) if err != nil { b.Errorf("No connection available") } for i := 0; i < b.N; i++ { - _, err = conn.Select(spaceNo, indexNo, 0, 1, IterAll, []interface{}{1}) + _, err = conn.Select(spaceNo, indexNo, 0, 1, IterAll, []interface{}{uint(1)}) if err != nil { b.Errorf("No connection available") } @@ -55,7 +55,7 @@ func BenchmarkClientFuture(b *testing.B) { b.Error(err) } - _, err = conn.Replace(spaceNo, []interface{}{1, "hello", "world"}) + _, err = conn.Replace(spaceNo, []interface{}{uint(1), "hello", "world"}) if err != nil { b.Error(err) } @@ -63,7 +63,7 @@ func BenchmarkClientFuture(b *testing.B) { for i := 0; i < b.N; i += N { var fs [N]*Future for j := 0; j < N; j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{1}) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{uint(1)}) } for j := 0; j < N; j++ { _, err = fs[j].Get() @@ -80,7 +80,7 @@ func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { if err := e.EncodeSliceLen(3); err != nil { return err } - if err := e.EncodeInt(t.Id); err != nil { + if err := e.EncodeUint(t.Id); err != nil { return err } if err := e.EncodeString(t.Msg); err != nil { @@ -102,7 +102,7 @@ func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { if l != 3 { return fmt.Errorf("array len doesn't match: %d", l) } - if t.Id, err = d.DecodeInt(); err != nil { + if t.Id, err = d.DecodeUint(); err != nil { return err } if t.Msg, err = d.DecodeString(); err != nil { @@ -122,7 +122,7 @@ func BenchmarkClientFutureTyped(b *testing.B) { b.Errorf("No connection available") } - _, err = conn.Replace(spaceNo, []interface{}{1, "hello", "world"}) + _, err = conn.Replace(spaceNo, []interface{}{uint(1), "hello", "world"}) if err != nil { b.Errorf("No connection available") } @@ -130,7 +130,7 @@ func BenchmarkClientFutureTyped(b *testing.B) { for i := 0; i < b.N; i += N { var fs [N]*Future for j := 0; j < N; j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{1}) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{uint(1)}) } for j := 0; j < N; j++ { var r []tuple @@ -154,7 +154,7 @@ func BenchmarkClientFutureParallel(b *testing.B) { b.Errorf("No connection available") } - _, err = conn.Replace(spaceNo, []interface{}{1, "hello", "world"}) + _, err = conn.Replace(spaceNo, []interface{}{uint(1), "hello", "world"}) if err != nil { b.Errorf("No connection available") } @@ -165,7 +165,7 @@ func BenchmarkClientFutureParallel(b *testing.B) { var fs [N]*Future var j int for j = 0; j < N && pb.Next(); j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{1}) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{uint(1)}) } exit = j < N for j > 0 { @@ -187,7 +187,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { b.Errorf("No connection available") } - _, err = conn.Replace(spaceNo, []interface{}{1, "hello", "world"}) + _, err = conn.Replace(spaceNo, []interface{}{uint(1), "hello", "world"}) if err != nil { b.Errorf("No connection available") } @@ -198,7 +198,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { var fs [N]*Future var j int for j = 0; j < N && pb.Next(); j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{1}) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{uint(1)}) } exit = j < N for j > 0 { @@ -222,14 +222,14 @@ func BenchmarkClientParrallel(b *testing.B) { b.Errorf("No connection available") } - _, err = conn.Replace(spaceNo, []interface{}{1, "hello", "world"}) + _, err = conn.Replace(spaceNo, []interface{}{uint(1), "hello", "world"}) if err != nil { b.Errorf("No connection available") } b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _, err = conn.Select(spaceNo, indexNo, 0, 1, IterAll, []interface{}{1}) + _, err = conn.Select(spaceNo, indexNo, 0, 1, IterAll, []interface{}{uint(1)}) if err != nil { b.Errorf("No connection available") } @@ -262,7 +262,7 @@ func TestClient(t *testing.T) { } // Insert - resp, err = conn.Insert(spaceNo, []interface{}{1, "hello", "world"}) + resp, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) if err != nil { t.Errorf("Failed to Insert: %s", err.Error()) } @@ -285,7 +285,7 @@ func TestClient(t *testing.T) { t.Errorf("Unexpected body of Insert (1)") } } - resp, err = conn.Insert(spaceNo, []interface{}{1, "hello", "world"}) + resp, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) if tntErr, ok := err.(Error); !ok || tntErr.Code != ErrTupleFound { t.Errorf("Expected ErrTupleFound but got: %v", err) } @@ -294,7 +294,7 @@ func TestClient(t *testing.T) { } // Delete - resp, err = conn.Delete(spaceNo, indexNo, []interface{}{1}) + resp, err = conn.Delete(spaceNo, indexNo, []interface{}{uint(1)}) if err != nil { t.Errorf("Failed to Delete: %s", err.Error()) } @@ -317,7 +317,7 @@ func TestClient(t *testing.T) { t.Errorf("Unexpected body of Delete (1)") } } - resp, err = conn.Delete(spaceNo, indexNo, []interface{}{101}) + resp, err = conn.Delete(spaceNo, indexNo, []interface{}{uint(101)}) if err != nil { t.Errorf("Failed to Replace: %s", err.Error()) } @@ -329,14 +329,14 @@ func TestClient(t *testing.T) { } // Replace - resp, err = conn.Replace(spaceNo, []interface{}{2, "hello", "world"}) + resp, err = conn.Replace(spaceNo, []interface{}{uint(2), "hello", "world"}) if err != nil { t.Errorf("Failed to Replace: %s", err.Error()) } if resp == nil { t.Errorf("Response is nil after Replace") } - resp, err = conn.Replace(spaceNo, []interface{}{2, "hi", "planet"}) + resp, err = conn.Replace(spaceNo, []interface{}{uint(2), "hi", "planet"}) if err != nil { t.Errorf("Failed to Replace (duplicate): %s", err.Error()) } @@ -361,7 +361,7 @@ func TestClient(t *testing.T) { } // Update - resp, err = conn.Update(spaceNo, indexNo, []interface{}{2}, []interface{}{[]interface{}{"=", 1, "bye"}, []interface{}{"#", 2, 1}}) + resp, err = conn.Update(spaceNo, indexNo, []interface{}{uint(2)}, []interface{}{[]interface{}{"=", 1, "bye"}, []interface{}{"#", 2, 1}}) if err != nil { t.Errorf("Failed to Update: %s", err.Error()) } @@ -386,14 +386,14 @@ func TestClient(t *testing.T) { } // Upsert - resp, err = conn.Upsert(spaceNo, []interface{}{3, 1}, []interface{}{[]interface{}{"+", 1, 1}}) + resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) if err != nil { t.Errorf("Failed to Upsert (insert): %s", err.Error()) } if resp == nil { t.Errorf("Response is nil after Upsert (insert)") } - resp, err = conn.Upsert(spaceNo, []interface{}{3, 1}, []interface{}{[]interface{}{"+", 1, 1}}) + resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) if err != nil { t.Errorf("Failed to Upsert (update): %s", err.Error()) } @@ -403,12 +403,12 @@ func TestClient(t *testing.T) { // Select for i := 10; i < 20; i++ { - resp, err = conn.Replace(spaceNo, []interface{}{i, fmt.Sprintf("val %d", i), "bla"}) + resp, err = conn.Replace(spaceNo, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) if err != nil { t.Errorf("Failed to Replace: %s", err.Error()) } } - resp, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{10}) + resp, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}) if err != nil { t.Errorf("Failed to Select: %s", err.Error()) } @@ -430,7 +430,7 @@ func TestClient(t *testing.T) { } // Select empty - resp, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{30}) + resp, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}) if err != nil { t.Errorf("Failed to Select: %s", err.Error()) } @@ -443,7 +443,7 @@ func TestClient(t *testing.T) { // Select Typed var tpl []tuple - err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{10}, &tpl) + err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}, &tpl) if err != nil { t.Errorf("Failed to SelectTyped: %s", err.Error()) } @@ -457,7 +457,7 @@ func TestClient(t *testing.T) { // Select Typed Empty var tpl2 []tuple - err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{30}, &tpl2) + err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}, &tpl2) if err != nil { t.Errorf("Failed to SelectTyped: %s", err.Error()) } From 48738d04e1b7cca4a3e81fce4cc4028461423fb6 Mon Sep 17 00:00:00 2001 From: Dinar Sabitov Date: Thu, 14 Jan 2016 14:17:10 +0300 Subject: [PATCH 059/605] Connection methods LocalAddr and RemoteAddr --- connection.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/connection.go b/connection.go index aef34e5e6..c6183b0f0 100644 --- a/connection.go +++ b/connection.go @@ -75,6 +75,20 @@ func (conn *Connection) Close() (err error) { return } +func (conn *Connection) RemoteAddr() string { + if conn.c == nil { + return "" + } + return conn.c.RemoteAddr().String() +} + +func (conn *Connection) LocalAddr() string { + if conn.c == nil { + return "" + } + return conn.c.LocalAddr().String() +} + func (conn *Connection) dial() (err error) { connection, err := net.Dial("tcp", conn.addr) if err != nil { From ea1aef338a34195a6e964a34e76a2514f98a31ce Mon Sep 17 00:00:00 2001 From: Dinar Sabitov Date: Thu, 14 Jan 2016 14:35:16 +0300 Subject: [PATCH 060/605] Connection fix race condition --- connection.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/connection.go b/connection.go index c6183b0f0..c14aaac01 100644 --- a/connection.go +++ b/connection.go @@ -76,6 +76,8 @@ func (conn *Connection) Close() (err error) { } func (conn *Connection) RemoteAddr() string { + conn.mutex.Lock() + defer conn.mutex.Unlock() if conn.c == nil { return "" } @@ -83,6 +85,8 @@ func (conn *Connection) RemoteAddr() string { } func (conn *Connection) LocalAddr() string { + conn.mutex.Lock() + defer conn.mutex.Unlock() if conn.c == nil { return "" } From 15230c38870fc306ae31fced38e76cb4baad9b1a Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Fri, 5 Feb 2016 00:03:54 +0300 Subject: [PATCH 061/605] test fixed for new msgpack --- tarantool_test.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tarantool_test.go b/tarantool_test.go index e9efa4a84..2882787e9 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -278,7 +278,7 @@ func TestClient(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Insert (tuple len)") } - if id, ok := tpl[0].(int64); !ok || id != 1 { + if id, ok := tpl[0].(uint64); !ok || id != 1 { t.Errorf("Unexpected body of Insert (0)") } if h, ok := tpl[1].(string); !ok || h != "hello" { @@ -310,7 +310,7 @@ func TestClient(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Delete (tuple len)") } - if id, ok := tpl[0].(int64); !ok || id != 1 { + if id, ok := tpl[0].(uint64); !ok || id != 1 { t.Errorf("Unexpected body of Delete (0)") } if h, ok := tpl[1].(string); !ok || h != "hello" { @@ -352,7 +352,7 @@ func TestClient(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Replace (tuple len)") } - if id, ok := tpl[0].(int64); !ok || id != 2 { + if id, ok := tpl[0].(uint64); !ok || id != 2 { t.Errorf("Unexpected body of Replace (0)") } if h, ok := tpl[1].(string); !ok || h != "hi" { @@ -377,7 +377,7 @@ func TestClient(t *testing.T) { if len(tpl) != 2 { t.Errorf("Unexpected body of Update (tuple len)") } - if id, ok := tpl[0].(int64); !ok || id != 2 { + if id, ok := tpl[0].(uint64); !ok || id != 2 { t.Errorf("Unexpected body of Update (0)") } if h, ok := tpl[1].(string); !ok || h != "bye" { @@ -421,7 +421,7 @@ func TestClient(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if id, ok := tpl[0].(int64); !ok || id != 10 { + if id, ok := tpl[0].(uint64); !ok || id != 10 { t.Errorf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "val 10" { @@ -488,9 +488,8 @@ func TestClient(t *testing.T) { if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } - val := resp.Data[0].(int64) + val := resp.Data[0].(uint64) if val != 11 { t.Errorf("5 + 6 == 11, but got %v", val) } - } From e680762df2189091e24ed01e282ca091ccab8563 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Mon, 15 Feb 2016 18:30:04 +0300 Subject: [PATCH 062/605] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 971973c13..2c3e199b6 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,6 @@ func main() { ## Options * Timeout - timeout for any particular request. If Timeout is zero request any request may block infinitely * Reconnect - timeout for between reconnect attempts. If Reconnect is zero, no reconnects will be performed -* MaxReconnects - maximal number of reconnect failures after that we give it up +* MaxReconnects - maximal number of reconnect failures after that we give it up. If MaxReconnects is zero, client will try to reconnect endlessly * User - user name to login tarantool * Pass - user password to login tarantool From 5725500a2dfc0ef6e9b6bbc932738ee6fa0c8a3a Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Mon, 25 Apr 2016 15:03:24 +0300 Subject: [PATCH 063/605] optimize request pack --- request.go | 25 ++++++++++++++++++++----- smallbuf.go | 17 +++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/request.go b/request.go index 6487d7df2..24a82d2d5 100644 --- a/request.go +++ b/request.go @@ -191,9 +191,8 @@ func (req *Request) performTyped(res interface{}) (err error) { } func (req *Request) pack() (packet []byte, err error) { - var body []byte rid := req.requestId - h := [...]byte{ + h := smallWBuf{ 0xce, 0, 0, 0, 0, // length 0x82, // 2 element map KeyCode, byte(req.requestCode), // request code @@ -202,18 +201,34 @@ func (req *Request) pack() (packet []byte, err error) { byte(rid >> 8), byte(rid), } - body, err = msgpack.Marshal(req.body) + enc := msgpack.NewEncoder(&h) + err = enc.EncodeMapLen(len(req.body)) if err != nil { return } + for k,v := range(req.body) { + err = enc.EncodeInt64(int64(k)) + if err != nil { + return + } + switch vv:=v.(type) { + case uint32: + err = enc.EncodeUint64(uint64(vv)) + default: + err = enc.Encode(vv) + } + if err != nil { + return + } + } - l := uint32(len(h) - 5 + len(body)) + l := uint32(len(h) - 5) h[1] = byte(l >> 24) h[2] = byte(l >> 16) h[3] = byte(l >> 8) h[4] = byte(l) - packet = append(h[:], body...) + packet = h return } diff --git a/smallbuf.go b/smallbuf.go index 2b5a9352f..4217b7af5 100644 --- a/smallbuf.go +++ b/smallbuf.go @@ -50,3 +50,20 @@ func (s *smallBuf) Bytes() []byte { } return nil } + +type smallWBuf []byte + +func (s *smallWBuf) Write(b []byte) (int, error) { + *s = append(*s, b...) + return len(b), nil +} + +func (s *smallWBuf) WriteByte(b byte) error { + *s = append(*s, b) + return nil +} + +func (s *smallWBuf) WriteString(ss string) (int, error) { + *s = append(*s, ss...) + return len(ss), nil +} From 07cc908afe35c777d956822fa2d185b0bb10e17d Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Mon, 25 Apr 2016 15:10:27 +0300 Subject: [PATCH 064/605] decodeHeader: skip instead of decodeInterface --- response.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/response.go b/response.go index d2c12d304..394278d2d 100644 --- a/response.go +++ b/response.go @@ -44,7 +44,7 @@ func (resp *Response) decodeHeader() (err error) { return } default: - if _, err = d.DecodeInterface(); err != nil { + if err = d.Skip(); err != nil { return } } From b516184e8f34216b545546be9a7ead12063881f4 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Wed, 18 May 2016 00:10:44 +0300 Subject: [PATCH 065/605] schema loading --- config.lua | 29 +++++++++- connection.go | 6 ++ schema.go | 132 +++++++++++++++++++++++++++++++++++++++++++ tarantool_test.go | 141 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 schema.go diff --git a/config.lua b/config.lua index ba5385028..7f60c8389 100644 --- a/config.lua +++ b/config.lua @@ -3,10 +3,37 @@ box.cfg{ wal_dir='xlog', snap_dir='snap', } + local s = box.schema.space.create('test', {if_not_exists = true}) -local i = s:create_index('primary', {type = 'hash', parts = {1, 'NUM'}, if_not_exists = true}) +s:create_index('primary', {type = 'hash', parts = {1, 'NUM'}, if_not_exists = true}) s:truncate() +local st = box.schema.space.create('schematest', { + id = 9991, + temporary = true, + if_not_exists = true, + field_count = 7, + format = { + [2] = {name = "name1"}, + [3] = {type = "type2"}, + [6] = {name = "name5", type = "str", more = "extra"}, + }, +}) +st:create_index('primary', { + type = 'hash', + parts = {1, 'NUM'}, + unique = true, + if_not_exists = true, +}) +st:create_index('secondary', { + id = 3, + type = 'tree', + unique = false, + parts = { 2, 'num', 3, 'STR' }, + if_not_exists = true, +}) + +s:truncate() --box.schema.user.grant('guest', 'read,write,execute', 'universe') -- auth testing: access control diff --git a/connection.go b/connection.go index c14aaac01..1b7a722b2 100644 --- a/connection.go +++ b/connection.go @@ -18,6 +18,7 @@ type Connection struct { r *bufio.Reader w *bufio.Writer mutex *sync.Mutex + Schema *Schema requestId uint32 Greeting *Greeting requests map[uint32]*Future @@ -65,6 +66,11 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { go conn.writer() go conn.reader() + if err = conn.loadSchema(); err != nil { + conn.closeConnection(err) + return nil, err + } + return conn, err } diff --git a/schema.go b/schema.go new file mode 100644 index 000000000..86b431868 --- /dev/null +++ b/schema.go @@ -0,0 +1,132 @@ +package tarantool + +import ( + _ "fmt" +) + +type Schema struct { + Version uint + Spaces map[uint32]*Space + SpacesN map[string]*Space +} + +type Space struct { + Id uint32 + Name string + Engine string + Temporary bool + FieldsCount uint32 + Fields map[uint32]*Field + FieldsN map[string]*Field + Indexes map[uint32]*Index + IndexesN map[string]*Index +} + +type Field struct { + Id uint32 + Name string + Type string +} + +type Index struct { + Id uint32 + Name string + Type string + Unique bool + Fields []*IndexField +} + +type IndexField struct { + Id uint32 + Type string +} + +const ( + maxSchemas = 10000 + spaceSpId = 280 + vspaceSpId = 281 + indexSpId = 288 + vindexSpId = 289 +) + +func (conn *Connection) loadSchema() (err error) { + var req *Request + var resp *Response + + schema := new(Schema) + schema.Spaces = make(map[uint32]*Space) + schema.SpacesN = make(map[string]*Space) + + // reload spaces + req = conn.NewRequest(SelectRequest) + req.fillSearch(spaceSpId, 0, []interface{}{}) + req.fillIterator(0, maxSchemas, IterAll) + resp, err = req.perform() + if err != nil { + return err + } + for _, row := range resp.Data { + row := row.([]interface{}) + space := new(Space) + space.Id = uint32(row[0].(uint64)) + space.Name = row[2].(string) + space.Engine = row[3].(string) + space.FieldsCount = uint32(row[4].(uint64)) + space.Temporary = bool(row[5].(string) == "temporary") + space.Fields = make(map[uint32]*Field) + space.FieldsN = make(map[string]*Field) + space.Indexes = make(map[uint32]*Index) + space.IndexesN = make(map[string]*Index) + for i, f := range row[6].([]interface{}) { + if f == nil { + continue + } + f := f.(map[interface{}]interface{}) + field := new(Field) + field.Id = uint32(i) + if name, ok := f["name"]; ok && name != nil { + field.Name = name.(string) + } + if type_, ok := f["type"]; ok && type_ != nil { + field.Type = type_.(string) + } + space.Fields[field.Id] = field + if field.Name != "" { + space.FieldsN[field.Name] = field + } + } + + schema.Spaces[space.Id] = space + schema.SpacesN[space.Name] = space + } + + // reload indexes + req = conn.NewRequest(SelectRequest) + req.fillSearch(indexSpId, 0, []interface{}{}) + req.fillIterator(0, maxSchemas, IterAll) + resp, err = req.perform() + if err != nil { + return err + } + for _, row := range resp.Data { + row := row.([]interface{}) + index := new(Index) + index.Id = uint32(row[1].(uint64)) + index.Name = row[2].(string) + index.Type = row[3].(string) + opts := row[4].(map[interface{}]interface{}) + index.Unique = opts["unique"].(bool) + for _, f := range row[5].([]interface{}) { + f := f.([]interface{}) + field := new(IndexField) + field.Id = uint32(f[0].(uint64)) + field.Type = f[1].(string) + index.Fields = append(index.Fields, field) + } + spaceId := uint32(row[0].(uint64)) + schema.Spaces[spaceId].Indexes[index.Id] = index + schema.Spaces[spaceId].IndexesN[index.Name] = index + } + conn.Schema = schema + return nil +} diff --git a/tarantool_test.go b/tarantool_test.go index 2882787e9..81ad1484d 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -492,4 +492,145 @@ func TestClient(t *testing.T) { if val != 11 { t.Errorf("5 + 6 == 11, but got %v", val) } + + // Schema + schema := conn.Schema + if schema.Spaces == nil { + t.Errorf("schema.Spaces is nil") + } + if schema.SpacesN == nil { + t.Errorf("schema.SpacesN is nil") + } + var space, space2 *Space + var ok bool + if space, ok = schema.Spaces[9991]; !ok { + t.Errorf("space with id = 9991 was not found in schema.Spaces") + } + if space2, ok = schema.SpacesN["schematest"]; !ok { + t.Errorf("space with name 'schematest' was not found in schema.Spaces") + } + if space != space2 { + t.Errorf("space with id = 9991 and space with name schematest are different") + } + if space.Id != 9991 { + t.Errorf("space 9991 has incorrect Id") + } + if space.Name != "schematest" { + t.Errorf("space 9991 has incorrect Name") + } + if !space.Temporary { + t.Errorf("space 9991 should be temporary") + } + if space.Engine != "memtx" { + t.Errorf("space 9991 engine should be memtx") + } + if space.FieldsCount != 7 { + t.Errorf("space 9991 has incorrect fields count") + } + + if space.Fields == nil { + t.Errorf("space.Fields is nill") + } + if space.FieldsN == nil { + t.Errorf("space.FieldsN is nill") + } + if len(space.Fields) != 3 { + t.Errorf("space.Fields len is incorrect") + } + if len(space.FieldsN) != 2 { + t.Errorf("space.FieldsN len is incorrect") + } + + var field1, field2, field5, field1_, field5_ *Field + if field1, ok = space.Fields[1]; !ok { + t.Errorf("field id = 1 was not found") + } + if field2, ok = space.Fields[2]; !ok { + t.Errorf("field id = 2 was not found") + } + if field5, ok = space.Fields[5]; !ok { + t.Errorf("field id = 5 was not found") + } + + if field1_, ok = space.FieldsN["name1"]; !ok { + t.Errorf("field name = name1 was not found") + } + if field5_, ok = space.FieldsN["name5"]; !ok { + t.Errorf("field name = name5 was not found") + } + if field1 != field1_ || field5 != field5_ { + t.Errorf("field with id = 1 and field with name 'name1' are different") + } + if field1.Name != "name1" { + t.Errorf("field 1 has incorrect Name") + } + if field1.Type != "" { + t.Errorf("field 1 has incorrect Type") + } + if field2.Name != "" { + t.Errorf("field 2 has incorrect Name") + } + if field2.Type != "type2" { + t.Errorf("field 2 has incorrect Type") + } + + if space.Indexes == nil { + t.Errorf("space.Indexes is nill") + } + if space.IndexesN == nil { + t.Errorf("space.IndexesN is nill") + } + if len(space.Indexes) != 2 { + t.Errorf("space.Indexes len is incorrect") + } + if len(space.IndexesN) != 2 { + t.Errorf("space.IndexesN len is incorrect") + } + + var index0, index3, index0_, index3_ *Index + if index0, ok = space.Indexes[0]; !ok { + t.Errorf("index id = 0 was not found") + } + if index3, ok = space.Indexes[3]; !ok { + t.Errorf("index id = 3 was not found") + } + if index0_, ok = space.IndexesN["primary"]; !ok { + t.Errorf("index name = primary was not found") + } + if index3_, ok = space.IndexesN["secondary"]; !ok { + t.Errorf("index name = secondary was not found") + } + if index0 != index0_ || index3 != index3_ { + t.Errorf("index with id = 3 and index with name 'secondary' are different") + } + if index3.Id != 3 { + t.Errorf("index has incorrect Id") + } + if index0.Name != "primary" { + t.Errorf("index has incorrect Name") + } + if index0.Type != "hash" || index3.Type != "tree" { + t.Errorf("index has incorrect Type") + } + if !index0.Unique || index3.Unique { + t.Errorf("index has incorrect Unique") + } + if index3.Fields == nil { + t.Errorf("index.Fields is nil") + } + if len(index3.Fields) != 2 { + t.Errorf("index.Fields len is incorrect") + } + + ifield1 := index3.Fields[0] + ifield2 := index3.Fields[1] + if ifield1 == nil || ifield2 == nil { + t.Errorf("index field is nil") + } + if ifield1.Id != 1 || ifield2.Id != 2 { + t.Errorf("index field has incorrect Id") + } + if ifield1.Type != "num" || ifield2.Type != "STR" { + t.Errorf("index field has incorrect Type[") + } } From a38eb367cb550d8bebd71a965cc7a7c878c26ecb Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Sat, 21 May 2016 02:44:36 +0300 Subject: [PATCH 066/605] support tarantool 1.6.5 schema format --- schema.go | 74 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/schema.go b/schema.go index 86b431868..27eca96b3 100644 --- a/schema.go +++ b/schema.go @@ -72,27 +72,31 @@ func (conn *Connection) loadSchema() (err error) { space.Name = row[2].(string) space.Engine = row[3].(string) space.FieldsCount = uint32(row[4].(uint64)) - space.Temporary = bool(row[5].(string) == "temporary") + if len(row) >= 6 { + space.Temporary = bool(row[5].(string) == "temporary") + } space.Fields = make(map[uint32]*Field) space.FieldsN = make(map[string]*Field) space.Indexes = make(map[uint32]*Index) space.IndexesN = make(map[string]*Index) - for i, f := range row[6].([]interface{}) { - if f == nil { - continue - } - f := f.(map[interface{}]interface{}) - field := new(Field) - field.Id = uint32(i) - if name, ok := f["name"]; ok && name != nil { - field.Name = name.(string) - } - if type_, ok := f["type"]; ok && type_ != nil { - field.Type = type_.(string) - } - space.Fields[field.Id] = field - if field.Name != "" { - space.FieldsN[field.Name] = field + if len(row) >= 7 { + for i, f := range row[6].([]interface{}) { + if f == nil { + continue + } + f := f.(map[interface{}]interface{}) + field := new(Field) + field.Id = uint32(i) + if name, ok := f["name"]; ok && name != nil { + field.Name = name.(string) + } + if type_, ok := f["type"]; ok && type_ != nil { + field.Type = type_.(string) + } + space.Fields[field.Id] = field + if field.Name != "" { + space.FieldsN[field.Name] = field + } } } @@ -114,14 +118,34 @@ func (conn *Connection) loadSchema() (err error) { index.Id = uint32(row[1].(uint64)) index.Name = row[2].(string) index.Type = row[3].(string) - opts := row[4].(map[interface{}]interface{}) - index.Unique = opts["unique"].(bool) - for _, f := range row[5].([]interface{}) { - f := f.([]interface{}) - field := new(IndexField) - field.Id = uint32(f[0].(uint64)) - field.Type = f[1].(string) - index.Fields = append(index.Fields, field) + switch row[4].(type) { + case uint64: + index.Unique = row[4].(uint64) > 0 + case map[interface{}]interface{}: + opts := row[4].(map[interface{}]interface{}) + index.Unique = opts["unique"].(bool) + default: + panic("unexpected schema format (index flags)") + } + switch row[5].(type) { + case uint64: + cnt := int(row[5].(uint64)) + for i := 0; i < cnt; i += 1 { + field := new(IndexField) + field.Id = uint32(row[6+i*2].(uint64)) + field.Type = row[7+i*2].(string) + index.Fields = append(index.Fields, field) + } + case []interface{}: + for _, f := range row[5].([]interface{}) { + f := f.([]interface{}) + field := new(IndexField) + field.Id = uint32(f[0].(uint64)) + field.Type = f[1].(string) + index.Fields = append(index.Fields, field) + } + default: + panic("unexpected schema format (index fields)") } spaceId := uint32(row[0].(uint64)) schema.Spaces[spaceId].Indexes[index.Id] = index From 436ddf2ae9b6f53623f91ec279b0f7ea7a02fa54 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Sun, 22 May 2016 16:50:42 +0300 Subject: [PATCH 067/605] space and index names in basic commands --- config.lua | 9 +- connection.go | 4 +- request.go | 205 ++++++++++++++++++++++++-------------- schema.go | 118 ++++++++++++++++++---- tarantool_test.go | 248 ++++++++++++++++++++++++++++++++++++---------- 5 files changed, 432 insertions(+), 152 deletions(-) diff --git a/config.lua b/config.lua index 7f60c8389..32442e612 100644 --- a/config.lua +++ b/config.lua @@ -4,12 +4,15 @@ box.cfg{ snap_dir='snap', } -local s = box.schema.space.create('test', {if_not_exists = true}) +local s = box.schema.space.create('test', { + id = 512, + if_not_exists = true, +}) s:create_index('primary', {type = 'hash', parts = {1, 'NUM'}, if_not_exists = true}) s:truncate() local st = box.schema.space.create('schematest', { - id = 9991, + id = 514, temporary = true, if_not_exists = true, field_count = 7, @@ -32,8 +35,8 @@ st:create_index('secondary', { parts = { 2, 'num', 3, 'STR' }, if_not_exists = true, }) +st:truncate() -s:truncate() --box.schema.user.grant('guest', 'read,write,execute', 'universe') -- auth testing: access control diff --git a/connection.go b/connection.go index 1b7a722b2..bbd99602f 100644 --- a/connection.go +++ b/connection.go @@ -227,7 +227,7 @@ func (conn *Connection) closeConnection(neterr error) (err error) { conn.w = nil for rid, fut := range conn.requests { fut.err = neterr - close(fut.c) + close(fut.ready) delete(conn.requests, rid) } return @@ -292,7 +292,7 @@ func (conn *Connection) reader() { if fut, ok := conn.requests[resp.RequestId]; ok { delete(conn.requests, resp.RequestId) fut.resp = resp - close(fut.c) + close(fut.ready) conn.mutex.Unlock() } else { conn.mutex.Unlock() diff --git a/request.go b/request.go index 6487d7df2..8f6fa483c 100644 --- a/request.go +++ b/request.go @@ -15,12 +15,11 @@ type Request struct { } type Future struct { - conn *Connection - id uint32 - resp Response - err error - c chan struct{} - t *time.Timer + req *Request + resp Response + err error + ready chan struct{} + timeout *time.Timer } func (conn *Connection) NewRequest(requestCode int32) (req *Request) { @@ -29,7 +28,6 @@ func (conn *Connection) NewRequest(requestCode int32) (req *Request) { req.requestId = conn.nextRequestId() req.requestCode = requestCode req.body = make(map[int]interface{}) - return } @@ -39,68 +37,97 @@ func (conn *Connection) Ping() (resp *Response, err error) { return } -func (r *Request) fillSearch(spaceNo, indexNo uint32, key []interface{}) { - r.body[KeySpaceNo] = spaceNo - r.body[KeyIndexNo] = indexNo - r.body[KeyKey] = key +func (req *Request) fillSearch(spaceNo, indexNo uint32, key []interface{}) { + req.body[KeySpaceNo] = spaceNo + req.body[KeyIndexNo] = indexNo + req.body[KeyKey] = key } -func (r *Request) fillIterator(offset, limit, iterator uint32) { - r.body[KeyIterator] = iterator - r.body[KeyOffset] = offset - r.body[KeyLimit] = limit + +func (req *Request) fillIterator(offset, limit, iterator uint32) { + req.body[KeyIterator] = iterator + req.body[KeyOffset] = offset + req.body[KeyLimit] = limit } -func (r *Request) fillInsert(spaceNo uint32, tuple []interface{}) { - r.body[KeySpaceNo] = spaceNo - r.body[KeyTuple] = tuple +func (req *Request) fillInsert(spaceNo uint32, tuple []interface{}) { + req.body[KeySpaceNo] = spaceNo + req.body[KeyTuple] = tuple } -func (conn *Connection) Select(spaceNo, indexNo, offset, limit, iterator uint32, key []interface{}) (resp *Response, err error) { +func (conn *Connection) Select(space, index interface{}, offset, limit, iterator uint32, key []interface{}) (resp *Response, err error) { request := conn.NewRequest(SelectRequest) + spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + if err != nil { + return + } request.fillSearch(spaceNo, indexNo, key) request.fillIterator(offset, limit, iterator) resp, err = request.perform() return } -func (conn *Connection) SelectTyped(spaceNo, indexNo, offset, limit, iterator uint32, key []interface{}, result interface{}) error { +func (conn *Connection) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key []interface{}, result interface{}) (err error) { request := conn.NewRequest(SelectRequest) + spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + if err != nil { + return + } request.fillSearch(spaceNo, indexNo, key) request.fillIterator(offset, limit, iterator) return request.performTyped(result) } -func (conn *Connection) Insert(spaceNo uint32, tuple []interface{}) (resp *Response, err error) { +func (conn *Connection) Insert(space interface{}, tuple []interface{}) (resp *Response, err error) { request := conn.NewRequest(InsertRequest) + spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) + if err != nil { + return + } request.fillInsert(spaceNo, tuple) resp, err = request.perform() return } -func (conn *Connection) Replace(spaceNo uint32, tuple []interface{}) (resp *Response, err error) { +func (conn *Connection) Replace(space interface{}, tuple []interface{}) (resp *Response, err error) { request := conn.NewRequest(ReplaceRequest) + spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) + if err != nil { + return + } request.fillInsert(spaceNo, tuple) resp, err = request.perform() return } -func (conn *Connection) Delete(spaceNo, indexNo uint32, key []interface{}) (resp *Response, err error) { +func (conn *Connection) Delete(space, index interface{}, key []interface{}) (resp *Response, err error) { request := conn.NewRequest(DeleteRequest) + spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + if err != nil { + return + } request.fillSearch(spaceNo, indexNo, key) resp, err = request.perform() return } -func (conn *Connection) Update(spaceNo, indexNo uint32, key, ops []interface{}) (resp *Response, err error) { +func (conn *Connection) Update(space, index interface{}, key, ops []interface{}) (resp *Response, err error) { request := conn.NewRequest(UpdateRequest) + spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + if err != nil { + return + } request.fillSearch(spaceNo, indexNo, key) request.body[KeyTuple] = ops resp, err = request.perform() return } -func (conn *Connection) Upsert(spaceNo uint32, tuple, ops []interface{}) (resp *Response, err error) { +func (conn *Connection) Upsert(space interface{}, tuple, ops []interface{}) (resp *Response, err error) { request := conn.NewRequest(UpsertRequest) + spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) + if err != nil { + return + } request.body[KeySpaceNo] = spaceNo request.body[KeyTuple] = tuple request.body[KeyDefTuple] = ops @@ -124,40 +151,64 @@ func (conn *Connection) Eval(expr string, tuple []interface{}) (resp *Response, return } -func (conn *Connection) SelectAsync(spaceNo, indexNo, offset, limit, iterator uint32, key []interface{}) *Future { +func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key []interface{}) *Future { request := conn.NewRequest(SelectRequest) + spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + if err != nil { + return badfuture(err) + } request.fillSearch(spaceNo, indexNo, key) request.fillIterator(offset, limit, iterator) return request.future() } -func (conn *Connection) InsertAsync(spaceNo uint32, tuple []interface{}) *Future { +func (conn *Connection) InsertAsync(space interface{}, tuple []interface{}) *Future { request := conn.NewRequest(InsertRequest) + spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) + if err != nil { + return badfuture(err) + } request.fillInsert(spaceNo, tuple) return request.future() } -func (conn *Connection) ReplaceAsync(spaceNo uint32, tuple []interface{}) *Future { +func (conn *Connection) ReplaceAsync(space interface{}, tuple []interface{}) *Future { request := conn.NewRequest(ReplaceRequest) + spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) + if err != nil { + return badfuture(err) + } request.fillInsert(spaceNo, tuple) return request.future() } -func (conn *Connection) DeleteAsync(spaceNo, indexNo uint32, key []interface{}) *Future { +func (conn *Connection) DeleteAsync(space, index interface{}, key []interface{}) *Future { request := conn.NewRequest(DeleteRequest) + spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + if err != nil { + return badfuture(err) + } request.fillSearch(spaceNo, indexNo, key) return request.future() } -func (conn *Connection) UpdateAsync(spaceNo, indexNo uint32, key, ops []interface{}) *Future { +func (conn *Connection) UpdateAsync(space, index interface{}, key, ops []interface{}) *Future { request := conn.NewRequest(UpdateRequest) + spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + if err != nil { + return badfuture(err) + } request.fillSearch(spaceNo, indexNo, key) request.body[KeyTuple] = ops return request.future() } -func (conn *Connection) UpsertAsync(spaceNo uint32, tuple, ops []interface{}) *Future { +func (conn *Connection) UpsertAsync(space interface{}, tuple, ops []interface{}) *Future { request := conn.NewRequest(UpsertRequest) + spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) + if err != nil { + return badfuture(err) + } request.body[KeySpaceNo] = spaceNo request.body[KeyTuple] = tuple request.body[KeyDefTuple] = ops @@ -217,84 +268,88 @@ func (req *Request) pack() (packet []byte, err error) { return } -func (req *Request) future() (f *Future) { - f = &Future{ - conn: req.conn, - id: req.requestId, - c: make(chan struct{}), - } +func (req *Request) future() (fut *Future) { + fut = &Future{req: req} // check connection ready to process packets - if c := f.conn.c; c == nil { - close(f.c) - f.err = errors.New("client connection is not ready") - return // we shouldn't perform this request + if c := req.conn.c; c == nil { + fut.err = errors.New("client connection is not ready") + return } var packet []byte - if packet, f.err = req.pack(); f.err != nil { - close(f.c) + if packet, fut.err = req.pack(); fut.err != nil { return } req.conn.mutex.Lock() if req.conn.closed { req.conn.mutex.Unlock() - f.err = errors.New("using closed connection") - close(f.c) + fut.err = errors.New("using closed connection") return } - - req.conn.requests[req.requestId] = f + req.conn.requests[req.requestId] = fut req.conn.mutex.Unlock() + + fut.ready = make(chan struct{}) + // TODO: packets may lock req.conn.packets <- (packet) if req.conn.opts.Timeout > 0 { - f.t = time.NewTimer(req.conn.opts.Timeout) + fut.timeout = time.NewTimer(req.conn.opts.Timeout) } return } -func (f *Future) wait() { +func badfuture(err error) *Future { + return &Future{err: err} +} + +func (fut *Future) wait() { + if fut.ready == nil { + return + } + conn := fut.req.conn + requestId := fut.req.requestId select { - case <-f.c: + case <-fut.ready: default: - if t := f.t; t != nil { + if timeout := fut.timeout; timeout != nil { select { - case <-f.c: - case <-t.C: - f.conn.mutex.Lock() - if _, ok := f.conn.requests[f.id]; ok { - delete(f.conn.requests, f.id) - close(f.c) - f.err = fmt.Errorf("client timeout for request %d", f.id) + case <-fut.ready: + case <-timeout.C: + conn.mutex.Lock() + if _, ok := conn.requests[requestId]; ok { + delete(conn.requests, requestId) + close(fut.ready) + fut.err = fmt.Errorf("client timeout for request %d", requestId) } - f.conn.mutex.Unlock() + conn.mutex.Unlock() } } else { - <-f.c + <-fut.ready } } - if f.t != nil { - f.t.Stop() - f.t = nil + if fut.timeout != nil { + fut.timeout.Stop() + fut.timeout = nil } } -func (f *Future) Get() (*Response, error) { - f.wait() - if f.err != nil { - return &f.resp, f.err +func (fut *Future) Get() (*Response, error) { + fut.wait() + if fut.err != nil { + return &fut.resp, fut.err } - f.err = f.resp.decodeBody() - return &f.resp, f.err + fut.err = fut.resp.decodeBody() + return &fut.resp, fut.err } -func (f *Future) GetTyped(r interface{}) error { - f.wait() - if f.err != nil { - return f.err +func (fut *Future) GetTyped(result interface{}) error { + fut.wait() + if fut.err != nil { + return fut.err } - f.err = f.resp.decodeBodyTyped(r) - return f.err + fut.err = fut.resp.decodeBodyTyped(result) + return fut.err } diff --git a/schema.go b/schema.go index 27eca96b3..0476a2b33 100644 --- a/schema.go +++ b/schema.go @@ -1,13 +1,13 @@ package tarantool import ( - _ "fmt" + "fmt" ) type Schema struct { - Version uint - Spaces map[uint32]*Space - SpacesN map[string]*Space + Version uint + Spaces map[string]*Space + SpacesById map[uint32]*Space } type Space struct { @@ -16,10 +16,10 @@ type Space struct { Engine string Temporary bool FieldsCount uint32 - Fields map[uint32]*Field - FieldsN map[string]*Field - Indexes map[uint32]*Index - IndexesN map[string]*Index + Fields map[string]*Field + FieldsById map[uint32]*Field + Indexes map[string]*Index + IndexesById map[uint32]*Index } type Field struct { @@ -54,8 +54,8 @@ func (conn *Connection) loadSchema() (err error) { var resp *Response schema := new(Schema) - schema.Spaces = make(map[uint32]*Space) - schema.SpacesN = make(map[string]*Space) + schema.SpacesById = make(map[uint32]*Space) + schema.Spaces = make(map[string]*Space) // reload spaces req = conn.NewRequest(SelectRequest) @@ -75,10 +75,10 @@ func (conn *Connection) loadSchema() (err error) { if len(row) >= 6 { space.Temporary = bool(row[5].(string) == "temporary") } - space.Fields = make(map[uint32]*Field) - space.FieldsN = make(map[string]*Field) - space.Indexes = make(map[uint32]*Index) - space.IndexesN = make(map[string]*Index) + space.FieldsById = make(map[uint32]*Field) + space.Fields = make(map[string]*Field) + space.IndexesById = make(map[uint32]*Index) + space.Indexes = make(map[string]*Index) if len(row) >= 7 { for i, f := range row[6].([]interface{}) { if f == nil { @@ -93,15 +93,15 @@ func (conn *Connection) loadSchema() (err error) { if type_, ok := f["type"]; ok && type_ != nil { field.Type = type_.(string) } - space.Fields[field.Id] = field + space.FieldsById[field.Id] = field if field.Name != "" { - space.FieldsN[field.Name] = field + space.Fields[field.Name] = field } } } - schema.Spaces[space.Id] = space - schema.SpacesN[space.Name] = space + schema.SpacesById[space.Id] = space + schema.Spaces[space.Name] = space } // reload indexes @@ -148,9 +148,87 @@ func (conn *Connection) loadSchema() (err error) { panic("unexpected schema format (index fields)") } spaceId := uint32(row[0].(uint64)) - schema.Spaces[spaceId].Indexes[index.Id] = index - schema.Spaces[spaceId].IndexesN[index.Name] = index + schema.SpacesById[spaceId].IndexesById[index.Id] = index + schema.SpacesById[spaceId].Indexes[index.Name] = index } conn.Schema = schema return nil } + +func (schema *Schema) resolveSpaceIndex(s interface{}, i interface{}) (spaceNo, indexNo uint32, err error) { + var space *Space + var index *Index + var ok bool + + switch s := s.(type) { + case string: + if space, ok = schema.Spaces[s]; !ok { + err = fmt.Errorf("there is no space with name %s", s) + return + } + spaceNo = space.Id + case uint: + spaceNo = uint32(s) + case uint64: + spaceNo = uint32(s) + case uint32: + spaceNo = uint32(s) + case uint16: + spaceNo = uint32(s) + case uint8: + spaceNo = uint32(s) + case int: + spaceNo = uint32(s) + case int64: + spaceNo = uint32(s) + case int32: + spaceNo = uint32(s) + case int16: + spaceNo = uint32(s) + case int8: + spaceNo = uint32(s) + default: + panic("unexpected type of space param") + } + + if i != nil { + switch i := i.(type) { + case string: + if space == nil { + if space, ok = schema.SpacesById[spaceNo]; !ok { + err = fmt.Errorf("there is no space with id %d", spaceNo) + return + } + } + if index, ok = space.Indexes[i]; !ok { + err = fmt.Errorf("space %s has not index with name %s", space.Name, i) + return + } + indexNo = index.Id + case uint: + indexNo = uint32(i) + case uint64: + indexNo = uint32(i) + case uint32: + indexNo = uint32(i) + case uint16: + indexNo = uint32(i) + case uint8: + indexNo = uint32(i) + case int: + indexNo = uint32(i) + case int64: + indexNo = uint32(i) + case int32: + indexNo = uint32(i) + case int16: + indexNo = uint32(i) + case int8: + indexNo = uint32(i) + default: + panic("unexpected type of index param") + } + } + + return +} diff --git a/tarantool_test.go b/tarantool_test.go index 81ad1484d..71226106b 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -20,8 +20,10 @@ func init() { var server = "127.0.0.1:3013" var spaceNo = uint32(512) +var spaceName = "test" var indexNo = uint32(0) -var opts = Opts{Timeout: 500 * time.Millisecond} +var indexName = "primary" +var opts = Opts{Timeout: 500 * time.Millisecond, User: "test", Pass: "test"} const N = 500 @@ -31,15 +33,16 @@ func BenchmarkClientSerial(b *testing.B) { conn, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") + return } - _, err = conn.Replace(spaceNo, []interface{}{uint(1), "hello", "world"}) + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("No connection available") } for i := 0; i < b.N; i++ { - _, err = conn.Select(spaceNo, indexNo, 0, 1, IterAll, []interface{}{uint(1)}) + _, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) if err != nil { b.Errorf("No connection available") } @@ -53,9 +56,10 @@ func BenchmarkClientFuture(b *testing.B) { conn, err := Connect(server, opts) if err != nil { b.Error(err) + return } - _, err = conn.Replace(spaceNo, []interface{}{uint(1), "hello", "world"}) + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Error(err) } @@ -63,7 +67,7 @@ func BenchmarkClientFuture(b *testing.B) { for i := 0; i < b.N; i += N { var fs [N]*Future for j := 0; j < N; j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{uint(1)}) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) } for j := 0; j < N; j++ { _, err = fs[j].Get() @@ -120,9 +124,10 @@ func BenchmarkClientFutureTyped(b *testing.B) { conn, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") + return } - _, err = conn.Replace(spaceNo, []interface{}{uint(1), "hello", "world"}) + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("No connection available") } @@ -130,7 +135,7 @@ func BenchmarkClientFutureTyped(b *testing.B) { for i := 0; i < b.N; i += N { var fs [N]*Future for j := 0; j < N; j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{uint(1)}) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) } for j := 0; j < N; j++ { var r []tuple @@ -138,11 +143,10 @@ func BenchmarkClientFutureTyped(b *testing.B) { if err != nil { b.Error(err) } - if len(r) != 1 || r[0].Id != 12 { + if len(r) != 1 || r[0].Id != 1111 { b.Errorf("Doesn't match %v", r) } } - } } @@ -152,9 +156,10 @@ func BenchmarkClientFutureParallel(b *testing.B) { conn, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") + return } - _, err = conn.Replace(spaceNo, []interface{}{uint(1), "hello", "world"}) + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("No connection available") } @@ -165,7 +170,7 @@ func BenchmarkClientFutureParallel(b *testing.B) { var fs [N]*Future var j int for j = 0; j < N && pb.Next(); j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{uint(1)}) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) } exit = j < N for j > 0 { @@ -173,6 +178,7 @@ func BenchmarkClientFutureParallel(b *testing.B) { _, err = fs[j].Get() if err != nil { b.Error(err) + break } } } @@ -185,9 +191,10 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { conn, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") + return } - _, err = conn.Replace(spaceNo, []interface{}{uint(1), "hello", "world"}) + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("No connection available") } @@ -198,7 +205,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { var fs [N]*Future var j int for j = 0; j < N && pb.Next(); j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterAll, []interface{}{uint(1)}) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) } exit = j < N for j > 0 { @@ -207,9 +214,11 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { err = fs[j].GetTyped(&r) if err != nil { b.Error(err) + break } - if len(r) != 1 || r[0].Id != 12 { + if len(r) != 1 || r[0].Id != 1111 { b.Errorf("Doesn't match %v", r) + break } } } @@ -220,18 +229,20 @@ func BenchmarkClientParrallel(b *testing.B) { conn, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") + return } - _, err = conn.Replace(spaceNo, []interface{}{uint(1), "hello", "world"}) + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("No connection available") } b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _, err = conn.Select(spaceNo, indexNo, 0, 1, IterAll, []interface{}{uint(1)}) + _, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) if err != nil { b.Errorf("No connection available") + break } } }) @@ -244,12 +255,14 @@ func TestClient(t *testing.T) { var err error var conn *Connection - conn, err = Connect(server, Opts{User: "test", Pass: "test"}) + conn, err = Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) + return } if conn == nil { t.Errorf("conn is nil after Connect") + return } // Ping @@ -319,7 +332,7 @@ func TestClient(t *testing.T) { } resp, err = conn.Delete(spaceNo, indexNo, []interface{}{uint(101)}) if err != nil { - t.Errorf("Failed to Replace: %s", err.Error()) + t.Errorf("Failed to Delete: %s", err.Error()) } if resp == nil { t.Errorf("Response is nil after Delete") @@ -492,70 +505,85 @@ func TestClient(t *testing.T) { if val != 11 { t.Errorf("5 + 6 == 11, but got %v", val) } +} + +func TestSchema(t *testing.T) { + var err error + var conn *Connection + + conn, err = Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } // Schema schema := conn.Schema + if schema.SpacesById == nil { + t.Errorf("schema.SpacesById is nil") + } if schema.Spaces == nil { t.Errorf("schema.Spaces is nil") } - if schema.SpacesN == nil { - t.Errorf("schema.SpacesN is nil") - } var space, space2 *Space var ok bool - if space, ok = schema.Spaces[9991]; !ok { - t.Errorf("space with id = 9991 was not found in schema.Spaces") + if space, ok = schema.SpacesById[514]; !ok { + t.Errorf("space with id = 514 was not found in schema.SpacesById") } - if space2, ok = schema.SpacesN["schematest"]; !ok { - t.Errorf("space with name 'schematest' was not found in schema.Spaces") + if space2, ok = schema.Spaces["schematest"]; !ok { + t.Errorf("space with name 'schematest' was not found in schema.SpacesById") } if space != space2 { - t.Errorf("space with id = 9991 and space with name schematest are different") + t.Errorf("space with id = 514 and space with name schematest are different") } - if space.Id != 9991 { - t.Errorf("space 9991 has incorrect Id") + if space.Id != 514 { + t.Errorf("space 514 has incorrect Id") } if space.Name != "schematest" { - t.Errorf("space 9991 has incorrect Name") + t.Errorf("space 514 has incorrect Name") } if !space.Temporary { - t.Errorf("space 9991 should be temporary") + t.Errorf("space 514 should be temporary") } if space.Engine != "memtx" { - t.Errorf("space 9991 engine should be memtx") + t.Errorf("space 514 engine should be memtx") } if space.FieldsCount != 7 { - t.Errorf("space 9991 has incorrect fields count") + t.Errorf("space 514 has incorrect fields count") } + if space.FieldsById == nil { + t.Errorf("space.FieldsById is nill") + } if space.Fields == nil { t.Errorf("space.Fields is nill") } - if space.FieldsN == nil { - t.Errorf("space.FieldsN is nill") + if len(space.FieldsById) != 3 { + t.Errorf("space.FieldsById len is incorrect") } - if len(space.Fields) != 3 { + if len(space.Fields) != 2 { t.Errorf("space.Fields len is incorrect") } - if len(space.FieldsN) != 2 { - t.Errorf("space.FieldsN len is incorrect") - } var field1, field2, field5, field1_, field5_ *Field - if field1, ok = space.Fields[1]; !ok { + if field1, ok = space.FieldsById[1]; !ok { t.Errorf("field id = 1 was not found") } - if field2, ok = space.Fields[2]; !ok { + if field2, ok = space.FieldsById[2]; !ok { t.Errorf("field id = 2 was not found") } - if field5, ok = space.Fields[5]; !ok { + if field5, ok = space.FieldsById[5]; !ok { t.Errorf("field id = 5 was not found") } - if field1_, ok = space.FieldsN["name1"]; !ok { + if field1_, ok = space.Fields["name1"]; !ok { t.Errorf("field name = name1 was not found") } - if field5_, ok = space.FieldsN["name5"]; !ok { + if field5_, ok = space.Fields["name5"]; !ok { t.Errorf("field name = name5 was not found") } if field1 != field1_ || field5 != field5_ { @@ -574,30 +602,30 @@ func TestClient(t *testing.T) { t.Errorf("field 2 has incorrect Type") } + if space.IndexesById == nil { + t.Errorf("space.IndexesById is nill") + } if space.Indexes == nil { t.Errorf("space.Indexes is nill") } - if space.IndexesN == nil { - t.Errorf("space.IndexesN is nill") + if len(space.IndexesById) != 2 { + t.Errorf("space.IndexesById len is incorrect") } if len(space.Indexes) != 2 { t.Errorf("space.Indexes len is incorrect") } - if len(space.IndexesN) != 2 { - t.Errorf("space.IndexesN len is incorrect") - } var index0, index3, index0_, index3_ *Index - if index0, ok = space.Indexes[0]; !ok { + if index0, ok = space.IndexesById[0]; !ok { t.Errorf("index id = 0 was not found") } - if index3, ok = space.Indexes[3]; !ok { + if index3, ok = space.IndexesById[3]; !ok { t.Errorf("index id = 3 was not found") } - if index0_, ok = space.IndexesN["primary"]; !ok { + if index0_, ok = space.Indexes["primary"]; !ok { t.Errorf("index name = primary was not found") } - if index3_, ok = space.IndexesN["secondary"]; !ok { + if index3_, ok = space.Indexes["secondary"]; !ok { t.Errorf("index name = secondary was not found") } if index0 != index0_ || index3 != index3_ { @@ -633,4 +661,120 @@ func TestClient(t *testing.T) { if ifield1.Type != "num" || ifield2.Type != "STR" { t.Errorf("index field has incorrect Type[") } + + var rSpaceNo, rIndexNo uint32 + rSpaceNo, rIndexNo, err = schema.resolveSpaceIndex(514, 3) + if err != nil || rSpaceNo != 514 || rIndexNo != 3 { + t.Errorf("numeric space and index params not resolved as-is") + } + rSpaceNo, rIndexNo, err = schema.resolveSpaceIndex(514, nil) + if err != nil || rSpaceNo != 514 { + t.Errorf("numeric space param not resolved as-is") + } + rSpaceNo, rIndexNo, err = schema.resolveSpaceIndex("schematest", "secondary") + if err != nil || rSpaceNo != 514 || rIndexNo != 3 { + t.Errorf("symbolic space and index params not resolved") + } + rSpaceNo, rIndexNo, err = schema.resolveSpaceIndex("schematest", nil) + if err != nil || rSpaceNo != 514 { + t.Errorf("symbolic space param not resolved") + } + rSpaceNo, rIndexNo, err = schema.resolveSpaceIndex("schematest22", "secondary") + if err == nil { + t.Errorf("resolveSpaceIndex didn't returned error with not existing space name") + } + rSpaceNo, rIndexNo, err = schema.resolveSpaceIndex("schematest", "secondary22") + if err == nil { + t.Errorf("resolveSpaceIndex didn't returned error with not existing index name") + } +} + +func TestClientNamed(t *testing.T) { + var resp *Response + var err error + var conn *Connection + + conn, err = Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + + // Insert + resp, err = conn.Insert(spaceName, []interface{}{uint(1001), "hello2", "world2"}) + if err != nil { + t.Errorf("Failed to Insert: %s", err.Error()) + } + + // Delete + resp, err = conn.Delete(spaceName, indexName, []interface{}{uint(1001)}) + if err != nil { + t.Errorf("Failed to Delete: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Delete") + } + + // Replace + resp, err = conn.Replace(spaceName, []interface{}{uint(1002), "hello", "world"}) + if err != nil { + t.Errorf("Failed to Replace: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Replace") + } + + // Update + resp, err = conn.Update(spaceName, indexName, []interface{}{uint(1002)}, []interface{}{[]interface{}{"=", 1, "bye"}, []interface{}{"#", 2, 1}}) + if err != nil { + t.Errorf("Failed to Update: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Update") + } + + // Upsert + resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + if err != nil { + t.Errorf("Failed to Upsert (insert): %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Upsert (insert)") + } + resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + if err != nil { + t.Errorf("Failed to Upsert (update): %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Upsert (update)") + } + + // Select + for i := 1010; i < 1020; i++ { + resp, err = conn.Replace(spaceName, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) + if err != nil { + t.Errorf("Failed to Replace: %s", err.Error()) + } + } + resp, err = conn.Select(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)}) + if err != nil { + t.Errorf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Select") + } + + // Select Typed + var tpl []tuple + err = conn.SelectTyped(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)}, &tpl) + if err != nil { + t.Errorf("Failed to SelectTyped: %s", err.Error()) + } + if len(tpl) != 1 { + t.Errorf("Result len of SelectTyped != 1") + } } From f53974e7758278ca145e4ee597b842cb22933af1 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Mon, 23 May 2016 15:58:25 +0300 Subject: [PATCH 068/605] documentation --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 2c3e199b6..f96d2c092 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ func main() { // insert new tuple { 10, 1 } resp, err = client.Insert(spaceNo, []interface{}{uint(10), 1}) + // or + resp, err = client.Insert("test", []interface{}{uint(10), 1}) log.Println("Insert") log.Println("Error", err) log.Println("Code", resp.Code) @@ -44,6 +46,8 @@ func main() { // delete tuple with primary key { 10 } resp, err = client.Delete(spaceNo, indexNo, []interface{}{uint(10)}) + // or + resp, err = client.Delete("test", "primary", []interface{}{uint(10)}) log.Println("Delete") log.Println("Error", err) log.Println("Code", resp.Code) @@ -51,6 +55,8 @@ func main() { // replace tuple with { 13, 1 } resp, err = client.Replace(spaceNo, []interface{}{uint(13), 1}) + // or + resp, err = client.Replace("test", []interface{}{uint(13), 1}) log.Println("Replace") log.Println("Error", err) log.Println("Code", resp.Code) @@ -58,6 +64,8 @@ func main() { // update tuple with primary key { 13 }, incrementing second field by 3 resp, err = client.Update(spaceNo, indexNo, []interface{}{uint(13)}, []interface{}{[]interface{}{"+", 1, 3}}) + // or + resp, err = client.Update("test", "primary", []interface{}{uint(13)}, []interface{}{[]interface{}{"+", 1, 3}}) log.Println("Update") log.Println("Error", err) log.Println("Code", resp.Code) @@ -65,6 +73,8 @@ func main() { // insert tuple {15, 1} or increment second field by 1 resp, err = client.Upsert(spaceNo, []interface{}{uint(15), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + // or + resp, err = client.Upsert("test", []interface{}{uint(15), 1}, []interface{}{[]interface{}{"+", 1, 1}}) log.Println("Upsert") log.Println("Error", err) log.Println("Code", resp.Code) @@ -72,6 +82,8 @@ func main() { // select just one tuple with primay key { 15 } resp, err = client.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{uint(15)}) + // or + resp, err = client.Select("test", "primary", 0, 1, tarantool.IterEq, []interface{}{uint(15)}) log.Println("Select") log.Println("Error", err) log.Println("Code", resp.Code) @@ -80,6 +92,8 @@ func main() { // select tuples by condition ( primay key > 15 ) with offset 7 limit 5 // BTREE index supposed resp, err = client.Select(spaceNo, indexNo, 7, 5, tarantool.IterGt, []interface{}{uint(15)}) + // or + resp, err = client.Select("test", "primary", 7, 5, tarantool.IterGt, []interface{}{uint(15)}) log.Println("Select") log.Println("Error", err) log.Println("Code", resp.Code) @@ -101,6 +115,34 @@ func main() { } ``` +## Schema +```go + // save Schema to local variable to avoid races + schema := client.Schema + + // access Space objects by name or id + space1 := schema.Spaces["some_space"] + space2 := schema.SpacesById[20] // it's a map + fmt.Printf("Space %d %s %s\n", space1.Id, space1.Name, space1.Engine) + fmt.Printf("Space %d %d\n", space1.FieldsCount, space1.Temporary) + + // access index information by name or id + index1 := schema.Indexes["some_index"] + index2 := schema.IndexesById[2] // it's a map + fmt.Printf("Index %d %s\n", index1.Id, index1.Name) + + // access index fields information by index + indexField1 := index1.Fields[0] // it's a slice + indexField2 := index1.Fields[1] // it's a slice + fmt.Printf("IndexFields %s %s\n", indexField1.Name, indexField1.Type) + + // access space fields information by name or id (index) + spaceField1 := space.Fields["some_field"] + spaceField2 := space.FieldsById[3] + fmt.Printf("SpaceField %s %s\n", spaceField1.Name, spaceField1.Type) +``` + + ## Options * Timeout - timeout for any particular request. If Timeout is zero request any request may block infinitely * Reconnect - timeout for between reconnect attempts. If Reconnect is zero, no reconnects will be performed From 82b0b5bd86d26fcd6f1c8cae2668d7d7785d20ad Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Wed, 25 May 2016 17:52:43 +0300 Subject: [PATCH 069/605] support tarantool 1.6.8 schema format --- schema.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/schema.go b/schema.go index 0476a2b33..50a516b82 100644 --- a/schema.go +++ b/schema.go @@ -73,7 +73,16 @@ func (conn *Connection) loadSchema() (err error) { space.Engine = row[3].(string) space.FieldsCount = uint32(row[4].(uint64)) if len(row) >= 6 { - space.Temporary = bool(row[5].(string) == "temporary") + switch row5 := row[5].(type) { + case string: + space.Temporary = row5 == "temporary" + case map[interface{}]interface{}: + if temp, ok := row5["temporary"]; ok { + space.Temporary = temp.(bool) + } + default: + panic("unexpected schema format (space flags)") + } } space.FieldsById = make(map[uint32]*Field) space.Fields = make(map[string]*Field) From cac3d39a8075fd31aaf84bb4d38d50f7c4c32607 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Fri, 27 May 2016 23:00:04 +0300 Subject: [PATCH 070/605] using _vspace / _vindex for intraspection --- config.lua | 4 +++- schema.go | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/config.lua b/config.lua index 32442e612..56fdc6ee0 100644 --- a/config.lua +++ b/config.lua @@ -42,7 +42,9 @@ st:truncate() -- auth testing: access control if not box.schema.user.exists('test') then box.schema.user.create('test', {password = 'test'}) - box.schema.user.grant('test', 'read,write,execute', 'universe') + box.schema.user.grant('test', 'execute', 'universe') + box.schema.user.grant('test', 'read,write', 'space', 'test') + box.schema.user.grant('test', 'read,write', 'space', 'schematest') end local console = require 'console' diff --git a/schema.go b/schema.go index 50a516b82..eb1dfd16f 100644 --- a/schema.go +++ b/schema.go @@ -59,7 +59,7 @@ func (conn *Connection) loadSchema() (err error) { // reload spaces req = conn.NewRequest(SelectRequest) - req.fillSearch(spaceSpId, 0, []interface{}{}) + req.fillSearch(vspaceSpId, 0, []interface{}{}) req.fillIterator(0, maxSchemas, IterAll) resp, err = req.perform() if err != nil { @@ -115,7 +115,7 @@ func (conn *Connection) loadSchema() (err error) { // reload indexes req = conn.NewRequest(SelectRequest) - req.fillSearch(indexSpId, 0, []interface{}{}) + req.fillSearch(vindexSpId, 0, []interface{}{}) req.fillIterator(0, maxSchemas, IterAll) resp, err = req.perform() if err != nil { From 59def42e62ffbb466ff4e6b9f1ac8a881ff83ce3 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Wed, 8 Jun 2016 18:29:43 +0300 Subject: [PATCH 071/605] Custom pack/unpack functions --- README.md | 125 +++++++++++++++++++++ connection.go | 4 +- request.go | 32 +++--- tarantool_test.go | 281 ++++++++++++++++++++++++++++++++++------------ 4 files changed, 352 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index f96d2c092..84e0647f0 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,131 @@ func main() { fmt.Printf("SpaceField %s %s\n", spaceField1.Name, spaceField1.Type) ``` +## Custom (un)packing and typed selects +It's possible to specify custom pack/unpack functions for your types. +It will allow you to store complex structures inside a tuple and may speed up you requests. +```go +import ( + "github.com/tarantool/go-tarantool" + "gopkg.in/vmihailenco/msgpack.v2" +) + +type Member struct { + Name string + Nonce string + Val uint +} + +type Tuple struct { + Cid uint + Orig string + Members []Member +} + +func init() { + msgpack.Register(reflect.TypeOf(Tuple{}), encodeTuple, decodeTuple) + msgpack.Register(reflect.TypeOf(Member{}), encodeMember, decodeMember) +} + +func encodeMember(e *msgpack.Encoder, v reflect.Value) error { + m := v.Interface().(Member) + if err := e.EncodeSliceLen(2); err != nil { + return err + } + if err := e.EncodeString(m.Name); err != nil { + return err + } + if err := e.EncodeUint(m.Val); err != nil { + return err + } + return nil +} + +func decodeMember(d *msgpack.Decoder, v reflect.Value) error { + var err error + var l int + m := v.Addr().Interface().(*Member) + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 2 { + return fmt.Errorf("array len doesn't match: %d", l) + } + if m.Name, err = d.DecodeString(); err != nil { + return err + } + if m.Val, err = d.DecodeUint(); err != nil { + return err + } + return nil +} + +func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { + c := v.Interface().(Tuple) + if err := e.EncodeSliceLen(3); err != nil { + return err + } + if err := e.EncodeUint(c.Cid); err != nil { + return err + } + if err := e.EncodeString(c.Orig); err != nil { + return err + } + if err := e.EncodeSliceLen(len(c.Members)); err != nil { + return err + } + for _, m := range c.Members { + e.Encode(m) + } + return nil +} + +func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { + var err error + var l int + c := v.Addr().Interface().(*Tuple) + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 3 { + return fmt.Errorf("array len doesn't match: %d", l) + } + if c.Cid, err = d.DecodeUint(); err != nil { + return err + } + if c.Orig, err = d.DecodeString(); err != nil { + return err + } + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + c.Members = make([]Member, l) + for i := 0; i < l; i++ { + d.Decode(&c.Members[i]) + } + return nil +} + +func main() { + // establish connection ... + + tuple := Tuple{777, "orig", []Member{{"lol", "", 1}, {"wut", "", 3}}} + _, err = conn.Replace(spaceNo, tuple) // NOTE: insert structure itself + if err != nil { + t.Errorf("Failed to insert: %s", err.Error()) + return + } + + var tuples []Tuple + err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{777}, &tuples) + if err != nil { + t.Errorf("Failed to selectTyped: %s", err.Error()) + return + } +} + +``` + ## Options * Timeout - timeout for any particular request. If Timeout is zero request any request may block infinitely diff --git a/connection.go b/connection.go index bbd99602f..34e122edd 100644 --- a/connection.go +++ b/connection.go @@ -29,7 +29,7 @@ type Connection struct { } type Greeting struct { - version string + Version string auth string } @@ -114,7 +114,7 @@ func (conn *Connection) dial() (err error) { c.Close() return } - conn.Greeting.version = bytes.NewBuffer(greeting[:64]).String() + conn.Greeting.Version = bytes.NewBuffer(greeting[:64]).String() conn.Greeting.auth = bytes.NewBuffer(greeting[64:108]).String() // Auth diff --git a/request.go b/request.go index 67f92460c..4ec1db0b9 100644 --- a/request.go +++ b/request.go @@ -49,7 +49,7 @@ func (req *Request) fillIterator(offset, limit, iterator uint32) { req.body[KeyLimit] = limit } -func (req *Request) fillInsert(spaceNo uint32, tuple []interface{}) { +func (req *Request) fillInsert(spaceNo uint32, tuple interface{}) { req.body[KeySpaceNo] = spaceNo req.body[KeyTuple] = tuple } @@ -77,7 +77,7 @@ func (conn *Connection) SelectTyped(space, index interface{}, offset, limit, ite return request.performTyped(result) } -func (conn *Connection) Insert(space interface{}, tuple []interface{}) (resp *Response, err error) { +func (conn *Connection) Insert(space interface{}, tuple interface{}) (resp *Response, err error) { request := conn.NewRequest(InsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { @@ -88,7 +88,7 @@ func (conn *Connection) Insert(space interface{}, tuple []interface{}) (resp *Re return } -func (conn *Connection) Replace(space interface{}, tuple []interface{}) (resp *Response, err error) { +func (conn *Connection) Replace(space interface{}, tuple interface{}) (resp *Response, err error) { request := conn.NewRequest(ReplaceRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { @@ -135,18 +135,18 @@ func (conn *Connection) Upsert(space interface{}, tuple, ops []interface{}) (res return } -func (conn *Connection) Call(functionName string, tuple []interface{}) (resp *Response, err error) { +func (conn *Connection) Call(functionName string, args []interface{}) (resp *Response, err error) { request := conn.NewRequest(CallRequest) request.body[KeyFunctionName] = functionName - request.body[KeyTuple] = tuple + request.body[KeyTuple] = args resp, err = request.perform() return } -func (conn *Connection) Eval(expr string, tuple []interface{}) (resp *Response, err error) { +func (conn *Connection) Eval(expr string, args []interface{}) (resp *Response, err error) { request := conn.NewRequest(EvalRequest) request.body[KeyExpression] = expr - request.body[KeyTuple] = tuple + request.body[KeyTuple] = args resp, err = request.perform() return } @@ -162,7 +162,7 @@ func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, ite return request.future() } -func (conn *Connection) InsertAsync(space interface{}, tuple []interface{}) *Future { +func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Future { request := conn.NewRequest(InsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { @@ -172,7 +172,7 @@ func (conn *Connection) InsertAsync(space interface{}, tuple []interface{}) *Fut return request.future() } -func (conn *Connection) ReplaceAsync(space interface{}, tuple []interface{}) *Future { +func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Future { request := conn.NewRequest(ReplaceRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { @@ -203,7 +203,7 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops []interfa return request.future() } -func (conn *Connection) UpsertAsync(space interface{}, tuple, ops []interface{}) *Future { +func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops []interface{}) *Future { request := conn.NewRequest(UpsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { @@ -215,17 +215,17 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple, ops []interface{}) return request.future() } -func (conn *Connection) CallAsync(functionName string, tuple []interface{}) *Future { +func (conn *Connection) CallAsync(functionName string, args []interface{}) *Future { request := conn.NewRequest(CallRequest) request.body[KeyFunctionName] = functionName - request.body[KeyTuple] = tuple + request.body[KeyTuple] = args return request.future() } -func (conn *Connection) EvalAsync(expr string, tuple []interface{}) *Future { +func (conn *Connection) EvalAsync(expr string, args []interface{}) *Future { request := conn.NewRequest(EvalRequest) request.body[KeyExpression] = expr - request.body[KeyTuple] = tuple + request.body[KeyTuple] = args return request.future() } @@ -257,12 +257,12 @@ func (req *Request) pack() (packet []byte, err error) { if err != nil { return } - for k,v := range(req.body) { + for k, v := range req.body { err = enc.EncodeInt64(int64(k)) if err != nil { return } - switch vv:=v.(type) { + switch vv := v.(type) { case uint32: err = enc.EncodeUint64(uint64(vv)) default: diff --git a/tarantool_test.go b/tarantool_test.go index 71226106b..fcb6c0dc3 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -4,18 +4,151 @@ import ( "fmt" "gopkg.in/vmihailenco/msgpack.v2" "reflect" + "strings" "testing" "time" ) -type tuple struct { +type Tuple struct { Id uint Msg string Name string } +type Member struct { + Name string + Nonce string + Val uint +} + +type Tuple2 struct { + Cid uint + Orig string + Members []Member +} + +func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { + t := v.Interface().(Tuple) + if err := e.EncodeSliceLen(3); err != nil { + return err + } + if err := e.EncodeUint(t.Id); err != nil { + return err + } + if err := e.EncodeString(t.Msg); err != nil { + return err + } + if err := e.EncodeString(t.Name); err != nil { + return err + } + return nil +} + +func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { + var err error + var l int + t := v.Addr().Interface().(*Tuple) + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 3 { + return fmt.Errorf("array len doesn't match: %d", l) + } + if t.Id, err = d.DecodeUint(); err != nil { + return err + } + if t.Msg, err = d.DecodeString(); err != nil { + return err + } + if t.Name, err = d.DecodeString(); err != nil { + return err + } + return nil +} + +func encodeMember(e *msgpack.Encoder, v reflect.Value) error { + m := v.Interface().(Member) + if err := e.EncodeSliceLen(2); err != nil { + return err + } + if err := e.EncodeString(m.Name); err != nil { + return err + } + if err := e.EncodeUint(m.Val); err != nil { + return err + } + return nil +} + +func decodeMember(d *msgpack.Decoder, v reflect.Value) error { + var err error + var l int + m := v.Addr().Interface().(*Member) + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 2 { + return fmt.Errorf("array len doesn't match: %d", l) + } + if m.Name, err = d.DecodeString(); err != nil { + return err + } + if m.Val, err = d.DecodeUint(); err != nil { + return err + } + return nil +} + +func encodeTuple2(e *msgpack.Encoder, v reflect.Value) error { + c := v.Interface().(Tuple2) + if err := e.EncodeSliceLen(3); err != nil { + return err + } + if err := e.EncodeUint(c.Cid); err != nil { + return err + } + if err := e.EncodeString(c.Orig); err != nil { + return err + } + if err := e.EncodeSliceLen(len(c.Members)); err != nil { + return err + } + for _, m := range c.Members { + e.Encode(m) + } + return nil +} + +func decodeTuple2(d *msgpack.Decoder, v reflect.Value) error { + var err error + var l int + c := v.Addr().Interface().(*Tuple2) + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 3 { + return fmt.Errorf("array len doesn't match: %d", l) + } + if c.Cid, err = d.DecodeUint(); err != nil { + return err + } + if c.Orig, err = d.DecodeString(); err != nil { + return err + } + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + c.Members = make([]Member, l) + for i := 0; i < l; i++ { + d.Decode(&c.Members[i]) + } + return nil +} + func init() { - msgpack.Register(reflect.TypeOf(new(tuple)).Elem(), encodeTuple, decodeTuple) + msgpack.Register(reflect.TypeOf(Tuple{}), encodeTuple, decodeTuple) + msgpack.Register(reflect.TypeOf(Tuple2{}), encodeTuple2, decodeTuple2) + msgpack.Register(reflect.TypeOf(Member{}), encodeMember, decodeMember) } var server = "127.0.0.1:3013" @@ -79,45 +212,6 @@ func BenchmarkClientFuture(b *testing.B) { } } -func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { - t := v.Interface().(tuple) - if err := e.EncodeSliceLen(3); err != nil { - return err - } - if err := e.EncodeUint(t.Id); err != nil { - return err - } - if err := e.EncodeString(t.Msg); err != nil { - return err - } - if err := e.EncodeString(t.Name); err != nil { - return err - } - return nil -} - -func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { - var err error - var l int - t := v.Addr().Interface().(*tuple) - if l, err = d.DecodeSliceLen(); err != nil { - return err - } - if l != 3 { - return fmt.Errorf("array len doesn't match: %d", l) - } - if t.Id, err = d.DecodeUint(); err != nil { - return err - } - if t.Msg, err = d.DecodeString(); err != nil { - return err - } - if t.Name, err = d.DecodeString(); err != nil { - return err - } - return nil -} - func BenchmarkClientFutureTyped(b *testing.B) { var err error @@ -138,7 +232,7 @@ func BenchmarkClientFutureTyped(b *testing.B) { fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) } for j := 0; j < N; j++ { - var r []tuple + var r []Tuple err = fs[j].GetTyped(&r) if err != nil { b.Error(err) @@ -209,7 +303,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { } exit = j < N for j > 0 { - var r []tuple + var r []Tuple j-- err = fs[j].GetTyped(&r) if err != nil { @@ -399,19 +493,21 @@ func TestClient(t *testing.T) { } // Upsert - resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) - if err != nil { - t.Errorf("Failed to Upsert (insert): %s", err.Error()) - } - if resp == nil { - t.Errorf("Response is nil after Upsert (insert)") - } - resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) - if err != nil { - t.Errorf("Failed to Upsert (update): %s", err.Error()) - } - if resp == nil { - t.Errorf("Response is nil after Upsert (update)") + if strings.Compare(conn.Greeting.Version, "Tarantool 1.6.7") >= 0 { + resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + if err != nil { + t.Errorf("Failed to Upsert (insert): %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Upsert (insert)") + } + resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + if err != nil { + t.Errorf("Failed to Upsert (update): %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Upsert (update)") + } } // Select @@ -455,7 +551,7 @@ func TestClient(t *testing.T) { } // Select Typed - var tpl []tuple + var tpl []Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}, &tpl) if err != nil { t.Errorf("Failed to SelectTyped: %s", err.Error()) @@ -469,7 +565,7 @@ func TestClient(t *testing.T) { } // Select Typed Empty - var tpl2 []tuple + var tpl2 []Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}, &tpl2) if err != nil { t.Errorf("Failed to SelectTyped: %s", err.Error()) @@ -738,19 +834,21 @@ func TestClientNamed(t *testing.T) { } // Upsert - resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) - if err != nil { - t.Errorf("Failed to Upsert (insert): %s", err.Error()) - } - if resp == nil { - t.Errorf("Response is nil after Upsert (insert)") - } - resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) - if err != nil { - t.Errorf("Failed to Upsert (update): %s", err.Error()) - } - if resp == nil { - t.Errorf("Response is nil after Upsert (update)") + if strings.Compare(conn.Greeting.Version, "Tarantool 1.6.7") >= 0 { + resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + if err != nil { + t.Errorf("Failed to Upsert (insert): %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Upsert (insert)") + } + resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + if err != nil { + t.Errorf("Failed to Upsert (update): %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Upsert (update)") + } } // Select @@ -769,7 +867,7 @@ func TestClientNamed(t *testing.T) { } // Select Typed - var tpl []tuple + var tpl []Tuple err = conn.SelectTyped(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)}, &tpl) if err != nil { t.Errorf("Failed to SelectTyped: %s", err.Error()) @@ -778,3 +876,42 @@ func TestClientNamed(t *testing.T) { t.Errorf("Result len of SelectTyped != 1") } } + +func TestComplexStructs(t *testing.T) { + var err error + var conn *Connection + + conn, err = Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + + tuple := Tuple2{777, "orig", []Member{{"lol", "", 1}, {"wut", "", 3}}} + _, err = conn.Replace(spaceNo, tuple) + if err != nil { + t.Errorf("Failed to insert: %s", err.Error()) + return + } + + var tuples []Tuple2 + err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{777}, &tuples) + if err != nil { + t.Errorf("Failed to selectTyped: %s", err.Error()) + return + } + + if len(tuples) != 1 { + t.Errorf("Failed to selectTyped: unexpected array length %d", len(tuples)) + return + } + + if tuple.Cid != tuples[0].Cid || len(tuple.Members) != len(tuples[0].Members) || tuple.Members[1].Name != tuples[0].Members[1].Name { + t.Errorf("Failed to selectTyped: incorrect data") + return + } +} From 65453b3c4912184da93cef5846e2cb957c526c71 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 6 Jul 2016 19:13:37 +0300 Subject: [PATCH 072/605] all drivers should cleanup 0x8000 from non-zero return code BTW, it will be more complex if we wish act as replica :-( (see python driver) --- const.go | 1 + response.go | 1 + 2 files changed, 2 insertions(+) diff --git a/const.go b/const.go index c9ebf089f..08d620f63 100644 --- a/const.go +++ b/const.go @@ -42,6 +42,7 @@ const ( IterBitsAllNotSet = uint32(9) // all bits are not set OkCode = uint32(0) + ErrorCodeBit = 0x8000 PacketLengthBytes = 5 ) diff --git a/response.go b/response.go index 394278d2d..96953a72a 100644 --- a/response.go +++ b/response.go @@ -100,6 +100,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { } } if resp.Code != OkCode { + resp.Code &^= ErrorCodeBit err = Error{resp.Code, resp.Error} } } From d39d7d799d524abc45ee924fde05adc4d2eb967a Mon Sep 17 00:00:00 2001 From: Kostya Vasilyev Date: Tue, 19 Jul 2016 20:28:38 +0300 Subject: [PATCH 073/605] Make possible to pass Space and Index objects into Select etc. --- schema.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/schema.go b/schema.go index eb1dfd16f..4dfc75b80 100644 --- a/schema.go +++ b/schema.go @@ -196,6 +196,10 @@ func (schema *Schema) resolveSpaceIndex(s interface{}, i interface{}) (spaceNo, spaceNo = uint32(s) case int8: spaceNo = uint32(s) + case Space: + spaceNo = s.Id + case *Space: + spaceNo = s.Id default: panic("unexpected type of space param") } @@ -234,6 +238,10 @@ func (schema *Schema) resolveSpaceIndex(s interface{}, i interface{}) (spaceNo, indexNo = uint32(i) case int8: indexNo = uint32(i) + case Index: + indexNo = i.Id + case *Index: + indexNo = i.Id default: panic("unexpected type of index param") } From 7a9e35fd8c2fcbfe63b4ead063958a82c2f9331e Mon Sep 17 00:00:00 2001 From: Kostya Vasilyev Date: Tue, 19 Jul 2016 20:36:31 +0300 Subject: [PATCH 074/605] Fixed code snippet showing index resolution (index is a property of space, not of schema) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 84e0647f0..8f253380f 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,8 @@ func main() { fmt.Printf("Space %d %d\n", space1.FieldsCount, space1.Temporary) // access index information by name or id - index1 := schema.Indexes["some_index"] - index2 := schema.IndexesById[2] // it's a map + index1 := space1.Indexes["some_index"] + index2 := space1.IndexesById[2] // it's a map fmt.Printf("Index %d %s\n", index1.Id, index1.Name) // access index fields information by index From 7a83058f3a7f3a694c5362cb93ae224d9420c917 Mon Sep 17 00:00:00 2001 From: Kostya Vasilyev Date: Tue, 19 Jul 2016 20:54:36 +0300 Subject: [PATCH 075/605] Added CallTyped, just like SelectTyped, type safety and convenience for function calls --- README.md | 12 ++++++++++-- request.go | 7 +++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8f253380f..ad492836b 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ func main() { fmt.Printf("SpaceField %s %s\n", spaceField1.Name, spaceField1.Type) ``` -## Custom (un)packing and typed selects +## Custom (un)packing and typed selects and function calls It's possible to specify custom pack/unpack functions for your types. It will allow you to store complex structures inside a tuple and may speed up you requests. ```go @@ -260,7 +260,15 @@ func main() { var tuples []Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{777}, &tuples) if err != nil { - t.Errorf("Failed to selectTyped: %s", err.Error()) + t.Errorf("Failed to SelectTyped: %s", err.Error()) + return + } + + // call function 'func_name' returning a table of custom tuples + var tuples2 []Tuple + err = client.CallTyped("func_name", []interface{}{1, 2, 3}, &tuples) + if err != nil { + t.Errorf("Failed to CallTyped: %s", err.Error()) return } } diff --git a/request.go b/request.go index 4ec1db0b9..9963cbee0 100644 --- a/request.go +++ b/request.go @@ -143,6 +143,13 @@ func (conn *Connection) Call(functionName string, args []interface{}) (resp *Res return } +func (conn *Connection) CallTyped(functionName string, args []interface{}, result interface{}) (err error) { + request := conn.NewRequest(CallRequest) + request.body[KeyFunctionName] = functionName + request.body[KeyTuple] = args + return request.performTyped(result) +} + func (conn *Connection) Eval(expr string, args []interface{}) (resp *Response, err error) { request := conn.NewRequest(EvalRequest) request.body[KeyExpression] = expr From a079f04003981f615a38839848acd67dd2bcec29 Mon Sep 17 00:00:00 2001 From: Kostya Vasilyev Date: Fri, 22 Jul 2016 07:16:36 +0300 Subject: [PATCH 076/605] Another convenience method: UpdateTyped --- request.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/request.go b/request.go index 9963cbee0..e246e5db1 100644 --- a/request.go +++ b/request.go @@ -122,6 +122,17 @@ func (conn *Connection) Update(space, index interface{}, key, ops []interface{}) return } +func (conn *Connection) UpdateTyped(space, index interface{}, key, ops []interface{}, result interface{}) (err error) { + request := conn.NewRequest(UpdateRequest) + spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + if err != nil { + return + } + request.fillSearch(spaceNo, indexNo, key) + request.body[KeyTuple] = ops + return request.performTyped(result) +} + func (conn *Connection) Upsert(space interface{}, tuple, ops []interface{}) (resp *Response, err error) { request := conn.NewRequest(UpsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) From 89bf02b938637d8c3369ab79dfb07f2a3796a073 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Fri, 22 Jul 2016 20:07:00 +0300 Subject: [PATCH 077/605] ClientError type to handle reconnect errors --- connection.go | 27 ++++++++++++++++++--------- const.go | 7 +++++++ errors.go | 20 +++++++++++++++++++- request.go | 9 ++++++--- tarantool_test.go | 1 - 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/connection.go b/connection.go index 34e122edd..b9fd8f2fd 100644 --- a/connection.go +++ b/connection.go @@ -66,6 +66,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { go conn.writer() go conn.reader() + // TODO: reload schema after reconnect if err = conn.loadSchema(); err != nil { conn.closeConnection(err) return nil, err @@ -74,11 +75,9 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { return conn, err } -func (conn *Connection) Close() (err error) { - conn.closed = true - close(conn.control) - err = conn.closeConnection(errors.New("client closed connection")) - return +func (conn *Connection) Close() error { + err := ClientError{ErrConnectionClosed, "connection closed by client"} + return conn.closeConnectionForever(err) } func (conn *Connection) RemoteAddr() string { @@ -186,7 +185,7 @@ func (conn *Connection) createConnection() (r *bufio.Reader, w *bufio.Writer, er conn.mutex.Lock() defer conn.mutex.Unlock() if conn.closed { - err = errors.New("connection already closed") + err = ClientError{ErrConnectionClosed, "using closed connection"} return } if conn.c == nil { @@ -198,6 +197,9 @@ func (conn *Connection) createConnection() (r *bufio.Reader, w *bufio.Writer, er } else if conn.opts.Reconnect > 0 { if conn.opts.MaxReconnects > 0 && reconnects > conn.opts.MaxReconnects { log.Printf("tarantool: last reconnect to %s failed: %s, giving it up.\n", conn.addr, err.Error()) + err = ClientError{ErrConnectionClosed, "last reconnect failed"} + // mark connection as closed to avoid reopening by another goroutine + conn.closed = true return } else { log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s\n", reconnects, conn.opts.MaxReconnects, conn.addr, err.Error()) @@ -233,10 +235,16 @@ func (conn *Connection) closeConnection(neterr error) (err error) { return } +func (conn *Connection) closeConnectionForever(err error) error { + conn.closed = true + close(conn.control) + return conn.closeConnection(err) +} + func (conn *Connection) writer() { var w *bufio.Writer var err error - for { + for !conn.closed { var packet []byte select { case packet = <-conn.packets: @@ -257,6 +265,7 @@ func (conn *Connection) writer() { } if w = conn.w; w == nil { if _, w, err = conn.createConnection(); err != nil { + conn.closeConnectionForever(err) return } } @@ -270,9 +279,10 @@ func (conn *Connection) writer() { func (conn *Connection) reader() { var r *bufio.Reader var err error - for { + for !conn.closed { if r = conn.r; r == nil { if r, _, err = conn.createConnection(); err != nil { + conn.closeConnectionForever(err) return } } @@ -283,7 +293,6 @@ func (conn *Connection) reader() { } resp := Response{buf: smallBuf{b: resp_bytes}} err = resp.decodeHeader() - //resp, err := newResponse(resp_bytes) if err != nil { conn.closeConnection(err) continue diff --git a/const.go b/const.go index c9ebf089f..0d8d1eb34 100644 --- a/const.go +++ b/const.go @@ -45,6 +45,13 @@ const ( PacketLengthBytes = 5 ) +// Tarantool client error codes +const ( + ErrConnectionNotReady = 0x4000 + iota + ErrConnectionClosed = 0x4000 + iota + ErrProtocolError = 0x4000 + iota +) + // Tarantool server error codes const ( ErrUnknown = 0x8000 + iota // Unknown error diff --git a/errors.go b/errors.go index 2178fa4b4..6a978cd9e 100644 --- a/errors.go +++ b/errors.go @@ -9,6 +9,24 @@ type Error struct { Msg string } -func (tnterr Error) Error() (string) { +func (tnterr Error) Error() string { return fmt.Sprintf("%s (0x%x)", tnterr.Msg, tnterr.Code) } + +type ClientError struct { + Code uint32 + Msg string +} + +func (clierr ClientError) Error() string { + return fmt.Sprintf("%s (0x%x)", clierr.Msg, clierr.Code) +} + +func (clierr ClientError) Temporary() bool { + switch clierr.Code { + case ErrConnectionNotReady: + return true + default: + return false + } +} diff --git a/request.go b/request.go index 4ec1db0b9..0712d6b68 100644 --- a/request.go +++ b/request.go @@ -1,7 +1,6 @@ package tarantool import ( - "errors" "fmt" "gopkg.in/vmihailenco/msgpack.v2" "time" @@ -287,8 +286,12 @@ func (req *Request) future() (fut *Future) { fut = &Future{req: req} // check connection ready to process packets + if closed := req.conn.closed; closed { + fut.err = ClientError{ErrConnectionClosed, "using closed connection"} + return + } if c := req.conn.c; c == nil { - fut.err = errors.New("client connection is not ready") + fut.err = ClientError{ErrConnectionNotReady, "client connection is not ready"} return } @@ -300,7 +303,7 @@ func (req *Request) future() (fut *Future) { req.conn.mutex.Lock() if req.conn.closed { req.conn.mutex.Unlock() - fut.err = errors.New("using closed connection") + fut.err = ClientError{ErrConnectionClosed, "using closed connection"} return } req.conn.requests[req.requestId] = fut diff --git a/tarantool_test.go b/tarantool_test.go index fcb6c0dc3..9806e34fb 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -179,7 +179,6 @@ func BenchmarkClientSerial(b *testing.B) { if err != nil { b.Errorf("No connection available") } - } } From 49b90d6921c5c993c9c5928f87453762eae68ab9 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Sat, 23 Jul 2016 19:41:09 +0300 Subject: [PATCH 078/605] More Typed methods --- request.go | 101 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 25 deletions(-) diff --git a/request.go b/request.go index e246e5db1..ed27cbab3 100644 --- a/request.go +++ b/request.go @@ -66,17 +66,6 @@ func (conn *Connection) Select(space, index interface{}, offset, limit, iterator return } -func (conn *Connection) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key []interface{}, result interface{}) (err error) { - request := conn.NewRequest(SelectRequest) - spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) - if err != nil { - return - } - request.fillSearch(spaceNo, indexNo, key) - request.fillIterator(offset, limit, iterator) - return request.performTyped(result) -} - func (conn *Connection) Insert(space interface{}, tuple interface{}) (resp *Response, err error) { request := conn.NewRequest(InsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) @@ -122,17 +111,6 @@ func (conn *Connection) Update(space, index interface{}, key, ops []interface{}) return } -func (conn *Connection) UpdateTyped(space, index interface{}, key, ops []interface{}, result interface{}) (err error) { - request := conn.NewRequest(UpdateRequest) - spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) - if err != nil { - return - } - request.fillSearch(spaceNo, indexNo, key) - request.body[KeyTuple] = ops - return request.performTyped(result) -} - func (conn *Connection) Upsert(space interface{}, tuple, ops []interface{}) (resp *Response, err error) { request := conn.NewRequest(UpsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) @@ -154,6 +132,79 @@ func (conn *Connection) Call(functionName string, args []interface{}) (resp *Res return } +func (conn *Connection) Eval(expr string, args []interface{}) (resp *Response, err error) { + request := conn.NewRequest(EvalRequest) + request.body[KeyExpression] = expr + request.body[KeyTuple] = args + resp, err = request.perform() + return +} + +// Typed methods +func (conn *Connection) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key []interface{}, result interface{}) (err error) { + request := conn.NewRequest(SelectRequest) + spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + if err != nil { + return + } + request.fillSearch(spaceNo, indexNo, key) + request.fillIterator(offset, limit, iterator) + return request.performTyped(result) +} + +func (conn *Connection) InsertTyped(space interface{}, tuple interface{}, result interface{}) (err error) { + request := conn.NewRequest(InsertRequest) + spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) + if err != nil { + return + } + request.fillInsert(spaceNo, tuple) + return request.performTyped(result) +} + +func (conn *Connection) ReplaceTyped(space interface{}, tuple interface{}, result interface{}) (err error) { + request := conn.NewRequest(ReplaceRequest) + spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) + if err != nil { + return + } + request.fillInsert(spaceNo, tuple) + return request.performTyped(result) +} + +func (conn *Connection) DeleteTyped(space, index interface{}, key []interface{}, result interface{}) (err error) { + request := conn.NewRequest(DeleteRequest) + spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + if err != nil { + return + } + request.fillSearch(spaceNo, indexNo, key) + return request.performTyped(result) +} + +func (conn *Connection) UpdateTyped(space, index interface{}, key, ops []interface{}, result interface{}) (err error) { + request := conn.NewRequest(UpdateRequest) + spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + if err != nil { + return + } + request.fillSearch(spaceNo, indexNo, key) + request.body[KeyTuple] = ops + return request.performTyped(result) +} + +func (conn *Connection) UpsertTyped(space interface{}, tuple, ops []interface{}, result interface{}) (err error) { + request := conn.NewRequest(UpsertRequest) + spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) + if err != nil { + return + } + request.body[KeySpaceNo] = spaceNo + request.body[KeyTuple] = tuple + request.body[KeyDefTuple] = ops + return request.performTyped(result) +} + func (conn *Connection) CallTyped(functionName string, args []interface{}, result interface{}) (err error) { request := conn.NewRequest(CallRequest) request.body[KeyFunctionName] = functionName @@ -161,14 +212,14 @@ func (conn *Connection) CallTyped(functionName string, args []interface{}, resul return request.performTyped(result) } -func (conn *Connection) Eval(expr string, args []interface{}) (resp *Response, err error) { +func (conn *Connection) EvalTyped(expr string, args []interface{}, result interface{}) (err error) { request := conn.NewRequest(EvalRequest) request.body[KeyExpression] = expr request.body[KeyTuple] = args - resp, err = request.perform() - return + return request.performTyped(result) } +// Async methods func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key []interface{}) *Future { request := conn.NewRequest(SelectRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) From 6ef9974960e5c173c7c3a51d022f511b1965c75c Mon Sep 17 00:00:00 2001 From: Shveenkov Dmitrij Date: Mon, 29 Aug 2016 14:12:37 +0300 Subject: [PATCH 079/605] add runtime.Gosched for decreasing writer.flush count --- connection.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/connection.go b/connection.go index b9fd8f2fd..d7008b408 100644 --- a/connection.go +++ b/connection.go @@ -7,6 +7,7 @@ import ( "io" "log" "net" + "runtime" "sync" "sync/atomic" "time" @@ -250,8 +251,11 @@ func (conn *Connection) writer() { case packet = <-conn.packets: default: if w = conn.w; w != nil { - if err := w.Flush(); err != nil { - conn.closeConnection(err) + runtime.Gosched() + if len(conn.packets) == 0 { + if err := w.Flush(); err != nil { + conn.closeConnection(err) + } } } select { From 3f1c5e91e7c27b627ca5078c251d066aeff73bd7 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Mon, 29 Aug 2016 16:29:20 +0300 Subject: [PATCH 080/605] fix previous commit --- connection.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/connection.go b/connection.go index d7008b408..b7981c75a 100644 --- a/connection.go +++ b/connection.go @@ -250,12 +250,10 @@ func (conn *Connection) writer() { select { case packet = <-conn.packets: default: - if w = conn.w; w != nil { - runtime.Gosched() - if len(conn.packets) == 0 { - if err := w.Flush(); err != nil { - conn.closeConnection(err) - } + runtime.Gosched() + if w = conn.w; len(conn.packets) == 0 && w != nil { + if err := w.Flush(); err != nil { + conn.closeConnection(err) } } select { From d4bc13a3ac3dd206b4c0cd5949982e4bbca576f0 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Mon, 29 Aug 2016 16:39:13 +0300 Subject: [PATCH 081/605] make race detector happier a bit. --- connection.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/connection.go b/connection.go index b7981c75a..705d14394 100644 --- a/connection.go +++ b/connection.go @@ -247,12 +247,18 @@ func (conn *Connection) writer() { var err error for !conn.closed { var packet []byte + if w == nil { + conn.mutex.Lock() + w = conn.w + conn.mutex.Unlock() + } select { case packet = <-conn.packets: default: runtime.Gosched() - if w = conn.w; len(conn.packets) == 0 && w != nil { + if len(conn.packets) == 0 && w != nil { if err := w.Flush(); err != nil { + w = nil conn.closeConnection(err) } } @@ -265,13 +271,14 @@ func (conn *Connection) writer() { if packet == nil { return } - if w = conn.w; w == nil { + if w == nil { if _, w, err = conn.createConnection(); err != nil { conn.closeConnectionForever(err) return } } if err := write(w, packet); err != nil { + w = nil conn.closeConnection(err) continue } @@ -282,7 +289,12 @@ func (conn *Connection) reader() { var r *bufio.Reader var err error for !conn.closed { - if r = conn.r; r == nil { + if r == nil { + conn.mutex.Lock() + r = conn.r + conn.mutex.Unlock() + } + if r == nil { if r, _, err = conn.createConnection(); err != nil { conn.closeConnectionForever(err) return @@ -290,12 +302,14 @@ func (conn *Connection) reader() { } resp_bytes, err := read(r) if err != nil { + r = nil conn.closeConnection(err) continue } resp := Response{buf: smallBuf{b: resp_bytes}} err = resp.decodeHeader() if err != nil { + r = nil conn.closeConnection(err) continue } From c0d7cf0203855b89911cd89fb6a9e4e9c1754bc6 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 1 Sep 2016 14:17:17 +0300 Subject: [PATCH 082/605] more correct use of Connection.closeConnection --- connection.go | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/connection.go b/connection.go index 705d14394..7d9c2a2d6 100644 --- a/connection.go +++ b/connection.go @@ -69,7 +69,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { // TODO: reload schema after reconnect if err = conn.loadSchema(); err != nil { - conn.closeConnection(err) + conn.closeConnection(err, nil, nil) return nil, err } @@ -218,12 +218,18 @@ func (conn *Connection) createConnection() (r *bufio.Reader, w *bufio.Writer, er return } -func (conn *Connection) closeConnection(neterr error) (err error) { +func (conn *Connection) closeConnection(neterr error, w *bufio.Writer, r *bufio.Reader) (ww *bufio.Writer, rr *bufio.Reader, err error) { conn.mutex.Lock() defer conn.mutex.Unlock() if conn.c == nil { return } + if w != nil && w != conn.w { + return conn.w, nil, nil + } + if r != nil && r != conn.r { + return nil, conn.r, nil + } err = conn.c.Close() conn.c = nil conn.r = nil @@ -239,7 +245,8 @@ func (conn *Connection) closeConnection(neterr error) (err error) { func (conn *Connection) closeConnectionForever(err error) error { conn.closed = true close(conn.control) - return conn.closeConnection(err) + _, _, err = conn.closeConnection(err, nil, nil) + return err } func (conn *Connection) writer() { @@ -258,8 +265,7 @@ func (conn *Connection) writer() { runtime.Gosched() if len(conn.packets) == 0 && w != nil { if err := w.Flush(); err != nil { - w = nil - conn.closeConnection(err) + w, _, _ = conn.closeConnection(err, w, nil) } } select { @@ -278,8 +284,7 @@ func (conn *Connection) writer() { } } if err := write(w, packet); err != nil { - w = nil - conn.closeConnection(err) + w, _, _ = conn.closeConnection(err, w, nil) continue } } @@ -302,15 +307,13 @@ func (conn *Connection) reader() { } resp_bytes, err := read(r) if err != nil { - r = nil - conn.closeConnection(err) + _, r, _ = conn.closeConnection(err, nil, r) continue } resp := Response{buf: smallBuf{b: resp_bytes}} err = resp.decodeHeader() if err != nil { - r = nil - conn.closeConnection(err) + _, r, _ = conn.closeConnection(err, nil, r) continue } conn.mutex.Lock() From ce1f22aeaf2381b46a7b5f946971f54172b4541a Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 1 Sep 2016 15:03:50 +0300 Subject: [PATCH 083/605] simplify a bit --- connection.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/connection.go b/connection.go index 7d9c2a2d6..82688c05d 100644 --- a/connection.go +++ b/connection.go @@ -254,11 +254,6 @@ func (conn *Connection) writer() { var err error for !conn.closed { var packet []byte - if w == nil { - conn.mutex.Lock() - w = conn.w - conn.mutex.Unlock() - } select { case packet = <-conn.packets: default: @@ -294,11 +289,6 @@ func (conn *Connection) reader() { var r *bufio.Reader var err error for !conn.closed { - if r == nil { - conn.mutex.Lock() - r = conn.r - conn.mutex.Unlock() - } if r == nil { if r, _, err = conn.createConnection(); err != nil { conn.closeConnectionForever(err) From 66443b186269cd0bb786aa03e7074e28d74ed659 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 1 Sep 2016 15:24:26 +0300 Subject: [PATCH 084/605] ... unify signature --- connection.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/connection.go b/connection.go index 82688c05d..3f73c79f6 100644 --- a/connection.go +++ b/connection.go @@ -218,17 +218,17 @@ func (conn *Connection) createConnection() (r *bufio.Reader, w *bufio.Writer, er return } -func (conn *Connection) closeConnection(neterr error, w *bufio.Writer, r *bufio.Reader) (ww *bufio.Writer, rr *bufio.Reader, err error) { +func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio.Writer) (rr *bufio.Reader, ww *bufio.Writer, err error) { conn.mutex.Lock() defer conn.mutex.Unlock() if conn.c == nil { return } if w != nil && w != conn.w { - return conn.w, nil, nil + return nil, conn.w, nil } if r != nil && r != conn.r { - return nil, conn.r, nil + return conn.r, nil, nil } err = conn.c.Close() conn.c = nil @@ -260,7 +260,7 @@ func (conn *Connection) writer() { runtime.Gosched() if len(conn.packets) == 0 && w != nil { if err := w.Flush(); err != nil { - w, _, _ = conn.closeConnection(err, w, nil) + _, w, _ = conn.closeConnection(err, nil, w) } } select { @@ -279,7 +279,7 @@ func (conn *Connection) writer() { } } if err := write(w, packet); err != nil { - w, _, _ = conn.closeConnection(err, w, nil) + _, w, _ = conn.closeConnection(err, nil, w) continue } } @@ -297,13 +297,13 @@ func (conn *Connection) reader() { } resp_bytes, err := read(r) if err != nil { - _, r, _ = conn.closeConnection(err, nil, r) + r, _, _ = conn.closeConnection(err, r, nil) continue } resp := Response{buf: smallBuf{b: resp_bytes}} err = resp.decodeHeader() if err != nil { - _, r, _ = conn.closeConnection(err, nil, r) + r, _, _ = conn.closeConnection(err, r, nil) continue } conn.mutex.Lock() From 145d07807d5d140cc18d317ffa8f6ded30d6032c Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Mon, 26 Sep 2016 11:54:34 +0300 Subject: [PATCH 085/605] some fiexes and tests --- const.go | 222 ++++++++++++++++++++++++++-------------------------- response.go | 1 + 2 files changed, 112 insertions(+), 111 deletions(-) diff --git a/const.go b/const.go index 08d620f63..d3271462b 100644 --- a/const.go +++ b/const.go @@ -48,115 +48,115 @@ const ( // Tarantool server error codes const ( - ErrUnknown = 0x8000 + iota // Unknown error - ErrIllegalParams = 0x8000 + iota // Illegal parameters, %s - ErrMemoryIssue = 0x8000 + iota // Failed to allocate %u bytes in %s for %s - ErrTupleFound = 0x8000 + iota // Duplicate key exists in unique index '%s' in space '%s' - ErrTupleNotFound = 0x8000 + iota // Tuple doesn't exist in index '%s' in space '%s' - ErrUnsupported = 0x8000 + iota // %s does not support %s - ErrNonmaster = 0x8000 + iota // Can't modify data on a replication slave. My master is: %s - ErrReadonly = 0x8000 + iota // Can't modify data because this server is in read-only mode. - ErrInjection = 0x8000 + iota // Error injection '%s' - ErrCreateSpace = 0x8000 + iota // Failed to create space '%s': %s - ErrSpaceExists = 0x8000 + iota // Space '%s' already exists - ErrDropSpace = 0x8000 + iota // Can't drop space '%s': %s - ErrAlterSpace = 0x8000 + iota // Can't modify space '%s': %s - ErrIndexType = 0x8000 + iota // Unsupported index type supplied for index '%s' in space '%s' - ErrModifyIndex = 0x8000 + iota // Can't create or modify index '%s' in space '%s': %s - ErrLastDrop = 0x8000 + iota // Can't drop the primary key in a system space, space '%s' - ErrTupleFormatLimit = 0x8000 + iota // Tuple format limit reached: %u - ErrDropPrimaryKey = 0x8000 + iota // Can't drop primary key in space '%s' while secondary keys exist - ErrKeyPartType = 0x8000 + iota // Supplied key type of part %u does not match index part type: expected %s - ErrExactMatch = 0x8000 + iota // Invalid key part count in an exact match (expected %u, got %u) - ErrInvalidMsgpack = 0x8000 + iota // Invalid MsgPack - %s - ErrProcRet = 0x8000 + iota // msgpack.encode: can not encode Lua type '%s' - ErrTupleNotArray = 0x8000 + iota // Tuple/Key must be MsgPack array - ErrFieldType = 0x8000 + iota // Tuple field %u type does not match one required by operation: expected %s - ErrFieldTypeMismatch = 0x8000 + iota // Ambiguous field type in index '%s', key part %u. Requested type is %s but the field has previously been defined as %s - ErrSplice = 0x8000 + iota // SPLICE error on field %u: %s - ErrArgType = 0x8000 + iota // Argument type in operation '%c' on field %u does not match field type: expected a %s - ErrTupleIsTooLong = 0x8000 + iota // Tuple is too long %u - ErrUnknownUpdateOp = 0x8000 + iota // Unknown UPDATE operation - ErrUpdateField = 0x8000 + iota // Field %u UPDATE error: %s - ErrFiberStack = 0x8000 + iota // Can not create a new fiber: recursion limit reached - ErrKeyPartCount = 0x8000 + iota // Invalid key part count (expected [0..%u], got %u) - ErrProcLua = 0x8000 + iota // %s - ErrNoSuchProc = 0x8000 + iota // Procedure '%.*s' is not defined - ErrNoSuchTrigger = 0x8000 + iota // Trigger is not found - ErrNoSuchIndex = 0x8000 + iota // No index #%u is defined in space '%s' - ErrNoSuchSpace = 0x8000 + iota // Space '%s' does not exist - ErrNoSuchField = 0x8000 + iota // Field %d was not found in the tuple - ErrSpaceFieldCount = 0x8000 + iota // Tuple field count %u does not match space '%s' field count %u - ErrIndexFieldCount = 0x8000 + iota // Tuple field count %u is less than required by a defined index (expected %u) - ErrWalIo = 0x8000 + iota // Failed to write to disk - ErrMoreThanOneTuple = 0x8000 + iota // More than one tuple found by get() - ErrAccessDenied = 0x8000 + iota // %s access denied for user '%s' - ErrCreateUser = 0x8000 + iota // Failed to create user '%s': %s - ErrDropUser = 0x8000 + iota // Failed to drop user '%s': %s - ErrNoSuchUser = 0x8000 + iota // User '%s' is not found - ErrUserExists = 0x8000 + iota // User '%s' already exists - ErrPasswordMismatch = 0x8000 + iota // Incorrect password supplied for user '%s' - ErrUnknownRequestType = 0x8000 + iota // Unknown request type %u - ErrUnknownSchemaObject = 0x8000 + iota // Unknown object type '%s' - ErrCreateFunction = 0x8000 + iota // Failed to create function '%s': %s - ErrNoSuchFunction = 0x8000 + iota // Function '%s' does not exist - ErrFunctionExists = 0x8000 + iota // Function '%s' already exists - ErrFunctionAccessDenied = 0x8000 + iota // %s access denied for user '%s' to function '%s' - ErrFunctionMax = 0x8000 + iota // A limit on the total number of functions has been reached: %u - ErrSpaceAccessDenied = 0x8000 + iota // %s access denied for user '%s' to space '%s' - ErrUserMax = 0x8000 + iota // A limit on the total number of users has been reached: %u - ErrNoSuchEngine = 0x8000 + iota // Space engine '%s' does not exist - ErrReloadCfg = 0x8000 + iota // Can't set option '%s' dynamically - ErrCfg = 0x8000 + iota // Incorrect value for option '%s': %s - ErrSophia = 0x8000 + iota // %s - ErrLocalServerIsNotActive = 0x8000 + iota // Local server is not active - ErrUnknownServer = 0x8000 + iota // Server %s is not registered with the cluster - ErrClusterIdMismatch = 0x8000 + iota // Cluster id of the replica %s doesn't match cluster id of the master %s - ErrInvalidUuid = 0x8000 + iota // Invalid UUID: %s - ErrClusterIdIsRo = 0x8000 + iota // Can't reset cluster id: it is already assigned - ErrReserved66 = 0x8000 + iota // Reserved66 - ErrServerIdIsReserved = 0x8000 + iota // Can't initialize server id with a reserved value %u - ErrInvalidOrder = 0x8000 + iota // Invalid LSN order for server %u: previous LSN = %llu, new lsn = %llu - ErrMissingRequestField = 0x8000 + iota // Missing mandatory field '%s' in request - ErrIdentifier = 0x8000 + iota // Invalid identifier '%s' (expected letters, digits or an underscore) - ErrDropFunction = 0x8000 + iota // Can't drop function %u: %s - ErrIteratorType = 0x8000 + iota // Unknown iterator type '%s' - ErrReplicaMax = 0x8000 + iota // Replica count limit reached: %u - ErrInvalidXlog = 0x8000 + iota // Failed to read xlog: %lld - ErrInvalidXlogName = 0x8000 + iota // Invalid xlog name: expected %lld got %lld - ErrInvalidXlogOrder = 0x8000 + iota // Invalid xlog order: %lld and %lld - ErrNoConnection = 0x8000 + iota // Connection is not established - ErrTimeout = 0x8000 + iota // Timeout exceeded - ErrActiveTransaction = 0x8000 + iota // Operation is not permitted when there is an active transaction - ErrNoActiveTransaction = 0x8000 + iota // Operation is not permitted when there is no active transaction - ErrCrossEngineTransaction = 0x8000 + iota // A multi-statement transaction can not use multiple storage engines - ErrNoSuchRole = 0x8000 + iota // Role '%s' is not found - ErrRoleExists = 0x8000 + iota // Role '%s' already exists - ErrCreateRole = 0x8000 + iota // Failed to create role '%s': %s - ErrIndexExists = 0x8000 + iota // Index '%s' already exists - ErrTupleRefOverflow = 0x8000 + iota // Tuple reference counter overflow - ErrRoleLoop = 0x8000 + iota // Granting role '%s' to role '%s' would create a loop - ErrGrant = 0x8000 + iota // Incorrect grant arguments: %s - ErrPrivGranted = 0x8000 + iota // User '%s' already has %s access on %s '%s' - ErrRoleGranted = 0x8000 + iota // User '%s' already has role '%s' - ErrPrivNotGranted = 0x8000 + iota // User '%s' does not have %s access on %s '%s' - ErrRoleNotGranted = 0x8000 + iota // User '%s' does not have role '%s' - ErrMissingSnapshot = 0x8000 + iota // Can't find snapshot - ErrCantUpdatePrimaryKey = 0x8000 + iota // Attempt to modify a tuple field which is part of index '%s' in space '%s' - ErrUpdateIntegerOverflow = 0x8000 + iota // Integer overflow when performing '%c' operation on field %u - ErrGuestUserPassword = 0x8000 + iota // Setting password for guest user has no effect - ErrTransactionConflict = 0x8000 + iota // Transaction has been aborted by conflict - ErrUnsupportedRolePriv = 0x8000 + iota // Unsupported role privilege '%s' - ErrLoadFunction = 0x8000 + iota // Failed to dynamically load function '%s': %s - ErrFunctionLanguage = 0x8000 + iota // Unsupported language '%s' specified for function '%s' - ErrRtreeRect = 0x8000 + iota // RTree: %s must be an array with %u (point) or %u (rectangle/box) numeric coordinates - ErrProcC = 0x8000 + iota // ??? - ErrUnknownRtreeIndexDistanceType = 0x8000 + iota //Unknown RTREE index distance type %s - ErrProtocol = 0x8000 + iota // %s - ErrUpsertUniqueSecondaryKey = 0x8000 + iota // Space %s has a unique secondary index and does not support UPSERT - ErrWrongIndexRecord = 0x8000 + iota // Wrong record in _index space: got {%s}, expected {%s} - ErrWrongIndexParts = 0x8000 + iota // Wrong index parts (field %u): %s; expected field1 id (number), field1 type (string), ... - ErrWrongIndexOptions = 0x8000 + iota // Wrong index options (field %u): %s - ErrWrongSchemaVaersion = 0x8000 + iota // Wrong schema version, current: %d, in request: %u - ErrSlabAllocMax = 0x8000 + iota // Failed to allocate %u bytes for tuple in the slab allocator: tuple is too large. Check 'slab_alloc_maximal' configuration option. + ErrUnknown = 0 // Unknown error + ErrIllegalParams = 1 // Illegal parameters, %s + ErrMemoryIssue = 2 // Failed to allocate %u bytes in %s for %s + ErrTupleFound = 3 // Duplicate key exists in unique index '%s' in space '%s' + ErrTupleNotFound = 4 // Tuple doesn't exist in index '%s' in space '%s' + ErrUnsupported = 5 // %s does not support %s + ErrNonmaster = 6 // Can't modify data on a replication slave. My master is: %s + ErrReadonly = 7 // Can't modify data because this server is in read-only mode. + ErrInjection = 8 // Error injection '%s' + ErrCreateSpace = 9 // Failed to create space '%s': %s + ErrSpaceExists = 10 // Space '%s' already exists + ErrDropSpace = 11 // Can't drop space '%s': %s + ErrAlterSpace = 12 // Can't modify space '%s': %s + ErrIndexType = 13 // Unsupported index type supplied for index '%s' in space '%s' + ErrModifyIndex = 14 // Can't create or modify index '%s' in space '%s': %s + ErrLastDrop = 15 // Can't drop the primary key in a system space, space '%s' + ErrTupleFormatLimit = 16 // Tuple format limit reached: %u + ErrDropPrimaryKey = 17 // Can't drop primary key in space '%s' while secondary keys exist + ErrKeyPartType = 18 // Supplied key type of part %u does not match index part type: expected %s + ErrExactMatch = 19 // Invalid key part count in an exact match (expected %u, got %u) + ErrInvalidMsgpack = 20 // Invalid MsgPack - %s + ErrProcRet = 21 // msgpack.encode: can not encode Lua type '%s' + ErrTupleNotArray = 22 // Tuple/Key must be MsgPack array + ErrFieldType = 23 // Tuple field %u type does not match one required by operation: expected %s + ErrFieldTypeMismatch = 24 // Ambiguous field type in index '%s', key part %u. Requested type is %s but the field has previously been defined as %s + ErrSplice = 25 // SPLICE error on field %u: %s + ErrArgType = 26 // Argument type in operation '%c' on field %u does not match field type: expected a %s + ErrTupleIsTooLong = 27 // Tuple is too long %u + ErrUnknownUpdateOp = 28 // Unknown UPDATE operation + ErrUpdateField = 29 // Field %u UPDATE error: %s + ErrFiberStack = 30 // Can not create a new fiber: recursion limit reached + ErrKeyPartCount = 31 // Invalid key part count (expected [0..%u], got %u) + ErrProcLua = 32 // %s + ErrNoSuchProc = 33 // Procedure '%.*s' is not defined + ErrNoSuchTrigger = 34 // Trigger is not found + ErrNoSuchIndex = 35 // No index #%u is defined in space '%s' + ErrNoSuchSpace = 36 // Space '%s' does not exist + ErrNoSuchField = 37 // Field %d was not found in the tuple + ErrSpaceFieldCount = 38 // Tuple field count %u does not match space '%s' field count %u + ErrIndexFieldCount = 39 // Tuple field count %u is less than required by a defined index (expected %u) + ErrWalIo = 40 // Failed to write to disk + ErrMoreThanOneTuple = 41 // More than one tuple found by get() + ErrAccessDenied = 42 // %s access denied for user '%s' + ErrCreateUser = 43 // Failed to create user '%s': %s + ErrDropUser = 44 // Failed to drop user '%s': %s + ErrNoSuchUser = 45 // User '%s' is not found + ErrUserExists = 46 // User '%s' already exists + ErrPasswordMismatch = 47 // Incorrect password supplied for user '%s' + ErrUnknownRequestType = 48 // Unknown request type %u + ErrUnknownSchemaObject = 49 // Unknown object type '%s' + ErrCreateFunction = 50 // Failed to create function '%s': %s + ErrNoSuchFunction = 51 // Function '%s' does not exist + ErrFunctionExists = 52 // Function '%s' already exists + ErrFunctionAccessDenied = 53 // %s access denied for user '%s' to function '%s' + ErrFunctionMax = 54 // A limit on the total number of functions has been reached: %u + ErrSpaceAccessDenied = 55 // %s access denied for user '%s' to space '%s' + ErrUserMax = 56 // A limit on the total number of users has been reached: %u + ErrNoSuchEngine = 57 // Space engine '%s' does not exist + ErrReloadCfg = 58 // Can't set option '%s' dynamically + ErrCfg = 59 // Incorrect value for option '%s': %s + ErrSophia = 60 // %s + ErrLocalServerIsNotActive = 61 // Local server is not active + ErrUnknownServer = 62 // Server %s is not registered with the cluster + ErrClusterIdMismatch = 63 // Cluster id of the replica %s doesn't match cluster id of the master %s + ErrInvalidUuid = 64 // Invalid UUID: %s + ErrClusterIdIsRo = 65 // Can't reset cluster id: it is already assigned + ErrReserved66 = 66 // Reserved66 + ErrServerIdIsReserved = 67 // Can't initialize server id with a reserved value %u + ErrInvalidOrder = 68 // Invalid LSN order for server %u: previous LSN = %llu, new lsn = %llu + ErrMissingRequestField = 69 // Missing mandatory field '%s' in request + ErrIdentifier = 70 // Invalid identifier '%s' (expected letters, digits or an underscore) + ErrDropFunction = 71 // Can't drop function %u: %s + ErrIteratorType = 72 // Unknown iterator type '%s' + ErrReplicaMax = 73 // Replica count limit reached: %u + ErrInvalidXlog = 74 // Failed to read xlog: %lld + ErrInvalidXlogName = 75 // Invalid xlog name: expected %lld got %lld + ErrInvalidXlogOrder = 76 // Invalid xlog order: %lld and %lld + ErrNoConnection = 77 // Connection is not established + ErrTimeout = 78 // Timeout exceeded + ErrActiveTransaction = 79 // Operation is not permitted when there is an active transaction + ErrNoActiveTransaction = 80 // Operation is not permitted when there is no active transaction + ErrCrossEngineTransaction = 81 // A multi-statement transaction can not use multiple storage engines + ErrNoSuchRole = 82 // Role '%s' is not found + ErrRoleExists = 83 // Role '%s' already exists + ErrCreateRole = 84 // Failed to create role '%s': %s + ErrIndexExists = 85 // Index '%s' already exists + ErrTupleRefOverflow = 86 // Tuple reference counter overflow + ErrRoleLoop = 87 // Granting role '%s' to role '%s' would create a loop + ErrGrant = 88 // Incorrect grant arguments: %s + ErrPrivGranted = 89 // User '%s' already has %s access on %s '%s' + ErrRoleGranted = 90 // User '%s' already has role '%s' + ErrPrivNotGranted = 91 // User '%s' does not have %s access on %s '%s' + ErrRoleNotGranted = 92 // User '%s' does not have role '%s' + ErrMissingSnapshot = 93 // Can't find snapshot + ErrCantUpdatePrimaryKey = 94 // Attempt to modify a tuple field which is part of index '%s' in space '%s' + ErrUpdateIntegerOverflow = 95 // Integer overflow when performing '%c' operation on field %u + ErrGuestUserPassword = 96 // Setting password for guest user has no effect + ErrTransactionConflict = 97 // Transaction has been aborted by conflict + ErrUnsupportedRolePriv = 98 // Unsupported role privilege '%s' + ErrLoadFunction = 99 // Failed to dynamically load function '%s': %s + ErrFunctionLanguage = 100 // Unsupported language '%s' specified for function '%s' + ErrRtreeRect = 101 // RTree: %s must be an array with %u (point) or %u (rectangle/box) numeric coordinates + ErrProcC = 102 // ??? + ErrUnknownRtreeIndexDistanceType = 103 //Unknown RTREE index distance type %s + ErrProtocol = 104 // %s + ErrUpsertUniqueSecondaryKey = 105 // Space %s has a unique secondary index and does not support UPSERT + ErrWrongIndexRecord = 106 // Wrong record in _index space: got {%s}, expected {%s} + ErrWrongIndexParts = 107 // Wrong index parts (field %u): %s; expected field1 id (number), field1 type (string), ... + ErrWrongIndexOptions = 108 // Wrong index options (field %u): %s + ErrWrongSchemaVaersion = 109 // Wrong schema version, current: %d, in request: %u + ErrSlabAllocMax = 110 // Failed to allocate %u bytes for tuple in the slab allocator: tuple is too large. Check 'slab_alloc_maximal' configuration option. ) diff --git a/response.go b/response.go index 96953a72a..caac6c56d 100644 --- a/response.go +++ b/response.go @@ -66,6 +66,7 @@ func (resp *Response) decodeBody() (err error) { resp.Error = body[KeyError].(string) } if resp.Code != OkCode { + resp.Code &^= ErrorCodeBit err = Error{resp.Code, resp.Error} } } From b1ad4bf868af2758e26be98c3f2a1b623bd56eb5 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Mon, 26 Sep 2016 12:23:06 +0300 Subject: [PATCH 086/605] more gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 535ca4a3b..8fcb790f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ +*.DS_Store +*.swp +.idea/ snap xlog From 7cdf3d617c4d358bacda8f19f40abfc475547d60 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Mon, 26 Sep 2016 12:25:38 +0300 Subject: [PATCH 087/605] and gofmt for all --- auth.go | 10 +++++----- const.go | 2 +- smallbuf.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/auth.go b/auth.go index 7d215e460..1aa8ed1ae 100644 --- a/auth.go +++ b/auth.go @@ -19,9 +19,9 @@ func scramble(encoded_salt, pass string) (scramble []byte, err error) { ===================================================================== */ scrambleSize := sha1.Size // == 20 - salt, err := base64.StdEncoding.DecodeString(encoded_salt) + salt, err := base64.StdEncoding.DecodeString(encoded_salt) if err != nil { - return + return } step_1 := sha1.Sum([]byte(pass)) step_2 := sha1.Sum(step_1[0:]) @@ -29,14 +29,14 @@ func scramble(encoded_salt, pass string) (scramble []byte, err error) { hash.Write(salt[0:scrambleSize]) hash.Write(step_2[0:]) step_3 := hash.Sum(nil) - + return xor(step_1[0:], step_3[0:], scrambleSize), nil } func xor(left, right []byte, size int) []byte { result := make([]byte, size) - for i := 0; i < size ; i++ { + for i := 0; i < size; i++ { result[i] = left[i] ^ right[i] } return result -} \ No newline at end of file +} diff --git a/const.go b/const.go index 0e225bcb7..f319a4f85 100644 --- a/const.go +++ b/const.go @@ -119,7 +119,7 @@ const ( ErrLocalServerIsNotActive = 61 // Local server is not active ErrUnknownServer = 62 // Server %s is not registered with the cluster ErrClusterIdMismatch = 63 // Cluster id of the replica %s doesn't match cluster id of the master %s - ErrInvalidUuid = 64 // Invalid UUID: %s + ErrInvalidUUID = 64 // Invalid UUID: %s ErrClusterIdIsRo = 65 // Can't reset cluster id: it is already assigned ErrReserved66 = 66 // Reserved66 ErrServerIdIsReserved = 67 // Can't initialize server id with a reserved value %u diff --git a/smallbuf.go b/smallbuf.go index 4217b7af5..6b0a13638 100644 --- a/smallbuf.go +++ b/smallbuf.go @@ -1,8 +1,8 @@ package tarantool import ( - "io" "errors" + "io" ) type smallBuf struct { From e1b2e9cf9b2e488b4f008e7e819295c24626c9b4 Mon Sep 17 00:00:00 2001 From: Dmitry Smal Date: Mon, 26 Sep 2016 12:39:58 +0300 Subject: [PATCH 088/605] and golint for some lines --- auth.go | 24 ++++++++++++------------ connection.go | 17 ++++++++--------- response.go | 3 +-- schema.go | 6 +++--- tarantool_test.go | 16 ++++++++-------- 5 files changed, 32 insertions(+), 34 deletions(-) diff --git a/auth.go b/auth.go index 1aa8ed1ae..9f6c79157 100644 --- a/auth.go +++ b/auth.go @@ -5,32 +5,32 @@ import ( "encoding/base64" ) -func scramble(encoded_salt, pass string) (scramble []byte, err error) { +func scramble(encodedSalt, pass string) (scramble []byte, err error) { /* ================================================================== According to: http://tarantool.org/doc/dev_guide/box-protocol.html - salt = base64_decode(encoded_salt); - step_1 = sha1(password); - step_2 = sha1(step_1); - step_3 = sha1(salt, step_2); - scramble = xor(step_1, step_3); + salt = base64_decode(encodedSalt); + step1 = sha1(password); + step2 = sha1(step1); + step3 = sha1(salt, step2); + scramble = xor(step1, step3); return scramble; ===================================================================== */ scrambleSize := sha1.Size // == 20 - salt, err := base64.StdEncoding.DecodeString(encoded_salt) + salt, err := base64.StdEncoding.DecodeString(encodedSalt) if err != nil { return } - step_1 := sha1.Sum([]byte(pass)) - step_2 := sha1.Sum(step_1[0:]) + step1 := sha1.Sum([]byte(pass)) + step2 := sha1.Sum(step1[0:]) hash := sha1.New() // may be create it once per connection ? hash.Write(salt[0:scrambleSize]) - hash.Write(step_2[0:]) - step_3 := hash.Sum(nil) + hash.Write(step2[0:]) + step3 := hash.Sum(nil) - return xor(step_1[0:], step_3[0:], scrambleSize), nil + return xor(step1[0:], step3[0:], scrambleSize), nil } func xor(left, right []byte, size int) []byte { diff --git a/connection.go b/connection.go index 3f73c79f6..d9ab79cb3 100644 --- a/connection.go +++ b/connection.go @@ -161,11 +161,11 @@ func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err } func (conn *Connection) readAuthResponse(r io.Reader) (err error) { - resp_bytes, err := read(r) + respBytes, err := read(r) if err != nil { return errors.New("auth: read error " + err.Error()) } - resp := Response{buf: smallBuf{b: resp_bytes}} + resp := Response{buf: smallBuf{b: respBytes}} err = resp.decodeHeader() if err != nil { return errors.New("auth: decode response header error " + err.Error()) @@ -202,12 +202,11 @@ func (conn *Connection) createConnection() (r *bufio.Reader, w *bufio.Writer, er // mark connection as closed to avoid reopening by another goroutine conn.closed = true return - } else { - log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s\n", reconnects, conn.opts.MaxReconnects, conn.addr, err.Error()) - reconnects += 1 - time.Sleep(conn.opts.Reconnect) - continue } + log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s\n", reconnects, conn.opts.MaxReconnects, conn.addr, err.Error()) + reconnects++ + time.Sleep(conn.opts.Reconnect) + continue } else { return } @@ -295,12 +294,12 @@ func (conn *Connection) reader() { return } } - resp_bytes, err := read(r) + respBytes, err := read(r) if err != nil { r, _, _ = conn.closeConnection(err, r, nil) continue } - resp := Response{buf: smallBuf{b: resp_bytes}} + resp := Response{buf: smallBuf{b: respBytes}} err = resp.decodeHeader() if err != nil { r, _, _ = conn.closeConnection(err, r, nil) diff --git a/response.go b/response.go index caac6c56d..2bc429d35 100644 --- a/response.go +++ b/response.go @@ -111,9 +111,8 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { func (resp *Response) String() (str string) { if resp.Code == OkCode { return fmt.Sprintf("<%d OK %v>", resp.RequestId, resp.Data) - } else { - return fmt.Sprintf("<%d ERR 0x%x %s>", resp.RequestId, resp.Code, resp.Error) } + return fmt.Sprintf("<%d ERR 0x%x %s>", resp.RequestId, resp.Code, resp.Error) } func (resp *Response) Tuples() (res [][]interface{}) { diff --git a/schema.go b/schema.go index 4dfc75b80..7728e815b 100644 --- a/schema.go +++ b/schema.go @@ -99,8 +99,8 @@ func (conn *Connection) loadSchema() (err error) { if name, ok := f["name"]; ok && name != nil { field.Name = name.(string) } - if type_, ok := f["type"]; ok && type_ != nil { - field.Type = type_.(string) + if type1, ok := f["type"]; ok && type1 != nil { + field.Type = type1.(string) } space.FieldsById[field.Id] = field if field.Name != "" { @@ -139,7 +139,7 @@ func (conn *Connection) loadSchema() (err error) { switch row[5].(type) { case uint64: cnt := int(row[5].(uint64)) - for i := 0; i < cnt; i += 1 { + for i := 0; i < cnt; i++ { field := new(IndexField) field.Id = uint32(row[6+i*2].(uint64)) field.Type = row[7+i*2].(string) diff --git a/tarantool_test.go b/tarantool_test.go index 9806e34fb..dcdb3e297 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -664,7 +664,7 @@ func TestSchema(t *testing.T) { t.Errorf("space.Fields len is incorrect") } - var field1, field2, field5, field1_, field5_ *Field + var field1, field2, field5, field1n, field5n *Field if field1, ok = space.FieldsById[1]; !ok { t.Errorf("field id = 1 was not found") } @@ -675,13 +675,13 @@ func TestSchema(t *testing.T) { t.Errorf("field id = 5 was not found") } - if field1_, ok = space.Fields["name1"]; !ok { + if field1n, ok = space.Fields["name1"]; !ok { t.Errorf("field name = name1 was not found") } - if field5_, ok = space.Fields["name5"]; !ok { + if field5n, ok = space.Fields["name5"]; !ok { t.Errorf("field name = name5 was not found") } - if field1 != field1_ || field5 != field5_ { + if field1 != field1n || field5 != field5n { t.Errorf("field with id = 1 and field with name 'name1' are different") } if field1.Name != "name1" { @@ -710,20 +710,20 @@ func TestSchema(t *testing.T) { t.Errorf("space.Indexes len is incorrect") } - var index0, index3, index0_, index3_ *Index + var index0, index3, index0n, index3n *Index if index0, ok = space.IndexesById[0]; !ok { t.Errorf("index id = 0 was not found") } if index3, ok = space.IndexesById[3]; !ok { t.Errorf("index id = 3 was not found") } - if index0_, ok = space.Indexes["primary"]; !ok { + if index0n, ok = space.Indexes["primary"]; !ok { t.Errorf("index name = primary was not found") } - if index3_, ok = space.Indexes["secondary"]; !ok { + if index3n, ok = space.Indexes["secondary"]; !ok { t.Errorf("index name = secondary was not found") } - if index0 != index0_ || index3 != index3_ { + if index0 != index0n || index3 != index3n { t.Errorf("index with id = 3 and index with name 'secondary' are different") } if index3.Id != 3 { From 69188f91322675323b120eef09c88f5cf8b5cad4 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 19 Oct 2016 18:58:33 +0300 Subject: [PATCH 089/605] fix block on closed-forever connection --- request.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/request.go b/request.go index dbf93f2a5..152f85479 100644 --- a/request.go +++ b/request.go @@ -379,8 +379,16 @@ func (req *Request) future() (fut *Future) { req.conn.mutex.Unlock() fut.ready = make(chan struct{}) - // TODO: packets may lock - req.conn.packets <- (packet) + select { + case req.conn.packets <- (packet): + default: + // if connection is totally closed, then req.conn.packets will be full + select { + case req.conn.packets <- (packet): + case <-fut.ready: + return + } + } if req.conn.opts.Timeout > 0 { fut.timeout = time.NewTimer(req.conn.opts.Timeout) From 2988e3a52c6c8a14d455f9604d85ef172cfd3ab7 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 19 Oct 2016 18:59:01 +0300 Subject: [PATCH 090/605] fix race condition on future channel creation --- request.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/request.go b/request.go index 152f85479..a8143b68a 100644 --- a/request.go +++ b/request.go @@ -369,6 +369,7 @@ func (req *Request) future() (fut *Future) { return } + fut.ready = make(chan struct{}) req.conn.mutex.Lock() if req.conn.closed { req.conn.mutex.Unlock() @@ -378,7 +379,6 @@ func (req *Request) future() (fut *Future) { req.conn.requests[req.requestId] = fut req.conn.mutex.Unlock() - fut.ready = make(chan struct{}) select { case req.conn.packets <- (packet): default: From eeebc2578dba98e1c024344beec1efc00af3a686 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 22 Oct 2016 17:15:55 +0300 Subject: [PATCH 091/605] fix to tests (1.7 compatibility) --- config.lua | 3 +++ tarantool_test.go | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/config.lua b/config.lua index 56fdc6ee0..52d0c2fa2 100644 --- a/config.lua +++ b/config.lua @@ -4,6 +4,7 @@ box.cfg{ snap_dir='snap', } +box.once("init", function() local s = box.schema.space.create('test', { id = 512, if_not_exists = true, @@ -38,6 +39,7 @@ st:create_index('secondary', { st:truncate() --box.schema.user.grant('guest', 'read,write,execute', 'universe') +box.schema.func.create('box.info') -- auth testing: access control if not box.schema.user.exists('test') then @@ -46,6 +48,7 @@ if not box.schema.user.exists('test') then box.schema.user.grant('test', 'read,write', 'space', 'test') box.schema.user.grant('test', 'read,write', 'space', 'schematest') end +end) local console = require 'console' console.listen '0.0.0.0:33015' diff --git a/tarantool_test.go b/tarantool_test.go index dcdb3e297..29e6e3cda 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -753,8 +753,8 @@ func TestSchema(t *testing.T) { if ifield1.Id != 1 || ifield2.Id != 2 { t.Errorf("index field has incorrect Id") } - if ifield1.Type != "num" || ifield2.Type != "STR" { - t.Errorf("index field has incorrect Type[") + if (ifield1.Type != "num" && ifield1.Type != "unsigned") || (ifield2.Type != "STR" && ifield2.Type != "string") { + t.Errorf("index field has incorrect Type '%s'", ifield2.Type) } var rSpaceNo, rIndexNo uint32 From 6ee1c5c8b3724be4e747d645f467647d983a0d57 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 22 Oct 2016 18:02:19 +0300 Subject: [PATCH 092/605] respect timeout on request sending --- connection.go | 14 +++++++++----- request.go | 53 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/connection.go b/connection.go index d9ab79cb3..4273d417a 100644 --- a/connection.go +++ b/connection.go @@ -18,7 +18,8 @@ type Connection struct { c *net.TCPConn r *bufio.Reader w *bufio.Writer - mutex *sync.Mutex + mutex sync.Mutex + reqmut sync.Mutex Schema *Schema requestId uint32 Greeting *Greeting @@ -46,7 +47,6 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { conn = &Connection{ addr: addr, - mutex: &sync.Mutex{}, requestId: 0, Greeting: &Greeting{}, requests: make(map[uint32]*Future), @@ -233,6 +233,8 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. conn.c = nil conn.r = nil conn.w = nil + conn.reqmut.Lock() + defer conn.reqmut.Unlock() for rid, fut := range conn.requests { fut.err = neterr close(fut.ready) @@ -242,7 +244,9 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. } func (conn *Connection) closeConnectionForever(err error) error { + conn.reqmut.Lock() conn.closed = true + conn.reqmut.Unlock() close(conn.control) _, _, err = conn.closeConnection(err, nil, nil) return err @@ -305,14 +309,14 @@ func (conn *Connection) reader() { r, _, _ = conn.closeConnection(err, r, nil) continue } - conn.mutex.Lock() + conn.reqmut.Lock() if fut, ok := conn.requests[resp.RequestId]; ok { delete(conn.requests, resp.RequestId) fut.resp = resp close(fut.ready) - conn.mutex.Unlock() + conn.reqmut.Unlock() } else { - conn.mutex.Unlock() + conn.reqmut.Unlock() log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) } } diff --git a/request.go b/request.go index a8143b68a..cefb410a7 100644 --- a/request.go +++ b/request.go @@ -370,32 +370,55 @@ func (req *Request) future() (fut *Future) { } fut.ready = make(chan struct{}) - req.conn.mutex.Lock() + req.conn.reqmut.Lock() if req.conn.closed { - req.conn.mutex.Unlock() + req.conn.reqmut.Unlock() fut.err = ClientError{ErrConnectionClosed, "using closed connection"} return } req.conn.requests[req.requestId] = fut - req.conn.mutex.Unlock() + req.conn.reqmut.Unlock() + var sent bool select { case req.conn.packets <- (packet): + sent = true default: + } + + var timeout <-chan time.Time + if req.conn.opts.Timeout > 0 { + fut.timeout = time.NewTimer(req.conn.opts.Timeout) + timeout = fut.timeout.C + } + + if !sent { // if connection is totally closed, then req.conn.packets will be full + // if connection is busy, we can reach timeout select { case req.conn.packets <- (packet): case <-fut.ready: - return + case <-timeout: + fut.timeout.C = closedtimechan + fut.timeouted() } } - if req.conn.opts.Timeout > 0 { - fut.timeout = time.NewTimer(req.conn.opts.Timeout) - } return } +func (fut *Future) timeouted() { + conn := fut.req.conn + requestId := fut.req.requestId + conn.reqmut.Lock() + if _, ok := conn.requests[requestId]; ok { + delete(conn.requests, requestId) + close(fut.ready) + fut.err = fmt.Errorf("client timeout for request %d", requestId) + } + conn.reqmut.Unlock() +} + func badfuture(err error) *Future { return &Future{err: err} } @@ -404,8 +427,6 @@ func (fut *Future) wait() { if fut.ready == nil { return } - conn := fut.req.conn - requestId := fut.req.requestId select { case <-fut.ready: default: @@ -413,13 +434,7 @@ func (fut *Future) wait() { select { case <-fut.ready: case <-timeout.C: - conn.mutex.Lock() - if _, ok := conn.requests[requestId]; ok { - delete(conn.requests, requestId) - close(fut.ready) - fut.err = fmt.Errorf("client timeout for request %d", requestId) - } - conn.mutex.Unlock() + fut.timeouted() } } else { <-fut.ready @@ -448,3 +463,9 @@ func (fut *Future) GetTyped(result interface{}) error { fut.err = fut.resp.decodeBodyTyped(result) return fut.err } + +var closedtimechan = make(chan time.Time) + +func init() { + close(closedtimechan) +} From 7a07c346185816642dd70b5fa2de3f6736bf2988 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 22 Oct 2016 18:02:32 +0300 Subject: [PATCH 093/605] test: fix for race condition --- tarantool_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tarantool_test.go b/tarantool_test.go index 29e6e3cda..5f7a0eb5c 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -268,7 +268,7 @@ func BenchmarkClientFutureParallel(b *testing.B) { exit = j < N for j > 0 { j-- - _, err = fs[j].Get() + _, err := fs[j].Get() if err != nil { b.Error(err) break @@ -304,7 +304,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { for j > 0 { var r []Tuple j-- - err = fs[j].GetTyped(&r) + err := fs[j].GetTyped(&r) if err != nil { b.Error(err) break @@ -332,7 +332,7 @@ func BenchmarkClientParrallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) + _, err := conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) if err != nil { b.Errorf("No connection available") break From 013d18158d3ebc06cadda61ddcc8ed2284a47eae Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Mon, 24 Oct 2016 15:46:58 +0300 Subject: [PATCH 094/605] test: fix truncate --- config.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.lua b/config.lua index 52d0c2fa2..3f4fda96e 100644 --- a/config.lua +++ b/config.lua @@ -10,7 +10,6 @@ local s = box.schema.space.create('test', { if_not_exists = true, }) s:create_index('primary', {type = 'hash', parts = {1, 'NUM'}, if_not_exists = true}) -s:truncate() local st = box.schema.space.create('schematest', { id = 514, @@ -50,6 +49,7 @@ if not box.schema.user.exists('test') then end end) +box.space.test:truncate() local console = require 'console' console.listen '0.0.0.0:33015' From e74c21123d72a79408d6b993c5e59d6eb860522a Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 1 Nov 2016 13:06:46 +0300 Subject: [PATCH 095/605] do not allocate map for request fields --- connection.go | 10 +- request.go | 276 +++++++++++++++++--------------------------------- schema.go | 11 +- 3 files changed, 102 insertions(+), 195 deletions(-) diff --git a/connection.go b/connection.go index 4273d417a..64e9fb3cd 100644 --- a/connection.go +++ b/connection.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "errors" + "gopkg.in/vmihailenco/msgpack.v2" "io" "log" "net" @@ -145,9 +146,12 @@ func (conn *Connection) dial() (err error) { func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { request := conn.NewRequest(AuthRequest) - request.body[KeyUserName] = conn.opts.User - request.body[KeyTuple] = []interface{}{string("chap-sha1"), string(scramble)} - packet, err := request.pack() + packet, err := request.pack(func(enc *msgpack.Encoder) error { + return enc.Encode(map[uint32]interface{} { + KeyUserName: conn.opts.User, + KeyTuple: []interface{}{string("chap-sha1"), string(scramble)}, + }) + }) if err != nil { return errors.New("auth: pack error " + err.Error()) } diff --git a/request.go b/request.go index cefb410a7..92a021f62 100644 --- a/request.go +++ b/request.go @@ -10,7 +10,6 @@ type Request struct { conn *Connection requestId uint32 requestCode int32 - body map[int]interface{} } type Future struct { @@ -26,196 +25,102 @@ func (conn *Connection) NewRequest(requestCode int32) (req *Request) { req.conn = conn req.requestId = conn.nextRequestId() req.requestCode = requestCode - req.body = make(map[int]interface{}) return } func (conn *Connection) Ping() (resp *Response, err error) { request := conn.NewRequest(PingRequest) - resp, err = request.perform() - return + return request.future(func(enc *msgpack.Encoder)error{enc.EncodeMapLen(0);return nil}).Get() } -func (req *Request) fillSearch(spaceNo, indexNo uint32, key []interface{}) { - req.body[KeySpaceNo] = spaceNo - req.body[KeyIndexNo] = indexNo - req.body[KeyKey] = key +func (req *Request) fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key []interface{}) error { + enc.EncodeUint64(KeySpaceNo) + enc.EncodeUint64(uint64(spaceNo)) + enc.EncodeUint64(KeyIndexNo) + enc.EncodeUint64(uint64(indexNo)) + enc.EncodeUint64(KeyKey) + return enc.Encode(key) } -func (req *Request) fillIterator(offset, limit, iterator uint32) { - req.body[KeyIterator] = iterator - req.body[KeyOffset] = offset - req.body[KeyLimit] = limit +func (req *Request) fillIterator(enc *msgpack.Encoder, offset, limit, iterator uint32) { + enc.EncodeUint64(KeyIterator) + enc.EncodeUint64(uint64(iterator)) + enc.EncodeUint64(KeyOffset) + enc.EncodeUint64(uint64(offset)) + enc.EncodeUint64(KeyLimit) + enc.EncodeUint64(uint64(limit)) } -func (req *Request) fillInsert(spaceNo uint32, tuple interface{}) { - req.body[KeySpaceNo] = spaceNo - req.body[KeyTuple] = tuple +func (req *Request) fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { + enc.EncodeUint64(KeySpaceNo) + enc.EncodeUint64(uint64(spaceNo)) + enc.EncodeUint64(KeyTuple) + return enc.Encode(tuple) } func (conn *Connection) Select(space, index interface{}, offset, limit, iterator uint32, key []interface{}) (resp *Response, err error) { - request := conn.NewRequest(SelectRequest) - spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) - if err != nil { - return - } - request.fillSearch(spaceNo, indexNo, key) - request.fillIterator(offset, limit, iterator) - resp, err = request.perform() - return + return conn.SelectAsync(space, index, offset, limit, iterator, key).Get() } func (conn *Connection) Insert(space interface{}, tuple interface{}) (resp *Response, err error) { - request := conn.NewRequest(InsertRequest) - spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) - if err != nil { - return - } - request.fillInsert(spaceNo, tuple) - resp, err = request.perform() - return + return conn.InsertAsync(space, tuple).Get() } func (conn *Connection) Replace(space interface{}, tuple interface{}) (resp *Response, err error) { - request := conn.NewRequest(ReplaceRequest) - spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) - if err != nil { - return - } - request.fillInsert(spaceNo, tuple) - resp, err = request.perform() - return + return conn.ReplaceAsync(space, tuple).Get() } func (conn *Connection) Delete(space, index interface{}, key []interface{}) (resp *Response, err error) { - request := conn.NewRequest(DeleteRequest) - spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) - if err != nil { - return - } - request.fillSearch(spaceNo, indexNo, key) - resp, err = request.perform() - return + return conn.DeleteAsync(space, index, key).Get() } func (conn *Connection) Update(space, index interface{}, key, ops []interface{}) (resp *Response, err error) { - request := conn.NewRequest(UpdateRequest) - spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) - if err != nil { - return - } - request.fillSearch(spaceNo, indexNo, key) - request.body[KeyTuple] = ops - resp, err = request.perform() - return + return conn.UpdateAsync(space, index, key, ops).Get() } func (conn *Connection) Upsert(space interface{}, tuple, ops []interface{}) (resp *Response, err error) { - request := conn.NewRequest(UpsertRequest) - spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) - if err != nil { - return - } - request.body[KeySpaceNo] = spaceNo - request.body[KeyTuple] = tuple - request.body[KeyDefTuple] = ops - resp, err = request.perform() - return + return conn.UpsertAsync(space, tuple, ops).Get() } func (conn *Connection) Call(functionName string, args []interface{}) (resp *Response, err error) { - request := conn.NewRequest(CallRequest) - request.body[KeyFunctionName] = functionName - request.body[KeyTuple] = args - resp, err = request.perform() - return + return conn.CallAsync(functionName, args).Get() } func (conn *Connection) Eval(expr string, args []interface{}) (resp *Response, err error) { - request := conn.NewRequest(EvalRequest) - request.body[KeyExpression] = expr - request.body[KeyTuple] = args - resp, err = request.perform() - return + return conn.EvalAsync(expr, args).Get() } // Typed methods func (conn *Connection) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key []interface{}, result interface{}) (err error) { - request := conn.NewRequest(SelectRequest) - spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) - if err != nil { - return - } - request.fillSearch(spaceNo, indexNo, key) - request.fillIterator(offset, limit, iterator) - return request.performTyped(result) + return conn.SelectAsync(space, index, offset, limit, iterator, key).GetTyped(result) } func (conn *Connection) InsertTyped(space interface{}, tuple interface{}, result interface{}) (err error) { - request := conn.NewRequest(InsertRequest) - spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) - if err != nil { - return - } - request.fillInsert(spaceNo, tuple) - return request.performTyped(result) + return conn.InsertAsync(space, tuple).GetTyped(result) } func (conn *Connection) ReplaceTyped(space interface{}, tuple interface{}, result interface{}) (err error) { - request := conn.NewRequest(ReplaceRequest) - spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) - if err != nil { - return - } - request.fillInsert(spaceNo, tuple) - return request.performTyped(result) + return conn.ReplaceAsync(space, tuple).GetTyped(result) } func (conn *Connection) DeleteTyped(space, index interface{}, key []interface{}, result interface{}) (err error) { - request := conn.NewRequest(DeleteRequest) - spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) - if err != nil { - return - } - request.fillSearch(spaceNo, indexNo, key) - return request.performTyped(result) + return conn.DeleteAsync(space, index, key).GetTyped(result) } func (conn *Connection) UpdateTyped(space, index interface{}, key, ops []interface{}, result interface{}) (err error) { - request := conn.NewRequest(UpdateRequest) - spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) - if err != nil { - return - } - request.fillSearch(spaceNo, indexNo, key) - request.body[KeyTuple] = ops - return request.performTyped(result) + return conn.UpdateAsync(space, index, key, ops).GetTyped(result) } func (conn *Connection) UpsertTyped(space interface{}, tuple, ops []interface{}, result interface{}) (err error) { - request := conn.NewRequest(UpsertRequest) - spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) - if err != nil { - return - } - request.body[KeySpaceNo] = spaceNo - request.body[KeyTuple] = tuple - request.body[KeyDefTuple] = ops - return request.performTyped(result) + return conn.UpsertAsync(space, tuple, ops).GetTyped(result) } func (conn *Connection) CallTyped(functionName string, args []interface{}, result interface{}) (err error) { - request := conn.NewRequest(CallRequest) - request.body[KeyFunctionName] = functionName - request.body[KeyTuple] = args - return request.performTyped(result) + return conn.CallAsync(functionName, args).GetTyped(result) } func (conn *Connection) EvalTyped(expr string, args []interface{}, result interface{}) (err error) { - request := conn.NewRequest(EvalRequest) - request.body[KeyExpression] = expr - request.body[KeyTuple] = args - return request.performTyped(result) + return conn.EvalAsync(expr, args).GetTyped(result) } // Async methods @@ -225,9 +130,11 @@ func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, ite if err != nil { return badfuture(err) } - request.fillSearch(spaceNo, indexNo, key) - request.fillIterator(offset, limit, iterator) - return request.future() + return request.future(func (enc *msgpack.Encoder) error { + enc.EncodeMapLen(6) + request.fillIterator(enc, offset, limit, iterator) + return request.fillSearch(enc, spaceNo, indexNo, key) + }) } func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Future { @@ -236,8 +143,10 @@ func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Futur if err != nil { return badfuture(err) } - request.fillInsert(spaceNo, tuple) - return request.future() + return request.future(func (enc *msgpack.Encoder) error { + enc.EncodeMapLen(2) + return request.fillInsert(enc, spaceNo, tuple) + }) } func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Future { @@ -246,8 +155,10 @@ func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Futu if err != nil { return badfuture(err) } - request.fillInsert(spaceNo, tuple) - return request.future() + return request.future(func (enc *msgpack.Encoder) error { + enc.EncodeMapLen(2) + return request.fillInsert(enc, spaceNo, tuple) + }) } func (conn *Connection) DeleteAsync(space, index interface{}, key []interface{}) *Future { @@ -256,8 +167,10 @@ func (conn *Connection) DeleteAsync(space, index interface{}, key []interface{}) if err != nil { return badfuture(err) } - request.fillSearch(spaceNo, indexNo, key) - return request.future() + return request.future(func (enc *msgpack.Encoder) error { + enc.EncodeMapLen(3) + return request.fillSearch(enc, spaceNo, indexNo, key) + }) } func (conn *Connection) UpdateAsync(space, index interface{}, key, ops []interface{}) *Future { @@ -266,9 +179,14 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops []interfa if err != nil { return badfuture(err) } - request.fillSearch(spaceNo, indexNo, key) - request.body[KeyTuple] = ops - return request.future() + return request.future(func (enc *msgpack.Encoder) error { + enc.EncodeMapLen(4) + if err := request.fillSearch(enc, spaceNo, indexNo, key); err != nil { + return err + } + enc.EncodeUint64(KeyTuple) + return enc.Encode(ops) + }) } func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops []interface{}) *Future { @@ -277,69 +195,61 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops [] if err != nil { return badfuture(err) } - request.body[KeySpaceNo] = spaceNo - request.body[KeyTuple] = tuple - request.body[KeyDefTuple] = ops - return request.future() + return request.future(func (enc *msgpack.Encoder) error { + enc.EncodeMapLen(3) + enc.EncodeUint64(KeySpaceNo) + enc.EncodeUint64(uint64(spaceNo)) + enc.EncodeUint64(KeyTuple) + if err := enc.Encode(tuple); err != nil { + return err + } + enc.EncodeUint64(KeyDefTuple) + return enc.Encode(ops) + }) } func (conn *Connection) CallAsync(functionName string, args []interface{}) *Future { request := conn.NewRequest(CallRequest) - request.body[KeyFunctionName] = functionName - request.body[KeyTuple] = args - return request.future() + return request.future(func (enc *msgpack.Encoder) error { + enc.EncodeMapLen(2) + enc.EncodeUint64(KeyFunctionName) + enc.EncodeString(functionName) + enc.EncodeUint64(KeyTuple) + return enc.Encode(args) + }) } func (conn *Connection) EvalAsync(expr string, args []interface{}) *Future { request := conn.NewRequest(EvalRequest) - request.body[KeyExpression] = expr - request.body[KeyTuple] = args - return request.future() + return request.future(func (enc *msgpack.Encoder) error { + enc.EncodeMapLen(2) + enc.EncodeUint64(KeyExpression) + enc.EncodeString(expr) + enc.EncodeUint64(KeyTuple) + return enc.Encode(args) + }) } // // private // -func (req *Request) perform() (resp *Response, err error) { - return req.future().Get() -} - -func (req *Request) performTyped(res interface{}) (err error) { - return req.future().GetTyped(res) -} - -func (req *Request) pack() (packet []byte, err error) { +func (req *Request) pack(body func (*msgpack.Encoder)error) (packet []byte, err error) { rid := req.requestId - h := smallWBuf{ + h := make(smallWBuf, 0, 48) + h = append(h, smallWBuf{ 0xce, 0, 0, 0, 0, // length 0x82, // 2 element map KeyCode, byte(req.requestCode), // request code KeySync, 0xce, byte(rid >> 24), byte(rid >> 16), byte(rid >> 8), byte(rid), - } + }...) enc := msgpack.NewEncoder(&h) - err = enc.EncodeMapLen(len(req.body)) - if err != nil { + if err = body(enc); err != nil { return } - for k, v := range req.body { - err = enc.EncodeInt64(int64(k)) - if err != nil { - return - } - switch vv := v.(type) { - case uint32: - err = enc.EncodeUint64(uint64(vv)) - default: - err = enc.Encode(vv) - } - if err != nil { - return - } - } l := uint32(len(h) - 5) h[1] = byte(l >> 24) @@ -351,7 +261,7 @@ func (req *Request) pack() (packet []byte, err error) { return } -func (req *Request) future() (fut *Future) { +func (req *Request) future(body func (*msgpack.Encoder)error) (fut *Future) { fut = &Future{req: req} // check connection ready to process packets @@ -365,7 +275,7 @@ func (req *Request) future() (fut *Future) { } var packet []byte - if packet, fut.err = req.pack(); fut.err != nil { + if packet, fut.err = req.pack(body); fut.err != nil { return } diff --git a/schema.go b/schema.go index 7728e815b..46502affc 100644 --- a/schema.go +++ b/schema.go @@ -50,7 +50,6 @@ const ( ) func (conn *Connection) loadSchema() (err error) { - var req *Request var resp *Response schema := new(Schema) @@ -58,10 +57,7 @@ func (conn *Connection) loadSchema() (err error) { schema.Spaces = make(map[string]*Space) // reload spaces - req = conn.NewRequest(SelectRequest) - req.fillSearch(vspaceSpId, 0, []interface{}{}) - req.fillIterator(0, maxSchemas, IterAll) - resp, err = req.perform() + resp, err = conn.Select(vspaceSpId, 0, 0, maxSchemas, IterAll, []interface{}{}) if err != nil { return err } @@ -114,10 +110,7 @@ func (conn *Connection) loadSchema() (err error) { } // reload indexes - req = conn.NewRequest(SelectRequest) - req.fillSearch(vindexSpId, 0, []interface{}{}) - req.fillIterator(0, maxSchemas, IterAll) - resp, err = req.perform() + resp, err = conn.Select(vindexSpId, 0, 0, maxSchemas, IterAll, []interface{}{}) if err != nil { return err } From bd4b766ba403d70f6fda81aeb01b48c501d5b59e Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 1 Nov 2016 14:21:45 +0300 Subject: [PATCH 096/605] add ClientParallelMassive benchmark --- tarantool_test.go | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tarantool_test.go b/tarantool_test.go index 5f7a0eb5c..64ad167f1 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -5,6 +5,7 @@ import ( "gopkg.in/vmihailenco/msgpack.v2" "reflect" "strings" + "sync" "testing" "time" ) @@ -156,7 +157,7 @@ var spaceNo = uint32(512) var spaceName = "test" var indexNo = uint32(0) var indexName = "primary" -var opts = Opts{Timeout: 500 * time.Millisecond, User: "test", Pass: "test"} +var opts = Opts{Timeout: 5000 * time.Millisecond, User: "test", Pass: "test"} const N = 500 @@ -318,7 +319,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { }) } -func BenchmarkClientParrallel(b *testing.B) { +func BenchmarkClientParallel(b *testing.B) { conn, err := Connect(server, opts) if err != nil { b.Errorf("No connection available") @@ -341,6 +342,33 @@ func BenchmarkClientParrallel(b *testing.B) { }) } +func BenchmarkClientParallelMassive(b *testing.B) { + conn, err := Connect(server, opts) + if err != nil { + b.Errorf("No connection available") + return + } + + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Errorf("No connection available") + } + + var wg sync.WaitGroup + for i:=0; i Date: Tue, 1 Nov 2016 14:29:08 +0300 Subject: [PATCH 097/605] use AfterFunc(fut.timeouted) instead of time.NewTimer() --- request.go | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/request.go b/request.go index 92a021f62..0e878bcd1 100644 --- a/request.go +++ b/request.go @@ -296,10 +296,8 @@ func (req *Request) future(body func (*msgpack.Encoder)error) (fut *Future) { default: } - var timeout <-chan time.Time if req.conn.opts.Timeout > 0 { - fut.timeout = time.NewTimer(req.conn.opts.Timeout) - timeout = fut.timeout.C + fut.timeout = time.AfterFunc(req.conn.opts.Timeout, fut.timeouted) } if !sent { @@ -308,9 +306,6 @@ func (req *Request) future(body func (*msgpack.Encoder)error) (fut *Future) { select { case req.conn.packets <- (packet): case <-fut.ready: - case <-timeout: - fut.timeout.C = closedtimechan - fut.timeouted() } } @@ -337,19 +332,7 @@ func (fut *Future) wait() { if fut.ready == nil { return } - select { - case <-fut.ready: - default: - if timeout := fut.timeout; timeout != nil { - select { - case <-fut.ready: - case <-timeout.C: - fut.timeouted() - } - } else { - <-fut.ready - } - } + <-fut.ready if fut.timeout != nil { fut.timeout.Stop() fut.timeout = nil From d450c0cd3564243f228a99c7255ceb5bf81e9e50 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 1 Nov 2016 14:30:34 +0300 Subject: [PATCH 098/605] increase write buffer, packets channel and preallocate futures map --- connection.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/connection.go b/connection.go index 64e9fb3cd..c31ed2049 100644 --- a/connection.go +++ b/connection.go @@ -50,8 +50,8 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { addr: addr, requestId: 0, Greeting: &Greeting{}, - requests: make(map[uint32]*Future), - packets: make(chan []byte, 64), + requests: make(map[uint32]*Future, 2048), + packets: make(chan []byte, 2048), control: make(chan struct{}), opts: opts, } @@ -108,7 +108,7 @@ func (conn *Connection) dial() (err error) { c := connection.(*net.TCPConn) c.SetNoDelay(true) r := bufio.NewReaderSize(c, 128*1024) - w := bufio.NewWriter(c) + w := bufio.NewWriterSize(c, 128*1024) greeting := make([]byte, 128) _, err = io.ReadFull(r, greeting) if err != nil { From 44d07c4b2da12eb48dfa8d42125c3b8254caa2cc Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 1 Nov 2016 14:34:43 +0300 Subject: [PATCH 099/605] ... a bit of simplification --- request.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/request.go b/request.go index 0e878bcd1..e54e40e79 100644 --- a/request.go +++ b/request.go @@ -289,18 +289,13 @@ func (req *Request) future(body func (*msgpack.Encoder)error) (fut *Future) { req.conn.requests[req.requestId] = fut req.conn.reqmut.Unlock() - var sent bool - select { - case req.conn.packets <- (packet): - sent = true - default: - } - if req.conn.opts.Timeout > 0 { fut.timeout = time.AfterFunc(req.conn.opts.Timeout, fut.timeouted) } - if !sent { + select { + case req.conn.packets <- (packet): + default: // if connection is totally closed, then req.conn.packets will be full // if connection is busy, we can reach timeout select { From e742cf8241dba7fd027c6937074a83b20759a0aa Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 1 Nov 2016 15:07:49 +0300 Subject: [PATCH 100/605] remove request type + replace requests map with custom hash table --- connection.go | 48 +++++++++++++++---- request.go | 129 ++++++++++++++++++++++++-------------------------- 2 files changed, 102 insertions(+), 75 deletions(-) diff --git a/connection.go b/connection.go index c31ed2049..2d1f8a5ee 100644 --- a/connection.go +++ b/connection.go @@ -14,6 +14,8 @@ import ( "time" ) +const requestsMap = 32*1024 + type Connection struct { addr string c *net.TCPConn @@ -24,7 +26,7 @@ type Connection struct { Schema *Schema requestId uint32 Greeting *Greeting - requests map[uint32]*Future + requests []*Future packets chan []byte control chan struct{} opts Opts @@ -50,7 +52,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { addr: addr, requestId: 0, Greeting: &Greeting{}, - requests: make(map[uint32]*Future, 2048), + requests: make([]*Future, requestsMap), packets: make(chan []byte, 2048), control: make(chan struct{}), opts: opts, @@ -145,7 +147,7 @@ func (conn *Connection) dial() (err error) { } func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { - request := conn.NewRequest(AuthRequest) + request := conn.newFuture(AuthRequest) packet, err := request.pack(func(enc *msgpack.Encoder) error { return enc.Encode(map[uint32]interface{} { KeyUserName: conn.opts.User, @@ -239,10 +241,13 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. conn.w = nil conn.reqmut.Lock() defer conn.reqmut.Unlock() - for rid, fut := range conn.requests { - fut.err = neterr - close(fut.ready) - delete(conn.requests, rid) + for pos, fut := range conn.requests { + conn.requests[pos] = nil + for fut != nil { + fut.err = neterr + close(fut.ready) + fut, fut.next = fut.next, nil + } } return } @@ -314,8 +319,7 @@ func (conn *Connection) reader() { continue } conn.reqmut.Lock() - if fut, ok := conn.requests[resp.RequestId]; ok { - delete(conn.requests, resp.RequestId) + if fut := conn.fetchFuture(resp.RequestId); fut != nil { fut.resp = resp close(fut.ready) conn.reqmut.Unlock() @@ -326,6 +330,32 @@ func (conn *Connection) reader() { } } +func (conn *Connection) putFuture(fut *Future) { + pos := fut.requestId & (requestsMap-1) + fut.next = conn.requests[pos] + conn.requests[pos] = fut +} + +func (conn *Connection) fetchFuture(reqid uint32) *Future { + pos := reqid & (requestsMap-1) + fut := conn.requests[pos] + if fut == nil { + return nil + } + if fut.requestId == reqid { + conn.requests[pos] = fut.next + return fut + } + for fut.next != nil { + if fut.next.requestId == reqid { + fut, fut.next = fut.next, fut.next.next + return fut + } + fut = fut.next + } + return nil +} + func write(w io.Writer, data []byte) (err error) { l, err := w.Write(data) if err != nil { diff --git a/request.go b/request.go index e54e40e79..69fceea13 100644 --- a/request.go +++ b/request.go @@ -6,34 +6,31 @@ import ( "time" ) -type Request struct { - conn *Connection +type Future struct { + conn *Connection requestId uint32 requestCode int32 -} - -type Future struct { - req *Request resp Response err error ready chan struct{} timeout *time.Timer + next *Future } -func (conn *Connection) NewRequest(requestCode int32) (req *Request) { - req = &Request{} - req.conn = conn - req.requestId = conn.nextRequestId() - req.requestCode = requestCode +func (conn *Connection) newFuture(requestCode int32) (fut *Future) { + fut = &Future{} + fut.conn = conn + fut.requestId = conn.nextRequestId() + fut.requestCode = requestCode return } func (conn *Connection) Ping() (resp *Response, err error) { - request := conn.NewRequest(PingRequest) - return request.future(func(enc *msgpack.Encoder)error{enc.EncodeMapLen(0);return nil}).Get() + future := conn.newFuture(PingRequest) + return future.send(func(enc *msgpack.Encoder)error{enc.EncodeMapLen(0);return nil}).Get() } -func (req *Request) fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key []interface{}) error { +func (req *Future) fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key []interface{}) error { enc.EncodeUint64(KeySpaceNo) enc.EncodeUint64(uint64(spaceNo)) enc.EncodeUint64(KeyIndexNo) @@ -42,7 +39,7 @@ func (req *Request) fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, ke return enc.Encode(key) } -func (req *Request) fillIterator(enc *msgpack.Encoder, offset, limit, iterator uint32) { +func (req *Future) fillIterator(enc *msgpack.Encoder, offset, limit, iterator uint32) { enc.EncodeUint64(KeyIterator) enc.EncodeUint64(uint64(iterator)) enc.EncodeUint64(KeyOffset) @@ -51,7 +48,7 @@ func (req *Request) fillIterator(enc *msgpack.Encoder, offset, limit, iterator u enc.EncodeUint64(uint64(limit)) } -func (req *Request) fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { +func (req *Future) fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { enc.EncodeUint64(KeySpaceNo) enc.EncodeUint64(uint64(spaceNo)) enc.EncodeUint64(KeyTuple) @@ -125,63 +122,63 @@ func (conn *Connection) EvalTyped(expr string, args []interface{}, result interf // Async methods func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key []interface{}) *Future { - request := conn.NewRequest(SelectRequest) + future := conn.newFuture(SelectRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) if err != nil { return badfuture(err) } - return request.future(func (enc *msgpack.Encoder) error { + return future.send(func (enc *msgpack.Encoder) error { enc.EncodeMapLen(6) - request.fillIterator(enc, offset, limit, iterator) - return request.fillSearch(enc, spaceNo, indexNo, key) + future.fillIterator(enc, offset, limit, iterator) + return future.fillSearch(enc, spaceNo, indexNo, key) }) } func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Future { - request := conn.NewRequest(InsertRequest) + future := conn.newFuture(InsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { return badfuture(err) } - return request.future(func (enc *msgpack.Encoder) error { + return future.send(func (enc *msgpack.Encoder) error { enc.EncodeMapLen(2) - return request.fillInsert(enc, spaceNo, tuple) + return future.fillInsert(enc, spaceNo, tuple) }) } func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Future { - request := conn.NewRequest(ReplaceRequest) + future := conn.newFuture(ReplaceRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { return badfuture(err) } - return request.future(func (enc *msgpack.Encoder) error { + return future.send(func (enc *msgpack.Encoder) error { enc.EncodeMapLen(2) - return request.fillInsert(enc, spaceNo, tuple) + return future.fillInsert(enc, spaceNo, tuple) }) } func (conn *Connection) DeleteAsync(space, index interface{}, key []interface{}) *Future { - request := conn.NewRequest(DeleteRequest) + future := conn.newFuture(DeleteRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) if err != nil { return badfuture(err) } - return request.future(func (enc *msgpack.Encoder) error { + return future.send(func (enc *msgpack.Encoder) error { enc.EncodeMapLen(3) - return request.fillSearch(enc, spaceNo, indexNo, key) + return future.fillSearch(enc, spaceNo, indexNo, key) }) } func (conn *Connection) UpdateAsync(space, index interface{}, key, ops []interface{}) *Future { - request := conn.NewRequest(UpdateRequest) + future := conn.newFuture(UpdateRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) if err != nil { return badfuture(err) } - return request.future(func (enc *msgpack.Encoder) error { + return future.send(func (enc *msgpack.Encoder) error { enc.EncodeMapLen(4) - if err := request.fillSearch(enc, spaceNo, indexNo, key); err != nil { + if err := future.fillSearch(enc, spaceNo, indexNo, key); err != nil { return err } enc.EncodeUint64(KeyTuple) @@ -190,12 +187,12 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops []interfa } func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops []interface{}) *Future { - request := conn.NewRequest(UpsertRequest) + future := conn.newFuture(UpsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { return badfuture(err) } - return request.future(func (enc *msgpack.Encoder) error { + return future.send(func (enc *msgpack.Encoder) error { enc.EncodeMapLen(3) enc.EncodeUint64(KeySpaceNo) enc.EncodeUint64(uint64(spaceNo)) @@ -209,8 +206,8 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops [] } func (conn *Connection) CallAsync(functionName string, args []interface{}) *Future { - request := conn.NewRequest(CallRequest) - return request.future(func (enc *msgpack.Encoder) error { + future := conn.newFuture(CallRequest) + return future.send(func (enc *msgpack.Encoder) error { enc.EncodeMapLen(2) enc.EncodeUint64(KeyFunctionName) enc.EncodeString(functionName) @@ -220,8 +217,8 @@ func (conn *Connection) CallAsync(functionName string, args []interface{}) *Futu } func (conn *Connection) EvalAsync(expr string, args []interface{}) *Future { - request := conn.NewRequest(EvalRequest) - return request.future(func (enc *msgpack.Encoder) error { + future := conn.newFuture(EvalRequest) + return future.send(func (enc *msgpack.Encoder) error { enc.EncodeMapLen(2) enc.EncodeUint64(KeyExpression) enc.EncodeString(expr) @@ -234,13 +231,13 @@ func (conn *Connection) EvalAsync(expr string, args []interface{}) *Future { // private // -func (req *Request) pack(body func (*msgpack.Encoder)error) (packet []byte, err error) { - rid := req.requestId +func (fut *Future) pack(body func (*msgpack.Encoder)error) (packet []byte, err error) { + rid := fut.requestId h := make(smallWBuf, 0, 48) h = append(h, smallWBuf{ 0xce, 0, 0, 0, 0, // length 0x82, // 2 element map - KeyCode, byte(req.requestCode), // request code + KeyCode, byte(fut.requestCode), // request code KeySync, 0xce, byte(rid >> 24), byte(rid >> 16), byte(rid >> 8), byte(rid), @@ -261,60 +258,60 @@ func (req *Request) pack(body func (*msgpack.Encoder)error) (packet []byte, err return } -func (req *Request) future(body func (*msgpack.Encoder)error) (fut *Future) { - fut = &Future{req: req} +func (fut *Future) send(body func (*msgpack.Encoder)error) *Future { // check connection ready to process packets - if closed := req.conn.closed; closed { + if closed := fut.conn.closed; closed { fut.err = ClientError{ErrConnectionClosed, "using closed connection"} - return + return fut } - if c := req.conn.c; c == nil { + if c := fut.conn.c; c == nil { fut.err = ClientError{ErrConnectionNotReady, "client connection is not ready"} - return + return fut } var packet []byte - if packet, fut.err = req.pack(body); fut.err != nil { - return + if packet, fut.err = fut.pack(body); fut.err != nil { + return fut } fut.ready = make(chan struct{}) - req.conn.reqmut.Lock() - if req.conn.closed { - req.conn.reqmut.Unlock() + fut.conn.reqmut.Lock() + if fut.conn.closed { + fut.conn.reqmut.Unlock() fut.err = ClientError{ErrConnectionClosed, "using closed connection"} - return + return fut } - req.conn.requests[req.requestId] = fut - req.conn.reqmut.Unlock() + fut.conn.putFuture(fut) + fut.conn.reqmut.Unlock() - if req.conn.opts.Timeout > 0 { - fut.timeout = time.AfterFunc(req.conn.opts.Timeout, fut.timeouted) + if fut.conn.opts.Timeout > 0 { + fut.timeout = time.AfterFunc(fut.conn.opts.Timeout, fut.timeouted) } select { - case req.conn.packets <- (packet): + case fut.conn.packets <- (packet): default: - // if connection is totally closed, then req.conn.packets will be full + // if connection is totally closed, then fut.conn.packets will be full // if connection is busy, we can reach timeout select { - case req.conn.packets <- (packet): + case fut.conn.packets <- (packet): case <-fut.ready: } } - return + return fut } func (fut *Future) timeouted() { - conn := fut.req.conn - requestId := fut.req.requestId + conn := fut.conn conn.reqmut.Lock() - if _, ok := conn.requests[requestId]; ok { - delete(conn.requests, requestId) + if f := conn.fetchFuture(fut.requestId); f != nil { + if f != fut { + panic("future doesn't match") + } close(fut.ready) - fut.err = fmt.Errorf("client timeout for request %d", requestId) + fut.err = fmt.Errorf("client timeout for request %d", fut.requestId) } conn.reqmut.Unlock() } From 37b264dd3468ac440f717e5466404469ed4a91ef Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 1 Nov 2016 17:35:44 +0300 Subject: [PATCH 101/605] fix race condition in Future.timeouted() --- request.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/request.go b/request.go index 69fceea13..121dd13a7 100644 --- a/request.go +++ b/request.go @@ -310,8 +310,8 @@ func (fut *Future) timeouted() { if f != fut { panic("future doesn't match") } - close(fut.ready) fut.err = fmt.Errorf("client timeout for request %d", fut.requestId) + close(fut.ready) } conn.reqmut.Unlock() } From 0c3fcf7919a03a5cd06a89eacf02b3013b193447 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 1 Nov 2016 17:37:05 +0300 Subject: [PATCH 102/605] limit ParallelMassive test --- tarantool_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tarantool_test.go b/tarantool_test.go index 64ad167f1..19cec4aa6 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -354,13 +354,16 @@ func BenchmarkClientParallelMassive(b *testing.B) { b.Errorf("No connection available") } + limit := make(chan struct{}, 128*1024) var wg sync.WaitGroup for i:=0; i Date: Tue, 1 Nov 2016 17:45:06 +0300 Subject: [PATCH 103/605] use custom timer expiration instead of Go timers --- connection.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ request.go | 14 +++++--------- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/connection.go b/connection.go index 2d1f8a5ee..78cb9a329 100644 --- a/connection.go +++ b/connection.go @@ -15,6 +15,7 @@ import ( ) const requestsMap = 32*1024 +var epoch = time.Now() type Connection struct { addr string @@ -27,6 +28,11 @@ type Connection struct { requestId uint32 Greeting *Greeting requests []*Future + time struct { + timeout time.Duration + first *Future + last **Future + } packets chan []byte control chan struct{} opts Opts @@ -57,6 +63,8 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { control: make(chan struct{}), opts: opts, } + conn.time.timeout = time.Now().Sub(epoch) + conn.opts.Timeout + conn.time.last = &conn.time.first var reconnect time.Duration // disable reconnecting for first connect @@ -69,6 +77,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { go conn.writer() go conn.reader() + go conn.timeouts() // TODO: reload schema after reconnect if err = conn.loadSchema(); err != nil { @@ -334,6 +343,23 @@ func (conn *Connection) putFuture(fut *Future) { pos := fut.requestId & (requestsMap-1) fut.next = conn.requests[pos] conn.requests[pos] = fut + if conn.opts.Timeout > 0 { + fut.timeout = conn.time.timeout + fut.time.prev = conn.time.last + *conn.time.last = fut + conn.time.last = &fut.time.next + } +} + +func (conn *Connection) unlinkFutureTime(fut *Future) { + if fut.time.prev != nil { + *fut.time.prev = fut.time.next + if fut.time.next != nil { + fut.time.next.time.prev = fut.time.prev + } else { + conn.time.last = fut.time.prev + } + } } func (conn *Connection) fetchFuture(reqid uint32) *Future { @@ -344,11 +370,13 @@ func (conn *Connection) fetchFuture(reqid uint32) *Future { } if fut.requestId == reqid { conn.requests[pos] = fut.next + conn.unlinkFutureTime(fut) return fut } for fut.next != nil { if fut.next.requestId == reqid { fut, fut.next = fut.next, fut.next.next + conn.unlinkFutureTime(fut) return fut } fut = fut.next @@ -356,6 +384,29 @@ func (conn *Connection) fetchFuture(reqid uint32) *Future { return nil } +func (conn *Connection) timeouts() { + t := time.NewTicker(1*time.Millisecond) + for { + var nowepoch time.Duration + select { + case <-conn.control: + t.Stop() + return + case now := <-t.C: + nowepoch = now.Sub(epoch) + } + conn.reqmut.Lock() + conn.time.timeout = nowepoch + conn.opts.Timeout + for conn.time.first != nil && conn.time.first.timeout < nowepoch { + fut := conn.time.first + conn.reqmut.Unlock() + fut.timeouted() + conn.reqmut.Lock() + } + conn.reqmut.Unlock() + } +} + func write(w io.Writer, data []byte) (err error) { l, err := w.Write(data) if err != nil { diff --git a/request.go b/request.go index 121dd13a7..dd68f6062 100644 --- a/request.go +++ b/request.go @@ -13,8 +13,12 @@ type Future struct { resp Response err error ready chan struct{} - timeout *time.Timer + timeout time.Duration next *Future + time struct { + next *Future + prev **Future + } } func (conn *Connection) newFuture(requestCode int32) (fut *Future) { @@ -285,10 +289,6 @@ func (fut *Future) send(body func (*msgpack.Encoder)error) *Future { fut.conn.putFuture(fut) fut.conn.reqmut.Unlock() - if fut.conn.opts.Timeout > 0 { - fut.timeout = time.AfterFunc(fut.conn.opts.Timeout, fut.timeouted) - } - select { case fut.conn.packets <- (packet): default: @@ -325,10 +325,6 @@ func (fut *Future) wait() { return } <-fut.ready - if fut.timeout != nil { - fut.timeout.Stop() - fut.timeout = nil - } } func (fut *Future) Get() (*Response, error) { From 726f94e9ae1a9c577eae6e7c439f2a96c5bbe25a Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 1 Nov 2016 17:45:57 +0300 Subject: [PATCH 104/605] shard requests --- connection.go | 111 +++++++++++++++++++++++++++++++------------------- request.go | 10 ++--- 2 files changed, 73 insertions(+), 48 deletions(-) diff --git a/connection.go b/connection.go index 78cb9a329..9b8c4fb8a 100644 --- a/connection.go +++ b/connection.go @@ -14,7 +14,8 @@ import ( "time" ) -const requestsMap = 32*1024 +const shards = 16 +const requestsMap = 2*1024 var epoch = time.Now() type Connection struct { @@ -23,15 +24,16 @@ type Connection struct { r *bufio.Reader w *bufio.Writer mutex sync.Mutex - reqmut sync.Mutex Schema *Schema requestId uint32 Greeting *Greeting - requests []*Future - time struct { + shard [shards]struct { + sync.Mutex timeout time.Duration + requests []*Future first *Future last **Future + _pad [128]byte } packets chan []byte control chan struct{} @@ -58,13 +60,15 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { addr: addr, requestId: 0, Greeting: &Greeting{}, - requests: make([]*Future, requestsMap), - packets: make(chan []byte, 2048), + packets: make(chan []byte, 16*1024), control: make(chan struct{}), opts: opts, } - conn.time.timeout = time.Now().Sub(epoch) + conn.opts.Timeout - conn.time.last = &conn.time.first + for i := range conn.shard { + conn.shard[i].timeout = time.Now().Sub(epoch) + conn.opts.Timeout + conn.shard[i].last = &conn.shard[i].first + conn.shard[i].requests = make([]*Future, requestsMap) + } var reconnect time.Duration // disable reconnecting for first connect @@ -248,23 +252,38 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. conn.c = nil conn.r = nil conn.w = nil - conn.reqmut.Lock() - defer conn.reqmut.Unlock() - for pos, fut := range conn.requests { - conn.requests[pos] = nil - for fut != nil { - fut.err = neterr - close(fut.ready) - fut, fut.next = fut.next, nil + conn.lockShards() + defer conn.unlockShards() + for i := range conn.shard { + requests := conn.shard[i].requests + for pos, fut := range requests { + requests[pos] = nil + for fut != nil { + fut.err = neterr + close(fut.ready) + fut, fut.next = fut.next, nil + } } } return } +func (conn *Connection) lockShards() { + for i := range conn.shard { + conn.shard[i].Lock() + } +} + +func (conn *Connection) unlockShards() { + for i := range conn.shard { + conn.shard[i].Unlock() + } +} + func (conn *Connection) closeConnectionForever(err error) error { - conn.reqmut.Lock() + conn.lockShards() conn.closed = true - conn.reqmut.Unlock() + conn.unlockShards() close(conn.control) _, _, err = conn.closeConnection(err, nil, nil) return err @@ -327,56 +346,60 @@ func (conn *Connection) reader() { r, _, _ = conn.closeConnection(err, r, nil) continue } - conn.reqmut.Lock() + shard := resp.RequestId&(shards-1) + conn.shard[shard].Lock() if fut := conn.fetchFuture(resp.RequestId); fut != nil { fut.resp = resp close(fut.ready) - conn.reqmut.Unlock() + conn.shard[shard].Unlock() } else { - conn.reqmut.Unlock() + conn.shard[shard].Unlock() log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) } } } func (conn *Connection) putFuture(fut *Future) { - pos := fut.requestId & (requestsMap-1) - fut.next = conn.requests[pos] - conn.requests[pos] = fut + shard := fut.requestId & (shards-1) + pos := (fut.requestId/shards) & (requestsMap-1) + fut.next = conn.shard[shard].requests[pos] + conn.shard[shard].requests[pos] = fut if conn.opts.Timeout > 0 { - fut.timeout = conn.time.timeout - fut.time.prev = conn.time.last - *conn.time.last = fut - conn.time.last = &fut.time.next + fut.timeout = conn.shard[shard].timeout + fut.time.prev = conn.shard[shard].last + *conn.shard[shard].last = fut + conn.shard[shard].last = &fut.time.next } } -func (conn *Connection) unlinkFutureTime(fut *Future) { +func (conn *Connection) unlinkFutureTime(shard uint32, fut *Future) { if fut.time.prev != nil { + i := fut.requestId&(shards-1) *fut.time.prev = fut.time.next if fut.time.next != nil { fut.time.next.time.prev = fut.time.prev } else { - conn.time.last = fut.time.prev + conn.shard[i].last = fut.time.prev } } } func (conn *Connection) fetchFuture(reqid uint32) *Future { - pos := reqid & (requestsMap-1) - fut := conn.requests[pos] + shard := reqid & (shards-1) + pos := (reqid/shards) & (requestsMap-1) + fut := conn.shard[shard].requests[pos] if fut == nil { return nil } if fut.requestId == reqid { - conn.requests[pos] = fut.next - conn.unlinkFutureTime(fut) + conn.shard[shard].requests[pos] = fut.next + conn.unlinkFutureTime(shard, fut) return fut } for fut.next != nil { if fut.next.requestId == reqid { fut, fut.next = fut.next, fut.next.next - conn.unlinkFutureTime(fut) + conn.unlinkFutureTime(shard, fut) return fut } fut = fut.next @@ -395,15 +418,17 @@ func (conn *Connection) timeouts() { case now := <-t.C: nowepoch = now.Sub(epoch) } - conn.reqmut.Lock() - conn.time.timeout = nowepoch + conn.opts.Timeout - for conn.time.first != nil && conn.time.first.timeout < nowepoch { - fut := conn.time.first - conn.reqmut.Unlock() - fut.timeouted() - conn.reqmut.Lock() + for i := range conn.shard { + conn.shard[i].Lock() + conn.shard[i].timeout = nowepoch + conn.opts.Timeout + for conn.shard[i].first != nil && conn.shard[i].first.timeout < nowepoch { + fut := conn.shard[i].first + conn.shard[i].Unlock() + fut.timeouted() + conn.shard[i].Lock() + } + conn.shard[i].Unlock() } - conn.reqmut.Unlock() } } diff --git a/request.go b/request.go index dd68f6062..b1879023b 100644 --- a/request.go +++ b/request.go @@ -280,14 +280,14 @@ func (fut *Future) send(body func (*msgpack.Encoder)error) *Future { } fut.ready = make(chan struct{}) - fut.conn.reqmut.Lock() + fut.conn.shard[fut.requestId&(shards-1)].Lock() if fut.conn.closed { - fut.conn.reqmut.Unlock() + fut.conn.shard[fut.requestId&shards].Unlock() fut.err = ClientError{ErrConnectionClosed, "using closed connection"} return fut } fut.conn.putFuture(fut) - fut.conn.reqmut.Unlock() + fut.conn.shard[fut.requestId&(shards-1)].Unlock() select { case fut.conn.packets <- (packet): @@ -305,7 +305,7 @@ func (fut *Future) send(body func (*msgpack.Encoder)error) *Future { func (fut *Future) timeouted() { conn := fut.conn - conn.reqmut.Lock() + conn.shard[fut.requestId&(shards-1)].Lock() if f := conn.fetchFuture(fut.requestId); f != nil { if f != fut { panic("future doesn't match") @@ -313,7 +313,7 @@ func (fut *Future) timeouted() { fut.err = fmt.Errorf("client timeout for request %d", fut.requestId) close(fut.ready) } - conn.reqmut.Unlock() + conn.shard[fut.requestId&(shards-1)].Unlock() } func badfuture(err error) *Future { From 6259a583a0f3fd95c8c4e81e084114a64e592553 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 1 Nov 2016 18:26:22 +0300 Subject: [PATCH 105/605] refactor lock a bit --- connection.go | 20 +++++++++++++++----- request.go | 10 ++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/connection.go b/connection.go index 9b8c4fb8a..9fa212c32 100644 --- a/connection.go +++ b/connection.go @@ -346,14 +346,10 @@ func (conn *Connection) reader() { r, _, _ = conn.closeConnection(err, r, nil) continue } - shard := resp.RequestId&(shards-1) - conn.shard[shard].Lock() if fut := conn.fetchFuture(resp.RequestId); fut != nil { fut.resp = resp close(fut.ready) - conn.shard[shard].Unlock() } else { - conn.shard[shard].Unlock() log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) } } @@ -361,6 +357,12 @@ func (conn *Connection) reader() { func (conn *Connection) putFuture(fut *Future) { shard := fut.requestId & (shards-1) + conn.shard[shard].Lock() + if conn.closed { + conn.shard[shard].Unlock() + fut.err = ClientError{ErrConnectionClosed, "using closed connection"} + return + } pos := (fut.requestId/shards) & (requestsMap-1) fut.next = conn.shard[shard].requests[pos] conn.shard[shard].requests[pos] = fut @@ -370,6 +372,7 @@ func (conn *Connection) putFuture(fut *Future) { *conn.shard[shard].last = fut conn.shard[shard].last = &fut.time.next } + conn.shard[shard].Unlock() } func (conn *Connection) unlinkFutureTime(shard uint32, fut *Future) { @@ -384,7 +387,14 @@ func (conn *Connection) unlinkFutureTime(shard uint32, fut *Future) { } } -func (conn *Connection) fetchFuture(reqid uint32) *Future { +func (conn *Connection) fetchFuture(reqid uint32) (fut *Future) { + conn.shard[reqid&(shards-1)].Lock() + fut = conn.fetchFutureImp(reqid) + conn.shard[reqid&(shards-1)].Unlock() + return fut +} + +func (conn *Connection) fetchFutureImp(reqid uint32) *Future { shard := reqid & (shards-1) pos := (reqid/shards) & (requestsMap-1) fut := conn.shard[shard].requests[pos] diff --git a/request.go b/request.go index b1879023b..c6a2f2eae 100644 --- a/request.go +++ b/request.go @@ -280,14 +280,10 @@ func (fut *Future) send(body func (*msgpack.Encoder)error) *Future { } fut.ready = make(chan struct{}) - fut.conn.shard[fut.requestId&(shards-1)].Lock() - if fut.conn.closed { - fut.conn.shard[fut.requestId&shards].Unlock() - fut.err = ClientError{ErrConnectionClosed, "using closed connection"} + fut.conn.putFuture(fut) + if fut.err != nil { return fut } - fut.conn.putFuture(fut) - fut.conn.shard[fut.requestId&(shards-1)].Unlock() select { case fut.conn.packets <- (packet): @@ -305,7 +301,6 @@ func (fut *Future) send(body func (*msgpack.Encoder)error) *Future { func (fut *Future) timeouted() { conn := fut.conn - conn.shard[fut.requestId&(shards-1)].Lock() if f := conn.fetchFuture(fut.requestId); f != nil { if f != fut { panic("future doesn't match") @@ -313,7 +308,6 @@ func (fut *Future) timeouted() { fut.err = fmt.Errorf("client timeout for request %d", fut.requestId) close(fut.ready) } - conn.shard[fut.requestId&(shards-1)].Unlock() } func badfuture(err error) *Future { From 08de027ed8162494d2835f0603aace75f522cb74 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 1 Nov 2016 18:26:37 +0300 Subject: [PATCH 106/605] fix race condition (?) --- request.go | 1 + 1 file changed, 1 insertion(+) diff --git a/request.go b/request.go index c6a2f2eae..fcc06a1d8 100644 --- a/request.go +++ b/request.go @@ -282,6 +282,7 @@ func (fut *Future) send(body func (*msgpack.Encoder)error) *Future { fut.ready = make(chan struct{}) fut.conn.putFuture(fut) if fut.err != nil { + fut.conn.fetchFuture(fut.requestId) return fut } From ff53cc54c9860a3d9135671d0cc1b0030025953c Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 1 Nov 2016 18:41:43 +0300 Subject: [PATCH 107/605] a bit refactor timeout --- connection.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/connection.go b/connection.go index 9fa212c32..1d9bdea83 100644 --- a/connection.go +++ b/connection.go @@ -29,7 +29,6 @@ type Connection struct { Greeting *Greeting shard [shards]struct { sync.Mutex - timeout time.Duration requests []*Future first *Future last **Future @@ -65,7 +64,6 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { opts: opts, } for i := range conn.shard { - conn.shard[i].timeout = time.Now().Sub(epoch) + conn.opts.Timeout conn.shard[i].last = &conn.shard[i].first conn.shard[i].requests = make([]*Future, requestsMap) } @@ -367,7 +365,7 @@ func (conn *Connection) putFuture(fut *Future) { fut.next = conn.shard[shard].requests[pos] conn.shard[shard].requests[pos] = fut if conn.opts.Timeout > 0 { - fut.timeout = conn.shard[shard].timeout + fut.timeout = time.Now().Sub(epoch) + conn.opts.Timeout fut.time.prev = conn.shard[shard].last *conn.shard[shard].last = fut conn.shard[shard].last = &fut.time.next @@ -418,7 +416,11 @@ func (conn *Connection) fetchFutureImp(reqid uint32) *Future { } func (conn *Connection) timeouts() { - t := time.NewTicker(1*time.Millisecond) + timeout := conn.opts.Timeout + if timeout == 0 { + timeout = time.Second + } + t := time.NewTimer(timeout) for { var nowepoch time.Duration select { @@ -428,17 +430,21 @@ func (conn *Connection) timeouts() { case now := <-t.C: nowepoch = now.Sub(epoch) } + minNext := nowepoch + timeout for i := range conn.shard { conn.shard[i].Lock() - conn.shard[i].timeout = nowepoch + conn.opts.Timeout for conn.shard[i].first != nil && conn.shard[i].first.timeout < nowepoch { fut := conn.shard[i].first conn.shard[i].Unlock() fut.timeouted() conn.shard[i].Lock() } + if conn.shard[i].first != nil && conn.shard[i].first.timeout < minNext { + minNext = conn.shard[i].first.timeout + } conn.shard[i].Unlock() } + t.Reset(minNext - time.Now().Sub(epoch)) } } From 4009c11e3687a07cb2749fa8d5f7b2c6ed32a71f Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Tue, 1 Nov 2016 19:20:40 +0300 Subject: [PATCH 108/605] go fmt --- connection.go | 35 ++++++++++++++++++----------------- request.go | 36 ++++++++++++++++++------------------ tarantool_test.go | 5 +++-- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/connection.go b/connection.go index 1d9bdea83..db67e65eb 100644 --- a/connection.go +++ b/connection.go @@ -15,7 +15,8 @@ import ( ) const shards = 16 -const requestsMap = 2*1024 +const requestsMap = 2 * 1024 + var epoch = time.Now() type Connection struct { @@ -27,17 +28,17 @@ type Connection struct { Schema *Schema requestId uint32 Greeting *Greeting - shard [shards]struct { + shard [shards]struct { sync.Mutex requests []*Future - first *Future - last **Future - _pad [128]byte - } - packets chan []byte - control chan struct{} - opts Opts - closed bool + first *Future + last **Future + _pad [128]byte + } + packets chan []byte + control chan struct{} + opts Opts + closed bool } type Greeting struct { @@ -160,9 +161,9 @@ func (conn *Connection) dial() (err error) { func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { request := conn.newFuture(AuthRequest) packet, err := request.pack(func(enc *msgpack.Encoder) error { - return enc.Encode(map[uint32]interface{} { + return enc.Encode(map[uint32]interface{}{ KeyUserName: conn.opts.User, - KeyTuple: []interface{}{string("chap-sha1"), string(scramble)}, + KeyTuple: []interface{}{string("chap-sha1"), string(scramble)}, }) }) if err != nil { @@ -354,14 +355,14 @@ func (conn *Connection) reader() { } func (conn *Connection) putFuture(fut *Future) { - shard := fut.requestId & (shards-1) + shard := fut.requestId & (shards - 1) conn.shard[shard].Lock() if conn.closed { conn.shard[shard].Unlock() fut.err = ClientError{ErrConnectionClosed, "using closed connection"} return } - pos := (fut.requestId/shards) & (requestsMap-1) + pos := (fut.requestId / shards) & (requestsMap - 1) fut.next = conn.shard[shard].requests[pos] conn.shard[shard].requests[pos] = fut if conn.opts.Timeout > 0 { @@ -375,7 +376,7 @@ func (conn *Connection) putFuture(fut *Future) { func (conn *Connection) unlinkFutureTime(shard uint32, fut *Future) { if fut.time.prev != nil { - i := fut.requestId&(shards-1) + i := fut.requestId & (shards - 1) *fut.time.prev = fut.time.next if fut.time.next != nil { fut.time.next.time.prev = fut.time.prev @@ -393,8 +394,8 @@ func (conn *Connection) fetchFuture(reqid uint32) (fut *Future) { } func (conn *Connection) fetchFutureImp(reqid uint32) *Future { - shard := reqid & (shards-1) - pos := (reqid/shards) & (requestsMap-1) + shard := reqid & (shards - 1) + pos := (reqid / shards) & (requestsMap - 1) fut := conn.shard[shard].requests[pos] if fut == nil { return nil diff --git a/request.go b/request.go index fcc06a1d8..8deb3e9e1 100644 --- a/request.go +++ b/request.go @@ -7,15 +7,15 @@ import ( ) type Future struct { - conn *Connection + conn *Connection requestId uint32 requestCode int32 - resp Response - err error - ready chan struct{} - timeout time.Duration - next *Future - time struct { + resp Response + err error + ready chan struct{} + timeout time.Duration + next *Future + time struct { next *Future prev **Future } @@ -31,7 +31,7 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { func (conn *Connection) Ping() (resp *Response, err error) { future := conn.newFuture(PingRequest) - return future.send(func(enc *msgpack.Encoder)error{enc.EncodeMapLen(0);return nil}).Get() + return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(0); return nil }).Get() } func (req *Future) fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key []interface{}) error { @@ -131,7 +131,7 @@ func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, ite if err != nil { return badfuture(err) } - return future.send(func (enc *msgpack.Encoder) error { + return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(6) future.fillIterator(enc, offset, limit, iterator) return future.fillSearch(enc, spaceNo, indexNo, key) @@ -144,7 +144,7 @@ func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Futur if err != nil { return badfuture(err) } - return future.send(func (enc *msgpack.Encoder) error { + return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) return future.fillInsert(enc, spaceNo, tuple) }) @@ -156,7 +156,7 @@ func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Futu if err != nil { return badfuture(err) } - return future.send(func (enc *msgpack.Encoder) error { + return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) return future.fillInsert(enc, spaceNo, tuple) }) @@ -168,7 +168,7 @@ func (conn *Connection) DeleteAsync(space, index interface{}, key []interface{}) if err != nil { return badfuture(err) } - return future.send(func (enc *msgpack.Encoder) error { + return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(3) return future.fillSearch(enc, spaceNo, indexNo, key) }) @@ -180,7 +180,7 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops []interfa if err != nil { return badfuture(err) } - return future.send(func (enc *msgpack.Encoder) error { + return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(4) if err := future.fillSearch(enc, spaceNo, indexNo, key); err != nil { return err @@ -196,7 +196,7 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops [] if err != nil { return badfuture(err) } - return future.send(func (enc *msgpack.Encoder) error { + return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(3) enc.EncodeUint64(KeySpaceNo) enc.EncodeUint64(uint64(spaceNo)) @@ -211,7 +211,7 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops [] func (conn *Connection) CallAsync(functionName string, args []interface{}) *Future { future := conn.newFuture(CallRequest) - return future.send(func (enc *msgpack.Encoder) error { + return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) enc.EncodeUint64(KeyFunctionName) enc.EncodeString(functionName) @@ -222,7 +222,7 @@ func (conn *Connection) CallAsync(functionName string, args []interface{}) *Futu func (conn *Connection) EvalAsync(expr string, args []interface{}) *Future { future := conn.newFuture(EvalRequest) - return future.send(func (enc *msgpack.Encoder) error { + return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) enc.EncodeUint64(KeyExpression) enc.EncodeString(expr) @@ -235,7 +235,7 @@ func (conn *Connection) EvalAsync(expr string, args []interface{}) *Future { // private // -func (fut *Future) pack(body func (*msgpack.Encoder)error) (packet []byte, err error) { +func (fut *Future) pack(body func(*msgpack.Encoder) error) (packet []byte, err error) { rid := fut.requestId h := make(smallWBuf, 0, 48) h = append(h, smallWBuf{ @@ -262,7 +262,7 @@ func (fut *Future) pack(body func (*msgpack.Encoder)error) (packet []byte, err e return } -func (fut *Future) send(body func (*msgpack.Encoder)error) *Future { +func (fut *Future) send(body func(*msgpack.Encoder) error) *Future { // check connection ready to process packets if closed := fut.conn.closed; closed { diff --git a/tarantool_test.go b/tarantool_test.go index 19cec4aa6..3db325249 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -356,14 +356,14 @@ func BenchmarkClientParallelMassive(b *testing.B) { limit := make(chan struct{}, 128*1024) var wg sync.WaitGroup - for i:=0; i Date: Tue, 1 Nov 2016 19:57:05 +0300 Subject: [PATCH 109/605] nulify link --- connection.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/connection.go b/connection.go index db67e65eb..41d317317 100644 --- a/connection.go +++ b/connection.go @@ -383,6 +383,8 @@ func (conn *Connection) unlinkFutureTime(shard uint32, fut *Future) { } else { conn.shard[i].last = fut.time.prev } + fut.time.next = nil + fut.time.prev = nil } } @@ -409,6 +411,7 @@ func (conn *Connection) fetchFutureImp(reqid uint32) *Future { if fut.next.requestId == reqid { fut, fut.next = fut.next, fut.next.next conn.unlinkFutureTime(shard, fut) + fut.next = nil return fut } fut = fut.next From abcfaaa0f33126b4ebc4b6674ed3bb53344f960c Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 6 Nov 2016 11:24:07 +0300 Subject: [PATCH 110/605] cleanup a bit --- request.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/request.go b/request.go index 8deb3e9e1..4f379f93e 100644 --- a/request.go +++ b/request.go @@ -339,9 +339,3 @@ func (fut *Future) GetTyped(result interface{}) error { fut.err = fut.resp.decodeBodyTyped(result) return fut.err } - -var closedtimechan = make(chan time.Time) - -func init() { - close(closedtimechan) -} From 7394e3de657fbac739c08e463353cdb9bce41975 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 6 Nov 2016 11:34:20 +0300 Subject: [PATCH 111/605] add Call17 to make a call in tarantool 1.7 format closes gh-25 --- config.lua | 4 ++++ const.go | 3 ++- request.go | 30 +++++++++++++++++++++++++++++- tarantool_test.go | 11 +++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/config.lua b/config.lua index 3f4fda96e..b53aac1f7 100644 --- a/config.lua +++ b/config.lua @@ -39,6 +39,10 @@ st:truncate() --box.schema.user.grant('guest', 'read,write,execute', 'universe') box.schema.func.create('box.info') +function simple_incr(a) + return a+1 +end +box.schema.func.create('simple_incr') -- auth testing: access control if not box.schema.user.exists('test') then diff --git a/const.go b/const.go index f319a4f85..2088ada3c 100644 --- a/const.go +++ b/const.go @@ -6,10 +6,11 @@ const ( ReplaceRequest = 3 UpdateRequest = 4 DeleteRequest = 5 - CallRequest = 6 + CallRequest = 6 /* call in 1.6 format */ AuthRequest = 7 EvalRequest = 8 UpsertRequest = 9 + Call17Request = 10 PingRequest = 64 SubscribeRequest = 66 diff --git a/request.go b/request.go index 4f379f93e..ac6602e50 100644 --- a/request.go +++ b/request.go @@ -83,15 +83,23 @@ func (conn *Connection) Upsert(space interface{}, tuple, ops []interface{}) (res return conn.UpsertAsync(space, tuple, ops).Get() } +// Call calls registered function. +// It uses request code for tarantool 1.6, so result is converted to array of arrays func (conn *Connection) Call(functionName string, args []interface{}) (resp *Response, err error) { return conn.CallAsync(functionName, args).Get() } +// Call17 calls registered function. +// It uses request code for tarantool 1.7, so result is not converted +// (though, keep in mind, result is always array) +func (conn *Connection) Call17(functionName string, args []interface{}) (resp *Response, err error) { + return conn.Call17Async(functionName, args).Get() +} + func (conn *Connection) Eval(expr string, args []interface{}) (resp *Response, err error) { return conn.EvalAsync(expr, args).Get() } -// Typed methods func (conn *Connection) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key []interface{}, result interface{}) (err error) { return conn.SelectAsync(space, index, offset, limit, iterator, key).GetTyped(result) } @@ -116,10 +124,19 @@ func (conn *Connection) UpsertTyped(space interface{}, tuple, ops []interface{}, return conn.UpsertAsync(space, tuple, ops).GetTyped(result) } +// CallTyped calls registered function. +// It uses request code for tarantool 1.6, so result is converted to array of arrays func (conn *Connection) CallTyped(functionName string, args []interface{}, result interface{}) (err error) { return conn.CallAsync(functionName, args).GetTyped(result) } +// Call17Typed calls registered function. +// It uses request code for tarantool 1.7, so result is not converted +// (though, keep in mind, result is always array) +func (conn *Connection) Call17Typed(functionName string, args []interface{}, result interface{}) (err error) { + return conn.Call17Async(functionName, args).GetTyped(result) +} + func (conn *Connection) EvalTyped(expr string, args []interface{}, result interface{}) (err error) { return conn.EvalAsync(expr, args).GetTyped(result) } @@ -220,6 +237,17 @@ func (conn *Connection) CallAsync(functionName string, args []interface{}) *Futu }) } +func (conn *Connection) Call17Async(functionName string, args []interface{}) *Future { + future := conn.newFuture(Call17Request) + return future.send(func(enc *msgpack.Encoder) error { + enc.EncodeMapLen(2) + enc.EncodeUint64(KeyFunctionName) + enc.EncodeString(functionName) + enc.EncodeUint64(KeyTuple) + return enc.Encode(args) + }) +} + func (conn *Connection) EvalAsync(expr string, args []interface{}) *Future { future := conn.newFuture(EvalRequest) return future.send(func(enc *msgpack.Encoder) error { diff --git a/tarantool_test.go b/tarantool_test.go index 3db325249..10f1ce02b 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -617,6 +617,17 @@ func TestClient(t *testing.T) { t.Errorf("Response.Data is empty after Eval") } + // Call vs Call17 + resp, err = conn.Call("simple_incr", []interface{}{1}) + if resp.Data[0].([]interface{})[0].(uint64) != 2 { + t.Errorf("result is not {{1}} : %v", resp.Data) + } + + resp, err = conn.Call17("simple_incr", []interface{}{1}) + if resp.Data[0].(uint64) != 2 { + t.Errorf("result is not {{1}} : %v", resp.Data) + } + // Eval resp, err = conn.Eval("return 5 + 6", []interface{}{}) if err != nil { From a7dcceec8ab40ba6d500c63d84917744ff06cf54 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 6 Nov 2016 11:34:41 +0300 Subject: [PATCH 112/605] ... --- request.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/request.go b/request.go index ac6602e50..4c198e5bb 100644 --- a/request.go +++ b/request.go @@ -22,11 +22,11 @@ type Future struct { } func (conn *Connection) newFuture(requestCode int32) (fut *Future) { - fut = &Future{} - fut.conn = conn - fut.requestId = conn.nextRequestId() - fut.requestCode = requestCode - return + return &Future{ + conn: conn, + requestId: conn.nextRequestId(), + requestCode: requestCode, + } } func (conn *Connection) Ping() (resp *Response, err error) { From e3e3843956132bead2c02d6c3f2f631901ba505a Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 6 Nov 2016 11:36:48 +0300 Subject: [PATCH 113/605] remove UpsertTyped, closes gh-23 --- request.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/request.go b/request.go index 4c198e5bb..96ff57600 100644 --- a/request.go +++ b/request.go @@ -120,10 +120,6 @@ func (conn *Connection) UpdateTyped(space, index interface{}, key, ops []interfa return conn.UpdateAsync(space, index, key, ops).GetTyped(result) } -func (conn *Connection) UpsertTyped(space interface{}, tuple, ops []interface{}, result interface{}) (err error) { - return conn.UpsertAsync(space, tuple, ops).GetTyped(result) -} - // CallTyped calls registered function. // It uses request code for tarantool 1.6, so result is converted to array of arrays func (conn *Connection) CallTyped(functionName string, args []interface{}, result interface{}) (err error) { From 10e2aa7963f4e83b044af0e334bc57f80683eb85 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 9 Nov 2016 19:15:58 +0300 Subject: [PATCH 114/605] fix error --- config.lua | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/config.lua b/config.lua index b53aac1f7..80589fd57 100644 --- a/config.lua +++ b/config.lua @@ -39,20 +39,19 @@ st:truncate() --box.schema.user.grant('guest', 'read,write,execute', 'universe') box.schema.func.create('box.info') -function simple_incr(a) - return a+1 -end box.schema.func.create('simple_incr') -- auth testing: access control -if not box.schema.user.exists('test') then - box.schema.user.create('test', {password = 'test'}) - box.schema.user.grant('test', 'execute', 'universe') - box.schema.user.grant('test', 'read,write', 'space', 'test') - box.schema.user.grant('test', 'read,write', 'space', 'schematest') -end +box.schema.user.create('test', {password = 'test'}) +box.schema.user.grant('test', 'execute', 'universe') +box.schema.user.grant('test', 'read,write', 'space', 'test') +box.schema.user.grant('test', 'read,write', 'space', 'schematest') end) +function simple_incr(a) + return a+1 +end + box.space.test:truncate() local console = require 'console' console.listen '0.0.0.0:33015' From 8b23679076b8fc19b60cf2f1f150ceb11ce00154 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 9 Nov 2016 19:05:20 +0300 Subject: [PATCH 115/605] try to reduce allocated memory - reuse msgpack decoder for header - reuse msgpack encoder for requests - reuse packet buffers --- connection.go | 111 +++++++++++++++++++++++++++++++++++--------------- request.go | 68 +++++++++++++++++-------------- response.go | 18 ++++---- 3 files changed, 123 insertions(+), 74 deletions(-) diff --git a/connection.go b/connection.go index 41d317317..1228027d0 100644 --- a/connection.go +++ b/connection.go @@ -14,8 +14,8 @@ import ( "time" ) -const shards = 16 -const requestsMap = 2 * 1024 +const shards = 128 +const requestsMap = 128 var epoch = time.Now() @@ -33,12 +33,18 @@ type Connection struct { requests []*Future first *Future last **Future - _pad [128]byte - } - packets chan []byte + buf smallWBuf + bcache smallWBuf + enc *msgpack.Encoder + count uint32 + _pad [3]uint64 + } + rlimit chan struct{} + packets chan struct{} control chan struct{} opts Opts closed bool + dec *msgpack.Decoder } type Greeting struct { @@ -60,9 +66,11 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { addr: addr, requestId: 0, Greeting: &Greeting{}, - packets: make(chan []byte, 16*1024), + rlimit: make(chan struct{}, 1024*1024), + packets: make(chan struct{}, 1024*1024), control: make(chan struct{}), opts: opts, + dec: msgpack.NewDecoder(&smallBuf{}), } for i := range conn.shard { conn.shard[i].last = &conn.shard[i].first @@ -160,7 +168,8 @@ func (conn *Connection) dial() (err error) { func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { request := conn.newFuture(AuthRequest) - packet, err := request.pack(func(enc *msgpack.Encoder) error { + var packet smallWBuf + err = request.pack(&packet, msgpack.NewEncoder(&packet), func(enc *msgpack.Encoder) error { return enc.Encode(map[uint32]interface{}{ KeyUserName: conn.opts.User, KeyTuple: []interface{}{string("chap-sha1"), string(scramble)}, @@ -184,7 +193,7 @@ func (conn *Connection) readAuthResponse(r io.Reader) (err error) { return errors.New("auth: read error " + err.Error()) } resp := Response{buf: smallBuf{b: respBytes}} - err = resp.decodeHeader() + err = resp.decodeHeader(conn.dec) if err != nil { return errors.New("auth: decode response header error " + err.Error()) } @@ -259,6 +268,7 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. requests[pos] = nil for fut != nil { fut.err = neterr + <-conn.rlimit close(fut.ready) fut, fut.next = fut.next, nil } @@ -291,10 +301,11 @@ func (conn *Connection) closeConnectionForever(err error) error { func (conn *Connection) writer() { var w *bufio.Writer var err error + var shardn int +Main: for !conn.closed { - var packet []byte select { - case packet = <-conn.packets: + case <-conn.packets: default: runtime.Gosched() if len(conn.packets) == 0 && w != nil { @@ -303,23 +314,41 @@ func (conn *Connection) writer() { } } select { - case packet = <-conn.packets: + case <-conn.packets: case <-conn.control: return } } - if packet == nil { - return - } if w == nil { if _, w, err = conn.createConnection(); err != nil { conn.closeConnectionForever(err) return } } - if err := write(w, packet); err != nil { - _, w, _ = conn.closeConnection(err, nil, w) - continue + for stop := shardn + shards; shardn != stop; shardn++ { + shard := &conn.shard[shardn&(shards-1)] + if nreq := atomic.LoadUint32(&shard.count); nreq > 0 { + shard.Lock() + nreq, shard.count = shard.count, 0 + packet := shard.buf + shard.buf = nil + shard.Unlock() + if err := write(w, packet); err != nil { + _, w, _ = conn.closeConnection(err, nil, w) + continue Main + } + shard.Lock() + shard.bcache = packet[0:0] + shard.Unlock() + for ; nreq > 1; nreq-- { + select { + case <-conn.packets: + default: + break + } + } + break + } } } } @@ -340,13 +369,14 @@ func (conn *Connection) reader() { continue } resp := Response{buf: smallBuf{b: respBytes}} - err = resp.decodeHeader() + err = resp.decodeHeader(conn.dec) if err != nil { r, _, _ = conn.closeConnection(err, r, nil) continue } if fut := conn.fetchFuture(resp.RequestId); fut != nil { fut.resp = resp + <-conn.rlimit close(fut.ready) } else { log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) @@ -354,24 +384,38 @@ func (conn *Connection) reader() { } } -func (conn *Connection) putFuture(fut *Future) { - shard := fut.requestId & (shards - 1) - conn.shard[shard].Lock() +func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error) { + shardn := fut.requestId & (shards - 1) + shard := &conn.shard[shardn] + shard.Lock() if conn.closed { - conn.shard[shard].Unlock() + shard.Unlock() fut.err = ClientError{ErrConnectionClosed, "using closed connection"} return } + if cap(shard.buf) == 0 { + shard.buf, shard.bcache = shard.bcache, nil + if cap(shard.buf) == 0 { + shard.buf = make(smallWBuf, 0, 128) + } + shard.enc = msgpack.NewEncoder(&shard.buf) + } + if err := fut.pack(&shard.buf, shard.enc, body); err != nil { + fut.err = err + shard.Unlock() + return + } pos := (fut.requestId / shards) & (requestsMap - 1) - fut.next = conn.shard[shard].requests[pos] - conn.shard[shard].requests[pos] = fut + fut.next = shard.requests[pos] + shard.requests[pos] = fut if conn.opts.Timeout > 0 { fut.timeout = time.Now().Sub(epoch) + conn.opts.Timeout - fut.time.prev = conn.shard[shard].last - *conn.shard[shard].last = fut - conn.shard[shard].last = &fut.time.next + fut.time.prev = shard.last + *shard.last = fut + shard.last = &fut.time.next } - conn.shard[shard].Unlock() + atomic.AddUint32(&shard.count, 1) + shard.Unlock() } func (conn *Connection) unlinkFutureTime(shard uint32, fut *Future) { @@ -396,21 +440,22 @@ func (conn *Connection) fetchFuture(reqid uint32) (fut *Future) { } func (conn *Connection) fetchFutureImp(reqid uint32) *Future { - shard := reqid & (shards - 1) + shardn := reqid & (shards - 1) + shard := &conn.shard[shardn] pos := (reqid / shards) & (requestsMap - 1) - fut := conn.shard[shard].requests[pos] + fut := shard.requests[pos] if fut == nil { return nil } if fut.requestId == reqid { - conn.shard[shard].requests[pos] = fut.next - conn.unlinkFutureTime(shard, fut) + shard.requests[pos] = fut.next + conn.unlinkFutureTime(shardn, fut) return fut } for fut.next != nil { if fut.next.requestId == reqid { fut, fut.next = fut.next, fut.next.next - conn.unlinkFutureTime(shard, fut) + conn.unlinkFutureTime(shardn, fut) fut.next = nil return fut } diff --git a/request.go b/request.go index 96ff57600..a95f6d79d 100644 --- a/request.go +++ b/request.go @@ -22,11 +22,28 @@ type Future struct { } func (conn *Connection) newFuture(requestCode int32) (fut *Future) { - return &Future{ - conn: conn, - requestId: conn.nextRequestId(), - requestCode: requestCode, + fut = &Future{} + if conn.opts.Timeout != 0 { + select { + case conn.rlimit <- struct{}{}: + default: + t := time.NewTimer(conn.opts.Timeout / 2) + select { + case conn.rlimit <- struct{}{}: + t.Stop() + case <-t.C: + fut.err = fmt.Errorf("client timeout for request %d", fut.requestId) + return fut + } + } + } else { + conn.rlimit <- struct{}{} } + + fut.conn = conn + fut.requestId = conn.nextRequestId() + fut.requestCode = requestCode + return } func (conn *Connection) Ping() (resp *Response, err error) { @@ -259,10 +276,11 @@ func (conn *Connection) EvalAsync(expr string, args []interface{}) *Future { // private // -func (fut *Future) pack(body func(*msgpack.Encoder) error) (packet []byte, err error) { +func (fut *Future) pack(h *smallWBuf, enc *msgpack.Encoder, body func(*msgpack.Encoder) error) (err error) { rid := fut.requestId - h := make(smallWBuf, 0, 48) - h = append(h, smallWBuf{ + //h := make(smallWBuf, 0, 48) + hl := len(*h) + *h = append(*h, smallWBuf{ 0xce, 0, 0, 0, 0, // length 0x82, // 2 element map KeyCode, byte(fut.requestCode), // request code @@ -271,23 +289,24 @@ func (fut *Future) pack(body func(*msgpack.Encoder) error) (packet []byte, err e byte(rid >> 8), byte(rid), }...) - enc := msgpack.NewEncoder(&h) if err = body(enc); err != nil { + *h = (*h)[:hl] return } - l := uint32(len(h) - 5) - h[1] = byte(l >> 24) - h[2] = byte(l >> 16) - h[3] = byte(l >> 8) - h[4] = byte(l) + l := uint32(len(*h) - 5 - hl) + (*h)[hl+1] = byte(l >> 24) + (*h)[hl+2] = byte(l >> 16) + (*h)[hl+3] = byte(l >> 8) + (*h)[hl+4] = byte(l) - packet = h return } func (fut *Future) send(body func(*msgpack.Encoder) error) *Future { - + if fut.err != nil { + return fut + } // check connection ready to process packets if closed := fut.conn.closed; closed { fut.err = ClientError{ErrConnectionClosed, "using closed connection"} @@ -298,28 +317,14 @@ func (fut *Future) send(body func(*msgpack.Encoder) error) *Future { return fut } - var packet []byte - if packet, fut.err = fut.pack(body); fut.err != nil { - return fut - } - fut.ready = make(chan struct{}) - fut.conn.putFuture(fut) + fut.conn.putFuture(fut, body) if fut.err != nil { fut.conn.fetchFuture(fut.requestId) return fut } - select { - case fut.conn.packets <- (packet): - default: - // if connection is totally closed, then fut.conn.packets will be full - // if connection is busy, we can reach timeout - select { - case fut.conn.packets <- (packet): - case <-fut.ready: - } - } + fut.conn.packets <- struct{}{} return fut } @@ -331,6 +336,7 @@ func (fut *Future) timeouted() { panic("future doesn't match") } fut.err = fmt.Errorf("client timeout for request %d", fut.requestId) + <-conn.rlimit close(fut.ready) } } diff --git a/response.go b/response.go index 2bc429d35..610661196 100644 --- a/response.go +++ b/response.go @@ -17,15 +17,9 @@ func (resp *Response) fill(b []byte) { resp.buf.b = b } -func newResponse(b []byte) (resp *Response, err error) { - resp = &Response{buf: smallBuf{b: b}} - err = resp.decodeHeader() - return -} - -func (resp *Response) decodeHeader() (err error) { +func (resp *Response) decodeHeader(d *msgpack.Decoder) (err error) { var l int - d := msgpack.NewDecoder(&resp.buf) + d.Reset(&resp.buf) if l, err = d.DecodeMapLen(); err != nil { return } @@ -36,13 +30,17 @@ func (resp *Response) decodeHeader() (err error) { } switch cd { case KeySync: - if resp.RequestId, err = d.DecodeUint32(); err != nil { + var rid uint64 + if rid, err = d.DecodeUint64(); err != nil { return } + resp.RequestId = uint32(rid) case KeyCode: - if resp.Code, err = d.DecodeUint32(); err != nil { + var rcode uint64 + if rcode, err = d.DecodeUint64(); err != nil { return } + resp.Code = uint32(rcode) default: if err = d.Skip(); err != nil { return From ee196370a003b998b626d4b60983ba354720ab5e Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 9 Nov 2016 19:30:59 +0300 Subject: [PATCH 116/605] tuning --- connection.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/connection.go b/connection.go index 1228027d0..cae94ffcb 100644 --- a/connection.go +++ b/connection.go @@ -14,8 +14,8 @@ import ( "time" ) -const shards = 128 -const requestsMap = 128 +const shards = 512 +const requestsMap = 16 var epoch = time.Now() @@ -30,14 +30,13 @@ type Connection struct { Greeting *Greeting shard [shards]struct { sync.Mutex - requests []*Future + count uint32 + requests [requestsMap]*Future first *Future last **Future buf smallWBuf - bcache smallWBuf enc *msgpack.Encoder - count uint32 - _pad [3]uint64 + bcache smallWBuf } rlimit chan struct{} packets chan struct{} @@ -74,7 +73,6 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { } for i := range conn.shard { conn.shard[i].last = &conn.shard[i].first - conn.shard[i].requests = make([]*Future, requestsMap) } var reconnect time.Duration From f62aac9c3510e87865329891813ee602fc366e93 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 9 Nov 2016 21:28:32 +0300 Subject: [PATCH 117/605] simplify shard choose + separate lock for buf and queue --- connection.go | 159 +++++++++++++++++++++++++------------------------- request.go | 4 -- 2 files changed, 81 insertions(+), 82 deletions(-) diff --git a/connection.go b/connection.go index cae94ffcb..0c3755d37 100644 --- a/connection.go +++ b/connection.go @@ -15,10 +15,22 @@ import ( ) const shards = 512 -const requestsMap = 16 +const requestsMap = 32 var epoch = time.Now() +type connShard struct { + rmut sync.Mutex + requests [requestsMap]*Future + first *Future + last **Future + bufmut sync.Mutex + buf smallWBuf + enc *msgpack.Encoder + bcache smallWBuf + _pad [16]uint64 +} + type Connection struct { addr string c *net.TCPConn @@ -28,18 +40,11 @@ type Connection struct { Schema *Schema requestId uint32 Greeting *Greeting - shard [shards]struct { - sync.Mutex - count uint32 - requests [requestsMap]*Future - first *Future - last **Future - buf smallWBuf - enc *msgpack.Encoder - bcache smallWBuf - } + + shard [shards]connShard + dirtyShard chan uint32 + rlimit chan struct{} - packets chan struct{} control chan struct{} opts Opts closed bool @@ -62,14 +67,14 @@ type Opts struct { func Connect(addr string, opts Opts) (conn *Connection, err error) { conn = &Connection{ - addr: addr, - requestId: 0, - Greeting: &Greeting{}, - rlimit: make(chan struct{}, 1024*1024), - packets: make(chan struct{}, 1024*1024), - control: make(chan struct{}), - opts: opts, - dec: msgpack.NewDecoder(&smallBuf{}), + addr: addr, + requestId: 0, + Greeting: &Greeting{}, + rlimit: make(chan struct{}, 1024*1024), + dirtyShard: make(chan uint32, shards), + control: make(chan struct{}), + opts: opts, + dec: msgpack.NewDecoder(&smallBuf{}), } for i := range conn.shard { conn.shard[i].last = &conn.shard[i].first @@ -277,13 +282,13 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. func (conn *Connection) lockShards() { for i := range conn.shard { - conn.shard[i].Lock() + conn.shard[i].rmut.Lock() } } func (conn *Connection) unlockShards() { for i := range conn.shard { - conn.shard[i].Unlock() + conn.shard[i].rmut.Unlock() } } @@ -299,20 +304,20 @@ func (conn *Connection) closeConnectionForever(err error) error { func (conn *Connection) writer() { var w *bufio.Writer var err error - var shardn int + var shardn uint32 Main: for !conn.closed { select { - case <-conn.packets: + case shardn = <-conn.dirtyShard: default: runtime.Gosched() - if len(conn.packets) == 0 && w != nil { + if len(conn.dirtyShard) == 0 && w != nil { if err := w.Flush(); err != nil { _, w, _ = conn.closeConnection(err, nil, w) } } select { - case <-conn.packets: + case shardn = <-conn.dirtyShard: case <-conn.control: return } @@ -323,31 +328,18 @@ Main: return } } - for stop := shardn + shards; shardn != stop; shardn++ { - shard := &conn.shard[shardn&(shards-1)] - if nreq := atomic.LoadUint32(&shard.count); nreq > 0 { - shard.Lock() - nreq, shard.count = shard.count, 0 - packet := shard.buf - shard.buf = nil - shard.Unlock() - if err := write(w, packet); err != nil { - _, w, _ = conn.closeConnection(err, nil, w) - continue Main - } - shard.Lock() - shard.bcache = packet[0:0] - shard.Unlock() - for ; nreq > 1; nreq-- { - select { - case <-conn.packets: - default: - break - } - } - break - } + shard := &conn.shard[shardn] + shard.bufmut.Lock() + packet := shard.buf + shard.buf = nil + shard.bufmut.Unlock() + if err := write(w, packet); err != nil { + _, w, _ = conn.closeConnection(err, nil, w) + continue Main } + shard.bufmut.Lock() + shard.bcache = packet[0:0] + shard.bufmut.Unlock() } } @@ -385,12 +377,8 @@ func (conn *Connection) reader() { func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error) { shardn := fut.requestId & (shards - 1) shard := &conn.shard[shardn] - shard.Lock() - if conn.closed { - shard.Unlock() - fut.err = ClientError{ErrConnectionClosed, "using closed connection"} - return - } + shard.bufmut.Lock() + firstWritten := len(shard.buf) == 0 if cap(shard.buf) == 0 { shard.buf, shard.bcache = shard.bcache, nil if cap(shard.buf) == 0 { @@ -398,11 +386,22 @@ func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error } shard.enc = msgpack.NewEncoder(&shard.buf) } + blen := len(shard.buf) if err := fut.pack(&shard.buf, shard.enc, body); err != nil { + shard.buf = shard.buf[:blen] fut.err = err - shard.Unlock() + shard.bufmut.Unlock() + return + } + shard.rmut.Lock() + if conn.closed { + shard.buf = shard.buf[:blen] + shard.bufmut.Unlock() + shard.rmut.Unlock() + fut.err = ClientError{ErrConnectionClosed, "using closed connection"} return } + shard.bufmut.Unlock() pos := (fut.requestId / shards) & (requestsMap - 1) fut.next = shard.requests[pos] shard.requests[pos] = fut @@ -412,17 +411,19 @@ func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error *shard.last = fut shard.last = &fut.time.next } - atomic.AddUint32(&shard.count, 1) - shard.Unlock() + shard.rmut.Unlock() + if firstWritten { + conn.dirtyShard <- shardn + } } -func (conn *Connection) unlinkFutureTime(shard uint32, fut *Future) { +func (conn *Connection) unlinkFutureTime(fut *Future) { if fut.time.prev != nil { - i := fut.requestId & (shards - 1) *fut.time.prev = fut.time.next if fut.time.next != nil { fut.time.next.time.prev = fut.time.prev } else { + i := fut.requestId & (shards - 1) conn.shard[i].last = fut.time.prev } fut.time.next = nil @@ -431,15 +432,15 @@ func (conn *Connection) unlinkFutureTime(shard uint32, fut *Future) { } func (conn *Connection) fetchFuture(reqid uint32) (fut *Future) { - conn.shard[reqid&(shards-1)].Lock() + shard := &conn.shard[reqid&(shards-1)] + shard.rmut.Lock() fut = conn.fetchFutureImp(reqid) - conn.shard[reqid&(shards-1)].Unlock() + shard.rmut.Unlock() return fut } func (conn *Connection) fetchFutureImp(reqid uint32) *Future { - shardn := reqid & (shards - 1) - shard := &conn.shard[shardn] + shard := &conn.shard[reqid&(shards-1)] pos := (reqid / shards) & (requestsMap - 1) fut := shard.requests[pos] if fut == nil { @@ -447,17 +448,18 @@ func (conn *Connection) fetchFutureImp(reqid uint32) *Future { } if fut.requestId == reqid { shard.requests[pos] = fut.next - conn.unlinkFutureTime(shardn, fut) + conn.unlinkFutureTime(fut) return fut } for fut.next != nil { - if fut.next.requestId == reqid { - fut, fut.next = fut.next, fut.next.next - conn.unlinkFutureTime(shardn, fut) + next := fut.next + if next.requestId == reqid { + fut, fut.next = next, next.next fut.next = nil + conn.unlinkFutureTime(fut) return fut } - fut = fut.next + fut = next } return nil } @@ -479,17 +481,18 @@ func (conn *Connection) timeouts() { } minNext := nowepoch + timeout for i := range conn.shard { - conn.shard[i].Lock() - for conn.shard[i].first != nil && conn.shard[i].first.timeout < nowepoch { - fut := conn.shard[i].first - conn.shard[i].Unlock() + shard := &conn.shard[i] + shard.rmut.Lock() + for shard.first != nil && shard.first.timeout < nowepoch { + fut := shard.first + shard.rmut.Unlock() fut.timeouted() - conn.shard[i].Lock() + shard.rmut.Lock() } - if conn.shard[i].first != nil && conn.shard[i].first.timeout < minNext { - minNext = conn.shard[i].first.timeout + if shard.first != nil && shard.first.timeout < minNext { + minNext = shard.first.timeout } - conn.shard[i].Unlock() + shard.rmut.Unlock() } t.Reset(minNext - time.Now().Sub(epoch)) } diff --git a/request.go b/request.go index a95f6d79d..0cdc82a72 100644 --- a/request.go +++ b/request.go @@ -278,7 +278,6 @@ func (conn *Connection) EvalAsync(expr string, args []interface{}) *Future { func (fut *Future) pack(h *smallWBuf, enc *msgpack.Encoder, body func(*msgpack.Encoder) error) (err error) { rid := fut.requestId - //h := make(smallWBuf, 0, 48) hl := len(*h) *h = append(*h, smallWBuf{ 0xce, 0, 0, 0, 0, // length @@ -290,7 +289,6 @@ func (fut *Future) pack(h *smallWBuf, enc *msgpack.Encoder, body func(*msgpack.E }...) if err = body(enc); err != nil { - *h = (*h)[:hl] return } @@ -324,8 +322,6 @@ func (fut *Future) send(body func(*msgpack.Encoder) error) *Future { return fut } - fut.conn.packets <- struct{}{} - return fut } From bd7308b2a7503562a4c3c13f9cf4d65fb91359ae Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 9 Nov 2016 21:56:17 +0300 Subject: [PATCH 118/605] simpler request buffer cache --- connection.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/connection.go b/connection.go index 0c3755d37..6015dd2b6 100644 --- a/connection.go +++ b/connection.go @@ -27,7 +27,6 @@ type connShard struct { bufmut sync.Mutex buf smallWBuf enc *msgpack.Encoder - bcache smallWBuf _pad [16]uint64 } @@ -305,6 +304,7 @@ func (conn *Connection) writer() { var w *bufio.Writer var err error var shardn uint32 + var packet smallWBuf Main: for !conn.closed { select { @@ -330,16 +330,13 @@ Main: } shard := &conn.shard[shardn] shard.bufmut.Lock() - packet := shard.buf - shard.buf = nil + packet, shard.buf = shard.buf, packet shard.bufmut.Unlock() if err := write(w, packet); err != nil { _, w, _ = conn.closeConnection(err, nil, w) continue Main } - shard.bufmut.Lock() - shard.bcache = packet[0:0] - shard.bufmut.Unlock() + packet = packet[0:0] } } @@ -380,10 +377,7 @@ func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error shard.bufmut.Lock() firstWritten := len(shard.buf) == 0 if cap(shard.buf) == 0 { - shard.buf, shard.bcache = shard.bcache, nil - if cap(shard.buf) == 0 { - shard.buf = make(smallWBuf, 0, 128) - } + shard.buf = make(smallWBuf, 0, 128) shard.enc = msgpack.NewEncoder(&shard.buf) } blen := len(shard.buf) From 58b298ece7c93b430292f5603163debf4e75c597 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 9 Nov 2016 22:14:39 +0300 Subject: [PATCH 119/605] remove rlimit --- connection.go | 5 ++--- request.go | 18 ------------------ 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/connection.go b/connection.go index 6015dd2b6..02277d05d 100644 --- a/connection.go +++ b/connection.go @@ -69,7 +69,6 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { addr: addr, requestId: 0, Greeting: &Greeting{}, - rlimit: make(chan struct{}, 1024*1024), dirtyShard: make(chan uint32, shards), control: make(chan struct{}), opts: opts, @@ -270,7 +269,7 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. requests[pos] = nil for fut != nil { fut.err = neterr - <-conn.rlimit + //<-conn.rlimit close(fut.ready) fut, fut.next = fut.next, nil } @@ -363,7 +362,7 @@ func (conn *Connection) reader() { } if fut := conn.fetchFuture(resp.RequestId); fut != nil { fut.resp = resp - <-conn.rlimit + //<-conn.rlimit close(fut.ready) } else { log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) diff --git a/request.go b/request.go index 0cdc82a72..986f2c4c6 100644 --- a/request.go +++ b/request.go @@ -23,23 +23,6 @@ type Future struct { func (conn *Connection) newFuture(requestCode int32) (fut *Future) { fut = &Future{} - if conn.opts.Timeout != 0 { - select { - case conn.rlimit <- struct{}{}: - default: - t := time.NewTimer(conn.opts.Timeout / 2) - select { - case conn.rlimit <- struct{}{}: - t.Stop() - case <-t.C: - fut.err = fmt.Errorf("client timeout for request %d", fut.requestId) - return fut - } - } - } else { - conn.rlimit <- struct{}{} - } - fut.conn = conn fut.requestId = conn.nextRequestId() fut.requestCode = requestCode @@ -332,7 +315,6 @@ func (fut *Future) timeouted() { panic("future doesn't match") } fut.err = fmt.Errorf("client timeout for request %d", fut.requestId) - <-conn.rlimit close(fut.ready) } } From 18ee1cd5a3fab514f73552b0746479d6aa77f517 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 9 Nov 2016 22:47:34 +0300 Subject: [PATCH 120/605] close connetion in tests --- tarantool_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tarantool_test.go b/tarantool_test.go index 10f1ce02b..c86a7d191 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -165,6 +165,7 @@ func BenchmarkClientSerial(b *testing.B) { var err error conn, err := Connect(server, opts) + defer conn.Close() if err != nil { b.Errorf("No connection available") return @@ -187,6 +188,7 @@ func BenchmarkClientFuture(b *testing.B) { var err error conn, err := Connect(server, opts) + defer conn.Close() if err != nil { b.Error(err) return @@ -216,6 +218,7 @@ func BenchmarkClientFutureTyped(b *testing.B) { var err error conn, err := Connect(server, opts) + defer conn.Close() if err != nil { b.Errorf("No connection available") return @@ -248,6 +251,7 @@ func BenchmarkClientFutureParallel(b *testing.B) { var err error conn, err := Connect(server, opts) + defer conn.Close() if err != nil { b.Errorf("No connection available") return @@ -283,6 +287,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { var err error conn, err := Connect(server, opts) + defer conn.Close() if err != nil { b.Errorf("No connection available") return @@ -321,6 +326,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { func BenchmarkClientParallel(b *testing.B) { conn, err := Connect(server, opts) + defer conn.Close() if err != nil { b.Errorf("No connection available") return @@ -344,6 +350,7 @@ func BenchmarkClientParallel(b *testing.B) { func BenchmarkClientParallelMassive(b *testing.B) { conn, err := Connect(server, opts) + defer conn.Close() if err != nil { b.Errorf("No connection available") return @@ -381,6 +388,7 @@ func TestClient(t *testing.T) { var conn *Connection conn, err = Connect(server, opts) + defer conn.Close() if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -650,6 +658,7 @@ func TestSchema(t *testing.T) { var conn *Connection conn, err = Connect(server, opts) + defer conn.Close() if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -833,6 +842,7 @@ func TestClientNamed(t *testing.T) { var conn *Connection conn, err = Connect(server, opts) + defer conn.Close() if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -924,6 +934,7 @@ func TestComplexStructs(t *testing.T) { var conn *Connection conn, err = Connect(server, opts) + defer conn.Close() if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return From ac59aadf3fe7c9b57a8dab6bbd069811a407daa6 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 9 Nov 2016 23:01:56 +0300 Subject: [PATCH 121/605] ... --- connection.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/connection.go b/connection.go index 02277d05d..8b417008a 100644 --- a/connection.go +++ b/connection.go @@ -43,7 +43,6 @@ type Connection struct { shard [shards]connShard dirtyShard chan uint32 - rlimit chan struct{} control chan struct{} opts Opts closed bool @@ -269,7 +268,6 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. requests[pos] = nil for fut != nil { fut.err = neterr - //<-conn.rlimit close(fut.ready) fut, fut.next = fut.next, nil } @@ -362,7 +360,6 @@ func (conn *Connection) reader() { } if fut := conn.fetchFuture(resp.RequestId); fut != nil { fut.resp = resp - //<-conn.rlimit close(fut.ready) } else { log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) From 58e7917ddc0ed89373248ded9b48d5e4853b4b0c Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 9 Nov 2016 23:12:21 +0300 Subject: [PATCH 122/605] refactor requests map, so less shards needed --- connection.go | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/connection.go b/connection.go index 8b417008a..a0422ed0c 100644 --- a/connection.go +++ b/connection.go @@ -14,14 +14,14 @@ import ( "time" ) -const shards = 512 +const shards = 32 const requestsMap = 32 var epoch = time.Now() type connShard struct { rmut sync.Mutex - requests [requestsMap]*Future + requests [requestsMap]struct{ first, last *Future } first *Future last **Future bufmut sync.Mutex @@ -264,8 +264,10 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. defer conn.unlockShards() for i := range conn.shard { requests := conn.shard[i].requests - for pos, fut := range requests { - requests[pos] = nil + for pos, pair := range requests { + fut := pair.first + requests[pos].first = nil + requests[pos].last = nil for fut != nil { fut.err = neterr close(fut.ready) @@ -393,8 +395,13 @@ func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error } shard.bufmut.Unlock() pos := (fut.requestId / shards) & (requestsMap - 1) - fut.next = shard.requests[pos] - shard.requests[pos] = fut + pair := &shard.requests[pos] + if pair.last == nil { + shard.requests[pos].first = fut + } else { + shard.requests[pos].last.next = fut + } + shard.requests[pos].last = fut if conn.opts.Timeout > 0 { fut.timeout = time.Now().Sub(epoch) + conn.opts.Timeout fut.time.prev = shard.last @@ -432,18 +439,25 @@ func (conn *Connection) fetchFuture(reqid uint32) (fut *Future) { func (conn *Connection) fetchFutureImp(reqid uint32) *Future { shard := &conn.shard[reqid&(shards-1)] pos := (reqid / shards) & (requestsMap - 1) - fut := shard.requests[pos] + pair := &shard.requests[pos] + fut := pair.first if fut == nil { return nil } if fut.requestId == reqid { - shard.requests[pos] = fut.next + pair.first = fut.next + if pair.last == fut { + pair.last = nil + } conn.unlinkFutureTime(fut) return fut } for fut.next != nil { next := fut.next if next.requestId == reqid { + if pair.last == next { + pair.last = fut + } fut, fut.next = next, next.next fut.next = nil conn.unlinkFutureTime(fut) From 27616ed0e5879d32ee9d7bb8a966d07427a59e4c Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 9 Nov 2016 23:20:16 +0300 Subject: [PATCH 123/605] do not allocate Respone inplace compiler didn't remove allocation in connection.reader :-( --- connection.go | 2 +- request.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/connection.go b/connection.go index a0422ed0c..43027759c 100644 --- a/connection.go +++ b/connection.go @@ -354,7 +354,7 @@ func (conn *Connection) reader() { r, _, _ = conn.closeConnection(err, r, nil) continue } - resp := Response{buf: smallBuf{b: respBytes}} + resp := &Response{buf: smallBuf{b: respBytes}} err = resp.decodeHeader(conn.dec) if err != nil { r, _, _ = conn.closeConnection(err, r, nil) diff --git a/request.go b/request.go index 986f2c4c6..e3cf0dc69 100644 --- a/request.go +++ b/request.go @@ -10,7 +10,7 @@ type Future struct { conn *Connection requestId uint32 requestCode int32 - resp Response + resp *Response err error ready chan struct{} timeout time.Duration @@ -333,10 +333,10 @@ func (fut *Future) wait() { func (fut *Future) Get() (*Response, error) { fut.wait() if fut.err != nil { - return &fut.resp, fut.err + return fut.resp, fut.err } fut.err = fut.resp.decodeBody() - return &fut.resp, fut.err + return fut.resp, fut.err } func (fut *Future) GetTyped(result interface{}) error { From 0a40156f2bc29a2ab4425c2ca9eaf96b9a7cb7ce Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 9 Nov 2016 23:26:23 +0300 Subject: [PATCH 124/605] cache lenbuf --- connection.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/connection.go b/connection.go index 43027759c..480607a77 100644 --- a/connection.go +++ b/connection.go @@ -47,6 +47,7 @@ type Connection struct { opts Opts closed bool dec *msgpack.Decoder + lenbuf [PacketLengthBytes]byte } type Greeting struct { @@ -188,7 +189,7 @@ func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err } func (conn *Connection) readAuthResponse(r io.Reader) (err error) { - respBytes, err := read(r) + respBytes, err := conn.read(r) if err != nil { return errors.New("auth: read error " + err.Error()) } @@ -349,7 +350,7 @@ func (conn *Connection) reader() { return } } - respBytes, err := read(r) + respBytes, err := conn.read(r) if err != nil { r, _, _ = conn.closeConnection(err, r, nil) continue @@ -513,21 +514,20 @@ func write(w io.Writer, data []byte) (err error) { return } -func read(r io.Reader) (response []byte, err error) { - var lenbuf [PacketLengthBytes]byte +func (conn *Connection) read(r io.Reader) (response []byte, err error) { var length int - if _, err = io.ReadFull(r, lenbuf[:]); err != nil { + if _, err = io.ReadFull(r, conn.lenbuf[:]); err != nil { return } - if lenbuf[0] != 0xce { + if conn.lenbuf[0] != 0xce { err = errors.New("Wrong reponse header") return } - length = (int(lenbuf[1]) << 24) + - (int(lenbuf[2]) << 16) + - (int(lenbuf[3]) << 8) + - int(lenbuf[4]) + length = (int(conn.lenbuf[1]) << 24) + + (int(conn.lenbuf[2]) << 16) + + (int(conn.lenbuf[3]) << 8) + + int(conn.lenbuf[4]) if length == 0 { err = errors.New("Response should not be 0 length") From 3af6afa1752bb35174f6f6db8d4be16e73ccd67e Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 9 Nov 2016 23:59:19 +0300 Subject: [PATCH 125/605] remove array requirements for keys and opts. FIXME: ADD DOCS!!!!!! --- request.go | 46 ++++++++++++++++++++++++-------------------- response.go | 49 ++++++++++++++++++++++++++++++++++++++--------- tarantool_test.go | 17 ++++++++++++---- 3 files changed, 78 insertions(+), 34 deletions(-) diff --git a/request.go b/request.go index e3cf0dc69..c7a8d1829 100644 --- a/request.go +++ b/request.go @@ -34,7 +34,7 @@ func (conn *Connection) Ping() (resp *Response, err error) { return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(0); return nil }).Get() } -func (req *Future) fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key []interface{}) error { +func (req *Future) fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { enc.EncodeUint64(KeySpaceNo) enc.EncodeUint64(uint64(spaceNo)) enc.EncodeUint64(KeyIndexNo) @@ -59,7 +59,7 @@ func (req *Future) fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interf return enc.Encode(tuple) } -func (conn *Connection) Select(space, index interface{}, offset, limit, iterator uint32, key []interface{}) (resp *Response, err error) { +func (conn *Connection) Select(space, index interface{}, offset, limit, iterator uint32, key interface{}) (resp *Response, err error) { return conn.SelectAsync(space, index, offset, limit, iterator, key).Get() } @@ -71,36 +71,36 @@ func (conn *Connection) Replace(space interface{}, tuple interface{}) (resp *Res return conn.ReplaceAsync(space, tuple).Get() } -func (conn *Connection) Delete(space, index interface{}, key []interface{}) (resp *Response, err error) { +func (conn *Connection) Delete(space, index interface{}, key interface{}) (resp *Response, err error) { return conn.DeleteAsync(space, index, key).Get() } -func (conn *Connection) Update(space, index interface{}, key, ops []interface{}) (resp *Response, err error) { +func (conn *Connection) Update(space, index interface{}, key, ops interface{}) (resp *Response, err error) { return conn.UpdateAsync(space, index, key, ops).Get() } -func (conn *Connection) Upsert(space interface{}, tuple, ops []interface{}) (resp *Response, err error) { +func (conn *Connection) Upsert(space interface{}, tuple, ops interface{}) (resp *Response, err error) { return conn.UpsertAsync(space, tuple, ops).Get() } // Call calls registered function. // It uses request code for tarantool 1.6, so result is converted to array of arrays -func (conn *Connection) Call(functionName string, args []interface{}) (resp *Response, err error) { +func (conn *Connection) Call(functionName string, args interface{}) (resp *Response, err error) { return conn.CallAsync(functionName, args).Get() } // Call17 calls registered function. // It uses request code for tarantool 1.7, so result is not converted // (though, keep in mind, result is always array) -func (conn *Connection) Call17(functionName string, args []interface{}) (resp *Response, err error) { +func (conn *Connection) Call17(functionName string, args interface{}) (resp *Response, err error) { return conn.Call17Async(functionName, args).Get() } -func (conn *Connection) Eval(expr string, args []interface{}) (resp *Response, err error) { +func (conn *Connection) Eval(expr string, args interface{}) (resp *Response, err error) { return conn.EvalAsync(expr, args).Get() } -func (conn *Connection) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key []interface{}, result interface{}) (err error) { +func (conn *Connection) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key interface{}, result interface{}) (err error) { return conn.SelectAsync(space, index, offset, limit, iterator, key).GetTyped(result) } @@ -112,33 +112,37 @@ func (conn *Connection) ReplaceTyped(space interface{}, tuple interface{}, resul return conn.ReplaceAsync(space, tuple).GetTyped(result) } -func (conn *Connection) DeleteTyped(space, index interface{}, key []interface{}, result interface{}) (err error) { +func (conn *Connection) DeleteTyped(space, index interface{}, key interface{}, result interface{}) (err error) { return conn.DeleteAsync(space, index, key).GetTyped(result) } -func (conn *Connection) UpdateTyped(space, index interface{}, key, ops []interface{}, result interface{}) (err error) { +func (conn *Connection) UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) (err error) { return conn.UpdateAsync(space, index, key, ops).GetTyped(result) } // CallTyped calls registered function. // It uses request code for tarantool 1.6, so result is converted to array of arrays -func (conn *Connection) CallTyped(functionName string, args []interface{}, result interface{}) (err error) { +// Attention: args should serialize into array of arguments +func (conn *Connection) CallTyped(functionName string, args interface{}, result interface{}) (err error) { return conn.CallAsync(functionName, args).GetTyped(result) } // Call17Typed calls registered function. // It uses request code for tarantool 1.7, so result is not converted // (though, keep in mind, result is always array) -func (conn *Connection) Call17Typed(functionName string, args []interface{}, result interface{}) (err error) { +// Attention: args should serialize into array of arguments +func (conn *Connection) Call17Typed(functionName string, args interface{}, result interface{}) (err error) { return conn.Call17Async(functionName, args).GetTyped(result) } -func (conn *Connection) EvalTyped(expr string, args []interface{}, result interface{}) (err error) { +// EvalTyped evals arbitrary lua expression +// Attention: args should serialize into array of arguments +func (conn *Connection) EvalTyped(expr string, args interface{}, result interface{}) (err error) { return conn.EvalAsync(expr, args).GetTyped(result) } // Async methods -func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key []interface{}) *Future { +func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Future { future := conn.newFuture(SelectRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) if err != nil { @@ -175,7 +179,7 @@ func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Futu }) } -func (conn *Connection) DeleteAsync(space, index interface{}, key []interface{}) *Future { +func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) *Future { future := conn.newFuture(DeleteRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) if err != nil { @@ -187,7 +191,7 @@ func (conn *Connection) DeleteAsync(space, index interface{}, key []interface{}) }) } -func (conn *Connection) UpdateAsync(space, index interface{}, key, ops []interface{}) *Future { +func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface{}) *Future { future := conn.newFuture(UpdateRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) if err != nil { @@ -203,7 +207,7 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops []interfa }) } -func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops []interface{}) *Future { +func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *Future { future := conn.newFuture(UpsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { @@ -222,7 +226,7 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops [] }) } -func (conn *Connection) CallAsync(functionName string, args []interface{}) *Future { +func (conn *Connection) CallAsync(functionName string, args interface{}) *Future { future := conn.newFuture(CallRequest) return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) @@ -233,7 +237,7 @@ func (conn *Connection) CallAsync(functionName string, args []interface{}) *Futu }) } -func (conn *Connection) Call17Async(functionName string, args []interface{}) *Future { +func (conn *Connection) Call17Async(functionName string, args interface{}) *Future { future := conn.newFuture(Call17Request) return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) @@ -244,7 +248,7 @@ func (conn *Connection) Call17Async(functionName string, args []interface{}) *Fu }) } -func (conn *Connection) EvalAsync(expr string, args []interface{}) *Future { +func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { future := conn.newFuture(EvalRequest) return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) diff --git a/response.go b/response.go index 610661196..dbb622f7f 100644 --- a/response.go +++ b/response.go @@ -17,6 +17,18 @@ func (resp *Response) fill(b []byte) { resp.buf.b = b } +func (resp *Response) smallInt(d *msgpack.Decoder) (i int, err error) { + b, err := resp.buf.ReadByte() + if err != nil { + return + } + if b <= 127 { + return int(b), nil + } + resp.buf.UnreadByte() + return d.DecodeInt() +} + func (resp *Response) decodeHeader(d *msgpack.Decoder) (err error) { var l int d.Reset(&resp.buf) @@ -25,7 +37,7 @@ func (resp *Response) decodeHeader(d *msgpack.Decoder) (err error) { } for ; l > 0; l-- { var cd int - if cd, err = d.DecodeInt(); err != nil { + if cd, err = resp.smallInt(d); err != nil { return } switch cd { @@ -52,16 +64,35 @@ func (resp *Response) decodeHeader(d *msgpack.Decoder) (err error) { func (resp *Response) decodeBody() (err error) { if resp.buf.Len() > 2 { - var body map[int]interface{} + var l int d := msgpack.NewDecoder(&resp.buf) - if err = d.Decode(&body); err != nil { + if l, err = d.DecodeMapLen(); err != nil { return err } - if body[KeyData] != nil { - resp.Data = body[KeyData].([]interface{}) - } - if body[KeyError] != nil { - resp.Error = body[KeyError].(string) + for ; l > 0; l-- { + var cd int + if cd, err = resp.smallInt(d); err != nil { + return err + } + switch cd { + case KeyData: + var res interface{} + var ok bool + if res, err = d.DecodeInterface(); err != nil { + return err + } + if resp.Data, ok = res.([]interface{}); !ok { + return fmt.Errorf("result is not array: %v", res) + } + case KeyError: + if resp.Error, err = d.DecodeString(); err != nil { + return err + } + default: + if err = d.Skip(); err != nil { + return err + } + } } if resp.Code != OkCode { resp.Code &^= ErrorCodeBit @@ -80,7 +111,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { } for ; l > 0; l-- { var cd int - if cd, err = d.DecodeInt(); err != nil { + if cd, err = resp.smallInt(d); err != nil { return err } switch cd { diff --git a/tarantool_test.go b/tarantool_test.go index c86a7d191..dc47a5243 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -28,6 +28,10 @@ type Tuple2 struct { Members []Member } +type IntKey struct { + id uint +} + func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { t := v.Interface().(Tuple) if err := e.EncodeSliceLen(3); err != nil { @@ -146,6 +150,12 @@ func decodeTuple2(d *msgpack.Decoder, v reflect.Value) error { return nil } +func (k IntKey) EncodeMsgpack(enc *msgpack.Encoder) error { + enc.EncodeSliceLen(1) + enc.EncodeUint(k.id) + return nil +} + func init() { msgpack.Register(reflect.TypeOf(Tuple{}), encodeTuple, decodeTuple) msgpack.Register(reflect.TypeOf(Tuple2{}), encodeTuple2, decodeTuple2) @@ -232,7 +242,7 @@ func BenchmarkClientFutureTyped(b *testing.B) { for i := 0; i < b.N; i += N { var fs [N]*Future for j := 0; j < N; j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, IntKey{1111}) } for j := 0; j < N; j++ { var r []Tuple @@ -304,7 +314,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { var fs [N]*Future var j int for j = 0; j < N && pb.Next(); j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) + fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, IntKey{1111}) } exit = j < N for j > 0 { @@ -368,8 +378,7 @@ func BenchmarkClientParallelMassive(b *testing.B) { limit <- struct{}{} go func() { var r []Tuple - err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}, &r) - //_, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) + err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, IntKey{1111}, &r) <-limit wg.Done() if err != nil { From 8c8b93a277655af236198af63a4def5a0aa77e2f Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 10 Nov 2016 00:06:49 +0300 Subject: [PATCH 126/605] change BenchmarkClientParallelMassive --- tarantool_test.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tarantool_test.go b/tarantool_test.go index dc47a5243..3aabbe198 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -371,22 +371,29 @@ func BenchmarkClientParallelMassive(b *testing.B) { b.Errorf("No connection available") } - limit := make(chan struct{}, 128*1024) var wg sync.WaitGroup - for i := 0; i < b.N; i++ { - wg.Add(1) - limit <- struct{}{} + limit := make(chan struct{}, 128*1024) + for i := 0; i < 512; i++ { go func() { var r []Tuple - err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, IntKey{1111}, &r) - <-limit - wg.Done() - if err != nil { - b.Errorf("No connection available") + for { + if _, ok := <-limit; !ok { + break + } + err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, IntKey{1111}, &r) + wg.Done() + if err != nil { + b.Errorf("No connection available") + } } }() } + for i := 0; i < b.N; i++ { + wg.Add(1) + limit <- struct{}{} + } wg.Wait() + close(limit) } /////////////////// From 6ab57b1b4a211168c598465ff49496446c144255 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 10 Nov 2016 00:42:36 +0300 Subject: [PATCH 127/605] clear buffers on connection close --- connection.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/connection.go b/connection.go index 480607a77..3897fb74f 100644 --- a/connection.go +++ b/connection.go @@ -228,7 +228,9 @@ func (conn *Connection) createConnection() (r *bufio.Reader, w *bufio.Writer, er log.Printf("tarantool: last reconnect to %s failed: %s, giving it up.\n", conn.addr, err.Error()) err = ClientError{ErrConnectionClosed, "last reconnect failed"} // mark connection as closed to avoid reopening by another goroutine + conn.lockShards() conn.closed = true + conn.unlockShards() return } log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s\n", reconnects, conn.opts.MaxReconnects, conn.addr, err.Error()) @@ -264,6 +266,7 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. conn.lockShards() defer conn.unlockShards() for i := range conn.shard { + conn.shard[i].buf = conn.shard[i].buf[:0] requests := conn.shard[i].requests for pos, pair := range requests { fut := pair.first @@ -281,12 +284,14 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. func (conn *Connection) lockShards() { for i := range conn.shard { + conn.shard[i].bufmut.Lock() conn.shard[i].rmut.Lock() } } func (conn *Connection) unlockShards() { for i := range conn.shard { + conn.shard[i].bufmut.Unlock() conn.shard[i].rmut.Unlock() } } @@ -332,9 +337,12 @@ Main: shard.bufmut.Lock() packet, shard.buf = shard.buf, packet shard.bufmut.Unlock() + if len(packet) == 0 { + continue + } if err := write(w, packet); err != nil { _, w, _ = conn.closeConnection(err, nil, w) - continue Main + continue } packet = packet[0:0] } From 90500dd8d118696e2fe29c99608756d988f2ac54 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 10 Nov 2016 00:44:59 +0300 Subject: [PATCH 128/605] ... fix --- connection.go | 1 - 1 file changed, 1 deletion(-) diff --git a/connection.go b/connection.go index 3897fb74f..04518945e 100644 --- a/connection.go +++ b/connection.go @@ -310,7 +310,6 @@ func (conn *Connection) writer() { var err error var shardn uint32 var packet smallWBuf -Main: for !conn.closed { select { case shardn = <-conn.dirtyShard: From 4e34cb38c83fabbbee9d01ff75d3b9dd39b03cf6 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 10 Nov 2016 00:46:16 +0300 Subject: [PATCH 129/605] ... --- connection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connection.go b/connection.go index 04518945e..80da34b4b 100644 --- a/connection.go +++ b/connection.go @@ -47,7 +47,7 @@ type Connection struct { opts Opts closed bool dec *msgpack.Decoder - lenbuf [PacketLengthBytes]byte + lenbuf [PacketLengthBytes]byte } type Greeting struct { From 2b838dba40e11cac903ae76f4a4889c1bfda13ee Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 10 Nov 2016 22:43:23 +0300 Subject: [PATCH 130/605] huge refactor --- connection.go | 182 +++++++++++++++++++++++++++++----------------- request.go | 26 +------ tarantool_test.go | 32 ++++---- 3 files changed, 139 insertions(+), 101 deletions(-) diff --git a/connection.go b/connection.go index 80da34b4b..7213d0a28 100644 --- a/connection.go +++ b/connection.go @@ -12,18 +12,16 @@ import ( "sync" "sync/atomic" "time" + "fmt" ) -const shards = 32 -const requestsMap = 32 +const requestsMap = 128 var epoch = time.Now() type connShard struct { rmut sync.Mutex requests [requestsMap]struct{ first, last *Future } - first *Future - last **Future bufmut sync.Mutex buf smallWBuf enc *msgpack.Encoder @@ -40,10 +38,11 @@ type Connection struct { requestId uint32 Greeting *Greeting - shard [shards]connShard + shard []connShard dirtyShard chan uint32 control chan struct{} + rlimit chan struct{} opts Opts closed bool dec *msgpack.Decoder @@ -61,6 +60,20 @@ type Opts struct { MaxReconnects uint User string Pass string + // RateLimit number of simultaneously executed requests. + // All new requests will wait until answer for some + // previous requests arrived, or will be timeouted. + // It has its runtime cost, so it is disabled by default. + // On localhost connection and fast selects, 8192 is already + // reasonably large value for RateLimit. + // But if command lasts more time (lua, updates), or network + // is laggy, then larger value should be considered. + RateLimit uint + // Concurrency is amount of separate mutexes for request + // queues and buffers inside of connection. + // It is rounded upto nearest power of 2. + // By default it is runtime.GOMAXPROCS(-1) * 4 + Concurrency uint32 } func Connect(addr string, opts Opts) (conn *Connection, err error) { @@ -69,13 +82,25 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { addr: addr, requestId: 0, Greeting: &Greeting{}, - dirtyShard: make(chan uint32, shards), control: make(chan struct{}), opts: opts, dec: msgpack.NewDecoder(&smallBuf{}), } - for i := range conn.shard { - conn.shard[i].last = &conn.shard[i].first + maxprocs := uint32(runtime.GOMAXPROCS(-1)) + if conn.opts.Concurrency == 0 || conn.opts.Concurrency > maxprocs * 128 { + conn.opts.Concurrency = maxprocs * 4 + } + if c := conn.opts.Concurrency; c & (c-1) != 0 { + for i := uint(1); i<32; i*=2 { + c |= c >> i + } + conn.opts.Concurrency = c + 1 + } + conn.dirtyShard = make(chan uint32, conn.opts.Concurrency) + conn.shard = make([]connShard, conn.opts.Concurrency) + + if opts.RateLimit > 0 { + conn.rlimit = make(chan struct{}, opts.RateLimit); } var reconnect time.Duration @@ -168,7 +193,11 @@ func (conn *Connection) dial() (err error) { } func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { - request := conn.newFuture(AuthRequest) + request := &Future{ + conn: conn, + requestId: 0, + requestCode: AuthRequest, + } var packet smallWBuf err = request.pack(&packet, msgpack.NewEncoder(&packet), func(enc *msgpack.Encoder) error { return enc.Encode(map[uint32]interface{}{ @@ -274,7 +303,7 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. requests[pos].last = nil for fut != nil { fut.err = neterr - close(fut.ready) + fut.markReady() fut, fut.next = fut.next, nil } } @@ -284,15 +313,15 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. func (conn *Connection) lockShards() { for i := range conn.shard { - conn.shard[i].bufmut.Lock() conn.shard[i].rmut.Lock() + conn.shard[i].bufmut.Lock() } } func (conn *Connection) unlockShards() { for i := range conn.shard { - conn.shard[i].bufmut.Unlock() conn.shard[i].rmut.Unlock() + conn.shard[i].bufmut.Unlock() } } @@ -370,39 +399,29 @@ func (conn *Connection) reader() { } if fut := conn.fetchFuture(resp.RequestId); fut != nil { fut.resp = resp - close(fut.ready) + fut.markReady() } else { log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) } } } -func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error) { - shardn := fut.requestId & (shards - 1) +func (conn *Connection) newFuture(requestCode int32) (fut *Future) { + fut = &Future{} + fut.ready = make(chan struct{}) + fut.conn = conn + fut.requestId = conn.nextRequestId() + fut.requestCode = requestCode + shardn := fut.requestId & (conn.opts.Concurrency - 1) shard := &conn.shard[shardn] - shard.bufmut.Lock() - firstWritten := len(shard.buf) == 0 - if cap(shard.buf) == 0 { - shard.buf = make(smallWBuf, 0, 128) - shard.enc = msgpack.NewEncoder(&shard.buf) - } - blen := len(shard.buf) - if err := fut.pack(&shard.buf, shard.enc, body); err != nil { - shard.buf = shard.buf[:blen] - fut.err = err - shard.bufmut.Unlock() - return - } shard.rmut.Lock() if conn.closed { - shard.buf = shard.buf[:blen] - shard.bufmut.Unlock() - shard.rmut.Unlock() fut.err = ClientError{ErrConnectionClosed, "using closed connection"} + close(fut.ready) + shard.rmut.Unlock() return } - shard.bufmut.Unlock() - pos := (fut.requestId / shards) & (requestsMap - 1) + pos := (fut.requestId / conn.opts.Concurrency) & (requestsMap - 1) pair := &shard.requests[pos] if pair.last == nil { shard.requests[pos].first = fut @@ -412,32 +431,56 @@ func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error shard.requests[pos].last = fut if conn.opts.Timeout > 0 { fut.timeout = time.Now().Sub(epoch) + conn.opts.Timeout - fut.time.prev = shard.last - *shard.last = fut - shard.last = &fut.time.next } shard.rmut.Unlock() - if firstWritten { - conn.dirtyShard <- shardn + if conn.rlimit != nil { + select { + case conn.rlimit <- struct{}{}: + default: + runtime.Gosched() + select { + case conn.rlimit <- struct{}{}: + case <-fut.ready: + if fut.err == nil { + panic("fut.ready is closed, but err is nil") + } + } + } } + return } -func (conn *Connection) unlinkFutureTime(fut *Future) { - if fut.time.prev != nil { - *fut.time.prev = fut.time.next - if fut.time.next != nil { - fut.time.next.time.prev = fut.time.prev - } else { - i := fut.requestId & (shards - 1) - conn.shard[i].last = fut.time.prev - } - fut.time.next = nil - fut.time.prev = nil + +func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error) { + shardn := fut.requestId & (conn.opts.Concurrency - 1) + shard := &conn.shard[shardn] + shard.bufmut.Lock() + select { + case <-fut.ready: + shard.bufmut.Unlock() + return + default: + } + firstWritten := len(shard.buf) == 0 + if cap(shard.buf) == 0 { + shard.buf = make(smallWBuf, 0, 128) + shard.enc = msgpack.NewEncoder(&shard.buf) + } + blen := len(shard.buf) + if err := fut.pack(&shard.buf, shard.enc, body); err != nil { + shard.buf = shard.buf[:blen] + fut.err = err + shard.bufmut.Unlock() + return + } + shard.bufmut.Unlock() + if firstWritten { + conn.dirtyShard <- shardn } } func (conn *Connection) fetchFuture(reqid uint32) (fut *Future) { - shard := &conn.shard[reqid&(shards-1)] + shard := &conn.shard[reqid&(conn.opts.Concurrency-1)] shard.rmut.Lock() fut = conn.fetchFutureImp(reqid) shard.rmut.Unlock() @@ -445,8 +488,8 @@ func (conn *Connection) fetchFuture(reqid uint32) (fut *Future) { } func (conn *Connection) fetchFutureImp(reqid uint32) *Future { - shard := &conn.shard[reqid&(shards-1)] - pos := (reqid / shards) & (requestsMap - 1) + shard := &conn.shard[reqid&(conn.opts.Concurrency-1)] + pos := (reqid / conn.opts.Concurrency) & (requestsMap - 1) pair := &shard.requests[pos] fut := pair.first if fut == nil { @@ -457,7 +500,6 @@ func (conn *Connection) fetchFutureImp(reqid uint32) *Future { if pair.last == fut { pair.last = nil } - conn.unlinkFutureTime(fut) return fut } for fut.next != nil { @@ -466,10 +508,9 @@ func (conn *Connection) fetchFutureImp(reqid uint32) *Future { if pair.last == next { pair.last = fut } - fut, fut.next = next, next.next - fut.next = nil - conn.unlinkFutureTime(fut) - return fut + fut.next = next.next + next.next = nil + return next } fut = next } @@ -494,17 +535,26 @@ func (conn *Connection) timeouts() { minNext := nowepoch + timeout for i := range conn.shard { shard := &conn.shard[i] - shard.rmut.Lock() - for shard.first != nil && shard.first.timeout < nowepoch { - fut := shard.first - shard.rmut.Unlock() - fut.timeouted() + for pos := range shard.requests { shard.rmut.Lock() + pair := &shard.requests[pos] + for pair.first != nil && pair.first.timeout < nowepoch { + shard.bufmut.Lock() + fut := pair.first + pair.first = fut.next + if pair.last == fut { + pair.last = nil + } + fut.next = nil + fut.err = fmt.Errorf("client timeout for request %d", fut.requestId) + fut.markReady() + shard.bufmut.Unlock() + } + if pair.first != nil && pair.first.timeout < minNext { + minNext = pair.first.timeout + } + shard.rmut.Unlock() } - if shard.first != nil && shard.first.timeout < minNext { - minNext = shard.first.timeout - } - shard.rmut.Unlock() } t.Reset(minNext - time.Now().Sub(epoch)) } diff --git a/request.go b/request.go index c7a8d1829..9cee8b9c1 100644 --- a/request.go +++ b/request.go @@ -1,7 +1,6 @@ package tarantool import ( - "fmt" "gopkg.in/vmihailenco/msgpack.v2" "time" ) @@ -15,18 +14,6 @@ type Future struct { ready chan struct{} timeout time.Duration next *Future - time struct { - next *Future - prev **Future - } -} - -func (conn *Connection) newFuture(requestCode int32) (fut *Future) { - fut = &Future{} - fut.conn = conn - fut.requestId = conn.nextRequestId() - fut.requestCode = requestCode - return } func (conn *Connection) Ping() (resp *Response, err error) { @@ -302,7 +289,6 @@ func (fut *Future) send(body func(*msgpack.Encoder) error) *Future { return fut } - fut.ready = make(chan struct{}) fut.conn.putFuture(fut, body) if fut.err != nil { fut.conn.fetchFuture(fut.requestId) @@ -312,14 +298,10 @@ func (fut *Future) send(body func(*msgpack.Encoder) error) *Future { return fut } -func (fut *Future) timeouted() { - conn := fut.conn - if f := conn.fetchFuture(fut.requestId); f != nil { - if f != fut { - panic("future doesn't match") - } - fut.err = fmt.Errorf("client timeout for request %d", fut.requestId) - close(fut.ready) +func (fut *Future) markReady() { + close(fut.ready) + if fut.conn.rlimit != nil { + <-fut.conn.rlimit } } diff --git a/tarantool_test.go b/tarantool_test.go index 3aabbe198..793b1df0b 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -167,7 +167,13 @@ var spaceNo = uint32(512) var spaceName = "test" var indexNo = uint32(0) var indexName = "primary" -var opts = Opts{Timeout: 5000 * time.Millisecond, User: "test", Pass: "test"} +var opts = Opts{ + Timeout: 5000 * time.Millisecond, + User: "test", + Pass: "test", + //Concurrency: 32, + //RateLimit: 4*1024, +} const N = 500 @@ -175,11 +181,11 @@ func BenchmarkClientSerial(b *testing.B) { var err error conn, err := Connect(server, opts) - defer conn.Close() if err != nil { b.Errorf("No connection available") return } + defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { @@ -198,11 +204,11 @@ func BenchmarkClientFuture(b *testing.B) { var err error conn, err := Connect(server, opts) - defer conn.Close() if err != nil { b.Error(err) return } + defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { @@ -228,11 +234,11 @@ func BenchmarkClientFutureTyped(b *testing.B) { var err error conn, err := Connect(server, opts) - defer conn.Close() if err != nil { b.Errorf("No connection available") return } + defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { @@ -261,11 +267,11 @@ func BenchmarkClientFutureParallel(b *testing.B) { var err error conn, err := Connect(server, opts) - defer conn.Close() if err != nil { b.Errorf("No connection available") return } + defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { @@ -297,11 +303,11 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { var err error conn, err := Connect(server, opts) - defer conn.Close() if err != nil { b.Errorf("No connection available") return } + defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { @@ -317,8 +323,8 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, IntKey{1111}) } exit = j < N + var r []Tuple for j > 0 { - var r []Tuple j-- err := fs[j].GetTyped(&r) if err != nil { @@ -336,11 +342,11 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { func BenchmarkClientParallel(b *testing.B) { conn, err := Connect(server, opts) - defer conn.Close() if err != nil { b.Errorf("No connection available") return } + defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { @@ -360,11 +366,11 @@ func BenchmarkClientParallel(b *testing.B) { func BenchmarkClientParallelMassive(b *testing.B) { conn, err := Connect(server, opts) - defer conn.Close() if err != nil { b.Errorf("No connection available") return } + defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { @@ -404,7 +410,6 @@ func TestClient(t *testing.T) { var conn *Connection conn, err = Connect(server, opts) - defer conn.Close() if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -413,6 +418,7 @@ func TestClient(t *testing.T) { t.Errorf("conn is nil after Connect") return } + defer conn.Close() // Ping resp, err = conn.Ping() @@ -674,7 +680,6 @@ func TestSchema(t *testing.T) { var conn *Connection conn, err = Connect(server, opts) - defer conn.Close() if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -683,6 +688,7 @@ func TestSchema(t *testing.T) { t.Errorf("conn is nil after Connect") return } + defer conn.Close() // Schema schema := conn.Schema @@ -858,7 +864,6 @@ func TestClientNamed(t *testing.T) { var conn *Connection conn, err = Connect(server, opts) - defer conn.Close() if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -867,6 +872,7 @@ func TestClientNamed(t *testing.T) { t.Errorf("conn is nil after Connect") return } + defer conn.Close() // Insert resp, err = conn.Insert(spaceName, []interface{}{uint(1001), "hello2", "world2"}) @@ -950,7 +956,6 @@ func TestComplexStructs(t *testing.T) { var conn *Connection conn, err = Connect(server, opts) - defer conn.Close() if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -959,6 +964,7 @@ func TestComplexStructs(t *testing.T) { t.Errorf("conn is nil after Connect") return } + defer conn.Close() tuple := Tuple2{777, "orig", []Member{{"lol", "", 1}, {"wut", "", 3}}} _, err = conn.Replace(spaceNo, tuple) From afb7ec1a295bcdd5a73c89ce7d49a16f4b830f1a Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Fri, 11 Nov 2016 11:04:17 +0300 Subject: [PATCH 131/605] do not start timeouts goroutine if no timeout specified --- connection.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/connection.go b/connection.go index 7213d0a28..c089237ea 100644 --- a/connection.go +++ b/connection.go @@ -114,7 +114,9 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { go conn.writer() go conn.reader() - go conn.timeouts() + if conn.opts.Timeout > 0 { + go conn.timeouts() + } // TODO: reload schema after reconnect if err = conn.loadSchema(); err != nil { @@ -519,9 +521,6 @@ func (conn *Connection) fetchFutureImp(reqid uint32) *Future { func (conn *Connection) timeouts() { timeout := conn.opts.Timeout - if timeout == 0 { - timeout = time.Second - } t := time.NewTimer(timeout) for { var nowepoch time.Duration From 6af74d907b47954218277108769ec842c96242a6 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Fri, 11 Nov 2016 11:31:34 +0300 Subject: [PATCH 132/605] add RLimitAction, refactor future's errors a bit. --- connection.go | 48 +++++++++++++++++++++++++++++++++++------------- const.go | 5 +++++ request.go | 10 ---------- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/connection.go b/connection.go index c089237ea..59cfa7fcd 100644 --- a/connection.go +++ b/connection.go @@ -60,15 +60,19 @@ type Opts struct { MaxReconnects uint User string Pass string - // RateLimit number of simultaneously executed requests. - // All new requests will wait until answer for some - // previous requests arrived, or will be timeouted. - // It has its runtime cost, so it is disabled by default. - // On localhost connection and fast selects, 8192 is already - // reasonably large value for RateLimit. - // But if command lasts more time (lua, updates), or network - // is laggy, then larger value should be considered. + // RateLimit limits number of 'in-fly' request, ie aready putted into + // requests queue, but not yet answered by server or timeouted. + // It is disabled by default. + // See RLimitAction for possible actions when RateLimit.reached. RateLimit uint + // RLimitAction tells what to do when RateLimit reached: + // RLimitDrop - immediatly abort request, + // RLimitWait - wait during timeout period for some request to be answered. + // If no request answered during timeout period, this request + // is aborted. + // If no timeout period is set, it will wait forever. + // Default is RLimitWait + RLimitAction uint // Concurrency is amount of separate mutexes for request // queues and buffers inside of connection. // It is rounded upto nearest power of 2. @@ -101,6 +105,9 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { if opts.RateLimit > 0 { conn.rlimit = make(chan struct{}, opts.RateLimit); + if opts.RLimitAction != RLimitDrop && opts.RLimitAction != RLimitWait { + return nil, errors.New("RLimitAction is not equal to RLimitDone nor RLimitWait") + } } var reconnect time.Duration @@ -187,7 +194,9 @@ func (conn *Connection) dial() (err error) { } // Only if connected and authenticated + conn.lockShards() conn.c = c + conn.unlockShards() conn.r = r conn.w = w @@ -259,9 +268,6 @@ func (conn *Connection) createConnection() (r *bufio.Reader, w *bufio.Writer, er log.Printf("tarantool: last reconnect to %s failed: %s, giving it up.\n", conn.addr, err.Error()) err = ClientError{ErrConnectionClosed, "last reconnect failed"} // mark connection as closed to avoid reopening by another goroutine - conn.lockShards() - conn.closed = true - conn.unlockShards() return } log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s\n", reconnects, conn.opts.MaxReconnects, conn.addr, err.Error()) @@ -291,7 +297,9 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. return conn.r, nil, nil } err = conn.c.Close() + conn.lockShards() conn.c = nil + conn.unlockShards() conn.r = nil conn.w = nil conn.lockShards() @@ -410,6 +418,13 @@ func (conn *Connection) reader() { func (conn *Connection) newFuture(requestCode int32) (fut *Future) { fut = &Future{} + if conn.rlimit != nil && conn.opts.RLimitAction == RLimitDrop { + select { + case conn.rlimit <- struct{}{}: + default: + fut.err = ClientError{ ErrRateLimited, "Request is rate limited on client"} + } + } fut.ready = make(chan struct{}) fut.conn = conn fut.requestId = conn.nextRequestId() @@ -423,6 +438,10 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { shard.rmut.Unlock() return } + if c := conn.c; c == nil { + fut.err = ClientError{ErrConnectionNotReady, "client connection is not ready"} + return fut + } pos := (fut.requestId / conn.opts.Concurrency) & (requestsMap - 1) pair := &shard.requests[pos] if pair.last == nil { @@ -435,7 +454,7 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { fut.timeout = time.Now().Sub(epoch) + conn.opts.Timeout } shard.rmut.Unlock() - if conn.rlimit != nil { + if conn.rlimit != nil && conn.opts.RLimitAction == RLimitWait { select { case conn.rlimit <- struct{}{}: default: @@ -545,7 +564,10 @@ func (conn *Connection) timeouts() { pair.last = nil } fut.next = nil - fut.err = fmt.Errorf("client timeout for request %d", fut.requestId) + fut.err = ClientError{ + Code: ErrTimeouted, + Msg: fmt.Sprintf("client timeout for request %d", fut.requestId), + } fut.markReady() shard.bufmut.Unlock() } diff --git a/const.go b/const.go index 2088ada3c..c06d44adb 100644 --- a/const.go +++ b/const.go @@ -42,6 +42,9 @@ const ( IterBitsAnySet = uint32(8) // at least one x's bit is set IterBitsAllNotSet = uint32(9) // all bits are not set + RLimitDrop = 0 + RLimitWait = 1 + OkCode = uint32(0) ErrorCodeBit = 0x8000 PacketLengthBytes = 5 @@ -52,6 +55,8 @@ const ( ErrConnectionNotReady = 0x4000 + iota ErrConnectionClosed = 0x4000 + iota ErrProtocolError = 0x4000 + iota + ErrTimeouted = 0x4000 + iota + ErrRateLimited = 0x4000 + iota ) // Tarantool server error codes diff --git a/request.go b/request.go index 9cee8b9c1..2f85da4d3 100644 --- a/request.go +++ b/request.go @@ -279,16 +279,6 @@ func (fut *Future) send(body func(*msgpack.Encoder) error) *Future { if fut.err != nil { return fut } - // check connection ready to process packets - if closed := fut.conn.closed; closed { - fut.err = ClientError{ErrConnectionClosed, "using closed connection"} - return fut - } - if c := fut.conn.c; c == nil { - fut.err = ClientError{ErrConnectionNotReady, "client connection is not ready"} - return fut - } - fut.conn.putFuture(fut, body) if fut.err != nil { fut.conn.fetchFuture(fut.requestId) From 261bfefe944ac6d78d8d45f2205945bf61982986 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Fri, 11 Nov 2016 11:34:10 +0300 Subject: [PATCH 133/605] tweak test :-) --- tarantool_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tarantool_test.go b/tarantool_test.go index 793b1df0b..e8c07cdc3 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -250,8 +250,8 @@ func BenchmarkClientFutureTyped(b *testing.B) { for j := 0; j < N; j++ { fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, IntKey{1111}) } + var r []Tuple for j := 0; j < N; j++ { - var r []Tuple err = fs[j].GetTyped(&r) if err != nil { b.Error(err) From a139a798d691a90543a27ca921ab5a38593c4ee5 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Fri, 11 Nov 2016 19:25:52 +0300 Subject: [PATCH 134/605] use newer typed interface of msgpack --- tarantool_test.go | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/tarantool_test.go b/tarantool_test.go index e8c07cdc3..dde0cb3d7 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3,7 +3,6 @@ package tarantool import ( "fmt" "gopkg.in/vmihailenco/msgpack.v2" - "reflect" "strings" "sync" "testing" @@ -32,8 +31,7 @@ type IntKey struct { id uint } -func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { - t := v.Interface().(Tuple) +func (t *Tuple) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeSliceLen(3); err != nil { return err } @@ -49,10 +47,9 @@ func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { return nil } -func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { +func (t *Tuple) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - t := v.Addr().Interface().(*Tuple) if l, err = d.DecodeSliceLen(); err != nil { return err } @@ -71,8 +68,7 @@ func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { return nil } -func encodeMember(e *msgpack.Encoder, v reflect.Value) error { - m := v.Interface().(Member) +func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeSliceLen(2); err != nil { return err } @@ -85,10 +81,9 @@ func encodeMember(e *msgpack.Encoder, v reflect.Value) error { return nil } -func decodeMember(d *msgpack.Decoder, v reflect.Value) error { +func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - m := v.Addr().Interface().(*Member) if l, err = d.DecodeSliceLen(); err != nil { return err } @@ -104,8 +99,7 @@ func decodeMember(d *msgpack.Decoder, v reflect.Value) error { return nil } -func encodeTuple2(e *msgpack.Encoder, v reflect.Value) error { - c := v.Interface().(Tuple2) +func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeSliceLen(3); err != nil { return err } @@ -115,19 +109,15 @@ func encodeTuple2(e *msgpack.Encoder, v reflect.Value) error { if err := e.EncodeString(c.Orig); err != nil { return err } - if err := e.EncodeSliceLen(len(c.Members)); err != nil { + if err := e.Encode(c.Members); err != nil { return err } - for _, m := range c.Members { - e.Encode(m) - } return nil } -func decodeTuple2(d *msgpack.Decoder, v reflect.Value) error { +func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - c := v.Addr().Interface().(*Tuple2) if l, err = d.DecodeSliceLen(); err != nil { return err } @@ -156,12 +146,6 @@ func (k IntKey) EncodeMsgpack(enc *msgpack.Encoder) error { return nil } -func init() { - msgpack.Register(reflect.TypeOf(Tuple{}), encodeTuple, decodeTuple) - msgpack.Register(reflect.TypeOf(Tuple2{}), encodeTuple2, decodeTuple2) - msgpack.Register(reflect.TypeOf(Member{}), encodeMember, decodeMember) -} - var server = "127.0.0.1:3013" var spaceNo = uint32(512) var spaceName = "test" @@ -967,7 +951,7 @@ func TestComplexStructs(t *testing.T) { defer conn.Close() tuple := Tuple2{777, "orig", []Member{{"lol", "", 1}, {"wut", "", 3}}} - _, err = conn.Replace(spaceNo, tuple) + _, err = conn.Replace(spaceNo, &tuple) if err != nil { t.Errorf("Failed to insert: %s", err.Error()) return From eb851dc52401c031ae64422ce43fd7b83046888a Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Fri, 11 Nov 2016 19:27:14 +0300 Subject: [PATCH 135/605] require RLimitAction to be specified if RateLimit is specified --- connection.go | 4 ++-- const.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/connection.go b/connection.go index 59cfa7fcd..316b5ac88 100644 --- a/connection.go +++ b/connection.go @@ -71,7 +71,7 @@ type Opts struct { // If no request answered during timeout period, this request // is aborted. // If no timeout period is set, it will wait forever. - // Default is RLimitWait + // It is required if RateLimit is specified. RLimitAction uint // Concurrency is amount of separate mutexes for request // queues and buffers inside of connection. @@ -106,7 +106,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { if opts.RateLimit > 0 { conn.rlimit = make(chan struct{}, opts.RateLimit); if opts.RLimitAction != RLimitDrop && opts.RLimitAction != RLimitWait { - return nil, errors.New("RLimitAction is not equal to RLimitDone nor RLimitWait") + return nil, errors.New("RLimitAction should be specified to RLimitDone nor RLimitWait") } } diff --git a/const.go b/const.go index c06d44adb..ab1b79ea7 100644 --- a/const.go +++ b/const.go @@ -42,8 +42,8 @@ const ( IterBitsAnySet = uint32(8) // at least one x's bit is set IterBitsAllNotSet = uint32(9) // all bits are not set - RLimitDrop = 0 - RLimitWait = 1 + RLimitDrop = 1 + RLimitWait = 2 OkCode = uint32(0) ErrorCodeBit = 0x8000 From 10d4b1def54666309f2d175188bdfdcaf307453c Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Fri, 11 Nov 2016 21:14:06 +0300 Subject: [PATCH 136/605] improve requests queue a bit --- connection.go | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/connection.go b/connection.go index 316b5ac88..c2f9032a0 100644 --- a/connection.go +++ b/connection.go @@ -21,7 +21,10 @@ var epoch = time.Now() type connShard struct { rmut sync.Mutex - requests [requestsMap]struct{ first, last *Future } + requests [requestsMap]struct{ + first *Future + last **Future + } bufmut sync.Mutex buf smallWBuf enc *msgpack.Encoder @@ -102,6 +105,12 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { } conn.dirtyShard = make(chan uint32, conn.opts.Concurrency) conn.shard = make([]connShard, conn.opts.Concurrency) + for i := range conn.shard { + shard := &conn.shard[i] + for j := range shard.requests { + shard.requests[j].last = &shard.requests[j].first + } + } if opts.RateLimit > 0 { conn.rlimit = make(chan struct{}, opts.RateLimit); @@ -310,7 +319,7 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. for pos, pair := range requests { fut := pair.first requests[pos].first = nil - requests[pos].last = nil + requests[pos].last = &requests[pos].first for fut != nil { fut.err = neterr fut.markReady() @@ -444,12 +453,8 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { } pos := (fut.requestId / conn.opts.Concurrency) & (requestsMap - 1) pair := &shard.requests[pos] - if pair.last == nil { - shard.requests[pos].first = fut - } else { - shard.requests[pos].last.next = fut - } - shard.requests[pos].last = fut + *pair.last = fut + pair.last = &fut.next if conn.opts.Timeout > 0 { fut.timeout = time.Now().Sub(epoch) + conn.opts.Timeout } @@ -518,19 +523,22 @@ func (conn *Connection) fetchFutureImp(reqid uint32) *Future { } if fut.requestId == reqid { pair.first = fut.next - if pair.last == fut { - pair.last = nil + if fut.next == nil { + pair.last = &pair.first + } else { + fut.next = nil } return fut } for fut.next != nil { next := fut.next if next.requestId == reqid { - if pair.last == next { - pair.last = fut - } fut.next = next.next - next.next = nil + if next.next == nil { + pair.last = &fut.next + } else { + next.next = nil + } return next } fut = next @@ -560,10 +568,11 @@ func (conn *Connection) timeouts() { shard.bufmut.Lock() fut := pair.first pair.first = fut.next - if pair.last == fut { - pair.last = nil + if fut.next == nil { + pair.last = &pair.first + } else { + fut.next = nil } - fut.next = nil fut.err = ClientError{ Code: ErrTimeouted, Msg: fmt.Sprintf("client timeout for request %d", fut.requestId), From 11bc05c2bf88da6d4245a5f1c8c5e90823b9012a Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 12 Nov 2016 02:07:05 +0300 Subject: [PATCH 137/605] fix mistake --- connection.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/connection.go b/connection.go index c2f9032a0..fbb6dc6ec 100644 --- a/connection.go +++ b/connection.go @@ -449,6 +449,8 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { } if c := conn.c; c == nil { fut.err = ClientError{ErrConnectionNotReady, "client connection is not ready"} + close(fut.ready) + shard.rmut.Unlock() return fut } pos := (fut.requestId / conn.opts.Concurrency) & (requestsMap - 1) From 9b6222b19b39f937a28d893c62304a851f74956e Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 12 Nov 2016 02:22:03 +0300 Subject: [PATCH 138/605] fix timeouts --- connection.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/connection.go b/connection.go index fbb6dc6ec..08896aacb 100644 --- a/connection.go +++ b/connection.go @@ -557,11 +557,11 @@ func (conn *Connection) timeouts() { case <-conn.control: t.Stop() return - case now := <-t.C: - nowepoch = now.Sub(epoch) + case <-t.C: } - minNext := nowepoch + timeout + minNext := time.Now().Sub(epoch) + timeout for i := range conn.shard { + nowepoch = time.Now().Sub(epoch) shard := &conn.shard[i] for pos := range shard.requests { shard.rmut.Lock() @@ -588,7 +588,12 @@ func (conn *Connection) timeouts() { shard.rmut.Unlock() } } - t.Reset(minNext - time.Now().Sub(epoch)) + nowepoch = time.Now().Sub(epoch) + if nowepoch + time.Microsecond < minNext { + t.Reset(minNext - nowepoch) + } else { + t.Reset(time.Microsecond) + } } } From 919cb9901a06cdf21f376af450bebb1a8266ff09 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 12 Nov 2016 12:50:52 +0300 Subject: [PATCH 139/605] ... --- connection.go | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/connection.go b/connection.go index 08896aacb..3bf1029c3 100644 --- a/connection.go +++ b/connection.go @@ -519,33 +519,23 @@ func (conn *Connection) fetchFutureImp(reqid uint32) *Future { shard := &conn.shard[reqid&(conn.opts.Concurrency-1)] pos := (reqid / conn.opts.Concurrency) & (requestsMap - 1) pair := &shard.requests[pos] - fut := pair.first - if fut == nil { - return nil - } - if fut.requestId == reqid { - pair.first = fut.next - if fut.next == nil { - pair.last = &pair.first - } else { - fut.next = nil + root := &pair.first + for { + fut := *root + if fut == nil { + return nil } - return fut - } - for fut.next != nil { - next := fut.next - if next.requestId == reqid { - fut.next = next.next - if next.next == nil { - pair.last = &fut.next + if fut.requestId == reqid { + *root = fut.next + if fut.next == nil { + pair.last = root } else { - next.next = nil + fut.next = nil } - return next + return fut } - fut = next + root = &fut.next } - return nil } func (conn *Connection) timeouts() { From ecf2f6b180fe57074dd4e11c8662d18c0f4d2e84 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 12 Nov 2016 12:53:51 +0300 Subject: [PATCH 140/605] go fmt --- connection.go | 51 +++++++++++++++++++++++++-------------------------- const.go | 4 ++-- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/connection.go b/connection.go index 3bf1029c3..30a63f428 100644 --- a/connection.go +++ b/connection.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "errors" + "fmt" "gopkg.in/vmihailenco/msgpack.v2" "io" "log" @@ -12,7 +13,6 @@ import ( "sync" "sync/atomic" "time" - "fmt" ) const requestsMap = 128 @@ -21,14 +21,14 @@ var epoch = time.Now() type connShard struct { rmut sync.Mutex - requests [requestsMap]struct{ + requests [requestsMap]struct { first *Future - last **Future + last **Future } - bufmut sync.Mutex - buf smallWBuf - enc *msgpack.Encoder - _pad [16]uint64 + bufmut sync.Mutex + buf smallWBuf + enc *msgpack.Encoder + _pad [16]uint64 } type Connection struct { @@ -67,7 +67,7 @@ type Opts struct { // requests queue, but not yet answered by server or timeouted. // It is disabled by default. // See RLimitAction for possible actions when RateLimit.reached. - RateLimit uint + RateLimit uint // RLimitAction tells what to do when RateLimit reached: // RLimitDrop - immediatly abort request, // RLimitWait - wait during timeout period for some request to be answered. @@ -75,30 +75,30 @@ type Opts struct { // is aborted. // If no timeout period is set, it will wait forever. // It is required if RateLimit is specified. - RLimitAction uint + RLimitAction uint // Concurrency is amount of separate mutexes for request // queues and buffers inside of connection. // It is rounded upto nearest power of 2. // By default it is runtime.GOMAXPROCS(-1) * 4 - Concurrency uint32 + Concurrency uint32 } func Connect(addr string, opts Opts) (conn *Connection, err error) { conn = &Connection{ - addr: addr, - requestId: 0, - Greeting: &Greeting{}, - control: make(chan struct{}), - opts: opts, - dec: msgpack.NewDecoder(&smallBuf{}), + addr: addr, + requestId: 0, + Greeting: &Greeting{}, + control: make(chan struct{}), + opts: opts, + dec: msgpack.NewDecoder(&smallBuf{}), } maxprocs := uint32(runtime.GOMAXPROCS(-1)) - if conn.opts.Concurrency == 0 || conn.opts.Concurrency > maxprocs * 128 { + if conn.opts.Concurrency == 0 || conn.opts.Concurrency > maxprocs*128 { conn.opts.Concurrency = maxprocs * 4 } - if c := conn.opts.Concurrency; c & (c-1) != 0 { - for i := uint(1); i<32; i*=2 { + if c := conn.opts.Concurrency; c&(c-1) != 0 { + for i := uint(1); i < 32; i *= 2 { c |= c >> i } conn.opts.Concurrency = c + 1 @@ -113,7 +113,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { } if opts.RateLimit > 0 { - conn.rlimit = make(chan struct{}, opts.RateLimit); + conn.rlimit = make(chan struct{}, opts.RateLimit) if opts.RLimitAction != RLimitDrop && opts.RLimitAction != RLimitWait { return nil, errors.New("RLimitAction should be specified to RLimitDone nor RLimitWait") } @@ -214,8 +214,8 @@ func (conn *Connection) dial() (err error) { func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { request := &Future{ - conn: conn, - requestId: 0, + conn: conn, + requestId: 0, requestCode: AuthRequest, } var packet smallWBuf @@ -431,7 +431,7 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { select { case conn.rlimit <- struct{}{}: default: - fut.err = ClientError{ ErrRateLimited, "Request is rate limited on client"} + fut.err = ClientError{ErrRateLimited, "Request is rate limited on client"} } } fut.ready = make(chan struct{}) @@ -478,7 +478,6 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { return } - func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error) { shardn := fut.requestId & (conn.opts.Concurrency - 1) shard := &conn.shard[shardn] @@ -567,7 +566,7 @@ func (conn *Connection) timeouts() { } fut.err = ClientError{ Code: ErrTimeouted, - Msg: fmt.Sprintf("client timeout for request %d", fut.requestId), + Msg: fmt.Sprintf("client timeout for request %d", fut.requestId), } fut.markReady() shard.bufmut.Unlock() @@ -579,7 +578,7 @@ func (conn *Connection) timeouts() { } } nowepoch = time.Now().Sub(epoch) - if nowepoch + time.Microsecond < minNext { + if nowepoch+time.Microsecond < minNext { t.Reset(minNext - nowepoch) } else { t.Reset(time.Microsecond) diff --git a/const.go b/const.go index ab1b79ea7..ae36e55db 100644 --- a/const.go +++ b/const.go @@ -42,8 +42,8 @@ const ( IterBitsAnySet = uint32(8) // at least one x's bit is set IterBitsAllNotSet = uint32(9) // all bits are not set - RLimitDrop = 1 - RLimitWait = 2 + RLimitDrop = 1 + RLimitWait = 2 OkCode = uint32(0) ErrorCodeBit = 0x8000 From 0ede5f9625a788ee75a06b3c4e81ce58a8904913 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 12 Nov 2016 17:38:55 +0300 Subject: [PATCH 141/605] add documentation and client_tools. --- client_tools.go | 86 +++++++++++++++++++ config.lua | 2 +- connection.go | 103 +++++++++++++++++----- const.go | 125 +-------------------------- errors.go | 134 ++++++++++++++++++++++++++++- example_test.go | 214 ++++++++++++++++++++++++++++++++++++++++++++++ export_test.go | 5 ++ request.go | 95 ++++++++++++++++++-- response.go | 10 ++- schema.go | 22 +++-- tarantool_test.go | 92 ++++++++------------ 11 files changed, 669 insertions(+), 219 deletions(-) create mode 100644 client_tools.go create mode 100644 example_test.go create mode 100644 export_test.go diff --git a/client_tools.go b/client_tools.go new file mode 100644 index 000000000..0477ccff5 --- /dev/null +++ b/client_tools.go @@ -0,0 +1,86 @@ +package tarantool + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +// IntKey is utility type for passing integer key to Select*, Update* and Delete* +// It serializes to array with single integer element. +type IntKey struct { + I int +} + +func (k IntKey) EncodeMsgpack(enc *msgpack.Encoder) error { + enc.EncodeSliceLen(1) + enc.EncodeInt(k.I) + return nil +} + +// UintKey is utility type for passing unsigned integer key to Select*, Update* and Delete* +// It serializes to array with single integer element. +type UintKey struct { + I uint +} + +func (k UintKey) EncodeMsgpack(enc *msgpack.Encoder) error { + enc.EncodeSliceLen(1) + enc.EncodeUint(k.I) + return nil +} + +// UintKey is utility type for passing string key to Select*, Update* and Delete* +// It serializes to array with single string element. +type StringKey struct { + S string +} + +func (k StringKey) EncodeMsgpack(enc *msgpack.Encoder) error { + enc.EncodeSliceLen(1) + enc.EncodeString(k.S) + return nil +} + +// IntIntKey is utility type for passing two integer keys to Select*, Update* and Delete* +// It serializes to array with two integer elements +type IntIntKey struct { + I1, I2 int +} + +func (k IntIntKey) EncodeMsgpack(enc *msgpack.Encoder) error { + enc.EncodeSliceLen(2) + enc.EncodeInt(k.I1) + enc.EncodeInt(k.I2) + return nil +} + +// Op - is update operation +type Op struct { + Op string + Field uint + Arg interface{} +} + +func (o Op) EncodeMsgpack(enc *msgpack.Encoder) error { + enc.EncodeSliceLen(3) + enc.EncodeString(o.Op) + enc.EncodeUint(o.Field) + return enc.Encode(o.Arg) +} + +type OpSplice struct { + Op string + Field uint + Pos uint + Len uint + Replace string +} + +func (o OpSplice) EncodeMsgpack(enc *msgpack.Encoder) error { + enc.EncodeSliceLen(5) + enc.EncodeString(o.Op) + enc.EncodeUint(o.Field) + enc.EncodeUint(o.Pos) + enc.EncodeUint(o.Len) + enc.EncodeString(o.Replace) + return nil +} diff --git a/config.lua b/config.lua index 80589fd57..5a070642f 100644 --- a/config.lua +++ b/config.lua @@ -9,7 +9,7 @@ local s = box.schema.space.create('test', { id = 512, if_not_exists = true, }) -s:create_index('primary', {type = 'hash', parts = {1, 'NUM'}, if_not_exists = true}) +s:create_index('primary', {type = 'tree', parts = {1, 'NUM'}, if_not_exists = true}) local st = box.schema.space.create('schematest', { id = 514, diff --git a/connection.go b/connection.go index 30a63f428..f927720f9 100644 --- a/connection.go +++ b/connection.go @@ -19,27 +19,49 @@ const requestsMap = 128 var epoch = time.Now() -type connShard struct { - rmut sync.Mutex - requests [requestsMap]struct { - first *Future - last **Future - } - bufmut sync.Mutex - buf smallWBuf - enc *msgpack.Encoder - _pad [16]uint64 -} - +// Connection is a handle to Tarantool. +// +// It is created and configured with Connect function, and could not be +// reconfigured later. +// +// It is could be "Connected", "Disconnected", and "Closed". +// +// When "Connected" it sends queries to Tarantool. +// +// When "Disconnected" it rejects queries with ClientError{Code: ErrConnectionNotReady} +// +// When "Closed" it rejects queries with ClientError{Code: ErrConnectionClosed} +// +// Connection could become "Closed" when Connection.Close() method called, +// or when Tarantool disconnected and Reconnect pause is not specified or +// MaxReconnects is specified and MaxReconnect reconnect attempts already performed. +// +// You may perform data manipulation operation by calling its methods: +// Call*, Insert*, Replace*, Update*, Upsert*, Call*, Eval*. +// +// In any method that accepts `space` you my pass either space number or +// space name (in this case it will be looked up in schema). Same is true for `index`. +// +// ATTENTION: `tuple`, `key`, `ops` and `args` arguments for any method should be +// and array or should serialize to msgpack array. +// +// ATTENTION: `result` argument for *Typed methods should deserialize from +// msgpack array, cause Tarantool always returns result as an array. +// For all space related methods and Call* (but not Call17*) methods Tarantool +// always returns array of array (array of tuples for space related methods). +// For Eval* and Call17* tarantool always returns array, but does not forces +// array of arrays. type Connection struct { - addr string - c *net.TCPConn - r *bufio.Reader - w *bufio.Writer - mutex sync.Mutex + addr string + c *net.TCPConn + r *bufio.Reader + w *bufio.Writer + mutex sync.Mutex + // Schema contains schema loaded on connection. Schema *Schema requestId uint32 - Greeting *Greeting + // Greeting contains first message sent by tarantool + Greeting *Greeting shard []connShard dirtyShard chan uint32 @@ -52,17 +74,41 @@ type Connection struct { lenbuf [PacketLengthBytes]byte } +type connShard struct { + rmut sync.Mutex + requests [requestsMap]struct { + first *Future + last **Future + } + bufmut sync.Mutex + buf smallWBuf + enc *msgpack.Encoder + _pad [16]uint64 +} + +// Greeting is a message sent by tarantool on connect. type Greeting struct { Version string auth string } +// Opts is a way to configure Connection type Opts struct { - Timeout time.Duration // milliseconds - Reconnect time.Duration // milliseconds + // Timeout is requests timeout. + Timeout time.Duration + // Reconnect is a pause between reconnection attempts. + // If specified, then when tarantool is not reachable or disconnected, + // new connect attempt is performed after pause. + // By default, no reconnection attempts are performed, + // so once disconnected, connection becomes Closed. + Reconnect time.Duration + // MaxReconnects is a maximum reconnect attempts. + // After MaxReconnects attempts Connection becomes closed. MaxReconnects uint - User string - Pass string + // User name for authorization + User string + // Pass is password for authorization + Pass string // RateLimit limits number of 'in-fly' request, ie aready putted into // requests queue, but not yet answered by server or timeouted. // It is disabled by default. @@ -83,6 +129,15 @@ type Opts struct { Concurrency uint32 } +// Connect creates and configures new Connection +// +// Note: +// +// - If opts.Reconnect is zero (default), then connection either already connected +// or error is returned. +// +// - If opts.Reconnect is non-zero, then error will be returned only if authorization// fails. But if Tarantool is not reachable, then it will attempt to reconnect later +// and will not end attempts on authorization failures. func Connect(addr string, opts Opts) (conn *Connection, err error) { conn = &Connection{ @@ -143,11 +198,14 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { return conn, err } +// Close closes Connection. +// After this method called, there is no way to reopen this Connection. func (conn *Connection) Close() error { err := ClientError{ErrConnectionClosed, "connection closed by client"} return conn.closeConnectionForever(err) } +// RemoteAddr is address of Tarantool socket func (conn *Connection) RemoteAddr() string { conn.mutex.Lock() defer conn.mutex.Unlock() @@ -157,6 +215,7 @@ func (conn *Connection) RemoteAddr() string { return conn.c.RemoteAddr().String() } +// LocalAddr is address of outgoing socket func (conn *Connection) LocalAddr() string { conn.mutex.Lock() defer conn.mutex.Unlock() diff --git a/const.go b/const.go index ae36e55db..50972ed43 100644 --- a/const.go +++ b/const.go @@ -31,6 +31,7 @@ const ( KeyError = 0x31 // https://github.com/fl00r/go-tarantool-1.6/issues/2 + IterEq = uint32(0) // key == x ASC order IterReq = uint32(1) // key == x DESC order IterAll = uint32(2) // all tuples @@ -49,127 +50,3 @@ const ( ErrorCodeBit = 0x8000 PacketLengthBytes = 5 ) - -// Tarantool client error codes -const ( - ErrConnectionNotReady = 0x4000 + iota - ErrConnectionClosed = 0x4000 + iota - ErrProtocolError = 0x4000 + iota - ErrTimeouted = 0x4000 + iota - ErrRateLimited = 0x4000 + iota -) - -// Tarantool server error codes -const ( - ErrUnknown = 0 // Unknown error - ErrIllegalParams = 1 // Illegal parameters, %s - ErrMemoryIssue = 2 // Failed to allocate %u bytes in %s for %s - ErrTupleFound = 3 // Duplicate key exists in unique index '%s' in space '%s' - ErrTupleNotFound = 4 // Tuple doesn't exist in index '%s' in space '%s' - ErrUnsupported = 5 // %s does not support %s - ErrNonmaster = 6 // Can't modify data on a replication slave. My master is: %s - ErrReadonly = 7 // Can't modify data because this server is in read-only mode. - ErrInjection = 8 // Error injection '%s' - ErrCreateSpace = 9 // Failed to create space '%s': %s - ErrSpaceExists = 10 // Space '%s' already exists - ErrDropSpace = 11 // Can't drop space '%s': %s - ErrAlterSpace = 12 // Can't modify space '%s': %s - ErrIndexType = 13 // Unsupported index type supplied for index '%s' in space '%s' - ErrModifyIndex = 14 // Can't create or modify index '%s' in space '%s': %s - ErrLastDrop = 15 // Can't drop the primary key in a system space, space '%s' - ErrTupleFormatLimit = 16 // Tuple format limit reached: %u - ErrDropPrimaryKey = 17 // Can't drop primary key in space '%s' while secondary keys exist - ErrKeyPartType = 18 // Supplied key type of part %u does not match index part type: expected %s - ErrExactMatch = 19 // Invalid key part count in an exact match (expected %u, got %u) - ErrInvalidMsgpack = 20 // Invalid MsgPack - %s - ErrProcRet = 21 // msgpack.encode: can not encode Lua type '%s' - ErrTupleNotArray = 22 // Tuple/Key must be MsgPack array - ErrFieldType = 23 // Tuple field %u type does not match one required by operation: expected %s - ErrFieldTypeMismatch = 24 // Ambiguous field type in index '%s', key part %u. Requested type is %s but the field has previously been defined as %s - ErrSplice = 25 // SPLICE error on field %u: %s - ErrArgType = 26 // Argument type in operation '%c' on field %u does not match field type: expected a %s - ErrTupleIsTooLong = 27 // Tuple is too long %u - ErrUnknownUpdateOp = 28 // Unknown UPDATE operation - ErrUpdateField = 29 // Field %u UPDATE error: %s - ErrFiberStack = 30 // Can not create a new fiber: recursion limit reached - ErrKeyPartCount = 31 // Invalid key part count (expected [0..%u], got %u) - ErrProcLua = 32 // %s - ErrNoSuchProc = 33 // Procedure '%.*s' is not defined - ErrNoSuchTrigger = 34 // Trigger is not found - ErrNoSuchIndex = 35 // No index #%u is defined in space '%s' - ErrNoSuchSpace = 36 // Space '%s' does not exist - ErrNoSuchField = 37 // Field %d was not found in the tuple - ErrSpaceFieldCount = 38 // Tuple field count %u does not match space '%s' field count %u - ErrIndexFieldCount = 39 // Tuple field count %u is less than required by a defined index (expected %u) - ErrWalIo = 40 // Failed to write to disk - ErrMoreThanOneTuple = 41 // More than one tuple found by get() - ErrAccessDenied = 42 // %s access denied for user '%s' - ErrCreateUser = 43 // Failed to create user '%s': %s - ErrDropUser = 44 // Failed to drop user '%s': %s - ErrNoSuchUser = 45 // User '%s' is not found - ErrUserExists = 46 // User '%s' already exists - ErrPasswordMismatch = 47 // Incorrect password supplied for user '%s' - ErrUnknownRequestType = 48 // Unknown request type %u - ErrUnknownSchemaObject = 49 // Unknown object type '%s' - ErrCreateFunction = 50 // Failed to create function '%s': %s - ErrNoSuchFunction = 51 // Function '%s' does not exist - ErrFunctionExists = 52 // Function '%s' already exists - ErrFunctionAccessDenied = 53 // %s access denied for user '%s' to function '%s' - ErrFunctionMax = 54 // A limit on the total number of functions has been reached: %u - ErrSpaceAccessDenied = 55 // %s access denied for user '%s' to space '%s' - ErrUserMax = 56 // A limit on the total number of users has been reached: %u - ErrNoSuchEngine = 57 // Space engine '%s' does not exist - ErrReloadCfg = 58 // Can't set option '%s' dynamically - ErrCfg = 59 // Incorrect value for option '%s': %s - ErrSophia = 60 // %s - ErrLocalServerIsNotActive = 61 // Local server is not active - ErrUnknownServer = 62 // Server %s is not registered with the cluster - ErrClusterIdMismatch = 63 // Cluster id of the replica %s doesn't match cluster id of the master %s - ErrInvalidUUID = 64 // Invalid UUID: %s - ErrClusterIdIsRo = 65 // Can't reset cluster id: it is already assigned - ErrReserved66 = 66 // Reserved66 - ErrServerIdIsReserved = 67 // Can't initialize server id with a reserved value %u - ErrInvalidOrder = 68 // Invalid LSN order for server %u: previous LSN = %llu, new lsn = %llu - ErrMissingRequestField = 69 // Missing mandatory field '%s' in request - ErrIdentifier = 70 // Invalid identifier '%s' (expected letters, digits or an underscore) - ErrDropFunction = 71 // Can't drop function %u: %s - ErrIteratorType = 72 // Unknown iterator type '%s' - ErrReplicaMax = 73 // Replica count limit reached: %u - ErrInvalidXlog = 74 // Failed to read xlog: %lld - ErrInvalidXlogName = 75 // Invalid xlog name: expected %lld got %lld - ErrInvalidXlogOrder = 76 // Invalid xlog order: %lld and %lld - ErrNoConnection = 77 // Connection is not established - ErrTimeout = 78 // Timeout exceeded - ErrActiveTransaction = 79 // Operation is not permitted when there is an active transaction - ErrNoActiveTransaction = 80 // Operation is not permitted when there is no active transaction - ErrCrossEngineTransaction = 81 // A multi-statement transaction can not use multiple storage engines - ErrNoSuchRole = 82 // Role '%s' is not found - ErrRoleExists = 83 // Role '%s' already exists - ErrCreateRole = 84 // Failed to create role '%s': %s - ErrIndexExists = 85 // Index '%s' already exists - ErrTupleRefOverflow = 86 // Tuple reference counter overflow - ErrRoleLoop = 87 // Granting role '%s' to role '%s' would create a loop - ErrGrant = 88 // Incorrect grant arguments: %s - ErrPrivGranted = 89 // User '%s' already has %s access on %s '%s' - ErrRoleGranted = 90 // User '%s' already has role '%s' - ErrPrivNotGranted = 91 // User '%s' does not have %s access on %s '%s' - ErrRoleNotGranted = 92 // User '%s' does not have role '%s' - ErrMissingSnapshot = 93 // Can't find snapshot - ErrCantUpdatePrimaryKey = 94 // Attempt to modify a tuple field which is part of index '%s' in space '%s' - ErrUpdateIntegerOverflow = 95 // Integer overflow when performing '%c' operation on field %u - ErrGuestUserPassword = 96 // Setting password for guest user has no effect - ErrTransactionConflict = 97 // Transaction has been aborted by conflict - ErrUnsupportedRolePriv = 98 // Unsupported role privilege '%s' - ErrLoadFunction = 99 // Failed to dynamically load function '%s': %s - ErrFunctionLanguage = 100 // Unsupported language '%s' specified for function '%s' - ErrRtreeRect = 101 // RTree: %s must be an array with %u (point) or %u (rectangle/box) numeric coordinates - ErrProcC = 102 // ??? - ErrUnknownRtreeIndexDistanceType = 103 //Unknown RTREE index distance type %s - ErrProtocol = 104 // %s - ErrUpsertUniqueSecondaryKey = 105 // Space %s has a unique secondary index and does not support UPSERT - ErrWrongIndexRecord = 106 // Wrong record in _index space: got {%s}, expected {%s} - ErrWrongIndexParts = 107 // Wrong index parts (field %u): %s; expected field1 id (number), field1 type (string), ... - ErrWrongIndexOptions = 108 // Wrong index options (field %u): %s - ErrWrongSchemaVaersion = 109 // Wrong schema version, current: %d, in request: %u - ErrSlabAllocMax = 110 // Failed to allocate %u bytes for tuple in the slab allocator: tuple is too large. Check 'slab_alloc_maximal' configuration option. -) diff --git a/errors.go b/errors.go index 6a978cd9e..8eef0bfb4 100644 --- a/errors.go +++ b/errors.go @@ -4,6 +4,7 @@ import ( "fmt" ) +// Error is wrapper around error returned by Tarantool type Error struct { Code uint32 Msg string @@ -13,6 +14,8 @@ func (tnterr Error) Error() string { return fmt.Sprintf("%s (0x%x)", tnterr.Msg, tnterr.Code) } +// ClientError is connection produced by this client, +// ie connection failures or timeouts. type ClientError struct { Code uint32 Msg string @@ -22,11 +25,140 @@ func (clierr ClientError) Error() string { return fmt.Sprintf("%s (0x%x)", clierr.Msg, clierr.Code) } +// Temporary returns true if next attempt to perform request may succeeed. +// Currently it returns true when: +// - Connection is not connected at the moment, +// - or request is timeouted, +// - or request is aborted due to rate limit. func (clierr ClientError) Temporary() bool { switch clierr.Code { - case ErrConnectionNotReady: + case ErrConnectionNotReady, ErrTimeouted, ErrRateLimited: return true default: return false } } + +// Tarantool client error codes +const ( + ErrConnectionNotReady = 0x4000 + iota + ErrConnectionClosed = 0x4000 + iota + ErrProtocolError = 0x4000 + iota + ErrTimeouted = 0x4000 + iota + ErrRateLimited = 0x4000 + iota +) + +// Tarantool server error codes +const ( + ErrUnknown = 0 // Unknown error + ErrIllegalParams = 1 // Illegal parameters, %s + ErrMemoryIssue = 2 // Failed to allocate %u bytes in %s for %s + ErrTupleFound = 3 // Duplicate key exists in unique index '%s' in space '%s' + ErrTupleNotFound = 4 // Tuple doesn't exist in index '%s' in space '%s' + ErrUnsupported = 5 // %s does not support %s + ErrNonmaster = 6 // Can't modify data on a replication slave. My master is: %s + ErrReadonly = 7 // Can't modify data because this server is in read-only mode. + ErrInjection = 8 // Error injection '%s' + ErrCreateSpace = 9 // Failed to create space '%s': %s + ErrSpaceExists = 10 // Space '%s' already exists + ErrDropSpace = 11 // Can't drop space '%s': %s + ErrAlterSpace = 12 // Can't modify space '%s': %s + ErrIndexType = 13 // Unsupported index type supplied for index '%s' in space '%s' + ErrModifyIndex = 14 // Can't create or modify index '%s' in space '%s': %s + ErrLastDrop = 15 // Can't drop the primary key in a system space, space '%s' + ErrTupleFormatLimit = 16 // Tuple format limit reached: %u + ErrDropPrimaryKey = 17 // Can't drop primary key in space '%s' while secondary keys exist + ErrKeyPartType = 18 // Supplied key type of part %u does not match index part type: expected %s + ErrExactMatch = 19 // Invalid key part count in an exact match (expected %u, got %u) + ErrInvalidMsgpack = 20 // Invalid MsgPack - %s + ErrProcRet = 21 // msgpack.encode: can not encode Lua type '%s' + ErrTupleNotArray = 22 // Tuple/Key must be MsgPack array + ErrFieldType = 23 // Tuple field %u type does not match one required by operation: expected %s + ErrFieldTypeMismatch = 24 // Ambiguous field type in index '%s', key part %u. Requested type is %s but the field has previously been defined as %s + ErrSplice = 25 // SPLICE error on field %u: %s + ErrArgType = 26 // Argument type in operation '%c' on field %u does not match field type: expected a %s + ErrTupleIsTooLong = 27 // Tuple is too long %u + ErrUnknownUpdateOp = 28 // Unknown UPDATE operation + ErrUpdateField = 29 // Field %u UPDATE error: %s + ErrFiberStack = 30 // Can not create a new fiber: recursion limit reached + ErrKeyPartCount = 31 // Invalid key part count (expected [0..%u], got %u) + ErrProcLua = 32 // %s + ErrNoSuchProc = 33 // Procedure '%.*s' is not defined + ErrNoSuchTrigger = 34 // Trigger is not found + ErrNoSuchIndex = 35 // No index #%u is defined in space '%s' + ErrNoSuchSpace = 36 // Space '%s' does not exist + ErrNoSuchField = 37 // Field %d was not found in the tuple + ErrSpaceFieldCount = 38 // Tuple field count %u does not match space '%s' field count %u + ErrIndexFieldCount = 39 // Tuple field count %u is less than required by a defined index (expected %u) + ErrWalIo = 40 // Failed to write to disk + ErrMoreThanOneTuple = 41 // More than one tuple found by get() + ErrAccessDenied = 42 // %s access denied for user '%s' + ErrCreateUser = 43 // Failed to create user '%s': %s + ErrDropUser = 44 // Failed to drop user '%s': %s + ErrNoSuchUser = 45 // User '%s' is not found + ErrUserExists = 46 // User '%s' already exists + ErrPasswordMismatch = 47 // Incorrect password supplied for user '%s' + ErrUnknownRequestType = 48 // Unknown request type %u + ErrUnknownSchemaObject = 49 // Unknown object type '%s' + ErrCreateFunction = 50 // Failed to create function '%s': %s + ErrNoSuchFunction = 51 // Function '%s' does not exist + ErrFunctionExists = 52 // Function '%s' already exists + ErrFunctionAccessDenied = 53 // %s access denied for user '%s' to function '%s' + ErrFunctionMax = 54 // A limit on the total number of functions has been reached: %u + ErrSpaceAccessDenied = 55 // %s access denied for user '%s' to space '%s' + ErrUserMax = 56 // A limit on the total number of users has been reached: %u + ErrNoSuchEngine = 57 // Space engine '%s' does not exist + ErrReloadCfg = 58 // Can't set option '%s' dynamically + ErrCfg = 59 // Incorrect value for option '%s': %s + ErrSophia = 60 // %s + ErrLocalServerIsNotActive = 61 // Local server is not active + ErrUnknownServer = 62 // Server %s is not registered with the cluster + ErrClusterIdMismatch = 63 // Cluster id of the replica %s doesn't match cluster id of the master %s + ErrInvalidUUID = 64 // Invalid UUID: %s + ErrClusterIdIsRo = 65 // Can't reset cluster id: it is already assigned + ErrReserved66 = 66 // Reserved66 + ErrServerIdIsReserved = 67 // Can't initialize server id with a reserved value %u + ErrInvalidOrder = 68 // Invalid LSN order for server %u: previous LSN = %llu, new lsn = %llu + ErrMissingRequestField = 69 // Missing mandatory field '%s' in request + ErrIdentifier = 70 // Invalid identifier '%s' (expected letters, digits or an underscore) + ErrDropFunction = 71 // Can't drop function %u: %s + ErrIteratorType = 72 // Unknown iterator type '%s' + ErrReplicaMax = 73 // Replica count limit reached: %u + ErrInvalidXlog = 74 // Failed to read xlog: %lld + ErrInvalidXlogName = 75 // Invalid xlog name: expected %lld got %lld + ErrInvalidXlogOrder = 76 // Invalid xlog order: %lld and %lld + ErrNoConnection = 77 // Connection is not established + ErrTimeout = 78 // Timeout exceeded + ErrActiveTransaction = 79 // Operation is not permitted when there is an active transaction + ErrNoActiveTransaction = 80 // Operation is not permitted when there is no active transaction + ErrCrossEngineTransaction = 81 // A multi-statement transaction can not use multiple storage engines + ErrNoSuchRole = 82 // Role '%s' is not found + ErrRoleExists = 83 // Role '%s' already exists + ErrCreateRole = 84 // Failed to create role '%s': %s + ErrIndexExists = 85 // Index '%s' already exists + ErrTupleRefOverflow = 86 // Tuple reference counter overflow + ErrRoleLoop = 87 // Granting role '%s' to role '%s' would create a loop + ErrGrant = 88 // Incorrect grant arguments: %s + ErrPrivGranted = 89 // User '%s' already has %s access on %s '%s' + ErrRoleGranted = 90 // User '%s' already has role '%s' + ErrPrivNotGranted = 91 // User '%s' does not have %s access on %s '%s' + ErrRoleNotGranted = 92 // User '%s' does not have role '%s' + ErrMissingSnapshot = 93 // Can't find snapshot + ErrCantUpdatePrimaryKey = 94 // Attempt to modify a tuple field which is part of index '%s' in space '%s' + ErrUpdateIntegerOverflow = 95 // Integer overflow when performing '%c' operation on field %u + ErrGuestUserPassword = 96 // Setting password for guest user has no effect + ErrTransactionConflict = 97 // Transaction has been aborted by conflict + ErrUnsupportedRolePriv = 98 // Unsupported role privilege '%s' + ErrLoadFunction = 99 // Failed to dynamically load function '%s': %s + ErrFunctionLanguage = 100 // Unsupported language '%s' specified for function '%s' + ErrRtreeRect = 101 // RTree: %s must be an array with %u (point) or %u (rectangle/box) numeric coordinates + ErrProcC = 102 // ??? + ErrUnknownRtreeIndexDistanceType = 103 //Unknown RTREE index distance type %s + ErrProtocol = 104 // %s + ErrUpsertUniqueSecondaryKey = 105 // Space %s has a unique secondary index and does not support UPSERT + ErrWrongIndexRecord = 106 // Wrong record in _index space: got {%s}, expected {%s} + ErrWrongIndexParts = 107 // Wrong index parts (field %u): %s; expected field1 id (number), field1 type (string), ... + ErrWrongIndexOptions = 108 // Wrong index options (field %u): %s + ErrWrongSchemaVaersion = 109 // Wrong schema version, current: %d, in request: %u + ErrSlabAllocMax = 110 // Failed to allocate %u bytes for tuple in the slab allocator: tuple is too large. Check 'slab_alloc_maximal' configuration option. +) diff --git a/example_test.go b/example_test.go new file mode 100644 index 000000000..3379fec88 --- /dev/null +++ b/example_test.go @@ -0,0 +1,214 @@ +package tarantool_test + +import ( + "fmt" + "github.com/tarantool/go-tarantool" + "time" +) + +func example_connect() (*tarantool.Connection, error) { + conn, err := tarantool.Connect(server, opts) + if err != nil { + return nil, err + } + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + conn.Close() + return nil, err + } + _, err = conn.Replace(spaceNo, []interface{}{uint(1112), "hallo", "werld"}) + if err != nil { + conn.Close() + return nil, err + } + return conn, nil +} + +func ExampleConnection_Select() { + var conn *tarantool.Connection + conn, err := example_connect() + if err != nil { + fmt.Printf("error in prepare is %v", err) + return + } + defer conn.Close() + resp, err := conn.Select(512, 0, 0, 100, tarantool.IterEq, []interface{}{uint(1111)}) + if err != nil { + fmt.Printf("error in select is %v", err) + return + } + fmt.Printf("response is %#v\n", resp.Data) + resp, err = conn.Select("test", "primary", 0, 100, tarantool.IterEq, tarantool.IntKey{1111}) + if err != nil { + fmt.Printf("error in select is %v", err) + return + } + fmt.Printf("response is %#v\n", resp.Data) + // Output: + // response is []interface {}{[]interface {}{0x457, "hello", "world"}} + // response is []interface {}{[]interface {}{0x457, "hello", "world"}} +} + +func ExampleConnection_SelectTyped() { + var conn *tarantool.Connection + conn, err := example_connect() + if err != nil { + fmt.Printf("error in prepare is %v", err) + return + } + defer conn.Close() + var res []Tuple + err = conn.SelectTyped(512, 0, 0, 100, tarantool.IterEq, tarantool.IntKey{1111}, &res) + if err != nil { + fmt.Printf("error in select is %v", err) + return + } + fmt.Printf("response is %#v\n", res) + err = conn.SelectTyped("test", "primary", 0, 100, tarantool.IterEq, tarantool.IntKey{1111}, &res) + if err != nil { + fmt.Printf("error in select is %v", err) + return + } + fmt.Printf("response is %#v\n", res) + // Output: + // response is []tarantool_test.Tuple{tarantool_test.Tuple{Id:0x457, Msg:"hello", Name:"world"}} + // response is []tarantool_test.Tuple{tarantool_test.Tuple{Id:0x457, Msg:"hello", Name:"world"}} +} + +func Example_main() { + spaceNo := uint32(512) + indexNo := uint32(0) + + server := "127.0.0.1:3013" + opts := tarantool.Opts{ + Timeout: 50 * time.Millisecond, + Reconnect: 100 * time.Millisecond, + MaxReconnects: 3, + User: "test", + Pass: "test", + } + client, err := tarantool.Connect(server, opts) + if err != nil { + fmt.Errorf("Failed to connect: %s", err.Error()) + return + } + + resp, err := client.Ping() + fmt.Println("Ping Code", resp.Code) + fmt.Println("Ping Data", resp.Data) + fmt.Println("Ping Error", err) + + // delete tuple for cleaning + client.Delete(spaceNo, indexNo, []interface{}{uint(10)}) + client.Delete(spaceNo, indexNo, []interface{}{uint(11)}) + + // insert new tuple { 10, 1 } + resp, err = client.Insert(spaceNo, []interface{}{uint(10), "test", "one"}) + fmt.Println("Insert Error", err) + fmt.Println("Insert Code", resp.Code) + fmt.Println("Insert Data", resp.Data) + + // insert new tuple { 11, 1 } + resp, err = client.Insert("test", &Tuple{10, "test", "one"}) + fmt.Println("Insert Error", err) + fmt.Println("Insert Code", resp.Code) + fmt.Println("Insert Data", resp.Data) + + // delete tuple with primary key { 10 } + resp, err = client.Delete(spaceNo, indexNo, []interface{}{uint(10)}) + // or + // resp, err = client.Delete("test", "primary", UintKey{10}}) + fmt.Println("Delete Error", err) + fmt.Println("Delete Code", resp.Code) + fmt.Println("Delete Data", resp.Data) + + // replace tuple with primary key 13 + // note, Tuple is defined within tests, and has EncdodeMsgpack and DecodeMsgpack + // methods + resp, err = client.Replace(spaceNo, []interface{}{uint(13), 1}) + fmt.Println("Replace Error", err) + fmt.Println("Replace Code", resp.Code) + fmt.Println("Replace Data", resp.Data) + + // update tuple with primary key { 13 }, incrementing second field by 3 + resp, err = client.Update("test", "primary", tarantool.UintKey{13}, []tarantool.Op{{"+", 1, 3}}) + // or + // resp, err = client.Update(spaceNo, indexNo, []interface{}{uint(13)}, []interface{}{[]interface{}{"+", 1, 3}}) + fmt.Println("Update Error", err) + fmt.Println("Update Code", resp.Code) + fmt.Println("Update Data", resp.Data) + + // select just one tuple with primay key { 15 } + resp, err = client.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{uint(15)}) + // or + // resp, err = client.Select("test", "primary", 0, 1, tarantool.IterEq, tarantool.UintKey{15}) + fmt.Println("Select Error", err) + fmt.Println("Select Code", resp.Code) + fmt.Println("Select Data", resp.Data) + + // call function 'func_name' with arguments + resp, err = client.Call17("simple_incr", []interface{}{1}) + fmt.Println("Call17 Error", err) + fmt.Println("Call17 Code", resp.Code) + fmt.Println("Call17 Data", resp.Data) + + // run raw lua code + resp, err = client.Eval("return 1 + 2", []interface{}{}) + fmt.Println("Eval Error", err) + fmt.Println("Eval Code", resp.Code) + fmt.Println("Eval Data", resp.Data) + + resp, err = client.Insert("test", &Tuple{11, "test", "one"}) + resp, err = client.Insert("test", &Tuple{12, "test", "one"}) + + var futs [3]*tarantool.Future + futs[0] = client.SelectAsync("test", "primary", 0, 2, tarantool.IterLe, tarantool.UintKey{12}) + futs[1] = client.SelectAsync("test", "primary", 0, 1, tarantool.IterEq, tarantool.UintKey{13}) + futs[2] = client.SelectAsync("test", "primary", 0, 1, tarantool.IterEq, tarantool.UintKey{15}) + var t []Tuple + err = futs[0].GetTyped(&t) + fmt.Println("Fut", 0, "Error", err) + fmt.Println("Fut", 0, "Data", t) + + resp, err = futs[1].Get() + fmt.Println("Fut", 1, "Error", err) + fmt.Println("Fut", 1, "Data", resp.Data) + + resp, err = futs[2].Get() + fmt.Println("Fut", 2, "Error", err) + fmt.Println("Fut", 2, "Data", resp.Data) + // Output: + // Ping Code 0 + // Ping Data [] + // Ping Error + // Insert Error + // Insert Code 0 + // Insert Data [[10 test one]] + // Insert Error Duplicate key exists in unique index 'primary' in space 'test' (0x3) + // Insert Code 3 + // Insert Data [] + // Delete Error + // Delete Code 0 + // Delete Data [[10 test one]] + // Replace Error + // Replace Code 0 + // Replace Data [[13 1]] + // Update Error + // Update Code 0 + // Update Data [[13 4]] + // Select Error + // Select Code 0 + // Select Data [[15 val 15 bla]] + // Call17 Error + // Call17 Code 0 + // Call17 Data [2] + // Eval Error + // Eval Code 0 + // Eval Data [3] + // Fut 0 Error + // Fut 0 Data [{12 val 12 bla} {11 test one}] + // Fut 1 Error + // Fut 1 Data [[13 4]] + // Fut 2 Error + // Fut 2 Data [[15 val 15 bla]] +} diff --git a/export_test.go b/export_test.go new file mode 100644 index 000000000..931e78c9b --- /dev/null +++ b/export_test.go @@ -0,0 +1,5 @@ +package tarantool + +func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (spaceNo, indexNo uint32, err error) { + return schema.resolveSpaceIndex(s, i) +} diff --git a/request.go b/request.go index 2f85da4d3..3a8e57883 100644 --- a/request.go +++ b/request.go @@ -5,6 +5,7 @@ import ( "time" ) +// Future is a handle for asynchronous request type Future struct { conn *Connection requestId uint32 @@ -16,6 +17,7 @@ type Future struct { next *Future } +// Ping sends empty request to Tarantool to check connection. func (conn *Connection) Ping() (resp *Response, err error) { future := conn.newFuture(PingRequest) return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(0); return nil }).Get() @@ -46,70 +48,118 @@ func (req *Future) fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interf return enc.Encode(tuple) } +// Select performs select to box space. +// +// It is equal to conn.SelectAsync(...).Get() func (conn *Connection) Select(space, index interface{}, offset, limit, iterator uint32, key interface{}) (resp *Response, err error) { return conn.SelectAsync(space, index, offset, limit, iterator, key).Get() } +// Insert performs insertion to box space. +// Tarantool will reject Insert when tuple with same primary key exists. +// +// It is equal to conn.InsertAsync(space, tuple).Get(). func (conn *Connection) Insert(space interface{}, tuple interface{}) (resp *Response, err error) { return conn.InsertAsync(space, tuple).Get() } +// Replace performs "insert or replace" action to box space. +// If tuple with same primary key exists, it will be replaced. +// +// It is equal to conn.ReplaceAsync(space, tuple).Get(). func (conn *Connection) Replace(space interface{}, tuple interface{}) (resp *Response, err error) { return conn.ReplaceAsync(space, tuple).Get() } +// Delete performs deletion of a tuple by key. +// Result will contain array with deleted tuple. +// +// It is equal to conn.DeleteAsync(space, tuple).Get(). func (conn *Connection) Delete(space, index interface{}, key interface{}) (resp *Response, err error) { return conn.DeleteAsync(space, index, key).Get() } +// Update performs update of a tuple by key. +// Result will contain array with updated tuple. +// +// It is equal to conn.UpdateAsync(space, tuple).Get(). func (conn *Connection) Update(space, index interface{}, key, ops interface{}) (resp *Response, err error) { return conn.UpdateAsync(space, index, key, ops).Get() } +// Upsert performs "update or insert" action of a tuple by key. +// Result will not contain any tuple. +// +// It is equal to conn.UpsertAsync(space, tuple, ops).Get(). func (conn *Connection) Upsert(space interface{}, tuple, ops interface{}) (resp *Response, err error) { return conn.UpsertAsync(space, tuple, ops).Get() } -// Call calls registered function. +// Call calls registered tarantool function. // It uses request code for tarantool 1.6, so result is converted to array of arrays +// +// It is equal to conn.CallAsync(functionName, args).Get(). func (conn *Connection) Call(functionName string, args interface{}) (resp *Response, err error) { return conn.CallAsync(functionName, args).Get() } -// Call17 calls registered function. +// Call17 calls registered tarantool function. // It uses request code for tarantool 1.7, so result is not converted // (though, keep in mind, result is always array) +// +// It is equal to conn.Call17Async(functionName, args).Get(). func (conn *Connection) Call17(functionName string, args interface{}) (resp *Response, err error) { return conn.Call17Async(functionName, args).Get() } +// Eval passes lua expression for evaluation. +// +// It is equal to conn.EvalAsync(space, tuple).Get(). func (conn *Connection) Eval(expr string, args interface{}) (resp *Response, err error) { return conn.EvalAsync(expr, args).Get() } +// SelectTyped performs select to box space and fills typed result. +// +// It is equal to conn.SelectAsync(space, index, offset, limit, iterator, key).GetTyped(&result) func (conn *Connection) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key interface{}, result interface{}) (err error) { return conn.SelectAsync(space, index, offset, limit, iterator, key).GetTyped(result) } +// InsertTyped performs insertion to box space. +// Tarantool will reject Insert when tuple with same primary key exists. +// +// It is equal to conn.InsertAsync(space, tuple).GetTyped(&result). func (conn *Connection) InsertTyped(space interface{}, tuple interface{}, result interface{}) (err error) { return conn.InsertAsync(space, tuple).GetTyped(result) } +// ReplaceTyped performs "insert or replace" action to box space. +// If tuple with same primary key exists, it will be replaced. +// +// It is equal to conn.ReplaceAsync(space, tuple).GetTyped(&result). func (conn *Connection) ReplaceTyped(space interface{}, tuple interface{}, result interface{}) (err error) { return conn.ReplaceAsync(space, tuple).GetTyped(result) } +// DeleteTyped performs deletion of a tuple by key and fills result with deleted tuple. +// +// It is equal to conn.DeleteAsync(space, tuple).GetTyped(&result). func (conn *Connection) DeleteTyped(space, index interface{}, key interface{}, result interface{}) (err error) { return conn.DeleteAsync(space, index, key).GetTyped(result) } +// UpdateTyped performs update of a tuple by key and fills result with updated tuple. +// +// It is equal to conn.UpdateAsync(space, tuple, ops).GetTyped(&result). func (conn *Connection) UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) (err error) { return conn.UpdateAsync(space, index, key, ops).GetTyped(result) } // CallTyped calls registered function. // It uses request code for tarantool 1.6, so result is converted to array of arrays -// Attention: args should serialize into array of arguments +// +// It is equal to conn.CallAsync(functionName, args).GetTyped(&result). func (conn *Connection) CallTyped(functionName string, args interface{}, result interface{}) (err error) { return conn.CallAsync(functionName, args).GetTyped(result) } @@ -117,18 +167,20 @@ func (conn *Connection) CallTyped(functionName string, args interface{}, result // Call17Typed calls registered function. // It uses request code for tarantool 1.7, so result is not converted // (though, keep in mind, result is always array) -// Attention: args should serialize into array of arguments +// +// It is equal to conn.Call17Async(functionName, args).GetTyped(&result). func (conn *Connection) Call17Typed(functionName string, args interface{}, result interface{}) (err error) { return conn.Call17Async(functionName, args).GetTyped(result) } -// EvalTyped evals arbitrary lua expression -// Attention: args should serialize into array of arguments +// EvalTyped passes lua expression for evaluation. +// +// It is equal to conn.EvalAsync(space, tuple).GetTyped(&result). func (conn *Connection) EvalTyped(expr string, args interface{}, result interface{}) (err error) { return conn.EvalAsync(expr, args).GetTyped(result) } -// Async methods +// SelectAsync sends select request to tarantool and returns Future. func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Future { future := conn.newFuture(SelectRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) @@ -142,6 +194,8 @@ func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, ite }) } +// InsertAsync sends insert action to tarantool and returns Future. +// Tarantool will reject Insert when tuple with same primary key exists. func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Future { future := conn.newFuture(InsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) @@ -154,6 +208,8 @@ func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Futur }) } +// ReplaceAsync sends "insert or replace" action to tarantool and returns Future. +// If tuple with same primary key exists, it will be replaced. func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Future { future := conn.newFuture(ReplaceRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) @@ -166,6 +222,8 @@ func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Futu }) } +// DeleteAsync sends deletion action to tarantool and returns Future. +// Future's result will contain array with deleted tuple. func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) *Future { future := conn.newFuture(DeleteRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) @@ -178,6 +236,8 @@ func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) * }) } +// Update sends deletion of a tuple by key and returns Future. +// Future's result will contain array with updated tuple. func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface{}) *Future { future := conn.newFuture(UpdateRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) @@ -194,6 +254,8 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface }) } +// UpsertAsync sends "update or insert" action to tarantool and returns Future. +// Future's sesult will not contain any tuple. func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *Future { future := conn.newFuture(UpsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) @@ -213,6 +275,8 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops in }) } +// CallAsync sends a call to registered tarantool function and returns Future. +// It uses request code for tarantool 1.6, so future's result is always array of arrays func (conn *Connection) CallAsync(functionName string, args interface{}) *Future { future := conn.newFuture(CallRequest) return future.send(func(enc *msgpack.Encoder) error { @@ -224,6 +288,9 @@ func (conn *Connection) CallAsync(functionName string, args interface{}) *Future }) } +// Call17Async sends a call to registered tarantool function and returns Future. +// It uses request code for tarantool 1.7, so future's result will not be converted +// (though, keep in mind, result is always array) func (conn *Connection) Call17Async(functionName string, args interface{}) *Future { future := conn.newFuture(Call17Request) return future.send(func(enc *msgpack.Encoder) error { @@ -235,6 +302,7 @@ func (conn *Connection) Call17Async(functionName string, args interface{}) *Futu }) } +// EvalAsync sends a lua expression for evaluation and returns Future. func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { future := conn.newFuture(EvalRequest) return future.send(func(enc *msgpack.Encoder) error { @@ -306,6 +374,15 @@ func (fut *Future) wait() { <-fut.ready } +// Get waits for Future to be filled and returns Response and error +// +// Response will contain deserialized result in Data field. +// It will be []interface{}, so if you want more performace, use GetTyped method. +// +// Note: Response could be equal to nil if ClientError is returned in error. +// +// "error" could be Error, if it is error returned by Tarantool, +// or ClientError, if something bad happens in a client process. func (fut *Future) Get() (*Response, error) { fut.wait() if fut.err != nil { @@ -315,6 +392,10 @@ func (fut *Future) Get() (*Response, error) { return fut.resp, fut.err } +// GetTyped waits for Future and calls msgpack.Decoder.Decode(result) if no error happens. +// It is could be much faster than Get() function. +// +// Note: Tarantool usually returns array of tuples (except for Eval and Call17 actions) func (fut *Future) GetTyped(result interface{}) error { fut.wait() if fut.err != nil { diff --git a/response.go b/response.go index dbb622f7f..2eb51591a 100644 --- a/response.go +++ b/response.go @@ -8,9 +8,10 @@ import ( type Response struct { RequestId uint32 Code uint32 - Error string - Data []interface{} - buf smallBuf + Error string // error message + // Data contains deserialized data for untyped requests + Data []interface{} + buf smallBuf } func (resp *Response) fill(b []byte) { @@ -137,6 +138,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { return } +// String implements Stringer interface func (resp *Response) String() (str string) { if resp.Code == OkCode { return fmt.Sprintf("<%d OK %v>", resp.RequestId, resp.Data) @@ -144,6 +146,8 @@ func (resp *Response) String() (str string) { return fmt.Sprintf("<%d ERR 0x%x %s>", resp.RequestId, resp.Code, resp.Error) } +// Tuples converts result of Eval and Call17 to same format +// as other actions returns (ie array of arrays). func (resp *Response) Tuples() (res [][]interface{}) { res = make([][]interface{}, len(resp.Data)) for i, t := range resp.Data { diff --git a/schema.go b/schema.go index 46502affc..339974c50 100644 --- a/schema.go +++ b/schema.go @@ -4,21 +4,28 @@ import ( "fmt" ) +// Schema contains information about spaces and indexes. type Schema struct { - Version uint - Spaces map[string]*Space + Version uint + // Spaces is map from space names to spaces + Spaces map[string]*Space + // SpacesById is map from space numbers to spaces SpacesById map[uint32]*Space } +// Space contains information about tarantool space type Space struct { - Id uint32 - Name string - Engine string - Temporary bool + Id uint32 + Name string + Engine string + Temporary bool // Is this space temporaray? + // Field configuration is not mandatory and not checked by tarantool. FieldsCount uint32 Fields map[string]*Field FieldsById map[uint32]*Field - Indexes map[string]*Index + // Indexes is map from index names to indexes + Indexes map[string]*Index + // IndexesById is map from index numbers to indexes IndexesById map[uint32]*Index } @@ -28,6 +35,7 @@ type Field struct { Type string } +// Index contains information about index type Index struct { Id uint32 Name string diff --git a/tarantool_test.go b/tarantool_test.go index dde0cb3d7..54bc6b6a9 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -1,7 +1,8 @@ -package tarantool +package tarantool_test import ( "fmt" + . "github.com/tarantool/go-tarantool" "gopkg.in/vmihailenco/msgpack.v2" "strings" "sync" @@ -27,23 +28,11 @@ type Tuple2 struct { Members []Member } -type IntKey struct { - id uint -} - func (t *Tuple) EncodeMsgpack(e *msgpack.Encoder) error { - if err := e.EncodeSliceLen(3); err != nil { - return err - } - if err := e.EncodeUint(t.Id); err != nil { - return err - } - if err := e.EncodeString(t.Msg); err != nil { - return err - } - if err := e.EncodeString(t.Name); err != nil { - return err - } + e.EncodeSliceLen(3) + e.EncodeUint(t.Id) + e.EncodeString(t.Msg) + e.EncodeString(t.Name) return nil } @@ -69,15 +58,9 @@ func (t *Tuple) DecodeMsgpack(d *msgpack.Decoder) error { } func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { - if err := e.EncodeSliceLen(2); err != nil { - return err - } - if err := e.EncodeString(m.Name); err != nil { - return err - } - if err := e.EncodeUint(m.Val); err != nil { - return err - } + e.EncodeSliceLen(2) + e.EncodeString(m.Name) + e.EncodeUint(m.Val) return nil } @@ -100,18 +83,10 @@ func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { } func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { - if err := e.EncodeSliceLen(3); err != nil { - return err - } - if err := e.EncodeUint(c.Cid); err != nil { - return err - } - if err := e.EncodeString(c.Orig); err != nil { - return err - } - if err := e.Encode(c.Members); err != nil { - return err - } + e.EncodeSliceLen(3) + e.EncodeUint(c.Cid) + e.EncodeString(c.Orig) + e.Encode(c.Members) return nil } @@ -140,21 +115,15 @@ func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { return nil } -func (k IntKey) EncodeMsgpack(enc *msgpack.Encoder) error { - enc.EncodeSliceLen(1) - enc.EncodeUint(k.id) - return nil -} - var server = "127.0.0.1:3013" var spaceNo = uint32(512) var spaceName = "test" var indexNo = uint32(0) var indexName = "primary" var opts = Opts{ - Timeout: 5000 * time.Millisecond, - User: "test", - Pass: "test", + Timeout: 0 * time.Millisecond, + User: "test", + Pass: "test", //Concurrency: 32, //RateLimit: 4*1024, } @@ -437,7 +406,8 @@ func TestClient(t *testing.T) { t.Errorf("Unexpected body of Insert (1)") } } - resp, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) + //resp, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) + resp, err = conn.Insert(spaceNo, &Tuple{1, "hello", "world"}) if tntErr, ok := err.(Error); !ok || tntErr.Code != ErrTupleFound { t.Errorf("Expected ErrTupleFound but got: %v", err) } @@ -609,6 +579,20 @@ func TestClient(t *testing.T) { } } + // Select Typed for one tuple + var tpl1 [1]Tuple + err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}, &tpl1) + if err != nil { + t.Errorf("Failed to SelectTyped: %s", err.Error()) + } + if len(tpl) != 1 { + t.Errorf("Result len of SelectTyped != 1") + } else { + if tpl[0].Id != 10 { + t.Errorf("Bad value loaded from SelectTyped") + } + } + // Select Typed Empty var tpl2 []Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}, &tpl2) @@ -816,27 +800,27 @@ func TestSchema(t *testing.T) { } var rSpaceNo, rIndexNo uint32 - rSpaceNo, rIndexNo, err = schema.resolveSpaceIndex(514, 3) + rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex(514, 3) if err != nil || rSpaceNo != 514 || rIndexNo != 3 { t.Errorf("numeric space and index params not resolved as-is") } - rSpaceNo, rIndexNo, err = schema.resolveSpaceIndex(514, nil) + rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex(514, nil) if err != nil || rSpaceNo != 514 { t.Errorf("numeric space param not resolved as-is") } - rSpaceNo, rIndexNo, err = schema.resolveSpaceIndex("schematest", "secondary") + rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex("schematest", "secondary") if err != nil || rSpaceNo != 514 || rIndexNo != 3 { t.Errorf("symbolic space and index params not resolved") } - rSpaceNo, rIndexNo, err = schema.resolveSpaceIndex("schematest", nil) + rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex("schematest", nil) if err != nil || rSpaceNo != 514 { t.Errorf("symbolic space param not resolved") } - rSpaceNo, rIndexNo, err = schema.resolveSpaceIndex("schematest22", "secondary") + rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex("schematest22", "secondary") if err == nil { t.Errorf("resolveSpaceIndex didn't returned error with not existing space name") } - rSpaceNo, rIndexNo, err = schema.resolveSpaceIndex("schematest", "secondary22") + rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex("schematest", "secondary22") if err == nil { t.Errorf("resolveSpaceIndex didn't returned error with not existing index name") } From 2b774f28ff09739e64cc587707c7d6b7c6a4f07e Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 12 Nov 2016 17:39:01 +0300 Subject: [PATCH 142/605] ... --- response.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/response.go b/response.go index 2eb51591a..d73607c51 100644 --- a/response.go +++ b/response.go @@ -125,7 +125,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { return err } default: - if _, err = d.DecodeInterface(); err != nil { + if err = d.Skip(); err != nil { return err } } From e43a99fdd978a370fe153b30737ffe66e8117dc2 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 12 Nov 2016 17:42:46 +0300 Subject: [PATCH 143/605] fix test --- example_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example_test.go b/example_test.go index 3379fec88..b415913fe 100644 --- a/example_test.go +++ b/example_test.go @@ -158,8 +158,8 @@ func Example_main() { fmt.Println("Eval Code", resp.Code) fmt.Println("Eval Data", resp.Data) - resp, err = client.Insert("test", &Tuple{11, "test", "one"}) - resp, err = client.Insert("test", &Tuple{12, "test", "one"}) + resp, err = client.Replace("test", &Tuple{11, "test", "eleven"}) + resp, err = client.Replace("test", &Tuple{12, "test", "twelve"}) var futs [3]*tarantool.Future futs[0] = client.SelectAsync("test", "primary", 0, 2, tarantool.IterLe, tarantool.UintKey{12}) @@ -206,7 +206,7 @@ func Example_main() { // Eval Code 0 // Eval Data [3] // Fut 0 Error - // Fut 0 Data [{12 val 12 bla} {11 test one}] + // Fut 0 Data [{12 test twelve} {11 test eleven}] // Fut 1 Error // Fut 1 Data [[13 4]] // Fut 2 Error From 8b78090340f8703803c7f791be27c3a01cdb8583 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 13 Nov 2016 12:33:13 +0300 Subject: [PATCH 144/605] update fields could be negative --- client_tools.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client_tools.go b/client_tools.go index 0477ccff5..ed76ea258 100644 --- a/client_tools.go +++ b/client_tools.go @@ -56,31 +56,31 @@ func (k IntIntKey) EncodeMsgpack(enc *msgpack.Encoder) error { // Op - is update operation type Op struct { Op string - Field uint + Field int Arg interface{} } func (o Op) EncodeMsgpack(enc *msgpack.Encoder) error { enc.EncodeSliceLen(3) enc.EncodeString(o.Op) - enc.EncodeUint(o.Field) + enc.EncodeInt(o.Field) return enc.Encode(o.Arg) } type OpSplice struct { Op string - Field uint - Pos uint - Len uint + Field int + Pos int + Len int Replace string } func (o OpSplice) EncodeMsgpack(enc *msgpack.Encoder) error { enc.EncodeSliceLen(5) enc.EncodeString(o.Op) - enc.EncodeUint(o.Field) - enc.EncodeUint(o.Pos) - enc.EncodeUint(o.Len) + enc.EncodeInt(o.Field) + enc.EncodeInt(o.Pos) + enc.EncodeInt(o.Len) enc.EncodeString(o.Replace) return nil } From 32cfea4d74049f97916e0b93d14cc8e3988825db Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 13 Nov 2016 12:33:25 +0300 Subject: [PATCH 145/605] ... --- example_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_test.go b/example_test.go index b415913fe..766560db8 100644 --- a/example_test.go +++ b/example_test.go @@ -75,7 +75,7 @@ func ExampleConnection_SelectTyped() { // response is []tarantool_test.Tuple{tarantool_test.Tuple{Id:0x457, Msg:"hello", Name:"world"}} } -func Example_main() { +func Example() { spaceNo := uint32(512) indexNo := uint32(0) From 3d5b419e435cd291971ec958bd3005e8044dc9d9 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 13 Nov 2016 12:36:22 +0300 Subject: [PATCH 146/605] fix ratelimit --- connection.go | 1 + 1 file changed, 1 insertion(+) diff --git a/connection.go b/connection.go index f927720f9..105bbf251 100644 --- a/connection.go +++ b/connection.go @@ -491,6 +491,7 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { case conn.rlimit <- struct{}{}: default: fut.err = ClientError{ErrRateLimited, "Request is rate limited on client"} + return } } fut.ready = make(chan struct{}) From 3fb94eb0822b00e60d877eff0c4f769d93136e07 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 13 Nov 2016 13:00:55 +0300 Subject: [PATCH 147/605] fix block on msgpack error --- request.go | 1 + 1 file changed, 1 insertion(+) diff --git a/request.go b/request.go index 3a8e57883..2fa0b041e 100644 --- a/request.go +++ b/request.go @@ -350,6 +350,7 @@ func (fut *Future) send(body func(*msgpack.Encoder) error) *Future { fut.conn.putFuture(fut, body) if fut.err != nil { fut.conn.fetchFuture(fut.requestId) + fut.markReady() return fut } From 945b9f6a5ab1df3c76156dbfabe1bb1330d4c7e6 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 13 Nov 2016 13:39:38 +0300 Subject: [PATCH 148/605] return timeout to test --- tarantool_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tarantool_test.go b/tarantool_test.go index 54bc6b6a9..e8323b8fb 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -121,7 +121,7 @@ var spaceName = "test" var indexNo = uint32(0) var indexName = "primary" var opts = Opts{ - Timeout: 0 * time.Millisecond, + Timeout: 500 * time.Millisecond, User: "test", Pass: "test", //Concurrency: 32, From 82fff4d8b6e576f724c33a8bfa0056352288eff6 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 20 Nov 2016 02:49:31 +0300 Subject: [PATCH 149/605] add timeout on connection attempt --- connection.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/connection.go b/connection.go index 105bbf251..2676716b1 100644 --- a/connection.go +++ b/connection.go @@ -226,7 +226,13 @@ func (conn *Connection) LocalAddr() string { } func (conn *Connection) dial() (err error) { - connection, err := net.Dial("tcp", conn.addr) + timeout := conn.opts.Reconnect / 2 + if timeout == 0 { + timeout = 500 * time.Millisecond + } else if timeout > 5*time.Second { + timeout = 5 * time.Second + } + connection, err := net.DialTimeout("tcp", conn.addr, timeout) if err != nil { return } @@ -328,6 +334,7 @@ func (conn *Connection) createConnection() (r *bufio.Reader, w *bufio.Writer, er if conn.c == nil { var reconnects uint for { + now := time.Now() err = conn.dial() if err == nil { break @@ -340,7 +347,7 @@ func (conn *Connection) createConnection() (r *bufio.Reader, w *bufio.Writer, er } log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s\n", reconnects, conn.opts.MaxReconnects, conn.addr, err.Error()) reconnects++ - time.Sleep(conn.opts.Reconnect) + time.Sleep(now.Add(conn.opts.Reconnect).Sub(time.Now())) continue } else { return From f17b84e4a0b94928b9f5b405a4a14adf31e8e83b Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 1 Dec 2016 19:42:50 +0300 Subject: [PATCH 150/605] fix panic on disconnect + timeout --- connection.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/connection.go b/connection.go index 2676716b1..acab9dc77 100644 --- a/connection.go +++ b/connection.go @@ -381,9 +381,9 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. defer conn.unlockShards() for i := range conn.shard { conn.shard[i].buf = conn.shard[i].buf[:0] - requests := conn.shard[i].requests - for pos, pair := range requests { - fut := pair.first + requests := &conn.shard[i].requests + for pos := range requests { + fut := requests[pos].first requests[pos].first = nil requests[pos].last = &requests[pos].first for fut != nil { From 3db54822c399205ed2cda458aaf8473daf95f17a Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 1 Dec 2016 16:42:09 +0300 Subject: [PATCH 151/605] add couple of benchmarks --- tarantool_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tarantool_test.go b/tarantool_test.go index e8323b8fb..286e293f3 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -153,6 +153,30 @@ func BenchmarkClientSerial(b *testing.B) { } } +func BenchmarkClientSerialTyped(b *testing.B) { + var err error + + conn, err := Connect(server, opts) + if err != nil { + b.Errorf("No connection available") + return + } + defer conn.Close() + + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Errorf("No connection available") + } + + var r []Tuple + for i := 0; i < b.N; i++ { + err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, IntKey{1111}, &r) + if err != nil { + b.Errorf("No connection available") + } + } +} + func BenchmarkClientFuture(b *testing.B) { var err error @@ -355,6 +379,43 @@ func BenchmarkClientParallelMassive(b *testing.B) { close(limit) } +func BenchmarkClientParallelMassiveUntyped(b *testing.B) { + conn, err := Connect(server, opts) + if err != nil { + b.Errorf("No connection available") + return + } + defer conn.Close() + + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Errorf("No connection available") + } + + var wg sync.WaitGroup + limit := make(chan struct{}, 128*1024) + for i := 0; i < 512; i++ { + go func() { + for { + if _, ok := <-limit; !ok { + break + } + _, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) + wg.Done() + if err != nil { + b.Errorf("No connection available") + } + } + }() + } + for i := 0; i < b.N; i++ { + wg.Add(1) + limit <- struct{}{} + } + wg.Wait() + close(limit) +} + /////////////////// func TestClient(t *testing.T) { From 5ecb87d7c777b6d70dd77d45a729374357e4e50c Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 1 Dec 2016 17:31:16 +0300 Subject: [PATCH 152/605] deadline io use net.Conn.Set(Read|Write)Deadline to fail fast on network unstability. run Pinger goroutine to force some Reads and Writes. --- connection.go | 23 +++++++++++++++++++++-- deadline_io.go | 27 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 deadline_io.go diff --git a/connection.go b/connection.go index acab9dc77..2187d02e2 100644 --- a/connection.go +++ b/connection.go @@ -95,6 +95,7 @@ type Greeting struct { // Opts is a way to configure Connection type Opts struct { // Timeout is requests timeout. + // Also used to setup net.TCPConn.Set(Read|Write)Deadline Timeout time.Duration // Reconnect is a pause between reconnection attempts. // If specified, then when tarantool is not reachable or disconnected, @@ -185,6 +186,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { go conn.writer() go conn.reader() + go conn.pinger() if conn.opts.Timeout > 0 { go conn.timeouts() } @@ -238,8 +240,9 @@ func (conn *Connection) dial() (err error) { } c := connection.(*net.TCPConn) c.SetNoDelay(true) - r := bufio.NewReaderSize(c, 128*1024) - w := bufio.NewWriterSize(c, 128*1024) + dc := &DeadlineIO{to: conn.opts.Timeout, c: c} + r := bufio.NewReaderSize(dc, 128*1024) + w := bufio.NewWriterSize(dc, 128*1024) greeting := make([]byte, 128) _, err = io.ReadFull(r, greeting) if err != nil { @@ -419,6 +422,22 @@ func (conn *Connection) closeConnectionForever(err error) error { return err } +func (conn *Connection) pinger() { + if conn.opts.Timeout == 0 { + return + } + t := time.NewTicker(conn.opts.Timeout / 3) + defer t.Stop() + for { + select { + case <-conn.control: + return + case <-t.C: + } + conn.Ping() + } +} + func (conn *Connection) writer() { var w *bufio.Writer var err error diff --git a/deadline_io.go b/deadline_io.go new file mode 100644 index 000000000..65fe4cd9f --- /dev/null +++ b/deadline_io.go @@ -0,0 +1,27 @@ +package tarantool + +import ( + "net" + "time" +) + +type DeadlineIO struct { + to time.Duration + c *net.TCPConn +} + +func (d *DeadlineIO) Write(b []byte) (n int, err error) { + if d.to > 0 { + d.c.SetWriteDeadline(time.Now().Add(d.to)) + } + n, err = d.c.Write(b) + return +} + +func (d *DeadlineIO) Read(b []byte) (n int, err error) { + if d.to > 0 { + d.c.SetReadDeadline(time.Now().Add(d.to)) + } + n, err = d.c.Read(b) + return +} From db47313bc5285ad258a7d3f5380fe82d335fb1a9 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 1 Dec 2016 19:04:00 +0300 Subject: [PATCH 153/605] small fix future sending. --- connection.go | 6 +++--- request.go | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/connection.go b/connection.go index 2187d02e2..f34729136 100644 --- a/connection.go +++ b/connection.go @@ -529,15 +529,15 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { shard.rmut.Lock() if conn.closed { fut.err = ClientError{ErrConnectionClosed, "using closed connection"} - close(fut.ready) + fut.ready = nil shard.rmut.Unlock() return } if c := conn.c; c == nil { fut.err = ClientError{ErrConnectionNotReady, "client connection is not ready"} - close(fut.ready) + fut.ready = nil shard.rmut.Unlock() - return fut + return } pos := (fut.requestId / conn.opts.Concurrency) & (requestsMap - 1) pair := &shard.requests[pos] diff --git a/request.go b/request.go index 2fa0b041e..e75736344 100644 --- a/request.go +++ b/request.go @@ -344,13 +344,14 @@ func (fut *Future) pack(h *smallWBuf, enc *msgpack.Encoder, body func(*msgpack.E } func (fut *Future) send(body func(*msgpack.Encoder) error) *Future { - if fut.err != nil { + if fut.ready == nil { return fut } fut.conn.putFuture(fut, body) if fut.err != nil { - fut.conn.fetchFuture(fut.requestId) - fut.markReady() + if f := fut.conn.fetchFuture(fut.requestId); f == fut { + fut.markReady() + } return fut } From 7d126c4be6a3092469171379013347c735416209 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 1 Dec 2016 19:25:01 +0300 Subject: [PATCH 154/605] Connection.ConnectedNow() --- connection.go | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/connection.go b/connection.go index f34729136..b02edb712 100644 --- a/connection.go +++ b/connection.go @@ -16,6 +16,11 @@ import ( ) const requestsMap = 128 +const ( + connDisconnected = 0 + connConnected = 1 + connClosed = 2 +) var epoch = time.Now() @@ -69,7 +74,7 @@ type Connection struct { control chan struct{} rlimit chan struct{} opts Opts - closed bool + state uint32 dec *msgpack.Decoder lenbuf [PacketLengthBytes]byte } @@ -200,6 +205,11 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { return conn, err } +// ConnectedNow reports if connection is established at the moment. +func (conn *Connection) ConnectedNow() bool { + return atomic.LoadUint32(&conn.state) == connConnected +} + // Close closes Connection. // After this method called, there is no way to reopen this Connection. func (conn *Connection) Close() error { @@ -271,11 +281,12 @@ func (conn *Connection) dial() (err error) { } // Only if connected and authenticated - conn.lockShards() conn.c = c - conn.unlockShards() conn.r = r conn.w = w + conn.lockShards() + atomic.StoreUint32(&conn.state, connConnected) + conn.unlockShards() return } @@ -330,7 +341,7 @@ func (conn *Connection) readAuthResponse(r io.Reader) (err error) { func (conn *Connection) createConnection() (r *bufio.Reader, w *bufio.Writer, err error) { conn.mutex.Lock() defer conn.mutex.Unlock() - if conn.closed { + if conn.state == connClosed { err = ClientError{ErrConnectionClosed, "using closed connection"} return } @@ -374,10 +385,11 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. if r != nil && r != conn.r { return conn.r, nil, nil } - err = conn.c.Close() conn.lockShards() - conn.c = nil + atomic.CompareAndSwapUint32(&conn.state, connConnected, connDisconnected) conn.unlockShards() + err = conn.c.Close() + conn.c = nil conn.r = nil conn.w = nil conn.lockShards() @@ -415,7 +427,7 @@ func (conn *Connection) unlockShards() { func (conn *Connection) closeConnectionForever(err error) error { conn.lockShards() - conn.closed = true + atomic.StoreUint32(&conn.state, connClosed) conn.unlockShards() close(conn.control) _, _, err = conn.closeConnection(err, nil, nil) @@ -443,7 +455,7 @@ func (conn *Connection) writer() { var err error var shardn uint32 var packet smallWBuf - for !conn.closed { + for atomic.LoadUint32(&conn.state) != connClosed { select { case shardn = <-conn.dirtyShard: default: @@ -483,7 +495,7 @@ func (conn *Connection) writer() { func (conn *Connection) reader() { var r *bufio.Reader var err error - for !conn.closed { + for atomic.LoadUint32(&conn.state) != connClosed { if r == nil { if r, _, err = conn.createConnection(); err != nil { conn.closeConnectionForever(err) @@ -527,13 +539,13 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { shardn := fut.requestId & (conn.opts.Concurrency - 1) shard := &conn.shard[shardn] shard.rmut.Lock() - if conn.closed { + switch conn.state { + case connClosed: fut.err = ClientError{ErrConnectionClosed, "using closed connection"} fut.ready = nil shard.rmut.Unlock() return - } - if c := conn.c; c == nil { + case connDisconnected: fut.err = ClientError{ErrConnectionNotReady, "client connection is not ready"} fut.ready = nil shard.rmut.Unlock() From 620328bffa25b8095ff5b97f5e38e4938280cab5 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 31 Dec 2016 03:46:38 +0300 Subject: [PATCH 155/605] fix reconnection logic. add Opts.SkipSchema --- connection.go | 212 ++++++++++++++++++++++++-------------------------- request.go | 57 +++++++------- schema.go | 8 ++ 3 files changed, 140 insertions(+), 137 deletions(-) diff --git a/connection.go b/connection.go index b02edb712..08b892c84 100644 --- a/connection.go +++ b/connection.go @@ -59,8 +59,6 @@ var epoch = time.Now() type Connection struct { addr string c *net.TCPConn - r *bufio.Reader - w *bufio.Writer mutex sync.Mutex // Schema contains schema loaded on connection. Schema *Schema @@ -133,6 +131,9 @@ type Opts struct { // It is rounded upto nearest power of 2. // By default it is runtime.GOMAXPROCS(-1) * 4 Concurrency uint32 + // SkipSchema disables schema loading. Without disabling schema loading, + // there is no way to create Connection for currently not accessible tarantool. + SkipSchema bool } // Connect creates and configures new Connection @@ -164,7 +165,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { } conn.opts.Concurrency = c + 1 } - conn.dirtyShard = make(chan uint32, conn.opts.Concurrency) + conn.dirtyShard = make(chan uint32, conn.opts.Concurrency*2) conn.shard = make([]connShard, conn.opts.Concurrency) for i := range conn.shard { shard := &conn.shard[i] @@ -180,26 +181,35 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { } } - var reconnect time.Duration - // disable reconnecting for first connect - reconnect, conn.opts.Reconnect = conn.opts.Reconnect, 0 - _, _, err = conn.createConnection() - conn.opts.Reconnect = reconnect - if err != nil && reconnect == 0 { - return nil, err + if err = conn.createConnection(false); err != nil { + if conn.opts.Reconnect <= 0 { + return nil, err + } else { + // without SkipSchema it is useless + go func(conn *Connection) { + conn.mutex.Lock() + defer conn.mutex.Unlock() + if err := conn.createConnection(true); err != nil { + conn.closeConnection(err, true) + } + }(conn) + err = nil + } } - go conn.writer() - go conn.reader() go conn.pinger() if conn.opts.Timeout > 0 { go conn.timeouts() } // TODO: reload schema after reconnect - if err = conn.loadSchema(); err != nil { - conn.closeConnection(err, nil, nil) - return nil, err + if !conn.opts.SkipSchema { + if err = conn.loadSchema(); err != nil { + conn.mutex.Lock() + defer conn.mutex.Unlock() + conn.closeConnection(err, true) + return nil, err + } } return conn, err @@ -214,7 +224,9 @@ func (conn *Connection) ConnectedNow() bool { // After this method called, there is no way to reopen this Connection. func (conn *Connection) Close() error { err := ClientError{ErrConnectionClosed, "connection closed by client"} - return conn.closeConnectionForever(err) + conn.mutex.Lock() + defer conn.mutex.Unlock() + return conn.closeConnection(err, true) } // RemoteAddr is address of Tarantool socket @@ -281,19 +293,18 @@ func (conn *Connection) dial() (err error) { } // Only if connected and authenticated - conn.c = c - conn.r = r - conn.w = w conn.lockShards() + conn.c = c atomic.StoreUint32(&conn.state, connConnected) conn.unlockShards() + go conn.writer(w, c) + go conn.reader(r, c) return } func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { request := &Future{ - conn: conn, requestId: 0, requestCode: AuthRequest, } @@ -338,62 +349,47 @@ func (conn *Connection) readAuthResponse(r io.Reader) (err error) { return } -func (conn *Connection) createConnection() (r *bufio.Reader, w *bufio.Writer, err error) { - conn.mutex.Lock() - defer conn.mutex.Unlock() +func (conn *Connection) createConnection(reconnect bool) (err error) { + var reconnects uint + for conn.c == nil && conn.state == connDisconnected { + now := time.Now() + err = conn.dial() + if err == nil || !reconnect { + return + } + if conn.opts.MaxReconnects > 0 && reconnects > conn.opts.MaxReconnects { + log.Printf("tarantool: last reconnect to %s failed: %s, giving it up.\n", conn.addr, err.Error()) + err = ClientError{ErrConnectionClosed, "last reconnect failed"} + // mark connection as closed to avoid reopening by another goroutine + return + } + log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s\n", reconnects, conn.opts.MaxReconnects, conn.addr, err.Error()) + reconnects++ + conn.mutex.Unlock() + time.Sleep(now.Add(conn.opts.Reconnect).Sub(time.Now())) + conn.mutex.Lock() + } if conn.state == connClosed { err = ClientError{ErrConnectionClosed, "using closed connection"} - return } - if conn.c == nil { - var reconnects uint - for { - now := time.Now() - err = conn.dial() - if err == nil { - break - } else if conn.opts.Reconnect > 0 { - if conn.opts.MaxReconnects > 0 && reconnects > conn.opts.MaxReconnects { - log.Printf("tarantool: last reconnect to %s failed: %s, giving it up.\n", conn.addr, err.Error()) - err = ClientError{ErrConnectionClosed, "last reconnect failed"} - // mark connection as closed to avoid reopening by another goroutine - return - } - log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s\n", reconnects, conn.opts.MaxReconnects, conn.addr, err.Error()) - reconnects++ - time.Sleep(now.Add(conn.opts.Reconnect).Sub(time.Now())) - continue - } else { - return - } - } - } - r = conn.r - w = conn.w return } -func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio.Writer) (rr *bufio.Reader, ww *bufio.Writer, err error) { - conn.mutex.Lock() - defer conn.mutex.Unlock() - if conn.c == nil { - return - } - if w != nil && w != conn.w { - return nil, conn.w, nil - } - if r != nil && r != conn.r { - return conn.r, nil, nil - } - conn.lockShards() - atomic.CompareAndSwapUint32(&conn.state, connConnected, connDisconnected) - conn.unlockShards() - err = conn.c.Close() - conn.c = nil - conn.r = nil - conn.w = nil +func (conn *Connection) closeConnection(neterr error, forever bool) (err error) { conn.lockShards() defer conn.unlockShards() + if forever { + if conn.state != connClosed { + close(conn.control) + atomic.StoreUint32(&conn.state, connClosed) + } + } else { + atomic.StoreUint32(&conn.state, connDisconnected) + } + if conn.c != nil { + err = conn.c.Close() + conn.c = nil + } for i := range conn.shard { conn.shard[i].buf = conn.shard[i].buf[:0] requests := &conn.shard[i].requests @@ -403,7 +399,7 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. requests[pos].last = &requests[pos].first for fut != nil { fut.err = neterr - fut.markReady() + fut.markReady(conn) fut, fut.next = fut.next, nil } } @@ -411,6 +407,21 @@ func (conn *Connection) closeConnection(neterr error, r *bufio.Reader, w *bufio. return } +func (conn *Connection) reconnect(neterr error, c *net.TCPConn) { + conn.mutex.Lock() + defer conn.mutex.Unlock() + if conn.opts.Reconnect > 0 { + if c == conn.c { + conn.closeConnection(neterr, false) + if err := conn.createConnection(true); err != nil { + conn.closeConnection(err, true) + } + } + } else { + conn.closeConnection(neterr, true) + } +} + func (conn *Connection) lockShards() { for i := range conn.shard { conn.shard[i].rmut.Lock() @@ -425,20 +436,12 @@ func (conn *Connection) unlockShards() { } } -func (conn *Connection) closeConnectionForever(err error) error { - conn.lockShards() - atomic.StoreUint32(&conn.state, connClosed) - conn.unlockShards() - close(conn.control) - _, _, err = conn.closeConnection(err, nil, nil) - return err -} - func (conn *Connection) pinger() { - if conn.opts.Timeout == 0 { - return + to := conn.opts.Timeout + if to == 0 { + to = 3*time.Second } - t := time.NewTicker(conn.opts.Timeout / 3) + t := time.NewTicker(to / 3) defer t.Stop() for { select { @@ -450,9 +453,7 @@ func (conn *Connection) pinger() { } } -func (conn *Connection) writer() { - var w *bufio.Writer - var err error +func (conn *Connection) writer(w *bufio.Writer, c *net.TCPConn) { var shardn uint32 var packet smallWBuf for atomic.LoadUint32(&conn.state) != connClosed { @@ -460,9 +461,10 @@ func (conn *Connection) writer() { case shardn = <-conn.dirtyShard: default: runtime.Gosched() - if len(conn.dirtyShard) == 0 && w != nil { + if len(conn.dirtyShard) == 0 { if err := w.Flush(); err != nil { - _, w, _ = conn.closeConnection(err, nil, w) + conn.reconnect(err, c) + return } } select { @@ -471,51 +473,42 @@ func (conn *Connection) writer() { return } } - if w == nil { - if _, w, err = conn.createConnection(); err != nil { - conn.closeConnectionForever(err) - return - } - } shard := &conn.shard[shardn] shard.bufmut.Lock() + if conn.c != c { + conn.dirtyShard <- shardn + shard.bufmut.Unlock() + return + } packet, shard.buf = shard.buf, packet shard.bufmut.Unlock() if len(packet) == 0 { continue } if err := write(w, packet); err != nil { - _, w, _ = conn.closeConnection(err, nil, w) - continue + conn.reconnect(err, c) + return } packet = packet[0:0] } } -func (conn *Connection) reader() { - var r *bufio.Reader - var err error +func (conn *Connection) reader(r *bufio.Reader, c *net.TCPConn) { for atomic.LoadUint32(&conn.state) != connClosed { - if r == nil { - if r, _, err = conn.createConnection(); err != nil { - conn.closeConnectionForever(err) - return - } - } respBytes, err := conn.read(r) if err != nil { - r, _, _ = conn.closeConnection(err, r, nil) - continue + conn.reconnect(err, c) + return } resp := &Response{buf: smallBuf{b: respBytes}} err = resp.decodeHeader(conn.dec) if err != nil { - r, _, _ = conn.closeConnection(err, r, nil) - continue + conn.reconnect(err, c) + return } if fut := conn.fetchFuture(resp.RequestId); fut != nil { fut.resp = resp - fut.markReady() + fut.markReady(conn) } else { log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) } @@ -533,7 +526,6 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { } } fut.ready = make(chan struct{}) - fut.conn = conn fut.requestId = conn.nextRequestId() fut.requestCode = requestCode shardn := fut.requestId & (conn.opts.Concurrency - 1) @@ -666,7 +658,7 @@ func (conn *Connection) timeouts() { Code: ErrTimeouted, Msg: fmt.Sprintf("client timeout for request %d", fut.requestId), } - fut.markReady() + fut.markReady(conn) shard.bufmut.Unlock() } if pair.first != nil && pair.first.timeout < minNext { diff --git a/request.go b/request.go index e75736344..9df52ea35 100644 --- a/request.go +++ b/request.go @@ -7,20 +7,19 @@ import ( // Future is a handle for asynchronous request type Future struct { - conn *Connection requestId uint32 requestCode int32 + timeout time.Duration resp *Response err error ready chan struct{} - timeout time.Duration next *Future } // Ping sends empty request to Tarantool to check connection. func (conn *Connection) Ping() (resp *Response, err error) { future := conn.newFuture(PingRequest) - return future.send(func(enc *msgpack.Encoder) error { enc.EncodeMapLen(0); return nil }).Get() + return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(0); return nil }).Get() } func (req *Future) fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { @@ -185,9 +184,9 @@ func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, ite future := conn.newFuture(SelectRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) if err != nil { - return badfuture(err) + return future.fail(conn, err) } - return future.send(func(enc *msgpack.Encoder) error { + return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(6) future.fillIterator(enc, offset, limit, iterator) return future.fillSearch(enc, spaceNo, indexNo, key) @@ -200,9 +199,9 @@ func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Futur future := conn.newFuture(InsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { - return badfuture(err) + return future.fail(conn, err) } - return future.send(func(enc *msgpack.Encoder) error { + return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) return future.fillInsert(enc, spaceNo, tuple) }) @@ -214,9 +213,9 @@ func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Futu future := conn.newFuture(ReplaceRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { - return badfuture(err) + return future.fail(conn, err) } - return future.send(func(enc *msgpack.Encoder) error { + return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) return future.fillInsert(enc, spaceNo, tuple) }) @@ -228,9 +227,9 @@ func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) * future := conn.newFuture(DeleteRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) if err != nil { - return badfuture(err) + return future.fail(conn, err) } - return future.send(func(enc *msgpack.Encoder) error { + return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(3) return future.fillSearch(enc, spaceNo, indexNo, key) }) @@ -242,9 +241,9 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface future := conn.newFuture(UpdateRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) if err != nil { - return badfuture(err) + return future.fail(conn, err) } - return future.send(func(enc *msgpack.Encoder) error { + return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(4) if err := future.fillSearch(enc, spaceNo, indexNo, key); err != nil { return err @@ -260,9 +259,9 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops in future := conn.newFuture(UpsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { - return badfuture(err) + return future.fail(conn, err) } - return future.send(func(enc *msgpack.Encoder) error { + return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(3) enc.EncodeUint64(KeySpaceNo) enc.EncodeUint64(uint64(spaceNo)) @@ -279,7 +278,7 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops in // It uses request code for tarantool 1.6, so future's result is always array of arrays func (conn *Connection) CallAsync(functionName string, args interface{}) *Future { future := conn.newFuture(CallRequest) - return future.send(func(enc *msgpack.Encoder) error { + return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) enc.EncodeUint64(KeyFunctionName) enc.EncodeString(functionName) @@ -293,7 +292,7 @@ func (conn *Connection) CallAsync(functionName string, args interface{}) *Future // (though, keep in mind, result is always array) func (conn *Connection) Call17Async(functionName string, args interface{}) *Future { future := conn.newFuture(Call17Request) - return future.send(func(enc *msgpack.Encoder) error { + return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) enc.EncodeUint64(KeyFunctionName) enc.EncodeString(functionName) @@ -305,7 +304,7 @@ func (conn *Connection) Call17Async(functionName string, args interface{}) *Futu // EvalAsync sends a lua expression for evaluation and returns Future. func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { future := conn.newFuture(EvalRequest) - return future.send(func(enc *msgpack.Encoder) error { + return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) enc.EncodeUint64(KeyExpression) enc.EncodeString(expr) @@ -343,14 +342,14 @@ func (fut *Future) pack(h *smallWBuf, enc *msgpack.Encoder, body func(*msgpack.E return } -func (fut *Future) send(body func(*msgpack.Encoder) error) *Future { +func (fut *Future) send(conn *Connection, body func(*msgpack.Encoder) error) *Future { if fut.ready == nil { return fut } - fut.conn.putFuture(fut, body) + conn.putFuture(fut, body) if fut.err != nil { - if f := fut.conn.fetchFuture(fut.requestId); f == fut { - fut.markReady() + if f := conn.fetchFuture(fut.requestId); f == fut { + fut.markReady(conn) } return fut } @@ -358,15 +357,19 @@ func (fut *Future) send(body func(*msgpack.Encoder) error) *Future { return fut } -func (fut *Future) markReady() { +func (fut *Future) markReady(conn *Connection) { close(fut.ready) - if fut.conn.rlimit != nil { - <-fut.conn.rlimit + if conn.rlimit != nil { + <-conn.rlimit } } -func badfuture(err error) *Future { - return &Future{err: err} +func (fut *Future) fail(conn *Connection, err error) *Future { + if f := conn.fetchFuture(fut.requestId); f == fut { + f.err = err + fut.markReady(conn) + } + return fut } func (fut *Future) wait() { diff --git a/schema.go b/schema.go index 339974c50..00596d12b 100644 --- a/schema.go +++ b/schema.go @@ -172,6 +172,10 @@ func (schema *Schema) resolveSpaceIndex(s interface{}, i interface{}) (spaceNo, switch s := s.(type) { case string: + if schema == nil { + err = fmt.Errorf("Schema is not loaded") + return + } if space, ok = schema.Spaces[s]; !ok { err = fmt.Errorf("there is no space with name %s", s) return @@ -208,6 +212,10 @@ func (schema *Schema) resolveSpaceIndex(s interface{}, i interface{}) (spaceNo, if i != nil { switch i := i.(type) { case string: + if schema == nil { + err = fmt.Errorf("Schema is not loaded") + return + } if space == nil { if space, ok = schema.SpacesById[spaceNo]; !ok { err = fmt.Errorf("there is no space with id %d", spaceNo) From eb82a68c0029ebb074537c156a3604bc458f3302 Mon Sep 17 00:00:00 2001 From: lenkis Date: Fri, 10 Feb 2017 10:11:02 +0300 Subject: [PATCH 156/605] Add info to README --- README.md | 192 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 182 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ad492836b..0ffa5d7d2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,172 @@ -# Tarantool + + + + + +# Client in Go for Tarantool 1.6+ + +The `go-tarantool` package has everything necessary for interfacing with +[Tarantool 1.6+](http://tarantool.org/). + +The advantage of integrating Go with Tarantool, which is an application server +plus a DBMS, is that Go programmers can handle databases and perform on-the-fly +recompilations of embedded Lua routines, just as in C, with responses that are +faster than other packages according to public benchmarks. + +## Table of contents + +* [Installation](#installation) +* [Hello World](#hello-world) +* [API reference](#api-reference) +* [Walking\-through example in Go](#walking-through-example-in-go) +* [Help](#help) +* [Usage](#usage) +* [Schema](#schema) +* [Custom (un)packing and typed selects and function calls](#custom-unpacking-and-typed-selects-and-function-calls) +* [Options](#options) + +## Installation + +We assume that you have Tarantool version 1.6 and a modern Linux or BSD +operating system. + +You will need a current version of `go`, version 1.3 or later (use +`go version` to check the version number). Do not use `gccgo-go`. + +**Note:** If your `go` version is younger than 1.3, or if `go` is not installed, +download the latest tarball from [golang.org](https://golang.org/dl/) and say: + +```bash +$ sudo tar -C /usr/local -xzf go1.7.5.linux-amd64.tar.gz +$ export PATH=$PATH:/usr/local/go/bin +$ export GOPATH="/usr/local/go/go-tarantool" +$ sudo chmod -R a+rwx /usr/local/go +``` + +The `go-tarantool` package is in +[tarantool/go-tarantool](github.com/tarantool/go-tarantool) repository. +To download and install, say: + +``` +$ go get github.com/tarantool/go-tarantool +``` + +This should bring source and binary files into subdirectories of `/usr/local/go`, +making it possible to access by adding `github.com/tarantool/go-tarantool` in +the `import {...}` section at the start of any Go program. + +

Hello World

+ +In the "[Connectors](http://tarantool.org/doc/book/connectors/index.html#go)" +chapter of the Tarantool manual, there is an explanation of a very short (18-line) +program written in Go. Follow the instructions at the start of the "Connectors" +chapter carefully. Then cut and paste the example into a file named `example.go`, +and run it. You should see: nothing. + +If that is what you see, then you have successfully installed `go-tarantool` and +successfully executed a program that manipulated the contents of a Tarantool +database. + +

API reference

+ +Read the [Tarantool manual](http://tarantool.org/doc.html) to find descriptions +of terms like "connect", "space", "index", and the requests for creating and +manipulating database objects or Lua functions. + +The source files for the requests library are: +* [connection.go](https://github.com/tarantool/go-tarantool/blob/master/connection.go) + for the `Connect()` function plus functions related to connecting, and +* [request.go](https://github.com/tarantool/go-tarantool/blob/master/request.go) + for data-manipulation functions and Lua invocations. + +See comments in those files for syntax details: +``` +Ping +closeConnection +Select +Insert +Replace +Delete +Update +Upsert +Call +Call17 +Eval +``` + +The supported requests have parameters and results equivalent to requests in the +Tarantool manual. There are also Typed and Async versions of each data-manipulation +function. + +The source file for error-handling tools is +[errors.go](https://github.com/tarantool/go-tarantool/blob/master/errors.go), +which has structure definitions and constants whose names are equivalent to names +of errors that the Tarantool server returns. + +## Walking-through example in Go + +We can now have a closer look at the `example.go` program and make some observations +about what it does. + +``` +package main -[Tarantool 1.6+](http://tarantool.org/) client in Go. +import ( + "fmt" + "github.com/tarantool/go-tarantool" +) + +func main() { + opts := tarantool.Opts{User: "guest"} + conn, err := tarantool.Connect("127.0.0.1:3301", opts) + if err != nil { + fmt.Println("Connection refused: %s", err.Error()) + } + resp, err := conn.Insert(999, []interface{}{99999, "BB"}) + if err != nil { + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + } +} +``` + +**Observation 1:** the line "`github.com/tarantool/go-tarantool`" in the +`import(...)` section brings in all Tarantool-related functions and structures. + +**Observation 2:** the line beginning with "`Opts :=`" sets up the options for +`Connect()`. In this example, there is only one thing in the structure, a user +name. The structure can also contain: + +* `Pass` (password), +* `Timeout` (maximum number of milliseconds to wait before giving up), +* `Reconnect` (number of seconds to wait before retrying if a connection fails), +* `MaxReconnect` (maximum number of times to retry). + +**Observation 3:** the line containing "`tarantool.Connect`" is essential for +beginning any session. There are two parameters: + +* a string with `host:port` format, and +* the option structure that was set up earlier. + +**Observation 4:** the `err` structure will be `nil` if there is no error, +otherwise it will have a description which can be retrieved with `err.Error()`. + +**Observation 5:** the `Insert` request, like almost all requests, is preceded by +"`conn.`" which is the name of the object that was returned by `Connect()`. +There are two parameters: + +* a space number (it could just as easily have been a space name), and +* a tuple. + +## Help + +To contact `go-tarantool` developers on any problems, create an issue at +[tarantool/go-tarantool](http://github.com/tarantool/go-tarantool/issues). + +The developers of the [Tarantool server](http://github.com/tarantool/tarantool) +will also be happy to provide advice or receive feedback. ## Usage @@ -116,6 +282,7 @@ func main() { ``` ## Schema + ```go // save Schema to local variable to avoid races schema := client.Schema @@ -143,8 +310,10 @@ func main() { ``` ## Custom (un)packing and typed selects and function calls -It's possible to specify custom pack/unpack functions for your types. -It will allow you to store complex structures inside a tuple and may speed up you requests. + +It's possible to specify custom pack/unpack functions for your types. It will +allow you to store complex structures inside a tuple and may speed up you requests. + ```go import ( "github.com/tarantool/go-tarantool" @@ -275,10 +444,13 @@ func main() { ``` - ## Options -* Timeout - timeout for any particular request. If Timeout is zero request any request may block infinitely -* Reconnect - timeout for between reconnect attempts. If Reconnect is zero, no reconnects will be performed -* MaxReconnects - maximal number of reconnect failures after that we give it up. If MaxReconnects is zero, client will try to reconnect endlessly -* User - user name to login tarantool -* Pass - user password to login tarantool + +* `Timeout` - timeout for any particular request. If `Timeout` is zero request, + any request may block infinitely. +* `Reconnect` - timeout between reconnect attempts. If `Reconnect` is zero, no + reconnects will be performed. +* `MaxReconnects` - maximal number of reconnect failures; after that we give it + up. If `MaxReconnects` is zero, the client will try to reconnect endlessly. +* `User` - user name to log into Tarantool. +* `Pass` - user password to log into Tarantool. From bf0b010d882b44ee051fb80ea3081df140d4739a Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 11 Feb 2017 21:02:00 +0300 Subject: [PATCH 157/605] workaround tarantool/tarantool#2060 fixes #32 --- schema.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/schema.go b/schema.go index 00596d12b..45c62b465 100644 --- a/schema.go +++ b/schema.go @@ -133,7 +133,11 @@ func (conn *Connection) loadSchema() (err error) { index.Unique = row[4].(uint64) > 0 case map[interface{}]interface{}: opts := row[4].(map[interface{}]interface{}) - index.Unique = opts["unique"].(bool) + var ok bool + if index.Unique, ok = opts["unique"].(bool); !ok { + /* see bug https://github.com/tarantool/tarantool/issues/2060 */ + index.Unique = true + } default: panic("unexpected schema format (index flags)") } From befab0d1f3d115776764698eec96c45f52129640 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 11 Feb 2017 21:08:28 +0300 Subject: [PATCH 158/605] notification about connection state change --- connection.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/connection.go b/connection.go index 08b892c84..da536892e 100644 --- a/connection.go +++ b/connection.go @@ -22,6 +22,26 @@ const ( connClosed = 2 ) +type ConnEventKind int + +const ( + // Connect signals that connection is established or reestablished + Connected ConnEventKind = iota + 1 + // Disconnect signals that connection is broken + Disconnected + // ReconnectFailed signals that attempt to reconnect has failed + ReconnectFailed + // Either reconnect attempts exhausted, or explicit Close is called + Closed +) + +// ConnEvent is sent throw Notify channel specified in Opts +type ConnEvent struct { + Conn *Connection + Kind ConnEventKind + When time.Time +} + var epoch = time.Now() // Connection is a handle to Tarantool. @@ -134,6 +154,11 @@ type Opts struct { // SkipSchema disables schema loading. Without disabling schema loading, // there is no way to create Connection for currently not accessible tarantool. SkipSchema bool + // Notify is a channel which receives notifications about Connection status + // changes. + Notify chan<- ConnEvent + // Handle is user specified value, that could be retrivied with Handle() method + Handle interface{} } // Connect creates and configures new Connection @@ -249,6 +274,11 @@ func (conn *Connection) LocalAddr() string { return conn.c.LocalAddr().String() } +// Handle returns user specified handle from Opts +func (conn *Connection) Handle() interface{} { + return conn.opts.Handle +} + func (conn *Connection) dial() (err error) { timeout := conn.opts.Reconnect / 2 if timeout == 0 { @@ -364,6 +394,7 @@ func (conn *Connection) createConnection(reconnect bool) (err error) { return } log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s\n", reconnects, conn.opts.MaxReconnects, conn.addr, err.Error()) + conn.notify(ReconnectFailed) reconnects++ conn.mutex.Unlock() time.Sleep(now.Add(conn.opts.Reconnect).Sub(time.Now())) @@ -382,9 +413,11 @@ func (conn *Connection) closeConnection(neterr error, forever bool) (err error) if conn.state != connClosed { close(conn.control) atomic.StoreUint32(&conn.state, connClosed) + conn.notify(Closed) } } else { atomic.StoreUint32(&conn.state, connDisconnected) + conn.notify(Disconnected) } if conn.c != nil { err = conn.c.Close() @@ -453,6 +486,15 @@ func (conn *Connection) pinger() { } } +func (conn *Connection) notify(kind ConnEventKind) { + if conn.opts.Notify != nil { + select { + case conn.opts.Notify <- ConnEvent{ Kind:kind, Conn: conn, When: time.Now() }: + default: + } + } +} + func (conn *Connection) writer(w *bufio.Writer, c *net.TCPConn) { var shardn uint32 var packet smallWBuf From 782226c6caadbe4caea07fb014ac60e87d74e17b Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 11 Feb 2017 21:09:25 +0300 Subject: [PATCH 159/605] ... go fmt --- connection.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/connection.go b/connection.go index da536892e..f1c2d84d5 100644 --- a/connection.go +++ b/connection.go @@ -18,8 +18,8 @@ import ( const requestsMap = 128 const ( connDisconnected = 0 - connConnected = 1 - connClosed = 2 + connConnected = 1 + connClosed = 2 ) type ConnEventKind int @@ -37,9 +37,9 @@ const ( // ConnEvent is sent throw Notify channel specified in Opts type ConnEvent struct { - Conn *Connection - Kind ConnEventKind - When time.Time + Conn *Connection + Kind ConnEventKind + When time.Time } var epoch = time.Now() @@ -472,7 +472,7 @@ func (conn *Connection) unlockShards() { func (conn *Connection) pinger() { to := conn.opts.Timeout if to == 0 { - to = 3*time.Second + to = 3 * time.Second } t := time.NewTicker(to / 3) defer t.Stop() @@ -489,7 +489,7 @@ func (conn *Connection) pinger() { func (conn *Connection) notify(kind ConnEventKind) { if conn.opts.Notify != nil { select { - case conn.opts.Notify <- ConnEvent{ Kind:kind, Conn: conn, When: time.Now() }: + case conn.opts.Notify <- ConnEvent{Kind: kind, Conn: conn, When: time.Now()}: default: } } From a49fe002504a1227438564f7c5049699a29c85f2 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sat, 11 Feb 2017 21:12:25 +0300 Subject: [PATCH 160/605] fix notify for Connected transition --- connection.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/connection.go b/connection.go index f1c2d84d5..ec561845e 100644 --- a/connection.go +++ b/connection.go @@ -385,6 +385,9 @@ func (conn *Connection) createConnection(reconnect bool) (err error) { now := time.Now() err = conn.dial() if err == nil || !reconnect { + if err == nil { + conn.notify(Connected) + } return } if conn.opts.MaxReconnects > 0 && reconnects > conn.opts.MaxReconnects { From 02adaa24cd947febdc0838e575f83e40dece8d56 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 22 Feb 2017 19:51:38 +0300 Subject: [PATCH 161/605] improve documentation for custom packers/unpackers --- README.md | 61 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 0ffa5d7d2..c3ccab064 100644 --- a/README.md +++ b/README.md @@ -326,19 +326,7 @@ type Member struct { Val uint } -type Tuple struct { - Cid uint - Orig string - Members []Member -} - -func init() { - msgpack.Register(reflect.TypeOf(Tuple{}), encodeTuple, decodeTuple) - msgpack.Register(reflect.TypeOf(Member{}), encodeMember, decodeMember) -} - -func encodeMember(e *msgpack.Encoder, v reflect.Value) error { - m := v.Interface().(Member) +func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeSliceLen(2); err != nil { return err } @@ -351,10 +339,9 @@ func encodeMember(e *msgpack.Encoder, v reflect.Value) error { return nil } -func decodeMember(d *msgpack.Decoder, v reflect.Value) error { +func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - m := v.Addr().Interface().(*Member) if l, err = d.DecodeSliceLen(); err != nil { return err } @@ -370,8 +357,13 @@ func decodeMember(d *msgpack.Decoder, v reflect.Value) error { return nil } -func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { - c := v.Interface().(Tuple) +type Tuple struct { + Cid uint + Orig string + Members []Member +} + +func (c *Tuple) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeSliceLen(3); err != nil { return err } @@ -390,10 +382,9 @@ func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { return nil } -func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { +func (c *Tuple) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - c := v.Addr().Interface().(*Tuple) if l, err = d.DecodeSliceLen(); err != nil { return err } @@ -442,6 +433,38 @@ func main() { } } +/* +// Old way to register types +func init() { + msgpack.Register(reflect.TypeOf(Tuple{}), encodeTuple, decodeTuple) + msgpack.Register(reflect.TypeOf(Member{}), encodeMember, decodeMember) +} + +func encodeMember(e *msgpack.Encoder, v reflect.Value) error { + m := v.Interface().(Member) + // same code as in EncodeMsgpack + return nil +} + +func decodeMember(d *msgpack.Decoder, v reflect.Value) error { + m := v.Addr().Interface().(*Member) + // same code as in DecodeMsgpack + return nil +} + +func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { + c := v.Interface().(Tuple) + // same code as in EncodeMsgpack + return nil +} + +func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { + c := v.Addr().Interface().(*Tuple) + // same code as in DecodeMsgpack + return nil +} +*/ + ``` ## Options From 28856a1a2c394d849dba138508ded5c1d3530a29 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Fri, 24 Feb 2017 20:31:47 +0300 Subject: [PATCH 162/605] note about magic way to pack tuples --- README.md | 36 ++++++++++++++++++++++++++++-------- example_test.go | 25 +++++++++++++++++-------- tarantool_test.go | 41 +++-------------------------------------- 3 files changed, 48 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index c3ccab064..5e5b6f87d 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,9 @@ func main() { It's possible to specify custom pack/unpack functions for your types. It will allow you to store complex structures inside a tuple and may speed up you requests. +Also you can just instruct msgpack library to encode your struct as an array. It +will be slower than custom packer/unpacker, but still safe. + ```go import ( "github.com/tarantool/go-tarantool" @@ -326,6 +329,21 @@ type Member struct { Val uint } +type Tuple struct { + Cid uint + Orig string + Members []Member +} + +/* same effect in magic way and slower */ +type Tuple2 struct { + _msgpack struct{} `msgpack:",asArray"` + + Cid uint + Orig string + Members []Member +} + func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeSliceLen(2); err != nil { return err @@ -357,12 +375,6 @@ func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { return nil } -type Tuple struct { - Cid uint - Orig string - Members []Member -} - func (c *Tuple) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeSliceLen(3); err != nil { return err @@ -424,9 +436,17 @@ func main() { return } + // same result in magic way. + var tuples2 []Tuple2 + err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{777}, &tuples2) + if err != nil { + t.Errorf("Failed to SelectTyped: %s", err.Error()) + return + } + // call function 'func_name' returning a table of custom tuples - var tuples2 []Tuple - err = client.CallTyped("func_name", []interface{}{1, 2, 3}, &tuples) + var tuples3 []Tuple + err = client.CallTyped("func_name", []interface{}{1, 2, 3}, &tuples3) if err != nil { t.Errorf("Failed to CallTyped: %s", err.Error()) return diff --git a/example_test.go b/example_test.go index 766560db8..c7b9e3a29 100644 --- a/example_test.go +++ b/example_test.go @@ -6,6 +6,15 @@ import ( "time" ) +type Tuple struct { + /* instruct msgpack to pack this struct as array, + * so no custom packer is needed */ + _msgpack struct{} `msgpack:",asArray"` + Id uint + Msg string + Name string +} + func example_connect() (*tarantool.Connection, error) { conn, err := tarantool.Connect(server, opts) if err != nil { @@ -63,16 +72,16 @@ func ExampleConnection_SelectTyped() { fmt.Printf("error in select is %v", err) return } - fmt.Printf("response is %#v\n", res) + fmt.Printf("response is %v\n", res) err = conn.SelectTyped("test", "primary", 0, 100, tarantool.IterEq, tarantool.IntKey{1111}, &res) if err != nil { fmt.Printf("error in select is %v", err) return } - fmt.Printf("response is %#v\n", res) + fmt.Printf("response is %v\n", res) // Output: - // response is []tarantool_test.Tuple{tarantool_test.Tuple{Id:0x457, Msg:"hello", Name:"world"}} - // response is []tarantool_test.Tuple{tarantool_test.Tuple{Id:0x457, Msg:"hello", Name:"world"}} + // response is [{{} 1111 hello world}] + // response is [{{} 1111 hello world}] } func Example() { @@ -109,7 +118,7 @@ func Example() { fmt.Println("Insert Data", resp.Data) // insert new tuple { 11, 1 } - resp, err = client.Insert("test", &Tuple{10, "test", "one"}) + resp, err = client.Insert("test", &Tuple{Id:10, Msg:"test", Name:"one"}) fmt.Println("Insert Error", err) fmt.Println("Insert Code", resp.Code) fmt.Println("Insert Data", resp.Data) @@ -158,8 +167,8 @@ func Example() { fmt.Println("Eval Code", resp.Code) fmt.Println("Eval Data", resp.Data) - resp, err = client.Replace("test", &Tuple{11, "test", "eleven"}) - resp, err = client.Replace("test", &Tuple{12, "test", "twelve"}) + resp, err = client.Replace("test", &Tuple{Id:11, Msg:"test", Name:"eleven"}) + resp, err = client.Replace("test", &Tuple{Id:12, Msg:"test", Name:"twelve"}) var futs [3]*tarantool.Future futs[0] = client.SelectAsync("test", "primary", 0, 2, tarantool.IterLe, tarantool.UintKey{12}) @@ -206,7 +215,7 @@ func Example() { // Eval Code 0 // Eval Data [3] // Fut 0 Error - // Fut 0 Data [{12 test twelve} {11 test eleven}] + // Fut 0 Data [{{} 12 test twelve} {{} 11 test eleven}] // Fut 1 Error // Fut 1 Data [[13 4]] // Fut 2 Error diff --git a/tarantool_test.go b/tarantool_test.go index 286e293f3..bbf6220a7 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -10,12 +10,6 @@ import ( "time" ) -type Tuple struct { - Id uint - Msg string - Name string -} - type Member struct { Name string Nonce string @@ -28,35 +22,6 @@ type Tuple2 struct { Members []Member } -func (t *Tuple) EncodeMsgpack(e *msgpack.Encoder) error { - e.EncodeSliceLen(3) - e.EncodeUint(t.Id) - e.EncodeString(t.Msg) - e.EncodeString(t.Name) - return nil -} - -func (t *Tuple) DecodeMsgpack(d *msgpack.Decoder) error { - var err error - var l int - if l, err = d.DecodeSliceLen(); err != nil { - return err - } - if l != 3 { - return fmt.Errorf("array len doesn't match: %d", l) - } - if t.Id, err = d.DecodeUint(); err != nil { - return err - } - if t.Msg, err = d.DecodeString(); err != nil { - return err - } - if t.Name, err = d.DecodeString(); err != nil { - return err - } - return nil -} - func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { e.EncodeSliceLen(2) e.EncodeString(m.Name) @@ -468,7 +433,7 @@ func TestClient(t *testing.T) { } } //resp, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) - resp, err = conn.Insert(spaceNo, &Tuple{1, "hello", "world"}) + resp, err = conn.Insert(spaceNo, &Tuple{Id:1, Msg:"hello", Name:"world"}) if tntErr, ok := err.(Error); !ok || tntErr.Code != ErrTupleFound { t.Errorf("Expected ErrTupleFound but got: %v", err) } @@ -995,14 +960,14 @@ func TestComplexStructs(t *testing.T) { } defer conn.Close() - tuple := Tuple2{777, "orig", []Member{{"lol", "", 1}, {"wut", "", 3}}} + tuple := Tuple2{Cid:777, Orig:"orig", Members:[]Member{{"lol", "", 1}, {"wut", "", 3}}} _, err = conn.Replace(spaceNo, &tuple) if err != nil { t.Errorf("Failed to insert: %s", err.Error()) return } - var tuples []Tuple2 + var tuples [1]Tuple2 err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{777}, &tuples) if err != nil { t.Errorf("Failed to selectTyped: %s", err.Error()) From f7718cb79ee15d81bf88a303890f8e8784c8ff23 Mon Sep 17 00:00:00 2001 From: lenkis Date: Mon, 27 Feb 2017 11:46:48 +0300 Subject: [PATCH 163/605] Rephrase "magic" annotation in readme --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5e5b6f87d..51967e910 100644 --- a/README.md +++ b/README.md @@ -311,11 +311,12 @@ func main() { ## Custom (un)packing and typed selects and function calls -It's possible to specify custom pack/unpack functions for your types. It will -allow you to store complex structures inside a tuple and may speed up you requests. +You can specify custom pack/unpack functions for your types. This will allow you +to store complex structures inside a tuple and may speed up you requests. -Also you can just instruct msgpack library to encode your struct as an array. It -will be slower than custom packer/unpacker, but still safe. +Alternatively, you can just instruct the `msgpack` library to encode your +structure as an array. This is safe "magic". It will be easier to implement than +a custom packer/unpacker, but it will work slower. ```go import ( @@ -335,7 +336,7 @@ type Tuple struct { Members []Member } -/* same effect in magic way and slower */ +/* same effect in a "magic" way, but slower */ type Tuple2 struct { _msgpack struct{} `msgpack:",asArray"` @@ -436,7 +437,7 @@ func main() { return } - // same result in magic way. + // same result in a "magic" way var tuples2 []Tuple2 err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{777}, &tuples2) if err != nil { From df0b2281613e49bc09b5ff4263f3ca8423b1a7d0 Mon Sep 17 00:00:00 2001 From: Alex Geer Date: Fri, 10 Mar 2017 07:22:49 +0300 Subject: [PATCH 164/605] Added the ability to work with the tarantool via unix socket --- connection.go | 38 +++++++++++++++++++++++--------------- deadline_io.go | 2 +- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/connection.go b/connection.go index ec561845e..c51b92e91 100644 --- a/connection.go +++ b/connection.go @@ -10,6 +10,7 @@ import ( "log" "net" "runtime" + "strings" "sync" "sync/atomic" "time" @@ -78,7 +79,7 @@ var epoch = time.Now() // array of arrays. type Connection struct { addr string - c *net.TCPConn + c net.Conn mutex sync.Mutex // Schema contains schema loaded on connection. Schema *Schema @@ -280,25 +281,32 @@ func (conn *Connection) Handle() interface{} { } func (conn *Connection) dial() (err error) { + const unixSocketKey = `unix/:` + var connection net.Conn + network := `tcp` + address := conn.addr timeout := conn.opts.Reconnect / 2 if timeout == 0 { timeout = 500 * time.Millisecond } else if timeout > 5*time.Second { timeout = 5 * time.Second } - connection, err := net.DialTimeout("tcp", conn.addr, timeout) + // Select tcp/ip or unix socket + if npi := len(unixSocketKey); len(address) >= npi && strings.ToLower(address[0:npi]) == unixSocketKey { + network = `unix` + address = address[npi:] + } + connection, err = net.DialTimeout(network, address, timeout) if err != nil { return } - c := connection.(*net.TCPConn) - c.SetNoDelay(true) - dc := &DeadlineIO{to: conn.opts.Timeout, c: c} + dc := &DeadlineIO{to: conn.opts.Timeout, c: connection} r := bufio.NewReaderSize(dc, 128*1024) w := bufio.NewWriterSize(dc, 128*1024) greeting := make([]byte, 128) _, err = io.ReadFull(r, greeting) if err != nil { - c.Close() + connection.Close() return } conn.Greeting.Version = bytes.NewBuffer(greeting[:64]).String() @@ -309,26 +317,26 @@ func (conn *Connection) dial() (err error) { scr, err := scramble(conn.Greeting.auth, conn.opts.Pass) if err != nil { err = errors.New("auth: scrambling failure " + err.Error()) - c.Close() + connection.Close() return err } if err = conn.writeAuthRequest(w, scr); err != nil { - c.Close() + connection.Close() return err } if err = conn.readAuthResponse(r); err != nil { - c.Close() + connection.Close() return err } } // Only if connected and authenticated conn.lockShards() - conn.c = c + conn.c = connection atomic.StoreUint32(&conn.state, connConnected) conn.unlockShards() - go conn.writer(w, c) - go conn.reader(r, c) + go conn.writer(w, connection) + go conn.reader(r, connection) return } @@ -443,7 +451,7 @@ func (conn *Connection) closeConnection(neterr error, forever bool) (err error) return } -func (conn *Connection) reconnect(neterr error, c *net.TCPConn) { +func (conn *Connection) reconnect(neterr error, c net.Conn) { conn.mutex.Lock() defer conn.mutex.Unlock() if conn.opts.Reconnect > 0 { @@ -498,7 +506,7 @@ func (conn *Connection) notify(kind ConnEventKind) { } } -func (conn *Connection) writer(w *bufio.Writer, c *net.TCPConn) { +func (conn *Connection) writer(w *bufio.Writer, c net.Conn) { var shardn uint32 var packet smallWBuf for atomic.LoadUint32(&conn.state) != connClosed { @@ -538,7 +546,7 @@ func (conn *Connection) writer(w *bufio.Writer, c *net.TCPConn) { } } -func (conn *Connection) reader(r *bufio.Reader, c *net.TCPConn) { +func (conn *Connection) reader(r *bufio.Reader, c net.Conn) { for atomic.LoadUint32(&conn.state) != connClosed { respBytes, err := conn.read(r) if err != nil { diff --git a/deadline_io.go b/deadline_io.go index 65fe4cd9f..3bda73ac8 100644 --- a/deadline_io.go +++ b/deadline_io.go @@ -7,7 +7,7 @@ import ( type DeadlineIO struct { to time.Duration - c *net.TCPConn + c net.Conn } func (d *DeadlineIO) Write(b []byte) (n int, err error) { From 13d6687484257050dacac3ac6f3a0f72e2e6f823 Mon Sep 17 00:00:00 2001 From: Alex Geer Date: Fri, 10 Mar 2017 14:07:36 +0300 Subject: [PATCH 165/605] Add check for prefix "tcp:" --- connection.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/connection.go b/connection.go index c51b92e91..5569f5e4d 100644 --- a/connection.go +++ b/connection.go @@ -282,7 +282,9 @@ func (conn *Connection) Handle() interface{} { func (conn *Connection) dial() (err error) { const unixSocketKey = `unix/:` + const tcpIpKey = `tcp:` var connection net.Conn + var i int network := `tcp` address := conn.addr timeout := conn.opts.Reconnect / 2 @@ -291,10 +293,12 @@ func (conn *Connection) dial() (err error) { } else if timeout > 5*time.Second { timeout = 5 * time.Second } - // Select tcp/ip or unix socket - if npi := len(unixSocketKey); len(address) >= npi && strings.ToLower(address[0:npi]) == unixSocketKey { + // Unix socket connection + if i = len(unixSocketKey); len(address) >= i && strings.ToLower(address[0:i]) == unixSocketKey { network = `unix` - address = address[npi:] + address = address[i:] + } else if i = len(tcpIpKey); len(address) >= i && strings.ToLower(address[0:i]) == tcpIpKey { + address = address[i:] } connection, err = net.DialTimeout(network, address, timeout) if err != nil { From 859ea9aa1f176d89024d8aba27e76dee175d0765 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 12 Mar 2017 01:57:39 +0300 Subject: [PATCH 166/605] extend ways to specify unix socket connection (and tcp) --- README.md | 1 + connection.go | 40 ++++++++++++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 51967e910..4ac5e9d95 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ import ( func main() { opts := tarantool.Opts{User: "guest"} conn, err := tarantool.Connect("127.0.0.1:3301", opts) + // conn, err := tarantool.Connect("/path/to/tarantool.socket", opts) if err != nil { fmt.Println("Connection refused: %s", err.Error()) } diff --git a/connection.go b/connection.go index 5569f5e4d..fa16c11f2 100644 --- a/connection.go +++ b/connection.go @@ -164,6 +164,22 @@ type Opts struct { // Connect creates and configures new Connection // +// Address could be specified in following ways: +// +// TCP connections: +// - tcp://192.168.1.1:3013 +// - tcp://my.host:3013 +// - tcp:192.168.1.1:3013 +// - tcp:my.host:3013 +// - 192.168.1.1:3013 +// - my.host:3013 +// Unix socket: +// - unix:///abs/path/tnt.sock +// - unix:path/tnt.sock +// - /abs/path/tnt.sock - first '/' indicates unix socket +// - ./rel/path/tnt.sock - first '.' indicates unix socket +// - unix/:path/tnt.sock - 'unix/' acts as a "host" and "/path..." as a port +// // Note: // // - If opts.Reconnect is zero (default), then connection either already connected @@ -281,11 +297,9 @@ func (conn *Connection) Handle() interface{} { } func (conn *Connection) dial() (err error) { - const unixSocketKey = `unix/:` - const tcpIpKey = `tcp:` var connection net.Conn var i int - network := `tcp` + network := "tcp" address := conn.addr timeout := conn.opts.Reconnect / 2 if timeout == 0 { @@ -294,11 +308,21 @@ func (conn *Connection) dial() (err error) { timeout = 5 * time.Second } // Unix socket connection - if i = len(unixSocketKey); len(address) >= i && strings.ToLower(address[0:i]) == unixSocketKey { - network = `unix` - address = address[i:] - } else if i = len(tcpIpKey); len(address) >= i && strings.ToLower(address[0:i]) == tcpIpKey { - address = address[i:] + if address[0] == '.' || address [0] == '/' { + network = "unix" + } else if address[0:7] == "unix://" { + network = "unix" + address = address[7:] + } else if address[0:5] == "unix:" { + network = "unix" + address = address[5:] + } else if address[0:6] == "unix/:" { + network = "unix" + address = address[6:] + } else if address[0:6] == "tcp://" { + address = address[6:] + } else if address[0:4] == "tcp:" { + address = address[4:] } connection, err = net.DialTimeout(network, address, timeout) if err != nil { From f76eefec18ede5a05c38f85e748046f96bb0c840 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 12 Mar 2017 02:45:40 +0300 Subject: [PATCH 167/605] ... fix --- connection.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/connection.go b/connection.go index fa16c11f2..4d5235279 100644 --- a/connection.go +++ b/connection.go @@ -10,7 +10,6 @@ import ( "log" "net" "runtime" - "strings" "sync" "sync/atomic" "time" @@ -298,7 +297,6 @@ func (conn *Connection) Handle() interface{} { func (conn *Connection) dial() (err error) { var connection net.Conn - var i int network := "tcp" address := conn.addr timeout := conn.opts.Reconnect / 2 From 06e251c072fd9569284f7d47d0393ccc92534022 Mon Sep 17 00:00:00 2001 From: Nikita Galushko Date: Mon, 27 Mar 2017 17:14:43 +0300 Subject: [PATCH 168/605] init --- const.go | 18 +++++ queue.go | 218 +++++++++++++++++++++++++++++++++++++++++++++++++++++ request.go | 4 + tube.go | 41 ++++++++++ 4 files changed, 281 insertions(+) create mode 100644 queue.go create mode 100644 tube.go diff --git a/const.go b/const.go index 50972ed43..a259a2ca0 100644 --- a/const.go +++ b/const.go @@ -50,3 +50,21 @@ const ( ErrorCodeBit = 0x8000 PacketLengthBytes = 5 ) + + +const ( + READY = "r" + TAKEN = "t" + DONE = "-" + BURIED = "!" + DELAYED = "~" +) + +type queueType string + +const ( + FIFO_QUEUE queueType = "fifo" + FIFO_TTL_QUEUE queueType = "fifottl" + UTUBE_QUEUE queueType = "utube" + UTUBE_TTL_QUEUE queueType = "utubettl" +) \ No newline at end of file diff --git a/queue.go b/queue.go new file mode 100644 index 000000000..49b4504a3 --- /dev/null +++ b/queue.go @@ -0,0 +1,218 @@ +package tarantool + +import ( + "fmt" + "time" + "strings" +) + +type queue struct { + name string + conn *Connection + cmd map[string]string +} + +type queueCfg interface { + String() string + Type() string +} + +type QueueCfg struct { + Temporary bool + IfNotExists bool + Kind queueType +} + +func (cfg QueueCfg) String() string { + return fmt.Sprintf("{ temporary = %v, if_not_exists = %v }", cfg.Temporary, cfg.IfNotExists) +} + +func (cfg QueueCfg) Type() string { + return string(cfg.Kind) +} + +type TtlQueueCfg struct { + QueueCfg + QueueOpts +} + +type QueueOpts struct { + Pri int + Ttl time.Duration + Ttr time.Duration + Delay time.Duration +} + +func (cfg TtlQueueCfg) String() string { + params := []string{fmt.Sprintf("temporary = %v, if_not_exists = %v", cfg.Temporary, cfg.IfNotExists)} + + if cfg.Ttl.Seconds() != 0 { + params = append(params, fmt.Sprintf("ttl = %f", cfg.Ttl.Seconds())) + } + + if cfg.Ttr.Seconds() != 0 { + params = append(params, fmt.Sprintf("ttr = %f", cfg.Ttr.Seconds())) + } + + if cfg.Delay.Seconds() != 0 { + params = append(params, fmt.Sprintf("delay = %f", cfg.Delay.Seconds())) + } + + return "{" + strings.Join(params, ",") + "}" +} + +func (cfg TtlQueueCfg) Type() string { + kind := string(cfg.Kind) + if kind == "" { + kind = string(FIFO_QUEUE) + } + + return kind +} + +func (opts QueueOpts) toMap() map[string]interface{} { + ret := make(map[string]interface{}) + + if opts.Ttl.Seconds() != 0 { + ret["ttl"] = opts.Ttl.Seconds() + } + + if opts.Ttr.Seconds() != 0 { + ret["ttr"] = opts.Ttr.Seconds() + } + + if opts.Delay.Seconds() != 0 { + ret["delay"] = opts.Delay.Seconds() + } + + return ret +} + +func newQueue(conn *Connection, name string, cfg queueCfg) (queue, error) { + cmd := fmt.Sprintf("queue.create_tube('%s', '%s', %s)", name, cfg.Type(), cfg.String()) + fmt.Println("STAETL: ", cmd) + fut := conn.EvalAsync(cmd, []interface{}{}) + fut.wait() + return queue{ + name, + conn, + makeCmdMap(name), + }, fut.err +} + +func (q queue) Put(data interface{}) (uint64, error) { + resp, err := q.conn.Call(q.cmd["put"], []interface{}{data}) + return convertRsponseToTubeData(resp.Data).id, err +} + +func (q queue) PutWithConfig(data interface{}, cfg QueueOpts) (uint64, error) { + resp, err := q.conn.Call(q.cmd["put"], []interface{}{data, cfg.toMap()}) + return convertRsponseToTubeData(resp.Data).id, err +} + +func (q queue) Take() (TubeData, error) { + return q.take(nil) +} + +func (q queue) TakeWithTimeout(timeout time.Duration) (TubeData, error) { + return q.take(timeout.Seconds()) +} + +func (q queue) take(params interface{}) (TubeData, error) { + var t TubeData + resp, err := q.conn.Call(q.cmd["take"], []interface{}{params}) + if err == nil && len(resp.Data) != 0 { + data, ok := resp.Data[0].([]interface{}) + if ok && len(data) >= 3 { + t = TubeData{ + data[0].(uint64), + data[1].(string), + data[2], + } + } + } + return t, err +} + +func (q queue) Drop() error { + _, err := q.conn.Call(q.cmd["drop"], []interface{}{}) + return err +} + +func (q queue) Peek(taskId uint64) (TubeData, error) { + resp, err := q.conn.Call(q.cmd["peek"], []interface{}{taskId}) + if err != nil { + return DEFAULT_TUBE_DATA, err + } + + return convertRsponseToTubeData(resp.Data), nil +} + +func (q queue) Ack(taskId uint64) error { + _, err := q.conn.Call(q.cmd["ack"], []interface{}{taskId}) + return err +} + +func (q queue) Delete(taskId uint64) error { + _, err := q.conn.Call(q.cmd["delete"], []interface{}{taskId}) + return err +} + +func (q queue) Bury(taskId uint64) error { + _, err := q.conn.Call(q.cmd["bury"], []interface{}{taskId}) + return err +} + +func (q queue) Kick(taskId uint64) (uint64, error) { + resp, err := q.conn.Call(q.cmd["kick"], []interface{}{taskId}) + var id uint64 + if err == nil { + id = resp.Data[0].([]interface{})[0].(uint64) + } + return id, err +} + +func (q queue) Statistic() (interface{}, error) { + resp, err := q.conn.Call(q.cmd["statistics"], []interface{}{}) + if err != nil { + return nil, err + } + + if len(resp.Data) != 0 { + data, ok := resp.Data[0].([]interface{}) + if ok && len(data) != 0 { + return data[0], nil + } + } + + return nil, nil +} + + +func makeCmdMap(name string) map[string]string { + return map[string]string{ + "put": "queue.tube." + name + ":put", + "take": "queue.tube." + name + ":take", + "drop": "queue.tube." + name + ":drop", + "peek": "queue.tube." + name + ":peek", + "ack": "queue.tube." + name + ":ack", + "delete": "queue.tube." + name + ":delete", + "bury": "queue.tube." + name + ":bury", + "kick": "queue.tube." + name + ":kick", + "statistics": "queue.statistics", + } +} + +func convertRsponseToTubeData(responseData []interface{}) TubeData { + t := DEFAULT_TUBE_DATA + if len(responseData) != 0 { + data, ok := responseData[0].([]interface{}) + if ok && len(data) >= 3 { + t.id = data[0].(uint64) + t.status = data[1].(string) + t.data = data[2] + } + } + + return t +} diff --git a/request.go b/request.go index 9df52ea35..51616a32f 100644 --- a/request.go +++ b/request.go @@ -313,6 +313,10 @@ func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { }) } +func (conn *Connection) NewQueue(name string, cfg queueCfg) (queue, error) { + return newQueue(conn, name, cfg) +} + // // private // diff --git a/tube.go b/tube.go new file mode 100644 index 000000000..a8ba530b6 --- /dev/null +++ b/tube.go @@ -0,0 +1,41 @@ +package tarantool + +type TubeData struct { + id uint64 + status string + data interface{} +} + +var DEFAULT_TUBE_DATA = TubeData{0, "", nil} + +func (t TubeData) GetId() uint64 { + return t.id +} + +func (t TubeData) GetData() interface{} { + return t.data +} + +func (t TubeData) GetStatus() string { + return t.status +} + +func (t TubeData) isReady() bool { + return t.status == READY +} + +func (t TubeData) isTaken() bool { + return t.status == TAKEN +} + +func (t TubeData) isDone() bool { + return t.status == DONE +} + +func (t TubeData) isBuried() bool { + return t.status == BURIED +} + +func (t TubeData) isDelayed() bool { + return t.status == DELAYED +} \ No newline at end of file From ae3cd41e63d1b3cae291734f1fd2d58e5be9cbc2 Mon Sep 17 00:00:00 2001 From: Nikita Galushko Date: Tue, 28 Mar 2017 10:41:58 +0300 Subject: [PATCH 169/605] implemented the "GetQueue" --- queue.go | 87 ++++++++++++++++++++++++++++++++++++------------------ request.go | 4 +++ task.go | 41 +++++++++++++++++++++++++ tube.go | 41 ------------------------- 4 files changed, 104 insertions(+), 69 deletions(-) create mode 100644 task.go delete mode 100644 tube.go diff --git a/queue.go b/queue.go index 49b4504a3..625d38477 100644 --- a/queue.go +++ b/queue.go @@ -89,47 +89,78 @@ func (opts QueueOpts) toMap() map[string]interface{} { } func newQueue(conn *Connection, name string, cfg queueCfg) (queue, error) { + var q queue cmd := fmt.Sprintf("queue.create_tube('%s', '%s', %s)", name, cfg.Type(), cfg.String()) - fmt.Println("STAETL: ", cmd) fut := conn.EvalAsync(cmd, []interface{}{}) fut.wait() - return queue{ - name, - conn, - makeCmdMap(name), - }, fut.err + if fut.err == nil { + q = queue{ + name, + conn, + makeCmdMap(name), + } + } + return q, fut.err +} + +func getQueue(conn *Connection, name string) (queue, error) { + var q queue + cmd := fmt.Sprintf("return queue.tube.%s ~= null", name) + resp, err := conn.Eval(cmd, []interface{}{}) + if err != nil { + return q, err + } + + exist := len(resp.Data) != 0 && resp.Data[0].(bool) + + if exist { + q = queue{ + name, + conn, + makeCmdMap(name), + } + } else { + err = fmt.Errorf("Tube %s does not exist", name) + } + + return q, err } func (q queue) Put(data interface{}) (uint64, error) { - resp, err := q.conn.Call(q.cmd["put"], []interface{}{data}) - return convertRsponseToTubeData(resp.Data).id, err + return q.put(data) } func (q queue) PutWithConfig(data interface{}, cfg QueueOpts) (uint64, error) { - resp, err := q.conn.Call(q.cmd["put"], []interface{}{data, cfg.toMap()}) - return convertRsponseToTubeData(resp.Data).id, err + return q.put(data, cfg.toMap()) } -func (q queue) Take() (TubeData, error) { +func (q queue) put(p... interface{}) (uint64, error) { + var ( + params []interface{} + id uint64 + ) + params = append(params, p...) + resp, err := q.conn.Call(q.cmd["put"], params) + if err != nil { + id = convertRsponseToTask(resp.Data).id + } + + return id, err +} + +func (q queue) Take() (Task, error) { return q.take(nil) } -func (q queue) TakeWithTimeout(timeout time.Duration) (TubeData, error) { +func (q queue) TakeWithTimeout(timeout time.Duration) (Task, error) { return q.take(timeout.Seconds()) } -func (q queue) take(params interface{}) (TubeData, error) { - var t TubeData +func (q queue) take(params interface{}) (Task, error) { + var t Task resp, err := q.conn.Call(q.cmd["take"], []interface{}{params}) - if err == nil && len(resp.Data) != 0 { - data, ok := resp.Data[0].([]interface{}) - if ok && len(data) >= 3 { - t = TubeData{ - data[0].(uint64), - data[1].(string), - data[2], - } - } + if err == nil { + t = convertRsponseToTask(resp.Data) } return t, err } @@ -139,13 +170,13 @@ func (q queue) Drop() error { return err } -func (q queue) Peek(taskId uint64) (TubeData, error) { +func (q queue) Peek(taskId uint64) (Task, error) { resp, err := q.conn.Call(q.cmd["peek"], []interface{}{taskId}) if err != nil { - return DEFAULT_TUBE_DATA, err + return DEFAULT_TASK, err } - return convertRsponseToTubeData(resp.Data), nil + return convertRsponseToTask(resp.Data), nil } func (q queue) Ack(taskId uint64) error { @@ -203,8 +234,8 @@ func makeCmdMap(name string) map[string]string { } } -func convertRsponseToTubeData(responseData []interface{}) TubeData { - t := DEFAULT_TUBE_DATA +func convertRsponseToTask(responseData []interface{}) Task { + t := DEFAULT_TASK if len(responseData) != 0 { data, ok := responseData[0].([]interface{}) if ok && len(data) >= 3 { diff --git a/request.go b/request.go index 51616a32f..2fc31514a 100644 --- a/request.go +++ b/request.go @@ -317,6 +317,10 @@ func (conn *Connection) NewQueue(name string, cfg queueCfg) (queue, error) { return newQueue(conn, name, cfg) } +func (conn *Connection) GetQueue(name string) (queue, error) { + return getQueue(conn, name) +} + // // private // diff --git a/task.go b/task.go new file mode 100644 index 000000000..12e847895 --- /dev/null +++ b/task.go @@ -0,0 +1,41 @@ +package tarantool + +type Task struct { + id uint64 + status string + data interface{} +} + +var DEFAULT_TASK = Task{} + +func (t Task) GetId() uint64 { + return t.id +} + +func (t Task) GetData() interface{} { + return t.data +} + +func (t Task) GetStatus() string { + return t.status +} + +func (t Task) IsReady() bool { + return t.status == READY +} + +func (t Task) IsTaken() bool { + return t.status == TAKEN +} + +func (t Task) IsDone() bool { + return t.status == DONE +} + +func (t Task) IsBuried() bool { + return t.status == BURIED +} + +func (t Task) IsDelayed() bool { + return t.status == DELAYED +} \ No newline at end of file diff --git a/tube.go b/tube.go deleted file mode 100644 index a8ba530b6..000000000 --- a/tube.go +++ /dev/null @@ -1,41 +0,0 @@ -package tarantool - -type TubeData struct { - id uint64 - status string - data interface{} -} - -var DEFAULT_TUBE_DATA = TubeData{0, "", nil} - -func (t TubeData) GetId() uint64 { - return t.id -} - -func (t TubeData) GetData() interface{} { - return t.data -} - -func (t TubeData) GetStatus() string { - return t.status -} - -func (t TubeData) isReady() bool { - return t.status == READY -} - -func (t TubeData) isTaken() bool { - return t.status == TAKEN -} - -func (t TubeData) isDone() bool { - return t.status == DONE -} - -func (t TubeData) isBuried() bool { - return t.status == BURIED -} - -func (t TubeData) isDelayed() bool { - return t.status == DELAYED -} \ No newline at end of file From 3b2bc80652231d5692a3abf5e6d41a931fda1910 Mon Sep 17 00:00:00 2001 From: Nikita Galushko Date: Wed, 29 Mar 2017 09:45:16 +0300 Subject: [PATCH 170/605] improvement the task --- queue.go | 65 ++++++++++++++++++++++++++++++++------------------------ task.go | 37 ++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/queue.go b/queue.go index 625d38477..6a12f38ea 100644 --- a/queue.go +++ b/queue.go @@ -2,8 +2,8 @@ package tarantool import ( "fmt" - "time" "strings" + "time" ) type queue struct { @@ -18,9 +18,9 @@ type queueCfg interface { } type QueueCfg struct { - Temporary bool + Temporary bool IfNotExists bool - Kind queueType + Kind queueType } func (cfg QueueCfg) String() string { @@ -37,9 +37,9 @@ type TtlQueueCfg struct { } type QueueOpts struct { - Pri int - Ttl time.Duration - Ttr time.Duration + Pri int + Ttl time.Duration + Ttr time.Duration Delay time.Duration } @@ -85,6 +85,8 @@ func (opts QueueOpts) toMap() map[string]interface{} { ret["delay"] = opts.Delay.Seconds() } + ret["pri"] = opts.Pri + return ret } @@ -134,33 +136,37 @@ func (q queue) PutWithConfig(data interface{}, cfg QueueOpts) (uint64, error) { return q.put(data, cfg.toMap()) } -func (q queue) put(p... interface{}) (uint64, error) { +func (q queue) put(p ...interface{}) (uint64, error) { var ( params []interface{} - id uint64 + id uint64 ) params = append(params, p...) resp, err := q.conn.Call(q.cmd["put"], params) - if err != nil { - id = convertRsponseToTask(resp.Data).id + if err == nil { + var task *Task + task, err = toTask(resp.Data, &q) + if err == nil { + id = task.id + } } return id, err } -func (q queue) Take() (Task, error) { +func (q queue) Take() (*Task, error) { return q.take(nil) } -func (q queue) TakeWithTimeout(timeout time.Duration) (Task, error) { +func (q queue) TakeWithTimeout(timeout time.Duration) (*Task, error) { return q.take(timeout.Seconds()) } -func (q queue) take(params interface{}) (Task, error) { - var t Task +func (q queue) take(params interface{}) (*Task, error) { + var t *Task resp, err := q.conn.Call(q.cmd["take"], []interface{}{params}) if err == nil { - t = convertRsponseToTask(resp.Data) + t, err = toTask(resp.Data, &q) } return t, err } @@ -170,26 +176,28 @@ func (q queue) Drop() error { return err } -func (q queue) Peek(taskId uint64) (Task, error) { +func (q queue) Peek(taskId uint64) (*Task, error) { resp, err := q.conn.Call(q.cmd["peek"], []interface{}{taskId}) if err != nil { - return DEFAULT_TASK, err + return nil, err } - return convertRsponseToTask(resp.Data), nil + t, err := toTask(resp.Data, &q) + + return t, err } -func (q queue) Ack(taskId uint64) error { +func (q queue) _ack(taskId uint64) error { _, err := q.conn.Call(q.cmd["ack"], []interface{}{taskId}) return err } -func (q queue) Delete(taskId uint64) error { +func (q queue) _delete(taskId uint64) error { _, err := q.conn.Call(q.cmd["delete"], []interface{}{taskId}) return err } -func (q queue) Bury(taskId uint64) error { +func (q queue) _bury(taskId uint64) error { _, err := q.conn.Call(q.cmd["bury"], []interface{}{taskId}) return err } @@ -219,7 +227,6 @@ func (q queue) Statistic() (interface{}, error) { return nil, nil } - func makeCmdMap(name string) map[string]string { return map[string]string{ "put": "queue.tube." + name + ":put", @@ -234,16 +241,18 @@ func makeCmdMap(name string) map[string]string { } } -func convertRsponseToTask(responseData []interface{}) Task { - t := DEFAULT_TASK +func toTask(responseData []interface{}, q *queue) (*Task, error) { if len(responseData) != 0 { data, ok := responseData[0].([]interface{}) if ok && len(data) >= 3 { - t.id = data[0].(uint64) - t.status = data[1].(string) - t.data = data[2] + return &Task{ + data[0].(uint64), + data[1].(string), + data[2], + q, + }, nil } } - return t + return nil, nil } diff --git a/task.go b/task.go index 12e847895..a9c1d3c1d 100644 --- a/task.go +++ b/task.go @@ -1,41 +1,52 @@ package tarantool type Task struct { - id uint64 + id uint64 status string - data interface{} + data interface{} + q *queue } -var DEFAULT_TASK = Task{} - -func (t Task) GetId() uint64 { +func (t *Task) GetId() uint64 { return t.id } -func (t Task) GetData() interface{} { +func (t *Task) GetData() interface{} { return t.data } -func (t Task) GetStatus() string { +func (t *Task) GetStatus() string { return t.status } -func (t Task) IsReady() bool { +func (t *Task) Ack() error { + return t.q._ack(t.id) +} + +func (t *Task) Delete() error { + return t.q._delete(t.id) +} + +func (t *Task) Bury() error { + return t.q._bury(t.id) +} + +func (t *Task) IsReady() bool { return t.status == READY } -func (t Task) IsTaken() bool { +func (t *Task) IsTaken() bool { return t.status == TAKEN } -func (t Task) IsDone() bool { +func (t *Task) IsDone() bool { return t.status == DONE } -func (t Task) IsBuried() bool { +func (t *Task) IsBuried() bool { return t.status == BURIED } -func (t Task) IsDelayed() bool { +func (t *Task) IsDelayed() bool { return t.status == DELAYED -} \ No newline at end of file +} From 1103e0ccb73ff1b845a380e1b9fff862e202bc0c Mon Sep 17 00:00:00 2001 From: Nikita Galushko Date: Wed, 29 Mar 2017 12:10:28 +0300 Subject: [PATCH 171/605] example; fix put --- example_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ queue.go | 14 ++++-------- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/example_test.go b/example_test.go index c7b9e3a29..f37bbce04 100644 --- a/example_test.go +++ b/example_test.go @@ -221,3 +221,64 @@ func Example() { // Fut 2 Error // Fut 2 Data [[15 val 15 bla]] } + +func ExampleConnection_Queue() { + conn, err := example_connect() + if err != nil { + fmt.Printf("error in prepare is %v", err) + return + } + defer conn.Close() + + cfg := tarantool.TtlQueueCfg{ + tarantool.QueueCfg{ + Temporary: false, + Kind: tarantool.FIFO_TTL_QUEUE, + }, + tarantool.QueueOpts{ + Ttl: 10 * time.Second, + }, + } + + q, err := conn.NewQueue("test_queue", cfg) + if err != nil { + fmt.Printf("error in queue is %v", err) + return + } + + defer q.Drop() + + testData_1 := "test_data_1" + _, err = q.Put(testData_1) + if err != nil { + fmt.Printf("error in put is %v", err) + return + } + + testData_2 := "test_data_2" + task_2, err := q.PutWithConfig(testData_2, tarantool.QueueOpts{Ttl: 2 * time.Second}) + if err != nil { + fmt.Printf("error in put with config is %v", err) + return + } + + task, err := q.Take() + if err != nil { + fmt.Printf("error in take with is %v", err) + return + } + task.Ack() + fmt.Println("data_1: ", task.GetData()) + + err = task_2.Bury() + if err != nil { + fmt.Printf("error in bury with is %v", err) + return + } + + task, err = q.TakeWithTimeout(2 * time.Second) + if task != nil { + fmt.Printf("Task should be nil, but %s", task) + return + } +} \ No newline at end of file diff --git a/queue.go b/queue.go index 6a12f38ea..f126c4b1c 100644 --- a/queue.go +++ b/queue.go @@ -128,30 +128,26 @@ func getQueue(conn *Connection, name string) (queue, error) { return q, err } -func (q queue) Put(data interface{}) (uint64, error) { +func (q queue) Put(data interface{}) (*Task, error) { return q.put(data) } -func (q queue) PutWithConfig(data interface{}, cfg QueueOpts) (uint64, error) { +func (q queue) PutWithConfig(data interface{}, cfg QueueOpts) (*Task, error) { return q.put(data, cfg.toMap()) } -func (q queue) put(p ...interface{}) (uint64, error) { +func (q queue) put(p ...interface{}) (*Task, error) { var ( params []interface{} - id uint64 + task *Task ) params = append(params, p...) resp, err := q.conn.Call(q.cmd["put"], params) if err == nil { - var task *Task task, err = toTask(resp.Data, &q) - if err == nil { - id = task.id - } } - return id, err + return task, err } func (q queue) Take() (*Task, error) { From 77cdd966ce217f2f0c700ce9aca68fed8abea5f4 Mon Sep 17 00:00:00 2001 From: Nikita Galushko Date: Wed, 29 Mar 2017 13:53:32 +0300 Subject: [PATCH 172/605] tests --- queue.go | 60 +++++-- tarantool_test.go | 402 ++++++++++++++++++++++++++++++++++++++++++++++ task.go | 24 ++- 3 files changed, 472 insertions(+), 14 deletions(-) diff --git a/queue.go b/queue.go index f126c4b1c..0b5a9623c 100644 --- a/queue.go +++ b/queue.go @@ -6,6 +6,17 @@ import ( "time" ) +type Queue interface { + Put(data interface{}) (*Task, error) + PutWithConfig(data interface{}, cfg QueueOpts) (*Task, error) + Take() (*Task, error) + TakeWithTimeout(timeout time.Duration) (*Task, error) + Drop() error + Peek(taskId uint64) (*Task, error) + Kick(taskId uint64) (uint64, error) + Statistic() (interface{}, error) +} + type queue struct { name string conn *Connection @@ -183,23 +194,50 @@ func (q queue) Peek(taskId uint64) (*Task, error) { return t, err } -func (q queue) _ack(taskId uint64) error { - _, err := q.conn.Call(q.cmd["ack"], []interface{}{taskId}) - return err +func (q queue) _ack(taskId uint64) (string, error) { + resp, err := q.conn.Call(q.cmd["ack"], []interface{}{taskId}) + if err != nil { + return "", err + } + + t, err := toTask(resp.Data, &q) + if err != nil { + return "", err + } + + return t.status, nil } -func (q queue) _delete(taskId uint64) error { - _, err := q.conn.Call(q.cmd["delete"], []interface{}{taskId}) - return err +func (q queue) _delete(taskId uint64) (string, error) { + resp, err := q.conn.Call(q.cmd["delete"], []interface{}{taskId}) + if err != nil { + return "", err + } + + t, err := toTask(resp.Data, &q) + if err != nil { + return "", err + } + + return t.status, nil } -func (q queue) _bury(taskId uint64) error { - _, err := q.conn.Call(q.cmd["bury"], []interface{}{taskId}) - return err +func (q queue) _bury(taskId uint64) (string, error) { + resp, err := q.conn.Call(q.cmd["bury"], []interface{}{taskId}) + if err != nil { + return "", err + } + + t, err := toTask(resp.Data, &q) + if err != nil { + return "", err + } + + return t.status, nil } -func (q queue) Kick(taskId uint64) (uint64, error) { - resp, err := q.conn.Call(q.cmd["kick"], []interface{}{taskId}) +func (q queue) Kick(count uint64) (uint64, error) { + resp, err := q.conn.Call(q.cmd["kick"], []interface{}{count}) var id uint64 if err == nil { id = resp.Data[0].([]interface{})[0].(uint64) diff --git a/tarantool_test.go b/tarantool_test.go index bbf6220a7..910444e51 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -984,3 +984,405 @@ func TestComplexStructs(t *testing.T) { return } } + +/////////QUEUE///////// + +func TestFifoQueue_Create(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + if err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } else { + t.Logf("Queue %s was created", name) + } + + //Drop + err = q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } else { + t.Logf("Queue %s was droped", name) + } +} + +func TestFifoQueue_Put(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + if err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } else { + t.Logf("Queue %s was created", name) + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } else { + t.Logf("Queue %s was droped", name) + } + }() + + //Put + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.GetData() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) + } else { + t.Logf("Task data after put equal with example. %s == %s", task.GetData(), putData) + } + } +} + +func TestFifoQueue_Take(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + if err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } else { + t.Logf("Queue %s was created", name) + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } else { + t.Logf("Queue %s was droped", name) + } + }() + + //Put + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.GetData() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) + } else { + t.Logf("Task data after put equal with example. %s == %s", task.GetData(), putData) + } + } + + + //Take + task, err = q.TakeWithTimeout(2 * time.Second) + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + } else if task == nil { + t.Errorf("Task is nil after take") + } else { + if task.GetData() != putData { + t.Errorf("Task data after take not equal with example. %s != %s", task.GetData(), putData) + } else { + t.Logf("Task data after take equal with example. %s == %s", task.GetData(), putData) + } + + if !task.IsTaken() { + t.Errorf("Task status after take is not taken. Status = ", task.GetStatus()) + } else { + t.Logf("Task status is taken") + } + + err = task.Ack() + if err != nil { + t.Errorf("Failed ack %s", err.Error()) + } else if !task.IsDone() { + t.Errorf("Task status after take is not done. Status = ", task.GetStatus()) + } else { + t.Logf("Task status is done after ack") + } + } +} + +func TestFifoQueue_Peek(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + if err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } else { + t.Logf("Queue %s was created", name) + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } else { + t.Logf("Queue %s was droped", name) + } + }() + + //Put + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.GetData() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) + } else { + t.Logf("Task data after put equal with example. %s == %s", task.GetData(), putData) + } + } + + + //Peek + task, err = q.Peek(task.GetId()) + if err != nil { + t.Errorf("Failed peek from queue: %s", err.Error()) + } else if task == nil { + t.Errorf("Task is nil after peek") + } else { + if task.GetData() != putData { + t.Errorf("Task data after peek not equal with example. %s != %s", task.GetData(), putData) + } else { + t.Logf("Task data after peek equal with example. %s == %s", task.GetData(), putData) + } + + if !task.IsReady() { + t.Errorf("Task status after peek is not ready. Status = ", task.GetStatus()) + } else { + t.Logf("Task status is ready after peek") + } + } +} + +func TestFifoQueue_Bury_Kick(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + if err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } else { + t.Logf("Queue %s was created", name) + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } else { + t.Logf("Queue %s was droped", name) + } + }() + + //Put + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.GetData() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) + } else { + t.Logf("Task data after put equal with example. %s == %s", task.GetData(), putData) + } + } + + + //Bury + err = task.Bury() + if err != nil { + t.Errorf("Failed bury task% %s", err.Error()) + return + } else if !task.IsBuried() { + t.Errorf("Task status after bury is not buried. Status = ", task.GetStatus()) + } else { + t.Logf("Task status is bury after bury") + } + + //Kick + count, err := q.Kick(1) + if err != nil { + t.Errorf("Failed kick task% %s", err.Error()) + return + } else if count != 2 { + t.Errorf("Kick result != 1") + return + } else { + t.Logf("Kick result == 1") + } + + //Take + task, err = q.TakeWithTimeout(2 * time.Second) + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + } else if task == nil { + t.Errorf("Task is nil after take") + } else { + if task.GetData() != putData { + t.Errorf("Task data after take not equal with example. %s != %s", task.GetData(), putData) + } else { + t.Logf("Task data after take equal with example. %s == %s", task.GetData(), putData) + } + + if !task.IsTaken() { + t.Errorf("Task status after take is not taken. Status = ", task.GetStatus()) + } else { + t.Logf("Task status is taken") + } + + err = task.Ack() + if err != nil { + t.Errorf("Failed ack %s", err.Error()) + }else if !task.IsDone() { + t.Errorf("Task status after take is not done. Status = ", task.GetStatus()) + } else { + t.Logf("Task status is done after ack") + } + } +} + +func TestFifoQueue_Delete(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + if err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } else { + t.Logf("Queue %s was created", name) + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } else { + t.Logf("Queue %s was droped", name) + } + }() + + //Put + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.GetData() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) + } else { + t.Logf("Task data after put equal with example. %s == %s", task.GetData(), putData) + } + } + + //Delete + err = task.Delete() + if err != nil { + t.Errorf("Failed bury task% %s", err.Error()) + return + } else if !task.IsDone() { + t.Errorf("Task status after delete is not done. Status = ", task.GetStatus()) + } else { + t.Logf("Task status is done after delete") + } + + //Take + task, err = q.TakeWithTimeout(2 * time.Second) + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + } else if task != nil { + t.Errorf("Task is not nil after take. Task is %s", task) + } else { + t.Logf("Queue is empty after delete task.") + } +} \ No newline at end of file diff --git a/task.go b/task.go index a9c1d3c1d..8db3f2155 100644 --- a/task.go +++ b/task.go @@ -20,15 +20,33 @@ func (t *Task) GetStatus() string { } func (t *Task) Ack() error { - return t.q._ack(t.id) + newStatus, err := t.q._ack(t.id) + if err != nil { + return err + } + + t.status = newStatus + return nil } func (t *Task) Delete() error { - return t.q._delete(t.id) + newStatus, err := t.q._delete(t.id) + if err != nil { + return err + } + + t.status = newStatus + return nil } func (t *Task) Bury() error { - return t.q._bury(t.id) + newStatus, err := t.q._bury(t.id) + if err != nil { + return err + } + + t.status = newStatus + return nil } func (t *Task) IsReady() bool { From a5006f816b00cf4edc828bdeb44528e0ab4a9594 Mon Sep 17 00:00:00 2001 From: Nikita Galushko Date: Wed, 29 Mar 2017 15:47:02 +0300 Subject: [PATCH 173/605] Tests to ttl_queue --- queue.go | 31 ++------ tarantool_test.go | 183 +++++++++++++++++++++++++++++++--------------- 2 files changed, 131 insertions(+), 83 deletions(-) diff --git a/queue.go b/queue.go index 0b5a9623c..e41fc69cd 100644 --- a/queue.go +++ b/queue.go @@ -195,35 +195,19 @@ func (q queue) Peek(taskId uint64) (*Task, error) { } func (q queue) _ack(taskId uint64) (string, error) { - resp, err := q.conn.Call(q.cmd["ack"], []interface{}{taskId}) - if err != nil { - return "", err - } - - t, err := toTask(resp.Data, &q) - if err != nil { - return "", err - } - - return t.status, nil + return q.produce("ack", taskId) } func (q queue) _delete(taskId uint64) (string, error) { - resp, err := q.conn.Call(q.cmd["delete"], []interface{}{taskId}) - if err != nil { - return "", err - } - - t, err := toTask(resp.Data, &q) - if err != nil { - return "", err - } - - return t.status, nil + return q.produce("delete", taskId) } func (q queue) _bury(taskId uint64) (string, error) { - resp, err := q.conn.Call(q.cmd["bury"], []interface{}{taskId}) + return q.produce("bury", taskId) +} + +func (q queue) produce(cmd string, taskId uint64) (string, error) { + resp, err := q.conn.Call(q.cmd[cmd], []interface{}{taskId}) if err != nil { return "", err } @@ -232,7 +216,6 @@ func (q queue) _bury(taskId uint64) (string, error) { if err != nil { return "", err } - return t.status, nil } diff --git a/tarantool_test.go b/tarantool_test.go index 910444e51..a3e5a3004 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -1004,16 +1004,12 @@ func TestFifoQueue_Create(t *testing.T) { if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return - } else { - t.Logf("Queue %s was created", name) } //Drop err = q.Drop() if err != nil { t.Errorf("Failed drop queue: %s", err.Error()) - } else { - t.Logf("Queue %s was droped", name) } } @@ -1034,8 +1030,6 @@ func TestFifoQueue_Put(t *testing.T) { if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return - } else { - t.Logf("Queue %s was created", name) } defer func() { @@ -1043,8 +1037,6 @@ func TestFifoQueue_Put(t *testing.T) { err := q.Drop() if err != nil { t.Errorf("Failed drop queue: %s", err.Error()) - } else { - t.Logf("Queue %s was droped", name) } }() @@ -1060,8 +1052,6 @@ func TestFifoQueue_Put(t *testing.T) { } else { if task.GetData() != putData { t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) - } else { - t.Logf("Task data after put equal with example. %s == %s", task.GetData(), putData) } } } @@ -1083,8 +1073,6 @@ func TestFifoQueue_Take(t *testing.T) { if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return - } else { - t.Logf("Queue %s was created", name) } defer func() { @@ -1092,8 +1080,6 @@ func TestFifoQueue_Take(t *testing.T) { err := q.Drop() if err != nil { t.Errorf("Failed drop queue: %s", err.Error()) - } else { - t.Logf("Queue %s was droped", name) } }() @@ -1109,8 +1095,6 @@ func TestFifoQueue_Take(t *testing.T) { } else { if task.GetData() != putData { t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) - } else { - t.Logf("Task data after put equal with example. %s == %s", task.GetData(), putData) } } @@ -1124,14 +1108,10 @@ func TestFifoQueue_Take(t *testing.T) { } else { if task.GetData() != putData { t.Errorf("Task data after take not equal with example. %s != %s", task.GetData(), putData) - } else { - t.Logf("Task data after take equal with example. %s == %s", task.GetData(), putData) } if !task.IsTaken() { t.Errorf("Task status after take is not taken. Status = ", task.GetStatus()) - } else { - t.Logf("Task status is taken") } err = task.Ack() @@ -1139,8 +1119,6 @@ func TestFifoQueue_Take(t *testing.T) { t.Errorf("Failed ack %s", err.Error()) } else if !task.IsDone() { t.Errorf("Task status after take is not done. Status = ", task.GetStatus()) - } else { - t.Logf("Task status is done after ack") } } } @@ -1162,8 +1140,6 @@ func TestFifoQueue_Peek(t *testing.T) { if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return - } else { - t.Logf("Queue %s was created", name) } defer func() { @@ -1171,8 +1147,6 @@ func TestFifoQueue_Peek(t *testing.T) { err := q.Drop() if err != nil { t.Errorf("Failed drop queue: %s", err.Error()) - } else { - t.Logf("Queue %s was droped", name) } }() @@ -1188,8 +1162,6 @@ func TestFifoQueue_Peek(t *testing.T) { } else { if task.GetData() != putData { t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) - } else { - t.Logf("Task data after put equal with example. %s == %s", task.GetData(), putData) } } @@ -1203,14 +1175,10 @@ func TestFifoQueue_Peek(t *testing.T) { } else { if task.GetData() != putData { t.Errorf("Task data after peek not equal with example. %s != %s", task.GetData(), putData) - } else { - t.Logf("Task data after peek equal with example. %s == %s", task.GetData(), putData) } if !task.IsReady() { t.Errorf("Task status after peek is not ready. Status = ", task.GetStatus()) - } else { - t.Logf("Task status is ready after peek") } } } @@ -1232,8 +1200,6 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return - } else { - t.Logf("Queue %s was created", name) } defer func() { @@ -1241,8 +1207,6 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { err := q.Drop() if err != nil { t.Errorf("Failed drop queue: %s", err.Error()) - } else { - t.Logf("Queue %s was droped", name) } }() @@ -1258,8 +1222,6 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { } else { if task.GetData() != putData { t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) - } else { - t.Logf("Task data after put equal with example. %s == %s", task.GetData(), putData) } } @@ -1271,8 +1233,6 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { return } else if !task.IsBuried() { t.Errorf("Task status after bury is not buried. Status = ", task.GetStatus()) - } else { - t.Logf("Task status is bury after bury") } //Kick @@ -1280,11 +1240,9 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { if err != nil { t.Errorf("Failed kick task% %s", err.Error()) return - } else if count != 2 { + } else if count != 1 { t.Errorf("Kick result != 1") return - } else { - t.Logf("Kick result == 1") } //Take @@ -1296,23 +1254,17 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { } else { if task.GetData() != putData { t.Errorf("Task data after take not equal with example. %s != %s", task.GetData(), putData) - } else { - t.Logf("Task data after take equal with example. %s == %s", task.GetData(), putData) } if !task.IsTaken() { t.Errorf("Task status after take is not taken. Status = ", task.GetStatus()) - } else { - t.Logf("Task status is taken") } err = task.Ack() if err != nil { t.Errorf("Failed ack %s", err.Error()) - }else if !task.IsDone() { + } else if !task.IsDone() { t.Errorf("Task status after take is not done. Status = ", task.GetStatus()) - } else { - t.Logf("Task status is done after ack") } } } @@ -1334,8 +1286,6 @@ func TestFifoQueue_Delete(t *testing.T) { if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return - } else { - t.Logf("Queue %s was created", name) } defer func() { @@ -1343,8 +1293,6 @@ func TestFifoQueue_Delete(t *testing.T) { err := q.Drop() if err != nil { t.Errorf("Failed drop queue: %s", err.Error()) - } else { - t.Logf("Queue %s was droped", name) } }() @@ -1360,8 +1308,6 @@ func TestFifoQueue_Delete(t *testing.T) { } else { if task.GetData() != putData { t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) - } else { - t.Logf("Task data after put equal with example. %s == %s", task.GetData(), putData) } } @@ -1372,8 +1318,6 @@ func TestFifoQueue_Delete(t *testing.T) { return } else if !task.IsDone() { t.Errorf("Task status after delete is not done. Status = ", task.GetStatus()) - } else { - t.Logf("Task status is done after delete") } //Take @@ -1382,7 +1326,128 @@ func TestFifoQueue_Delete(t *testing.T) { t.Errorf("Failed take from queue: %s", err.Error()) } else if task != nil { t.Errorf("Task is not nil after take. Task is %s", task) + } +} + +func TestTtlQueue(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + cfg := TtlQueueCfg{ + QueueCfg{Temporary: true, Kind: FIFO_TTL_QUEUE}, + QueueOpts{Ttl: 5*time.Second}, + } + q, err := conn.NewQueue(name, cfg) + if err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.GetData() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) + } + } + + time.Sleep(1 * time.Second) + + //Take + task, err = q.TakeWithTimeout(2 * time.Second) + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + } else if task != nil { + t.Errorf("Task is not nil after sleep") + } + + //Drop + err = q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } +} + +func TestTtlQueue_Put(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + cfg := TtlQueueCfg{ + QueueCfg{Temporary: true, Kind: FIFO_TTL_QUEUE}, + QueueOpts{Ttl: 5*time.Second}, + } + q, err := conn.NewQueue(name, cfg) + if err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + putData := "put_data" + task, err := q.PutWithConfig(putData, QueueOpts{Ttl: 10 * time.Second}) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return } else { - t.Logf("Queue is empty after delete task.") + if task.GetData() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) + } + } + + time.Sleep(5 * time.Second) + + //Take + task, err = q.TakeWithTimeout(2 * time.Second) + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + } else if task == nil { + t.Errorf("Task is nil after sleep") + } else { + if task.GetData() != putData { + t.Errorf("Task data after take not equal with example. %s != %s", task.GetData(), putData) + } + + if !task.IsTaken() { + t.Errorf("Task status after take is not taken. Status = ", task.GetStatus()) + } + + err = task.Ack() + if err != nil { + t.Errorf("Failed ack %s", err.Error()) + } else if !task.IsDone() { + t.Errorf("Task status after take is not done. Status = ", task.GetStatus()) + } + } + + //Drop + err = q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) } } \ No newline at end of file From ccf979e360374d2f764a7516b00c9192337bc7d8 Mon Sep 17 00:00:00 2001 From: Nikita Galushko Date: Wed, 29 Mar 2017 16:33:27 +0300 Subject: [PATCH 174/605] release --- queue.go | 20 ++++-- request.go | 4 +- tarantool_test.go | 167 ++++++++++++++++++++++++++++++++++++++++++---- task.go | 30 ++++++--- 4 files changed, 189 insertions(+), 32 deletions(-) diff --git a/queue.go b/queue.go index e41fc69cd..5e2e3dbc6 100644 --- a/queue.go +++ b/queue.go @@ -96,12 +96,14 @@ func (opts QueueOpts) toMap() map[string]interface{} { ret["delay"] = opts.Delay.Seconds() } - ret["pri"] = opts.Pri + if opts.Pri != 0 { + ret["pri"] = opts.Pri + } return ret } -func newQueue(conn *Connection, name string, cfg queueCfg) (queue, error) { +func newQueue(conn *Connection, name string, cfg queueCfg) (Queue, error) { var q queue cmd := fmt.Sprintf("queue.create_tube('%s', '%s', %s)", name, cfg.Type(), cfg.String()) fut := conn.EvalAsync(cmd, []interface{}{}) @@ -116,7 +118,7 @@ func newQueue(conn *Connection, name string, cfg queueCfg) (queue, error) { return q, fut.err } -func getQueue(conn *Connection, name string) (queue, error) { +func getQueue(conn *Connection, name string) (Queue, error) { var q queue cmd := fmt.Sprintf("return queue.tube.%s ~= null", name) resp, err := conn.Eval(cmd, []interface{}{}) @@ -150,7 +152,7 @@ func (q queue) PutWithConfig(data interface{}, cfg QueueOpts) (*Task, error) { func (q queue) put(p ...interface{}) (*Task, error) { var ( params []interface{} - task *Task + task *Task ) params = append(params, p...) resp, err := q.conn.Call(q.cmd["put"], params) @@ -206,8 +208,13 @@ func (q queue) _bury(taskId uint64) (string, error) { return q.produce("bury", taskId) } -func (q queue) produce(cmd string, taskId uint64) (string, error) { - resp, err := q.conn.Call(q.cmd[cmd], []interface{}{taskId}) +func (q queue) _release(taskId uint64, cfg QueueOpts) (string, error) { + return q.produce("release", taskId, cfg.toMap()) +} +func (q queue) produce(cmd string, p ...interface{}) (string, error) { + var params []interface{} + params = append(params, p...) + resp, err := q.conn.Call(q.cmd[cmd], params) if err != nil { return "", err } @@ -254,6 +261,7 @@ func makeCmdMap(name string) map[string]string { "delete": "queue.tube." + name + ":delete", "bury": "queue.tube." + name + ":bury", "kick": "queue.tube." + name + ":kick", + "release": "queue.tube." + name + ":release", "statistics": "queue.statistics", } } diff --git a/request.go b/request.go index 2fc31514a..305a74778 100644 --- a/request.go +++ b/request.go @@ -313,11 +313,11 @@ func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { }) } -func (conn *Connection) NewQueue(name string, cfg queueCfg) (queue, error) { +func (conn *Connection) NewQueue(name string, cfg queueCfg) (Queue, error) { return newQueue(conn, name, cfg) } -func (conn *Connection) GetQueue(name string) (queue, error) { +func (conn *Connection) GetQueue(name string) (Queue, error) { return getQueue(conn, name) } diff --git a/tarantool_test.go b/tarantool_test.go index a3e5a3004..61d45cfc8 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -987,7 +987,7 @@ func TestComplexStructs(t *testing.T) { /////////QUEUE///////// -func TestFifoQueue_Create(t *testing.T) { +func TestFifoQueue(t *testing.T) { conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) @@ -1013,6 +1013,51 @@ func TestFifoQueue_Create(t *testing.T) { } } +func TestFifoQueue_GetExist_Statistic(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + _, err = conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + if err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + q, err := conn.GetQueue(name) + if err != nil { + t.Errorf("Failed to get exist queue: %s", err.Error()) + return + } + + putData := "put_data" + _, err = q.Put(putData) + if err != nil { + t.Errorf("Failed to put queue: %s", err.Error()) + return + } + + stat, err := q.Statistic() + if err != nil { + t.Errorf("Failed to get statistic queue: %s", err.Error()) + } else if stat == nil { + t.Error("Statistic is nil") + } + //Drop + err = q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } +} + func TestFifoQueue_Put(t *testing.T) { conn, err := Connect(server, opts) if err != nil { @@ -1329,6 +1374,96 @@ func TestFifoQueue_Delete(t *testing.T) { } } +func TestFifoQueue_Release(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + if err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() + + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.GetData() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) + } + } + + //Take + task, err = q.Take() + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + return + } else if task == nil { + t.Error("Task is nil after take") + return + } + + //Release + err = task.Release() + if err != nil { + t.Errorf("Failed release task% %s", err.Error()) + return + } + + if !task.IsReady() { + t.Errorf("Task status is not ready, but %s", task.GetStatus()) + return + } + + + //Take + task, err = q.Take() + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + return + } else if task == nil { + t.Error("Task is nil after take") + return + } else { + if task.GetData() != putData { + t.Errorf("Task data after take not equal with example. %s != %s", task.GetData(), putData) + } + + if !task.IsTaken() { + t.Errorf("Task status after take is not taken. Status = ", task.GetStatus()) + } + + err = task.Ack() + if err != nil { + t.Errorf("Failed ack %s", err.Error()) + } else if !task.IsDone() { + t.Errorf("Task status after take is not done. Status = ", task.GetStatus()) + } + } +} + func TestTtlQueue(t *testing.T) { conn, err := Connect(server, opts) if err != nil { @@ -1352,6 +1487,14 @@ func TestTtlQueue(t *testing.T) { return } + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() + putData := "put_data" task, err := q.Put(putData) if err != nil { @@ -1366,7 +1509,7 @@ func TestTtlQueue(t *testing.T) { } } - time.Sleep(1 * time.Second) + time.Sleep(5 * time.Second) //Take task, err = q.TakeWithTimeout(2 * time.Second) @@ -1375,12 +1518,6 @@ func TestTtlQueue(t *testing.T) { } else if task != nil { t.Errorf("Task is not nil after sleep") } - - //Drop - err = q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } } func TestTtlQueue_Put(t *testing.T) { @@ -1406,6 +1543,14 @@ func TestTtlQueue_Put(t *testing.T) { return } + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() + putData := "put_data" task, err := q.PutWithConfig(putData, QueueOpts{Ttl: 10 * time.Second}) if err != nil { @@ -1444,10 +1589,4 @@ func TestTtlQueue_Put(t *testing.T) { t.Errorf("Task status after take is not done. Status = ", task.GetStatus()) } } - - //Drop - err = q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } } \ No newline at end of file diff --git a/task.go b/task.go index 8db3f2155..394a0c7ee 100644 --- a/task.go +++ b/task.go @@ -20,17 +20,19 @@ func (t *Task) GetStatus() string { } func (t *Task) Ack() error { - newStatus, err := t.q._ack(t.id) - if err != nil { - return err - } - - t.status = newStatus - return nil + return t.produce(t.q._ack) } func (t *Task) Delete() error { - newStatus, err := t.q._delete(t.id) + return t.produce(t.q._delete) +} + +func (t *Task) Bury() error { + return t.produce(t.q._bury) +} + +func (t *Task) produce(f func(taskId uint64) (string, error)) error { + newStatus, err := f(t.id) if err != nil { return err } @@ -39,8 +41,16 @@ func (t *Task) Delete() error { return nil } -func (t *Task) Bury() error { - newStatus, err := t.q._bury(t.id) +func (t *Task) Release() error { + return t.release(QueueOpts{}) +} + +func (t *Task) ReleaseWithCinfig(cfg QueueOpts) error { + return t.release(cfg) +} + +func (t *Task) release(cfg QueueOpts) error { + newStatus, err := t.q._release(t.id, cfg) if err != nil { return err } From aa2a0e250355cdf807684f9c0a17e95c98fb701a Mon Sep 17 00:00:00 2001 From: Ilya Sinelnikov Date: Wed, 29 Mar 2017 17:30:11 +0300 Subject: [PATCH 175/605] Fix IterGe/IterGt documentation --- const.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/const.go b/const.go index 50972ed43..03b00c6b1 100644 --- a/const.go +++ b/const.go @@ -37,8 +37,8 @@ const ( IterAll = uint32(2) // all tuples IterLt = uint32(3) // key < x IterLe = uint32(4) // key <= x - IterGe = uint32(5) // key > x - IterGt = uint32(6) // key >= x + IterGe = uint32(5) // key >= x + IterGt = uint32(6) // key > x IterBitsAllSet = uint32(7) // all bits from x are set in key IterBitsAnySet = uint32(8) // at least one x's bit is set IterBitsAllNotSet = uint32(9) // all bits are not set From a1e1bb0f948ac13c8349bfb877d66cd862fd2d39 Mon Sep 17 00:00:00 2001 From: Nikita Galushko Date: Fri, 31 Mar 2017 14:30:40 +0300 Subject: [PATCH 176/605] fix take; add readme --- README.md | 47 ++++++++++++++++++++++++++++++++++++++++++++++- queue.go | 16 +++++++++++----- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4ac5e9d95..0da00aee0 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ faster than other packages according to public benchmarks. * [Schema](#schema) * [Custom (un)packing and typed selects and function calls](#custom-unpacking-and-typed-selects-and-function-calls) * [Options](#options) +* [Working with queue](#working-with-queue) ## Installation @@ -280,7 +281,7 @@ func main() { log.Println("Code", resp.Code) log.Println("Data", resp.Data) } -``` +``` ## Schema @@ -499,3 +500,47 @@ func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { up. If `MaxReconnects` is zero, the client will try to reconnect endlessly. * `User` - user name to log into Tarantool. * `Pass` - user password to log into Tarantool. + +## Working-with-queue +```go +opts := tarantool.Opts{...} +conn, err := tarantool.Connect("127.0.0.1:3301", opts) + +cfg := tarantool.TtlQueueCfg{ + tarantool.QueueCfg{ + Temporary: true, + IfNotExist: true, + Kind: tarantool.FIFO_TTL_QUEUE, + }, + tarantool.QueueOpts{ + Ttl: 10 * time.Second, + Ttr: 5 * time.Second, + Delay: 3 * time.Second, + Pri: 1, + }, +} + + //If you want simple fifo queue use only tarantool.QueueCfg{...} + +queue, err := conn.NewQueue("test_queue", cfg) +task, err := queue.Put("test_data") +fmt.Println("Task id is ", task.GetId()) + +task, err = queue.Take() //blocking operation +fmt.Println("Data is ", task.GetData()) +task.Ack() + +task, err = queue.Put([]int{1, 2, 3}) +task.Bury() + +task, err = queue.TakeWithTimeout(2 * time.Second) +if task == nil { + fmt.Println("Task is nil") +} + +q.Drop() +``` +Features of the implementation: + +- If you use connection timeout and call `TakeWithTimeout` with parameter greater than the connection timeout then parameter reduced to it +- If you use connection timeout and call `Take` then we return a error if we can not take task from queue in a time equal to the connection timeout \ No newline at end of file diff --git a/queue.go b/queue.go index 5e2e3dbc6..00a10bd2f 100644 --- a/queue.go +++ b/queue.go @@ -106,16 +106,15 @@ func (opts QueueOpts) toMap() map[string]interface{} { func newQueue(conn *Connection, name string, cfg queueCfg) (Queue, error) { var q queue cmd := fmt.Sprintf("queue.create_tube('%s', '%s', %s)", name, cfg.Type(), cfg.String()) - fut := conn.EvalAsync(cmd, []interface{}{}) - fut.wait() - if fut.err == nil { + _, err := conn.Eval(cmd, []interface{}{}) + if err == nil { q = queue{ name, conn, makeCmdMap(name), } } - return q, fut.err + return q, err } func getQueue(conn *Connection, name string) (Queue, error) { @@ -164,10 +163,17 @@ func (q queue) put(p ...interface{}) (*Task, error) { } func (q queue) Take() (*Task, error) { - return q.take(nil) + var params interface{} + if q.conn.opts.Timeout > 0 { + params = q.conn.opts.Timeout.Seconds() + } + return q.take(params) } func (q queue) TakeWithTimeout(timeout time.Duration) (*Task, error) { + if q.conn.opts.Timeout > 0 && timeout > q.conn.opts.Timeout { + timeout = q.conn.opts.Timeout + } return q.take(timeout.Seconds()) } From ae838e295638fee1783f58985f6196424da1a745 Mon Sep 17 00:00:00 2001 From: Nikita Galushko Date: Tue, 4 Apr 2017 16:21:42 +0300 Subject: [PATCH 177/605] docs --- queue.go | 34 +++++++++++++++++++++------------- request.go | 2 ++ task.go | 15 ++++++++++++++- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/queue.go b/queue.go index 00a10bd2f..b42c09872 100644 --- a/queue.go +++ b/queue.go @@ -24,21 +24,21 @@ type queue struct { } type queueCfg interface { - String() string - Type() string + toString() string + getType() string } type QueueCfg struct { - Temporary bool - IfNotExists bool + Temporary bool // if true, the contents do not persist on disk + IfNotExists bool // if true, no error will be returned if the tube already exists Kind queueType } -func (cfg QueueCfg) String() string { +func (cfg QueueCfg) toString() string { return fmt.Sprintf("{ temporary = %v, if_not_exists = %v }", cfg.Temporary, cfg.IfNotExists) } -func (cfg QueueCfg) Type() string { +func (cfg QueueCfg) getType() string { return string(cfg.Kind) } @@ -48,13 +48,13 @@ type TtlQueueCfg struct { } type QueueOpts struct { - Pri int - Ttl time.Duration - Ttr time.Duration - Delay time.Duration + Pri int // task priorities + Ttl time.Duration // task time to live + Ttr time.Duration // task time to execute + Delay time.Duration // delayed execution } -func (cfg TtlQueueCfg) String() string { +func (cfg TtlQueueCfg) toString() string { params := []string{fmt.Sprintf("temporary = %v, if_not_exists = %v", cfg.Temporary, cfg.IfNotExists)} if cfg.Ttl.Seconds() != 0 { @@ -72,7 +72,7 @@ func (cfg TtlQueueCfg) String() string { return "{" + strings.Join(params, ",") + "}" } -func (cfg TtlQueueCfg) Type() string { +func (cfg TtlQueueCfg) getType() string { kind := string(cfg.Kind) if kind == "" { kind = string(FIFO_QUEUE) @@ -105,7 +105,7 @@ func (opts QueueOpts) toMap() map[string]interface{} { func newQueue(conn *Connection, name string, cfg queueCfg) (Queue, error) { var q queue - cmd := fmt.Sprintf("queue.create_tube('%s', '%s', %s)", name, cfg.Type(), cfg.String()) + cmd := fmt.Sprintf("queue.create_tube('%s', '%s', %s)", name, cfg.getType(), cfg.toString()) _, err := conn.Eval(cmd, []interface{}{}) if err == nil { q = queue{ @@ -140,10 +140,12 @@ func getQueue(conn *Connection, name string) (Queue, error) { return q, err } +// Put data to queue. Returns task. func (q queue) Put(data interface{}) (*Task, error) { return q.put(data) } +// Put data with options (ttl/ttr/pri/delay) to queue. Returns task. func (q queue) PutWithConfig(data interface{}, cfg QueueOpts) (*Task, error) { return q.put(data, cfg.toMap()) } @@ -162,6 +164,7 @@ func (q queue) put(p ...interface{}) (*Task, error) { return task, err } +// The take request searches for a task in the queue. func (q queue) Take() (*Task, error) { var params interface{} if q.conn.opts.Timeout > 0 { @@ -170,6 +173,7 @@ func (q queue) Take() (*Task, error) { return q.take(params) } +// The take request searches for a task in the queue. Waits until a task becomes ready or the timeout expires. func (q queue) TakeWithTimeout(timeout time.Duration) (*Task, error) { if q.conn.opts.Timeout > 0 && timeout > q.conn.opts.Timeout { timeout = q.conn.opts.Timeout @@ -186,11 +190,13 @@ func (q queue) take(params interface{}) (*Task, error) { return t, err } +// Drop queue. func (q queue) Drop() error { _, err := q.conn.Call(q.cmd["drop"], []interface{}{}) return err } +// Look at a task without changing its state. func (q queue) Peek(taskId uint64) (*Task, error) { resp, err := q.conn.Call(q.cmd["peek"], []interface{}{taskId}) if err != nil { @@ -232,6 +238,7 @@ func (q queue) produce(cmd string, p ...interface{}) (string, error) { return t.status, nil } +// Reverse the effect of a bury request on one or more tasks. func (q queue) Kick(count uint64) (uint64, error) { resp, err := q.conn.Call(q.cmd["kick"], []interface{}{count}) var id uint64 @@ -241,6 +248,7 @@ func (q queue) Kick(count uint64) (uint64, error) { return id, err } +// Return the number of tasks in a queue broken down by task_state, and the number of requests broken down by the type of request. func (q queue) Statistic() (interface{}, error) { resp, err := q.conn.Call(q.cmd["statistics"], []interface{}{}) if err != nil { diff --git a/request.go b/request.go index 305a74778..908e197b4 100644 --- a/request.go +++ b/request.go @@ -313,10 +313,12 @@ func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { }) } +// NewQueue creates a new queue with config and return Queue. func (conn *Connection) NewQueue(name string, cfg queueCfg) (Queue, error) { return newQueue(conn, name, cfg) } +// GetQueue returns an existing queue by name. func (conn *Connection) GetQueue(name string) (Queue, error) { return getQueue(conn, name) } diff --git a/task.go b/task.go index 394a0c7ee..c01a0d38f 100644 --- a/task.go +++ b/task.go @@ -7,26 +7,32 @@ type Task struct { q *queue } +// Return a task id func (t *Task) GetId() uint64 { return t.id } +// Return a task data func (t *Task) GetData() interface{} { return t.data } +// Return a task status func (t *Task) GetStatus() string { return t.status } +// Signal that the task has been completed func (t *Task) Ack() error { return t.produce(t.q._ack) } +// Delete task from queue func (t *Task) Delete() error { return t.produce(t.q._delete) } +// If it becomes clear that a task cannot be executed in the current circumstances, you can "bury" the task -- that is, disable it until the circumstances change. func (t *Task) Bury() error { return t.produce(t.q._bury) } @@ -41,11 +47,13 @@ func (t *Task) produce(f func(taskId uint64) (string, error)) error { return nil } +// Put the task back in the queue, 'release' implies unsuccessful completion of a taken task. func (t *Task) Release() error { return t.release(QueueOpts{}) } -func (t *Task) ReleaseWithCinfig(cfg QueueOpts) error { +// Put the task back in the queue with config, 'release' implies unsuccessful completion of a taken task. +func (t *Task) ReleaseWithConfig(cfg QueueOpts) error { return t.release(cfg) } @@ -59,22 +67,27 @@ func (t *Task) release(cfg QueueOpts) error { return nil } +// Return true if task status is ready func (t *Task) IsReady() bool { return t.status == READY } +// Return true if task status is taken func (t *Task) IsTaken() bool { return t.status == TAKEN } +// Return true if task status is done func (t *Task) IsDone() bool { return t.status == DONE } +// Return true if task status is bury func (t *Task) IsBuried() bool { return t.status == BURIED } +// Return true if task status is delayed func (t *Task) IsDelayed() bool { return t.status == DELAYED } From d244db0b82f8e0bc137c9cfe8fdd9bd57415cc03 Mon Sep 17 00:00:00 2001 From: Nikita Galushko Date: Wed, 5 Apr 2017 16:58:16 +0300 Subject: [PATCH 178/605] refactoring --- const.go | 18 ------- example_test.go | 39 +++++++-------- queue/const.go | 18 +++++++ queue.go => queue/queue.go | 100 ++++++++++++++++++++----------------- task.go => queue/task.go | 8 +-- request.go | 10 ---- tarantool_test.go | 69 ++++++++++++------------- 7 files changed, 129 insertions(+), 133 deletions(-) create mode 100644 queue/const.go rename queue.go => queue/queue.go (75%) rename task.go => queue/task.go (92%) diff --git a/const.go b/const.go index a259a2ca0..ea204dded 100644 --- a/const.go +++ b/const.go @@ -49,22 +49,4 @@ const ( OkCode = uint32(0) ErrorCodeBit = 0x8000 PacketLengthBytes = 5 -) - - -const ( - READY = "r" - TAKEN = "t" - DONE = "-" - BURIED = "!" - DELAYED = "~" -) - -type queueType string - -const ( - FIFO_QUEUE queueType = "fifo" - FIFO_TTL_QUEUE queueType = "fifottl" - UTUBE_QUEUE queueType = "utube" - UTUBE_TTL_QUEUE queueType = "utubettl" ) \ No newline at end of file diff --git a/example_test.go b/example_test.go index f37bbce04..34fa8d2da 100644 --- a/example_test.go +++ b/example_test.go @@ -4,15 +4,16 @@ import ( "fmt" "github.com/tarantool/go-tarantool" "time" + "github.com/tarantool/go-tarantool/queue" ) type Tuple struct { /* instruct msgpack to pack this struct as array, * so no custom packer is needed */ _msgpack struct{} `msgpack:",asArray"` - Id uint - Msg string - Name string + Id uint + Msg string + Name string } func example_connect() (*tarantool.Connection, error) { @@ -118,7 +119,7 @@ func Example() { fmt.Println("Insert Data", resp.Data) // insert new tuple { 11, 1 } - resp, err = client.Insert("test", &Tuple{Id:10, Msg:"test", Name:"one"}) + resp, err = client.Insert("test", &Tuple{Id: 10, Msg: "test", Name: "one"}) fmt.Println("Insert Error", err) fmt.Println("Insert Code", resp.Code) fmt.Println("Insert Data", resp.Data) @@ -167,8 +168,8 @@ func Example() { fmt.Println("Eval Code", resp.Code) fmt.Println("Eval Data", resp.Data) - resp, err = client.Replace("test", &Tuple{Id:11, Msg:"test", Name:"eleven"}) - resp, err = client.Replace("test", &Tuple{Id:12, Msg:"test", Name:"twelve"}) + resp, err = client.Replace("test", &Tuple{Id: 11, Msg: "test", Name: "eleven"}) + resp, err = client.Replace("test", &Tuple{Id: 12, Msg: "test", Name: "twelve"}) var futs [3]*tarantool.Future futs[0] = client.SelectAsync("test", "primary", 0, 2, tarantool.IterLe, tarantool.UintKey{12}) @@ -223,23 +224,21 @@ func Example() { } func ExampleConnection_Queue() { - conn, err := example_connect() + cfg := queue.Cfg{ + Temporary: false, + Kind: queue.FIFO, + Opts: queue.Opts{ + Ttl: 10 * time.Second, + }, + } + + conn, err := queue.Connect(server, opts) if err != nil { fmt.Printf("error in prepare is %v", err) return } defer conn.Close() - cfg := tarantool.TtlQueueCfg{ - tarantool.QueueCfg{ - Temporary: false, - Kind: tarantool.FIFO_TTL_QUEUE, - }, - tarantool.QueueOpts{ - Ttl: 10 * time.Second, - }, - } - q, err := conn.NewQueue("test_queue", cfg) if err != nil { fmt.Printf("error in queue is %v", err) @@ -256,7 +255,7 @@ func ExampleConnection_Queue() { } testData_2 := "test_data_2" - task_2, err := q.PutWithConfig(testData_2, tarantool.QueueOpts{Ttl: 2 * time.Second}) + task_2, err := q.PutWithOpts(testData_2, queue.Opts{Ttl: 2 * time.Second}) if err != nil { fmt.Printf("error in put with config is %v", err) return @@ -276,9 +275,9 @@ func ExampleConnection_Queue() { return } - task, err = q.TakeWithTimeout(2 * time.Second) + task, err = q.TakeTimeout(2 * time.Second) if task != nil { fmt.Printf("Task should be nil, but %s", task) return } -} \ No newline at end of file +} diff --git a/queue/const.go b/queue/const.go new file mode 100644 index 000000000..f984fac62 --- /dev/null +++ b/queue/const.go @@ -0,0 +1,18 @@ +package queue + +const ( + READY = "r" + TAKEN = "t" + DONE = "-" + BURIED = "!" + DELAYED = "~" +) + +type queueType string + +const ( + FIFO queueType = "fifo" + FIFO_TTL queueType = "fifottl" + UTUBE queueType = "utube" + UTUBE_TTL queueType = "utubettl" +) diff --git a/queue.go b/queue/queue.go similarity index 75% rename from queue.go rename to queue/queue.go index b42c09872..e0da76454 100644 --- a/queue.go +++ b/queue/queue.go @@ -1,60 +1,42 @@ -package tarantool +package queue import ( "fmt" + "github.com/tarantool/go-tarantool" "strings" "time" ) type Queue interface { Put(data interface{}) (*Task, error) - PutWithConfig(data interface{}, cfg QueueOpts) (*Task, error) + PutWithOpts(data interface{}, cfg Opts) (*Task, error) Take() (*Task, error) - TakeWithTimeout(timeout time.Duration) (*Task, error) + TakeTimeout(timeout time.Duration) (*Task, error) Drop() error Peek(taskId uint64) (*Task, error) Kick(taskId uint64) (uint64, error) Statistic() (interface{}, error) } +type Connection struct { + c *tarantool.Connection + opts tarantool.Opts +} + type queue struct { name string conn *Connection cmd map[string]string } -type queueCfg interface { - toString() string - getType() string -} - -type QueueCfg struct { +type Cfg struct { Temporary bool // if true, the contents do not persist on disk IfNotExists bool // if true, no error will be returned if the tube already exists Kind queueType + Opts } -func (cfg QueueCfg) toString() string { - return fmt.Sprintf("{ temporary = %v, if_not_exists = %v }", cfg.Temporary, cfg.IfNotExists) -} - -func (cfg QueueCfg) getType() string { - return string(cfg.Kind) -} - -type TtlQueueCfg struct { - QueueCfg - QueueOpts -} - -type QueueOpts struct { - Pri int // task priorities - Ttl time.Duration // task time to live - Ttr time.Duration // task time to execute - Delay time.Duration // delayed execution -} - -func (cfg TtlQueueCfg) toString() string { +func (cfg Cfg) toString() string { params := []string{fmt.Sprintf("temporary = %v, if_not_exists = %v", cfg.Temporary, cfg.IfNotExists)} if cfg.Ttl.Seconds() != 0 { @@ -72,16 +54,23 @@ func (cfg TtlQueueCfg) toString() string { return "{" + strings.Join(params, ",") + "}" } -func (cfg TtlQueueCfg) getType() string { +func (cfg Cfg) getType() string { kind := string(cfg.Kind) if kind == "" { - kind = string(FIFO_QUEUE) + kind = string(FIFO) } return kind } -func (opts QueueOpts) toMap() map[string]interface{} { +type Opts struct { + Pri int // task priorities + Ttl time.Duration // task time to live + Ttr time.Duration // task time to execute + Delay time.Duration // delayed execution +} + +func (opts Opts) toMap() map[string]interface{} { ret := make(map[string]interface{}) if opts.Ttl.Seconds() != 0 { @@ -103,10 +92,21 @@ func (opts QueueOpts) toMap() map[string]interface{} { return ret } -func newQueue(conn *Connection, name string, cfg queueCfg) (Queue, error) { +// Connect creates and configures new Connection +func Connect(addr string, opts tarantool.Opts) (*Connection, error) { + conn, err := tarantool.Connect(addr, opts) + if err != nil { + return nil, err + } + + return &Connection{conn, opts}, nil +} + +// New creates a new queue with config and return Queue. +func (conn *Connection) NewQueue(name string, cfg Cfg) (Queue, error) { var q queue cmd := fmt.Sprintf("queue.create_tube('%s', '%s', %s)", name, cfg.getType(), cfg.toString()) - _, err := conn.Eval(cmd, []interface{}{}) + _, err := conn.c.Eval(cmd, []interface{}{}) if err == nil { q = queue{ name, @@ -117,10 +117,11 @@ func newQueue(conn *Connection, name string, cfg queueCfg) (Queue, error) { return q, err } -func getQueue(conn *Connection, name string) (Queue, error) { +// GetQueue returns an existing queue by name. +func (conn *Connection) GetQueue(name string) (Queue, error) { var q queue cmd := fmt.Sprintf("return queue.tube.%s ~= null", name) - resp, err := conn.Eval(cmd, []interface{}{}) + resp, err := conn.c.Eval(cmd, []interface{}{}) if err != nil { return q, err } @@ -140,13 +141,18 @@ func getQueue(conn *Connection, name string) (Queue, error) { return q, err } +// Close closes Connection. +func (conn *Connection) Close() error { + return conn.c.Close() +} + // Put data to queue. Returns task. func (q queue) Put(data interface{}) (*Task, error) { return q.put(data) } // Put data with options (ttl/ttr/pri/delay) to queue. Returns task. -func (q queue) PutWithConfig(data interface{}, cfg QueueOpts) (*Task, error) { +func (q queue) PutWithOpts(data interface{}, cfg Opts) (*Task, error) { return q.put(data, cfg.toMap()) } @@ -156,7 +162,7 @@ func (q queue) put(p ...interface{}) (*Task, error) { task *Task ) params = append(params, p...) - resp, err := q.conn.Call(q.cmd["put"], params) + resp, err := q.conn.c.Call(q.cmd["put"], params) if err == nil { task, err = toTask(resp.Data, &q) } @@ -174,7 +180,7 @@ func (q queue) Take() (*Task, error) { } // The take request searches for a task in the queue. Waits until a task becomes ready or the timeout expires. -func (q queue) TakeWithTimeout(timeout time.Duration) (*Task, error) { +func (q queue) TakeTimeout(timeout time.Duration) (*Task, error) { if q.conn.opts.Timeout > 0 && timeout > q.conn.opts.Timeout { timeout = q.conn.opts.Timeout } @@ -183,7 +189,7 @@ func (q queue) TakeWithTimeout(timeout time.Duration) (*Task, error) { func (q queue) take(params interface{}) (*Task, error) { var t *Task - resp, err := q.conn.Call(q.cmd["take"], []interface{}{params}) + resp, err := q.conn.c.Call(q.cmd["take"], []interface{}{params}) if err == nil { t, err = toTask(resp.Data, &q) } @@ -192,13 +198,13 @@ func (q queue) take(params interface{}) (*Task, error) { // Drop queue. func (q queue) Drop() error { - _, err := q.conn.Call(q.cmd["drop"], []interface{}{}) + _, err := q.conn.c.Call(q.cmd["drop"], []interface{}{}) return err } // Look at a task without changing its state. func (q queue) Peek(taskId uint64) (*Task, error) { - resp, err := q.conn.Call(q.cmd["peek"], []interface{}{taskId}) + resp, err := q.conn.c.Call(q.cmd["peek"], []interface{}{taskId}) if err != nil { return nil, err } @@ -220,13 +226,13 @@ func (q queue) _bury(taskId uint64) (string, error) { return q.produce("bury", taskId) } -func (q queue) _release(taskId uint64, cfg QueueOpts) (string, error) { +func (q queue) _release(taskId uint64, cfg Opts) (string, error) { return q.produce("release", taskId, cfg.toMap()) } func (q queue) produce(cmd string, p ...interface{}) (string, error) { var params []interface{} params = append(params, p...) - resp, err := q.conn.Call(q.cmd[cmd], params) + resp, err := q.conn.c.Call(q.cmd[cmd], params) if err != nil { return "", err } @@ -240,7 +246,7 @@ func (q queue) produce(cmd string, p ...interface{}) (string, error) { // Reverse the effect of a bury request on one or more tasks. func (q queue) Kick(count uint64) (uint64, error) { - resp, err := q.conn.Call(q.cmd["kick"], []interface{}{count}) + resp, err := q.conn.c.Call(q.cmd["kick"], []interface{}{count}) var id uint64 if err == nil { id = resp.Data[0].([]interface{})[0].(uint64) @@ -250,7 +256,7 @@ func (q queue) Kick(count uint64) (uint64, error) { // Return the number of tasks in a queue broken down by task_state, and the number of requests broken down by the type of request. func (q queue) Statistic() (interface{}, error) { - resp, err := q.conn.Call(q.cmd["statistics"], []interface{}{}) + resp, err := q.conn.c.Call(q.cmd["statistics"], []interface{}{}) if err != nil { return nil, err } diff --git a/task.go b/queue/task.go similarity index 92% rename from task.go rename to queue/task.go index c01a0d38f..7fd52b102 100644 --- a/task.go +++ b/queue/task.go @@ -1,4 +1,4 @@ -package tarantool +package queue type Task struct { id uint64 @@ -49,15 +49,15 @@ func (t *Task) produce(f func(taskId uint64) (string, error)) error { // Put the task back in the queue, 'release' implies unsuccessful completion of a taken task. func (t *Task) Release() error { - return t.release(QueueOpts{}) + return t.release(Opts{}) } // Put the task back in the queue with config, 'release' implies unsuccessful completion of a taken task. -func (t *Task) ReleaseWithConfig(cfg QueueOpts) error { +func (t *Task) ReleaseWithConfig(cfg Opts) error { return t.release(cfg) } -func (t *Task) release(cfg QueueOpts) error { +func (t *Task) release(cfg Opts) error { newStatus, err := t.q._release(t.id, cfg) if err != nil { return err diff --git a/request.go b/request.go index 908e197b4..9df52ea35 100644 --- a/request.go +++ b/request.go @@ -313,16 +313,6 @@ func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { }) } -// NewQueue creates a new queue with config and return Queue. -func (conn *Connection) NewQueue(name string, cfg queueCfg) (Queue, error) { - return newQueue(conn, name, cfg) -} - -// GetQueue returns an existing queue by name. -func (conn *Connection) GetQueue(name string) (Queue, error) { - return getQueue(conn, name) -} - // // private // diff --git a/tarantool_test.go b/tarantool_test.go index 61d45cfc8..a0aee72e1 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -8,6 +8,7 @@ import ( "sync" "testing" "time" + "github.com/tarantool/go-tarantool/queue" ) type Member struct { @@ -93,6 +94,8 @@ var opts = Opts{ //RateLimit: 4*1024, } + + const N = 500 func BenchmarkClientSerial(b *testing.B) { @@ -988,7 +991,7 @@ func TestComplexStructs(t *testing.T) { /////////QUEUE///////// func TestFifoQueue(t *testing.T) { - conn, err := Connect(server, opts) + conn, err := queue.Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1000,7 +1003,7 @@ func TestFifoQueue(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1014,7 +1017,7 @@ func TestFifoQueue(t *testing.T) { } func TestFifoQueue_GetExist_Statistic(t *testing.T) { - conn, err := Connect(server, opts) + conn, err := queue.Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1026,7 +1029,7 @@ func TestFifoQueue_GetExist_Statistic(t *testing.T) { defer conn.Close() name := "test_queue" - _, err = conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + _, err = conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1059,7 +1062,7 @@ func TestFifoQueue_GetExist_Statistic(t *testing.T) { } func TestFifoQueue_Put(t *testing.T) { - conn, err := Connect(server, opts) + conn, err := queue.Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1071,7 +1074,7 @@ func TestFifoQueue_Put(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1102,7 +1105,7 @@ func TestFifoQueue_Put(t *testing.T) { } func TestFifoQueue_Take(t *testing.T) { - conn, err := Connect(server, opts) + conn, err := queue.Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1114,7 +1117,7 @@ func TestFifoQueue_Take(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1145,7 +1148,7 @@ func TestFifoQueue_Take(t *testing.T) { //Take - task, err = q.TakeWithTimeout(2 * time.Second) + task, err = q.TakeTimeout(2 * time.Second) if err != nil { t.Errorf("Failed take from queue: %s", err.Error()) } else if task == nil { @@ -1169,7 +1172,7 @@ func TestFifoQueue_Take(t *testing.T) { } func TestFifoQueue_Peek(t *testing.T) { - conn, err := Connect(server, opts) + conn, err := queue.Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1181,7 +1184,7 @@ func TestFifoQueue_Peek(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1229,7 +1232,7 @@ func TestFifoQueue_Peek(t *testing.T) { } func TestFifoQueue_Bury_Kick(t *testing.T) { - conn, err := Connect(server, opts) + conn, err := queue.Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1241,7 +1244,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1291,7 +1294,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { } //Take - task, err = q.TakeWithTimeout(2 * time.Second) + task, err = q.TakeTimeout(2 * time.Second) if err != nil { t.Errorf("Failed take from queue: %s", err.Error()) } else if task == nil { @@ -1315,7 +1318,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { } func TestFifoQueue_Delete(t *testing.T) { - conn, err := Connect(server, opts) + conn, err := queue.Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1327,7 +1330,7 @@ func TestFifoQueue_Delete(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1366,7 +1369,7 @@ func TestFifoQueue_Delete(t *testing.T) { } //Take - task, err = q.TakeWithTimeout(2 * time.Second) + task, err = q.TakeTimeout(2 * time.Second) if err != nil { t.Errorf("Failed take from queue: %s", err.Error()) } else if task != nil { @@ -1375,7 +1378,7 @@ func TestFifoQueue_Delete(t *testing.T) { } func TestFifoQueue_Release(t *testing.T) { - conn, err := Connect(server, opts) + conn, err := queue.Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1387,7 +1390,7 @@ func TestFifoQueue_Release(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, QueueCfg{Temporary: true, Kind: FIFO_QUEUE}) + q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1465,21 +1468,18 @@ func TestFifoQueue_Release(t *testing.T) { } func TestTtlQueue(t *testing.T) { - conn, err := Connect(server, opts) + conn, err := queue.Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } defer conn.Close() name := "test_queue" - cfg := TtlQueueCfg{ - QueueCfg{Temporary: true, Kind: FIFO_TTL_QUEUE}, - QueueOpts{Ttl: 5*time.Second}, + cfg := queue.Cfg{ + Temporary: true, + Kind: queue.FIFO_TTL, + Opts: queue.Opts{Ttl: 5*time.Second}, } q, err := conn.NewQueue(name, cfg) if err != nil { @@ -1512,7 +1512,7 @@ func TestTtlQueue(t *testing.T) { time.Sleep(5 * time.Second) //Take - task, err = q.TakeWithTimeout(2 * time.Second) + task, err = q.TakeTimeout(2 * time.Second) if err != nil { t.Errorf("Failed take from queue: %s", err.Error()) } else if task != nil { @@ -1521,7 +1521,7 @@ func TestTtlQueue(t *testing.T) { } func TestTtlQueue_Put(t *testing.T) { - conn, err := Connect(server, opts) + conn, err := queue.Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1533,9 +1533,10 @@ func TestTtlQueue_Put(t *testing.T) { defer conn.Close() name := "test_queue" - cfg := TtlQueueCfg{ - QueueCfg{Temporary: true, Kind: FIFO_TTL_QUEUE}, - QueueOpts{Ttl: 5*time.Second}, + cfg := queue.Cfg{ + Temporary: true, + Kind: queue.FIFO_TTL, + Opts: queue.Opts{Ttl: 5*time.Second}, } q, err := conn.NewQueue(name, cfg) if err != nil { @@ -1552,7 +1553,7 @@ func TestTtlQueue_Put(t *testing.T) { }() putData := "put_data" - task, err := q.PutWithConfig(putData, QueueOpts{Ttl: 10 * time.Second}) + task, err := q.PutWithOpts(putData, queue.Opts{Ttl: 10 * time.Second}) if err != nil { t.Errorf("Failed put to queue: %s", err.Error()) return @@ -1568,7 +1569,7 @@ func TestTtlQueue_Put(t *testing.T) { time.Sleep(5 * time.Second) //Take - task, err = q.TakeWithTimeout(2 * time.Second) + task, err = q.TakeTimeout(2 * time.Second) if err != nil { t.Errorf("Failed take from queue: %s", err.Error()) } else if task == nil { From 5d462cda2dcaf453e6b52b98753204ae0f901216 Mon Sep 17 00:00:00 2001 From: Nikita Galushko Date: Thu, 6 Apr 2017 10:14:22 +0300 Subject: [PATCH 179/605] fix readme; remove queueConnection --- README.md | 23 ++++++++--------- connection.go | 5 ++++ example_test.go | 4 +-- queue/queue.go | 65 +++++++++++++++-------------------------------- tarantool_test.go | 42 +++++++++++++++--------------- 5 files changed, 59 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 0da00aee0..8d775b27a 100644 --- a/README.md +++ b/README.md @@ -503,26 +503,25 @@ func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { ## Working-with-queue ```go + +import "github.com/tarantool/go-tarantool/queue" + opts := tarantool.Opts{...} conn, err := tarantool.Connect("127.0.0.1:3301", opts) -cfg := tarantool.TtlQueueCfg{ - tarantool.QueueCfg{ +cfg := queue.Cfg{ Temporary: true, IfNotExist: true, Kind: tarantool.FIFO_TTL_QUEUE, - }, - tarantool.QueueOpts{ - Ttl: 10 * time.Second, - Ttr: 5 * time.Second, - Delay: 3 * time.Second, - Pri: 1, - }, + Opts: queue.Opts{ + Ttl: 10 * time.Second, + Ttr: 5 * time.Second, + Delay: 3 * time.Second, + Pri: 1, + }, } - //If you want simple fifo queue use only tarantool.QueueCfg{...} - -queue, err := conn.NewQueue("test_queue", cfg) +queue, err := queue.NewQueue(conn, "test_queue", cfg) task, err := queue.Put("test_data") fmt.Println("Task id is ", task.GetId()) diff --git a/connection.go b/connection.go index 4d5235279..b7e228dab 100644 --- a/connection.go +++ b/connection.go @@ -794,3 +794,8 @@ func (conn *Connection) read(r io.Reader) (response []byte, err error) { func (conn *Connection) nextRequestId() (requestId uint32) { return atomic.AddUint32(&conn.requestId, 1) } + +// ConfiguredTimeout returns timeout from connection config +func (conn *Connection) ConfiguredTimeout() time.Duration { + return conn.opts.Timeout +} \ No newline at end of file diff --git a/example_test.go b/example_test.go index 34fa8d2da..c58e99851 100644 --- a/example_test.go +++ b/example_test.go @@ -232,14 +232,14 @@ func ExampleConnection_Queue() { }, } - conn, err := queue.Connect(server, opts) + conn, err := tarantool.Connect(server, opts) if err != nil { fmt.Printf("error in prepare is %v", err) return } defer conn.Close() - q, err := conn.NewQueue("test_queue", cfg) + q, err := queue.NewQueue(conn, "test_queue", cfg) if err != nil { fmt.Printf("error in queue is %v", err) return diff --git a/queue/queue.go b/queue/queue.go index e0da76454..15bc25064 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -18,14 +18,9 @@ type Queue interface { Statistic() (interface{}, error) } -type Connection struct { - c *tarantool.Connection - opts tarantool.Opts -} - type queue struct { name string - conn *Connection + conn *tarantool.Connection cmd map[string]string } @@ -92,21 +87,11 @@ func (opts Opts) toMap() map[string]interface{} { return ret } -// Connect creates and configures new Connection -func Connect(addr string, opts tarantool.Opts) (*Connection, error) { - conn, err := tarantool.Connect(addr, opts) - if err != nil { - return nil, err - } - - return &Connection{conn, opts}, nil -} - // New creates a new queue with config and return Queue. -func (conn *Connection) NewQueue(name string, cfg Cfg) (Queue, error) { +func NewQueue(conn *tarantool.Connection, name string, cfg Cfg) (Queue, error) { var q queue cmd := fmt.Sprintf("queue.create_tube('%s', '%s', %s)", name, cfg.getType(), cfg.toString()) - _, err := conn.c.Eval(cmd, []interface{}{}) + _, err := conn.Eval(cmd, []interface{}{}) if err == nil { q = queue{ name, @@ -118,10 +103,10 @@ func (conn *Connection) NewQueue(name string, cfg Cfg) (Queue, error) { } // GetQueue returns an existing queue by name. -func (conn *Connection) GetQueue(name string) (Queue, error) { +func GetQueue(conn *tarantool.Connection, name string) (Queue, error) { var q queue cmd := fmt.Sprintf("return queue.tube.%s ~= null", name) - resp, err := conn.c.Eval(cmd, []interface{}{}) + resp, err := conn.Eval(cmd, []interface{}{}) if err != nil { return q, err } @@ -141,11 +126,6 @@ func (conn *Connection) GetQueue(name string) (Queue, error) { return q, err } -// Close closes Connection. -func (conn *Connection) Close() error { - return conn.c.Close() -} - // Put data to queue. Returns task. func (q queue) Put(data interface{}) (*Task, error) { return q.put(data) @@ -156,13 +136,9 @@ func (q queue) PutWithOpts(data interface{}, cfg Opts) (*Task, error) { return q.put(data, cfg.toMap()) } -func (q queue) put(p ...interface{}) (*Task, error) { - var ( - params []interface{} - task *Task - ) - params = append(params, p...) - resp, err := q.conn.c.Call(q.cmd["put"], params) +func (q queue) put(params ...interface{}) (*Task, error) { + var task *Task + resp, err := q.conn.Call(q.cmd["put"], params) if err == nil { task, err = toTask(resp.Data, &q) } @@ -173,23 +149,24 @@ func (q queue) put(p ...interface{}) (*Task, error) { // The take request searches for a task in the queue. func (q queue) Take() (*Task, error) { var params interface{} - if q.conn.opts.Timeout > 0 { - params = q.conn.opts.Timeout.Seconds() + if q.conn.ConfiguredTimeout() > 0 { + params = q.conn.ConfiguredTimeout().Seconds() } return q.take(params) } // The take request searches for a task in the queue. Waits until a task becomes ready or the timeout expires. func (q queue) TakeTimeout(timeout time.Duration) (*Task, error) { - if q.conn.opts.Timeout > 0 && timeout > q.conn.opts.Timeout { - timeout = q.conn.opts.Timeout + t := q.conn.ConfiguredTimeout() + if t > 0 && timeout > t { + timeout = t } return q.take(timeout.Seconds()) } func (q queue) take(params interface{}) (*Task, error) { var t *Task - resp, err := q.conn.c.Call(q.cmd["take"], []interface{}{params}) + resp, err := q.conn.Call(q.cmd["take"], []interface{}{params}) if err == nil { t, err = toTask(resp.Data, &q) } @@ -198,13 +175,13 @@ func (q queue) take(params interface{}) (*Task, error) { // Drop queue. func (q queue) Drop() error { - _, err := q.conn.c.Call(q.cmd["drop"], []interface{}{}) + _, err := q.conn.Call(q.cmd["drop"], []interface{}{}) return err } // Look at a task without changing its state. func (q queue) Peek(taskId uint64) (*Task, error) { - resp, err := q.conn.c.Call(q.cmd["peek"], []interface{}{taskId}) + resp, err := q.conn.Call(q.cmd["peek"], []interface{}{taskId}) if err != nil { return nil, err } @@ -229,10 +206,8 @@ func (q queue) _bury(taskId uint64) (string, error) { func (q queue) _release(taskId uint64, cfg Opts) (string, error) { return q.produce("release", taskId, cfg.toMap()) } -func (q queue) produce(cmd string, p ...interface{}) (string, error) { - var params []interface{} - params = append(params, p...) - resp, err := q.conn.c.Call(q.cmd[cmd], params) +func (q queue) produce(cmd string, params ...interface{}) (string, error) { + resp, err := q.conn.Call(q.cmd[cmd], params) if err != nil { return "", err } @@ -246,7 +221,7 @@ func (q queue) produce(cmd string, p ...interface{}) (string, error) { // Reverse the effect of a bury request on one or more tasks. func (q queue) Kick(count uint64) (uint64, error) { - resp, err := q.conn.c.Call(q.cmd["kick"], []interface{}{count}) + resp, err := q.conn.Call(q.cmd["kick"], []interface{}{count}) var id uint64 if err == nil { id = resp.Data[0].([]interface{})[0].(uint64) @@ -256,7 +231,7 @@ func (q queue) Kick(count uint64) (uint64, error) { // Return the number of tasks in a queue broken down by task_state, and the number of requests broken down by the type of request. func (q queue) Statistic() (interface{}, error) { - resp, err := q.conn.c.Call(q.cmd["statistics"], []interface{}{}) + resp, err := q.conn.Call(q.cmd["statistics"], []interface{}{}) if err != nil { return nil, err } diff --git a/tarantool_test.go b/tarantool_test.go index a0aee72e1..355495c88 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -991,7 +991,7 @@ func TestComplexStructs(t *testing.T) { /////////QUEUE///////// func TestFifoQueue(t *testing.T) { - conn, err := queue.Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1003,7 +1003,7 @@ func TestFifoQueue(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1017,7 +1017,7 @@ func TestFifoQueue(t *testing.T) { } func TestFifoQueue_GetExist_Statistic(t *testing.T) { - conn, err := queue.Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1029,13 +1029,13 @@ func TestFifoQueue_GetExist_Statistic(t *testing.T) { defer conn.Close() name := "test_queue" - _, err = conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + _, err = queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return } - q, err := conn.GetQueue(name) + q, err := queue.GetQueue(conn, name) if err != nil { t.Errorf("Failed to get exist queue: %s", err.Error()) return @@ -1062,7 +1062,7 @@ func TestFifoQueue_GetExist_Statistic(t *testing.T) { } func TestFifoQueue_Put(t *testing.T) { - conn, err := queue.Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1074,7 +1074,7 @@ func TestFifoQueue_Put(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1105,7 +1105,7 @@ func TestFifoQueue_Put(t *testing.T) { } func TestFifoQueue_Take(t *testing.T) { - conn, err := queue.Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1117,7 +1117,7 @@ func TestFifoQueue_Take(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1172,7 +1172,7 @@ func TestFifoQueue_Take(t *testing.T) { } func TestFifoQueue_Peek(t *testing.T) { - conn, err := queue.Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1184,7 +1184,7 @@ func TestFifoQueue_Peek(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1232,7 +1232,7 @@ func TestFifoQueue_Peek(t *testing.T) { } func TestFifoQueue_Bury_Kick(t *testing.T) { - conn, err := queue.Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1244,7 +1244,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1318,7 +1318,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { } func TestFifoQueue_Delete(t *testing.T) { - conn, err := queue.Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1330,7 +1330,7 @@ func TestFifoQueue_Delete(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1378,7 +1378,7 @@ func TestFifoQueue_Delete(t *testing.T) { } func TestFifoQueue_Release(t *testing.T) { - conn, err := queue.Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1390,7 +1390,7 @@ func TestFifoQueue_Release(t *testing.T) { defer conn.Close() name := "test_queue" - q, err := conn.NewQueue(name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1468,7 +1468,7 @@ func TestFifoQueue_Release(t *testing.T) { } func TestTtlQueue(t *testing.T) { - conn, err := queue.Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1481,7 +1481,7 @@ func TestTtlQueue(t *testing.T) { Kind: queue.FIFO_TTL, Opts: queue.Opts{Ttl: 5*time.Second}, } - q, err := conn.NewQueue(name, cfg) + q, err := queue.NewQueue(conn, name, cfg) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return @@ -1521,7 +1521,7 @@ func TestTtlQueue(t *testing.T) { } func TestTtlQueue_Put(t *testing.T) { - conn, err := queue.Connect(server, opts) + conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) return @@ -1538,7 +1538,7 @@ func TestTtlQueue_Put(t *testing.T) { Kind: queue.FIFO_TTL, Opts: queue.Opts{Ttl: 5*time.Second}, } - q, err := conn.NewQueue(name, cfg) + q, err := queue.NewQueue(conn, name, cfg) if err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return From 7912c39b2fc03ce77a51c9908393471912f40ece Mon Sep 17 00:00:00 2001 From: Nikita Galushko Date: Fri, 7 Apr 2017 13:47:57 +0300 Subject: [PATCH 180/605] fix statistics; change map to struct; use pointers --- queue/queue.go | 111 +++++++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 49 deletions(-) diff --git a/queue/queue.go b/queue/queue.go index 15bc25064..a891e592a 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -21,7 +21,20 @@ type Queue interface { type queue struct { name string conn *tarantool.Connection - cmd map[string]string + cmds *cmd +} + +type cmd struct { + put string + take string + drop string + peek string + ack string + delete string + bury string + kick string + release string + statistics string } type Cfg struct { @@ -89,14 +102,14 @@ func (opts Opts) toMap() map[string]interface{} { // New creates a new queue with config and return Queue. func NewQueue(conn *tarantool.Connection, name string, cfg Cfg) (Queue, error) { - var q queue + var q *queue cmd := fmt.Sprintf("queue.create_tube('%s', '%s', %s)", name, cfg.getType(), cfg.toString()) _, err := conn.Eval(cmd, []interface{}{}) if err == nil { - q = queue{ + q = &queue{ name, conn, - makeCmdMap(name), + makeCmd(name), } } return q, err @@ -104,7 +117,7 @@ func NewQueue(conn *tarantool.Connection, name string, cfg Cfg) (Queue, error) { // GetQueue returns an existing queue by name. func GetQueue(conn *tarantool.Connection, name string) (Queue, error) { - var q queue + var q *queue cmd := fmt.Sprintf("return queue.tube.%s ~= null", name) resp, err := conn.Eval(cmd, []interface{}{}) if err != nil { @@ -114,10 +127,10 @@ func GetQueue(conn *tarantool.Connection, name string) (Queue, error) { exist := len(resp.Data) != 0 && resp.Data[0].(bool) if exist { - q = queue{ + q = &queue{ name, conn, - makeCmdMap(name), + makeCmd(name), } } else { err = fmt.Errorf("Tube %s does not exist", name) @@ -127,27 +140,27 @@ func GetQueue(conn *tarantool.Connection, name string) (Queue, error) { } // Put data to queue. Returns task. -func (q queue) Put(data interface{}) (*Task, error) { +func (q *queue) Put(data interface{}) (*Task, error) { return q.put(data) } // Put data with options (ttl/ttr/pri/delay) to queue. Returns task. -func (q queue) PutWithOpts(data interface{}, cfg Opts) (*Task, error) { +func (q *queue) PutWithOpts(data interface{}, cfg Opts) (*Task, error) { return q.put(data, cfg.toMap()) } -func (q queue) put(params ...interface{}) (*Task, error) { +func (q *queue) put(params ...interface{}) (*Task, error) { var task *Task - resp, err := q.conn.Call(q.cmd["put"], params) + resp, err := q.conn.Call(q.cmds.put, params) if err == nil { - task, err = toTask(resp.Data, &q) + task, err = toTask(resp.Data, q) } return task, err } // The take request searches for a task in the queue. -func (q queue) Take() (*Task, error) { +func (q *queue) Take() (*Task, error) { var params interface{} if q.conn.ConfiguredTimeout() > 0 { params = q.conn.ConfiguredTimeout().Seconds() @@ -156,7 +169,7 @@ func (q queue) Take() (*Task, error) { } // The take request searches for a task in the queue. Waits until a task becomes ready or the timeout expires. -func (q queue) TakeTimeout(timeout time.Duration) (*Task, error) { +func (q *queue) TakeTimeout(timeout time.Duration) (*Task, error) { t := q.conn.ConfiguredTimeout() if t > 0 && timeout > t { timeout = t @@ -164,55 +177,55 @@ func (q queue) TakeTimeout(timeout time.Duration) (*Task, error) { return q.take(timeout.Seconds()) } -func (q queue) take(params interface{}) (*Task, error) { +func (q *queue) take(params interface{}) (*Task, error) { var t *Task - resp, err := q.conn.Call(q.cmd["take"], []interface{}{params}) + resp, err := q.conn.Call(q.cmds.take, []interface{}{params}) if err == nil { - t, err = toTask(resp.Data, &q) + t, err = toTask(resp.Data, q) } return t, err } // Drop queue. -func (q queue) Drop() error { - _, err := q.conn.Call(q.cmd["drop"], []interface{}{}) +func (q *queue) Drop() error { + _, err := q.conn.Call(q.cmds.drop, []interface{}{}) return err } // Look at a task without changing its state. -func (q queue) Peek(taskId uint64) (*Task, error) { - resp, err := q.conn.Call(q.cmd["peek"], []interface{}{taskId}) +func (q *queue) Peek(taskId uint64) (*Task, error) { + resp, err := q.conn.Call(q.cmds.peek, []interface{}{taskId}) if err != nil { return nil, err } - t, err := toTask(resp.Data, &q) + t, err := toTask(resp.Data, q) return t, err } -func (q queue) _ack(taskId uint64) (string, error) { - return q.produce("ack", taskId) +func (q *queue) _ack(taskId uint64) (string, error) { + return q.produce(q.cmds.ack, taskId) } -func (q queue) _delete(taskId uint64) (string, error) { - return q.produce("delete", taskId) +func (q *queue) _delete(taskId uint64) (string, error) { + return q.produce(q.cmds.delete, taskId) } -func (q queue) _bury(taskId uint64) (string, error) { - return q.produce("bury", taskId) +func (q *queue) _bury(taskId uint64) (string, error) { + return q.produce(q.cmds.bury, taskId) } -func (q queue) _release(taskId uint64, cfg Opts) (string, error) { - return q.produce("release", taskId, cfg.toMap()) +func (q *queue) _release(taskId uint64, cfg Opts) (string, error) { + return q.produce(q.cmds.release, taskId, cfg.toMap()) } -func (q queue) produce(cmd string, params ...interface{}) (string, error) { - resp, err := q.conn.Call(q.cmd[cmd], params) +func (q *queue) produce(cmd string, params ...interface{}) (string, error) { + resp, err := q.conn.Call(cmd, params) if err != nil { return "", err } - t, err := toTask(resp.Data, &q) + t, err := toTask(resp.Data, q) if err != nil { return "", err } @@ -220,8 +233,8 @@ func (q queue) produce(cmd string, params ...interface{}) (string, error) { } // Reverse the effect of a bury request on one or more tasks. -func (q queue) Kick(count uint64) (uint64, error) { - resp, err := q.conn.Call(q.cmd["kick"], []interface{}{count}) +func (q *queue) Kick(count uint64) (uint64, error) { + resp, err := q.conn.Call(q.cmds.kick, []interface{}{count}) var id uint64 if err == nil { id = resp.Data[0].([]interface{})[0].(uint64) @@ -230,8 +243,8 @@ func (q queue) Kick(count uint64) (uint64, error) { } // Return the number of tasks in a queue broken down by task_state, and the number of requests broken down by the type of request. -func (q queue) Statistic() (interface{}, error) { - resp, err := q.conn.Call(q.cmd["statistics"], []interface{}{}) +func (q *queue) Statistic() (interface{}, error) { + resp, err := q.conn.Call(q.cmds.statistics, []interface{}{q.name}) if err != nil { return nil, err } @@ -246,18 +259,18 @@ func (q queue) Statistic() (interface{}, error) { return nil, nil } -func makeCmdMap(name string) map[string]string { - return map[string]string{ - "put": "queue.tube." + name + ":put", - "take": "queue.tube." + name + ":take", - "drop": "queue.tube." + name + ":drop", - "peek": "queue.tube." + name + ":peek", - "ack": "queue.tube." + name + ":ack", - "delete": "queue.tube." + name + ":delete", - "bury": "queue.tube." + name + ":bury", - "kick": "queue.tube." + name + ":kick", - "release": "queue.tube." + name + ":release", - "statistics": "queue.statistics", +func makeCmd(name string) *cmd { + return &cmd{ + put: "queue.tube." + name + ":put", + take: "queue.tube." + name + ":take", + drop: "queue.tube." + name + ":drop", + peek: "queue.tube." + name + ":peek", + ack: "queue.tube." + name + ":ack", + delete: "queue.tube." + name + ":delete", + bury: "queue.tube." + name + ":bury", + kick: "queue.tube." + name + ":kick", + release: "queue.tube." + name + ":release", + statistics: "queue.statistics", } } From d26edc79c694c2672a6541f6a9869325d61b2427 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 9 Apr 2017 23:27:30 +0300 Subject: [PATCH 181/605] refactor queue interface --- README.md | 13 +- example_test.go | 60 ---- queue/config.lua | 29 ++ queue/example_test.go | 66 +++++ queue/queue.go | 125 ++++----- queue/queue_test.go | 624 ++++++++++++++++++++++++++++++++++++++++++ queue/task.go | 69 +++-- tarantool_test.go | 607 ---------------------------------------- 8 files changed, 820 insertions(+), 773 deletions(-) create mode 100644 queue/config.lua create mode 100644 queue/example_test.go create mode 100644 queue/queue_test.go diff --git a/README.md b/README.md index 8d775b27a..3cd7356c8 100644 --- a/README.md +++ b/README.md @@ -521,18 +521,19 @@ cfg := queue.Cfg{ }, } -queue, err := queue.NewQueue(conn, "test_queue", cfg) -task, err := queue.Put("test_data") +que := queue.New(conn, "test_queue") +err = que.Create(cfg) +task, err := que.Put("test_data") fmt.Println("Task id is ", task.GetId()) -task, err = queue.Take() //blocking operation +task, err = que.Take() //blocking operation fmt.Println("Data is ", task.GetData()) task.Ack() -task, err = queue.Put([]int{1, 2, 3}) +task, err = que.Put([]int{1, 2, 3}) task.Bury() -task, err = queue.TakeWithTimeout(2 * time.Second) +task, err = que.TakeTimeout(2 * time.Second) if task == nil { fmt.Println("Task is nil") } @@ -542,4 +543,4 @@ q.Drop() Features of the implementation: - If you use connection timeout and call `TakeWithTimeout` with parameter greater than the connection timeout then parameter reduced to it -- If you use connection timeout and call `Take` then we return a error if we can not take task from queue in a time equal to the connection timeout \ No newline at end of file +- If you use connection timeout and call `Take` then we return a error if we can not take task from queue in a time equal to the connection timeout diff --git a/example_test.go b/example_test.go index c58e99851..4176025ee 100644 --- a/example_test.go +++ b/example_test.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/tarantool/go-tarantool" "time" - "github.com/tarantool/go-tarantool/queue" ) type Tuple struct { @@ -222,62 +221,3 @@ func Example() { // Fut 2 Error // Fut 2 Data [[15 val 15 bla]] } - -func ExampleConnection_Queue() { - cfg := queue.Cfg{ - Temporary: false, - Kind: queue.FIFO, - Opts: queue.Opts{ - Ttl: 10 * time.Second, - }, - } - - conn, err := tarantool.Connect(server, opts) - if err != nil { - fmt.Printf("error in prepare is %v", err) - return - } - defer conn.Close() - - q, err := queue.NewQueue(conn, "test_queue", cfg) - if err != nil { - fmt.Printf("error in queue is %v", err) - return - } - - defer q.Drop() - - testData_1 := "test_data_1" - _, err = q.Put(testData_1) - if err != nil { - fmt.Printf("error in put is %v", err) - return - } - - testData_2 := "test_data_2" - task_2, err := q.PutWithOpts(testData_2, queue.Opts{Ttl: 2 * time.Second}) - if err != nil { - fmt.Printf("error in put with config is %v", err) - return - } - - task, err := q.Take() - if err != nil { - fmt.Printf("error in take with is %v", err) - return - } - task.Ack() - fmt.Println("data_1: ", task.GetData()) - - err = task_2.Bury() - if err != nil { - fmt.Printf("error in bury with is %v", err) - return - } - - task, err = q.TakeTimeout(2 * time.Second) - if task != nil { - fmt.Printf("Task should be nil, but %s", task) - return - } -} diff --git a/queue/config.lua b/queue/config.lua new file mode 100644 index 000000000..2488ff300 --- /dev/null +++ b/queue/config.lua @@ -0,0 +1,29 @@ +queue = require 'queue' + +box.cfg{ + listen = 3013, + wal_dir='xlog', + snap_dir='snap', +} + +box.once("init", function() +box.schema.user.create('test', {password = 'test'}) +box.schema.func.create('queue.tube.test_queue:ack') +box.schema.func.create('queue.tube.test_queue:put') +box.schema.func.create('queue.tube.test_queue:drop') +box.schema.func.create('queue.tube.test_queue:peek') +box.schema.func.create('queue.tube.test_queue:kick') +box.schema.func.create('queue.tube.test_queue:take') +box.schema.func.create('queue.tube.test_queue:delete') +box.schema.func.create('queue.tube.test_queue:release') +box.schema.func.create('queue.tube.test_queue:bury') +box.schema.func.create('queue.statistics') +box.schema.user.grant('test', 'execute', 'universe') +box.schema.user.grant('test', 'read,write', 'space', '_queue') +box.schema.user.grant('test', 'read,write', 'space', '_schema') +box.schema.user.grant('test', 'read,write', 'space', '_space') +box.schema.user.grant('test', 'read,write', 'space', '_index') +box.schema.user.grant('test', 'read,write', 'space', '_queue_consumers') +box.schema.user.grant('test', 'read,write', 'space', '_priv') +box.schema.user.grant('test', 'read,write', 'space', '_queue_taken') +end) diff --git a/queue/example_test.go b/queue/example_test.go new file mode 100644 index 000000000..21b83ccf0 --- /dev/null +++ b/queue/example_test.go @@ -0,0 +1,66 @@ +package queue_test + +import ( + "fmt" + "github.com/tarantool/go-tarantool" + "time" + "github.com/tarantool/go-tarantool/queue" +) + +func ExampleConnection_Queue() { + cfg := queue.Cfg{ + Temporary: false, + Kind: queue.FIFO, + Opts: queue.Opts{ + Ttl: 10 * time.Second, + }, + } + + conn, err := tarantool.Connect(server, opts) + if err != nil { + fmt.Printf("error in prepare is %v", err) + return + } + defer conn.Close() + + q := queue.New(conn, "test_queue") + if err := q.Create(cfg); err != nil { + fmt.Printf("error in queue is %v", err) + return + } + + defer q.Drop() + + testData_1 := "test_data_1" + if _, err = q.Put(testData_1); err != nil { + fmt.Printf("error in put is %v", err) + return + } + + testData_2 := "test_data_2" + task_2, err := q.PutWithOpts(testData_2, queue.Opts{Ttl: 2 * time.Second}) + if err != nil { + fmt.Printf("error in put with config is %v", err) + return + } + + task, err := q.Take() + if err != nil { + fmt.Printf("error in take with is %v", err) + return + } + task.Ack() + fmt.Println("data_1: ", task.Data()) + + err = task_2.Bury() + if err != nil { + fmt.Printf("error in bury with is %v", err) + return + } + + task, err = q.TakeTimeout(2 * time.Second) + if task != nil { + fmt.Printf("Task should be nil, but %s", task) + return + } +} diff --git a/queue/queue.go b/queue/queue.go index a891e592a..0d8c2a968 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -1,27 +1,49 @@ package queue import ( - "fmt" "github.com/tarantool/go-tarantool" - "strings" "time" ) +// Queue is a handle to tarantool queue's tube type Queue interface { + // Exists checks tube for existence + // Note: it uses Eval, so user needs 'execute universe' privilege + Exists() (bool, error) + // Create creates new tube with configuration + // Note: it uses Eval, so user needs 'execute universe' privilege + // Note: you'd better not use this function in your application, cause it is + // administrative task to create or delete queue. + Create(cfg Cfg) (error) + // Drop destroys tube. + // Note: you'd better not use this function in your application, cause it is + // administrative task to create or delete queue. + Drop() error + // Put creates new task in a tube Put(data interface{}) (*Task, error) + // PutWithOpts creates new task with options different from tube's defaults PutWithOpts(data interface{}, cfg Opts) (*Task, error) + // Take takes 'ready' task from a tube and marks it as 'in progress' + // Note: if connection has a request Timeout, then 0.9 * connection.Timeout is + // used as a timeout. Take() (*Task, error) + // TakeWithTimout takes 'ready' task from a tube and marks it as "in progress", + // or it is timeouted after "timeout" period. + // Note: if connection has a request Timeout, and conn.Timeout * 0.9 < timeout + // then timeout = conn.Timeout*0.9 TakeTimeout(timeout time.Duration) (*Task, error) - Drop() error + // Peek returns task by its id. Peek(taskId uint64) (*Task, error) - Kick(taskId uint64) (uint64, error) + // Kick reverts effect of Task.Bury() for `count` tasks. + Kick(count uint64) (uint64, error) + // Statistic returns some statistic about queue. Statistic() (interface{}, error) } type queue struct { name string conn *tarantool.Connection - cmds *cmd + cmds cmd } type cmd struct { @@ -44,22 +66,11 @@ type Cfg struct { Opts } -func (cfg Cfg) toString() string { - params := []string{fmt.Sprintf("temporary = %v, if_not_exists = %v", cfg.Temporary, cfg.IfNotExists)} - - if cfg.Ttl.Seconds() != 0 { - params = append(params, fmt.Sprintf("ttl = %f", cfg.Ttl.Seconds())) - } - - if cfg.Ttr.Seconds() != 0 { - params = append(params, fmt.Sprintf("ttr = %f", cfg.Ttr.Seconds())) - } - - if cfg.Delay.Seconds() != 0 { - params = append(params, fmt.Sprintf("delay = %f", cfg.Delay.Seconds())) - } - - return "{" + strings.Join(params, ",") + "}" +func (cfg Cfg) toMap() map[string]interface{} { + res := cfg.Opts.toMap() + res["temporary"] = cfg.Temporary + res["if_not_exists"] = cfg.IfNotExists + return res } func (cfg Cfg) getType() string { @@ -100,43 +111,33 @@ func (opts Opts) toMap() map[string]interface{} { return ret } -// New creates a new queue with config and return Queue. -func NewQueue(conn *tarantool.Connection, name string, cfg Cfg) (Queue, error) { - var q *queue - cmd := fmt.Sprintf("queue.create_tube('%s', '%s', %s)", name, cfg.getType(), cfg.toString()) - _, err := conn.Eval(cmd, []interface{}{}) - if err == nil { - q = &queue{ - name, - conn, - makeCmd(name), - } +// New creates a queue handle +func New(conn *tarantool.Connection, name string) Queue { + q := &queue{ + name: name, + conn: conn, } - return q, err + makeCmd(q) + return q +} + +// Create creates a new queue with config +func (q *queue) Create(cfg Cfg) error { + cmd := "local name, type, cfg = ... ; queue.create_tube(name, type, cfg)" + _, err := q.conn.Eval(cmd, []interface{}{q.name, cfg.getType(), cfg.toMap()}) + return err } -// GetQueue returns an existing queue by name. -func GetQueue(conn *tarantool.Connection, name string) (Queue, error) { - var q *queue - cmd := fmt.Sprintf("return queue.tube.%s ~= null", name) - resp, err := conn.Eval(cmd, []interface{}{}) +// Exists checks existance of a tube +func (q *queue) Exists() (bool, error) { + cmd := "local name = ... ; return queue.tube[name] ~= null" + resp, err := q.conn.Eval(cmd, []string{q.name}) if err != nil { - return q, err + return false, err } exist := len(resp.Data) != 0 && resp.Data[0].(bool) - - if exist { - q = &queue{ - name, - conn, - makeCmd(name), - } - } else { - err = fmt.Errorf("Tube %s does not exist", name) - } - - return q, err + return exist, nil } // Put data to queue. Returns task. @@ -259,17 +260,17 @@ func (q *queue) Statistic() (interface{}, error) { return nil, nil } -func makeCmd(name string) *cmd { - return &cmd{ - put: "queue.tube." + name + ":put", - take: "queue.tube." + name + ":take", - drop: "queue.tube." + name + ":drop", - peek: "queue.tube." + name + ":peek", - ack: "queue.tube." + name + ":ack", - delete: "queue.tube." + name + ":delete", - bury: "queue.tube." + name + ":bury", - kick: "queue.tube." + name + ":kick", - release: "queue.tube." + name + ":release", +func makeCmd(q *queue) { + q.cmds = cmd{ + put: "queue.tube." + q.name + ":put", + take: "queue.tube." + q.name + ":take", + drop: "queue.tube." + q.name + ":drop", + peek: "queue.tube." + q.name + ":peek", + ack: "queue.tube." + q.name + ":ack", + delete: "queue.tube." + q.name + ":delete", + bury: "queue.tube." + q.name + ":bury", + kick: "queue.tube." + q.name + ":kick", + release: "queue.tube." + q.name + ":release", statistics: "queue.statistics", } } diff --git a/queue/queue_test.go b/queue/queue_test.go new file mode 100644 index 000000000..b259aecba --- /dev/null +++ b/queue/queue_test.go @@ -0,0 +1,624 @@ +package queue_test + +import ( + . "github.com/tarantool/go-tarantool" + "testing" + "time" + "github.com/tarantool/go-tarantool/queue" +) + +var server = "127.0.0.1:3013" +var opts = Opts{ + Timeout: 3000 * time.Millisecond, + User: "test", + Pass: "test", + //Concurrency: 32, + //RateLimit: 4*1024, +} + +/////////QUEUE///////// + +func TestFifoQueue(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q := queue.New(conn, name) + if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + //Drop + if err = q.Drop(); err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } +} + +func TestFifoQueue_GetExist_Statistic(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q := queue.New(conn, name) + if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + ok, err := q.Exists() + if err != nil { + t.Errorf("Failed to get exist queue: %s", err.Error()) + return + } + if !ok { + t.Error("Queue is not found") + return + } + + putData := "put_data" + _, err = q.Put(putData) + if err != nil { + t.Errorf("Failed to put queue: %s", err.Error()) + return + } + + stat, err := q.Statistic() + if err != nil { + t.Errorf("Failed to get statistic queue: %s", err.Error()) + } else if stat == nil { + t.Error("Statistic is nil") + } + //Drop + err = q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } +} + +func TestFifoQueue_Put(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q := queue.New(conn, name) + if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() + + //Put + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) + } + } +} + +func TestFifoQueue_Take(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q := queue.New(conn, name) + if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() + + //Put + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) + } + } + + + //Take + task, err = q.TakeTimeout(2 * time.Second) + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + } else if task == nil { + t.Errorf("Task is nil after take") + } else { + if task.Data() != putData { + t.Errorf("Task data after take not equal with example. %s != %s", task.Data(), putData) + } + + if !task.IsTaken() { + t.Errorf("Task status after take is not taken. Status = ", task.Status()) + } + + err = task.Ack() + if err != nil { + t.Errorf("Failed ack %s", err.Error()) + } else if !task.IsDone() { + t.Errorf("Task status after take is not done. Status = ", task.Status()) + } + } +} + +func TestFifoQueue_Peek(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q := queue.New(conn, name) + if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() + + //Put + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) + } + } + + + //Peek + task, err = q.Peek(task.Id()) + if err != nil { + t.Errorf("Failed peek from queue: %s", err.Error()) + } else if task == nil { + t.Errorf("Task is nil after peek") + } else { + if task.Data() != putData { + t.Errorf("Task data after peek not equal with example. %s != %s", task.Data(), putData) + } + + if !task.IsReady() { + t.Errorf("Task status after peek is not ready. Status = ", task.Status()) + } + } +} + +func TestFifoQueue_Bury_Kick(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q := queue.New(conn, name) + if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() + + //Put + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) + } + } + + + //Bury + err = task.Bury() + if err != nil { + t.Errorf("Failed bury task %s", err.Error()) + return + } else if !task.IsBuried() { + t.Errorf("Task status after bury is not buried. Status = ", task.Status()) + } + + //Kick + count, err := q.Kick(1) + if err != nil { + t.Errorf("Failed kick task %s", err.Error()) + return + } else if count != 1 { + t.Errorf("Kick result != 1") + return + } + + //Take + task, err = q.TakeTimeout(2 * time.Second) + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + } else if task == nil { + t.Errorf("Task is nil after take") + } else { + if task.Data() != putData { + t.Errorf("Task data after take not equal with example. %s != %s", task.Data(), putData) + } + + if !task.IsTaken() { + t.Errorf("Task status after take is not taken. Status = ", task.Status()) + } + + err = task.Ack() + if err != nil { + t.Errorf("Failed ack %s", err.Error()) + } else if !task.IsDone() { + t.Errorf("Task status after take is not done. Status = ", task.Status()) + } + } +} + +func TestFifoQueue_Delete(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q := queue.New(conn, name) + if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() + + //Put + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) + } + } + + //Delete + err = task.Delete() + if err != nil { + t.Errorf("Failed bury task %s", err.Error()) + return + } else if !task.IsDone() { + t.Errorf("Task status after delete is not done. Status = ", task.Status()) + } + + //Take + task, err = q.TakeTimeout(2 * time.Second) + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + } else if task != nil { + t.Errorf("Task is not nil after take. Task is %s", task) + } +} + +func TestFifoQueue_Release(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q := queue.New(conn, name) + if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() + + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) + } + } + + //Take + task, err = q.Take() + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + return + } else if task == nil { + t.Error("Task is nil after take") + return + } + + //Release + err = task.Release() + if err != nil { + t.Errorf("Failed release task% %s", err.Error()) + return + } + + if !task.IsReady() { + t.Errorf("Task status is not ready, but %s", task.Status()) + return + } + + + //Take + task, err = q.Take() + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + return + } else if task == nil { + t.Error("Task is nil after take") + return + } else { + if task.Data() != putData { + t.Errorf("Task data after take not equal with example. %s != %s", task.Data(), putData) + } + + if !task.IsTaken() { + t.Errorf("Task status after take is not taken. Status = ", task.Status()) + } + + err = task.Ack() + if err != nil { + t.Errorf("Failed ack %s", err.Error()) + } else if !task.IsDone() { + t.Errorf("Task status after take is not done. Status = ", task.Status()) + } + } +} + +func TestTtlQueue(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + defer conn.Close() + + name := "test_queue" + cfg := queue.Cfg{ + Temporary: true, + Kind: queue.FIFO_TTL, + Opts: queue.Opts{Ttl: 5*time.Second}, + } + q := queue.New(conn, name) + if err = q.Create(cfg); err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() + + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) + } + } + + time.Sleep(5 * time.Second) + + //Take + task, err = q.TakeTimeout(2 * time.Second) + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + } else if task != nil { + t.Errorf("Task is not nil after sleep") + } +} + +func TestTtlQueue_Put(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + cfg := queue.Cfg{ + Temporary: true, + Kind: queue.FIFO_TTL, + Opts: queue.Opts{Ttl: 5*time.Second}, + } + q := queue.New(conn, name) + if err = q.Create(cfg); err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() + + putData := "put_data" + task, err := q.PutWithOpts(putData, queue.Opts{Ttl: 10 * time.Second}) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) + } + } + + time.Sleep(5 * time.Second) + + //Take + task, err = q.TakeTimeout(2 * time.Second) + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + } else if task == nil { + t.Errorf("Task is nil after sleep") + } else { + if task.Data() != putData { + t.Errorf("Task data after take not equal with example. %s != %s", task.Data(), putData) + } + + if !task.IsTaken() { + t.Errorf("Task status after take is not taken. Status = ", task.Status()) + } + + err = task.Ack() + if err != nil { + t.Errorf("Failed ack %s", err.Error()) + } else if !task.IsDone() { + t.Errorf("Task status after take is not done. Status = ", task.Status()) + } + } +} diff --git a/queue/task.go b/queue/task.go index 7fd52b102..906974ce8 100644 --- a/queue/task.go +++ b/queue/task.go @@ -1,5 +1,6 @@ package queue +// Task represents a task from tarantool queue's tube type Task struct { id uint64 status string @@ -7,87 +8,79 @@ type Task struct { q *queue } -// Return a task id -func (t *Task) GetId() uint64 { +// Id is a getter for task id +func (t *Task) Id() uint64 { return t.id } -// Return a task data -func (t *Task) GetData() interface{} { +// Data is a getter for task data +func (t *Task) Data() interface{} { return t.data } -// Return a task status -func (t *Task) GetStatus() string { +// Status is a getter for task status +func (t *Task) Status() string { return t.status } -// Signal that the task has been completed +// Ack signals about task completion func (t *Task) Ack() error { - return t.produce(t.q._ack) + return t.accept(t.q._ack(t.id)) } // Delete task from queue func (t *Task) Delete() error { - return t.produce(t.q._delete) + return t.accept(t.q._delete(t.id)) } -// If it becomes clear that a task cannot be executed in the current circumstances, you can "bury" the task -- that is, disable it until the circumstances change. +// Bury signals that task task cannot be executed in the current circumstances, +// task becomes "buried" - ie neither completed, nor ready, so it could not be +// deleted or taken by other worker. +// To revert "burying" call queue.Kick(numberOfBurried). func (t *Task) Bury() error { - return t.produce(t.q._bury) + return t.accept(t.q._bury(t.id)) } -func (t *Task) produce(f func(taskId uint64) (string, error)) error { - newStatus, err := f(t.id) - if err != nil { - return err - } - - t.status = newStatus - return nil -} - -// Put the task back in the queue, 'release' implies unsuccessful completion of a taken task. +// Release returns task back in the queue without making it complete. +// In outher words, this worker failed to complete the task, and +// it, so other worker could try to do that again. func (t *Task) Release() error { - return t.release(Opts{}) + return t.accept(t.q._release(t.id, Opts{})) } -// Put the task back in the queue with config, 'release' implies unsuccessful completion of a taken task. -func (t *Task) ReleaseWithConfig(cfg Opts) error { - return t.release(cfg) +// ReleaseCfg returns task to a queue and changes its configuration. +func (t *Task) ReleaseCfg(cfg Opts) error { + return t.accept(t.q._release(t.id, cfg)) } -func (t *Task) release(cfg Opts) error { - newStatus, err := t.q._release(t.id, cfg) - if err != nil { - return err +func (t *Task) accept(newStatus string, err error) error { + if err == nil { + t.status = newStatus } - - t.status = newStatus - return nil + return err } -// Return true if task status is ready +// IsReady returns if task is ready func (t *Task) IsReady() bool { return t.status == READY } -// Return true if task status is taken +// IsTaken returns if task is taken func (t *Task) IsTaken() bool { return t.status == TAKEN } -// Return true if task status is done +// IsDone returns if task is done func (t *Task) IsDone() bool { return t.status == DONE } -// Return true if task status is bury +// IsBurred returns if task is buried func (t *Task) IsBuried() bool { return t.status == BURIED } -// Return true if task status is delayed +// IsDelayed returns if task is delayed func (t *Task) IsDelayed() bool { return t.status == DELAYED } diff --git a/tarantool_test.go b/tarantool_test.go index 355495c88..bbf6220a7 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -8,7 +8,6 @@ import ( "sync" "testing" "time" - "github.com/tarantool/go-tarantool/queue" ) type Member struct { @@ -94,8 +93,6 @@ var opts = Opts{ //RateLimit: 4*1024, } - - const N = 500 func BenchmarkClientSerial(b *testing.B) { @@ -987,607 +984,3 @@ func TestComplexStructs(t *testing.T) { return } } - -/////////QUEUE///////// - -func TestFifoQueue(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } - defer conn.Close() - - name := "test_queue" - q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) - if err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - //Drop - err = q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } -} - -func TestFifoQueue_GetExist_Statistic(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } - defer conn.Close() - - name := "test_queue" - _, err = queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) - if err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - q, err := queue.GetQueue(conn, name) - if err != nil { - t.Errorf("Failed to get exist queue: %s", err.Error()) - return - } - - putData := "put_data" - _, err = q.Put(putData) - if err != nil { - t.Errorf("Failed to put queue: %s", err.Error()) - return - } - - stat, err := q.Statistic() - if err != nil { - t.Errorf("Failed to get statistic queue: %s", err.Error()) - } else if stat == nil { - t.Error("Statistic is nil") - } - //Drop - err = q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } -} - -func TestFifoQueue_Put(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } - defer conn.Close() - - name := "test_queue" - q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) - if err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() - - //Put - putData := "put_data" - task, err := q.Put(putData) - if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return - } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return - } else { - if task.GetData() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) - } - } -} - -func TestFifoQueue_Take(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } - defer conn.Close() - - name := "test_queue" - q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) - if err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() - - //Put - putData := "put_data" - task, err := q.Put(putData) - if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return - } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return - } else { - if task.GetData() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) - } - } - - - //Take - task, err = q.TakeTimeout(2 * time.Second) - if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) - } else if task == nil { - t.Errorf("Task is nil after take") - } else { - if task.GetData() != putData { - t.Errorf("Task data after take not equal with example. %s != %s", task.GetData(), putData) - } - - if !task.IsTaken() { - t.Errorf("Task status after take is not taken. Status = ", task.GetStatus()) - } - - err = task.Ack() - if err != nil { - t.Errorf("Failed ack %s", err.Error()) - } else if !task.IsDone() { - t.Errorf("Task status after take is not done. Status = ", task.GetStatus()) - } - } -} - -func TestFifoQueue_Peek(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } - defer conn.Close() - - name := "test_queue" - q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) - if err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() - - //Put - putData := "put_data" - task, err := q.Put(putData) - if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return - } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return - } else { - if task.GetData() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) - } - } - - - //Peek - task, err = q.Peek(task.GetId()) - if err != nil { - t.Errorf("Failed peek from queue: %s", err.Error()) - } else if task == nil { - t.Errorf("Task is nil after peek") - } else { - if task.GetData() != putData { - t.Errorf("Task data after peek not equal with example. %s != %s", task.GetData(), putData) - } - - if !task.IsReady() { - t.Errorf("Task status after peek is not ready. Status = ", task.GetStatus()) - } - } -} - -func TestFifoQueue_Bury_Kick(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } - defer conn.Close() - - name := "test_queue" - q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) - if err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() - - //Put - putData := "put_data" - task, err := q.Put(putData) - if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return - } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return - } else { - if task.GetData() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) - } - } - - - //Bury - err = task.Bury() - if err != nil { - t.Errorf("Failed bury task% %s", err.Error()) - return - } else if !task.IsBuried() { - t.Errorf("Task status after bury is not buried. Status = ", task.GetStatus()) - } - - //Kick - count, err := q.Kick(1) - if err != nil { - t.Errorf("Failed kick task% %s", err.Error()) - return - } else if count != 1 { - t.Errorf("Kick result != 1") - return - } - - //Take - task, err = q.TakeTimeout(2 * time.Second) - if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) - } else if task == nil { - t.Errorf("Task is nil after take") - } else { - if task.GetData() != putData { - t.Errorf("Task data after take not equal with example. %s != %s", task.GetData(), putData) - } - - if !task.IsTaken() { - t.Errorf("Task status after take is not taken. Status = ", task.GetStatus()) - } - - err = task.Ack() - if err != nil { - t.Errorf("Failed ack %s", err.Error()) - } else if !task.IsDone() { - t.Errorf("Task status after take is not done. Status = ", task.GetStatus()) - } - } -} - -func TestFifoQueue_Delete(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } - defer conn.Close() - - name := "test_queue" - q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) - if err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() - - //Put - putData := "put_data" - task, err := q.Put(putData) - if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return - } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return - } else { - if task.GetData() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) - } - } - - //Delete - err = task.Delete() - if err != nil { - t.Errorf("Failed bury task% %s", err.Error()) - return - } else if !task.IsDone() { - t.Errorf("Task status after delete is not done. Status = ", task.GetStatus()) - } - - //Take - task, err = q.TakeTimeout(2 * time.Second) - if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) - } else if task != nil { - t.Errorf("Task is not nil after take. Task is %s", task) - } -} - -func TestFifoQueue_Release(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } - defer conn.Close() - - name := "test_queue" - q, err := queue.NewQueue(conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) - if err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() - - putData := "put_data" - task, err := q.Put(putData) - if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return - } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return - } else { - if task.GetData() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) - } - } - - //Take - task, err = q.Take() - if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) - return - } else if task == nil { - t.Error("Task is nil after take") - return - } - - //Release - err = task.Release() - if err != nil { - t.Errorf("Failed release task% %s", err.Error()) - return - } - - if !task.IsReady() { - t.Errorf("Task status is not ready, but %s", task.GetStatus()) - return - } - - - //Take - task, err = q.Take() - if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) - return - } else if task == nil { - t.Error("Task is nil after take") - return - } else { - if task.GetData() != putData { - t.Errorf("Task data after take not equal with example. %s != %s", task.GetData(), putData) - } - - if !task.IsTaken() { - t.Errorf("Task status after take is not taken. Status = ", task.GetStatus()) - } - - err = task.Ack() - if err != nil { - t.Errorf("Failed ack %s", err.Error()) - } else if !task.IsDone() { - t.Errorf("Task status after take is not done. Status = ", task.GetStatus()) - } - } -} - -func TestTtlQueue(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - defer conn.Close() - - name := "test_queue" - cfg := queue.Cfg{ - Temporary: true, - Kind: queue.FIFO_TTL, - Opts: queue.Opts{Ttl: 5*time.Second}, - } - q, err := queue.NewQueue(conn, name, cfg) - if err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() - - putData := "put_data" - task, err := q.Put(putData) - if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return - } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return - } else { - if task.GetData() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) - } - } - - time.Sleep(5 * time.Second) - - //Take - task, err = q.TakeTimeout(2 * time.Second) - if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) - } else if task != nil { - t.Errorf("Task is not nil after sleep") - } -} - -func TestTtlQueue_Put(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } - defer conn.Close() - - name := "test_queue" - cfg := queue.Cfg{ - Temporary: true, - Kind: queue.FIFO_TTL, - Opts: queue.Opts{Ttl: 5*time.Second}, - } - q, err := queue.NewQueue(conn, name, cfg) - if err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() - - putData := "put_data" - task, err := q.PutWithOpts(putData, queue.Opts{Ttl: 10 * time.Second}) - if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return - } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return - } else { - if task.GetData() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.GetData(), putData) - } - } - - time.Sleep(5 * time.Second) - - //Take - task, err = q.TakeTimeout(2 * time.Second) - if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) - } else if task == nil { - t.Errorf("Task is nil after sleep") - } else { - if task.GetData() != putData { - t.Errorf("Task data after take not equal with example. %s != %s", task.GetData(), putData) - } - - if !task.IsTaken() { - t.Errorf("Task status after take is not taken. Status = ", task.GetStatus()) - } - - err = task.Ack() - if err != nil { - t.Errorf("Failed ack %s", err.Error()) - } else if !task.IsDone() { - t.Errorf("Task status after take is not done. Status = ", task.GetStatus()) - } - } -} \ No newline at end of file From 506f4c374194fb12a07bd28fcad68ccb117b632f Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Sun, 16 Apr 2017 21:25:54 +0300 Subject: [PATCH 182/605] queue: limit timeout by 0.9 of connection request timeout --- queue/queue.go | 4 ++-- queue/queue_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/queue/queue.go b/queue/queue.go index 0d8c2a968..6a7be3e90 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -164,14 +164,14 @@ func (q *queue) put(params ...interface{}) (*Task, error) { func (q *queue) Take() (*Task, error) { var params interface{} if q.conn.ConfiguredTimeout() > 0 { - params = q.conn.ConfiguredTimeout().Seconds() + params = (q.conn.ConfiguredTimeout() * 9 / 10).Seconds() } return q.take(params) } // The take request searches for a task in the queue. Waits until a task becomes ready or the timeout expires. func (q *queue) TakeTimeout(timeout time.Duration) (*Task, error) { - t := q.conn.ConfiguredTimeout() + t := q.conn.ConfiguredTimeout() * 9 / 10 if t > 0 && timeout > t { timeout = t } diff --git a/queue/queue_test.go b/queue/queue_test.go index b259aecba..53b9b2415 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -9,7 +9,7 @@ import ( var server = "127.0.0.1:3013" var opts = Opts{ - Timeout: 3000 * time.Millisecond, + Timeout: 500 * time.Millisecond, User: "test", Pass: "test", //Concurrency: 32, From 1cf10c4be9c761cfcdc7469a2e7beb7ffff89cd0 Mon Sep 17 00:00:00 2001 From: Kirill Zhuharev Date: Sun, 4 Jun 2017 23:49:24 +0300 Subject: [PATCH 183/605] Get feature (#40) * add fmt * add Get feature * update request.go * add doc comtents * add tests for GetTyped * remove GetTyped and GetAsync, fix GetTyped --- request.go | 35 ++++++++++++++++++++++++++++++++++- tarantool_test.go | 20 ++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/request.go b/request.go index 9df52ea35..ec8be59d4 100644 --- a/request.go +++ b/request.go @@ -1,8 +1,10 @@ package tarantool import ( - "gopkg.in/vmihailenco/msgpack.v2" + "errors" "time" + + "gopkg.in/vmihailenco/msgpack.v2" ) // Future is a handle for asynchronous request @@ -118,6 +120,37 @@ func (conn *Connection) Eval(expr string, args interface{}) (resp *Response, err return conn.EvalAsync(expr, args).Get() } +// single used for conn.GetTyped for decode one tuple +type single struct { + res interface{} + found bool +} + +func (s *single) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var len int + if len, err = d.DecodeSliceLen(); err != nil { + return err + } + if s.found = len >= 1; !s.found { + return nil + } + if len != 1 { + return errors.New("Tarantool returns unexpected value for Select(limit=1)") + } + return d.Decode(s.res) +} + +// GetTyped performs select (with limit = 1 and offset = 0) +// to box space and fills typed result. +// +// It is equal to conn.SelectAsync(space, index, 0, 1, IterEq, key).GetTyped(&result) +func (conn *Connection) GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) { + s := single{res: result} + err = conn.SelectAsync(space, index, 0, 1, IterEq, key).GetTyped(&s) + return +} + // SelectTyped performs select to box space and fills typed result. // // It is equal to conn.SelectAsync(space, index, offset, limit, iterator, key).GetTyped(&result) diff --git a/tarantool_test.go b/tarantool_test.go index bbf6220a7..2f4dab887 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -605,6 +605,16 @@ func TestClient(t *testing.T) { } } + // Get Typed + var singleTpl = Tuple{} + err = conn.GetTyped(spaceNo, indexNo, []interface{}{uint(10)}, &singleTpl) + if err != nil { + t.Errorf("Failed to GetTyped: %s", err.Error()) + } + if singleTpl.Id != 10 { + t.Errorf("Bad value loaded from GetTyped") + } + // Select Typed for one tuple var tpl1 [1]Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}, &tpl1) @@ -619,6 +629,16 @@ func TestClient(t *testing.T) { } } + // Get Typed Empty + var singleTpl2 Tuple + err = conn.GetTyped(spaceNo, indexNo, []interface{}{uint(30)}, &singleTpl2) + if err != nil { + t.Errorf("Failed to GetTyped: %s", err.Error()) + } + if singleTpl2.Id != 0 { + t.Errorf("Bad value loaded from GetTyped") + } + // Select Typed Empty var tpl2 []Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}, &tpl2) From a1a677f664b6ae04d686d884a9cf055f94f25059 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Mon, 12 Jun 2017 13:13:37 +0300 Subject: [PATCH 184/605] report user mismatch error immediately --- connection.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/connection.go b/connection.go index b7e228dab..762eec611 100644 --- a/connection.go +++ b/connection.go @@ -223,8 +223,13 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { } if err = conn.createConnection(false); err != nil { + ter, ok := err.(Error) if conn.opts.Reconnect <= 0 { return nil, err + } else if ok && (ter.Code == ErrNoSuchUser || + ter.Code == ErrPasswordMismatch) { + /* reported auth errors immediatly */ + return nil, err } else { // without SkipSchema it is useless go func(conn *Connection) { From 98a924d31cb709f5f6ab7ef5bfdd53d3e02ed513 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Mon, 12 Jun 2017 13:13:46 +0300 Subject: [PATCH 185/605] go fmt --- connection.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connection.go b/connection.go index 762eec611..2e069a14d 100644 --- a/connection.go +++ b/connection.go @@ -311,7 +311,7 @@ func (conn *Connection) dial() (err error) { timeout = 5 * time.Second } // Unix socket connection - if address[0] == '.' || address [0] == '/' { + if address[0] == '.' || address[0] == '/' { network = "unix" } else if address[0:7] == "unix://" { network = "unix" @@ -803,4 +803,4 @@ func (conn *Connection) nextRequestId() (requestId uint32) { // ConfiguredTimeout returns timeout from connection config func (conn *Connection) ConfiguredTimeout() time.Duration { return conn.opts.Timeout -} \ No newline at end of file +} From 728550a7ad41522305a52bce369aa7493b97916d Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Mon, 12 Jun 2017 13:16:02 +0300 Subject: [PATCH 186/605] ... --- const.go | 2 +- tarantool_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/const.go b/const.go index 7bdd20989..03b00c6b1 100644 --- a/const.go +++ b/const.go @@ -49,4 +49,4 @@ const ( OkCode = uint32(0) ErrorCodeBit = 0x8000 PacketLengthBytes = 5 -) \ No newline at end of file +) diff --git a/tarantool_test.go b/tarantool_test.go index 2f4dab887..94c21064c 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -433,7 +433,7 @@ func TestClient(t *testing.T) { } } //resp, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) - resp, err = conn.Insert(spaceNo, &Tuple{Id:1, Msg:"hello", Name:"world"}) + resp, err = conn.Insert(spaceNo, &Tuple{Id: 1, Msg: "hello", Name: "world"}) if tntErr, ok := err.(Error); !ok || tntErr.Code != ErrTupleFound { t.Errorf("Expected ErrTupleFound but got: %v", err) } @@ -980,7 +980,7 @@ func TestComplexStructs(t *testing.T) { } defer conn.Close() - tuple := Tuple2{Cid:777, Orig:"orig", Members:[]Member{{"lol", "", 1}, {"wut", "", 3}}} + tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} _, err = conn.Replace(spaceNo, &tuple) if err != nil { t.Errorf("Failed to insert: %s", err.Error()) From 6b262217ee353f0440358090e04ff9eeb1f8d162 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 29 Jun 2017 11:19:06 +0300 Subject: [PATCH 187/605] fix race condition on extremely small request timeouts (I hope) Fixes #43 (I hope) --- connection.go | 20 +++++++++++++++++++- request.go | 7 ------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/connection.go b/connection.go index 2e069a14d..e99ee0755 100644 --- a/connection.go +++ b/connection.go @@ -670,8 +670,26 @@ func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error blen := len(shard.buf) if err := fut.pack(&shard.buf, shard.enc, body); err != nil { shard.buf = shard.buf[:blen] - fut.err = err shard.bufmut.Unlock() + if f := conn.fetchFuture(fut.requestId); f == fut { + fut.markReady(conn) + fut.err = err + } else if f != nil { + /* in theory, it is possible. In practice, you have + * to have race condition that lasts hours */ + panic("Unknown future") + } else { + fut.wait() + if fut.err == nil { + panic("Future removed from queue without error") + } + if _, ok := fut.err.(ClientError); ok { + // packing error is more important than connection + // error, because it is indication of programmer's + // mistake. + fut.err = err + } + } return } shard.bufmut.Unlock() diff --git a/request.go b/request.go index ec8be59d4..bc90dd7fc 100644 --- a/request.go +++ b/request.go @@ -380,13 +380,6 @@ func (fut *Future) send(conn *Connection, body func(*msgpack.Encoder) error) *Fu return fut } conn.putFuture(fut, body) - if fut.err != nil { - if f := conn.fetchFuture(fut.requestId); f == fut { - fut.markReady(conn) - } - return fut - } - return fut } From 9ae07f3d08f2c529429a9d497352c41dfec01a68 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 3 Aug 2017 10:38:47 +0300 Subject: [PATCH 188/605] Mention alternative connector. --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3cd7356c8..8cb348b38 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ faster than other packages according to public benchmarks. * [Custom (un)packing and typed selects and function calls](#custom-unpacking-and-typed-selects-and-function-calls) * [Options](#options) * [Working with queue](#working-with-queue) +* [Alternative connectors](#alternative-connectors) ## Installation @@ -501,7 +502,7 @@ func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { * `User` - user name to log into Tarantool. * `Pass` - user password to log into Tarantool. -## Working-with-queue +## Working with queue ```go import "github.com/tarantool/go-tarantool/queue" @@ -544,3 +545,8 @@ Features of the implementation: - If you use connection timeout and call `TakeWithTimeout` with parameter greater than the connection timeout then parameter reduced to it - If you use connection timeout and call `Take` then we return a error if we can not take task from queue in a time equal to the connection timeout + +## Alternative connectors + +- https://github.com/viciious/go-tarantool + Has tools to emulate tarantool, and to being replica for tarantool. From 09c6a76333c792fc820ef06efbd78dac1dd0eb53 Mon Sep 17 00:00:00 2001 From: Sokolov Yura Date: Thu, 3 Aug 2017 18:35:32 +0300 Subject: [PATCH 189/605] Add license (BSD-2 clause as for tarantool) --- LICENSE | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..9295b5220 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +BSD 2-Clause License + +Copyright (c) 2014-2017, Tarantool AUTHORS +Copyright (c) 2014-2017, Dmitry Smal +Copyright (c) 2014-2017, Yura Sokolov aka funny_falcon +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 66930e287b4dcb16c07502e906f9797b1e06d3d4 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 6 Sep 2017 11:49:53 +0300 Subject: [PATCH 190/605] Make logger configurable --- connection.go | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/connection.go b/connection.go index e99ee0755..071557721 100644 --- a/connection.go +++ b/connection.go @@ -23,6 +23,7 @@ const ( ) type ConnEventKind int +type ConnLogKind int const ( // Connect signals that connection is established or reestablished @@ -33,6 +34,15 @@ const ( ReconnectFailed // Either reconnect attempts exhausted, or explicit Close is called Closed + + // LogReconnectFailed is logged when reconnect attempt failed + LogReconnectFailed ConnLogKind = iota + 1 + // LogLastReconnectFailed is logged when last reconnect attempt failed, + // connection will be closed after that. + LogLastReconnectFailed + // LogUnexpectedResultId is logged when response with unknown id were received. + // Most probably it is due to request timeout. + LogUnexpectedResultId ) // ConnEvent is sent throw Notify channel specified in Opts @@ -44,6 +54,28 @@ type ConnEvent struct { var epoch = time.Now() +// Logger is logger type expected to be passed in options. +type Logger interface { + Report(event ConnLogKind, conn *Connection, v ...interface{}) +} + +type defaultLogger struct{} + +func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interface{}) { + switch event { + case LogReconnectFailed: + reconnects := v[0].(uint) + err := v[1].(error) + log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s\n", reconnects, conn.opts.MaxReconnects, conn.addr, err.Error()) + case LogLastReconnectFailed: + err := v[0].(error) + log.Printf("tarantool: last reconnect to %s failed: %s, giving it up.\n", conn.addr, err.Error()) + case LogUnexpectedResultId: + resp := v[0].(Response) + log.Printf("tarantool: connection %s got unexpected resultId (%d) in response", conn.addr, resp.RequestId) + } +} + // Connection is a handle to Tarantool. // // It is created and configured with Connect function, and could not be @@ -159,6 +191,8 @@ type Opts struct { Notify chan<- ConnEvent // Handle is user specified value, that could be retrivied with Handle() method Handle interface{} + // Logger is user specified logger used for error messages + Logger Logger } // Connect creates and configures new Connection @@ -222,6 +256,10 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { } } + if conn.opts.Logger == nil { + conn.opts.Logger = defaultLogger{} + } + if err = conn.createConnection(false); err != nil { ter, ok := err.(Error) if conn.opts.Reconnect <= 0 { @@ -430,12 +468,12 @@ func (conn *Connection) createConnection(reconnect bool) (err error) { return } if conn.opts.MaxReconnects > 0 && reconnects > conn.opts.MaxReconnects { - log.Printf("tarantool: last reconnect to %s failed: %s, giving it up.\n", conn.addr, err.Error()) + conn.opts.Logger.Report(LogLastReconnectFailed, conn, err) err = ClientError{ErrConnectionClosed, "last reconnect failed"} // mark connection as closed to avoid reopening by another goroutine return } - log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s\n", reconnects, conn.opts.MaxReconnects, conn.addr, err.Error()) + conn.opts.Logger.Report(LogReconnectFailed, conn, reconnects, err) conn.notify(ReconnectFailed) reconnects++ conn.mutex.Unlock() @@ -594,7 +632,7 @@ func (conn *Connection) reader(r *bufio.Reader, c net.Conn) { fut.resp = resp fut.markReady(conn) } else { - log.Printf("tarantool: unexpected requestId (%d) in response", uint(resp.RequestId)) + conn.opts.Logger.Report(LogUnexpectedResultId, conn, resp) } } } From cfec0688988c86cd1bf0bcff839866e7d3338b6c Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 27 Sep 2017 10:24:36 +0300 Subject: [PATCH 191/605] add default case to default logger. --- connection.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/connection.go b/connection.go index 071557721..221ae4814 100644 --- a/connection.go +++ b/connection.go @@ -73,6 +73,9 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac case LogUnexpectedResultId: resp := v[0].(Response) log.Printf("tarantool: connection %s got unexpected resultId (%d) in response", conn.addr, resp.RequestId) + default: + args := append([]interface{}{"tarantool: unexpecting event ", event, conn}, v...) + log.Print(args...) } } From facd47797bf4a0bd594272496022413676c09916 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 11 Oct 2017 12:16:07 +0300 Subject: [PATCH 192/605] fix cast in defaultLogger.Report fixes #49 --- connection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connection.go b/connection.go index 221ae4814..2f386e46e 100644 --- a/connection.go +++ b/connection.go @@ -71,7 +71,7 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac err := v[0].(error) log.Printf("tarantool: last reconnect to %s failed: %s, giving it up.\n", conn.addr, err.Error()) case LogUnexpectedResultId: - resp := v[0].(Response) + resp := v[0].(*Response) log.Printf("tarantool: connection %s got unexpected resultId (%d) in response", conn.addr, resp.RequestId) default: args := append([]interface{}{"tarantool: unexpecting event ", event, conn}, v...) From f15d0a2b87549ef598c5c59e1c70ffb25985a8f1 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Wed, 11 Oct 2017 12:23:31 +0300 Subject: [PATCH 193/605] recognize new index field definitions --- schema.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/schema.go b/schema.go index 45c62b465..e4fd0ccec 100644 --- a/schema.go +++ b/schema.go @@ -152,10 +152,15 @@ func (conn *Connection) loadSchema() (err error) { } case []interface{}: for _, f := range row[5].([]interface{}) { - f := f.([]interface{}) field := new(IndexField) - field.Id = uint32(f[0].(uint64)) - field.Type = f[1].(string) + switch f := f.(type) { + case []interface{}: + field.Id = uint32(f[0].(uint64)) + field.Type = f[1].(string) + case map[string]interface{}: + field.Id = uint32(f["field"].(uint64)) + field.Type = f["type"].(string) + } index.Fields = append(index.Fields, field) } default: From 5fc50b4c9bc132c829af24b789cc76e9f9f7cae3 Mon Sep 17 00:00:00 2001 From: Sokolov Yura aka funny_falcon Date: Thu, 12 Oct 2017 23:05:02 +0300 Subject: [PATCH 194/605] refix new index schema --- config.lua | 15 +++++++++------ schema.go | 8 ++++---- tarantool_test.go | 10 +++++----- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/config.lua b/config.lua index 5a070642f..3c869cd76 100644 --- a/config.lua +++ b/config.lua @@ -9,7 +9,7 @@ local s = box.schema.space.create('test', { id = 512, if_not_exists = true, }) -s:create_index('primary', {type = 'tree', parts = {1, 'NUM'}, if_not_exists = true}) +s:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) local st = box.schema.space.create('schematest', { id = 514, @@ -17,14 +17,17 @@ local st = box.schema.space.create('schematest', { if_not_exists = true, field_count = 7, format = { - [2] = {name = "name1"}, - [3] = {type = "type2"}, - [6] = {name = "name5", type = "str", more = "extra"}, + {name = "name0", type = "unsigned"}, + {name = "name1", type = "unsigned"}, + {name = "name2", type = "string"}, + {name = "name3", type = "unsigned"}, + {name = "name4", type = "unsigned"}, + {name = "name5", type = "string"}, }, }) st:create_index('primary', { type = 'hash', - parts = {1, 'NUM'}, + parts = {1, 'uint'}, unique = true, if_not_exists = true, }) @@ -32,7 +35,7 @@ st:create_index('secondary', { id = 3, type = 'tree', unique = false, - parts = { 2, 'num', 3, 'STR' }, + parts = { 2, 'uint', 3, 'string' }, if_not_exists = true, }) st:truncate() diff --git a/schema.go b/schema.go index e4fd0ccec..9b2dfae75 100644 --- a/schema.go +++ b/schema.go @@ -141,9 +141,9 @@ func (conn *Connection) loadSchema() (err error) { default: panic("unexpected schema format (index flags)") } - switch row[5].(type) { + switch fields := row[5].(type) { case uint64: - cnt := int(row[5].(uint64)) + cnt := int(fields) for i := 0; i < cnt; i++ { field := new(IndexField) field.Id = uint32(row[6+i*2].(uint64)) @@ -151,13 +151,13 @@ func (conn *Connection) loadSchema() (err error) { index.Fields = append(index.Fields, field) } case []interface{}: - for _, f := range row[5].([]interface{}) { + for _, f := range fields { field := new(IndexField) switch f := f.(type) { case []interface{}: field.Id = uint32(f[0].(uint64)) field.Type = f[1].(string) - case map[string]interface{}: + case map[interface{}]interface{}: field.Id = uint32(f["field"].(uint64)) field.Type = f["type"].(string) } diff --git a/tarantool_test.go b/tarantool_test.go index 94c21064c..31a14f59c 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -745,10 +745,10 @@ func TestSchema(t *testing.T) { if space.Fields == nil { t.Errorf("space.Fields is nill") } - if len(space.FieldsById) != 3 { + if len(space.FieldsById) != 6 { t.Errorf("space.FieldsById len is incorrect") } - if len(space.Fields) != 2 { + if len(space.Fields) != 6 { t.Errorf("space.Fields len is incorrect") } @@ -775,13 +775,13 @@ func TestSchema(t *testing.T) { if field1.Name != "name1" { t.Errorf("field 1 has incorrect Name") } - if field1.Type != "" { + if field1.Type != "unsigned" { t.Errorf("field 1 has incorrect Type") } - if field2.Name != "" { + if field2.Name != "name2" { t.Errorf("field 2 has incorrect Name") } - if field2.Type != "type2" { + if field2.Type != "string" { t.Errorf("field 2 has incorrect Type") } From 6e2137a592e0589989474894fd91f52eb303e062 Mon Sep 17 00:00:00 2001 From: Mstislav Bobakov Date: Mon, 23 Oct 2017 12:36:34 +0300 Subject: [PATCH 195/605] OverrideSchema method for the connection --- connection.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/connection.go b/connection.go index 2f386e46e..309c63a87 100644 --- a/connection.go +++ b/connection.go @@ -863,3 +863,12 @@ func (conn *Connection) nextRequestId() (requestId uint32) { func (conn *Connection) ConfiguredTimeout() time.Duration { return conn.opts.Timeout } + +// OverrideSchema sets Schema for the connection +func (conn *Connection) OverrideSchema(s *Schema) { + if s != nil { + conn.mutex.Lock() + defer conn.mutex.Unlock() + conn.Schema = s + } +} From 5de9a66537ce6f0d98834cebd9c1348b68042107 Mon Sep 17 00:00:00 2001 From: Sergey Grechkin-Pogrebnyakov Date: Mon, 19 Feb 2018 14:35:20 +0300 Subject: [PATCH 196/605] implemented typed taking from queue + go fmt (#55) * implemented typed taking from queue + go fmt --- README.md | 33 +++++++++- queue/queue.go | 115 +++++++++++++++++++++----------- queue/queue_test.go | 157 +++++++++++++++++++++++++++++++++++++------- queue/task.go | 33 ++++++++++ 4 files changed, 271 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 8cb348b38..dae5bda63 100644 --- a/README.md +++ b/README.md @@ -282,7 +282,7 @@ func main() { log.Println("Code", resp.Code) log.Println("Data", resp.Data) } -``` +``` ## Schema @@ -423,7 +423,7 @@ func (c *Tuple) DecodeMsgpack(d *msgpack.Decoder) error { return nil } -func main() { +func main() { // establish connection ... tuple := Tuple{777, "orig", []Member{{"lol", "", 1}, {"wut", "", 3}}} @@ -507,6 +507,17 @@ func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { import "github.com/tarantool/go-tarantool/queue" + +type customData struct {} + +func (c *customData) DecodeMsgpack(d *msgpack.Decoder) error { + return nil +} + +func (c *customData) EncodeMsgpack(e *msgpack.Encoder) error { + return nil +} + opts := tarantool.Opts{...} conn, err := tarantool.Connect("127.0.0.1:3301", opts) @@ -524,13 +535,31 @@ cfg := queue.Cfg{ que := queue.New(conn, "test_queue") err = que.Create(cfg) + +// put data task, err := que.Put("test_data") fmt.Println("Task id is ", task.GetId()) + +// take data task, err = que.Take() //blocking operation fmt.Println("Data is ", task.GetData()) task.Ack() + +// take typed example +putData := customData{} +// put data +task, err = que.Put(&putData) +fmt.Println("Task id is ", task.GetId()) + +takeData := customData{} +//take data +task, err = que.TakeTyped(&takeData) //blocking operation +fmt.Println("Data is ", takeData) +// same data +fmt.Println("Data is ", task.GetData()) + task, err = que.Put([]int{1, 2, 3}) task.Bury() diff --git a/queue/queue.go b/queue/queue.go index 6a7be3e90..e41744e63 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -1,8 +1,11 @@ package queue import ( - "github.com/tarantool/go-tarantool" + "fmt" "time" + + "github.com/tarantool/go-tarantool" + msgpack "gopkg.in/vmihailenco/msgpack.v2" ) // Queue is a handle to tarantool queue's tube @@ -14,7 +17,7 @@ type Queue interface { // Note: it uses Eval, so user needs 'execute universe' privilege // Note: you'd better not use this function in your application, cause it is // administrative task to create or delete queue. - Create(cfg Cfg) (error) + Create(cfg Cfg) error // Drop destroys tube. // Note: you'd better not use this function in your application, cause it is // administrative task to create or delete queue. @@ -32,6 +35,17 @@ type Queue interface { // Note: if connection has a request Timeout, and conn.Timeout * 0.9 < timeout // then timeout = conn.Timeout*0.9 TakeTimeout(timeout time.Duration) (*Task, error) + // Take takes 'ready' task from a tube and marks it as 'in progress' + // Note: if connection has a request Timeout, then 0.9 * connection.Timeout is + // used as a timeout. + // Data will be unpacked to result + TakeTyped(interface{}) (*Task, error) + // TakeWithTimout takes 'ready' task from a tube and marks it as "in progress", + // or it is timeouted after "timeout" period. + // Note: if connection has a request Timeout, and conn.Timeout * 0.9 < timeout + // then timeout = conn.Timeout*0.9 + // data will be unpacked to result + TakeTypedTimeout(timeout time.Duration, result interface{}) (*Task, error) // Peek returns task by its id. Peek(taskId uint64) (*Task, error) // Kick reverts effect of Task.Bury() for `count` tasks. @@ -151,13 +165,14 @@ func (q *queue) PutWithOpts(data interface{}, cfg Opts) (*Task, error) { } func (q *queue) put(params ...interface{}) (*Task, error) { - var task *Task - resp, err := q.conn.Call(q.cmds.put, params) - if err == nil { - task, err = toTask(resp.Data, q) + qd := queueData{ + result: params[0], + q: q, } - - return task, err + if err := q.conn.CallTyped(q.cmds.put, params, &qd); err != nil { + return nil, err + } + return qd.task, nil } // The take request searches for a task in the queue. @@ -178,13 +193,33 @@ func (q *queue) TakeTimeout(timeout time.Duration) (*Task, error) { return q.take(timeout.Seconds()) } -func (q *queue) take(params interface{}) (*Task, error) { - var t *Task - resp, err := q.conn.Call(q.cmds.take, []interface{}{params}) - if err == nil { - t, err = toTask(resp.Data, q) +// The take request searches for a task in the queue. +func (q *queue) TakeTyped(result interface{}) (*Task, error) { + var params interface{} + if q.conn.ConfiguredTimeout() > 0 { + params = (q.conn.ConfiguredTimeout() * 9 / 10).Seconds() } - return t, err + return q.take(params, result) +} + +// The take request searches for a task in the queue. Waits until a task becomes ready or the timeout expires. +func (q *queue) TakeTypedTimeout(timeout time.Duration, result interface{}) (*Task, error) { + t := q.conn.ConfiguredTimeout() * 9 / 10 + if t > 0 && timeout > t { + timeout = t + } + return q.take(timeout.Seconds(), result) +} + +func (q *queue) take(params interface{}, result ...interface{}) (*Task, error) { + qd := queueData{q: q} + if len(result) > 0 { + qd.result = result[0] + } + if err := q.conn.CallTyped(q.cmds.take, []interface{}{params}, &qd); err != nil { + return nil, err + } + return qd.task, nil } // Drop queue. @@ -195,14 +230,11 @@ func (q *queue) Drop() error { // Look at a task without changing its state. func (q *queue) Peek(taskId uint64) (*Task, error) { - resp, err := q.conn.Call(q.cmds.peek, []interface{}{taskId}) - if err != nil { + qd := queueData{q: q} + if err := q.conn.CallTyped(q.cmds.peek, []interface{}{taskId}, &qd); err != nil { return nil, err } - - t, err := toTask(resp.Data, q) - - return t, err + return qd.task, nil } func (q *queue) _ack(taskId uint64) (string, error) { @@ -221,16 +253,11 @@ func (q *queue) _release(taskId uint64, cfg Opts) (string, error) { return q.produce(q.cmds.release, taskId, cfg.toMap()) } func (q *queue) produce(cmd string, params ...interface{}) (string, error) { - resp, err := q.conn.Call(cmd, params) - if err != nil { - return "", err - } - - t, err := toTask(resp.Data, q) - if err != nil { + qd := queueData{q: q} + if err := q.conn.CallTyped(cmd, params, &qd); err != nil || qd.task == nil { return "", err } - return t.status, nil + return qd.task.status, nil } // Reverse the effect of a bury request on one or more tasks. @@ -275,18 +302,26 @@ func makeCmd(q *queue) { } } -func toTask(responseData []interface{}, q *queue) (*Task, error) { - if len(responseData) != 0 { - data, ok := responseData[0].([]interface{}) - if ok && len(data) >= 3 { - return &Task{ - data[0].(uint64), - data[1].(string), - data[2], - q, - }, nil - } +type queueData struct { + q *queue + task *Task + result interface{} +} + +func (qd *queueData) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l > 1 { + return fmt.Errorf("array len doesn't match for queue data: %d", l) + } + if l == 0 { + return nil } - return nil, nil + qd.task = &Task{data: qd.result, q: qd.q} + d.Decode(&qd.task) + return nil } diff --git a/queue/queue_test.go b/queue/queue_test.go index 53b9b2415..3488b2896 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -1,10 +1,13 @@ package queue_test import ( - . "github.com/tarantool/go-tarantool" + "fmt" "testing" "time" + + . "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/queue" + msgpack "gopkg.in/vmihailenco/msgpack.v2" ) var server = "127.0.0.1:3013" @@ -18,7 +21,7 @@ var opts = Opts{ /////////QUEUE///////// -func TestFifoQueue(t *testing.T) { +func TestFifoQueue(t *testing.T) { conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) @@ -43,7 +46,7 @@ func TestFifoQueue(t *testing.T) { } } -func TestFifoQueue_GetExist_Statistic(t *testing.T) { +func TestFifoQueue_GetExist_Statistic(t *testing.T) { conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) @@ -61,6 +64,13 @@ func TestFifoQueue_GetExist_Statistic(t *testing.T) { t.Errorf("Failed to create queue: %s", err.Error()) return } + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() ok, err := q.Exists() if err != nil { @@ -85,14 +95,9 @@ func TestFifoQueue_GetExist_Statistic(t *testing.T) { } else if stat == nil { t.Error("Statistic is nil") } - //Drop - err = q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } } -func TestFifoQueue_Put(t *testing.T) { +func TestFifoQueue_Put(t *testing.T) { conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) @@ -135,7 +140,7 @@ func TestFifoQueue_Put(t *testing.T) { } } -func TestFifoQueue_Take(t *testing.T) { +func TestFifoQueue_Take(t *testing.T) { conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) @@ -177,7 +182,6 @@ func TestFifoQueue_Take(t *testing.T) { } } - //Take task, err = q.TakeTimeout(2 * time.Second) if err != nil { @@ -193,7 +197,113 @@ func TestFifoQueue_Take(t *testing.T) { t.Errorf("Task status after take is not taken. Status = ", task.Status()) } - err = task.Ack() + err = task.Ack() + if err != nil { + t.Errorf("Failed ack %s", err.Error()) + } else if !task.IsDone() { + t.Errorf("Task status after take is not done. Status = ", task.Status()) + } + } +} + +type customData struct { + customField string +} + +func (c *customData) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 1 { + return fmt.Errorf("array len doesn't match: %d", l) + } + if c.customField, err = d.DecodeString(); err != nil { + return err + } + return nil +} + +func (c *customData) EncodeMsgpack(e *msgpack.Encoder) error { + if err := e.EncodeSliceLen(1); err != nil { + return err + } + if err := e.EncodeString(c.customField); err != nil { + return err + } + return nil +} + +func TestFifoQueue_TakeTyped(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_queue" + q := queue.New(conn, name) + if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() + + //Put + putData := &customData{customField: "put_data"} + task, err := q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && task == nil { + t.Errorf("Task is nil after put") + return + } else { + typedData, ok := task.Data().(*customData) + if !ok { + t.Errorf("Task data after put has diferent type. %#v != %#v", task.Data(), putData) + } + if *typedData != *putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) + } + } + + //Take + takeData := &customData{} + task, err = q.TakeTypedTimeout(2*time.Second, takeData) + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + } else if task == nil { + t.Errorf("Task is nil after take") + } else { + typedData, ok := task.Data().(*customData) + if !ok { + t.Errorf("Task data after put has diferent type. %#v != %#v", task.Data(), putData) + } + if *typedData != *putData { + t.Errorf("Task data after take not equal with example. %#v != %#v", task.Data(), putData) + } + if *takeData != *putData { + t.Errorf("Task data after take not equal with example. %#v != %#v", task.Data(), putData) + } + if !task.IsTaken() { + t.Errorf("Task status after take is not taken. Status = ", task.Status()) + } + + err = task.Ack() if err != nil { t.Errorf("Failed ack %s", err.Error()) } else if !task.IsDone() { @@ -202,7 +312,7 @@ func TestFifoQueue_Take(t *testing.T) { } } -func TestFifoQueue_Peek(t *testing.T) { +func TestFifoQueue_Peek(t *testing.T) { conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) @@ -244,7 +354,6 @@ func TestFifoQueue_Peek(t *testing.T) { } } - //Peek task, err = q.Peek(task.Id()) if err != nil { @@ -262,7 +371,7 @@ func TestFifoQueue_Peek(t *testing.T) { } } -func TestFifoQueue_Bury_Kick(t *testing.T) { +func TestFifoQueue_Bury_Kick(t *testing.T) { conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) @@ -304,7 +413,6 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { } } - //Bury err = task.Bury() if err != nil { @@ -339,7 +447,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { t.Errorf("Task status after take is not taken. Status = ", task.Status()) } - err = task.Ack() + err = task.Ack() if err != nil { t.Errorf("Failed ack %s", err.Error()) } else if !task.IsDone() { @@ -348,7 +456,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { } } -func TestFifoQueue_Delete(t *testing.T) { +func TestFifoQueue_Delete(t *testing.T) { conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) @@ -471,7 +579,6 @@ func TestFifoQueue_Release(t *testing.T) { return } - //Take task, err = q.Take() if err != nil { @@ -489,7 +596,7 @@ func TestFifoQueue_Release(t *testing.T) { t.Errorf("Task status after take is not taken. Status = ", task.Status()) } - err = task.Ack() + err = task.Ack() if err != nil { t.Errorf("Failed ack %s", err.Error()) } else if !task.IsDone() { @@ -509,8 +616,8 @@ func TestTtlQueue(t *testing.T) { name := "test_queue" cfg := queue.Cfg{ Temporary: true, - Kind: queue.FIFO_TTL, - Opts: queue.Opts{Ttl: 5*time.Second}, + Kind: queue.FIFO_TTL, + Opts: queue.Opts{Ttl: 5 * time.Second}, } q := queue.New(conn, name) if err = q.Create(cfg); err != nil { @@ -566,8 +673,8 @@ func TestTtlQueue_Put(t *testing.T) { name := "test_queue" cfg := queue.Cfg{ Temporary: true, - Kind: queue.FIFO_TTL, - Opts: queue.Opts{Ttl: 5*time.Second}, + Kind: queue.FIFO_TTL, + Opts: queue.Opts{Ttl: 5 * time.Second}, } q := queue.New(conn, name) if err = q.Create(cfg); err != nil { @@ -614,7 +721,7 @@ func TestTtlQueue_Put(t *testing.T) { t.Errorf("Task status after take is not taken. Status = ", task.Status()) } - err = task.Ack() + err = task.Ack() if err != nil { t.Errorf("Failed ack %s", err.Error()) } else if !task.IsDone() { diff --git a/queue/task.go b/queue/task.go index 906974ce8..6d8560bd7 100644 --- a/queue/task.go +++ b/queue/task.go @@ -1,5 +1,11 @@ package queue +import ( + "fmt" + + msgpack "gopkg.in/vmihailenco/msgpack.v2" +) + // Task represents a task from tarantool queue's tube type Task struct { id uint64 @@ -8,6 +14,33 @@ type Task struct { q *queue } +func (t *Task) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l < 3 { + return fmt.Errorf("array len doesn't match: %d", l) + } + if t.id, err = d.DecodeUint64(); err != nil { + return err + } + if t.status, err = d.DecodeString(); err != nil { + return err + } + if t.data != nil { + if err = d.Decode(t.data); err != nil { + return fmt.Errorf("fffuuuu: %s", err) + } + } else { + if t.data, err = d.DecodeInterface(); err != nil { + return err + } + } + return nil +} + // Id is a getter for task id func (t *Task) Id() uint64 { return t.id From c73dddcb2643c128b96bf7db6ae5404703d8ca64 Mon Sep 17 00:00:00 2001 From: Tema Novikov Date: Tue, 8 May 2018 13:37:16 +0300 Subject: [PATCH 197/605] Implement queue.Delete() method --- queue/queue.go | 8 +++++++ queue/queue_test.go | 55 +++++++++++++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/queue/queue.go b/queue/queue.go index e41744e63..331bd2a20 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -50,6 +50,8 @@ type Queue interface { Peek(taskId uint64) (*Task, error) // Kick reverts effect of Task.Bury() for `count` tasks. Kick(count uint64) (uint64, error) + // Delete the task identified by its id. + Delete(taskId uint64) error // Statistic returns some statistic about queue. Statistic() (interface{}, error) } @@ -270,6 +272,12 @@ func (q *queue) Kick(count uint64) (uint64, error) { return id, err } +// Delete the task identified by its id. +func (q *queue) Delete(taskId uint64) error { + _, err := q._delete(taskId) + return err +} + // Return the number of tasks in a queue broken down by task_state, and the number of requests broken down by the type of request. func (q *queue) Statistic() (interface{}, error) { resp, err := q.conn.Call(q.cmds.statistics, []interface{}{q.name}) diff --git a/queue/queue_test.go b/queue/queue_test.go index 3488b2896..1e00354ae 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -457,6 +457,8 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { } func TestFifoQueue_Delete(t *testing.T) { + var err error + conn, err := Connect(server, opts) if err != nil { t.Errorf("Failed to connect: %s", err.Error()) @@ -484,35 +486,50 @@ func TestFifoQueue_Delete(t *testing.T) { }() //Put - putData := "put_data" - task, err := q.Put(putData) + var putData = "put_data" + var tasks = [2]*queue.Task{} + + for i := 0; i < 2; i++ { + tasks[i], err = q.Put(putData) + if err != nil { + t.Errorf("Failed put to queue: %s", err.Error()) + return + } else if err == nil && tasks[i] == nil { + t.Errorf("Task is nil after put") + return + } else { + if tasks[i].Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", tasks[i].Data(), putData) + } + } + } + + //Delete by task method + err = tasks[0].Delete() if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return - } else if err == nil && task == nil { - t.Errorf("Task is nil after put") + t.Errorf("Failed bury task %s", err.Error()) return - } else { - if task.Data() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) - } + } else if !tasks[0].IsDone() { + t.Errorf("Task status after delete is not done. Status = ", tasks[0].Status()) } - //Delete - err = task.Delete() + //Delete by task ID + err = q.Delete(tasks[1].Id()) if err != nil { t.Errorf("Failed bury task %s", err.Error()) return - } else if !task.IsDone() { - t.Errorf("Task status after delete is not done. Status = ", task.Status()) + } else if !tasks[0].IsDone() { + t.Errorf("Task status after delete is not done. Status = ", tasks[0].Status()) } //Take - task, err = q.TakeTimeout(2 * time.Second) - if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) - } else if task != nil { - t.Errorf("Task is not nil after take. Task is %s", task) + for i := 0; i < 2; i++ { + tasks[i], err = q.TakeTimeout(2 * time.Second) + if err != nil { + t.Errorf("Failed take from queue: %s", err.Error()) + } else if tasks[i] != nil { + t.Errorf("Task is not nil after take. Task is %s", tasks[i]) + } } } From d6f007b1fd47addf9955963487d5892a9988d63a Mon Sep 17 00:00:00 2001 From: Sokolov Yura Date: Thu, 10 May 2018 14:31:59 +0300 Subject: [PATCH 198/605] Add Connection.Addr() It returns configured address without any locking, so it can be used in custom logger. Probably fixes #64 --- connection.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/connection.go b/connection.go index 309c63a87..e124f253b 100644 --- a/connection.go +++ b/connection.go @@ -316,6 +316,11 @@ func (conn *Connection) Close() error { return conn.closeConnection(err, true) } +// Addr is configured address of Tarantool socket +func (conn *Connection) Addr() string { + return conn.addr +} + // RemoteAddr is address of Tarantool socket func (conn *Connection) RemoteAddr() string { conn.mutex.Lock() From 25f5fbac45e4280ff297933856c6b178d53762d0 Mon Sep 17 00:00:00 2001 From: Max Shegai Date: Mon, 28 May 2018 14:31:58 +0300 Subject: [PATCH 199/605] fixed index out of range panic on dial() to short address --- connection.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/connection.go b/connection.go index e124f253b..efb7c2f6c 100644 --- a/connection.go +++ b/connection.go @@ -357,20 +357,21 @@ func (conn *Connection) dial() (err error) { timeout = 5 * time.Second } // Unix socket connection - if address[0] == '.' || address[0] == '/' { + addrLen := len(address) + if addrLen > 0 && (address[0] == '.' || address[0] == '/') { network = "unix" - } else if address[0:7] == "unix://" { + } else if addrLen >= 7 && address[0:7] == "unix://" { network = "unix" address = address[7:] - } else if address[0:5] == "unix:" { + } else if addrLen >= 5 && address[0:5] == "unix:" { network = "unix" address = address[5:] - } else if address[0:6] == "unix/:" { + } else if addrLen >= 6 && address[0:6] == "unix/:" { network = "unix" address = address[6:] - } else if address[0:6] == "tcp://" { + } else if addrLen >= 6 && address[0:6] == "tcp://" { address = address[6:] - } else if address[0:4] == "tcp:" { + } else if addrLen >= 4 && address[0:4] == "tcp:" { address = address[4:] } connection, err = net.DialTimeout(network, address, timeout) From bf6ba0700e09c087038fe58a39f000283187afbf Mon Sep 17 00:00:00 2001 From: Babin Oleg Date: Sat, 24 Feb 2018 12:42:49 +0300 Subject: [PATCH 200/605] README changes --- README.md | 133 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 86 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index dae5bda63..91369fd6e 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ func main() { conn, err := tarantool.Connect("127.0.0.1:3301", opts) // conn, err := tarantool.Connect("/path/to/tarantool.socket", opts) if err != nil { - fmt.Println("Connection refused: %s", err.Error()) + fmt.Println("Connection refused:", err) } resp, err := conn.Insert(999, []interface{}{99999, "BB"}) if err != nil { @@ -504,71 +504,110 @@ func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { ## Working with queue ```go +package main +import ( + "gopkg.in/vmihailenco/msgpack.v2" + "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/queue" + "time" + "fmt" + "log" +) -import "github.com/tarantool/go-tarantool/queue" - - -type customData struct {} +type customData struct{ + Dummy bool +} func (c *customData) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + if c.Dummy, err = d.DecodeBool(); err != nil { + return err + } return nil } func (c *customData) EncodeMsgpack(e *msgpack.Encoder) error { - return nil + return e.EncodeBool(c.Dummy) } -opts := tarantool.Opts{...} -conn, err := tarantool.Connect("127.0.0.1:3301", opts) - -cfg := queue.Cfg{ - Temporary: true, - IfNotExist: true, - Kind: tarantool.FIFO_TTL_QUEUE, - Opts: queue.Opts{ - Ttl: 10 * time.Second, - Ttr: 5 * time.Second, - Delay: 3 * time.Second, - Pri: 1, - }, -} +func main() { + opts := tarantool.Opts{ + Timeout: time.Second, + Reconnect: time.Second, + MaxReconnects: 5, + User: "user", + Pass: "pass", + // ... + } + conn, err := tarantool.Connect("127.0.0.1:3301", opts) -que := queue.New(conn, "test_queue") -err = que.Create(cfg) + if err != nil { + log.Fatalf("connection: %s", err) + return + } -// put data -task, err := que.Put("test_data") -fmt.Println("Task id is ", task.GetId()) + cfg := queue.Cfg{ + Temporary: true, + IfNotExists: true, + Kind: queue.FIFO, + Opts: queue.Opts{ + Ttl: 10 * time.Second, + Ttr: 5 * time.Second, + Delay: 3 * time.Second, + Pri: 1, + }, + } + que := queue.New(conn, "test_queue") + if err = que.Create(cfg); err != nil { + log.Fatalf("queue create: %s", err) + return + } -// take data -task, err = que.Take() //blocking operation -fmt.Println("Data is ", task.GetData()) -task.Ack() + // put data + task, err := que.Put("test_data") + if err != nil { + log.Fatalf("put task: %s", err) + } + fmt.Println("Task id is", task.Id()) + // take data + task, err = que.Take() //blocking operation + if err != nil { + log.Fatalf("take task: %s", err) + } + fmt.Println("Data is", task.Data()) + task.Ack() + + // take typed example + putData := customData{} + // put data + task, err = que.Put(&putData) + if err != nil { + log.Fatalf("put typed task: %s", err) + } + fmt.Println("Task id is ", task.Id()) -// take typed example -putData := customData{} -// put data -task, err = que.Put(&putData) -fmt.Println("Task id is ", task.GetId()) + takeData := customData{} + //take data + task, err = que.TakeTyped(&takeData) //blocking operation + if err != nil { + log.Fatalf("take take typed: %s", err) + } + fmt.Println("Data is ", takeData) + // same data + fmt.Println("Data is ", task.Data()) -takeData := customData{} -//take data -task, err = que.TakeTyped(&takeData) //blocking operation -fmt.Println("Data is ", takeData) -// same data -fmt.Println("Data is ", task.GetData()) + task, err = que.Put([]int{1, 2, 3}) + task.Bury() -task, err = que.Put([]int{1, 2, 3}) -task.Bury() + task, err = que.TakeTimeout(2 * time.Second) + if task == nil { + fmt.Println("Task is nil") + } -task, err = que.TakeTimeout(2 * time.Second) -if task == nil { - fmt.Println("Task is nil") + que.Drop() } - -q.Drop() ``` Features of the implementation: From 439146efdfc577091f7f6611ad895568199cad3f Mon Sep 17 00:00:00 2001 From: Alex Opryshko Date: Fri, 30 Nov 2018 14:50:08 +0300 Subject: [PATCH 201/605] fix tests --- queue/example_test.go | 2 +- queue/queue_test.go | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/queue/example_test.go b/queue/example_test.go index 21b83ccf0..b7580cb76 100644 --- a/queue/example_test.go +++ b/queue/example_test.go @@ -60,7 +60,7 @@ func ExampleConnection_Queue() { task, err = q.TakeTimeout(2 * time.Second) if task != nil { - fmt.Printf("Task should be nil, but %s", task) + fmt.Printf("Task should be nil, but %d", task.Id()) return } } diff --git a/queue/queue_test.go b/queue/queue_test.go index 1e00354ae..81e74b780 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -194,14 +194,14 @@ func TestFifoQueue_Take(t *testing.T) { } if !task.IsTaken() { - t.Errorf("Task status after take is not taken. Status = ", task.Status()) + t.Errorf("Task status after take is not taken. Status = %s", task.Status()) } err = task.Ack() if err != nil { t.Errorf("Failed ack %s", err.Error()) } else if !task.IsDone() { - t.Errorf("Task status after take is not done. Status = ", task.Status()) + t.Errorf("Task status after take is not done. Status = %s", task.Status()) } } } @@ -300,14 +300,14 @@ func TestFifoQueue_TakeTyped(t *testing.T) { t.Errorf("Task data after take not equal with example. %#v != %#v", task.Data(), putData) } if !task.IsTaken() { - t.Errorf("Task status after take is not taken. Status = ", task.Status()) + t.Errorf("Task status after take is not taken. Status = %s", task.Status()) } err = task.Ack() if err != nil { t.Errorf("Failed ack %s", err.Error()) } else if !task.IsDone() { - t.Errorf("Task status after take is not done. Status = ", task.Status()) + t.Errorf("Task status after take is not done. Status = %s", task.Status()) } } } @@ -366,7 +366,7 @@ func TestFifoQueue_Peek(t *testing.T) { } if !task.IsReady() { - t.Errorf("Task status after peek is not ready. Status = ", task.Status()) + t.Errorf("Task status after peek is not ready. Status = %s", task.Status()) } } } @@ -419,7 +419,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { t.Errorf("Failed bury task %s", err.Error()) return } else if !task.IsBuried() { - t.Errorf("Task status after bury is not buried. Status = ", task.Status()) + t.Errorf("Task status after bury is not buried. Status = %s", task.Status()) } //Kick @@ -444,14 +444,14 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { } if !task.IsTaken() { - t.Errorf("Task status after take is not taken. Status = ", task.Status()) + t.Errorf("Task status after take is not taken. Status = %s", task.Status()) } err = task.Ack() if err != nil { t.Errorf("Failed ack %s", err.Error()) } else if !task.IsDone() { - t.Errorf("Task status after take is not done. Status = ", task.Status()) + t.Errorf("Task status after take is not done. Status = %s", task.Status()) } } } @@ -510,7 +510,7 @@ func TestFifoQueue_Delete(t *testing.T) { t.Errorf("Failed bury task %s", err.Error()) return } else if !tasks[0].IsDone() { - t.Errorf("Task status after delete is not done. Status = ", tasks[0].Status()) + t.Errorf("Task status after delete is not done. Status = %s", tasks[0].Status()) } //Delete by task ID @@ -519,7 +519,7 @@ func TestFifoQueue_Delete(t *testing.T) { t.Errorf("Failed bury task %s", err.Error()) return } else if !tasks[0].IsDone() { - t.Errorf("Task status after delete is not done. Status = ", tasks[0].Status()) + t.Errorf("Task status after delete is not done. Status = %s", tasks[0].Status()) } //Take @@ -528,7 +528,7 @@ func TestFifoQueue_Delete(t *testing.T) { if err != nil { t.Errorf("Failed take from queue: %s", err.Error()) } else if tasks[i] != nil { - t.Errorf("Task is not nil after take. Task is %s", tasks[i]) + t.Errorf("Task is not nil after take. Task is %d", tasks[i].Id()) } } } @@ -587,7 +587,7 @@ func TestFifoQueue_Release(t *testing.T) { //Release err = task.Release() if err != nil { - t.Errorf("Failed release task% %s", err.Error()) + t.Errorf("Failed release task %s", err.Error()) return } @@ -610,14 +610,14 @@ func TestFifoQueue_Release(t *testing.T) { } if !task.IsTaken() { - t.Errorf("Task status after take is not taken. Status = ", task.Status()) + t.Errorf("Task status after take is not taken. Status = %s", task.Status()) } err = task.Ack() if err != nil { t.Errorf("Failed ack %s", err.Error()) } else if !task.IsDone() { - t.Errorf("Task status after take is not done. Status = ", task.Status()) + t.Errorf("Task status after take is not done. Status = %s", task.Status()) } } } @@ -735,14 +735,14 @@ func TestTtlQueue_Put(t *testing.T) { } if !task.IsTaken() { - t.Errorf("Task status after take is not taken. Status = ", task.Status()) + t.Errorf("Task status after take is not taken. Status = %s", task.Status()) } err = task.Ack() if err != nil { t.Errorf("Failed ack %s", err.Error()) } else if !task.IsDone() { - t.Errorf("Task status after take is not done. Status = ", task.Status()) + t.Errorf("Task status after take is not done. Status = %s", task.Status()) } } } From f65454ca8adaff2c00bec71892192896b55171ca Mon Sep 17 00:00:00 2001 From: Alex Opryshko Date: Fri, 30 Nov 2018 16:51:33 +0300 Subject: [PATCH 202/605] multiconnections --- connection.go | 9 +- connector.go | 40 ++++++ multi/config_m1.lua | 9 ++ multi/config_m2.lua | 9 ++ multi/multi.go | 292 ++++++++++++++++++++++++++++++++++++++++++ multi/multi_test.go | 172 +++++++++++++++++++++++++ queue/example_test.go | 2 +- queue/queue.go | 14 +- queue/queue_test.go | 3 +- 9 files changed, 541 insertions(+), 9 deletions(-) create mode 100644 connector.go create mode 100644 multi/config_m1.lua create mode 100644 multi/config_m2.lua create mode 100644 multi/multi.go create mode 100644 multi/multi_test.go diff --git a/connection.go b/connection.go index efb7c2f6c..4f47629e3 100644 --- a/connection.go +++ b/connection.go @@ -111,6 +111,7 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac // always returns array of array (array of tuples for space related methods). // For Eval* and Call17* tarantool always returns array, but does not forces // array of arrays. + type Connection struct { addr string c net.Conn @@ -132,6 +133,8 @@ type Connection struct { lenbuf [PacketLengthBytes]byte } +var _ = Connector(&Connection{}) // check compatibility with connector interface + type connShard struct { rmut sync.Mutex requests [requestsMap]struct { @@ -224,7 +227,6 @@ type Opts struct { // - If opts.Reconnect is non-zero, then error will be returned only if authorization// fails. But if Tarantool is not reachable, then it will attempt to reconnect later // and will not end attempts on authorization failures. func Connect(addr string, opts Opts) (conn *Connection, err error) { - conn = &Connection{ addr: addr, requestId: 0, @@ -307,6 +309,11 @@ func (conn *Connection) ConnectedNow() bool { return atomic.LoadUint32(&conn.state) == connConnected } +// ClosedNow reports if connection is closed by user or after reconnect. +func (conn *Connection) ClosedNow() bool { + return atomic.LoadUint32(&conn.state) == connClosed +} + // Close closes Connection. // After this method called, there is no way to reopen this Connection. func (conn *Connection) Close() error { diff --git a/connector.go b/connector.go new file mode 100644 index 000000000..d6d87eaca --- /dev/null +++ b/connector.go @@ -0,0 +1,40 @@ +package tarantool + +import "time" + +type Connector interface { + ConnectedNow() bool + Close() error + Ping() (resp *Response, err error) + ConfiguredTimeout() time.Duration + + Select(space, index interface{}, offset, limit, iterator uint32, key interface{}) (resp *Response, err error) + Insert(space interface{}, tuple interface{}) (resp *Response, err error) + Replace(space interface{}, tuple interface{}) (resp *Response, err error) + Delete(space, index interface{}, key interface{}) (resp *Response, err error) + Update(space, index interface{}, key, ops interface{}) (resp *Response, err error) + Upsert(space interface{}, tuple, ops interface{}) (resp *Response, err error) + Call(functionName string, args interface{}) (resp *Response, err error) + Call17(functionName string, args interface{}) (resp *Response, err error) + Eval(expr string, args interface{}) (resp *Response, err error) + + GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) + SelectTyped(space, index interface{}, offset, limit, iterator uint32, key interface{}, result interface{}) (err error) + InsertTyped(space interface{}, tuple interface{}, result interface{}) (err error) + ReplaceTyped(space interface{}, tuple interface{}, result interface{}) (err error) + DeleteTyped(space, index interface{}, key interface{}, result interface{}) (err error) + UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) (err error) + CallTyped(functionName string, args interface{}, result interface{}) (err error) + Call17Typed(functionName string, args interface{}, result interface{}) (err error) + EvalTyped(expr string, args interface{}, result interface{}) (err error) + + SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Future + InsertAsync(space interface{}, tuple interface{}) *Future + ReplaceAsync(space interface{}, tuple interface{}) *Future + DeleteAsync(space, index interface{}, key interface{}) *Future + UpdateAsync(space, index interface{}, key, ops interface{}) *Future + UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *Future + CallAsync(functionName string, args interface{}) *Future + Call17Async(functionName string, args interface{}) *Future + EvalAsync(expr string, args interface{}) *Future +} diff --git a/multi/config_m1.lua b/multi/config_m1.lua new file mode 100644 index 000000000..55f0c63a7 --- /dev/null +++ b/multi/config_m1.lua @@ -0,0 +1,9 @@ +box.cfg{ + listen = 3013, + wal_dir='m1/xlog', + snap_dir='m1/snap', +} + +box.once("init", function() +box.schema.user.create('test', {password = 'test'}) +end) diff --git a/multi/config_m2.lua b/multi/config_m2.lua new file mode 100644 index 000000000..ec8e19eb9 --- /dev/null +++ b/multi/config_m2.lua @@ -0,0 +1,9 @@ +box.cfg{ + listen = 3014, + wal_dir='m2/xlog', + snap_dir='m2/snap', +} + +box.once("init", function() +box.schema.user.create('test', {password = 'test'}) +end) diff --git a/multi/multi.go b/multi/multi.go new file mode 100644 index 000000000..d311cf105 --- /dev/null +++ b/multi/multi.go @@ -0,0 +1,292 @@ +package multi + +import ( + "errors" + "github.com/tarantool/go-tarantool" + "sync" + "sync/atomic" + "time" +) + +const ( + connConnected = iota + connClosed +) + +var ( + ErrEmptyAddrs = errors.New("addrs should not be empty") + ErrWrongCheckTimeout = errors.New("wrong check timeout, must be greater than 0") + ErrNoConnection = errors.New("no active connections") +) + +type ConnectionMulti struct { + addrs []string + connOpts tarantool.Opts + opts OptsMulti + + mutex sync.RWMutex + notify chan tarantool.ConnEvent + state uint32 + control chan struct{} + pool map[string]*tarantool.Connection +} + +var _ = tarantool.Connector(&ConnectionMulti{}) // check compatibility with connector interface + +type OptsMulti struct { + CheckTimeout time.Duration +} + +func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsMulti) (connMulti *ConnectionMulti, err error) { + if len(addrs) == 0 { + return nil, ErrEmptyAddrs + } + if opts.CheckTimeout <= 0 { + return nil, ErrWrongCheckTimeout + } + + notify := make(chan tarantool.ConnEvent, 10 * len(addrs)) // x10 to accept disconnected and closed event (with a margin) + connOpts.Notify = notify + connMulti = &ConnectionMulti{ + addrs: addrs, + connOpts: connOpts, + opts: opts, + notify: notify, + control: make(chan struct{}), + pool: make(map[string]*tarantool.Connection), + } + somebodyAlive, _ := connMulti.warmUp() + if !somebodyAlive { + connMulti.Close() + return nil, ErrNoConnection + } + go connMulti.checker() + + return connMulti, nil +} + +func Connect(addrs []string, connOpts tarantool.Opts) (connMulti *ConnectionMulti, err error) { + opts := OptsMulti{ + CheckTimeout: 1 * time.Second, + } + return ConnectWithOpts(addrs, connOpts, opts) +} + +func (connMulti *ConnectionMulti) warmUp() (somebodyAlive bool, errs []error) { + errs = make([]error, len(connMulti.addrs)) + + for i, addr := range connMulti.addrs { + conn, err := tarantool.Connect(addr, connMulti.connOpts) + errs[i] = err + if conn != nil && err == nil { + connMulti.pool[addr] = conn + if conn.ConnectedNow() { + somebodyAlive = true + } + } + } + return +} + +func (connMulti *ConnectionMulti) getState() uint32 { + return atomic.LoadUint32(&connMulti.state) +} + +func (connMulti *ConnectionMulti) getConnectionFromPool(addr string) (*tarantool.Connection, bool) { + connMulti.mutex.RLock() + defer connMulti.mutex.RUnlock() + conn, ok := connMulti.pool[addr] + return conn, ok +} + +func (connMulti *ConnectionMulti) setConnectionToPool(addr string, conn *tarantool.Connection) { + connMulti.mutex.Lock() + defer connMulti.mutex.Unlock() + connMulti.pool[addr] = conn +} + +func (connMulti *ConnectionMulti) checker() { + for connMulti.getState() != connClosed { + timer := time.NewTimer(connMulti.opts.CheckTimeout) + select { + case <-connMulti.control: + return + case e := <-connMulti.notify: + if connMulti.getState() == connClosed { + return + } + if e.Kind == tarantool.Closed { + addr := e.Conn.Addr() + conn, _ := tarantool.Connect(addr, connMulti.connOpts) + if conn != nil { + connMulti.setConnectionToPool(addr, conn) + } + } + case <-timer.C: + for _, addr := range connMulti.addrs { + if connMulti.getState() == connClosed { + return + } + if conn, ok := connMulti.getConnectionFromPool(addr); ok { + if !conn.ClosedNow() { + continue + } + } + conn, _ := tarantool.Connect(addr, connMulti.connOpts) + if conn != nil { + connMulti.setConnectionToPool(addr, conn) + } + } + } + } +} + +func (connMulti *ConnectionMulti) getCurrentConnection() *tarantool.Connection { + connMulti.mutex.RLock() + defer connMulti.mutex.RUnlock() + for _, addr := range connMulti.addrs { + conn := connMulti.pool[addr] + if conn != nil && conn.ConnectedNow() { + return conn + } + } + + return connMulti.pool[connMulti.addrs[0]] +} + +func (connMulti *ConnectionMulti) ConnectedNow() bool { + return connMulti.getState() == connConnected && connMulti.getCurrentConnection().ConnectedNow() +} + +func (connMulti *ConnectionMulti) Close() (err error) { + connMulti.mutex.Lock() + defer connMulti.mutex.Unlock() + + close(connMulti.control) + connMulti.state = connClosed + + for _, conn := range connMulti.pool { + if err == nil { + err = conn.Close() + } else { + conn.Close() + } + } + return +} + +func (connMulti *ConnectionMulti) Ping() (resp *tarantool.Response, err error) { + return connMulti.getCurrentConnection().Ping() +} + +func (connMulti *ConnectionMulti) ConfiguredTimeout() time.Duration { + return connMulti.getCurrentConnection().ConfiguredTimeout() +} + +func (connMulti *ConnectionMulti) Select(space, index interface{}, offset, limit, iterator uint32, key interface{}) (resp *tarantool.Response, err error) { + return connMulti.getCurrentConnection().Select(space, index, offset, limit, iterator, key) +} + +func (connMulti *ConnectionMulti) Insert(space interface{}, tuple interface{}) (resp *tarantool.Response, err error) { + return connMulti.getCurrentConnection().Insert(space, tuple) +} + +func (connMulti *ConnectionMulti) Replace(space interface{}, tuple interface{}) (resp *tarantool.Response, err error) { + return connMulti.getCurrentConnection().Replace(space, tuple) +} + +func (connMulti *ConnectionMulti) Delete(space, index interface{}, key interface{}) (resp *tarantool.Response, err error) { + return connMulti.getCurrentConnection().Delete(space, index, key) +} + +func (connMulti *ConnectionMulti) Update(space, index interface{}, key, ops interface{}) (resp *tarantool.Response, err error) { + return connMulti.getCurrentConnection().Update(space, index, key, ops) +} + +func (connMulti *ConnectionMulti) Upsert(space interface{}, tuple, ops interface{}) (resp *tarantool.Response, err error) { + return connMulti.Upsert(space, tuple, ops) +} + +func (connMulti *ConnectionMulti) Call(functionName string, args interface{}) (resp *tarantool.Response, err error) { + return connMulti.getCurrentConnection().Call(functionName, args) +} + +func (connMulti *ConnectionMulti) Call17(functionName string, args interface{}) (resp *tarantool.Response, err error) { + return connMulti.getCurrentConnection().Call17(functionName, args) +} + +func (connMulti *ConnectionMulti) Eval(expr string, args interface{}) (resp *tarantool.Response, err error) { + return connMulti.getCurrentConnection().Eval(expr, args) +} + +func (connMulti *ConnectionMulti) GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) { + return connMulti.getCurrentConnection().GetTyped(space, index, key, result) +} + +func (connMulti *ConnectionMulti) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key interface{}, result interface{}) (err error) { + return connMulti.getCurrentConnection().SelectTyped(space, index, offset, limit, iterator, key, result) +} + +func (connMulti *ConnectionMulti) InsertTyped(space interface{}, tuple interface{}, result interface{}) (err error) { + return connMulti.getCurrentConnection().InsertTyped(space, tuple, result) +} + +func (connMulti *ConnectionMulti) ReplaceTyped(space interface{}, tuple interface{}, result interface{}) (err error) { + return connMulti.getCurrentConnection().ReplaceTyped(space, tuple, result) +} + +func (connMulti *ConnectionMulti) DeleteTyped(space, index interface{}, key interface{}, result interface{}) (err error) { + return connMulti.getCurrentConnection().DeleteTyped(space, index, key, result) +} + +func (connMulti *ConnectionMulti) UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) (err error) { + return connMulti.getCurrentConnection().UpdateTyped(space, index, key, ops, result) +} + +func (connMulti *ConnectionMulti) CallTyped(functionName string, args interface{}, result interface{}) (err error) { + return connMulti.getCurrentConnection().CallTyped(functionName, args, result) +} + +func (connMulti *ConnectionMulti) Call17Typed(functionName string, args interface{}, result interface{}) (err error) { + return connMulti.getCurrentConnection().Call17Typed(functionName, args, result) +} + +func (connMulti *ConnectionMulti) EvalTyped(expr string, args interface{}, result interface{}) (err error) { + return connMulti.getCurrentConnection().EvalTyped(expr, args, result) +} + +func (connMulti *ConnectionMulti) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *tarantool.Future { + return connMulti.getCurrentConnection().SelectAsync(space, index, offset, limit, iterator, key) +} + +func (connMulti *ConnectionMulti) InsertAsync(space interface{}, tuple interface{}) *tarantool.Future { + return connMulti.getCurrentConnection().InsertAsync(space, tuple) +} + +func (connMulti *ConnectionMulti) ReplaceAsync(space interface{}, tuple interface{}) *tarantool.Future { + return connMulti.getCurrentConnection().ReplaceAsync(space, tuple) +} + +func (connMulti *ConnectionMulti) DeleteAsync(space, index interface{}, key interface{}) *tarantool.Future { + return connMulti.getCurrentConnection().DeleteAsync(space, index, key) +} + +func (connMulti *ConnectionMulti) UpdateAsync(space, index interface{}, key, ops interface{}) *tarantool.Future { + return connMulti.getCurrentConnection().UpdateAsync(space, index, key, ops) +} + +func (connMulti *ConnectionMulti) UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *tarantool.Future { + return connMulti.getCurrentConnection().UpsertAsync(space, tuple, ops) +} + +func (connMulti *ConnectionMulti) CallAsync(functionName string, args interface{}) *tarantool.Future { + return connMulti.getCurrentConnection().CallAsync(functionName, args) +} + +func (connMulti *ConnectionMulti) Call17Async(functionName string, args interface{}) *tarantool.Future { + return connMulti.getCurrentConnection().Call17Async(functionName, args) +} + +func (connMulti *ConnectionMulti) EvalAsync(expr string, args interface{}) *tarantool.Future { + return connMulti.getCurrentConnection().EvalAsync(expr, args) +} diff --git a/multi/multi_test.go b/multi/multi_test.go new file mode 100644 index 000000000..7be9f9f00 --- /dev/null +++ b/multi/multi_test.go @@ -0,0 +1,172 @@ +package multi + +import ( + "testing" + "time" + + "github.com/tarantool/go-tarantool" +) + +var server1 = "127.0.0.1:3013" +var server2 = "127.0.0.1:3014" +var connOpts = tarantool.Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +func TestConnError_IncorrectParams(t *testing.T) { + multiConn, err := Connect([]string{}, tarantool.Opts{}) + if err == nil { + t.Errorf("err is nil with incorrect params") + } + if multiConn != nil { + t.Errorf("conn is not nill with incorrect params") + } + if err.Error() != "addrs should not be empty" { + t.Errorf("incorrect error: %s", err.Error()) + } + + multiConn, err = ConnectWithOpts([]string{server1}, tarantool.Opts{}, OptsMulti{}) + if err == nil { + t.Errorf("err is nil with incorrect params") + } + if multiConn != nil { + t.Errorf("conn is not nill with incorrect params") + } + if err.Error() != "wrong check timeout, must be greater than 0" { + t.Errorf("incorrect error: %s", err.Error()) + } +} + +func TestConnError_Connection(t *testing.T) { + multiConn, err := Connect([]string{"err1", "err2"}, connOpts) + if err == nil { + t.Errorf("err is nil with incorrect params") + return + } + if multiConn != nil { + t.Errorf("conn is not nil with incorrect params") + return + } +} + +func TestConnSuccessfully(t *testing.T) { + multiConn, err := Connect([]string{"err", server1}, connOpts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if multiConn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer multiConn.Close() + + if !multiConn.ConnectedNow() { + t.Errorf("conn has incorrect status") + return + } + if multiConn.getCurrentConnection().Addr() != server1 { + t.Errorf("conn has incorrect addr") + return + } +} + +func TestReconnect(t *testing.T) { + multiConn, _ := Connect([]string{server1, server2}, connOpts) + if multiConn == nil { + t.Errorf("conn is nil after Connect") + return + } + timer := time.NewTimer(300 * time.Millisecond) + <-timer.C + defer multiConn.Close() + + conn, _ := multiConn.getConnectionFromPool(server1) + conn.Close() + + if multiConn.getCurrentConnection().Addr() == server1 { + t.Errorf("conn has incorrect addr: %s after disconnect server1", multiConn.getCurrentConnection().Addr()) + } + if !multiConn.ConnectedNow() { + t.Errorf("incorrect multiConn status after reconecting") + } + + timer = time.NewTimer(100 * time.Millisecond) + <-timer.C + conn, _ = multiConn.getConnectionFromPool(server1) + if !conn.ConnectedNow() { + t.Errorf("incorrect conn status after reconecting") + } +} + +func TestDisconnectAll(t *testing.T) { + multiConn, _ := Connect([]string{server1, server2}, connOpts) + if multiConn == nil { + t.Errorf("conn is nil after Connect") + return + } + timer := time.NewTimer(300 * time.Millisecond) + <-timer.C + defer multiConn.Close() + + conn, _ := multiConn.getConnectionFromPool(server1) + conn.Close() + conn, _ = multiConn.getConnectionFromPool(server2) + conn.Close() + + if multiConn.ConnectedNow() { + t.Errorf("incorrect status after desconnect all") + } + + timer = time.NewTimer(100 * time.Millisecond) + <-timer.C + if !multiConn.ConnectedNow() { + t.Errorf("incorrect multiConn status after reconecting") + } + conn, _ = multiConn.getConnectionFromPool(server1) + if !conn.ConnectedNow() { + t.Errorf("incorrect server1 conn status after reconecting") + } + conn, _ = multiConn.getConnectionFromPool(server2) + if !conn.ConnectedNow() { + t.Errorf("incorrect server2 conn status after reconecting") + } +} + +func TestClose(t *testing.T) { + multiConn, _ := Connect([]string{server1, server2}, connOpts) + if multiConn == nil { + t.Errorf("conn is nil after Connect") + return + } + timer := time.NewTimer(300 * time.Millisecond) + <-timer.C + + conn, _ := multiConn.getConnectionFromPool(server1) + if !conn.ConnectedNow() { + t.Errorf("incorrect conn server1 status") + } + conn, _ = multiConn.getConnectionFromPool(server2) + if !conn.ConnectedNow() { + t.Errorf("incorrect conn server2 status") + } + + multiConn.Close() + timer = time.NewTimer(100 * time.Millisecond) + <-timer.C + + if multiConn.ConnectedNow() { + t.Errorf("incorrect multiConn status after close") + } + conn, _ = multiConn.getConnectionFromPool(server1) + if conn.ConnectedNow() { + t.Errorf("incorrect server1 conn status after close") + } + conn, _ = multiConn.getConnectionFromPool(server2) + if conn.ConnectedNow() { + t.Errorf("incorrect server2 conn status after close") + } +} + diff --git a/queue/example_test.go b/queue/example_test.go index b7580cb76..0e14a9fca 100644 --- a/queue/example_test.go +++ b/queue/example_test.go @@ -3,8 +3,8 @@ package queue_test import ( "fmt" "github.com/tarantool/go-tarantool" - "time" "github.com/tarantool/go-tarantool/queue" + "time" ) func ExampleConnection_Queue() { diff --git a/queue/queue.go b/queue/queue.go index 331bd2a20..e32309952 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -58,7 +58,7 @@ type Queue interface { type queue struct { name string - conn *tarantool.Connection + conn tarantool.Connector cmds cmd } @@ -128,7 +128,7 @@ func (opts Opts) toMap() map[string]interface{} { } // New creates a queue handle -func New(conn *tarantool.Connection, name string) Queue { +func New(conn tarantool.Connector, name string) Queue { q := &queue{ name: name, conn: conn, @@ -180,8 +180,9 @@ func (q *queue) put(params ...interface{}) (*Task, error) { // The take request searches for a task in the queue. func (q *queue) Take() (*Task, error) { var params interface{} - if q.conn.ConfiguredTimeout() > 0 { - params = (q.conn.ConfiguredTimeout() * 9 / 10).Seconds() + timeout := q.conn.ConfiguredTimeout() + if timeout > 0 { + params = (timeout * 9 / 10).Seconds() } return q.take(params) } @@ -198,8 +199,9 @@ func (q *queue) TakeTimeout(timeout time.Duration) (*Task, error) { // The take request searches for a task in the queue. func (q *queue) TakeTyped(result interface{}) (*Task, error) { var params interface{} - if q.conn.ConfiguredTimeout() > 0 { - params = (q.conn.ConfiguredTimeout() * 9 / 10).Seconds() + timeout := q.conn.ConfiguredTimeout() + if timeout > 0 { + params = (timeout * 9 / 10).Seconds() } return q.take(params, result) } diff --git a/queue/queue_test.go b/queue/queue_test.go index 81e74b780..535faec5f 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -7,7 +7,7 @@ import ( . "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/queue" - msgpack "gopkg.in/vmihailenco/msgpack.v2" + "gopkg.in/vmihailenco/msgpack.v2" ) var server = "127.0.0.1:3013" @@ -195,6 +195,7 @@ func TestFifoQueue_Take(t *testing.T) { if !task.IsTaken() { t.Errorf("Task status after take is not taken. Status = %s", task.Status()) + } err = task.Ack() From 61b54eff8f6861149f5086eebeafefcecefdf61b Mon Sep 17 00:00:00 2001 From: Alex Opryshko Date: Fri, 30 Nov 2018 19:04:22 +0300 Subject: [PATCH 203/605] improvements --- multi/config_m1.lua | 9 +++++---- multi/config_m2.lua | 9 +++++---- multi/multi.go | 32 +++++++++++++++++++++++++++----- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/multi/config_m1.lua b/multi/config_m1.lua index 55f0c63a7..67e991b14 100644 --- a/multi/config_m1.lua +++ b/multi/config_m1.lua @@ -1,9 +1,10 @@ -box.cfg{ +box.cfg { listen = 3013, - wal_dir='m1/xlog', - snap_dir='m1/snap', + wal_dir = 'm1/xlog', + snap_dir = 'm1/snap', } box.once("init", function() -box.schema.user.create('test', {password = 'test'}) + box.schema.user.create('test', { password = 'test' }) + box.schema.user.grant('test', 'read,write,execute', 'universe') end) diff --git a/multi/config_m2.lua b/multi/config_m2.lua index ec8e19eb9..4e1ec21cb 100644 --- a/multi/config_m2.lua +++ b/multi/config_m2.lua @@ -1,9 +1,10 @@ -box.cfg{ +box.cfg { listen = 3014, - wal_dir='m2/xlog', - snap_dir='m2/snap', + wal_dir = 'm2/xlog', + snap_dir = 'm2/snap', } box.once("init", function() -box.schema.user.create('test', {password = 'test'}) + box.schema.user.create('test', { password = 'test' }) + box.schema.user.grant('test', 'read,write,execute', 'universe') end) diff --git a/multi/multi.go b/multi/multi.go index d311cf105..c4acbc373 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -29,6 +29,7 @@ type ConnectionMulti struct { state uint32 control chan struct{} pool map[string]*tarantool.Connection + fallback *tarantool.Connection } var _ = tarantool.Connector(&ConnectionMulti{}) // check compatibility with connector interface @@ -79,6 +80,9 @@ func (connMulti *ConnectionMulti) warmUp() (somebodyAlive bool, errs []error) { conn, err := tarantool.Connect(addr, connMulti.connOpts) errs[i] = err if conn != nil && err == nil { + if connMulti.fallback == nil { + connMulti.fallback = conn + } connMulti.pool[addr] = conn if conn.ConnectedNow() { somebodyAlive = true @@ -105,6 +109,12 @@ func (connMulti *ConnectionMulti) setConnectionToPool(addr string, conn *taranto connMulti.pool[addr] = conn } +func (connMulti *ConnectionMulti) deleteConnectionFromPool(addr string) { + connMulti.mutex.Lock() + defer connMulti.mutex.Unlock() + delete(connMulti.pool, addr) +} + func (connMulti *ConnectionMulti) checker() { for connMulti.getState() != connClosed { timer := time.NewTimer(connMulti.opts.CheckTimeout) @@ -115,11 +125,16 @@ func (connMulti *ConnectionMulti) checker() { if connMulti.getState() == connClosed { return } - if e.Kind == tarantool.Closed { + if e.Conn.ClosedNow() { addr := e.Conn.Addr() + if _, ok := connMulti.getConnectionFromPool(addr); !ok { + continue + } conn, _ := tarantool.Connect(addr, connMulti.connOpts) if conn != nil { connMulti.setConnectionToPool(addr, conn) + } else { + connMulti.deleteConnectionFromPool(addr) } } case <-timer.C: @@ -144,14 +159,17 @@ func (connMulti *ConnectionMulti) checker() { func (connMulti *ConnectionMulti) getCurrentConnection() *tarantool.Connection { connMulti.mutex.RLock() defer connMulti.mutex.RUnlock() + for _, addr := range connMulti.addrs { conn := connMulti.pool[addr] - if conn != nil && conn.ConnectedNow() { - return conn + if conn != nil { + if conn.ConnectedNow() { + return conn + } + connMulti.fallback = conn } } - - return connMulti.pool[connMulti.addrs[0]] + return connMulti.fallback } func (connMulti *ConnectionMulti) ConnectedNow() bool { @@ -172,6 +190,10 @@ func (connMulti *ConnectionMulti) Close() (err error) { conn.Close() } } + if connMulti.fallback != nil { + connMulti.fallback.Close() + } + return } From fe3fbd74beebd57de15f706a7e1b63a18bdd9fc3 Mon Sep 17 00:00:00 2001 From: "Motkov.Kirill" Date: Wed, 9 Jan 2019 15:52:24 +0300 Subject: [PATCH 204/605] readme: add missing "go" marker --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 91369fd6e..de0fc746f 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ of errors that the Tarantool server returns. We can now have a closer look at the `example.go` program and make some observations about what it does. -``` +```go package main import ( From aaa93c4bdc35e6e90b852a1f5a070d4319a623cb Mon Sep 17 00:00:00 2001 From: Mike Siomkin Date: Sat, 30 Mar 2019 19:29:21 +0300 Subject: [PATCH 205/605] Fix ConnectionMulti Upsert infinite recursive call --- multi/multi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi/multi.go b/multi/multi.go index c4acbc373..50e77d46c 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -226,7 +226,7 @@ func (connMulti *ConnectionMulti) Update(space, index interface{}, key, ops inte } func (connMulti *ConnectionMulti) Upsert(space interface{}, tuple, ops interface{}) (resp *tarantool.Response, err error) { - return connMulti.Upsert(space, tuple, ops) + return connMulti.getCurrentConnection().Upsert(space, tuple, ops) } func (connMulti *ConnectionMulti) Call(functionName string, args interface{}) (resp *tarantool.Response, err error) { From 30b00068fca47a9fd36caf0dfbceda59758d27ed Mon Sep 17 00:00:00 2001 From: pavemaksim Date: Wed, 13 Feb 2019 18:14:56 +0300 Subject: [PATCH 206/605] Typo fixed in Opts.Reconnect description Past participle for "put" is "put" --- connection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connection.go b/connection.go index 4f47629e3..e67a0aeb7 100644 --- a/connection.go +++ b/connection.go @@ -171,7 +171,7 @@ type Opts struct { User string // Pass is password for authorization Pass string - // RateLimit limits number of 'in-fly' request, ie aready putted into + // RateLimit limits number of 'in-fly' request, ie already put into // requests queue, but not yet answered by server or timeouted. // It is disabled by default. // See RLimitAction for possible actions when RateLimit.reached. From a23ca0eac5396f62b743dcc2b732bf41ce65e046 Mon Sep 17 00:00:00 2001 From: Denis Ignatenko Date: Wed, 24 Jul 2019 18:29:54 +0300 Subject: [PATCH 207/605] Get node list from nodes (#81) * NodesGetFunctionName - lua function name to get nodes list ClusterDiscoveryTime - interval in seconds for refresh After new nodes list accquired current pool will be rebuild. New connections will be established and old wich is not in list will be closed and removed from pool. Lua function example added * Move from Call17 to Call17Typed * Timer-Ticker fix for correct channel processing --- README.md | 15 +++++++ multi/config_load_nodes.lua | 7 ++++ multi/config_m1.lua | 4 ++ multi/config_m2.lua | 4 ++ multi/multi.go | 79 +++++++++++++++++++++++++++++++------ multi/multi_test.go | 40 +++++++++++++++++-- 6 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 multi/config_load_nodes.lua diff --git a/README.md b/README.md index de0fc746f..0f1c85928 100644 --- a/README.md +++ b/README.md @@ -614,6 +614,21 @@ Features of the implementation: - If you use connection timeout and call `TakeWithTimeout` with parameter greater than the connection timeout then parameter reduced to it - If you use connection timeout and call `Take` then we return a error if we can not take task from queue in a time equal to the connection timeout +## Multi connections + +You can use multiple connections config with tatantool/multi. + +Main features: + +- Check active connection with configurable time interval and on connection fail switch to next in pool. +- Get addresses list from server and reconfigure to use in MultiConnection. + +Additional options (configurable via `ConnectWithOpts`): + +* `CheckTimeout` - time interval to check for connection timeout and try to switch connection +* `ClusterDiscoveryTime` - time interval to ask server for updated address list (works on with `NodesGetFunctionName` set) +* `NodesGetFunctionName` - server lua function name to call for getting address list + ## Alternative connectors - https://github.com/viciious/go-tarantool diff --git a/multi/config_load_nodes.lua b/multi/config_load_nodes.lua new file mode 100644 index 000000000..4df87fc28 --- /dev/null +++ b/multi/config_load_nodes.lua @@ -0,0 +1,7 @@ +local function get_cluster_nodes() + return { 'localhost:3013', 'localhost:3014' } +end + +return { + get_cluster_nodes = get_cluster_nodes +} \ No newline at end of file diff --git a/multi/config_m1.lua b/multi/config_m1.lua index 67e991b14..5e364b5cc 100644 --- a/multi/config_m1.lua +++ b/multi/config_m1.lua @@ -1,9 +1,13 @@ +local nodes_load = require("config_load_nodes") + box.cfg { listen = 3013, wal_dir = 'm1/xlog', snap_dir = 'm1/snap', } +get_cluster_nodes = nodes_load.get_cluster_nodes + box.once("init", function() box.schema.user.create('test', { password = 'test' }) box.schema.user.grant('test', 'read,write,execute', 'universe') diff --git a/multi/config_m2.lua b/multi/config_m2.lua index 4e1ec21cb..cf73da319 100644 --- a/multi/config_m2.lua +++ b/multi/config_m2.lua @@ -1,9 +1,13 @@ +local nodes_load = require("config_load_nodes") + box.cfg { listen = 3014, wal_dir = 'm2/xlog', snap_dir = 'm2/snap', } +get_cluster_nodes = nodes_load.get_cluster_nodes + box.once("init", function() box.schema.user.create('test', { password = 'test' }) box.schema.user.grant('test', 'read,write,execute', 'universe') diff --git a/multi/multi.go b/multi/multi.go index 50e77d46c..0bf2b03c9 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -2,23 +2,33 @@ package multi import ( "errors" - "github.com/tarantool/go-tarantool" "sync" "sync/atomic" "time" + + "github.com/tarantool/go-tarantool" ) const ( - connConnected = iota + connConnected = iota connClosed ) var ( - ErrEmptyAddrs = errors.New("addrs should not be empty") + ErrEmptyAddrs = errors.New("addrs should not be empty") ErrWrongCheckTimeout = errors.New("wrong check timeout, must be greater than 0") - ErrNoConnection = errors.New("no active connections") + ErrNoConnection = errors.New("no active connections") ) +func indexOf(sstring string, data []string) int { + for i, v := range data { + if sstring == v { + return i + } + } + return -1 +} + type ConnectionMulti struct { addrs []string connOpts tarantool.Opts @@ -35,7 +45,9 @@ type ConnectionMulti struct { var _ = tarantool.Connector(&ConnectionMulti{}) // check compatibility with connector interface type OptsMulti struct { - CheckTimeout time.Duration + CheckTimeout time.Duration + NodesGetFunctionName string + ClusterDiscoveryTime time.Duration } func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsMulti) (connMulti *ConnectionMulti, err error) { @@ -45,16 +57,19 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsMulti) (c if opts.CheckTimeout <= 0 { return nil, ErrWrongCheckTimeout } + if opts.ClusterDiscoveryTime <= 0 { + opts.ClusterDiscoveryTime = 60 * time.Second + } - notify := make(chan tarantool.ConnEvent, 10 * len(addrs)) // x10 to accept disconnected and closed event (with a margin) + notify := make(chan tarantool.ConnEvent, 10*len(addrs)) // x10 to accept disconnected and closed event (with a margin) connOpts.Notify = notify connMulti = &ConnectionMulti{ - addrs: addrs, - connOpts: connOpts, - opts: opts, - notify: notify, - control: make(chan struct{}), - pool: make(map[string]*tarantool.Connection), + addrs: addrs, + connOpts: connOpts, + opts: opts, + notify: notify, + control: make(chan struct{}), + pool: make(map[string]*tarantool.Connection), } somebodyAlive, _ := connMulti.warmUp() if !somebodyAlive { @@ -116,8 +131,14 @@ func (connMulti *ConnectionMulti) deleteConnectionFromPool(addr string) { } func (connMulti *ConnectionMulti) checker() { + + refreshTimer := time.NewTicker(connMulti.opts.ClusterDiscoveryTime) + timer := time.NewTicker(connMulti.opts.CheckTimeout) + defer refreshTimer.Stop() + defer timer.Stop() + for connMulti.getState() != connClosed { - timer := time.NewTimer(connMulti.opts.CheckTimeout) + select { case <-connMulti.control: return @@ -137,6 +158,38 @@ func (connMulti *ConnectionMulti) checker() { connMulti.deleteConnectionFromPool(addr) } } + case <-refreshTimer.C: + if connMulti.getState() == connClosed || connMulti.opts.NodesGetFunctionName == "" { + continue + } + var resp [][]string + err := connMulti.Call17Typed(connMulti.opts.NodesGetFunctionName, []interface{}{}, &resp) + if err != nil { + continue + } + if len(resp) > 0 && len(resp[0]) > 0 { + addrs := resp[0] + // Fill pool with new connections + for _, v := range addrs { + if indexOf(v, connMulti.addrs) < 0 { + conn, _ := tarantool.Connect(v, connMulti.connOpts) + if conn != nil { + connMulti.setConnectionToPool(v, conn) + } + } + } + // Clear pool from obsolete connections + for _, v := range connMulti.addrs { + if indexOf(v, addrs) < 0 { + con, ok := connMulti.getConnectionFromPool(v) + if con != nil && ok { + con.Close() + } + connMulti.deleteConnectionFromPool(v) + } + } + connMulti.addrs = addrs + } case <-timer.C: for _, addr := range connMulti.addrs { if connMulti.getState() == connClosed { diff --git a/multi/multi_test.go b/multi/multi_test.go index 7be9f9f00..4515c6232 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -10,9 +10,15 @@ import ( var server1 = "127.0.0.1:3013" var server2 = "127.0.0.1:3014" var connOpts = tarantool.Opts{ - Timeout: 500 * time.Millisecond, - User: "test", - Pass: "test", + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +var connOptsMulti = OptsMulti{ + CheckTimeout: 1 * time.Second, + NodesGetFunctionName: "get_cluster_nodes", + ClusterDiscoveryTime: 3 * time.Second, } func TestConnError_IncorrectParams(t *testing.T) { @@ -170,3 +176,31 @@ func TestClose(t *testing.T) { } } +func TestRefresh(t *testing.T) { + + multiConn, _ := ConnectWithOpts([]string{server1, server2}, connOpts, connOptsMulti) + if multiConn == nil { + t.Errorf("conn is nil after Connect") + return + } + curAddr := multiConn.addrs[0] + + // wait for refresh timer + // scenario 1 nodeload, 1 refresh, 1 nodeload + time.Sleep(10 * time.Second) + + newAddr := multiConn.addrs[0] + + if curAddr == newAddr { + t.Errorf("Expect address refresh") + } + + if !multiConn.ConnectedNow() { + t.Errorf("Expect connection to exist") + } + + _, err := multiConn.Call(multiConn.opts.NodesGetFunctionName, []interface{}{}) + if err != nil { + t.Error("Expect to get data after reconnect") + } +} From 821fb5b5955098f8960dfa91862382da5eb0e986 Mon Sep 17 00:00:00 2001 From: Oleg Utkin Date: Sun, 10 Nov 2019 18:34:32 +0300 Subject: [PATCH 208/605] fix typo (#84) --- queue/queue.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queue/queue.go b/queue/queue.go index e32309952..d42caef7b 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -146,7 +146,7 @@ func (q *queue) Create(cfg Cfg) error { // Exists checks existance of a tube func (q *queue) Exists() (bool, error) { - cmd := "local name = ... ; return queue.tube[name] ~= null" + cmd := "local name = ... ; return queue.tube[name] ~= nil" resp, err := q.conn.Eval(cmd, []string{q.name}) if err != nil { return false, err From 4d6019668ea32874fd94bb9c06e0f4f698fbe51e Mon Sep 17 00:00:00 2001 From: yura Date: Sun, 29 Dec 2019 21:03:47 +0300 Subject: [PATCH 209/605] goimports a bit --- connection.go | 3 ++- example_test.go | 3 ++- queue/example_test.go | 5 +++-- queue/queue.go | 4 ++-- response.go | 1 + tarantool_test.go | 5 +++-- 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/connection.go b/connection.go index e67a0aeb7..443276ac4 100644 --- a/connection.go +++ b/connection.go @@ -5,7 +5,6 @@ import ( "bytes" "errors" "fmt" - "gopkg.in/vmihailenco/msgpack.v2" "io" "log" "net" @@ -13,6 +12,8 @@ import ( "sync" "sync/atomic" "time" + + "gopkg.in/vmihailenco/msgpack.v2" ) const requestsMap = 128 diff --git a/example_test.go b/example_test.go index 4176025ee..f5f6f8f90 100644 --- a/example_test.go +++ b/example_test.go @@ -2,8 +2,9 @@ package tarantool_test import ( "fmt" - "github.com/tarantool/go-tarantool" "time" + + "github.com/tarantool/go-tarantool" ) type Tuple struct { diff --git a/queue/example_test.go b/queue/example_test.go index 0e14a9fca..cf3150260 100644 --- a/queue/example_test.go +++ b/queue/example_test.go @@ -2,16 +2,17 @@ package queue_test import ( "fmt" + "time" + "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/queue" - "time" ) func ExampleConnection_Queue() { cfg := queue.Cfg{ Temporary: false, Kind: queue.FIFO, - Opts: queue.Opts{ + Opts: queue.Opts{ Ttl: 10 * time.Second, }, } diff --git a/queue/queue.go b/queue/queue.go index d42caef7b..bc308f37c 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -276,8 +276,8 @@ func (q *queue) Kick(count uint64) (uint64, error) { // Delete the task identified by its id. func (q *queue) Delete(taskId uint64) error { - _, err := q._delete(taskId) - return err + _, err := q._delete(taskId) + return err } // Return the number of tasks in a queue broken down by task_state, and the number of requests broken down by the type of request. diff --git a/response.go b/response.go index d73607c51..6363a4fe4 100644 --- a/response.go +++ b/response.go @@ -2,6 +2,7 @@ package tarantool import ( "fmt" + "gopkg.in/vmihailenco/msgpack.v2" ) diff --git a/tarantool_test.go b/tarantool_test.go index 31a14f59c..7c0552138 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2,12 +2,13 @@ package tarantool_test import ( "fmt" - . "github.com/tarantool/go-tarantool" - "gopkg.in/vmihailenco/msgpack.v2" "strings" "sync" "testing" "time" + + . "github.com/tarantool/go-tarantool" + "gopkg.in/vmihailenco/msgpack.v2" ) type Member struct { From f4ece3508d872855a094b54eff26b2017033b641 Mon Sep 17 00:00:00 2001 From: yura Date: Sun, 29 Dec 2019 21:18:00 +0300 Subject: [PATCH 210/605] add Future.WaitChan() and Future.Err() resolves #86 --- request.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/request.go b/request.go index bc90dd7fc..606dcb91a 100644 --- a/request.go +++ b/request.go @@ -435,3 +435,25 @@ func (fut *Future) GetTyped(result interface{}) error { fut.err = fut.resp.decodeBodyTyped(result) return fut.err } + +var closedChan = make(chan struct{}) + +func init() { + close(closedChan) +} + +// WaitChan returns channel which becomes closed when response arrived or error occured +func (fut *Future) WaitChan() <-chan struct{} { + if fut.ready == nil { + return closedChan + } + return fut.ready +} + +// Err returns error set on Future. +// It waits for future to be set. +// Note: it doesn't decode body, therefore decoding error are not set here. +func (fut *Future) Err() error { + fut.wait() + return fut.err +} From 1120a47d1d7d753f2f69d772289aa7681c9cd822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D1=81=D0=BA=D0=B0=D0=BD=D0=B4=D0=B0=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=95=D0=B3=D0=BE=D1=80?= Date: Wed, 29 Jul 2020 22:07:11 +0300 Subject: [PATCH 211/605] Add utube handling (without unrelated changes) (#85) * Add utube handling * undo changes from previous commit that are unrelated to the utube handling PR Co-authored-by: andrew Co-authored-by: egor.iskandarov --- queue/queue.go | 5 ++++ queue/queue_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/queue/queue.go b/queue/queue.go index bc308f37c..69dcf8bcd 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -103,6 +103,7 @@ type Opts struct { Ttl time.Duration // task time to live Ttr time.Duration // task time to execute Delay time.Duration // delayed execution + Utube string } func (opts Opts) toMap() map[string]interface{} { @@ -124,6 +125,10 @@ func (opts Opts) toMap() map[string]interface{} { ret["pri"] = opts.Pri } + if opts.Utube != "" { + ret["utube"] = opts.Utube + } + return ret } diff --git a/queue/queue_test.go b/queue/queue_test.go index 535faec5f..d1a909120 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -2,6 +2,7 @@ package queue_test import ( "fmt" + "math" "testing" "time" @@ -747,3 +748,73 @@ func TestTtlQueue_Put(t *testing.T) { } } } + +func TestUtube_Put(t *testing.T) { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + return + } + if conn == nil { + t.Errorf("conn is nil after Connect") + return + } + defer conn.Close() + + name := "test_utube" + cfg := queue.Cfg{ + Temporary: true, + Kind: queue.UTUBE, + IfNotExists: true, + } + q := queue.New(conn, name) + if err = q.Create(cfg); err != nil { + t.Errorf("Failed to create queue: %s", err.Error()) + return + } + defer func() { + //Drop + err := q.Drop() + if err != nil { + t.Errorf("Failed drop queue: %s", err.Error()) + } + }() + + data1 := &customData{"test-data-0"} + _, err = q.PutWithOpts(data1, queue.Opts{Utube: "test-utube-consumer-key"}) + if err != nil { + t.Fatalf("Failed put task to queue: %s", err.Error()) + } + data2 := &customData{"test-data-1"} + _, err = q.PutWithOpts(data2, queue.Opts{Utube: "test-utube-consumer-key"}) + if err != nil { + t.Fatalf("Failed put task to queue: %s", err.Error()) + } + + go func() { + t1, err := q.TakeTimeout(2 * time.Second) + if err != nil { + t.Fatalf("Failed to take task from utube: %s", err.Error()) + } + + time.Sleep(2 * time.Second) + if err := t1.Ack(); err != nil { + t.Fatalf("Failed to ack task: %s", err.Error()) + } + }() + + time.Sleep(100 * time.Millisecond) + // the queue should be blocked for ~2 seconds + start := time.Now() + t2, err := q.TakeTimeout(2 * time.Second) + if err != nil { + t.Fatalf("Failed to take task from utube: %s", err.Error()) + } + if err := t2.Ack(); err != nil { + t.Fatalf("Failed to ack task: %s", err.Error()) + } + end := time.Now() + if math.Abs(float64(end.Sub(start)-2*time.Second)) > float64(200*time.Millisecond) { + t.Fatalf("Blocking time is less than expected: actual = %.2fs, expected = 1s", end.Sub(start).Seconds()) + } +} From a535b8e0224a63869dc911e8c39e72844929b322 Mon Sep 17 00:00:00 2001 From: Oleg Utkin Date: Sun, 16 Aug 2020 20:25:06 +0300 Subject: [PATCH 212/605] add go modules support (#91) --- go.mod | 10 ++++++++++ go.sum | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..80dcee6f9 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/tarantool/go-tarantool + +go 1.11 + +require ( + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + google.golang.org/appengine v1.6.6 // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + gopkg.in/vmihailenco/msgpack.v2 v2.9.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..838f2f45a --- /dev/null +++ b/go.sum @@ -0,0 +1,20 @@ +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/vmihailenco/msgpack.v2 v2.9.1 h1:kb0VV7NuIojvRfzwslQeP3yArBqJHW9tOl4t38VS1jM= +gopkg.in/vmihailenco/msgpack.v2 v2.9.1/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= From 77ce7d9a407a093419eb5cfb23f3a525f30f9a8f Mon Sep 17 00:00:00 2001 From: Sokolov Yura Date: Sun, 20 Dec 2020 14:14:23 +0300 Subject: [PATCH 213/605] reset buffer if its average use size smaller than quater of capacity closes #95 --- connection.go | 20 ++++++++++---------- request.go | 16 ++++++++-------- smallbuf.go | 38 +++++++++++++++++++++++++++++++++----- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/connection.go b/connection.go index 443276ac4..87d7876c5 100644 --- a/connection.go +++ b/connection.go @@ -442,7 +442,7 @@ func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err if err != nil { return errors.New("auth: pack error " + err.Error()) } - if err := write(w, packet); err != nil { + if err := write(w, packet.b); err != nil { return errors.New("auth: write error " + err.Error()) } if err = w.Flush(); err != nil { @@ -521,7 +521,7 @@ func (conn *Connection) closeConnection(neterr error, forever bool) (err error) conn.c = nil } for i := range conn.shard { - conn.shard[i].buf = conn.shard[i].buf[:0] + conn.shard[i].buf.Reset() requests := &conn.shard[i].requests for pos := range requests { fut := requests[pos].first @@ -621,14 +621,14 @@ func (conn *Connection) writer(w *bufio.Writer, c net.Conn) { } packet, shard.buf = shard.buf, packet shard.bufmut.Unlock() - if len(packet) == 0 { + if packet.Len() == 0 { continue } - if err := write(w, packet); err != nil { + if err := write(w, packet.b); err != nil { conn.reconnect(err, c) return } - packet = packet[0:0] + packet.Reset() } } @@ -717,14 +717,14 @@ func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error return default: } - firstWritten := len(shard.buf) == 0 - if cap(shard.buf) == 0 { - shard.buf = make(smallWBuf, 0, 128) + firstWritten := shard.buf.Len() == 0 + if shard.buf.Cap() == 0 { + shard.buf.b = make([]byte, 0, 128) shard.enc = msgpack.NewEncoder(&shard.buf) } - blen := len(shard.buf) + blen := shard.buf.Len() if err := fut.pack(&shard.buf, shard.enc, body); err != nil { - shard.buf = shard.buf[:blen] + shard.buf.Trunc(blen) shard.bufmut.Unlock() if f := conn.fetchFuture(fut.requestId); f == fut { fut.markReady(conn) diff --git a/request.go b/request.go index 606dcb91a..ae42eb440 100644 --- a/request.go +++ b/request.go @@ -352,25 +352,25 @@ func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { func (fut *Future) pack(h *smallWBuf, enc *msgpack.Encoder, body func(*msgpack.Encoder) error) (err error) { rid := fut.requestId - hl := len(*h) - *h = append(*h, smallWBuf{ + hl := h.Len() + h.Write([]byte{ 0xce, 0, 0, 0, 0, // length 0x82, // 2 element map KeyCode, byte(fut.requestCode), // request code KeySync, 0xce, byte(rid >> 24), byte(rid >> 16), byte(rid >> 8), byte(rid), - }...) + }) if err = body(enc); err != nil { return } - l := uint32(len(*h) - 5 - hl) - (*h)[hl+1] = byte(l >> 24) - (*h)[hl+2] = byte(l >> 16) - (*h)[hl+3] = byte(l >> 8) - (*h)[hl+4] = byte(l) + l := uint32(h.Len() - 5 - hl) + h.b[hl+1] = byte(l >> 24) + h.b[hl+2] = byte(l >> 16) + h.b[hl+3] = byte(l >> 8) + h.b[hl+4] = byte(l) return } diff --git a/smallbuf.go b/smallbuf.go index 6b0a13638..c1e3cf740 100644 --- a/smallbuf.go +++ b/smallbuf.go @@ -51,19 +51,47 @@ func (s *smallBuf) Bytes() []byte { return nil } -type smallWBuf []byte +type smallWBuf struct { + b []byte + sum uint + n uint +} func (s *smallWBuf) Write(b []byte) (int, error) { - *s = append(*s, b...) - return len(b), nil + s.b = append(s.b, b...) + return len(s.b), nil } func (s *smallWBuf) WriteByte(b byte) error { - *s = append(*s, b) + s.b = append(s.b, b) return nil } func (s *smallWBuf) WriteString(ss string) (int, error) { - *s = append(*s, ss...) + s.b = append(s.b, ss...) return len(ss), nil } + +func (s smallWBuf) Len() int { + return len(s.b) +} + +func (s smallWBuf) Cap() int { + return cap(s.b) +} + +func (s *smallWBuf) Trunc(n int) { + s.b = s.b[:n] +} + +func (s *smallWBuf) Reset() { + s.sum = uint(uint64(s.sum) * 15 / 16) + uint(len(s.b)) + if s.n < 16 { + s.n++ + } + if cap(s.b) > 1024 && s.sum / s.n < uint(cap(s.b))/4 { + s.b = make([]byte, 0, s.sum/s.n) + } else { + s.b = s.b[:0] + } +} From 012b8a8970d5b83e4eaa26d42512a7790cfbb12d Mon Sep 17 00:00:00 2001 From: Michael Filonenko Date: Tue, 30 Mar 2021 23:46:06 +0300 Subject: [PATCH 214/605] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f1c85928..c27ee07ab 100644 --- a/README.md +++ b/README.md @@ -616,7 +616,7 @@ Features of the implementation: ## Multi connections -You can use multiple connections config with tatantool/multi. +You can use multiple connections config with tarantool/multi. Main features: From 4ad94c32272ed118fc61b4bb0e1dde7d906e983c Mon Sep 17 00:00:00 2001 From: Roman Kunin Date: Wed, 25 Aug 2021 13:52:13 +1000 Subject: [PATCH 215/605] fix typo --- connection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connection.go b/connection.go index 87d7876c5..102b58d50 100644 --- a/connection.go +++ b/connection.go @@ -75,7 +75,7 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac resp := v[0].(*Response) log.Printf("tarantool: connection %s got unexpected resultId (%d) in response", conn.addr, resp.RequestId) default: - args := append([]interface{}{"tarantool: unexpecting event ", event, conn}, v...) + args := append([]interface{}{"tarantool: unexpected event ", event, conn}, v...) log.Print(args...) } } From b50fcb1f816435141dad1beda45837454ecbb1a0 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Mon, 15 Nov 2021 10:38:52 +0300 Subject: [PATCH 216/605] Make test case consistent with comments Original test case had failed with Tarantool 2.8.1 or newer due to error message rework [1]. Based on code comments and @funny-falcon response in #105, the test has been planned to be success insert test and not a duplicate key error test. This patch changes test case and asserts to its original idea. Since duplicate key test already exists in tarantool_test.go file [2], coverage should not decrease. 1. https://github.com/tarantool/tarantool/commit/d11fb3061e15faf4e0eb5375fb8056b4e64348ae 2. https://github.com/tarantool/go-tarantool/blob/61f3a41907b6bcb060e9fa07069cde5b33ba9764/tarantool_test.go#L437-L443 Closes #105 --- example_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example_test.go b/example_test.go index f5f6f8f90..7b81bc28e 100644 --- a/example_test.go +++ b/example_test.go @@ -119,7 +119,7 @@ func Example() { fmt.Println("Insert Data", resp.Data) // insert new tuple { 11, 1 } - resp, err = client.Insert("test", &Tuple{Id: 10, Msg: "test", Name: "one"}) + resp, err = client.Insert("test", &Tuple{Id: 11, Msg: "test", Name: "one"}) fmt.Println("Insert Error", err) fmt.Println("Insert Code", resp.Code) fmt.Println("Insert Data", resp.Data) @@ -194,9 +194,9 @@ func Example() { // Insert Error // Insert Code 0 // Insert Data [[10 test one]] - // Insert Error Duplicate key exists in unique index 'primary' in space 'test' (0x3) - // Insert Code 3 - // Insert Data [] + // Insert Error + // Insert Code 0 + // Insert Data [[11 test one]] // Delete Error // Delete Code 0 // Delete Data [[10 test one]] From c806e1c98786e74caa09e9588f4e9081adf526c7 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Mon, 15 Nov 2021 10:44:19 +0300 Subject: [PATCH 217/605] Fix ErrTupleFound assert fail message --- tarantool_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tarantool_test.go b/tarantool_test.go index 7c0552138..ae725fdc7 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -439,7 +439,7 @@ func TestClient(t *testing.T) { t.Errorf("Expected ErrTupleFound but got: %v", err) } if len(resp.Data) != 0 { - t.Errorf("Response Body len != 1") + t.Errorf("Response Body len != 0") } // Delete From fd68e125e7f13b4150e7e3b8dc6586bf91fceca1 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Fri, 29 Oct 2021 17:34:06 +0300 Subject: [PATCH 218/605] Support UUID type in msgpack This patch provides UUID support for all space operations and as function return result. UUID type was introduced in Tarantool 2.4.1. See more in commit messages [1]. To use UUID with google/uuid in msgpack, import tarantool/go-tarantool/uuid submodule. 1. https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5 Closes #90 --- README.md | 44 ++++++++++++++ config.lua | 21 +++++++ go.mod | 4 +- go.sum | 8 +-- uuid/config.lua | 31 ++++++++++ uuid/go.mod | 11 ++++ uuid/go.sum | 28 +++++++++ uuid/uuid.go | 57 +++++++++++++++++ uuid/uuid_test.go | 151 ++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 349 insertions(+), 6 deletions(-) create mode 100644 uuid/config.lua create mode 100644 uuid/go.mod create mode 100644 uuid/go.sum create mode 100644 uuid/uuid.go create mode 100644 uuid/uuid_test.go diff --git a/README.md b/README.md index c27ee07ab..721958084 100644 --- a/README.md +++ b/README.md @@ -284,6 +284,50 @@ func main() { } ``` +To enable support of UUID in msgpack with [google/uuid](https://github.com/google/uuid), +import tarantool/uuid submodule. +```go +package main + +import ( + "log" + "time" + + "github.com/tarantool/go-tarantool" + _ "github.com/tarantool/go-tarantool/uuid" + "github.com/google/uuid" +) + +func main() { + server := "127.0.0.1:3013" + opts := tarantool.Opts{ + Timeout: 500 * time.Millisecond, + Reconnect: 1 * time.Second, + MaxReconnects: 3, + User: "test", + Pass: "test", + } + client, err := tarantool.Connect(server, opts) + if err != nil { + log.Fatalf("Failed to connect: %s", err.Error()) + } + + spaceNo := uint32(524) + + id, uuidErr := uuid.Parse("c8f0fa1f-da29-438c-a040-393f1126ad39") + if uuidErr != nil { + log.Fatalf("Failed to prepare uuid: %s", uuidErr) + } + + resp, err := client.Replace(spaceNo, []interface{}{ id }) + + log.Println("UUID tuple replace") + log.Println("Error", err) + log.Println("Code", resp.Code) + log.Println("Data", resp.Data) +} +``` + ## Schema ```go diff --git a/config.lua b/config.lua index 3c869cd76..96017160c 100644 --- a/config.lua +++ b/config.lua @@ -61,3 +61,24 @@ console.listen '0.0.0.0:33015' --box.schema.user.revoke('guest', 'read,write,execute', 'universe') +-- Create space with UUID pk if supported +local uuid = require('uuid') +local msgpack = require('msgpack') + +local uuid_msgpack_supported = pcall(msgpack.encode, uuid.new()) +if uuid_msgpack_supported then + local suuid = box.schema.space.create('testUUID', { + id = 524, + if_not_exists = true, + }) + suuid:create_index('primary', { + type = 'tree', + parts = {{ field = 1, type = 'uuid' }}, + if_not_exists = true + }) + suuid:truncate() + + box.schema.user.grant('test', 'read,write', 'space', 'testUUID', { if_not_exists = true }) + + suuid:insert({ uuid.fromstr("c8f0fa1f-da29-438c-a040-393f1126ad39") }) +end diff --git a/go.mod b/go.mod index 80dcee6f9..604bc731c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.11 require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - google.golang.org/appengine v1.6.6 // indirect + google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect - gopkg.in/vmihailenco/msgpack.v2 v2.9.1 + gopkg.in/vmihailenco/msgpack.v2 v2.9.2 ) diff --git a/go.sum b/go.sum index 838f2f45a..7f493923c 100644 --- a/go.sum +++ b/go.sum @@ -12,9 +12,9 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/vmihailenco/msgpack.v2 v2.9.1 h1:kb0VV7NuIojvRfzwslQeP3yArBqJHW9tOl4t38VS1jM= -gopkg.in/vmihailenco/msgpack.v2 v2.9.1/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= +gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= +gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= diff --git a/uuid/config.lua b/uuid/config.lua new file mode 100644 index 000000000..34b5d26c4 --- /dev/null +++ b/uuid/config.lua @@ -0,0 +1,31 @@ +local uuid = require('uuid') +local msgpack = require('msgpack') + +box.cfg{ + listen = 3013, + wal_dir = 'xlog', + snap_dir = 'snap', +} + +box.schema.user.create('test', { password = 'test' , if_not_exists = true }) +box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) + +local uuid_msgpack_supported = pcall(msgpack.encode, uuid.new()) +if not uuid_msgpack_supported then + error('UUID unsupported, use Tarantool 2.4.1 or newer') +end + +local s = box.schema.space.create('testUUID', { + id = 524, + if_not_exists = true, +}) +s:create_index('primary', { + type = 'tree', + parts = {{ field = 1, type = 'uuid' }}, + if_not_exists = true +}) +s:truncate() + +box.schema.user.grant('test', 'read,write', 'space', 'testUUID', { if_not_exists = true }) + +s:insert({ uuid.fromstr("c8f0fa1f-da29-438c-a040-393f1126ad39") }) diff --git a/uuid/go.mod b/uuid/go.mod new file mode 100644 index 000000000..baee6648d --- /dev/null +++ b/uuid/go.mod @@ -0,0 +1,11 @@ +module github.com/tarantool/go-tarantool/uuid + +go 1.11 + +require ( + github.com/google/uuid v1.3.0 + github.com/tarantool/go-tarantool v0.0.0-20211104105631-61f3a41907b6 + google.golang.org/appengine v1.6.7 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/vmihailenco/msgpack.v2 v2.9.2 +) diff --git a/uuid/go.sum b/uuid/go.sum new file mode 100644 index 000000000..dc902998f --- /dev/null +++ b/uuid/go.sum @@ -0,0 +1,28 @@ +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/tarantool/go-tarantool v0.0.0-20211104105631-61f3a41907b6 h1:WGaVN8FgSHg3xaiYnJvTk9bnXIgW8nAOUg5aVH4RLX8= +github.com/tarantool/go-tarantool v0.0.0-20211104105631-61f3a41907b6/go.mod h1:m/mppmrDtgvS3tqUvaZRdRtlgzK1Gz/T6uGndkOItmQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/vmihailenco/msgpack.v2 v2.9.1/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= +gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= +gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= diff --git a/uuid/uuid.go b/uuid/uuid.go new file mode 100644 index 000000000..f50b6d92a --- /dev/null +++ b/uuid/uuid.go @@ -0,0 +1,57 @@ +package uuid + +import ( + "fmt" + "reflect" + + "github.com/google/uuid" + "gopkg.in/vmihailenco/msgpack.v2" +) + +// UUID external type +// Supported since Tarantool 2.4.1. See more in commit messages. +// https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5 + +const UUID_extId = 2 + +func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { + id := v.Interface().(uuid.UUID) + + bytes, err := id.MarshalBinary() + if err != nil { + return fmt.Errorf("msgpack: can't marshal binary uuid: %w", err) + } + + _, err = e.Writer().Write(bytes) + if err != nil { + return fmt.Errorf("msgpack: can't write bytes to encoder writer: %w", err) + } + + return nil +} + +func decodeUUID(d *msgpack.Decoder, v reflect.Value) error { + var bytesCount int = 16; + bytes := make([]byte, bytesCount) + + n, err := d.Buffered().Read(bytes) + if err != nil { + return fmt.Errorf("msgpack: can't read bytes on uuid decode: %w", err) + } + if n < bytesCount { + return fmt.Errorf("msgpack: unexpected end of stream after %d uuid bytes", n) + } + + id, err := uuid.FromBytes(bytes) + if err != nil { + return fmt.Errorf("msgpack: can't create uuid from bytes: %w", err) + } + + v.Set(reflect.ValueOf(id)) + return nil +} + +func init() { + msgpack.Register(reflect.TypeOf((*uuid.UUID)(nil)).Elem(), encodeUUID, decodeUUID) + msgpack.RegisterExt(UUID_extId, (*uuid.UUID)(nil)) +} diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go new file mode 100644 index 000000000..61eb632d2 --- /dev/null +++ b/uuid/uuid_test.go @@ -0,0 +1,151 @@ +package uuid_test + +import ( + "fmt" + "testing" + "time" + + . "github.com/tarantool/go-tarantool" + _ "github.com/tarantool/go-tarantool/uuid" + "gopkg.in/vmihailenco/msgpack.v2" + "github.com/google/uuid" +) + +var server = "127.0.0.1:3013" +var opts = Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +var space = "testUUID" +var index = "primary" + +type TupleUUID struct { + id uuid.UUID +} + +func (t *TupleUUID) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 1 { + return fmt.Errorf("array len doesn't match: %d", l) + } + res, err := d.DecodeInterface() + if err != nil { + return err + } + t.id = res.(uuid.UUID) + return nil +} + +func connectWithValidation(t *testing.T) *Connection { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + } + if conn == nil { + t.Errorf("conn is nil after Connect") + } + return conn +} + +func skipIfUUIDUnsupported(t *testing.T, conn *Connection) { + resp, err := conn.Eval("return pcall(require('msgpack').encode, require('uuid').new())", []interface{}{}) + if err != nil { + t.Errorf("Failed to Eval: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Eval") + } + if len(resp.Data) < 1 { + t.Errorf("Response.Data is empty after Eval") + } + val := resp.Data[0].(bool) + if val != true { + t.Skip("Skipping test for Tarantool without UUID support in msgpack") + } +} + +func tupleValueIsId(t *testing.T, tuples []interface{}, id uuid.UUID) { + if len(tuples) != 1 { + t.Errorf("Response Data len != 1") + } + + if tpl, ok := tuples[0].([]interface{}); !ok { + t.Errorf("Unexpected return value body") + } else { + if len(tpl) != 1 { + t.Errorf("Unexpected return value body (tuple len)") + } + if val, ok := tpl[0].(uuid.UUID); !ok || val != id { + t.Errorf("Unexpected return value body (tuple 0 field)") + } + } +} + +func TestSelect(t *testing.T) { + conn := connectWithValidation(t) + defer conn.Close() + + skipIfUUIDUnsupported(t, conn) + + id, uuidErr := uuid.Parse("c8f0fa1f-da29-438c-a040-393f1126ad39") + if uuidErr != nil { + t.Errorf("Failed to prepare test uuid: %s", uuidErr) + } + + resp, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{ id }) + if errSel != nil { + t.Errorf("UUID select failed: %s", errSel.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Select") + } + tupleValueIsId(t, resp.Data, id) + + var tuples []TupleUUID + errTyp := conn.SelectTyped(space, index, 0, 1, IterEq, []interface{}{ id }, &tuples) + if errTyp != nil { + t.Errorf("Failed to SelectTyped: %s", errTyp.Error()) + } + if len(tuples) != 1 { + t.Errorf("Result len of SelectTyped != 1") + } + if tuples[0].id != id { + t.Errorf("Bad value loaded from SelectTyped: %s", tuples[0].id) + } +} + +func TestReplace(t *testing.T) { + conn := connectWithValidation(t) + defer conn.Close() + + skipIfUUIDUnsupported(t, conn) + + id, uuidErr := uuid.Parse("64d22e4d-ac92-4a23-899a-e59f34af5479") + if uuidErr != nil { + t.Errorf("Failed to prepare test uuid: %s", uuidErr) + } + + respRep, errRep := conn.Replace(space, []interface{}{ id }) + if errRep != nil { + t.Errorf("UUID replace failed: %s", errRep) + } + if respRep == nil { + t.Errorf("Response is nil after Replace") + } + tupleValueIsId(t, respRep.Data, id) + + respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{ id }) + if errSel != nil { + t.Errorf("UUID select failed: %s", errSel) + } + if respSel == nil { + t.Errorf("Response is nil after Select") + } + tupleValueIsId(t, respSel.Data, id) +} From 981fa6fdc39b341a06570a8c0c45088b2cc470bf Mon Sep 17 00:00:00 2001 From: Yaroslav Lobankov Date: Thu, 2 Dec 2021 13:38:25 +0400 Subject: [PATCH 219/605] github-ci: add simple CI based on GitHub actions In the provided workflow we install tarantool (1.10, 2.8, 2.9) and just run tests from the connector against it. Closes #114 --- .github/workflows/testing.yml | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 .github/workflows/testing.yml diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 000000000..8b894210a --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,75 @@ +name: testing + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + linux: + # We want to run on external PRs, but not on our own internal + # PRs as they'll be run by the push to the branch. + # + # The main trick is described here: + # https://github.com/Dart-Code/Dart-Code/pull/2375 + if: github.event_name == 'push' || + github.event.pull_request.head.repo.full_name != github.repository + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + tarantool: + - '1.10' + - '2.8' + - '2.9' + + steps: + - name: Clone the connector + uses: actions/checkout@v2 + + - name: Setup tarantool ${{ matrix.tarantool }} + uses: tarantool/setup-tarantool@v1 + with: + tarantool-version: ${{ matrix.tarantool }} + + - name: Setup golang for the connector and tests + uses: actions/setup-go@v2 + with: + go-version: 1.13 + + - name: Run base tests + run: | + mkdir snap xlog + TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) + go clean -testcache && go test -v + kill $TNT_PID + +# TODO(ylobankov): Uncomment this when tarantool/go-tarantool#115 is resolved. +# - name: Run queue tests +# working-directory: ./queue +# run: | +# mkdir snap xlog +# tarantoolctl rocks install queue 1.1.0 +# TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) +# go clean -testcache && go test -v +# kill $TNT_PID + + - name: Run uuid tests + working-directory: ./uuid + run: | + mkdir snap xlog + TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) + go clean -testcache && go test -v + kill $TNT_PID + if: ${{ matrix.tarantool != 1.10 }} + + - name: Run multi tests + working-directory: ./multi + run: | + mkdir -p m1/{snap,xlog} m2/{snap,xlog} + TNT_PID_1=$(tarantool ./config_m1.lua > tarantool_m1.log 2>&1 & echo $!) + TNT_PID_2=$(tarantool ./config_m2.lua > tarantool_m2.log 2>&1 & echo $!) + go clean -testcache && go test -v + kill $TNT_PID_1 $TNT_PID_2 From cbef4af23fade3235f902e3d5f950a6819f52885 Mon Sep 17 00:00:00 2001 From: Yaroslav Lobankov Date: Fri, 12 Nov 2021 16:08:55 +0300 Subject: [PATCH 220/605] github-ci: add reusable testing workflow The idea of this workflow is to be a part of the 'integration.yml' workflow from the tarantool repo to verify the integration of the go-tarantool connector with an arbitrary tarantool version. This workflow is not triggered on a push to the repo and cannot be run manually since it has only the 'workflow_call' trigger. This workflow will be included in the tarantool development cycle and called by the 'integration.yml' workflow from the tarantool project on a push to the master and release branches for verifying integration of tarantool with go-tarantool. Part of tarantool/tarantool#6607 Part of tarantool/tarantool#5265 Part of tarantool/tarantool#6056 --- .github/workflows/reusable_testing.yml | 74 ++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/reusable_testing.yml diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml new file mode 100644 index 000000000..f0897a8d9 --- /dev/null +++ b/.github/workflows/reusable_testing.yml @@ -0,0 +1,74 @@ +name: reusable_testing + +on: + workflow_call: + inputs: + artifact_name: + description: The name of the tarantool build artifact + default: ubuntu-focal + required: false + type: string + +jobs: + run_tests: + runs-on: ubuntu-20.04 + steps: + - name: Clone the go-tarantool connector + uses: actions/checkout@v2 + with: + repository: ${{ github.repository_owner }}/go-tarantool + + - name: Download the tarantool build artifact + uses: actions/download-artifact@v2 + with: + name: ${{ inputs.artifact_name }} + + - name: Install tarantool + # Now we're lucky: all dependencies are already installed. Check package + # dependencies when migrating to other OS version. + run: sudo dpkg -i tarantool*.deb + + - name: Get the tarantool version + run: | + TNT_VERSION=$(tarantool --version | grep -e '^Tarantool') + echo "TNT_VERSION=$TNT_VERSION" >> $GITHUB_ENV + + - name: Setup golang for connector and tests + uses: actions/setup-go@v2 + with: + go-version: 1.13 + + - name: Run base tests + run: | + mkdir snap xlog + TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) + go clean -testcache && go test -v + kill $TNT_PID + +# TODO(ylobankov): Uncomment this when tarantool/go-tarantool#115 is resolved. +# - name: Run queue tests +# working-directory: ./queue +# run: | +# mkdir snap xlog +# tarantoolctl rocks install queue 1.1.0 +# TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) +# go clean -testcache && go test -v +# kill $TNT_PID + + - name: Run uuid tests + working-directory: ./uuid + run: | + mkdir snap xlog + TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) + go clean -testcache && go test -v + kill $TNT_PID + if: ${{ !startsWith(env.TNT_VERSION, 'Tarantool 1.10') }} + + - name: Run multi tests + working-directory: ./multi + run: | + mkdir -p m1/{snap,xlog} m2/{snap,xlog} + TNT_PID_1=$(tarantool ./config_m1.lua > tarantool_m1.log 2>&1 & echo $!) + TNT_PID_2=$(tarantool ./config_m2.lua > tarantool_m2.log 2>&1 & echo $!) + go clean -testcache && go test -v + kill $TNT_PID_1 $TNT_PID_2 From f297cfed2a03b0842c89947cc781204760782a1c Mon Sep 17 00:00:00 2001 From: Pavel Balaev Date: Sun, 9 Jan 2022 23:19:54 +0300 Subject: [PATCH 221/605] readme: update link to tarantool manual closes #130 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 721958084..207f8ff7f 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ the `import {...}` section at the start of any Go program.

Hello World

-In the "[Connectors](http://tarantool.org/doc/book/connectors/index.html#go)" +In the "[Connectors](https://www.tarantool.io/en/doc/latest/getting_started/getting_started_go/)" chapter of the Tarantool manual, there is an explanation of a very short (18-line) program written in Go. Follow the instructions at the start of the "Connectors" chapter carefully. Then cut and paste the example into a file named `example.go`, From 9c9a68e09870257cd44bc75da9ffdc829b5e1d19 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Thu, 13 Jan 2022 17:36:37 +0300 Subject: [PATCH 222/605] Clean up excessive test setup After moving UUID-related code (including tests) in PR #104 to separate folder, test setup of UUID space was remained in main folder config.lua by mistake. This patch removes it. Closes #128 --- config.lua | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/config.lua b/config.lua index 96017160c..c2a9ae966 100644 --- a/config.lua +++ b/config.lua @@ -60,25 +60,3 @@ local console = require 'console' console.listen '0.0.0.0:33015' --box.schema.user.revoke('guest', 'read,write,execute', 'universe') - --- Create space with UUID pk if supported -local uuid = require('uuid') -local msgpack = require('msgpack') - -local uuid_msgpack_supported = pcall(msgpack.encode, uuid.new()) -if uuid_msgpack_supported then - local suuid = box.schema.space.create('testUUID', { - id = 524, - if_not_exists = true, - }) - suuid:create_index('primary', { - type = 'tree', - parts = {{ field = 1, type = 'uuid' }}, - if_not_exists = true - }) - suuid:truncate() - - box.schema.user.grant('test', 'read,write', 'space', 'testUUID', { if_not_exists = true }) - - suuid:insert({ uuid.fromstr("c8f0fa1f-da29-438c-a040-393f1126ad39") }) -end From 56cb3ef5368168a407acb50d6093ab0081fb0620 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Fri, 14 Jan 2022 19:59:48 +0300 Subject: [PATCH 223/605] uuid: use plain package instead of module Using separate module instead of plain package introduced several issues related to dependencies inside the repository (see #134). At the same time, same functionality could be provided with package. The only drawback is that google/uuid will be included in main package dependency even if user decide to use custom UUID parser or ignore UUID completely. Closes #134 --- go.mod | 1 + go.sum | 2 ++ uuid/go.mod | 11 ----------- uuid/go.sum | 28 ---------------------------- 4 files changed, 3 insertions(+), 39 deletions(-) delete mode 100644 uuid/go.mod delete mode 100644 uuid/go.sum diff --git a/go.mod b/go.mod index 604bc731c..6914a5870 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/tarantool/go-tarantool go 1.11 require ( + github.com/google/uuid v1.3.0 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect diff --git a/go.sum b/go.sum index 7f493923c..8a6ae1fa0 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= diff --git a/uuid/go.mod b/uuid/go.mod deleted file mode 100644 index baee6648d..000000000 --- a/uuid/go.mod +++ /dev/null @@ -1,11 +0,0 @@ -module github.com/tarantool/go-tarantool/uuid - -go 1.11 - -require ( - github.com/google/uuid v1.3.0 - github.com/tarantool/go-tarantool v0.0.0-20211104105631-61f3a41907b6 - google.golang.org/appengine v1.6.7 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/vmihailenco/msgpack.v2 v2.9.2 -) diff --git a/uuid/go.sum b/uuid/go.sum deleted file mode 100644 index dc902998f..000000000 --- a/uuid/go.sum +++ /dev/null @@ -1,28 +0,0 @@ -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/tarantool/go-tarantool v0.0.0-20211104105631-61f3a41907b6 h1:WGaVN8FgSHg3xaiYnJvTk9bnXIgW8nAOUg5aVH4RLX8= -github.com/tarantool/go-tarantool v0.0.0-20211104105631-61f3a41907b6/go.mod h1:m/mppmrDtgvS3tqUvaZRdRtlgzK1Gz/T6uGndkOItmQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/vmihailenco/msgpack.v2 v2.9.1/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= -gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= -gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= From d9c4090f1e6614d6e02ca40c1d1cfa993bbe00cd Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Mon, 31 Jan 2022 14:33:49 +0300 Subject: [PATCH 224/605] readme: update "Alternative connectors" section Add FZambia/tarantool [1] connector to README "Alternative connectors" section. Add a link to feature comparison table in documentation. 1. https://github.com/FZambia/tarantool Closes #138 --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 207f8ff7f..abf1fed25 100644 --- a/README.md +++ b/README.md @@ -675,5 +675,8 @@ Additional options (configurable via `ConnectWithOpts`): ## Alternative connectors -- https://github.com/viciious/go-tarantool - Has tools to emulate tarantool, and to being replica for tarantool. +There are two more connectors from the open-source community available: +* [viciious/go-tarantool](https://github.com/viciious/go-tarantool), +* [FZambia/tarantool](https://github.com/FZambia/tarantool). + +See feature comparison in [documentation](https://www.tarantool.io/en/doc/latest/book/connectors/#feature-comparison). From c84ec853329c8d29b9b1201a16eb1f8692751a57 Mon Sep 17 00:00:00 2001 From: vr009 Date: Mon, 31 Jan 2022 16:32:43 +0300 Subject: [PATCH 225/605] tests: queue tests fix Before this patch, queue tests failed due to impossibility to use _queue_taken, which was deprecated in tarantool/queue [1]. Switch to _queue_taken_2 fixed it. Create access and some other actions were denied for test user. Additional grants fixed this problem. The test `TestUtube_Put` was affected by replacing timeout in the call of `TakeTimeout()` with timeout of connection structure. For clarification of this effect see annotation for function `TakeTimeout()` [2]. That was fixed by making connection timeout longer than the timeout in the function call. The step in testing.yml was uncommented for enabling queue tests in CI. 1. https://github.com/tarantool/queue/tree/7d05b6db5b0d596db4281d53b57811e156234a0d#fields-of-the-_queue_consumers-space 2. https://github.com/tarantool/go-tarantool/blob/master/queue/queue.go#L35 Fixes #115 --- .github/workflows/reusable_testing.yml | 17 ++++++++--------- .github/workflows/testing.yml | 19 ++++++++++--------- queue/config.lua | 20 +++++++++++++++++++- queue/queue_test.go | 2 +- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index f0897a8d9..54cd379c6 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -45,15 +45,14 @@ jobs: go clean -testcache && go test -v kill $TNT_PID -# TODO(ylobankov): Uncomment this when tarantool/go-tarantool#115 is resolved. -# - name: Run queue tests -# working-directory: ./queue -# run: | -# mkdir snap xlog -# tarantoolctl rocks install queue 1.1.0 -# TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) -# go clean -testcache && go test -v -# kill $TNT_PID + - name: Run queue tests + working-directory: ./queue + run: | + mkdir snap xlog + tarantoolctl rocks install queue 1.1.0 + TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) + go clean -testcache && go test -v + kill $TNT_PID - name: Run uuid tests working-directory: ./uuid diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 8b894210a..68cf3d6b0 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -46,15 +46,16 @@ jobs: go clean -testcache && go test -v kill $TNT_PID -# TODO(ylobankov): Uncomment this when tarantool/go-tarantool#115 is resolved. -# - name: Run queue tests -# working-directory: ./queue -# run: | -# mkdir snap xlog -# tarantoolctl rocks install queue 1.1.0 -# TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) -# go clean -testcache && go test -v -# kill $TNT_PID + - name: Run queue tests + working-directory: ./queue + run: | + rm -rf snap + rm -rf xlog + mkdir snap xlog + tarantoolctl rocks install queue 1.1.0 + TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) + go clean -testcache && go test -v + kill $TNT_PID - name: Run uuid tests working-directory: ./uuid diff --git a/queue/config.lua b/queue/config.lua index 2488ff300..57aef4d20 100644 --- a/queue/config.lua +++ b/queue/config.lua @@ -18,6 +18,12 @@ box.schema.func.create('queue.tube.test_queue:delete') box.schema.func.create('queue.tube.test_queue:release') box.schema.func.create('queue.tube.test_queue:bury') box.schema.func.create('queue.statistics') +box.schema.user.grant('test', 'create', 'space') +box.schema.user.grant('test', 'write', 'space', '_schema') +box.schema.user.grant('test', 'write', 'space', '_space') +box.schema.user.grant('test', 'read,write', 'space', '_space_sequence') +box.schema.user.grant('test', 'write', 'space', '_index') +box.schema.user.grant('test', 'read, write', 'space', '_queue_session_ids') box.schema.user.grant('test', 'execute', 'universe') box.schema.user.grant('test', 'read,write', 'space', '_queue') box.schema.user.grant('test', 'read,write', 'space', '_schema') @@ -25,5 +31,17 @@ box.schema.user.grant('test', 'read,write', 'space', '_space') box.schema.user.grant('test', 'read,write', 'space', '_index') box.schema.user.grant('test', 'read,write', 'space', '_queue_consumers') box.schema.user.grant('test', 'read,write', 'space', '_priv') -box.schema.user.grant('test', 'read,write', 'space', '_queue_taken') +box.schema.user.grant('test', 'read,write', 'space', '_queue_taken_2') +if box.space._trigger ~= nil then + box.schema.user.grant('test', 'read', 'space', '_trigger') +end +if box.space._fk_constraint ~= nil then + box.schema.user.grant('test', 'read', 'space', '_fk_constraint') +end +if box.space._ck_constraint ~= nil then + box.schema.user.grant('test', 'read', 'space', '_ck_constraint') +end +if box.space._func_index ~= nil then + box.schema.user.grant('test', 'read', 'space', '_func_index') +end end) diff --git a/queue/queue_test.go b/queue/queue_test.go index d1a909120..4419e12eb 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -13,7 +13,7 @@ import ( var server = "127.0.0.1:3013" var opts = Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 2500 * time.Millisecond, User: "test", Pass: "test", //Concurrency: 32, From fc8ce8a1a5d09c6a9837d6aecfb287a17c7aaa89 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 8 Feb 2022 14:13:03 +0300 Subject: [PATCH 226/605] code: run go fmt --- smallbuf.go | 8 ++++---- uuid/uuid.go | 2 +- uuid/uuid_test.go | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/smallbuf.go b/smallbuf.go index c1e3cf740..27b79e864 100644 --- a/smallbuf.go +++ b/smallbuf.go @@ -52,9 +52,9 @@ func (s *smallBuf) Bytes() []byte { } type smallWBuf struct { - b []byte + b []byte sum uint - n uint + n uint } func (s *smallWBuf) Write(b []byte) (int, error) { @@ -85,11 +85,11 @@ func (s *smallWBuf) Trunc(n int) { } func (s *smallWBuf) Reset() { - s.sum = uint(uint64(s.sum) * 15 / 16) + uint(len(s.b)) + s.sum = uint(uint64(s.sum)*15/16) + uint(len(s.b)) if s.n < 16 { s.n++ } - if cap(s.b) > 1024 && s.sum / s.n < uint(cap(s.b))/4 { + if cap(s.b) > 1024 && s.sum/s.n < uint(cap(s.b))/4 { s.b = make([]byte, 0, s.sum/s.n) } else { s.b = s.b[:0] diff --git a/uuid/uuid.go b/uuid/uuid.go index f50b6d92a..11d3153e1 100644 --- a/uuid/uuid.go +++ b/uuid/uuid.go @@ -31,7 +31,7 @@ func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { } func decodeUUID(d *msgpack.Decoder, v reflect.Value) error { - var bytesCount int = 16; + var bytesCount int = 16 bytes := make([]byte, bytesCount) n, err := d.Buffered().Read(bytes) diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index 61eb632d2..ed3bab6e0 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -5,10 +5,10 @@ import ( "testing" "time" + "github.com/google/uuid" . "github.com/tarantool/go-tarantool" - _ "github.com/tarantool/go-tarantool/uuid" + _ "github.com/tarantool/go-tarantool/uuid" "gopkg.in/vmihailenco/msgpack.v2" - "github.com/google/uuid" ) var server = "127.0.0.1:3013" @@ -98,7 +98,7 @@ func TestSelect(t *testing.T) { t.Errorf("Failed to prepare test uuid: %s", uuidErr) } - resp, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{ id }) + resp, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{id}) if errSel != nil { t.Errorf("UUID select failed: %s", errSel.Error()) } @@ -108,7 +108,7 @@ func TestSelect(t *testing.T) { tupleValueIsId(t, resp.Data, id) var tuples []TupleUUID - errTyp := conn.SelectTyped(space, index, 0, 1, IterEq, []interface{}{ id }, &tuples) + errTyp := conn.SelectTyped(space, index, 0, 1, IterEq, []interface{}{id}, &tuples) if errTyp != nil { t.Errorf("Failed to SelectTyped: %s", errTyp.Error()) } @@ -131,7 +131,7 @@ func TestReplace(t *testing.T) { t.Errorf("Failed to prepare test uuid: %s", uuidErr) } - respRep, errRep := conn.Replace(space, []interface{}{ id }) + respRep, errRep := conn.Replace(space, []interface{}{id}) if errRep != nil { t.Errorf("UUID replace failed: %s", errRep) } @@ -140,7 +140,7 @@ func TestReplace(t *testing.T) { } tupleValueIsId(t, respRep.Data, id) - respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{ id }) + respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{id}) if errSel != nil { t.Errorf("UUID select failed: %s", errSel) } From 9a6159a242d8491bf221333ad2a61426e080e876 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Fri, 14 Jan 2022 11:13:18 +0300 Subject: [PATCH 227/605] test: handle everything with go test Before this patch, it was required to set up test tarantool processes manually (and also handle their dependencies, like making working dir). You can see an example in CI scripts. This patch introduces go helpers for starting a tarantool process and validating Tarantool version. Helpers are based on `os/exec` calls. Retries to connect test tarantool instance handled explicitly, see tarantool/go-tarantool#136. Setup scripts are reworked to use environment variables to configure `box.cfg`. Listen port is set in the end of script so it is possible to connect only if every other thing was set up already. Every test is reworked to start a tarantool process (or processes) in TestMain before test run. To run tests, install all dependencies with running `make deps` and then run `make test`. Flag `-p 1` in `go test` command means no parallel runs. If you run tests without this flag, several test tarantool instances will try to bind the same port, resulting in run fail. Closes #107 --- .github/workflows/reusable_testing.yml | 36 +--- .github/workflows/testing.yml | 38 +--- .gitignore | 4 +- Makefile | 14 ++ config.lua | 13 +- multi/config.lua | 19 ++ multi/config_m1.lua | 14 -- multi/config_m2.lua | 14 -- multi/multi_test.go | 54 ++++++ queue/config.lua | 13 +- queue/queue_test.go | 33 ++++ tarantool_test.go | 33 ++++ test_helpers/main.go | 234 +++++++++++++++++++++++++ uuid/config.lua | 11 +- uuid/uuid_test.go | 80 ++++++--- 15 files changed, 481 insertions(+), 129 deletions(-) create mode 100644 Makefile create mode 100644 multi/config.lua delete mode 100644 multi/config_m1.lua delete mode 100644 multi/config_m2.lua create mode 100644 test_helpers/main.go diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index 54cd379c6..987d1d734 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -38,36 +38,8 @@ jobs: with: go-version: 1.13 - - name: Run base tests - run: | - mkdir snap xlog - TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID - - - name: Run queue tests - working-directory: ./queue - run: | - mkdir snap xlog - tarantoolctl rocks install queue 1.1.0 - TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID + - name: Install test dependencies + run: make deps - - name: Run uuid tests - working-directory: ./uuid - run: | - mkdir snap xlog - TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID - if: ${{ !startsWith(env.TNT_VERSION, 'Tarantool 1.10') }} - - - name: Run multi tests - working-directory: ./multi - run: | - mkdir -p m1/{snap,xlog} m2/{snap,xlog} - TNT_PID_1=$(tarantool ./config_m1.lua > tarantool_m1.log 2>&1 & echo $!) - TNT_PID_2=$(tarantool ./config_m2.lua > tarantool_m2.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID_1 $TNT_PID_2 + - name: Run tests + run: make test diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 68cf3d6b0..a2d99cfa2 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -39,38 +39,8 @@ jobs: with: go-version: 1.13 - - name: Run base tests - run: | - mkdir snap xlog - TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID + - name: Install test dependencies + run: make deps - - name: Run queue tests - working-directory: ./queue - run: | - rm -rf snap - rm -rf xlog - mkdir snap xlog - tarantoolctl rocks install queue 1.1.0 - TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID - - - name: Run uuid tests - working-directory: ./uuid - run: | - mkdir snap xlog - TNT_PID=$(tarantool ./config.lua > tarantool.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID - if: ${{ matrix.tarantool != 1.10 }} - - - name: Run multi tests - working-directory: ./multi - run: | - mkdir -p m1/{snap,xlog} m2/{snap,xlog} - TNT_PID_1=$(tarantool ./config_m1.lua > tarantool_m1.log 2>&1 & echo $!) - TNT_PID_2=$(tarantool ./config_m2.lua > tarantool_m2.log 2>&1 & echo $!) - go clean -testcache && go test -v - kill $TNT_PID_1 $TNT_PID_2 + - name: Run tests + run: make test diff --git a/.gitignore b/.gitignore index 8fcb790f1..8958050e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ *.DS_Store *.swp .idea/ -snap -xlog +work_dir* +.rocks diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..ec51cca52 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +SHELL := /bin/bash + +.PHONY: clean +clean: + ( cd ./queue; rm -rf .rocks ) + +.PHONY: deps +deps: clean + ( cd ./queue; tarantoolctl rocks install queue 1.1.0 ) + +.PHONY: test +test: + go clean -testcache + go test ./... -v -p 1 diff --git a/config.lua b/config.lua index c2a9ae966..d06e05395 100644 --- a/config.lua +++ b/config.lua @@ -1,7 +1,7 @@ +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. box.cfg{ - listen = 3013, - wal_dir='xlog', - snap_dir='snap', + work_dir = os.getenv("TEST_TNT_WORK_DIR"), } box.once("init", function() @@ -56,7 +56,10 @@ function simple_incr(a) end box.space.test:truncate() -local console = require 'console' -console.listen '0.0.0.0:33015' --box.schema.user.revoke('guest', 'read,write,execute', 'universe') + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} diff --git a/multi/config.lua b/multi/config.lua new file mode 100644 index 000000000..e511cfad9 --- /dev/null +++ b/multi/config.lua @@ -0,0 +1,19 @@ +local nodes_load = require("config_load_nodes") + +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +box.cfg{ + work_dir = os.getenv("TEST_TNT_WORK_DIR"), +} + +get_cluster_nodes = nodes_load.get_cluster_nodes + +box.once("init", function() + box.schema.user.create('test', { password = 'test' }) + box.schema.user.grant('test', 'read,write,execute', 'universe') +end) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} diff --git a/multi/config_m1.lua b/multi/config_m1.lua deleted file mode 100644 index 5e364b5cc..000000000 --- a/multi/config_m1.lua +++ /dev/null @@ -1,14 +0,0 @@ -local nodes_load = require("config_load_nodes") - -box.cfg { - listen = 3013, - wal_dir = 'm1/xlog', - snap_dir = 'm1/snap', -} - -get_cluster_nodes = nodes_load.get_cluster_nodes - -box.once("init", function() - box.schema.user.create('test', { password = 'test' }) - box.schema.user.grant('test', 'read,write,execute', 'universe') -end) diff --git a/multi/config_m2.lua b/multi/config_m2.lua deleted file mode 100644 index cf73da319..000000000 --- a/multi/config_m2.lua +++ /dev/null @@ -1,14 +0,0 @@ -local nodes_load = require("config_load_nodes") - -box.cfg { - listen = 3014, - wal_dir = 'm2/xlog', - snap_dir = 'm2/snap', -} - -get_cluster_nodes = nodes_load.get_cluster_nodes - -box.once("init", function() - box.schema.user.create('test', { password = 'test' }) - box.schema.user.grant('test', 'read,write,execute', 'universe') -end) diff --git a/multi/multi_test.go b/multi/multi_test.go index 4515c6232..65ae439f8 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -1,10 +1,13 @@ package multi import ( + "log" + "os" "testing" "time" "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" ) var server1 = "127.0.0.1:3013" @@ -204,3 +207,54 @@ func TestRefresh(t *testing.T) { t.Error("Expect to get data after reconnect") } } + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + initScript := "config.lua" + waitStart := 100 * time.Millisecond + var connectRetry uint = 3 + retryTimeout := 500 * time.Millisecond + + inst1, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: initScript, + Listen: server1, + WorkDir: "work_dir1", + User: connOpts.User, + Pass: connOpts.Pass, + WaitStart: waitStart, + ConnectRetry: connectRetry, + RetryTimeout: retryTimeout, + }) + defer test_helpers.StopTarantoolWithCleanup(inst1) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + inst2, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: initScript, + Listen: server2, + WorkDir: "work_dir2", + User: connOpts.User, + Pass: connOpts.Pass, + WaitStart: waitStart, + ConnectRetry: connectRetry, + RetryTimeout: retryTimeout, + }) + defer test_helpers.StopTarantoolWithCleanup(inst2) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/queue/config.lua b/queue/config.lua index 57aef4d20..767f259ad 100644 --- a/queue/config.lua +++ b/queue/config.lua @@ -1,9 +1,9 @@ -queue = require 'queue' +queue = require('queue') +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. box.cfg{ - listen = 3013, - wal_dir='xlog', - snap_dir='snap', + work_dir = os.getenv("TEST_TNT_WORK_DIR"), } box.once("init", function() @@ -45,3 +45,8 @@ if box.space._func_index ~= nil then box.schema.user.grant('test', 'read', 'space', '_func_index') end end) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} diff --git a/queue/queue_test.go b/queue/queue_test.go index 4419e12eb..db531eb4c 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -2,12 +2,15 @@ package queue_test import ( "fmt" + "log" "math" + "os" "testing" "time" . "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/queue" + "github.com/tarantool/go-tarantool/test_helpers" "gopkg.in/vmihailenco/msgpack.v2" ) @@ -818,3 +821,33 @@ func TestUtube_Put(t *testing.T) { t.Fatalf("Blocking time is less than expected: actual = %.2fs, expected = 1s", end.Sub(start).Seconds()) } } + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: server, + WorkDir: "work_dir", + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(inst) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/tarantool_test.go b/tarantool_test.go index ae725fdc7..41bdfe830 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2,12 +2,15 @@ package tarantool_test import ( "fmt" + "log" + "os" "strings" "sync" "testing" "time" . "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" "gopkg.in/vmihailenco/msgpack.v2" ) @@ -1005,3 +1008,33 @@ func TestComplexStructs(t *testing.T) { return } } + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: server, + WorkDir: "work_dir", + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(inst) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/test_helpers/main.go b/test_helpers/main.go new file mode 100644 index 000000000..e5c73bfe5 --- /dev/null +++ b/test_helpers/main.go @@ -0,0 +1,234 @@ +package test_helpers + +import ( + "errors" + "fmt" + "log" + "os" + "os/exec" + "regexp" + "strconv" + "time" + + "github.com/tarantool/go-tarantool" +) + +type StartOpts struct { + // InitScript is a Lua script for tarantool to run on start. + InitScript string + + // Listen is box.cfg listen parameter for tarantool. + // Use this address to connect to tarantool after configuration. + // https://www.tarantool.io/en/doc/latest/reference/configuration/#cfg-basic-listen + Listen string + + // WorkDir is box.cfg work_dir parameter for tarantool. + // Specify folder to store tarantool data files. + // Folder must be unique for each tarantool process used simultaneously. + // https://www.tarantool.io/en/doc/latest/reference/configuration/#confval-work_dir + WorkDir string + + // User is a username used to connect to tarantool. + // All required grants must be given in InitScript. + User string + + // Pass is a password for specified User. + Pass string + + // WaitStart is a time to wait before starting to ping tarantool. + WaitStart time.Duration + + // ConnectRetry is a count of attempts to ping tarantool. + ConnectRetry uint + + // RetryTimeout is a time between tarantool ping retries. + RetryTimeout time.Duration +} + +// TarantoolInstance is a data for instance graceful shutdown and cleanup. +type TarantoolInstance struct { + // Cmd is a Tarantool command. Used to kill Tarantool process. + Cmd *exec.Cmd + + // WorkDir is a directory with tarantool data. Cleaned up after run. + WorkDir string +} + +func isReady(server string, opts *tarantool.Opts) error { + var err error + var conn *tarantool.Connection + var resp *tarantool.Response + + conn, err = tarantool.Connect(server, *opts) + if err != nil { + return err + } + if conn == nil { + return errors.New("Conn is nil after connect") + } + defer conn.Close() + + resp, err = conn.Ping() + if err != nil { + return err + } + if resp == nil { + return errors.New("Response is nil after ping") + } + + return nil +} + +var ( + // Used to extract Tarantool version (major.minor.patch). + tarantoolVersionRegexp *regexp.Regexp +) + +func init() { + tarantoolVersionRegexp = regexp.MustCompile(`Tarantool (?:Enterprise )?(\d+)\.(\d+)\.(\d+).*`) +} + +// atoiUint64 parses string to uint64. +func atoiUint64(str string) (uint64, error) { + res, err := strconv.ParseUint(str, 10, 64) + if err != nil { + return 0, fmt.Errorf("cast to number error (%s)", err) + } + return res, nil +} + +// IsTarantoolVersionLess checks if tarantool version is less +// than passed . Returns error if failed +// to extract version. +func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) (bool, error) { + var major, minor, patch uint64 + + out, err := exec.Command("tarantool", "--version").Output() + + if err != nil { + return true, err + } + + parsed := tarantoolVersionRegexp.FindStringSubmatch(string(out)) + + if parsed == nil { + return true, errors.New("regexp parse failed") + } + + if major, err = atoiUint64(parsed[1]); err != nil { + return true, fmt.Errorf("failed to parse major: %s", err) + } + + if minor, err = atoiUint64(parsed[2]); err != nil { + return true, fmt.Errorf("failed to parse minor: %s", err) + } + + if patch, err = atoiUint64(parsed[3]); err != nil { + return true, fmt.Errorf("failed to parse patch: %s", err) + } + + if major != majorMin { + return major < majorMin, nil + } else if minor != minorMin { + return minor < minorMin, nil + } else { + return patch < patchMin, nil + } + + return false, nil +} + +// StartTarantool starts a tarantool instance for tests +// with specifies parameters (refer to StartOpts). +// Process must be stopped with StopTarantool. +func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { + // Prepare tarantool command. + var inst TarantoolInstance + inst.Cmd = exec.Command("tarantool", startOpts.InitScript) + + inst.Cmd.Env = append( + os.Environ(), + fmt.Sprintf("TEST_TNT_WORK_DIR=%s", startOpts.WorkDir), + fmt.Sprintf("TEST_TNT_LISTEN=%s", startOpts.Listen), + ) + + // Clean up existing work_dir. + err := os.RemoveAll(startOpts.WorkDir) + if err != nil { + return inst, err + } + + // Create work_dir. + err = os.Mkdir(startOpts.WorkDir, 0755) + if err != nil { + return inst, err + } + + inst.WorkDir = startOpts.WorkDir + + // Start tarantool. + err = inst.Cmd.Start() + if err != nil { + return inst, err + } + + // Try to connect and ping tarantool. + // Using reconnect opts do not help on Connect, + // see https://github.com/tarantool/go-tarantool/issues/136 + time.Sleep(startOpts.WaitStart) + + opts := tarantool.Opts{ + Timeout: 500 * time.Millisecond, + User: startOpts.User, + Pass: startOpts.Pass, + SkipSchema: true, + } + + var i uint + for i = 0; i <= startOpts.ConnectRetry; i++ { + err = isReady(startOpts.Listen, &opts) + + // Both connect and ping is ok. + if err == nil { + break + } + + if i != startOpts.ConnectRetry { + time.Sleep(startOpts.RetryTimeout) + } + } + + return inst, err +} + +// StopTarantool stops a tarantool instance started +// with StartTarantool. Waits until any resources +// associated with the process is released. If something went wrong, fails. +func StopTarantool(inst TarantoolInstance) { + if inst.Cmd != nil && inst.Cmd.Process != nil { + if err := inst.Cmd.Process.Kill(); err != nil { + log.Fatalf("Failed to kill tarantool (pid %d), got %s", inst.Cmd.Process.Pid, err) + } + + // Wait releases any resources associated with the Process. + if _, err := inst.Cmd.Process.Wait(); err != nil { + log.Fatalf("Failed to wait for Tarantool process to exit, got %s", err) + } + + inst.Cmd = nil + } +} + +// StopTarantoolWithCleanup stops a tarantool instance started +// with StartTarantool. Waits until any resources +// associated with the process is released. +// Cleans work directory after stop. If something went wrong, fails. +func StopTarantoolWithCleanup(inst TarantoolInstance) { + StopTarantool(inst) + + if inst.WorkDir != "" { + if err := os.RemoveAll(inst.WorkDir); err != nil { + log.Fatalf("Failed to clean work directory, got %s", err) + } + } +} diff --git a/uuid/config.lua b/uuid/config.lua index 34b5d26c4..b8fe1fe08 100644 --- a/uuid/config.lua +++ b/uuid/config.lua @@ -1,10 +1,10 @@ local uuid = require('uuid') local msgpack = require('msgpack') +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. box.cfg{ - listen = 3013, - wal_dir = 'xlog', - snap_dir = 'snap', + work_dir = os.getenv("TEST_TNT_WORK_DIR"), } box.schema.user.create('test', { password = 'test' , if_not_exists = true }) @@ -29,3 +29,8 @@ s:truncate() box.schema.user.grant('test', 'read,write', 'space', 'testUUID', { if_not_exists = true }) s:insert({ uuid.fromstr("c8f0fa1f-da29-438c-a040-393f1126ad39") }) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index ed3bab6e0..b8987b0bc 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -2,15 +2,23 @@ package uuid_test import ( "fmt" + "log" + "os" "testing" "time" "github.com/google/uuid" . "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" _ "github.com/tarantool/go-tarantool/uuid" "gopkg.in/vmihailenco/msgpack.v2" ) +// There is no way to skip tests in testing.M, +// so we use this variable to pass info +// to each testing.T that it should skip. +var isUUIDSupported = false + var server = "127.0.0.1:3013" var opts = Opts{ Timeout: 500 * time.Millisecond, @@ -53,23 +61,6 @@ func connectWithValidation(t *testing.T) *Connection { return conn } -func skipIfUUIDUnsupported(t *testing.T, conn *Connection) { - resp, err := conn.Eval("return pcall(require('msgpack').encode, require('uuid').new())", []interface{}{}) - if err != nil { - t.Errorf("Failed to Eval: %s", err.Error()) - } - if resp == nil { - t.Errorf("Response is nil after Eval") - } - if len(resp.Data) < 1 { - t.Errorf("Response.Data is empty after Eval") - } - val := resp.Data[0].(bool) - if val != true { - t.Skip("Skipping test for Tarantool without UUID support in msgpack") - } -} - func tupleValueIsId(t *testing.T, tuples []interface{}, id uuid.UUID) { if len(tuples) != 1 { t.Errorf("Response Data len != 1") @@ -88,11 +79,13 @@ func tupleValueIsId(t *testing.T, tuples []interface{}, id uuid.UUID) { } func TestSelect(t *testing.T) { + if isUUIDSupported == false { + t.Skip("Skipping test for Tarantool without UUID support in msgpack") + } + conn := connectWithValidation(t) defer conn.Close() - skipIfUUIDUnsupported(t, conn) - id, uuidErr := uuid.Parse("c8f0fa1f-da29-438c-a040-393f1126ad39") if uuidErr != nil { t.Errorf("Failed to prepare test uuid: %s", uuidErr) @@ -121,11 +114,13 @@ func TestSelect(t *testing.T) { } func TestReplace(t *testing.T) { + if isUUIDSupported == false { + t.Skip("Skipping test for Tarantool without UUID support in msgpack") + } + conn := connectWithValidation(t) defer conn.Close() - skipIfUUIDUnsupported(t, conn) - id, uuidErr := uuid.Parse("64d22e4d-ac92-4a23-899a-e59f34af5479") if uuidErr != nil { t.Errorf("Failed to prepare test uuid: %s", uuidErr) @@ -149,3 +144,46 @@ func TestReplace(t *testing.T) { } tupleValueIsId(t, respSel.Data, id) } + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + isLess, err := test_helpers.IsTarantoolVersionLess(2, 4, 1) + if err != nil { + log.Fatalf("Failed to extract tarantool version: %s", err) + } + + if isLess { + log.Println("Skipping UUID tests...") + isUUIDSupported = false + return m.Run() + } else { + isUUIDSupported = true + } + + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: server, + WorkDir: "work_dir", + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(inst) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} From ec3bdeeb55335061964bd788cee6c01203d699a1 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 1 Feb 2022 18:57:52 +0300 Subject: [PATCH 228/605] multi: describe init script logic --- multi/config.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/multi/config.lua b/multi/config.lua index e511cfad9..fbd4befa1 100644 --- a/multi/config.lua +++ b/multi/config.lua @@ -6,6 +6,7 @@ box.cfg{ work_dir = os.getenv("TEST_TNT_WORK_DIR"), } +-- Function to call for getting address list, part of tarantool/multi API. get_cluster_nodes = nodes_load.get_cluster_nodes box.once("init", function() From 17b725f8c50462efffad7fec9c4df77da4097277 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Thu, 3 Feb 2022 19:17:06 +0300 Subject: [PATCH 229/605] lua: fix code style in test scripts Added standard .luacheckrc config. Added indentation to box.once blocks. Fix trailing spaces and add missing spaces to mathematical expressions. Set global variables through rawset. --- .luacheckrc | 28 ++++++++++++++++ config.lua | 87 ++++++++++++++++++++++++------------------------ multi/config.lua | 3 +- queue/config.lua | 79 +++++++++++++++++++++---------------------- 4 files changed, 114 insertions(+), 83 deletions(-) create mode 100644 .luacheckrc diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 000000000..4e8998348 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,28 @@ +redefined = false + +globals = { + 'box', + 'utf8', + 'checkers', + '_TARANTOOL' +} + +include_files = { + '**/*.lua', + '*.luacheckrc', + '*.rockspec' +} + +exclude_files = { + '**/*.rocks/' +} + +max_line_length = 120 + +ignore = { + "212/self", -- Unused argument . + "411", -- Redefining a local variable. + "421", -- Shadowing a local variable. + "431", -- Shadowing an upvalue. + "432", -- Shadowing an upvalue argument. +} diff --git a/config.lua b/config.lua index d06e05395..2528e7b81 100644 --- a/config.lua +++ b/config.lua @@ -5,55 +5,56 @@ box.cfg{ } box.once("init", function() -local s = box.schema.space.create('test', { - id = 512, - if_not_exists = true, -}) -s:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) + local s = box.schema.space.create('test', { + id = 512, + if_not_exists = true, + }) + s:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) -local st = box.schema.space.create('schematest', { - id = 514, - temporary = true, - if_not_exists = true, - field_count = 7, - format = { - {name = "name0", type = "unsigned"}, - {name = "name1", type = "unsigned"}, - {name = "name2", type = "string"}, - {name = "name3", type = "unsigned"}, - {name = "name4", type = "unsigned"}, - {name = "name5", type = "string"}, - }, -}) -st:create_index('primary', { - type = 'hash', - parts = {1, 'uint'}, - unique = true, - if_not_exists = true, -}) -st:create_index('secondary', { - id = 3, - type = 'tree', - unique = false, - parts = { 2, 'uint', 3, 'string' }, - if_not_exists = true, -}) -st:truncate() + local st = box.schema.space.create('schematest', { + id = 514, + temporary = true, + if_not_exists = true, + field_count = 7, + format = { + {name = "name0", type = "unsigned"}, + {name = "name1", type = "unsigned"}, + {name = "name2", type = "string"}, + {name = "name3", type = "unsigned"}, + {name = "name4", type = "unsigned"}, + {name = "name5", type = "string"}, + }, + }) + st:create_index('primary', { + type = 'hash', + parts = {1, 'uint'}, + unique = true, + if_not_exists = true, + }) + st:create_index('secondary', { + id = 3, + type = 'tree', + unique = false, + parts = { 2, 'uint', 3, 'string' }, + if_not_exists = true, + }) + st:truncate() ---box.schema.user.grant('guest', 'read,write,execute', 'universe') -box.schema.func.create('box.info') -box.schema.func.create('simple_incr') + --box.schema.user.grant('guest', 'read,write,execute', 'universe') + box.schema.func.create('box.info') + box.schema.func.create('simple_incr') --- auth testing: access control -box.schema.user.create('test', {password = 'test'}) -box.schema.user.grant('test', 'execute', 'universe') -box.schema.user.grant('test', 'read,write', 'space', 'test') -box.schema.user.grant('test', 'read,write', 'space', 'schematest') + -- auth testing: access control + box.schema.user.create('test', {password = 'test'}) + box.schema.user.grant('test', 'execute', 'universe') + box.schema.user.grant('test', 'read,write', 'space', 'test') + box.schema.user.grant('test', 'read,write', 'space', 'schematest') end) -function simple_incr(a) - return a+1 +local function simple_incr(a) + return a + 1 end +rawset(_G, 'simple_incr', simple_incr) box.space.test:truncate() diff --git a/multi/config.lua b/multi/config.lua index fbd4befa1..25b0eb4f9 100644 --- a/multi/config.lua +++ b/multi/config.lua @@ -7,7 +7,8 @@ box.cfg{ } -- Function to call for getting address list, part of tarantool/multi API. -get_cluster_nodes = nodes_load.get_cluster_nodes +local get_cluster_nodes = nodes_load.get_cluster_nodes +rawset(_G, 'get_cluster_nodes', get_cluster_nodes) box.once("init", function() box.schema.user.create('test', { password = 'test' }) diff --git a/queue/config.lua b/queue/config.lua index 767f259ad..cb64f4df8 100644 --- a/queue/config.lua +++ b/queue/config.lua @@ -1,4 +1,5 @@ -queue = require('queue') +local queue = require('queue') +rawset(_G, 'queue', queue) -- Do not set listen for now so connector won't be -- able to send requests until everything is configured. @@ -6,44 +7,44 @@ box.cfg{ work_dir = os.getenv("TEST_TNT_WORK_DIR"), } -box.once("init", function() -box.schema.user.create('test', {password = 'test'}) -box.schema.func.create('queue.tube.test_queue:ack') -box.schema.func.create('queue.tube.test_queue:put') -box.schema.func.create('queue.tube.test_queue:drop') -box.schema.func.create('queue.tube.test_queue:peek') -box.schema.func.create('queue.tube.test_queue:kick') -box.schema.func.create('queue.tube.test_queue:take') -box.schema.func.create('queue.tube.test_queue:delete') -box.schema.func.create('queue.tube.test_queue:release') -box.schema.func.create('queue.tube.test_queue:bury') -box.schema.func.create('queue.statistics') -box.schema.user.grant('test', 'create', 'space') -box.schema.user.grant('test', 'write', 'space', '_schema') -box.schema.user.grant('test', 'write', 'space', '_space') -box.schema.user.grant('test', 'read,write', 'space', '_space_sequence') -box.schema.user.grant('test', 'write', 'space', '_index') -box.schema.user.grant('test', 'read, write', 'space', '_queue_session_ids') -box.schema.user.grant('test', 'execute', 'universe') -box.schema.user.grant('test', 'read,write', 'space', '_queue') -box.schema.user.grant('test', 'read,write', 'space', '_schema') -box.schema.user.grant('test', 'read,write', 'space', '_space') -box.schema.user.grant('test', 'read,write', 'space', '_index') -box.schema.user.grant('test', 'read,write', 'space', '_queue_consumers') -box.schema.user.grant('test', 'read,write', 'space', '_priv') -box.schema.user.grant('test', 'read,write', 'space', '_queue_taken_2') -if box.space._trigger ~= nil then - box.schema.user.grant('test', 'read', 'space', '_trigger') -end -if box.space._fk_constraint ~= nil then - box.schema.user.grant('test', 'read', 'space', '_fk_constraint') -end -if box.space._ck_constraint ~= nil then - box.schema.user.grant('test', 'read', 'space', '_ck_constraint') -end -if box.space._func_index ~= nil then - box.schema.user.grant('test', 'read', 'space', '_func_index') -end + box.once("init", function() + box.schema.user.create('test', {password = 'test'}) + box.schema.func.create('queue.tube.test_queue:ack') + box.schema.func.create('queue.tube.test_queue:put') + box.schema.func.create('queue.tube.test_queue:drop') + box.schema.func.create('queue.tube.test_queue:peek') + box.schema.func.create('queue.tube.test_queue:kick') + box.schema.func.create('queue.tube.test_queue:take') + box.schema.func.create('queue.tube.test_queue:delete') + box.schema.func.create('queue.tube.test_queue:release') + box.schema.func.create('queue.tube.test_queue:bury') + box.schema.func.create('queue.statistics') + box.schema.user.grant('test', 'create', 'space') + box.schema.user.grant('test', 'write', 'space', '_schema') + box.schema.user.grant('test', 'write', 'space', '_space') + box.schema.user.grant('test', 'read,write', 'space', '_space_sequence') + box.schema.user.grant('test', 'write', 'space', '_index') + box.schema.user.grant('test', 'read, write', 'space', '_queue_session_ids') + box.schema.user.grant('test', 'execute', 'universe') + box.schema.user.grant('test', 'read,write', 'space', '_queue') + box.schema.user.grant('test', 'read,write', 'space', '_schema') + box.schema.user.grant('test', 'read,write', 'space', '_space') + box.schema.user.grant('test', 'read,write', 'space', '_index') + box.schema.user.grant('test', 'read,write', 'space', '_queue_consumers') + box.schema.user.grant('test', 'read,write', 'space', '_priv') + box.schema.user.grant('test', 'read,write', 'space', '_queue_taken_2') + if box.space._trigger ~= nil then + box.schema.user.grant('test', 'read', 'space', '_trigger') + end + if box.space._fk_constraint ~= nil then + box.schema.user.grant('test', 'read', 'space', '_fk_constraint') + end + if box.space._ck_constraint ~= nil then + box.schema.user.grant('test', 'read', 'space', '_ck_constraint') + end + if box.space._func_index ~= nil then + box.schema.user.grant('test', 'read', 'space', '_func_index') + end end) -- Set listen only when every other thing is configured. From b2a3b71b3b2fe3666a5ecc22903dcf4e4e181faa Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Mon, 17 Jan 2022 18:21:18 +0300 Subject: [PATCH 230/605] readme: describe how to run tests Describe how to run connector tests (with submodules) and test requirements. Closes #106 --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index abf1fed25..7f3599347 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ faster than other packages according to public benchmarks. * [Custom (un)packing and typed selects and function calls](#custom-unpacking-and-typed-selects-and-function-calls) * [Options](#options) * [Working with queue](#working-with-queue) +* [Tests](#tests) * [Alternative connectors](#alternative-connectors) ## Installation @@ -673,6 +674,34 @@ Additional options (configurable via `ConnectWithOpts`): * `ClusterDiscoveryTime` - time interval to ask server for updated address list (works on with `NodesGetFunctionName` set) * `NodesGetFunctionName` - server lua function name to call for getting address list +## Tests + +You need to [install Tarantool](https://www.tarantool.io/en/download/) to run tests. +See [Installation](#installation) section for requirements. + +To install test dependencies (like [tarantool/queue](https://github.com/tarantool/queue) module), run +```bash +make deps +``` + +To run tests for the main package and each subpackage, call +```bash +make test +``` +Tests set up all required `tarantool` processes before run and clean up after. + +If you want to run a specific package tests, go to a package folder +```bash +cd multi +``` +and call +```bash +go clean -testcache && go test -v +``` +Use the same for main `tarantool` package and `queue` and `uuid` subpackages. +`uuid` tests require +[Tarantool 2.4.1 or newer](https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5). + ## Alternative connectors There are two more connectors from the open-source community available: From bec9f72b8e4fc7aadf02a3bced221639158fee2a Mon Sep 17 00:00:00 2001 From: Alexander Turenko Date: Fri, 18 Feb 2022 11:07:06 +0300 Subject: [PATCH 231/605] readme: update URL of connectors comparison page The new link is more persistent: should not become broken in a future. See https://github.com/tarantool/doc/pull/2694. Follows up #138 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7f3599347..c10ee8212 100644 --- a/README.md +++ b/README.md @@ -708,4 +708,4 @@ There are two more connectors from the open-source community available: * [viciious/go-tarantool](https://github.com/viciious/go-tarantool), * [FZambia/tarantool](https://github.com/FZambia/tarantool). -See feature comparison in [documentation](https://www.tarantool.io/en/doc/latest/book/connectors/#feature-comparison). +See feature comparison in [documentation](https://www.tarantool.io/en/doc/latest/book/connectors/#go-feature-comparison). From 90ee76c13b2dd5faddd986e598b6301689b75cd6 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 30 Mar 2022 15:18:17 +0300 Subject: [PATCH 232/605] github-ci: add Tarantool 2.x-latest --- .github/workflows/testing.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index a2d99cfa2..3056593dd 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -24,16 +24,24 @@ jobs: - '1.10' - '2.8' - '2.9' + - '2.x-latest' steps: - name: Clone the connector uses: actions/checkout@v2 - - name: Setup tarantool ${{ matrix.tarantool }} + - name: Setup Tarantool (version is not equal to latest 2.x) + if: matrix.tarantool != '2.x-latest' uses: tarantool/setup-tarantool@v1 with: tarantool-version: ${{ matrix.tarantool }} + - name: Setup Tarantool 2.x (latest) + if: matrix.tarantool == '2.x-latest' + run: | + curl -L https://tarantool.io/pre-release/2/installer.sh | sudo bash + sudo apt install -y tarantool tarantool-dev + - name: Setup golang for the connector and tests uses: actions/setup-go@v2 with: From 31ebde8f41b3caef17a44b51d69db19661d1b82d Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Tue, 22 Mar 2022 13:54:54 +0300 Subject: [PATCH 233/605] github-ci: add Coveralls support Patch adds an additional job to Github Actions that runs tests with enabled code coverage and send information about coverage to Coveralls, see documentation [1]. Coveralls provides a convenient UI for analysis code coverage [2]. 1. https://docs.coveralls.io/go 2. https://coveralls.io/github/tarantool/go-tarantool --- .github/workflows/testing.yml | 13 ++++++++++++- Makefile | 13 +++++++++++++ README.md | 2 ++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 3056593dd..26d1a24d6 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -25,12 +25,16 @@ jobs: - '2.8' - '2.9' - '2.x-latest' + coveralls: [false] + include: + - tarantool: '2.x-latest' + coveralls: true steps: - name: Clone the connector uses: actions/checkout@v2 - - name: Setup Tarantool (version is not equal to latest 2.x) + - name: Setup Tarantool ${{ matrix.tarantool }} if: matrix.tarantool != '2.x-latest' uses: tarantool/setup-tarantool@v1 with: @@ -52,3 +56,10 @@ jobs: - name: Run tests run: make test + + - name: Run tests, collect code coverage data and send to Coveralls + if: ${{ matrix.coveralls }} + env: + COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + make coveralls diff --git a/Makefile b/Makefile index ec51cca52..72959eeaf 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ SHELL := /bin/bash +COVERAGE_FILE := coverage.out .PHONY: clean clean: ( cd ./queue; rm -rf .rocks ) + rm -f $(COVERAGE_FILE) .PHONY: deps deps: clean @@ -12,3 +14,14 @@ deps: clean test: go clean -testcache go test ./... -v -p 1 + +.PHONY: coverage +coverage: + go clean -testcache + go get golang.org/x/tools/cmd/cover + go test ./... -v -p 1 -covermode=count -coverprofile=$(COVERAGE_FILE) + +.PHONY: coveralls +coveralls: coverage + go get github.com/mattn/goveralls + goveralls -coverprofile=$(COVERAGE_FILE) -service=github diff --git a/README.md b/README.md index c10ee8212..0cd576994 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ +[![Coverage Status](https://coveralls.io/repos/github/tarantool/go-tarantool/badge.svg?branch=master)](https://coveralls.io/github/tarantool/go-tarantool?branch=master) + # Client in Go for Tarantool 1.6+ The `go-tarantool` package has everything necessary for interfacing with From 4a135a750d304ea976336995398ad5f834b7f59a Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 31 Mar 2022 21:32:52 +0300 Subject: [PATCH 234/605] changelog: add an initial version Changelog contains commits added after tagging of alpha version (commit 855df82a175cb5c1c2a52778a70ca56381c5963e). Changelog entries and format conforms to format described in "Keep a changelog" [1]. 1. https://keepachangelog.com/en/1.0.0/ Closes #121 --- CHANGELOG.md | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..7385dd47c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,123 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic +Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. + +## [Unreleased] + +### Added + +- Coveralls support (#149) +- Reusable testing workflow (integration testing with latest Tarantool) (#123) +- Simple CI based on GitHub actions (#114) +- Support UUID type in msgpack (#90) +- Go modules support (#91) +- queue-utube handling (#85) + +### Fixed + +- Fix queue tests (#107) +- Make test case consistent with comments (#105) + +### Changed + +- Handle everything with `go test` (#115) +- Use plain package instead of module for UUID submodule (#134) +- Reset buffer if its average use size smaller than quater of capacity (#95) + +## [1.5] - 2019-12-29 + +First release. + +### Fixed + +- Fix infinite recursive call of `Upsert` method for `ConnectionMulti` +- Fix index out of range panic on `dial()` to short address +- Fix cast in `defaultLogger.Report` (#49) +- Fix race condition on extremely small request timeouts (#43) +- Fix notify for `Connected` transition +- Fix reconnection logic and add `Opts.SkipSchema` method +- Fix future sending +- Fix panic on disconnect + timeout +- Fix block on msgpack error +- Fix ratelimit +- Fix `timeouts` method for `Connection` +- Fix possible race condition on extremely small request timeouts +- Fix race condition on future channel creation +- Fix block on forever closed connection +- Fix race condition in `Connection` +- Fix extra map fields +- Fix response header parsing +- Fix reconnect logic in `Connection` + +### Changed + +- Make logger configurable +- Report user mismatch error immediately +- Set limit timeout by 0.9 of connection to queue request timeout +- Update fields could be negative +- Require `RLimitAction` to be specified if `RateLimit` is specified +- Use newer typed msgpack interface +- Do not start timeouts goroutine if no timeout specified +- Clear buffers on connection close +- Update `BenchmarkClientParallelMassive` +- Remove array requirements for keys and opts +- Do not allocate `Response` inplace +- Respect timeout on request sending +- Use `AfterFunc(fut.timeouted)` instead of `time.NewTimer()` +- Use `_vspace`/`_vindex` for introspection +- Method `Tuples()` always returns table for response + +### Removed + +- Remove `UpsertTyped()` method (#23) + +### Added + +- Add methods `Future.WaitChan` and `Future.Err` (#86) +- Get node list from nodes (#81) +- Add method `deleteConnectionFromPool` +- Add multiconnections support +- Add `Addr` method for the connection (#64) +- Add `Delete` method for the queue +- Implemented typed taking from queue (#55) +- Add `OverrideSchema` method for the connection +- Add default case to default logger +- Add license (BSD-2 clause as for Tarantool) +- Add `GetTyped` method for the connection (#40) +- Add `ConfiguredTimeout` method for the connection, change queue interface +- Add an example for queue +- Add `GetQueue` method for the queue +- Add queue support +- Add support of Unix socket address +- Add check for prefix "tcp:" +- Add the ability to work with the Tarantool via Unix socket +- Add note about magic way to pack tuples +- Add notification about connection state change +- Add workaround for tarantool/tarantool#2060 (#32) +- Add `ConnectedNow` method for the connection +- Add IO deadline and use `net.Conn.Set(Read|Write)Deadline` +- Add a couple of benchmarks +- Add timeout on connection attempt +- Add `RLimitAction` option +- Add `Call17` method for the connection to make a call compatible with Tarantool 1.7 +- Add `ClientParallelMassive` benchmark +- Add `runtime.Gosched` for decreasing `writer.flush` count +- Add `Eval`, `EvalTyped`, `SelectTyped`, `InsertTyped`, `ReplaceTyped`, `DeleteRequest`, `UpdateTyped`, `UpsertTyped` methods +- Add `UpdateTyped` method +- Add `CallTyped` method +- Add possibility to pass `Space` and `Index` objects into `Select` etc. +- Add custom MsgPack pack/unpack functions +- Add support of Tarantool 1.6.8 schema format +- Add support of Tarantool 1.6.5 schema format +- Add schema loading +- Add `LocalAddr` and `RemoteAddr` methods for the connection +- Add `Upsert` method for the connection +- Add `Eval` and `EvalAsync` methods for the connection +- Add Tarantool error codes +- Add auth support +- Add auth during reconnect +- Add auth request From 2c3af56968b70eb0645d9f15809876e01fbf30ed Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 6 Apr 2022 17:46:09 +0300 Subject: [PATCH 235/605] github: add template for pull-request description Commit for #153 introduces changelog and to remind contributors about changelog entries this commit adds a template for pull-request description. Learn more about templates on Github in [1]. 1. https://docs.github.com/en/github-ae@latest/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository Follows up #121 --- .github/pull_request_template.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..70638f98a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,9 @@ +What has been done? Why? What problem is being solved? + +I didn't forget about (remove if it is not applicable): + +- [ ] Tests (see [documentation](https://pkg.go.dev/testing) for a testing package) +- [ ] Changelog (see [documentation](https://keepachangelog.com/en/1.0.0/) for changelog format) +- [ ] Documentation (see [documentation](https://go.dev/blog/godoc) for documentation style guide) + +Related issues: From f37ec7966939a148794ddffdbd7848550f017022 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Fri, 1 Apr 2022 16:46:54 +0300 Subject: [PATCH 236/605] github-ci: add a job with luacheck luacheck config has been already added in commit 'lua: fix code style in test scripts' (17b725f8c50462efffad7fec9c4df77da4097277). As well as fixes for warnings found by luacheck. Part of #142 --- .github/workflows/check.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/check.yaml diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml new file mode 100644 index 000000000..963e2266b --- /dev/null +++ b/.github/workflows/check.yaml @@ -0,0 +1,26 @@ +name: Run checks + +on: + push: + pull_request: + +jobs: + luacheck: + runs-on: ubuntu-latest + if: | + github.event_name == 'push' || + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.repository + steps: + - uses: actions/checkout@master + + - name: Setup Tarantool + uses: tarantool/setup-tarantool@v1 + with: + tarantool-version: '2.8' + + - name: Setup luacheck + run: tarantoolctl rocks install luacheck 0.25.0 + + - name: Run luacheck + run: ./.rocks/bin/luacheck . From 7897bafc1b6b8a292afb33e57a813791e46bf613 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 7 Apr 2022 16:22:28 +0300 Subject: [PATCH 237/605] github-ci: add job with golang-lint Part of #142 --- .github/workflows/check.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 963e2266b..8d28529ba 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -24,3 +24,19 @@ jobs: - name: Run luacheck run: ./.rocks/bin/luacheck . + + golangci-lint: + runs-on: ubuntu-latest + if: | + github.event_name == 'push' || + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.repository + steps: + - uses: actions/setup-go@v2 + + - uses: actions/checkout@v2 + + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + args: --issues-exit-code=0 -E gofmt From d79c3fa89197303bb8f26a7cbd4fa0a8b8259716 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 31 Mar 2022 12:21:38 +0300 Subject: [PATCH 238/605] readme: remove commented out travis-ci badge Badge was added in commit "Add info to README" (eb82a68c0029ebb074537c156a3604bc458f3302) and was commented out from the beginning. We don't use Travis CI, so it was removed. Part of #123 --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 0cd576994..0d1fc111e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ - [![Coverage Status](https://coveralls.io/repos/github/tarantool/go-tarantool/badge.svg?branch=master)](https://coveralls.io/github/tarantool/go-tarantool?branch=master) From 2ee09e14d77ee7ddaedbb745d50742e9bb738816 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 30 Mar 2022 14:40:24 +0300 Subject: [PATCH 239/605] readme: add badges Add GoDoc bagde generated by [1] with link to HTML documentation, badge for GH Actions, badges for Telegram channel, StackOverflow topic and Github Discussions. Part of #123 1. https://pkg.go.dev/badge/ --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d1fc111e..9d8ede078 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,12 @@ -[![Coverage Status](https://coveralls.io/repos/github/tarantool/go-tarantool/badge.svg?branch=master)](https://coveralls.io/github/tarantool/go-tarantool?branch=master) +[![Go Reference][godoc-badge]][godoc-url] +[![Actions Status][actions-badge]][actions-url] +[![Code Coverage][coverage-badge]][coverage-url] +[![Telegram][telegram-badge]][telegram-url] +[![GitHub Discussions][discussions-badge]][discussions-url] +[![Stack Overflow][stackoverflow-badge]][stackoverflow-url] # Client in Go for Tarantool 1.6+ @@ -708,3 +713,16 @@ There are two more connectors from the open-source community available: * [FZambia/tarantool](https://github.com/FZambia/tarantool). See feature comparison in [documentation](https://www.tarantool.io/en/doc/latest/book/connectors/#go-feature-comparison). + +[godoc-badge]: https://pkg.go.dev/badge/github.com/tarantool/go-tarantool.svg +[godoc-url]: https://pkg.go.dev/github.com/tarantool/go-tarantool +[actions-badge]: https://github.com/tarantool/go-tarantool/actions/workflows/testing.yml/badge.svg +[actions-url]: https://github.com/tarantool/go-tarantool/actions/workflows/testing.yml +[coverage-badge]: https://coveralls.io/repos/github/tarantool/go-tarantool/badge.svg?branch=master +[coverage-url]: https://coveralls.io/github/tarantool/go-tarantool?branch=master +[telegram-badge]: https://img.shields.io/badge/Telegram-join%20chat-blue.svg +[telegram-url]: http://telegram.me/tarantool +[discussions-badge]: https://img.shields.io/github/discussions/tarantool/tarantool +[discussions-url]: https://github.com/tarantool/tarantool/discussions +[stackoverflow-badge]: https://img.shields.io/badge/stackoverflow-tarantool-orange.svg +[stackoverflow-url]: https://stackoverflow.com/questions/tagged/tarantool From 16020919f51308f943b651704b53c20410e179ce Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 20 Apr 2022 08:18:18 +0300 Subject: [PATCH 240/605] readme: update description Part of #123 --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9d8ede078..e9521ab4d 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ [![GitHub Discussions][discussions-badge]][discussions-url] [![Stack Overflow][stackoverflow-badge]][stackoverflow-url] -# Client in Go for Tarantool 1.6+ +# Client in Go for Tarantool -The `go-tarantool` package has everything necessary for interfacing with -[Tarantool 1.6+](http://tarantool.org/). +The package `go-tarantool` contains everything you need to connect to +[Tarantool 1.6+][tarantool-site]. The advantage of integrating Go with Tarantool, which is an application server plus a DBMS, is that Go programmers can handle databases and perform on-the-fly @@ -714,6 +714,7 @@ There are two more connectors from the open-source community available: See feature comparison in [documentation](https://www.tarantool.io/en/doc/latest/book/connectors/#go-feature-comparison). +[tarantool-site]: https://tarantool.io/ [godoc-badge]: https://pkg.go.dev/badge/github.com/tarantool/go-tarantool.svg [godoc-url]: https://pkg.go.dev/github.com/tarantool/go-tarantool [actions-badge]: https://github.com/tarantool/go-tarantool/actions/workflows/testing.yml/badge.svg From 1ca88f65f509e5c7ab8516ed4f8aeccafe141f67 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 30 Mar 2022 14:21:57 +0300 Subject: [PATCH 241/605] doc: update inline comments - replace "tarantool" with "Tarantool" - replace "lua" with "Lua" - remove apostrophes in comments, because apostrophes ignored by godoc - fixed typos - remove unneeded empty lines that breaks documentation generation - add a version where new methods or packages were added - start sentences with capitalized letter - add dots to the end of sentences everywhere except iterator and error codes (const.go and errors.go) Part of #123 --- auth.go | 2 +- client_tools.go | 12 ++--- connection.go | 117 +++++++++++++++++++++---------------------- errors.go | 22 +++++--- example_test.go | 26 +++++----- multi/multi.go | 8 +-- multi/multi_test.go | 4 +- queue/queue.go | 61 +++++++++++++--------- queue/task.go | 22 ++++---- request.go | 46 ++++++++--------- response.go | 8 +-- schema.go | 27 +++++----- test_helpers/main.go | 10 ++++ uuid/uuid.go | 18 +++++-- 14 files changed, 210 insertions(+), 173 deletions(-) diff --git a/auth.go b/auth.go index 9f6c79157..60c219d69 100644 --- a/auth.go +++ b/auth.go @@ -25,7 +25,7 @@ func scramble(encodedSalt, pass string) (scramble []byte, err error) { } step1 := sha1.Sum([]byte(pass)) step2 := sha1.Sum(step1[0:]) - hash := sha1.New() // may be create it once per connection ? + hash := sha1.New() // May be create it once per connection? hash.Write(salt[0:scrambleSize]) hash.Write(step2[0:]) step3 := hash.Sum(nil) diff --git a/client_tools.go b/client_tools.go index ed76ea258..de10a366b 100644 --- a/client_tools.go +++ b/client_tools.go @@ -4,7 +4,7 @@ import ( "gopkg.in/vmihailenco/msgpack.v2" ) -// IntKey is utility type for passing integer key to Select*, Update* and Delete* +// IntKey is utility type for passing integer key to Select*, Update* and Delete*. // It serializes to array with single integer element. type IntKey struct { I int @@ -16,7 +16,7 @@ func (k IntKey) EncodeMsgpack(enc *msgpack.Encoder) error { return nil } -// UintKey is utility type for passing unsigned integer key to Select*, Update* and Delete* +// UintKey is utility type for passing unsigned integer key to Select*, Update* and Delete*. // It serializes to array with single integer element. type UintKey struct { I uint @@ -28,7 +28,7 @@ func (k UintKey) EncodeMsgpack(enc *msgpack.Encoder) error { return nil } -// UintKey is utility type for passing string key to Select*, Update* and Delete* +// UintKey is utility type for passing string key to Select*, Update* and Delete*. // It serializes to array with single string element. type StringKey struct { S string @@ -40,8 +40,8 @@ func (k StringKey) EncodeMsgpack(enc *msgpack.Encoder) error { return nil } -// IntIntKey is utility type for passing two integer keys to Select*, Update* and Delete* -// It serializes to array with two integer elements +// IntIntKey is utility type for passing two integer keys to Select*, Update* and Delete*. +// It serializes to array with two integer elements. type IntIntKey struct { I1, I2 int } @@ -53,7 +53,7 @@ func (k IntIntKey) EncodeMsgpack(enc *msgpack.Encoder) error { return nil } -// Op - is update operation +// Op - is update operation. type Op struct { Op string Field int diff --git a/connection.go b/connection.go index 102b58d50..5a5d3921a 100644 --- a/connection.go +++ b/connection.go @@ -1,3 +1,5 @@ +// Package with implementation of methods and structures for work with +// Tarantool instance. package tarantool import ( @@ -27,26 +29,26 @@ type ConnEventKind int type ConnLogKind int const ( - // Connect signals that connection is established or reestablished + // Connected signals that connection is established or reestablished. Connected ConnEventKind = iota + 1 - // Disconnect signals that connection is broken + // Disconnected signals that connection is broken. Disconnected - // ReconnectFailed signals that attempt to reconnect has failed + // ReconnectFailed signals that attempt to reconnect has failed. ReconnectFailed - // Either reconnect attempts exhausted, or explicit Close is called + // Either reconnect attempts exhausted, or explicit Close is called. Closed - // LogReconnectFailed is logged when reconnect attempt failed + // LogReconnectFailed is logged when reconnect attempt failed. LogReconnectFailed ConnLogKind = iota + 1 // LogLastReconnectFailed is logged when last reconnect attempt failed, // connection will be closed after that. LogLastReconnectFailed - // LogUnexpectedResultId is logged when response with unknown id were received. + // LogUnexpectedResultId is logged when response with unknown id was received. // Most probably it is due to request timeout. LogUnexpectedResultId ) -// ConnEvent is sent throw Notify channel specified in Opts +// ConnEvent is sent throw Notify channel specified in Opts. type ConnEvent struct { Conn *Connection Kind ConnEventKind @@ -80,39 +82,39 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac } } -// Connection is a handle to Tarantool. +// Connection is a handle with a single connection to a Tarantool instance. // // It is created and configured with Connect function, and could not be // reconfigured later. // -// It is could be "Connected", "Disconnected", and "Closed". +// Connection could be in three possible states: // -// When "Connected" it sends queries to Tarantool. +// - In "Connected" state it sends queries to Tarantool. // -// When "Disconnected" it rejects queries with ClientError{Code: ErrConnectionNotReady} +// - In "Disconnected" state it rejects queries with ClientError{Code: +// ErrConnectionNotReady} // -// When "Closed" it rejects queries with ClientError{Code: ErrConnectionClosed} -// -// Connection could become "Closed" when Connection.Close() method called, -// or when Tarantool disconnected and Reconnect pause is not specified or -// MaxReconnects is specified and MaxReconnect reconnect attempts already performed. +// - In "Closed" state it rejects queries with ClientError{Code: +// ErrConnectionClosed}. Connection could become "Closed" when +// Connection.Close() method called, or when Tarantool disconnected and +// Reconnect pause is not specified or MaxReconnects is specified and +// MaxReconnect reconnect attempts already performed. // // You may perform data manipulation operation by calling its methods: // Call*, Insert*, Replace*, Update*, Upsert*, Call*, Eval*. // -// In any method that accepts `space` you my pass either space number or -// space name (in this case it will be looked up in schema). Same is true for `index`. +// In any method that accepts space you my pass either space number or space +// name (in this case it will be looked up in schema). Same is true for index. // -// ATTENTION: `tuple`, `key`, `ops` and `args` arguments for any method should be +// ATTENTION: tuple, key, ops and args arguments for any method should be // and array or should serialize to msgpack array. // -// ATTENTION: `result` argument for *Typed methods should deserialize from +// ATTENTION: result argument for *Typed methods should deserialize from // msgpack array, cause Tarantool always returns result as an array. // For all space related methods and Call* (but not Call17*) methods Tarantool // always returns array of array (array of tuples for space related methods). -// For Eval* and Call17* tarantool always returns array, but does not forces +// For Eval* and Call17* Tarantool always returns array, but does not forces // array of arrays. - type Connection struct { addr string c net.Conn @@ -120,7 +122,7 @@ type Connection struct { // Schema contains schema loaded on connection. Schema *Schema requestId uint32 - // Greeting contains first message sent by tarantool + // Greeting contains first message sent by Tarantool. Greeting *Greeting shard []connShard @@ -134,7 +136,7 @@ type Connection struct { lenbuf [PacketLengthBytes]byte } -var _ = Connector(&Connection{}) // check compatibility with connector interface +var _ = Connector(&Connection{}) // Check compatibility with connector interface. type connShard struct { rmut sync.Mutex @@ -148,7 +150,7 @@ type connShard struct { _pad [16]uint64 } -// Greeting is a message sent by tarantool on connect. +// Greeting is a message sent by Tarantool on connect. type Greeting struct { Version string auth string @@ -157,10 +159,10 @@ type Greeting struct { // Opts is a way to configure Connection type Opts struct { // Timeout is requests timeout. - // Also used to setup net.TCPConn.Set(Read|Write)Deadline + // Also used to setup net.TCPConn.Set(Read|Write)Deadline. Timeout time.Duration // Reconnect is a pause between reconnection attempts. - // If specified, then when tarantool is not reachable or disconnected, + // If specified, then when Tarantool is not reachable or disconnected, // new connect attempt is performed after pause. // By default, no reconnection attempts are performed, // so once disconnected, connection becomes Closed. @@ -168,11 +170,11 @@ type Opts struct { // MaxReconnects is a maximum reconnect attempts. // After MaxReconnects attempts Connection becomes closed. MaxReconnects uint - // User name for authorization + // User name for authorization. User string - // Pass is password for authorization + // Pass is password for authorization. Pass string - // RateLimit limits number of 'in-fly' request, ie already put into + // RateLimit limits number of 'in-fly' request, i.e. already put into // requests queue, but not yet answered by server or timeouted. // It is disabled by default. // See RLimitAction for possible actions when RateLimit.reached. @@ -191,42 +193,37 @@ type Opts struct { // By default it is runtime.GOMAXPROCS(-1) * 4 Concurrency uint32 // SkipSchema disables schema loading. Without disabling schema loading, - // there is no way to create Connection for currently not accessible tarantool. + // there is no way to create Connection for currently not accessible Tarantool. SkipSchema bool // Notify is a channel which receives notifications about Connection status // changes. Notify chan<- ConnEvent - // Handle is user specified value, that could be retrivied with Handle() method + // Handle is user specified value, that could be retrivied with + // Handle() method. Handle interface{} - // Logger is user specified logger used for error messages + // Logger is user specified logger used for error messages. Logger Logger } -// Connect creates and configures new Connection +// Connect creates and configures a new Connection. // // Address could be specified in following ways: // -// TCP connections: -// - tcp://192.168.1.1:3013 -// - tcp://my.host:3013 -// - tcp:192.168.1.1:3013 -// - tcp:my.host:3013 -// - 192.168.1.1:3013 -// - my.host:3013 -// Unix socket: -// - unix:///abs/path/tnt.sock -// - unix:path/tnt.sock -// - /abs/path/tnt.sock - first '/' indicates unix socket -// - ./rel/path/tnt.sock - first '.' indicates unix socket -// - unix/:path/tnt.sock - 'unix/' acts as a "host" and "/path..." as a port +// - TCP connections (tcp://192.168.1.1:3013, tcp://my.host:3013, +// tcp:192.168.1.1:3013, tcp:my.host:3013, 192.168.1.1:3013, my.host:3013) +// +// - Unix socket, first '/' or '.' indicates Unix socket +// (unix:///abs/path/tnt.sock, unix:path/tnt.sock, /abs/path/tnt.sock, +// ./rel/path/tnt.sock, unix/:path/tnt.sock) // -// Note: +// Notes: // // - If opts.Reconnect is zero (default), then connection either already connected // or error is returned. // -// - If opts.Reconnect is non-zero, then error will be returned only if authorization// fails. But if Tarantool is not reachable, then it will attempt to reconnect later -// and will not end attempts on authorization failures. +// - If opts.Reconnect is non-zero, then error will be returned only if authorization +// fails. But if Tarantool is not reachable, then it will make an attempt to reconnect later +// and will not finish to make attempts on authorization failures. func Connect(addr string, opts Opts) (conn *Connection, err error) { conn = &Connection{ addr: addr, @@ -272,10 +269,10 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { return nil, err } else if ok && (ter.Code == ErrNoSuchUser || ter.Code == ErrPasswordMismatch) { - /* reported auth errors immediatly */ + // Reported auth errors immediately. return nil, err } else { - // without SkipSchema it is useless + // Without SkipSchema it is useless. go func(conn *Connection) { conn.mutex.Lock() defer conn.mutex.Unlock() @@ -292,7 +289,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { go conn.timeouts() } - // TODO: reload schema after reconnect + // TODO: reload schema after reconnect. if !conn.opts.SkipSchema { if err = conn.loadSchema(); err != nil { conn.mutex.Lock() @@ -324,12 +321,12 @@ func (conn *Connection) Close() error { return conn.closeConnection(err, true) } -// Addr is configured address of Tarantool socket +// Addr returns a configured address of Tarantool socket. func (conn *Connection) Addr() string { return conn.addr } -// RemoteAddr is address of Tarantool socket +// RemoteAddr returns an address of Tarantool socket. func (conn *Connection) RemoteAddr() string { conn.mutex.Lock() defer conn.mutex.Unlock() @@ -339,7 +336,7 @@ func (conn *Connection) RemoteAddr() string { return conn.c.RemoteAddr().String() } -// LocalAddr is address of outgoing socket +// LocalAddr returns an address of outgoing socket. func (conn *Connection) LocalAddr() string { conn.mutex.Lock() defer conn.mutex.Unlock() @@ -349,7 +346,7 @@ func (conn *Connection) LocalAddr() string { return conn.c.LocalAddr().String() } -// Handle returns user specified handle from Opts +// Handle returns a user-specified handle from Opts. func (conn *Connection) Handle() interface{} { return conn.opts.Handle } @@ -416,7 +413,7 @@ func (conn *Connection) dial() (err error) { } } - // Only if connected and authenticated + // Only if connected and authenticated. conn.lockShards() conn.c = connection atomic.StoreUint32(&conn.state, connConnected) @@ -873,12 +870,12 @@ func (conn *Connection) nextRequestId() (requestId uint32) { return atomic.AddUint32(&conn.requestId, 1) } -// ConfiguredTimeout returns timeout from connection config +// ConfiguredTimeout returns a timeout from connection config. func (conn *Connection) ConfiguredTimeout() time.Duration { return conn.opts.Timeout } -// OverrideSchema sets Schema for the connection +// OverrideSchema sets Schema for the connection. func (conn *Connection) OverrideSchema(s *Schema) { if s != nil { conn.mutex.Lock() diff --git a/errors.go b/errors.go index 8eef0bfb4..a6895ccbc 100644 --- a/errors.go +++ b/errors.go @@ -4,32 +4,38 @@ import ( "fmt" ) -// Error is wrapper around error returned by Tarantool +// Error is wrapper around error returned by Tarantool. type Error struct { Code uint32 Msg string } +// Error converts an Error to a string. func (tnterr Error) Error() string { return fmt.Sprintf("%s (0x%x)", tnterr.Msg, tnterr.Code) } -// ClientError is connection produced by this client, -// ie connection failures or timeouts. +// ClientError is connection error produced by this client, +// i.e. connection failures or timeouts. type ClientError struct { Code uint32 Msg string } +// Error converts a ClientError to a string. func (clierr ClientError) Error() string { return fmt.Sprintf("%s (0x%x)", clierr.Msg, clierr.Code) } // Temporary returns true if next attempt to perform request may succeeed. +// // Currently it returns true when: -// - Connection is not connected at the moment, -// - or request is timeouted, -// - or request is aborted due to rate limit. +// +// - Connection is not connected at the moment +// +// - request is timeouted +// +// - request is aborted due to rate limit func (clierr ClientError) Temporary() bool { switch clierr.Code { case ErrConnectionNotReady, ErrTimeouted, ErrRateLimited: @@ -39,7 +45,7 @@ func (clierr ClientError) Temporary() bool { } } -// Tarantool client error codes +// Tarantool client error codes. const ( ErrConnectionNotReady = 0x4000 + iota ErrConnectionClosed = 0x4000 + iota @@ -48,7 +54,7 @@ const ( ErrRateLimited = 0x4000 + iota ) -// Tarantool server error codes +// Tarantool server error codes. const ( ErrUnknown = 0 // Unknown error ErrIllegalParams = 1 // Illegal parameters, %s diff --git a/example_test.go b/example_test.go index 7b81bc28e..ee5f3f305 100644 --- a/example_test.go +++ b/example_test.go @@ -8,8 +8,8 @@ import ( ) type Tuple struct { - /* instruct msgpack to pack this struct as array, - * so no custom packer is needed */ + // Instruct msgpack to pack this struct as array, so no custom packer + // is needed. _msgpack struct{} `msgpack:",asArray"` Id uint Msg string @@ -108,23 +108,23 @@ func Example() { fmt.Println("Ping Data", resp.Data) fmt.Println("Ping Error", err) - // delete tuple for cleaning + // Delete tuple for cleaning. client.Delete(spaceNo, indexNo, []interface{}{uint(10)}) client.Delete(spaceNo, indexNo, []interface{}{uint(11)}) - // insert new tuple { 10, 1 } + // Insert new tuple { 10, 1 }. resp, err = client.Insert(spaceNo, []interface{}{uint(10), "test", "one"}) fmt.Println("Insert Error", err) fmt.Println("Insert Code", resp.Code) fmt.Println("Insert Data", resp.Data) - // insert new tuple { 11, 1 } + // Insert new tuple { 11, 1 }. resp, err = client.Insert("test", &Tuple{Id: 11, Msg: "test", Name: "one"}) fmt.Println("Insert Error", err) fmt.Println("Insert Code", resp.Code) fmt.Println("Insert Data", resp.Data) - // delete tuple with primary key { 10 } + // Delete tuple with primary key { 10 }. resp, err = client.Delete(spaceNo, indexNo, []interface{}{uint(10)}) // or // resp, err = client.Delete("test", "primary", UintKey{10}}) @@ -132,15 +132,15 @@ func Example() { fmt.Println("Delete Code", resp.Code) fmt.Println("Delete Data", resp.Data) - // replace tuple with primary key 13 - // note, Tuple is defined within tests, and has EncdodeMsgpack and DecodeMsgpack - // methods + // Replace tuple with primary key 13. + // Note, Tuple is defined within tests, and has EncdodeMsgpack and DecodeMsgpack + // methods. resp, err = client.Replace(spaceNo, []interface{}{uint(13), 1}) fmt.Println("Replace Error", err) fmt.Println("Replace Code", resp.Code) fmt.Println("Replace Data", resp.Data) - // update tuple with primary key { 13 }, incrementing second field by 3 + // Update tuple with primary key { 13 }, incrementing second field by 3. resp, err = client.Update("test", "primary", tarantool.UintKey{13}, []tarantool.Op{{"+", 1, 3}}) // or // resp, err = client.Update(spaceNo, indexNo, []interface{}{uint(13)}, []interface{}{[]interface{}{"+", 1, 3}}) @@ -148,7 +148,7 @@ func Example() { fmt.Println("Update Code", resp.Code) fmt.Println("Update Data", resp.Data) - // select just one tuple with primay key { 15 } + // Select just one tuple with primay key { 15 }. resp, err = client.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{uint(15)}) // or // resp, err = client.Select("test", "primary", 0, 1, tarantool.IterEq, tarantool.UintKey{15}) @@ -156,13 +156,13 @@ func Example() { fmt.Println("Select Code", resp.Code) fmt.Println("Select Data", resp.Data) - // call function 'func_name' with arguments + // Call function 'func_name' with arguments. resp, err = client.Call17("simple_incr", []interface{}{1}) fmt.Println("Call17 Error", err) fmt.Println("Call17 Code", resp.Code) fmt.Println("Call17 Data", resp.Data) - // run raw lua code + // Run raw Lua code. resp, err = client.Eval("return 1 + 2", []interface{}{}) fmt.Println("Eval Error", err) fmt.Println("Eval Code", resp.Code) diff --git a/multi/multi.go b/multi/multi.go index 0bf2b03c9..a139853e0 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -42,7 +42,7 @@ type ConnectionMulti struct { fallback *tarantool.Connection } -var _ = tarantool.Connector(&ConnectionMulti{}) // check compatibility with connector interface +var _ = tarantool.Connector(&ConnectionMulti{}) // Check compatibility with connector interface. type OptsMulti struct { CheckTimeout time.Duration @@ -61,7 +61,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsMulti) (c opts.ClusterDiscoveryTime = 60 * time.Second } - notify := make(chan tarantool.ConnEvent, 10*len(addrs)) // x10 to accept disconnected and closed event (with a margin) + notify := make(chan tarantool.ConnEvent, 10*len(addrs)) // x10 to accept disconnected and closed event (with a margin). connOpts.Notify = notify connMulti = &ConnectionMulti{ addrs: addrs, @@ -169,7 +169,7 @@ func (connMulti *ConnectionMulti) checker() { } if len(resp) > 0 && len(resp[0]) > 0 { addrs := resp[0] - // Fill pool with new connections + // Fill pool with new connections. for _, v := range addrs { if indexOf(v, connMulti.addrs) < 0 { conn, _ := tarantool.Connect(v, connMulti.connOpts) @@ -178,7 +178,7 @@ func (connMulti *ConnectionMulti) checker() { } } } - // Clear pool from obsolete connections + // Clear pool from obsolete connections. for _, v := range connMulti.addrs { if indexOf(v, addrs) < 0 { con, ok := connMulti.getConnectionFromPool(v) diff --git a/multi/multi_test.go b/multi/multi_test.go index 65ae439f8..aae659101 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -188,8 +188,8 @@ func TestRefresh(t *testing.T) { } curAddr := multiConn.addrs[0] - // wait for refresh timer - // scenario 1 nodeload, 1 refresh, 1 nodeload + // Wait for refresh timer. + // Scenario 1 nodeload, 1 refresh, 1 nodeload. time.Sleep(10 * time.Second) newAddr := multiConn.addrs[0] diff --git a/queue/queue.go b/queue/queue.go index 69dcf8bcd..4b79f6d35 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -1,3 +1,11 @@ +// Package with implementation of methods for work with a Tarantool's queue +// implementations. +// +// Since: 1.5. +// +// See also +// +// * Tarantool queue module https://github.com/tarantool/queue package queue import ( @@ -8,12 +16,12 @@ import ( msgpack "gopkg.in/vmihailenco/msgpack.v2" ) -// Queue is a handle to tarantool queue's tube +// Queue is a handle to Tarantool queue's tube. type Queue interface { - // Exists checks tube for existence - // Note: it uses Eval, so user needs 'execute universe' privilege + // Exists checks tube for existence. + // Note: it uses Eval, so user needs 'execute universe' privilege. Exists() (bool, error) - // Create creates new tube with configuration + // Create creates new tube with configuration. // Note: it uses Eval, so user needs 'execute universe' privilege // Note: you'd better not use this function in your application, cause it is // administrative task to create or delete queue. @@ -22,33 +30,35 @@ type Queue interface { // Note: you'd better not use this function in your application, cause it is // administrative task to create or delete queue. Drop() error - // Put creates new task in a tube + // Put creates new task in a tube. Put(data interface{}) (*Task, error) - // PutWithOpts creates new task with options different from tube's defaults + // PutWithOpts creates new task with options different from tube's defaults. PutWithOpts(data interface{}, cfg Opts) (*Task, error) - // Take takes 'ready' task from a tube and marks it as 'in progress' + // Take takes 'ready' task from a tube and marks it as 'in progress'. // Note: if connection has a request Timeout, then 0.9 * connection.Timeout is // used as a timeout. Take() (*Task, error) // TakeWithTimout takes 'ready' task from a tube and marks it as "in progress", // or it is timeouted after "timeout" period. // Note: if connection has a request Timeout, and conn.Timeout * 0.9 < timeout - // then timeout = conn.Timeout*0.9 + // then timeout = conn.Timeout*0.9. + // If you use connection timeout and call `TakeTimeout` with parameter + // greater than the connection timeout then parameter reduced to it. TakeTimeout(timeout time.Duration) (*Task, error) // Take takes 'ready' task from a tube and marks it as 'in progress' // Note: if connection has a request Timeout, then 0.9 * connection.Timeout is // used as a timeout. - // Data will be unpacked to result + // Data will be unpacked to result. TakeTyped(interface{}) (*Task, error) // TakeWithTimout takes 'ready' task from a tube and marks it as "in progress", // or it is timeouted after "timeout" period. // Note: if connection has a request Timeout, and conn.Timeout * 0.9 < timeout - // then timeout = conn.Timeout*0.9 - // data will be unpacked to result + // then timeout = conn.Timeout*0.9. + // Data will be unpacked to result. TakeTypedTimeout(timeout time.Duration, result interface{}) (*Task, error) // Peek returns task by its id. Peek(taskId uint64) (*Task, error) - // Kick reverts effect of Task.Bury() for `count` tasks. + // Kick reverts effect of Task.Bury() for count tasks. Kick(count uint64) (uint64, error) // Delete the task identified by its id. Delete(taskId uint64) error @@ -76,8 +86,8 @@ type cmd struct { } type Cfg struct { - Temporary bool // if true, the contents do not persist on disk - IfNotExists bool // if true, no error will be returned if the tube already exists + Temporary bool // If true, the contents do not persist on disk. + IfNotExists bool // If true, no error will be returned if the tube already exists. Kind queueType Opts } @@ -99,10 +109,10 @@ func (cfg Cfg) getType() string { } type Opts struct { - Pri int // task priorities - Ttl time.Duration // task time to live - Ttr time.Duration // task time to execute - Delay time.Duration // delayed execution + Pri int // Task priorities. + Ttl time.Duration // Task time to live. + Ttr time.Duration // Task time to execute. + Delay time.Duration // Delayed execution. Utube string } @@ -132,7 +142,7 @@ func (opts Opts) toMap() map[string]interface{} { return ret } -// New creates a queue handle +// New creates a queue handle. func New(conn tarantool.Connector, name string) Queue { q := &queue{ name: name, @@ -142,14 +152,14 @@ func New(conn tarantool.Connector, name string) Queue { return q } -// Create creates a new queue with config +// Create creates a new queue with config. func (q *queue) Create(cfg Cfg) error { cmd := "local name, type, cfg = ... ; queue.create_tube(name, type, cfg)" _, err := q.conn.Eval(cmd, []interface{}{q.name, cfg.getType(), cfg.toMap()}) return err } -// Exists checks existance of a tube +// Exists checks existance of a tube. func (q *queue) Exists() (bool, error) { cmd := "local name = ... ; return queue.tube[name] ~= nil" resp, err := q.conn.Eval(cmd, []string{q.name}) @@ -192,7 +202,8 @@ func (q *queue) Take() (*Task, error) { return q.take(params) } -// The take request searches for a task in the queue. Waits until a task becomes ready or the timeout expires. +// The take request searches for a task in the queue. Waits until a task +// becomes ready or the timeout expires. func (q *queue) TakeTimeout(timeout time.Duration) (*Task, error) { t := q.conn.ConfiguredTimeout() * 9 / 10 if t > 0 && timeout > t { @@ -211,7 +222,8 @@ func (q *queue) TakeTyped(result interface{}) (*Task, error) { return q.take(params, result) } -// The take request searches for a task in the queue. Waits until a task becomes ready or the timeout expires. +// The take request searches for a task in the queue. Waits until a task +// becomes ready or the timeout expires. func (q *queue) TakeTypedTimeout(timeout time.Duration, result interface{}) (*Task, error) { t := q.conn.ConfiguredTimeout() * 9 / 10 if t > 0 && timeout > t { @@ -285,7 +297,8 @@ func (q *queue) Delete(taskId uint64) error { return err } -// Return the number of tasks in a queue broken down by task_state, and the number of requests broken down by the type of request. +// Return the number of tasks in a queue broken down by task_state, and the +// number of requests broken down by the type of request. func (q *queue) Statistic() (interface{}, error) { resp, err := q.conn.Call(q.cmds.statistics, []interface{}{q.name}) if err != nil { diff --git a/queue/task.go b/queue/task.go index 6d8560bd7..fc16ffb88 100644 --- a/queue/task.go +++ b/queue/task.go @@ -6,7 +6,7 @@ import ( msgpack "gopkg.in/vmihailenco/msgpack.v2" ) -// Task represents a task from tarantool queue's tube +// Task represents a task from Tarantool queue's tube. type Task struct { id uint64 status string @@ -41,27 +41,27 @@ func (t *Task) DecodeMsgpack(d *msgpack.Decoder) error { return nil } -// Id is a getter for task id +// Id is a getter for task id. func (t *Task) Id() uint64 { return t.id } -// Data is a getter for task data +// Data is a getter for task data. func (t *Task) Data() interface{} { return t.data } -// Status is a getter for task status +// Status is a getter for task status. func (t *Task) Status() string { return t.status } -// Ack signals about task completion +// Ack signals about task completion. func (t *Task) Ack() error { return t.accept(t.q._ack(t.id)) } -// Delete task from queue +// Delete task from queue. func (t *Task) Delete() error { return t.accept(t.q._delete(t.id)) } @@ -93,27 +93,27 @@ func (t *Task) accept(newStatus string, err error) error { return err } -// IsReady returns if task is ready +// IsReady returns if task is ready. func (t *Task) IsReady() bool { return t.status == READY } -// IsTaken returns if task is taken +// IsTaken returns if task is taken. func (t *Task) IsTaken() bool { return t.status == TAKEN } -// IsDone returns if task is done +// IsDone returns if task is done. func (t *Task) IsDone() bool { return t.status == DONE } -// IsBurred returns if task is buried +// IsBurred returns if task is buried. func (t *Task) IsBuried() bool { return t.status == BURIED } -// IsDelayed returns if task is delayed +// IsDelayed returns if task is delayed. func (t *Task) IsDelayed() bool { return t.status == DELAYED } diff --git a/request.go b/request.go index ae42eb440..ff8a7e8a5 100644 --- a/request.go +++ b/request.go @@ -7,7 +7,7 @@ import ( "gopkg.in/vmihailenco/msgpack.v2" ) -// Future is a handle for asynchronous request +// Future is a handle for asynchronous request. type Future struct { requestId uint32 requestCode int32 @@ -51,7 +51,7 @@ func (req *Future) fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interf // Select performs select to box space. // -// It is equal to conn.SelectAsync(...).Get() +// It is equal to conn.SelectAsync(...).Get(). func (conn *Connection) Select(space, index interface{}, offset, limit, iterator uint32, key interface{}) (resp *Response, err error) { return conn.SelectAsync(space, index, offset, limit, iterator, key).Get() } @@ -96,16 +96,16 @@ func (conn *Connection) Upsert(space interface{}, tuple, ops interface{}) (resp return conn.UpsertAsync(space, tuple, ops).Get() } -// Call calls registered tarantool function. -// It uses request code for tarantool 1.6, so result is converted to array of arrays +// Call calls registered Tarantool function. +// It uses request code for Tarantool 1.6, so result is converted to array of arrays // // It is equal to conn.CallAsync(functionName, args).Get(). func (conn *Connection) Call(functionName string, args interface{}) (resp *Response, err error) { return conn.CallAsync(functionName, args).Get() } -// Call17 calls registered tarantool function. -// It uses request code for tarantool 1.7, so result is not converted +// Call17 calls registered Tarantool function. +// It uses request code for Tarantool 1.7, so result is not converted // (though, keep in mind, result is always array) // // It is equal to conn.Call17Async(functionName, args).Get(). @@ -113,14 +113,14 @@ func (conn *Connection) Call17(functionName string, args interface{}) (resp *Res return conn.Call17Async(functionName, args).Get() } -// Eval passes lua expression for evaluation. +// Eval passes Lua expression for evaluation. // // It is equal to conn.EvalAsync(space, tuple).Get(). func (conn *Connection) Eval(expr string, args interface{}) (resp *Response, err error) { return conn.EvalAsync(expr, args).Get() } -// single used for conn.GetTyped for decode one tuple +// single used for conn.GetTyped for decode one tuple. type single struct { res interface{} found bool @@ -189,7 +189,7 @@ func (conn *Connection) UpdateTyped(space, index interface{}, key, ops interface } // CallTyped calls registered function. -// It uses request code for tarantool 1.6, so result is converted to array of arrays +// It uses request code for Tarantool 1.6, so result is converted to array of arrays // // It is equal to conn.CallAsync(functionName, args).GetTyped(&result). func (conn *Connection) CallTyped(functionName string, args interface{}, result interface{}) (err error) { @@ -197,7 +197,7 @@ func (conn *Connection) CallTyped(functionName string, args interface{}, result } // Call17Typed calls registered function. -// It uses request code for tarantool 1.7, so result is not converted +// It uses request code for Tarantool 1.7, so result is not converted // (though, keep in mind, result is always array) // // It is equal to conn.Call17Async(functionName, args).GetTyped(&result). @@ -205,14 +205,14 @@ func (conn *Connection) Call17Typed(functionName string, args interface{}, resul return conn.Call17Async(functionName, args).GetTyped(result) } -// EvalTyped passes lua expression for evaluation. +// EvalTyped passes Lua expression for evaluation. // // It is equal to conn.EvalAsync(space, tuple).GetTyped(&result). func (conn *Connection) EvalTyped(expr string, args interface{}, result interface{}) (err error) { return conn.EvalAsync(expr, args).GetTyped(result) } -// SelectAsync sends select request to tarantool and returns Future. +// SelectAsync sends select request to Tarantool and returns Future. func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Future { future := conn.newFuture(SelectRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) @@ -226,7 +226,7 @@ func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, ite }) } -// InsertAsync sends insert action to tarantool and returns Future. +// InsertAsync sends insert action to Tarantool and returns Future. // Tarantool will reject Insert when tuple with same primary key exists. func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Future { future := conn.newFuture(InsertRequest) @@ -240,7 +240,7 @@ func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Futur }) } -// ReplaceAsync sends "insert or replace" action to tarantool and returns Future. +// ReplaceAsync sends "insert or replace" action to Tarantool and returns Future. // If tuple with same primary key exists, it will be replaced. func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Future { future := conn.newFuture(ReplaceRequest) @@ -254,7 +254,7 @@ func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Futu }) } -// DeleteAsync sends deletion action to tarantool and returns Future. +// DeleteAsync sends deletion action to Tarantool and returns Future. // Future's result will contain array with deleted tuple. func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) *Future { future := conn.newFuture(DeleteRequest) @@ -286,7 +286,7 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface }) } -// UpsertAsync sends "update or insert" action to tarantool and returns Future. +// UpsertAsync sends "update or insert" action to Tarantool and returns Future. // Future's sesult will not contain any tuple. func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *Future { future := conn.newFuture(UpsertRequest) @@ -307,8 +307,8 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops in }) } -// CallAsync sends a call to registered tarantool function and returns Future. -// It uses request code for tarantool 1.6, so future's result is always array of arrays +// CallAsync sends a call to registered Tarantool function and returns Future. +// It uses request code for Tarantool 1.6, so future's result is always array of arrays func (conn *Connection) CallAsync(functionName string, args interface{}) *Future { future := conn.newFuture(CallRequest) return future.send(conn, func(enc *msgpack.Encoder) error { @@ -320,8 +320,8 @@ func (conn *Connection) CallAsync(functionName string, args interface{}) *Future }) } -// Call17Async sends a call to registered tarantool function and returns Future. -// It uses request code for tarantool 1.7, so future's result will not be converted +// Call17Async sends a call to registered Tarantool function and returns Future. +// It uses request code for Tarantool 1.7, so future's result will not be converted // (though, keep in mind, result is always array) func (conn *Connection) Call17Async(functionName string, args interface{}) *Future { future := conn.newFuture(Call17Request) @@ -334,7 +334,7 @@ func (conn *Connection) Call17Async(functionName string, args interface{}) *Futu }) } -// EvalAsync sends a lua expression for evaluation and returns Future. +// EvalAsync sends a Lua expression for evaluation and returns Future. func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { future := conn.newFuture(EvalRequest) return future.send(conn, func(enc *msgpack.Encoder) error { @@ -405,7 +405,7 @@ func (fut *Future) wait() { <-fut.ready } -// Get waits for Future to be filled and returns Response and error +// Get waits for Future to be filled and returns Response and error. // // Response will contain deserialized result in Data field. // It will be []interface{}, so if you want more performace, use GetTyped method. @@ -442,7 +442,7 @@ func init() { close(closedChan) } -// WaitChan returns channel which becomes closed when response arrived or error occured +// WaitChan returns channel which becomes closed when response arrived or error occured. func (fut *Future) WaitChan() <-chan struct{} { if fut.ready == nil { return closedChan diff --git a/response.go b/response.go index 6363a4fe4..2e0783659 100644 --- a/response.go +++ b/response.go @@ -9,8 +9,8 @@ import ( type Response struct { RequestId uint32 Code uint32 - Error string // error message - // Data contains deserialized data for untyped requests + Error string // Error message. + // Data contains deserialized data for untyped requests. Data []interface{} buf smallBuf } @@ -139,7 +139,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { return } -// String implements Stringer interface +// String implements Stringer interface. func (resp *Response) String() (str string) { if resp.Code == OkCode { return fmt.Sprintf("<%d OK %v>", resp.RequestId, resp.Data) @@ -148,7 +148,7 @@ func (resp *Response) String() (str string) { } // Tuples converts result of Eval and Call17 to same format -// as other actions returns (ie array of arrays). +// as other actions returns (i.e. array of arrays). func (resp *Response) Tuples() (res [][]interface{}) { res = make([][]interface{}, len(resp.Data)) for i, t := range resp.Data { diff --git a/schema.go b/schema.go index 9b2dfae75..91c0faedd 100644 --- a/schema.go +++ b/schema.go @@ -7,25 +7,26 @@ import ( // Schema contains information about spaces and indexes. type Schema struct { Version uint - // Spaces is map from space names to spaces + // Spaces is map from space names to spaces. Spaces map[string]*Space - // SpacesById is map from space numbers to spaces + // SpacesById is map from space numbers to spaces. SpacesById map[uint32]*Space } -// Space contains information about tarantool space +// Space contains information about Tarantool's space. type Space struct { - Id uint32 - Name string + Id uint32 + Name string + // Could be "memtx" or "vinyl". Engine string - Temporary bool // Is this space temporaray? - // Field configuration is not mandatory and not checked by tarantool. + Temporary bool // Is this space temporary? + // Field configuration is not mandatory and not checked by Tarantool. FieldsCount uint32 Fields map[string]*Field FieldsById map[uint32]*Field - // Indexes is map from index names to indexes + // Indexes is map from index names to indexes. Indexes map[string]*Index - // IndexesById is map from index numbers to indexes + // IndexesById is map from index numbers to indexes. IndexesById map[uint32]*Index } @@ -35,7 +36,7 @@ type Field struct { Type string } -// Index contains information about index +// Index contains information about index. type Index struct { Id uint32 Name string @@ -64,7 +65,7 @@ func (conn *Connection) loadSchema() (err error) { schema.SpacesById = make(map[uint32]*Space) schema.Spaces = make(map[string]*Space) - // reload spaces + // Reload spaces. resp, err = conn.Select(vspaceSpId, 0, 0, maxSchemas, IterAll, []interface{}{}) if err != nil { return err @@ -117,7 +118,7 @@ func (conn *Connection) loadSchema() (err error) { schema.Spaces[space.Name] = space } - // reload indexes + // Reload indexes. resp, err = conn.Select(vindexSpId, 0, 0, maxSchemas, IterAll, []interface{}{}) if err != nil { return err @@ -135,7 +136,7 @@ func (conn *Connection) loadSchema() (err error) { opts := row[4].(map[interface{}]interface{}) var ok bool if index.Unique, ok = opts["unique"].(bool); !ok { - /* see bug https://github.com/tarantool/tarantool/issues/2060 */ + /* See bug https://github.com/tarantool/tarantool/issues/2060. */ index.Unique = true } default: diff --git a/test_helpers/main.go b/test_helpers/main.go index e5c73bfe5..ad45e00d9 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -1,3 +1,13 @@ +// Helpers for managing Tarantool process for testing purposes. +// +// Package introduces go helpers for starting a tarantool process and +// validating Tarantool version. Helpers are based on os/exec calls. +// Retries to connect test tarantool instance handled explicitly, +// see tarantool/go-tarantool#136. +// +// Tarantool's instance Lua scripts use environment variables to configure +// box.cfg. Listen port is set in the end of script so it is possible to +// connect only if every other thing was set up already. package test_helpers import ( diff --git a/uuid/uuid.go b/uuid/uuid.go index 11d3153e1..44e72f77c 100644 --- a/uuid/uuid.go +++ b/uuid/uuid.go @@ -1,3 +1,16 @@ +// Package with support of Tarantool's UUID data type. +// +// UUID data type supported in Tarantool since 2.4.1. +// +// Since: 1.6.0. +// +// See also +// +// * Tarantool's commit wht UUID support https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5 +// +// * Tarantool data model https://www.tarantool.io/en/doc/latest/book/box/data_model/ +// +// * Module UUID https://www.tarantool.io/en/doc/latest/reference/reference_lua/uuid/ package uuid import ( @@ -8,10 +21,7 @@ import ( "gopkg.in/vmihailenco/msgpack.v2" ) -// UUID external type -// Supported since Tarantool 2.4.1. See more in commit messages. -// https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5 - +// UUID external type. const UUID_extId = 2 func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { From 0a221e2b8dd02f640f846d851ec19b3c06da390b Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 30 Mar 2022 23:02:02 +0300 Subject: [PATCH 242/605] queue: make example executable - make example executable - add description with steps how to run example manually Part of #123 --- queue/example_test.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/queue/example_test.go b/queue/example_test.go index cf3150260..cb0f62f1a 100644 --- a/queue/example_test.go +++ b/queue/example_test.go @@ -1,3 +1,11 @@ +// Setup queue module and start Tarantool instance before execution: +// Terminal 1: +// $ make deps +// $ TEST_TNT_LISTEN=3013 tarantool queue/config.lua +// +// Terminal 2: +// $ cd queue +// $ go test -v example_test.go package queue_test import ( @@ -8,7 +16,8 @@ import ( "github.com/tarantool/go-tarantool/queue" ) -func ExampleConnection_Queue() { +// Example demonstrates an operations like Put and Take with queue. +func Example_simpleQueue() { cfg := queue.Cfg{ Temporary: false, Kind: queue.FIFO, @@ -16,8 +25,13 @@ func ExampleConnection_Queue() { Ttl: 10 * time.Second, }, } + opts := tarantool.Opts{ + Timeout: 2500 * time.Millisecond, + User: "test", + Pass: "test", + } - conn, err := tarantool.Connect(server, opts) + conn, err := tarantool.Connect("127.0.0.1:3013", opts) if err != nil { fmt.Printf("error in prepare is %v", err) return @@ -64,4 +78,6 @@ func ExampleConnection_Queue() { fmt.Printf("Task should be nil, but %d", task.Id()) return } + + // Output: data_1: test_data_1 } From 32b1a8a343bf238ffba8ffbad6e2719c5b6a2e13 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 30 Mar 2022 18:37:18 +0300 Subject: [PATCH 243/605] readme: move queue example to tests - make example executable with adding reference output - add description of steps how to run example manually - update comments for methods in queue.go See more about running examples in "Testable Examples in Go" [1] and testing package documentation [2]. Part of #123 1. https://go.dev/blog/examples 2. https://pkg.go.dev/testing#hdr-Examples --- README.md | 113 ----------------------------- queue/example_msgpack_test.go | 133 ++++++++++++++++++++++++++++++++++ queue/queue.go | 9 ++- 3 files changed, 139 insertions(+), 116 deletions(-) create mode 100644 queue/example_msgpack_test.go diff --git a/README.md b/README.md index e9521ab4d..ea19ccdea 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ faster than other packages according to public benchmarks. * [Schema](#schema) * [Custom (un)packing and typed selects and function calls](#custom-unpacking-and-typed-selects-and-function-calls) * [Options](#options) -* [Working with queue](#working-with-queue) * [Tests](#tests) * [Alternative connectors](#alternative-connectors) @@ -551,118 +550,6 @@ func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { * `User` - user name to log into Tarantool. * `Pass` - user password to log into Tarantool. -## Working with queue -```go -package main -import ( - "gopkg.in/vmihailenco/msgpack.v2" - "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/queue" - "time" - "fmt" - "log" -) - -type customData struct{ - Dummy bool -} - -func (c *customData) DecodeMsgpack(d *msgpack.Decoder) error { - var err error - if c.Dummy, err = d.DecodeBool(); err != nil { - return err - } - return nil -} - -func (c *customData) EncodeMsgpack(e *msgpack.Encoder) error { - return e.EncodeBool(c.Dummy) -} - -func main() { - opts := tarantool.Opts{ - Timeout: time.Second, - Reconnect: time.Second, - MaxReconnects: 5, - User: "user", - Pass: "pass", - // ... - } - conn, err := tarantool.Connect("127.0.0.1:3301", opts) - - if err != nil { - log.Fatalf("connection: %s", err) - return - } - - cfg := queue.Cfg{ - Temporary: true, - IfNotExists: true, - Kind: queue.FIFO, - Opts: queue.Opts{ - Ttl: 10 * time.Second, - Ttr: 5 * time.Second, - Delay: 3 * time.Second, - Pri: 1, - }, - } - - que := queue.New(conn, "test_queue") - if err = que.Create(cfg); err != nil { - log.Fatalf("queue create: %s", err) - return - } - - // put data - task, err := que.Put("test_data") - if err != nil { - log.Fatalf("put task: %s", err) - } - fmt.Println("Task id is", task.Id()) - - // take data - task, err = que.Take() //blocking operation - if err != nil { - log.Fatalf("take task: %s", err) - } - fmt.Println("Data is", task.Data()) - task.Ack() - - // take typed example - putData := customData{} - // put data - task, err = que.Put(&putData) - if err != nil { - log.Fatalf("put typed task: %s", err) - } - fmt.Println("Task id is ", task.Id()) - - takeData := customData{} - //take data - task, err = que.TakeTyped(&takeData) //blocking operation - if err != nil { - log.Fatalf("take take typed: %s", err) - } - fmt.Println("Data is ", takeData) - // same data - fmt.Println("Data is ", task.Data()) - - task, err = que.Put([]int{1, 2, 3}) - task.Bury() - - task, err = que.TakeTimeout(2 * time.Second) - if task == nil { - fmt.Println("Task is nil") - } - - que.Drop() -} -``` -Features of the implementation: - -- If you use connection timeout and call `TakeWithTimeout` with parameter greater than the connection timeout then parameter reduced to it -- If you use connection timeout and call `Take` then we return a error if we can not take task from queue in a time equal to the connection timeout - ## Multi connections You can use multiple connections config with tarantool/multi. diff --git a/queue/example_msgpack_test.go b/queue/example_msgpack_test.go new file mode 100644 index 000000000..ecfb60a94 --- /dev/null +++ b/queue/example_msgpack_test.go @@ -0,0 +1,133 @@ +// Setup queue module and start Tarantool instance before execution: +// Terminal 1: +// $ make deps +// $ TEST_TNT_LISTEN=3013 tarantool queue/config.lua +// +// Terminal 2: +// $ cd queue +// $ go test -v example_msgpack_test.go +package queue_test + +import ( + "fmt" + "time" + + "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/queue" + "gopkg.in/vmihailenco/msgpack.v2" + "log" +) + +type dummyData struct { + Dummy bool +} + +func (c *dummyData) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + if c.Dummy, err = d.DecodeBool(); err != nil { + return err + } + return nil +} + +func (c *dummyData) EncodeMsgpack(e *msgpack.Encoder) error { + return e.EncodeBool(c.Dummy) +} + +// Example demonstrates an operations like Put and Take with queue and custom +// MsgPack structure. +// +// Features of the implementation: +// +// - If you use the connection timeout and call TakeWithTimeout with a +// parameter greater than the connection timeout, the parameter is reduced to +// it. +// +// - If you use the connection timeout and call Take, we return an error if we +// cannot take the task out of the queue within the time corresponding to the +// connection timeout. +func Example_simpleQueueCustomMsgPack() { + opts := tarantool.Opts{ + Reconnect: time.Second, + Timeout: 2500 * time.Millisecond, + MaxReconnects: 5, + User: "test", + Pass: "test", + } + conn, err := tarantool.Connect("127.0.0.1:3013", opts) + if err != nil { + log.Fatalf("connection: %s", err) + return + } + defer conn.Close() + + cfg := queue.Cfg{ + Temporary: true, + IfNotExists: true, + Kind: queue.FIFO, + Opts: queue.Opts{ + Ttl: 10 * time.Second, + Ttr: 5 * time.Second, + Delay: 3 * time.Second, + Pri: 1, + }, + } + + que := queue.New(conn, "test_queue_msgpack") + if err = que.Create(cfg); err != nil { + log.Fatalf("queue create: %s", err) + return + } + + // Put data. + task, err := que.Put("test_data") + if err != nil { + log.Fatalf("put task: %s", err) + } + fmt.Println("Task id is", task.Id()) + + // Take data. + task, err = que.Take() // Blocking operation. + if err != nil { + log.Fatalf("take task: %s", err) + } + fmt.Println("Data is", task.Data()) + task.Ack() + + // Take typed example. + putData := dummyData{} + // Put data. + task, err = que.Put(&putData) + if err != nil { + log.Fatalf("put typed task: %s", err) + } + fmt.Println("Task id is ", task.Id()) + + takeData := dummyData{} + // Take data. + task, err = que.TakeTyped(&takeData) // Blocking operation. + if err != nil { + log.Fatalf("take take typed: %s", err) + } + fmt.Println("Data is ", takeData) + // Same data. + fmt.Println("Data is ", task.Data()) + + task, err = que.Put([]int{1, 2, 3}) + task.Bury() + + task, err = que.TakeTimeout(2 * time.Second) + if task == nil { + fmt.Println("Task is nil") + } + + que.Drop() + + // Unordered output: + // Task id is 0 + // Data is test_data + // Task id is 0 + // Data is {false} + // Data is &{false} + // Task is nil +} diff --git a/queue/queue.go b/queue/queue.go index 4b79f6d35..ce0eeb49f 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -37,20 +37,23 @@ type Queue interface { // Take takes 'ready' task from a tube and marks it as 'in progress'. // Note: if connection has a request Timeout, then 0.9 * connection.Timeout is // used as a timeout. + // If you use a connection timeout and we can not take task from queue in + // a time equal to the connection timeout after calling `Take` then we + // return an error. Take() (*Task, error) - // TakeWithTimout takes 'ready' task from a tube and marks it as "in progress", + // TakeTimeout takes 'ready' task from a tube and marks it as "in progress", // or it is timeouted after "timeout" period. // Note: if connection has a request Timeout, and conn.Timeout * 0.9 < timeout // then timeout = conn.Timeout*0.9. // If you use connection timeout and call `TakeTimeout` with parameter // greater than the connection timeout then parameter reduced to it. TakeTimeout(timeout time.Duration) (*Task, error) - // Take takes 'ready' task from a tube and marks it as 'in progress' + // TakeTyped takes 'ready' task from a tube and marks it as 'in progress' // Note: if connection has a request Timeout, then 0.9 * connection.Timeout is // used as a timeout. // Data will be unpacked to result. TakeTyped(interface{}) (*Task, error) - // TakeWithTimout takes 'ready' task from a tube and marks it as "in progress", + // TakeTypedTimeout takes 'ready' task from a tube and marks it as "in progress", // or it is timeouted after "timeout" period. // Note: if connection has a request Timeout, and conn.Timeout * 0.9 < timeout // then timeout = conn.Timeout*0.9. From d4816d9a97e915f8cb8dbd7ea215dd1c37ab41ae Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 30 Mar 2022 20:20:02 +0300 Subject: [PATCH 244/605] readme: move usage examples to tests README contains an example like already exist in a example_test.go, so example in a README was removed and example in tests has been updated: - single example splitted to multiple examples for Connection methods - added new examples for Ping(), Insert(), Delete(), Replace(), Update(), Call17(), Eval() and Connect() Part of #123 --- README.md | 111 -------------- example_test.go | 381 +++++++++++++++++++++++++++++++++--------------- 2 files changed, 260 insertions(+), 232 deletions(-) diff --git a/README.md b/README.md index ea19ccdea..cda5819ca 100644 --- a/README.md +++ b/README.md @@ -177,117 +177,6 @@ will also be happy to provide advice or receive feedback. ## Usage -```go -package main - -import ( - "github.com/tarantool/go-tarantool" - "log" - "time" -) - -func main() { - spaceNo := uint32(512) - indexNo := uint32(0) - - server := "127.0.0.1:3013" - opts := tarantool.Opts{ - Timeout: 500 * time.Millisecond, - Reconnect: 1 * time.Second, - MaxReconnects: 3, - User: "test", - Pass: "test", - } - client, err := tarantool.Connect(server, opts) - if err != nil { - log.Fatalf("Failed to connect: %s", err.Error()) - } - - resp, err := client.Ping() - log.Println(resp.Code) - log.Println(resp.Data) - log.Println(err) - - // insert new tuple { 10, 1 } - resp, err = client.Insert(spaceNo, []interface{}{uint(10), 1}) - // or - resp, err = client.Insert("test", []interface{}{uint(10), 1}) - log.Println("Insert") - log.Println("Error", err) - log.Println("Code", resp.Code) - log.Println("Data", resp.Data) - - // delete tuple with primary key { 10 } - resp, err = client.Delete(spaceNo, indexNo, []interface{}{uint(10)}) - // or - resp, err = client.Delete("test", "primary", []interface{}{uint(10)}) - log.Println("Delete") - log.Println("Error", err) - log.Println("Code", resp.Code) - log.Println("Data", resp.Data) - - // replace tuple with { 13, 1 } - resp, err = client.Replace(spaceNo, []interface{}{uint(13), 1}) - // or - resp, err = client.Replace("test", []interface{}{uint(13), 1}) - log.Println("Replace") - log.Println("Error", err) - log.Println("Code", resp.Code) - log.Println("Data", resp.Data) - - // update tuple with primary key { 13 }, incrementing second field by 3 - resp, err = client.Update(spaceNo, indexNo, []interface{}{uint(13)}, []interface{}{[]interface{}{"+", 1, 3}}) - // or - resp, err = client.Update("test", "primary", []interface{}{uint(13)}, []interface{}{[]interface{}{"+", 1, 3}}) - log.Println("Update") - log.Println("Error", err) - log.Println("Code", resp.Code) - log.Println("Data", resp.Data) - - // insert tuple {15, 1} or increment second field by 1 - resp, err = client.Upsert(spaceNo, []interface{}{uint(15), 1}, []interface{}{[]interface{}{"+", 1, 1}}) - // or - resp, err = client.Upsert("test", []interface{}{uint(15), 1}, []interface{}{[]interface{}{"+", 1, 1}}) - log.Println("Upsert") - log.Println("Error", err) - log.Println("Code", resp.Code) - log.Println("Data", resp.Data) - - // select just one tuple with primay key { 15 } - resp, err = client.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{uint(15)}) - // or - resp, err = client.Select("test", "primary", 0, 1, tarantool.IterEq, []interface{}{uint(15)}) - log.Println("Select") - log.Println("Error", err) - log.Println("Code", resp.Code) - log.Println("Data", resp.Data) - - // select tuples by condition ( primay key > 15 ) with offset 7 limit 5 - // BTREE index supposed - resp, err = client.Select(spaceNo, indexNo, 7, 5, tarantool.IterGt, []interface{}{uint(15)}) - // or - resp, err = client.Select("test", "primary", 7, 5, tarantool.IterGt, []interface{}{uint(15)}) - log.Println("Select") - log.Println("Error", err) - log.Println("Code", resp.Code) - log.Println("Data", resp.Data) - - // call function 'func_name' with arguments - resp, err = client.Call("func_name", []interface{}{1, 2, 3}) - log.Println("Call") - log.Println("Error", err) - log.Println("Code", resp.Code) - log.Println("Data", resp.Data) - - // run raw lua code - resp, err = client.Eval("return 1 + 2", []interface{}{}) - log.Println("Eval") - log.Println("Error", err) - log.Println("Code", resp.Code) - log.Println("Data", resp.Data) -} -``` - To enable support of UUID in msgpack with [google/uuid](https://github.com/google/uuid), import tarantool/uuid submodule. ```go diff --git a/example_test.go b/example_test.go index ee5f3f305..05594e370 100644 --- a/example_test.go +++ b/example_test.go @@ -54,6 +54,7 @@ func ExampleConnection_Select() { return } fmt.Printf("response is %#v\n", resp.Data) + // Output: // response is []interface {}{[]interface {}{0x457, "hello", "world"}} // response is []interface {}{[]interface {}{0x457, "hello", "world"}} @@ -85,140 +86,278 @@ func ExampleConnection_SelectTyped() { // response is [{{} 1111 hello world}] } -func Example() { - spaceNo := uint32(512) - indexNo := uint32(0) - - server := "127.0.0.1:3013" - opts := tarantool.Opts{ - Timeout: 50 * time.Millisecond, - Reconnect: 100 * time.Millisecond, - MaxReconnects: 3, - User: "test", - Pass: "test", - } - client, err := tarantool.Connect(server, opts) +func ExampleConnection_SelectAsync() { + var conn *tarantool.Connection + conn, err := example_connect() if err != nil { - fmt.Errorf("Failed to connect: %s", err.Error()) + fmt.Printf("error in prepare is %v", err) return } + defer conn.Close() - resp, err := client.Ping() - fmt.Println("Ping Code", resp.Code) - fmt.Println("Ping Data", resp.Data) - fmt.Println("Ping Error", err) - - // Delete tuple for cleaning. - client.Delete(spaceNo, indexNo, []interface{}{uint(10)}) - client.Delete(spaceNo, indexNo, []interface{}{uint(11)}) - - // Insert new tuple { 10, 1 }. - resp, err = client.Insert(spaceNo, []interface{}{uint(10), "test", "one"}) - fmt.Println("Insert Error", err) - fmt.Println("Insert Code", resp.Code) - fmt.Println("Insert Data", resp.Data) - - // Insert new tuple { 11, 1 }. - resp, err = client.Insert("test", &Tuple{Id: 11, Msg: "test", Name: "one"}) - fmt.Println("Insert Error", err) - fmt.Println("Insert Code", resp.Code) - fmt.Println("Insert Data", resp.Data) - - // Delete tuple with primary key { 10 }. - resp, err = client.Delete(spaceNo, indexNo, []interface{}{uint(10)}) - // or - // resp, err = client.Delete("test", "primary", UintKey{10}}) - fmt.Println("Delete Error", err) - fmt.Println("Delete Code", resp.Code) - fmt.Println("Delete Data", resp.Data) - - // Replace tuple with primary key 13. - // Note, Tuple is defined within tests, and has EncdodeMsgpack and DecodeMsgpack - // methods. - resp, err = client.Replace(spaceNo, []interface{}{uint(13), 1}) - fmt.Println("Replace Error", err) - fmt.Println("Replace Code", resp.Code) - fmt.Println("Replace Data", resp.Data) - - // Update tuple with primary key { 13 }, incrementing second field by 3. - resp, err = client.Update("test", "primary", tarantool.UintKey{13}, []tarantool.Op{{"+", 1, 3}}) - // or - // resp, err = client.Update(spaceNo, indexNo, []interface{}{uint(13)}, []interface{}{[]interface{}{"+", 1, 3}}) - fmt.Println("Update Error", err) - fmt.Println("Update Code", resp.Code) - fmt.Println("Update Data", resp.Data) - - // Select just one tuple with primay key { 15 }. - resp, err = client.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{uint(15)}) - // or - // resp, err = client.Select("test", "primary", 0, 1, tarantool.IterEq, tarantool.UintKey{15}) - fmt.Println("Select Error", err) - fmt.Println("Select Code", resp.Code) - fmt.Println("Select Data", resp.Data) - - // Call function 'func_name' with arguments. - resp, err = client.Call17("simple_incr", []interface{}{1}) - fmt.Println("Call17 Error", err) - fmt.Println("Call17 Code", resp.Code) - fmt.Println("Call17 Data", resp.Data) - - // Run raw Lua code. - resp, err = client.Eval("return 1 + 2", []interface{}{}) - fmt.Println("Eval Error", err) - fmt.Println("Eval Code", resp.Code) - fmt.Println("Eval Data", resp.Data) - - resp, err = client.Replace("test", &Tuple{Id: 11, Msg: "test", Name: "eleven"}) - resp, err = client.Replace("test", &Tuple{Id: 12, Msg: "test", Name: "twelve"}) + conn.Insert(spaceNo, []interface{}{uint(16), "test", "one"}) + conn.Insert(spaceNo, []interface{}{uint(17), "test", "one"}) + conn.Insert(spaceNo, []interface{}{uint(18), "test", "one"}) var futs [3]*tarantool.Future - futs[0] = client.SelectAsync("test", "primary", 0, 2, tarantool.IterLe, tarantool.UintKey{12}) - futs[1] = client.SelectAsync("test", "primary", 0, 1, tarantool.IterEq, tarantool.UintKey{13}) - futs[2] = client.SelectAsync("test", "primary", 0, 1, tarantool.IterEq, tarantool.UintKey{15}) + futs[0] = conn.SelectAsync("test", "primary", 0, 2, tarantool.IterLe, tarantool.UintKey{16}) + futs[1] = conn.SelectAsync("test", "primary", 0, 1, tarantool.IterEq, tarantool.UintKey{17}) + futs[2] = conn.SelectAsync("test", "primary", 0, 1, tarantool.IterEq, tarantool.UintKey{18}) var t []Tuple err = futs[0].GetTyped(&t) - fmt.Println("Fut", 0, "Error", err) - fmt.Println("Fut", 0, "Data", t) + fmt.Println("Future", 0, "Error", err) + fmt.Println("Future", 0, "Data", t) - resp, err = futs[1].Get() - fmt.Println("Fut", 1, "Error", err) - fmt.Println("Fut", 1, "Data", resp.Data) + resp, err := futs[1].Get() + fmt.Println("Future", 1, "Error", err) + fmt.Println("Future", 1, "Data", resp.Data) resp, err = futs[2].Get() - fmt.Println("Fut", 2, "Error", err) - fmt.Println("Fut", 2, "Data", resp.Data) + fmt.Println("Future", 2, "Error", err) + fmt.Println("Future", 2, "Data", resp.Data) + // Output: + // Future 0 Error + // Future 0 Data [{{} 16 val 16 bla} {{} 15 val 15 bla}] + // Future 1 Error + // Future 1 Data [[17 val 17 bla]] + // Future 2 Error + // Future 2 Data [[18 val 18 bla]] +} + +func ExampleConnection_Ping() { + var conn *tarantool.Connection + conn, err := example_connect() + if err != nil { + fmt.Printf("error in prepare is %v", err) + return + } + defer conn.Close() + + // Ping a Tarantool instance to check connection. + resp, err := conn.Ping() + fmt.Println("Ping Code", resp.Code) + fmt.Println("Ping Data", resp.Data) + fmt.Println("Ping Error", err) // Output: // Ping Code 0 // Ping Data [] // Ping Error - // Insert Error - // Insert Code 0 - // Insert Data [[10 test one]] - // Insert Error - // Insert Code 0 - // Insert Data [[11 test one]] - // Delete Error - // Delete Code 0 - // Delete Data [[10 test one]] - // Replace Error - // Replace Code 0 - // Replace Data [[13 1]] - // Update Error - // Update Code 0 - // Update Data [[13 4]] - // Select Error - // Select Code 0 - // Select Data [[15 val 15 bla]] - // Call17 Error - // Call17 Code 0 - // Call17 Data [2] - // Eval Error - // Eval Code 0 - // Eval Data [3] - // Fut 0 Error - // Fut 0 Data [{{} 12 test twelve} {{} 11 test eleven}] - // Fut 1 Error - // Fut 1 Data [[13 4]] - // Fut 2 Error - // Fut 2 Data [[15 val 15 bla]] +} + +func ExampleConnection_Insert() { + var conn *tarantool.Connection + conn, err := example_connect() + if err != nil { + fmt.Printf("error in prepare is %v", err) + return + } + defer conn.Close() + + // Insert a new tuple { 31, 1 }. + resp, err := conn.Insert(spaceNo, []interface{}{uint(31), "test", "one"}) + fmt.Println("Insert 31") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Insert a new tuple { 32, 1 }. + resp, err = conn.Insert("test", &Tuple{Id: 32, Msg: "test", Name: "one"}) + fmt.Println("Insert 32") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + + // Delete tuple with primary key { 31 }. + conn.Delete("test", "primary", []interface{}{uint(31)}) + // Delete tuple with primary key { 32 }. + conn.Delete(spaceNo, indexNo, []interface{}{uint(32)}) + // Output: + // Insert 31 + // Error + // Code 0 + // Data [[31 test one]] + // Insert 32 + // Error + // Code 0 + // Data [[32 test one]] + +} + +func ExampleConnection_Delete() { + var conn *tarantool.Connection + conn, err := example_connect() + if err != nil { + fmt.Printf("error in prepare is %v", err) + return + } + defer conn.Close() + + // Insert a new tuple { 35, 1 }. + conn.Insert(spaceNo, []interface{}{uint(35), "test", "one"}) + // Insert a new tuple { 36, 1 }. + conn.Insert("test", &Tuple{Id: 36, Msg: "test", Name: "one"}) + + // Delete tuple with primary key { 35 }. + resp, err := conn.Delete(spaceNo, indexNo, []interface{}{uint(35)}) + fmt.Println("Delete 35") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + + // Delete tuple with primary key { 36 }. + resp, err = conn.Delete("test", "primary", []interface{}{uint(36)}) + fmt.Println("Delete 36") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Output: + // Delete 35 + // Error + // Code 0 + // Data [[35 test one]] + // Delete 36 + // Error + // Code 0 + // Data [[36 test one]] +} + +func ExampleConnection_Replace() { + var conn *tarantool.Connection + conn, err := example_connect() + if err != nil { + fmt.Printf("error in prepare is %v", err) + return + } + defer conn.Close() + + // Insert a new tuple { 13, 1 }. + conn.Insert(spaceNo, []interface{}{uint(13), "test", "one"}) + + // Replace a tuple with primary key 13. + // Note, Tuple is defined within tests, and has EncdodeMsgpack and + // DecodeMsgpack methods. + resp, err := conn.Replace(spaceNo, []interface{}{uint(13), 1}) + fmt.Println("Replace 13") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + resp, err = conn.Replace("test", []interface{}{uint(13), 1}) + fmt.Println("Replace 13") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + resp, err = conn.Replace("test", &Tuple{Id: 13, Msg: "test", Name: "eleven"}) + fmt.Println("Replace 13") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + resp, err = conn.Replace("test", &Tuple{Id: 13, Msg: "test", Name: "twelve"}) + fmt.Println("Replace 13") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Output: + // Replace 13 + // Error + // Code 0 + // Data [[13 1]] + // Replace 13 + // Error + // Code 0 + // Data [[13 1]] + // Replace 13 + // Error + // Code 0 + // Data [[13 test eleven]] + // Replace 13 + // Error + // Code 0 + // Data [[13 test twelve]] +} + +func ExampleConnection_Update() { + var conn *tarantool.Connection + conn, err := example_connect() + if err != nil { + fmt.Printf("error in prepare is %v", err) + return + } + defer conn.Close() + + // Insert a new tuple { 14, 1 }. + conn.Insert(spaceNo, []interface{}{uint(14), "test", "one"}) + + // Update tuple with primary key { 14 }. + resp, err := conn.Update(spaceName, indexName, []interface{}{uint(14)}, []interface{}{[]interface{}{"=", 1, "bye"}}) + fmt.Println("Update 14") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Output: + // Update 14 + // Error + // Code 0 + // Data [[14 bye bla]] +} + +func ExampleConnection_Call17() { + var conn *tarantool.Connection + conn, err := example_connect() + if err != nil { + fmt.Printf("error in prepare is %v", err) + return + } + defer conn.Close() + + // Call a function 'simple_incr' with arguments. + resp, err := conn.Call17("simple_incr", []interface{}{1}) + fmt.Println("Call simple_incr()") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Output: + // Call simple_incr() + // Error + // Code 0 + // Data [2] +} + +func ExampleConnection_Eval() { + var conn *tarantool.Connection + conn, err := example_connect() + if err != nil { + fmt.Printf("error in prepare is %v", err) + return + } + defer conn.Close() + + // Run raw Lua code. + resp, err := conn.Eval("return 1 + 2", []interface{}{}) + fmt.Println("Eval 'return 1 + 2'") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Output: + // Eval 'return 1 + 2' + // Error + // Code 0 + // Data [3] +} + +func ExampleConnect() { + conn, err := tarantool.Connect("127.0.0.1:3013", tarantool.Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", + Concurrency: 32, + }) + if err != nil { + fmt.Println("No connection available") + return + } + defer conn.Close() + if conn != nil { + fmt.Println("Connection is ready") + } + // Output: + // Connection is ready } From 1cd820b2a76bc5197bc215afae2c282f8f47a3bb Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Tue, 12 Apr 2022 18:26:27 +0300 Subject: [PATCH 245/605] example: make examples more compact Previously each example created a connection and processed error. To make examples more compact and clear for documentation reader patch handles connection error in example_connect() used in examples and panics in case of error. Part of #123 --- example_test.go | 94 ++++++++++--------------------------------------- 1 file changed, 19 insertions(+), 75 deletions(-) diff --git a/example_test.go b/example_test.go index 05594e370..fb99e3254 100644 --- a/example_test.go +++ b/example_test.go @@ -16,32 +16,21 @@ type Tuple struct { Name string } -func example_connect() (*tarantool.Connection, error) { +func example_connect() *tarantool.Connection { conn, err := tarantool.Connect(server, opts) if err != nil { - return nil, err + panic("Connection is not established") } - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) - if err != nil { - conn.Close() - return nil, err - } - _, err = conn.Replace(spaceNo, []interface{}{uint(1112), "hallo", "werld"}) - if err != nil { - conn.Close() - return nil, err - } - return conn, nil + return conn } func ExampleConnection_Select() { - var conn *tarantool.Connection - conn, err := example_connect() - if err != nil { - fmt.Printf("error in prepare is %v", err) - return - } + conn := example_connect() defer conn.Close() + + conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + conn.Replace(spaceNo, []interface{}{uint(1112), "hallo", "werld"}) + resp, err := conn.Select(512, 0, 0, 100, tarantool.IterEq, []interface{}{uint(1111)}) if err != nil { fmt.Printf("error in select is %v", err) @@ -61,15 +50,10 @@ func ExampleConnection_Select() { } func ExampleConnection_SelectTyped() { - var conn *tarantool.Connection - conn, err := example_connect() - if err != nil { - fmt.Printf("error in prepare is %v", err) - return - } + conn := example_connect() defer conn.Close() var res []Tuple - err = conn.SelectTyped(512, 0, 0, 100, tarantool.IterEq, tarantool.IntKey{1111}, &res) + err := conn.SelectTyped(512, 0, 0, 100, tarantool.IterEq, tarantool.IntKey{1111}, &res) if err != nil { fmt.Printf("error in select is %v", err) return @@ -87,12 +71,7 @@ func ExampleConnection_SelectTyped() { } func ExampleConnection_SelectAsync() { - var conn *tarantool.Connection - conn, err := example_connect() - if err != nil { - fmt.Printf("error in prepare is %v", err) - return - } + conn := example_connect() defer conn.Close() conn.Insert(spaceNo, []interface{}{uint(16), "test", "one"}) @@ -104,7 +83,7 @@ func ExampleConnection_SelectAsync() { futs[1] = conn.SelectAsync("test", "primary", 0, 1, tarantool.IterEq, tarantool.UintKey{17}) futs[2] = conn.SelectAsync("test", "primary", 0, 1, tarantool.IterEq, tarantool.UintKey{18}) var t []Tuple - err = futs[0].GetTyped(&t) + err := futs[0].GetTyped(&t) fmt.Println("Future", 0, "Error", err) fmt.Println("Future", 0, "Data", t) @@ -125,12 +104,7 @@ func ExampleConnection_SelectAsync() { } func ExampleConnection_Ping() { - var conn *tarantool.Connection - conn, err := example_connect() - if err != nil { - fmt.Printf("error in prepare is %v", err) - return - } + conn := example_connect() defer conn.Close() // Ping a Tarantool instance to check connection. @@ -145,12 +119,7 @@ func ExampleConnection_Ping() { } func ExampleConnection_Insert() { - var conn *tarantool.Connection - conn, err := example_connect() - if err != nil { - fmt.Printf("error in prepare is %v", err) - return - } + conn := example_connect() defer conn.Close() // Insert a new tuple { 31, 1 }. @@ -183,12 +152,7 @@ func ExampleConnection_Insert() { } func ExampleConnection_Delete() { - var conn *tarantool.Connection - conn, err := example_connect() - if err != nil { - fmt.Printf("error in prepare is %v", err) - return - } + conn := example_connect() defer conn.Close() // Insert a new tuple { 35, 1 }. @@ -221,12 +185,7 @@ func ExampleConnection_Delete() { } func ExampleConnection_Replace() { - var conn *tarantool.Connection - conn, err := example_connect() - if err != nil { - fmt.Printf("error in prepare is %v", err) - return - } + conn := example_connect() defer conn.Close() // Insert a new tuple { 13, 1 }. @@ -275,12 +234,7 @@ func ExampleConnection_Replace() { } func ExampleConnection_Update() { - var conn *tarantool.Connection - conn, err := example_connect() - if err != nil { - fmt.Printf("error in prepare is %v", err) - return - } + conn := example_connect() defer conn.Close() // Insert a new tuple { 14, 1 }. @@ -300,12 +254,7 @@ func ExampleConnection_Update() { } func ExampleConnection_Call17() { - var conn *tarantool.Connection - conn, err := example_connect() - if err != nil { - fmt.Printf("error in prepare is %v", err) - return - } + conn := example_connect() defer conn.Close() // Call a function 'simple_incr' with arguments. @@ -322,12 +271,7 @@ func ExampleConnection_Call17() { } func ExampleConnection_Eval() { - var conn *tarantool.Connection - conn, err := example_connect() - if err != nil { - fmt.Printf("error in prepare is %v", err) - return - } + conn := example_connect() defer conn.Close() // Run raw Lua code. From 3cd205c60ce4e711dd394f4db1ce93562dcca2be Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 30 Mar 2022 20:21:45 +0300 Subject: [PATCH 246/605] readme: move uuid example to tests Make example executable by adding reference output to the comments. Part of #123 --- README.md | 47 -------------------------------------------- uuid/example_test.go | 46 +++++++++++++++++++++++++++++++++++++++++++ uuid/uuid.go | 2 +- 3 files changed, 47 insertions(+), 48 deletions(-) create mode 100644 uuid/example_test.go diff --git a/README.md b/README.md index cda5819ca..827f0be33 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ faster than other packages according to public benchmarks. * [API reference](#api-reference) * [Walking\-through example in Go](#walking-through-example-in-go) * [Help](#help) -* [Usage](#usage) * [Schema](#schema) * [Custom (un)packing and typed selects and function calls](#custom-unpacking-and-typed-selects-and-function-calls) * [Options](#options) @@ -175,52 +174,6 @@ To contact `go-tarantool` developers on any problems, create an issue at The developers of the [Tarantool server](http://github.com/tarantool/tarantool) will also be happy to provide advice or receive feedback. -## Usage - -To enable support of UUID in msgpack with [google/uuid](https://github.com/google/uuid), -import tarantool/uuid submodule. -```go -package main - -import ( - "log" - "time" - - "github.com/tarantool/go-tarantool" - _ "github.com/tarantool/go-tarantool/uuid" - "github.com/google/uuid" -) - -func main() { - server := "127.0.0.1:3013" - opts := tarantool.Opts{ - Timeout: 500 * time.Millisecond, - Reconnect: 1 * time.Second, - MaxReconnects: 3, - User: "test", - Pass: "test", - } - client, err := tarantool.Connect(server, opts) - if err != nil { - log.Fatalf("Failed to connect: %s", err.Error()) - } - - spaceNo := uint32(524) - - id, uuidErr := uuid.Parse("c8f0fa1f-da29-438c-a040-393f1126ad39") - if uuidErr != nil { - log.Fatalf("Failed to prepare uuid: %s", uuidErr) - } - - resp, err := client.Replace(spaceNo, []interface{}{ id }) - - log.Println("UUID tuple replace") - log.Println("Error", err) - log.Println("Code", resp.Code) - log.Println("Data", resp.Data) -} -``` - ## Schema ```go diff --git a/uuid/example_test.go b/uuid/example_test.go new file mode 100644 index 000000000..ef0993e59 --- /dev/null +++ b/uuid/example_test.go @@ -0,0 +1,46 @@ +// Run Tarantool instance before example execution: +// Terminal 1: +// $ cd uuid +// $ TEST_TNT_LISTEN=3013 TEST_TNT_WORK_DIR=$(mktemp -d -t 'tarantool.XXX') tarantool config.lua +// +// Terminal 2: +// $ cd uuid +// $ go test -v example_test.go +package uuid_test + +import ( + "fmt" + "log" + + "github.com/google/uuid" + "github.com/tarantool/go-tarantool" + _ "github.com/tarantool/go-tarantool/uuid" +) + +// Example demonstrates how to use tuples with UUID. To enable UUID support +// in msgpack with google/uuid (https://github.com/google/uuid), import +// tarantool/uuid submodule. +func Example() { + opts := tarantool.Opts{ + User: "test", + Pass: "test", + } + client, err := tarantool.Connect("127.0.0.1:3013", opts) + if err != nil { + log.Fatalf("Failed to connect: %s", err.Error()) + } + + spaceNo := uint32(524) + + id, uuidErr := uuid.Parse("c8f0fa1f-da29-438c-a040-393f1126ad39") + if uuidErr != nil { + log.Fatalf("Failed to prepare uuid: %s", uuidErr) + } + + resp, err := client.Replace(spaceNo, []interface{}{id}) + + fmt.Println("UUID tuple replace") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) +} diff --git a/uuid/uuid.go b/uuid/uuid.go index 44e72f77c..7d362c3d8 100644 --- a/uuid/uuid.go +++ b/uuid/uuid.go @@ -6,7 +6,7 @@ // // See also // -// * Tarantool's commit wht UUID support https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5 +// * Tarantool commit with UUID support https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5 // // * Tarantool data model https://www.tarantool.io/en/doc/latest/book/box/data_model/ // From b74ca2372025f8b452abcf4308ad5824a2d6e3d3 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 30 Mar 2022 20:29:37 +0300 Subject: [PATCH 247/605] readme: move schema example to tests - split schema example to multiple examples - make examples executable Part of #123 --- README.md | 29 --------------------- example_test.go | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 827f0be33..12af00fee 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ faster than other packages according to public benchmarks. * [API reference](#api-reference) * [Walking\-through example in Go](#walking-through-example-in-go) * [Help](#help) -* [Schema](#schema) * [Custom (un)packing and typed selects and function calls](#custom-unpacking-and-typed-selects-and-function-calls) * [Options](#options) * [Tests](#tests) @@ -174,34 +173,6 @@ To contact `go-tarantool` developers on any problems, create an issue at The developers of the [Tarantool server](http://github.com/tarantool/tarantool) will also be happy to provide advice or receive feedback. -## Schema - -```go - // save Schema to local variable to avoid races - schema := client.Schema - - // access Space objects by name or id - space1 := schema.Spaces["some_space"] - space2 := schema.SpacesById[20] // it's a map - fmt.Printf("Space %d %s %s\n", space1.Id, space1.Name, space1.Engine) - fmt.Printf("Space %d %d\n", space1.FieldsCount, space1.Temporary) - - // access index information by name or id - index1 := space1.Indexes["some_index"] - index2 := space1.IndexesById[2] // it's a map - fmt.Printf("Index %d %s\n", index1.Id, index1.Name) - - // access index fields information by index - indexField1 := index1.Fields[0] // it's a slice - indexField2 := index1.Fields[1] // it's a slice - fmt.Printf("IndexFields %s %s\n", indexField1.Name, indexField1.Type) - - // access space fields information by name or id (index) - spaceField1 := space.Fields["some_field"] - spaceField2 := space.FieldsById[3] - fmt.Printf("SpaceField %s %s\n", spaceField1.Name, spaceField1.Type) -``` - ## Custom (un)packing and typed selects and function calls You can specify custom pack/unpack functions for your types. This will allow you diff --git a/example_test.go b/example_test.go index fb99e3254..1e8c883ab 100644 --- a/example_test.go +++ b/example_test.go @@ -305,3 +305,70 @@ func ExampleConnect() { // Output: // Connection is ready } + +// Example demonstrates how to retrieve information with space schema. +func ExampleSchema() { + conn := example_connect() + defer conn.Close() + + schema := conn.Schema + if schema.SpacesById == nil { + fmt.Println("schema.SpacesById is nil") + } + if schema.Spaces == nil { + fmt.Println("schema.Spaces is nil") + } + + space1 := schema.Spaces["test"] + space2 := schema.SpacesById[514] + fmt.Printf("Space 1 ID %d %s\n", space1.Id, space1.Name) + fmt.Printf("Space 2 ID %d %s\n", space2.Id, space2.Name) + // Output: + // Space 1 ID 512 test + // Space 2 ID 514 schematest +} + +// Example demonstrates how to retrieve information with space schema. +func ExampleSpace() { + conn := example_connect() + defer conn.Close() + + // Save Schema to a local variable to avoid races + schema := conn.Schema + if schema.SpacesById == nil { + fmt.Println("schema.SpacesById is nil") + } + if schema.Spaces == nil { + fmt.Println("schema.Spaces is nil") + } + + // Access Space objects by name or ID. + space1 := schema.Spaces["test"] + space2 := schema.SpacesById[514] // It's a map. + fmt.Printf("Space 1 ID %d %s %s\n", space1.Id, space1.Name, space1.Engine) + fmt.Printf("Space 1 ID %d %t\n", space1.FieldsCount, space1.Temporary) + + // Access index information by name or ID. + index1 := space1.Indexes["primary"] + index2 := space2.IndexesById[3] // It's a map. + fmt.Printf("Index %d %s\n", index1.Id, index1.Name) + + // Access index fields information by index. + indexField1 := index1.Fields[0] // It's a slice. + indexField2 := index2.Fields[1] // It's a slice. + fmt.Println(indexField1, indexField2) + + // Access space fields information by name or id (index). + spaceField1 := space2.Fields["name0"] + spaceField2 := space2.FieldsById[3] + fmt.Printf("SpaceField 1 %s %s\n", spaceField1.Name, spaceField1.Type) + fmt.Printf("SpaceField 2 %s %s\n", spaceField2.Name, spaceField2.Type) + + // Output: + // Space 1 ID 512 test memtx + // Space 1 ID 0 false + // Index 0 primary + // &{0 unsigned} &{2 string} + // SpaceField 1 name0 unsigned + // SpaceField 2 name3 unsigned +} From 2fff09e0f0f49c3e2f3d22fa351da13160d4b3c9 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 30 Mar 2022 20:38:07 +0300 Subject: [PATCH 248/605] readme: move custom unpacking example to tests - commented out code with old way to register types has been removed. - examples and tests shares common data structures and it's methods, so tarantool_test.go was updated as well to allow building a new example. Part of #123 --- README.md | 180 ------------------------------- config.lua | 12 +++ example_custom_unpacking_test.go | 135 +++++++++++++++++++++++ tarantool_test.go | 47 ++------ 4 files changed, 154 insertions(+), 220 deletions(-) create mode 100644 example_custom_unpacking_test.go diff --git a/README.md b/README.md index 12af00fee..92227288e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ faster than other packages according to public benchmarks. * [API reference](#api-reference) * [Walking\-through example in Go](#walking-through-example-in-go) * [Help](#help) -* [Custom (un)packing and typed selects and function calls](#custom-unpacking-and-typed-selects-and-function-calls) * [Options](#options) * [Tests](#tests) * [Alternative connectors](#alternative-connectors) @@ -173,185 +172,6 @@ To contact `go-tarantool` developers on any problems, create an issue at The developers of the [Tarantool server](http://github.com/tarantool/tarantool) will also be happy to provide advice or receive feedback. -## Custom (un)packing and typed selects and function calls - -You can specify custom pack/unpack functions for your types. This will allow you -to store complex structures inside a tuple and may speed up you requests. - -Alternatively, you can just instruct the `msgpack` library to encode your -structure as an array. This is safe "magic". It will be easier to implement than -a custom packer/unpacker, but it will work slower. - -```go -import ( - "github.com/tarantool/go-tarantool" - "gopkg.in/vmihailenco/msgpack.v2" -) - -type Member struct { - Name string - Nonce string - Val uint -} - -type Tuple struct { - Cid uint - Orig string - Members []Member -} - -/* same effect in a "magic" way, but slower */ -type Tuple2 struct { - _msgpack struct{} `msgpack:",asArray"` - - Cid uint - Orig string - Members []Member -} - -func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { - if err := e.EncodeSliceLen(2); err != nil { - return err - } - if err := e.EncodeString(m.Name); err != nil { - return err - } - if err := e.EncodeUint(m.Val); err != nil { - return err - } - return nil -} - -func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { - var err error - var l int - if l, err = d.DecodeSliceLen(); err != nil { - return err - } - if l != 2 { - return fmt.Errorf("array len doesn't match: %d", l) - } - if m.Name, err = d.DecodeString(); err != nil { - return err - } - if m.Val, err = d.DecodeUint(); err != nil { - return err - } - return nil -} - -func (c *Tuple) EncodeMsgpack(e *msgpack.Encoder) error { - if err := e.EncodeSliceLen(3); err != nil { - return err - } - if err := e.EncodeUint(c.Cid); err != nil { - return err - } - if err := e.EncodeString(c.Orig); err != nil { - return err - } - if err := e.EncodeSliceLen(len(c.Members)); err != nil { - return err - } - for _, m := range c.Members { - e.Encode(m) - } - return nil -} - -func (c *Tuple) DecodeMsgpack(d *msgpack.Decoder) error { - var err error - var l int - if l, err = d.DecodeSliceLen(); err != nil { - return err - } - if l != 3 { - return fmt.Errorf("array len doesn't match: %d", l) - } - if c.Cid, err = d.DecodeUint(); err != nil { - return err - } - if c.Orig, err = d.DecodeString(); err != nil { - return err - } - if l, err = d.DecodeSliceLen(); err != nil { - return err - } - c.Members = make([]Member, l) - for i := 0; i < l; i++ { - d.Decode(&c.Members[i]) - } - return nil -} - -func main() { - // establish connection ... - - tuple := Tuple{777, "orig", []Member{{"lol", "", 1}, {"wut", "", 3}}} - _, err = conn.Replace(spaceNo, tuple) // NOTE: insert structure itself - if err != nil { - t.Errorf("Failed to insert: %s", err.Error()) - return - } - - var tuples []Tuple - err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{777}, &tuples) - if err != nil { - t.Errorf("Failed to SelectTyped: %s", err.Error()) - return - } - - // same result in a "magic" way - var tuples2 []Tuple2 - err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{777}, &tuples2) - if err != nil { - t.Errorf("Failed to SelectTyped: %s", err.Error()) - return - } - - // call function 'func_name' returning a table of custom tuples - var tuples3 []Tuple - err = client.CallTyped("func_name", []interface{}{1, 2, 3}, &tuples3) - if err != nil { - t.Errorf("Failed to CallTyped: %s", err.Error()) - return - } -} - -/* -// Old way to register types -func init() { - msgpack.Register(reflect.TypeOf(Tuple{}), encodeTuple, decodeTuple) - msgpack.Register(reflect.TypeOf(Member{}), encodeMember, decodeMember) -} - -func encodeMember(e *msgpack.Encoder, v reflect.Value) error { - m := v.Interface().(Member) - // same code as in EncodeMsgpack - return nil -} - -func decodeMember(d *msgpack.Decoder, v reflect.Value) error { - m := v.Addr().Interface().(*Member) - // same code as in DecodeMsgpack - return nil -} - -func encodeTuple(e *msgpack.Encoder, v reflect.Value) error { - c := v.Interface().(Tuple) - // same code as in EncodeMsgpack - return nil -} - -func decodeTuple(d *msgpack.Decoder, v reflect.Value) error { - c := v.Addr().Interface().(*Tuple) - // same code as in DecodeMsgpack - return nil -} -*/ - -``` - ## Options * `Timeout` - timeout for any particular request. If `Timeout` is zero request, diff --git a/config.lua b/config.lua index 2528e7b81..c768cd746 100644 --- a/config.lua +++ b/config.lua @@ -51,6 +51,18 @@ box.once("init", function() box.schema.user.grant('test', 'read,write', 'space', 'schematest') end) +local function func_name() + return { + {221, "", { + {"Moscow", 34}, + {"Minsk", 23}, + {"Kiev", 31}, + } + } + } +end +rawset(_G, 'func_name', func_name) + local function simple_incr(a) return a + 1 end diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go new file mode 100644 index 000000000..772c9faf3 --- /dev/null +++ b/example_custom_unpacking_test.go @@ -0,0 +1,135 @@ +package tarantool_test + +import ( + "fmt" + "github.com/tarantool/go-tarantool" + "gopkg.in/vmihailenco/msgpack.v2" + "log" + "time" +) + +type Tuple2 struct { + Cid uint + Orig string + Members []Member +} + +// Same effect in a "magic" way, but slower. +type Tuple3 struct { + _msgpack struct{} `msgpack:",asArray"` + + Cid uint + Orig string + Members []Member +} + +func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { + if err := e.EncodeSliceLen(3); err != nil { + return err + } + if err := e.EncodeUint(c.Cid); err != nil { + return err + } + if err := e.EncodeString(c.Orig); err != nil { + return err + } + e.Encode(c.Members) + return nil +} + +func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 3 { + return fmt.Errorf("array len doesn't match: %d", l) + } + if c.Cid, err = d.DecodeUint(); err != nil { + return err + } + if c.Orig, err = d.DecodeString(); err != nil { + return err + } + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + c.Members = make([]Member, l) + for i := 0; i < l; i++ { + d.Decode(&c.Members[i]) + } + return nil +} + +// Example demonstrates how to use custom (un)packing with typed selects and +// function calls. +// +// You can specify user-defined packing/unpacking functions for your types. +// This allows you to store complex structures within a tuple and may speed up +// your requests. +// +// Alternatively, you can just instruct the msgpack library to encode your +// structure as an array. This is safe "magic". It is easier to implement than +// a custom packer/unpacker, but it will work slower. +func Example_customUnpacking() { + // Establish a connection. + server := "127.0.0.1:3013" + opts := tarantool.Opts{ + Timeout: 500 * time.Millisecond, + Reconnect: 1 * time.Second, + MaxReconnects: 3, + User: "test", + Pass: "test", + } + conn, err := tarantool.Connect(server, opts) + if err != nil { + log.Fatalf("Failed to connect: %s", err.Error()) + } + + spaceNo := uint32(512) + indexNo := uint32(0) + + tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} + resp, err := conn.Replace(spaceNo, &tuple) // NOTE: insert a structure itself. + if err != nil { + log.Fatalf("Failed to insert: %s", err.Error()) + return + } + fmt.Println("Data", resp.Data) + fmt.Println("Code", resp.Code) + + var tuples1 []Tuple2 + err = conn.SelectTyped(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{777}, &tuples1) + if err != nil { + log.Fatalf("Failed to SelectTyped: %s", err.Error()) + return + } + fmt.Println("Tuples (tuples1)", tuples1) + + // Same result in a "magic" way. + var tuples2 []Tuple3 + err = conn.SelectTyped(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{777}, &tuples2) + if err != nil { + log.Fatalf("Failed to SelectTyped: %s", err.Error()) + return + } + fmt.Println("Tuples (tuples2):", tuples2) + + // Call a function "func_name" returning a table of custom tuples. + var tuples3 []Tuple3 + err = conn.CallTyped("func_name", []interface{}{}, &tuples3) + if err != nil { + log.Fatalf("Failed to CallTyped: %s", err.Error()) + return + } + fmt.Println("Tuples (tuples3):", tuples3) + + // Output: + // Data [[777 orig [[lol 1] [wut 3]]]] + // Code 0 + // Tuples (tuples1) [{777 orig [{lol 1} {wut 3}]}] + // Tuples (tuples2): [{{} 777 orig [{lol 1} {wut 3}]}] + // Tuples (tuples3): [{{} 221 [{Moscow 34} {Minsk 23} {Kiev 31}]}] + +} diff --git a/tarantool_test.go b/tarantool_test.go index 41bdfe830..5f5078a8b 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -20,67 +20,34 @@ type Member struct { Val uint } -type Tuple2 struct { - Cid uint - Orig string - Members []Member -} - func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { - e.EncodeSliceLen(2) - e.EncodeString(m.Name) - e.EncodeUint(m.Val) - return nil -} - -func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { - var err error - var l int - if l, err = d.DecodeSliceLen(); err != nil { + if err := e.EncodeSliceLen(2); err != nil { return err } - if l != 2 { - return fmt.Errorf("array len doesn't match: %d", l) - } - if m.Name, err = d.DecodeString(); err != nil { + if err := e.EncodeString(m.Name); err != nil { return err } - if m.Val, err = d.DecodeUint(); err != nil { + if err := e.EncodeUint(m.Val); err != nil { return err } return nil } -func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { - e.EncodeSliceLen(3) - e.EncodeUint(c.Cid) - e.EncodeString(c.Orig) - e.Encode(c.Members) - return nil -} - -func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { +func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeSliceLen(); err != nil { return err } - if l != 3 { + if l != 2 { return fmt.Errorf("array len doesn't match: %d", l) } - if c.Cid, err = d.DecodeUint(); err != nil { - return err - } - if c.Orig, err = d.DecodeString(); err != nil { + if m.Name, err = d.DecodeString(); err != nil { return err } - if l, err = d.DecodeSliceLen(); err != nil { + if m.Val, err = d.DecodeUint(); err != nil { return err } - c.Members = make([]Member, l) - for i := 0; i < l; i++ { - d.Decode(&c.Members[i]) - } return nil } From 785b718baa1fba57c1f094405a43b3ec679b9d5d Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 30 Mar 2022 22:24:41 +0300 Subject: [PATCH 249/605] readme: move description of options to inline comment Part of #123 --- README.md | 12 ------------ connection.go | 14 +++++++++----- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 92227288e..be622b2d8 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ faster than other packages according to public benchmarks. * [API reference](#api-reference) * [Walking\-through example in Go](#walking-through-example-in-go) * [Help](#help) -* [Options](#options) * [Tests](#tests) * [Alternative connectors](#alternative-connectors) @@ -172,17 +171,6 @@ To contact `go-tarantool` developers on any problems, create an issue at The developers of the [Tarantool server](http://github.com/tarantool/tarantool) will also be happy to provide advice or receive feedback. -## Options - -* `Timeout` - timeout for any particular request. If `Timeout` is zero request, - any request may block infinitely. -* `Reconnect` - timeout between reconnect attempts. If `Reconnect` is zero, no - reconnects will be performed. -* `MaxReconnects` - maximal number of reconnect failures; after that we give it - up. If `MaxReconnects` is zero, the client will try to reconnect endlessly. -* `User` - user name to log into Tarantool. -* `Pass` - user password to log into Tarantool. - ## Multi connections You can use multiple connections config with tarantool/multi. diff --git a/connection.go b/connection.go index 5a5d3921a..d8e381364 100644 --- a/connection.go +++ b/connection.go @@ -158,21 +158,25 @@ type Greeting struct { // Opts is a way to configure Connection type Opts struct { - // Timeout is requests timeout. + // Timeout for any particular request. If Timeout is zero request, any + // request can be blocked infinitely. // Also used to setup net.TCPConn.Set(Read|Write)Deadline. Timeout time.Duration - // Reconnect is a pause between reconnection attempts. + // Timeout between reconnect attempts. If Reconnect is zero, no + // reconnect attempts will be made. // If specified, then when Tarantool is not reachable or disconnected, // new connect attempt is performed after pause. // By default, no reconnection attempts are performed, // so once disconnected, connection becomes Closed. Reconnect time.Duration - // MaxReconnects is a maximum reconnect attempts. + // Maximum number of reconnect failures; after that we give it up to + // on. If MaxReconnects is zero, the client will try to reconnect + // endlessly. // After MaxReconnects attempts Connection becomes closed. MaxReconnects uint - // User name for authorization. + // Username for logging in to Tarantool. User string - // Pass is password for authorization. + // User password for logging in to Tarantool. Pass string // RateLimit limits number of 'in-fly' request, i.e. already put into // requests queue, but not yet answered by server or timeouted. From 676c5abf681030901b97d9e87aa3b842055c9c31 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Mon, 11 Apr 2022 17:24:08 +0300 Subject: [PATCH 250/605] readme: move description of multiple connections to comment Part of #123 --- README.md | 15 --------------- multi/multi.go | 11 +++++++++++ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index be622b2d8..7bb490dd7 100644 --- a/README.md +++ b/README.md @@ -171,21 +171,6 @@ To contact `go-tarantool` developers on any problems, create an issue at The developers of the [Tarantool server](http://github.com/tarantool/tarantool) will also be happy to provide advice or receive feedback. -## Multi connections - -You can use multiple connections config with tarantool/multi. - -Main features: - -- Check active connection with configurable time interval and on connection fail switch to next in pool. -- Get addresses list from server and reconfigure to use in MultiConnection. - -Additional options (configurable via `ConnectWithOpts`): - -* `CheckTimeout` - time interval to check for connection timeout and try to switch connection -* `ClusterDiscoveryTime` - time interval to ask server for updated address list (works on with `NodesGetFunctionName` set) -* `NodesGetFunctionName` - server lua function name to call for getting address list - ## Tests You need to [install Tarantool](https://www.tarantool.io/en/download/) to run tests. diff --git a/multi/multi.go b/multi/multi.go index a139853e0..0b5c83348 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -1,3 +1,14 @@ +// Package with methods to work with a Tarantool cluster. +// +// Main features: +// +// - Check the active connection with a configurable time interval and switch +// to the next connection in the pool if there is a connection failure. +// +// - Get the address list from the server and reconfigure it for use in +// MultiConnection. +// +// Since: 1.5 package multi import ( From cc56b786b42cf862ac6a46952bc8a4c19d1372af Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 31 Mar 2022 11:33:22 +0300 Subject: [PATCH 251/605] multi: add documentation comments Part of #123 --- multi/multi.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/multi/multi.go b/multi/multi.go index 0b5c83348..c83010c36 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -40,6 +40,10 @@ func indexOf(sstring string, data []string) int { return -1 } +// ConnectionMulti is a handle with connections to a number of Tarantool instances. +// +// It is created and configured with Connect function, and could not be +// reconfigured later. type ConnectionMulti struct { addrs []string connOpts tarantool.Opts @@ -55,12 +59,19 @@ type ConnectionMulti struct { var _ = tarantool.Connector(&ConnectionMulti{}) // Check compatibility with connector interface. +// OptsMulti is a way to configure Connection with multiconnect-specific options. type OptsMulti struct { - CheckTimeout time.Duration + // CheckTimeout is a time interval to check for connection timeout and try to + // switch connection. + CheckTimeout time.Duration + // Lua function name of the server called to retrieve the address list. NodesGetFunctionName string + // Time interval to ask the server for an updated address list (works + // if NodesGetFunctionName is set). ClusterDiscoveryTime time.Duration } +// Connect creates and configures new ConnectionMulti with multiconnection options. func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsMulti) (connMulti *ConnectionMulti, err error) { if len(addrs) == 0 { return nil, ErrEmptyAddrs @@ -92,6 +103,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsMulti) (c return connMulti, nil } +// Connect creates and configures new ConnectionMulti. func Connect(addrs []string, connOpts tarantool.Opts) (connMulti *ConnectionMulti, err error) { opts := OptsMulti{ CheckTimeout: 1 * time.Second, @@ -236,10 +248,13 @@ func (connMulti *ConnectionMulti) getCurrentConnection() *tarantool.Connection { return connMulti.fallback } +// ConnectedNow reports if connection is established at the moment. func (connMulti *ConnectionMulti) ConnectedNow() bool { return connMulti.getState() == connConnected && connMulti.getCurrentConnection().ConnectedNow() } +// Close closes Connection. +// After this method called, there is no way to reopen this Connection. func (connMulti *ConnectionMulti) Close() (err error) { connMulti.mutex.Lock() defer connMulti.mutex.Unlock() @@ -261,118 +276,174 @@ func (connMulti *ConnectionMulti) Close() (err error) { return } +// Ping sends empty request to Tarantool to check connection. func (connMulti *ConnectionMulti) Ping() (resp *tarantool.Response, err error) { return connMulti.getCurrentConnection().Ping() } +// ConfiguredTimeout returns a timeout from connection config. func (connMulti *ConnectionMulti) ConfiguredTimeout() time.Duration { return connMulti.getCurrentConnection().ConfiguredTimeout() } +// Select performs select to box space. func (connMulti *ConnectionMulti) Select(space, index interface{}, offset, limit, iterator uint32, key interface{}) (resp *tarantool.Response, err error) { return connMulti.getCurrentConnection().Select(space, index, offset, limit, iterator, key) } +// Insert performs insertion to box space. +// Tarantool will reject Insert when tuple with same primary key exists. func (connMulti *ConnectionMulti) Insert(space interface{}, tuple interface{}) (resp *tarantool.Response, err error) { return connMulti.getCurrentConnection().Insert(space, tuple) } +// Replace performs "insert or replace" action to box space. +// If tuple with same primary key exists, it will be replaced. func (connMulti *ConnectionMulti) Replace(space interface{}, tuple interface{}) (resp *tarantool.Response, err error) { return connMulti.getCurrentConnection().Replace(space, tuple) } +// Delete performs deletion of a tuple by key. +// Result will contain array with deleted tuple. func (connMulti *ConnectionMulti) Delete(space, index interface{}, key interface{}) (resp *tarantool.Response, err error) { return connMulti.getCurrentConnection().Delete(space, index, key) } +// Update performs update of a tuple by key. +// Result will contain array with updated tuple. func (connMulti *ConnectionMulti) Update(space, index interface{}, key, ops interface{}) (resp *tarantool.Response, err error) { return connMulti.getCurrentConnection().Update(space, index, key, ops) } +// Upsert performs "update or insert" action of a tuple by key. +// Result will not contain any tuple. func (connMulti *ConnectionMulti) Upsert(space interface{}, tuple, ops interface{}) (resp *tarantool.Response, err error) { return connMulti.getCurrentConnection().Upsert(space, tuple, ops) } +// Call calls registered Tarantool function. +// It uses request code for Tarantool 1.6, so result is converted to array of +// arrays. func (connMulti *ConnectionMulti) Call(functionName string, args interface{}) (resp *tarantool.Response, err error) { return connMulti.getCurrentConnection().Call(functionName, args) } +// Call17 calls registered Tarantool function. +// It uses request code for Tarantool 1.7, so result is not converted +// (though, keep in mind, result is always array). func (connMulti *ConnectionMulti) Call17(functionName string, args interface{}) (resp *tarantool.Response, err error) { return connMulti.getCurrentConnection().Call17(functionName, args) } +// Eval passes Lua expression for evaluation. func (connMulti *ConnectionMulti) Eval(expr string, args interface{}) (resp *tarantool.Response, err error) { return connMulti.getCurrentConnection().Eval(expr, args) } +// GetTyped performs select (with limit = 1 and offset = 0) to box space and +// fills typed result. func (connMulti *ConnectionMulti) GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) { return connMulti.getCurrentConnection().GetTyped(space, index, key, result) } +// SelectTyped performs select to box space and fills typed result. func (connMulti *ConnectionMulti) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key interface{}, result interface{}) (err error) { return connMulti.getCurrentConnection().SelectTyped(space, index, offset, limit, iterator, key, result) } +// InsertTyped performs insertion to box space. +// Tarantool will reject Insert when tuple with same primary key exists. func (connMulti *ConnectionMulti) InsertTyped(space interface{}, tuple interface{}, result interface{}) (err error) { return connMulti.getCurrentConnection().InsertTyped(space, tuple, result) } +// ReplaceTyped performs "insert or replace" action to box space. +// If tuple with same primary key exists, it will be replaced. func (connMulti *ConnectionMulti) ReplaceTyped(space interface{}, tuple interface{}, result interface{}) (err error) { return connMulti.getCurrentConnection().ReplaceTyped(space, tuple, result) } +// DeleteTyped performs deletion of a tuple by key and fills result with +// deleted tuple. func (connMulti *ConnectionMulti) DeleteTyped(space, index interface{}, key interface{}, result interface{}) (err error) { return connMulti.getCurrentConnection().DeleteTyped(space, index, key, result) } +// UpdateTyped performs update of a tuple by key and fills result with updated +// tuple. func (connMulti *ConnectionMulti) UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) (err error) { return connMulti.getCurrentConnection().UpdateTyped(space, index, key, ops, result) } +// CallTyped calls registered function. +// It uses request code for Tarantool 1.6, so result is converted to array of +// arrays. func (connMulti *ConnectionMulti) CallTyped(functionName string, args interface{}, result interface{}) (err error) { return connMulti.getCurrentConnection().CallTyped(functionName, args, result) } +// Call17Typed calls registered function. +// It uses request code for Tarantool 1.7, so result is not converted (though, +// keep in mind, result is always array) func (connMulti *ConnectionMulti) Call17Typed(functionName string, args interface{}, result interface{}) (err error) { return connMulti.getCurrentConnection().Call17Typed(functionName, args, result) } +// EvalTyped passes Lua expression for evaluation. func (connMulti *ConnectionMulti) EvalTyped(expr string, args interface{}, result interface{}) (err error) { return connMulti.getCurrentConnection().EvalTyped(expr, args, result) } +// SelectAsync sends select request to Tarantool and returns Future. func (connMulti *ConnectionMulti) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *tarantool.Future { return connMulti.getCurrentConnection().SelectAsync(space, index, offset, limit, iterator, key) } +// InsertAsync sends insert action to Tarantool and returns Future. +// Tarantool will reject Insert when tuple with same primary key exists. func (connMulti *ConnectionMulti) InsertAsync(space interface{}, tuple interface{}) *tarantool.Future { return connMulti.getCurrentConnection().InsertAsync(space, tuple) } +// ReplaceAsync sends "insert or replace" action to Tarantool and returns Future. +// If tuple with same primary key exists, it will be replaced. func (connMulti *ConnectionMulti) ReplaceAsync(space interface{}, tuple interface{}) *tarantool.Future { return connMulti.getCurrentConnection().ReplaceAsync(space, tuple) } +// DeleteAsync sends deletion action to Tarantool and returns Future. +// Future's result will contain array with deleted tuple. func (connMulti *ConnectionMulti) DeleteAsync(space, index interface{}, key interface{}) *tarantool.Future { return connMulti.getCurrentConnection().DeleteAsync(space, index, key) } +// Update sends deletion of a tuple by key and returns Future. +// Future's result will contain array with updated tuple. func (connMulti *ConnectionMulti) UpdateAsync(space, index interface{}, key, ops interface{}) *tarantool.Future { return connMulti.getCurrentConnection().UpdateAsync(space, index, key, ops) } +// UpsertAsync sends "update or insert" action to Tarantool and returns Future. +// Future's sesult will not contain any tuple. func (connMulti *ConnectionMulti) UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *tarantool.Future { return connMulti.getCurrentConnection().UpsertAsync(space, tuple, ops) } +// CallAsync sends a call to registered Tarantool function and returns Future. +// It uses request code for Tarantool 1.6, so future's result is always array +// of arrays. func (connMulti *ConnectionMulti) CallAsync(functionName string, args interface{}) *tarantool.Future { return connMulti.getCurrentConnection().CallAsync(functionName, args) } +// Call17Async sends a call to registered Tarantool function and returns Future. +// It uses request code for Tarantool 1.7, so future's result will not be converted +// (though, keep in mind, result is always array). func (connMulti *ConnectionMulti) Call17Async(functionName string, args interface{}) *tarantool.Future { return connMulti.getCurrentConnection().Call17Async(functionName, args) } +// EvalAsync passes Lua expression for evaluation. func (connMulti *ConnectionMulti) EvalAsync(expr string, args interface{}) *tarantool.Future { return connMulti.getCurrentConnection().EvalAsync(expr, args) } From 3a0c91c342918a2139c57b9b8586974489412858 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Mon, 11 Apr 2022 17:15:46 +0300 Subject: [PATCH 252/605] multi: add examples for connect functions Part of #123 --- multi/example_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 multi/example_test.go diff --git a/multi/example_test.go b/multi/example_test.go new file mode 100644 index 000000000..e095504a2 --- /dev/null +++ b/multi/example_test.go @@ -0,0 +1,38 @@ +package multi + +import ( + "fmt" + "github.com/tarantool/go-tarantool" + "time" +) + +func ExampleConnect() { + multiConn, err := Connect([]string{"127.0.0.1:3031", "127.0.0.1:3032"}, tarantool.Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", + }) + if err != nil { + fmt.Printf("error in connect is %v", err) + } + fmt.Println(multiConn) +} + +func ExampleConnectWithOpts() { + multiConn, err := ConnectWithOpts([]string{"127.0.0.1:3301", "127.0.0.1:3302"}, tarantool.Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", + }, OptsMulti{ + // Check for connection timeout every 1 second. + CheckTimeout: 1 * time.Second, + // Lua function name for getting address list. + NodesGetFunctionName: "get_cluster_nodes", + // Ask server for updated address list every 3 seconds. + ClusterDiscoveryTime: 3 * time.Second, + }) + if err != nil { + fmt.Printf("error in connect is %v", err) + } + fmt.Println(multiConn) +} From 4f4fefa68d9212287279ecd44cd5b7a994cb8b77 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 31 Mar 2022 11:46:12 +0300 Subject: [PATCH 253/605] readme: update section about testing Part of #123 --- Makefile | 26 +++++++++++++++++++++++++- README.md | 11 ++++------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 72959eeaf..5af3358d0 100644 --- a/Makefile +++ b/Makefile @@ -12,8 +12,32 @@ deps: clean .PHONY: test test: + go test ./... -v -p 1 + +.PHONY: test-multi +test-multi: + @echo "Running tests in multiconnection package" + go clean -testcache + go test ./multi/ -v -p 1 + +.PHONY: test-queue +test-queue: + @echo "Running tests in queue package" + cd ./queue/ && tarantool -e "require('queue')" + go clean -testcache + go test ./queue/ -v -p 1 + +.PHONY: test-uuid +test-uuid: + @echo "Running tests in UUID package" + go clean -testcache + go test ./uuid/ -v -p 1 + +.PHONY: test-main +test-main: + @echo "Running tests in main package" go clean -testcache - go test ./... -v -p 1 + go test . -v -p 1 .PHONY: coverage coverage: diff --git a/README.md b/README.md index 7bb490dd7..547fd725b 100644 --- a/README.md +++ b/README.md @@ -187,17 +187,14 @@ make test ``` Tests set up all required `tarantool` processes before run and clean up after. -If you want to run a specific package tests, go to a package folder +If you want to run a specific package tests, call ```bash -cd multi +make test- ``` -and call +For example, for running tests in `multi`, `uuid` and `main` packages, call ```bash -go clean -testcache && go test -v +make test-multi test-uuid test-main ``` -Use the same for main `tarantool` package and `queue` and `uuid` subpackages. -`uuid` tests require -[Tarantool 2.4.1 or newer](https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5). ## Alternative connectors From f157ee5db98334150b0df31754518a140a97831b Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 31 Mar 2022 12:18:25 +0300 Subject: [PATCH 254/605] readme: update section about installation Go 1.3 mentioned in installation section was EOLed many years ago. No sense to describe it's installation. Go 1.3 replaced with Go 1.13. Go 1.13 is outdated too and unsupported, but we run tests with it in Github Actions so it is a guarantee that everything will works with it. Part of #123 --- README.md | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 547fd725b..0376b1d8e 100644 --- a/README.md +++ b/README.md @@ -31,33 +31,26 @@ faster than other packages according to public benchmarks. ## Installation -We assume that you have Tarantool version 1.6 and a modern Linux or BSD +We assume that you have Tarantool version 1.6+ and a modern Linux or BSD operating system. -You will need a current version of `go`, version 1.3 or later (use -`go version` to check the version number). Do not use `gccgo-go`. +You need a current version of `go`, version 1.13 or later (use `go version` to +check the version number). Do not use `gccgo-go`. -**Note:** If your `go` version is younger than 1.3, or if `go` is not installed, -download the latest tarball from [golang.org](https://golang.org/dl/) and say: +**Note:** If your `go` version is older than 1.3 or if `go` is not installed, +download and run the latest tarball from [golang.org][golang-dl]. -```bash -$ sudo tar -C /usr/local -xzf go1.7.5.linux-amd64.tar.gz -$ export PATH=$PATH:/usr/local/go/bin -$ export GOPATH="/usr/local/go/go-tarantool" -$ sudo chmod -R a+rwx /usr/local/go -``` - -The `go-tarantool` package is in -[tarantool/go-tarantool](github.com/tarantool/go-tarantool) repository. -To download and install, say: +The package `go-tarantool` is located in [tarantool/go-tarantool][go-tarantool] +repository. To download and install, say: ``` $ go get github.com/tarantool/go-tarantool ``` -This should bring source and binary files into subdirectories of `/usr/local/go`, -making it possible to access by adding `github.com/tarantool/go-tarantool` in -the `import {...}` section at the start of any Go program. +This should put the source and binary files in subdirectories of +`/usr/local/go`, so that you can access them by adding +`github.com/tarantool/go-tarantool` to the `import {...}` section at the start +of any Go program.

Hello World

@@ -217,3 +210,5 @@ See feature comparison in [documentation](https://www.tarantool.io/en/doc/latest [discussions-url]: https://github.com/tarantool/tarantool/discussions [stackoverflow-badge]: https://img.shields.io/badge/stackoverflow-tarantool-orange.svg [stackoverflow-url]: https://stackoverflow.com/questions/tagged/tarantool +[golang-dl]: https://go.dev/dl/ +[go-tarantool]: https://github.com/tarantool/go-tarantool From 70cf9b4db39617b8487ff38b1515cabae236e9f4 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 6 Apr 2022 18:02:55 +0300 Subject: [PATCH 255/605] readme: move section about testing to contributing guide There is a guide made by Github with recommendations how to make project healthy for contributors [1]. One of them is a contributing guide with steps for new contributors. Knowledge about running tests in a project is not needed to all people who will read our README, so I moved this section to contributing guide. Part of #123 1. https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions --- CONTRIBUTING.md | 29 +++++++++++++++++++++++++++++ README.md | 28 ++++------------------------ 2 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..ac9ba94da --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# Contribution Guide + +## Running tests + +You need to [install Tarantool](https://tarantool.io/en/download/) to run tests. +See the Installation section in the README for requirements. + +To install test dependencies (such as the +[tarantool/queue](https://github.com/tarantool/queue) module), run: +```bash +make deps +``` + +To run tests for the main package and each subpackage: +```bash +make test +``` + +The tests set up all required `tarantool` processes before run and clean up +afterwards. + +If you want to run the tests for a specific package: +```bash +make test- +``` +For example, for running tests in `multi`, `uuid` and `main` packages, call +```bash +make test-multi test-uuid test-main +``` diff --git a/README.md b/README.md index 0376b1d8e..33d64cfeb 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ faster than other packages according to public benchmarks. * [API reference](#api-reference) * [Walking\-through example in Go](#walking-through-example-in-go) * [Help](#help) -* [Tests](#tests) +* [Contributing](#contributing) * [Alternative connectors](#alternative-connectors) ## Installation @@ -164,30 +164,10 @@ To contact `go-tarantool` developers on any problems, create an issue at The developers of the [Tarantool server](http://github.com/tarantool/tarantool) will also be happy to provide advice or receive feedback. -## Tests +## Contributing -You need to [install Tarantool](https://www.tarantool.io/en/download/) to run tests. -See [Installation](#installation) section for requirements. - -To install test dependencies (like [tarantool/queue](https://github.com/tarantool/queue) module), run -```bash -make deps -``` - -To run tests for the main package and each subpackage, call -```bash -make test -``` -Tests set up all required `tarantool` processes before run and clean up after. - -If you want to run a specific package tests, call -```bash -make test- -``` -For example, for running tests in `multi`, `uuid` and `main` packages, call -```bash -make test-multi test-uuid test-main -``` +See [the contributing guide](CONTRIBUTING.md) for detailed instructions on how +to get started with our project. ## Alternative connectors From 823a408aa3d0fd1d52b11880e547716d2083db87 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 6 Apr 2022 18:05:12 +0300 Subject: [PATCH 256/605] contributing: describe first steps Part of #123 --- CONTRIBUTING.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac9ba94da..0d8423689 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,15 @@ # Contribution Guide +## First steps + +Clone the repository and install dependencies. + +```sh +$ git clone git@github.com:/tarantool/go-tarantool +$ cd go-tarantool +$ go get . +``` + ## Running tests You need to [install Tarantool](https://tarantool.io/en/download/) to run tests. From 7706f0365c4ba96f6f57172106d4c20b2cbe509e Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 31 Mar 2022 12:02:05 +0300 Subject: [PATCH 257/605] readme: update section about API reference Part of #123 --- README.md | 41 +++++++++++------------------------------ 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 33d64cfeb..d97f7b2e5 100644 --- a/README.md +++ b/README.md @@ -66,39 +66,18 @@ database.

API reference

-Read the [Tarantool manual](http://tarantool.org/doc.html) to find descriptions -of terms like "connect", "space", "index", and the requests for creating and -manipulating database objects or Lua functions. +Read the [Tarantool documentation](tarantool-doc-data-model-url) +to find descriptions of terms such as "connect", "space", "index", and the +requests to create and manipulate database objects or Lua functions. -The source files for the requests library are: -* [connection.go](https://github.com/tarantool/go-tarantool/blob/master/connection.go) - for the `Connect()` function plus functions related to connecting, and -* [request.go](https://github.com/tarantool/go-tarantool/blob/master/request.go) - for data-manipulation functions and Lua invocations. +In general, connector methods can be divided into two main parts: -See comments in those files for syntax details: -``` -Ping -closeConnection -Select -Insert -Replace -Delete -Update -Upsert -Call -Call17 -Eval -``` - -The supported requests have parameters and results equivalent to requests in the -Tarantool manual. There are also Typed and Async versions of each data-manipulation -function. +* `Connect()` function and functions related to connecting, and +* Data manipulation functions and Lua invocations such as `Insert()` or `Call()`. -The source file for error-handling tools is -[errors.go](https://github.com/tarantool/go-tarantool/blob/master/errors.go), -which has structure definitions and constants whose names are equivalent to names -of errors that the Tarantool server returns. +The supported requests have parameters and results equivalent to requests in +the [Tarantool CRUD operations](tarantool-doc-box-space-url). +There are also Typed and Async versions of each data-manipulation function. ## Walking-through example in Go @@ -192,3 +171,5 @@ See feature comparison in [documentation](https://www.tarantool.io/en/doc/latest [stackoverflow-url]: https://stackoverflow.com/questions/tagged/tarantool [golang-dl]: https://go.dev/dl/ [go-tarantool]: https://github.com/tarantool/go-tarantool +[tarantool-doc-data-model-url]: https://www.tarantool.io/en/doc/latest/book/box/data_model/ +[tarantool-doc-box-space-url]: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/ From 9845962fe778be44acf74c32cca50862c16fdeaf Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 14 Apr 2022 16:47:02 +0300 Subject: [PATCH 258/605] readme: remove hello world example We have a walking through example in a README and examples in GoDoc. Seems it is more than enough. Part of #123 --- README.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/README.md b/README.md index d97f7b2e5..63de8be4f 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@ faster than other packages according to public benchmarks. ## Table of contents * [Installation](#installation) -* [Hello World](#hello-world) * [API reference](#api-reference) * [Walking\-through example in Go](#walking-through-example-in-go) * [Help](#help) @@ -52,18 +51,6 @@ This should put the source and binary files in subdirectories of `github.com/tarantool/go-tarantool` to the `import {...}` section at the start of any Go program. -

Hello World

- -In the "[Connectors](https://www.tarantool.io/en/doc/latest/getting_started/getting_started_go/)" -chapter of the Tarantool manual, there is an explanation of a very short (18-line) -program written in Go. Follow the instructions at the start of the "Connectors" -chapter carefully. Then cut and paste the example into a file named `example.go`, -and run it. You should see: nothing. - -If that is what you see, then you have successfully installed `go-tarantool` and -successfully executed a program that manipulated the contents of a Tarantool -database. -

API reference

Read the [Tarantool documentation](tarantool-doc-data-model-url) From d206a8503fd404867d8c4df406524369af0a820b Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 14 Apr 2022 16:58:25 +0300 Subject: [PATCH 259/605] readme: remove help section Part of #123 --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index 63de8be4f..9fafc2212 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ faster than other packages according to public benchmarks. * [Installation](#installation) * [API reference](#api-reference) * [Walking\-through example in Go](#walking-through-example-in-go) -* [Help](#help) * [Contributing](#contributing) * [Alternative connectors](#alternative-connectors) @@ -122,14 +121,6 @@ There are two parameters: * a space number (it could just as easily have been a space name), and * a tuple. -## Help - -To contact `go-tarantool` developers on any problems, create an issue at -[tarantool/go-tarantool](http://github.com/tarantool/go-tarantool/issues). - -The developers of the [Tarantool server](http://github.com/tarantool/tarantool) -will also be happy to provide advice or receive feedback. - ## Contributing See [the contributing guide](CONTRIBUTING.md) for detailed instructions on how From 253cb27fa2eef9cd40c8004c7022cd3deb7d3142 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 7 Apr 2022 11:40:22 +0300 Subject: [PATCH 260/605] readme: merge documentation sections into single one - add a section 'Documentation' and two subsections: - with 'Walking Through' example - with API documentation - fix code formatting in 'Walking-Through' example - remove description of Opts settings in description for 'Walking-Through' example - remove a program name in description for 'Walking-Through' example - update English grammar in description for 'Walking Through' example Part of #123 --- README.md | 66 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 9fafc2212..4e5f22334 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,9 @@ faster than other packages according to public benchmarks. ## Table of contents * [Installation](#installation) -* [API reference](#api-reference) -* [Walking\-through example in Go](#walking-through-example-in-go) +* [Documentation](#documentation) + * [API reference](#api-reference) + * [Walking\-through example](#walking-through-example) * [Contributing](#contributing) * [Alternative connectors](#alternative-connectors) @@ -50,7 +51,7 @@ This should put the source and binary files in subdirectories of `github.com/tarantool/go-tarantool` to the `import {...}` section at the start of any Go program. -

API reference

+## Documentation Read the [Tarantool documentation](tarantool-doc-data-model-url) to find descriptions of terms such as "connect", "space", "index", and the @@ -65,56 +66,56 @@ The supported requests have parameters and results equivalent to requests in the [Tarantool CRUD operations](tarantool-doc-box-space-url). There are also Typed and Async versions of each data-manipulation function. -## Walking-through example in Go +### API Reference -We can now have a closer look at the `example.go` program and make some observations +Learn API documentation and examples at +[pkg.go.dev](https://pkg.go.dev/github.com/tarantool/go-tarantool). + +### Walking-through example + +We can now have a closer look at the example and make some observations about what it does. ```go -package main +package tarantool import ( - "fmt" - "github.com/tarantool/go-tarantool" + "fmt" + "github.com/tarantool/go-tarantool" ) func main() { - opts := tarantool.Opts{User: "guest"} - conn, err := tarantool.Connect("127.0.0.1:3301", opts) - // conn, err := tarantool.Connect("/path/to/tarantool.socket", opts) - if err != nil { - fmt.Println("Connection refused:", err) - } - resp, err := conn.Insert(999, []interface{}{99999, "BB"}) - if err != nil { - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - } + opts := tarantool.Opts{User: "guest"} + conn, err := tarantool.Connect("127.0.0.1:3301", opts) + if err != nil { + fmt.Println("Connection refused:", err) + } + resp, err := conn.Insert(999, []interface{}{99999, "BB"}) + if err != nil { + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + } } ``` -**Observation 1:** the line "`github.com/tarantool/go-tarantool`" in the +**Observation 1:** The line "`github.com/tarantool/go-tarantool`" in the `import(...)` section brings in all Tarantool-related functions and structures. -**Observation 2:** the line beginning with "`Opts :=`" sets up the options for -`Connect()`. In this example, there is only one thing in the structure, a user -name. The structure can also contain: - -* `Pass` (password), -* `Timeout` (maximum number of milliseconds to wait before giving up), -* `Reconnect` (number of seconds to wait before retrying if a connection fails), -* `MaxReconnect` (maximum number of times to retry). +**Observation 2:** The line starting with "`Opts :=`" sets up the options for +`Connect()`. In this example, the structure contains only a single value, the +username. The structure may also contain other settings, see more in +[documentation][godoc-opts-url] for the "`Opts`" structure. -**Observation 3:** the line containing "`tarantool.Connect`" is essential for -beginning any session. There are two parameters: +**Observation 3:** The line containing "`tarantool.Connect`" is essential for +starting a session. There are two parameters: * a string with `host:port` format, and * the option structure that was set up earlier. -**Observation 4:** the `err` structure will be `nil` if there is no error, +**Observation 4:** The `err` structure will be `nil` if there is no error, otherwise it will have a description which can be retrieved with `err.Error()`. -**Observation 5:** the `Insert` request, like almost all requests, is preceded by +**Observation 5:** The `Insert` request, like almost all requests, is preceded by "`conn.`" which is the name of the object that was returned by `Connect()`. There are two parameters: @@ -151,3 +152,4 @@ See feature comparison in [documentation](https://www.tarantool.io/en/doc/latest [go-tarantool]: https://github.com/tarantool/go-tarantool [tarantool-doc-data-model-url]: https://www.tarantool.io/en/doc/latest/book/box/data_model/ [tarantool-doc-box-space-url]: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/ +[godoc-opts-url]: https://pkg.go.dev/github.com/tarantool/go-tarantool#Opts From 6e9f65ea76ec6d39474dc8fbed5c10665dcf2204 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 20 Apr 2022 10:37:24 +0300 Subject: [PATCH 261/605] readme: update section about alternative connectors Part of #123 --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4e5f22334..92d760315 100644 --- a/README.md +++ b/README.md @@ -129,11 +129,12 @@ to get started with our project. ## Alternative connectors -There are two more connectors from the open-source community available: +There are two other connectors available from the open source community: + * [viciious/go-tarantool](https://github.com/viciious/go-tarantool), * [FZambia/tarantool](https://github.com/FZambia/tarantool). -See feature comparison in [documentation](https://www.tarantool.io/en/doc/latest/book/connectors/#go-feature-comparison). +See feature comparison in the [documentation][tarantool-doc-connectors-comparison]. [tarantool-site]: https://tarantool.io/ [godoc-badge]: https://pkg.go.dev/badge/github.com/tarantool/go-tarantool.svg @@ -153,3 +154,4 @@ See feature comparison in [documentation](https://www.tarantool.io/en/doc/latest [tarantool-doc-data-model-url]: https://www.tarantool.io/en/doc/latest/book/box/data_model/ [tarantool-doc-box-space-url]: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/ [godoc-opts-url]: https://pkg.go.dev/github.com/tarantool/go-tarantool#Opts +[tarantool-doc-connectors-comparison]: https://www.tarantool.io/en/doc/latest/book/connectors/#go-feature-comparison From fa856fc7cc42d00574d7e55f132ab826f109c9f6 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Tue, 12 Apr 2022 16:08:14 +0300 Subject: [PATCH 262/605] changelog: add entry about updated documentation Closes #123 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7385dd47c..f0cc0b40a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Handle everything with `go test` (#115) - Use plain package instead of module for UUID submodule (#134) - Reset buffer if its average use size smaller than quater of capacity (#95) +- Update API documentation: comments and examples (#123). ## [1.5] - 2019-12-29 From 1d06618833d3af2f1068fc9c11c6e2015f4ded47 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 14 Apr 2022 16:43:59 +0300 Subject: [PATCH 263/605] license: bump copyright year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 9295b5220..4ec1f2574 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 2-Clause License -Copyright (c) 2014-2017, Tarantool AUTHORS +Copyright (c) 2014-2022, Tarantool AUTHORS Copyright (c) 2014-2017, Dmitry Smal Copyright (c) 2014-2017, Yura Sokolov aka funny_falcon All rights reserved. From dd4f88fb2eb8010f4f7912df87adafd9abda63f6 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Thu, 21 Apr 2022 16:50:29 +0300 Subject: [PATCH 264/605] readme: make Go versions consistent After PR #151 Go requirement in README was bumped to version 1.13. But README still contains a line describing version 1.3. This patch fixes it. Follows up #123 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92d760315..83f1c601c 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ operating system. You need a current version of `go`, version 1.13 or later (use `go version` to check the version number). Do not use `gccgo-go`. -**Note:** If your `go` version is older than 1.3 or if `go` is not installed, +**Note:** If your `go` version is older than 1.13 or if `go` is not installed, download and run the latest tarball from [golang.org][golang-dl]. The package `go-tarantool` is located in [tarantool/go-tarantool][go-tarantool] From dff20b3258b98a85a6a3504dfefc331323bc96b1 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 21 Apr 2022 09:33:14 +0300 Subject: [PATCH 265/605] readme: fix broken links Follows up #123 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 83f1c601c..6e1d295fa 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ of any Go program. ## Documentation -Read the [Tarantool documentation](tarantool-doc-data-model-url) +Read the [Tarantool documentation][tarantool-doc-data-model-url] to find descriptions of terms such as "connect", "space", "index", and the requests to create and manipulate database objects or Lua functions. @@ -63,7 +63,7 @@ In general, connector methods can be divided into two main parts: * Data manipulation functions and Lua invocations such as `Insert()` or `Call()`. The supported requests have parameters and results equivalent to requests in -the [Tarantool CRUD operations](tarantool-doc-box-space-url). +the [Tarantool CRUD operations][tarantool-doc-box-space-url]. There are also Typed and Async versions of each data-manipulation function. ### API Reference From cb9f156b8fa5dc56b28ca3f83b7f360bf8f93d0f Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 21 Apr 2022 09:33:54 +0300 Subject: [PATCH 266/605] readme: use an existed link shortcut to godoc Follows up #123 --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 6e1d295fa..82c886720 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,7 @@ There are also Typed and Async versions of each data-manipulation function. ### API Reference -Learn API documentation and examples at -[pkg.go.dev](https://pkg.go.dev/github.com/tarantool/go-tarantool). +Learn API documentation and examples at [pkg.go.dev][godoc-url]. ### Walking-through example From f388f6ddda9253d298f8c42d305cbcb4c0d1be63 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 20 Apr 2022 15:29:49 +0300 Subject: [PATCH 267/605] contributing: add code review checklist --- CONTRIBUTING.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d8423689..05602f23e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,3 +37,32 @@ For example, for running tests in `multi`, `uuid` and `main` packages, call ```bash make test-multi test-uuid test-main ``` + +## Code review checklist + +- Public API contains functions, variables, constants that are needed from + outside by users. All the rest should be left closed. +- Public functions, variables and constants contain at least a single-line + comment. +- Code is DRY (see "Do not Repeat Yourself" principle). +- New features have functional and probably performance tests. +- There are no changes in files not related to the issue. +- There are no obvious flaky tests. +- Commits with bugfixes have tests based on reproducers. +- Changelog entry is present in `CHANGELOG.md`. +- Public methods contain executable examples (contains a comment with + reference output). +- Autogenerated documentation looks good. Run `godoc -http=:6060` and point + your web browser to address "http://127.0.0.1:6060/" for evaluating. +- Commit message header may start with a prefix with a short description + follows after colon. It is applicable to changes in a README, examples, tests + and CI configuration files. Examples: `github-ci: add Tarantool 2.x-latest` + and `readme: describe how to run tests`. +- Check your comments, commit title, and even variable names to be + grammatically correct. Start sentences from a capital letter, end with a dot. + Everywhere - in the code, in the tests, in the commit message. + +See also: + +- https://github.com/tarantool/tarantool/wiki/Code-review-procedure +- https://www.tarantool.io/en/doc/latest/contributing/ From e9b9ba1ed26aa2c429397d0cd32602abad42497e Mon Sep 17 00:00:00 2001 From: AnaNek Date: Wed, 9 Mar 2022 13:07:32 +0300 Subject: [PATCH 268/605] connection-pool: implement connection pool with master discovery Main features: - Return available connection from pool according to round-robin strategy. - Automatic master discovery by `mode` parameter. Additional options (configurable via `ConnectWithOpts`): * `CheckTimeout` - time interval to check for connection timeout and try to switch connection `Mode` parameter: * `ANY` (use any instance) - the request can be executed on any instance (master or replica). * `RW` (writeable instance (master)) - the request can only be executed on master. * `RO` (read only instance (replica)) - the request can only be executed on replica. * `PREFER_RO` (prefer read only instance (replica)) - if there is one, otherwise fallback to a writeable one (master). * `PREFER_RW` (prefer write only instance (master)) - if there is one, otherwise fallback to a read only one (replica). Closes #113 --- CHANGELOG.md | 1 + Makefile | 6 + connection_pool/config.lua | 35 + connection_pool/connection_pool.go | 732 +++++++++++++ connection_pool/connection_pool_test.go | 1293 +++++++++++++++++++++++ connection_pool/const.go | 52 + connection_pool/example_test.go | 531 ++++++++++ connection_pool/round_robin.go | 117 ++ go.mod | 1 + go.sum | 10 + request.go | 5 + test_helpers/main.go | 24 +- test_helpers/pool_helper.go | 248 +++++ 13 files changed, 3050 insertions(+), 5 deletions(-) create mode 100644 connection_pool/config.lua create mode 100644 connection_pool/connection_pool.go create mode 100644 connection_pool/connection_pool_test.go create mode 100644 connection_pool/const.go create mode 100644 connection_pool/example_test.go create mode 100644 connection_pool/round_robin.go create mode 100644 test_helpers/pool_helper.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f0cc0b40a..6168ede00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support UUID type in msgpack (#90) - Go modules support (#91) - queue-utube handling (#85) +- Master discovery (#113) ### Fixed diff --git a/Makefile b/Makefile index 5af3358d0..ed94ae137 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,12 @@ deps: clean test: go test ./... -v -p 1 +.PHONY: test-connection-pool +test-connection-pool: + @echo "Running tests in connection_pool package" + go clean -testcache + go test ./connection_pool/ -v -p 1 + .PHONY: test-multi test-multi: @echo "Running tests in multiconnection package" diff --git a/connection_pool/config.lua b/connection_pool/config.lua new file mode 100644 index 000000000..b1492dd13 --- /dev/null +++ b/connection_pool/config.lua @@ -0,0 +1,35 @@ +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +box.cfg{ + work_dir = os.getenv("TEST_TNT_WORK_DIR"), +} + +box.once("init", function() + box.schema.user.create('test', { password = 'test' }) + box.schema.user.grant('test', 'read,write,execute', 'universe') + + local s = box.schema.space.create('testPool', { + id = 520, + if_not_exists = true, + format = { + {name = "key", type = "string"}, + {name = "value", type = "string"}, + }, + }) + s:create_index('pk', { + type = 'tree', + parts = {{ field = 1, type = 'string' }}, + if_not_exists = true + }) +end) + +local function simple_incr(a) + return a + 1 +end + +rawset(_G, 'simple_incr', simple_incr) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go new file mode 100644 index 000000000..d4a343911 --- /dev/null +++ b/connection_pool/connection_pool.go @@ -0,0 +1,732 @@ +// Package with methods to work with a Tarantool cluster +// considering master discovery. +// +// Main features: +// +// - Return available connection from pool according to round-robin strategy. +// +// - Automatic master discovery by mode parameter. +// +// Since: 1.6.0 +package connection_pool + +import ( + "errors" + "log" + "sync/atomic" + "time" + + "github.com/tarantool/go-tarantool" +) + +var ( + ErrEmptyAddrs = errors.New("addrs (first argument) should not be empty") + ErrWrongCheckTimeout = errors.New("wrong check timeout, must be greater than 0") + ErrNoConnection = errors.New("no active connections") + ErrTooManyArgs = errors.New("too many arguments") + ErrIncorrectResponse = errors.New("Incorrect response format") + ErrIncorrectStatus = errors.New("Incorrect instance status: status should be `running`") + ErrNoRwInstance = errors.New("Can't find rw instance in pool") + ErrNoRoInstance = errors.New("Can't find ro instance in pool") + ErrNoHealthyInstance = errors.New("Can't find healthy instance in pool") +) + +/* +Additional options (configurable via ConnectWithOpts): + +- CheckTimeout - time interval to check for connection timeout and try to switch connection. +*/ +type OptsPool struct { + // timeout for timer to reopen connections + // that have been closed by some events and + // to relocate connection between subpools + // if ro/rw role has been updated + CheckTimeout time.Duration +} + +/* +ConnectionInfo structure for information about connection statuses: + +- ConnectedNow reports if connection is established at the moment. + +- ConnRole reports master/replica role of instance. +*/ +type ConnectionInfo struct { + ConnectedNow bool + ConnRole Role +} + +/* +Main features: + +- Return available connection from pool according to round-robin strategy. + +- Automatic master discovery by mode parameter. +*/ +type ConnectionPool struct { + addrs []string + connOpts tarantool.Opts + opts OptsPool + + notify chan tarantool.ConnEvent + state State + control chan struct{} + roPool *RoundRobinStrategy + rwPool *RoundRobinStrategy + anyPool *RoundRobinStrategy +} + +// ConnectWithOpts creates pool for instances with addresses addrs +// with options opts. +func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (connPool *ConnectionPool, err error) { + if len(addrs) == 0 { + return nil, ErrEmptyAddrs + } + if opts.CheckTimeout <= 0 { + return nil, ErrWrongCheckTimeout + } + + notify := make(chan tarantool.ConnEvent, 10*len(addrs)) // x10 to accept disconnected and closed event (with a margin) + connOpts.Notify = notify + + size := len(addrs) + rwPool := NewEmptyRoundRobin(size) + roPool := NewEmptyRoundRobin(size) + anyPool := NewEmptyRoundRobin(size) + + connPool = &ConnectionPool{ + addrs: addrs, + connOpts: connOpts, + opts: opts, + notify: notify, + control: make(chan struct{}), + rwPool: rwPool, + roPool: roPool, + anyPool: anyPool, + } + + somebodyAlive := connPool.fillPools() + if !somebodyAlive { + connPool.Close() + return nil, ErrNoConnection + } + + go connPool.checker() + + return connPool, nil +} + +// ConnectWithOpts creates pool for instances with addresses addrs. +func Connect(addrs []string, connOpts tarantool.Opts) (connPool *ConnectionPool, err error) { + opts := OptsPool{ + CheckTimeout: 1 * time.Second, + } + return ConnectWithOpts(addrs, connOpts, opts) +} + +// ConnectedNow gets connected status of pool. +func (connPool *ConnectionPool) ConnectedNow(mode Mode) (bool, error) { + if connPool.getState() != connConnected { + return false, nil + } + + conn, err := connPool.getNextConnection(mode) + if err != nil || conn == nil { + return false, err + } + + return conn.ConnectedNow(), nil +} + +// ConfiguredTimeout gets timeout of current connection. +func (connPool *ConnectionPool) ConfiguredTimeout(mode Mode) (time.Duration, error) { + conn, err := connPool.getNextConnection(mode) + if err != nil { + return 0, err + } + + return conn.ConfiguredTimeout(), nil +} + +// Close closes connections in pool. +func (connPool *ConnectionPool) Close() []error { + close(connPool.control) + connPool.state = connClosed + + rwErrs := connPool.rwPool.CloseConns() + roErrs := connPool.roPool.CloseConns() + + allErrs := append(rwErrs, roErrs...) + + return allErrs +} + +// GetAddrs gets addresses of connections in pool. +func (connPool *ConnectionPool) GetAddrs() []string { + return connPool.addrs +} + +// GetPoolInfo gets information of connections (connected status, ro/rw role). +func (connPool *ConnectionPool) GetPoolInfo() map[string]*ConnectionInfo { + info := make(map[string]*ConnectionInfo) + + for _, addr := range connPool.addrs { + conn, role := connPool.getConnectionFromPool(addr) + if conn != nil { + info[addr] = &ConnectionInfo{ConnectedNow: conn.ConnectedNow(), ConnRole: role} + } + } + + return info +} + +// Ping sends empty request to Tarantool to check connection. +func (connPool *ConnectionPool) Ping(userMode Mode) (*tarantool.Response, error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return nil, err + } + + return conn.Ping() +} + +// Select performs select to box space. +func (connPool *ConnectionPool) Select(space, index interface{}, offset, limit, iterator uint32, key interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { + conn, err := connPool.getConnByMode(ANY, userMode) + if err != nil { + return nil, err + } + + return conn.Select(space, index, offset, limit, iterator, key) +} + +// Insert performs insertion to box space. +// Tarantool will reject Insert when tuple with same primary key exists. +func (connPool *ConnectionPool) Insert(space interface{}, tuple interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return nil, err + } + + return conn.Insert(space, tuple) +} + +// Replace performs "insert or replace" action to box space. +// If tuple with same primary key exists, it will be replaced. +func (connPool *ConnectionPool) Replace(space interface{}, tuple interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return nil, err + } + + return conn.Replace(space, tuple) +} + +// Delete performs deletion of a tuple by key. +// Result will contain array with deleted tuple. +func (connPool *ConnectionPool) Delete(space, index interface{}, key interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return nil, err + } + + return conn.Delete(space, index, key) +} + +// Update performs update of a tuple by key. +// Result will contain array with updated tuple. +func (connPool *ConnectionPool) Update(space, index interface{}, key, ops interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return nil, err + } + + return conn.Update(space, index, key, ops) +} + +// Upsert performs "update or insert" action of a tuple by key. +// Result will not contain any tuple. +func (connPool *ConnectionPool) Upsert(space interface{}, tuple, ops interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return nil, err + } + + return conn.Upsert(space, tuple, ops) +} + +// Call calls registered Tarantool function. +// It uses request code for Tarantool 1.6, so result is converted to array of arrays. +func (connPool *ConnectionPool) Call(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return nil, err + } + + return conn.Call(functionName, args) +} + +// Call17 calls registered Tarantool function. +// It uses request code for Tarantool 1.7, so result is not converted +// (though, keep in mind, result is always array). +func (connPool *ConnectionPool) Call17(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return nil, err + } + + return conn.Call17(functionName, args) +} + +// Eval passes lua expression for evaluation. +func (connPool *ConnectionPool) Eval(expr string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return nil, err + } + + return conn.Eval(expr, args) +} + +// GetTyped performs select (with limit = 1 and offset = 0) +// to box space and fills typed result. +func (connPool *ConnectionPool) GetTyped(space, index interface{}, key interface{}, result interface{}, userMode ...Mode) (err error) { + conn, err := connPool.getConnByMode(ANY, userMode) + if err != nil { + return err + } + + return conn.GetTyped(space, index, key, result) +} + +// SelectTyped performs select to box space and fills typed result. +func (connPool *ConnectionPool) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key interface{}, result interface{}, userMode ...Mode) (err error) { + conn, err := connPool.getConnByMode(ANY, userMode) + if err != nil { + return err + } + + return conn.SelectTyped(space, index, offset, limit, iterator, key, result) +} + +// InsertTyped performs insertion to box space. +// Tarantool will reject Insert when tuple with same primary key exists. +func (connPool *ConnectionPool) InsertTyped(space interface{}, tuple interface{}, result interface{}, userMode ...Mode) (err error) { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return err + } + + return conn.InsertTyped(space, tuple, result) +} + +// ReplaceTyped performs "insert or replace" action to box space. +// If tuple with same primary key exists, it will be replaced. +func (connPool *ConnectionPool) ReplaceTyped(space interface{}, tuple interface{}, result interface{}, userMode ...Mode) (err error) { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return err + } + + return conn.ReplaceTyped(space, tuple, result) +} + +// DeleteTyped performs deletion of a tuple by key and fills result with deleted tuple. +func (connPool *ConnectionPool) DeleteTyped(space, index interface{}, key interface{}, result interface{}, userMode ...Mode) (err error) { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return err + } + + return conn.DeleteTyped(space, index, key, result) +} + +// UpdateTyped performs update of a tuple by key and fills result with updated tuple. +func (connPool *ConnectionPool) UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}, userMode ...Mode) (err error) { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return err + } + + return conn.UpdateTyped(space, index, key, ops, result) +} + +// CallTyped calls registered function. +// It uses request code for Tarantool 1.6, so result is converted to array of arrays. +func (connPool *ConnectionPool) CallTyped(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return err + } + + return conn.CallTyped(functionName, args, result) +} + +// Call17Typed calls registered function. +// It uses request code for Tarantool 1.7, so result is not converted +// (though, keep in mind, result is always array). +func (connPool *ConnectionPool) Call17Typed(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return err + } + + return conn.Call17Typed(functionName, args, result) +} + +// EvalTyped passes lua expression for evaluation. +func (connPool *ConnectionPool) EvalTyped(expr string, args interface{}, result interface{}, userMode Mode) (err error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return err + } + + return conn.EvalTyped(expr, args, result) +} + +// SelectAsync sends select request to Tarantool and returns Future. +func (connPool *ConnectionPool) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}, userMode ...Mode) *tarantool.Future { + conn, err := connPool.getConnByMode(ANY, userMode) + if err != nil { + return tarantool.NewErrorFuture(err) + } + + return conn.SelectAsync(space, index, offset, limit, iterator, key) +} + +// InsertAsync sends insert action to Tarantool and returns Future. +// Tarantool will reject Insert when tuple with same primary key exists. +func (connPool *ConnectionPool) InsertAsync(space interface{}, tuple interface{}, userMode ...Mode) *tarantool.Future { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return tarantool.NewErrorFuture(err) + } + + return conn.InsertAsync(space, tuple) +} + +// ReplaceAsync sends "insert or replace" action to Tarantool and returns Future. +// If tuple with same primary key exists, it will be replaced. +func (connPool *ConnectionPool) ReplaceAsync(space interface{}, tuple interface{}, userMode ...Mode) *tarantool.Future { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return tarantool.NewErrorFuture(err) + } + + return conn.ReplaceAsync(space, tuple) +} + +// DeleteAsync sends deletion action to Tarantool and returns Future. +// Future's result will contain array with deleted tuple. +func (connPool *ConnectionPool) DeleteAsync(space, index interface{}, key interface{}, userMode ...Mode) *tarantool.Future { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return tarantool.NewErrorFuture(err) + } + + return conn.DeleteAsync(space, index, key) +} + +// UpdateAsync sends deletion of a tuple by key and returns Future. +// Future's result will contain array with updated tuple. +func (connPool *ConnectionPool) UpdateAsync(space, index interface{}, key, ops interface{}, userMode ...Mode) *tarantool.Future { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return tarantool.NewErrorFuture(err) + } + + return conn.UpdateAsync(space, index, key, ops) +} + +// UpsertAsync sends "update or insert" action to Tarantool and returns Future. +// Future's sesult will not contain any tuple. +func (connPool *ConnectionPool) UpsertAsync(space interface{}, tuple interface{}, ops interface{}, userMode ...Mode) *tarantool.Future { + conn, err := connPool.getConnByMode(RW, userMode) + if err != nil { + return tarantool.NewErrorFuture(err) + } + + return conn.UpsertAsync(space, tuple, ops) +} + +// CallAsync sends a call to registered Tarantool function and returns Future. +// It uses request code for Tarantool 1.6, so future's result is always array of arrays. +func (connPool *ConnectionPool) CallAsync(functionName string, args interface{}, userMode Mode) *tarantool.Future { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return tarantool.NewErrorFuture(err) + } + + return conn.CallAsync(functionName, args) +} + +// Call17Async sends a call to registered Tarantool function and returns Future. +// It uses request code for Tarantool 1.7, so future's result will not be converted +// (though, keep in mind, result is always array). +func (connPool *ConnectionPool) Call17Async(functionName string, args interface{}, userMode Mode) *tarantool.Future { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return tarantool.NewErrorFuture(err) + } + + return conn.Call17Async(functionName, args) +} + +// EvalAsync sends a lua expression for evaluation and returns Future. +func (connPool *ConnectionPool) EvalAsync(expr string, args interface{}, userMode Mode) *tarantool.Future { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return tarantool.NewErrorFuture(err) + } + + return conn.EvalAsync(expr, args) +} + +// +// private +// + +func (connPool *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (Role, error) { + resp, err := conn.Call17("box.info", []interface{}{}) + if err != nil { + return unknown, err + } + if resp == nil { + return unknown, ErrIncorrectResponse + } + if len(resp.Data) < 1 { + return unknown, ErrIncorrectResponse + } + + instanceStatus, ok := resp.Data[0].(map[interface{}]interface{})["status"] + if !ok { + return unknown, ErrIncorrectResponse + } + if instanceStatus != "running" { + return unknown, ErrIncorrectStatus + } + + resp, err = conn.Call17("box.info", []interface{}{}) + if err != nil { + return unknown, err + } + if resp == nil { + return unknown, ErrIncorrectResponse + } + if len(resp.Data) < 1 { + return unknown, ErrIncorrectResponse + } + + replicaRole, ok := resp.Data[0].(map[interface{}]interface{})["ro"] + if !ok { + return unknown, ErrIncorrectResponse + } + + switch replicaRole { + case false: + return master, nil + case true: + return replica, nil + } + + return unknown, nil +} + +func (connPool *ConnectionPool) getConnectionFromPool(addr string) (*tarantool.Connection, Role) { + conn := connPool.rwPool.GetConnByAddr(addr) + if conn != nil { + return conn, master + } + + conn = connPool.roPool.GetConnByAddr(addr) + if conn != nil { + return conn, replica + } + + return connPool.anyPool.GetConnByAddr(addr), unknown +} + +func (connPool *ConnectionPool) deleteConnectionFromPool(addr string) { + _ = connPool.anyPool.DeleteConnByAddr(addr) + conn := connPool.rwPool.DeleteConnByAddr(addr) + if conn != nil { + return + } + + connPool.roPool.DeleteConnByAddr(addr) +} + +func (connPool *ConnectionPool) setConnectionToPool(addr string, conn *tarantool.Connection) error { + role, err := connPool.getConnectionRole(conn) + if err != nil { + return err + } + + connPool.anyPool.AddConn(addr, conn) + + switch role { + case master: + connPool.rwPool.AddConn(addr, conn) + case replica: + connPool.roPool.AddConn(addr, conn) + } + + return nil +} + +func (connPool *ConnectionPool) refreshConnection(addr string) { + if conn, oldRole := connPool.getConnectionFromPool(addr); conn != nil { + if !conn.ClosedNow() { + curRole, _ := connPool.getConnectionRole(conn) + if oldRole != curRole { + connPool.deleteConnectionFromPool(addr) + err := connPool.setConnectionToPool(addr, conn) + if err != nil { + conn.Close() + log.Printf("tarantool: storing connection to %s failed: %s\n", addr, err.Error()) + } + } + } + } else { + conn, _ := tarantool.Connect(addr, connPool.connOpts) + if conn != nil { + err := connPool.setConnectionToPool(addr, conn) + if err != nil { + conn.Close() + log.Printf("tarantool: storing connection to %s failed: %s\n", addr, err.Error()) + } + } + } +} + +func (connPool *ConnectionPool) checker() { + + timer := time.NewTicker(connPool.opts.CheckTimeout) + defer timer.Stop() + + for connPool.getState() != connClosed { + select { + case <-connPool.control: + return + case e := <-connPool.notify: + if connPool.getState() == connClosed { + return + } + if e.Conn.ClosedNow() { + addr := e.Conn.Addr() + if conn, _ := connPool.getConnectionFromPool(addr); conn == nil { + continue + } + conn, _ := tarantool.Connect(addr, connPool.connOpts) + if conn != nil { + err := connPool.setConnectionToPool(addr, conn) + if err != nil { + conn.Close() + log.Printf("tarantool: storing connection to %s failed: %s\n", addr, err.Error()) + } + } else { + connPool.deleteConnectionFromPool(addr) + } + } + case <-timer.C: + for _, addr := range connPool.addrs { + if connPool.getState() == connClosed { + return + } + + // Reopen connection + // Relocate connection between subpools + // if ro/rw was updated + connPool.refreshConnection(addr) + } + } + } +} + +func (connPool *ConnectionPool) fillPools() bool { + somebodyAlive := false + + for _, addr := range connPool.addrs { + conn, err := tarantool.Connect(addr, connPool.connOpts) + if err != nil { + log.Printf("tarantool: connect to %s failed: %s\n", addr, err.Error()) + } else if conn != nil { + err = connPool.setConnectionToPool(addr, conn) + if err != nil { + conn.Close() + log.Printf("tarantool: storing connection to %s failed: %s\n", addr, err.Error()) + } else if conn.ConnectedNow() { + somebodyAlive = true + } + } + } + + return somebodyAlive +} + +func (connPool *ConnectionPool) getState() uint32 { + return atomic.LoadUint32((*uint32)(&connPool.state)) +} + +func (connPool *ConnectionPool) getNextConnection(mode Mode) (*tarantool.Connection, error) { + + switch mode { + case ANY: + if connPool.anyPool.IsEmpty() { + return nil, ErrNoHealthyInstance + } + + return connPool.anyPool.GetNextConnection(), nil + + case RW: + if connPool.rwPool.IsEmpty() { + return nil, ErrNoRwInstance + } + + return connPool.rwPool.GetNextConnection(), nil + + case RO: + if connPool.roPool.IsEmpty() { + return nil, ErrNoRoInstance + } + + return connPool.roPool.GetNextConnection(), nil + + case PreferRW: + if !connPool.rwPool.IsEmpty() { + return connPool.rwPool.GetNextConnection(), nil + } + + if !connPool.roPool.IsEmpty() { + return connPool.roPool.GetNextConnection(), nil + } + + return nil, ErrNoHealthyInstance + + case PreferRO: + if !connPool.roPool.IsEmpty() { + return connPool.roPool.GetNextConnection(), nil + } + + if !connPool.rwPool.IsEmpty() { + return connPool.rwPool.GetNextConnection(), nil + } + + return nil, ErrNoHealthyInstance + } + + return nil, ErrNoHealthyInstance +} + +func (connPool *ConnectionPool) getConnByMode(defaultMode Mode, userMode []Mode) (*tarantool.Connection, error) { + if len(userMode) > 1 { + return nil, ErrTooManyArgs + } + + mode := defaultMode + if len(userMode) > 0 { + mode = userMode[0] + } + + return connPool.getNextConnection(mode) +} diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go new file mode 100644 index 000000000..0f0b0a6a6 --- /dev/null +++ b/connection_pool/connection_pool_test.go @@ -0,0 +1,1293 @@ +package connection_pool_test + +import ( + "log" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/connection_pool" + "github.com/tarantool/go-tarantool/test_helpers" +) + +var spaceNo = uint32(520) +var spaceName = "testPool" +var indexNo = uint32(0) +var indexName = "pk" + +var ports = []string{"3013", "3014", "3015", "3016", "3017"} +var host = "127.0.0.1" +var servers = []string{ + strings.Join([]string{host, ports[0]}, ":"), + strings.Join([]string{host, ports[1]}, ":"), + strings.Join([]string{host, ports[2]}, ":"), + strings.Join([]string{host, ports[3]}, ":"), + strings.Join([]string{host, ports[4]}, ":"), +} + +var connOpts = tarantool.Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +var defaultCountRetry = 5 +var defaultTimeoutRetry = 500 * time.Millisecond + +var instances []test_helpers.TarantoolInstance + +func TestConnError_IncorrectParams(t *testing.T) { + connPool, err := connection_pool.Connect([]string{}, tarantool.Opts{}) + require.Nilf(t, connPool, "conn is not nil with incorrect param") + require.NotNilf(t, err, "err is nil with incorrect params") + require.Equal(t, "addrs (first argument) should not be empty", err.Error()) + + connPool, err = connection_pool.Connect([]string{"err1", "err2"}, connOpts) + require.Nilf(t, connPool, "conn is not nil with incorrect param") + require.NotNilf(t, err, "err is nil with incorrect params") + require.Equal(t, "no active connections", err.Error()) + + connPool, err = connection_pool.ConnectWithOpts(servers, tarantool.Opts{}, connection_pool.OptsPool{}) + require.Nilf(t, connPool, "conn is not nil with incorrect param") + require.NotNilf(t, err, "err is nil with incorrect params") + require.Equal(t, "wrong check timeout, must be greater than 0", err.Error()) +} + +func TestConnSuccessfully(t *testing.T) { + server := servers[0] + connPool, err := connection_pool.Connect([]string{"err", server}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server}, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + server: true, + }, + } + + err = test_helpers.CheckPoolStatuses(args) + require.Nil(t, err) +} + +func TestReconnect(t *testing.T) { + server := servers[0] + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + test_helpers.StopTarantoolWithCleanup(instances[0]) + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server}, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + server: false, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) + + err = test_helpers.RestartTarantool(&instances[0]) + require.Nilf(t, err, "failed to restart tarantool") + + args = test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server}, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + server: true, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) +} + +func TestDisconnectAll(t *testing.T) { + server1 := servers[0] + server2 := servers[1] + + connPool, err := connection_pool.Connect([]string{server1, server2}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + test_helpers.StopTarantoolWithCleanup(instances[0]) + test_helpers.StopTarantoolWithCleanup(instances[1]) + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server1, server2}, + ExpectedPoolStatus: false, + ExpectedStatuses: map[string]bool{ + server1: false, + server2: false, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) + + err = test_helpers.RestartTarantool(&instances[0]) + require.Nilf(t, err, "failed to restart tarantool") + + err = test_helpers.RestartTarantool(&instances[1]) + require.Nilf(t, err, "failed to restart tarantool") + + args = test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server1, server2}, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + server1: true, + server2: true, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) +} + +func TestClose(t *testing.T) { + server1 := servers[0] + server2 := servers[1] + + connPool, err := connection_pool.Connect([]string{server1, server2}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server1, server2}, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + server1: true, + server2: true, + }, + } + + err = test_helpers.CheckPoolStatuses(args) + require.Nil(t, err) + + connPool.Close() + + args = test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server1, server2}, + ExpectedPoolStatus: false, + ExpectedStatuses: map[string]bool{ + server1: false, + server2: false, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) +} + +func TestCall(t *testing.T) { + roles := []bool{false, true, false, false, true} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // PreferRO + resp, err := connPool.Call17("box.info", []interface{}{}, connection_pool.PreferRO) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val := resp.Data[0].(map[interface{}]interface{})["ro"] + ro, ok := val.(bool) + require.Truef(t, ok, "expected `true` with mode `PreferRO`") + require.Truef(t, ro, "expected `true` with mode `PreferRO`") + + // PreferRW + resp, err = connPool.Call17("box.info", []interface{}{}, connection_pool.PreferRW) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `false` with mode `PreferRW`") + require.Falsef(t, ro, "expected `false` with mode `PreferRW`") + + // RO + resp, err = connPool.Call17("box.info", []interface{}{}, connection_pool.RO) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `true` with mode `RO`") + require.Truef(t, ro, "expected `true` with mode `RO`") + + // RW + resp, err = connPool.Call17("box.info", []interface{}{}, connection_pool.RW) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `false` with mode `RW`") + require.Falsef(t, ro, "expected `false` with mode `RW`") +} + +func TestEval(t *testing.T) { + roles := []bool{false, true, false, false, true} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // PreferRO + resp, err := connPool.Eval("return box.info().ro", []interface{}{}, connection_pool.PreferRO) + require.Nilf(t, err, "failed to Eval") + require.NotNilf(t, resp, "response is nil after Eval") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Eval") + + val, ok := resp.Data[0].(bool) + require.Truef(t, ok, "expected `true` with mode `PreferRO`") + require.Truef(t, val, "expected `true` with mode `PreferRO`") + + // PreferRW + resp, err = connPool.Eval("return box.info().ro", []interface{}{}, connection_pool.PreferRW) + require.Nilf(t, err, "failed to Eval") + require.NotNilf(t, resp, "response is nil after Eval") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Eval") + + val, ok = resp.Data[0].(bool) + require.Truef(t, ok, "expected `false` with mode `PreferRW`") + require.Falsef(t, val, "expected `false` with mode `PreferRW`") + + // RO + resp, err = connPool.Eval("return box.info().ro", []interface{}{}, connection_pool.RO) + require.Nilf(t, err, "failed to Eval") + require.NotNilf(t, resp, "response is nil after Eval") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Eval") + + val, ok = resp.Data[0].(bool) + require.Truef(t, ok, "expected `true` with mode `RO`") + require.Truef(t, val, "expected `true` with mode `RO`") + + // RW + resp, err = connPool.Eval("return box.info().ro", []interface{}{}, connection_pool.RW) + require.Nilf(t, err, "failed to Eval") + require.NotNilf(t, resp, "response is nil after Eval") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Eval") + + val, ok = resp.Data[0].(bool) + require.Truef(t, ok, "expected `false` with mode `RW`") + require.Falsef(t, val, "expected `false` with mode `RW`") +} + +func TestRoundRobinStrategy(t *testing.T) { + roles := []bool{false, true, false, false, true} + + allPorts := map[string]bool{ + servers[0]: true, + servers[1]: true, + servers[2]: true, + servers[3]: true, + servers[4]: true, + } + + masterPorts := map[string]bool{ + servers[0]: true, + servers[2]: true, + servers[3]: true, + } + + replicaPorts := map[string]bool{ + servers[1]: true, + servers[4]: true, + } + + serversNumber := len(servers) + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // ANY + args := test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: allPorts, + ConnPool: connPool, + Mode: connection_pool.ANY, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // RW + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: masterPorts, + ConnPool: connPool, + Mode: connection_pool.RW, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // RO + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: replicaPorts, + ConnPool: connPool, + Mode: connection_pool.RO, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // PreferRW + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: masterPorts, + ConnPool: connPool, + Mode: connection_pool.PreferRW, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // PreferRO + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: replicaPorts, + ConnPool: connPool, + Mode: connection_pool.PreferRO, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) +} + +func TestRoundRobinStrategy_NoReplica(t *testing.T) { + roles := []bool{false, false, false, false, false} + serversNumber := len(servers) + + allPorts := map[string]bool{ + servers[0]: true, + servers[1]: true, + servers[2]: true, + servers[3]: true, + servers[4]: true, + } + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // RO + _, err = connPool.Eval("return box.cfg.listen", []interface{}{}, connection_pool.RO) + require.NotNilf(t, err, "expected to fail after Eval, but error is nil") + require.Equal(t, "Can't find ro instance in pool", err.Error()) + + // ANY + args := test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: allPorts, + ConnPool: connPool, + Mode: connection_pool.ANY, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // RW + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: allPorts, + ConnPool: connPool, + Mode: connection_pool.RW, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // PreferRW + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: allPorts, + ConnPool: connPool, + Mode: connection_pool.PreferRW, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // PreferRO + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: allPorts, + ConnPool: connPool, + Mode: connection_pool.PreferRO, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) +} + +func TestRoundRobinStrategy_NoMaster(t *testing.T) { + roles := []bool{true, true, true, true, true} + serversNumber := len(servers) + + allPorts := map[string]bool{ + servers[0]: true, + servers[1]: true, + servers[2]: true, + servers[3]: true, + servers[4]: true, + } + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // RW + _, err = connPool.Eval("return box.cfg.listen", []interface{}{}, connection_pool.RW) + require.NotNilf(t, err, "expected to fail after Eval, but error is nil") + require.Equal(t, "Can't find rw instance in pool", err.Error()) + + // ANY + args := test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: allPorts, + ConnPool: connPool, + Mode: connection_pool.ANY, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // RO + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: allPorts, + ConnPool: connPool, + Mode: connection_pool.RO, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // PreferRW + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: allPorts, + ConnPool: connPool, + Mode: connection_pool.PreferRW, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // PreferRO + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: allPorts, + ConnPool: connPool, + Mode: connection_pool.PreferRO, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) +} + +func TestUpdateInstancesRoles(t *testing.T) { + roles := []bool{false, true, false, false, true} + + allPorts := map[string]bool{ + servers[0]: true, + servers[1]: true, + servers[2]: true, + servers[3]: true, + servers[4]: true, + } + + masterPorts := map[string]bool{ + servers[0]: true, + servers[2]: true, + servers[3]: true, + } + + replicaPorts := map[string]bool{ + servers[1]: true, + servers[4]: true, + } + + serversNumber := len(servers) + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // ANY + args := test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: allPorts, + ConnPool: connPool, + Mode: connection_pool.ANY, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // RW + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: masterPorts, + ConnPool: connPool, + Mode: connection_pool.RW, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // RO + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: replicaPorts, + ConnPool: connPool, + Mode: connection_pool.RO, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // PreferRW + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: masterPorts, + ConnPool: connPool, + Mode: connection_pool.PreferRW, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + // PreferRO + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: replicaPorts, + ConnPool: connPool, + Mode: connection_pool.PreferRO, + } + + err = test_helpers.ProcessListenOnInstance(args) + require.Nil(t, err) + + roles = []bool{true, false, true, true, false} + + masterPorts = map[string]bool{ + servers[1]: true, + servers[4]: true, + } + + replicaPorts = map[string]bool{ + servers[0]: true, + servers[2]: true, + servers[3]: true, + } + + err = test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + // ANY + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: allPorts, + ConnPool: connPool, + Mode: connection_pool.ANY, + } + + err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) + + // RW + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: masterPorts, + ConnPool: connPool, + Mode: connection_pool.RW, + } + + err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) + + // RO + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: replicaPorts, + ConnPool: connPool, + Mode: connection_pool.RO, + } + + err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) + + // PreferRW + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: masterPorts, + ConnPool: connPool, + Mode: connection_pool.PreferRW, + } + + err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) + + // PreferRO + args = test_helpers.ListenOnInstanceArgs{ + ServersNumber: serversNumber, + ExpectedPorts: replicaPorts, + ConnPool: connPool, + Mode: connection_pool.PreferRO, + } + + err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) +} + +func TestInsert(t *testing.T) { + roles := []bool{true, true, false, true, true} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // Mode is `RW` by default, we have only one RW instance (servers[2]) + resp, err := connPool.Insert(spaceName, []interface{}{"rw_insert_key", "rw_insert_value"}) + require.Nilf(t, err, "failed to Insert") + require.NotNilf(t, resp, "response is nil after Insert") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") + + tpl, ok := resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Insert") + require.Equalf(t, 2, len(tpl), "unexpected body of Insert") + + key, ok := tpl[0].(string) + require.Truef(t, ok, "unexpected body of Insert (0)") + require.Equalf(t, "rw_insert_key", key, "unexpected body of Insert (0)") + + value, ok := tpl[1].(string) + require.Truef(t, ok, "unexpected body of Insert (1)") + require.Equalf(t, "rw_insert_value", value, "unexpected body of Insert (1)") + + // Connect to servers[2] to check if tuple + // was inserted on RW instance + conn, err := tarantool.Connect(servers[2], connOpts) + require.Nilf(t, err, "failed to connect %s", servers[2]) + require.NotNilf(t, conn, "conn is nil after Connect") + + defer conn.Close() + + resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"rw_insert_key"}) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "rw_insert_key", key, "unexpected body of Select (0)") + + value, ok = tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "rw_insert_value", value, "unexpected body of Select (1)") + + // PreferRW + resp, err = connPool.Insert(spaceName, []interface{}{"preferRW_insert_key", "preferRW_insert_value"}) + require.Nilf(t, err, "failed to Insert") + require.NotNilf(t, resp, "response is nil after Insert") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Insert") + require.Equalf(t, 2, len(tpl), "unexpected body of Insert") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Insert (0)") + require.Equalf(t, "preferRW_insert_key", key, "unexpected body of Insert (0)") + + value, ok = tpl[1].(string) + require.Truef(t, ok, "unexpected body of Insert (1)") + require.Equalf(t, "preferRW_insert_value", value, "unexpected body of Insert (1)") + + resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"preferRW_insert_key"}) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "preferRW_insert_key", key, "unexpected body of Select (0)") + + value, ok = tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "preferRW_insert_value", value, "unexpected body of Select (1)") +} + +func TestDelete(t *testing.T) { + roles := []bool{true, true, false, true, true} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // Connect to servers[2] to check if tuple + // was inserted on RW instance + conn, err := tarantool.Connect(servers[2], connOpts) + require.Nilf(t, err, "failed to connect %s", servers[2]) + require.NotNilf(t, conn, "conn is nil after Connect") + + defer conn.Close() + + resp, err := conn.Insert(spaceNo, []interface{}{"delete_key", "delete_value"}) + require.Nilf(t, err, "failed to Insert") + require.NotNilf(t, resp, "response is nil after Insert") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") + + tpl, ok := resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Insert") + require.Equalf(t, 2, len(tpl), "unexpected body of Insert") + + key, ok := tpl[0].(string) + require.Truef(t, ok, "unexpected body of Insert (0)") + require.Equalf(t, "delete_key", key, "unexpected body of Insert (0)") + + value, ok := tpl[1].(string) + require.Truef(t, ok, "unexpected body of Insert (1)") + require.Equalf(t, "delete_value", value, "unexpected body of Insert (1)") + + // Mode is `RW` by default, we have only one RW instance (servers[2]) + resp, err = connPool.Delete(spaceName, indexNo, []interface{}{"delete_key"}) + require.Nilf(t, err, "failed to Delete") + require.NotNilf(t, resp, "response is nil after Delete") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Delete") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Delete") + require.Equalf(t, 2, len(tpl), "unexpected body of Delete") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Delete (0)") + require.Equalf(t, "delete_key", key, "unexpected body of Delete (0)") + + value, ok = tpl[1].(string) + require.Truef(t, ok, "unexpected body of Delete (1)") + require.Equalf(t, "delete_value", value, "unexpected body of Delete (1)") + + resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"delete_key"}) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 0, len(resp.Data), "response Body len != 0 after Select") +} + +func TestUpsert(t *testing.T) { + roles := []bool{true, true, false, true, true} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // Connect to servers[2] to check if tuple + // was inserted on RW instance + conn, err := tarantool.Connect(servers[2], connOpts) + require.Nilf(t, err, "failed to connect %s", servers[2]) + require.NotNilf(t, conn, "conn is nil after Connect") + + defer conn.Close() + + // Mode is `RW` by default, we have only one RW instance (servers[2]) + resp, err := connPool.Upsert(spaceName, []interface{}{"upsert_key", "upsert_value"}, []interface{}{[]interface{}{"=", 1, "new_value"}}) + require.Nilf(t, err, "failed to Upsert") + require.NotNilf(t, resp, "response is nil after Upsert") + + resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"upsert_key"}) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok := resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok := tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "upsert_key", key, "unexpected body of Select (0)") + + value, ok := tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "upsert_value", value, "unexpected body of Select (1)") + + // PreferRW + resp, err = connPool.Upsert( + spaceName, []interface{}{"upsert_key", "upsert_value"}, + []interface{}{[]interface{}{"=", 1, "new_value"}}, connection_pool.PreferRW) + + require.Nilf(t, err, "failed to Upsert") + require.NotNilf(t, resp, "response is nil after Upsert") + + resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"upsert_key"}) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "upsert_key", key, "unexpected body of Select (0)") + + value, ok = tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "new_value", value, "unexpected body of Select (1)") +} + +func TestUpdate(t *testing.T) { + roles := []bool{true, true, false, true, true} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // Connect to servers[2] to check if tuple + // was inserted on RW instance + conn, err := tarantool.Connect(servers[2], connOpts) + require.Nilf(t, err, "failed to connect %s", servers[2]) + require.NotNilf(t, conn, "conn is nil after Connect") + + defer conn.Close() + + resp, err := conn.Insert(spaceNo, []interface{}{"update_key", "update_value"}) + require.Nilf(t, err, "failed to Insert") + require.NotNilf(t, resp, "response is nil after Insert") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") + + tpl, ok := resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Insert") + require.Equalf(t, 2, len(tpl), "unexpected body of Insert") + + key, ok := tpl[0].(string) + require.Truef(t, ok, "unexpected body of Insert (0)") + require.Equalf(t, "update_key", key, "unexpected body of Insert (0)") + + value, ok := tpl[1].(string) + require.Truef(t, ok, "unexpected body of Insert (1)") + require.Equalf(t, "update_value", value, "unexpected body of Insert (1)") + + // Mode is `RW` by default, we have only one RW instance (servers[2]) + resp, err = connPool.Update(spaceName, indexNo, []interface{}{"update_key"}, []interface{}{[]interface{}{"=", 1, "new_value"}}) + require.Nilf(t, err, "failed to Update") + require.NotNilf(t, resp, "response is nil after Update") + + resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"update_key"}) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "update_key", key, "unexpected body of Select (0)") + + value, ok = tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "new_value", value, "unexpected body of Select (1)") + + // PreferRW + resp, err = connPool.Update( + spaceName, indexNo, []interface{}{"update_key"}, + []interface{}{[]interface{}{"=", 1, "another_value"}}, connection_pool.PreferRW) + + require.Nilf(t, err, "failed to Update") + require.NotNilf(t, resp, "response is nil after Update") + + resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"update_key"}) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "update_key", key, "unexpected body of Select (0)") + + value, ok = tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "another_value", value, "unexpected body of Select (1)") +} + +func TestReplace(t *testing.T) { + roles := []bool{true, true, false, true, true} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // Connect to servers[2] to check if tuple + // was inserted on RW instance + conn, err := tarantool.Connect(servers[2], connOpts) + require.Nilf(t, err, "failed to connect %s", servers[2]) + require.NotNilf(t, conn, "conn is nil after Connect") + + defer conn.Close() + + resp, err := conn.Insert(spaceNo, []interface{}{"replace_key", "replace_value"}) + require.Nilf(t, err, "failed to Insert") + require.NotNilf(t, resp, "response is nil after Insert") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") + + tpl, ok := resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Insert") + require.Equalf(t, 2, len(tpl), "unexpected body of Insert") + + key, ok := tpl[0].(string) + require.Truef(t, ok, "unexpected body of Insert (0)") + require.Equalf(t, "replace_key", key, "unexpected body of Insert (0)") + + value, ok := tpl[1].(string) + require.Truef(t, ok, "unexpected body of Insert (1)") + require.Equalf(t, "replace_value", value, "unexpected body of Insert (1)") + + // Mode is `RW` by default, we have only one RW instance (servers[2]) + resp, err = connPool.Replace(spaceNo, []interface{}{"new_key", "new_value"}) + require.Nilf(t, err, "failed to Replace") + require.NotNilf(t, resp, "response is nil after Replace") + + resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"new_key"}) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "new_key", key, "unexpected body of Select (0)") + + value, ok = tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "new_value", value, "unexpected body of Select (1)") + + // PreferRW + resp, err = connPool.Replace(spaceNo, []interface{}{"new_key", "new_value"}, connection_pool.PreferRW) + require.Nilf(t, err, "failed to Replace") + require.NotNilf(t, resp, "response is nil after Replace") + + resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"new_key"}) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "new_key", key, "unexpected body of Select (0)") + + value, ok = tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "new_value", value, "unexpected body of Select (1)") +} + +func TestSelect(t *testing.T) { + roles := []bool{true, true, false, true, false} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + roServers := []string{servers[0], servers[1], servers[3]} + rwServers := []string{servers[2], servers[4]} + allServers := []string{servers[0], servers[1], servers[2], servers[3], servers[4]} + + roTpl := []interface{}{"ro_select_key", "ro_select_value"} + rwTpl := []interface{}{"rw_select_key", "rw_select_value"} + anyTpl := []interface{}{"any_select_key", "any_select_value"} + + roKey := []interface{}{"ro_select_key"} + rwKey := []interface{}{"rw_select_key"} + anyKey := []interface{}{"any_select_key"} + + err = test_helpers.InsertOnInstances(roServers, connOpts, spaceNo, roTpl) + require.Nil(t, err) + + err = test_helpers.InsertOnInstances(rwServers, connOpts, spaceNo, rwTpl) + require.Nil(t, err) + + err = test_helpers.InsertOnInstances(allServers, connOpts, spaceNo, anyTpl) + require.Nil(t, err) + + //default: ANY + resp, err := connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, anyKey) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok := resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok := tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "any_select_key", key, "unexpected body of Select (0)") + + value, ok := tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "any_select_value", value, "unexpected body of Select (1)") + + // PreferRO + resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, connection_pool.PreferRO) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "ro_select_key", key, "unexpected body of Select (0)") + + // PreferRW + resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, connection_pool.PreferRW) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "rw_select_key", key, "unexpected body of Select (0)") + + value, ok = tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "rw_select_value", value, "unexpected body of Select (1)") + + // RO + resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, connection_pool.RO) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "ro_select_key", key, "unexpected body of Select (0)") + + value, ok = tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "ro_select_value", value, "unexpected body of Select (1)") + + // RW + resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, connection_pool.RW) + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "rw_select_key", key, "unexpected body of Select (0)") + + value, ok = tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "rw_select_value", value, "unexpected body of Select (1)") +} + +func TestPing(t *testing.T) { + roles := []bool{true, true, false, true, false} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // ANY + resp, err := connPool.Ping(connection_pool.ANY) + require.Nilf(t, err, "failed to Ping") + require.NotNilf(t, resp, "response is nil after Ping") + + // RW + resp, err = connPool.Ping(connection_pool.RW) + require.Nilf(t, err, "failed to Ping") + require.NotNilf(t, resp, "response is nil after Ping") + + // RO + resp, err = connPool.Ping(connection_pool.RO) + require.Nilf(t, err, "failed to Ping") + require.NotNilf(t, resp, "response is nil after Ping") + + // PreferRW + resp, err = connPool.Ping(connection_pool.PreferRW) + require.Nilf(t, err, "failed to Ping") + require.NotNilf(t, resp, "response is nil after Ping") + + // PreferRO + resp, err = connPool.Ping(connection_pool.PreferRO) + require.Nilf(t, err, "failed to Ping") + require.NotNilf(t, resp, "response is nil after Ping") +} + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + initScript := "config.lua" + waitStart := 100 * time.Millisecond + var connectRetry uint = 3 + retryTimeout := 500 * time.Millisecond + workDirs := []string{ + "work_dir1", "work_dir2", + "work_dir3", "work_dir4", + "work_dir5"} + var err error + + instances, err = test_helpers.StartTarantoolInstances(servers, workDirs, test_helpers.StartOpts{ + InitScript: initScript, + User: connOpts.User, + Pass: connOpts.Pass, + WaitStart: waitStart, + ConnectRetry: connectRetry, + RetryTimeout: retryTimeout, + }) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + return -1 + } + + defer test_helpers.StopTarantoolInstances(instances) + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/connection_pool/const.go b/connection_pool/const.go new file mode 100644 index 000000000..04690d4f5 --- /dev/null +++ b/connection_pool/const.go @@ -0,0 +1,52 @@ +package connection_pool + +type Mode uint32 +type Role uint32 +type State uint32 + +/* +Mode parameter: + +- ANY (use any instance) - the request can be executed on any instance (master or replica). + +- RW (writeable instance (master)) - the request can only be executed on master. + +- RO (read only instance (replica)) - the request can only be executed on replica. + +- PREFER_RO (prefer read only instance (replica)) - if there is one, otherwise fallback to a writeable one (master). + +- PREFER_RW (prefer write only instance (master)) - if there is one, otherwise fallback to a read only one (replica). + + Request Default mode + ---------- -------------- + | call | no default | + | eval | no default | + | ping | no default | + | insert | RW | + | delete | RW | + | replace | RW | + | update | RW | + | upsert | RW | + | select | ANY | + | get | ANY | + */ +const ( + ANY = iota + RW + RO + PreferRW + PreferRO +) + +// master/replica role +const ( + unknown = iota + master + replica +) + +// pool state +const ( + connConnected = iota + connClosed +) diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go new file mode 100644 index 000000000..06341a9b2 --- /dev/null +++ b/connection_pool/example_test.go @@ -0,0 +1,531 @@ +package connection_pool_test + +import ( + "fmt" + + "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/connection_pool" + "github.com/tarantool/go-tarantool/test_helpers" +) + +type Tuple struct { + // Instruct msgpack to pack this struct as array, so no custom packer + // is needed. + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Key string + Value string +} + +var testRoles = []bool{true, true, false, true, true} + +func examplePool(roles []bool) (*connection_pool.ConnectionPool, error) { + err := test_helpers.SetClusterRO(servers, connOpts, roles) + if err != nil { + return nil, fmt.Errorf("ConnectionPool is not established") + } + connPool, err := connection_pool.Connect(servers, connOpts) + if err != nil || connPool == nil { + return nil, fmt.Errorf("ConnectionPool is not established") + } + + return connPool, nil +} + +func ExampleConnectionPool_Select() { + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + // Connect to servers[2] to check if tuple + // was inserted on RW instance + conn, err := tarantool.Connect(servers[2], connOpts) + if err != nil || conn == nil { + fmt.Printf("failed to connect to %s", servers[2]) + return + } + + // Insert a new tuple {"key1", "value1"}. + _, err = conn.Insert(spaceNo, []interface{}{"key1", "value1"}) + if err != nil { + fmt.Printf("Failed to insert: %s", err.Error()) + return + } + // Insert a new tuple {"key2", "value2"}. + _, err = conn.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}) + if err != nil { + fmt.Printf("Failed to insert: %s", err.Error()) + return + } + + resp, err := pool.Select( + spaceNo, indexNo, 0, 100, tarantool.IterEq, + []interface{}{"key1"}, connection_pool.PreferRW) + if err != nil { + fmt.Printf("error in select is %v", err) + return + } + fmt.Printf("response is %#v\n", resp.Data) + resp, err = pool.Select( + spaceNo, indexNo, 0, 100, tarantool.IterEq, + []interface{}{"key2"}, connection_pool.PreferRW) + if err != nil { + fmt.Printf("error in select is %v", err) + return + } + fmt.Printf("response is %#v\n", resp.Data) + + // Delete tuple with primary key "key1". + _, err = conn.Delete(spaceName, indexName, []interface{}{"key1"}) + if err != nil { + fmt.Printf("Failed to delete: %s", err.Error()) + } + // Delete tuple with primary key "key2". + _, err = conn.Delete(spaceNo, indexNo, []interface{}{"key2"}) + if err != nil { + fmt.Printf("Failed to delete: %s", err.Error()) + } + + // Output: + // response is []interface {}{[]interface {}{"key1", "value1"}} + // response is []interface {}{[]interface {}{"key2", "value2"}} +} + +func ExampleConnectionPool_SelectTyped() { + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + // Connect to servers[2] to check if tuple + // was inserted on RW instance + conn, err := tarantool.Connect(servers[2], connOpts) + if err != nil || conn == nil { + fmt.Printf("failed to connect to %s", servers[2]) + return + } + + // Insert a new tuple {"key1", "value1"}. + _, err = conn.Insert(spaceNo, []interface{}{"key1", "value1"}) + if err != nil { + fmt.Printf("Failed to insert: %s", err.Error()) + return + } + // Insert a new tuple {"key2", "value2"}. + _, err = conn.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}) + if err != nil { + fmt.Printf("Failed to insert: %s", err.Error()) + return + } + + var res []Tuple + err = pool.SelectTyped( + spaceNo, indexNo, 0, 100, tarantool.IterEq, + []interface{}{"key1"}, &res, connection_pool.PreferRW) + if err != nil { + fmt.Printf("error in select is %v", err) + return + } + fmt.Printf("response is %v\n", res) + err = pool.SelectTyped( + spaceName, indexName, 0, 100, tarantool.IterEq, + []interface{}{"key2"}, &res, connection_pool.PreferRW) + if err != nil { + fmt.Printf("error in select is %v", err) + return + } + fmt.Printf("response is %v\n", res) + + // Delete tuple with primary key "key1". + _, err = conn.Delete(spaceName, indexName, []interface{}{"key1"}) + if err != nil { + fmt.Printf("Failed to delete: %s", err.Error()) + } + // Delete tuple with primary key "key2". + _, err = conn.Delete(spaceNo, indexNo, []interface{}{"key2"}) + if err != nil { + fmt.Printf("Failed to delete: %s", err.Error()) + } + + // Output: + // response is [{{} key1 value1}] + // response is [{{} key2 value2}] +} + +func ExampleConnectionPool_SelectAsync() { + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + // Connect to servers[2] to check if tuple + // was inserted on RW instance + conn, err := tarantool.Connect(servers[2], connOpts) + if err != nil || conn == nil { + fmt.Printf("failed to connect to %s", servers[2]) + return + } + + // Insert a new tuple {"key1", "value1"}. + _, err = conn.Insert(spaceNo, []interface{}{"key1", "value1"}) + if err != nil { + fmt.Printf("Failed to insert: %s", err.Error()) + return + } + // Insert a new tuple {"key2", "value2"}. + _, err = conn.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}) + if err != nil { + fmt.Printf("Failed to insert: %s", err.Error()) + return + } + // Insert a new tuple {"key3", "value3"}. + _, err = conn.Insert(spaceNo, []interface{}{"key3", "value3"}) + if err != nil { + fmt.Printf("Failed to insert: %s", err.Error()) + return + } + + var futs [3]*tarantool.Future + futs[0] = pool.SelectAsync( + spaceName, indexName, 0, 2, tarantool.IterEq, + []interface{}{"key1"}, connection_pool.PreferRW) + futs[1] = pool.SelectAsync( + spaceName, indexName, 0, 1, tarantool.IterEq, + []interface{}{"key2"}, connection_pool.RW) + futs[2] = pool.SelectAsync( + spaceName, indexName, 0, 1,tarantool.IterEq, + []interface{}{"key3"}, connection_pool.RW) + var t []Tuple + err = futs[0].GetTyped(&t) + fmt.Println("Future", 0, "Error", err) + fmt.Println("Future", 0, "Data", t) + + resp, err := futs[1].Get() + fmt.Println("Future", 1, "Error", err) + fmt.Println("Future", 1, "Data", resp.Data) + + resp, err = futs[2].Get() + fmt.Println("Future", 2, "Error", err) + fmt.Println("Future", 2, "Data", resp.Data) + + // Delete tuple with primary key "key1". + _, err = conn.Delete(spaceName, indexName, []interface{}{"key1"}) + if err != nil { + fmt.Printf("Failed to delete: %s", err.Error()) + } + // Delete tuple with primary key "key2". + _, err = conn.Delete(spaceNo, indexNo, []interface{}{"key2"}) + if err != nil { + fmt.Printf("Failed to delete: %s", err.Error()) + } + // Delete tuple with primary key "key3". + _, err = conn.Delete(spaceNo, indexNo, []interface{}{"key3"}) + if err != nil { + fmt.Printf("Failed to delete: %s", err.Error()) + } + + // Output: + // Future 0 Error + // Future 0 Data [{{} key1 value1}] + // Future 1 Error + // Future 1 Data [[key2 value2]] + // Future 2 Error + // Future 2 Data [[key3 value3]] +} + +func ExampleConnectionPool_SelectAsync_err() { + roles := []bool{true, true, true, true, true} + pool, err := examplePool(roles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + var futs [3]*tarantool.Future + futs[0] = pool.SelectAsync( + spaceName, indexName, 0, 2, tarantool.IterEq, + []interface{}{"key1"}, connection_pool.RW) + + err = futs[0].Err() + fmt.Println("Future", 0, "Error", err) + + // Output: + // Future 0 Error Can't find rw instance in pool +} + +func ExampleConnectionPool_Ping() { + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + // Ping a Tarantool instance to check connection. + resp, err := pool.Ping(connection_pool.ANY) + fmt.Println("Ping Code", resp.Code) + fmt.Println("Ping Data", resp.Data) + fmt.Println("Ping Error", err) + // Output: + // Ping Code 0 + // Ping Data [] + // Ping Error +} + +func ExampleConnectionPool_Insert() { + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + // Insert a new tuple {"key1", "value1"}. + resp, err := pool.Insert(spaceNo, []interface{}{"key1", "value1"}) + fmt.Println("Insert key1") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Insert a new tuple {"key2", "value2"}. + resp, err = pool.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}, connection_pool.PreferRW) + fmt.Println("Insert key2") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + + // Connect to servers[2] to check if tuple + // was inserted on RW instance + conn, err := tarantool.Connect(servers[2], connOpts) + if err != nil || conn == nil { + fmt.Printf("failed to connect to %s", servers[2]) + return + } + + // Delete tuple with primary key "key1". + _, err = conn.Delete(spaceName, indexName, []interface{}{"key1"}) + if err != nil { + fmt.Printf("Failed to delete: %s", err.Error()) + } + // Delete tuple with primary key "key2". + _, err = conn.Delete(spaceNo, indexNo, []interface{}{"key2"}) + if err != nil { + fmt.Printf("Failed to delete: %s", err.Error()) + } + // Output: + // Insert key1 + // Error + // Code 0 + // Data [[key1 value1]] + // Insert key2 + // Error + // Code 0 + // Data [[key2 value2]] +} + +func ExampleConnectionPool_Delete() { + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + // Connect to servers[2] to check if tuple + // was inserted on RW instance + conn, err := tarantool.Connect(servers[2], connOpts) + if err != nil || conn == nil { + fmt.Printf("failed to connect to %s", servers[2]) + return + } + + // Insert a new tuple {"key1", "value1"}. + _, err = conn.Insert(spaceNo, []interface{}{"key1", "value1"}) + if err != nil { + fmt.Printf("Failed to insert: %s", err.Error()) + return + } + // Insert a new tuple {"key2", "value2"}. + _, err = conn.Insert(spaceNo, []interface{}{"key2", "value2"}) + if err != nil { + fmt.Printf("Failed to insert: %s", err.Error()) + return + } + + // Delete tuple with primary key {"key1"}. + resp, err := pool.Delete(spaceNo, indexNo, []interface{}{"key1"}) + fmt.Println("Delete key1") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + + // Delete tuple with primary key { "key2" }. + resp, err = pool.Delete(spaceName, indexName, []interface{}{"key2"}, connection_pool.PreferRW) + fmt.Println("Delete key2") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Output: + // Delete key1 + // Error + // Code 0 + // Data [[key1 value1]] + // Delete key2 + // Error + // Code 0 + // Data [[key2 value2]] +} + +func ExampleConnectionPool_Replace() { + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + // Connect to servers[2] to check if tuple + // was inserted on RW instance + conn, err := tarantool.Connect(servers[2], connOpts) + if err != nil || conn == nil { + fmt.Printf("failed to connect to %s", servers[2]) + return + } + + // Insert a new tuple {"key1", "value1"}. + _, err = conn.Insert(spaceNo, []interface{}{"key1", "value1"}) + if err != nil { + fmt.Printf("Failed to insert: %s", err.Error()) + return + } + + // Replace a tuple with primary key ""key1. + // Note, Tuple is defined within tests, and has EncdodeMsgpack and + // DecodeMsgpack methods. + resp, err := pool.Replace(spaceNo, []interface{}{"key1", "new_value"}) + fmt.Println("Replace key1") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + resp, err = pool.Replace(spaceName, []interface{}{"key1", "another_value"}) + fmt.Println("Replace key1") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + resp, err = pool.Replace(spaceName, &Tuple{Key: "key1", Value: "value2"}) + fmt.Println("Replace key1") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + resp, err = pool.Replace(spaceName, &Tuple{Key: "key1", Value: "new_value2"}, connection_pool.PreferRW) + fmt.Println("Replace key1") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + + // Delete tuple with primary key "key1". + _, err = conn.Delete(spaceName, indexName, []interface{}{"key1"}) + if err != nil { + fmt.Printf("Failed to delete: %s", err.Error()) + } + + // Output: + // Replace key1 + // Error + // Code 0 + // Data [[key1 new_value]] + // Replace key1 + // Error + // Code 0 + // Data [[key1 another_value]] + // Replace key1 + // Error + // Code 0 + // Data [[key1 value2]] + // Replace key1 + // Error + // Code 0 + // Data [[key1 new_value2]] +} + +func ExampleConnectionPool_Update() { + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + // Connect to servers[2] to check if tuple + // was inserted on RW instance + conn, err := tarantool.Connect(servers[2], connOpts) + if err != nil || conn == nil { + fmt.Printf("failed to connect to %s", servers[2]) + return + } + + // Insert a new tuple {"key1", "value1"}. + _, err = conn.Insert(spaceNo, []interface{}{"key1", "value1"}) + if err != nil { + fmt.Printf("Failed to insert: %s", err.Error()) + return + } + + // Update tuple with primary key { "key1" }. + resp, err := pool.Update( + spaceName, indexName, []interface{}{"key1"}, + []interface{}{[]interface{}{"=", 1, "new_value"}}, connection_pool.PreferRW) + fmt.Println("Update key1") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + + // Delete tuple with primary key "key1". + _, err = conn.Delete(spaceName, indexName, []interface{}{"key1"}) + if err != nil { + fmt.Printf("Failed to delete: %s", err.Error()) + } + + // Output: + // Update key1 + // Error + // Code 0 + // Data [[key1 new_value]] +} + +func ExampleConnectionPool_Call17() { + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + // Call a function 'simple_incr' with arguments. + resp, err := pool.Call17("simple_incr", []interface{}{1}, connection_pool.PreferRW) + fmt.Println("Call simple_incr()") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Output: + // Call simple_incr() + // Error + // Code 0 + // Data [2] +} + +func ExampleConnectionPool_Eval() { + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + // Run raw Lua code. + resp, err := pool.Eval("return 1 + 2", []interface{}{}, connection_pool.PreferRW) + fmt.Println("Eval 'return 1 + 2'") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Output: + // Eval 'return 1 + 2' + // Error + // Code 0 + // Data [3] +} diff --git a/connection_pool/round_robin.go b/connection_pool/round_robin.go new file mode 100644 index 000000000..7f3f0d098 --- /dev/null +++ b/connection_pool/round_robin.go @@ -0,0 +1,117 @@ +package connection_pool + +import ( + "sync" + "sync/atomic" + + "github.com/tarantool/go-tarantool" +) + +type RoundRobinStrategy struct { + conns []*tarantool.Connection + indexByAddr map[string]int + mutex sync.RWMutex + size int + current uint64 +} + +func (r *RoundRobinStrategy) GetConnByAddr(addr string) *tarantool.Connection { + r.mutex.RLock() + defer r.mutex.RUnlock() + + index, found := r.indexByAddr[addr] + if !found { + return nil + } + + return r.conns[index] +} + +func (r *RoundRobinStrategy) DeleteConnByAddr(addr string) *tarantool.Connection { + r.mutex.Lock() + defer r.mutex.Unlock() + + if r.size == 0 { + return nil + } + + index, found := r.indexByAddr[addr] + if !found { + return nil + } + + delete(r.indexByAddr, addr) + + conn := r.conns[index] + r.conns = append(r.conns[:index], r.conns[index+1:]...) + r.size -= 1 + + for index, conn := range r.conns { + r.indexByAddr[conn.Addr()] = index + } + + return conn +} + +func (r *RoundRobinStrategy) IsEmpty() bool { + r.mutex.RLock() + defer r.mutex.RUnlock() + + return r.size == 0 +} + +func (r *RoundRobinStrategy) CloseConns() []error { + r.mutex.Lock() + defer r.mutex.Unlock() + + errs := make([]error, len(r.conns)) + + for i, conn := range r.conns { + errs[i] = conn.Close() + } + + return errs +} + +func (r *RoundRobinStrategy) GetNextConnection() *tarantool.Connection { + r.mutex.RLock() + defer r.mutex.RUnlock() + + // We want to iterate through the elements in a circular order + // so the first element in cycle is connections[next] + // and the last one is connections[next + length]. + next := r.nextIndex() + cycleLen := len(r.conns) + next + for i := next; i < cycleLen; i++ { + idx := i % len(r.conns) + if r.conns[idx].ConnectedNow() { + if i != next { + atomic.StoreUint64(&r.current, uint64(idx)) + } + return r.conns[idx] + } + } + + return nil +} + +func NewEmptyRoundRobin(size int) *RoundRobinStrategy { + return &RoundRobinStrategy{ + conns: make([]*tarantool.Connection, 0, size), + indexByAddr: make(map[string]int), + size: 0, + } +} + +func (r *RoundRobinStrategy) AddConn(addr string, conn *tarantool.Connection) { + r.mutex.Lock() + defer r.mutex.Unlock() + + r.conns = append(r.conns, conn) + r.indexByAddr[addr] = r.size + r.size += 1 +} + +func (r *RoundRobinStrategy) nextIndex() int { + return int(atomic.AddUint64(&r.current, uint64(1)) % uint64(len(r.conns))) +} diff --git a/go.mod b/go.mod index 6914a5870..152329b1f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.11 require ( github.com/google/uuid v1.3.0 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/stretchr/testify v1.7.1 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/vmihailenco/msgpack.v2 v2.9.2 diff --git a/go.sum b/go.sum index 8a6ae1fa0..1f9e791b4 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -7,6 +9,11 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -16,7 +23,10 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/request.go b/request.go index ff8a7e8a5..6065959c4 100644 --- a/request.go +++ b/request.go @@ -457,3 +457,8 @@ func (fut *Future) Err() error { fut.wait() return fut.err } + +// NewErrorFuture returns new set empty Future with filled error field. +func NewErrorFuture(err error) *Future { + return &Future{err: err} +} diff --git a/test_helpers/main.go b/test_helpers/main.go index ad45e00d9..92ee27692 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -60,8 +60,8 @@ type TarantoolInstance struct { // Cmd is a Tarantool command. Used to kill Tarantool process. Cmd *exec.Cmd - // WorkDir is a directory with tarantool data. Cleaned up after run. - WorkDir string + // Options for restarting a tarantool instance. + Opts StartOpts } func isReady(server string, opts *tarantool.Opts) error { @@ -148,6 +148,19 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) ( return false, nil } +// RestartTarantool restarts a tarantool instance for tests +// with specifies parameters (refer to StartOpts) +// which were specified in inst parameter. +// inst is a tarantool instance that was started by +// StartTarantool. Rewrites inst.Cmd.Process to stop +// instance with StopTarantool. +// Process must be stopped with StopTarantool. +func RestartTarantool(inst *TarantoolInstance) error { + startedInst, err := StartTarantool(inst.Opts) + inst.Cmd.Process = startedInst.Cmd.Process + return err +} + // StartTarantool starts a tarantool instance for tests // with specifies parameters (refer to StartOpts). // Process must be stopped with StopTarantool. @@ -174,7 +187,8 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { return inst, err } - inst.WorkDir = startOpts.WorkDir + // Options for restarting tarantool instance. + inst.Opts = startOpts // Start tarantool. err = inst.Cmd.Start() @@ -236,8 +250,8 @@ func StopTarantool(inst TarantoolInstance) { func StopTarantoolWithCleanup(inst TarantoolInstance) { StopTarantool(inst) - if inst.WorkDir != "" { - if err := os.RemoveAll(inst.WorkDir); err != nil { + if inst.Opts.WorkDir != "" { + if err := os.RemoveAll(inst.Opts.WorkDir); err != nil { log.Fatalf("Failed to clean work directory, got %s", err) } } diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go new file mode 100644 index 000000000..8293b8185 --- /dev/null +++ b/test_helpers/pool_helper.go @@ -0,0 +1,248 @@ +package test_helpers + +import ( + "fmt" + "reflect" + "time" + + "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/connection_pool" +) + +type ListenOnInstanceArgs struct { + ConnPool *connection_pool.ConnectionPool + Mode connection_pool.Mode + ServersNumber int + ExpectedPorts map[string]bool +} + +type CheckStatusesArgs struct { + ConnPool *connection_pool.ConnectionPool + Servers []string + Mode connection_pool.Mode + ExpectedPoolStatus bool + ExpectedStatuses map[string]bool +} + +func compareTuples(expectedTpl []interface{}, actualTpl []interface{}) error { + if len(actualTpl) != len(expectedTpl) { + return fmt.Errorf("Unexpected body of Insert (tuple len)") + } + + for i, field := range actualTpl { + if field != expectedTpl[i] { + return fmt.Errorf("Unexpected field, expected: %v actual: %v", expectedTpl[i], field) + } + } + + return nil +} + +func CheckPoolStatuses(args interface{}) error { + checkArgs, ok := args.(CheckStatusesArgs) + if !ok { + return fmt.Errorf("incorrect args") + } + + connected, _ := checkArgs.ConnPool.ConnectedNow(checkArgs.Mode) + if connected != checkArgs.ExpectedPoolStatus { + return fmt.Errorf( + "incorrect connection pool status: expected status %t actual status %t", + checkArgs.ExpectedPoolStatus, connected) + } + + poolInfo := checkArgs.ConnPool.GetPoolInfo() + for _, server := range checkArgs.Servers { + status := poolInfo[server] != nil && poolInfo[server].ConnectedNow + if checkArgs.ExpectedStatuses[server] != status { + return fmt.Errorf( + "incorrect conn status: addr %s expected status %t actual status %t", + server, checkArgs.ExpectedStatuses[server], status) + } + } + + return nil +} + +// ProcessListenOnInstance helper calls "return box.cfg.listen" +// as many times as there are servers in the connection pool +// with specified mode. +// For RO mode expected received ports equals to replica ports. +// For RW mode expected received ports equals to master ports. +// For PreferRO mode expected received ports equals to replica +// ports or to all ports. +// For PreferRW mode expected received ports equals to master ports +// or to all ports. +func ProcessListenOnInstance(args interface{}) error { + actualPorts := map[string]bool{} + + listenArgs, ok := args.(ListenOnInstanceArgs) + if !ok { + return fmt.Errorf("incorrect args") + } + + for i := 0; i < listenArgs.ServersNumber; i++ { + resp, err := listenArgs.ConnPool.Eval("return box.cfg.listen", []interface{}{}, listenArgs.Mode) + if err != nil { + return fmt.Errorf("fail to Eval: %s", err.Error()) + } + if resp == nil { + return fmt.Errorf("response is nil after Eval") + } + if len(resp.Data) < 1 { + return fmt.Errorf("response.Data is empty after Eval") + } + + port, ok := resp.Data[0].(string) + if !ok { + return fmt.Errorf("response.Data is incorrect after Eval") + } + + actualPorts[port] = true + } + + equal := reflect.DeepEqual(actualPorts, listenArgs.ExpectedPorts) + if !equal { + return fmt.Errorf("expected ports: %v, actual ports: %v", actualPorts, listenArgs.ExpectedPorts) + } + + return nil +} + +func Retry(f func(interface{}) error, args interface{}, count int, timeout time.Duration) error { + var err error + + for i := 0; ; i++ { + err = f(args) + if err == nil { + return err + } + + if i >= (count - 1) { + break + } + + time.Sleep(timeout) + } + + return err +} + +func InsertOnInstance(server string, connOpts tarantool.Opts, space interface{}, tuple interface{}) error { + conn, err := tarantool.Connect(server, connOpts) + if err != nil { + return fmt.Errorf("Fail to connect to %s: %s", server, err.Error()) + } + if conn == nil { + return fmt.Errorf("conn is nil after Connect") + } + defer conn.Close() + + resp, err := conn.Insert(space, tuple) + if err != nil { + return fmt.Errorf("Failed to Insert: %s", err.Error()) + } + if resp == nil { + return fmt.Errorf("Response is nil after Insert") + } + if len(resp.Data) != 1 { + return fmt.Errorf("Response Body len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + return fmt.Errorf("Unexpected body of Insert") + } else { + expectedTpl, ok := tuple.([]interface{}) + if !ok { + return fmt.Errorf("Failed to cast") + } + + err = compareTuples(expectedTpl, tpl) + if err != nil { + return err + } + } + + return nil +} + +func InsertOnInstances(servers []string, connOpts tarantool.Opts, space interface{}, tuple interface{}) error { + serversNumber := len(servers) + roles := make([]bool, serversNumber) + for i:= 0; i < serversNumber; i++{ + roles[i] = false + } + + err := SetClusterRO(servers, connOpts, roles) + if err != nil { + return fmt.Errorf("fail to set roles for cluster: %s", err.Error()) + } + + for _, server := range servers { + err := InsertOnInstance(server, connOpts, space, tuple) + if err != nil { + return err + } + } + + return nil +} + +func SetInstanceRO(server string, connOpts tarantool.Opts, isReplica bool) error { + conn, err := tarantool.Connect(server, connOpts) + if err != nil { + return err + } + + defer conn.Close() + + _, err = conn.Call("box.cfg", []interface{}{map[string]bool{"read_only": isReplica}}) + if err != nil { + return err + } + + return nil +} + +func SetClusterRO(servers []string, connOpts tarantool.Opts, roles []bool) error { + if len(servers) != len(roles) { + return fmt.Errorf("number of servers should be equal to number of roles") + } + + for i, server := range servers { + err := SetInstanceRO(server, connOpts, roles[i]) + if err != nil { + return err + } + } + + return nil +} + +func StartTarantoolInstances(servers []string, workDirs []string, opts StartOpts) ([]TarantoolInstance, error) { + if len(servers) != len(workDirs) { + return nil, fmt.Errorf("number of servers should be equal to number of workDirs") + } + + instances := make([]TarantoolInstance, 0, len(servers)) + + for i, server := range servers { + opts.Listen = server + opts.WorkDir = workDirs[i] + + instance, err := StartTarantool(opts) + if err != nil { + StopTarantoolInstances(instances) + return nil, err + } + + instances = append(instances, instance) + } + + return instances, nil +} + +func StopTarantoolInstances(instances []TarantoolInstance) { + for _, instance := range instances { + StopTarantoolWithCleanup(instance) + } +} From de95e3184df6bde148e7cc87229572d4247f599b Mon Sep 17 00:00:00 2001 From: vr009 Date: Tue, 12 Apr 2022 07:14:58 +0300 Subject: [PATCH 269/605] code health: fix all places highlighted by linter Changed the warning's suppression in check.yaml. The suppression of the rule errcheck may be removed after adding errors check in all methods calling encodeXxx inside. See details below. Suppressed the highlighting lacks of error's check in all methods, having encodeXxx inside. For now those methods are not able to return any error due to internal implementation of writer interface (see smallbuf.go). For future, if the implementation of the writer will be changed, and there will be a need to check errors, we must think about how to say to compiler that the error check is 'unlikely' for keeping performance (If there will be any affect on it). Fixed the use of time package API in all places with calls of time.Now().Sub(). Now time package propose the explicit time.Until(). Replaced all calls of Errorf() with Fatalf() in tests, where it is needed. That change prevents nil dereferences below in the code and stops the test execution, where it is expected in tests. Suppressed the highlighting of all unused constants and functions with //nolint comment. Fixed the calling of Fatalf() from non-testing goroutine in queue tests. It is not a valid way to stop test from another goroutine. Fixed the gofmt-highlighted places in test_helpers/pool_helper.go and connection_pool/const.go. Added instructions to CONTRIBUTING.md how to run CI-linter locally. Closes #142 Closes #150 --- .github/workflows/check.yaml | 10 +- CONTRIBUTING.md | 7 ++ connection.go | 12 +- connection_pool/connection_pool_test.go | 134 +++++++++++------------ connection_pool/const.go | 2 +- connection_pool/example_test.go | 8 +- example_custom_unpacking_test.go | 2 +- example_test.go | 2 +- multi/multi_test.go | 8 +- queue/example_msgpack_test.go | 6 + queue/example_test.go | 3 + queue/queue_test.go | 28 +++-- response.go | 4 - schema.go | 1 + tarantool_test.go | 139 ++++++++++++------------ test_helpers/main.go | 2 - test_helpers/pool_helper.go | 14 +-- uuid/uuid_test.go | 16 +-- 18 files changed, 213 insertions(+), 185 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 8d28529ba..9b785d0df 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -39,4 +39,12 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: - args: --issues-exit-code=0 -E gofmt + # The suppression of the rule `errcheck` may be removed after adding + # errors check in all methods calling EncodeXxx inside. + # For now those methods are not even able to return any error + # cause of internal implementation of writer interface (see smallbuf.go). + # + # The `//nolint` workaround was not the acceptable way of warnings suppression, + # cause those comments get rendered in documentation by godoc. + # See https://github.com/tarantool/go-tarantool/pull/160#discussion_r858608221 + args: -E gofmt -D errcheck diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05602f23e..c42babc35 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,13 @@ For example, for running tests in `multi`, `uuid` and `main` packages, call make test-multi test-uuid test-main ``` +To check if the current changes will pass the linter in CI, install +golnagci-lint from [sources](https://golangci-lint.run/usage/install/) +and run it with next flags: +```bash +golangci-lint run -E gofmt -D errcheck +``` + ## Code review checklist - Public API contains functions, variables, constants that are needed from diff --git a/connection.go b/connection.go index d8e381364..3be878011 100644 --- a/connection.go +++ b/connection.go @@ -147,7 +147,7 @@ type connShard struct { bufmut sync.Mutex buf smallWBuf enc *msgpack.Encoder - _pad [16]uint64 + _pad [16]uint64 //nolint: unused,structcheck } // Greeting is a message sent by Tarantool on connect. @@ -495,7 +495,7 @@ func (conn *Connection) createConnection(reconnect bool) (err error) { conn.notify(ReconnectFailed) reconnects++ conn.mutex.Unlock() - time.Sleep(now.Add(conn.opts.Reconnect).Sub(time.Now())) + time.Sleep(time.Until(now.Add(conn.opts.Reconnect))) conn.mutex.Lock() } if conn.state == connClosed { @@ -688,7 +688,7 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { *pair.last = fut pair.last = &fut.next if conn.opts.Timeout > 0 { - fut.timeout = time.Now().Sub(epoch) + conn.opts.Timeout + fut.timeout = time.Since(epoch) + conn.opts.Timeout } shard.rmut.Unlock() if conn.rlimit != nil && conn.opts.RLimitAction == RLimitWait { @@ -796,9 +796,9 @@ func (conn *Connection) timeouts() { return case <-t.C: } - minNext := time.Now().Sub(epoch) + timeout + minNext := time.Since(epoch) + timeout for i := range conn.shard { - nowepoch = time.Now().Sub(epoch) + nowepoch = time.Since(epoch) shard := &conn.shard[i] for pos := range shard.requests { shard.rmut.Lock() @@ -825,7 +825,7 @@ func (conn *Connection) timeouts() { shard.rmut.Unlock() } } - nowepoch = time.Now().Sub(epoch) + nowepoch = time.Since(epoch) if nowepoch+time.Microsecond < minNext { t.Reset(minNext - nowepoch) } else { diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 0f0b0a6a6..4d98ddd9e 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -65,9 +65,9 @@ func TestConnSuccessfully(t *testing.T) { defer connPool.Close() args := test_helpers.CheckStatusesArgs{ - ConnPool: connPool, - Mode: connection_pool.ANY, - Servers: []string{server}, + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ server: true, @@ -90,9 +90,9 @@ func TestReconnect(t *testing.T) { test_helpers.StopTarantoolWithCleanup(instances[0]) args := test_helpers.CheckStatusesArgs{ - ConnPool: connPool, - Mode: connection_pool.ANY, - Servers: []string{server}, + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ server: false, @@ -106,9 +106,9 @@ func TestReconnect(t *testing.T) { require.Nilf(t, err, "failed to restart tarantool") args = test_helpers.CheckStatusesArgs{ - ConnPool: connPool, - Mode: connection_pool.ANY, - Servers: []string{server}, + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ server: true, @@ -133,9 +133,9 @@ func TestDisconnectAll(t *testing.T) { test_helpers.StopTarantoolWithCleanup(instances[1]) args := test_helpers.CheckStatusesArgs{ - ConnPool: connPool, - Mode: connection_pool.ANY, - Servers: []string{server1, server2}, + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server1, server2}, ExpectedPoolStatus: false, ExpectedStatuses: map[string]bool{ server1: false, @@ -153,9 +153,9 @@ func TestDisconnectAll(t *testing.T) { require.Nilf(t, err, "failed to restart tarantool") args = test_helpers.CheckStatusesArgs{ - ConnPool: connPool, - Mode: connection_pool.ANY, - Servers: []string{server1, server2}, + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server1, server2}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ server1: true, @@ -176,9 +176,9 @@ func TestClose(t *testing.T) { require.NotNilf(t, connPool, "conn is nil after Connect") args := test_helpers.CheckStatusesArgs{ - ConnPool: connPool, - Mode: connection_pool.ANY, - Servers: []string{server1, server2}, + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server1, server2}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ server1: true, @@ -192,9 +192,9 @@ func TestClose(t *testing.T) { connPool.Close() args = test_helpers.CheckStatusesArgs{ - ConnPool: connPool, - Mode: connection_pool.ANY, - Servers: []string{server1, server2}, + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server1, server2}, ExpectedPoolStatus: false, ExpectedStatuses: map[string]bool{ server1: false, @@ -353,8 +353,8 @@ func TestRoundRobinStrategy(t *testing.T) { args := test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: allPorts, - ConnPool: connPool, - Mode: connection_pool.ANY, + ConnPool: connPool, + Mode: connection_pool.ANY, } err = test_helpers.ProcessListenOnInstance(args) @@ -364,8 +364,8 @@ func TestRoundRobinStrategy(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: masterPorts, - ConnPool: connPool, - Mode: connection_pool.RW, + ConnPool: connPool, + Mode: connection_pool.RW, } err = test_helpers.ProcessListenOnInstance(args) @@ -375,8 +375,8 @@ func TestRoundRobinStrategy(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: replicaPorts, - ConnPool: connPool, - Mode: connection_pool.RO, + ConnPool: connPool, + Mode: connection_pool.RO, } err = test_helpers.ProcessListenOnInstance(args) @@ -386,8 +386,8 @@ func TestRoundRobinStrategy(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: masterPorts, - ConnPool: connPool, - Mode: connection_pool.PreferRW, + ConnPool: connPool, + Mode: connection_pool.PreferRW, } err = test_helpers.ProcessListenOnInstance(args) @@ -397,8 +397,8 @@ func TestRoundRobinStrategy(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: replicaPorts, - ConnPool: connPool, - Mode: connection_pool.PreferRO, + ConnPool: connPool, + Mode: connection_pool.PreferRO, } err = test_helpers.ProcessListenOnInstance(args) @@ -435,8 +435,8 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { args := test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: allPorts, - ConnPool: connPool, - Mode: connection_pool.ANY, + ConnPool: connPool, + Mode: connection_pool.ANY, } err = test_helpers.ProcessListenOnInstance(args) @@ -446,8 +446,8 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: allPorts, - ConnPool: connPool, - Mode: connection_pool.RW, + ConnPool: connPool, + Mode: connection_pool.RW, } err = test_helpers.ProcessListenOnInstance(args) @@ -457,8 +457,8 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: allPorts, - ConnPool: connPool, - Mode: connection_pool.PreferRW, + ConnPool: connPool, + Mode: connection_pool.PreferRW, } err = test_helpers.ProcessListenOnInstance(args) @@ -468,8 +468,8 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: allPorts, - ConnPool: connPool, - Mode: connection_pool.PreferRO, + ConnPool: connPool, + Mode: connection_pool.PreferRO, } err = test_helpers.ProcessListenOnInstance(args) @@ -506,8 +506,8 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { args := test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: allPorts, - ConnPool: connPool, - Mode: connection_pool.ANY, + ConnPool: connPool, + Mode: connection_pool.ANY, } err = test_helpers.ProcessListenOnInstance(args) @@ -517,8 +517,8 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: allPorts, - ConnPool: connPool, - Mode: connection_pool.RO, + ConnPool: connPool, + Mode: connection_pool.RO, } err = test_helpers.ProcessListenOnInstance(args) @@ -528,8 +528,8 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: allPorts, - ConnPool: connPool, - Mode: connection_pool.PreferRW, + ConnPool: connPool, + Mode: connection_pool.PreferRW, } err = test_helpers.ProcessListenOnInstance(args) @@ -539,8 +539,8 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: allPorts, - ConnPool: connPool, - Mode: connection_pool.PreferRO, + ConnPool: connPool, + Mode: connection_pool.PreferRO, } err = test_helpers.ProcessListenOnInstance(args) @@ -584,8 +584,8 @@ func TestUpdateInstancesRoles(t *testing.T) { args := test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: allPorts, - ConnPool: connPool, - Mode: connection_pool.ANY, + ConnPool: connPool, + Mode: connection_pool.ANY, } err = test_helpers.ProcessListenOnInstance(args) @@ -595,8 +595,8 @@ func TestUpdateInstancesRoles(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: masterPorts, - ConnPool: connPool, - Mode: connection_pool.RW, + ConnPool: connPool, + Mode: connection_pool.RW, } err = test_helpers.ProcessListenOnInstance(args) @@ -606,8 +606,8 @@ func TestUpdateInstancesRoles(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: replicaPorts, - ConnPool: connPool, - Mode: connection_pool.RO, + ConnPool: connPool, + Mode: connection_pool.RO, } err = test_helpers.ProcessListenOnInstance(args) @@ -617,8 +617,8 @@ func TestUpdateInstancesRoles(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: masterPorts, - ConnPool: connPool, - Mode: connection_pool.PreferRW, + ConnPool: connPool, + Mode: connection_pool.PreferRW, } err = test_helpers.ProcessListenOnInstance(args) @@ -628,8 +628,8 @@ func TestUpdateInstancesRoles(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: replicaPorts, - ConnPool: connPool, - Mode: connection_pool.PreferRO, + ConnPool: connPool, + Mode: connection_pool.PreferRO, } err = test_helpers.ProcessListenOnInstance(args) @@ -655,8 +655,8 @@ func TestUpdateInstancesRoles(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: allPorts, - ConnPool: connPool, - Mode: connection_pool.ANY, + ConnPool: connPool, + Mode: connection_pool.ANY, } err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) @@ -666,8 +666,8 @@ func TestUpdateInstancesRoles(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: masterPorts, - ConnPool: connPool, - Mode: connection_pool.RW, + ConnPool: connPool, + Mode: connection_pool.RW, } err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) @@ -677,8 +677,8 @@ func TestUpdateInstancesRoles(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: replicaPorts, - ConnPool: connPool, - Mode: connection_pool.RO, + ConnPool: connPool, + Mode: connection_pool.RO, } err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) @@ -688,8 +688,8 @@ func TestUpdateInstancesRoles(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: masterPorts, - ConnPool: connPool, - Mode: connection_pool.PreferRW, + ConnPool: connPool, + Mode: connection_pool.PreferRW, } err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) @@ -699,8 +699,8 @@ func TestUpdateInstancesRoles(t *testing.T) { args = test_helpers.ListenOnInstanceArgs{ ServersNumber: serversNumber, ExpectedPorts: replicaPorts, - ConnPool: connPool, - Mode: connection_pool.PreferRO, + ConnPool: connPool, + Mode: connection_pool.PreferRO, } err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) diff --git a/connection_pool/const.go b/connection_pool/const.go index 04690d4f5..eb41a47ea 100644 --- a/connection_pool/const.go +++ b/connection_pool/const.go @@ -29,7 +29,7 @@ Mode parameter: | upsert | RW | | select | ANY | | get | ANY | - */ +*/ const ( ANY = iota RW diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go index 06341a9b2..4da811daf 100644 --- a/connection_pool/example_test.go +++ b/connection_pool/example_test.go @@ -53,7 +53,7 @@ func ExampleConnectionPool_Select() { return } // Insert a new tuple {"key2", "value2"}. - _, err = conn.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}) + _, err = conn.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}) if err != nil { fmt.Printf("Failed to insert: %s", err.Error()) return @@ -114,7 +114,7 @@ func ExampleConnectionPool_SelectTyped() { return } // Insert a new tuple {"key2", "value2"}. - _, err = conn.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}) + _, err = conn.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}) if err != nil { fmt.Printf("Failed to insert: %s", err.Error()) return @@ -176,7 +176,7 @@ func ExampleConnectionPool_SelectAsync() { return } // Insert a new tuple {"key2", "value2"}. - _, err = conn.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}) + _, err = conn.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}) if err != nil { fmt.Printf("Failed to insert: %s", err.Error()) return @@ -196,7 +196,7 @@ func ExampleConnectionPool_SelectAsync() { spaceName, indexName, 0, 1, tarantool.IterEq, []interface{}{"key2"}, connection_pool.RW) futs[2] = pool.SelectAsync( - spaceName, indexName, 0, 1,tarantool.IterEq, + spaceName, indexName, 0, 1, tarantool.IterEq, []interface{}{"key3"}, connection_pool.RW) var t []Tuple err = futs[0].GetTyped(&t) diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index 772c9faf3..a6f9ab55e 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -16,7 +16,7 @@ type Tuple2 struct { // Same effect in a "magic" way, but slower. type Tuple3 struct { - _msgpack struct{} `msgpack:",asArray"` + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused Cid uint Orig string diff --git a/example_test.go b/example_test.go index 1e8c883ab..0a6b6cb37 100644 --- a/example_test.go +++ b/example_test.go @@ -10,7 +10,7 @@ import ( type Tuple struct { // Instruct msgpack to pack this struct as array, so no custom packer // is needed. - _msgpack struct{} `msgpack:",asArray"` + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused Id uint Msg string Name string diff --git a/multi/multi_test.go b/multi/multi_test.go index aae659101..e501ced13 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -27,10 +27,10 @@ var connOptsMulti = OptsMulti{ func TestConnError_IncorrectParams(t *testing.T) { multiConn, err := Connect([]string{}, tarantool.Opts{}) if err == nil { - t.Errorf("err is nil with incorrect params") + t.Fatalf("err is nil with incorrect params") } if multiConn != nil { - t.Errorf("conn is not nill with incorrect params") + t.Fatalf("conn is not nill with incorrect params") } if err.Error() != "addrs should not be empty" { t.Errorf("incorrect error: %s", err.Error()) @@ -38,10 +38,10 @@ func TestConnError_IncorrectParams(t *testing.T) { multiConn, err = ConnectWithOpts([]string{server1}, tarantool.Opts{}, OptsMulti{}) if err == nil { - t.Errorf("err is nil with incorrect params") + t.Fatal("err is nil with incorrect params") } if multiConn != nil { - t.Errorf("conn is not nill with incorrect params") + t.Fatal("conn is not nill with incorrect params") } if err.Error() != "wrong check timeout, must be greater than 0" { t.Errorf("incorrect error: %s", err.Error()) diff --git a/queue/example_msgpack_test.go b/queue/example_msgpack_test.go index ecfb60a94..e66cebfa4 100644 --- a/queue/example_msgpack_test.go +++ b/queue/example_msgpack_test.go @@ -114,9 +114,15 @@ func Example_simpleQueueCustomMsgPack() { fmt.Println("Data is ", task.Data()) task, err = que.Put([]int{1, 2, 3}) + if err != nil { + log.Fatalf("Put failed: %s", err) + } task.Bury() task, err = que.TakeTimeout(2 * time.Second) + if err != nil { + log.Fatalf("Take with timeout failed: %s", err) + } if task == nil { fmt.Println("Task is nil") } diff --git a/queue/example_test.go b/queue/example_test.go index cb0f62f1a..d546b43d7 100644 --- a/queue/example_test.go +++ b/queue/example_test.go @@ -74,6 +74,9 @@ func Example_simpleQueue() { } task, err = q.TakeTimeout(2 * time.Second) + if err != nil { + fmt.Printf("error in take with timeout") + } if task != nil { fmt.Printf("Task should be nil, but %d", task.Id()) return diff --git a/queue/queue_test.go b/queue/queue_test.go index db531eb4c..cd9f417cf 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -630,8 +630,7 @@ func TestFifoQueue_Release(t *testing.T) { func TestTtlQueue(t *testing.T) { conn, err := Connect(server, opts) if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return + t.Fatalf("Failed to connect: %s", err.Error()) } defer conn.Close() @@ -683,12 +682,10 @@ func TestTtlQueue(t *testing.T) { func TestTtlQueue_Put(t *testing.T) { conn, err := Connect(server, opts) if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return + t.Fatalf("Failed to connect: %s", err.Error()) } if conn == nil { - t.Errorf("conn is nil after Connect") - return + t.Fatalf("conn is nil after Connect") } defer conn.Close() @@ -755,12 +752,10 @@ func TestTtlQueue_Put(t *testing.T) { func TestUtube_Put(t *testing.T) { conn, err := Connect(server, opts) if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return + t.Fatalf("Failed to connect: %s", err.Error()) } if conn == nil { - t.Errorf("conn is nil after Connect") - return + t.Fatalf("conn is nil after Connect") } defer conn.Close() @@ -794,16 +789,22 @@ func TestUtube_Put(t *testing.T) { t.Fatalf("Failed put task to queue: %s", err.Error()) } + errChan := make(chan struct{}) go func() { t1, err := q.TakeTimeout(2 * time.Second) if err != nil { - t.Fatalf("Failed to take task from utube: %s", err.Error()) + t.Errorf("Failed to take task from utube: %s", err.Error()) + errChan <- struct{}{} + return } time.Sleep(2 * time.Second) if err := t1.Ack(); err != nil { - t.Fatalf("Failed to ack task: %s", err.Error()) + t.Errorf("Failed to ack task: %s", err.Error()) + errChan <- struct{}{} + return } + close(errChan) }() time.Sleep(100 * time.Millisecond) @@ -817,6 +818,9 @@ func TestUtube_Put(t *testing.T) { t.Fatalf("Failed to ack task: %s", err.Error()) } end := time.Now() + if _, ok := <-errChan; ok { + t.Fatalf("One of tasks failed") + } if math.Abs(float64(end.Sub(start)-2*time.Second)) > float64(200*time.Millisecond) { t.Fatalf("Blocking time is less than expected: actual = %.2fs, expected = 1s", end.Sub(start).Seconds()) } diff --git a/response.go b/response.go index 2e0783659..c56eaa483 100644 --- a/response.go +++ b/response.go @@ -15,10 +15,6 @@ type Response struct { buf smallBuf } -func (resp *Response) fill(b []byte) { - resp.buf.b = b -} - func (resp *Response) smallInt(d *msgpack.Decoder) (i int, err error) { b, err := resp.buf.ReadByte() if err != nil { diff --git a/schema.go b/schema.go index 91c0faedd..ab937f2f1 100644 --- a/schema.go +++ b/schema.go @@ -50,6 +50,7 @@ type IndexField struct { Type string } +//nolint: varcheck,deadcode const ( maxSchemas = 10000 spaceSpId = 280 diff --git a/tarantool_test.go b/tarantool_test.go index 5f5078a8b..be9dafbce 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -101,7 +101,7 @@ func BenchmarkClientSerialTyped(b *testing.B) { _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { - b.Errorf("No connection available") + b.Fatal("No connection available") } var r []Tuple @@ -224,7 +224,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { - b.Errorf("No connection available") + b.Fatal("No connection available") } b.RunParallel(func(pb *testing.PB) { @@ -263,7 +263,7 @@ func BenchmarkClientParallel(b *testing.B) { _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { - b.Errorf("No connection available") + b.Fatal("No connection available") } b.RunParallel(func(pb *testing.PB) { @@ -287,7 +287,7 @@ func BenchmarkClientParallelMassive(b *testing.B) { _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { - b.Errorf("No connection available") + b.Fatal("No connection available") } var wg sync.WaitGroup @@ -361,31 +361,29 @@ func TestClient(t *testing.T) { conn, err = Connect(server, opts) if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return + t.Fatalf("Failed to connect: %s", err.Error()) } if conn == nil { - t.Errorf("conn is nil after Connect") - return + t.Fatalf("conn is nil after Connect") } defer conn.Close() // Ping resp, err = conn.Ping() if err != nil { - t.Errorf("Failed to Ping: %s", err.Error()) + t.Fatalf("Failed to Ping: %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Ping") + t.Fatalf("Response is nil after Ping") } // Insert resp, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) if err != nil { - t.Errorf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Insert") + t.Fatalf("Response is nil after Insert") } if len(resp.Data) != 1 { t.Errorf("Response Body len != 1") @@ -415,10 +413,10 @@ func TestClient(t *testing.T) { // Delete resp, err = conn.Delete(spaceNo, indexNo, []interface{}{uint(1)}) if err != nil { - t.Errorf("Failed to Delete: %s", err.Error()) + t.Fatalf("Failed to Delete: %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Delete") + t.Fatalf("Response is nil after Delete") } if len(resp.Data) != 1 { t.Errorf("Response Body len != 1") @@ -438,10 +436,10 @@ func TestClient(t *testing.T) { } resp, err = conn.Delete(spaceNo, indexNo, []interface{}{uint(101)}) if err != nil { - t.Errorf("Failed to Delete: %s", err.Error()) + t.Fatalf("Failed to Delete: %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Delete") + t.Fatalf("Response is nil after Delete") } if len(resp.Data) != 0 { t.Errorf("Response Data len != 0") @@ -450,17 +448,17 @@ func TestClient(t *testing.T) { // Replace resp, err = conn.Replace(spaceNo, []interface{}{uint(2), "hello", "world"}) if err != nil { - t.Errorf("Failed to Replace: %s", err.Error()) + t.Fatalf("Failed to Replace: %s", err.Error()) } if resp == nil { t.Errorf("Response is nil after Replace") } resp, err = conn.Replace(spaceNo, []interface{}{uint(2), "hi", "planet"}) if err != nil { - t.Errorf("Failed to Replace (duplicate): %s", err.Error()) + t.Fatalf("Failed to Replace (duplicate): %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Replace (duplicate)") + t.Fatalf("Response is nil after Replace (duplicate)") } if len(resp.Data) != 1 { t.Errorf("Response Data len != 1") @@ -482,10 +480,10 @@ func TestClient(t *testing.T) { // Update resp, err = conn.Update(spaceNo, indexNo, []interface{}{uint(2)}, []interface{}{[]interface{}{"=", 1, "bye"}, []interface{}{"#", 2, 1}}) if err != nil { - t.Errorf("Failed to Update: %s", err.Error()) + t.Fatalf("Failed to Update: %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Update") + t.Fatalf("Response is nil after Update") } if len(resp.Data) != 1 { t.Errorf("Response Data len != 1") @@ -508,14 +506,14 @@ func TestClient(t *testing.T) { if strings.Compare(conn.Greeting.Version, "Tarantool 1.6.7") >= 0 { resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) if err != nil { - t.Errorf("Failed to Upsert (insert): %s", err.Error()) + t.Fatalf("Failed to Upsert (insert): %s", err.Error()) } if resp == nil { t.Errorf("Response is nil after Upsert (insert)") } resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) if err != nil { - t.Errorf("Failed to Upsert (update): %s", err.Error()) + t.Fatalf("Failed to Upsert (update): %s", err.Error()) } if resp == nil { t.Errorf("Response is nil after Upsert (update)") @@ -526,15 +524,18 @@ func TestClient(t *testing.T) { for i := 10; i < 20; i++ { resp, err = conn.Replace(spaceNo, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) if err != nil { - t.Errorf("Failed to Replace: %s", err.Error()) + t.Fatalf("Failed to Replace: %s", err.Error()) + } + if resp.Code != 0 { + t.Errorf("Failed to replace") } } resp, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}) if err != nil { - t.Errorf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Select") + t.Fatalf("Response is nil after Select") } if len(resp.Data) != 1 { t.Errorf("Response Data len != 1") @@ -553,10 +554,10 @@ func TestClient(t *testing.T) { // Select empty resp, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}) if err != nil { - t.Errorf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Select") + t.Fatalf("Response is nil after Select") } if len(resp.Data) != 0 { t.Errorf("Response Data len != 0") @@ -566,7 +567,7 @@ func TestClient(t *testing.T) { var tpl []Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}, &tpl) if err != nil { - t.Errorf("Failed to SelectTyped: %s", err.Error()) + t.Fatalf("Failed to SelectTyped: %s", err.Error()) } if len(tpl) != 1 { t.Errorf("Result len of SelectTyped != 1") @@ -580,7 +581,7 @@ func TestClient(t *testing.T) { var singleTpl = Tuple{} err = conn.GetTyped(spaceNo, indexNo, []interface{}{uint(10)}, &singleTpl) if err != nil { - t.Errorf("Failed to GetTyped: %s", err.Error()) + t.Fatalf("Failed to GetTyped: %s", err.Error()) } if singleTpl.Id != 10 { t.Errorf("Bad value loaded from GetTyped") @@ -590,7 +591,7 @@ func TestClient(t *testing.T) { var tpl1 [1]Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}, &tpl1) if err != nil { - t.Errorf("Failed to SelectTyped: %s", err.Error()) + t.Fatalf("Failed to SelectTyped: %s", err.Error()) } if len(tpl) != 1 { t.Errorf("Result len of SelectTyped != 1") @@ -604,7 +605,7 @@ func TestClient(t *testing.T) { var singleTpl2 Tuple err = conn.GetTyped(spaceNo, indexNo, []interface{}{uint(30)}, &singleTpl2) if err != nil { - t.Errorf("Failed to GetTyped: %s", err.Error()) + t.Fatalf("Failed to GetTyped: %s", err.Error()) } if singleTpl2.Id != 0 { t.Errorf("Bad value loaded from GetTyped") @@ -614,7 +615,7 @@ func TestClient(t *testing.T) { var tpl2 []Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}, &tpl2) if err != nil { - t.Errorf("Failed to SelectTyped: %s", err.Error()) + t.Fatalf("Failed to SelectTyped: %s", err.Error()) } if len(tpl2) != 0 { t.Errorf("Result len of SelectTyped != 1") @@ -623,10 +624,10 @@ func TestClient(t *testing.T) { // Call resp, err = conn.Call("box.info", []interface{}{"box.schema.SPACE_ID"}) if err != nil { - t.Errorf("Failed to Call: %s", err.Error()) + t.Fatalf("Failed to Call: %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Call") + t.Fatalf("Response is nil after Call") } if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") @@ -634,11 +635,17 @@ func TestClient(t *testing.T) { // Call vs Call17 resp, err = conn.Call("simple_incr", []interface{}{1}) + if err != nil { + t.Errorf("Failed to use Call") + } if resp.Data[0].([]interface{})[0].(uint64) != 2 { t.Errorf("result is not {{1}} : %v", resp.Data) } resp, err = conn.Call17("simple_incr", []interface{}{1}) + if err != nil { + t.Errorf("Failed to use Call17") + } if resp.Data[0].(uint64) != 2 { t.Errorf("result is not {{1}} : %v", resp.Data) } @@ -646,10 +653,10 @@ func TestClient(t *testing.T) { // Eval resp, err = conn.Eval("return 5 + 6", []interface{}{}) if err != nil { - t.Errorf("Failed to Eval: %s", err.Error()) + t.Fatalf("Failed to Eval: %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Eval") + t.Fatalf("Response is nil after Eval") } if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") @@ -666,12 +673,10 @@ func TestSchema(t *testing.T) { conn, err = Connect(server, opts) if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return + t.Fatalf("Failed to connect: %s", err.Error()) } if conn == nil { - t.Errorf("conn is nil after Connect") - return + t.Fatalf("conn is nil after Connect") } defer conn.Close() @@ -807,7 +812,7 @@ func TestSchema(t *testing.T) { ifield1 := index3.Fields[0] ifield2 := index3.Fields[1] if ifield1 == nil || ifield2 == nil { - t.Errorf("index field is nil") + t.Fatalf("index field is nil") } if ifield1.Id != 1 || ifield2.Id != 2 { t.Errorf("index field has incorrect Id") @@ -821,7 +826,7 @@ func TestSchema(t *testing.T) { if err != nil || rSpaceNo != 514 || rIndexNo != 3 { t.Errorf("numeric space and index params not resolved as-is") } - rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex(514, nil) + rSpaceNo, _, err = schema.ResolveSpaceIndex(514, nil) if err != nil || rSpaceNo != 514 { t.Errorf("numeric space param not resolved as-is") } @@ -829,15 +834,15 @@ func TestSchema(t *testing.T) { if err != nil || rSpaceNo != 514 || rIndexNo != 3 { t.Errorf("symbolic space and index params not resolved") } - rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex("schematest", nil) + rSpaceNo, _, err = schema.ResolveSpaceIndex("schematest", nil) if err != nil || rSpaceNo != 514 { t.Errorf("symbolic space param not resolved") } - rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex("schematest22", "secondary") + _, _, err = schema.ResolveSpaceIndex("schematest22", "secondary") if err == nil { t.Errorf("resolveSpaceIndex didn't returned error with not existing space name") } - rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex("schematest", "secondary22") + _, _, err = schema.ResolveSpaceIndex("schematest", "secondary22") if err == nil { t.Errorf("resolveSpaceIndex didn't returned error with not existing index name") } @@ -850,25 +855,26 @@ func TestClientNamed(t *testing.T) { conn, err = Connect(server, opts) if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return + t.Fatalf("Failed to connect: %s", err.Error()) } if conn == nil { - t.Errorf("conn is nil after Connect") - return + t.Fatalf("conn is nil after Connect") } defer conn.Close() // Insert resp, err = conn.Insert(spaceName, []interface{}{uint(1001), "hello2", "world2"}) if err != nil { - t.Errorf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err.Error()) + } + if resp.Code != 0 { + t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) } // Delete resp, err = conn.Delete(spaceName, indexName, []interface{}{uint(1001)}) if err != nil { - t.Errorf("Failed to Delete: %s", err.Error()) + t.Fatalf("Failed to Delete: %s", err.Error()) } if resp == nil { t.Errorf("Response is nil after Delete") @@ -877,7 +883,7 @@ func TestClientNamed(t *testing.T) { // Replace resp, err = conn.Replace(spaceName, []interface{}{uint(1002), "hello", "world"}) if err != nil { - t.Errorf("Failed to Replace: %s", err.Error()) + t.Fatalf("Failed to Replace: %s", err.Error()) } if resp == nil { t.Errorf("Response is nil after Replace") @@ -886,7 +892,7 @@ func TestClientNamed(t *testing.T) { // Update resp, err = conn.Update(spaceName, indexName, []interface{}{uint(1002)}, []interface{}{[]interface{}{"=", 1, "bye"}, []interface{}{"#", 2, 1}}) if err != nil { - t.Errorf("Failed to Update: %s", err.Error()) + t.Fatalf("Failed to Update: %s", err.Error()) } if resp == nil { t.Errorf("Response is nil after Update") @@ -896,14 +902,14 @@ func TestClientNamed(t *testing.T) { if strings.Compare(conn.Greeting.Version, "Tarantool 1.6.7") >= 0 { resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) if err != nil { - t.Errorf("Failed to Upsert (insert): %s", err.Error()) + t.Fatalf("Failed to Upsert (insert): %s", err.Error()) } if resp == nil { t.Errorf("Response is nil after Upsert (insert)") } resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) if err != nil { - t.Errorf("Failed to Upsert (update): %s", err.Error()) + t.Fatalf("Failed to Upsert (update): %s", err.Error()) } if resp == nil { t.Errorf("Response is nil after Upsert (update)") @@ -914,12 +920,15 @@ func TestClientNamed(t *testing.T) { for i := 1010; i < 1020; i++ { resp, err = conn.Replace(spaceName, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) if err != nil { - t.Errorf("Failed to Replace: %s", err.Error()) + t.Fatalf("Failed to Replace: %s", err.Error()) + } + if resp.Code != 0 { + t.Errorf("Failed to Replace: wrong code returned %d", resp.Code) } } resp, err = conn.Select(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)}) if err != nil { - t.Errorf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err.Error()) } if resp == nil { t.Errorf("Response is nil after Select") @@ -929,7 +938,7 @@ func TestClientNamed(t *testing.T) { var tpl []Tuple err = conn.SelectTyped(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)}, &tpl) if err != nil { - t.Errorf("Failed to SelectTyped: %s", err.Error()) + t.Fatalf("Failed to SelectTyped: %s", err.Error()) } if len(tpl) != 1 { t.Errorf("Result len of SelectTyped != 1") @@ -942,27 +951,23 @@ func TestComplexStructs(t *testing.T) { conn, err = Connect(server, opts) if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return + t.Fatalf("Failed to connect: %s", err.Error()) } if conn == nil { - t.Errorf("conn is nil after Connect") - return + t.Fatalf("conn is nil after Connect") } defer conn.Close() tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} _, err = conn.Replace(spaceNo, &tuple) if err != nil { - t.Errorf("Failed to insert: %s", err.Error()) - return + t.Fatalf("Failed to insert: %s", err.Error()) } var tuples [1]Tuple2 err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{777}, &tuples) if err != nil { - t.Errorf("Failed to selectTyped: %s", err.Error()) - return + t.Fatalf("Failed to selectTyped: %s", err.Error()) } if len(tuples) != 1 { diff --git a/test_helpers/main.go b/test_helpers/main.go index 92ee27692..cc3416b91 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -144,8 +144,6 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) ( } else { return patch < patchMin, nil } - - return false, nil } // RestartTarantool restarts a tarantool instance for tests diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index 8293b8185..63f4d09f5 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -10,18 +10,18 @@ import ( ) type ListenOnInstanceArgs struct { - ConnPool *connection_pool.ConnectionPool - Mode connection_pool.Mode + ConnPool *connection_pool.ConnectionPool + Mode connection_pool.Mode ServersNumber int ExpectedPorts map[string]bool } type CheckStatusesArgs struct { - ConnPool *connection_pool.ConnectionPool - Servers []string - Mode connection_pool.Mode + ConnPool *connection_pool.ConnectionPool + Servers []string + Mode connection_pool.Mode ExpectedPoolStatus bool - ExpectedStatuses map[string]bool + ExpectedStatuses map[string]bool } func compareTuples(expectedTpl []interface{}, actualTpl []interface{}) error { @@ -168,7 +168,7 @@ func InsertOnInstance(server string, connOpts tarantool.Opts, space interface{}, func InsertOnInstances(servers []string, connOpts tarantool.Opts, space interface{}, tuple interface{}) error { serversNumber := len(servers) roles := make([]bool, serversNumber) - for i:= 0; i < serversNumber; i++{ + for i := 0; i < serversNumber; i++ { roles[i] = false } diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index b8987b0bc..67c45b4f3 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -53,7 +53,7 @@ func (t *TupleUUID) DecodeMsgpack(d *msgpack.Decoder) error { func connectWithValidation(t *testing.T) *Connection { conn, err := Connect(server, opts) if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) + t.Fatalf("Failed to connect: %s", err.Error()) } if conn == nil { t.Errorf("conn is nil after Connect") @@ -63,7 +63,7 @@ func connectWithValidation(t *testing.T) *Connection { func tupleValueIsId(t *testing.T, tuples []interface{}, id uuid.UUID) { if len(tuples) != 1 { - t.Errorf("Response Data len != 1") + t.Fatalf("Response Data len != 1") } if tpl, ok := tuples[0].([]interface{}); !ok { @@ -88,22 +88,22 @@ func TestSelect(t *testing.T) { id, uuidErr := uuid.Parse("c8f0fa1f-da29-438c-a040-393f1126ad39") if uuidErr != nil { - t.Errorf("Failed to prepare test uuid: %s", uuidErr) + t.Fatalf("Failed to prepare test uuid: %s", uuidErr) } resp, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{id}) if errSel != nil { - t.Errorf("UUID select failed: %s", errSel.Error()) + t.Fatalf("UUID select failed: %s", errSel.Error()) } if resp == nil { - t.Errorf("Response is nil after Select") + t.Fatalf("Response is nil after Select") } tupleValueIsId(t, resp.Data, id) var tuples []TupleUUID errTyp := conn.SelectTyped(space, index, 0, 1, IterEq, []interface{}{id}, &tuples) if errTyp != nil { - t.Errorf("Failed to SelectTyped: %s", errTyp.Error()) + t.Fatalf("Failed to SelectTyped: %s", errTyp.Error()) } if len(tuples) != 1 { t.Errorf("Result len of SelectTyped != 1") @@ -131,7 +131,7 @@ func TestReplace(t *testing.T) { t.Errorf("UUID replace failed: %s", errRep) } if respRep == nil { - t.Errorf("Response is nil after Replace") + t.Fatalf("Response is nil after Replace") } tupleValueIsId(t, respRep.Data, id) @@ -140,7 +140,7 @@ func TestReplace(t *testing.T) { t.Errorf("UUID select failed: %s", errSel) } if respSel == nil { - t.Errorf("Response is nil after Select") + t.Fatalf("Response is nil after Select") } tupleValueIsId(t, respSel.Data, id) } From 9fb381c628adcda738a3b1bbbfdde2230f6e49ff Mon Sep 17 00:00:00 2001 From: Rianov Viacheslav Date: Thu, 28 Apr 2022 14:40:28 +0300 Subject: [PATCH 270/605] test: fix incorrect perf measurement Added calls of b.ResetTimer() to all Benchmark tests before running bench loop. That call clears all counters of allocs, timers etc. The preparation before bench loop affected perf results. Part of #122 --- tarantool_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tarantool_test.go b/tarantool_test.go index be9dafbce..2aaee1b45 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -81,6 +81,7 @@ func BenchmarkClientSerial(b *testing.B) { b.Errorf("No connection available") } + b.ResetTimer() for i := 0; i < b.N; i++ { _, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) if err != nil { @@ -105,6 +106,7 @@ func BenchmarkClientSerialTyped(b *testing.B) { } var r []Tuple + b.ResetTimer() for i := 0; i < b.N; i++ { err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, IntKey{1111}, &r) if err != nil { @@ -128,6 +130,7 @@ func BenchmarkClientFuture(b *testing.B) { b.Error(err) } + b.ResetTimer() for i := 0; i < b.N; i += N { var fs [N]*Future for j := 0; j < N; j++ { @@ -158,6 +161,7 @@ func BenchmarkClientFutureTyped(b *testing.B) { b.Errorf("No connection available") } + b.ResetTimer() for i := 0; i < b.N; i += N { var fs [N]*Future for j := 0; j < N; j++ { @@ -191,6 +195,7 @@ func BenchmarkClientFutureParallel(b *testing.B) { b.Errorf("No connection available") } + b.ResetTimer() b.RunParallel(func(pb *testing.PB) { exit := false for !exit { @@ -227,6 +232,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { b.Fatal("No connection available") } + b.ResetTimer() b.RunParallel(func(pb *testing.PB) { exit := false for !exit { @@ -266,6 +272,7 @@ func BenchmarkClientParallel(b *testing.B) { b.Fatal("No connection available") } + b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { _, err := conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) @@ -307,6 +314,7 @@ func BenchmarkClientParallelMassive(b *testing.B) { } }() } + b.ResetTimer() for i := 0; i < b.N; i++ { wg.Add(1) limit <- struct{}{} @@ -344,6 +352,8 @@ func BenchmarkClientParallelMassiveUntyped(b *testing.B) { } }() } + + b.ResetTimer() for i := 0; i < b.N; i++ { wg.Add(1) limit <- struct{}{} From 976020c610ac34b4789a5e896ceb532e6dac285a Mon Sep 17 00:00:00 2001 From: Rianov Viacheslav Date: Thu, 28 Apr 2022 14:41:55 +0300 Subject: [PATCH 271/605] test: add benchmark tests Added benchmarks of large Select and Replace. Added a new target in Makefile for running benchmark tests. Added a new space in config.lua for large Select tests. Added a new target in Makefile for measuring performance degradation between current changes and master. Added a new line in gitignore for ignoring artifacts from bench target. Added a new step for running benchmark tests in ci. Added description to the CONTRIBUTING.md for how to run benchmark tests. Closes #122 --- .github/workflows/testing.yml | 3 + .gitignore | 1 + CONTRIBUTING.md | 107 ++++++++++++++++++++++++++++++++++ Makefile | 43 ++++++++++++++ config.lua | 26 +++++++++ tarantool_test.go | 51 ++++++++++++++++ 6 files changed, 231 insertions(+) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 26d1a24d6..c3793a741 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -63,3 +63,6 @@ jobs: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | make coveralls + + - name: Check workability of benchmark tests + run: make bench-deps bench DURATION=1x COUNT=1 diff --git a/.gitignore b/.gitignore index 8958050e9..fcd3c3236 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .idea/ work_dir* .rocks +bench* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c42babc35..1cdde994b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,113 @@ and run it with next flags: golangci-lint run -E gofmt -D errcheck ``` +## Benchmarking + +### Quick start + +To run all benchmark tests from the current branch run: + +```bash +make bench +``` + +To measure performance difference between master and the current branch run: + +```bash +make bench-diff +``` + +Note: `benchstat` should be in `PATH`. If it is not set, call: + +```bash +export PATH="/home/${USER}/go/bin:${PATH}" +``` + +or + +```bash +export PATH="${HOME}/go/bin:${PATH}" +``` + +### Customize benchmarking + +Before running benchmark or measuring performance degradation, install benchmark dependencies: +```bash +make bench-deps BENCH_PATH=custom_path +``` + +Use the variable `BENCH_PATH` to specify the path of benchmark artifacts. +It is set to `bench` by default. + +To run benchmark tests, call: +```bash +make bench DURATION=5s COUNT=7 BENCH_PATH=custom_path TEST_PATH=. +``` + +Use the variable `DURATION` to set the duration of perf tests. That variable is mapped on +testing [flag](https://pkg.go.dev/cmd/go#hdr-Testing_flags) `-benchtime` for gotest. +It may take the values in seconds (e.g, `5s`) or count of iterations (e.g, `1000x`). +It is set to `3s` by default. + +Use the variable `COUNT` to control the count of benchmark runs for each test. +It is set to `5` by default. That variable is mapped on testing flag `-count`. +Use higher values if the benchmark numbers aren't stable. + +Use the variable `TEST_PATH` to set the directory of test files. +It is set to `./...` by default, so it runs all the Benchmark tests in the project. + +To measure performance degradation after changes in code, run: +```bash +make bench-diff BENCH_PATH=custom_path +``` + +Note: the variable `BENCH_PATH` is not purposed to be used with absolute paths. + +## Recommendations for how to achieve stable results + +Before any judgments, verify whether results are stable on given host and how large the noise. Run `make bench-diff` without changes and look on the report. Several times. + +There are suggestions how to achieve best results: + +* Close all background applications, especially web browser. Look at `top` (`htop`, `atop`, ...) and if something bubbles there, close it. +* Disable cron daemon. +* Disable TurboBoost and set fixed frequency. + * If you're using `intel_pstate` frequency driver (it is usually default): + + Disable TurboBoost: + + ```shell + $ echo 0 > /sys/devices/system/cpu/intel_pstate/no_turbo + ``` + + Set fixed frequency: not sure it is possible. + + * If you're using `acpi-cpufreq` driver: + + Ensure you actually don't use intel_pstate: + + ```shell + $ grep -o 'intel_pstate=\w\+' /proc/cmdline + intel_pstate=disable + $ cpupower -c all frequency-info | grep driver: + driver: acpi-cpufreq + <...> + ``` + + Disable TurboBoost: + + ```shell + $ echo 0 > /sys/devices/system/cpu/cpufreq/boost + ``` + + Set fixed frequency: + + ```shell + $ cpupower -c all frequency-set -g userspace + $ cpupower -c all frequency-set -f 1.80GHz # adjust for your CPU + ``` + + ## Code review checklist - Public API contains functions, variables, constants that are needed from diff --git a/Makefile b/Makefile index ed94ae137..dfc38c496 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,18 @@ SHELL := /bin/bash COVERAGE_FILE := coverage.out +MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) +PROJECT_DIR := $(patsubst %/,%,$(dir $(MAKEFILE_PATH))) +DURATION ?= 3s +COUNT ?= 5 +BENCH_PATH ?= bench-dir +TEST_PATH ?= ${PROJECT_DIR}/... +BENCH_FILE := ${PROJECT_DIR}/${BENCH_PATH}/bench.txt +REFERENCE_FILE := ${PROJECT_DIR}/${BENCH_PATH}/reference.txt +BENCH_FILES := ${REFERENCE_FILE} ${BENCH_FILE} +BENCH_REFERENCE_REPO := ${BENCH_PATH}/go-tarantool +BENCH_OPTIONS := -bench=. -run=^Benchmark -benchmem -benchtime=${DURATION} -count=${COUNT} +GO_TARANTOOL_URL := https://github.com/tarantool/go-tarantool +GO_TARANTOOL_DIR := ${PROJECT_DIR}/${BENCH_PATH}/go-tarantool .PHONY: clean clean: @@ -55,3 +68,33 @@ coverage: coveralls: coverage go get github.com/mattn/goveralls goveralls -coverprofile=$(COVERAGE_FILE) -service=github + +.PHONY: bench-deps +${BENCH_PATH} bench-deps: + @echo "Installing benchstat tool" + rm -rf ${BENCH_PATH} + mkdir ${BENCH_PATH} + go clean -testcache + cd ${BENCH_PATH} && git clone https://go.googlesource.com/perf && cd perf && go install ./cmd/benchstat + rm -rf ${BENCH_PATH}/perf + +.PHONY: bench +${BENCH_FILE} bench: ${BENCH_PATH} + @echo "Running benchmark tests from the current branch" + go test ${TEST_PATH} ${BENCH_OPTIONS} 2>&1 \ + | tee ${BENCH_FILE} + benchstat ${BENCH_FILE} + +${GO_TARANTOOL_DIR}: + @echo "Cloning the repository into ${GO_TARANTOOL_DIR}" + [ ! -e ${GO_TARANTOOL_DIR} ] && git clone --depth=1 ${GO_TARANTOOL_URL} ${GO_TARANTOOL_DIR} + +${REFERENCE_FILE}: ${GO_TARANTOOL_DIR} + @echo "Running benchmark tests from master for using results in bench-diff target" + cd ${GO_TARANTOOL_DIR} && git pull && go test ./... ${BENCH_OPTIONS} 2>&1 \ + | tee ${REFERENCE_FILE} + +bench-diff: ${BENCH_FILES} + @echo "Comparing performance between master and the current branch" + @echo "'old' is a version in master branch, 'new' is a version in a current branch" + benchstat ${BENCH_FILES} | grep -v pkg: diff --git a/config.lua b/config.lua index c768cd746..06bec1303 100644 --- a/config.lua +++ b/config.lua @@ -40,6 +40,31 @@ box.once("init", function() }) st:truncate() + local s2 = box.schema.space.create('test_perf', { + id = 520, + temporary = true, + if_not_exists = true, + field_count = 3, + format = { + {name = "id", type = "unsigned"}, + {name = "name", type = "string"}, + {name = "arr1", type = "array"}, + }, + }) + s2:create_index('primary', {type = 'tree', unique = true, parts = {1, 'unsigned'}, if_not_exists = true}) + s2:create_index('secondary', {id = 5, type = 'tree', unique = false, parts = {2, 'string'}, if_not_exists = true}) + local arr_data = {} + for i = 1,100 do + arr_data[i] = i + end + for i = 1,1000 do + s2:insert{ + i, + 'test_name', + arr_data, + } + end + --box.schema.user.grant('guest', 'read,write,execute', 'universe') box.schema.func.create('box.info') box.schema.func.create('simple_incr') @@ -49,6 +74,7 @@ box.once("init", function() box.schema.user.grant('test', 'execute', 'universe') box.schema.user.grant('test', 'read,write', 'space', 'test') box.schema.user.grant('test', 'read,write', 'space', 'schematest') + box.schema.user.grant('test', 'read,write', 'space', 'test_perf') end) local function func_name() diff --git a/tarantool_test.go b/tarantool_test.go index 2aaee1b45..fcfed56e4 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -362,6 +362,57 @@ func BenchmarkClientParallelMassiveUntyped(b *testing.B) { close(limit) } +func BenchmarkClientReplaceParallel(b *testing.B) { + conn, err := Connect(server, opts) + if err != nil { + b.Errorf("No connection available") + return + } + defer conn.Close() + spaceNo = 520 + + rSpaceNo, _, err := conn.Schema.ResolveSpaceIndex("test_perf", "secondary") + if err != nil { + b.Fatalf("Space is not resolved: %s", err.Error()) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := conn.Replace(rSpaceNo, []interface{}{uint(1), "hello", []interface{}{}}) + if err != nil { + b.Error(err) + } + } + }) +} + +func BenchmarkClientLargeSelectParallel(b *testing.B) { + conn, err := Connect(server, opts) + if err != nil { + b.Errorf("No connection available") + return + } + defer conn.Close() + + schema := conn.Schema + rSpaceNo, rIndexNo, err := schema.ResolveSpaceIndex("test_perf", "secondary") + if err != nil { + b.Fatalf("symbolic space and index params not resolved") + } + + offset, limit := uint32(0), uint32(1000) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := conn.Select(rSpaceNo, rIndexNo, offset, limit, IterEq, []interface{}{"test_name"}) + if err != nil { + b.Fatal(err) + } + } + }) +} + /////////////////// func TestClient(t *testing.T) { From a54d9f6035f5dafab607240b5fa03efa8c8fcf42 Mon Sep 17 00:00:00 2001 From: Alexander Turenko Date: Wed, 18 May 2022 23:53:47 +0300 Subject: [PATCH 272/605] doc: remove trailing whitespaces from dev guide --- CONTRIBUTING.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1cdde994b..a276cd6e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,7 @@ make test-multi test-uuid test-main ``` To check if the current changes will pass the linter in CI, install -golnagci-lint from [sources](https://golangci-lint.run/usage/install/) +golnagci-lint from [sources](https://golangci-lint.run/usage/install/) and run it with next flags: ```bash golangci-lint run -E gofmt -D errcheck @@ -49,7 +49,7 @@ golangci-lint run -E gofmt -D errcheck ### Quick start -To run all benchmark tests from the current branch run: +To run all benchmark tests from the current branch run: ```bash make bench @@ -93,7 +93,7 @@ testing [flag](https://pkg.go.dev/cmd/go#hdr-Testing_flags) `-benchtime` for got It may take the values in seconds (e.g, `5s`) or count of iterations (e.g, `1000x`). It is set to `3s` by default. -Use the variable `COUNT` to control the count of benchmark runs for each test. +Use the variable `COUNT` to control the count of benchmark runs for each test. It is set to `5` by default. That variable is mapped on testing flag `-count`. Use higher values if the benchmark numbers aren't stable. @@ -119,17 +119,17 @@ There are suggestions how to achieve best results: * If you're using `intel_pstate` frequency driver (it is usually default): Disable TurboBoost: - + ```shell $ echo 0 > /sys/devices/system/cpu/intel_pstate/no_turbo ``` - + Set fixed frequency: not sure it is possible. * If you're using `acpi-cpufreq` driver: - + Ensure you actually don't use intel_pstate: - + ```shell $ grep -o 'intel_pstate=\w\+' /proc/cmdline intel_pstate=disable @@ -137,21 +137,20 @@ There are suggestions how to achieve best results: driver: acpi-cpufreq <...> ``` - + Disable TurboBoost: - + ```shell $ echo 0 > /sys/devices/system/cpu/cpufreq/boost ``` - + Set fixed frequency: - + ```shell $ cpupower -c all frequency-set -g userspace $ cpupower -c all frequency-set -f 1.80GHz # adjust for your CPU ``` - ## Code review checklist - Public API contains functions, variables, constants that are needed from From a9e491fa36d9601e2d8063b50a97d33d19f62e05 Mon Sep 17 00:00:00 2001 From: vr009 Date: Thu, 3 Mar 2022 09:23:10 +0300 Subject: [PATCH 273/605] sql: add minimal sql support This patch adds the support of SQL in connector. Added support of positional and named arguments. Added ExecuteTyped() method for use with custom packing/unpacking for a type. Added all required constants to const.go for encoding SQL in msgpack and decoding response. Added SQL tests. Updated config.lua for creation the space for using SQL in tests. Added the check of Tarantool version to skip SQL tests if tarantool version < 2.0.0. Changed id of the test spaces with id=512 and id=514, cause if using SQL in tarantool there is no ability to set space id explicitly, so it gets created with id=512 by default and conflicts with already existing space with the same id. Added new dependency in go.sum, go.mod for using assert package. Added examples of using SQL queries in example_test.go for compiling the future documentation from sources. Added notes about the version since which Execute() is supported. Closes #62 --- CHANGELOG.md | 1 + config.lua | 20 +- connector.go | 1 + const.go | 16 + example_custom_unpacking_test.go | 2 +- example_test.go | 141 +++++++- multi/multi.go | 7 + request.go | 171 +++++++++ response.go | 108 +++++- tarantool_test.go | 575 ++++++++++++++++++++++++++++++- 10 files changed, 1012 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6168ede00..854ce6153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Go modules support (#91) - queue-utube handling (#85) - Master discovery (#113) +- SQL support (#62) ### Fixed diff --git a/config.lua b/config.lua index 06bec1303..d024b3a28 100644 --- a/config.lua +++ b/config.lua @@ -6,13 +6,25 @@ box.cfg{ box.once("init", function() local s = box.schema.space.create('test', { - id = 512, + id = 517, if_not_exists = true, }) s:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) + local sp = box.schema.space.create('SQL_TEST', { + id = 519, + if_not_exists = true, + format = { + {name = "NAME0", type = "unsigned"}, + {name = "NAME1", type = "string"}, + {name = "NAME2", type = "string"}, + } + }) + sp:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) + sp:insert{1, "test", "test"} + local st = box.schema.space.create('schematest', { - id = 514, + id = 516, temporary = true, if_not_exists = true, field_count = 7, @@ -75,6 +87,10 @@ box.once("init", function() box.schema.user.grant('test', 'read,write', 'space', 'test') box.schema.user.grant('test', 'read,write', 'space', 'schematest') box.schema.user.grant('test', 'read,write', 'space', 'test_perf') + + -- grants for sql tests + box.schema.user.grant('test', 'create,read,write,drop,alter', 'space') + box.schema.user.grant('test', 'create', 'sequence') end) local function func_name() diff --git a/connector.go b/connector.go index d6d87eaca..0e79c6aaf 100644 --- a/connector.go +++ b/connector.go @@ -17,6 +17,7 @@ type Connector interface { Call(functionName string, args interface{}) (resp *Response, err error) Call17(functionName string, args interface{}) (resp *Response, err error) Eval(expr string, args interface{}) (resp *Response, err error) + Execute(expr string, args interface{}) (resp *Response, err error) GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key interface{}, result interface{}) (err error) diff --git a/const.go b/const.go index 03b00c6b1..5152f8e43 100644 --- a/const.go +++ b/const.go @@ -11,6 +11,7 @@ const ( EvalRequest = 8 UpsertRequest = 9 Call17Request = 10 + ExecuteRequest = 11 PingRequest = 64 SubscribeRequest = 66 @@ -29,6 +30,19 @@ const ( KeyDefTuple = 0x28 KeyData = 0x30 KeyError = 0x31 + KeyMetaData = 0x32 + KeySQLText = 0x40 + KeySQLBind = 0x41 + KeySQLInfo = 0x42 + + KeyFieldName = 0x00 + KeyFieldType = 0x01 + KeyFieldColl = 0x02 + KeyFieldIsNullable = 0x03 + KeyIsAutoincrement = 0x04 + KeyFieldSpan = 0x05 + KeySQLInfoRowCount = 0x00 + KeySQLInfoAutoincrementIds = 0x01 // https://github.com/fl00r/go-tarantool-1.6/issues/2 @@ -49,4 +63,6 @@ const ( OkCode = uint32(0) ErrorCodeBit = 0x8000 PacketLengthBytes = 5 + ErSpaceExistsCode = 0xa + IteratorCode = 0x14 ) diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index a6f9ab55e..1bc955151 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -87,7 +87,7 @@ func Example_customUnpacking() { log.Fatalf("Failed to connect: %s", err.Error()) } - spaceNo := uint32(512) + spaceNo := uint32(517) indexNo := uint32(0) tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} diff --git a/example_test.go b/example_test.go index 0a6b6cb37..386ad11f2 100644 --- a/example_test.go +++ b/example_test.go @@ -2,6 +2,7 @@ package tarantool_test import ( "fmt" + "github.com/tarantool/go-tarantool/test_helpers" "time" "github.com/tarantool/go-tarantool" @@ -31,7 +32,8 @@ func ExampleConnection_Select() { conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) conn.Replace(spaceNo, []interface{}{uint(1112), "hallo", "werld"}) - resp, err := conn.Select(512, 0, 0, 100, tarantool.IterEq, []interface{}{uint(1111)}) + resp, err := conn.Select(517, 0, 0, 100, tarantool.IterEq, []interface{}{uint(1111)}) + if err != nil { fmt.Printf("error in select is %v", err) return @@ -53,7 +55,9 @@ func ExampleConnection_SelectTyped() { conn := example_connect() defer conn.Close() var res []Tuple - err := conn.SelectTyped(512, 0, 0, 100, tarantool.IterEq, tarantool.IntKey{1111}, &res) + + err := conn.SelectTyped(517, 0, 0, 100, tarantool.IterEq, tarantool.IntKey{1111}, &res) + if err != nil { fmt.Printf("error in select is %v", err) return @@ -73,6 +77,7 @@ func ExampleConnection_SelectTyped() { func ExampleConnection_SelectAsync() { conn := example_connect() defer conn.Close() + spaceNo := uint32(517) conn.Insert(spaceNo, []interface{}{uint(16), "test", "one"}) conn.Insert(spaceNo, []interface{}{uint(17), "test", "one"}) @@ -320,12 +325,12 @@ func ExampleSchema() { } space1 := schema.Spaces["test"] - space2 := schema.SpacesById[514] + space2 := schema.SpacesById[516] fmt.Printf("Space 1 ID %d %s\n", space1.Id, space1.Name) fmt.Printf("Space 2 ID %d %s\n", space2.Id, space2.Name) // Output: - // Space 1 ID 512 test - // Space 2 ID 514 schematest + // Space 1 ID 517 test + // Space 2 ID 516 schematest } // Example demonstrates how to retrieve information with space schema. @@ -344,7 +349,7 @@ func ExampleSpace() { // Access Space objects by name or ID. space1 := schema.Spaces["test"] - space2 := schema.SpacesById[514] // It's a map. + space2 := schema.SpacesById[516] // It's a map. fmt.Printf("Space 1 ID %d %s %s\n", space1.Id, space1.Name, space1.Engine) fmt.Printf("Space 1 ID %d %t\n", space1.FieldsCount, space1.Temporary) @@ -365,10 +370,132 @@ func ExampleSpace() { fmt.Printf("SpaceField 2 %s %s\n", spaceField2.Name, spaceField2.Type) // Output: - // Space 1 ID 512 test memtx + // Space 1 ID 517 test memtx // Space 1 ID 0 false // Index 0 primary // &{0 unsigned} &{2 string} // SpaceField 1 name0 unsigned // SpaceField 2 name3 unsigned } + +// To use SQL to query a tarantool instance, call Execute. +// +// Pay attention that with different types of queries (DDL, DQL, DML etc.) +// some fields of the response structure (MetaData and InfoAutoincrementIds in SQLInfo) may be nil. +func ExampleConnection_Execute() { + // Tarantool supports SQL since version 2.0.0 + isLess, _ := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if isLess { + return + } + server := "127.0.0.1:3013" + opts := tarantool.Opts{ + Timeout: 500 * time.Millisecond, + Reconnect: 1 * time.Second, + MaxReconnects: 3, + User: "test", + Pass: "test", + } + client, err := tarantool.Connect(server, opts) + if err != nil { + fmt.Printf("Failed to connect: %s", err.Error()) + } + + resp, err := client.Execute("CREATE TABLE SQL_TEST (id INTEGER PRIMARY KEY, name STRING)", []interface{}{}) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // there are 4 options to pass named parameters to an SQL query + // the simple map: + sqlBind1 := map[string]interface{}{ + "id": 1, + "name": "test", + } + + // any type of structure + sqlBind2 := struct { + Id int + Name string + }{1, "test"} + + // it is possible to use []tarantool.KeyValueBind + sqlBind3 := []interface{}{ + tarantool.KeyValueBind{Key: "id", Value: 1}, + tarantool.KeyValueBind{Key: "name", Value: "test"}, + } + + // or []interface{} slice with tarantool.KeyValueBind items inside + sqlBind4 := []tarantool.KeyValueBind{ + {"id", 1}, + {"name", "test"}, + } + + // the next usage + resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=:name", sqlBind1) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // the same as + resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=:name", sqlBind2) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // the same as + resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=:name", sqlBind3) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // the same as + resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=:name", sqlBind4) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // the way to pass positional arguments to an SQL query + resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=? AND name=?", []interface{}{2, "test"}) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // the way to pass SQL expression with using custom packing/unpacking for a type + var res []Tuple + sqlInfo, metaData, err := client.ExecuteTyped("SELECT id, name, name FROM SQL_TEST WHERE id=?", []interface{}{2}, &res) + fmt.Println("ExecuteTyped") + fmt.Println("Error", err) + fmt.Println("Data", res) + fmt.Println("MetaData", metaData) + fmt.Println("SQL Info", sqlInfo) + + // for using different types of parameters (positioned/named), collect all items in []interface{} + // all "named" items must be passed with tarantool.KeyValueBind{} + resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=?", + []interface{}{tarantool.KeyValueBind{"id", 1}, "test"}) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) +} diff --git a/multi/multi.go b/multi/multi.go index c83010c36..89ec33ad6 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -340,6 +340,13 @@ func (connMulti *ConnectionMulti) Eval(expr string, args interface{}) (resp *tar return connMulti.getCurrentConnection().Eval(expr, args) } +// Execute passes sql expression to Tarantool for execution. +// +// Since 1.6.0 +func (connMulti *ConnectionMulti) Execute(expr string, args interface{}) (resp *tarantool.Response, err error) { + return connMulti.getCurrentConnection().Execute(expr, args) +} + // GetTyped performs select (with limit = 1 and offset = 0) to box space and // fills typed result. func (connMulti *ConnectionMulti) GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) { diff --git a/request.go b/request.go index 6065959c4..dd9486ae1 100644 --- a/request.go +++ b/request.go @@ -2,6 +2,9 @@ package tarantool import ( "errors" + "reflect" + "strings" + "sync" "time" "gopkg.in/vmihailenco/msgpack.v2" @@ -120,6 +123,14 @@ func (conn *Connection) Eval(expr string, args interface{}) (resp *Response, err return conn.EvalAsync(expr, args).Get() } +// Execute passes sql expression to Tarantool for execution. +// +// It is equal to conn.ExecuteAsync(expr, args).Get(). +// Since 1.6.0 +func (conn *Connection) Execute(expr string, args interface{}) (resp *Response, err error) { + return conn.ExecuteAsync(expr, args).Get() +} + // single used for conn.GetTyped for decode one tuple. type single struct { res interface{} @@ -212,6 +223,16 @@ func (conn *Connection) EvalTyped(expr string, args interface{}, result interfac return conn.EvalAsync(expr, args).GetTyped(result) } +// ExecuteTyped passes sql expression to Tarantool for execution. +// +// In addition to error returns sql info and columns meta data +// Since 1.6.0 +func (conn *Connection) ExecuteTyped(expr string, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error) { + fut := conn.ExecuteAsync(expr, args) + err := fut.GetTyped(&result) + return fut.resp.SQLInfo, fut.resp.MetaData, err +} + // SelectAsync sends select request to Tarantool and returns Future. func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Future { future := conn.newFuture(SelectRequest) @@ -346,10 +367,160 @@ func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { }) } +// ExecuteAsync sends a sql expression for execution and returns Future. +// Since 1.6.0 +func (conn *Connection) ExecuteAsync(expr string, args interface{}) *Future { + future := conn.newFuture(ExecuteRequest) + return future.send(conn, func(enc *msgpack.Encoder) error { + enc.EncodeMapLen(2) + enc.EncodeUint64(KeySQLText) + enc.EncodeString(expr) + enc.EncodeUint64(KeySQLBind) + return encodeSQLBind(enc, args) + }) +} + +// KeyValueBind is a type for encoding named SQL parameters +type KeyValueBind struct { + Key string + Value interface{} +} + // // private // +// this map is needed for caching names of struct fields in lower case +// to avoid extra allocations in heap by calling strings.ToLower() +var lowerCaseNames sync.Map + +func encodeSQLBind(enc *msgpack.Encoder, from interface{}) error { + // internal function for encoding single map in msgpack + encodeKeyInterface := func(key string, val interface{}) error { + if err := enc.EncodeMapLen(1); err != nil { + return err + } + if err := enc.EncodeString(":" + key); err != nil { + return err + } + if err := enc.Encode(val); err != nil { + return err + } + return nil + } + + encodeKeyValue := func(key string, val reflect.Value) error { + if err := enc.EncodeMapLen(1); err != nil { + return err + } + if err := enc.EncodeString(":" + key); err != nil { + return err + } + if err := enc.EncodeValue(val); err != nil { + return err + } + return nil + } + + encodeNamedFromMap := func(mp map[string]interface{}) error { + if err := enc.EncodeSliceLen(len(mp)); err != nil { + return err + } + for k, v := range mp { + if err := encodeKeyInterface(k, v); err != nil { + return err + } + } + return nil + } + + encodeNamedFromStruct := func(val reflect.Value) error { + if err := enc.EncodeSliceLen(val.NumField()); err != nil { + return err + } + cached, ok := lowerCaseNames.Load(val.Type()) + if !ok { + fields := make([]string, val.NumField()) + for i := 0; i < val.NumField(); i++ { + key := val.Type().Field(i).Name + fields[i] = strings.ToLower(key) + v := val.Field(i) + if err := encodeKeyValue(fields[i], v); err != nil { + return err + } + } + lowerCaseNames.Store(val.Type(), fields) + return nil + } + + fields := cached.([]string) + for i := 0; i < val.NumField(); i++ { + k := fields[i] + v := val.Field(i) + if err := encodeKeyValue(k, v); err != nil { + return err + } + } + return nil + } + + encodeSlice := func(from interface{}) error { + castedSlice, ok := from.([]interface{}) + if !ok { + castedKVSlice := from.([]KeyValueBind) + t := len(castedKVSlice) + if err := enc.EncodeSliceLen(t); err != nil { + return err + } + for _, v := range castedKVSlice { + if err := encodeKeyInterface(v.Key, v.Value); err != nil { + return err + } + } + return nil + } + + if err := enc.EncodeSliceLen(len(castedSlice)); err != nil { + return err + } + for i := 0; i < len(castedSlice); i++ { + if kvb, ok := castedSlice[i].(KeyValueBind); ok { + k := kvb.Key + v := kvb.Value + if err := encodeKeyInterface(k, v); err != nil { + return err + } + } else { + if err := enc.Encode(castedSlice[i]); err != nil { + return err + } + } + } + return nil + } + + val := reflect.ValueOf(from) + switch val.Kind() { + case reflect.Map: + mp, ok := from.(map[string]interface{}) + if !ok { + return errors.New("failed to encode map: wrong format") + } + if err := encodeNamedFromMap(mp); err != nil { + return err + } + case reflect.Struct: + if err := encodeNamedFromStruct(val); err != nil { + return err + } + case reflect.Slice, reflect.Array: + if err := encodeSlice(from); err != nil { + return err + } + } + return nil +} + func (fut *Future) pack(h *smallWBuf, enc *msgpack.Encoder, body func(*msgpack.Encoder) error) (err error) { rid := fut.requestId hl := h.Len() diff --git a/response.go b/response.go index c56eaa483..9fcca64da 100644 --- a/response.go +++ b/response.go @@ -9,10 +9,94 @@ import ( type Response struct { RequestId uint32 Code uint32 - Error string // Error message. - // Data contains deserialized data for untyped requests. - Data []interface{} - buf smallBuf + Error string // error message + // Data contains deserialized data for untyped requests + Data []interface{} + MetaData []ColumnMetaData + SQLInfo SQLInfo + buf smallBuf +} + +type ColumnMetaData struct { + FieldName string + FieldType string + FieldCollation string + FieldIsNullable bool + FieldIsAutoincrement bool + FieldSpan string +} + +type SQLInfo struct { + AffectedCount uint64 + InfoAutoincrementIds []uint64 +} + +func (meta *ColumnMetaData) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeMapLen(); err != nil { + return err + } + if l == 0 { + return fmt.Errorf("map len doesn't match: %d", l) + } + for i := 0; i < l; i++ { + var mk uint64 + var mv interface{} + if mk, err = d.DecodeUint64(); err != nil { + return fmt.Errorf("failed to decode meta data") + } + if mv, err = d.DecodeInterface(); err != nil { + return fmt.Errorf("failed to decode meta data") + } + switch mk { + case KeyFieldName: + meta.FieldName = mv.(string) + case KeyFieldType: + meta.FieldType = mv.(string) + case KeyFieldColl: + meta.FieldCollation = mv.(string) + case KeyFieldIsNullable: + meta.FieldIsNullable = mv.(bool) + case KeyIsAutoincrement: + meta.FieldIsAutoincrement = mv.(bool) + case KeyFieldSpan: + meta.FieldSpan = mv.(string) + default: + return fmt.Errorf("failed to decode meta data") + } + } + return nil +} + +func (info *SQLInfo) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeMapLen(); err != nil { + return err + } + if l == 0 { + return fmt.Errorf("map len doesn't match") + } + for i := 0; i < l; i++ { + var mk uint64 + if mk, err = d.DecodeUint64(); err != nil { + return fmt.Errorf("failed to decode meta data") + } + switch mk { + case KeySQLInfoRowCount: + if info.AffectedCount, err = d.DecodeUint64(); err != nil { + return fmt.Errorf("failed to decode meta data") + } + case KeySQLInfoAutoincrementIds: + if err = d.Decode(&info.InfoAutoincrementIds); err != nil { + return fmt.Errorf("failed to decode meta data") + } + default: + return fmt.Errorf("failed to decode meta data") + } + } + return nil } func (resp *Response) smallInt(d *msgpack.Decoder) (i int, err error) { @@ -86,6 +170,14 @@ func (resp *Response) decodeBody() (err error) { if resp.Error, err = d.DecodeString(); err != nil { return err } + case KeySQLInfo: + if err = d.Decode(&resp.SQLInfo); err != nil { + return err + } + case KeyMetaData: + if err = d.Decode(&resp.MetaData); err != nil { + return err + } default: if err = d.Skip(); err != nil { return err @@ -121,6 +213,14 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if resp.Error, err = d.DecodeString(); err != nil { return err } + case KeySQLInfo: + if err = d.Decode(&resp.SQLInfo); err != nil { + return err + } + case KeyMetaData: + if err = d.Decode(&resp.MetaData); err != nil { + return err + } default: if err = d.Skip(); err != nil { return err diff --git a/tarantool_test.go b/tarantool_test.go index fcfed56e4..3a6a839e9 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2,8 +2,10 @@ package tarantool_test import ( "fmt" + "github.com/stretchr/testify/assert" "log" "os" + "reflect" "strings" "sync" "testing" @@ -52,7 +54,7 @@ func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { } var server = "127.0.0.1:3013" -var spaceNo = uint32(512) +var spaceNo = uint32(517) var spaceName = "test" var indexNo = uint32(0) var indexName = "primary" @@ -413,6 +415,74 @@ func BenchmarkClientLargeSelectParallel(b *testing.B) { }) } +func BenchmarkSQLParallel(b *testing.B) { + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + b.Fatal("Could not check the Tarantool version") + } + if isLess { + b.Skip() + } + + conn, err := Connect(server, opts) + if err != nil { + b.Errorf("No connection available") + return + } + defer conn.Close() + + spaceNo := 519 + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Errorf("No connection available") + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", []interface{}{uint(1111)}) + if err != nil { + b.Errorf("Select failed: %s", err.Error()) + break + } + } + }) +} + +func BenchmarkSQLSerial(b *testing.B) { + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + b.Fatal("Could not check the Tarantool version") + } + if isLess { + b.Skip() + } + + conn, err := Connect(server, opts) + if err != nil { + b.Errorf("Failed to connect: %s", err) + return + } + defer conn.Close() + + spaceNo := 519 + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Errorf("Failed to replace: %s", err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", []interface{}{uint(1111)}) + if err != nil { + b.Errorf("Select failed: %s", err.Error()) + break + } + } +} + /////////////////// func TestClient(t *testing.T) { @@ -728,6 +798,479 @@ func TestClient(t *testing.T) { } } +const ( + createTableQuery = "CREATE TABLE SQL_SPACE (id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING COLLATE \"unicode\" DEFAULT NULL);" + insertQuery = "INSERT INTO SQL_SPACE VALUES (?, ?);" + selectNamedQuery = "SELECT id, name FROM SQL_SPACE WHERE id=:id AND name=:name;" + selectPosQuery = "SELECT id, name FROM SQL_SPACE WHERE id=? AND name=?;" + updateQuery = "UPDATE SQL_SPACE SET name=? WHERE id=?;" + enableFullMetaDataQuery = "SET SESSION \"sql_full_metadata\" = true;" + selectSpanDifQuery = "SELECT id*2, name, id FROM SQL_SPACE WHERE name=?;" + alterTableQuery = "ALTER TABLE SQL_SPACE RENAME TO SQL_SPACE2;" + insertIncrQuery = "INSERT INTO SQL_SPACE2 VALUES (?, ?);" + deleteQuery = "DELETE FROM SQL_SPACE2 WHERE name=?;" + dropQuery = "DROP TABLE SQL_SPACE2;" + dropQuery2 = "DROP TABLE SQL_SPACE;" + disableFullMetaDataQuery = "SET SESSION \"sql_full_metadata\" = false;" + + selectTypedQuery = "SELECT NAME1, NAME0 FROM SQL_TEST WHERE NAME0=?" + selectNamedQuery2 = "SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:id AND NAME1=:name;" + selectPosQuery2 = "SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=? AND NAME1=?;" + mixedQuery = "SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:name0 AND NAME1=?;" +) + +func TestSQL(t *testing.T) { + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + if isLess { + t.Skip() + } + + type testCase struct { + Query string + Args interface{} + Resp Response + } + + testCases := []testCase{ + { + createTableQuery, + []interface{}{}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + insertQuery, + []interface{}{1, "test"}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + selectNamedQuery, + map[string]interface{}{ + "id": 1, + "name": "test", + }, + Response{ + SQLInfo: SQLInfo{AffectedCount: 0}, + Data: []interface{}{[]interface{}{uint64(1), "test"}}, + MetaData: []ColumnMetaData{ + {FieldType: "integer", FieldName: "ID"}, + {FieldType: "string", FieldName: "NAME"}}, + }, + }, + { + selectPosQuery, + []interface{}{1, "test"}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 0}, + Data: []interface{}{[]interface{}{uint64(1), "test"}}, + MetaData: []ColumnMetaData{ + {FieldType: "integer", FieldName: "ID"}, + {FieldType: "string", FieldName: "NAME"}}, + }, + }, + { + updateQuery, + []interface{}{"test_test", 1}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + enableFullMetaDataQuery, + []interface{}{}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + selectSpanDifQuery, + []interface{}{"test_test"}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 0}, Data: []interface{}{[]interface{}{uint64(2), "test_test", uint64(1)}}, + MetaData: []ColumnMetaData{ + { + FieldType: "integer", + FieldName: "COLUMN_1", + FieldIsNullable: false, + FieldIsAutoincrement: false, + FieldSpan: "id*2", + }, + { + FieldType: "string", + FieldName: "NAME", + FieldIsNullable: true, + FieldIsAutoincrement: false, + FieldSpan: "name", + FieldCollation: "unicode", + }, + { + FieldType: "integer", + FieldName: "ID", + FieldIsNullable: false, + FieldIsAutoincrement: true, + FieldSpan: "id", + }, + }}, + }, + { + alterTableQuery, + []interface{}{}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 0}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + insertIncrQuery, + []interface{}{2, "test_2"}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1, InfoAutoincrementIds: []uint64{1}}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + deleteQuery, + []interface{}{"test_2"}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + dropQuery, + []interface{}{}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + disableFullMetaDataQuery, + []interface{}{}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + } + + var conn *Connection + conn, err = Connect(server, opts) + assert.Nil(t, err, "Failed to Connect") + assert.NotNil(t, conn, "conn is nil after Connect") + defer conn.Close() + + for i, test := range testCases { + resp, err := conn.Execute(test.Query, test.Args) + assert.NoError(t, err, "Failed to Execute, Query number: %d", i) + assert.NotNil(t, resp, "Response is nil after Execute\nQuery number: %d", i) + for j := range resp.Data { + assert.Equal(t, resp.Data[j], test.Resp.Data[j], "Response data is wrong") + } + assert.Equal(t, resp.SQLInfo.AffectedCount, test.Resp.SQLInfo.AffectedCount, "Affected count is wrong") + + errorMsg := "Response Metadata is wrong" + for j := range resp.MetaData { + assert.Equal(t, resp.MetaData[j].FieldIsAutoincrement, test.Resp.MetaData[j].FieldIsAutoincrement, errorMsg) + assert.Equal(t, resp.MetaData[j].FieldIsNullable, test.Resp.MetaData[j].FieldIsNullable, errorMsg) + assert.Equal(t, resp.MetaData[j].FieldCollation, test.Resp.MetaData[j].FieldCollation, errorMsg) + assert.Equal(t, resp.MetaData[j].FieldName, test.Resp.MetaData[j].FieldName, errorMsg) + assert.Equal(t, resp.MetaData[j].FieldSpan, test.Resp.MetaData[j].FieldSpan, errorMsg) + assert.Equal(t, resp.MetaData[j].FieldType, test.Resp.MetaData[j].FieldType, errorMsg) + } + } +} + +func TestSQLTyped(t *testing.T) { + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + t.Fatal("Could not check the Tarantool version") + } + if isLess { + t.Skip() + } + + var conn *Connection + + conn, err = Connect(server, opts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if conn == nil { + t.Fatal("conn is nil after Connect") + } + defer conn.Close() + + mem := []Member{} + info, meta, err := conn.ExecuteTyped(selectTypedQuery, []interface{}{1}, &mem) + if info.AffectedCount != 0 { + t.Errorf("Rows affected count must be 0") + } + if len(meta) != 2 { + t.Errorf("Meta data is not full") + } + if len(mem) != 1 { + t.Errorf("Wrong length of result") + } + if err != nil { + t.Error(err) + } +} + +func TestSQLBindings(t *testing.T) { + // Data for test table + testData := map[int]string{ + 1: "test", + } + + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + t.Fatal("Could not check the Tarantool version") + } + if isLess { + t.Skip() + } + + var resp *Response + var conn *Connection + + conn, err = Connect(server, opts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if conn == nil { + t.Fatal("conn is nil after Connect") + } + defer conn.Close() + + // test all types of supported bindings + // prepare named sql bind + sqlBind := map[string]interface{}{ + "id": 1, + "name": "test", + } + + sqlBind2 := struct { + Id int + Name string + }{1, "test"} + + sqlBind3 := []KeyValueBind{ + {"id", 1}, + {"name", "test"}, + } + + sqlBind4 := []interface{}{ + KeyValueBind{Key: "id", Value: 1}, + KeyValueBind{Key: "name", Value: "test"}, + } + + namedSQLBinds := []interface{}{ + sqlBind, + sqlBind2, + sqlBind3, + sqlBind4, + } + + //positioned sql bind + sqlBind5 := []interface{}{ + 1, "test", + } + + // mixed sql bind + sqlBind6 := []interface{}{ + KeyValueBind{Key: "name0", Value: 1}, + "test", + } + + for _, bind := range namedSQLBinds { + resp, err = conn.Execute(selectNamedQuery2, bind) + if err != nil { + t.Fatalf("Failed to Execute: %s", err.Error()) + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if reflect.DeepEqual(resp.Data[0], []interface{}{1, testData[1]}) { + t.Error("Select with named arguments failed") + } + if resp.MetaData[0].FieldType != "unsigned" || + resp.MetaData[0].FieldName != "NAME0" || + resp.MetaData[1].FieldType != "string" || + resp.MetaData[1].FieldName != "NAME1" { + t.Error("Wrong metadata") + } + } + + resp, err = conn.Execute(selectPosQuery2, sqlBind5) + if err != nil { + t.Fatalf("Failed to Execute: %s", err.Error()) + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if reflect.DeepEqual(resp.Data[0], []interface{}{1, testData[1]}) { + t.Error("Select with positioned arguments failed") + } + if resp.MetaData[0].FieldType != "unsigned" || + resp.MetaData[0].FieldName != "NAME0" || + resp.MetaData[1].FieldType != "string" || + resp.MetaData[1].FieldName != "NAME1" { + t.Error("Wrong metadata") + } + + resp, err = conn.Execute(mixedQuery, sqlBind6) + if err != nil { + t.Fatalf("Failed to Execute: %s", err.Error()) + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if reflect.DeepEqual(resp.Data[0], []interface{}{1, testData[1]}) { + t.Error("Select with positioned arguments failed") + } + if resp.MetaData[0].FieldType != "unsigned" || + resp.MetaData[0].FieldName != "NAME0" || + resp.MetaData[1].FieldType != "string" || + resp.MetaData[1].FieldName != "NAME1" { + t.Error("Wrong metadata") + } +} + +func TestStressSQL(t *testing.T) { + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + if isLess { + t.Skip() + } + + var resp *Response + var conn *Connection + + conn, err = Connect(server, opts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if conn == nil { + t.Fatalf("conn is nil after Connect") + } + defer conn.Close() + + resp, err = conn.Execute(createTableQuery, []interface{}{}) + if err != nil { + t.Fatalf("Failed to Execute: %s", err.Error()) + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if resp.Code != 0 { + t.Fatalf("Failed to Execute: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 1 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + } + + // create table with the same name + resp, err = conn.Execute(createTableQuery, []interface{}{}) + if err == nil { + t.Fatal("Unexpected lack of error") + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if resp.Code != ErSpaceExistsCode { + t.Fatalf("Unexpected response code: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + } + + // execute with nil argument + resp, err = conn.Execute(createTableQuery, nil) + if err == nil { + t.Fatal("Unexpected lack of error") + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if resp.Code == 0 { + t.Fatalf("Unexpected response code: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + } + + // execute with zero string + resp, err = conn.Execute("", []interface{}{}) + if err == nil { + t.Fatal("Unexpected lack of error") + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if resp.Code == 0 { + t.Fatalf("Unexpected response code: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + } + + // drop table query + resp, err = conn.Execute(dropQuery2, []interface{}{}) + if err != nil { + t.Fatalf("Failed to Execute: %s", err.Error()) + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if resp.Code != 0 { + t.Fatalf("Failed to Execute: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 1 { + t.Errorf("Incorrect count of dropped spaces: %d", resp.SQLInfo.AffectedCount) + } + + // drop the same table + resp, err = conn.Execute(dropQuery2, []interface{}{}) + if err == nil { + t.Fatal("Unexpected lack of error") + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if resp.Code == 0 { + t.Fatalf("Unexpected response code: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + } +} + func TestSchema(t *testing.T) { var err error var conn *Connection @@ -751,29 +1294,29 @@ func TestSchema(t *testing.T) { } var space, space2 *Space var ok bool - if space, ok = schema.SpacesById[514]; !ok { - t.Errorf("space with id = 514 was not found in schema.SpacesById") + if space, ok = schema.SpacesById[516]; !ok { + t.Errorf("space with id = 516 was not found in schema.SpacesById") } if space2, ok = schema.Spaces["schematest"]; !ok { t.Errorf("space with name 'schematest' was not found in schema.SpacesById") } if space != space2 { - t.Errorf("space with id = 514 and space with name schematest are different") + t.Errorf("space with id = 516 and space with name schematest are different") } - if space.Id != 514 { - t.Errorf("space 514 has incorrect Id") + if space.Id != 516 { + t.Errorf("space 516 has incorrect Id") } if space.Name != "schematest" { - t.Errorf("space 514 has incorrect Name") + t.Errorf("space 516 has incorrect Name") } if !space.Temporary { - t.Errorf("space 514 should be temporary") + t.Errorf("space 516 should be temporary") } if space.Engine != "memtx" { - t.Errorf("space 514 engine should be memtx") + t.Errorf("space 516 engine should be memtx") } if space.FieldsCount != 7 { - t.Errorf("space 514 has incorrect fields count") + t.Errorf("space 516 has incorrect fields count") } if space.FieldsById == nil { @@ -883,20 +1426,20 @@ func TestSchema(t *testing.T) { } var rSpaceNo, rIndexNo uint32 - rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex(514, 3) - if err != nil || rSpaceNo != 514 || rIndexNo != 3 { + rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex(516, 3) + if err != nil || rSpaceNo != 516 || rIndexNo != 3 { t.Errorf("numeric space and index params not resolved as-is") } - rSpaceNo, _, err = schema.ResolveSpaceIndex(514, nil) - if err != nil || rSpaceNo != 514 { + rSpaceNo, _, err = schema.ResolveSpaceIndex(516, nil) + if err != nil || rSpaceNo != 516 { t.Errorf("numeric space param not resolved as-is") } rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex("schematest", "secondary") - if err != nil || rSpaceNo != 514 || rIndexNo != 3 { + if err != nil || rSpaceNo != 516 || rIndexNo != 3 { t.Errorf("symbolic space and index params not resolved") } rSpaceNo, _, err = schema.ResolveSpaceIndex("schematest", nil) - if err != nil || rSpaceNo != 514 { + if err != nil || rSpaceNo != 516 { t.Errorf("symbolic space param not resolved") } _, _, err = schema.ResolveSpaceIndex("schematest22", "secondary") From 54464235a704bda00ea3324abe19df497347dccb Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 19 May 2022 18:25:26 +0300 Subject: [PATCH 274/605] test: fix coverage coverage accounting Commit "github-ci: add Coveralls support" (31ebde8f41b3caef17a44b51d69db19661d1b82d) introduced code coverage support. However there is wrong value for option -covermode was specified - "count". According to documentation [1] "count" calculates how many times did each statement run and "atomic" mode is like "count", but counts precisely in parallel programs. We don't use parallel mode in tests at all (-p 1) and for code coverage measurement particularly, but it is better using "atomic" mode instead "count". Option "-coverpkg" was missed. "coverpkg" applies coverage analysis in each test to packages matching the patterns. The default is for each test to analyze only the package being tested. Total statement coverage without -coverpkg is 66.8% and with -coverpkg is 69.3% according to "go tool cover". 1. https://go.dev/blog/cover --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index dfc38c496..d4d4cab17 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ test-main: coverage: go clean -testcache go get golang.org/x/tools/cmd/cover - go test ./... -v -p 1 -covermode=count -coverprofile=$(COVERAGE_FILE) + go test ./... -v -p 1 -covermode=atomic -coverprofile=$(COVERAGE_FILE) -coverpkg=./... .PHONY: coveralls coveralls: coverage From d44ffa0cda16579bf8ec41534067b29762f1326d Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 19 May 2022 18:32:40 +0300 Subject: [PATCH 275/605] test: show code coverage numbers per function We have added a visualization of code coverage in Coveralls, but sometimes it is convenient to show percents of covered statements locally without pushing to remote branch and running CI. Example of output: $ go tool cover -func=coverage.out github.com/tarantool/go-tarantool/auth.go:8: scramble 90.9% github.com/tarantool/go-tarantool/auth.go:36: xor 100.0% github.com/tarantool/go-tarantool/client_tools.go:13: EncodeMsgpack 100.0% github.com/tarantool/go-tarantool/client_tools.go:25: EncodeMsgpack 100.0% github.com/tarantool/go-tarantool/client_tools.go:37: EncodeMsgpack 0.0% github.com/tarantool/go-tarantool/client_tools.go:49: EncodeMsgpack 0.0% github.com/tarantool/go-tarantool/client_tools.go:63: EncodeMsgpack 0.0% github.com/tarantool/go-tarantool/client_tools.go:78: EncodeMsgpack 0.0% github.com/tarantool/go-tarantool/connection.go:67: Report 0.0% ... --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index d4d4cab17..12d369606 100644 --- a/Makefile +++ b/Makefile @@ -63,6 +63,7 @@ coverage: go clean -testcache go get golang.org/x/tools/cmd/cover go test ./... -v -p 1 -covermode=atomic -coverprofile=$(COVERAGE_FILE) -coverpkg=./... + go tool cover -func=$(COVERAGE_FILE) .PHONY: coveralls coveralls: coverage From a0685c477861cd467c3d2231954e0394621d26cd Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 1 Jun 2022 14:47:46 +0300 Subject: [PATCH 276/605] Release 1.6.0 Overview This release adds a number of features. Also it significantly improves testing, CI and documentation. Breaking changes There are no breaking changes in the release. New features Support UUID type in msgpack (#90). queue-utube handling (#85). Master discovery (#113). SQL support (#62). Bugfixes Reset buffer if its average use size smaller than quater of capacity (#95). Testing Coveralls support (#149). Reusable testing workflow (integration testing with latest Tarantool) (#112). Simple CI based on GitHub actions (#114). Handle everything with `go test` (#115). Fix queue tests (#107). Make test case consistent with comments (#105). Other Go modules support (#91). Update API documentation: comments and examples (#123). --- CHANGELOG.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 854ce6153..dc5fe01da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,19 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +### Changed + +### Fixed + +## [1.6.0] - 2022-06-01 + +This release adds a number of features. Also it significantly improves testing, +CI and documentation. + +### Added + - Coveralls support (#149) -- Reusable testing workflow (integration testing with latest Tarantool) (#123) +- Reusable testing workflow (integration testing with latest Tarantool) (#112) - Simple CI based on GitHub actions (#114) - Support UUID type in msgpack (#90) - Go modules support (#91) @@ -19,17 +30,17 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Master discovery (#113) - SQL support (#62) -### Fixed - -- Fix queue tests (#107) -- Make test case consistent with comments (#105) - ### Changed - Handle everything with `go test` (#115) - Use plain package instead of module for UUID submodule (#134) - Reset buffer if its average use size smaller than quater of capacity (#95) -- Update API documentation: comments and examples (#123). +- Update API documentation: comments and examples (#123) + +### Fixed + +- Fix queue tests (#107) +- Make test case consistent with comments (#105) ## [1.5] - 2019-12-29 From 282ba27b996ae4a2fb1e89a880336631efad6eaa Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 31 May 2022 22:57:56 +0300 Subject: [PATCH 277/605] github-ci: enable the goimports linter goimports[1] helps to sort imports in the same order. In addition to fixing imports, it also formats the code in the same style as gofmt. goimports linter for golangci-lint[2] provides checks for this formatter. 1. https://pkg.go.dev/golang.org/x/tools/cmd/goimports 2. https://golangci-lint.run/usage/linters/ --- .github/workflows/check.yaml | 2 +- CONTRIBUTING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 9b785d0df..aa6e7b139 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -47,4 +47,4 @@ jobs: # The `//nolint` workaround was not the acceptable way of warnings suppression, # cause those comments get rendered in documentation by godoc. # See https://github.com/tarantool/go-tarantool/pull/160#discussion_r858608221 - args: -E gofmt -D errcheck + args: -E goimports -D errcheck diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a276cd6e2..23c742cec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,7 @@ To check if the current changes will pass the linter in CI, install golnagci-lint from [sources](https://golangci-lint.run/usage/install/) and run it with next flags: ```bash -golangci-lint run -E gofmt -D errcheck +golangci-lint run -E goimports -D errcheck ``` ## Benchmarking From e0d9c21a79d8b0bfdd274496214135a38ce31364 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 31 May 2022 23:40:24 +0300 Subject: [PATCH 278/605] code health: fix all imports highlighted by the linter --- example_custom_unpacking_test.go | 5 +++-- example_test.go | 2 +- multi/example_test.go | 3 ++- queue/example_msgpack_test.go | 2 +- tarantool_test.go | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index 1bc955151..65404c0d6 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -2,10 +2,11 @@ package tarantool_test import ( "fmt" - "github.com/tarantool/go-tarantool" - "gopkg.in/vmihailenco/msgpack.v2" "log" "time" + + "github.com/tarantool/go-tarantool" + "gopkg.in/vmihailenco/msgpack.v2" ) type Tuple2 struct { diff --git a/example_test.go b/example_test.go index 386ad11f2..49fe97b5f 100644 --- a/example_test.go +++ b/example_test.go @@ -2,10 +2,10 @@ package tarantool_test import ( "fmt" - "github.com/tarantool/go-tarantool/test_helpers" "time" "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" ) type Tuple struct { diff --git a/multi/example_test.go b/multi/example_test.go index e095504a2..e43461141 100644 --- a/multi/example_test.go +++ b/multi/example_test.go @@ -2,8 +2,9 @@ package multi import ( "fmt" - "github.com/tarantool/go-tarantool" "time" + + "github.com/tarantool/go-tarantool" ) func ExampleConnect() { diff --git a/queue/example_msgpack_test.go b/queue/example_msgpack_test.go index e66cebfa4..89bdad5a3 100644 --- a/queue/example_msgpack_test.go +++ b/queue/example_msgpack_test.go @@ -10,12 +10,12 @@ package queue_test import ( "fmt" + "log" "time" "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/queue" "gopkg.in/vmihailenco/msgpack.v2" - "log" ) type dummyData struct { diff --git a/tarantool_test.go b/tarantool_test.go index 3a6a839e9..acd8577c9 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2,7 +2,6 @@ package tarantool_test import ( "fmt" - "github.com/stretchr/testify/assert" "log" "os" "reflect" @@ -11,6 +10,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" . "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/test_helpers" "gopkg.in/vmihailenco/msgpack.v2" From 0322d14c4e623063dba277b35823241026ba07eb Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 1 Jun 2022 00:00:01 +0300 Subject: [PATCH 279/605] makefile: add format and golangci-lint targets This makes easier to run locally a linter and a formatter. Also it updates instructions in CONTRIBUTING.md how to run the linter and the code formatter locally. --- CONTRIBUTING.md | 12 +++++++++--- Makefile | 8 ++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23c742cec..2122899e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,10 +39,16 @@ make test-multi test-uuid test-main ``` To check if the current changes will pass the linter in CI, install -golnagci-lint from [sources](https://golangci-lint.run/usage/install/) -and run it with next flags: +golangci-lint from [sources](https://golangci-lint.run/usage/install/) +and run it with next command: ```bash -golangci-lint run -E goimports -D errcheck +make golangci-lint +``` + +To format the code install [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) +and run it with next command: +```bash +make format ``` ## Benchmarking diff --git a/Makefile b/Makefile index 12d369606..01b228c13 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,14 @@ clean: deps: clean ( cd ./queue; tarantoolctl rocks install queue 1.1.0 ) +.PHONY: format +format: + goimports -l -w . + +.PHONY: golangci-lint +golangci-lint: + golangci-lint run -E goimports -D errcheck + .PHONY: test test: go test ./... -v -p 1 From f8e9c70e9f4b2c5c9496c22048ba8839474064cc Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 22 Apr 2022 12:11:22 +0300 Subject: [PATCH 280/605] Support of SSL protocol The patch adds support for using SSL to encrypt the client-server communications [1]. It uses a wrapper around the OpenSSL library for full compatibility with Tarantool Enterprise (GOST cryptographic algorithms [2] are not supported by Golang's crypto/tls). The feature can be disabled using a build tag [3] 'go_tarantool_ssl_disable'. 1. https://www.tarantool.io/en/enterprise_doc/security/#enterprise-iproto-encryption 2. https://github.com/gost-engine/engine 3. https://pkg.go.dev/go/build#hdr-Build_Constraints Closes #155 --- .github/workflows/testing.yml | 64 ++++- CHANGELOG.md | 2 + CONTRIBUTING.md | 7 + Makefile | 4 + README.md | 8 + connection.go | 49 +++- example_test.go | 20 +- export_test.go | 14 + go.mod | 1 + go.sum | 8 + ssl.go | 110 ++++++++ ssl_disable.go | 19 ++ ssl_test.go | 466 ++++++++++++++++++++++++++++++++++ test_helpers/main.go | 94 ++++++- testdata/ca.crt | 20 ++ testdata/empty | 0 testdata/generate.sh | 25 ++ testdata/localhost.crt | 22 ++ testdata/localhost.key | 28 ++ 19 files changed, 950 insertions(+), 11 deletions(-) create mode 100644 ssl.go create mode 100644 ssl_disable.go create mode 100644 ssl_test.go create mode 100644 testdata/ca.crt create mode 100644 testdata/empty create mode 100755 testdata/generate.sh create mode 100644 testdata/localhost.crt create mode 100644 testdata/localhost.key diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index c3793a741..6baf43d43 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: jobs: - linux: + run-tests-ce: # We want to run on external PRs, but not on our own internal # PRs as they'll be run by the push to the branch. # @@ -26,9 +26,6 @@ jobs: - '2.9' - '2.x-latest' coveralls: [false] - include: - - tarantool: '2.x-latest' - coveralls: true steps: - name: Clone the connector @@ -66,3 +63,62 @@ jobs: - name: Check workability of benchmark tests run: make bench-deps bench DURATION=1x COUNT=1 + + run-tests-ee: + if: github.event_name == 'push' || + github.event.pull_request.head.repo.full_name != github.repository + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + sdk-version: + - '1.10.11-0-gf0b0e7ecf-r470' + - '2.8.3-21-g7d35cd2be-r470' + coveralls: [false] + ssl: [false] + include: + - sdk-version: '2.10.0-1-gfa775b383-r486-linux-x86_64' + coveralls: true + ssl: true + + steps: + - name: Clone the connector + uses: actions/checkout@v2 + + - name: Setup Tarantool ${{ matrix.sdk-version }} + run: | + ARCHIVE_NAME=tarantool-enterprise-bundle-${{ matrix.sdk-version }}.tar.gz + curl -O -L https://${{ secrets.SDK_DOWNLOAD_TOKEN }}@download.tarantool.io/enterprise/${ARCHIVE_NAME} + tar -xzf ${ARCHIVE_NAME} + rm -f ${ARCHIVE_NAME} + + - name: Setup golang for the connector and tests + uses: actions/setup-go@v2 + with: + go-version: 1.13 + + - name: Install test dependencies + run: | + source tarantool-enterprise/env.sh + make deps + + - name: Run tests + run: | + source tarantool-enterprise/env.sh + make test + env: + TEST_TNT_SSL: ${{matrix.ssl}} + + - name: Run tests, collect code coverage data and send to Coveralls + if: ${{ matrix.coveralls }} + env: + COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TEST_TNT_SSL: ${{matrix.ssl}} + run: | + source tarantool-enterprise/env.sh + make coveralls + + - name: Check workability of benchmark tests + run: make bench-deps bench DURATION=1x COUNT=1 diff --git a/CHANGELOG.md b/CHANGELOG.md index dc5fe01da..9a1f304a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +- SSL support (#155) + ### Changed ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2122899e5..bd9075767 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,13 @@ make test The tests set up all required `tarantool` processes before run and clean up afterwards. +If you have Tarantool Enterprise Edition 2.10 or newer, you can run additional +SSL tests. To do this, you need to set an environment variable 'TEST_TNT_SSL': + +```bash +TEST_TNT_SSL=true make test +``` + If you want to run the tests for a specific package: ```bash make test- diff --git a/Makefile b/Makefile index 01b228c13..d88ce323e 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,10 @@ golangci-lint: test: go test ./... -v -p 1 +.PHONY: testdata +testdata: + (cd ./testdata; ./generate.sh) + .PHONY: test-connection-pool test-connection-pool: @echo "Running tests in connection_pool package" diff --git a/README.md b/README.md index 82c886720..a2b77eaa7 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ faster than other packages according to public benchmarks. ## Table of contents * [Installation](#installation) + * [Build tags](#build-tags) * [Documentation](#documentation) * [API reference](#api-reference) * [Walking\-through example](#walking-through-example) @@ -51,6 +52,13 @@ This should put the source and binary files in subdirectories of `github.com/tarantool/go-tarantool` to the `import {...}` section at the start of any Go program. +### Build tags + +To disable SSL support and linking with OpenSSL, you can use the tag: +``` +go_tarantool_ssl_disable +``` + ## Documentation Read the [Tarantool documentation][tarantool-doc-data-model-url] diff --git a/connection.go b/connection.go index 3be878011..71c22890e 100644 --- a/connection.go +++ b/connection.go @@ -25,6 +25,11 @@ const ( connClosed = 2 ) +const ( + connTransportNone = "" + connTransportSsl = "ssl" +) + type ConnEventKind int type ConnLogKind int @@ -207,6 +212,32 @@ type Opts struct { Handle interface{} // Logger is user specified logger used for error messages. Logger Logger + // Transport is the connection type, by default the connection is unencrypted. + Transport string + // SslOpts is used only if the Transport == 'ssl' is set. + Ssl SslOpts +} + +// SslOpts is a way to configure ssl transport. +type SslOpts struct { + // KeyFile is a path to a private SSL key file. + KeyFile string + // CertFile is a path to an SSL sertificate file. + CertFile string + // CaFile is a path to a trusted certificate authorities (CA) file. + CaFile string + // Ciphers is a colon-separated (:) list of SSL cipher suites the connection + // can use. + // + // We don't provide a list of supported ciphers. This is what OpenSSL + // does. The only limitation is usage of TLSv1.2 (because other protocol + // versions don't seem to support the GOST cipher). To add additional + // ciphers (GOST cipher), you must configure OpenSSL. + // + // See also + // + // * https://www.openssl.org/docs/man1.1.1/man1/ciphers.html + Ciphers string } // Connect creates and configures a new Connection. @@ -358,8 +389,10 @@ func (conn *Connection) Handle() interface{} { func (conn *Connection) dial() (err error) { var connection net.Conn network := "tcp" + opts := conn.opts address := conn.addr - timeout := conn.opts.Reconnect / 2 + timeout := opts.Reconnect / 2 + transport := opts.Transport if timeout == 0 { timeout = 500 * time.Millisecond } else if timeout > 5*time.Second { @@ -383,11 +416,17 @@ func (conn *Connection) dial() (err error) { } else if addrLen >= 4 && address[0:4] == "tcp:" { address = address[4:] } - connection, err = net.DialTimeout(network, address, timeout) + if transport == connTransportNone { + connection, err = net.DialTimeout(network, address, timeout) + } else if transport == connTransportSsl { + connection, err = sslDialTimeout(network, address, timeout, opts.Ssl) + } else { + err = errors.New("An unsupported transport type: " + transport) + } if err != nil { return } - dc := &DeadlineIO{to: conn.opts.Timeout, c: connection} + dc := &DeadlineIO{to: opts.Timeout, c: connection} r := bufio.NewReaderSize(dc, 128*1024) w := bufio.NewWriterSize(dc, 128*1024) greeting := make([]byte, 128) @@ -400,8 +439,8 @@ func (conn *Connection) dial() (err error) { conn.Greeting.auth = bytes.NewBuffer(greeting[64:108]).String() // Auth - if conn.opts.User != "" { - scr, err := scramble(conn.Greeting.auth, conn.opts.Pass) + if opts.User != "" { + scr, err := scramble(conn.Greeting.auth, opts.Pass) if err != nil { err = errors.New("auth: scrambling failure " + err.Error()) connection.Close() diff --git a/example_test.go b/example_test.go index 49fe97b5f..b14dc4e81 100644 --- a/example_test.go +++ b/example_test.go @@ -20,11 +20,29 @@ type Tuple struct { func example_connect() *tarantool.Connection { conn, err := tarantool.Connect(server, opts) if err != nil { - panic("Connection is not established") + panic("Connection is not established: " + err.Error()) } return conn } +// Example demonstrates how to use SSL transport. +func ExampleSslOpts() { + var opts = tarantool.Opts{ + User: "test", + Pass: "test", + Transport: "ssl", + Ssl: tarantool.SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + } + _, err := tarantool.Connect("127.0.0.1:3013", opts) + if err != nil { + panic("Connection is not established: " + err.Error()) + } +} + func ExampleConnection_Select() { conn := example_connect() defer conn.Close() diff --git a/export_test.go b/export_test.go index 931e78c9b..8cb2b713a 100644 --- a/export_test.go +++ b/export_test.go @@ -1,5 +1,19 @@ package tarantool +import ( + "net" + "time" +) + func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (spaceNo, indexNo uint32, err error) { return schema.resolveSpaceIndex(s, i) } + +func SslDialTimeout(network, address string, timeout time.Duration, + opts SslOpts) (connection net.Conn, err error) { + return sslDialTimeout(network, address, timeout, opts) +} + +func SslCreateContext(opts SslOpts) (ctx interface{}, err error) { + return sslCreateContext(opts) +} diff --git a/go.mod b/go.mod index 152329b1f..6dcaee974 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/google/uuid v1.3.0 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/stretchr/testify v1.7.1 // indirect + github.com/tarantool/go-openssl v0.0.8-0.20220419150948-be4921aa2f87 google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/vmihailenco/msgpack.v2 v2.9.2 diff --git a/go.sum b/go.sum index 1f9e791b4..1af7f9933 100644 --- a/go.sum +++ b/go.sum @@ -7,17 +7,25 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= +github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tarantool/go-openssl v0.0.8-0.20220419150948-be4921aa2f87 h1:JGzuBxNBq5saVtPUcuu5Y4+kbJON6H02//OT+RNqGts= +github.com/tarantool/go-openssl v0.0.8-0.20220419150948-be4921aa2f87/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/ssl.go b/ssl.go new file mode 100644 index 000000000..d9373ace2 --- /dev/null +++ b/ssl.go @@ -0,0 +1,110 @@ +//go:build !go_tarantool_ssl_disable +// +build !go_tarantool_ssl_disable + +package tarantool + +import ( + "errors" + "io/ioutil" + "net" + "time" + + "github.com/tarantool/go-openssl" +) + +func sslDialTimeout(network, address string, timeout time.Duration, + opts SslOpts) (connection net.Conn, err error) { + var ctx interface{} + if ctx, err = sslCreateContext(opts); err != nil { + return + } + + return openssl.DialTimeout(network, address, timeout, ctx.(*openssl.Ctx), 0) +} + +// interface{} is a hack. It helps to avoid dependency of go-openssl in build +// of tests with the tag 'go_tarantool_ssl_disable'. +func sslCreateContext(opts SslOpts) (ctx interface{}, err error) { + var sslCtx *openssl.Ctx + + // Require TLSv1.2, because other protocol versions don't seem to + // support the GOST cipher. + if sslCtx, err = openssl.NewCtxWithVersion(openssl.TLSv1_2); err != nil { + return + } + ctx = sslCtx + sslCtx.SetMaxProtoVersion(openssl.TLS1_2_VERSION) + sslCtx.SetMinProtoVersion(openssl.TLS1_2_VERSION) + + if opts.CertFile != "" { + if err = sslLoadCert(sslCtx, opts.CertFile); err != nil { + return + } + } + + if opts.KeyFile != "" { + if err = sslLoadKey(sslCtx, opts.KeyFile); err != nil { + return + } + } + + if opts.CaFile != "" { + if err = sslCtx.LoadVerifyLocations(opts.CaFile, ""); err != nil { + return + } + verifyFlags := openssl.VerifyPeer | openssl.VerifyFailIfNoPeerCert + sslCtx.SetVerify(verifyFlags, nil) + } + + if opts.Ciphers != "" { + sslCtx.SetCipherList(opts.Ciphers) + } + + return +} + +func sslLoadCert(ctx *openssl.Ctx, certFile string) (err error) { + var certBytes []byte + if certBytes, err = ioutil.ReadFile(certFile); err != nil { + return + } + + certs := openssl.SplitPEM(certBytes) + if len(certs) == 0 { + err = errors.New("No PEM certificate found in " + certFile) + return + } + first, certs := certs[0], certs[1:] + + var cert *openssl.Certificate + if cert, err = openssl.LoadCertificateFromPEM(first); err != nil { + return + } + if err = ctx.UseCertificate(cert); err != nil { + return + } + + for _, pem := range certs { + if cert, err = openssl.LoadCertificateFromPEM(pem); err != nil { + break + } + if err = ctx.AddChainCertificate(cert); err != nil { + break + } + } + return +} + +func sslLoadKey(ctx *openssl.Ctx, keyFile string) (err error) { + var keyBytes []byte + if keyBytes, err = ioutil.ReadFile(keyFile); err != nil { + return + } + + var key openssl.PrivateKey + if key, err = openssl.LoadPrivateKeyFromPEM(keyBytes); err != nil { + return + } + + return ctx.UsePrivateKey(key) +} diff --git a/ssl_disable.go b/ssl_disable.go new file mode 100644 index 000000000..8d0ab406b --- /dev/null +++ b/ssl_disable.go @@ -0,0 +1,19 @@ +//go:build go_tarantool_ssl_disable +// +build go_tarantool_ssl_disable + +package tarantool + +import ( + "errors" + "net" + "time" +) + +func sslDialTimeout(network, address string, timeout time.Duration, + opts SslOpts) (connection net.Conn, err error) { + return nil, errors.New("SSL support is disabled.") +} + +func sslCreateContext(opts SslOpts) (ctx interface{}, err error) { + return nil, errors.New("SSL support is disabled.") +} diff --git a/ssl_test.go b/ssl_test.go new file mode 100644 index 000000000..3bf9d8ba3 --- /dev/null +++ b/ssl_test.go @@ -0,0 +1,466 @@ +//go:build !go_tarantool_ssl_disable +// +build !go_tarantool_ssl_disable + +package tarantool_test + +import ( + "errors" + "fmt" + "io/ioutil" + "net" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/tarantool/go-openssl" + . "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" +) + +const sslHost = "127.0.0.1" +const tntHost = "127.0.0.1:3014" + +func serverSsl(network, address string, opts SslOpts) (net.Listener, error) { + ctx, err := SslCreateContext(opts) + if err != nil { + return nil, errors.New("Unable to create SSL context: " + err.Error()) + } + + return openssl.Listen(network, address, ctx.(*openssl.Ctx)) +} + +func serverSslAccept(l net.Listener) (<-chan string, <-chan error) { + message := make(chan string, 1) + errors := make(chan error, 1) + + go func() { + conn, err := l.Accept() + if err != nil { + errors <- err + } else { + bytes, err := ioutil.ReadAll(conn) + if err != nil { + errors <- err + } else { + message <- string(bytes) + } + conn.Close() + } + + close(message) + close(errors) + }() + + return message, errors +} + +func serverSslRecv(msgs <-chan string, errs <-chan error) (string, error) { + return <-msgs, <-errs +} + +func clientSsl(network, address string, opts SslOpts) (net.Conn, error) { + timeout := 5 * time.Second + return SslDialTimeout(network, address, timeout, opts) +} + +func createClientServerSsl(t testing.TB, serverOpts, + clientOpts SslOpts) (net.Listener, net.Conn, error, <-chan string, <-chan error) { + t.Helper() + + l, err := serverSsl("tcp", sslHost+":0", serverOpts) + if err != nil { + t.Fatalf("Unable to create server, error %q", err.Error()) + } + + msgs, errs := serverSslAccept(l) + + port := l.Addr().(*net.TCPAddr).Port + c, err := clientSsl("tcp", sslHost+":"+strconv.Itoa(port), clientOpts) + + return l, c, err, msgs, errs +} + +func createClientServerSslOk(t testing.TB, serverOpts, + clientOpts SslOpts) (net.Listener, net.Conn, <-chan string, <-chan error) { + t.Helper() + + l, c, err, msgs, errs := createClientServerSsl(t, serverOpts, clientOpts) + if err != nil { + t.Fatalf("Unable to create client, error %q", err.Error()) + } + + return l, c, msgs, errs +} + +func serverTnt(serverOpts, clientOpts SslOpts) (test_helpers.TarantoolInstance, error) { + listen := tntHost + "?transport=ssl&" + + key := serverOpts.KeyFile + if key != "" { + listen += fmt.Sprintf("ssl_key_file=%s&", key) + } + + cert := serverOpts.CertFile + if cert != "" { + listen += fmt.Sprintf("ssl_cert_file=%s&", cert) + } + + ca := serverOpts.CaFile + if ca != "" { + listen += fmt.Sprintf("ssl_ca_file=%s&", ca) + } + + ciphers := serverOpts.Ciphers + if ciphers != "" { + listen += fmt.Sprintf("ssl_ciphers=%s&", ciphers) + } + + listen = listen[:len(listen)-1] + + return test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: listen, + SslCertsDir: "testdata", + ClientServer: tntHost, + ClientTransport: "ssl", + ClientSsl: clientOpts, + WorkDir: "work_dir_ssl", + User: "test", + Pass: "test", + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) +} + +func serverTntStop(inst test_helpers.TarantoolInstance) { + test_helpers.StopTarantoolWithCleanup(inst) +} + +func assertConnectionSslFail(t testing.TB, serverOpts, clientOpts SslOpts) { + t.Helper() + + l, c, err, _, _ := createClientServerSsl(t, serverOpts, clientOpts) + l.Close() + if err == nil { + c.Close() + t.Errorf("An unexpected connection to the server.") + } +} + +func assertConnectionSslOk(t testing.TB, serverOpts, clientOpts SslOpts) { + t.Helper() + + l, c, msgs, errs := createClientServerSslOk(t, serverOpts, clientOpts) + const message = "any test string" + c.Write([]byte(message)) + c.Close() + + recv, err := serverSslRecv(msgs, errs) + l.Close() + + if err != nil { + t.Errorf("An unexpected server error: %q", err.Error()) + } else if recv != message { + t.Errorf("An unexpected server message: %q, expected %q", recv, message) + } +} + +func assertConnectionTntFail(t testing.TB, serverOpts, clientOpts SslOpts) { + t.Helper() + + inst, err := serverTnt(serverOpts, clientOpts) + serverTntStop(inst) + + if err == nil { + t.Errorf("An unexpected connection to the server") + } +} + +func assertConnectionTntOk(t testing.TB, serverOpts, clientOpts SslOpts) { + t.Helper() + + inst, err := serverTnt(serverOpts, clientOpts) + serverTntStop(inst) + + if err != nil { + t.Errorf("An unexpected server error %q", err.Error()) + } +} + +type test struct { + name string + ok bool + serverOpts SslOpts + clientOpts SslOpts +} + +/* + Requirements from Tarantool Enterprise Edition manual: + https://www.tarantool.io/ru/enterprise_doc/security/#configuration + + For a server: + KeyFile - mandatory + CertFile - mandatory + CaFile - optional + Ciphers - optional + + For a client: + KeyFile - optional, mandatory if server.CaFile set + CertFile - optional, mandatory if server.CaFile set + CaFile - optional, + Ciphers - optional +*/ +var tests = []test{ + { + "empty", + false, + SslOpts{}, + SslOpts{}, + }, + { + "key_crt_client", + false, + SslOpts{}, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + }, + { + "key_crt_server", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + SslOpts{}, + }, + { + "key_crt_server_and_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + }, + { + "key_crt_ca_server", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{}, + }, + { + "key_crt_ca_server_key_crt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + }, + { + "key_crt_ca_server_and_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_invalid_path_key", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "any_invalid_path", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_invalid_path_crt", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "any_invalid_path", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_invalid_path_ca", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "any_invalid_path", + }, + }, + { + "key_crt_ca_server_and_client_empty_key", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/empty", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_empty_crt", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/empty", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_empty_ca", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/empty", + }, + }, + { + "key_crt_server_and_key_crt_ca_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_ciphers_server_key_crt_ca_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_ciphers_server_and_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + }, + }, + { + "non_equal_ciphers_client", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "TLS_AES_128_GCM_SHA256", + }, + }, +} + +func TestSslOpts(t *testing.T) { + testTntSsl, exists := os.LookupEnv("TEST_TNT_SSL") + isTntSsl := false + if exists && (testTntSsl == "1" || strings.ToUpper(testTntSsl) == "TRUE") { + isTntSsl = true + } + + for _, test := range tests { + if test.ok { + t.Run("ok_ssl_"+test.name, func(t *testing.T) { + assertConnectionSslOk(t, test.serverOpts, test.clientOpts) + }) + } else { + t.Run("fail_ssl_"+test.name, func(t *testing.T) { + assertConnectionSslFail(t, test.serverOpts, test.clientOpts) + }) + } + if !isTntSsl { + continue + } + if test.ok { + t.Run("ok_tnt_"+test.name, func(t *testing.T) { + assertConnectionTntOk(t, test.serverOpts, test.clientOpts) + }) + } else { + t.Run("fail_tnt_"+test.name, func(t *testing.T) { + assertConnectionTntFail(t, test.serverOpts, test.clientOpts) + }) + } + } +} diff --git a/test_helpers/main.go b/test_helpers/main.go index cc3416b91..5c9d5135e 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -13,9 +13,12 @@ package test_helpers import ( "errors" "fmt" + "io" + "io/ioutil" "log" "os" "os/exec" + "path/filepath" "regexp" "strconv" "time" @@ -32,12 +35,28 @@ type StartOpts struct { // https://www.tarantool.io/en/doc/latest/reference/configuration/#cfg-basic-listen Listen string + // ClientServer changes a host to connect to test startup of a Tarantool + // instance. By default, it uses Listen value as the host for the connection. + ClientServer string + + // ClientTransport changes Opts.Transport for a connection that checks startup + // of a Tarantool instance. + ClientTransport string + + // ClientSsl changes Opts.Ssl for a connection that checks startup of + // a Tarantool instance. + ClientSsl tarantool.SslOpts + // WorkDir is box.cfg work_dir parameter for tarantool. // Specify folder to store tarantool data files. // Folder must be unique for each tarantool process used simultaneously. // https://www.tarantool.io/en/doc/latest/reference/configuration/#confval-work_dir WorkDir string + // SslCertsDir is a path to a directory with SSL certificates. It will be + // copied to the working directory. + SslCertsDir string + // User is a username used to connect to tarantool. // All required grants must be given in InitScript. User string @@ -185,6 +204,14 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { return inst, err } + // Copy SSL certificates. + if startOpts.SslCertsDir != "" { + err = copySslCerts(startOpts.WorkDir, startOpts.SslCertsDir) + if err != nil { + return inst, err + } + } + // Options for restarting tarantool instance. inst.Opts = startOpts @@ -204,11 +231,19 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { User: startOpts.User, Pass: startOpts.Pass, SkipSchema: true, + Transport: startOpts.ClientTransport, + Ssl: startOpts.ClientSsl, } var i uint + var server string + if startOpts.ClientServer != "" { + server = startOpts.ClientServer + } else { + server = startOpts.Listen + } for i = 0; i <= startOpts.ConnectRetry; i++ { - err = isReady(startOpts.Listen, &opts) + err = isReady(server, &opts) // Both connect and ping is ok. if err == nil { @@ -254,3 +289,60 @@ func StopTarantoolWithCleanup(inst TarantoolInstance) { } } } + +func copySslCerts(dst string, sslCertsDir string) (err error) { + dstCertPath := filepath.Join(dst, sslCertsDir) + if err = os.Mkdir(dstCertPath, 0755); err != nil { + return + } + if err = copyDirectoryFiles(sslCertsDir, dstCertPath); err != nil { + return + } + return +} + +func copyDirectoryFiles(scrDir, dest string) error { + entries, err := ioutil.ReadDir(scrDir) + if err != nil { + return err + } + for _, entry := range entries { + sourcePath := filepath.Join(scrDir, entry.Name()) + destPath := filepath.Join(dest, entry.Name()) + _, err := os.Stat(sourcePath) + if err != nil { + return err + } + + if err := copyFile(sourcePath, destPath); err != nil { + return err + } + + if err := os.Chmod(destPath, entry.Mode()); err != nil { + return err + } + } + return nil +} + +func copyFile(srcFile, dstFile string) error { + out, err := os.Create(dstFile) + if err != nil { + return err + } + + defer out.Close() + + in, err := os.Open(srcFile) + if err != nil { + return err + } + defer in.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + + return nil +} diff --git a/testdata/ca.crt b/testdata/ca.crt new file mode 100644 index 000000000..2fa1a12ff --- /dev/null +++ b/testdata/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLzCCAhegAwIBAgIUMMZTmNkhr4qOfSwInVk2dAJvoBEwDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +MjA1MjYwNjE3NDBaFw00NDEwMjkwNjE3NDBaMCcxCzAJBgNVBAYTAlVTMRgwFgYD +VQQDDA9FeGFtcGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCRq/eaA3I6CB8t770H2XDdzcp1yuC/+TZOxV5o0LuRkogTvL2kYULBrfx1 +rVZu8zQJTx1fmSRj1cN8j+IrmXN5goZ3mYFTnnIOgkyi+hJysVlo5s0Kp0qtLLGM +OuaVbxw2oAy75if5X3pFpiDaMvFBtJKsh8+SkncBIC5bbKC5AoLdFANLmPiH0CGr +Mv3rL3ycnbciI6J4uKHcWnYGGiMjBomaZ7jd/cOjcjmGfpI5d0nq13G11omkyEyR +wNX0eJRL02W+93Xu7tD+FEFMxFvak+70GvX+XWomwYw/Pjlio8KbTAlJxhfK2Lh6 +H798k17VfxIrOk0KjzZS7+a20hZ/AgMBAAGjUzBRMB0GA1UdDgQWBBT2f5o8r75C +PWST36akpkKRRTbhvjAfBgNVHSMEGDAWgBT2f5o8r75CPWST36akpkKRRTbhvjAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA9pb75p6mnqp2MQHSr +5SKRf2UV4wQIUtXgF6V9vNfvVzJii+Lzrqir1YMk5QgavCzD96KlJcqJCcH559RY +5743AxI3tdWfA3wajBctoy35oYnT4M30qbkryYLTUlv7PmeNWvrksTURchyyDt5/ +3T73yj5ZalmzKN6+xLfUDdnudspfWlUMutKU50MU1iuQESf4Fwd53vOg9jMcWJ2E +vAgfVI0XAvYdU3ybJrUvBq5zokYR2RzGv14uHxwVPnLBjrBEHRnbrXvLZJhuIS2b +xZ3CqwWi+9bvNqHz09HvhkU2b6fCGweKaAUGSo8OfQ5FRkjTUomMI/ZLs/qtJ6JR +zzVt +-----END CERTIFICATE----- diff --git a/testdata/empty b/testdata/empty new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/generate.sh b/testdata/generate.sh new file mode 100755 index 000000000..f29f41c90 --- /dev/null +++ b/testdata/generate.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -xeuo pipefail +# An example how-to re-generate testing certificates (because usually +# TLS certificates have expiration dates and some day they will expire). +# +# The instruction is valid for: +# +# $ openssl version +# OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022) + +cat < domains.ext +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names +[alt_names] +DNS.1 = localhost +IP.1 = 127.0.0.1 +EOF + +openssl req -x509 -nodes -new -sha256 -days 8192 -newkey rsa:2048 -keyout ca.key -out ca.pem -subj "/C=US/CN=Example-Root-CA" +openssl x509 -outform pem -in ca.pem -out ca.crt + +openssl req -new -nodes -newkey rsa:2048 -keyout localhost.key -out localhost.csr -subj "/C=US/ST=YourState/L=YourCity/O=Example-Certificates/CN=localhost" +openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains.ext -out localhost.crt diff --git a/testdata/localhost.crt b/testdata/localhost.crt new file mode 100644 index 000000000..fd04b9900 --- /dev/null +++ b/testdata/localhost.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIUAvSBJ3nSv7kdKw1IQ7AjchzI7T8wDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +MjA1MjYwNjE3NDBaFw00NDEwMjkwNjE3NDBaMGcxCzAJBgNVBAYTAlVTMRIwEAYD +VQQIDAlZb3VyU3RhdGUxETAPBgNVBAcMCFlvdXJDaXR5MR0wGwYDVQQKDBRFeGFt +cGxlLUNlcnRpZmljYXRlczESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnbFY+BMqlddktbitgaZICws4Zyj8LFy9QzO+ +AYSQyqFuTCI+cGqbP5r6Qf4f3xHNGykHJGn18brpiFWhNMaVkkgU3dycU8fFayVN +hLEJAXd4acWP1h5/aH4cOZgl+xJlmU2iLHtP/TLYEDDiVkfqL/MgUIMxbndIaiU0 +/e81v+2gi8ydyI6aElN8KbAaFPzXCZ28/RmO/0m36YzF+FSMVD1Hx8xO5V+Q9N1q +dsyrMdh0nCxDDXGdBgDrKt5+U1uJkDpTHfjMAkf7oBoRd8DJ8O74bpue03W5WxKQ +NjNfvHSgkBaQSdnxR93FSCr/Gs6WcUd50Y8z+ZCTNkup0KROTwIDAQABo3YwdDAf +BgNVHSMEGDAWgBT2f5o8r75CPWST36akpkKRRTbhvjAJBgNVHRMEAjAAMAsGA1Ud +DwQEAwIE8DAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwHQYDVR0OBBYEFOwH +aHK6QrEfltP7wwldUWrQJ9viMA0GCSqGSIb3DQEBCwUAA4IBAQAGHGuloGJqLoPZ +2iRnb/NaiArowLnUz4Z3ENKMB2KbZFGijMJSXO9i9ZLCFL+O93vCTspKGtHqVX2o +dxcrF7EZ9EaHIijWjKGEp1PszunBIca+Te+zyRg9Z+F9gwRsJYB8ctBGjIhe4qEv +ZSlRY489UVNKLTcHcl6nlUled9hciBJHpXuitiiNhzUareP38hROyiUhrAy8L21L +t7Ww5YGRuSTxM5LQfPZcCy40++TlyvXs0DCQ8ZuUbqZv64bNHbaLOyxIqKfPypXa +nS3AYZzUJjHj7vZwHoL1SyvBjx/DQAsWaEv137d8FlMqCsWLXfCsuNpKeQYZOyDS +7ploP9Gl +-----END CERTIFICATE----- diff --git a/testdata/localhost.key b/testdata/localhost.key new file mode 100644 index 000000000..ed0f55876 --- /dev/null +++ b/testdata/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCdsVj4EyqV12S1 +uK2BpkgLCzhnKPwsXL1DM74BhJDKoW5MIj5waps/mvpB/h/fEc0bKQckafXxuumI +VaE0xpWSSBTd3JxTx8VrJU2EsQkBd3hpxY/WHn9ofhw5mCX7EmWZTaIse0/9MtgQ +MOJWR+ov8yBQgzFud0hqJTT97zW/7aCLzJ3IjpoSU3wpsBoU/NcJnbz9GY7/Sbfp +jMX4VIxUPUfHzE7lX5D03Wp2zKsx2HScLEMNcZ0GAOsq3n5TW4mQOlMd+MwCR/ug +GhF3wMnw7vhum57TdblbEpA2M1+8dKCQFpBJ2fFH3cVIKv8azpZxR3nRjzP5kJM2 +S6nQpE5PAgMBAAECggEAFv81l9wHsll6pOu9VfJ/gCjPPXAjMn8F1OaXV5ZTHVHk +iXLXA0LwyBpcU8JxOHFapZLaqUtQpEObahf+zfkF+BLOBDr3i1pPZpxGjUraIt4e +7+HxY4sIDp+Rky6mn1JkAbLqKy2CkUzYaKgQYf/T3dFJjaRMUa1QoLYzX7MCdi5B +GnBICzi2UVsn3HU934l/gJKV+SlprdbrGJ+fRklP2AxLey3EOrwooUViy+k3+w5E +dzBH2HpLL0XuIHaBXQ01J6Mu3ud9ApFLC+Rh+2UFTW/WPnNe+B6BO5CGNN52Pfdr +Q5l+VzmRkXXo2fio+w4z/az8axT/DdhKGT2oBlp35QKBgQDZVGdKjkalH3QH2pdy +CWJIiybzY1R0CpimfgDLIdqEsps9qqgLXsUFB5yTcCRmg8RSWWHvhMVMyJtBcsdY +xGhmHxsFBxuray60UljxBcRQTwqvAX7mP8WEv8t80kbhyaxvOfkg8JD1n2hS7NjL +dOIG9Mh8L0YSOCRkbfv90OnYXQKBgQC5wGs35Ksnqi2swX5MLYfcBaImzoNde86n +cXJ0yyF82O1pk8DkmU2EDcUoQfkKxr3ANvVDG4vYaguIhYsJqPg/t8XQt/epDz/O +WZhqogn0ysaTv2FHrWcgPAkq82hpNII5NfPP8aRaYh8OUSfh4WHkW84m6+usqwjI +wbOq36qmmwKBgGMFFdreYEmzvwYlDoOiyukKncCfLUeB3HNfTbU/w3RafGjobJBh +qZrVEP4MRkl/F9/9YaXj9JE7haGYTkOfmYGOAp2T04OS3kDClEucuQluOgvqvorh +23jUej5xAGK3pJ046M2dTi7bZokB6PUqWCGbPg127JI4ijxH8FyA50rxAoGAQO2d +jMAFg6vco1JPT1lq7+GYOHBfQsIQDj99fo2yeu1or0rSVhWwHsShcdz9rGKj2Rhc +ysRKMa9/sIzdeNbzT3JxVu+3RgTqjLqMqFlTmZl3qBVxb5iRP5c8rSLAEGYmTtEp +FDqm9GDv8hU0F6SsjyH4AWrdylFOlL4Ai237PJkCgYBDC1wAwBD8WXJqRrYVGj7X +l4TQQ0hO7La/zgbasSgLNaJcYu32nut6D0O8IlmcQ2nO0BGPjQmJFGp6xawjViRu +np7fEkJQEf1pK0yeA8A3urjXccuUXEA9kKeqaSZYDzICPFaOlezPPPpW0hbkhnPe +dQn3DcoY6e6o0K5ltt1RvQ== +-----END PRIVATE KEY----- From 1c80774a7b6a17ce39deeccfb05db21c7d97b6c6 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 2 Jun 2022 17:34:32 +0300 Subject: [PATCH 281/605] github-ci: don't run ee tests for outside pull requests by default Such pull requests may be labeled with `full-ci`. It will run tests with Tarantool EE. To avoid security problems, the label must be reset manually for every run. --- .github/workflows/testing.yml | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 6baf43d43..311ffac95 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -3,6 +3,8 @@ name: testing on: push: pull_request: + pull_request_target: + types: [labeled] workflow_dispatch: jobs: @@ -12,8 +14,12 @@ jobs: # # The main trick is described here: # https://github.com/Dart-Code/Dart-Code/pull/2375 - if: github.event_name == 'push' || - github.event.pull_request.head.repo.full_name != github.repository + # + # Also we want to run it always for manually triggered workflows. + if: (github.event_name == 'push') || + (github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.repository) || + (github.event_name == 'workflow_dispatch') runs-on: ubuntu-latest @@ -65,8 +71,18 @@ jobs: run: make bench-deps bench DURATION=1x COUNT=1 run-tests-ee: - if: github.event_name == 'push' || - github.event.pull_request.head.repo.full_name != github.repository + # The same as for run-tests-ce, but it does not run on pull requests from + # forks by default. Tests will run only when the pull request is labeled + # with `full-ci`. To avoid security problems, the label must be reset + # manually for every run. + # + # We need to use `pull_request_target` because it has access to base + # repository secrets unlike `pull_request`. + if: (github.event_name == 'push') || + (github.event_name == 'pull_request_target' && + github.event.pull_request.head.repo.full_name != github.repository && + github.event.label.name == 'full-ci') || + (github.event_name == 'workflow_dispatch') runs-on: ubuntu-latest @@ -86,6 +102,13 @@ jobs: steps: - name: Clone the connector uses: actions/checkout@v2 + # This is needed for pull_request_target because this event runs in the + # context of the base commit of the pull request. It works fine for + # `push` and `workflow_dispatch` because the default behavior is used + # if `ref` and `repository` are empty. + with: + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} - name: Setup Tarantool ${{ matrix.sdk-version }} run: | From e844c034ed8bccaef309cb79f02b75458963d2c0 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 25 May 2022 13:40:46 +0300 Subject: [PATCH 282/605] code health: extract the Future type to future.go This is not a critical change. It just splits request.go into request.go and future.go. --- future.go | 131 ++++++++++++++++++++++++++++++++++++++++++++++++ request.go | 143 ++++------------------------------------------------- 2 files changed, 140 insertions(+), 134 deletions(-) create mode 100644 future.go diff --git a/future.go b/future.go new file mode 100644 index 000000000..fbf141f82 --- /dev/null +++ b/future.go @@ -0,0 +1,131 @@ +package tarantool + +import ( + "time" + + "gopkg.in/vmihailenco/msgpack.v2" +) + +// Future is a handle for asynchronous request. +type Future struct { + requestId uint32 + requestCode int32 + timeout time.Duration + resp *Response + err error + ready chan struct{} + next *Future +} + +// NewErrorFuture returns new set empty Future with filled error field. +func NewErrorFuture(err error) *Future { + return &Future{err: err} +} + +// Get waits for Future to be filled and returns Response and error. +// +// Response will contain deserialized result in Data field. +// It will be []interface{}, so if you want more performace, use GetTyped method. +// +// Note: Response could be equal to nil if ClientError is returned in error. +// +// "error" could be Error, if it is error returned by Tarantool, +// or ClientError, if something bad happens in a client process. +func (fut *Future) Get() (*Response, error) { + fut.wait() + if fut.err != nil { + return fut.resp, fut.err + } + fut.err = fut.resp.decodeBody() + return fut.resp, fut.err +} + +// GetTyped waits for Future and calls msgpack.Decoder.Decode(result) if no error happens. +// It is could be much faster than Get() function. +// +// Note: Tarantool usually returns array of tuples (except for Eval and Call17 actions). +func (fut *Future) GetTyped(result interface{}) error { + fut.wait() + if fut.err != nil { + return fut.err + } + fut.err = fut.resp.decodeBodyTyped(result) + return fut.err +} + +var closedChan = make(chan struct{}) + +func init() { + close(closedChan) +} + +// WaitChan returns channel which becomes closed when response arrived or error occured. +func (fut *Future) WaitChan() <-chan struct{} { + if fut.ready == nil { + return closedChan + } + return fut.ready +} + +// Err returns error set on Future. +// It waits for future to be set. +// Note: it doesn't decode body, therefore decoding error are not set here. +func (fut *Future) Err() error { + fut.wait() + return fut.err +} + +func (fut *Future) pack(h *smallWBuf, enc *msgpack.Encoder, body func(*msgpack.Encoder) error) (err error) { + rid := fut.requestId + hl := h.Len() + h.Write([]byte{ + 0xce, 0, 0, 0, 0, // Length. + 0x82, // 2 element map. + KeyCode, byte(fut.requestCode), // Request code. + KeySync, 0xce, + byte(rid >> 24), byte(rid >> 16), + byte(rid >> 8), byte(rid), + }) + + if err = body(enc); err != nil { + return + } + + l := uint32(h.Len() - 5 - hl) + h.b[hl+1] = byte(l >> 24) + h.b[hl+2] = byte(l >> 16) + h.b[hl+3] = byte(l >> 8) + h.b[hl+4] = byte(l) + + return +} + +func (fut *Future) send(conn *Connection, body func(*msgpack.Encoder) error) *Future { + if fut.ready == nil { + return fut + } + conn.putFuture(fut, body) + return fut +} + +func (fut *Future) markReady(conn *Connection) { + close(fut.ready) + if conn.rlimit != nil { + <-conn.rlimit + } +} + +func (fut *Future) fail(conn *Connection, err error) *Future { + if f := conn.fetchFuture(fut.requestId); f == fut { + f.err = err + fut.markReady(conn) + } + return fut +} + +func (fut *Future) wait() { + if fut.ready == nil { + return + } + <-fut.ready +} diff --git a/request.go b/request.go index dd9486ae1..541fb2b4f 100644 --- a/request.go +++ b/request.go @@ -5,29 +5,17 @@ import ( "reflect" "strings" "sync" - "time" "gopkg.in/vmihailenco/msgpack.v2" ) -// Future is a handle for asynchronous request. -type Future struct { - requestId uint32 - requestCode int32 - timeout time.Duration - resp *Response - err error - ready chan struct{} - next *Future -} - // Ping sends empty request to Tarantool to check connection. func (conn *Connection) Ping() (resp *Response, err error) { future := conn.newFuture(PingRequest) return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(0); return nil }).Get() } -func (req *Future) fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { +func fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { enc.EncodeUint64(KeySpaceNo) enc.EncodeUint64(uint64(spaceNo)) enc.EncodeUint64(KeyIndexNo) @@ -36,7 +24,7 @@ func (req *Future) fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key return enc.Encode(key) } -func (req *Future) fillIterator(enc *msgpack.Encoder, offset, limit, iterator uint32) { +func fillIterator(enc *msgpack.Encoder, offset, limit, iterator uint32) { enc.EncodeUint64(KeyIterator) enc.EncodeUint64(uint64(iterator)) enc.EncodeUint64(KeyOffset) @@ -45,7 +33,7 @@ func (req *Future) fillIterator(enc *msgpack.Encoder, offset, limit, iterator ui enc.EncodeUint64(uint64(limit)) } -func (req *Future) fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { +func fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { enc.EncodeUint64(KeySpaceNo) enc.EncodeUint64(uint64(spaceNo)) enc.EncodeUint64(KeyTuple) @@ -242,8 +230,8 @@ func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, ite } return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(6) - future.fillIterator(enc, offset, limit, iterator) - return future.fillSearch(enc, spaceNo, indexNo, key) + fillIterator(enc, offset, limit, iterator) + return fillSearch(enc, spaceNo, indexNo, key) }) } @@ -257,7 +245,7 @@ func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Futur } return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) - return future.fillInsert(enc, spaceNo, tuple) + return fillInsert(enc, spaceNo, tuple) }) } @@ -271,7 +259,7 @@ func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Futu } return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) - return future.fillInsert(enc, spaceNo, tuple) + return fillInsert(enc, spaceNo, tuple) }) } @@ -285,7 +273,7 @@ func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) * } return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(3) - return future.fillSearch(enc, spaceNo, indexNo, key) + return fillSearch(enc, spaceNo, indexNo, key) }) } @@ -299,7 +287,7 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface } return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(4) - if err := future.fillSearch(enc, spaceNo, indexNo, key); err != nil { + if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { return err } enc.EncodeUint64(KeyTuple) @@ -520,116 +508,3 @@ func encodeSQLBind(enc *msgpack.Encoder, from interface{}) error { } return nil } - -func (fut *Future) pack(h *smallWBuf, enc *msgpack.Encoder, body func(*msgpack.Encoder) error) (err error) { - rid := fut.requestId - hl := h.Len() - h.Write([]byte{ - 0xce, 0, 0, 0, 0, // length - 0x82, // 2 element map - KeyCode, byte(fut.requestCode), // request code - KeySync, 0xce, - byte(rid >> 24), byte(rid >> 16), - byte(rid >> 8), byte(rid), - }) - - if err = body(enc); err != nil { - return - } - - l := uint32(h.Len() - 5 - hl) - h.b[hl+1] = byte(l >> 24) - h.b[hl+2] = byte(l >> 16) - h.b[hl+3] = byte(l >> 8) - h.b[hl+4] = byte(l) - - return -} - -func (fut *Future) send(conn *Connection, body func(*msgpack.Encoder) error) *Future { - if fut.ready == nil { - return fut - } - conn.putFuture(fut, body) - return fut -} - -func (fut *Future) markReady(conn *Connection) { - close(fut.ready) - if conn.rlimit != nil { - <-conn.rlimit - } -} - -func (fut *Future) fail(conn *Connection, err error) *Future { - if f := conn.fetchFuture(fut.requestId); f == fut { - f.err = err - fut.markReady(conn) - } - return fut -} - -func (fut *Future) wait() { - if fut.ready == nil { - return - } - <-fut.ready -} - -// Get waits for Future to be filled and returns Response and error. -// -// Response will contain deserialized result in Data field. -// It will be []interface{}, so if you want more performace, use GetTyped method. -// -// Note: Response could be equal to nil if ClientError is returned in error. -// -// "error" could be Error, if it is error returned by Tarantool, -// or ClientError, if something bad happens in a client process. -func (fut *Future) Get() (*Response, error) { - fut.wait() - if fut.err != nil { - return fut.resp, fut.err - } - fut.err = fut.resp.decodeBody() - return fut.resp, fut.err -} - -// GetTyped waits for Future and calls msgpack.Decoder.Decode(result) if no error happens. -// It is could be much faster than Get() function. -// -// Note: Tarantool usually returns array of tuples (except for Eval and Call17 actions) -func (fut *Future) GetTyped(result interface{}) error { - fut.wait() - if fut.err != nil { - return fut.err - } - fut.err = fut.resp.decodeBodyTyped(result) - return fut.err -} - -var closedChan = make(chan struct{}) - -func init() { - close(closedChan) -} - -// WaitChan returns channel which becomes closed when response arrived or error occured. -func (fut *Future) WaitChan() <-chan struct{} { - if fut.ready == nil { - return closedChan - } - return fut.ready -} - -// Err returns error set on Future. -// It waits for future to be set. -// Note: it doesn't decode body, therefore decoding error are not set here. -func (fut *Future) Err() error { - fut.wait() - return fut.err -} - -// NewErrorFuture returns new set empty Future with filled error field. -func NewErrorFuture(err error) *Future { - return &Future{err: err} -} From 1a1fe7b4e2ea77aa316ed4006aa445e512812c3c Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 6 Apr 2022 15:40:49 +0300 Subject: [PATCH 283/605] api: support IPROTO_PUSH messages This patch adds support for receiving messages sent using box.session.push() via an iterator in the manner of asynchronous case of a Lua implementation[1]. Now the calls Future.Get() and Future.GetTyped() ignore push messages, and do not report an error. 1. https://www.tarantool.io/ru/doc/latest/reference/reference_lua/box_session/push/ Closes #67 --- CHANGELOG.md | 1 + config.lua | 8 ++ connection.go | 121 ++++++++++++++++----- const.go | 1 + example_test.go | 33 ++++++ future.go | 269 +++++++++++++++++++++++++++++++++++----------- future_test.go | 235 ++++++++++++++++++++++++++++++++++++++++ request.go | 34 +++--- response.go | 4 +- response_it.go | 26 +++++ tarantool_test.go | 236 +++++++++++++++++++++------------------- 11 files changed, 750 insertions(+), 218 deletions(-) create mode 100644 future_test.go create mode 100644 response_it.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a1f304a9..7f4e2f887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added - SSL support (#155) +- IPROTO_PUSH messages support (#67) ### Changed diff --git a/config.lua b/config.lua index d024b3a28..abea45742 100644 --- a/config.lua +++ b/config.lua @@ -110,6 +110,14 @@ local function simple_incr(a) end rawset(_G, 'simple_incr', simple_incr) +local function push_func(cnt) + for i = 1, cnt do + box.session.push(i) + end + return cnt +end +rawset(_G, 'push_func', push_func) + box.space.test:truncate() --box.schema.user.revoke('guest', 'read,write,execute', 'universe') diff --git a/connection.go b/connection.go index 71c22890e..5f5425531 100644 --- a/connection.go +++ b/connection.go @@ -163,8 +163,9 @@ type Greeting struct { // Opts is a way to configure Connection type Opts struct { - // Timeout for any particular request. If Timeout is zero request, any - // request can be blocked infinitely. + // Timeout for response to a particular request. The timeout is reset when + // push messages are received. If Timeout is zero, any request can be + // blocked infinitely. // Also used to setup net.TCPConn.Set(Read|Write)Deadline. Timeout time.Duration // Timeout between reconnect attempts. If Reconnect is zero, no @@ -568,8 +569,8 @@ func (conn *Connection) closeConnection(neterr error, forever bool) (err error) requests[pos].first = nil requests[pos].last = &requests[pos].first for fut != nil { - fut.err = neterr - fut.markReady(conn) + fut.SetError(neterr) + conn.markDone(fut) fut, fut.next = fut.next, nil } } @@ -685,26 +686,39 @@ func (conn *Connection) reader(r *bufio.Reader, c net.Conn) { conn.reconnect(err, c) return } - if fut := conn.fetchFuture(resp.RequestId); fut != nil { - fut.resp = resp - fut.markReady(conn) + + var fut *Future = nil + if resp.Code == PushCode { + if fut = conn.peekFuture(resp.RequestId); fut != nil { + fut.AppendPush(resp) + } } else { + if fut = conn.fetchFuture(resp.RequestId); fut != nil { + fut.SetResponse(resp) + conn.markDone(fut) + } + } + if fut == nil { conn.opts.Logger.Report(LogUnexpectedResultId, conn, resp) } } } func (conn *Connection) newFuture(requestCode int32) (fut *Future) { - fut = &Future{} + fut = NewFuture() if conn.rlimit != nil && conn.opts.RLimitAction == RLimitDrop { select { case conn.rlimit <- struct{}{}: default: - fut.err = ClientError{ErrRateLimited, "Request is rate limited on client"} + fut.err = ClientError{ + ErrRateLimited, + "Request is rate limited on client", + } + fut.ready = nil + fut.done = nil return } } - fut.ready = make(chan struct{}) fut.requestId = conn.nextRequestId() fut.requestCode = requestCode shardn := fut.requestId & (conn.opts.Concurrency - 1) @@ -712,13 +726,21 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { shard.rmut.Lock() switch conn.state { case connClosed: - fut.err = ClientError{ErrConnectionClosed, "using closed connection"} + fut.err = ClientError{ + ErrConnectionClosed, + "using closed connection", + } fut.ready = nil + fut.done = nil shard.rmut.Unlock() return case connDisconnected: - fut.err = ClientError{ErrConnectionNotReady, "client connection is not ready"} + fut.err = ClientError{ + ErrConnectionNotReady, + "client connection is not ready", + } fut.ready = nil + fut.done = nil shard.rmut.Unlock() return } @@ -737,9 +759,9 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { runtime.Gosched() select { case conn.rlimit <- struct{}{}: - case <-fut.ready: + case <-fut.done: if fut.err == nil { - panic("fut.ready is closed, but err is nil") + panic("fut.done is closed, but err is nil") } } } @@ -747,12 +769,28 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { return } +func (conn *Connection) sendFuture(fut *Future, body func(*msgpack.Encoder) error) *Future { + if fut.ready == nil { + return fut + } + conn.putFuture(fut, body) + return fut +} + +func (conn *Connection) failFuture(fut *Future, err error) *Future { + if f := conn.fetchFuture(fut.requestId); f == fut { + fut.SetError(err) + conn.markDone(fut) + } + return fut +} + func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error) { shardn := fut.requestId & (conn.opts.Concurrency - 1) shard := &conn.shard[shardn] shard.bufmut.Lock() select { - case <-fut.ready: + case <-fut.done: shard.bufmut.Unlock() return default: @@ -767,8 +805,8 @@ func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error shard.buf.Trunc(blen) shard.bufmut.Unlock() if f := conn.fetchFuture(fut.requestId); f == fut { - fut.markReady(conn) - fut.err = err + fut.SetError(err) + conn.markDone(fut) } else if f != nil { /* in theory, it is possible. In practice, you have * to have race condition that lasts hours */ @@ -782,7 +820,7 @@ func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error // packing error is more important than connection // error, because it is indication of programmer's // mistake. - fut.err = err + fut.SetError(err) } } return @@ -793,15 +831,40 @@ func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error } } +func (conn *Connection) markDone(fut *Future) { + if conn.rlimit != nil { + <-conn.rlimit + } +} + +func (conn *Connection) peekFuture(reqid uint32) (fut *Future) { + shard := &conn.shard[reqid&(conn.opts.Concurrency-1)] + pos := (reqid / conn.opts.Concurrency) & (requestsMap - 1) + shard.rmut.Lock() + defer shard.rmut.Unlock() + + if conn.opts.Timeout > 0 { + fut = conn.getFutureImp(reqid, true) + pair := &shard.requests[pos] + *pair.last = fut + pair.last = &fut.next + fut.timeout = time.Since(epoch) + conn.opts.Timeout + } else { + fut = conn.getFutureImp(reqid, false) + } + + return fut +} + func (conn *Connection) fetchFuture(reqid uint32) (fut *Future) { shard := &conn.shard[reqid&(conn.opts.Concurrency-1)] shard.rmut.Lock() - fut = conn.fetchFutureImp(reqid) + fut = conn.getFutureImp(reqid, true) shard.rmut.Unlock() return fut } -func (conn *Connection) fetchFutureImp(reqid uint32) *Future { +func (conn *Connection) getFutureImp(reqid uint32, fetch bool) *Future { shard := &conn.shard[reqid&(conn.opts.Concurrency-1)] pos := (reqid / conn.opts.Concurrency) & (requestsMap - 1) pair := &shard.requests[pos] @@ -812,11 +875,13 @@ func (conn *Connection) fetchFutureImp(reqid uint32) *Future { return nil } if fut.requestId == reqid { - *root = fut.next - if fut.next == nil { - pair.last = root - } else { - fut.next = nil + if fetch { + *root = fut.next + if fut.next == nil { + pair.last = root + } else { + fut.next = nil + } } return fut } @@ -851,11 +916,11 @@ func (conn *Connection) timeouts() { } else { fut.next = nil } - fut.err = ClientError{ + fut.SetError(ClientError{ Code: ErrTimeouted, Msg: fmt.Sprintf("client timeout for request %d", fut.requestId), - } - fut.markReady(conn) + }) + conn.markDone(fut) shard.bufmut.Unlock() } if pair.first != nil && pair.first.timeout < minNext { diff --git a/const.go b/const.go index 5152f8e43..f542c8171 100644 --- a/const.go +++ b/const.go @@ -61,6 +61,7 @@ const ( RLimitWait = 2 OkCode = uint32(0) + PushCode = uint32(0x80) ErrorCodeBit = 0x8000 PacketLengthBytes = 5 ErSpaceExistsCode = 0xa diff --git a/example_test.go b/example_test.go index b14dc4e81..b8f8ee110 100644 --- a/example_test.go +++ b/example_test.go @@ -126,6 +126,39 @@ func ExampleConnection_SelectAsync() { // Future 2 Data [[18 val 18 bla]] } +func ExampleFuture_GetIterator() { + conn := example_connect() + defer conn.Close() + + const timeout = 3 * time.Second + // Or any other Connection.*Async() call. + fut := conn.Call17Async("push_func", []interface{}{4}) + + var it tarantool.ResponseIterator + for it = fut.GetIterator().WithTimeout(timeout); it.Next(); { + resp := it.Value() + if resp.Code == tarantool.PushCode { + // It is a push message. + fmt.Printf("push message: %d\n", resp.Data[0].(uint64)) + } else if resp.Code == tarantool.OkCode { + // It is a regular response. + fmt.Printf("response: %d", resp.Data[0].(uint64)) + } else { + fmt.Printf("an unexpected response code %d", resp.Code) + } + } + if err := it.Err(); err != nil { + fmt.Printf("error in call of push_func is %v", err) + return + } + // Output: + // push message: 1 + // push message: 2 + // push message: 3 + // push message: 4 + // response: 4 +} + func ExampleConnection_Ping() { conn := example_connect() defer conn.Close() diff --git a/future.go b/future.go index fbf141f82..c077e8271 100644 --- a/future.go +++ b/future.go @@ -1,6 +1,7 @@ package tarantool import ( + "sync" "time" "gopkg.in/vmihailenco/msgpack.v2" @@ -11,15 +12,193 @@ type Future struct { requestId uint32 requestCode int32 timeout time.Duration + mutex sync.Mutex + pushes []*Response resp *Response err error ready chan struct{} + done chan struct{} next *Future } +func (fut *Future) pack(h *smallWBuf, enc *msgpack.Encoder, body func(*msgpack.Encoder) error) (err error) { + rid := fut.requestId + hl := h.Len() + h.Write([]byte{ + 0xce, 0, 0, 0, 0, // Length. + 0x82, // 2 element map. + KeyCode, byte(fut.requestCode), // Request code. + KeySync, 0xce, + byte(rid >> 24), byte(rid >> 16), + byte(rid >> 8), byte(rid), + }) + + if err = body(enc); err != nil { + return + } + + l := uint32(h.Len() - 5 - hl) + h.b[hl+1] = byte(l >> 24) + h.b[hl+2] = byte(l >> 16) + h.b[hl+3] = byte(l >> 8) + h.b[hl+4] = byte(l) + + return +} + +func (fut *Future) wait() { + if fut.done == nil { + return + } + <-fut.done +} + +func (fut *Future) isDone() bool { + if fut.done == nil { + return true + } + select { + case <-fut.done: + return true + default: + return false + } +} + +type asyncResponseIterator struct { + fut *Future + timeout time.Duration + resp *Response + err error + curPos int + done bool +} + +func (it *asyncResponseIterator) Next() bool { + if it.done || it.err != nil { + it.resp = nil + return false + } + + var last = false + var exit = false + for !exit { + // We try to read at least once. + it.fut.mutex.Lock() + it.resp = it.nextResponse() + it.err = it.fut.err + last = it.resp == it.fut.resp + it.fut.mutex.Unlock() + + if it.timeout == 0 || it.resp != nil || it.err != nil { + break + } + + select { + case <-it.fut.ready: + case <-time.After(it.timeout): + exit = true + } + } + + if it.resp == nil { + return false + } + + if it.err = it.resp.decodeBody(); it.err != nil { + it.resp = nil + return false + } + + if last { + it.done = true + } else { + it.curPos += 1 + } + + return true +} + +func (it *asyncResponseIterator) Value() *Response { + return it.resp +} + +func (it *asyncResponseIterator) Err() error { + return it.err +} + +func (it *asyncResponseIterator) WithTimeout(timeout time.Duration) TimeoutResponseIterator { + it.timeout = timeout + return it +} + +func (it *asyncResponseIterator) nextResponse() (resp *Response) { + fut := it.fut + pushesLen := len(fut.pushes) + + if it.curPos < pushesLen { + resp = fut.pushes[it.curPos] + } else if it.curPos == pushesLen { + resp = fut.resp + } + + return resp +} + +// NewFuture creates a new empty Future. +func NewFuture() (fut *Future) { + fut = &Future{} + fut.ready = make(chan struct{}, 1000000000) + fut.done = make(chan struct{}) + fut.pushes = make([]*Response, 0) + return fut +} + // NewErrorFuture returns new set empty Future with filled error field. func NewErrorFuture(err error) *Future { - return &Future{err: err} + fut := NewFuture() + fut.SetError(err) + return fut +} + +// AppendPush appends the push response to the future. +// Note: it works only before SetResponse() or SetError() +func (fut *Future) AppendPush(resp *Response) { + if fut.isDone() { + return + } + resp.Code = PushCode + fut.mutex.Lock() + fut.pushes = append(fut.pushes, resp) + fut.mutex.Unlock() + + fut.ready <- struct{}{} +} + +// SetResponse sets a response for the future and finishes the future. +func (fut *Future) SetResponse(resp *Response) { + if fut.isDone() { + return + } + fut.mutex.Lock() + fut.resp = resp + fut.mutex.Unlock() + + close(fut.ready) + close(fut.done) +} + +// SetError sets an error for the future and finishes the future. +func (fut *Future) SetError(err error) { + if fut.isDone() { + return + } + fut.mutex.Lock() + fut.err = err + fut.mutex.Unlock() + + close(fut.ready) + close(fut.done) } // Get waits for Future to be filled and returns Response and error. @@ -36,8 +215,11 @@ func (fut *Future) Get() (*Response, error) { if fut.err != nil { return fut.resp, fut.err } - fut.err = fut.resp.decodeBody() - return fut.resp, fut.err + err := fut.resp.decodeBody() + if err != nil { + fut.err = err + } + return fut.resp, err } // GetTyped waits for Future and calls msgpack.Decoder.Decode(result) if no error happens. @@ -49,8 +231,26 @@ func (fut *Future) GetTyped(result interface{}) error { if fut.err != nil { return fut.err } - fut.err = fut.resp.decodeBodyTyped(result) - return fut.err + err := fut.resp.decodeBodyTyped(result) + if err != nil { + fut.err = err + } + return err +} + +// GetIterator returns an iterator for iterating through push messages +// and a response. Push messages and the response will contain deserialized +// result in Data field as for the Get() function. +// +// See also +// +// * box.session.push() https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_session/push/ +// +func (fut *Future) GetIterator() (it TimeoutResponseIterator) { + futit := &asyncResponseIterator{ + fut: fut, + } + return futit } var closedChan = make(chan struct{}) @@ -61,10 +261,10 @@ func init() { // WaitChan returns channel which becomes closed when response arrived or error occured. func (fut *Future) WaitChan() <-chan struct{} { - if fut.ready == nil { + if fut.done == nil { return closedChan } - return fut.ready + return fut.done } // Err returns error set on Future. @@ -74,58 +274,3 @@ func (fut *Future) Err() error { fut.wait() return fut.err } - -func (fut *Future) pack(h *smallWBuf, enc *msgpack.Encoder, body func(*msgpack.Encoder) error) (err error) { - rid := fut.requestId - hl := h.Len() - h.Write([]byte{ - 0xce, 0, 0, 0, 0, // Length. - 0x82, // 2 element map. - KeyCode, byte(fut.requestCode), // Request code. - KeySync, 0xce, - byte(rid >> 24), byte(rid >> 16), - byte(rid >> 8), byte(rid), - }) - - if err = body(enc); err != nil { - return - } - - l := uint32(h.Len() - 5 - hl) - h.b[hl+1] = byte(l >> 24) - h.b[hl+2] = byte(l >> 16) - h.b[hl+3] = byte(l >> 8) - h.b[hl+4] = byte(l) - - return -} - -func (fut *Future) send(conn *Connection, body func(*msgpack.Encoder) error) *Future { - if fut.ready == nil { - return fut - } - conn.putFuture(fut, body) - return fut -} - -func (fut *Future) markReady(conn *Connection) { - close(fut.ready) - if conn.rlimit != nil { - <-conn.rlimit - } -} - -func (fut *Future) fail(conn *Connection, err error) *Future { - if f := conn.fetchFuture(fut.requestId); f == fut { - f.err = err - fut.markReady(conn) - } - return fut -} - -func (fut *Future) wait() { - if fut.ready == nil { - return - } - <-fut.ready -} diff --git a/future_test.go b/future_test.go new file mode 100644 index 000000000..d6d800530 --- /dev/null +++ b/future_test.go @@ -0,0 +1,235 @@ +package tarantool_test + +import ( + "errors" + "sync" + "testing" + "time" + + . "github.com/tarantool/go-tarantool" +) + +func assertResponseIteratorValue(t testing.TB, it ResponseIterator, + code uint32, resp *Response) { + t.Helper() + + if it.Err() != nil { + t.Errorf("An unexpected iteration error: %q", it.Err().Error()) + } + + if it.Value() == nil { + t.Errorf("An unexpected nil value") + } else if it.Value().Code != code { + t.Errorf("An unexpected response code %d, expected %d", it.Value().Code, code) + } + + if it.Value() != resp { + t.Errorf("An unexpected response %v, expected %v", it.Value(), resp) + } +} + +func assertResponseIteratorFinished(t testing.TB, it ResponseIterator) { + t.Helper() + + if it.Err() != nil { + t.Errorf("An unexpected iteration error: %q", it.Err().Error()) + } + if it.Value() != nil { + t.Errorf("An unexpected value %v", it.Value()) + } +} + +func TestFutureGetIteratorNoItems(t *testing.T) { + fut := NewFuture() + + it := fut.GetIterator() + if it.Next() { + t.Errorf("An unexpected next value.") + } else { + assertResponseIteratorFinished(t, it) + } +} + +func TestFutureGetIteratorNoResponse(t *testing.T) { + push := &Response{} + fut := NewFuture() + fut.AppendPush(push) + + if it := fut.GetIterator(); it.Next() { + assertResponseIteratorValue(t, it, PushCode, push) + if it.Next() == true { + t.Errorf("An unexpected next value.") + } + assertResponseIteratorFinished(t, it) + } else { + t.Errorf("A push message expected.") + } +} + +func TestFutureGetIteratorNoResponseTimeout(t *testing.T) { + push := &Response{} + fut := NewFuture() + fut.AppendPush(push) + + if it := fut.GetIterator().WithTimeout(1 * time.Nanosecond); it.Next() { + assertResponseIteratorValue(t, it, PushCode, push) + if it.Next() == true { + t.Errorf("An unexpected next value.") + } + assertResponseIteratorFinished(t, it) + } else { + t.Errorf("A push message expected.") + } +} + +func TestFutureGetIteratorResponseOnTimeout(t *testing.T) { + push := &Response{} + resp := &Response{} + fut := NewFuture() + fut.AppendPush(push) + + var done sync.WaitGroup + var wait sync.WaitGroup + wait.Add(1) + done.Add(1) + + go func() { + defer done.Done() + + var it ResponseIterator + var cnt = 0 + for it = fut.GetIterator().WithTimeout(5 * time.Second); it.Next(); { + code := PushCode + r := push + if cnt == 1 { + code = OkCode + r = resp + } + assertResponseIteratorValue(t, it, code, r) + cnt += 1 + if cnt == 1 { + wait.Done() + } + } + assertResponseIteratorFinished(t, it) + + if cnt != 2 { + t.Errorf("An unexpected count of responses %d != %d", cnt, 2) + } + }() + + wait.Wait() + fut.SetResponse(resp) + done.Wait() +} + +func TestFutureGetIteratorFirstResponse(t *testing.T) { + resp1 := &Response{} + resp2 := &Response{} + fut := NewFuture() + fut.SetResponse(resp1) + fut.SetResponse(resp2) + + if it := fut.GetIterator(); it.Next() { + assertResponseIteratorValue(t, it, OkCode, resp1) + if it.Next() == true { + t.Errorf("An unexpected next value.") + } + assertResponseIteratorFinished(t, it) + } else { + t.Errorf("A response expected.") + } +} + +func TestFutureGetIteratorFirstError(t *testing.T) { + const errMsg1 = "error1" + const errMsg2 = "error2" + + fut := NewFuture() + fut.SetError(errors.New(errMsg1)) + fut.SetError(errors.New(errMsg2)) + + it := fut.GetIterator() + if it.Next() { + t.Errorf("An unexpected value.") + } else if it.Err() == nil { + t.Errorf("An error expected.") + } else if it.Err().Error() != errMsg1 { + t.Errorf("An unexpected error %q, expected %q", it.Err().Error(), errMsg1) + } +} + +func TestFutureGetIteratorResponse(t *testing.T) { + responses := []*Response{ + {}, + {}, + {Code: OkCode}, + } + fut := NewFuture() + for i, resp := range responses { + if i == len(responses)-1 { + fut.SetResponse(resp) + } else { + fut.AppendPush(resp) + } + } + + var its = []ResponseIterator{ + fut.GetIterator(), + fut.GetIterator().WithTimeout(5 * time.Second), + } + for _, it := range its { + var cnt = 0 + for it.Next() { + code := PushCode + if cnt == len(responses)-1 { + code = OkCode + } + assertResponseIteratorValue(t, it, code, responses[cnt]) + cnt += 1 + } + assertResponseIteratorFinished(t, it) + + if cnt != len(responses) { + t.Errorf("An unexpected count of responses %d != %d", cnt, len(responses)) + } + } +} + +func TestFutureGetIteratorError(t *testing.T) { + const errMsg = "error message" + responses := []*Response{ + {}, + {}, + } + err := errors.New(errMsg) + fut := NewFuture() + for _, resp := range responses { + fut.AppendPush(resp) + } + fut.SetError(err) + + var its = []ResponseIterator{ + fut.GetIterator(), + fut.GetIterator().WithTimeout(5 * time.Second), + } + for _, it := range its { + var cnt = 0 + for it.Next() { + code := PushCode + assertResponseIteratorValue(t, it, code, responses[cnt]) + cnt += 1 + } + if err = it.Err(); err != nil { + if err.Error() != errMsg { + t.Errorf("An unexpected error %q, expected %q", err.Error(), errMsg) + } + } else { + t.Errorf("An error expected.") + } + + if cnt != len(responses) { + t.Errorf("An unexpected count of responses %d != %d", cnt, len(responses)) + } + } +} diff --git a/request.go b/request.go index 541fb2b4f..e672f0b17 100644 --- a/request.go +++ b/request.go @@ -12,7 +12,7 @@ import ( // Ping sends empty request to Tarantool to check connection. func (conn *Connection) Ping() (resp *Response, err error) { future := conn.newFuture(PingRequest) - return future.send(conn, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(0); return nil }).Get() + return conn.sendFuture(future, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(0); return nil }).Get() } func fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { @@ -226,9 +226,9 @@ func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, ite future := conn.newFuture(SelectRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) if err != nil { - return future.fail(conn, err) + return conn.failFuture(future, err) } - return future.send(conn, func(enc *msgpack.Encoder) error { + return conn.sendFuture(future, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(6) fillIterator(enc, offset, limit, iterator) return fillSearch(enc, spaceNo, indexNo, key) @@ -241,9 +241,9 @@ func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Futur future := conn.newFuture(InsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { - return future.fail(conn, err) + return conn.failFuture(future, err) } - return future.send(conn, func(enc *msgpack.Encoder) error { + return conn.sendFuture(future, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) return fillInsert(enc, spaceNo, tuple) }) @@ -255,9 +255,9 @@ func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Futu future := conn.newFuture(ReplaceRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { - return future.fail(conn, err) + return conn.failFuture(future, err) } - return future.send(conn, func(enc *msgpack.Encoder) error { + return conn.sendFuture(future, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) return fillInsert(enc, spaceNo, tuple) }) @@ -269,9 +269,9 @@ func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) * future := conn.newFuture(DeleteRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) if err != nil { - return future.fail(conn, err) + return conn.failFuture(future, err) } - return future.send(conn, func(enc *msgpack.Encoder) error { + return conn.sendFuture(future, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(3) return fillSearch(enc, spaceNo, indexNo, key) }) @@ -283,9 +283,9 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface future := conn.newFuture(UpdateRequest) spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) if err != nil { - return future.fail(conn, err) + return conn.failFuture(future, err) } - return future.send(conn, func(enc *msgpack.Encoder) error { + return conn.sendFuture(future, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(4) if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { return err @@ -301,9 +301,9 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops in future := conn.newFuture(UpsertRequest) spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) if err != nil { - return future.fail(conn, err) + return conn.failFuture(future, err) } - return future.send(conn, func(enc *msgpack.Encoder) error { + return conn.sendFuture(future, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(3) enc.EncodeUint64(KeySpaceNo) enc.EncodeUint64(uint64(spaceNo)) @@ -320,7 +320,7 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops in // It uses request code for Tarantool 1.6, so future's result is always array of arrays func (conn *Connection) CallAsync(functionName string, args interface{}) *Future { future := conn.newFuture(CallRequest) - return future.send(conn, func(enc *msgpack.Encoder) error { + return conn.sendFuture(future, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) enc.EncodeUint64(KeyFunctionName) enc.EncodeString(functionName) @@ -334,7 +334,7 @@ func (conn *Connection) CallAsync(functionName string, args interface{}) *Future // (though, keep in mind, result is always array) func (conn *Connection) Call17Async(functionName string, args interface{}) *Future { future := conn.newFuture(Call17Request) - return future.send(conn, func(enc *msgpack.Encoder) error { + return conn.sendFuture(future, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) enc.EncodeUint64(KeyFunctionName) enc.EncodeString(functionName) @@ -346,7 +346,7 @@ func (conn *Connection) Call17Async(functionName string, args interface{}) *Futu // EvalAsync sends a Lua expression for evaluation and returns Future. func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { future := conn.newFuture(EvalRequest) - return future.send(conn, func(enc *msgpack.Encoder) error { + return conn.sendFuture(future, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) enc.EncodeUint64(KeyExpression) enc.EncodeString(expr) @@ -359,7 +359,7 @@ func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { // Since 1.6.0 func (conn *Connection) ExecuteAsync(expr string, args interface{}) *Future { future := conn.newFuture(ExecuteRequest) - return future.send(conn, func(enc *msgpack.Encoder) error { + return conn.sendFuture(future, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(2) enc.EncodeUint64(KeySQLText) enc.EncodeString(expr) diff --git a/response.go b/response.go index 9fcca64da..b7a15706f 100644 --- a/response.go +++ b/response.go @@ -184,7 +184,7 @@ func (resp *Response) decodeBody() (err error) { } } } - if resp.Code != OkCode { + if resp.Code != OkCode && resp.Code != PushCode { resp.Code &^= ErrorCodeBit err = Error{resp.Code, resp.Error} } @@ -227,7 +227,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { } } } - if resp.Code != OkCode { + if resp.Code != OkCode && resp.Code != PushCode { resp.Code &^= ErrorCodeBit err = Error{resp.Code, resp.Error} } diff --git a/response_it.go b/response_it.go new file mode 100644 index 000000000..3ae296449 --- /dev/null +++ b/response_it.go @@ -0,0 +1,26 @@ +package tarantool + +import ( + "time" +) + +// ResponseIterator is an interface for iteration over a set of responses. +type ResponseIterator interface { + // Next tries to switch to a next Response and returns true if it exists. + Next() bool + // Value returns a current Response if it exists, nil otherwise. + Value() *Response + // Err returns error if it happens. + Err() error +} + +// TimeoutResponseIterator is an interface that extends ResponseIterator +// and adds the ability to change a timeout for the Next() call. +type TimeoutResponseIterator interface { + ResponseIterator + // WithTimeout allows to set up a timeout for the Next() call. + // Note: in the current implementation, there is a timeout for each + // response (the timeout for the request is reset by each push message): + // Connection's Opts.Timeout. You need to increase the value if necessary. + WithTimeout(timeout time.Duration) TimeoutResponseIterator +} diff --git a/tarantool_test.go b/tarantool_test.go index acd8577c9..438f3a18b 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -22,6 +22,19 @@ type Member struct { Val uint } +func connect(t testing.TB, server string, opts Opts) (conn *Connection) { + t.Helper() + + conn, err := Connect(server, opts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if conn == nil { + t.Fatalf("conn is nil after Connect") + } + return conn +} + func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeSliceLen(2); err != nil { return err @@ -71,11 +84,7 @@ const N = 500 func BenchmarkClientSerial(b *testing.B) { var err error - conn, err := Connect(server, opts) - if err != nil { - b.Errorf("No connection available") - return - } + conn := connect(b, server, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -95,11 +104,7 @@ func BenchmarkClientSerial(b *testing.B) { func BenchmarkClientSerialTyped(b *testing.B) { var err error - conn, err := Connect(server, opts) - if err != nil { - b.Errorf("No connection available") - return - } + conn := connect(b, server, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -120,11 +125,7 @@ func BenchmarkClientSerialTyped(b *testing.B) { func BenchmarkClientFuture(b *testing.B) { var err error - conn, err := Connect(server, opts) - if err != nil { - b.Error(err) - return - } + conn := connect(b, server, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -151,11 +152,7 @@ func BenchmarkClientFuture(b *testing.B) { func BenchmarkClientFutureTyped(b *testing.B) { var err error - conn, err := Connect(server, opts) - if err != nil { - b.Errorf("No connection available") - return - } + conn := connect(b, server, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -185,11 +182,7 @@ func BenchmarkClientFutureTyped(b *testing.B) { func BenchmarkClientFutureParallel(b *testing.B) { var err error - conn, err := Connect(server, opts) - if err != nil { - b.Errorf("No connection available") - return - } + conn := connect(b, server, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -222,11 +215,7 @@ func BenchmarkClientFutureParallel(b *testing.B) { func BenchmarkClientFutureParallelTyped(b *testing.B) { var err error - conn, err := Connect(server, opts) - if err != nil { - b.Errorf("No connection available") - return - } + conn := connect(b, server, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -262,14 +251,10 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { } func BenchmarkClientParallel(b *testing.B) { - conn, err := Connect(server, opts) - if err != nil { - b.Errorf("No connection available") - return - } + conn := connect(b, server, opts) defer conn.Close() - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Fatal("No connection available") } @@ -287,14 +272,10 @@ func BenchmarkClientParallel(b *testing.B) { } func BenchmarkClientParallelMassive(b *testing.B) { - conn, err := Connect(server, opts) - if err != nil { - b.Errorf("No connection available") - return - } + conn := connect(b, server, opts) defer conn.Close() - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Fatal("No connection available") } @@ -326,14 +307,10 @@ func BenchmarkClientParallelMassive(b *testing.B) { } func BenchmarkClientParallelMassiveUntyped(b *testing.B) { - conn, err := Connect(server, opts) - if err != nil { - b.Errorf("No connection available") - return - } + conn := connect(b, server, opts) defer conn.Close() - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("No connection available") } @@ -488,15 +465,8 @@ func BenchmarkSQLSerial(b *testing.B) { func TestClient(t *testing.T) { var resp *Response var err error - var conn *Connection - conn, err = Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if conn == nil { - t.Fatalf("conn is nil after Connect") - } + conn := connect(t, server, opts) defer conn.Close() // Ping @@ -798,6 +768,100 @@ func TestClient(t *testing.T) { } } +func TestClientSessionPush(t *testing.T) { + conn := connect(t, server, opts) + defer conn.Close() + + var it ResponseIterator + const pushMax = 3 + // It will be iterated immediately. + fut0 := conn.Call17Async("push_func", []interface{}{pushMax}) + respCnt := 0 + for it = fut0.GetIterator(); it.Next(); { + err := it.Err() + resp := it.Value() + if err != nil { + t.Errorf("Unexpected error after it.Next() == true: %q", err.Error()) + break + } + if resp == nil { + t.Errorf("Response is empty after it.Next() == true") + break + } + respCnt += 1 + } + if err := it.Err(); err != nil { + t.Errorf("An unexpected iteration error: %s", err.Error()) + } + if respCnt > pushMax+1 { + t.Errorf("Unexpected respCnt = %d, expected 0 <= respCnt <= %d", respCnt, pushMax+1) + } + _, _ = fut0.Get() + + // It will wait a response before iteration. + fut1 := conn.Call17Async("push_func", []interface{}{pushMax}) + // Future.Get ignores push messages. + resp, err := fut1.Get() + if err != nil { + t.Errorf("Failed to Call: %s", err.Error()) + } else if resp == nil { + t.Errorf("Response is nil after CallAsync") + } else if len(resp.Data) < 1 { + t.Errorf("Response.Data is empty after CallAsync") + } else if resp.Data[0].(uint64) != pushMax { + t.Errorf("result is not {{1}} : %v", resp.Data) + } + + // It will will be iterated with a timeout. + fut2 := conn.Call17Async("push_func", []interface{}{pushMax}) + + var its = []ResponseIterator{ + fut1.GetIterator(), + fut2.GetIterator().WithTimeout(5 * time.Second), + } + + for i := 0; i < len(its); i++ { + pushCnt := uint64(0) + respCnt := uint64(0) + + it = its[i] + for it.Next() { + resp = it.Value() + if resp == nil { + t.Errorf("Response is empty after it.Next() == true") + break + } + if len(resp.Data) < 1 { + t.Errorf("Response.Data is empty after CallAsync") + break + } + if resp.Code == PushCode { + pushCnt += 1 + if resp.Data[0].(uint64) != pushCnt { + t.Errorf("Unexpected push data = %v", resp.Data) + } + } else { + respCnt += 1 + if resp.Data[0].(uint64) != pushMax { + t.Errorf("result is not {{1}} : %v", resp.Data) + } + } + } + + if err = it.Err(); err != nil { + t.Errorf("An unexpected iteration error: %s", err.Error()) + } + + if pushCnt != pushMax { + t.Errorf("Expect %d pushes but got %d", pushMax, pushCnt) + } + + if respCnt != 1 { + t.Errorf("Expect %d responses but got %d", 1, respCnt) + } + } +} + const ( createTableQuery = "CREATE TABLE SQL_SPACE (id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING COLLATE \"unicode\" DEFAULT NULL);" insertQuery = "INSERT INTO SQL_SPACE VALUES (?, ?);" @@ -974,10 +1038,7 @@ func TestSQL(t *testing.T) { }, } - var conn *Connection - conn, err = Connect(server, opts) - assert.Nil(t, err, "Failed to Connect") - assert.NotNil(t, conn, "conn is nil after Connect") + conn := connect(t, server, opts) defer conn.Close() for i, test := range testCases { @@ -1011,15 +1072,7 @@ func TestSQLTyped(t *testing.T) { t.Skip() } - var conn *Connection - - conn, err = Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if conn == nil { - t.Fatal("conn is nil after Connect") - } + conn := connect(t, server, opts) defer conn.Close() mem := []Member{} @@ -1054,15 +1107,8 @@ func TestSQLBindings(t *testing.T) { } var resp *Response - var conn *Connection - conn, err = Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if conn == nil { - t.Fatal("conn is nil after Connect") - } + conn := connect(t, server, opts) defer conn.Close() // test all types of supported bindings @@ -1170,15 +1216,8 @@ func TestStressSQL(t *testing.T) { } var resp *Response - var conn *Connection - conn, err = Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if conn == nil { - t.Fatalf("conn is nil after Connect") - } + conn := connect(t, server, opts) defer conn.Close() resp, err = conn.Execute(createTableQuery, []interface{}{}) @@ -1273,15 +1312,8 @@ func TestStressSQL(t *testing.T) { func TestSchema(t *testing.T) { var err error - var conn *Connection - conn, err = Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if conn == nil { - t.Fatalf("conn is nil after Connect") - } + conn := connect(t, server, opts) defer conn.Close() // Schema @@ -1455,15 +1487,8 @@ func TestSchema(t *testing.T) { func TestClientNamed(t *testing.T) { var resp *Response var err error - var conn *Connection - conn, err = Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if conn == nil { - t.Fatalf("conn is nil after Connect") - } + conn := connect(t, server, opts) defer conn.Close() // Insert @@ -1551,15 +1576,8 @@ func TestClientNamed(t *testing.T) { func TestComplexStructs(t *testing.T) { var err error - var conn *Connection - conn, err = Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if conn == nil { - t.Fatalf("conn is nil after Connect") - } + conn := connect(t, server, opts) defer conn.Close() tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} From 988edce5b1935d563db811a60731f809e5e5f5c5 Mon Sep 17 00:00:00 2001 From: AnaNek Date: Sat, 23 Apr 2022 17:08:03 +0300 Subject: [PATCH 284/605] code-health: allow to use `Call17` as default `Call` An incompatible change was introduced in Tarantool 1.7 which was released in 2016. This change is about a new binary protocol command for CALL, which no more restricts a function to returning an array of tuples and allows returning an arbitrary MsgPack/JSON result, including scalars, nil and void (nothing). We should be in-line with tarantool here and provide `Call17` as just `Call`. For backward compatibility in current `go-tarantool` version build tag `go_tarantool_call_17` have been defined. This tag used to change the default `Call` behavior from `Call16` to `Call17`. Closes #125 --- .github/workflows/testing.yml | 10 ++++ CHANGELOG.md | 3 ++ Makefile | 19 +++---- README.md | 18 +++++-- call_16_test.go | 27 ++++++++++ call_17_test.go | 27 ++++++++++ connection.go | 4 +- connection_pool/call_16_test.go | 69 +++++++++++++++++++++++++ connection_pool/call_17_test.go | 69 +++++++++++++++++++++++++ connection_pool/connection_pool.go | 56 +++++++++++++++++--- connection_pool/connection_pool_test.go | 2 +- connection_pool/example_test.go | 2 +- connector.go | 3 ++ const.go | 4 +- const_call_16.go | 8 +++ const_call_17.go | 8 +++ example_custom_unpacking_test.go | 6 +-- example_test.go | 2 +- multi/call_16_test.go | 33 ++++++++++++ multi/call_17_test.go | 33 ++++++++++++ multi/config.lua | 6 +++ multi/multi.go | 45 ++++++++++++---- multi/multi_test.go | 25 ++++++++- queue/queue.go | 21 ++++---- request.go | 52 ++++++++++++++++--- response.go | 2 +- tarantool_test.go | 20 +++---- test_helpers/pool_helper.go | 2 +- 28 files changed, 505 insertions(+), 71 deletions(-) create mode 100644 call_16_test.go create mode 100644 call_17_test.go create mode 100644 connection_pool/call_16_test.go create mode 100644 connection_pool/call_17_test.go create mode 100644 const_call_16.go create mode 100644 const_call_17.go create mode 100644 multi/call_16_test.go create mode 100644 multi/call_17_test.go diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 311ffac95..5c3c84eae 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -60,6 +60,9 @@ jobs: - name: Run tests run: make test + - name: Run tests with call_17 + run: make test TAGS="go_tarantool_call_17" + - name: Run tests, collect code coverage data and send to Coveralls if: ${{ matrix.coveralls }} env: @@ -134,6 +137,13 @@ jobs: env: TEST_TNT_SSL: ${{matrix.ssl}} + - name: Run tests with call_17 + run: | + source tarantool-enterprise/env.sh + make test TAGS="go_tarantool_call_17" + env: + TEST_TNT_SSL: ${{matrix.ssl}} + - name: Run tests, collect code coverage data and send to Coveralls if: ${{ matrix.coveralls }} env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f4e2f887..ddb5cbc8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Changed +- Add `Call16` method, support build tag `go_tarantool_call_17` + to choose behavior for `Call` method (#125) + ### Fixed ## [1.6.0] - 2022-06-01 diff --git a/Makefile b/Makefile index d88ce323e..06aa01189 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ BENCH_REFERENCE_REPO := ${BENCH_PATH}/go-tarantool BENCH_OPTIONS := -bench=. -run=^Benchmark -benchmem -benchtime=${DURATION} -count=${COUNT} GO_TARANTOOL_URL := https://github.com/tarantool/go-tarantool GO_TARANTOOL_DIR := ${PROJECT_DIR}/${BENCH_PATH}/go-tarantool +TAGS := .PHONY: clean clean: @@ -33,7 +34,7 @@ golangci-lint: .PHONY: test test: - go test ./... -v -p 1 + go test -tags "$(TAGS)" ./... -v -p 1 .PHONY: testdata testdata: @@ -43,38 +44,38 @@ testdata: test-connection-pool: @echo "Running tests in connection_pool package" go clean -testcache - go test ./connection_pool/ -v -p 1 + go test -tags "$(TAGS)" ./connection_pool/ -v -p 1 .PHONY: test-multi test-multi: @echo "Running tests in multiconnection package" go clean -testcache - go test ./multi/ -v -p 1 + go test -tags "$(TAGS)" ./multi/ -v -p 1 .PHONY: test-queue test-queue: @echo "Running tests in queue package" cd ./queue/ && tarantool -e "require('queue')" go clean -testcache - go test ./queue/ -v -p 1 + go test -tags "$(TAGS)" ./queue/ -v -p 1 .PHONY: test-uuid test-uuid: @echo "Running tests in UUID package" go clean -testcache - go test ./uuid/ -v -p 1 + go test -tags "$(TAGS)" ./uuid/ -v -p 1 .PHONY: test-main test-main: @echo "Running tests in main package" go clean -testcache - go test . -v -p 1 + go test -tags "$(TAGS)" . -v -p 1 .PHONY: coverage coverage: go clean -testcache go get golang.org/x/tools/cmd/cover - go test ./... -v -p 1 -covermode=atomic -coverprofile=$(COVERAGE_FILE) -coverpkg=./... + go test -tags "$(TAGS)" ./... -v -p 1 -covermode=atomic -coverprofile=$(COVERAGE_FILE) -coverpkg=./... go tool cover -func=$(COVERAGE_FILE) .PHONY: coveralls @@ -94,7 +95,7 @@ ${BENCH_PATH} bench-deps: .PHONY: bench ${BENCH_FILE} bench: ${BENCH_PATH} @echo "Running benchmark tests from the current branch" - go test ${TEST_PATH} ${BENCH_OPTIONS} 2>&1 \ + go test -tags "$(TAGS)" ${TEST_PATH} ${BENCH_OPTIONS} 2>&1 \ | tee ${BENCH_FILE} benchstat ${BENCH_FILE} @@ -104,7 +105,7 @@ ${GO_TARANTOOL_DIR}: ${REFERENCE_FILE}: ${GO_TARANTOOL_DIR} @echo "Running benchmark tests from master for using results in bench-diff target" - cd ${GO_TARANTOOL_DIR} && git pull && go test ./... ${BENCH_OPTIONS} 2>&1 \ + cd ${GO_TARANTOOL_DIR} && git pull && go test ./... -tags "$(TAGS)" ${BENCH_OPTIONS} 2>&1 \ | tee ${REFERENCE_FILE} bench-diff: ${BENCH_FILES} diff --git a/README.md b/README.md index a2b77eaa7..7ddf956bf 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,20 @@ of any Go program. ### Build tags -To disable SSL support and linking with OpenSSL, you can use the tag: -``` -go_tarantool_ssl_disable -``` +We define multiple [build tags](https://pkg.go.dev/go/build#hdr-Build_Constraints). + +This allows us to introduce new features without losing backward compatibility. + +1. To disable SSL support and linking with OpenSSL, you can use the tag: + ``` + go_tarantool_ssl_disable + ``` +2. to change the default `Call` behavior from `Call16` to `Call17`, you can use the build + tag: + ``` + go_tarantool_call_17 + ``` + **Note:** In future releases, `Call17` may be used as default `Call` behavior. ## Documentation diff --git a/call_16_test.go b/call_16_test.go new file mode 100644 index 000000000..1aff4e62e --- /dev/null +++ b/call_16_test.go @@ -0,0 +1,27 @@ +//go:build !go_tarantool_call_17 +// +build !go_tarantool_call_17 + +package tarantool_test + +import ( + "testing" + + . "github.com/tarantool/go-tarantool" +) + +func TestConnection_Call(t *testing.T) { + var resp *Response + var err error + + conn := connect(t, server, opts) + defer conn.Close() + + // Call16 + resp, err = conn.Call("simple_incr", []interface{}{1}) + if err != nil { + t.Errorf("Failed to use Call") + } + if resp.Data[0].([]interface{})[0].(uint64) != 2 { + t.Errorf("result is not {{1}} : %v", resp.Data) + } +} diff --git a/call_17_test.go b/call_17_test.go new file mode 100644 index 000000000..83f441401 --- /dev/null +++ b/call_17_test.go @@ -0,0 +1,27 @@ +//go:build go_tarantool_call_17 +// +build go_tarantool_call_17 + +package tarantool_test + +import ( + "testing" + + . "github.com/tarantool/go-tarantool" +) + +func TestConnection_Call(t *testing.T) { + var resp *Response + var err error + + conn := connect(t, server, opts) + defer conn.Close() + + // Call17 + resp, err = conn.Call17("simple_incr", []interface{}{1}) + if err != nil { + t.Errorf("Failed to use Call") + } + if resp.Data[0].(uint64) != 2 { + t.Errorf("result is not {{1}} : %v", resp.Data) + } +} diff --git a/connection.go b/connection.go index 5f5425531..c5a55b36f 100644 --- a/connection.go +++ b/connection.go @@ -116,9 +116,9 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac // // ATTENTION: result argument for *Typed methods should deserialize from // msgpack array, cause Tarantool always returns result as an array. -// For all space related methods and Call* (but not Call17*) methods Tarantool +// For all space related methods and Call16* (but not Call17*) methods Tarantool // always returns array of array (array of tuples for space related methods). -// For Eval* and Call17* Tarantool always returns array, but does not forces +// For Eval* and Call* Tarantool always returns array, but does not forces // array of arrays. type Connection struct { addr string diff --git a/connection_pool/call_16_test.go b/connection_pool/call_16_test.go new file mode 100644 index 000000000..bf2ab2eb7 --- /dev/null +++ b/connection_pool/call_16_test.go @@ -0,0 +1,69 @@ +//go:build !go_tarantool_call_17 +// +build !go_tarantool_call_17 + +package connection_pool_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/connection_pool" + "github.com/tarantool/go-tarantool/test_helpers" +) + +func TestCall(t *testing.T) { + roles := []bool{false, true, false, false, true} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // PreferRO + resp, err := connPool.Call("box.info", []interface{}{}, connection_pool.PreferRO) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val := resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] + ro, ok := val.(bool) + require.Truef(t, ok, "expected `true` with mode `PreferRO`") + require.Truef(t, ro, "expected `true` with mode `PreferRO`") + + // PreferRW + resp, err = connPool.Call("box.info", []interface{}{}, connection_pool.PreferRW) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `false` with mode `PreferRW`") + require.Falsef(t, ro, "expected `false` with mode `PreferRW`") + + // RO + resp, err = connPool.Call("box.info", []interface{}{}, connection_pool.RO) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `true` with mode `RO`") + require.Truef(t, ro, "expected `true` with mode `RO`") + + // RW + resp, err = connPool.Call("box.info", []interface{}{}, connection_pool.RW) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `false` with mode `RW`") + require.Falsef(t, ro, "expected `false` with mode `RW`") +} diff --git a/connection_pool/call_17_test.go b/connection_pool/call_17_test.go new file mode 100644 index 000000000..94e5bb888 --- /dev/null +++ b/connection_pool/call_17_test.go @@ -0,0 +1,69 @@ +//go:build go_tarantool_call_17 +// +build go_tarantool_call_17 + +package connection_pool_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/connection_pool" + "github.com/tarantool/go-tarantool/test_helpers" +) + +func TestCall(t *testing.T) { + roles := []bool{false, true, false, false, true} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // PreferRO + resp, err := connPool.Call("box.info", []interface{}{}, connection_pool.PreferRO) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val := resp.Data[0].(map[interface{}]interface{})["ro"] + ro, ok := val.(bool) + require.Truef(t, ok, "expected `true` with mode `PreferRO`") + require.Truef(t, ro, "expected `true` with mode `PreferRO`") + + // PreferRW + resp, err = connPool.Call("box.info", []interface{}{}, connection_pool.PreferRW) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `false` with mode `PreferRW`") + require.Falsef(t, ro, "expected `false` with mode `PreferRW`") + + // RO + resp, err = connPool.Call("box.info", []interface{}{}, connection_pool.RO) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `true` with mode `RO`") + require.Truef(t, ro, "expected `true` with mode `RO`") + + // RW + resp, err = connPool.Call("box.info", []interface{}{}, connection_pool.RW) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `false` with mode `RW`") + require.Falsef(t, ro, "expected `false` with mode `RW`") +} diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index d4a343911..be56806c5 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -255,8 +255,10 @@ func (connPool *ConnectionPool) Upsert(space interface{}, tuple, ops interface{} return conn.Upsert(space, tuple, ops) } -// Call calls registered Tarantool function. -// It uses request code for Tarantool 1.6, so result is converted to array of arrays. +// Call16 calls registered Tarantool function. +// It uses request code for Tarantool >= 1.7 if go-tarantool +// was build with go_tarantool_call_17 tag. +// Otherwise, uses request code for Tarantool 1.6. func (connPool *ConnectionPool) Call(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -266,8 +268,20 @@ func (connPool *ConnectionPool) Call(functionName string, args interface{}, user return conn.Call(functionName, args) } +// Call16 calls registered Tarantool function. +// It uses request code for Tarantool 1.6, so result is converted to array of arrays. +// Deprecated since Tarantool 1.7.2. +func (connPool *ConnectionPool) Call16(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return nil, err + } + + return conn.Call16(functionName, args) +} + // Call17 calls registered Tarantool function. -// It uses request code for Tarantool 1.7, so result is not converted +// It uses request code for Tarantool >= 1.7, so result is not converted // (though, keep in mind, result is always array). func (connPool *ConnectionPool) Call17(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getNextConnection(userMode) @@ -352,7 +366,9 @@ func (connPool *ConnectionPool) UpdateTyped(space, index interface{}, key, ops i } // CallTyped calls registered function. -// It uses request code for Tarantool 1.6, so result is converted to array of arrays. +// It uses request code for Tarantool >= 1.7 if go-tarantool +// was build with go_tarantool_call_17 tag. +// Otherwise, uses request code for Tarantool 1.6. func (connPool *ConnectionPool) CallTyped(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -362,8 +378,20 @@ func (connPool *ConnectionPool) CallTyped(functionName string, args interface{}, return conn.CallTyped(functionName, args, result) } +// Call16Typed calls registered function. +// It uses request code for Tarantool 1.6, so result is converted to array of arrays. +// Deprecated since Tarantool 1.7.2. +func (connPool *ConnectionPool) Call16Typed(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return err + } + + return conn.Call16Typed(functionName, args, result) +} + // Call17Typed calls registered function. -// It uses request code for Tarantool 1.7, so result is not converted +// It uses request code for Tarantool >= 1.7, so result is not converted // (though, keep in mind, result is always array). func (connPool *ConnectionPool) Call17Typed(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { conn, err := connPool.getNextConnection(userMode) @@ -450,7 +478,9 @@ func (connPool *ConnectionPool) UpsertAsync(space interface{}, tuple interface{} } // CallAsync sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool 1.6, so future's result is always array of arrays. +// It uses request code for Tarantool >= 1.7 if go-tarantool +// was build with go_tarantool_call_17 tag. +// Otherwise, uses request code for Tarantool 1.6. func (connPool *ConnectionPool) CallAsync(functionName string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -460,8 +490,20 @@ func (connPool *ConnectionPool) CallAsync(functionName string, args interface{}, return conn.CallAsync(functionName, args) } +// Call16Async sends a call to registered Tarantool function and returns Future. +// It uses request code for Tarantool 1.6, so future's result is always array of arrays. +// Deprecated since Tarantool 1.7.2. +func (connPool *ConnectionPool) Call16Async(functionName string, args interface{}, userMode Mode) *tarantool.Future { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return tarantool.NewErrorFuture(err) + } + + return conn.Call16Async(functionName, args) +} + // Call17Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool 1.7, so future's result will not be converted +// It uses request code for Tarantool >= 1.7, so future's result will not be converted // (though, keep in mind, result is always array). func (connPool *ConnectionPool) Call17Async(functionName string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 4d98ddd9e..5b8a606cd 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -206,7 +206,7 @@ func TestClose(t *testing.T) { require.Nil(t, err) } -func TestCall(t *testing.T) { +func TestCall17(t *testing.T) { roles := []bool{false, true, false, false, true} err := test_helpers.SetClusterRO(servers, connOpts, roles) diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go index 4da811daf..f567128dd 100644 --- a/connection_pool/example_test.go +++ b/connection_pool/example_test.go @@ -490,7 +490,7 @@ func ExampleConnectionPool_Update() { // Data [[key1 new_value]] } -func ExampleConnectionPool_Call17() { +func ExampleConnectionPool_Call() { pool, err := examplePool(testRoles) if err != nil { fmt.Println(err) diff --git a/connector.go b/connector.go index 0e79c6aaf..d8bdc13a0 100644 --- a/connector.go +++ b/connector.go @@ -15,6 +15,7 @@ type Connector interface { Update(space, index interface{}, key, ops interface{}) (resp *Response, err error) Upsert(space interface{}, tuple, ops interface{}) (resp *Response, err error) Call(functionName string, args interface{}) (resp *Response, err error) + Call16(functionName string, args interface{}) (resp *Response, err error) Call17(functionName string, args interface{}) (resp *Response, err error) Eval(expr string, args interface{}) (resp *Response, err error) Execute(expr string, args interface{}) (resp *Response, err error) @@ -26,6 +27,7 @@ type Connector interface { DeleteTyped(space, index interface{}, key interface{}, result interface{}) (err error) UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) (err error) CallTyped(functionName string, args interface{}, result interface{}) (err error) + Call16Typed(functionName string, args interface{}, result interface{}) (err error) Call17Typed(functionName string, args interface{}, result interface{}) (err error) EvalTyped(expr string, args interface{}, result interface{}) (err error) @@ -36,6 +38,7 @@ type Connector interface { UpdateAsync(space, index interface{}, key, ops interface{}) *Future UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *Future CallAsync(functionName string, args interface{}) *Future + Call16Async(functionName string, args interface{}) *Future Call17Async(functionName string, args interface{}) *Future EvalAsync(expr string, args interface{}) *Future } diff --git a/const.go b/const.go index f542c8171..53e7017dd 100644 --- a/const.go +++ b/const.go @@ -6,11 +6,11 @@ const ( ReplaceRequest = 3 UpdateRequest = 4 DeleteRequest = 5 - CallRequest = 6 /* call in 1.6 format */ + Call16Request = 6 /* call in 1.6 format */ AuthRequest = 7 EvalRequest = 8 UpsertRequest = 9 - Call17Request = 10 + Call17Request = 10 /* call in >= 1.7 format */ ExecuteRequest = 11 PingRequest = 64 SubscribeRequest = 66 diff --git a/const_call_16.go b/const_call_16.go new file mode 100644 index 000000000..90ce7e6ce --- /dev/null +++ b/const_call_16.go @@ -0,0 +1,8 @@ +//go:build !go_tarantool_call_17 +// +build !go_tarantool_call_17 + +package tarantool + +const ( + CallRequest = Call16Request +) diff --git a/const_call_17.go b/const_call_17.go new file mode 100644 index 000000000..3b012c8ac --- /dev/null +++ b/const_call_17.go @@ -0,0 +1,8 @@ +//go:build go_tarantool_call_17 +// +build go_tarantool_call_17 + +package tarantool + +const ( + CallRequest = Call17Request +) diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index 65404c0d6..2910010cd 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -118,8 +118,8 @@ func Example_customUnpacking() { fmt.Println("Tuples (tuples2):", tuples2) // Call a function "func_name" returning a table of custom tuples. - var tuples3 []Tuple3 - err = conn.CallTyped("func_name", []interface{}{}, &tuples3) + var tuples3 [][]Tuple3 + err = conn.Call17Typed("func_name", []interface{}{}, &tuples3) if err != nil { log.Fatalf("Failed to CallTyped: %s", err.Error()) return @@ -131,6 +131,6 @@ func Example_customUnpacking() { // Code 0 // Tuples (tuples1) [{777 orig [{lol 1} {wut 3}]}] // Tuples (tuples2): [{{} 777 orig [{lol 1} {wut 3}]}] - // Tuples (tuples3): [{{} 221 [{Moscow 34} {Minsk 23} {Kiev 31}]}] + // Tuples (tuples3): [[{{} 221 [{Moscow 34} {Minsk 23} {Kiev 31}]}]] } diff --git a/example_test.go b/example_test.go index b8f8ee110..4092bbd27 100644 --- a/example_test.go +++ b/example_test.go @@ -309,7 +309,7 @@ func ExampleConnection_Update() { // Data [[14 bye bla]] } -func ExampleConnection_Call17() { +func ExampleConnection_Call() { conn := example_connect() defer conn.Close() diff --git a/multi/call_16_test.go b/multi/call_16_test.go new file mode 100644 index 000000000..11a1a766d --- /dev/null +++ b/multi/call_16_test.go @@ -0,0 +1,33 @@ +//go:build !go_tarantool_call_17 +// +build !go_tarantool_call_17 + +package multi + +import ( + "testing" + + "github.com/tarantool/go-tarantool" +) + +func TestCall(t *testing.T) { + var resp *tarantool.Response + var err error + + multiConn, err := Connect([]string{server1, server2}, connOpts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if multiConn == nil { + t.Fatalf("conn is nil after Connect") + } + defer multiConn.Close() + + // Call16 + resp, err = multiConn.Call("simple_incr", []interface{}{1}) + if err != nil { + t.Fatalf("Failed to use Call: %s", err.Error()) + } + if resp.Data[0].([]interface{})[0].(uint64) != 2 { + t.Fatalf("result is not {{1}} : %v", resp.Data) + } +} diff --git a/multi/call_17_test.go b/multi/call_17_test.go new file mode 100644 index 000000000..86a3cfc13 --- /dev/null +++ b/multi/call_17_test.go @@ -0,0 +1,33 @@ +//go:build go_tarantool_call_17 +// +build go_tarantool_call_17 + +package multi + +import ( + "testing" + + "github.com/tarantool/go-tarantool" +) + +func TestCall(t *testing.T) { + var resp *tarantool.Response + var err error + + multiConn, err := Connect([]string{server1, server2}, connOpts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if multiConn == nil { + t.Fatalf("conn is nil after Connect") + } + defer multiConn.Close() + + // Call17 + resp, err = multiConn.Call("simple_incr", []interface{}{1}) + if err != nil { + t.Fatalf("Failed to use Call: %s", err.Error()) + } + if resp.Data[0].(uint64) != 2 { + t.Fatalf("result is not {{1}} : %v", resp.Data) + } +} diff --git a/multi/config.lua b/multi/config.lua index 25b0eb4f9..2b745185a 100644 --- a/multi/config.lua +++ b/multi/config.lua @@ -15,6 +15,12 @@ box.once("init", function() box.schema.user.grant('test', 'read,write,execute', 'universe') end) +local function simple_incr(a) + return a + 1 +end + +rawset(_G, 'simple_incr', simple_incr) + -- Set listen only when every other thing is configured. box.cfg{ listen = os.getenv("TEST_TNT_LISTEN"), diff --git a/multi/multi.go b/multi/multi.go index 89ec33ad6..edd6728b0 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -322,14 +322,23 @@ func (connMulti *ConnectionMulti) Upsert(space interface{}, tuple, ops interface } // Call calls registered Tarantool function. -// It uses request code for Tarantool 1.6, so result is converted to array of -// arrays. +// It uses request code for Tarantool >= 1.7 if go-tarantool +// was build with go_tarantool_call_17 tag. +// Otherwise, uses request code for Tarantool 1.6. func (connMulti *ConnectionMulti) Call(functionName string, args interface{}) (resp *tarantool.Response, err error) { return connMulti.getCurrentConnection().Call(functionName, args) } +// Call16 calls registered Tarantool function. +// It uses request code for Tarantool 1.6, so result is converted to array of +// arrays. +// Deprecated since Tarantool 1.7.2. +func (connMulti *ConnectionMulti) Call16(functionName string, args interface{}) (resp *tarantool.Response, err error) { + return connMulti.getCurrentConnection().Call16(functionName, args) +} + // Call17 calls registered Tarantool function. -// It uses request code for Tarantool 1.7, so result is not converted +// It uses request code for Tarantool >= 1.7, so result is not converted // (though, keep in mind, result is always array). func (connMulti *ConnectionMulti) Call17(functionName string, args interface{}) (resp *tarantool.Response, err error) { return connMulti.getCurrentConnection().Call17(functionName, args) @@ -383,14 +392,23 @@ func (connMulti *ConnectionMulti) UpdateTyped(space, index interface{}, key, ops } // CallTyped calls registered function. -// It uses request code for Tarantool 1.6, so result is converted to array of -// arrays. +// It uses request code for Tarantool >= 1.7 if go-tarantool +// was build with go_tarantool_call_17 tag. +// Otherwise, uses request code for Tarantool 1.6. func (connMulti *ConnectionMulti) CallTyped(functionName string, args interface{}, result interface{}) (err error) { return connMulti.getCurrentConnection().CallTyped(functionName, args, result) } +// Call16Typed calls registered function. +// It uses request code for Tarantool 1.6, so result is converted to array of +// arrays. +// Deprecated since Tarantool 1.7.2. +func (connMulti *ConnectionMulti) Call16Typed(functionName string, args interface{}, result interface{}) (err error) { + return connMulti.getCurrentConnection().Call16Typed(functionName, args, result) +} + // Call17Typed calls registered function. -// It uses request code for Tarantool 1.7, so result is not converted (though, +// It uses request code for Tarantool >= 1.7, so result is not converted (though, // keep in mind, result is always array) func (connMulti *ConnectionMulti) Call17Typed(functionName string, args interface{}, result interface{}) (err error) { return connMulti.getCurrentConnection().Call17Typed(functionName, args, result) @@ -437,14 +455,23 @@ func (connMulti *ConnectionMulti) UpsertAsync(space interface{}, tuple interface } // CallAsync sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool 1.6, so future's result is always array -// of arrays. +// It uses request code for Tarantool >= 1.7 if go-tarantool +// was build with go_tarantool_call_17 tag. +// Otherwise, uses request code for Tarantool 1.6. func (connMulti *ConnectionMulti) CallAsync(functionName string, args interface{}) *tarantool.Future { return connMulti.getCurrentConnection().CallAsync(functionName, args) } +// Call16Async sends a call to registered Tarantool function and returns Future. +// It uses request code for Tarantool 1.6, so future's result is always array +// of arrays. +// Deprecated since Tarantool 1.7.2. +func (connMulti *ConnectionMulti) Call16Async(functionName string, args interface{}) *tarantool.Future { + return connMulti.getCurrentConnection().Call16Async(functionName, args) +} + // Call17Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool 1.7, so future's result will not be converted +// It uses request code for Tarantool >= 1.7, so future's result will not be converted // (though, keep in mind, result is always array). func (connMulti *ConnectionMulti) Call17Async(functionName string, args interface{}) *tarantool.Future { return connMulti.getCurrentConnection().Call17Async(functionName, args) diff --git a/multi/multi_test.go b/multi/multi_test.go index e501ced13..0f84cdb4d 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -202,12 +202,35 @@ func TestRefresh(t *testing.T) { t.Errorf("Expect connection to exist") } - _, err := multiConn.Call(multiConn.opts.NodesGetFunctionName, []interface{}{}) + _, err := multiConn.Call17(multiConn.opts.NodesGetFunctionName, []interface{}{}) if err != nil { t.Error("Expect to get data after reconnect") } } +func TestCall17(t *testing.T) { + var resp *tarantool.Response + var err error + + multiConn, err := Connect([]string{server1, server2}, connOpts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if multiConn == nil { + t.Fatalf("conn is nil after Connect") + } + defer multiConn.Close() + + // Call17 + resp, err = multiConn.Call17("simple_incr", []interface{}{1}) + if err != nil { + t.Fatalf("Failed to use Call: %s", err.Error()) + } + if resp.Data[0].(uint64) != 2 { + t.Fatalf("result is not {{1}} : %v", resp.Data) + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/queue/queue.go b/queue/queue.go index ce0eeb49f..8a3e3e17d 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -189,7 +189,7 @@ func (q *queue) put(params ...interface{}) (*Task, error) { result: params[0], q: q, } - if err := q.conn.CallTyped(q.cmds.put, params, &qd); err != nil { + if err := q.conn.Call17Typed(q.cmds.put, params, &qd); err != nil { return nil, err } return qd.task, nil @@ -240,7 +240,7 @@ func (q *queue) take(params interface{}, result ...interface{}) (*Task, error) { if len(result) > 0 { qd.result = result[0] } - if err := q.conn.CallTyped(q.cmds.take, []interface{}{params}, &qd); err != nil { + if err := q.conn.Call17Typed(q.cmds.take, []interface{}{params}, &qd); err != nil { return nil, err } return qd.task, nil @@ -248,14 +248,14 @@ func (q *queue) take(params interface{}, result ...interface{}) (*Task, error) { // Drop queue. func (q *queue) Drop() error { - _, err := q.conn.Call(q.cmds.drop, []interface{}{}) + _, err := q.conn.Call17(q.cmds.drop, []interface{}{}) return err } // Look at a task without changing its state. func (q *queue) Peek(taskId uint64) (*Task, error) { qd := queueData{q: q} - if err := q.conn.CallTyped(q.cmds.peek, []interface{}{taskId}, &qd); err != nil { + if err := q.conn.Call17Typed(q.cmds.peek, []interface{}{taskId}, &qd); err != nil { return nil, err } return qd.task, nil @@ -278,7 +278,7 @@ func (q *queue) _release(taskId uint64, cfg Opts) (string, error) { } func (q *queue) produce(cmd string, params ...interface{}) (string, error) { qd := queueData{q: q} - if err := q.conn.CallTyped(cmd, params, &qd); err != nil || qd.task == nil { + if err := q.conn.Call17Typed(cmd, params, &qd); err != nil || qd.task == nil { return "", err } return qd.task.status, nil @@ -286,10 +286,10 @@ func (q *queue) produce(cmd string, params ...interface{}) (string, error) { // Reverse the effect of a bury request on one or more tasks. func (q *queue) Kick(count uint64) (uint64, error) { - resp, err := q.conn.Call(q.cmds.kick, []interface{}{count}) + resp, err := q.conn.Call17(q.cmds.kick, []interface{}{count}) var id uint64 if err == nil { - id = resp.Data[0].([]interface{})[0].(uint64) + id = resp.Data[0].(uint64) } return id, err } @@ -303,16 +303,13 @@ func (q *queue) Delete(taskId uint64) error { // Return the number of tasks in a queue broken down by task_state, and the // number of requests broken down by the type of request. func (q *queue) Statistic() (interface{}, error) { - resp, err := q.conn.Call(q.cmds.statistics, []interface{}{q.name}) + resp, err := q.conn.Call17(q.cmds.statistics, []interface{}{q.name}) if err != nil { return nil, err } if len(resp.Data) != 0 { - data, ok := resp.Data[0].([]interface{}) - if ok && len(data) != 0 { - return data[0], nil - } + return resp.Data[0], nil } return nil, nil diff --git a/request.go b/request.go index e672f0b17..df758ef3f 100644 --- a/request.go +++ b/request.go @@ -88,15 +88,26 @@ func (conn *Connection) Upsert(space interface{}, tuple, ops interface{}) (resp } // Call calls registered Tarantool function. -// It uses request code for Tarantool 1.6, so result is converted to array of arrays +// It uses request code for Tarantool >= 1.7 if go-tarantool +// was build with go_tarantool_call_17 tag. +// Otherwise, uses request code for Tarantool 1.6. // // It is equal to conn.CallAsync(functionName, args).Get(). func (conn *Connection) Call(functionName string, args interface{}) (resp *Response, err error) { return conn.CallAsync(functionName, args).Get() } +// Call16 calls registered Tarantool function. +// It uses request code for Tarantool 1.6, so result is converted to array of arrays +// Deprecated since Tarantool 1.7.2. +// +// It is equal to conn.Call16Async(functionName, args).Get(). +func (conn *Connection) Call16(functionName string, args interface{}) (resp *Response, err error) { + return conn.Call16Async(functionName, args).Get() +} + // Call17 calls registered Tarantool function. -// It uses request code for Tarantool 1.7, so result is not converted +// It uses request code for Tarantool >= 1.7, so result is not converted // (though, keep in mind, result is always array) // // It is equal to conn.Call17Async(functionName, args).Get(). @@ -188,15 +199,26 @@ func (conn *Connection) UpdateTyped(space, index interface{}, key, ops interface } // CallTyped calls registered function. -// It uses request code for Tarantool 1.6, so result is converted to array of arrays +// It uses request code for Tarantool >= 1.7 if go-tarantool +// was build with go_tarantool_call_17 tag. +// Otherwise, uses request code for Tarantool 1.6. // -// It is equal to conn.CallAsync(functionName, args).GetTyped(&result). +// It is equal to conn.Call16Async(functionName, args).GetTyped(&result). func (conn *Connection) CallTyped(functionName string, args interface{}, result interface{}) (err error) { return conn.CallAsync(functionName, args).GetTyped(result) } +// Call16Typed calls registered function. +// It uses request code for Tarantool 1.6, so result is converted to array of arrays +// Deprecated since Tarantool 1.7.2. +// +// It is equal to conn.Call16Async(functionName, args).GetTyped(&result). +func (conn *Connection) Call16Typed(functionName string, args interface{}, result interface{}) (err error) { + return conn.Call16Async(functionName, args).GetTyped(result) +} + // Call17Typed calls registered function. -// It uses request code for Tarantool 1.7, so result is not converted +// It uses request code for Tarantool >= 1.7, so result is not converted // (though, keep in mind, result is always array) // // It is equal to conn.Call17Async(functionName, args).GetTyped(&result). @@ -317,7 +339,9 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops in } // CallAsync sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool 1.6, so future's result is always array of arrays +// It uses request code for Tarantool >= 1.7 if go-tarantool +// was build with go_tarantool_call_17 tag. +// Otherwise, uses request code for Tarantool 1.6. func (conn *Connection) CallAsync(functionName string, args interface{}) *Future { future := conn.newFuture(CallRequest) return conn.sendFuture(future, func(enc *msgpack.Encoder) error { @@ -329,8 +353,22 @@ func (conn *Connection) CallAsync(functionName string, args interface{}) *Future }) } +// Call16Async sends a call to registered Tarantool function and returns Future. +// It uses request code for Tarantool 1.6, so future's result is always array of arrays. +// Deprecated since Tarantool 1.7.2. +func (conn *Connection) Call16Async(functionName string, args interface{}) *Future { + future := conn.newFuture(Call16Request) + return conn.sendFuture(future, func(enc *msgpack.Encoder) error { + enc.EncodeMapLen(2) + enc.EncodeUint64(KeyFunctionName) + enc.EncodeString(functionName) + enc.EncodeUint64(KeyTuple) + return enc.Encode(args) + }) +} + // Call17Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool 1.7, so future's result will not be converted +// It uses request code for Tarantool >= 1.7, so future's result will not be converted // (though, keep in mind, result is always array) func (conn *Connection) Call17Async(functionName string, args interface{}) *Future { future := conn.newFuture(Call17Request) diff --git a/response.go b/response.go index b7a15706f..80b38849b 100644 --- a/response.go +++ b/response.go @@ -243,7 +243,7 @@ func (resp *Response) String() (str string) { return fmt.Sprintf("<%d ERR 0x%x %s>", resp.RequestId, resp.Code, resp.Error) } -// Tuples converts result of Eval and Call17 to same format +// Tuples converts result of Eval and Call to same format // as other actions returns (i.e. array of arrays). func (resp *Response) Tuples() (res [][]interface{}) { res = make([][]interface{}, len(resp.Data)) diff --git a/tarantool_test.go b/tarantool_test.go index 438f3a18b..9e4c5f7c6 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -722,22 +722,22 @@ func TestClient(t *testing.T) { t.Errorf("Result len of SelectTyped != 1") } - // Call - resp, err = conn.Call("box.info", []interface{}{"box.schema.SPACE_ID"}) + // Call16 + resp, err = conn.Call16("box.info", []interface{}{"box.schema.SPACE_ID"}) if err != nil { - t.Fatalf("Failed to Call: %s", err.Error()) + t.Fatalf("Failed to Call16: %s", err.Error()) } if resp == nil { - t.Fatalf("Response is nil after Call") + t.Fatalf("Response is nil after Call16") } if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } - // Call vs Call17 - resp, err = conn.Call("simple_incr", []interface{}{1}) + // Call16 vs Call17 + resp, err = conn.Call16("simple_incr", []interface{}{1}) if err != nil { - t.Errorf("Failed to use Call") + t.Errorf("Failed to use Call16") } if resp.Data[0].([]interface{})[0].(uint64) != 2 { t.Errorf("result is not {{1}} : %v", resp.Data) @@ -745,7 +745,7 @@ func TestClient(t *testing.T) { resp, err = conn.Call17("simple_incr", []interface{}{1}) if err != nil { - t.Errorf("Failed to use Call17") + t.Errorf("Failed to use Call") } if resp.Data[0].(uint64) != 2 { t.Errorf("result is not {{1}} : %v", resp.Data) @@ -803,11 +803,11 @@ func TestClientSessionPush(t *testing.T) { // Future.Get ignores push messages. resp, err := fut1.Get() if err != nil { - t.Errorf("Failed to Call: %s", err.Error()) + t.Errorf("Failed to Call17: %s", err.Error()) } else if resp == nil { t.Errorf("Response is nil after CallAsync") } else if len(resp.Data) < 1 { - t.Errorf("Response.Data is empty after CallAsync") + t.Errorf("Response.Data is empty after Call17Async") } else if resp.Data[0].(uint64) != pushMax { t.Errorf("result is not {{1}} : %v", resp.Data) } diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index 63f4d09f5..0061870de 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -195,7 +195,7 @@ func SetInstanceRO(server string, connOpts tarantool.Opts, isReplica bool) error defer conn.Close() - _, err = conn.Call("box.cfg", []interface{}{map[string]bool{"read_only": isReplica}}) + _, err = conn.Call17("box.cfg", []interface{}{map[string]bool{"read_only": isReplica}}) if err != nil { return err } From d6a79d906173083158229d74015b4237d99a63ae Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 13 Apr 2022 09:52:48 +0300 Subject: [PATCH 285/605] api: add a public API with request types This patch provides request types. It allows to create requests step by step. The main idea here is too provide more extensible approach to create requests. It renames IPROTO_* constants that identify requests from `Request` to `RequestCode` to provide names for request types. Part of #126 --- CHANGELOG.md | 3 + call_16_test.go | 26 ++ call_17_test.go | 26 ++ client_tools.go | 73 ++++++ connection.go | 39 ++- const.go | 26 +- const_call_16.go | 2 +- const_call_17.go | 2 +- example_test.go | 110 ++++++++ export_test.go | 66 ++++- request.go | 635 +++++++++++++++++++++++++++++++++++++++++----- request_test.go | 520 +++++++++++++++++++++++++++++++++++++ schema.go | 12 +- tarantool_test.go | 374 ++++++++++++++++++++++++++- 14 files changed, 1821 insertions(+), 93 deletions(-) create mode 100644 request_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb5cbc8c..6664a1cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,14 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - SSL support (#155) - IPROTO_PUSH messages support (#67) +- Public API with request object types (#126) ### Changed - Add `Call16` method, support build tag `go_tarantool_call_17` to choose behavior for `Call` method (#125) +- `IPROTO_*` constants that identify requests renamed from `Request` to + `RequestCode` (#126) ### Fixed diff --git a/call_16_test.go b/call_16_test.go index 1aff4e62e..c20049fca 100644 --- a/call_16_test.go +++ b/call_16_test.go @@ -25,3 +25,29 @@ func TestConnection_Call(t *testing.T) { t.Errorf("result is not {{1}} : %v", resp.Data) } } + +func TestCallRequest(t *testing.T) { + var resp *Response + var err error + + conn := connect(t, server, opts) + defer conn.Close() + + req := NewCallRequest("simple_incr").Args([]interface{}{1}) + resp, err = conn.Do(req) + if err != nil { + t.Errorf("Failed to use Call") + } + if resp.Data[0].([]interface{})[0].(uint64) != 2 { + t.Errorf("result is not {{1}} : %v", resp.Data) + } +} + +func TestCallRequestCode(t *testing.T) { + req := NewCallRequest("simple_incrt") + code := req.Code() + expected := Call16RequestCode + if code != int32(expected) { + t.Errorf("CallRequest actual code %v != %v", code, expected) + } +} diff --git a/call_17_test.go b/call_17_test.go index 83f441401..e4b030bf2 100644 --- a/call_17_test.go +++ b/call_17_test.go @@ -25,3 +25,29 @@ func TestConnection_Call(t *testing.T) { t.Errorf("result is not {{1}} : %v", resp.Data) } } + +func TestCallRequest(t *testing.T) { + var resp *Response + var err error + + conn := connect(t, server, opts) + defer conn.Close() + + req := NewCallRequest("simple_incr").Args([]interface{}{1}) + resp, err = conn.Do(req) + if err != nil { + t.Errorf("Failed to use Call") + } + if resp.Data[0].(uint64) != 2 { + t.Errorf("result is not {{1}} : %v", resp.Data) + } +} + +func TestCallRequestCode(t *testing.T) { + req := NewCallRequest("simple_incrt") + code := req.Code() + expected := Call17RequestCode + if code != int32(expected) { + t.Errorf("CallRequest actual code %v != %v", code, expected) + } +} diff --git a/client_tools.go b/client_tools.go index de10a366b..27976a9a7 100644 --- a/client_tools.go +++ b/client_tools.go @@ -67,6 +67,79 @@ func (o Op) EncodeMsgpack(enc *msgpack.Encoder) error { return enc.Encode(o.Arg) } +const ( + appendOperator = "+" + subtractionOperator = "-" + bitwiseAndOperator = "&" + bitwiseOrOperator = "|" + bitwiseXorOperator = "^" + spliceOperator = ":" + insertOperator = "!" + deleteOperator = "#" + assignOperator = "=" +) + +// Operations is a collection of update operations. +type Operations struct { + ops []Op +} + +// NewOperations returns a new empty collection of update operations. +func NewOperations() *Operations { + ops := new(Operations) + return ops +} + +func (ops *Operations) append(op string, field int, arg interface{}) *Operations { + ops.ops = append(ops.ops, Op{op, field, arg}) + return ops +} + +// Add adds an additional operation to the collection of update operations. +func (ops *Operations) Add(field int, arg interface{}) *Operations { + return ops.append(appendOperator, field, arg) +} + +// Subtract adds a subtraction operation to the collection of update operations. +func (ops *Operations) Subtract(field int, arg interface{}) *Operations { + return ops.append(subtractionOperator, field, arg) +} + +// BitwiseAnd adds a bitwise AND operation to the collection of update operations. +func (ops *Operations) BitwiseAnd(field int, arg interface{}) *Operations { + return ops.append(bitwiseAndOperator, field, arg) +} + +// BitwiseOr adds a bitwise OR operation to the collection of update operations. +func (ops *Operations) BitwiseOr(field int, arg interface{}) *Operations { + return ops.append(bitwiseOrOperator, field, arg) +} + +// BitwiseXor adds a bitwise XOR operation to the collection of update operations. +func (ops *Operations) BitwiseXor(field int, arg interface{}) *Operations { + return ops.append(bitwiseXorOperator, field, arg) +} + +// Splice adds a splice operation to the collection of update operations. +func (ops *Operations) Splice(field int, arg interface{}) *Operations { + return ops.append(spliceOperator, field, arg) +} + +// Insert adds an insert operation to the collection of update operations. +func (ops *Operations) Insert(field int, arg interface{}) *Operations { + return ops.append(insertOperator, field, arg) +} + +// Delete adds a delete operation to the collection of update operations. +func (ops *Operations) Delete(field int, arg interface{}) *Operations { + return ops.append(deleteOperator, field, arg) +} + +// Assign adds an assign operation to the collection of update operations. +func (ops *Operations) Assign(field int, arg interface{}) *Operations { + return ops.append(assignOperator, field, arg) +} + type OpSplice struct { Op string Field int diff --git a/connection.go b/connection.go index c5a55b36f..9958a0352 100644 --- a/connection.go +++ b/connection.go @@ -471,7 +471,7 @@ func (conn *Connection) dial() (err error) { func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { request := &Future{ requestId: 0, - requestCode: AuthRequest, + requestCode: AuthRequestCode, } var packet smallWBuf err = request.pack(&packet, msgpack.NewEncoder(&packet), func(enc *msgpack.Encoder) error { @@ -978,6 +978,43 @@ func (conn *Connection) nextRequestId() (requestId uint32) { return atomic.AddUint32(&conn.requestId, 1) } +// Do verifies, sends the request and returns a response. +// +// An error is returned if the request was formed incorrectly, or failed to +// communicate by the connection, or unable to decode the response. +func (conn *Connection) Do(req Request) (*Response, error) { + fut, err := conn.DoAsync(req) + if err != nil { + return nil, err + } + return fut.Get() +} + +// DoTyped verifies, sends the request and fills the typed result. +// +// An error is returned if the request was formed incorrectly, or failed to +// communicate by the connection, or unable to decode the response. +func (conn *Connection) DoTyped(req Request, result interface{}) error { + fut, err := conn.DoAsync(req) + if err != nil { + return err + } + return fut.GetTyped(result) +} + +// DoAsync verifies, sends the request and returns a future. +// +// An error is returned if the request was formed incorrectly, or failed to +// create the future. +func (conn *Connection) DoAsync(req Request) (*Future, error) { + bodyFunc, err := req.BodyFunc(conn.Schema) + if err != nil { + return nil, err + } + future := conn.newFuture(req.Code()) + return conn.sendFuture(future, bodyFunc), nil +} + // ConfiguredTimeout returns a timeout from connection config. func (conn *Connection) ConfiguredTimeout() time.Duration { return conn.opts.Timeout diff --git a/const.go b/const.go index 53e7017dd..2ad7f3b76 100644 --- a/const.go +++ b/const.go @@ -1,19 +1,19 @@ package tarantool const ( - SelectRequest = 1 - InsertRequest = 2 - ReplaceRequest = 3 - UpdateRequest = 4 - DeleteRequest = 5 - Call16Request = 6 /* call in 1.6 format */ - AuthRequest = 7 - EvalRequest = 8 - UpsertRequest = 9 - Call17Request = 10 /* call in >= 1.7 format */ - ExecuteRequest = 11 - PingRequest = 64 - SubscribeRequest = 66 + SelectRequestCode = 1 + InsertRequestCode = 2 + ReplaceRequestCode = 3 + UpdateRequestCode = 4 + DeleteRequestCode = 5 + Call16RequestCode = 6 /* call in 1.6 format */ + AuthRequestCode = 7 + EvalRequestCode = 8 + UpsertRequestCode = 9 + Call17RequestCode = 10 /* call in >= 1.7 format */ + ExecuteRequestCode = 11 + PingRequestCode = 64 + SubscribeRequestCode = 66 KeyCode = 0x00 KeySync = 0x01 diff --git a/const_call_16.go b/const_call_16.go index 90ce7e6ce..7d80cc631 100644 --- a/const_call_16.go +++ b/const_call_16.go @@ -4,5 +4,5 @@ package tarantool const ( - CallRequest = Call16Request + CallRequestCode = Call16RequestCode ) diff --git a/const_call_17.go b/const_call_17.go index 3b012c8ac..d50d8f1c1 100644 --- a/const_call_17.go +++ b/const_call_17.go @@ -4,5 +4,5 @@ package tarantool const ( - CallRequest = Call17Request + CallRequestCode = Call17RequestCode ) diff --git a/example_test.go b/example_test.go index 4092bbd27..5ad471148 100644 --- a/example_test.go +++ b/example_test.go @@ -126,6 +126,116 @@ func ExampleConnection_SelectAsync() { // Future 2 Data [[18 val 18 bla]] } +func ExampleSelectRequest() { + conn := example_connect() + defer conn.Close() + + req := tarantool.NewSelectRequest(517). + Limit(100). + Key(tarantool.IntKey{1111}) + resp, err := conn.Do(req) + if err != nil { + fmt.Printf("error in do select request is %v", err) + return + } + fmt.Printf("response is %#v\n", resp.Data) + + req = tarantool.NewSelectRequest("test"). + Index("primary"). + Limit(100). + Key(tarantool.IntKey{1111}) + fut, err := conn.DoAsync(req) + if err != nil { + fmt.Printf("error in do async select request is %v", err) + } + resp, err = fut.Get() + if err != nil { + fmt.Printf("error in do async select request is %v", err) + return + } + fmt.Printf("response is %#v\n", resp.Data) + // Output: + // response is []interface {}{[]interface {}{0x457, "hello", "world"}} + // response is []interface {}{[]interface {}{0x457, "hello", "world"}} +} + +func ExampleUpdateRequest() { + conn := example_connect() + defer conn.Close() + + req := tarantool.NewUpdateRequest(517). + Key(tarantool.IntKey{1111}). + Operations(tarantool.NewOperations().Assign(1, "bye")) + resp, err := conn.Do(req) + if err != nil { + fmt.Printf("error in do update request is %v", err) + return + } + fmt.Printf("response is %#v\n", resp.Data) + + req = tarantool.NewUpdateRequest("test"). + Index("primary"). + Key(tarantool.IntKey{1111}). + Operations(tarantool.NewOperations().Assign(1, "hello")) + fut, err := conn.DoAsync(req) + if err != nil { + fmt.Printf("error in do async update request is %v", err) + } + resp, err = fut.Get() + if err != nil { + fmt.Printf("error in do async update request is %v", err) + return + } + fmt.Printf("response is %#v\n", resp.Data) + // Output: + // response is []interface {}{[]interface {}{0x457, "bye", "world"}} + // response is []interface {}{[]interface {}{0x457, "hello", "world"}} +} + +func ExampleUpsertRequest() { + conn := example_connect() + defer conn.Close() + + var req tarantool.Request + req = tarantool.NewUpsertRequest(517). + Tuple([]interface{}{uint(1113), "first", "first"}). + Operations(tarantool.NewOperations().Assign(1, "updated")) + resp, err := conn.Do(req) + if err != nil { + fmt.Printf("error in do select upsert is %v", err) + return + } + fmt.Printf("response is %#v\n", resp.Data) + + req = tarantool.NewUpsertRequest("test"). + Tuple([]interface{}{uint(1113), "second", "second"}). + Operations(tarantool.NewOperations().Assign(2, "updated")) + fut, err := conn.DoAsync(req) + if err != nil { + fmt.Printf("error in do async upsert request is %v", err) + } + resp, err = fut.Get() + if err != nil { + fmt.Printf("error in do async upsert request is %v", err) + return + } + fmt.Printf("response is %#v\n", resp.Data) + + req = tarantool.NewSelectRequest(517). + Limit(100). + Key(tarantool.IntKey{1113}) + resp, err = conn.Do(req) + if err != nil { + fmt.Printf("error in do select request is %v", err) + return + } + fmt.Printf("response is %#v\n", resp.Data) + // Output: + // response is []interface {}{} + // response is []interface {}{} + // response is []interface {}{[]interface {}{0x459, "first", "updated"}} +} + func ExampleFuture_GetIterator() { conn := example_connect() defer conn.Close() diff --git a/export_test.go b/export_test.go index 8cb2b713a..8bdfb9812 100644 --- a/export_test.go +++ b/export_test.go @@ -3,11 +3,9 @@ package tarantool import ( "net" "time" -) -func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (spaceNo, indexNo uint32, err error) { - return schema.resolveSpaceIndex(s, i) -} + "gopkg.in/vmihailenco/msgpack.v2" +) func SslDialTimeout(network, address string, timeout time.Duration, opts SslOpts) (connection net.Conn, err error) { @@ -17,3 +15,63 @@ func SslDialTimeout(network, address string, timeout time.Duration, func SslCreateContext(opts SslOpts) (ctx interface{}, err error) { return sslCreateContext(opts) } + +// RefImplPingBody is reference implementation for filling of a ping +// request's body. +func RefImplPingBody(enc *msgpack.Encoder) error { + return fillPing(enc) +} + +// RefImplSelectBody is reference implementation for filling of a select +// request's body. +func RefImplSelectBody(enc *msgpack.Encoder, space, index, offset, limit, iterator uint32, key interface{}) error { + return fillSelect(enc, space, index, offset, limit, iterator, key) +} + +// RefImplInsertBody is reference implementation for filling of an insert +// request's body. +func RefImplInsertBody(enc *msgpack.Encoder, space uint32, tuple interface{}) error { + return fillInsert(enc, space, tuple) +} + +// RefImplReplaceBody is reference implementation for filling of a replace +// request's body. +func RefImplReplaceBody(enc *msgpack.Encoder, space uint32, tuple interface{}) error { + return fillInsert(enc, space, tuple) +} + +// RefImplDeleteBody is reference implementation for filling of a delete +// request's body. +func RefImplDeleteBody(enc *msgpack.Encoder, space, index uint32, key interface{}) error { + return fillDelete(enc, space, index, key) +} + +// RefImplUpdateBody is reference implementation for filling of an update +// request's body. +func RefImplUpdateBody(enc *msgpack.Encoder, space, index uint32, key, ops interface{}) error { + return fillUpdate(enc, space, index, key, ops) +} + +// RefImplUpsertBody is reference implementation for filling of an upsert +// request's body. +func RefImplUpsertBody(enc *msgpack.Encoder, space uint32, tuple, ops interface{}) error { + return fillUpsert(enc, space, tuple, ops) +} + +// RefImplCallBody is reference implementation for filling of a call or call17 +// request's body. +func RefImplCallBody(enc *msgpack.Encoder, function string, args interface{}) error { + return fillCall(enc, function, args) +} + +// RefImplEvalBody is reference implementation for filling of an eval +// request's body. +func RefImplEvalBody(enc *msgpack.Encoder, expr string, args interface{}) error { + return fillEval(enc, expr, args) +} + +// RefImplExecuteBody is reference implementation for filling of an execute +// request's body. +func RefImplExecuteBody(enc *msgpack.Encoder, expr string, args interface{}) error { + return fillExecute(enc, expr, args) +} diff --git a/request.go b/request.go index df758ef3f..c2777778b 100644 --- a/request.go +++ b/request.go @@ -9,12 +9,6 @@ import ( "gopkg.in/vmihailenco/msgpack.v2" ) -// Ping sends empty request to Tarantool to check connection. -func (conn *Connection) Ping() (resp *Response, err error) { - future := conn.newFuture(PingRequest) - return conn.sendFuture(future, func(enc *msgpack.Encoder) error { enc.EncodeMapLen(0); return nil }).Get() -} - func fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { enc.EncodeUint64(KeySpaceNo) enc.EncodeUint64(uint64(spaceNo)) @@ -34,12 +28,79 @@ func fillIterator(enc *msgpack.Encoder, offset, limit, iterator uint32) { } func fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { + enc.EncodeMapLen(2) enc.EncodeUint64(KeySpaceNo) enc.EncodeUint64(uint64(spaceNo)) enc.EncodeUint64(KeyTuple) return enc.Encode(tuple) } +func fillSelect(enc *msgpack.Encoder, spaceNo, indexNo, offset, limit, iterator uint32, key interface{}) error { + enc.EncodeMapLen(6) + fillIterator(enc, offset, limit, iterator) + return fillSearch(enc, spaceNo, indexNo, key) +} + +func fillUpdate(enc *msgpack.Encoder, spaceNo, indexNo uint32, key, ops interface{}) error { + enc.EncodeMapLen(4) + if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { + return err + } + enc.EncodeUint64(KeyTuple) + return enc.Encode(ops) +} + +func fillUpsert(enc *msgpack.Encoder, spaceNo uint32, tuple, ops interface{}) error { + enc.EncodeMapLen(3) + enc.EncodeUint64(KeySpaceNo) + enc.EncodeUint64(uint64(spaceNo)) + enc.EncodeUint64(KeyTuple) + if err := enc.Encode(tuple); err != nil { + return err + } + enc.EncodeUint64(KeyDefTuple) + return enc.Encode(ops) +} + +func fillDelete(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { + enc.EncodeMapLen(3) + return fillSearch(enc, spaceNo, indexNo, key) +} + +func fillCall(enc *msgpack.Encoder, functionName string, args interface{}) error { + enc.EncodeMapLen(2) + enc.EncodeUint64(KeyFunctionName) + enc.EncodeString(functionName) + enc.EncodeUint64(KeyTuple) + return enc.Encode(args) +} + +func fillEval(enc *msgpack.Encoder, expr string, args interface{}) error { + enc.EncodeMapLen(2) + enc.EncodeUint64(KeyExpression) + enc.EncodeString(expr) + enc.EncodeUint64(KeyTuple) + return enc.Encode(args) +} + +func fillExecute(enc *msgpack.Encoder, expr string, args interface{}) error { + enc.EncodeMapLen(2) + enc.EncodeUint64(KeySQLText) + enc.EncodeString(expr) + enc.EncodeUint64(KeySQLBind) + return encodeSQLBind(enc, args) +} + +func fillPing(enc *msgpack.Encoder) error { + return enc.EncodeMapLen(0) +} + +// Ping sends empty request to Tarantool to check connection. +func (conn *Connection) Ping() (resp *Response, err error) { + future := conn.newFuture(PingRequestCode) + return conn.sendFuture(future, func(enc *msgpack.Encoder) error { return fillPing(enc) }).Get() +} + // Select performs select to box space. // // It is equal to conn.SelectAsync(...).Get(). @@ -245,28 +306,25 @@ func (conn *Connection) ExecuteTyped(expr string, args interface{}, result inter // SelectAsync sends select request to Tarantool and returns Future. func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Future { - future := conn.newFuture(SelectRequest) - spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + future := conn.newFuture(SelectRequestCode) + spaceNo, indexNo, err := conn.Schema.ResolveSpaceIndex(space, index) if err != nil { return conn.failFuture(future, err) } return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - enc.EncodeMapLen(6) - fillIterator(enc, offset, limit, iterator) - return fillSearch(enc, spaceNo, indexNo, key) + return fillSelect(enc, spaceNo, indexNo, offset, limit, iterator, key) }) } // InsertAsync sends insert action to Tarantool and returns Future. // Tarantool will reject Insert when tuple with same primary key exists. func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Future { - future := conn.newFuture(InsertRequest) - spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) + future := conn.newFuture(InsertRequestCode) + spaceNo, _, err := conn.Schema.ResolveSpaceIndex(space, nil) if err != nil { return conn.failFuture(future, err) } return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - enc.EncodeMapLen(2) return fillInsert(enc, spaceNo, tuple) }) } @@ -274,13 +332,12 @@ func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Futur // ReplaceAsync sends "insert or replace" action to Tarantool and returns Future. // If tuple with same primary key exists, it will be replaced. func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Future { - future := conn.newFuture(ReplaceRequest) - spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) + future := conn.newFuture(ReplaceRequestCode) + spaceNo, _, err := conn.Schema.ResolveSpaceIndex(space, nil) if err != nil { return conn.failFuture(future, err) } return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - enc.EncodeMapLen(2) return fillInsert(enc, spaceNo, tuple) }) } @@ -288,53 +345,39 @@ func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Futu // DeleteAsync sends deletion action to Tarantool and returns Future. // Future's result will contain array with deleted tuple. func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) *Future { - future := conn.newFuture(DeleteRequest) - spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + future := conn.newFuture(DeleteRequestCode) + spaceNo, indexNo, err := conn.Schema.ResolveSpaceIndex(space, index) if err != nil { return conn.failFuture(future, err) } return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - enc.EncodeMapLen(3) - return fillSearch(enc, spaceNo, indexNo, key) + return fillDelete(enc, spaceNo, indexNo, key) }) } // Update sends deletion of a tuple by key and returns Future. // Future's result will contain array with updated tuple. func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface{}) *Future { - future := conn.newFuture(UpdateRequest) - spaceNo, indexNo, err := conn.Schema.resolveSpaceIndex(space, index) + future := conn.newFuture(UpdateRequestCode) + spaceNo, indexNo, err := conn.Schema.ResolveSpaceIndex(space, index) if err != nil { return conn.failFuture(future, err) } return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - enc.EncodeMapLen(4) - if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { - return err - } - enc.EncodeUint64(KeyTuple) - return enc.Encode(ops) + return fillUpdate(enc, spaceNo, indexNo, key, ops) }) } // UpsertAsync sends "update or insert" action to Tarantool and returns Future. // Future's sesult will not contain any tuple. func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *Future { - future := conn.newFuture(UpsertRequest) - spaceNo, _, err := conn.Schema.resolveSpaceIndex(space, nil) + future := conn.newFuture(UpsertRequestCode) + spaceNo, _, err := conn.Schema.ResolveSpaceIndex(space, nil) if err != nil { return conn.failFuture(future, err) } return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - enc.EncodeMapLen(3) - enc.EncodeUint64(KeySpaceNo) - enc.EncodeUint64(uint64(spaceNo)) - enc.EncodeUint64(KeyTuple) - if err := enc.Encode(tuple); err != nil { - return err - } - enc.EncodeUint64(KeyDefTuple) - return enc.Encode(ops) + return fillUpsert(enc, spaceNo, tuple, ops) }) } @@ -343,13 +386,9 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops in // was build with go_tarantool_call_17 tag. // Otherwise, uses request code for Tarantool 1.6. func (conn *Connection) CallAsync(functionName string, args interface{}) *Future { - future := conn.newFuture(CallRequest) + future := conn.newFuture(CallRequestCode) return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - enc.EncodeMapLen(2) - enc.EncodeUint64(KeyFunctionName) - enc.EncodeString(functionName) - enc.EncodeUint64(KeyTuple) - return enc.Encode(args) + return fillCall(enc, functionName, args) }) } @@ -357,13 +396,9 @@ func (conn *Connection) CallAsync(functionName string, args interface{}) *Future // It uses request code for Tarantool 1.6, so future's result is always array of arrays. // Deprecated since Tarantool 1.7.2. func (conn *Connection) Call16Async(functionName string, args interface{}) *Future { - future := conn.newFuture(Call16Request) + future := conn.newFuture(Call16RequestCode) return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - enc.EncodeMapLen(2) - enc.EncodeUint64(KeyFunctionName) - enc.EncodeString(functionName) - enc.EncodeUint64(KeyTuple) - return enc.Encode(args) + return fillCall(enc, functionName, args) }) } @@ -371,38 +406,26 @@ func (conn *Connection) Call16Async(functionName string, args interface{}) *Futu // It uses request code for Tarantool >= 1.7, so future's result will not be converted // (though, keep in mind, result is always array) func (conn *Connection) Call17Async(functionName string, args interface{}) *Future { - future := conn.newFuture(Call17Request) + future := conn.newFuture(Call17RequestCode) return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - enc.EncodeMapLen(2) - enc.EncodeUint64(KeyFunctionName) - enc.EncodeString(functionName) - enc.EncodeUint64(KeyTuple) - return enc.Encode(args) + return fillCall(enc, functionName, args) }) } // EvalAsync sends a Lua expression for evaluation and returns Future. func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { - future := conn.newFuture(EvalRequest) + future := conn.newFuture(EvalRequestCode) return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - enc.EncodeMapLen(2) - enc.EncodeUint64(KeyExpression) - enc.EncodeString(expr) - enc.EncodeUint64(KeyTuple) - return enc.Encode(args) + return fillEval(enc, expr, args) }) } // ExecuteAsync sends a sql expression for execution and returns Future. // Since 1.6.0 func (conn *Connection) ExecuteAsync(expr string, args interface{}) *Future { - future := conn.newFuture(ExecuteRequest) + future := conn.newFuture(ExecuteRequestCode) return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - enc.EncodeMapLen(2) - enc.EncodeUint64(KeySQLText) - enc.EncodeString(expr) - enc.EncodeUint64(KeySQLBind) - return encodeSQLBind(enc, args) + return fillExecute(enc, expr, args) }) } @@ -546,3 +569,475 @@ func encodeSQLBind(enc *msgpack.Encoder, from interface{}) error { } return nil } + +// Request is an interface that provides the necessary data to create a request +// that will be sent to a tarantool instance. +type Request interface { + // Code returns a IPROTO code for the request. + Code() int32 + // BodyFunc returns a functions that can fill an encoder with + // the request body or it returns an error if unable to create the function. + BodyFunc(resolver SchemaResolver) (func(enc *msgpack.Encoder) error, error) +} + +type baseRequest struct { + requestCode int32 +} + +// Code returns a IPROTO code for the request. +func (req *baseRequest) Code() int32 { + return req.requestCode +} + +type spaceRequest struct { + baseRequest + space interface{} +} + +func (req *spaceRequest) setSpace(space interface{}) { + req.space = space +} + +type spaceIndexRequest struct { + spaceRequest + index interface{} +} + +func (req *spaceIndexRequest) setIndex(index interface{}) { + req.index = index +} + +// PingRequest helps you to create an execute request object for execution +// by a Connection. +type PingRequest struct { + baseRequest +} + +// NewPingRequest returns a new PingRequest. +func NewPingRequest() *PingRequest { + req := new(PingRequest) + req.requestCode = PingRequestCode + return req +} + +// BodyFunc returns a function that can create an encoded body of +// the ping request. +func (req *PingRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { + return func(enc *msgpack.Encoder) error { + return fillPing(enc) + }, nil +} + +// SelectRequest allows you to create a select request object for execution +// by a Connection. +type SelectRequest struct { + spaceIndexRequest + isIteratorSet bool + offset, limit, iterator uint32 + key interface{} +} + +// NewSelectRequest returns a new empty SelectRequest. +func NewSelectRequest(space interface{}) *SelectRequest { + req := new(SelectRequest) + req.requestCode = SelectRequestCode + req.setSpace(space) + req.isIteratorSet = false + req.iterator = IterAll + req.key = []interface{}{} + req.limit = 0xFFFFFFFF + return req +} + +// Index sets the index for the select request. +// Note: default value is 0. +func (req *SelectRequest) Index(index interface{}) *SelectRequest { + req.setIndex(index) + return req +} + +// Offset sets the offset for the select request. +// Note: default value is 0. +func (req *SelectRequest) Offset(offset uint32) *SelectRequest { + req.offset = offset + return req +} + +// Limit sets the limit for the select request. +// Note: default value is 0xFFFFFFFF. +func (req *SelectRequest) Limit(limit uint32) *SelectRequest { + req.limit = limit + return req +} + +// Iterator set the iterator for the select request. +// Note: default value is IterAll if key is not set or IterEq otherwise. +func (req *SelectRequest) Iterator(iterator uint32) *SelectRequest { + req.iterator = iterator + req.isIteratorSet = true + return req +} + +// Key set the key for the select request. +// Note: default value is empty. +func (req *SelectRequest) Key(key interface{}) *SelectRequest { + req.key = key + if !req.isIteratorSet { + req.iterator = IterEq + } + return req +} + +// BodyFunc returns a function that can create an encoded body of +// the select request. +// It returns an error if the request space or the request index cannot +// be resolved. +func (req *SelectRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { + spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) + if err != nil { + return nil, err + } + + return func(enc *msgpack.Encoder) error { + return fillSelect(enc, spaceNo, indexNo, req.offset, req.limit, req.iterator, req.key) + }, nil +} + +// InsertRequest helps you to create an insert request object for execution +// by a Connection. +type InsertRequest struct { + spaceRequest + tuple interface{} +} + +// NewInsertRequest returns a new empty InsertRequest. +func NewInsertRequest(space interface{}) *InsertRequest { + req := new(InsertRequest) + req.requestCode = InsertRequestCode + req.setSpace(space) + req.tuple = []interface{}{} + return req +} + +// Tuple sets the tuple for insertion the insert request. +// Note: default value is nil. +func (req *InsertRequest) Tuple(tuple interface{}) *InsertRequest { + req.tuple = tuple + return req +} + +// BodyFunc returns a function that can create an encoded body of +// the insert request. +// It returns an error if the request space cannot be resolved. +func (req *InsertRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { + spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) + if err != nil { + return nil, err + } + + return func(enc *msgpack.Encoder) error { + return fillInsert(enc, spaceNo, req.tuple) + }, nil +} + +// ReplaceRequest helps you to create a replace request object for execution +// by a Connection. +type ReplaceRequest struct { + spaceRequest + tuple interface{} +} + +// NewReplaceRequest returns a new empty ReplaceRequest. +func NewReplaceRequest(space interface{}) *ReplaceRequest { + req := new(ReplaceRequest) + req.requestCode = ReplaceRequestCode + req.setSpace(space) + req.tuple = []interface{}{} + return req +} + +// Tuple sets the tuple for replace by the replace request. +// Note: default value is nil. +func (req *ReplaceRequest) Tuple(tuple interface{}) *ReplaceRequest { + req.tuple = tuple + return req +} + +// BodyFunc returns a function that can create an encoded body of +// the replace request. +// It returns an error if the request space cannot be resolved. +func (req *ReplaceRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { + spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) + if err != nil { + return nil, err + } + + return func(enc *msgpack.Encoder) error { + return fillInsert(enc, spaceNo, req.tuple) + }, nil +} + +// DeleteRequest helps you to create a delete request object for execution +// by a Connection. +type DeleteRequest struct { + spaceIndexRequest + key interface{} +} + +// NewDeleteRequest returns a new empty DeleteRequest. +func NewDeleteRequest(space interface{}) *DeleteRequest { + req := new(DeleteRequest) + req.requestCode = DeleteRequestCode + req.setSpace(space) + req.key = []interface{}{} + return req +} + +// Index sets the index for the delete request. +// Note: default value is 0. +func (req *DeleteRequest) Index(index interface{}) *DeleteRequest { + req.setIndex(index) + return req +} + +// Key sets the key of tuple for the delete request. +// Note: default value is empty. +func (req *DeleteRequest) Key(key interface{}) *DeleteRequest { + req.key = key + return req +} + +// BodyFunc returns a function that can create an encoded body of +// the delete request. +// It returns an error if the request space or the request index cannot +// be resolved. +func (req *DeleteRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { + spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) + if err != nil { + return nil, err + } + + return func(enc *msgpack.Encoder) error { + return fillDelete(enc, spaceNo, indexNo, req.key) + }, nil +} + +// UpdateRequest helps you to create an update request object for execution +// by a Connection. +type UpdateRequest struct { + spaceIndexRequest + key interface{} + ops []Op +} + +// NewUpdateRequest returns a new empty UpdateRequest. +func NewUpdateRequest(space interface{}) *UpdateRequest { + req := new(UpdateRequest) + req.requestCode = UpdateRequestCode + req.setSpace(space) + req.key = []interface{}{} + req.ops = []Op{} + return req +} + +// Index sets the index for the update request. +// Note: default value is 0. +func (req *UpdateRequest) Index(index interface{}) *UpdateRequest { + req.setIndex(index) + return req +} + +// Key sets the key of tuple for the update request. +// Note: default value is empty. +func (req *UpdateRequest) Key(key interface{}) *UpdateRequest { + req.key = key + return req +} + +// Operations sets operations to be performed on update. +// Note: default value is empty. +func (req *UpdateRequest) Operations(ops *Operations) *UpdateRequest { + if ops != nil { + req.ops = ops.ops + } + return req +} + +// BodyFunc returns a function that can create an encoded body of +// the update request. +// It returns an error if the request space or the request index cannot +// be resolved. +func (req *UpdateRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { + spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) + if err != nil { + return nil, err + } + + return func(enc *msgpack.Encoder) error { + return fillUpdate(enc, spaceNo, indexNo, req.key, req.ops) + }, nil +} + +// UpsertRequest helps you to create an upsert request object for execution +// by a Connection. +type UpsertRequest struct { + spaceRequest + tuple interface{} + ops []Op +} + +// NewUpsertRequest returns a new empty UpsertRequest. +func NewUpsertRequest(space interface{}) *UpsertRequest { + req := new(UpsertRequest) + req.requestCode = UpsertRequestCode + req.setSpace(space) + req.tuple = []interface{}{} + req.ops = []Op{} + return req +} + +// Tuple sets the tuple for insertion or update by the upsert request. +// Note: default value is empty. +func (req *UpsertRequest) Tuple(tuple interface{}) *UpsertRequest { + req.tuple = tuple + return req +} + +// Operations sets operations to be performed on update case by the upsert request. +// Note: default value is empty. +func (req *UpsertRequest) Operations(ops *Operations) *UpsertRequest { + if ops != nil { + req.ops = ops.ops + } + return req +} + +// BodyFunc returns a function that can create an encoded body of +// the upsert request. +// It returns an error if the request space or the request index cannot +// be resolved. +func (req *UpsertRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { + spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) + if err != nil { + return nil, err + } + + return func(enc *msgpack.Encoder) error { + return fillUpsert(enc, spaceNo, req.tuple, req.ops) + }, nil +} + +// CallRequest helps you to create a call request object for execution +// by a Connection. +type CallRequest struct { + baseRequest + function string + args interface{} +} + +// NewCallRequest return a new empty CallRequest. It uses request code for +// Tarantool >= 1.7 if go-tarantool was build with go_tarantool_call_17 tag. +// Otherwise, uses request code for Tarantool 1.6. +func NewCallRequest(function string) *CallRequest { + req := new(CallRequest) + req.requestCode = CallRequestCode + req.function = function + req.args = []interface{}{} + return req +} + +// Args sets the args for the call request. +// Note: default value is empty. +func (req *CallRequest) Args(args interface{}) *CallRequest { + req.args = args + return req +} + +// BodyFunc returns a function that can create an encoded body of +// the call request. +func (req *CallRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { + return func(enc *msgpack.Encoder) error { + return fillCall(enc, req.function, req.args) + }, nil +} + +// NewCall16Request returns a new empty Call16Request. It uses request code for +// Tarantool 1.6. +// Deprecated since Tarantool 1.7.2. +func NewCall16Request(function string) *CallRequest { + req := NewCallRequest(function) + req.requestCode = Call16RequestCode + return req +} + +// NewCall17Request returns a new empty CallRequest. It uses request code for +// Tarantool >= 1.7. +func NewCall17Request(function string) *CallRequest { + req := NewCallRequest(function) + req.requestCode = Call17RequestCode + return req +} + +// EvalRequest helps you to create an eval request object for execution +// by a Connection. +type EvalRequest struct { + baseRequest + expr string + args interface{} +} + +// NewEvalRequest returns a new empty EvalRequest. +func NewEvalRequest(expr string) *EvalRequest { + req := new(EvalRequest) + req.requestCode = EvalRequestCode + req.expr = expr + req.args = []interface{}{} + return req +} + +// Args sets the args for the eval request. +// Note: default value is empty. +func (req *EvalRequest) Args(args interface{}) *EvalRequest { + req.args = args + return req +} + +// BodyFunc returns a function that can create an encoded body of +// the eval request. +func (req *EvalRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { + return func(enc *msgpack.Encoder) error { + return fillEval(enc, req.expr, req.args) + }, nil +} + +// ExecuteRequest helps you to create an execute request object for execution +// by a Connection. +type ExecuteRequest struct { + baseRequest + expr string + args interface{} +} + +// NewExecuteRequest returns a new empty ExecuteRequest. +func NewExecuteRequest(expr string) *ExecuteRequest { + req := new(ExecuteRequest) + req.requestCode = ExecuteRequestCode + req.expr = expr + req.args = []interface{}{} + return req +} + +// Args sets the args for the execute request. +// Note: default value is empty. +func (req *ExecuteRequest) Args(args interface{}) *ExecuteRequest { + req.args = args + return req +} + +// BodyFunc returns a function that can create an encoded body of +// the execute request. +func (req *ExecuteRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { + return func(enc *msgpack.Encoder) error { + return fillExecute(enc, req.expr, req.args) + }, nil +} diff --git a/request_test.go b/request_test.go new file mode 100644 index 000000000..84ff78ab1 --- /dev/null +++ b/request_test.go @@ -0,0 +1,520 @@ +package tarantool_test + +import ( + "bytes" + "errors" + "testing" + + . "github.com/tarantool/go-tarantool" + "gopkg.in/vmihailenco/msgpack.v2" +) + +const invalidSpaceMsg = "invalid space" +const invalidIndexMsg = "invalid index" + +const invalidSpace = 2 +const invalidIndex = 2 +const validSpace = 1 // Any valid value != default. +const validIndex = 3 // Any valid value != default. +const validExpr = "any string" // We don't check the value here. +const defaultSpace = 0 // And valid too. +const defaultIndex = 0 // And valid too. + +type ValidSchemeResolver struct { +} + +func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (spaceNo, indexNo uint32, err error) { + if s != nil { + spaceNo = uint32(s.(int)) + } else { + spaceNo = defaultSpace + } + if i != nil { + indexNo = uint32(i.(int)) + } else { + indexNo = defaultIndex + } + if spaceNo == invalidSpace { + return 0, 0, errors.New(invalidSpaceMsg) + } + if indexNo == invalidIndex { + return 0, 0, errors.New(invalidIndexMsg) + } + return spaceNo, indexNo, nil +} + +var resolver ValidSchemeResolver + +func assertBodyFuncCall(t testing.TB, requests []Request, errorMsg string) { + t.Helper() + + const errBegin = "An unexpected Request.BodyFunc() " + for _, req := range requests { + _, err := req.BodyFunc(&resolver) + if err != nil && errorMsg != "" && err.Error() != errorMsg { + t.Errorf(errBegin+"error %q expected %q", err.Error(), errorMsg) + } + if err != nil && errorMsg == "" { + t.Errorf(errBegin+"error %q", err.Error()) + } + if err == nil && errorMsg != "" { + t.Errorf(errBegin+"result, expexted error %q", errorMsg) + } + } +} + +func assertBodyEqual(t testing.TB, reference []byte, req Request) { + t.Helper() + + var reqBuf bytes.Buffer + reqEnc := msgpack.NewEncoder(&reqBuf) + + f, err := req.BodyFunc(&resolver) + if err != nil { + t.Errorf("An unexpected Response.BodyFunc() error: %q", err.Error()) + } else { + err = f(reqEnc) + if err != nil { + t.Errorf("An unexpected encode body error: %q", err.Error()) + } + reqBody := reqBuf.Bytes() + if !bytes.Equal(reqBody, reference) { + t.Errorf("Encoded request %v != reference %v", reqBody, reference) + } + } +} + +func getTestOps() ([]Op, *Operations) { + ops := []Op{ + {"+", 1, 2}, + {"-", 3, 4}, + {"&", 5, 6}, + {"|", 7, 8}, + {"^", 9, 1}, + {"^", 9, 1}, // The duplication is for test purposes. + {":", 2, 3}, + {"!", 4, 5}, + {"#", 6, 7}, + {"=", 8, 9}, + } + operations := NewOperations(). + Add(1, 2). + Subtract(3, 4). + BitwiseAnd(5, 6). + BitwiseOr(7, 8). + BitwiseXor(9, 1). + BitwiseXor(9, 1). // The duplication is for test purposes. + Splice(2, 3). + Insert(4, 5). + Delete(6, 7). + Assign(8, 9) + return ops, operations +} + +func TestRequestsValidSpaceAndIndex(t *testing.T) { + requests := []Request{ + NewSelectRequest(validSpace), + NewSelectRequest(validSpace).Index(validIndex), + NewUpdateRequest(validSpace), + NewUpdateRequest(validSpace).Index(validIndex), + NewUpsertRequest(validSpace), + NewInsertRequest(validSpace), + NewReplaceRequest(validSpace), + NewDeleteRequest(validSpace), + NewDeleteRequest(validSpace).Index(validIndex), + } + + assertBodyFuncCall(t, requests, "") +} + +func TestRequestsInvalidSpace(t *testing.T) { + requests := []Request{ + NewSelectRequest(invalidSpace).Index(validIndex), + NewSelectRequest(invalidSpace), + NewUpdateRequest(invalidSpace).Index(validIndex), + NewUpdateRequest(invalidSpace), + NewUpsertRequest(invalidSpace), + NewInsertRequest(invalidSpace), + NewReplaceRequest(invalidSpace), + NewDeleteRequest(invalidSpace).Index(validIndex), + NewDeleteRequest(invalidSpace), + } + + assertBodyFuncCall(t, requests, invalidSpaceMsg) +} + +func TestRequestsInvalidIndex(t *testing.T) { + requests := []Request{ + NewSelectRequest(validSpace).Index(invalidIndex), + NewUpdateRequest(validSpace).Index(invalidIndex), + NewDeleteRequest(validSpace).Index(invalidIndex), + } + + assertBodyFuncCall(t, requests, invalidIndexMsg) +} + +func TestRequestsCodes(t *testing.T) { + tests := []struct { + req Request + code int32 + }{ + {req: NewSelectRequest(validSpace), code: SelectRequestCode}, + {req: NewUpdateRequest(validSpace), code: UpdateRequestCode}, + {req: NewUpsertRequest(validSpace), code: UpsertRequestCode}, + {req: NewInsertRequest(validSpace), code: InsertRequestCode}, + {req: NewReplaceRequest(validSpace), code: ReplaceRequestCode}, + {req: NewDeleteRequest(validSpace), code: DeleteRequestCode}, + {req: NewCall16Request(validExpr), code: Call16RequestCode}, + {req: NewCall17Request(validExpr), code: Call17RequestCode}, + {req: NewEvalRequest(validExpr), code: EvalRequestCode}, + {req: NewExecuteRequest(validExpr), code: ExecuteRequestCode}, + {req: NewPingRequest(), code: PingRequestCode}, + } + + for _, test := range tests { + if code := test.req.Code(); code != test.code { + t.Errorf("An invalid request code 0x%x, expected 0x%x", code, test.code) + } + } +} + +func TestPingRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplPingBody(refEnc) + if err != nil { + t.Errorf("An unexpected RefImplPingBody() error: %q", err.Error()) + return + } + + req := NewPingRequest() + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestSelectRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterAll, []interface{}{}) + if err != nil { + t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) + return + } + + req := NewSelectRequest(validSpace) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestSelectRequestDefaultIteratorEqIfKey(t *testing.T) { + var refBuf bytes.Buffer + key := []interface{}{uint(18)} + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterEq, key) + if err != nil { + t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) + return + } + + req := NewSelectRequest(validSpace). + Key(key) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestSelectRequestIteratorNotChangedIfKey(t *testing.T) { + var refBuf bytes.Buffer + key := []interface{}{uint(678)} + const iter = IterGe + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, iter, key) + if err != nil { + t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) + return + } + + req := NewSelectRequest(validSpace). + Iterator(iter). + Key(key) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestSelectRequestSetters(t *testing.T) { + const offset = 4 + const limit = 5 + const iter = IterLt + key := []interface{}{uint(36)} + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplSelectBody(refEnc, validSpace, validIndex, offset, limit, iter, key) + if err != nil { + t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) + return + } + + req := NewSelectRequest(validSpace). + Index(validIndex). + Offset(offset). + Limit(limit). + Iterator(iter). + Key(key) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestInsertRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplInsertBody(refEnc, validSpace, []interface{}{}) + if err != nil { + t.Errorf("An unexpected RefImplInsertBody() error: %q", err.Error()) + return + } + + req := NewInsertRequest(validSpace) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestInsertRequestSetters(t *testing.T) { + tuple := []interface{}{uint(24)} + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplInsertBody(refEnc, validSpace, tuple) + if err != nil { + t.Errorf("An unexpected RefImplInsertBody() error: %q", err.Error()) + return + } + + req := NewInsertRequest(validSpace). + Tuple(tuple) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestReplaceRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplReplaceBody(refEnc, validSpace, []interface{}{}) + if err != nil { + t.Errorf("An unexpected RefImplReplaceBody() error: %q", err.Error()) + return + } + + req := NewReplaceRequest(validSpace) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestReplaceRequestSetters(t *testing.T) { + tuple := []interface{}{uint(99)} + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplReplaceBody(refEnc, validSpace, tuple) + if err != nil { + t.Errorf("An unexpected RefImplReplaceBody() error: %q", err.Error()) + return + } + + req := NewReplaceRequest(validSpace). + Tuple(tuple) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestDeleteRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplDeleteBody(refEnc, validSpace, defaultIndex, []interface{}{}) + if err != nil { + t.Errorf("An unexpected RefImplDeleteBody() error: %q", err.Error()) + return + } + + req := NewDeleteRequest(validSpace) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestDeleteRequestSetters(t *testing.T) { + key := []interface{}{uint(923)} + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplDeleteBody(refEnc, validSpace, validIndex, key) + if err != nil { + t.Errorf("An unexpected RefImplDeleteBody() error: %q", err.Error()) + return + } + + req := NewDeleteRequest(validSpace). + Index(validIndex). + Key(key) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestUpdateRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplUpdateBody(refEnc, validSpace, defaultIndex, []interface{}{}, []Op{}) + if err != nil { + t.Errorf("An unexpected RefImplUpdateBody() error: %q", err.Error()) + return + } + + req := NewUpdateRequest(validSpace) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestUpdateRequestSetters(t *testing.T) { + key := []interface{}{uint(44)} + refOps, reqOps := getTestOps() + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplUpdateBody(refEnc, validSpace, validIndex, key, refOps) + if err != nil { + t.Errorf("An unexpected RefImplUpdateBody() error: %q", err.Error()) + return + } + + req := NewUpdateRequest(validSpace). + Index(validIndex). + Key(key). + Operations(reqOps) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestUpsertRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplUpsertBody(refEnc, validSpace, []interface{}{}, []Op{}) + if err != nil { + t.Errorf("An unexpected RefImplUpsertBody() error: %q", err.Error()) + return + } + + req := NewUpsertRequest(validSpace) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestUpsertRequestSetters(t *testing.T) { + tuple := []interface{}{uint(64)} + refOps, reqOps := getTestOps() + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplUpsertBody(refEnc, validSpace, tuple, refOps) + if err != nil { + t.Errorf("An unexpected RefImplUpsertBody() error: %q", err.Error()) + return + } + + req := NewUpsertRequest(validSpace). + Tuple(tuple). + Operations(reqOps) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestCallRequestsDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplCallBody(refEnc, validExpr, []interface{}{}) + if err != nil { + t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) + return + } + + req := NewCallRequest(validExpr) + req16 := NewCall16Request(validExpr) + req17 := NewCall17Request(validExpr) + assertBodyEqual(t, refBuf.Bytes(), req) + assertBodyEqual(t, refBuf.Bytes(), req16) + assertBodyEqual(t, refBuf.Bytes(), req17) +} + +func TestCallRequestsSetters(t *testing.T) { + args := []interface{}{uint(34)} + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplCallBody(refEnc, validExpr, args) + if err != nil { + t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) + return + } + + req := NewCall16Request(validExpr). + Args(args) + req16 := NewCall16Request(validExpr). + Args(args) + req17 := NewCall17Request(validExpr). + Args(args) + assertBodyEqual(t, refBuf.Bytes(), req) + assertBodyEqual(t, refBuf.Bytes(), req16) + assertBodyEqual(t, refBuf.Bytes(), req17) +} + +func TestEvalRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplEvalBody(refEnc, validExpr, []interface{}{}) + if err != nil { + t.Errorf("An unexpected RefImplEvalBody() error: %q", err.Error()) + return + } + + req := NewEvalRequest(validExpr) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestEvalRequestSetters(t *testing.T) { + args := []interface{}{uint(34), int(12)} + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplEvalBody(refEnc, validExpr, args) + if err != nil { + t.Errorf("An unexpected RefImplEvalBody() error: %q", err.Error()) + return + } + + req := NewEvalRequest(validExpr). + Args(args) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestExecuteRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplExecuteBody(refEnc, validExpr, []interface{}{}) + if err != nil { + t.Errorf("An unexpected RefImplExecuteBody() error: %q", err.Error()) + return + } + + req := NewExecuteRequest(validExpr) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestExecuteRequestSetters(t *testing.T) { + args := []interface{}{uint(11)} + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplExecuteBody(refEnc, validExpr, args) + if err != nil { + t.Errorf("An unexpected RefImplExecuteBody() error: %q", err.Error()) + return + } + + req := NewExecuteRequest(validExpr). + Args(args) + assertBodyEqual(t, refBuf.Bytes(), req) +} diff --git a/schema.go b/schema.go index ab937f2f1..b1e8562d2 100644 --- a/schema.go +++ b/schema.go @@ -4,6 +4,13 @@ import ( "fmt" ) +// SchemaResolver is an interface for resolving schema details. +type SchemaResolver interface { + // ResolveSpaceIndex returns resolved space and index numbers or an + // error if it cannot be resolved. + ResolveSpaceIndex(s interface{}, i interface{}) (spaceNo, indexNo uint32, err error) +} + // Schema contains information about spaces and indexes. type Schema struct { Version uint @@ -176,7 +183,10 @@ func (conn *Connection) loadSchema() (err error) { return nil } -func (schema *Schema) resolveSpaceIndex(s interface{}, i interface{}) (spaceNo, indexNo uint32, err error) { +// ResolveSpaceIndex tries to resolve space and index numbers. +// Note: s can be a number, string, or an object of Space type. +// Note: i can be a number, string, or an object of Index type. +func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (spaceNo, indexNo uint32, err error) { var space *Space var index *Index var ok bool diff --git a/tarantool_test.go b/tarantool_test.go index 9e4c5f7c6..d76f82d1c 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -101,6 +101,33 @@ func BenchmarkClientSerial(b *testing.B) { } } +func BenchmarkClientSerialRequestObject(b *testing.B) { + var err error + + conn := connect(b, server, opts) + defer conn.Close() + + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Error(err) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + req := NewSelectRequest(spaceNo). + Index(indexNo). + Offset(0). + Limit(1). + Iterator(IterEq). + Key([]interface{}{uint(1111)}) + _, err := conn.Do(req) + if err != nil { + b.Error(err) + } + } +} + func BenchmarkClientSerialTyped(b *testing.B) { var err error @@ -1476,11 +1503,11 @@ func TestSchema(t *testing.T) { } _, _, err = schema.ResolveSpaceIndex("schematest22", "secondary") if err == nil { - t.Errorf("resolveSpaceIndex didn't returned error with not existing space name") + t.Errorf("ResolveSpaceIndex didn't returned error with not existing space name") } _, _, err = schema.ResolveSpaceIndex("schematest", "secondary22") if err == nil { - t.Errorf("resolveSpaceIndex didn't returned error with not existing index name") + t.Errorf("ResolveSpaceIndex didn't returned error with not existing index name") } } @@ -1574,6 +1601,349 @@ func TestClientNamed(t *testing.T) { } } +func TestClientRequestObjects(t *testing.T) { + var ( + req Request + resp *Response + err error + ) + + conn := connect(t, server, opts) + defer conn.Close() + + // Ping + req = NewPingRequest() + resp, err = conn.Do(req) + if err != nil { + t.Fatalf("Failed to Ping: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Ping") + } + if len(resp.Data) != 0 { + t.Errorf("Response Body len != 0") + } + + // The code prepares data. + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } + + // Insert + for i := 1010; i < 1020; i++ { + req = NewInsertRequest(spaceName). + Tuple([]interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) + resp, err = conn.Do(req) + if err != nil { + t.Fatalf("Failed to Insert: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Insert") + } + if resp.Data == nil { + t.Fatalf("Response data is nil after Insert") + } + if len(resp.Data) != 1 { + t.Fatalf("Response Body len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Insert") + } else { + if len(tpl) != 3 { + t.Errorf("Unexpected body of Insert (tuple len)") + } + if id, ok := tpl[0].(uint64); !ok || id != uint64(i) { + t.Errorf("Unexpected body of Insert (0)") + } + if h, ok := tpl[1].(string); !ok || h != fmt.Sprintf("val %d", i) { + t.Errorf("Unexpected body of Insert (1)") + } + if h, ok := tpl[2].(string); !ok || h != "bla" { + t.Errorf("Unexpected body of Insert (2)") + } + } + } + + // Replace + for i := 1015; i < 1020; i++ { + req = NewReplaceRequest(spaceName). + Tuple([]interface{}{uint(i), fmt.Sprintf("val %d", i), "blar"}) + resp, err = conn.Do(req) + if err != nil { + t.Fatalf("Failed to Replace: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Replace") + } + if resp.Data == nil { + t.Fatalf("Response data is nil after Replace") + } + if len(resp.Data) != 1 { + t.Fatalf("Response Body len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Replace") + } else { + if len(tpl) != 3 { + t.Errorf("Unexpected body of Replace (tuple len)") + } + if id, ok := tpl[0].(uint64); !ok || id != uint64(i) { + t.Errorf("Unexpected body of Replace (0)") + } + if h, ok := tpl[1].(string); !ok || h != fmt.Sprintf("val %d", i) { + t.Errorf("Unexpected body of Replace (1)") + } + if h, ok := tpl[2].(string); !ok || h != "blar" { + t.Errorf("Unexpected body of Replace (2)") + } + } + } + + // Delete + req = NewDeleteRequest(spaceName). + Key([]interface{}{uint(1016)}) + resp, err = conn.Do(req) + if err != nil { + t.Fatalf("Failed to Delete: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Delete") + } + if resp.Data == nil { + t.Fatalf("Response data is nil after Delete") + } + if len(resp.Data) != 1 { + t.Fatalf("Response Body len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Delete") + } else { + if len(tpl) != 3 { + t.Errorf("Unexpected body of Delete (tuple len)") + } + if id, ok := tpl[0].(uint64); !ok || id != uint64(1016) { + t.Errorf("Unexpected body of Delete (0)") + } + if h, ok := tpl[1].(string); !ok || h != "val 1016" { + t.Errorf("Unexpected body of Delete (1)") + } + if h, ok := tpl[2].(string); !ok || h != "blar" { + t.Errorf("Unexpected body of Delete (2)") + } + } + + // Update without operations. + req = NewUpdateRequest(spaceName). + Index(indexName). + Key([]interface{}{uint(1010)}) + resp, err = conn.Do(req) + if err != nil { + t.Errorf("Failed to Update: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Update") + } + if resp.Data == nil { + t.Fatalf("Response data is nil after Update") + } + if len(resp.Data) != 1 { + t.Fatalf("Response Data len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Update") + } else { + if id, ok := tpl[0].(uint64); !ok || id != 1010 { + t.Errorf("Unexpected body of Update (0)") + } + if h, ok := tpl[1].(string); !ok || h != "val 1010" { + t.Errorf("Unexpected body of Update (1)") + } + if h, ok := tpl[2].(string); !ok || h != "bla" { + t.Errorf("Unexpected body of Update (2)") + } + } + + // Update. + req = NewUpdateRequest(spaceName). + Index(indexName). + Key([]interface{}{uint(1010)}). + Operations(NewOperations().Assign(1, "bye").Insert(2, 1)) + resp, err = conn.Do(req) + if err != nil { + t.Errorf("Failed to Update: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Update") + } + if resp.Data == nil { + t.Fatalf("Response data is nil after Update") + } + if len(resp.Data) != 1 { + t.Fatalf("Response Data len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Select") + } else { + if id, ok := tpl[0].(uint64); !ok || id != 1010 { + t.Errorf("Unexpected body of Update (0)") + } + if h, ok := tpl[1].(string); !ok || h != "bye" { + t.Errorf("Unexpected body of Update (1)") + } + if h, ok := tpl[2].(uint64); !ok || h != 1 { + t.Errorf("Unexpected body of Update (2)") + } + } + + // Upsert without operations. + req = NewUpsertRequest(spaceNo). + Tuple([]interface{}{uint(1010), "hi", "hi"}) + resp, err = conn.Do(req) + if err != nil { + t.Errorf("Failed to Upsert (update): %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Upsert (update)") + } + if resp.Data == nil { + t.Fatalf("Response data is nil after Upsert") + } + if len(resp.Data) != 0 { + t.Fatalf("Response Data len != 0") + } + + // Upsert. + req = NewUpsertRequest(spaceNo). + Tuple([]interface{}{uint(1010), "hi", "hi"}). + Operations(NewOperations().Assign(2, "bye")) + resp, err = conn.Do(req) + if err != nil { + t.Errorf("Failed to Upsert (update): %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Upsert (update)") + } + if resp.Data == nil { + t.Fatalf("Response data is nil after Upsert") + } + if len(resp.Data) != 0 { + t.Fatalf("Response Data len != 0") + } + + // Select. + req = NewSelectRequest(spaceNo). + Index(indexNo). + Limit(20). + Iterator(IterGe). + Key([]interface{}{uint(1010)}) + resp, err = conn.Do(req) + if err != nil { + t.Errorf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Select") + return + } + if len(resp.Data) != 9 { + t.Fatalf("Response Data len %d != 9", len(resp.Data)) + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Select") + } else { + if id, ok := tpl[0].(uint64); !ok || id != 1010 { + t.Errorf("Unexpected body of Select (0) %d, expected %d", tpl[0].(uint64), 1010) + } + if h, ok := tpl[1].(string); !ok || h != "bye" { + t.Errorf("Unexpected body of Select (1) %q, expected %q", tpl[1].(string), "bye") + } + if h, ok := tpl[2].(string); !ok || h != "bye" { + t.Errorf("Unexpected body of Select (2) %q, expected %q", tpl[2].(string), "bye") + } + } + + // Call16 vs Call17 + req = NewCall16Request("simple_incr").Args([]interface{}{1}) + resp, err = conn.Do(req) + if err != nil { + t.Errorf("Failed to use Call") + } + if resp.Data[0].([]interface{})[0].(uint64) != 2 { + t.Errorf("result is not {{1}} : %v", resp.Data) + } + + // Call17 + req = NewCall17Request("simple_incr").Args([]interface{}{1}) + resp, err = conn.Do(req) + if err != nil { + t.Errorf("Failed to use Call17") + } + if resp.Data[0].(uint64) != 2 { + t.Errorf("result is not {{1}} : %v", resp.Data) + } + + // Eval + req = NewEvalRequest("return 5 + 6") + resp, err = conn.Do(req) + if err != nil { + t.Fatalf("Failed to Eval: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Eval") + } + if len(resp.Data) < 1 { + t.Errorf("Response.Data is empty after Eval") + } + val := resp.Data[0].(uint64) + if val != 11 { + t.Errorf("5 + 6 == 11, but got %v", val) + } + + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + if isLess { + return + } + + req = NewExecuteRequest(createTableQuery) + resp, err = conn.Do(req) + if err != nil { + t.Fatalf("Failed to Execute: %s", err.Error()) + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if len(resp.Data) != 0 { + t.Fatalf("Response Body len != 0") + } + if resp.Code != OkCode { + t.Fatalf("Failed to Execute: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 1 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + } + + req = NewExecuteRequest(dropQuery2) + resp, err = conn.Do(req) + if err != nil { + t.Fatalf("Failed to Execute: %s", err.Error()) + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if len(resp.Data) != 0 { + t.Fatalf("Response Body len != 0") + } + if resp.Code != OkCode { + t.Fatalf("Failed to Execute: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 1 { + t.Errorf("Incorrect count of dropped spaces: %d", resp.SQLInfo.AffectedCount) + } +} + func TestComplexStructs(t *testing.T) { var err error From 1e1f8f4151a622ab1c243d282cabfc8c5cc145cb Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 2 Jun 2022 11:07:26 +0300 Subject: [PATCH 286/605] api: add Do* functions to ConnectionPool and ConnectionMulti This patch provides Do, DoTyped and DoAsync functions for ConnectionPool and ConnectionMulti types. Part of #126 --- connection_pool/connection_pool.go | 30 +++++++++++++++++++ connection_pool/connection_pool_test.go | 39 +++++++++++++++++++++++++ connection_pool/example_test.go | 19 ++++++++++++ connector.go | 4 +++ multi/multi.go | 15 ++++++++++ 5 files changed, 107 insertions(+) diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index be56806c5..a54ff8d86 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -524,6 +524,36 @@ func (connPool *ConnectionPool) EvalAsync(expr string, args interface{}, userMod return conn.EvalAsync(expr, args) } +// Do sends the request and returns a response. +func (connPool *ConnectionPool) Do(req tarantool.Request, userMode Mode) (*tarantool.Response, error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return nil, err + } + + return conn.Do(req) +} + +// DoTyped sends the request and fills the typed result. +func (connPool *ConnectionPool) DoTyped(req tarantool.Request, result interface{}, userMode Mode) error { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return err + } + + return conn.DoTyped(req, result) +} + +// DoAsync sends the request and returns a future. +func (connPool *ConnectionPool) DoAsync(req tarantool.Request, userMode Mode) (*tarantool.Future, error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return nil, err + } + + return conn.DoAsync(req) +} + // // private // diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 5b8a606cd..6bad15dd7 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -1252,6 +1252,45 @@ func TestPing(t *testing.T) { require.NotNilf(t, resp, "response is nil after Ping") } +func TestDo(t *testing.T) { + roles := []bool{true, true, false, true, false} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + req := tarantool.NewPingRequest() + // ANY + resp, err := connPool.Do(req, connection_pool.ANY) + require.Nilf(t, err, "failed to Ping") + require.NotNilf(t, resp, "response is nil after Ping") + + // RW + resp, err = connPool.Do(req, connection_pool.RW) + require.Nilf(t, err, "failed to Ping") + require.NotNilf(t, resp, "response is nil after Ping") + + // RO + resp, err = connPool.Do(req, connection_pool.RO) + require.Nilf(t, err, "failed to Ping") + require.NotNilf(t, resp, "response is nil after Ping") + + // PreferRW + resp, err = connPool.Do(req, connection_pool.PreferRW) + require.Nilf(t, err, "failed to Ping") + require.NotNilf(t, resp, "response is nil after Ping") + + // PreferRO + resp, err = connPool.Do(req, connection_pool.PreferRO) + require.Nilf(t, err, "failed to Ping") + require.NotNilf(t, resp, "response is nil after Ping") +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go index f567128dd..f7b0009b0 100644 --- a/connection_pool/example_test.go +++ b/connection_pool/example_test.go @@ -529,3 +529,22 @@ func ExampleConnectionPool_Eval() { // Code 0 // Data [3] } + +func ExampleConnectionPool_Do() { + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + // Ping a Tarantool instance to check connection. + req := tarantool.NewPingRequest() + resp, err := pool.Do(req, connection_pool.ANY) + fmt.Println("Ping Code", resp.Code) + fmt.Println("Ping Data", resp.Data) + fmt.Println("Ping Error", err) + // Output: + // Ping Code 0 + // Ping Data [] + // Ping Error +} diff --git a/connector.go b/connector.go index d8bdc13a0..46225451e 100644 --- a/connector.go +++ b/connector.go @@ -41,4 +41,8 @@ type Connector interface { Call16Async(functionName string, args interface{}) *Future Call17Async(functionName string, args interface{}) *Future EvalAsync(expr string, args interface{}) *Future + + Do(req Request) (resp *Response, err error) + DoTyped(req Request, result interface{}) (err error) + DoAsync(req Request) (fut *Future, err error) } diff --git a/multi/multi.go b/multi/multi.go index edd6728b0..b7140ae3a 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -481,3 +481,18 @@ func (connMulti *ConnectionMulti) Call17Async(functionName string, args interfac func (connMulti *ConnectionMulti) EvalAsync(expr string, args interface{}) *tarantool.Future { return connMulti.getCurrentConnection().EvalAsync(expr, args) } + +// Do sends the request and returns a response. +func (connMulti *ConnectionMulti) Do(req tarantool.Request) (*tarantool.Response, error) { + return connMulti.getCurrentConnection().Do(req) +} + +// DoTyped sends the request and fills the typed result. +func (connMulti *ConnectionMulti) DoTyped(req tarantool.Request, result interface{}) error { + return connMulti.getCurrentConnection().DoTyped(req, result) +} + +// DoAsync sends the request and returns a future. +func (connMulti *ConnectionMulti) DoAsync(req tarantool.Request) (*tarantool.Future, error) { + return connMulti.getCurrentConnection().DoAsync(req) +} From 7116a33aa26bb7e2390a59a20f555ff3d120387d Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 14 Jun 2022 16:05:27 +0300 Subject: [PATCH 287/605] code health: all requests use request objects The patch is a refactoring of an internal logic. It replaces the usage of closures to request objects to construct a request body. After the patch all Connection.* requests use request objects inside. Closes #126 --- connection.go | 79 +++++---- connection_pool/connection_pool.go | 4 +- connector.go | 2 +- example_test.go | 15 +- future.go | 46 ++---- multi/multi.go | 2 +- request.go | 246 +++++++++++------------------ request_test.go | 23 ++- 8 files changed, 159 insertions(+), 258 deletions(-) diff --git a/connection.go b/connection.go index 9958a0352..80e609354 100644 --- a/connection.go +++ b/connection.go @@ -468,18 +468,35 @@ func (conn *Connection) dial() (err error) { return } -func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { - request := &Future{ - requestId: 0, - requestCode: AuthRequestCode, +func pack(h *smallWBuf, enc *msgpack.Encoder, reqid uint32, req Request, res SchemaResolver) (err error) { + hl := h.Len() + h.Write([]byte{ + 0xce, 0, 0, 0, 0, // Length. + 0x82, // 2 element map. + KeyCode, byte(req.Code()), // Request code. + KeySync, 0xce, + byte(reqid >> 24), byte(reqid >> 16), + byte(reqid >> 8), byte(reqid), + }) + + if err = req.Body(res, enc); err != nil { + return } + + l := uint32(h.Len() - 5 - hl) + h.b[hl+1] = byte(l >> 24) + h.b[hl+2] = byte(l >> 16) + h.b[hl+3] = byte(l >> 8) + h.b[hl+4] = byte(l) + + return +} + +func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { var packet smallWBuf - err = request.pack(&packet, msgpack.NewEncoder(&packet), func(enc *msgpack.Encoder) error { - return enc.Encode(map[uint32]interface{}{ - KeyUserName: conn.opts.User, - KeyTuple: []interface{}{string("chap-sha1"), string(scramble)}, - }) - }) + req := newAuthRequest(conn.opts.User, string(scramble)) + err = pack(&packet, msgpack.NewEncoder(&packet), 0, req, conn.Schema) + if err != nil { return errors.New("auth: pack error " + err.Error()) } @@ -704,7 +721,7 @@ func (conn *Connection) reader(r *bufio.Reader, c net.Conn) { } } -func (conn *Connection) newFuture(requestCode int32) (fut *Future) { +func (conn *Connection) newFuture() (fut *Future) { fut = NewFuture() if conn.rlimit != nil && conn.opts.RLimitAction == RLimitDrop { select { @@ -720,7 +737,6 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { } } fut.requestId = conn.nextRequestId() - fut.requestCode = requestCode shardn := fut.requestId & (conn.opts.Concurrency - 1) shard := &conn.shard[shardn] shard.rmut.Lock() @@ -769,23 +785,16 @@ func (conn *Connection) newFuture(requestCode int32) (fut *Future) { return } -func (conn *Connection) sendFuture(fut *Future, body func(*msgpack.Encoder) error) *Future { +func (conn *Connection) send(req Request) *Future { + fut := conn.newFuture() if fut.ready == nil { return fut } - conn.putFuture(fut, body) - return fut -} - -func (conn *Connection) failFuture(fut *Future, err error) *Future { - if f := conn.fetchFuture(fut.requestId); f == fut { - fut.SetError(err) - conn.markDone(fut) - } + conn.putFuture(fut, req) return fut } -func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error) { +func (conn *Connection) putFuture(fut *Future, req Request) { shardn := fut.requestId & (conn.opts.Concurrency - 1) shard := &conn.shard[shardn] shard.bufmut.Lock() @@ -801,10 +810,11 @@ func (conn *Connection) putFuture(fut *Future, body func(*msgpack.Encoder) error shard.enc = msgpack.NewEncoder(&shard.buf) } blen := shard.buf.Len() - if err := fut.pack(&shard.buf, shard.enc, body); err != nil { + reqid := fut.requestId + if err := pack(&shard.buf, shard.enc, reqid, req, conn.Schema); err != nil { shard.buf.Trunc(blen) shard.bufmut.Unlock() - if f := conn.fetchFuture(fut.requestId); f == fut { + if f := conn.fetchFuture(reqid); f == fut { fut.SetError(err) conn.markDone(fut) } else if f != nil { @@ -983,10 +993,7 @@ func (conn *Connection) nextRequestId() (requestId uint32) { // An error is returned if the request was formed incorrectly, or failed to // communicate by the connection, or unable to decode the response. func (conn *Connection) Do(req Request) (*Response, error) { - fut, err := conn.DoAsync(req) - if err != nil { - return nil, err - } + fut := conn.DoAsync(req) return fut.Get() } @@ -995,10 +1002,7 @@ func (conn *Connection) Do(req Request) (*Response, error) { // An error is returned if the request was formed incorrectly, or failed to // communicate by the connection, or unable to decode the response. func (conn *Connection) DoTyped(req Request, result interface{}) error { - fut, err := conn.DoAsync(req) - if err != nil { - return err - } + fut := conn.DoAsync(req) return fut.GetTyped(result) } @@ -1006,13 +1010,8 @@ func (conn *Connection) DoTyped(req Request, result interface{}) error { // // An error is returned if the request was formed incorrectly, or failed to // create the future. -func (conn *Connection) DoAsync(req Request) (*Future, error) { - bodyFunc, err := req.BodyFunc(conn.Schema) - if err != nil { - return nil, err - } - future := conn.newFuture(req.Code()) - return conn.sendFuture(future, bodyFunc), nil +func (conn *Connection) DoAsync(req Request) *Future { + return conn.send(req) } // ConfiguredTimeout returns a timeout from connection config. diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index a54ff8d86..bc7573c8d 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -545,10 +545,10 @@ func (connPool *ConnectionPool) DoTyped(req tarantool.Request, result interface{ } // DoAsync sends the request and returns a future. -func (connPool *ConnectionPool) DoAsync(req tarantool.Request, userMode Mode) (*tarantool.Future, error) { +func (connPool *ConnectionPool) DoAsync(req tarantool.Request, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { - return nil, err + return tarantool.NewErrorFuture(err) } return conn.DoAsync(req) diff --git a/connector.go b/connector.go index 46225451e..a987ccca3 100644 --- a/connector.go +++ b/connector.go @@ -44,5 +44,5 @@ type Connector interface { Do(req Request) (resp *Response, err error) DoTyped(req Request, result interface{}) (err error) - DoAsync(req Request) (fut *Future, err error) + DoAsync(req Request) (fut *Future) } diff --git a/example_test.go b/example_test.go index 5ad471148..070b964da 100644 --- a/example_test.go +++ b/example_test.go @@ -144,10 +144,7 @@ func ExampleSelectRequest() { Index("primary"). Limit(100). Key(tarantool.IntKey{1111}) - fut, err := conn.DoAsync(req) - if err != nil { - fmt.Printf("error in do async select request is %v", err) - } + fut := conn.DoAsync(req) resp, err = fut.Get() if err != nil { fmt.Printf("error in do async select request is %v", err) @@ -177,10 +174,7 @@ func ExampleUpdateRequest() { Index("primary"). Key(tarantool.IntKey{1111}). Operations(tarantool.NewOperations().Assign(1, "hello")) - fut, err := conn.DoAsync(req) - if err != nil { - fmt.Printf("error in do async update request is %v", err) - } + fut := conn.DoAsync(req) resp, err = fut.Get() if err != nil { fmt.Printf("error in do async update request is %v", err) @@ -210,10 +204,7 @@ func ExampleUpsertRequest() { req = tarantool.NewUpsertRequest("test"). Tuple([]interface{}{uint(1113), "second", "second"}). Operations(tarantool.NewOperations().Assign(2, "updated")) - fut, err := conn.DoAsync(req) - if err != nil { - fmt.Printf("error in do async upsert request is %v", err) - } + fut := conn.DoAsync(req) resp, err = fut.Get() if err != nil { fmt.Printf("error in do async upsert request is %v", err) diff --git a/future.go b/future.go index c077e8271..6f21ce749 100644 --- a/future.go +++ b/future.go @@ -3,47 +3,19 @@ package tarantool import ( "sync" "time" - - "gopkg.in/vmihailenco/msgpack.v2" ) // Future is a handle for asynchronous request. type Future struct { - requestId uint32 - requestCode int32 - timeout time.Duration - mutex sync.Mutex - pushes []*Response - resp *Response - err error - ready chan struct{} - done chan struct{} - next *Future -} - -func (fut *Future) pack(h *smallWBuf, enc *msgpack.Encoder, body func(*msgpack.Encoder) error) (err error) { - rid := fut.requestId - hl := h.Len() - h.Write([]byte{ - 0xce, 0, 0, 0, 0, // Length. - 0x82, // 2 element map. - KeyCode, byte(fut.requestCode), // Request code. - KeySync, 0xce, - byte(rid >> 24), byte(rid >> 16), - byte(rid >> 8), byte(rid), - }) - - if err = body(enc); err != nil { - return - } - - l := uint32(h.Len() - 5 - hl) - h.b[hl+1] = byte(l >> 24) - h.b[hl+2] = byte(l >> 16) - h.b[hl+3] = byte(l >> 8) - h.b[hl+4] = byte(l) - - return + requestId uint32 + next *Future + timeout time.Duration + mutex sync.Mutex + pushes []*Response + resp *Response + err error + ready chan struct{} + done chan struct{} } func (fut *Future) wait() { diff --git a/multi/multi.go b/multi/multi.go index b7140ae3a..a28bbb40a 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -493,6 +493,6 @@ func (connMulti *ConnectionMulti) DoTyped(req tarantool.Request, result interfac } // DoAsync sends the request and returns a future. -func (connMulti *ConnectionMulti) DoAsync(req tarantool.Request) (*tarantool.Future, error) { +func (connMulti *ConnectionMulti) DoAsync(req tarantool.Request) *tarantool.Future { return connMulti.getCurrentConnection().DoAsync(req) } diff --git a/request.go b/request.go index c2777778b..c3a83ee56 100644 --- a/request.go +++ b/request.go @@ -97,8 +97,7 @@ func fillPing(enc *msgpack.Encoder) error { // Ping sends empty request to Tarantool to check connection. func (conn *Connection) Ping() (resp *Response, err error) { - future := conn.newFuture(PingRequestCode) - return conn.sendFuture(future, func(enc *msgpack.Encoder) error { return fillPing(enc) }).Get() + return conn.Do(NewPingRequest()) } // Select performs select to box space. @@ -306,79 +305,50 @@ func (conn *Connection) ExecuteTyped(expr string, args interface{}, result inter // SelectAsync sends select request to Tarantool and returns Future. func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Future { - future := conn.newFuture(SelectRequestCode) - spaceNo, indexNo, err := conn.Schema.ResolveSpaceIndex(space, index) - if err != nil { - return conn.failFuture(future, err) - } - return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - return fillSelect(enc, spaceNo, indexNo, offset, limit, iterator, key) - }) + req := NewSelectRequest(space). + Index(index). + Offset(offset). + Limit(limit). + Iterator(iterator). + Key(key) + return conn.DoAsync(req) } // InsertAsync sends insert action to Tarantool and returns Future. // Tarantool will reject Insert when tuple with same primary key exists. func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Future { - future := conn.newFuture(InsertRequestCode) - spaceNo, _, err := conn.Schema.ResolveSpaceIndex(space, nil) - if err != nil { - return conn.failFuture(future, err) - } - return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - return fillInsert(enc, spaceNo, tuple) - }) + req := NewInsertRequest(space).Tuple(tuple) + return conn.DoAsync(req) } // ReplaceAsync sends "insert or replace" action to Tarantool and returns Future. // If tuple with same primary key exists, it will be replaced. func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Future { - future := conn.newFuture(ReplaceRequestCode) - spaceNo, _, err := conn.Schema.ResolveSpaceIndex(space, nil) - if err != nil { - return conn.failFuture(future, err) - } - return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - return fillInsert(enc, spaceNo, tuple) - }) + req := NewReplaceRequest(space).Tuple(tuple) + return conn.DoAsync(req) } // DeleteAsync sends deletion action to Tarantool and returns Future. // Future's result will contain array with deleted tuple. func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) *Future { - future := conn.newFuture(DeleteRequestCode) - spaceNo, indexNo, err := conn.Schema.ResolveSpaceIndex(space, index) - if err != nil { - return conn.failFuture(future, err) - } - return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - return fillDelete(enc, spaceNo, indexNo, key) - }) + req := NewDeleteRequest(space).Index(index).Key(key) + return conn.DoAsync(req) } // Update sends deletion of a tuple by key and returns Future. // Future's result will contain array with updated tuple. func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface{}) *Future { - future := conn.newFuture(UpdateRequestCode) - spaceNo, indexNo, err := conn.Schema.ResolveSpaceIndex(space, index) - if err != nil { - return conn.failFuture(future, err) - } - return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - return fillUpdate(enc, spaceNo, indexNo, key, ops) - }) + req := NewUpdateRequest(space).Index(index).Key(key) + req.ops = ops + return conn.DoAsync(req) } // UpsertAsync sends "update or insert" action to Tarantool and returns Future. // Future's sesult will not contain any tuple. func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *Future { - future := conn.newFuture(UpsertRequestCode) - spaceNo, _, err := conn.Schema.ResolveSpaceIndex(space, nil) - if err != nil { - return conn.failFuture(future, err) - } - return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - return fillUpsert(enc, spaceNo, tuple, ops) - }) + req := NewUpsertRequest(space).Tuple(tuple) + req.ops = ops + return conn.DoAsync(req) } // CallAsync sends a call to registered Tarantool function and returns Future. @@ -386,47 +356,37 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops in // was build with go_tarantool_call_17 tag. // Otherwise, uses request code for Tarantool 1.6. func (conn *Connection) CallAsync(functionName string, args interface{}) *Future { - future := conn.newFuture(CallRequestCode) - return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - return fillCall(enc, functionName, args) - }) + req := NewCallRequest(functionName).Args(args) + return conn.DoAsync(req) } // Call16Async sends a call to registered Tarantool function and returns Future. // It uses request code for Tarantool 1.6, so future's result is always array of arrays. // Deprecated since Tarantool 1.7.2. func (conn *Connection) Call16Async(functionName string, args interface{}) *Future { - future := conn.newFuture(Call16RequestCode) - return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - return fillCall(enc, functionName, args) - }) + req := NewCall16Request(functionName).Args(args) + return conn.DoAsync(req) } // Call17Async sends a call to registered Tarantool function and returns Future. // It uses request code for Tarantool >= 1.7, so future's result will not be converted // (though, keep in mind, result is always array) func (conn *Connection) Call17Async(functionName string, args interface{}) *Future { - future := conn.newFuture(Call17RequestCode) - return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - return fillCall(enc, functionName, args) - }) + req := NewCall17Request(functionName).Args(args) + return conn.DoAsync(req) } // EvalAsync sends a Lua expression for evaluation and returns Future. func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { - future := conn.newFuture(EvalRequestCode) - return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - return fillEval(enc, expr, args) - }) + req := NewEvalRequest(expr).Args(args) + return conn.DoAsync(req) } // ExecuteAsync sends a sql expression for execution and returns Future. // Since 1.6.0 func (conn *Connection) ExecuteAsync(expr string, args interface{}) *Future { - future := conn.newFuture(ExecuteRequestCode) - return conn.sendFuture(future, func(enc *msgpack.Encoder) error { - return fillExecute(enc, expr, args) - }) + req := NewExecuteRequest(expr).Args(args) + return conn.DoAsync(req) } // KeyValueBind is a type for encoding named SQL parameters @@ -575,9 +535,8 @@ func encodeSQLBind(enc *msgpack.Encoder, from interface{}) error { type Request interface { // Code returns a IPROTO code for the request. Code() int32 - // BodyFunc returns a functions that can fill an encoder with - // the request body or it returns an error if unable to create the function. - BodyFunc(resolver SchemaResolver) (func(enc *msgpack.Encoder) error, error) + // Body fills an encoder with a request body. + Body(resolver SchemaResolver, enc *msgpack.Encoder) error } type baseRequest struct { @@ -607,6 +566,27 @@ func (req *spaceIndexRequest) setIndex(index interface{}) { req.index = index } +type authRequest struct { + baseRequest + user, scramble string +} + +func newAuthRequest(user, scramble string) *authRequest { + req := new(authRequest) + req.requestCode = AuthRequestCode + req.user = user + req.scramble = scramble + return req +} + +// Body fills an encoder with the auth request body. +func (req *authRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { + return enc.Encode(map[uint32]interface{}{ + KeyUserName: req.user, + KeyTuple: []interface{}{string("chap-sha1"), string(req.scramble)}, + }) +} + // PingRequest helps you to create an execute request object for execution // by a Connection. type PingRequest struct { @@ -620,12 +600,9 @@ func NewPingRequest() *PingRequest { return req } -// BodyFunc returns a function that can create an encoded body of -// the ping request. -func (req *PingRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { - return func(enc *msgpack.Encoder) error { - return fillPing(enc) - }, nil +// Body fills an encoder with the ping request body. +func (req *PingRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { + return fillPing(enc) } // SelectRequest allows you to create a select request object for execution @@ -688,19 +665,14 @@ func (req *SelectRequest) Key(key interface{}) *SelectRequest { return req } -// BodyFunc returns a function that can create an encoded body of -// the select request. -// It returns an error if the request space or the request index cannot -// be resolved. -func (req *SelectRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { +// Body fills an encoder with the select request body. +func (req *SelectRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) if err != nil { - return nil, err + return err } - return func(enc *msgpack.Encoder) error { - return fillSelect(enc, spaceNo, indexNo, req.offset, req.limit, req.iterator, req.key) - }, nil + return fillSelect(enc, spaceNo, indexNo, req.offset, req.limit, req.iterator, req.key) } // InsertRequest helps you to create an insert request object for execution @@ -726,18 +698,14 @@ func (req *InsertRequest) Tuple(tuple interface{}) *InsertRequest { return req } -// BodyFunc returns a function that can create an encoded body of -// the insert request. -// It returns an error if the request space cannot be resolved. -func (req *InsertRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { +// Body fills an encoder with the insert request body. +func (req *InsertRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) if err != nil { - return nil, err + return err } - return func(enc *msgpack.Encoder) error { - return fillInsert(enc, spaceNo, req.tuple) - }, nil + return fillInsert(enc, spaceNo, req.tuple) } // ReplaceRequest helps you to create a replace request object for execution @@ -763,18 +731,14 @@ func (req *ReplaceRequest) Tuple(tuple interface{}) *ReplaceRequest { return req } -// BodyFunc returns a function that can create an encoded body of -// the replace request. -// It returns an error if the request space cannot be resolved. -func (req *ReplaceRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { +// Body fills an encoder with the replace request body. +func (req *ReplaceRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) if err != nil { - return nil, err + return err } - return func(enc *msgpack.Encoder) error { - return fillInsert(enc, spaceNo, req.tuple) - }, nil + return fillInsert(enc, spaceNo, req.tuple) } // DeleteRequest helps you to create a delete request object for execution @@ -807,19 +771,14 @@ func (req *DeleteRequest) Key(key interface{}) *DeleteRequest { return req } -// BodyFunc returns a function that can create an encoded body of -// the delete request. -// It returns an error if the request space or the request index cannot -// be resolved. -func (req *DeleteRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { +// Body fills an encoder with the delete request body. +func (req *DeleteRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) if err != nil { - return nil, err + return err } - return func(enc *msgpack.Encoder) error { - return fillDelete(enc, spaceNo, indexNo, req.key) - }, nil + return fillDelete(enc, spaceNo, indexNo, req.key) } // UpdateRequest helps you to create an update request object for execution @@ -827,7 +786,7 @@ func (req *DeleteRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encode type UpdateRequest struct { spaceIndexRequest key interface{} - ops []Op + ops interface{} } // NewUpdateRequest returns a new empty UpdateRequest. @@ -836,7 +795,7 @@ func NewUpdateRequest(space interface{}) *UpdateRequest { req.requestCode = UpdateRequestCode req.setSpace(space) req.key = []interface{}{} - req.ops = []Op{} + req.ops = []interface{}{} return req } @@ -863,19 +822,14 @@ func (req *UpdateRequest) Operations(ops *Operations) *UpdateRequest { return req } -// BodyFunc returns a function that can create an encoded body of -// the update request. -// It returns an error if the request space or the request index cannot -// be resolved. -func (req *UpdateRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { +// Body fills an encoder with the update request body. +func (req *UpdateRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) if err != nil { - return nil, err + return err } - return func(enc *msgpack.Encoder) error { - return fillUpdate(enc, spaceNo, indexNo, req.key, req.ops) - }, nil + return fillUpdate(enc, spaceNo, indexNo, req.key, req.ops) } // UpsertRequest helps you to create an upsert request object for execution @@ -883,7 +837,7 @@ func (req *UpdateRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encode type UpsertRequest struct { spaceRequest tuple interface{} - ops []Op + ops interface{} } // NewUpsertRequest returns a new empty UpsertRequest. @@ -892,7 +846,7 @@ func NewUpsertRequest(space interface{}) *UpsertRequest { req.requestCode = UpsertRequestCode req.setSpace(space) req.tuple = []interface{}{} - req.ops = []Op{} + req.ops = []interface{}{} return req } @@ -912,19 +866,14 @@ func (req *UpsertRequest) Operations(ops *Operations) *UpsertRequest { return req } -// BodyFunc returns a function that can create an encoded body of -// the upsert request. -// It returns an error if the request space or the request index cannot -// be resolved. -func (req *UpsertRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { +// Body fills an encoder with the upsert request body. +func (req *UpsertRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) if err != nil { - return nil, err + return err } - return func(enc *msgpack.Encoder) error { - return fillUpsert(enc, spaceNo, req.tuple, req.ops) - }, nil + return fillUpsert(enc, spaceNo, req.tuple, req.ops) } // CallRequest helps you to create a call request object for execution @@ -953,12 +902,9 @@ func (req *CallRequest) Args(args interface{}) *CallRequest { return req } -// BodyFunc returns a function that can create an encoded body of -// the call request. -func (req *CallRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { - return func(enc *msgpack.Encoder) error { - return fillCall(enc, req.function, req.args) - }, nil +// Body fills an encoder with the call request body. +func (req *CallRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { + return fillCall(enc, req.function, req.args) } // NewCall16Request returns a new empty Call16Request. It uses request code for @@ -1002,12 +948,9 @@ func (req *EvalRequest) Args(args interface{}) *EvalRequest { return req } -// BodyFunc returns a function that can create an encoded body of -// the eval request. -func (req *EvalRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { - return func(enc *msgpack.Encoder) error { - return fillEval(enc, req.expr, req.args) - }, nil +// Body fills an encoder with the eval request body. +func (req *EvalRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { + return fillEval(enc, req.expr, req.args) } // ExecuteRequest helps you to create an execute request object for execution @@ -1034,10 +977,7 @@ func (req *ExecuteRequest) Args(args interface{}) *ExecuteRequest { return req } -// BodyFunc returns a function that can create an encoded body of -// the execute request. -func (req *ExecuteRequest) BodyFunc(res SchemaResolver) (func(enc *msgpack.Encoder) error, error) { - return func(enc *msgpack.Encoder) error { - return fillExecute(enc, req.expr, req.args) - }, nil +// Body fills an encoder with the execute request body. +func (req *ExecuteRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { + return fillExecute(enc, req.expr, req.args) } diff --git a/request_test.go b/request_test.go index 84ff78ab1..f0da3f865 100644 --- a/request_test.go +++ b/request_test.go @@ -45,12 +45,15 @@ func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (spaceNo, indexN var resolver ValidSchemeResolver -func assertBodyFuncCall(t testing.TB, requests []Request, errorMsg string) { +func assertBodyCall(t testing.TB, requests []Request, errorMsg string) { t.Helper() - const errBegin = "An unexpected Request.BodyFunc() " + const errBegin = "An unexpected Request.Body() " for _, req := range requests { - _, err := req.BodyFunc(&resolver) + var reqBuf bytes.Buffer + enc := msgpack.NewEncoder(&reqBuf) + + err := req.Body(&resolver, enc) if err != nil && errorMsg != "" && err.Error() != errorMsg { t.Errorf(errBegin+"error %q expected %q", err.Error(), errorMsg) } @@ -69,14 +72,10 @@ func assertBodyEqual(t testing.TB, reference []byte, req Request) { var reqBuf bytes.Buffer reqEnc := msgpack.NewEncoder(&reqBuf) - f, err := req.BodyFunc(&resolver) + err := req.Body(&resolver, reqEnc) if err != nil { - t.Errorf("An unexpected Response.BodyFunc() error: %q", err.Error()) + t.Errorf("An unexpected Response.Body() error: %q", err.Error()) } else { - err = f(reqEnc) - if err != nil { - t.Errorf("An unexpected encode body error: %q", err.Error()) - } reqBody := reqBuf.Bytes() if !bytes.Equal(reqBody, reference) { t.Errorf("Encoded request %v != reference %v", reqBody, reference) @@ -124,7 +123,7 @@ func TestRequestsValidSpaceAndIndex(t *testing.T) { NewDeleteRequest(validSpace).Index(validIndex), } - assertBodyFuncCall(t, requests, "") + assertBodyCall(t, requests, "") } func TestRequestsInvalidSpace(t *testing.T) { @@ -140,7 +139,7 @@ func TestRequestsInvalidSpace(t *testing.T) { NewDeleteRequest(invalidSpace), } - assertBodyFuncCall(t, requests, invalidSpaceMsg) + assertBodyCall(t, requests, invalidSpaceMsg) } func TestRequestsInvalidIndex(t *testing.T) { @@ -150,7 +149,7 @@ func TestRequestsInvalidIndex(t *testing.T) { NewDeleteRequest(validSpace).Index(invalidIndex), } - assertBodyFuncCall(t, requests, invalidIndexMsg) + assertBodyCall(t, requests, invalidIndexMsg) } func TestRequestsCodes(t *testing.T) { From 2c13fdff0b13e9d81567d3abe656560844a1771a Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 2 Feb 2022 18:57:45 +0300 Subject: [PATCH 288/605] decimal: add support decimal type in msgpack This patch provides decimal support for all space operations and as function return result. Decimal type was introduced in Tarantool 2.2. See more about decimal type in [1] and [2]. According to BCD encoding/decoding specification sign is encoded by letters: '0x0a', '0x0c', '0x0e', '0x0f' stands for plus, and '0x0b' and '0x0d' for minus. Tarantool always uses '0x0c' for plus and '0x0d' for minus. Implementation in Golang follows the same rule and in all test samples sign encoded by '0x0d' and '0x0c' for simplification. Because 'c' used by Tarantool. To use decimal with github.com/shopspring/decimal in msgpack, import tarantool/decimal submodule. 1. https://www.tarantool.io/en/doc/latest/book/box/data_model/ 2. https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type 3. https://github.com/douglascrockford/DEC64/blob/663f562a5f0621021b98bfdd4693571993316174/dec64_test.c#L62-L104 4. https://github.com/shopspring/decimal/blob/v1.3.1/decimal_test.go#L27-L64 5. https://github.com/tarantool/tarantool/blob/60fe9d14c1c7896aa7d961e4b68649eddb4d2d6c/test/unit/decimal.c#L154-L171 Lua snippet for encoding number to MsgPack representation: local decimal = require('decimal') local msgpack = require('msgpack') local function mp_encode_dec(num) local dec = msgpack.encode(decimal.new(num)) return dec:gsub('.', function (c) return string.format('%02x', string.byte(c)) end) end print(mp_encode_dec(-12.34)) -- 0xd6010201234d Follows up https://github.com/tarantool/tarantool/issues/692 Part of #96 Co-authored-by: Oleg Jukovec --- CHANGELOG.md | 1 + Makefile | 8 +- decimal/bcd.go | 257 ++++++++++++++++ decimal/config.lua | 41 +++ decimal/decimal.go | 105 +++++++ decimal/decimal_test.go | 644 ++++++++++++++++++++++++++++++++++++++++ decimal/example_test.go | 55 ++++ decimal/export_test.go | 17 ++ go.mod | 4 + go.sum | 29 ++ 10 files changed, 1160 insertions(+), 1 deletion(-) create mode 100644 decimal/bcd.go create mode 100644 decimal/config.lua create mode 100644 decimal/decimal.go create mode 100644 decimal/decimal_test.go create mode 100644 decimal/example_test.go create mode 100644 decimal/export_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6664a1cd4..2e4c72d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - SSL support (#155) - IPROTO_PUSH messages support (#67) - Public API with request object types (#126) +- Support decimal type in msgpack (#96) ### Changed diff --git a/Makefile b/Makefile index 06aa01189..c9daf85ae 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,12 @@ test-connection-pool: go clean -testcache go test -tags "$(TAGS)" ./connection_pool/ -v -p 1 +.PHONY: test-decimal +test-decimal: + @echo "Running tests in decimal package" + go clean -testcache + go test -tags "$(TAGS)" ./decimal/ -v -p 1 + .PHONY: test-multi test-multi: @echo "Running tests in multiconnection package" @@ -75,7 +81,7 @@ test-main: coverage: go clean -testcache go get golang.org/x/tools/cmd/cover - go test -tags "$(TAGS)" ./... -v -p 1 -covermode=atomic -coverprofile=$(COVERAGE_FILE) -coverpkg=./... + go test -tags "$(TAGS)" ./... -v -p 1 -covermode=atomic -coverprofile=$(COVERAGE_FILE) go tool cover -func=$(COVERAGE_FILE) .PHONY: coveralls diff --git a/decimal/bcd.go b/decimal/bcd.go new file mode 100644 index 000000000..4cc57aa45 --- /dev/null +++ b/decimal/bcd.go @@ -0,0 +1,257 @@ +// Package decimal implements methods to encode and decode BCD. +// +// BCD (Binary-Coded Decimal) is a sequence of bytes representing decimal +// digits of the encoded number (each byte has two decimal digits each encoded +// using 4-bit nibbles), so byte >> 4 is the first digit and byte & 0x0f is the +// second digit. The leftmost digit in the array is the most significant. The +// rightmost digit in the array is the least significant. +// +// The first byte of the BCD array contains the first digit of the number, +// represented as follows: +// +// | 4 bits | 4 bits | +// = 0x = the 1st digit +// +// (The first nibble contains 0 if the decimal number has an even number of +// digits). The last byte of the BCD array contains the last digit of the +// number and the final nibble, represented as follows: +// +// | 4 bits | 4 bits | +// = the last digit = nibble +// +// The final nibble represents the number's sign: 0x0a, 0x0c, 0x0e, 0x0f stand +// for plus, 0x0b and 0x0d stand for minus. +// +// Examples: +// +// The decimal -12.34 will be encoded as 0xd6, 0x01, 0x02, 0x01, 0x23, 0x4d: +// +// | MP_EXT (fixext 4) | MP_DECIMAL | scale | 1 | 2,3 | 4 (minus) | +// | 0xd6 | 0x01 | 0x02 | 0x01 | 0x23 | 0x4d | +// +// The decimal 0.000000000000000000000000000000000010 will be encoded as +// 0xc7, 0x03, 0x01, 0x24, 0x01, 0x0c: +// +// | MP_EXT (ext 8) | length | MP_DECIMAL | scale | 1 | 0 (plus) | +// | 0xc7 | 0x03 | 0x01 | 0x24 | 0x01 | 0x0c | +// +// See also: +// +// * MessagePack extensions https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/ +// +// * An implementation in C language https://github.com/tarantool/decNumber/blob/master/decPacked.c +package decimal + +import ( + "fmt" + "strings" +) + +const ( + bytePlus = byte(0x0c) + byteMinus = byte(0x0d) +) + +var isNegative = [256]bool{ + 0x0a: false, + 0x0b: true, + 0x0c: false, + 0x0d: true, + 0x0e: false, + 0x0f: false, +} + +// Calculate a number of digits in a buffer with decimal number. +// +// Plus, minus, point and leading zeroes do not count. +// Contains a quirk for a zero - returns 1. +// +// Examples (see more examples in tests): +// +// - 0.0000000000000001 - 1 digit +// +// - 00012.34 - 4 digits +// +// - 0.340 - 3 digits +// +// - 0 - 1 digit +func getNumberLength(buf string) int { + if len(buf) == 0 { + return 0 + } + n := 0 + for _, ch := range []byte(buf) { + if ch >= '1' && ch <= '9' { + n += 1 + } else if ch == '0' && n != 0 { + n += 1 + } + } + + // Fix a case with a single 0. + if n == 0 { + n = 1 + } + + return n +} + +// encodeStringToBCD converts a string buffer to BCD Packed Decimal. +// +// The number is converted to a BCD packed decimal byte array, right aligned in +// the BCD array, whose length is indicated by the second parameter. The final +// 4-bit nibble in the array will be a sign nibble, 0x0c for "+" and 0x0d for +// "-". Unused bytes and nibbles to the left of the number are set to 0. scale +// is set to the scale of the number (this is the exponent, negated). +func encodeStringToBCD(buf string) ([]byte, error) { + if len(buf) == 0 { + return nil, fmt.Errorf("Length of number is zero") + } + signByte := bytePlus // By default number is positive. + if buf[0] == '-' { + signByte = byteMinus + } + + // The first nibble should contain 0, if the decimal number has an even + // number of digits. Therefore highNibble is false when decimal number + // is even. + highNibble := true + l := GetNumberLength(buf) + if l%2 == 0 { + highNibble = false + } + scale := 0 // By default decimal number is integer. + var byteBuf []byte + for i, ch := range []byte(buf) { + // Skip leading zeroes. + if (len(byteBuf) == 0) && ch == '0' { + continue + } + if (i == 0) && (ch == '-' || ch == '+') { + continue + } + // Calculate a number of digits after the decimal point. + if ch == '.' { + if scale != 0 { + return nil, fmt.Errorf("Number contains more than one point") + } + scale = len(buf) - i - 1 + continue + } + + if ch < '0' || ch > '9' { + return nil, fmt.Errorf("Failed to convert symbol '%c' to a digit", ch) + } + digit := byte(ch - '0') + if highNibble { + // Add a digit to a high nibble. + digit = digit << 4 + byteBuf = append(byteBuf, digit) + highNibble = false + } else { + if len(byteBuf) == 0 { + byteBuf = make([]byte, 1) + } + // Add a digit to a low nibble. + lowByteIdx := len(byteBuf) - 1 + byteBuf[lowByteIdx] = byteBuf[lowByteIdx] | digit + highNibble = true + } + } + if len(byteBuf) == 0 { + // a special case: -0 + signByte = bytePlus + } + if highNibble { + // Put a sign to a high nibble. + byteBuf = append(byteBuf, signByte) + } else { + // Put a sign to a low nibble. + lowByteIdx := len(byteBuf) - 1 + byteBuf[lowByteIdx] = byteBuf[lowByteIdx] | signByte + } + byteBuf = append([]byte{byte(scale)}, byteBuf...) + + return byteBuf, nil +} + +// decodeStringFromBCD converts a BCD Packed Decimal to a string buffer. +// +// The BCD packed decimal byte array, together with an associated scale, is +// converted to a string. The BCD array is assumed full of digits, and must be +// ended by a 4-bit sign nibble in the least significant four bits of the final +// byte. The scale is used (negated) as the exponent of the decimal number. +// Note that zeroes may have a sign and/or a scale. +func decodeStringFromBCD(bcdBuf []byte) (string, error) { + // Index of a byte with scale. + const scaleIdx = 0 + scale := int(bcdBuf[scaleIdx]) + + // Get a BCD buffer without scale. + bcdBuf = bcdBuf[scaleIdx+1:] + bufLen := len(bcdBuf) + + // Every nibble contains a digit, and the last low nibble contains a + // sign. + ndigits := bufLen*2 - 1 + + // The first nibble contains 0 if the decimal number has an even number of + // digits. Decrease a number of digits if so. + if bcdBuf[0]&0xf0 == 0 { + ndigits -= 1 + } + + // Reserve bytes for dot and sign. + numLen := ndigits + 2 + // Reserve bytes for zeroes. + if scale >= ndigits { + numLen += scale - ndigits + } + + var bld strings.Builder + bld.Grow(numLen) + + // Add a sign, it is encoded in a low nibble of a last byte. + lastByte := bcdBuf[bufLen-1] + sign := lastByte & 0x0f + if isNegative[sign] { + bld.WriteByte('-') + } + + // Add missing zeroes to the left side when scale is bigger than a + // number of digits and a single missed zero to the right side when + // equal. + if scale > ndigits { + bld.WriteByte('0') + bld.WriteByte('.') + for diff := scale - ndigits; diff > 0; diff-- { + bld.WriteByte('0') + } + } else if scale == ndigits { + bld.WriteByte('0') + } + + const MaxDigit = 0x09 + // Builds a buffer with symbols of decimal number (digits, dot and sign). + processNibble := func(nibble byte) { + if nibble <= MaxDigit { + if ndigits == scale { + bld.WriteByte('.') + } + bld.WriteByte(nibble + '0') + ndigits-- + } + } + + for i, bcdByte := range bcdBuf { + highNibble := bcdByte >> 4 + lowNibble := bcdByte & 0x0f + // Skip a first high nibble as no digit there. + if i != 0 || highNibble != 0 { + processNibble(highNibble) + } + processNibble(lowNibble) + } + + return bld.String(), nil +} diff --git a/decimal/config.lua b/decimal/config.lua new file mode 100644 index 000000000..58b038958 --- /dev/null +++ b/decimal/config.lua @@ -0,0 +1,41 @@ +local decimal = require('decimal') +local msgpack = require('msgpack') + +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +box.cfg{ + work_dir = os.getenv("TEST_TNT_WORK_DIR"), +} + +box.schema.user.create('test', { password = 'test' , if_not_exists = true }) +box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) + +local decimal_msgpack_supported = pcall(msgpack.encode, decimal.new(1)) +if not decimal_msgpack_supported then + error('Decimal unsupported, use Tarantool 2.2 or newer') +end + +local s = box.schema.space.create('testDecimal', { + id = 524, + if_not_exists = true, +}) +s:create_index('primary', { + type = 'TREE', + parts = { + { + field = 1, + type = 'decimal', + }, + }, + if_not_exists = true +}) +s:truncate() + +box.schema.user.grant('test', 'read,write', 'space', 'testDecimal', { if_not_exists = true }) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} + +require('console').start() diff --git a/decimal/decimal.go b/decimal/decimal.go new file mode 100644 index 000000000..e6513af29 --- /dev/null +++ b/decimal/decimal.go @@ -0,0 +1,105 @@ +// Package decimal with support of Tarantool's decimal data type. +// +// Decimal data type supported in Tarantool since 2.2. +// +// Since: 1.7.0 +// +// See also: +// +// * Tarantool MessagePack extensions https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type +// +// * Tarantool data model https://www.tarantool.io/en/doc/latest/book/box/data_model/ +// +// * Tarantool issue for support decimal type https://github.com/tarantool/tarantool/issues/692 +// +// * Tarantool module decimal https://www.tarantool.io/en/doc/latest/reference/reference_lua/decimal/ +package decimal + +import ( + "fmt" + + "github.com/shopspring/decimal" + "gopkg.in/vmihailenco/msgpack.v2" +) + +// Decimal numbers have 38 digits of precision, that is, the total +// number of digits before and after the decimal point can be 38. +// A decimal operation will fail if overflow happens (when a number is +// greater than 10^38 - 1 or less than -10^38 - 1). +// +// See also: +// +// * Tarantool module decimal https://www.tarantool.io/en/doc/latest/reference/reference_lua/decimal/ + +const ( + // Decimal external type. + decimalExtID = 1 + decimalPrecision = 38 +) + +type DecNumber struct { + decimal.Decimal +} + +// NewDecNumber creates a new DecNumber from a decimal.Decimal. +func NewDecNumber(decimal decimal.Decimal) *DecNumber { + return &DecNumber{Decimal: decimal} +} + +// NewDecNumberFromString creates a new DecNumber from a string. +func NewDecNumberFromString(src string) (result *DecNumber, err error) { + dec, err := decimal.NewFromString(src) + if err != nil { + return + } + result = NewDecNumber(dec) + return +} + +var _ msgpack.Marshaler = (*DecNumber)(nil) +var _ msgpack.Unmarshaler = (*DecNumber)(nil) + +func (decNum *DecNumber) MarshalMsgpack() ([]byte, error) { + one := decimal.NewFromInt(1) + maxSupportedDecimal := decimal.New(1, DecimalPrecision).Sub(one) // 10^DecimalPrecision - 1 + minSupportedDecimal := maxSupportedDecimal.Neg().Sub(one) // -10^DecimalPrecision - 1 + if decNum.GreaterThan(maxSupportedDecimal) { + return nil, fmt.Errorf("msgpack: decimal number is bigger than maximum supported number (10^%d - 1)", DecimalPrecision) + } + if decNum.LessThan(minSupportedDecimal) { + return nil, fmt.Errorf("msgpack: decimal number is lesser than minimum supported number (-10^%d - 1)", DecimalPrecision) + } + + strBuf := decNum.String() + bcdBuf, err := encodeStringToBCD(strBuf) + if err != nil { + return nil, fmt.Errorf("msgpack: can't encode string (%s) to a BCD buffer: %w", strBuf, err) + } + return bcdBuf, nil +} + +// Decimal values can be encoded to fixext MessagePack, where buffer +// has a fixed length encoded by first byte, and ext MessagePack, where +// buffer length is not fixed and encoded by a number in a separate +// field: +// +// +--------+-------------------+------------+===============+ +// | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal | +// +--------+-------------------+------------+===============+ +func (decNum *DecNumber) UnmarshalMsgpack(b []byte) error { + digits, err := decodeStringFromBCD(b) + if err != nil { + return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", b, err) + } + dec, err := decimal.NewFromString(digits) + *decNum = *NewDecNumber(dec) + if err != nil { + return fmt.Errorf("msgpack: can't encode string (%s) to a decimal number: %w", digits, err) + } + + return nil +} + +func init() { + msgpack.RegisterExt(decimalExtID, &DecNumber{}) +} diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go new file mode 100644 index 000000000..68bf299e0 --- /dev/null +++ b/decimal/decimal_test.go @@ -0,0 +1,644 @@ +package decimal_test + +import ( + "encoding/hex" + "fmt" + "log" + "os" + "reflect" + "testing" + "time" + + "github.com/shopspring/decimal" + . "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/decimal" + "github.com/tarantool/go-tarantool/test_helpers" + "gopkg.in/vmihailenco/msgpack.v2" +) + +var isDecimalSupported = false + +var server = "127.0.0.1:3013" +var opts = Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +func skipIfDecimalUnsupported(t *testing.T) { + t.Helper() + + if isDecimalSupported == false { + t.Skip("Skipping test for Tarantool without datetime support in msgpack") + } +} + +var space = "testDecimal" +var index = "primary" + +type TupleDecimal struct { + number DecNumber +} + +func (t *TupleDecimal) EncodeMsgpack(e *msgpack.Encoder) error { + if err := e.EncodeSliceLen(1); err != nil { + return err + } + return e.EncodeValue(reflect.ValueOf(&t.number)) +} + +func (t *TupleDecimal) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 1 { + return fmt.Errorf("Array length doesn't match: %d", l) + } + + res, err := d.DecodeInterface() + if err != nil { + return err + } + t.number = res.(DecNumber) + + return nil +} + +var benchmarkSamples = []struct { + numString string + mpBuf string + fixExt bool +}{ + {"0.7", "d501017c", true}, + {"0.3", "d501013c", true}, + {"0.00000000000000000000000000000000000001", "d501261c", true}, + {"0.00000000000000000000000000000000000009", "d501269c", true}, + {"-18.34", "d6010201834d", true}, + {"-108.123456789", "d701090108123456789d", true}, + {"-11111111111111111111111111111111111111", "c7150100011111111111111111111111111111111111111d", false}, +} + +var correctnessSamples = []struct { + numString string + mpBuf string + fixExt bool +}{ + {"100", "c7030100100c", false}, + {"0.1", "d501011c", true}, + {"-0.1", "d501011d", true}, + {"0.0000000000000000000000000000000000001", "d501251c", true}, + {"-0.0000000000000000000000000000000000001", "d501251d", true}, + {"0.00000000000000000000000000000000000001", "d501261c", true}, + {"-0.00000000000000000000000000000000000001", "d501261d", true}, + {"1", "d501001c", true}, + {"-1", "d501001d", true}, + {"0", "d501000c", true}, + {"-0", "d501000c", true}, + {"0.01", "d501021c", true}, + {"0.001", "d501031c", true}, + {"99999999999999999999999999999999999999", "c7150100099999999999999999999999999999999999999c", false}, + {"-99999999999999999999999999999999999999", "c7150100099999999999999999999999999999999999999d", false}, + {"-12.34", "d6010201234d", true}, + {"12.34", "d6010201234c", true}, + {"1.4", "c7030101014c", false}, + {"2.718281828459045", "c70a010f02718281828459045c", false}, + {"-2.718281828459045", "c70a010f02718281828459045d", false}, + {"3.141592653589793", "c70a010f03141592653589793c", false}, + {"-3.141592653589793", "c70a010f03141592653589793d", false}, + {"1234567891234567890.0987654321987654321", "c7150113012345678912345678900987654321987654321c", false}, + {"-1234567891234567890.0987654321987654321", "c7150113012345678912345678900987654321987654321d", false}, +} + +// There is a difference between encoding result from a raw string and from +// decimal.Decimal. It's expected because decimal.Decimal simplifies decimals: +// 0.00010000 -> 0.0001 + +var rawSamples = []struct { + numString string + mpBuf string + fixExt bool +}{ + {"0.000000000000000000000000000000000010", "c7030124010c", false}, + {"0.010", "c7030103010c", false}, + {"123.456789000000000", "c70b010f0123456789000000000c", false}, +} + +var decimalSamples = []struct { + numString string + mpBuf string + fixExt bool +}{ + {"0.000000000000000000000000000000000010", "d501231c", true}, + {"0.010", "d501021c", true}, + {"123.456789000000000", "c7060106123456789c", false}, +} + +func TestMPEncodeDecode(t *testing.T) { + for _, testcase := range benchmarkSamples { + t.Run(testcase.numString, func(t *testing.T) { + decNum, err := NewDecNumberFromString(testcase.numString) + if err != nil { + t.Fatal(err) + } + var buf []byte + tuple := TupleDecimal{number: *decNum} + if buf, err = msgpack.Marshal(&tuple); err != nil { + t.Fatalf("Failed to encode decimal number '%s' to a MessagePack buffer: %s", testcase.numString, err) + } + var v TupleDecimal + if err = msgpack.Unmarshal(buf, &v); err != nil { + t.Fatalf("Failed to decode MessagePack buffer '%x' to a decimal number: %s", buf, err) + } + if !decNum.Equal(v.number.Decimal) { + fmt.Println(decNum) + fmt.Println(v.number) + t.Fatal("Decimal numbers are not equal") + } + }) + } +} + +var lengthSamples = []struct { + numString string + length int +}{ + {"0.010", 2}, + {"0.01", 1}, + {"-0.1", 1}, + {"0.1", 1}, + {"0", 1}, + {"00.1", 1}, + {"100", 3}, + {"0100", 3}, + {"+1", 1}, + {"-1", 1}, + {"1", 1}, + {"-12.34", 4}, + {"123.456789000000000", 18}, +} + +func TestGetNumberLength(t *testing.T) { + for _, testcase := range lengthSamples { + t.Run(testcase.numString, func(t *testing.T) { + l := GetNumberLength(testcase.numString) + if l != testcase.length { + t.Fatalf("Length is wrong: correct %d, incorrect %d", testcase.length, l) + } + }) + } + + if l := GetNumberLength(""); l != 0 { + t.Fatalf("Length is wrong: correct 0, incorrect %d", l) + } + + if l := GetNumberLength("0"); l != 1 { + t.Fatalf("Length is wrong: correct 0, incorrect %d", l) + } + + if l := GetNumberLength("10"); l != 2 { + t.Fatalf("Length is wrong: correct 0, incorrect %d", l) + } +} + +func TestEncodeStringToBCDIncorrectNumber(t *testing.T) { + referenceErrMsg := "Number contains more than one point" + var numString = "0.1.0" + buf, err := EncodeStringToBCD(numString) + if err == nil { + t.Fatalf("no error on encoding a string with incorrect number") + } + if buf != nil { + t.Fatalf("buf is not nil on encoding of a string with double points") + } + if err.Error() != referenceErrMsg { + t.Fatalf("wrong error message on encoding of a string double points") + } + + referenceErrMsg = "Length of number is zero" + numString = "" + buf, err = EncodeStringToBCD(numString) + if err == nil { + t.Fatalf("no error on encoding of an empty string") + } + if buf != nil { + t.Fatalf("buf is not nil on encoding of an empty string") + } + if err.Error() != referenceErrMsg { + t.Fatalf("wrong error message on encoding of an empty string") + } + + referenceErrMsg = "Failed to convert symbol 'a' to a digit" + numString = "0.1a" + buf, err = EncodeStringToBCD(numString) + if err == nil { + t.Fatalf("no error on encoding of a string number with non-digit symbol") + } + if buf != nil { + t.Fatalf("buf is not nil on encoding of a string number with non-digit symbol") + } + if err.Error() != referenceErrMsg { + t.Fatalf("wrong error message on encoding of a string number with non-digit symbol") + } +} + +func TestEncodeMaxNumber(t *testing.T) { + referenceErrMsg := "msgpack: decimal number is bigger than maximum supported number (10^38 - 1)" + decNum := decimal.New(1, DecimalPrecision) // // 10^DecimalPrecision + tuple := TupleDecimal{number: *NewDecNumber(decNum)} + _, err := msgpack.Marshal(&tuple) + if err == nil { + t.Fatalf("It is possible to encode a number unsupported by Tarantool") + } + if err.Error() != referenceErrMsg { + t.Fatalf("Incorrect error message on attempt to encode number unsupported by Tarantool") + } +} + +func TestEncodeMinNumber(t *testing.T) { + referenceErrMsg := "msgpack: decimal number is lesser than minimum supported number (-10^38 - 1)" + two := decimal.NewFromInt(2) + decNum := decimal.New(1, DecimalPrecision).Neg().Sub(two) // -10^DecimalPrecision - 2 + tuple := TupleDecimal{number: *NewDecNumber(decNum)} + _, err := msgpack.Marshal(&tuple) + if err == nil { + t.Fatalf("It is possible to encode a number unsupported by Tarantool") + } + if err.Error() != referenceErrMsg { + fmt.Println("Actual message: ", err.Error()) + fmt.Println("Expected message: ", referenceErrMsg) + t.Fatalf("Incorrect error message on attempt to encode number unsupported by Tarantool") + } +} + +func benchmarkMPEncodeDecode(b *testing.B, src decimal.Decimal, dst interface{}) { + b.ResetTimer() + + var v TupleDecimal + var buf []byte + var err error + for i := 0; i < b.N; i++ { + tuple := TupleDecimal{number: *NewDecNumber(src)} + if buf, err = msgpack.Marshal(&tuple); err != nil { + b.Fatal(err) + } + if err = msgpack.Unmarshal(buf, &v); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkMPEncodeDecodeDecimal(b *testing.B) { + for _, testcase := range benchmarkSamples { + b.Run(testcase.numString, func(b *testing.B) { + dec, err := decimal.NewFromString(testcase.numString) + if err != nil { + b.Fatal(err) + } + benchmarkMPEncodeDecode(b, dec, &dec) + }) + } +} + +func BenchmarkMPEncodeDecimal(b *testing.B) { + for _, testcase := range benchmarkSamples { + b.Run(testcase.numString, func(b *testing.B) { + decNum, err := NewDecNumberFromString(testcase.numString) + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + msgpack.Marshal(decNum) + } + }) + } +} + +func BenchmarkMPDecodeDecimal(b *testing.B) { + for _, testcase := range benchmarkSamples { + b.Run(testcase.numString, func(b *testing.B) { + decNum, err := NewDecNumberFromString(testcase.numString) + if err != nil { + b.Fatal(err) + } + var buf []byte + if buf, err = msgpack.Marshal(decNum); err != nil { + b.Fatal(err) + } + b.ResetTimer() + var v TupleDecimal + for i := 0; i < b.N; i++ { + msgpack.Unmarshal(buf, &v) + } + + }) + } +} + +func connectWithValidation(t *testing.T) *Connection { + t.Helper() + + conn, err := Connect(server, opts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if conn == nil { + t.Fatalf("conn is nil after Connect") + } + return conn +} + +func tupleValueIsDecimal(t *testing.T, tuples []interface{}, number decimal.Decimal) { + if len(tuples) != 1 { + t.Fatalf("Response Data len (%d) != 1", len(tuples)) + } + + if tpl, ok := tuples[0].([]interface{}); !ok { + t.Fatalf("Unexpected return value body") + } else { + if len(tpl) != 1 { + t.Fatalf("Unexpected return value body (tuple len)") + } + if val, ok := tpl[0].(DecNumber); !ok || !val.Equal(number) { + t.Fatalf("Unexpected return value body (tuple 0 field)") + } + } +} + +func trimMPHeader(mpBuf []byte, fixExt bool) []byte { + mpHeaderLen := 2 + if fixExt == false { + mpHeaderLen = 3 + } + return mpBuf[mpHeaderLen:] +} + +func TestEncodeStringToBCD(t *testing.T) { + samples := append(correctnessSamples, rawSamples...) + samples = append(samples, benchmarkSamples...) + for _, testcase := range samples { + t.Run(testcase.numString, func(t *testing.T) { + buf, err := EncodeStringToBCD(testcase.numString) + if err != nil { + t.Fatalf("Failed to encode decimal '%s' to BCD: %s", testcase.numString, err) + + } + b, _ := hex.DecodeString(testcase.mpBuf) + bcdBuf := trimMPHeader(b, testcase.fixExt) + if reflect.DeepEqual(buf, bcdBuf) != true { + t.Fatalf("Failed to encode decimal '%s' to BCD: expected '%x', actual '%x'", testcase.numString, bcdBuf, buf) + } + }) + } +} + +func TestDecodeStringFromBCD(t *testing.T) { + samples := append(correctnessSamples, rawSamples...) + samples = append(samples, benchmarkSamples...) + for _, testcase := range samples { + t.Run(testcase.numString, func(t *testing.T) { + b, _ := hex.DecodeString(testcase.mpBuf) + bcdBuf := trimMPHeader(b, testcase.fixExt) + s, err := DecodeStringFromBCD(bcdBuf) + if err != nil { + t.Fatalf("Failed to decode BCD '%x' to decimal: %s", bcdBuf, err) + } + + decActual, err := decimal.NewFromString(s) + if err != nil { + t.Fatalf("Failed to encode string ('%s') to decimal", s) + } + decExpected, err := decimal.NewFromString(testcase.numString) + if err != nil { + t.Fatalf("Failed to encode string ('%s') to decimal", testcase.numString) + } + if !decExpected.Equal(decActual) { + t.Fatalf("Decoded decimal from BCD ('%x') is incorrect: expected '%s', actual '%s'", bcdBuf, testcase.numString, s) + } + }) + } +} + +func TestMPEncode(t *testing.T) { + samples := append(correctnessSamples, decimalSamples...) + samples = append(samples, benchmarkSamples...) + for _, testcase := range samples { + t.Run(testcase.numString, func(t *testing.T) { + dec, err := NewDecNumberFromString(testcase.numString) + if err != nil { + t.Fatalf("NewDecNumberFromString() failed: %s", err.Error()) + } + buf, err := msgpack.Marshal(dec) + if err != nil { + t.Fatalf("Marshalling failed: %s", err.Error()) + } + refBuf, _ := hex.DecodeString(testcase.mpBuf) + if reflect.DeepEqual(buf, refBuf) != true { + t.Fatalf("Failed to encode decimal '%s', actual %x, expected %x", + testcase.numString, + buf, + refBuf) + } + }) + } +} + +func TestMPDecode(t *testing.T) { + samples := append(correctnessSamples, decimalSamples...) + samples = append(samples, benchmarkSamples...) + for _, testcase := range samples { + t.Run(testcase.numString, func(t *testing.T) { + mpBuf, err := hex.DecodeString(testcase.mpBuf) + if err != nil { + t.Fatalf("hex.DecodeString() failed: %s", err) + } + var v interface{} + err = msgpack.Unmarshal(mpBuf, &v) + if err != nil { + t.Fatalf("Unmarshalling failed: %s", err.Error()) + } + decActual := v.(DecNumber) + + decExpected, err := decimal.NewFromString(testcase.numString) + if err != nil { + t.Fatalf("decimal.NewFromString() failed: %s", err.Error()) + } + if !decExpected.Equal(decActual.Decimal) { + t.Fatalf("Decoded decimal ('%s') is incorrect", testcase.mpBuf) + } + }) + } +} + +func BenchmarkEncodeStringToBCD(b *testing.B) { + for _, testcase := range benchmarkSamples { + b.Run(testcase.numString, func(b *testing.B) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + EncodeStringToBCD(testcase.numString) + } + }) + } +} + +func BenchmarkDecodeStringFromBCD(b *testing.B) { + for _, testcase := range benchmarkSamples { + b.Run(testcase.numString, func(b *testing.B) { + buf, _ := hex.DecodeString(testcase.mpBuf) + bcdBuf := trimMPHeader(buf, testcase.fixExt) + b.ResetTimer() + for n := 0; n < b.N; n++ { + DecodeStringFromBCD(bcdBuf) + } + }) + } +} + +func TestSelect(t *testing.T) { + skipIfDecimalUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + number, err := decimal.NewFromString("-12.34") + if err != nil { + t.Fatalf("Failed to prepare test decimal: %s", err) + } + + resp, err := conn.Insert(space, []interface{}{NewDecNumber(number)}) + if err != nil { + t.Fatalf("Decimal insert failed: %s", err) + } + if resp == nil { + t.Fatalf("Response is nil after Replace") + } + tupleValueIsDecimal(t, resp.Data, number) + + var offset uint32 = 0 + var limit uint32 = 1 + resp, err = conn.Select(space, index, offset, limit, IterEq, []interface{}{NewDecNumber(number)}) + if err != nil { + t.Fatalf("Decimal select failed: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + tupleValueIsDecimal(t, resp.Data, number) + + resp, err = conn.Delete(space, index, []interface{}{NewDecNumber(number)}) + if err != nil { + t.Fatalf("Decimal delete failed: %s", err) + } + tupleValueIsDecimal(t, resp.Data, number) +} + +func assertInsert(t *testing.T, conn *Connection, numString string) { + number, err := decimal.NewFromString(numString) + if err != nil { + t.Fatalf("Failed to prepare test decimal: %s", err) + } + + resp, err := conn.Insert(space, []interface{}{NewDecNumber(number)}) + if err != nil { + t.Fatalf("Decimal insert failed: %s", err) + } + if resp == nil { + t.Fatalf("Response is nil after Replace") + } + tupleValueIsDecimal(t, resp.Data, number) + + resp, err = conn.Delete(space, index, []interface{}{NewDecNumber(number)}) + if err != nil { + t.Fatalf("Decimal delete failed: %s", err) + } + tupleValueIsDecimal(t, resp.Data, number) +} + +func TestInsert(t *testing.T) { + skipIfDecimalUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + samples := append(correctnessSamples, benchmarkSamples...) + for _, testcase := range samples { + t.Run(testcase.numString, func(t *testing.T) { + assertInsert(t, conn, testcase.numString) + }) + } +} + +func TestReplace(t *testing.T) { + skipIfDecimalUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + number, err := decimal.NewFromString("-12.34") + if err != nil { + t.Fatalf("Failed to prepare test decimal: %s", err) + } + + respRep, errRep := conn.Replace(space, []interface{}{NewDecNumber(number)}) + if errRep != nil { + t.Fatalf("Decimal replace failed: %s", errRep) + } + if respRep == nil { + t.Fatalf("Response is nil after Replace") + } + tupleValueIsDecimal(t, respRep.Data, number) + + respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{NewDecNumber(number)}) + if errSel != nil { + t.Fatalf("Decimal select failed: %s", errSel) + } + if respSel == nil { + t.Fatalf("Response is nil after Select") + } + tupleValueIsDecimal(t, respSel.Data, number) +} + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + isLess, err := test_helpers.IsTarantoolVersionLess(2, 2, 0) + if err != nil { + log.Fatalf("Failed to extract Tarantool version: %s", err) + } + + if isLess { + log.Println("Skipping decimal tests...") + isDecimalSupported = false + return m.Run() + } else { + isDecimalSupported = true + } + + instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: server, + WorkDir: "work_dir", + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(instance) + + if err != nil { + log.Fatalf("Failed to prepare test Tarantool: %s", err) + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/decimal/example_test.go b/decimal/example_test.go new file mode 100644 index 000000000..f509b2492 --- /dev/null +++ b/decimal/example_test.go @@ -0,0 +1,55 @@ +// Run Tarantool instance before example execution: +// +// Terminal 1: +// $ cd decimal +// $ TEST_TNT_LISTEN=3013 TEST_TNT_WORK_DIR=$(mktemp -d -t 'tarantool.XXX') tarantool config.lua +// +// Terminal 2: +// $ go test -v example_test.go +package decimal_test + +import ( + "log" + "time" + + "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/decimal" +) + +// To enable support of decimal in msgpack with +// https://github.com/shopspring/decimal, +// import tarantool/decimal submodule. +func Example() { + server := "127.0.0.1:3013" + opts := tarantool.Opts{ + Timeout: 500 * time.Millisecond, + Reconnect: 1 * time.Second, + MaxReconnects: 3, + User: "test", + Pass: "test", + } + client, err := tarantool.Connect(server, opts) + if err != nil { + log.Fatalf("Failed to connect: %s", err.Error()) + } + + spaceNo := uint32(524) + + number, err := NewDecNumberFromString("-22.804") + if err != nil { + log.Fatalf("Failed to prepare test decimal: %s", err) + } + + resp, err := client.Replace(spaceNo, []interface{}{number}) + if err != nil { + log.Fatalf("Decimal replace failed: %s", err) + } + if resp == nil { + log.Fatalf("Response is nil after Replace") + } + + log.Println("Decimal tuple replace") + log.Println("Error", err) + log.Println("Code", resp.Code) + log.Println("Data", resp.Data) +} diff --git a/decimal/export_test.go b/decimal/export_test.go new file mode 100644 index 000000000..c43a812c6 --- /dev/null +++ b/decimal/export_test.go @@ -0,0 +1,17 @@ +package decimal + +func EncodeStringToBCD(buf string) ([]byte, error) { + return encodeStringToBCD(buf) +} + +func DecodeStringFromBCD(bcdBuf []byte) (string, error) { + return decodeStringFromBCD(bcdBuf) +} + +func GetNumberLength(buf string) int { + return getNumberLength(buf) +} + +const ( + DecimalPrecision = decimalPrecision +) diff --git a/go.mod b/go.mod index 6dcaee974..306725105 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,15 @@ module github.com/tarantool/go-tarantool go 1.11 require ( + github.com/google/go-cmp v0.5.7 // indirect github.com/google/uuid v1.3.0 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.7.1 // indirect github.com/tarantool/go-openssl v0.0.8-0.20220419150948-be4921aa2f87 google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/vmihailenco/msgpack.v2 v2.9.2 + gotest.tools/v3 v3.2.0 // indirect ) diff --git a/go.sum b/go.sum index 1af7f9933..f9ffc6b0a 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,9 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -11,24 +14,48 @@ github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tarantool/go-openssl v0.0.8-0.20220419150948-be4921aa2f87 h1:JGzuBxNBq5saVtPUcuu5Y4+kbJON6H02//OT+RNqGts= github.com/tarantool/go-openssl v0.0.8-0.20220419150948-be4921aa2f87/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -38,3 +65,5 @@ gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizX gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= +gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= From 624b728c704abf766b09cb20db6b20142c280cbc Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 28 Apr 2022 11:52:25 +0300 Subject: [PATCH 289/605] decimal: add fuzzing test Fuzzing tests in Golang, see [1] and [2], requires Go 1.18+. However in CI we use Go 1.13 that fails on running fuzzing tests. To avoid this fuzzing test has been moved to a separate file an marked with build tag. 1. https://go.dev/doc/tutorial/fuzz 2. https://go.dev/doc/fuzz/ Closes #96 Co-authored-by: Oleg Jukovec --- .github/workflows/testing.yml | 24 ++++++++++++++++-- CONTRIBUTING.md | 5 ++++ Makefile | 6 +++++ README.md | 9 +++++-- decimal/fuzzing_test.go | 46 +++++++++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 decimal/fuzzing_test.go diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 5c3c84eae..09d6528a4 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -26,12 +26,23 @@ jobs: strategy: fail-fast: false matrix: + golang: + - 1.13 tarantool: - '1.10' - '2.8' - '2.9' - '2.x-latest' coveralls: [false] + fuzzing: [false] + include: + - tarantool: '2.x-latest' + coveralls: true + golang: 1.13 + - tarantool: '2.x-latest' + fuzzing: true + golang: 1.18 + coveralls: false steps: - name: Clone the connector @@ -52,17 +63,21 @@ jobs: - name: Setup golang for the connector and tests uses: actions/setup-go@v2 with: - go-version: 1.13 + go-version: ${{ matrix.golang }} - name: Install test dependencies run: make deps - - name: Run tests + - name: Run regression tests run: make test - name: Run tests with call_17 run: make test TAGS="go_tarantool_call_17" + - name: Run fuzzing tests + if: ${{ matrix.fuzzing }} + run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" + - name: Run tests, collect code coverage data and send to Coveralls if: ${{ matrix.coveralls }} env: @@ -96,6 +111,7 @@ jobs: - '1.10.11-0-gf0b0e7ecf-r470' - '2.8.3-21-g7d35cd2be-r470' coveralls: [false] + fuzzing: [false] ssl: [false] include: - sdk-version: '2.10.0-1-gfa775b383-r486-linux-x86_64' @@ -144,6 +160,10 @@ jobs: env: TEST_TNT_SSL: ${{matrix.ssl}} + - name: Run fuzzing tests + if: ${{ matrix.fuzzing }} + run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" + - name: Run tests, collect code coverage data and send to Coveralls if: ${{ matrix.coveralls }} env: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd9075767..e8d493e40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,11 @@ For example, for running tests in `multi`, `uuid` and `main` packages, call make test-multi test-uuid test-main ``` +To run [fuzz tests](https://go.dev/doc/tutorial/fuzz) for the main package and each subpackage: +```bash +make TAGS="go_tarantool_decimal_fuzzing" fuzzing +``` + To check if the current changes will pass the linter in CI, install golangci-lint from [sources](https://golangci-lint.run/usage/install/) and run it with next command: diff --git a/Makefile b/Makefile index c9daf85ae..1c6e0ce18 100644 --- a/Makefile +++ b/Makefile @@ -118,3 +118,9 @@ bench-diff: ${BENCH_FILES} @echo "Comparing performance between master and the current branch" @echo "'old' is a version in master branch, 'new' is a version in a current branch" benchstat ${BENCH_FILES} | grep -v pkg: + +.PHONY: fuzzing +fuzzing: + @echo "Running fuzzing tests" + go clean -testcache + go test -tags "$(TAGS)" ./... -run=^Fuzz -v -p 1 diff --git a/README.md b/README.md index 7ddf956bf..0d992a344 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,17 @@ This allows us to introduce new features without losing backward compatibility. ``` go_tarantool_ssl_disable ``` -2. to change the default `Call` behavior from `Call16` to `Call17`, you can use the build - tag: +2. To change the default `Call` behavior from `Call16` to `Call17`, you can use + the build tag: ``` go_tarantool_call_17 ``` **Note:** In future releases, `Call17` may be used as default `Call` behavior. +3. To run fuzz tests with decimals, you can use the build tag: + ``` + go_tarantool_decimal_fuzzing + ``` + **Note:** It crashes old Tarantool versions and requires Go 1.18+. ## Documentation diff --git a/decimal/fuzzing_test.go b/decimal/fuzzing_test.go new file mode 100644 index 000000000..c69a68719 --- /dev/null +++ b/decimal/fuzzing_test.go @@ -0,0 +1,46 @@ +//go:build go_tarantool_decimal_fuzzing +// +build go_tarantool_decimal_fuzzing + +package decimal_test + +import ( + "testing" + + "github.com/shopspring/decimal" + . "github.com/tarantool/go-tarantool/decimal" +) + +func strToDecimal(t *testing.T, buf string) decimal.Decimal { + decNum, err := decimal.NewFromString(buf) + if err != nil { + t.Fatal(err) + } + return decNum +} + +func FuzzEncodeDecodeBCD(f *testing.F) { + samples := append(correctnessSamples, benchmarkSamples...) + for _, testcase := range samples { + if len(testcase.numString) > 0 { + f.Add(testcase.numString) // Use f.Add to provide a seed corpus. + } + } + f.Fuzz(func(t *testing.T, orig string) { + if l := GetNumberLength(orig); l > DecimalPrecision { + t.Skip("max number length is exceeded") + } + bcdBuf, err := EncodeStringToBCD(orig) + if err != nil { + t.Skip("Only correct requests are interesting: %w", err) + } + var dec string + dec, err = DecodeStringFromBCD(bcdBuf) + if err != nil { + t.Fatalf("Failed to decode encoded value ('%s')", orig) + } + + if !strToDecimal(t, dec).Equal(strToDecimal(t, orig)) { + t.Fatal("Decimal numbers are not equal") + } + }) +} From fac3a36d0500bd2c55d08b2c4e6e2d38d6468997 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 23 Jun 2022 14:48:33 +0300 Subject: [PATCH 290/605] code health: rename type DecNumber to Decimal The new name is shorter and relates to the current code. --- decimal/decimal.go | 26 +++++++++++++------------- decimal/decimal_test.go | 38 +++++++++++++++++++------------------- decimal/example_test.go | 2 +- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/decimal/decimal.go b/decimal/decimal.go index e6513af29..66587feec 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -37,29 +37,29 @@ const ( decimalPrecision = 38 ) -type DecNumber struct { +type Decimal struct { decimal.Decimal } -// NewDecNumber creates a new DecNumber from a decimal.Decimal. -func NewDecNumber(decimal decimal.Decimal) *DecNumber { - return &DecNumber{Decimal: decimal} +// NewDecimal creates a new Decimal from a decimal.Decimal. +func NewDecimal(decimal decimal.Decimal) *Decimal { + return &Decimal{Decimal: decimal} } -// NewDecNumberFromString creates a new DecNumber from a string. -func NewDecNumberFromString(src string) (result *DecNumber, err error) { +// NewDecimalFromString creates a new Decimal from a string. +func NewDecimalFromString(src string) (result *Decimal, err error) { dec, err := decimal.NewFromString(src) if err != nil { return } - result = NewDecNumber(dec) + result = NewDecimal(dec) return } -var _ msgpack.Marshaler = (*DecNumber)(nil) -var _ msgpack.Unmarshaler = (*DecNumber)(nil) +var _ msgpack.Marshaler = (*Decimal)(nil) +var _ msgpack.Unmarshaler = (*Decimal)(nil) -func (decNum *DecNumber) MarshalMsgpack() ([]byte, error) { +func (decNum *Decimal) MarshalMsgpack() ([]byte, error) { one := decimal.NewFromInt(1) maxSupportedDecimal := decimal.New(1, DecimalPrecision).Sub(one) // 10^DecimalPrecision - 1 minSupportedDecimal := maxSupportedDecimal.Neg().Sub(one) // -10^DecimalPrecision - 1 @@ -86,13 +86,13 @@ func (decNum *DecNumber) MarshalMsgpack() ([]byte, error) { // +--------+-------------------+------------+===============+ // | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal | // +--------+-------------------+------------+===============+ -func (decNum *DecNumber) UnmarshalMsgpack(b []byte) error { +func (decNum *Decimal) UnmarshalMsgpack(b []byte) error { digits, err := decodeStringFromBCD(b) if err != nil { return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", b, err) } dec, err := decimal.NewFromString(digits) - *decNum = *NewDecNumber(dec) + *decNum = *NewDecimal(dec) if err != nil { return fmt.Errorf("msgpack: can't encode string (%s) to a decimal number: %w", digits, err) } @@ -101,5 +101,5 @@ func (decNum *DecNumber) UnmarshalMsgpack(b []byte) error { } func init() { - msgpack.RegisterExt(decimalExtID, &DecNumber{}) + msgpack.RegisterExt(decimalExtID, &Decimal{}) } diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 68bf299e0..d7d1091e6 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -37,7 +37,7 @@ var space = "testDecimal" var index = "primary" type TupleDecimal struct { - number DecNumber + number Decimal } func (t *TupleDecimal) EncodeMsgpack(e *msgpack.Encoder) error { @@ -61,7 +61,7 @@ func (t *TupleDecimal) DecodeMsgpack(d *msgpack.Decoder) error { if err != nil { return err } - t.number = res.(DecNumber) + t.number = res.(Decimal) return nil } @@ -138,7 +138,7 @@ var decimalSamples = []struct { func TestMPEncodeDecode(t *testing.T) { for _, testcase := range benchmarkSamples { t.Run(testcase.numString, func(t *testing.T) { - decNum, err := NewDecNumberFromString(testcase.numString) + decNum, err := NewDecimalFromString(testcase.numString) if err != nil { t.Fatal(err) } @@ -246,7 +246,7 @@ func TestEncodeStringToBCDIncorrectNumber(t *testing.T) { func TestEncodeMaxNumber(t *testing.T) { referenceErrMsg := "msgpack: decimal number is bigger than maximum supported number (10^38 - 1)" decNum := decimal.New(1, DecimalPrecision) // // 10^DecimalPrecision - tuple := TupleDecimal{number: *NewDecNumber(decNum)} + tuple := TupleDecimal{number: *NewDecimal(decNum)} _, err := msgpack.Marshal(&tuple) if err == nil { t.Fatalf("It is possible to encode a number unsupported by Tarantool") @@ -260,7 +260,7 @@ func TestEncodeMinNumber(t *testing.T) { referenceErrMsg := "msgpack: decimal number is lesser than minimum supported number (-10^38 - 1)" two := decimal.NewFromInt(2) decNum := decimal.New(1, DecimalPrecision).Neg().Sub(two) // -10^DecimalPrecision - 2 - tuple := TupleDecimal{number: *NewDecNumber(decNum)} + tuple := TupleDecimal{number: *NewDecimal(decNum)} _, err := msgpack.Marshal(&tuple) if err == nil { t.Fatalf("It is possible to encode a number unsupported by Tarantool") @@ -279,7 +279,7 @@ func benchmarkMPEncodeDecode(b *testing.B, src decimal.Decimal, dst interface{}) var buf []byte var err error for i := 0; i < b.N; i++ { - tuple := TupleDecimal{number: *NewDecNumber(src)} + tuple := TupleDecimal{number: *NewDecimal(src)} if buf, err = msgpack.Marshal(&tuple); err != nil { b.Fatal(err) } @@ -304,7 +304,7 @@ func BenchmarkMPEncodeDecodeDecimal(b *testing.B) { func BenchmarkMPEncodeDecimal(b *testing.B) { for _, testcase := range benchmarkSamples { b.Run(testcase.numString, func(b *testing.B) { - decNum, err := NewDecNumberFromString(testcase.numString) + decNum, err := NewDecimalFromString(testcase.numString) if err != nil { b.Fatal(err) } @@ -319,7 +319,7 @@ func BenchmarkMPEncodeDecimal(b *testing.B) { func BenchmarkMPDecodeDecimal(b *testing.B) { for _, testcase := range benchmarkSamples { b.Run(testcase.numString, func(b *testing.B) { - decNum, err := NewDecNumberFromString(testcase.numString) + decNum, err := NewDecimalFromString(testcase.numString) if err != nil { b.Fatal(err) } @@ -361,7 +361,7 @@ func tupleValueIsDecimal(t *testing.T, tuples []interface{}, number decimal.Deci if len(tpl) != 1 { t.Fatalf("Unexpected return value body (tuple len)") } - if val, ok := tpl[0].(DecNumber); !ok || !val.Equal(number) { + if val, ok := tpl[0].(Decimal); !ok || !val.Equal(number) { t.Fatalf("Unexpected return value body (tuple 0 field)") } } @@ -426,9 +426,9 @@ func TestMPEncode(t *testing.T) { samples = append(samples, benchmarkSamples...) for _, testcase := range samples { t.Run(testcase.numString, func(t *testing.T) { - dec, err := NewDecNumberFromString(testcase.numString) + dec, err := NewDecimalFromString(testcase.numString) if err != nil { - t.Fatalf("NewDecNumberFromString() failed: %s", err.Error()) + t.Fatalf("NewDecimalFromString() failed: %s", err.Error()) } buf, err := msgpack.Marshal(dec) if err != nil { @@ -459,7 +459,7 @@ func TestMPDecode(t *testing.T) { if err != nil { t.Fatalf("Unmarshalling failed: %s", err.Error()) } - decActual := v.(DecNumber) + decActual := v.(Decimal) decExpected, err := decimal.NewFromString(testcase.numString) if err != nil { @@ -507,7 +507,7 @@ func TestSelect(t *testing.T) { t.Fatalf("Failed to prepare test decimal: %s", err) } - resp, err := conn.Insert(space, []interface{}{NewDecNumber(number)}) + resp, err := conn.Insert(space, []interface{}{NewDecimal(number)}) if err != nil { t.Fatalf("Decimal insert failed: %s", err) } @@ -518,7 +518,7 @@ func TestSelect(t *testing.T) { var offset uint32 = 0 var limit uint32 = 1 - resp, err = conn.Select(space, index, offset, limit, IterEq, []interface{}{NewDecNumber(number)}) + resp, err = conn.Select(space, index, offset, limit, IterEq, []interface{}{NewDecimal(number)}) if err != nil { t.Fatalf("Decimal select failed: %s", err.Error()) } @@ -527,7 +527,7 @@ func TestSelect(t *testing.T) { } tupleValueIsDecimal(t, resp.Data, number) - resp, err = conn.Delete(space, index, []interface{}{NewDecNumber(number)}) + resp, err = conn.Delete(space, index, []interface{}{NewDecimal(number)}) if err != nil { t.Fatalf("Decimal delete failed: %s", err) } @@ -540,7 +540,7 @@ func assertInsert(t *testing.T, conn *Connection, numString string) { t.Fatalf("Failed to prepare test decimal: %s", err) } - resp, err := conn.Insert(space, []interface{}{NewDecNumber(number)}) + resp, err := conn.Insert(space, []interface{}{NewDecimal(number)}) if err != nil { t.Fatalf("Decimal insert failed: %s", err) } @@ -549,7 +549,7 @@ func assertInsert(t *testing.T, conn *Connection, numString string) { } tupleValueIsDecimal(t, resp.Data, number) - resp, err = conn.Delete(space, index, []interface{}{NewDecNumber(number)}) + resp, err = conn.Delete(space, index, []interface{}{NewDecimal(number)}) if err != nil { t.Fatalf("Decimal delete failed: %s", err) } @@ -581,7 +581,7 @@ func TestReplace(t *testing.T) { t.Fatalf("Failed to prepare test decimal: %s", err) } - respRep, errRep := conn.Replace(space, []interface{}{NewDecNumber(number)}) + respRep, errRep := conn.Replace(space, []interface{}{NewDecimal(number)}) if errRep != nil { t.Fatalf("Decimal replace failed: %s", errRep) } @@ -590,7 +590,7 @@ func TestReplace(t *testing.T) { } tupleValueIsDecimal(t, respRep.Data, number) - respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{NewDecNumber(number)}) + respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{NewDecimal(number)}) if errSel != nil { t.Fatalf("Decimal select failed: %s", errSel) } diff --git a/decimal/example_test.go b/decimal/example_test.go index f509b2492..1d335a4c3 100644 --- a/decimal/example_test.go +++ b/decimal/example_test.go @@ -35,7 +35,7 @@ func Example() { spaceNo := uint32(524) - number, err := NewDecNumberFromString("-22.804") + number, err := NewDecimalFromString("-22.804") if err != nil { log.Fatalf("Failed to prepare test decimal: %s", err) } From 7345161f14a68509a811934a11b9226c31edffc1 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Thu, 16 Jun 2022 15:48:42 +0300 Subject: [PATCH 291/605] github-ci: add Tarantool 2.10 Add Tarantool 2.10 [1] to testing matrix, it is a first release with datetime support. Tarantool 2.9 has been removed, it was never published [2]. setup-tarantool action does not support new Tarantool release policy [3], and Tarantool 2.10 is installed without action but using curl and apt. New issue to fix this has been submitted [4]. 1. https://www.tarantool.io/en/doc/latest/release/2.10.0/ 2. https://www.tarantool.io/en/doc/latest/release/calendar/ 3. https://github.com/tarantool/setup-tarantool/issues/19 4. https://github.com/tarantool/go-tarantool/issues/186 Needed for #118 --- .github/workflows/testing.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 09d6528a4..cf6ee8f3a 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -31,7 +31,7 @@ jobs: tarantool: - '1.10' - '2.8' - - '2.9' + - '2.10' - '2.x-latest' coveralls: [false] fuzzing: [false] @@ -48,8 +48,8 @@ jobs: - name: Clone the connector uses: actions/checkout@v2 - - name: Setup Tarantool ${{ matrix.tarantool }} - if: matrix.tarantool != '2.x-latest' + - name: Setup Tarantool ${{ matrix.tarantool }} (< 2.10) + if: matrix.tarantool != '2.x-latest' && matrix.tarantool != '2.10' uses: tarantool/setup-tarantool@v1 with: tarantool-version: ${{ matrix.tarantool }} @@ -60,6 +60,12 @@ jobs: curl -L https://tarantool.io/pre-release/2/installer.sh | sudo bash sudo apt install -y tarantool tarantool-dev + - name: Setup Tarantool 2.10 + if: matrix.tarantool == '2.10' + run: | + curl -L https://tarantool.io/tWsLBdI/release/2/installer.sh | bash + sudo apt install -y tarantool tarantool-dev + - name: Setup golang for the connector and tests uses: actions/setup-go@v2 with: From 61d07393abce686605aac0c5c105fd29660fcea0 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Sat, 5 Mar 2022 10:48:09 +0300 Subject: [PATCH 292/605] datetime: add datetime type in msgpack This patch provides datetime support for all space operations and as function return result. Datetime type was introduced in Tarantool 2.10. See more in issue [1]. Note that timezone's index and offset and intervals are not implemented in Tarantool, see [2] and [3]. This Lua snippet was quite useful for debugging encoding and decoding datetime in MessagePack: local msgpack = require('msgpack') local datetime = require('datetime') local dt = datetime.parse('2012-01-31T23:59:59.000000010Z') local mp_dt = msgpack.encode(dt):gsub('.', function (c) return string.format('%02x', string.byte(c)) end) print(mp_dt) -- d8047f80284f000000000a00000000000000 1. https://github.com/tarantool/tarantool/issues/5946 2. https://github.com/tarantool/go-tarantool/issues/163 3. https://github.com/tarantool/go-tarantool/issues/165 Closes #118 --- CHANGELOG.md | 1 + Makefile | 6 + datetime/config.lua | 69 +++++ datetime/datetime.go | 140 +++++++++ datetime/datetime_test.go | 578 ++++++++++++++++++++++++++++++++++++++ datetime/example_test.go | 77 +++++ decimal/decimal_test.go | 2 +- 7 files changed, 872 insertions(+), 1 deletion(-) create mode 100644 datetime/config.lua create mode 100644 datetime/datetime.go create mode 100644 datetime/datetime_test.go create mode 100644 datetime/example_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e4c72d53..bf3c3eb06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - IPROTO_PUSH messages support (#67) - Public API with request object types (#126) - Support decimal type in msgpack (#96) +- Support datetime type in msgpack (#118) ### Changed diff --git a/Makefile b/Makefile index 1c6e0ce18..7bc6411e4 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,12 @@ test-connection-pool: go clean -testcache go test -tags "$(TAGS)" ./connection_pool/ -v -p 1 +.PHONY: test-datetime +test-datetime: + @echo "Running tests in datetime package" + go clean -testcache + go test -tags "$(TAGS)" ./datetime/ -v -p 1 + .PHONY: test-decimal test-decimal: @echo "Running tests in decimal package" diff --git a/datetime/config.lua b/datetime/config.lua new file mode 100644 index 000000000..8b1ba2316 --- /dev/null +++ b/datetime/config.lua @@ -0,0 +1,69 @@ +local has_datetime, datetime = pcall(require, 'datetime') + +if not has_datetime then + error('Datetime unsupported, use Tarantool 2.10 or newer') +end + +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +box.cfg{ + work_dir = os.getenv("TEST_TNT_WORK_DIR"), +} + +box.schema.user.create('test', { password = 'test' , if_not_exists = true }) +box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) + +box.once("init", function() + local s_1 = box.schema.space.create('testDatetime_1', { + id = 524, + if_not_exists = true, + }) + s_1:create_index('primary', { + type = 'TREE', + parts = { + { field = 1, type = 'datetime' }, + }, + if_not_exists = true + }) + s_1:truncate() + + local s_3 = box.schema.space.create('testDatetime_2', { + id = 526, + if_not_exists = true, + }) + s_3:create_index('primary', { + type = 'tree', + parts = { + {1, 'uint'}, + }, + if_not_exists = true + }) + s_3:truncate() + + box.schema.func.create('call_datetime_testdata') + box.schema.user.grant('test', 'read,write', 'space', 'testDatetime_1', { if_not_exists = true }) + box.schema.user.grant('test', 'read,write', 'space', 'testDatetime_2', { if_not_exists = true }) +end) + +local function call_datetime_testdata() + local dt1 = datetime.new({ year = 1934 }) + local dt2 = datetime.new({ year = 1961 }) + local dt3 = datetime.new({ year = 1968 }) + return { + { + 5, "Go!", { + {"Klushino", dt1}, + {"Baikonur", dt2}, + {"Novoselovo", dt3}, + }, + } + } +end +rawset(_G, 'call_datetime_testdata', call_datetime_testdata) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} + +require('console').start() diff --git a/datetime/datetime.go b/datetime/datetime.go new file mode 100644 index 000000000..e861da837 --- /dev/null +++ b/datetime/datetime.go @@ -0,0 +1,140 @@ +// Package with support of Tarantool's datetime data type. +// +// Datetime data type supported in Tarantool since 2.10. +// +// Since: 1.7.0 +// +// See also: +// +// * Datetime Internals https://github.com/tarantool/tarantool/wiki/Datetime-Internals +package datetime + +import ( + "encoding/binary" + "fmt" + "time" + + "gopkg.in/vmihailenco/msgpack.v2" +) + +// Datetime MessagePack serialization schema is an MP_EXT extension, which +// creates container of 8 or 16 bytes long payload. +// +// +---------+--------+===============+-------------------------------+ +// |0xd7/0xd8|type (4)| seconds (8b) | nsec; tzoffset; tzindex; (8b) | +// +---------+--------+===============+-------------------------------+ +// +// MessagePack data encoded using fixext8 (0xd7) or fixext16 (0xd8), and may +// contain: +// +// * [required] seconds parts as full, unencoded, signed 64-bit integer, +// stored in little-endian order; +// +// * [optional] all the other fields (nsec, tzoffset, tzindex) if any of them +// were having not 0 value. They are packed naturally in little-endian order; + +// Datetime external type. Supported since Tarantool 2.10. See more details in +// issue https://github.com/tarantool/tarantool/issues/5946. +const datetime_extId = 4 + +// datetime structure keeps a number of seconds and nanoseconds since Unix Epoch. +// Time is normalized by UTC, so time-zone offset is informative only. +type datetime struct { + // Seconds since Epoch, where the epoch is the point where the time + // starts, and is platform dependent. For Unix, the epoch is January 1, + // 1970, 00:00:00 (UTC). Tarantool uses a double type, see a structure + // definition in src/lib/core/datetime.h and reasons in + // https://github.com/tarantool/tarantool/wiki/Datetime-internals#intervals-in-c + seconds int64 + // Nanoseconds, fractional part of seconds. Tarantool uses int32_t, see + // a definition in src/lib/core/datetime.h. + nsec int32 + // Timezone offset in minutes from UTC (not implemented in Tarantool, + // see gh-163). Tarantool uses a int16_t type, see a structure + // definition in src/lib/core/datetime.h. + tzOffset int16 + // Olson timezone id (not implemented in Tarantool, see gh-163). + // Tarantool uses a int16_t type, see a structure definition in + // src/lib/core/datetime.h. + tzIndex int16 +} + +// Size of datetime fields in a MessagePack value. +const ( + secondsSize = 8 + nsecSize = 4 + tzIndexSize = 2 + tzOffsetSize = 2 +) + +const maxSize = secondsSize + nsecSize + tzIndexSize + tzOffsetSize + +type Datetime struct { + time time.Time +} + +// NewDatetime returns a pointer to a new datetime.Datetime that contains a +// specified time.Time. +func NewDatetime(t time.Time) *Datetime { + dt := new(Datetime) + dt.time = t + return dt +} + +// ToTime returns a time.Time that Datetime contains. +func (dtime *Datetime) ToTime() time.Time { + return dtime.time +} + +var _ msgpack.Marshaler = (*Datetime)(nil) +var _ msgpack.Unmarshaler = (*Datetime)(nil) + +func (dtime *Datetime) MarshalMsgpack() ([]byte, error) { + tm := dtime.ToTime() + + var dt datetime + dt.seconds = tm.Unix() + dt.nsec = int32(tm.Nanosecond()) + dt.tzIndex = 0 // It is not implemented, see gh-163. + dt.tzOffset = 0 // It is not implemented, see gh-163. + + var bytesSize = secondsSize + if dt.nsec != 0 || dt.tzOffset != 0 || dt.tzIndex != 0 { + bytesSize += nsecSize + tzIndexSize + tzOffsetSize + } + + buf := make([]byte, bytesSize) + binary.LittleEndian.PutUint64(buf, uint64(dt.seconds)) + if bytesSize == maxSize { + binary.LittleEndian.PutUint32(buf[secondsSize:], uint32(dt.nsec)) + binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize:], uint16(dt.tzOffset)) + binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize+tzOffsetSize:], uint16(dt.tzIndex)) + } + + return buf, nil +} + +func (tm *Datetime) UnmarshalMsgpack(b []byte) error { + l := len(b) + if l != maxSize && l != secondsSize { + return fmt.Errorf("invalid data length: got %d, wanted %d or %d", len(b), secondsSize, maxSize) + } + + var dt datetime + sec := binary.LittleEndian.Uint64(b) + dt.seconds = int64(sec) + dt.nsec = 0 + if l == maxSize { + dt.nsec = int32(binary.LittleEndian.Uint32(b[secondsSize:])) + dt.tzOffset = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize:])) + dt.tzIndex = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize+tzOffsetSize:])) + } + tt := time.Unix(dt.seconds, int64(dt.nsec)).UTC() + *tm = *NewDatetime(tt) + + return nil +} + +func init() { + msgpack.RegisterExt(datetime_extId, &Datetime{}) +} diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go new file mode 100644 index 000000000..e1aeef23d --- /dev/null +++ b/datetime/datetime_test.go @@ -0,0 +1,578 @@ +package datetime_test + +import ( + "encoding/hex" + "fmt" + "log" + "os" + "reflect" + "testing" + "time" + + . "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/datetime" + "github.com/tarantool/go-tarantool/test_helpers" + "gopkg.in/vmihailenco/msgpack.v2" +) + +var ( + minTime = time.Unix(0, 0) + maxTime = time.Unix(1<<63-1, 999999999) +) + +var isDatetimeSupported = false + +var server = "127.0.0.1:3013" +var opts = Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +var spaceTuple1 = "testDatetime_1" +var spaceTuple2 = "testDatetime_2" +var index = "primary" + +func connectWithValidation(t *testing.T) *Connection { + t.Helper() + + conn, err := Connect(server, opts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if conn == nil { + t.Fatalf("conn is nil after Connect") + } + return conn +} + +func skipIfDatetimeUnsupported(t *testing.T) { + t.Helper() + + if isDatetimeSupported == false { + t.Skip("Skipping test for Tarantool without datetime support in msgpack") + } +} + +// Expect that first element of tuple is time.Time. Compare extracted actual +// and expected datetime values. +func assertDatetimeIsEqual(t *testing.T, tuples []interface{}, tm time.Time) { + t.Helper() + + dtIndex := 0 + if tpl, ok := tuples[dtIndex].([]interface{}); !ok { + t.Fatalf("Unexpected return value body") + } else { + if len(tpl) != 2 { + t.Fatalf("Unexpected return value body (tuple len = %d)", len(tpl)) + } + if val, ok := tpl[dtIndex].(Datetime); !ok || !val.ToTime().Equal(tm) { + t.Fatalf("Unexpected tuple %d field %v, expected %v", + dtIndex, + val.ToTime(), + tm) + } + } +} + +func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) { + dt := NewDatetime(tm) + + // Insert tuple with datetime. + _, err := conn.Insert(spaceTuple1, []interface{}{dt, "payload"}) + if err != nil { + t.Fatalf("Datetime insert failed: %s", err.Error()) + } + + // Select tuple with datetime. + var offset uint32 = 0 + var limit uint32 = 1 + resp, err := conn.Select(spaceTuple1, index, offset, limit, IterEq, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime select failed: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + assertDatetimeIsEqual(t, resp.Data, tm) + + // Delete tuple with datetime. + resp, err = conn.Delete(spaceTuple1, index, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime delete failed: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Delete") + } + assertDatetimeIsEqual(t, resp.Data, tm) +} + +var datetimeSample = []struct { + dt string + mpBuf string // MessagePack buffer. +}{ + {"2012-01-31T23:59:59.000000010Z", "d8047f80284f000000000a00000000000000"}, + {"1970-01-01T00:00:00.000000010Z", "d80400000000000000000a00000000000000"}, + {"2010-08-12T11:39:14Z", "d70462dd634c00000000"}, + {"1984-03-24T18:04:05Z", "d7041530c31a00000000"}, + {"2010-01-12T00:00:00Z", "d70480bb4b4b00000000"}, + {"1970-01-01T00:00:00Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.123456789Z", "d804000000000000000015cd5b0700000000"}, + {"1970-01-01T00:00:00.12345678Z", "d80400000000000000000ccd5b0700000000"}, + {"1970-01-01T00:00:00.1234567Z", "d8040000000000000000bccc5b0700000000"}, + {"1970-01-01T00:00:00.123456Z", "d804000000000000000000ca5b0700000000"}, + {"1970-01-01T00:00:00.12345Z", "d804000000000000000090b25b0700000000"}, + {"1970-01-01T00:00:00.1234Z", "d804000000000000000040ef5a0700000000"}, + {"1970-01-01T00:00:00.123Z", "d8040000000000000000c0d4540700000000"}, + {"1970-01-01T00:00:00.12Z", "d8040000000000000000000e270700000000"}, + {"1970-01-01T00:00:00.1Z", "d804000000000000000000e1f50500000000"}, + {"1970-01-01T00:00:00.01Z", "d80400000000000000008096980000000000"}, + {"1970-01-01T00:00:00.001Z", "d804000000000000000040420f0000000000"}, + {"1970-01-01T00:00:00.0001Z", "d8040000000000000000a086010000000000"}, + {"1970-01-01T00:00:00.00001Z", "d80400000000000000001027000000000000"}, + {"1970-01-01T00:00:00.000001Z", "d8040000000000000000e803000000000000"}, + {"1970-01-01T00:00:00.0000001Z", "d80400000000000000006400000000000000"}, + {"1970-01-01T00:00:00.00000001Z", "d80400000000000000000a00000000000000"}, + {"1970-01-01T00:00:00.000000001Z", "d80400000000000000000100000000000000"}, + {"1970-01-01T00:00:00.000000009Z", "d80400000000000000000900000000000000"}, + {"1970-01-01T00:00:00.00000009Z", "d80400000000000000005a00000000000000"}, + {"1970-01-01T00:00:00.0000009Z", "d80400000000000000008403000000000000"}, + {"1970-01-01T00:00:00.000009Z", "d80400000000000000002823000000000000"}, + {"1970-01-01T00:00:00.00009Z", "d8040000000000000000905f010000000000"}, + {"1970-01-01T00:00:00.0009Z", "d8040000000000000000a0bb0d0000000000"}, + {"1970-01-01T00:00:00.009Z", "d80400000000000000004054890000000000"}, + {"1970-01-01T00:00:00.09Z", "d8040000000000000000804a5d0500000000"}, + {"1970-01-01T00:00:00.9Z", "d804000000000000000000e9a43500000000"}, + {"1970-01-01T00:00:00.99Z", "d80400000000000000008033023b00000000"}, + {"1970-01-01T00:00:00.999Z", "d8040000000000000000c0878b3b00000000"}, + {"1970-01-01T00:00:00.9999Z", "d80400000000000000006043993b00000000"}, + {"1970-01-01T00:00:00.99999Z", "d8040000000000000000f0a29a3b00000000"}, + {"1970-01-01T00:00:00.999999Z", "d804000000000000000018c69a3b00000000"}, + {"1970-01-01T00:00:00.9999999Z", "d80400000000000000009cc99a3b00000000"}, + {"1970-01-01T00:00:00.99999999Z", "d8040000000000000000f6c99a3b00000000"}, + {"1970-01-01T00:00:00.999999999Z", "d8040000000000000000ffc99a3b00000000"}, + {"1970-01-01T00:00:00.0Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.00Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.000Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.0000Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.00000Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.000000Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.0000000Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.00000000Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.000000000Z", "d7040000000000000000"}, + {"1973-11-29T21:33:09Z", "d70415cd5b0700000000"}, + {"2013-10-28T17:51:56Z", "d7043ca46e5200000000"}, + {"9999-12-31T23:59:59Z", "d7047f41f4ff3a000000"}, +} + +func TestDatetimeInsertSelectDelete(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + for _, testcase := range datetimeSample { + t.Run(testcase.dt, func(t *testing.T) { + tm, err := time.Parse(time.RFC3339, testcase.dt) + if err != nil { + t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) + } + tupleInsertSelectDelete(t, conn, tm) + }) + } +} + +// time.Parse() could not parse formatted string with datetime where year is +// bigger than 9999. That's why testcase with maximum datetime value represented +// as a separate testcase. Testcase with minimal value added for consistency. +func TestDatetimeMax(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + tupleInsertSelectDelete(t, conn, maxTime) +} + +func TestDatetimeMin(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + tupleInsertSelectDelete(t, conn, minTime) +} + +func TestDatetimeReplace(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + tm, err := time.Parse(time.RFC3339, "2007-01-02T15:04:05Z") + if err != nil { + t.Fatalf("Time parse failed: %s", err) + } + + dt := NewDatetime(tm) + resp, err := conn.Replace(spaceTuple1, []interface{}{dt, "payload"}) + if err != nil { + t.Fatalf("Datetime replace failed: %s", err) + } + if resp == nil { + t.Fatalf("Response is nil after Replace") + } + assertDatetimeIsEqual(t, resp.Data, tm) + + resp, err = conn.Select(spaceTuple1, index, 0, 1, IterEq, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime select failed: %s", err) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + assertDatetimeIsEqual(t, resp.Data, tm) + + // Delete tuple with datetime. + _, err = conn.Delete(spaceTuple1, index, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime delete failed: %s", err.Error()) + } +} + +type Event struct { + Datetime Datetime + Location string +} + +type Tuple2 struct { + Cid uint + Orig string + Events []Event +} + +type Tuple1 struct { + Datetime Datetime +} + +func (t *Tuple1) EncodeMsgpack(e *msgpack.Encoder) error { + if err := e.EncodeSliceLen(2); err != nil { + return err + } + if err := e.Encode(&t.Datetime); err != nil { + return err + } + return nil +} + +func (t *Tuple1) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 1 { + return fmt.Errorf("array len doesn't match: %d", l) + } + err = d.Decode(&t.Datetime) + if err != nil { + return err + } + return nil +} + +func (ev *Event) EncodeMsgpack(e *msgpack.Encoder) error { + if err := e.EncodeSliceLen(2); err != nil { + return err + } + if err := e.EncodeString(ev.Location); err != nil { + return err + } + if err := e.Encode(&ev.Datetime); err != nil { + return err + } + return nil +} + +func (ev *Event) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 2 { + return fmt.Errorf("array len doesn't match: %d", l) + } + if ev.Location, err = d.DecodeString(); err != nil { + return err + } + res, err := d.DecodeInterface() + if err != nil { + return err + } + ev.Datetime = res.(Datetime) + return nil +} + +func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { + if err := e.EncodeSliceLen(3); err != nil { + return err + } + if err := e.EncodeUint(c.Cid); err != nil { + return err + } + if err := e.EncodeString(c.Orig); err != nil { + return err + } + e.Encode(c.Events) + return nil +} + +func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 3 { + return fmt.Errorf("array len doesn't match: %d", l) + } + if c.Cid, err = d.DecodeUint(); err != nil { + return err + } + if c.Orig, err = d.DecodeString(); err != nil { + return err + } + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + c.Events = make([]Event, l) + for i := 0; i < l; i++ { + d.Decode(&c.Events[i]) + } + return nil +} + +func TestCustomEncodeDecodeTuple1(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + dt1, _ := time.Parse(time.RFC3339, "2010-05-24T17:51:56.000000009Z") + dt2, _ := time.Parse(time.RFC3339, "2022-05-24T17:51:56.000000009Z") + const cid = 13 + const orig = "orig" + + tuple := Tuple2{Cid: cid, + Orig: orig, + Events: []Event{ + {*NewDatetime(dt1), "Minsk"}, + {*NewDatetime(dt2), "Moscow"}, + }, + } + resp, err := conn.Replace(spaceTuple2, &tuple) + if err != nil || resp.Code != 0 { + t.Fatalf("Failed to replace: %s", err.Error()) + } + if len(resp.Data) != 1 { + t.Fatalf("Response Body len != 1") + } + + tpl, ok := resp.Data[0].([]interface{}) + if !ok { + t.Fatalf("Unexpected body of Replace") + } + + // Delete the tuple. + _, err = conn.Delete(spaceTuple2, index, []interface{}{cid}) + if err != nil { + t.Fatalf("Datetime delete failed: %s", err.Error()) + } + + if len(tpl) != 3 { + t.Fatalf("Unexpected body of Replace (tuple len)") + } + if id, ok := tpl[0].(uint64); !ok || id != cid { + t.Fatalf("Unexpected body of Replace (%d)", cid) + } + if o, ok := tpl[1].(string); !ok || o != orig { + t.Fatalf("Unexpected body of Replace (%s)", orig) + } + + events, ok := tpl[2].([]interface{}) + if !ok { + t.Fatalf("Unable to convert 2 field to []interface{}") + } + + for i, tv := range []time.Time{dt1, dt2} { + dt := events[i].([]interface{})[1].(Datetime) + if !dt.ToTime().Equal(tv) { + t.Fatalf("%v != %v", dt.ToTime(), tv) + } + } +} + +func TestCustomDecodeFunction(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + // Call function 'call_datetime_testdata' returning a table of custom tuples. + var tuples []Tuple2 + err := conn.Call16Typed("call_datetime_testdata", []interface{}{1}, &tuples) + if err != nil { + t.Fatalf("Failed to CallTyped: %s", err.Error()) + } + + if cid := tuples[0].Cid; cid != 5 { + t.Fatalf("Wrong Cid (%d), should be 5", cid) + } + if orig := tuples[0].Orig; orig != "Go!" { + t.Fatalf("Wrong Orig (%s), should be 'Hello, there!'", orig) + } + + events := tuples[0].Events + if len(events) != 3 { + t.Fatalf("Wrong a number of Events (%d), should be 3", len(events)) + } + + locations := []string{ + "Klushino", + "Baikonur", + "Novoselovo", + } + + for i, ev := range events { + loc := ev.Location + dt := ev.Datetime + if loc != locations[i] || dt.ToTime().IsZero() { + t.Fatalf("Expected: %s non-zero time, got %s %v", + locations[i], + loc, + dt.ToTime()) + } + } +} + +func TestCustomEncodeDecodeTuple5(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + tm := time.Unix(500, 1000) + dt := NewDatetime(tm) + _, err := conn.Insert(spaceTuple1, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime insert failed: %s", err.Error()) + } + + resp, errSel := conn.Select(spaceTuple1, index, 0, 1, IterEq, []interface{}{dt}) + if errSel != nil { + t.Errorf("Failed to Select: %s", errSel.Error()) + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Select") + } else { + if val, ok := tpl[0].(Datetime); !ok || !val.ToTime().Equal(tm) { + t.Fatalf("Unexpected body of Select") + } + } + + // Teardown: delete a value. + _, err = conn.Delete(spaceTuple1, index, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime delete failed: %s", err.Error()) + } +} + +func TestMPEncode(t *testing.T) { + for _, testcase := range datetimeSample { + t.Run(testcase.dt, func(t *testing.T) { + tm, err := time.Parse(time.RFC3339, testcase.dt) + if err != nil { + t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) + } + dt := NewDatetime(tm) + buf, err := msgpack.Marshal(dt) + if err != nil { + t.Fatalf("Marshalling failed: %s", err.Error()) + } + refBuf, _ := hex.DecodeString(testcase.mpBuf) + if reflect.DeepEqual(buf, refBuf) != true { + t.Fatalf("Failed to encode datetime '%s', actual %v, expected %v", + testcase.dt, + buf, + refBuf) + } + }) + } +} + +func TestMPDecode(t *testing.T) { + for _, testcase := range datetimeSample { + t.Run(testcase.dt, func(t *testing.T) { + tm, err := time.Parse(time.RFC3339, testcase.dt) + if err != nil { + t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) + } + buf, _ := hex.DecodeString(testcase.mpBuf) + var v Datetime + err = msgpack.Unmarshal(buf, &v) + if err != nil { + t.Fatalf("Unmarshalling failed: %s", err.Error()) + } + if !tm.Equal(v.ToTime()) { + t.Fatalf("Failed to decode datetime buf '%s', actual %v, expected %v", + testcase.mpBuf, + testcase.dt, + v.ToTime()) + } + }) + } +} + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + isLess, err := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil { + log.Fatalf("Failed to extract Tarantool version: %s", err) + } + + if isLess { + log.Println("Skipping datetime tests...") + isDatetimeSupported = false + return m.Run() + } else { + isDatetimeSupported = true + } + + instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: server, + WorkDir: "work_dir", + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(instance) + + if err != nil { + log.Fatalf("Failed to prepare test Tarantool: %s", err) + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/datetime/example_test.go b/datetime/example_test.go new file mode 100644 index 000000000..4dbba20dc --- /dev/null +++ b/datetime/example_test.go @@ -0,0 +1,77 @@ +// Run a Tarantool instance before example execution: +// Terminal 1: +// $ cd datetime +// $ TEST_TNT_LISTEN=3013 TEST_TNT_WORK_DIR=$(mktemp -d -t 'tarantool.XXX') tarantool config.lua +// +// Terminal 2: +// $ cd datetime +// $ go test -v example_test.go +package datetime_test + +import ( + "fmt" + "time" + + "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/datetime" +) + +// Example demonstrates how to use tuples with datetime. To enable support of +// datetime import tarantool/datetime package. +func Example() { + opts := tarantool.Opts{ + User: "test", + Pass: "test", + } + conn, err := tarantool.Connect("127.0.0.1:3013", opts) + if err != nil { + fmt.Printf("error in connect is %v", err) + return + } + + var datetime = "2013-10-28T17:51:56.000000009Z" + tm, err := time.Parse(time.RFC3339, datetime) + if err != nil { + fmt.Printf("error in time.Parse() is %v", err) + return + } + dt := NewDatetime(tm) + + space := "testDatetime_1" + index := "primary" + + // Replace a tuple with datetime. + resp, err := conn.Replace(space, []interface{}{dt}) + if err != nil { + fmt.Printf("error in replace is %v", err) + return + } + respDt := resp.Data[0].([]interface{})[0].(Datetime) + fmt.Println("Datetime tuple replace") + fmt.Printf("Code: %d\n", resp.Code) + fmt.Printf("Data: %v\n", respDt.ToTime()) + + // Select a tuple with datetime. + var offset uint32 = 0 + var limit uint32 = 1 + resp, err = conn.Select(space, index, offset, limit, tarantool.IterEq, []interface{}{dt}) + if err != nil { + fmt.Printf("error in select is %v", err) + return + } + respDt = resp.Data[0].([]interface{})[0].(Datetime) + fmt.Println("Datetime tuple select") + fmt.Printf("Code: %d\n", resp.Code) + fmt.Printf("Data: %v\n", respDt.ToTime()) + + // Delete a tuple with datetime. + resp, err = conn.Delete(space, index, []interface{}{dt}) + if err != nil { + fmt.Printf("error in delete is %v", err) + return + } + respDt = resp.Data[0].([]interface{})[0].(Datetime) + fmt.Println("Datetime tuple delete") + fmt.Printf("Code: %d\n", resp.Code) + fmt.Printf("Data: %v\n", respDt.ToTime()) +} diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index d7d1091e6..eb5bcfe1c 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -29,7 +29,7 @@ func skipIfDecimalUnsupported(t *testing.T) { t.Helper() if isDecimalSupported == false { - t.Skip("Skipping test for Tarantool without datetime support in msgpack") + t.Skip("Skipping test for Tarantool without decimal support in msgpack") } } From 691fb7eda24b0d4f07c7f1ecc583680b9e7979b4 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 24 Jun 2022 23:53:00 +0300 Subject: [PATCH 293/605] github-ci: remove Tarantool 2.10 setup workaround Tarantool 2.10 can be installed with the setup-tarantool GitHub action. --- .github/workflows/testing.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index cf6ee8f3a..978073afd 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -48,8 +48,8 @@ jobs: - name: Clone the connector uses: actions/checkout@v2 - - name: Setup Tarantool ${{ matrix.tarantool }} (< 2.10) - if: matrix.tarantool != '2.x-latest' && matrix.tarantool != '2.10' + - name: Setup Tarantool ${{ matrix.tarantool }} + if: matrix.tarantool != '2.x-latest' uses: tarantool/setup-tarantool@v1 with: tarantool-version: ${{ matrix.tarantool }} @@ -60,12 +60,6 @@ jobs: curl -L https://tarantool.io/pre-release/2/installer.sh | sudo bash sudo apt install -y tarantool tarantool-dev - - name: Setup Tarantool 2.10 - if: matrix.tarantool == '2.10' - run: | - curl -L https://tarantool.io/tWsLBdI/release/2/installer.sh | bash - sudo apt install -y tarantool tarantool-dev - - name: Setup golang for the connector and tests uses: actions/setup-go@v2 with: From 3b983297a46259e7251d401c937c8f0a6d941a98 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 27 Jun 2022 14:42:05 +0300 Subject: [PATCH 294/605] api: remove NewErrorFuture It was introduced in e9b9ba1e . The function looks strange in the public API. The patch moves it to the connection_pool's internal implementation. --- CHANGELOG.md | 4 ++++ connection_pool/connection_pool.go | 28 +++++++++++++++++----------- future.go | 7 ------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3c3eb06..427cf2b33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - `IPROTO_*` constants that identify requests renamed from `Request` to `RequestCode` (#126) +### Removed + +- NewErrorFuture function (#190) + ### Fixed ## [1.6.0] - 2022-06-01 diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index bc7573c8d..644a93dbe 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -416,7 +416,7 @@ func (connPool *ConnectionPool) EvalTyped(expr string, args interface{}, result func (connPool *ConnectionPool) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}, userMode ...Mode) *tarantool.Future { conn, err := connPool.getConnByMode(ANY, userMode) if err != nil { - return tarantool.NewErrorFuture(err) + return newErrorFuture(err) } return conn.SelectAsync(space, index, offset, limit, iterator, key) @@ -427,7 +427,7 @@ func (connPool *ConnectionPool) SelectAsync(space, index interface{}, offset, li func (connPool *ConnectionPool) InsertAsync(space interface{}, tuple interface{}, userMode ...Mode) *tarantool.Future { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { - return tarantool.NewErrorFuture(err) + return newErrorFuture(err) } return conn.InsertAsync(space, tuple) @@ -438,7 +438,7 @@ func (connPool *ConnectionPool) InsertAsync(space interface{}, tuple interface{} func (connPool *ConnectionPool) ReplaceAsync(space interface{}, tuple interface{}, userMode ...Mode) *tarantool.Future { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { - return tarantool.NewErrorFuture(err) + return newErrorFuture(err) } return conn.ReplaceAsync(space, tuple) @@ -449,7 +449,7 @@ func (connPool *ConnectionPool) ReplaceAsync(space interface{}, tuple interface{ func (connPool *ConnectionPool) DeleteAsync(space, index interface{}, key interface{}, userMode ...Mode) *tarantool.Future { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { - return tarantool.NewErrorFuture(err) + return newErrorFuture(err) } return conn.DeleteAsync(space, index, key) @@ -460,7 +460,7 @@ func (connPool *ConnectionPool) DeleteAsync(space, index interface{}, key interf func (connPool *ConnectionPool) UpdateAsync(space, index interface{}, key, ops interface{}, userMode ...Mode) *tarantool.Future { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { - return tarantool.NewErrorFuture(err) + return newErrorFuture(err) } return conn.UpdateAsync(space, index, key, ops) @@ -471,7 +471,7 @@ func (connPool *ConnectionPool) UpdateAsync(space, index interface{}, key, ops i func (connPool *ConnectionPool) UpsertAsync(space interface{}, tuple interface{}, ops interface{}, userMode ...Mode) *tarantool.Future { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { - return tarantool.NewErrorFuture(err) + return newErrorFuture(err) } return conn.UpsertAsync(space, tuple, ops) @@ -484,7 +484,7 @@ func (connPool *ConnectionPool) UpsertAsync(space interface{}, tuple interface{} func (connPool *ConnectionPool) CallAsync(functionName string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { - return tarantool.NewErrorFuture(err) + return newErrorFuture(err) } return conn.CallAsync(functionName, args) @@ -496,7 +496,7 @@ func (connPool *ConnectionPool) CallAsync(functionName string, args interface{}, func (connPool *ConnectionPool) Call16Async(functionName string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { - return tarantool.NewErrorFuture(err) + return newErrorFuture(err) } return conn.Call16Async(functionName, args) @@ -508,7 +508,7 @@ func (connPool *ConnectionPool) Call16Async(functionName string, args interface{ func (connPool *ConnectionPool) Call17Async(functionName string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { - return tarantool.NewErrorFuture(err) + return newErrorFuture(err) } return conn.Call17Async(functionName, args) @@ -518,7 +518,7 @@ func (connPool *ConnectionPool) Call17Async(functionName string, args interface{ func (connPool *ConnectionPool) EvalAsync(expr string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { - return tarantool.NewErrorFuture(err) + return newErrorFuture(err) } return conn.EvalAsync(expr, args) @@ -548,7 +548,7 @@ func (connPool *ConnectionPool) DoTyped(req tarantool.Request, result interface{ func (connPool *ConnectionPool) DoAsync(req tarantool.Request, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { - return tarantool.NewErrorFuture(err) + return newErrorFuture(err) } return conn.DoAsync(req) @@ -802,3 +802,9 @@ func (connPool *ConnectionPool) getConnByMode(defaultMode Mode, userMode []Mode) return connPool.getNextConnection(mode) } + +func newErrorFuture(err error) *tarantool.Future { + fut := tarantool.NewFuture() + fut.SetError(err) + return fut +} diff --git a/future.go b/future.go index 6f21ce749..d6ba3743c 100644 --- a/future.go +++ b/future.go @@ -126,13 +126,6 @@ func NewFuture() (fut *Future) { return fut } -// NewErrorFuture returns new set empty Future with filled error field. -func NewErrorFuture(err error) *Future { - fut := NewFuture() - fut.SetError(err) - return fut -} - // AppendPush appends the push response to the future. // Note: it works only before SetResponse() or SetError() func (fut *Future) AppendPush(resp *Response) { From d04f8bede55925133ac1e013c97f78f11373b3b2 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Sat, 25 Jun 2022 09:54:03 +0300 Subject: [PATCH 295/605] code health: add test_helpers.ConnectWithValidation The code that creates and verifies a connection to a Tarantool instance spreads across tests. The patch extracts the code into a separate test helper function. --- call_16_test.go | 5 +- call_17_test.go | 5 +- connection_pool/connection_pool_test.go | 25 +---- datetime/datetime_test.go | 27 ++--- decimal/decimal_test.go | 19 +--- queue/queue_test.go | 135 +++++------------------- tarantool_test.go | 77 +++++--------- test_helpers/utils.go | 25 +++++ uuid/uuid_test.go | 15 +-- 9 files changed, 96 insertions(+), 237 deletions(-) create mode 100644 test_helpers/utils.go diff --git a/call_16_test.go b/call_16_test.go index c20049fca..e49274daa 100644 --- a/call_16_test.go +++ b/call_16_test.go @@ -7,13 +7,14 @@ import ( "testing" . "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" ) func TestConnection_Call(t *testing.T) { var resp *Response var err error - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() // Call16 @@ -30,7 +31,7 @@ func TestCallRequest(t *testing.T) { var resp *Response var err error - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() req := NewCallRequest("simple_incr").Args([]interface{}{1}) diff --git a/call_17_test.go b/call_17_test.go index e4b030bf2..7094b9943 100644 --- a/call_17_test.go +++ b/call_17_test.go @@ -7,13 +7,14 @@ import ( "testing" . "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" ) func TestConnection_Call(t *testing.T) { var resp *Response var err error - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() // Call17 @@ -30,7 +31,7 @@ func TestCallRequest(t *testing.T) { var resp *Response var err error - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() req := NewCallRequest("simple_incr").Args([]interface{}{1}) diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 6bad15dd7..90f296306 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -739,10 +739,7 @@ func TestInsert(t *testing.T) { // Connect to servers[2] to check if tuple // was inserted on RW instance - conn, err := tarantool.Connect(servers[2], connOpts) - require.Nilf(t, err, "failed to connect %s", servers[2]) - require.NotNilf(t, conn, "conn is nil after Connect") - + conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) defer conn.Close() resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"rw_insert_key"}) @@ -812,10 +809,7 @@ func TestDelete(t *testing.T) { // Connect to servers[2] to check if tuple // was inserted on RW instance - conn, err := tarantool.Connect(servers[2], connOpts) - require.Nilf(t, err, "failed to connect %s", servers[2]) - require.NotNilf(t, conn, "conn is nil after Connect") - + conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) defer conn.Close() resp, err := conn.Insert(spaceNo, []interface{}{"delete_key", "delete_value"}) @@ -873,10 +867,7 @@ func TestUpsert(t *testing.T) { // Connect to servers[2] to check if tuple // was inserted on RW instance - conn, err := tarantool.Connect(servers[2], connOpts) - require.Nilf(t, err, "failed to connect %s", servers[2]) - require.NotNilf(t, conn, "conn is nil after Connect") - + conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) defer conn.Close() // Mode is `RW` by default, we have only one RW instance (servers[2]) @@ -941,10 +932,7 @@ func TestUpdate(t *testing.T) { // Connect to servers[2] to check if tuple // was inserted on RW instance - conn, err := tarantool.Connect(servers[2], connOpts) - require.Nilf(t, err, "failed to connect %s", servers[2]) - require.NotNilf(t, conn, "conn is nil after Connect") - + conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) defer conn.Close() resp, err := conn.Insert(spaceNo, []interface{}{"update_key", "update_value"}) @@ -1026,10 +1014,7 @@ func TestReplace(t *testing.T) { // Connect to servers[2] to check if tuple // was inserted on RW instance - conn, err := tarantool.Connect(servers[2], connOpts) - require.Nilf(t, err, "failed to connect %s", servers[2]) - require.NotNilf(t, conn, "conn is nil after Connect") - + conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) defer conn.Close() resp, err := conn.Insert(spaceNo, []interface{}{"replace_key", "replace_value"}) diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index e1aeef23d..d5f89a29a 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -33,19 +33,6 @@ var spaceTuple1 = "testDatetime_1" var spaceTuple2 = "testDatetime_2" var index = "primary" -func connectWithValidation(t *testing.T) *Connection { - t.Helper() - - conn, err := Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if conn == nil { - t.Fatalf("conn is nil after Connect") - } - return conn -} - func skipIfDatetimeUnsupported(t *testing.T) { t.Helper() @@ -168,7 +155,7 @@ var datetimeSample = []struct { func TestDatetimeInsertSelectDelete(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := connectWithValidation(t) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() for _, testcase := range datetimeSample { @@ -188,7 +175,7 @@ func TestDatetimeInsertSelectDelete(t *testing.T) { func TestDatetimeMax(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := connectWithValidation(t) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() tupleInsertSelectDelete(t, conn, maxTime) @@ -197,7 +184,7 @@ func TestDatetimeMax(t *testing.T) { func TestDatetimeMin(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := connectWithValidation(t) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() tupleInsertSelectDelete(t, conn, minTime) @@ -206,7 +193,7 @@ func TestDatetimeMin(t *testing.T) { func TestDatetimeReplace(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := connectWithValidation(t) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() tm, err := time.Parse(time.RFC3339, "2007-01-02T15:04:05Z") @@ -356,7 +343,7 @@ func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { func TestCustomEncodeDecodeTuple1(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := connectWithValidation(t) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() dt1, _ := time.Parse(time.RFC3339, "2010-05-24T17:51:56.000000009Z") @@ -416,7 +403,7 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { func TestCustomDecodeFunction(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := connectWithValidation(t) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() // Call function 'call_datetime_testdata' returning a table of custom tuples. @@ -459,7 +446,7 @@ func TestCustomDecodeFunction(t *testing.T) { func TestCustomEncodeDecodeTuple5(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := connectWithValidation(t) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() tm := time.Unix(500, 1000) diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index eb5bcfe1c..1c395b393 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -337,19 +337,6 @@ func BenchmarkMPDecodeDecimal(b *testing.B) { } } -func connectWithValidation(t *testing.T) *Connection { - t.Helper() - - conn, err := Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if conn == nil { - t.Fatalf("conn is nil after Connect") - } - return conn -} - func tupleValueIsDecimal(t *testing.T, tuples []interface{}, number decimal.Decimal) { if len(tuples) != 1 { t.Fatalf("Response Data len (%d) != 1", len(tuples)) @@ -499,7 +486,7 @@ func BenchmarkDecodeStringFromBCD(b *testing.B) { func TestSelect(t *testing.T) { skipIfDecimalUnsupported(t) - conn := connectWithValidation(t) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() number, err := decimal.NewFromString("-12.34") @@ -559,7 +546,7 @@ func assertInsert(t *testing.T, conn *Connection, numString string) { func TestInsert(t *testing.T) { skipIfDecimalUnsupported(t) - conn := connectWithValidation(t) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() samples := append(correctnessSamples, benchmarkSamples...) @@ -573,7 +560,7 @@ func TestInsert(t *testing.T) { func TestReplace(t *testing.T) { skipIfDecimalUnsupported(t) - conn := connectWithValidation(t) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() number, err := decimal.NewFromString("-12.34") diff --git a/queue/queue_test.go b/queue/queue_test.go index cd9f417cf..48fb71257 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -26,45 +26,29 @@ var opts = Opts{ /////////QUEUE///////// func TestFifoQueue(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() name := "test_queue" q := queue.New(conn, name) - if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return } //Drop - if err = q.Drop(); err != nil { + if err := q.Drop(); err != nil { t.Errorf("Failed drop queue: %s", err.Error()) } } func TestFifoQueue_GetExist_Statistic(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() name := "test_queue" q := queue.New(conn, name) - if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return } @@ -102,15 +86,7 @@ func TestFifoQueue_GetExist_Statistic(t *testing.T) { } func TestFifoQueue_Put(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() name := "test_queue" @@ -145,20 +121,12 @@ func TestFifoQueue_Put(t *testing.T) { } func TestFifoQueue_Take(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() name := "test_queue" q := queue.New(conn, name) - if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return } @@ -241,20 +209,12 @@ func (c *customData) EncodeMsgpack(e *msgpack.Encoder) error { } func TestFifoQueue_TakeTyped(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() name := "test_queue" q := queue.New(conn, name) - if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return } @@ -318,20 +278,12 @@ func TestFifoQueue_TakeTyped(t *testing.T) { } func TestFifoQueue_Peek(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() name := "test_queue" q := queue.New(conn, name) - if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return } @@ -377,20 +329,12 @@ func TestFifoQueue_Peek(t *testing.T) { } func TestFifoQueue_Bury_Kick(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() name := "test_queue" q := queue.New(conn, name) - if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return } @@ -464,15 +408,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { func TestFifoQueue_Delete(t *testing.T) { var err error - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() name := "test_queue" @@ -539,20 +475,12 @@ func TestFifoQueue_Delete(t *testing.T) { } func TestFifoQueue_Release(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if conn == nil { - t.Errorf("conn is nil after Connect") - return - } + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() name := "test_queue" q := queue.New(conn, name) - if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { + if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return } @@ -628,10 +556,7 @@ func TestFifoQueue_Release(t *testing.T) { } func TestTtlQueue(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() name := "test_queue" @@ -641,7 +566,7 @@ func TestTtlQueue(t *testing.T) { Opts: queue.Opts{Ttl: 5 * time.Second}, } q := queue.New(conn, name) - if err = q.Create(cfg); err != nil { + if err := q.Create(cfg); err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return } @@ -680,13 +605,7 @@ func TestTtlQueue(t *testing.T) { } func TestTtlQueue_Put(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if conn == nil { - t.Fatalf("conn is nil after Connect") - } + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() name := "test_queue" @@ -696,7 +615,7 @@ func TestTtlQueue_Put(t *testing.T) { Opts: queue.Opts{Ttl: 5 * time.Second}, } q := queue.New(conn, name) - if err = q.Create(cfg); err != nil { + if err := q.Create(cfg); err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return } @@ -750,13 +669,7 @@ func TestTtlQueue_Put(t *testing.T) { } func TestUtube_Put(t *testing.T) { - conn, err := Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if conn == nil { - t.Fatalf("conn is nil after Connect") - } + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() name := "test_utube" @@ -766,7 +679,7 @@ func TestUtube_Put(t *testing.T) { IfNotExists: true, } q := queue.New(conn, name) - if err = q.Create(cfg); err != nil { + if err := q.Create(cfg); err != nil { t.Errorf("Failed to create queue: %s", err.Error()) return } @@ -779,7 +692,7 @@ func TestUtube_Put(t *testing.T) { }() data1 := &customData{"test-data-0"} - _, err = q.PutWithOpts(data1, queue.Opts{Utube: "test-utube-consumer-key"}) + _, err := q.PutWithOpts(data1, queue.Opts{Utube: "test-utube-consumer-key"}) if err != nil { t.Fatalf("Failed put task to queue: %s", err.Error()) } diff --git a/tarantool_test.go b/tarantool_test.go index d76f82d1c..0ae26f154 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -22,19 +22,6 @@ type Member struct { Val uint } -func connect(t testing.TB, server string, opts Opts) (conn *Connection) { - t.Helper() - - conn, err := Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if conn == nil { - t.Fatalf("conn is nil after Connect") - } - return conn -} - func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeSliceLen(2); err != nil { return err @@ -84,7 +71,7 @@ const N = 500 func BenchmarkClientSerial(b *testing.B) { var err error - conn := connect(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -104,7 +91,7 @@ func BenchmarkClientSerial(b *testing.B) { func BenchmarkClientSerialRequestObject(b *testing.B) { var err error - conn := connect(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -131,7 +118,7 @@ func BenchmarkClientSerialRequestObject(b *testing.B) { func BenchmarkClientSerialTyped(b *testing.B) { var err error - conn := connect(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -152,7 +139,7 @@ func BenchmarkClientSerialTyped(b *testing.B) { func BenchmarkClientFuture(b *testing.B) { var err error - conn := connect(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -179,7 +166,7 @@ func BenchmarkClientFuture(b *testing.B) { func BenchmarkClientFutureTyped(b *testing.B) { var err error - conn := connect(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -209,7 +196,7 @@ func BenchmarkClientFutureTyped(b *testing.B) { func BenchmarkClientFutureParallel(b *testing.B) { var err error - conn := connect(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -242,7 +229,7 @@ func BenchmarkClientFutureParallel(b *testing.B) { func BenchmarkClientFutureParallelTyped(b *testing.B) { var err error - conn := connect(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -278,7 +265,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { } func BenchmarkClientParallel(b *testing.B) { - conn := connect(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -299,7 +286,7 @@ func BenchmarkClientParallel(b *testing.B) { } func BenchmarkClientParallelMassive(b *testing.B) { - conn := connect(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -334,7 +321,7 @@ func BenchmarkClientParallelMassive(b *testing.B) { } func BenchmarkClientParallelMassiveUntyped(b *testing.B) { - conn := connect(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -369,11 +356,7 @@ func BenchmarkClientParallelMassiveUntyped(b *testing.B) { } func BenchmarkClientReplaceParallel(b *testing.B) { - conn, err := Connect(server, opts) - if err != nil { - b.Errorf("No connection available") - return - } + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() spaceNo = 520 @@ -394,11 +377,7 @@ func BenchmarkClientReplaceParallel(b *testing.B) { } func BenchmarkClientLargeSelectParallel(b *testing.B) { - conn, err := Connect(server, opts) - if err != nil { - b.Errorf("No connection available") - return - } + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() schema := conn.Schema @@ -429,11 +408,7 @@ func BenchmarkSQLParallel(b *testing.B) { b.Skip() } - conn, err := Connect(server, opts) - if err != nil { - b.Errorf("No connection available") - return - } + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() spaceNo := 519 @@ -464,11 +439,7 @@ func BenchmarkSQLSerial(b *testing.B) { b.Skip() } - conn, err := Connect(server, opts) - if err != nil { - b.Errorf("Failed to connect: %s", err) - return - } + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() spaceNo := 519 @@ -493,7 +464,7 @@ func TestClient(t *testing.T) { var resp *Response var err error - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() // Ping @@ -796,7 +767,7 @@ func TestClient(t *testing.T) { } func TestClientSessionPush(t *testing.T) { - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() var it ResponseIterator @@ -1065,7 +1036,7 @@ func TestSQL(t *testing.T) { }, } - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() for i, test := range testCases { @@ -1099,7 +1070,7 @@ func TestSQLTyped(t *testing.T) { t.Skip() } - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() mem := []Member{} @@ -1135,7 +1106,7 @@ func TestSQLBindings(t *testing.T) { var resp *Response - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() // test all types of supported bindings @@ -1244,7 +1215,7 @@ func TestStressSQL(t *testing.T) { var resp *Response - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() resp, err = conn.Execute(createTableQuery, []interface{}{}) @@ -1340,7 +1311,7 @@ func TestStressSQL(t *testing.T) { func TestSchema(t *testing.T) { var err error - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() // Schema @@ -1515,7 +1486,7 @@ func TestClientNamed(t *testing.T) { var resp *Response var err error - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() // Insert @@ -1608,7 +1579,7 @@ func TestClientRequestObjects(t *testing.T) { err error ) - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() // Ping @@ -1947,7 +1918,7 @@ func TestClientRequestObjects(t *testing.T) { func TestComplexStructs(t *testing.T) { var err error - conn := connect(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} diff --git a/test_helpers/utils.go b/test_helpers/utils.go new file mode 100644 index 000000000..b7c8fdc96 --- /dev/null +++ b/test_helpers/utils.go @@ -0,0 +1,25 @@ +package test_helpers + +import ( + "testing" + + "github.com/tarantool/go-tarantool" +) + +// ConnectWithValidation tries to connect to a Tarantool instance. +// It returns a valid connection if it is successful, otherwise finishes a test +// with an error. +func ConnectWithValidation(t testing.TB, + server string, + opts tarantool.Opts) *tarantool.Connection { + t.Helper() + + conn, err := tarantool.Connect(server, opts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if conn == nil { + t.Fatalf("conn is nil after Connect") + } + return conn +} diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index 67c45b4f3..554dadbe1 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -50,17 +50,6 @@ func (t *TupleUUID) DecodeMsgpack(d *msgpack.Decoder) error { return nil } -func connectWithValidation(t *testing.T) *Connection { - conn, err := Connect(server, opts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if conn == nil { - t.Errorf("conn is nil after Connect") - } - return conn -} - func tupleValueIsId(t *testing.T, tuples []interface{}, id uuid.UUID) { if len(tuples) != 1 { t.Fatalf("Response Data len != 1") @@ -83,7 +72,7 @@ func TestSelect(t *testing.T) { t.Skip("Skipping test for Tarantool without UUID support in msgpack") } - conn := connectWithValidation(t) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() id, uuidErr := uuid.Parse("c8f0fa1f-da29-438c-a040-393f1126ad39") @@ -118,7 +107,7 @@ func TestReplace(t *testing.T) { t.Skip("Skipping test for Tarantool without UUID support in msgpack") } - conn := connectWithValidation(t) + conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() id, uuidErr := uuid.Parse("64d22e4d-ac92-4a23-899a-e59f34af5479") From 72d7457e28bf636448ffef97c7a78c94b447381e Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 27 Jun 2022 13:39:46 +0300 Subject: [PATCH 296/605] api: left only asynchronous Connection.Do(request) The patch deletes synchronous Connection.Do and Connection.DoTyped and renames an asynchronous Connection.DoAsync to Connection.Do. There are several reasons for this: 1. It helps to make the public API shorter. 2. It makes the asynchronous essence of the connector is more clear. 3. It helps not to spread the unnecessary API to submodules. 4. We will have more freedom for changes in the future. --- call_16_test.go | 2 +- call_17_test.go | 2 +- connection.go | 22 ++---------------- connection_pool/connection_pool.go | 26 +++------------------ connection_pool/connection_pool_test.go | 10 ++++----- connection_pool/example_test.go | 2 +- connector.go | 4 +--- example_test.go | 14 ++++++------ multi/multi.go | 14 ++---------- request.go | 24 ++++++++++---------- tarantool_test.go | 30 ++++++++++++------------- 11 files changed, 50 insertions(+), 100 deletions(-) diff --git a/call_16_test.go b/call_16_test.go index e49274daa..1dab3079d 100644 --- a/call_16_test.go +++ b/call_16_test.go @@ -35,7 +35,7 @@ func TestCallRequest(t *testing.T) { defer conn.Close() req := NewCallRequest("simple_incr").Args([]interface{}{1}) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to use Call") } diff --git a/call_17_test.go b/call_17_test.go index 7094b9943..400c69693 100644 --- a/call_17_test.go +++ b/call_17_test.go @@ -35,7 +35,7 @@ func TestCallRequest(t *testing.T) { defer conn.Close() req := NewCallRequest("simple_incr").Args([]interface{}{1}) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to use Call") } diff --git a/connection.go b/connection.go index 80e609354..871890673 100644 --- a/connection.go +++ b/connection.go @@ -988,29 +988,11 @@ func (conn *Connection) nextRequestId() (requestId uint32) { return atomic.AddUint32(&conn.requestId, 1) } -// Do verifies, sends the request and returns a response. -// -// An error is returned if the request was formed incorrectly, or failed to -// communicate by the connection, or unable to decode the response. -func (conn *Connection) Do(req Request) (*Response, error) { - fut := conn.DoAsync(req) - return fut.Get() -} - -// DoTyped verifies, sends the request and fills the typed result. -// -// An error is returned if the request was formed incorrectly, or failed to -// communicate by the connection, or unable to decode the response. -func (conn *Connection) DoTyped(req Request, result interface{}) error { - fut := conn.DoAsync(req) - return fut.GetTyped(result) -} - -// DoAsync verifies, sends the request and returns a future. +// Do performs a request asynchronously on the connection. // // An error is returned if the request was formed incorrectly, or failed to // create the future. -func (conn *Connection) DoAsync(req Request) *Future { +func (conn *Connection) Do(req Request) *Future { return conn.send(req) } diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 644a93dbe..0752038f0 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -524,34 +524,14 @@ func (connPool *ConnectionPool) EvalAsync(expr string, args interface{}, userMod return conn.EvalAsync(expr, args) } -// Do sends the request and returns a response. -func (connPool *ConnectionPool) Do(req tarantool.Request, userMode Mode) (*tarantool.Response, error) { - conn, err := connPool.getNextConnection(userMode) - if err != nil { - return nil, err - } - - return conn.Do(req) -} - -// DoTyped sends the request and fills the typed result. -func (connPool *ConnectionPool) DoTyped(req tarantool.Request, result interface{}, userMode Mode) error { - conn, err := connPool.getNextConnection(userMode) - if err != nil { - return err - } - - return conn.DoTyped(req, result) -} - -// DoAsync sends the request and returns a future. -func (connPool *ConnectionPool) DoAsync(req tarantool.Request, userMode Mode) *tarantool.Future { +// Do sends the request and returns a future. +func (connPool *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { return newErrorFuture(err) } - return conn.DoAsync(req) + return conn.Do(req) } // diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 90f296306..00337005d 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -1251,27 +1251,27 @@ func TestDo(t *testing.T) { req := tarantool.NewPingRequest() // ANY - resp, err := connPool.Do(req, connection_pool.ANY) + resp, err := connPool.Do(req, connection_pool.ANY).Get() require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") // RW - resp, err = connPool.Do(req, connection_pool.RW) + resp, err = connPool.Do(req, connection_pool.RW).Get() require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") // RO - resp, err = connPool.Do(req, connection_pool.RO) + resp, err = connPool.Do(req, connection_pool.RO).Get() require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") // PreferRW - resp, err = connPool.Do(req, connection_pool.PreferRW) + resp, err = connPool.Do(req, connection_pool.PreferRW).Get() require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") // PreferRO - resp, err = connPool.Do(req, connection_pool.PreferRO) + resp, err = connPool.Do(req, connection_pool.PreferRO).Get() require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") } diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go index f7b0009b0..faf97bc66 100644 --- a/connection_pool/example_test.go +++ b/connection_pool/example_test.go @@ -539,7 +539,7 @@ func ExampleConnectionPool_Do() { // Ping a Tarantool instance to check connection. req := tarantool.NewPingRequest() - resp, err := pool.Do(req, connection_pool.ANY) + resp, err := pool.Do(req, connection_pool.ANY).Get() fmt.Println("Ping Code", resp.Code) fmt.Println("Ping Data", resp.Data) fmt.Println("Ping Error", err) diff --git a/connector.go b/connector.go index a987ccca3..cd77d2c4c 100644 --- a/connector.go +++ b/connector.go @@ -42,7 +42,5 @@ type Connector interface { Call17Async(functionName string, args interface{}) *Future EvalAsync(expr string, args interface{}) *Future - Do(req Request) (resp *Response, err error) - DoTyped(req Request, result interface{}) (err error) - DoAsync(req Request) (fut *Future) + Do(req Request) (fut *Future) } diff --git a/example_test.go b/example_test.go index 070b964da..0e5487a53 100644 --- a/example_test.go +++ b/example_test.go @@ -133,7 +133,7 @@ func ExampleSelectRequest() { req := tarantool.NewSelectRequest(517). Limit(100). Key(tarantool.IntKey{1111}) - resp, err := conn.Do(req) + resp, err := conn.Do(req).Get() if err != nil { fmt.Printf("error in do select request is %v", err) return @@ -144,7 +144,7 @@ func ExampleSelectRequest() { Index("primary"). Limit(100). Key(tarantool.IntKey{1111}) - fut := conn.DoAsync(req) + fut := conn.Do(req) resp, err = fut.Get() if err != nil { fmt.Printf("error in do async select request is %v", err) @@ -163,7 +163,7 @@ func ExampleUpdateRequest() { req := tarantool.NewUpdateRequest(517). Key(tarantool.IntKey{1111}). Operations(tarantool.NewOperations().Assign(1, "bye")) - resp, err := conn.Do(req) + resp, err := conn.Do(req).Get() if err != nil { fmt.Printf("error in do update request is %v", err) return @@ -174,7 +174,7 @@ func ExampleUpdateRequest() { Index("primary"). Key(tarantool.IntKey{1111}). Operations(tarantool.NewOperations().Assign(1, "hello")) - fut := conn.DoAsync(req) + fut := conn.Do(req) resp, err = fut.Get() if err != nil { fmt.Printf("error in do async update request is %v", err) @@ -194,7 +194,7 @@ func ExampleUpsertRequest() { req = tarantool.NewUpsertRequest(517). Tuple([]interface{}{uint(1113), "first", "first"}). Operations(tarantool.NewOperations().Assign(1, "updated")) - resp, err := conn.Do(req) + resp, err := conn.Do(req).Get() if err != nil { fmt.Printf("error in do select upsert is %v", err) return @@ -204,7 +204,7 @@ func ExampleUpsertRequest() { req = tarantool.NewUpsertRequest("test"). Tuple([]interface{}{uint(1113), "second", "second"}). Operations(tarantool.NewOperations().Assign(2, "updated")) - fut := conn.DoAsync(req) + fut := conn.Do(req) resp, err = fut.Get() if err != nil { fmt.Printf("error in do async upsert request is %v", err) @@ -215,7 +215,7 @@ func ExampleUpsertRequest() { req = tarantool.NewSelectRequest(517). Limit(100). Key(tarantool.IntKey{1113}) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { fmt.Printf("error in do select request is %v", err) return diff --git a/multi/multi.go b/multi/multi.go index a28bbb40a..3a98342a7 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -482,17 +482,7 @@ func (connMulti *ConnectionMulti) EvalAsync(expr string, args interface{}) *tara return connMulti.getCurrentConnection().EvalAsync(expr, args) } -// Do sends the request and returns a response. -func (connMulti *ConnectionMulti) Do(req tarantool.Request) (*tarantool.Response, error) { +// Do sends the request and returns a future. +func (connMulti *ConnectionMulti) Do(req tarantool.Request) *tarantool.Future { return connMulti.getCurrentConnection().Do(req) } - -// DoTyped sends the request and fills the typed result. -func (connMulti *ConnectionMulti) DoTyped(req tarantool.Request, result interface{}) error { - return connMulti.getCurrentConnection().DoTyped(req, result) -} - -// DoAsync sends the request and returns a future. -func (connMulti *ConnectionMulti) DoAsync(req tarantool.Request) *tarantool.Future { - return connMulti.getCurrentConnection().DoAsync(req) -} diff --git a/request.go b/request.go index c3a83ee56..3b6b33f07 100644 --- a/request.go +++ b/request.go @@ -97,7 +97,7 @@ func fillPing(enc *msgpack.Encoder) error { // Ping sends empty request to Tarantool to check connection. func (conn *Connection) Ping() (resp *Response, err error) { - return conn.Do(NewPingRequest()) + return conn.Do(NewPingRequest()).Get() } // Select performs select to box space. @@ -311,28 +311,28 @@ func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, ite Limit(limit). Iterator(iterator). Key(key) - return conn.DoAsync(req) + return conn.Do(req) } // InsertAsync sends insert action to Tarantool and returns Future. // Tarantool will reject Insert when tuple with same primary key exists. func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Future { req := NewInsertRequest(space).Tuple(tuple) - return conn.DoAsync(req) + return conn.Do(req) } // ReplaceAsync sends "insert or replace" action to Tarantool and returns Future. // If tuple with same primary key exists, it will be replaced. func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Future { req := NewReplaceRequest(space).Tuple(tuple) - return conn.DoAsync(req) + return conn.Do(req) } // DeleteAsync sends deletion action to Tarantool and returns Future. // Future's result will contain array with deleted tuple. func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) *Future { req := NewDeleteRequest(space).Index(index).Key(key) - return conn.DoAsync(req) + return conn.Do(req) } // Update sends deletion of a tuple by key and returns Future. @@ -340,7 +340,7 @@ func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) * func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface{}) *Future { req := NewUpdateRequest(space).Index(index).Key(key) req.ops = ops - return conn.DoAsync(req) + return conn.Do(req) } // UpsertAsync sends "update or insert" action to Tarantool and returns Future. @@ -348,7 +348,7 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *Future { req := NewUpsertRequest(space).Tuple(tuple) req.ops = ops - return conn.DoAsync(req) + return conn.Do(req) } // CallAsync sends a call to registered Tarantool function and returns Future. @@ -357,7 +357,7 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops in // Otherwise, uses request code for Tarantool 1.6. func (conn *Connection) CallAsync(functionName string, args interface{}) *Future { req := NewCallRequest(functionName).Args(args) - return conn.DoAsync(req) + return conn.Do(req) } // Call16Async sends a call to registered Tarantool function and returns Future. @@ -365,7 +365,7 @@ func (conn *Connection) CallAsync(functionName string, args interface{}) *Future // Deprecated since Tarantool 1.7.2. func (conn *Connection) Call16Async(functionName string, args interface{}) *Future { req := NewCall16Request(functionName).Args(args) - return conn.DoAsync(req) + return conn.Do(req) } // Call17Async sends a call to registered Tarantool function and returns Future. @@ -373,20 +373,20 @@ func (conn *Connection) Call16Async(functionName string, args interface{}) *Futu // (though, keep in mind, result is always array) func (conn *Connection) Call17Async(functionName string, args interface{}) *Future { req := NewCall17Request(functionName).Args(args) - return conn.DoAsync(req) + return conn.Do(req) } // EvalAsync sends a Lua expression for evaluation and returns Future. func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { req := NewEvalRequest(expr).Args(args) - return conn.DoAsync(req) + return conn.Do(req) } // ExecuteAsync sends a sql expression for execution and returns Future. // Since 1.6.0 func (conn *Connection) ExecuteAsync(expr string, args interface{}) *Future { req := NewExecuteRequest(expr).Args(args) - return conn.DoAsync(req) + return conn.Do(req) } // KeyValueBind is a type for encoding named SQL parameters diff --git a/tarantool_test.go b/tarantool_test.go index 0ae26f154..64e9c7942 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -108,7 +108,7 @@ func BenchmarkClientSerialRequestObject(b *testing.B) { Limit(1). Iterator(IterEq). Key([]interface{}{uint(1111)}) - _, err := conn.Do(req) + _, err := conn.Do(req).Get() if err != nil { b.Error(err) } @@ -1584,7 +1584,7 @@ func TestClientRequestObjects(t *testing.T) { // Ping req = NewPingRequest() - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Ping: %s", err.Error()) } @@ -1604,7 +1604,7 @@ func TestClientRequestObjects(t *testing.T) { for i := 1010; i < 1020; i++ { req = NewInsertRequest(spaceName). Tuple([]interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Insert: %s", err.Error()) } @@ -1639,7 +1639,7 @@ func TestClientRequestObjects(t *testing.T) { for i := 1015; i < 1020; i++ { req = NewReplaceRequest(spaceName). Tuple([]interface{}{uint(i), fmt.Sprintf("val %d", i), "blar"}) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Replace: %s", err.Error()) } @@ -1673,7 +1673,7 @@ func TestClientRequestObjects(t *testing.T) { // Delete req = NewDeleteRequest(spaceName). Key([]interface{}{uint(1016)}) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Delete: %s", err.Error()) } @@ -1707,7 +1707,7 @@ func TestClientRequestObjects(t *testing.T) { req = NewUpdateRequest(spaceName). Index(indexName). Key([]interface{}{uint(1010)}) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to Update: %s", err.Error()) } @@ -1739,7 +1739,7 @@ func TestClientRequestObjects(t *testing.T) { Index(indexName). Key([]interface{}{uint(1010)}). Operations(NewOperations().Assign(1, "bye").Insert(2, 1)) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to Update: %s", err.Error()) } @@ -1769,7 +1769,7 @@ func TestClientRequestObjects(t *testing.T) { // Upsert without operations. req = NewUpsertRequest(spaceNo). Tuple([]interface{}{uint(1010), "hi", "hi"}) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to Upsert (update): %s", err.Error()) } @@ -1787,7 +1787,7 @@ func TestClientRequestObjects(t *testing.T) { req = NewUpsertRequest(spaceNo). Tuple([]interface{}{uint(1010), "hi", "hi"}). Operations(NewOperations().Assign(2, "bye")) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to Upsert (update): %s", err.Error()) } @@ -1807,7 +1807,7 @@ func TestClientRequestObjects(t *testing.T) { Limit(20). Iterator(IterGe). Key([]interface{}{uint(1010)}) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to Select: %s", err.Error()) } @@ -1834,7 +1834,7 @@ func TestClientRequestObjects(t *testing.T) { // Call16 vs Call17 req = NewCall16Request("simple_incr").Args([]interface{}{1}) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to use Call") } @@ -1844,7 +1844,7 @@ func TestClientRequestObjects(t *testing.T) { // Call17 req = NewCall17Request("simple_incr").Args([]interface{}{1}) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to use Call17") } @@ -1854,7 +1854,7 @@ func TestClientRequestObjects(t *testing.T) { // Eval req = NewEvalRequest("return 5 + 6") - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Eval: %s", err.Error()) } @@ -1879,7 +1879,7 @@ func TestClientRequestObjects(t *testing.T) { } req = NewExecuteRequest(createTableQuery) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Execute: %s", err.Error()) } @@ -1897,7 +1897,7 @@ func TestClientRequestObjects(t *testing.T) { } req = NewExecuteRequest(dropQuery2) - resp, err = conn.Do(req) + resp, err = conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Execute: %s", err.Error()) } From 9b0ec8a9e30ed5120e888a43fed5237833a627e5 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 30 Jun 2022 14:27:32 +0300 Subject: [PATCH 297/605] datetime: sync datetime range with Tarantool The patch adds check for supported Datetime values. The supported range comes from Tarantool implementation [1] and c-dt library [2]. 1. https://github.com/tarantool/tarantool/blob/a99ccce5f517d2a04670289d3d09a8cc2f5916f9/src/lib/core/datetime.h#L44-L61 2. https://github.com/tarantool/c-dt/blob/e6214325fe8d4336464ebae859ac2b456fd22b77/API.pod#introduction Closes #191 --- datetime/datetime.go | 30 +++++++++++--- datetime/datetime_test.go | 86 +++++++++++++++++++++++++++++---------- datetime/example_test.go | 6 ++- 3 files changed, 93 insertions(+), 29 deletions(-) diff --git a/datetime/datetime.go b/datetime/datetime.go index e861da837..e9664c7ae 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -67,6 +67,14 @@ const ( tzOffsetSize = 2 ) +// Limits are from c-dt library: +// https://github.com/tarantool/c-dt/blob/e6214325fe8d4336464ebae859ac2b456fd22b77/API.pod#introduction +// https://github.com/tarantool/tarantool/blob/a99ccce5f517d2a04670289d3d09a8cc2f5916f9/src/lib/core/datetime.h#L44-L61 +const ( + minSeconds = -185604722870400 + maxSeconds = 185480451417600 +) + const maxSize = secondsSize + nsecSize + tzIndexSize + tzOffsetSize type Datetime struct { @@ -74,11 +82,18 @@ type Datetime struct { } // NewDatetime returns a pointer to a new datetime.Datetime that contains a -// specified time.Time. -func NewDatetime(t time.Time) *Datetime { +// specified time.Time. It may returns an error if the Time value is out of +// supported range: [-5879610-06-22T00:00Z .. 5879611-07-11T00:00Z] +func NewDatetime(t time.Time) (*Datetime, error) { + seconds := t.Unix() + + if seconds < minSeconds || seconds > maxSeconds { + return nil, fmt.Errorf("Time %s is out of supported range.", t) + } + dt := new(Datetime) dt.time = t - return dt + return dt, nil } // ToTime returns a time.Time that Datetime contains. @@ -129,10 +144,13 @@ func (tm *Datetime) UnmarshalMsgpack(b []byte) error { dt.tzOffset = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize:])) dt.tzIndex = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize+tzOffsetSize:])) } - tt := time.Unix(dt.seconds, int64(dt.nsec)).UTC() - *tm = *NewDatetime(tt) - return nil + tt := time.Unix(dt.seconds, int64(dt.nsec)).UTC() + dtp, err := NewDatetime(tt) + if dtp != nil { + *tm = *dtp + } + return err } func init() { diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index d5f89a29a..93d292ecd 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -15,10 +15,22 @@ import ( "gopkg.in/vmihailenco/msgpack.v2" ) -var ( - minTime = time.Unix(0, 0) - maxTime = time.Unix(1<<63-1, 999999999) -) +var lesserBoundaryTimes = []time.Time{ + time.Date(-5879610, 06, 22, 0, 0, 1, 0, time.UTC), + time.Date(-5879610, 06, 22, 0, 0, 0, 1, time.UTC), + time.Date(5879611, 07, 10, 23, 59, 59, 0, time.UTC), + time.Date(5879611, 07, 10, 23, 59, 59, 999999999, time.UTC), +} + +var boundaryTimes = []time.Time{ + time.Date(-5879610, 06, 22, 0, 0, 0, 0, time.UTC), + time.Date(5879611, 07, 11, 0, 0, 0, 999999999, time.UTC), +} + +var greaterBoundaryTimes = []time.Time{ + time.Date(-5879610, 06, 21, 23, 59, 59, 999999999, time.UTC), + time.Date(5879611, 07, 11, 0, 0, 1, 0, time.UTC), +} var isDatetimeSupported = false @@ -63,10 +75,15 @@ func assertDatetimeIsEqual(t *testing.T, tuples []interface{}, tm time.Time) { } func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) { - dt := NewDatetime(tm) + t.Helper() + + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Unable to create Datetime from %s: %s", tm, err) + } // Insert tuple with datetime. - _, err := conn.Insert(spaceTuple1, []interface{}{dt, "payload"}) + _, err = conn.Insert(spaceTuple1, []interface{}{dt, "payload"}) if err != nil { t.Fatalf("Datetime insert failed: %s", err.Error()) } @@ -172,22 +189,30 @@ func TestDatetimeInsertSelectDelete(t *testing.T) { // time.Parse() could not parse formatted string with datetime where year is // bigger than 9999. That's why testcase with maximum datetime value represented // as a separate testcase. Testcase with minimal value added for consistency. -func TestDatetimeMax(t *testing.T) { +func TestDatetimeBoundaryRange(t *testing.T) { skipIfDatetimeUnsupported(t) conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - tupleInsertSelectDelete(t, conn, maxTime) + for _, tm := range append(lesserBoundaryTimes, boundaryTimes...) { + t.Run(tm.String(), func(t *testing.T) { + tupleInsertSelectDelete(t, conn, tm) + }) + } } -func TestDatetimeMin(t *testing.T) { +func TestDatetimeOutOfRange(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) - defer conn.Close() - - tupleInsertSelectDelete(t, conn, minTime) + for _, tm := range greaterBoundaryTimes { + t.Run(tm.String(), func(t *testing.T) { + _, err := NewDatetime(tm) + if err == nil { + t.Errorf("Time %s should be unsupported!", tm) + } + }) + } } func TestDatetimeReplace(t *testing.T) { @@ -201,7 +226,10 @@ func TestDatetimeReplace(t *testing.T) { t.Fatalf("Time parse failed: %s", err) } - dt := NewDatetime(tm) + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Unable to create Datetime from %s: %s", tm, err) + } resp, err := conn.Replace(spaceTuple1, []interface{}{dt, "payload"}) if err != nil { t.Fatalf("Datetime replace failed: %s", err) @@ -346,16 +374,24 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - dt1, _ := time.Parse(time.RFC3339, "2010-05-24T17:51:56.000000009Z") - dt2, _ := time.Parse(time.RFC3339, "2022-05-24T17:51:56.000000009Z") + tm1, _ := time.Parse(time.RFC3339, "2010-05-24T17:51:56.000000009Z") + tm2, _ := time.Parse(time.RFC3339, "2022-05-24T17:51:56.000000009Z") + dt1, err := NewDatetime(tm1) + if err != nil { + t.Fatalf("Unable to create Datetime from %s: %s", tm1, err) + } + dt2, err := NewDatetime(tm2) + if err != nil { + t.Fatalf("Unable to create Datetime from %s: %s", tm2, err) + } const cid = 13 const orig = "orig" tuple := Tuple2{Cid: cid, Orig: orig, Events: []Event{ - {*NewDatetime(dt1), "Minsk"}, - {*NewDatetime(dt2), "Moscow"}, + {*dt1, "Minsk"}, + {*dt2, "Moscow"}, }, } resp, err := conn.Replace(spaceTuple2, &tuple) @@ -392,7 +428,7 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { t.Fatalf("Unable to convert 2 field to []interface{}") } - for i, tv := range []time.Time{dt1, dt2} { + for i, tv := range []time.Time{tm1, tm2} { dt := events[i].([]interface{})[1].(Datetime) if !dt.ToTime().Equal(tv) { t.Fatalf("%v != %v", dt.ToTime(), tv) @@ -450,8 +486,11 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) { defer conn.Close() tm := time.Unix(500, 1000) - dt := NewDatetime(tm) - _, err := conn.Insert(spaceTuple1, []interface{}{dt}) + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Unable to create Datetime from %s: %s", tm, err) + } + _, err = conn.Insert(spaceTuple1, []interface{}{dt}) if err != nil { t.Fatalf("Datetime insert failed: %s", err.Error()) } @@ -482,7 +521,10 @@ func TestMPEncode(t *testing.T) { if err != nil { t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) } - dt := NewDatetime(tm) + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Unable to create Datetime from %s: %s", tm, err) + } buf, err := msgpack.Marshal(dt) if err != nil { t.Fatalf("Marshalling failed: %s", err.Error()) diff --git a/datetime/example_test.go b/datetime/example_test.go index 4dbba20dc..97359f43c 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -35,7 +35,11 @@ func Example() { fmt.Printf("error in time.Parse() is %v", err) return } - dt := NewDatetime(tm) + dt, err := NewDatetime(tm) + if err != nil { + fmt.Printf("Unable to create Datetime from %s: %s", tm, err) + return + } space := "testDatetime_1" index := "primary" From 832d4c5ce94013e4f65bc6bb67f0a14b6f787286 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 11 Jul 2022 12:57:34 +0300 Subject: [PATCH 298/605] mod: bump version of go-openssl The patch fixes build with OpenSSL < 1.1.1 [1] on CetnOS 7. 1. https://github.com/tarantool/go-openssl/pull/7 --- CHANGELOG.md | 2 ++ go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 427cf2b33..4459f978a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +- Build with OpenSSL < 1.1.1 (#194) + ## [1.6.0] - 2022-06-01 This release adds a number of features. Also it significantly improves testing, diff --git a/go.mod b/go.mod index 306725105..c10d60795 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.7.1 // indirect - github.com/tarantool/go-openssl v0.0.8-0.20220419150948-be4921aa2f87 + github.com/tarantool/go-openssl v0.0.8-0.20220711094538-d93c1eff4f49 google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/vmihailenco/msgpack.v2 v2.9.2 diff --git a/go.sum b/go.sum index f9ffc6b0a..fb3474afc 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tarantool/go-openssl v0.0.8-0.20220419150948-be4921aa2f87 h1:JGzuBxNBq5saVtPUcuu5Y4+kbJON6H02//OT+RNqGts= -github.com/tarantool/go-openssl v0.0.8-0.20220419150948-be4921aa2f87/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= +github.com/tarantool/go-openssl v0.0.8-0.20220711094538-d93c1eff4f49 h1:rZYYi1cI3QXZ3yRFZd2ItYM1XA2BaJqP0buDroMbjNo= +github.com/tarantool/go-openssl v0.0.8-0.20220711094538-d93c1eff4f49/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= From ac4f61113278bb292c74d542795094a92b94c129 Mon Sep 17 00:00:00 2001 From: Rianov Viacheslav Date: Mon, 30 May 2022 15:24:06 +0300 Subject: [PATCH 299/605] test: fix benchmark tests names When using go test tool with different regexp for filtering test functions, it is sometimes helpful to filter some of them with any prefix like BenchmarkClientSerial or BenchmarkClientParallel. This way it is possible to get results just for one type of load. Follows up #62 Follows up #122 --- tarantool_test.go | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tarantool_test.go b/tarantool_test.go index 64e9c7942..f6b5e530a 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -136,6 +136,39 @@ func BenchmarkClientSerialTyped(b *testing.B) { } } +func BenchmarkClientSerialSQL(b *testing.B) { + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + b.Fatal("Could not check the Tarantool version") + } + if isLess { + b.Skip() + } + + conn, err := Connect(server, opts) + if err != nil { + b.Errorf("Failed to connect: %s", err) + return + } + defer conn.Close() + + spaceNo := 519 + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Errorf("Failed to replace: %s", err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", []interface{}{uint(1111)}) + if err != nil { + b.Errorf("Select failed: %s", err.Error()) + break + } + } +} + func BenchmarkClientFuture(b *testing.B) { var err error @@ -398,7 +431,7 @@ func BenchmarkClientLargeSelectParallel(b *testing.B) { }) } -func BenchmarkSQLParallel(b *testing.B) { +func BenchmarkClientParallelSQL(b *testing.B) { // Tarantool supports SQL since version 2.0.0 isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) if err != nil { From f3f88864f92108ac4be434c2f3d4bc53dbd25dd2 Mon Sep 17 00:00:00 2001 From: Rianov Viacheslav Date: Mon, 11 Jul 2022 11:51:21 +0300 Subject: [PATCH 300/605] code health: add ExecuteTyped/ExecuteAsync to the common interface Added ExecuteTyped/ExecuteAsync implementation to multi package. CHANGELOG.md updated. Follows up #62 --- CHANGELOG.md | 1 + connector.go | 2 ++ multi/multi.go | 10 ++++++++++ 3 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4459f978a..0841286f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed - Build with OpenSSL < 1.1.1 (#194) +- Add `ExecuteAsync` and `ExecuteTyped` to common connector interface (#62) ## [1.6.0] - 2022-06-01 diff --git a/connector.go b/connector.go index cd77d2c4c..dffa29dae 100644 --- a/connector.go +++ b/connector.go @@ -30,6 +30,7 @@ type Connector interface { Call16Typed(functionName string, args interface{}, result interface{}) (err error) Call17Typed(functionName string, args interface{}, result interface{}) (err error) EvalTyped(expr string, args interface{}, result interface{}) (err error) + ExecuteTyped(expr string, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Future InsertAsync(space interface{}, tuple interface{}) *Future @@ -41,6 +42,7 @@ type Connector interface { Call16Async(functionName string, args interface{}) *Future Call17Async(functionName string, args interface{}) *Future EvalAsync(expr string, args interface{}) *Future + ExecuteAsync(expr string, args interface{}) *Future Do(req Request) (fut *Future) } diff --git a/multi/multi.go b/multi/multi.go index 3a98342a7..98e6a25d3 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -419,6 +419,11 @@ func (connMulti *ConnectionMulti) EvalTyped(expr string, args interface{}, resul return connMulti.getCurrentConnection().EvalTyped(expr, args, result) } +// ExecuteTyped passes sql expression to Tarantool for execution. +func (connMulti *ConnectionMulti) ExecuteTyped(expr string, args interface{}, result interface{}) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) { + return connMulti.getCurrentConnection().ExecuteTyped(expr, args, result) +} + // SelectAsync sends select request to Tarantool and returns Future. func (connMulti *ConnectionMulti) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *tarantool.Future { return connMulti.getCurrentConnection().SelectAsync(space, index, offset, limit, iterator, key) @@ -482,6 +487,11 @@ func (connMulti *ConnectionMulti) EvalAsync(expr string, args interface{}) *tara return connMulti.getCurrentConnection().EvalAsync(expr, args) } +// ExecuteAsync passes sql expression to Tarantool for execution. +func (connMulti *ConnectionMulti) ExecuteAsync(expr string, args interface{}) *tarantool.Future { + return connMulti.getCurrentConnection().ExecuteAsync(expr, args) +} + // Do sends the request and returns a future. func (connMulti *ConnectionMulti) Do(req tarantool.Request) *tarantool.Future { return connMulti.getCurrentConnection().Do(req) From e1bb59cf1c8c90df14197d0974a6178a66d46b84 Mon Sep 17 00:00:00 2001 From: Rianov Viacheslav Date: Mon, 11 Jul 2022 19:14:07 +0300 Subject: [PATCH 301/605] sql: support prepared statements This patch adds the support of prepared statements. Added a new type for handling prepared statements. Added new IPROTO-constants for support of prepared statements in const.go. Added benchmarks for SQL-select prepared statement. Added examples of using Prepare in example_test.go. Fixed some grammar inconsistencies for the method Execute. Added a test helper for checking if SQL is supported in connected Tarantool. Updated CHANGELOG.md. Added mock for ConnectedRequest interface for tests. Follows up #62 Closes #117 --- CHANGELOG.md | 1 + connection.go | 17 ++ connection_pool/config.lua | 15 ++ connection_pool/connection_pool.go | 19 ++ connection_pool/connection_pool_test.go | 93 +++++++++ connection_pool/example_test.go | 25 +++ connector.go | 2 + const.go | 3 + errors.go | 4 +- example_test.go | 40 ++++ export_test.go | 18 ++ multi/config.lua | 15 ++ multi/multi.go | 15 ++ multi/multi_test.go | 87 ++++++++ prepared.go | 138 +++++++++++++ request.go | 8 + request_test.go | 68 ++++++ response.go | 17 ++ tarantool_test.go | 261 ++++++++++++++++++------ test_helpers/request_mock.go | 25 +++ test_helpers/utils.go | 13 ++ 21 files changed, 817 insertions(+), 67 deletions(-) create mode 100644 prepared.go create mode 100644 test_helpers/request_mock.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0841286f7..ad693e0f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Public API with request object types (#126) - Support decimal type in msgpack (#96) - Support datetime type in msgpack (#118) +- Prepared SQL statements (#117) ### Changed diff --git a/connection.go b/connection.go index 871890673..6de1e9d01 100644 --- a/connection.go +++ b/connection.go @@ -993,6 +993,13 @@ func (conn *Connection) nextRequestId() (requestId uint32) { // An error is returned if the request was formed incorrectly, or failed to // create the future. func (conn *Connection) Do(req Request) *Future { + if connectedReq, ok := req.(ConnectedRequest); ok { + if connectedReq.Conn() != conn { + fut := NewFuture() + fut.SetError(fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool")) + return fut + } + } return conn.send(req) } @@ -1009,3 +1016,13 @@ func (conn *Connection) OverrideSchema(s *Schema) { conn.Schema = s } } + +// NewPrepared passes a sql statement to Tarantool for preparation synchronously. +func (conn *Connection) NewPrepared(expr string) (*Prepared, error) { + req := NewPrepareRequest(expr) + resp, err := conn.Do(req).Get() + if err != nil { + return nil, err + } + return NewPreparedFromResponse(conn, resp) +} diff --git a/connection_pool/config.lua b/connection_pool/config.lua index b1492dd13..fb3859297 100644 --- a/connection_pool/config.lua +++ b/connection_pool/config.lua @@ -21,6 +21,21 @@ box.once("init", function() parts = {{ field = 1, type = 'string' }}, if_not_exists = true }) + + local sp = box.schema.space.create('SQL_TEST', { + id = 521, + if_not_exists = true, + format = { + {name = "NAME0", type = "unsigned"}, + {name = "NAME1", type = "string"}, + {name = "NAME2", type = "string"}, + } + }) + sp:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) + sp:insert{1, "test", "test"} + -- grants for sql tests + box.schema.user.grant('test', 'create,read,write,drop,alter', 'space') + box.schema.user.grant('test', 'create', 'sequence') end) local function simple_incr(a) diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 0752038f0..ad2e936cc 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -12,6 +12,7 @@ package connection_pool import ( "errors" + "fmt" "log" "sync/atomic" "time" @@ -525,7 +526,16 @@ func (connPool *ConnectionPool) EvalAsync(expr string, args interface{}, userMod } // Do sends the request and returns a future. +// For requests that belong to an only one connection (e.g. Unprepare or ExecutePrepared) +// the argument of type Mode is unused. func (connPool *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarantool.Future { + if connectedReq, ok := req.(tarantool.ConnectedRequest); ok { + conn, _ := connPool.getConnectionFromPool(connectedReq.Conn().Addr()) + if conn == nil { + return newErrorFuture(fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool")) + } + return connectedReq.Conn().Do(req) + } conn, err := connPool.getNextConnection(userMode) if err != nil { return newErrorFuture(err) @@ -788,3 +798,12 @@ func newErrorFuture(err error) *tarantool.Future { fut.SetError(err) return fut } + +// NewPrepared passes a sql statement to Tarantool for preparation synchronously. +func (connPool *ConnectionPool) NewPrepared(expr string, userMode Mode) (*tarantool.Prepared, error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return nil, err + } + return conn.NewPrepared(expr) +} diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 00337005d..2e462e4eb 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -1,8 +1,10 @@ package connection_pool_test import ( + "fmt" "log" "os" + "reflect" "strings" "testing" "time" @@ -1276,6 +1278,97 @@ func TestDo(t *testing.T) { require.NotNilf(t, resp, "response is nil after Ping") } +func TestNewPrepared(t *testing.T) { + test_helpers.SkipIfSQLUnsupported(t) + + roles := []bool{true, true, false, true, false} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + stmt, err := connPool.NewPrepared("SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:id AND NAME1=:name;", connection_pool.RO) + require.Nilf(t, err, "fail to prepare statement: %v", err) + + if connPool.GetPoolInfo()[stmt.Conn.Addr()].ConnRole != connection_pool.RO { + t.Errorf("wrong role for the statement's connection") + } + + executeReq := tarantool.NewExecutePreparedRequest(stmt) + unprepareReq := tarantool.NewUnprepareRequest(stmt) + + resp, err := connPool.Do(executeReq.Args([]interface{}{1, "test"}), connection_pool.ANY).Get() + if err != nil { + t.Fatalf("failed to execute prepared: %v", err) + } + if resp == nil { + t.Fatalf("nil response") + } + if resp.Code != tarantool.OkCode { + t.Fatalf("failed to execute prepared: code %d", resp.Code) + } + if reflect.DeepEqual(resp.Data[0], []interface{}{1, "test"}) { + t.Error("Select with named arguments failed") + } + if resp.MetaData[0].FieldType != "unsigned" || + resp.MetaData[0].FieldName != "NAME0" || + resp.MetaData[1].FieldType != "string" || + resp.MetaData[1].FieldName != "NAME1" { + t.Error("Wrong metadata") + } + + // the second argument for unprepare request is unused - it already belongs to some connection + resp, err = connPool.Do(unprepareReq, connection_pool.ANY).Get() + if err != nil { + t.Errorf("failed to unprepare prepared statement: %v", err) + } + if resp.Code != tarantool.OkCode { + t.Errorf("failed to unprepare prepared statement: code %d", resp.Code) + } + + _, err = connPool.Do(unprepareReq, connection_pool.ANY).Get() + if err == nil { + t.Errorf("the statement must be already unprepared") + } + require.Contains(t, err.Error(), "Prepared statement with id") + + _, err = connPool.Do(executeReq, connection_pool.ANY).Get() + if err == nil { + t.Errorf("the statement must be already unprepared") + } + require.Contains(t, err.Error(), "Prepared statement with id") +} + +func TestDoWithStrangerConn(t *testing.T) { + expectedErr := fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool") + + roles := []bool{true, true, false, true, false} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + req := test_helpers.NewStrangerRequest() + + _, err = connPool.Do(req, connection_pool.ANY).Get() + if err == nil { + t.Fatalf("nil error catched") + } + if err.Error() != expectedErr.Error() { + t.Fatalf("Unexpected error catched") + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go index faf97bc66..08995d03e 100644 --- a/connection_pool/example_test.go +++ b/connection_pool/example_test.go @@ -548,3 +548,28 @@ func ExampleConnectionPool_Do() { // Ping Data [] // Ping Error } + +func ExampleConnectionPool_NewPrepared() { + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + stmt, err := pool.NewPrepared("SELECT 1", connection_pool.ANY) + if err != nil { + fmt.Println(err) + } + + executeReq := tarantool.NewExecutePreparedRequest(stmt) + unprepareReq := tarantool.NewUnprepareRequest(stmt) + + _, err = pool.Do(executeReq, connection_pool.ANY).Get() + if err != nil { + fmt.Printf("Failed to execute prepared stmt") + } + _, err = pool.Do(unprepareReq, connection_pool.ANY).Get() + if err != nil { + fmt.Printf("Failed to prepare") + } +} diff --git a/connector.go b/connector.go index dffa29dae..3084b9124 100644 --- a/connector.go +++ b/connector.go @@ -44,5 +44,7 @@ type Connector interface { EvalAsync(expr string, args interface{}) *Future ExecuteAsync(expr string, args interface{}) *Future + NewPrepared(expr string) (*Prepared, error) + Do(req Request) (fut *Future) } diff --git a/const.go b/const.go index 2ad7f3b76..3d0d7424f 100644 --- a/const.go +++ b/const.go @@ -12,6 +12,7 @@ const ( UpsertRequestCode = 9 Call17RequestCode = 10 /* call in >= 1.7 format */ ExecuteRequestCode = 11 + PrepareRequestCode = 13 PingRequestCode = 64 SubscribeRequestCode = 66 @@ -31,9 +32,11 @@ const ( KeyData = 0x30 KeyError = 0x31 KeyMetaData = 0x32 + KeyBindCount = 0x34 KeySQLText = 0x40 KeySQLBind = 0x41 KeySQLInfo = 0x42 + KeyStmtID = 0x43 KeyFieldName = 0x00 KeyFieldType = 0x01 diff --git a/errors.go b/errors.go index a6895ccbc..760a59c5e 100644 --- a/errors.go +++ b/errors.go @@ -1,8 +1,6 @@ package tarantool -import ( - "fmt" -) +import "fmt" // Error is wrapper around error returned by Tarantool. type Error struct { diff --git a/example_test.go b/example_test.go index 0e5487a53..65dc971a0 100644 --- a/example_test.go +++ b/example_test.go @@ -651,3 +651,43 @@ func ExampleConnection_Execute() { fmt.Println("MetaData", resp.MetaData) fmt.Println("SQL Info", resp.SQLInfo) } + +// To use prepared statements to query a tarantool instance, call NewPrepared. +func ExampleConnection_NewPrepared() { + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil || isLess { + return + } + + server := "127.0.0.1:3013" + opts := tarantool.Opts{ + Timeout: 500 * time.Millisecond, + Reconnect: 1 * time.Second, + MaxReconnects: 3, + User: "test", + Pass: "test", + } + conn, err := tarantool.Connect(server, opts) + if err != nil { + fmt.Printf("Failed to connect: %s", err.Error()) + } + + stmt, err := conn.NewPrepared("SELECT 1") + if err != nil { + fmt.Printf("Failed to connect: %s", err.Error()) + } + + executeReq := tarantool.NewExecutePreparedRequest(stmt) + unprepareReq := tarantool.NewUnprepareRequest(stmt) + + _, err = conn.Do(executeReq).Get() + if err != nil { + fmt.Printf("Failed to execute prepared stmt") + } + + _, err = conn.Do(unprepareReq).Get() + if err != nil { + fmt.Printf("Failed to prepare") + } +} diff --git a/export_test.go b/export_test.go index 8bdfb9812..315f444de 100644 --- a/export_test.go +++ b/export_test.go @@ -75,3 +75,21 @@ func RefImplEvalBody(enc *msgpack.Encoder, expr string, args interface{}) error func RefImplExecuteBody(enc *msgpack.Encoder, expr string, args interface{}) error { return fillExecute(enc, expr, args) } + +// RefImplPrepareBody is reference implementation for filling of an prepare +// request's body. +func RefImplPrepareBody(enc *msgpack.Encoder, expr string) error { + return fillPrepare(enc, expr) +} + +// RefImplUnprepareBody is reference implementation for filling of an execute prepared +// request's body. +func RefImplExecutePreparedBody(enc *msgpack.Encoder, stmt Prepared, args interface{}) error { + return fillExecutePrepared(enc, stmt, args) +} + +// RefImplUnprepareBody is reference implementation for filling of an unprepare +// request's body. +func RefImplUnprepareBody(enc *msgpack.Encoder, stmt Prepared) error { + return fillUnprepare(enc, stmt) +} diff --git a/multi/config.lua b/multi/config.lua index 2b745185a..5d75da513 100644 --- a/multi/config.lua +++ b/multi/config.lua @@ -13,6 +13,21 @@ rawset(_G, 'get_cluster_nodes', get_cluster_nodes) box.once("init", function() box.schema.user.create('test', { password = 'test' }) box.schema.user.grant('test', 'read,write,execute', 'universe') + + local sp = box.schema.space.create('SQL_TEST', { + id = 521, + if_not_exists = true, + format = { + {name = "NAME0", type = "unsigned"}, + {name = "NAME1", type = "string"}, + {name = "NAME2", type = "string"}, + } + }) + sp:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) + sp:insert{1, "test", "test"} + -- grants for sql tests + box.schema.user.grant('test', 'create,read,write,drop,alter', 'space') + box.schema.user.grant('test', 'create', 'sequence') end) local function simple_incr(a) diff --git a/multi/multi.go b/multi/multi.go index 98e6a25d3..03531a817 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -13,6 +13,7 @@ package multi import ( "errors" + "fmt" "sync" "sync/atomic" "time" @@ -492,7 +493,21 @@ func (connMulti *ConnectionMulti) ExecuteAsync(expr string, args interface{}) *t return connMulti.getCurrentConnection().ExecuteAsync(expr, args) } +// NewPrepared passes a sql statement to Tarantool for preparation synchronously. +func (connMulti *ConnectionMulti) NewPrepared(expr string) (*tarantool.Prepared, error) { + return connMulti.getCurrentConnection().NewPrepared(expr) +} + // Do sends the request and returns a future. func (connMulti *ConnectionMulti) Do(req tarantool.Request) *tarantool.Future { + if connectedReq, ok := req.(tarantool.ConnectedRequest); ok { + _, belongs := connMulti.getConnectionFromPool(connectedReq.Conn().Addr()) + if !belongs { + fut := tarantool.NewFuture() + fut.SetError(fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool")) + return fut + } + return connectedReq.Conn().Do(req) + } return connMulti.getCurrentConnection().Do(req) } diff --git a/multi/multi_test.go b/multi/multi_test.go index 0f84cdb4d..628a2ab28 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -1,11 +1,15 @@ package multi import ( + "fmt" "log" "os" + "reflect" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/test_helpers" ) @@ -231,6 +235,89 @@ func TestCall17(t *testing.T) { } } +func TestNewPrepared(t *testing.T) { + test_helpers.SkipIfSQLUnsupported(t) + + multiConn, err := Connect([]string{server1, server2}, connOpts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if multiConn == nil { + t.Fatalf("conn is nil after Connect") + } + defer multiConn.Close() + + stmt, err := multiConn.NewPrepared("SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:id AND NAME1=:name;") + require.Nilf(t, err, "fail to prepare statement: %v", err) + + executeReq := tarantool.NewExecutePreparedRequest(stmt) + unprepareReq := tarantool.NewUnprepareRequest(stmt) + + resp, err := multiConn.Do(executeReq.Args([]interface{}{1, "test"})).Get() + if err != nil { + t.Fatalf("failed to execute prepared: %v", err) + } + if resp == nil { + t.Fatalf("nil response") + } + if resp.Code != tarantool.OkCode { + t.Fatalf("failed to execute prepared: code %d", resp.Code) + } + if reflect.DeepEqual(resp.Data[0], []interface{}{1, "test"}) { + t.Error("Select with named arguments failed") + } + if resp.MetaData[0].FieldType != "unsigned" || + resp.MetaData[0].FieldName != "NAME0" || + resp.MetaData[1].FieldType != "string" || + resp.MetaData[1].FieldName != "NAME1" { + t.Error("Wrong metadata") + } + + // the second argument for unprepare request is unused - it already belongs to some connection + resp, err = multiConn.Do(unprepareReq).Get() + if err != nil { + t.Errorf("failed to unprepare prepared statement: %v", err) + } + if resp.Code != tarantool.OkCode { + t.Errorf("failed to unprepare prepared statement: code %d", resp.Code) + } + + _, err = multiConn.Do(unprepareReq).Get() + if err == nil { + t.Errorf("the statement must be already unprepared") + } + require.Contains(t, err.Error(), "Prepared statement with id") + + _, err = multiConn.Do(executeReq).Get() + if err == nil { + t.Errorf("the statement must be already unprepared") + } + require.Contains(t, err.Error(), "Prepared statement with id") +} + +func TestDoWithStrangerConn(t *testing.T) { + expectedErr := fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool") + + multiConn, err := Connect([]string{server1, server2}, connOpts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if multiConn == nil { + t.Fatalf("conn is nil after Connect") + } + defer multiConn.Close() + + req := test_helpers.NewStrangerRequest() + + _, err = multiConn.Do(req).Get() + if err == nil { + t.Fatalf("nil error catched") + } + if err.Error() != expectedErr.Error() { + t.Fatalf("Unexpected error catched") + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/prepared.go b/prepared.go new file mode 100644 index 000000000..9508f0546 --- /dev/null +++ b/prepared.go @@ -0,0 +1,138 @@ +package tarantool + +import ( + "fmt" + + "gopkg.in/vmihailenco/msgpack.v2" +) + +// PreparedID is a type for Prepared Statement ID +type PreparedID uint64 + +// Prepared is a type for handling prepared statements +// +// Since 1.7.0 +type Prepared struct { + StatementID PreparedID + MetaData []ColumnMetaData + ParamCount uint64 + Conn *Connection +} + +// NewPreparedFromResponse constructs a Prepared object. +func NewPreparedFromResponse(conn *Connection, resp *Response) (*Prepared, error) { + if resp == nil { + return nil, fmt.Errorf("pased nil response") + } + if resp.Data == nil { + return nil, fmt.Errorf("response Data is nil") + } + if len(resp.Data) == 0 { + return nil, fmt.Errorf("response Data format is wrong") + } + stmt, ok := resp.Data[0].(*Prepared) + if !ok { + return nil, fmt.Errorf("response Data format is wrong") + } + stmt.Conn = conn + return stmt, nil +} + +// PrepareRequest helps you to create a prepare request object for execution +// by a Connection. +type PrepareRequest struct { + baseRequest + expr string +} + +// NewPrepareRequest returns a new empty PrepareRequest. +func NewPrepareRequest(expr string) *PrepareRequest { + req := new(PrepareRequest) + req.requestCode = PrepareRequestCode + req.expr = expr + return req +} + +// Body fills an encoder with the execute request body. +func (req *PrepareRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { + return fillPrepare(enc, req.expr) +} + +// UnprepareRequest helps you to create an unprepare request object for +// execution by a Connection. +type UnprepareRequest struct { + baseRequest + stmt *Prepared +} + +// NewUnprepareRequest returns a new empty UnprepareRequest. +func NewUnprepareRequest(stmt *Prepared) *UnprepareRequest { + req := new(UnprepareRequest) + req.requestCode = PrepareRequestCode + req.stmt = stmt + return req +} + +// Conn returns the Connection object the request belongs to +func (req *UnprepareRequest) Conn() *Connection { + return req.stmt.Conn +} + +// Body fills an encoder with the execute request body. +func (req *UnprepareRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { + return fillUnprepare(enc, *req.stmt) +} + +// ExecutePreparedRequest helps you to create an execute prepared request +// object for execution by a Connection. +type ExecutePreparedRequest struct { + baseRequest + stmt *Prepared + args interface{} +} + +// NewExecutePreparedRequest returns a new empty preparedExecuteRequest. +func NewExecutePreparedRequest(stmt *Prepared) *ExecutePreparedRequest { + req := new(ExecutePreparedRequest) + req.requestCode = ExecuteRequestCode + req.stmt = stmt + req.args = []interface{}{} + return req +} + +// Conn returns the Connection object the request belongs to +func (req *ExecutePreparedRequest) Conn() *Connection { + return req.stmt.Conn +} + +// Args sets the args for execute the prepared request. +// Note: default value is empty. +func (req *ExecutePreparedRequest) Args(args interface{}) *ExecutePreparedRequest { + req.args = args + return req +} + +// Body fills an encoder with the execute request body. +func (req *ExecutePreparedRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { + return fillExecutePrepared(enc, *req.stmt, req.args) +} + +func fillPrepare(enc *msgpack.Encoder, expr string) error { + enc.EncodeMapLen(1) + enc.EncodeUint64(KeySQLText) + return enc.EncodeString(expr) +} + +func fillUnprepare(enc *msgpack.Encoder, stmt Prepared) error { + enc.EncodeMapLen(1) + enc.EncodeUint64(KeyStmtID) + return enc.EncodeUint64(uint64(stmt.StatementID)) +} + +func fillExecutePrepared(enc *msgpack.Encoder, stmt Prepared, args interface{}) error { + enc.EncodeMapLen(2) + enc.EncodeUint64(KeyStmtID) + enc.EncodeUint64(uint64(stmt.StatementID)) + enc.EncodeUint64(KeySQLBind) + return encodeSQLBind(enc, args) +} diff --git a/request.go b/request.go index 3b6b33f07..a83094145 100644 --- a/request.go +++ b/request.go @@ -539,6 +539,14 @@ type Request interface { Body(resolver SchemaResolver, enc *msgpack.Encoder) error } +// ConnectedRequest is an interface that provides the info about a Connection +// the request belongs to. +type ConnectedRequest interface { + Request + // Conn returns a Connection the request belongs to. + Conn() *Connection +} + type baseRequest struct { requestCode int32 } diff --git a/request_test.go b/request_test.go index f0da3f865..7c1805155 100644 --- a/request_test.go +++ b/request_test.go @@ -5,6 +5,8 @@ import ( "errors" "testing" + "github.com/stretchr/testify/assert" + . "github.com/tarantool/go-tarantool" "gopkg.in/vmihailenco/msgpack.v2" ) @@ -20,6 +22,8 @@ const validExpr = "any string" // We don't check the value here. const defaultSpace = 0 // And valid too. const defaultIndex = 0 // And valid too. +var validStmt *Prepared = &Prepared{StatementID: 1, Conn: &Connection{}} + type ValidSchemeResolver struct { } @@ -168,6 +172,9 @@ func TestRequestsCodes(t *testing.T) { {req: NewEvalRequest(validExpr), code: EvalRequestCode}, {req: NewExecuteRequest(validExpr), code: ExecuteRequestCode}, {req: NewPingRequest(), code: PingRequestCode}, + {req: NewPrepareRequest(validExpr), code: PrepareRequestCode}, + {req: NewUnprepareRequest(validStmt), code: PrepareRequestCode}, + {req: NewExecutePreparedRequest(validStmt), code: ExecuteRequestCode}, } for _, test := range tests { @@ -517,3 +524,64 @@ func TestExecuteRequestSetters(t *testing.T) { Args(args) assertBodyEqual(t, refBuf.Bytes(), req) } + +func TestPrepareRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplPrepareBody(refEnc, validExpr) + if err != nil { + t.Errorf("An unexpected RefImplPrepareBody() error: %q", err.Error()) + return + } + + req := NewPrepareRequest(validExpr) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestUnprepareRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplUnprepareBody(refEnc, *validStmt) + if err != nil { + t.Errorf("An unexpected RefImplUnprepareBody() error: %q", err.Error()) + return + } + + req := NewUnprepareRequest(validStmt) + assert.Equal(t, req.Conn(), validStmt.Conn) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestExecutePreparedRequestSetters(t *testing.T) { + args := []interface{}{uint(11)} + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplExecutePreparedBody(refEnc, *validStmt, args) + if err != nil { + t.Errorf("An unexpected RefImplExecutePreparedBody() error: %q", err.Error()) + return + } + + req := NewExecutePreparedRequest(validStmt). + Args(args) + assert.Equal(t, req.Conn(), validStmt.Conn) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestExecutePreparedRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplExecutePreparedBody(refEnc, *validStmt, []interface{}{}) + if err != nil { + t.Errorf("An unexpected RefImplExecutePreparedBody() error: %q", err.Error()) + return + } + + req := NewExecutePreparedRequest(validStmt) + assert.Equal(t, req.Conn(), validStmt.Conn) + assertBodyEqual(t, refBuf.Bytes(), req) +} diff --git a/response.go b/response.go index 80b38849b..3fd7322b0 100644 --- a/response.go +++ b/response.go @@ -147,6 +147,7 @@ func (resp *Response) decodeHeader(d *msgpack.Decoder) (err error) { func (resp *Response) decodeBody() (err error) { if resp.buf.Len() > 2 { var l int + var stmtID, bindCount uint64 d := msgpack.NewDecoder(&resp.buf) if l, err = d.DecodeMapLen(); err != nil { return err @@ -178,12 +179,28 @@ func (resp *Response) decodeBody() (err error) { if err = d.Decode(&resp.MetaData); err != nil { return err } + case KeyStmtID: + if stmtID, err = d.DecodeUint64(); err != nil { + return err + } + case KeyBindCount: + if bindCount, err = d.DecodeUint64(); err != nil { + return err + } default: if err = d.Skip(); err != nil { return err } } } + if stmtID != 0 { + stmt := &Prepared{ + StatementID: PreparedID(stmtID), + ParamCount: bindCount, + MetaData: resp.MetaData, + } + resp.Data = []interface{}{stmt} + } if resp.Code != OkCode && resp.Code != PushCode { resp.Code &^= ErrorCodeBit err = Error{resp.Code, resp.Error} diff --git a/tarantool_test.go b/tarantool_test.go index f6b5e530a..06771338c 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -10,6 +10,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" . "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/test_helpers" @@ -137,36 +139,58 @@ func BenchmarkClientSerialTyped(b *testing.B) { } func BenchmarkClientSerialSQL(b *testing.B) { - // Tarantool supports SQL since version 2.0.0 - isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + test_helpers.SkipIfSQLUnsupported(b) + + conn := test_helpers.ConnectWithValidation(b, server, opts) + defer conn.Close() + + spaceNo := 519 + _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { - b.Fatal("Could not check the Tarantool version") - } - if isLess { - b.Skip() + b.Errorf("Failed to replace: %s", err) } - conn, err := Connect(server, opts) - if err != nil { - b.Errorf("Failed to connect: %s", err) - return + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", []interface{}{uint(1111)}) + if err != nil { + b.Errorf("Select failed: %s", err.Error()) + break + } } +} + +func BenchmarkClientSerialSQLPrepared(b *testing.B) { + test_helpers.SkipIfSQLUnsupported(b) + + conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() spaceNo := 519 - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("Failed to replace: %s", err) } + stmt, err := conn.NewPrepared("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?") + if err != nil { + b.Fatalf("failed to prepare a SQL statement") + } + executeReq := NewExecutePreparedRequest(stmt) + unprepareReq := NewUnprepareRequest(stmt) + b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", []interface{}{uint(1111)}) + _, err := conn.Do(executeReq.Args([]interface{}{uint(1111)})).Get() if err != nil { b.Errorf("Select failed: %s", err.Error()) break } } + _, err = conn.Do(unprepareReq).Get() + if err != nil { + b.Fatalf("failed to unprepare a SQL statement") + } } func BenchmarkClientFuture(b *testing.B) { @@ -432,20 +456,13 @@ func BenchmarkClientLargeSelectParallel(b *testing.B) { } func BenchmarkClientParallelSQL(b *testing.B) { - // Tarantool supports SQL since version 2.0.0 - isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) - if err != nil { - b.Fatal("Could not check the Tarantool version") - } - if isLess { - b.Skip() - } + test_helpers.SkipIfSQLUnsupported(b) conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() spaceNo := 519 - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("No connection available") } @@ -462,21 +479,49 @@ func BenchmarkClientParallelSQL(b *testing.B) { }) } -func BenchmarkSQLSerial(b *testing.B) { - // Tarantool supports SQL since version 2.0.0 - isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) +func BenchmarkClientParallelSQLPrepared(b *testing.B) { + test_helpers.SkipIfSQLUnsupported(b) + + conn := test_helpers.ConnectWithValidation(b, server, opts) + defer conn.Close() + + spaceNo := 519 + _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { - b.Fatal("Could not check the Tarantool version") + b.Errorf("No connection available") } - if isLess { - b.Skip() + + stmt, err := conn.NewPrepared("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?") + if err != nil { + b.Fatalf("failed to prepare a SQL statement") } + executeReq := NewExecutePreparedRequest(stmt) + unprepareReq := NewUnprepareRequest(stmt) + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := conn.Do(executeReq.Args([]interface{}{uint(1111)})).Get() + if err != nil { + b.Errorf("Select failed: %s", err.Error()) + break + } + } + }) + _, err = conn.Do(unprepareReq).Get() + if err != nil { + b.Fatalf("failed to unprepare a SQL statement") + } +} + +func BenchmarkSQLSerial(b *testing.B) { + test_helpers.SkipIfSQLUnsupported(b) conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() spaceNo := 519 - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("Failed to replace: %s", err) } @@ -915,14 +960,7 @@ const ( ) func TestSQL(t *testing.T) { - // Tarantool supports SQL since version 2.0.0 - isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) - if err != nil { - t.Fatalf("Could not check the Tarantool version") - } - if isLess { - t.Skip() - } + test_helpers.SkipIfSQLUnsupported(t) type testCase struct { Query string @@ -1094,14 +1132,7 @@ func TestSQL(t *testing.T) { } func TestSQLTyped(t *testing.T) { - // Tarantool supports SQL since version 2.0.0 - isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) - if err != nil { - t.Fatal("Could not check the Tarantool version") - } - if isLess { - t.Skip() - } + test_helpers.SkipIfSQLUnsupported(t) conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() @@ -1123,20 +1154,13 @@ func TestSQLTyped(t *testing.T) { } func TestSQLBindings(t *testing.T) { + test_helpers.SkipIfSQLUnsupported(t) + // Data for test table testData := map[int]string{ 1: "test", } - // Tarantool supports SQL since version 2.0.0 - isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) - if err != nil { - t.Fatal("Could not check the Tarantool version") - } - if isLess { - t.Skip() - } - var resp *Response conn := test_helpers.ConnectWithValidation(t, server, opts) @@ -1183,7 +1207,7 @@ func TestSQLBindings(t *testing.T) { } for _, bind := range namedSQLBinds { - resp, err = conn.Execute(selectNamedQuery2, bind) + resp, err := conn.Execute(selectNamedQuery2, bind) if err != nil { t.Fatalf("Failed to Execute: %s", err.Error()) } @@ -1201,7 +1225,7 @@ func TestSQLBindings(t *testing.T) { } } - resp, err = conn.Execute(selectPosQuery2, sqlBind5) + resp, err := conn.Execute(selectPosQuery2, sqlBind5) if err != nil { t.Fatalf("Failed to Execute: %s", err.Error()) } @@ -1237,21 +1261,14 @@ func TestSQLBindings(t *testing.T) { } func TestStressSQL(t *testing.T) { - // Tarantool supports SQL since version 2.0.0 - isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) - if err != nil { - t.Fatalf("Could not check the Tarantool version") - } - if isLess { - t.Skip() - } + test_helpers.SkipIfSQLUnsupported(t) var resp *Response conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - resp, err = conn.Execute(createTableQuery, []interface{}{}) + resp, err := conn.Execute(createTableQuery, []interface{}{}) if err != nil { t.Fatalf("Failed to Execute: %s", err.Error()) } @@ -1341,6 +1358,122 @@ func TestStressSQL(t *testing.T) { } } +func TestNewPrepared(t *testing.T) { + test_helpers.SkipIfSQLUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + stmt, err := conn.NewPrepared(selectNamedQuery2) + if err != nil { + t.Errorf("failed to prepare: %v", err) + } + + executeReq := NewExecutePreparedRequest(stmt) + unprepareReq := NewUnprepareRequest(stmt) + + resp, err := conn.Do(executeReq.Args([]interface{}{1, "test"})).Get() + if err != nil { + t.Errorf("failed to execute prepared: %v", err) + } + if resp.Code != OkCode { + t.Errorf("failed to execute prepared: code %d", resp.Code) + } + if reflect.DeepEqual(resp.Data[0], []interface{}{1, "test"}) { + t.Error("Select with named arguments failed") + } + if resp.MetaData[0].FieldType != "unsigned" || + resp.MetaData[0].FieldName != "NAME0" || + resp.MetaData[1].FieldType != "string" || + resp.MetaData[1].FieldName != "NAME1" { + t.Error("Wrong metadata") + } + + resp, err = conn.Do(unprepareReq).Get() + if err != nil { + t.Errorf("failed to unprepare prepared statement: %v", err) + } + if resp.Code != OkCode { + t.Errorf("failed to unprepare prepared statement: code %d", resp.Code) + } + + _, err = conn.Do(unprepareReq).Get() + if err == nil { + t.Errorf("the statement must be already unprepared") + } + require.Contains(t, err.Error(), "Prepared statement with id") + + _, err = conn.Do(executeReq).Get() + if err == nil { + t.Errorf("the statement must be already unprepared") + } + require.Contains(t, err.Error(), "Prepared statement with id") + + prepareReq := NewPrepareRequest(selectNamedQuery2) + resp, err = conn.Do(prepareReq).Get() + if err != nil { + t.Errorf("failed to prepare: %v", err) + } + if resp.Data == nil { + t.Errorf("failed to prepare: Data is nil") + } + if resp.Code != OkCode { + t.Errorf("failed to unprepare prepared statement: code %d", resp.Code) + } + + if len(resp.Data) == 0 { + t.Errorf("failed to prepare: response Data has no elements") + } + stmt, ok := resp.Data[0].(*Prepared) + if !ok { + t.Errorf("failed to prepare: failed to cast the response Data to Prepared object") + } + if stmt.StatementID == 0 { + t.Errorf("failed to prepare: statement id is 0") + } +} + +func TestConnection_DoWithStrangerConn(t *testing.T) { + expectedErr := fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool") + + conn1 := &Connection{} + req := test_helpers.NewStrangerRequest() + + _, err := conn1.Do(req).Get() + if err == nil { + t.Fatalf("nil error catched") + } + if err.Error() != expectedErr.Error() { + t.Fatalf("Unexpected error catched") + } +} + +func TestNewPreparedFromResponse(t *testing.T) { + var ( + ErrNilResponsePassed = fmt.Errorf("pased nil response") + ErrNilResponseData = fmt.Errorf("response Data is nil") + ErrWrongDataFormat = fmt.Errorf("response Data format is wrong") + ) + testConn := &Connection{} + testCases := []struct { + name string + resp *Response + expectedError error + }{ + {"ErrNilResponsePassed", nil, ErrNilResponsePassed}, + {"ErrNilResponseData", &Response{Data: nil}, ErrNilResponseData}, + {"ErrWrongDataFormat", &Response{Data: []interface{}{}}, ErrWrongDataFormat}, + {"ErrWrongDataFormat", &Response{Data: []interface{}{"test"}}, ErrWrongDataFormat}, + {"nil", &Response{Data: []interface{}{&Prepared{}}}, nil}, + } + for _, testCase := range testCases { + t.Run("Expecting error "+testCase.name, func(t *testing.T) { + _, err := NewPreparedFromResponse(testConn, testCase.resp) + assert.Equal(t, err, testCase.expectedError) + }) + } +} + func TestSchema(t *testing.T) { var err error diff --git a/test_helpers/request_mock.go b/test_helpers/request_mock.go new file mode 100644 index 000000000..00674a3a7 --- /dev/null +++ b/test_helpers/request_mock.go @@ -0,0 +1,25 @@ +package test_helpers + +import ( + "github.com/tarantool/go-tarantool" + "gopkg.in/vmihailenco/msgpack.v2" +) + +type StrangerRequest struct { +} + +func NewStrangerRequest() *StrangerRequest { + return &StrangerRequest{} +} + +func (sr *StrangerRequest) Code() int32 { + return 0 +} + +func (sr *StrangerRequest) Body(resolver tarantool.SchemaResolver, enc *msgpack.Encoder) error { + return nil +} + +func (sr *StrangerRequest) Conn() *tarantool.Connection { + return &tarantool.Connection{} +} diff --git a/test_helpers/utils.go b/test_helpers/utils.go index b7c8fdc96..e07f34bf8 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -23,3 +23,16 @@ func ConnectWithValidation(t testing.TB, } return conn } + +func SkipIfSQLUnsupported(t testing.TB) { + t.Helper() + + // Tarantool supports SQL since version 2.0.0 + isLess, err := IsTarantoolVersionLess(2, 0, 0) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + if isLess { + t.Skip() + } +} From 395901e6e38ef041bd007c54fe9cae862b56a19c Mon Sep 17 00:00:00 2001 From: Rianov Viacheslav Date: Fri, 15 Jul 2022 10:32:13 +0300 Subject: [PATCH 302/605] api: context support This patch adds the support of using context in API. The API is based on using request objects. Added tests that cover almost all cases of using the context in a query. Added benchamrk tests are equivalent to other, that use the same query but without any context. Closes #48 --- CHANGELOG.md | 1 + connection.go | 207 +++++++++++++++++++++++++--------- example_test.go | 31 +++++ prepared.go | 34 ++++++ request.go | 119 +++++++++++++++++++ tarantool_test.go | 213 ++++++++++++++++++++++++++++++++++- test_helpers/request_mock.go | 6 + 7 files changed, 553 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad693e0f0..00c2ded38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support decimal type in msgpack (#96) - Support datetime type in msgpack (#118) - Prepared SQL statements (#117) +- Context support for request objects (#48) ### Changed diff --git a/connection.go b/connection.go index 6de1e9d01..14902b13f 100644 --- a/connection.go +++ b/connection.go @@ -5,6 +5,7 @@ package tarantool import ( "bufio" "bytes" + "context" "errors" "fmt" "io" @@ -125,8 +126,11 @@ type Connection struct { c net.Conn mutex sync.Mutex // Schema contains schema loaded on connection. - Schema *Schema + Schema *Schema + // requestId contains the last request ID for requests with nil context. requestId uint32 + // contextRequestId contains the last request ID for requests with context. + contextRequestId uint32 // Greeting contains first message sent by Tarantool. Greeting *Greeting @@ -143,16 +147,57 @@ type Connection struct { var _ = Connector(&Connection{}) // Check compatibility with connector interface. +type futureList struct { + first *Future + last **Future +} + +func (list *futureList) findFuture(reqid uint32, fetch bool) *Future { + root := &list.first + for { + fut := *root + if fut == nil { + return nil + } + if fut.requestId == reqid { + if fetch { + *root = fut.next + if fut.next == nil { + list.last = root + } else { + fut.next = nil + } + } + return fut + } + root = &fut.next + } +} + +func (list *futureList) addFuture(fut *Future) { + *list.last = fut + list.last = &fut.next +} + +func (list *futureList) clear(err error, conn *Connection) { + fut := list.first + list.first = nil + list.last = &list.first + for fut != nil { + fut.SetError(err) + conn.markDone(fut) + fut, fut.next = fut.next, nil + } +} + type connShard struct { - rmut sync.Mutex - requests [requestsMap]struct { - first *Future - last **Future - } - bufmut sync.Mutex - buf smallWBuf - enc *msgpack.Encoder - _pad [16]uint64 //nolint: unused,structcheck + rmut sync.Mutex + requests [requestsMap]futureList + requestsWithCtx [requestsMap]futureList + bufmut sync.Mutex + buf smallWBuf + enc *msgpack.Encoder + _pad [16]uint64 //nolint: unused,structcheck } // Greeting is a message sent by Tarantool on connect. @@ -167,6 +212,11 @@ type Opts struct { // push messages are received. If Timeout is zero, any request can be // blocked infinitely. // Also used to setup net.TCPConn.Set(Read|Write)Deadline. + // + // Pay attention, when using contexts with request objects, + // the timeout option for Connection does not affect the lifetime + // of the request. For those purposes use context.WithTimeout() as + // the root context. Timeout time.Duration // Timeout between reconnect attempts. If Reconnect is zero, no // reconnect attempts will be made. @@ -262,12 +312,13 @@ type SslOpts struct { // and will not finish to make attempts on authorization failures. func Connect(addr string, opts Opts) (conn *Connection, err error) { conn = &Connection{ - addr: addr, - requestId: 0, - Greeting: &Greeting{}, - control: make(chan struct{}), - opts: opts, - dec: msgpack.NewDecoder(&smallBuf{}), + addr: addr, + requestId: 0, + contextRequestId: 1, + Greeting: &Greeting{}, + control: make(chan struct{}), + opts: opts, + dec: msgpack.NewDecoder(&smallBuf{}), } maxprocs := uint32(runtime.GOMAXPROCS(-1)) if conn.opts.Concurrency == 0 || conn.opts.Concurrency > maxprocs*128 { @@ -283,8 +334,11 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { conn.shard = make([]connShard, conn.opts.Concurrency) for i := range conn.shard { shard := &conn.shard[i] - for j := range shard.requests { - shard.requests[j].last = &shard.requests[j].first + requestsLists := []*[requestsMap]futureList{&shard.requests, &shard.requestsWithCtx} + for _, requests := range requestsLists { + for j := range requests { + requests[j].last = &requests[j].first + } } } @@ -387,6 +441,13 @@ func (conn *Connection) Handle() interface{} { return conn.opts.Handle } +func (conn *Connection) cancelFuture(fut *Future, err error) { + if fut = conn.fetchFuture(fut.requestId); fut != nil { + fut.SetError(err) + conn.markDone(fut) + } +} + func (conn *Connection) dial() (err error) { var connection net.Conn network := "tcp" @@ -580,15 +641,10 @@ func (conn *Connection) closeConnection(neterr error, forever bool) (err error) } for i := range conn.shard { conn.shard[i].buf.Reset() - requests := &conn.shard[i].requests - for pos := range requests { - fut := requests[pos].first - requests[pos].first = nil - requests[pos].last = &requests[pos].first - for fut != nil { - fut.SetError(neterr) - conn.markDone(fut) - fut, fut.next = fut.next, nil + requestsLists := []*[requestsMap]futureList{&conn.shard[i].requests, &conn.shard[i].requestsWithCtx} + for _, requests := range requestsLists { + for pos := range requests { + requests[pos].clear(neterr, conn) } } } @@ -721,7 +777,7 @@ func (conn *Connection) reader(r *bufio.Reader, c net.Conn) { } } -func (conn *Connection) newFuture() (fut *Future) { +func (conn *Connection) newFuture(ctx context.Context) (fut *Future) { fut = NewFuture() if conn.rlimit != nil && conn.opts.RLimitAction == RLimitDrop { select { @@ -736,7 +792,7 @@ func (conn *Connection) newFuture() (fut *Future) { return } } - fut.requestId = conn.nextRequestId() + fut.requestId = conn.nextRequestId(ctx != nil) shardn := fut.requestId & (conn.opts.Concurrency - 1) shard := &conn.shard[shardn] shard.rmut.Lock() @@ -761,11 +817,20 @@ func (conn *Connection) newFuture() (fut *Future) { return } pos := (fut.requestId / conn.opts.Concurrency) & (requestsMap - 1) - pair := &shard.requests[pos] - *pair.last = fut - pair.last = &fut.next - if conn.opts.Timeout > 0 { - fut.timeout = time.Since(epoch) + conn.opts.Timeout + if ctx != nil { + select { + case <-ctx.Done(): + fut.SetError(fmt.Errorf("context is done")) + shard.rmut.Unlock() + return + default: + } + shard.requestsWithCtx[pos].addFuture(fut) + } else { + shard.requests[pos].addFuture(fut) + if conn.opts.Timeout > 0 { + fut.timeout = time.Since(epoch) + conn.opts.Timeout + } } shard.rmut.Unlock() if conn.rlimit != nil && conn.opts.RLimitAction == RLimitWait { @@ -785,12 +850,43 @@ func (conn *Connection) newFuture() (fut *Future) { return } +// This method removes a future from the internal queue if the context +// is "done" before the response is come. Such select logic is inspired +// from this thread: https://groups.google.com/g/golang-dev/c/jX4oQEls3uk +func (conn *Connection) contextWatchdog(fut *Future, ctx context.Context) { + select { + case <-fut.done: + default: + select { + case <-ctx.Done(): + conn.cancelFuture(fut, fmt.Errorf("context is done")) + default: + select { + case <-fut.done: + case <-ctx.Done(): + conn.cancelFuture(fut, fmt.Errorf("context is done")) + } + } + } +} + func (conn *Connection) send(req Request) *Future { - fut := conn.newFuture() + fut := conn.newFuture(req.Ctx()) if fut.ready == nil { return fut } + if req.Ctx() != nil { + select { + case <-req.Ctx().Done(): + conn.cancelFuture(fut, fmt.Errorf("context is done")) + return fut + default: + } + } conn.putFuture(fut, req) + if req.Ctx() != nil { + go conn.contextWatchdog(fut, req.Ctx()) + } return fut } @@ -877,25 +973,11 @@ func (conn *Connection) fetchFuture(reqid uint32) (fut *Future) { func (conn *Connection) getFutureImp(reqid uint32, fetch bool) *Future { shard := &conn.shard[reqid&(conn.opts.Concurrency-1)] pos := (reqid / conn.opts.Concurrency) & (requestsMap - 1) - pair := &shard.requests[pos] - root := &pair.first - for { - fut := *root - if fut == nil { - return nil - } - if fut.requestId == reqid { - if fetch { - *root = fut.next - if fut.next == nil { - pair.last = root - } else { - fut.next = nil - } - } - return fut - } - root = &fut.next + // futures with even requests id belong to requests list with nil context + if reqid%2 == 0 { + return shard.requests[pos].findFuture(reqid, fetch) + } else { + return shard.requestsWithCtx[pos].findFuture(reqid, fetch) } } @@ -984,8 +1066,12 @@ func (conn *Connection) read(r io.Reader) (response []byte, err error) { return } -func (conn *Connection) nextRequestId() (requestId uint32) { - return atomic.AddUint32(&conn.requestId, 1) +func (conn *Connection) nextRequestId(context bool) (requestId uint32) { + if context { + return atomic.AddUint32(&conn.contextRequestId, 2) + } else { + return atomic.AddUint32(&conn.requestId, 2) + } } // Do performs a request asynchronously on the connection. @@ -1000,6 +1086,15 @@ func (conn *Connection) Do(req Request) *Future { return fut } } + if req.Ctx() != nil { + select { + case <-req.Ctx().Done(): + fut := NewFuture() + fut.SetError(fmt.Errorf("context is done")) + return fut + default: + } + } return conn.send(req) } diff --git a/example_test.go b/example_test.go index 65dc971a0..cd4c7874c 100644 --- a/example_test.go +++ b/example_test.go @@ -1,6 +1,7 @@ package tarantool_test import ( + "context" "fmt" "time" @@ -691,3 +692,33 @@ func ExampleConnection_NewPrepared() { fmt.Printf("Failed to prepare") } } + +// To pass contexts to request objects, use the Context() method. +// Pay attention that when using context with request objects, +// the timeout option for Connection will not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func ExamplePingRequest_Context() { + conn := example_connect() + defer conn.Close() + + timeout := time.Nanosecond + + // this way you may set the common timeout for requests with context + rootCtx, cancelRoot := context.WithTimeout(context.Background(), timeout) + defer cancelRoot() + + // this context will be canceled with the root after commonTimeout + ctx, cancel := context.WithCancel(rootCtx) + defer cancel() + + req := tarantool.NewPingRequest().Context(ctx) + + // Ping a Tarantool instance to check connection. + resp, err := conn.Do(req).Get() + fmt.Println("Ping Resp", resp) + fmt.Println("Ping Error", err) + // Output: + // Ping Resp + // Ping Error context is done +} diff --git a/prepared.go b/prepared.go index 9508f0546..6a41538ed 100644 --- a/prepared.go +++ b/prepared.go @@ -1,6 +1,7 @@ package tarantool import ( + "context" "fmt" "gopkg.in/vmihailenco/msgpack.v2" @@ -58,6 +59,17 @@ func (req *PrepareRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error return fillPrepare(enc, req.expr) } +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *PrepareRequest) Context(ctx context.Context) *PrepareRequest { + req.ctx = ctx + return req +} + // UnprepareRequest helps you to create an unprepare request object for // execution by a Connection. type UnprepareRequest struct { @@ -83,6 +95,17 @@ func (req *UnprepareRequest) Body(res SchemaResolver, enc *msgpack.Encoder) erro return fillUnprepare(enc, *req.stmt) } +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *UnprepareRequest) Context(ctx context.Context) *UnprepareRequest { + req.ctx = ctx + return req +} + // ExecutePreparedRequest helps you to create an execute prepared request // object for execution by a Connection. type ExecutePreparedRequest struct { @@ -117,6 +140,17 @@ func (req *ExecutePreparedRequest) Body(res SchemaResolver, enc *msgpack.Encoder return fillExecutePrepared(enc, *req.stmt, req.args) } +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *ExecutePreparedRequest) Context(ctx context.Context) *ExecutePreparedRequest { + req.ctx = ctx + return req +} + func fillPrepare(enc *msgpack.Encoder, expr string) error { enc.EncodeMapLen(1) enc.EncodeUint64(KeySQLText) diff --git a/request.go b/request.go index a83094145..c708b79b4 100644 --- a/request.go +++ b/request.go @@ -1,6 +1,7 @@ package tarantool import ( + "context" "errors" "reflect" "strings" @@ -537,6 +538,8 @@ type Request interface { Code() int32 // Body fills an encoder with a request body. Body(resolver SchemaResolver, enc *msgpack.Encoder) error + // Ctx returns a context of the request. + Ctx() context.Context } // ConnectedRequest is an interface that provides the info about a Connection @@ -549,6 +552,7 @@ type ConnectedRequest interface { type baseRequest struct { requestCode int32 + ctx context.Context } // Code returns a IPROTO code for the request. @@ -556,6 +560,11 @@ func (req *baseRequest) Code() int32 { return req.requestCode } +// Ctx returns a context of the request. +func (req *baseRequest) Ctx() context.Context { + return req.ctx +} + type spaceRequest struct { baseRequest space interface{} @@ -613,6 +622,17 @@ func (req *PingRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillPing(enc) } +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *PingRequest) Context(ctx context.Context) *PingRequest { + req.ctx = ctx + return req +} + // SelectRequest allows you to create a select request object for execution // by a Connection. type SelectRequest struct { @@ -683,6 +703,17 @@ func (req *SelectRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillSelect(enc, spaceNo, indexNo, req.offset, req.limit, req.iterator, req.key) } +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *SelectRequest) Context(ctx context.Context) *SelectRequest { + req.ctx = ctx + return req +} + // InsertRequest helps you to create an insert request object for execution // by a Connection. type InsertRequest struct { @@ -716,6 +747,17 @@ func (req *InsertRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillInsert(enc, spaceNo, req.tuple) } +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *InsertRequest) Context(ctx context.Context) *InsertRequest { + req.ctx = ctx + return req +} + // ReplaceRequest helps you to create a replace request object for execution // by a Connection. type ReplaceRequest struct { @@ -749,6 +791,17 @@ func (req *ReplaceRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error return fillInsert(enc, spaceNo, req.tuple) } +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *ReplaceRequest) Context(ctx context.Context) *ReplaceRequest { + req.ctx = ctx + return req +} + // DeleteRequest helps you to create a delete request object for execution // by a Connection. type DeleteRequest struct { @@ -789,6 +842,17 @@ func (req *DeleteRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillDelete(enc, spaceNo, indexNo, req.key) } +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *DeleteRequest) Context(ctx context.Context) *DeleteRequest { + req.ctx = ctx + return req +} + // UpdateRequest helps you to create an update request object for execution // by a Connection. type UpdateRequest struct { @@ -840,6 +904,17 @@ func (req *UpdateRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillUpdate(enc, spaceNo, indexNo, req.key, req.ops) } +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *UpdateRequest) Context(ctx context.Context) *UpdateRequest { + req.ctx = ctx + return req +} + // UpsertRequest helps you to create an upsert request object for execution // by a Connection. type UpsertRequest struct { @@ -884,6 +959,17 @@ func (req *UpsertRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillUpsert(enc, spaceNo, req.tuple, req.ops) } +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *UpsertRequest) Context(ctx context.Context) *UpsertRequest { + req.ctx = ctx + return req +} + // CallRequest helps you to create a call request object for execution // by a Connection. type CallRequest struct { @@ -915,6 +1001,17 @@ func (req *CallRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillCall(enc, req.function, req.args) } +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *CallRequest) Context(ctx context.Context) *CallRequest { + req.ctx = ctx + return req +} + // NewCall16Request returns a new empty Call16Request. It uses request code for // Tarantool 1.6. // Deprecated since Tarantool 1.7.2. @@ -961,6 +1058,17 @@ func (req *EvalRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillEval(enc, req.expr, req.args) } +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *EvalRequest) Context(ctx context.Context) *EvalRequest { + req.ctx = ctx + return req +} + // ExecuteRequest helps you to create an execute request object for execution // by a Connection. type ExecuteRequest struct { @@ -989,3 +1097,14 @@ func (req *ExecuteRequest) Args(args interface{}) *ExecuteRequest { func (req *ExecuteRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillExecute(enc, req.expr, req.args) } + +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *ExecuteRequest) Context(ctx context.Context) *ExecuteRequest { + req.ctx = ctx + return req +} diff --git a/tarantool_test.go b/tarantool_test.go index 06771338c..f5360ba6b 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -1,10 +1,12 @@ package tarantool_test import ( + "context" "fmt" "log" "os" "reflect" + "runtime" "strings" "sync" "testing" @@ -100,16 +102,45 @@ func BenchmarkClientSerialRequestObject(b *testing.B) { if err != nil { b.Error(err) } + req := NewSelectRequest(spaceNo). + Index(indexNo). + Offset(0). + Limit(1). + Iterator(IterEq). + Key([]interface{}{uint(1111)}) b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := conn.Do(req).Get() + if err != nil { + b.Error(err) + } + } +} + +func BenchmarkClientSerialRequestObjectWithContext(b *testing.B) { + var err error + + conn := test_helpers.ConnectWithValidation(b, server, opts) + defer conn.Close() + + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Error(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + b.ResetTimer() + for i := 0; i < b.N; i++ { req := NewSelectRequest(spaceNo). Index(indexNo). - Offset(0). Limit(1). Iterator(IterEq). - Key([]interface{}{uint(1111)}) + Key([]interface{}{uint(1111)}). + Context(ctx) _, err := conn.Do(req).Get() if err != nil { b.Error(err) @@ -342,6 +373,131 @@ func BenchmarkClientParallel(b *testing.B) { }) } +func benchmarkClientParallelRequestObject(multiplier int, b *testing.B) { + conn := test_helpers.ConnectWithValidation(b, server, opts) + defer conn.Close() + + _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Fatal("No connection available") + } + + req := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(IterEq). + Key([]interface{}{uint(1111)}) + + b.SetParallelism(multiplier) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = conn.Do(req) + _, err := conn.Do(req).Get() + if err != nil { + b.Error(err) + } + } + }) +} + +func benchmarkClientParallelRequestObjectWithContext(multiplier int, b *testing.B) { + conn := test_helpers.ConnectWithValidation(b, server, opts) + defer conn.Close() + + _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Fatal("No connection available") + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + req := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(IterEq). + Key([]interface{}{uint(1111)}). + Context(ctx) + + b.SetParallelism(multiplier) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = conn.Do(req) + _, err := conn.Do(req).Get() + if err != nil { + b.Error(err) + } + } + }) +} + +func benchmarkClientParallelRequestObjectMixed(multiplier int, b *testing.B) { + conn := test_helpers.ConnectWithValidation(b, server, opts) + defer conn.Close() + + _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Fatal("No connection available") + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + req := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(IterEq). + Key([]interface{}{uint(1111)}) + + reqWithCtx := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(IterEq). + Key([]interface{}{uint(1111)}). + Context(ctx) + + b.SetParallelism(multiplier) + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = conn.Do(req) + _, err := conn.Do(reqWithCtx).Get() + if err != nil { + b.Error(err) + } + } + }) +} + +func BenchmarkClientParallelRequestObject(b *testing.B) { + multipliers := []int{10, 50, 500, 1000} + conn := test_helpers.ConnectWithValidation(b, server, opts) + defer conn.Close() + + _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Fatal("No connection available") + } + + for _, m := range multipliers { + goroutinesNum := runtime.GOMAXPROCS(0) * m + + b.Run(fmt.Sprintf("Plain %d goroutines", goroutinesNum), func(b *testing.B) { + benchmarkClientParallelRequestObject(m, b) + }) + + b.Run(fmt.Sprintf("With Context %d goroutines", goroutinesNum), func(b *testing.B) { + benchmarkClientParallelRequestObjectWithContext(m, b) + }) + + b.Run(fmt.Sprintf("Mixed %d goroutines", goroutinesNum), func(b *testing.B) { + benchmarkClientParallelRequestObjectMixed(m, b) + }) + } +} + func BenchmarkClientParallelMassive(b *testing.B) { conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() @@ -2081,6 +2237,59 @@ func TestClientRequestObjects(t *testing.T) { } } +func TestClientRequestObjectsWithNilContext(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + req := NewPingRequest().Context(nil) //nolint + resp, err := conn.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Ping: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Ping") + } + if len(resp.Data) != 0 { + t.Errorf("Response Body len != 0") + } +} + +func TestClientRequestObjectsWithPassedCanceledContext(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + ctx, cancel := context.WithCancel(context.Background()) + req := NewPingRequest().Context(ctx) + cancel() + resp, err := conn.Do(req).Get() + if err.Error() != "context is done" { + t.Fatalf("Failed to catch an error from done context") + } + if resp != nil { + t.Fatalf("Response is not nil after the occured error") + } +} + +func TestClientRequestObjectsWithContext(t *testing.T) { + var err error + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + ctx, cancel := context.WithCancel(context.Background()) + req := NewPingRequest().Context(ctx) + fut := conn.Do(req) + cancel() + resp, err := fut.Get() + if resp != nil { + t.Fatalf("response must be nil") + } + if err == nil { + t.Fatalf("catched nil error") + } + if err.Error() != "context is done" { + t.Fatalf("wrong error catched: %v", err) + } +} + func TestComplexStructs(t *testing.T) { var err error diff --git a/test_helpers/request_mock.go b/test_helpers/request_mock.go index 00674a3a7..630d57e66 100644 --- a/test_helpers/request_mock.go +++ b/test_helpers/request_mock.go @@ -1,6 +1,8 @@ package test_helpers import ( + "context" + "github.com/tarantool/go-tarantool" "gopkg.in/vmihailenco/msgpack.v2" ) @@ -23,3 +25,7 @@ func (sr *StrangerRequest) Body(resolver tarantool.SchemaResolver, enc *msgpack. func (sr *StrangerRequest) Conn() *tarantool.Connection { return &tarantool.Connection{} } + +func (sr *StrangerRequest) Ctx() context.Context { + return nil +} From 4b066ae3755f0cec247d62bfdc655aa000293099 Mon Sep 17 00:00:00 2001 From: Rianov Viacheslav Date: Fri, 22 Jul 2022 12:59:07 +0300 Subject: [PATCH 303/605] code health: remove the magic padding This patch removes the padding field in a connection shard structure. The _pad field is unused in the code and there is no sense to keep it anymore. Closes #197 --- connection.go | 1 - 1 file changed, 1 deletion(-) diff --git a/connection.go b/connection.go index 14902b13f..6a1829837 100644 --- a/connection.go +++ b/connection.go @@ -197,7 +197,6 @@ type connShard struct { bufmut sync.Mutex buf smallWBuf enc *msgpack.Encoder - _pad [16]uint64 //nolint: unused,structcheck } // Greeting is a message sent by Tarantool on connect. From 5498e2dcbb9f9f7ab58f94b132f1ffa1b1700613 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 19 Jul 2022 14:03:07 +0300 Subject: [PATCH 304/605] bugfix: race conditions in Future methods Before the patch it may be possible to close several times fut.ready and fut.done channels from the public API calls. --- CHANGELOG.md | 1 + future.go | 15 +++++++++------ future_test.go | 23 +++++++++++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00c2ded38..d32d36517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Build with OpenSSL < 1.1.1 (#194) - Add `ExecuteAsync` and `ExecuteTyped` to common connector interface (#62) +- Race conditions in methods of `Future` type (#195) ## [1.6.0] - 2022-06-01 diff --git a/future.go b/future.go index d6ba3743c..e34bc2f65 100644 --- a/future.go +++ b/future.go @@ -129,25 +129,27 @@ func NewFuture() (fut *Future) { // AppendPush appends the push response to the future. // Note: it works only before SetResponse() or SetError() func (fut *Future) AppendPush(resp *Response) { + fut.mutex.Lock() + defer fut.mutex.Unlock() + if fut.isDone() { return } resp.Code = PushCode - fut.mutex.Lock() fut.pushes = append(fut.pushes, resp) - fut.mutex.Unlock() fut.ready <- struct{}{} } // SetResponse sets a response for the future and finishes the future. func (fut *Future) SetResponse(resp *Response) { + fut.mutex.Lock() + defer fut.mutex.Unlock() + if fut.isDone() { return } - fut.mutex.Lock() fut.resp = resp - fut.mutex.Unlock() close(fut.ready) close(fut.done) @@ -155,12 +157,13 @@ func (fut *Future) SetResponse(resp *Response) { // SetError sets an error for the future and finishes the future. func (fut *Future) SetError(err error) { + fut.mutex.Lock() + defer fut.mutex.Unlock() + if fut.isDone() { return } - fut.mutex.Lock() fut.err = err - fut.mutex.Unlock() close(fut.ready) close(fut.done) diff --git a/future_test.go b/future_test.go index d6d800530..bae974158 100644 --- a/future_test.go +++ b/future_test.go @@ -233,3 +233,26 @@ func TestFutureGetIteratorError(t *testing.T) { } } } + +func TestFutureSetStateRaceCondition(t *testing.T) { + err := errors.New("any error") + resp := &Response{} + respAppend := &Response{} + + for i := 0; i < 1000; i++ { + fut := NewFuture() + for j := 0; j < 9; j++ { + go func(opt int) { + if opt%3 == 0 { + fut.AppendPush(respAppend) + } else if opt%3 == 1 { + fut.SetError(err) + } else { + fut.SetResponse(resp) + } + }(j) + } + } + // It may be false-positive, but very rarely - it's ok for such very + // simple race conditions tests. +} From 609268f8a6143aba7332150f226b3836ac7ee2f7 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 19 Jul 2022 14:21:15 +0300 Subject: [PATCH 305/605] bugfix: usage of nil in Connection.peekFuture() It may be possible to get a nil value from conn.getFutureImp(). We need to check the value before using. --- CHANGELOG.md | 1 + connection.go | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d32d36517..3a840c13c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Build with OpenSSL < 1.1.1 (#194) - Add `ExecuteAsync` and `ExecuteTyped` to common connector interface (#62) - Race conditions in methods of `Future` type (#195) +- Usage of nil pointer in Connection.peekFuture (#195) ## [1.6.0] - 2022-06-01 diff --git a/connection.go b/connection.go index 6a1829837..e5595b5b1 100644 --- a/connection.go +++ b/connection.go @@ -949,11 +949,12 @@ func (conn *Connection) peekFuture(reqid uint32) (fut *Future) { defer shard.rmut.Unlock() if conn.opts.Timeout > 0 { - fut = conn.getFutureImp(reqid, true) - pair := &shard.requests[pos] - *pair.last = fut - pair.last = &fut.next - fut.timeout = time.Since(epoch) + conn.opts.Timeout + if fut = conn.getFutureImp(reqid, true); fut != nil { + pair := &shard.requests[pos] + *pair.last = fut + pair.last = &fut.next + fut.timeout = time.Since(epoch) + conn.opts.Timeout + } } else { fut = conn.getFutureImp(reqid, false) } From 1e37dc2c8624a84f56b9b1fb53f1be1f4d8fcc77 Mon Sep 17 00:00:00 2001 From: AnaNek Date: Tue, 19 Jul 2022 18:27:42 +0300 Subject: [PATCH 306/605] code health: place `fill*` functions in the beginning of the files File `requests.go` has `fill*` functions as well as `prepare.go` file. We should sync with `requests.go` and place `fill*` functions in the beginning of `prepare.go` file. Follows up #117 Part of #101 --- prepared.go | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/prepared.go b/prepared.go index 6a41538ed..0f9303344 100644 --- a/prepared.go +++ b/prepared.go @@ -20,6 +20,26 @@ type Prepared struct { Conn *Connection } +func fillPrepare(enc *msgpack.Encoder, expr string) error { + enc.EncodeMapLen(1) + enc.EncodeUint64(KeySQLText) + return enc.EncodeString(expr) +} + +func fillUnprepare(enc *msgpack.Encoder, stmt Prepared) error { + enc.EncodeMapLen(1) + enc.EncodeUint64(KeyStmtID) + return enc.EncodeUint64(uint64(stmt.StatementID)) +} + +func fillExecutePrepared(enc *msgpack.Encoder, stmt Prepared, args interface{}) error { + enc.EncodeMapLen(2) + enc.EncodeUint64(KeyStmtID) + enc.EncodeUint64(uint64(stmt.StatementID)) + enc.EncodeUint64(KeySQLBind) + return encodeSQLBind(enc, args) +} + // NewPreparedFromResponse constructs a Prepared object. func NewPreparedFromResponse(conn *Connection, resp *Response) (*Prepared, error) { if resp == nil { @@ -150,23 +170,3 @@ func (req *ExecutePreparedRequest) Context(ctx context.Context) *ExecutePrepared req.ctx = ctx return req } - -func fillPrepare(enc *msgpack.Encoder, expr string) error { - enc.EncodeMapLen(1) - enc.EncodeUint64(KeySQLText) - return enc.EncodeString(expr) -} - -func fillUnprepare(enc *msgpack.Encoder, stmt Prepared) error { - enc.EncodeMapLen(1) - enc.EncodeUint64(KeyStmtID) - return enc.EncodeUint64(uint64(stmt.StatementID)) -} - -func fillExecutePrepared(enc *msgpack.Encoder, stmt Prepared, args interface{}) error { - enc.EncodeMapLen(2) - enc.EncodeUint64(KeyStmtID) - enc.EncodeUint64(uint64(stmt.StatementID)) - enc.EncodeUint64(KeySQLBind) - return encodeSQLBind(enc, args) -} From c0ca261d31e3ba4fd9973e3a40944f7d1e0271ca Mon Sep 17 00:00:00 2001 From: AnaNek Date: Fri, 10 Jun 2022 13:26:57 +0300 Subject: [PATCH 307/605] streams: interactive transactions and support The main purpose of streams is transactions via iproto. Since v. 2.10.0, Tarantool supports streams and interactive transactions over them. Each stream can start its own transaction, so they allows multiplexing several transactions over one connection. API for this feature is the following: * `NewStream()` method to create a stream object for `Connection` and `NewStream(userMode Mode)` method to create a stream object for `ConnectionPool` * stream object `Stream` with `Do()` method and new request objects to work with stream, `BeginRequest` - start transaction via iproto stream; `CommitRequest` - commit transaction; `RollbackRequest` - rollback transaction. Closes #101 --- CHANGELOG.md | 1 + config.lua | 1 + connection.go | 47 ++- connection_pool/config.lua | 1 + connection_pool/connection_pool.go | 14 + connection_pool/connection_pool_test.go | 314 ++++++++++++++++++- connection_pool/example_test.go | 261 ++++++++++++++++ connector.go | 1 + const.go | 6 + example_test.go | 230 ++++++++++++++ export_test.go | 18 ++ multi/config.lua | 7 + multi/multi.go | 9 + multi/multi_test.go | 253 +++++++++++++++- request_test.go | 65 ++++ stream.go | 202 +++++++++++++ tarantool_test.go | 384 +++++++++++++++++++++++- test_helpers/main.go | 5 + test_helpers/utils.go | 30 ++ 19 files changed, 1816 insertions(+), 33 deletions(-) create mode 100644 stream.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a840c13c..97b6e07d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support datetime type in msgpack (#118) - Prepared SQL statements (#117) - Context support for request objects (#48) +- Streams and interactive transactions support (#101) ### Changed diff --git a/config.lua b/config.lua index abea45742..9ec12f7d5 100644 --- a/config.lua +++ b/config.lua @@ -2,6 +2,7 @@ -- able to send requests until everything is configured. box.cfg{ work_dir = os.getenv("TEST_TNT_WORK_DIR"), + memtx_use_mvcc_engine = os.getenv("TEST_TNT_MEMTX_USE_MVCC_ENGINE") == 'true' or nil, } box.once("init", function() diff --git a/connection.go b/connection.go index e5595b5b1..a1e485ba8 100644 --- a/connection.go +++ b/connection.go @@ -20,6 +20,7 @@ import ( ) const requestsMap = 128 +const ignoreStreamId = 0 const ( connDisconnected = 0 connConnected = 1 @@ -143,6 +144,8 @@ type Connection struct { state uint32 dec *msgpack.Decoder lenbuf [PacketLengthBytes]byte + + lastStreamId uint64 } var _ = Connector(&Connection{}) // Check compatibility with connector interface. @@ -528,16 +531,27 @@ func (conn *Connection) dial() (err error) { return } -func pack(h *smallWBuf, enc *msgpack.Encoder, reqid uint32, req Request, res SchemaResolver) (err error) { +func pack(h *smallWBuf, enc *msgpack.Encoder, reqid uint32, + req Request, streamId uint64, res SchemaResolver) (err error) { hl := h.Len() - h.Write([]byte{ + + hMapLen := byte(0x82) // 2 element map. + if streamId != ignoreStreamId { + hMapLen = byte(0x83) // 3 element map. + } + hBytes := []byte{ 0xce, 0, 0, 0, 0, // Length. - 0x82, // 2 element map. + hMapLen, KeyCode, byte(req.Code()), // Request code. KeySync, 0xce, byte(reqid >> 24), byte(reqid >> 16), byte(reqid >> 8), byte(reqid), - }) + } + if streamId != ignoreStreamId { + hBytes = append(hBytes, KeyStreamId, byte(streamId)) + } + + h.Write(hBytes) if err = req.Body(res, enc); err != nil { return @@ -555,7 +569,7 @@ func pack(h *smallWBuf, enc *msgpack.Encoder, reqid uint32, req Request, res Sch func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { var packet smallWBuf req := newAuthRequest(conn.opts.User, string(scramble)) - err = pack(&packet, msgpack.NewEncoder(&packet), 0, req, conn.Schema) + err = pack(&packet, msgpack.NewEncoder(&packet), 0, req, ignoreStreamId, conn.Schema) if err != nil { return errors.New("auth: pack error " + err.Error()) @@ -869,7 +883,7 @@ func (conn *Connection) contextWatchdog(fut *Future, ctx context.Context) { } } -func (conn *Connection) send(req Request) *Future { +func (conn *Connection) send(req Request, streamId uint64) *Future { fut := conn.newFuture(req.Ctx()) if fut.ready == nil { return fut @@ -882,14 +896,14 @@ func (conn *Connection) send(req Request) *Future { default: } } - conn.putFuture(fut, req) + conn.putFuture(fut, req, streamId) if req.Ctx() != nil { go conn.contextWatchdog(fut, req.Ctx()) } return fut } -func (conn *Connection) putFuture(fut *Future, req Request) { +func (conn *Connection) putFuture(fut *Future, req Request, streamId uint64) { shardn := fut.requestId & (conn.opts.Concurrency - 1) shard := &conn.shard[shardn] shard.bufmut.Lock() @@ -906,7 +920,7 @@ func (conn *Connection) putFuture(fut *Future, req Request) { } blen := shard.buf.Len() reqid := fut.requestId - if err := pack(&shard.buf, shard.enc, reqid, req, conn.Schema); err != nil { + if err := pack(&shard.buf, shard.enc, reqid, req, streamId, conn.Schema); err != nil { shard.buf.Trunc(blen) shard.bufmut.Unlock() if f := conn.fetchFuture(reqid); f == fut { @@ -1095,7 +1109,7 @@ func (conn *Connection) Do(req Request) *Future { default: } } - return conn.send(req) + return conn.send(req, ignoreStreamId) } // ConfiguredTimeout returns a timeout from connection config. @@ -1121,3 +1135,16 @@ func (conn *Connection) NewPrepared(expr string) (*Prepared, error) { } return NewPreparedFromResponse(conn, resp) } + +// NewStream creates new Stream object for connection. +// +// Since v. 2.10.0, Tarantool supports streams and interactive transactions over them. +// To use interactive transactions, memtx_use_mvcc_engine box option should be set to true. +// Since 1.7.0 +func (conn *Connection) NewStream() (*Stream, error) { + next := atomic.AddUint64(&conn.lastStreamId, 1) + return &Stream{ + Id: next, + Conn: conn, + }, nil +} diff --git a/connection_pool/config.lua b/connection_pool/config.lua index fb3859297..4df392ba8 100644 --- a/connection_pool/config.lua +++ b/connection_pool/config.lua @@ -2,6 +2,7 @@ -- able to send requests until everything is configured. box.cfg{ work_dir = os.getenv("TEST_TNT_WORK_DIR"), + memtx_use_mvcc_engine = os.getenv("TEST_TNT_MEMTX_USE_MVCC_ENGINE") == 'true' or nil, } box.once("init", function() diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index ad2e936cc..1b080fc1f 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -544,6 +544,20 @@ func (connPool *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarant return conn.Do(req) } +// NewStream creates new Stream object for connection selected +// by userMode from connPool. +// +// Since v. 2.10.0, Tarantool supports streams and interactive transactions over them. +// To use interactive transactions, memtx_use_mvcc_engine box option should be set to true. +// Since 1.7.0 +func (connPool *ConnectionPool) NewStream(userMode Mode) (*tarantool.Stream, error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return nil, err + } + return conn.NewStream() +} + // // private // diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 2e462e4eb..4dc3ac12c 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -1369,6 +1369,300 @@ func TestDoWithStrangerConn(t *testing.T) { } } +func TestStream_Commit(t *testing.T) { + var req tarantool.Request + var resp *tarantool.Response + var err error + + test_helpers.SkipIfStreamsUnsupported(t) + + roles := []bool{true, true, false, true, true} + + err = test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + defer connPool.Close() + + stream, err := connPool.NewStream(connection_pool.PreferRW) + require.Nilf(t, err, "failed to create stream") + require.NotNilf(t, connPool, "stream is nil after NewStream") + + // Begin transaction + req = tarantool.NewBeginRequest() + resp, err = stream.Do(req).Get() + require.Nilf(t, err, "failed to Begin") + require.NotNilf(t, resp, "response is nil after Begin") + require.Equalf(t, tarantool.OkCode, resp.Code, "failed to Begin: wrong code returned") + + // Insert in stream + req = tarantool.NewInsertRequest(spaceName). + Tuple([]interface{}{"commit_key", "commit_value"}) + resp, err = stream.Do(req).Get() + require.Nilf(t, err, "failed to Insert") + require.NotNilf(t, resp, "response is nil after Insert") + require.Equalf(t, tarantool.OkCode, resp.Code, "failed to Insert: wrong code returned") + + // Connect to servers[2] to check if tuple + // was inserted outside of stream on RW instance + // before transaction commit + conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) + defer conn.Close() + + // Select not related to the transaction + // while transaction is not committed + // result of select is empty + selectReq := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{"commit_key"}) + resp, err = conn.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + + // Select in stream + resp, err = stream.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 1, len(resp.Data), "response Body len != 1 after Select") + + tpl, ok := resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok := tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "commit_key", key, "unexpected body of Select (0)") + + value, ok := tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "commit_value", value, "unexpected body of Select (1)") + + // Commit transaction + req = tarantool.NewCommitRequest() + resp, err = stream.Do(req).Get() + require.Nilf(t, err, "failed to Commit") + require.NotNilf(t, resp, "response is nil after Commit") + require.Equalf(t, tarantool.OkCode, resp.Code, "failed to Commit: wrong code returned") + + // Select outside of transaction + resp, err = conn.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + + tpl, ok = resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok = tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "commit_key", key, "unexpected body of Select (0)") + + value, ok = tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "commit_value", value, "unexpected body of Select (1)") +} + +func TestStream_Rollback(t *testing.T) { + var req tarantool.Request + var resp *tarantool.Response + var err error + + test_helpers.SkipIfStreamsUnsupported(t) + + roles := []bool{true, true, false, true, true} + + err = test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + defer connPool.Close() + + stream, err := connPool.NewStream(connection_pool.PreferRW) + require.Nilf(t, err, "failed to create stream") + require.NotNilf(t, connPool, "stream is nil after NewStream") + + // Begin transaction + req = tarantool.NewBeginRequest() + resp, err = stream.Do(req).Get() + require.Nilf(t, err, "failed to Begin") + require.NotNilf(t, resp, "response is nil after Begin") + require.Equalf(t, tarantool.OkCode, resp.Code, "failed to Begin: wrong code returned") + + // Insert in stream + req = tarantool.NewInsertRequest(spaceName). + Tuple([]interface{}{"rollback_key", "rollback_value"}) + resp, err = stream.Do(req).Get() + require.Nilf(t, err, "failed to Insert") + require.NotNilf(t, resp, "response is nil after Insert") + require.Equalf(t, tarantool.OkCode, resp.Code, "failed to Insert: wrong code returned") + + // Connect to servers[2] to check if tuple + // was not inserted outside of stream on RW instance + conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) + defer conn.Close() + + // Select not related to the transaction + // while transaction is not committed + // result of select is empty + selectReq := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{"rollback_key"}) + resp, err = conn.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + + // Select in stream + resp, err = stream.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 1, len(resp.Data), "response Body len != 1 after Select") + + tpl, ok := resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok := tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "rollback_key", key, "unexpected body of Select (0)") + + value, ok := tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "rollback_value", value, "unexpected body of Select (1)") + + // Rollback transaction + req = tarantool.NewRollbackRequest() + resp, err = stream.Do(req).Get() + require.Nilf(t, err, "failed to Rollback") + require.NotNilf(t, resp, "response is nil after Rollback") + require.Equalf(t, tarantool.OkCode, resp.Code, "failed to Rollback: wrong code returned") + + // Select outside of transaction + resp, err = conn.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 0, len(resp.Data), "response Body len != 0 after Select") + + // Select inside of stream after rollback + resp, err = stream.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 0, len(resp.Data), "response Body len != 0 after Select") +} + +func TestStream_TxnIsolationLevel(t *testing.T) { + var req tarantool.Request + var resp *tarantool.Response + var err error + + txnIsolationLevels := []tarantool.TxnIsolationLevel{ + tarantool.DefaultIsolationLevel, + tarantool.ReadCommittedLevel, + tarantool.ReadConfirmedLevel, + tarantool.BestEffortLevel, + } + + test_helpers.SkipIfStreamsUnsupported(t) + + roles := []bool{true, true, false, true, true} + + err = test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + defer connPool.Close() + + stream, err := connPool.NewStream(connection_pool.PreferRW) + require.Nilf(t, err, "failed to create stream") + require.NotNilf(t, connPool, "stream is nil after NewStream") + + // Connect to servers[2] to check if tuple + // was not inserted outside of stream on RW instance + conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) + defer conn.Close() + + for _, level := range txnIsolationLevels { + // Begin transaction + req = tarantool.NewBeginRequest().TxnIsolation(level).Timeout(500 * time.Millisecond) + resp, err = stream.Do(req).Get() + require.Nilf(t, err, "failed to Begin") + require.NotNilf(t, resp, "response is nil after Begin") + require.Equalf(t, tarantool.OkCode, resp.Code, "wrong code returned") + + // Insert in stream + req = tarantool.NewInsertRequest(spaceName). + Tuple([]interface{}{"level_key", "level_value"}) + resp, err = stream.Do(req).Get() + require.Nilf(t, err, "failed to Insert") + require.NotNilf(t, resp, "response is nil after Insert") + require.Equalf(t, tarantool.OkCode, resp.Code, "wrong code returned") + + // Select not related to the transaction + // while transaction is not committed + // result of select is empty + selectReq := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{"level_key"}) + resp, err = conn.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + + // Select in stream + resp, err = stream.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 1, len(resp.Data), "response Body len != 1 after Select") + + tpl, ok := resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 2, len(tpl), "unexpected body of Select") + + key, ok := tpl[0].(string) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, "level_key", key, "unexpected body of Select (0)") + + value, ok := tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "level_value", value, "unexpected body of Select (1)") + + // Rollback transaction + req = tarantool.NewRollbackRequest() + resp, err = stream.Do(req).Get() + require.Nilf(t, err, "failed to Rollback") + require.NotNilf(t, resp, "response is nil after Rollback") + require.Equalf(t, tarantool.OkCode, resp.Code, "wrong code returned") + + // Select outside of transaction + resp, err = conn.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + + // Select inside of stream after rollback + resp, err = stream.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + + test_helpers.DeleteRecordByKey(t, conn, spaceNo, indexNo, []interface{}{"level_key"}) + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body @@ -1383,15 +1677,21 @@ func runTestMain(m *testing.M) int { "work_dir1", "work_dir2", "work_dir3", "work_dir4", "work_dir5"} - var err error + + // Tarantool supports streams and interactive transactions since version 2.10.0 + isStreamUnsupported, err := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil { + log.Fatalf("Could not check the Tarantool version") + } instances, err = test_helpers.StartTarantoolInstances(servers, workDirs, test_helpers.StartOpts{ - InitScript: initScript, - User: connOpts.User, - Pass: connOpts.Pass, - WaitStart: waitStart, - ConnectRetry: connectRetry, - RetryTimeout: retryTimeout, + InitScript: initScript, + User: connOpts.User, + Pass: connOpts.Pass, + WaitStart: waitStart, + ConnectRetry: connectRetry, + RetryTimeout: retryTimeout, + MemtxUseMvccEngine: !isStreamUnsupported, }) if err != nil { diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go index 08995d03e..7f6c34700 100644 --- a/connection_pool/example_test.go +++ b/connection_pool/example_test.go @@ -2,6 +2,7 @@ package connection_pool_test import ( "fmt" + "time" "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/connection_pool" @@ -573,3 +574,263 @@ func ExampleConnectionPool_NewPrepared() { fmt.Printf("Failed to prepare") } } + +func ExampleCommitRequest() { + var req tarantool.Request + var resp *tarantool.Response + var err error + + // Tarantool supports streams and interactive transactions since version 2.10.0 + isLess, _ := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil || isLess { + return + } + + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + return + } + defer pool.Close() + + // example pool has only one rw instance + stream, err := pool.NewStream(connection_pool.RW) + if err != nil { + fmt.Println(err) + return + } + + // Begin transaction + req = tarantool.NewBeginRequest() + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Begin: %s", err.Error()) + return + } + fmt.Printf("Begin transaction: response is %#v\n", resp.Code) + + // Insert in stream + req = tarantool.NewInsertRequest(spaceName). + Tuple([]interface{}{"example_commit_key", "example_commit_value"}) + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Insert: %s", err.Error()) + return + } + fmt.Printf("Insert in stream: response is %#v\n", resp.Code) + + // Select not related to the transaction + // while transaction is not commited + // result of select is empty + selectReq := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{"example_commit_key"}) + resp, err = pool.Do(selectReq, connection_pool.RW).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select out of stream before commit: response is %#v\n", resp.Data) + + // Select in stream + resp, err = stream.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select in stream: response is %#v\n", resp.Data) + + // Commit transaction + req = tarantool.NewCommitRequest() + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Commit: %s", err.Error()) + return + } + fmt.Printf("Commit transaction: response is %#v\n", resp.Code) + + // Select outside of transaction + // example pool has only one rw instance + resp, err = pool.Do(selectReq, connection_pool.RW).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select after commit: response is %#v\n", resp.Data) +} + +func ExampleRollbackRequest() { + var req tarantool.Request + var resp *tarantool.Response + var err error + + // Tarantool supports streams and interactive transactions since version 2.10.0 + isLess, _ := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil || isLess { + return + } + + // example pool has only one rw instance + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + return + } + defer pool.Close() + + stream, err := pool.NewStream(connection_pool.RW) + if err != nil { + fmt.Println(err) + return + } + + // Begin transaction + req = tarantool.NewBeginRequest() + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Begin: %s", err.Error()) + return + } + fmt.Printf("Begin transaction: response is %#v\n", resp.Code) + + // Insert in stream + req = tarantool.NewInsertRequest(spaceName). + Tuple([]interface{}{"example_rollback_key", "example_rollback_value"}) + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Insert: %s", err.Error()) + return + } + fmt.Printf("Insert in stream: response is %#v\n", resp.Code) + + // Select not related to the transaction + // while transaction is not commited + // result of select is empty + selectReq := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{"example_rollback_key"}) + resp, err = pool.Do(selectReq, connection_pool.RW).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select out of stream: response is %#v\n", resp.Data) + + // Select in stream + resp, err = stream.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select in stream: response is %#v\n", resp.Data) + + // Rollback transaction + req = tarantool.NewRollbackRequest() + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Rollback: %s", err.Error()) + return + } + fmt.Printf("Rollback transaction: response is %#v\n", resp.Code) + + // Select outside of transaction + // example pool has only one rw instance + resp, err = pool.Do(selectReq, connection_pool.RW).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select after Rollback: response is %#v\n", resp.Data) +} + +func ExampleBeginRequest_TxnIsolation() { + var req tarantool.Request + var resp *tarantool.Response + var err error + + // Tarantool supports streams and interactive transactions since version 2.10.0 + isLess, _ := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil || isLess { + return + } + + // example pool has only one rw instance + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + return + } + defer pool.Close() + + stream, err := pool.NewStream(connection_pool.RW) + if err != nil { + fmt.Println(err) + return + } + + // Begin transaction + req = tarantool.NewBeginRequest(). + TxnIsolation(tarantool.ReadConfirmedLevel). + Timeout(500 * time.Millisecond) + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Begin: %s", err.Error()) + return + } + fmt.Printf("Begin transaction: response is %#v\n", resp.Code) + + // Insert in stream + req = tarantool.NewInsertRequest(spaceName). + Tuple([]interface{}{"isolation_level_key", "isolation_level_value"}) + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Insert: %s", err.Error()) + return + } + fmt.Printf("Insert in stream: response is %#v\n", resp.Code) + + // Select not related to the transaction + // while transaction is not commited + // result of select is empty + selectReq := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{"isolation_level_key"}) + resp, err = pool.Do(selectReq, connection_pool.RW).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select out of stream: response is %#v\n", resp.Data) + + // Select in stream + resp, err = stream.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select in stream: response is %#v\n", resp.Data) + + // Rollback transaction + req = tarantool.NewRollbackRequest() + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Rollback: %s", err.Error()) + return + } + fmt.Printf("Rollback transaction: response is %#v\n", resp.Code) + + // Select outside of transaction + // example pool has only one rw instance + resp, err = pool.Do(selectReq, connection_pool.RW).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select after Rollback: response is %#v\n", resp.Data) +} diff --git a/connector.go b/connector.go index 3084b9124..d6c44c8dd 100644 --- a/connector.go +++ b/connector.go @@ -45,6 +45,7 @@ type Connector interface { ExecuteAsync(expr string, args interface{}) *Future NewPrepared(expr string) (*Prepared, error) + NewStream() (*Stream, error) Do(req Request) (fut *Future) } diff --git a/const.go b/const.go index 3d0d7424f..4a3cb6833 100644 --- a/const.go +++ b/const.go @@ -13,11 +13,15 @@ const ( Call17RequestCode = 10 /* call in >= 1.7 format */ ExecuteRequestCode = 11 PrepareRequestCode = 13 + BeginRequestCode = 14 + CommitRequestCode = 15 + RollbackRequestCode = 16 PingRequestCode = 64 SubscribeRequestCode = 66 KeyCode = 0x00 KeySync = 0x01 + KeyStreamId = 0x0a KeySpaceNo = 0x10 KeyIndexNo = 0x11 KeyLimit = 0x12 @@ -37,6 +41,8 @@ const ( KeySQLBind = 0x41 KeySQLInfo = 0x42 KeyStmtID = 0x43 + KeyTimeout = 0x56 + KeyTxnIsolation = 0x59 KeyFieldName = 0x00 KeyFieldType = 0x01 diff --git a/example_test.go b/example_test.go index cd4c7874c..df7dad770 100644 --- a/example_test.go +++ b/example_test.go @@ -228,6 +228,236 @@ func ExampleUpsertRequest() { // response is []interface {}{[]interface {}{0x459, "first", "updated"}} } +func ExampleCommitRequest() { + var req tarantool.Request + var resp *tarantool.Response + var err error + + // Tarantool supports streams and interactive transactions since version 2.10.0 + isLess, _ := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil || isLess { + return + } + + conn := example_connect() + defer conn.Close() + + stream, _ := conn.NewStream() + + // Begin transaction + req = tarantool.NewBeginRequest() + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Begin: %s", err.Error()) + return + } + fmt.Printf("Begin transaction: response is %#v\n", resp.Code) + + // Insert in stream + req = tarantool.NewInsertRequest(spaceName). + Tuple([]interface{}{uint(1001), "commit_hello", "commit_world"}) + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Insert: %s", err.Error()) + return + } + fmt.Printf("Insert in stream: response is %#v\n", resp.Code) + + // Select not related to the transaction + // while transaction is not commited + // result of select is empty + selectReq := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{uint(1001)}) + resp, err = conn.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select out of stream before commit: response is %#v\n", resp.Data) + + // Select in stream + resp, err = stream.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select in stream: response is %#v\n", resp.Data) + + // Commit transaction + req = tarantool.NewCommitRequest() + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Commit: %s", err.Error()) + return + } + fmt.Printf("Commit transaction: response is %#v\n", resp.Code) + + // Select outside of transaction + resp, err = conn.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select after commit: response is %#v\n", resp.Data) +} + +func ExampleRollbackRequest() { + var req tarantool.Request + var resp *tarantool.Response + var err error + + // Tarantool supports streams and interactive transactions since version 2.10.0 + isLess, _ := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil || isLess { + return + } + + conn := example_connect() + defer conn.Close() + + stream, _ := conn.NewStream() + + // Begin transaction + req = tarantool.NewBeginRequest() + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Begin: %s", err.Error()) + return + } + fmt.Printf("Begin transaction: response is %#v\n", resp.Code) + + // Insert in stream + req = tarantool.NewInsertRequest(spaceName). + Tuple([]interface{}{uint(2001), "rollback_hello", "rollback_world"}) + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Insert: %s", err.Error()) + return + } + fmt.Printf("Insert in stream: response is %#v\n", resp.Code) + + // Select not related to the transaction + // while transaction is not commited + // result of select is empty + selectReq := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{uint(2001)}) + resp, err = conn.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select out of stream: response is %#v\n", resp.Data) + + // Select in stream + resp, err = stream.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select in stream: response is %#v\n", resp.Data) + + // Rollback transaction + req = tarantool.NewRollbackRequest() + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Rollback: %s", err.Error()) + return + } + fmt.Printf("Rollback transaction: response is %#v\n", resp.Code) + + // Select outside of transaction + resp, err = conn.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select after Rollback: response is %#v\n", resp.Data) +} + +func ExampleBeginRequest_TxnIsolation() { + var req tarantool.Request + var resp *tarantool.Response + var err error + + // Tarantool supports streams and interactive transactions since version 2.10.0 + isLess, _ := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil || isLess { + return + } + + conn := example_connect() + defer conn.Close() + + stream, _ := conn.NewStream() + + // Begin transaction + req = tarantool.NewBeginRequest(). + TxnIsolation(tarantool.ReadConfirmedLevel). + Timeout(500 * time.Millisecond) + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Begin: %s", err.Error()) + return + } + fmt.Printf("Begin transaction: response is %#v\n", resp.Code) + + // Insert in stream + req = tarantool.NewInsertRequest(spaceName). + Tuple([]interface{}{uint(2001), "rollback_hello", "rollback_world"}) + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Insert: %s", err.Error()) + return + } + fmt.Printf("Insert in stream: response is %#v\n", resp.Code) + + // Select not related to the transaction + // while transaction is not commited + // result of select is empty + selectReq := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{uint(2001)}) + resp, err = conn.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select out of stream: response is %#v\n", resp.Data) + + // Select in stream + resp, err = stream.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select in stream: response is %#v\n", resp.Data) + + // Rollback transaction + req = tarantool.NewRollbackRequest() + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Rollback: %s", err.Error()) + return + } + fmt.Printf("Rollback transaction: response is %#v\n", resp.Code) + + // Select outside of transaction + resp, err = conn.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select after Rollback: response is %#v\n", resp.Data) +} + func ExampleFuture_GetIterator() { conn := example_connect() defer conn.Close() diff --git a/export_test.go b/export_test.go index 315f444de..0492c1a5b 100644 --- a/export_test.go +++ b/export_test.go @@ -93,3 +93,21 @@ func RefImplExecutePreparedBody(enc *msgpack.Encoder, stmt Prepared, args interf func RefImplUnprepareBody(enc *msgpack.Encoder, stmt Prepared) error { return fillUnprepare(enc, stmt) } + +// RefImplBeginBody is reference implementation for filling of an begin +// request's body. +func RefImplBeginBody(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, timeout time.Duration) error { + return fillBegin(enc, txnIsolation, timeout) +} + +// RefImplCommitBody is reference implementation for filling of an commit +// request's body. +func RefImplCommitBody(enc *msgpack.Encoder) error { + return fillCommit(enc) +} + +// RefImplRollbackBody is reference implementation for filling of an rollback +// request's body. +func RefImplRollbackBody(enc *msgpack.Encoder) error { + return fillRollback(enc) +} diff --git a/multi/config.lua b/multi/config.lua index 5d75da513..0f032b204 100644 --- a/multi/config.lua +++ b/multi/config.lua @@ -4,6 +4,7 @@ local nodes_load = require("config_load_nodes") -- able to send requests until everything is configured. box.cfg{ work_dir = os.getenv("TEST_TNT_WORK_DIR"), + memtx_use_mvcc_engine = os.getenv("TEST_TNT_MEMTX_USE_MVCC_ENGINE") == 'true' or nil, } -- Function to call for getting address list, part of tarantool/multi API. @@ -11,6 +12,12 @@ local get_cluster_nodes = nodes_load.get_cluster_nodes rawset(_G, 'get_cluster_nodes', get_cluster_nodes) box.once("init", function() + local s = box.schema.space.create('test', { + id = 517, + if_not_exists = true, + }) + s:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) + box.schema.user.create('test', { password = 'test' }) box.schema.user.grant('test', 'read,write,execute', 'universe') diff --git a/multi/multi.go b/multi/multi.go index 03531a817..67f450c5c 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -498,6 +498,15 @@ func (connMulti *ConnectionMulti) NewPrepared(expr string) (*tarantool.Prepared, return connMulti.getCurrentConnection().NewPrepared(expr) } +// NewStream creates new Stream object for connection. +// +// Since v. 2.10.0, Tarantool supports streams and interactive transactions over them. +// To use interactive transactions, memtx_use_mvcc_engine box option should be set to true. +// Since 1.7.0 +func (connMulti *ConnectionMulti) NewStream() (*tarantool.Stream, error) { + return connMulti.getCurrentConnection().NewStream() +} + // Do sends the request and returns a future. func (connMulti *ConnectionMulti) Do(req tarantool.Request) *tarantool.Future { if connectedReq, ok := req.(tarantool.ConnectedRequest); ok { diff --git a/multi/multi_test.go b/multi/multi_test.go index 628a2ab28..b4cdf18af 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -16,6 +16,9 @@ import ( var server1 = "127.0.0.1:3013" var server2 = "127.0.0.1:3014" +var spaceNo = uint32(517) +var spaceName = "test" +var indexNo = uint32(0) var connOpts = tarantool.Opts{ Timeout: 500 * time.Millisecond, User: "test", @@ -318,6 +321,233 @@ func TestDoWithStrangerConn(t *testing.T) { } } +func TestStream_Commit(t *testing.T) { + var req tarantool.Request + var resp *tarantool.Response + var err error + + test_helpers.SkipIfStreamsUnsupported(t) + + multiConn, err := Connect([]string{server1, server2}, connOpts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if multiConn == nil { + t.Fatalf("conn is nil after Connect") + } + defer multiConn.Close() + + stream, _ := multiConn.NewStream() + + // Begin transaction + req = tarantool.NewBeginRequest() + resp, err = stream.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Begin: %s", err.Error()) + } + if resp.Code != tarantool.OkCode { + t.Fatalf("Failed to Begin: wrong code returned %d", resp.Code) + } + + // Insert in stream + req = tarantool.NewInsertRequest(spaceName). + Tuple([]interface{}{uint(1001), "hello2", "world2"}) + resp, err = stream.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Insert: %s", err.Error()) + } + if resp.Code != tarantool.OkCode { + t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) + } + defer test_helpers.DeleteRecordByKey(t, multiConn, spaceNo, indexNo, []interface{}{uint(1001)}) + + // Select not related to the transaction + // while transaction is not committed + // result of select is empty + selectReq := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{uint(1001)}) + resp, err = multiConn.Do(selectReq).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != 0 { + t.Fatalf("Response Data len != 0") + } + + // Select in stream + resp, err = stream.Do(selectReq).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != 1 { + t.Fatalf("Response Data len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Fatalf("Unexpected body of Select") + } else { + if id, ok := tpl[0].(uint64); !ok || id != 1001 { + t.Fatalf("Unexpected body of Select (0)") + } + if h, ok := tpl[1].(string); !ok || h != "hello2" { + t.Fatalf("Unexpected body of Select (1)") + } + if h, ok := tpl[2].(string); !ok || h != "world2" { + t.Fatalf("Unexpected body of Select (2)") + } + } + + // Commit transaction + req = tarantool.NewCommitRequest() + resp, err = stream.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Commit: %s", err.Error()) + } + if resp.Code != tarantool.OkCode { + t.Fatalf("Failed to Commit: wrong code returned %d", resp.Code) + } + + // Select outside of transaction + resp, err = multiConn.Do(selectReq).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != 1 { + t.Fatalf("Response Data len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Fatalf("Unexpected body of Select") + } else { + if id, ok := tpl[0].(uint64); !ok || id != 1001 { + t.Fatalf("Unexpected body of Select (0)") + } + if h, ok := tpl[1].(string); !ok || h != "hello2" { + t.Fatalf("Unexpected body of Select (1)") + } + if h, ok := tpl[2].(string); !ok || h != "world2" { + t.Fatalf("Unexpected body of Select (2)") + } + } +} + +func TestStream_Rollback(t *testing.T) { + var req tarantool.Request + var resp *tarantool.Response + var err error + + test_helpers.SkipIfStreamsUnsupported(t) + + multiConn, err := Connect([]string{server1, server2}, connOpts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if multiConn == nil { + t.Fatalf("conn is nil after Connect") + } + defer multiConn.Close() + + stream, _ := multiConn.NewStream() + + // Begin transaction + req = tarantool.NewBeginRequest() + resp, err = stream.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Begin: %s", err.Error()) + } + if resp.Code != tarantool.OkCode { + t.Fatalf("Failed to Begin: wrong code returned %d", resp.Code) + } + + // Insert in stream + req = tarantool.NewInsertRequest(spaceName). + Tuple([]interface{}{uint(1001), "hello2", "world2"}) + resp, err = stream.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Insert: %s", err.Error()) + } + if resp.Code != tarantool.OkCode { + t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) + } + defer test_helpers.DeleteRecordByKey(t, multiConn, spaceNo, indexNo, []interface{}{uint(1001)}) + + // Select not related to the transaction + // while transaction is not committed + // result of select is empty + selectReq := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{uint(1001)}) + resp, err = multiConn.Do(selectReq).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != 0 { + t.Fatalf("Response Data len != 0") + } + + // Select in stream + resp, err = stream.Do(selectReq).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != 1 { + t.Fatalf("Response Data len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Fatalf("Unexpected body of Select") + } else { + if id, ok := tpl[0].(uint64); !ok || id != 1001 { + t.Fatalf("Unexpected body of Select (0)") + } + if h, ok := tpl[1].(string); !ok || h != "hello2" { + t.Fatalf("Unexpected body of Select (1)") + } + if h, ok := tpl[2].(string); !ok || h != "world2" { + t.Fatalf("Unexpected body of Select (2)") + } + } + + // Rollback transaction + req = tarantool.NewRollbackRequest() + resp, err = stream.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Rollback: %s", err.Error()) + } + if resp.Code != tarantool.OkCode { + t.Fatalf("Failed to Rollback: wrong code returned %d", resp.Code) + } + + // Select outside of transaction + resp, err = multiConn.Do(selectReq).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != 0 { + t.Fatalf("Response Data len != 0") + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body @@ -329,15 +559,22 @@ func runTestMain(m *testing.M) int { var connectRetry uint = 3 retryTimeout := 500 * time.Millisecond + // Tarantool supports streams and interactive transactions since version 2.10.0 + isStreamUnsupported, err := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil { + log.Fatalf("Could not check the Tarantool version") + } + inst1, err := test_helpers.StartTarantool(test_helpers.StartOpts{ - InitScript: initScript, - Listen: server1, - WorkDir: "work_dir1", - User: connOpts.User, - Pass: connOpts.Pass, - WaitStart: waitStart, - ConnectRetry: connectRetry, - RetryTimeout: retryTimeout, + InitScript: initScript, + Listen: server1, + WorkDir: "work_dir1", + User: connOpts.User, + Pass: connOpts.Pass, + WaitStart: waitStart, + ConnectRetry: connectRetry, + RetryTimeout: retryTimeout, + MemtxUseMvccEngine: !isStreamUnsupported, }) defer test_helpers.StopTarantoolWithCleanup(inst1) diff --git a/request_test.go b/request_test.go index 7c1805155..b1a558b59 100644 --- a/request_test.go +++ b/request_test.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "testing" + "time" "github.com/stretchr/testify/assert" @@ -22,6 +23,11 @@ const validExpr = "any string" // We don't check the value here. const defaultSpace = 0 // And valid too. const defaultIndex = 0 // And valid too. +const defaultIsolationLevel = DefaultIsolationLevel +const defaultTimeout = 0 + +const validTimeout = 500 * time.Millisecond + var validStmt *Prepared = &Prepared{StatementID: 1, Conn: &Connection{}} type ValidSchemeResolver struct { @@ -175,6 +181,9 @@ func TestRequestsCodes(t *testing.T) { {req: NewPrepareRequest(validExpr), code: PrepareRequestCode}, {req: NewUnprepareRequest(validStmt), code: PrepareRequestCode}, {req: NewExecutePreparedRequest(validStmt), code: ExecuteRequestCode}, + {req: NewBeginRequest(), code: BeginRequestCode}, + {req: NewCommitRequest(), code: CommitRequestCode}, + {req: NewRollbackRequest(), code: RollbackRequestCode}, } for _, test := range tests { @@ -585,3 +594,59 @@ func TestExecutePreparedRequestDefaultValues(t *testing.T) { assert.Equal(t, req.Conn(), validStmt.Conn) assertBodyEqual(t, refBuf.Bytes(), req) } + +func TestBeginRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplBeginBody(refEnc, defaultIsolationLevel, defaultTimeout) + if err != nil { + t.Errorf("An unexpected RefImplBeginBody() error: %q", err.Error()) + return + } + + req := NewBeginRequest() + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestBeginRequestSetters(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplBeginBody(refEnc, ReadConfirmedLevel, validTimeout) + if err != nil { + t.Errorf("An unexpected RefImplBeginBody() error: %q", err.Error()) + return + } + + req := NewBeginRequest().TxnIsolation(ReadConfirmedLevel).Timeout(validTimeout) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestCommitRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplCommitBody(refEnc) + if err != nil { + t.Errorf("An unexpected RefImplCommitBody() error: %q", err.Error()) + return + } + + req := NewCommitRequest() + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestRollbackRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplRollbackBody(refEnc) + if err != nil { + t.Errorf("An unexpected RefImplRollbackBody() error: %q", err.Error()) + return + } + + req := NewRollbackRequest() + assertBodyEqual(t, refBuf.Bytes(), req) +} diff --git a/stream.go b/stream.go new file mode 100644 index 000000000..fdfe8408c --- /dev/null +++ b/stream.go @@ -0,0 +1,202 @@ +package tarantool + +import ( + "context" + "fmt" + "time" + + "gopkg.in/vmihailenco/msgpack.v2" +) + +type TxnIsolationLevel uint + +const ( + // By default, the isolation level of Tarantool is serializable. + DefaultIsolationLevel TxnIsolationLevel = 0 + // The ReadCommittedLevel isolation level makes visible all transactions + // that started commit (stream.Do(NewCommitRequest()) was called). + ReadCommittedLevel TxnIsolationLevel = 1 + // The ReadConfirmedLevel isolation level makes visible all transactions + // that finished the commit (stream.Do(NewCommitRequest()) was returned). + ReadConfirmedLevel TxnIsolationLevel = 2 + // If the BestEffortLevel (serializable) isolation level becomes unreachable, + // the transaction is marked as «conflicted» and can no longer be committed. + BestEffortLevel TxnIsolationLevel = 3 +) + +type Stream struct { + Id uint64 + Conn *Connection +} + +func fillBegin(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, timeout time.Duration) error { + hasTimeout := timeout > 0 + hasIsolationLevel := txnIsolation != DefaultIsolationLevel + mapLen := 0 + if hasTimeout { + mapLen += 1 + } + if hasIsolationLevel { + mapLen += 1 + } + + err := enc.EncodeMapLen(mapLen) + if err != nil { + return err + } + + if hasTimeout { + err = enc.EncodeUint64(KeyTimeout) + if err != nil { + return err + } + + err = enc.Encode(timeout.Seconds()) + if err != nil { + return err + } + } + + if hasIsolationLevel { + err = enc.EncodeUint(KeyTxnIsolation) + if err != nil { + return err + } + + err = enc.Encode(txnIsolation) + if err != nil { + return err + } + } + + return err +} + +func fillCommit(enc *msgpack.Encoder) error { + return enc.EncodeMapLen(0) +} + +func fillRollback(enc *msgpack.Encoder) error { + return enc.EncodeMapLen(0) +} + +// BeginRequest helps you to create a begin request object for execution +// by a Stream. +// Begin request can not be processed out of stream. +type BeginRequest struct { + baseRequest + txnIsolation TxnIsolationLevel + timeout time.Duration +} + +// NewBeginRequest returns a new BeginRequest. +func NewBeginRequest() *BeginRequest { + req := new(BeginRequest) + req.requestCode = BeginRequestCode + req.txnIsolation = DefaultIsolationLevel + return req +} + +// TxnIsolation sets the the transaction isolation level for transaction manager. +// By default, the isolation level of Tarantool is serializable. +func (req *BeginRequest) TxnIsolation(txnIsolation TxnIsolationLevel) *BeginRequest { + req.txnIsolation = txnIsolation + return req +} + +// WithTimeout allows to set up a timeout for call BeginRequest. +func (req *BeginRequest) Timeout(timeout time.Duration) *BeginRequest { + req.timeout = timeout + return req +} + +// Body fills an encoder with the begin request body. +func (req *BeginRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { + return fillBegin(enc, req.txnIsolation, req.timeout) +} + +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *BeginRequest) Context(ctx context.Context) *BeginRequest { + req.ctx = ctx + return req +} + +// CommitRequest helps you to create a commit request object for execution +// by a Stream. +// Commit request can not be processed out of stream. +type CommitRequest struct { + baseRequest +} + +// NewCommitRequest returns a new CommitRequest. +func NewCommitRequest() *CommitRequest { + req := new(CommitRequest) + req.requestCode = CommitRequestCode + return req +} + +// Body fills an encoder with the commit request body. +func (req *CommitRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { + return fillCommit(enc) +} + +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *CommitRequest) Context(ctx context.Context) *CommitRequest { + req.ctx = ctx + return req +} + +// RollbackRequest helps you to create a rollback request object for execution +// by a Stream. +// Rollback request can not be processed out of stream. +type RollbackRequest struct { + baseRequest +} + +// NewRollbackRequest returns a new RollbackRequest. +func NewRollbackRequest() *RollbackRequest { + req := new(RollbackRequest) + req.requestCode = RollbackRequestCode + return req +} + +// Body fills an encoder with the rollback request body. +func (req *RollbackRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { + return fillRollback(enc) +} + +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *RollbackRequest) Context(ctx context.Context) *RollbackRequest { + req.ctx = ctx + return req +} + +// Do verifies, sends the request and returns a future. +// +// An error is returned if the request was formed incorrectly, or failure to +// create the future. +func (s *Stream) Do(req Request) *Future { + if connectedReq, ok := req.(ConnectedRequest); ok { + if connectedReq.Conn() != s.Conn { + fut := NewFuture() + fut.SetError(fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool")) + return fut + } + } + return s.Conn.send(req, s.Id) +} diff --git a/tarantool_test.go b/tarantool_test.go index f5360ba6b..de4062c9e 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2319,21 +2319,389 @@ func TestComplexStructs(t *testing.T) { } } +func TestStream_Commit(t *testing.T) { + var req Request + var resp *Response + var err error + var conn *Connection + + test_helpers.SkipIfStreamsUnsupported(t) + + conn = test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + stream, _ := conn.NewStream() + + // Begin transaction + req = NewBeginRequest() + resp, err = stream.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Begin: %s", err.Error()) + } + if resp.Code != OkCode { + t.Fatalf("Failed to Begin: wrong code returned %d", resp.Code) + } + + // Insert in stream + req = NewInsertRequest(spaceName). + Tuple([]interface{}{uint(1001), "hello2", "world2"}) + resp, err = stream.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Insert: %s", err.Error()) + } + if resp.Code != OkCode { + t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) + } + defer test_helpers.DeleteRecordByKey(t, conn, spaceNo, indexNo, []interface{}{uint(1001)}) + + // Select not related to the transaction + // while transaction is not committed + // result of select is empty + selectReq := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(IterEq). + Key([]interface{}{uint(1001)}) + resp, err = conn.Do(selectReq).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != 0 { + t.Fatalf("Response Data len != 0") + } + + // Select in stream + resp, err = stream.Do(selectReq).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != 1 { + t.Fatalf("Response Data len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Fatalf("Unexpected body of Select") + } else { + if id, ok := tpl[0].(uint64); !ok || id != 1001 { + t.Fatalf("Unexpected body of Select (0)") + } + if h, ok := tpl[1].(string); !ok || h != "hello2" { + t.Fatalf("Unexpected body of Select (1)") + } + if h, ok := tpl[2].(string); !ok || h != "world2" { + t.Fatalf("Unexpected body of Select (2)") + } + } + + // Commit transaction + req = NewCommitRequest() + resp, err = stream.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Commit: %s", err.Error()) + } + if resp.Code != OkCode { + t.Fatalf("Failed to Commit: wrong code returned %d", resp.Code) + } + + // Select outside of transaction + resp, err = conn.Do(selectReq).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != 1 { + t.Fatalf("Response Data len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Fatalf("Unexpected body of Select") + } else { + if id, ok := tpl[0].(uint64); !ok || id != 1001 { + t.Fatalf("Unexpected body of Select (0)") + } + if h, ok := tpl[1].(string); !ok || h != "hello2" { + t.Fatalf("Unexpected body of Select (1)") + } + if h, ok := tpl[2].(string); !ok || h != "world2" { + t.Fatalf("Unexpected body of Select (2)") + } + } +} + +func TestStream_Rollback(t *testing.T) { + var req Request + var resp *Response + var err error + var conn *Connection + + test_helpers.SkipIfStreamsUnsupported(t) + + conn = test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + stream, _ := conn.NewStream() + + // Begin transaction + req = NewBeginRequest() + resp, err = stream.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Begin: %s", err.Error()) + } + if resp.Code != OkCode { + t.Fatalf("Failed to Begin: wrong code returned %d", resp.Code) + } + + // Insert in stream + req = NewInsertRequest(spaceName). + Tuple([]interface{}{uint(1001), "hello2", "world2"}) + resp, err = stream.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Insert: %s", err.Error()) + } + if resp.Code != OkCode { + t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) + } + defer test_helpers.DeleteRecordByKey(t, conn, spaceNo, indexNo, []interface{}{uint(1001)}) + + // Select not related to the transaction + // while transaction is not committed + // result of select is empty + selectReq := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(IterEq). + Key([]interface{}{uint(1001)}) + resp, err = conn.Do(selectReq).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != 0 { + t.Fatalf("Response Data len != 0") + } + + // Select in stream + resp, err = stream.Do(selectReq).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != 1 { + t.Fatalf("Response Data len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Fatalf("Unexpected body of Select") + } else { + if id, ok := tpl[0].(uint64); !ok || id != 1001 { + t.Fatalf("Unexpected body of Select (0)") + } + if h, ok := tpl[1].(string); !ok || h != "hello2" { + t.Fatalf("Unexpected body of Select (1)") + } + if h, ok := tpl[2].(string); !ok || h != "world2" { + t.Fatalf("Unexpected body of Select (2)") + } + } + + // Rollback transaction + req = NewRollbackRequest() + resp, err = stream.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Rollback: %s", err.Error()) + } + if resp.Code != OkCode { + t.Fatalf("Failed to Rollback: wrong code returned %d", resp.Code) + } + + // Select outside of transaction + resp, err = conn.Do(selectReq).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != 0 { + t.Fatalf("Response Data len != 0") + } + + // Select inside of stream after rollback + resp, err = stream.Do(selectReq).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != 0 { + t.Fatalf("Response Data len != 0") + } +} + +func TestStream_TxnIsolationLevel(t *testing.T) { + var req Request + var resp *Response + var err error + var conn *Connection + + txnIsolationLevels := []TxnIsolationLevel{ + DefaultIsolationLevel, + ReadCommittedLevel, + ReadConfirmedLevel, + BestEffortLevel, + } + + test_helpers.SkipIfStreamsUnsupported(t) + + conn = test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + stream, _ := conn.NewStream() + + for _, level := range txnIsolationLevels { + // Begin transaction + req = NewBeginRequest().TxnIsolation(level).Timeout(500 * time.Millisecond) + resp, err = stream.Do(req).Get() + require.Nilf(t, err, "failed to Begin") + require.NotNilf(t, resp, "response is nil after Begin") + require.Equalf(t, OkCode, resp.Code, "wrong code returned") + + // Insert in stream + req = NewInsertRequest(spaceName). + Tuple([]interface{}{uint(1001), "hello2", "world2"}) + resp, err = stream.Do(req).Get() + require.Nilf(t, err, "failed to Insert") + require.NotNilf(t, resp, "response is nil after Insert") + require.Equalf(t, OkCode, resp.Code, "wrong code returned") + + // Select not related to the transaction + // while transaction is not committed + // result of select is empty + selectReq := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(IterEq). + Key([]interface{}{uint(1001)}) + resp, err = conn.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + + // Select in stream + resp, err = stream.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 1, len(resp.Data), "response Body len != 1 after Select") + + tpl, ok := resp.Data[0].([]interface{}) + require.Truef(t, ok, "unexpected body of Select") + require.Equalf(t, 3, len(tpl), "unexpected body of Select") + + key, ok := tpl[0].(uint64) + require.Truef(t, ok, "unexpected body of Select (0)") + require.Equalf(t, uint64(1001), key, "unexpected body of Select (0)") + + value1, ok := tpl[1].(string) + require.Truef(t, ok, "unexpected body of Select (1)") + require.Equalf(t, "hello2", value1, "unexpected body of Select (1)") + + value2, ok := tpl[2].(string) + require.Truef(t, ok, "unexpected body of Select (2)") + require.Equalf(t, "world2", value2, "unexpected body of Select (2)") + + // Rollback transaction + req = NewRollbackRequest() + resp, err = stream.Do(req).Get() + require.Nilf(t, err, "failed to Rollback") + require.NotNilf(t, resp, "response is nil after Rollback") + require.Equalf(t, OkCode, resp.Code, "wrong code returned") + + // Select outside of transaction + resp, err = conn.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + + // Select inside of stream after rollback + resp, err = stream.Do(selectReq).Get() + require.Nilf(t, err, "failed to Select") + require.NotNilf(t, resp, "response is nil after Select") + require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + + test_helpers.DeleteRecordByKey(t, conn, spaceNo, indexNo, []interface{}{uint(1001)}) + } +} + +func TestStream_DoWithStrangerConn(t *testing.T) { + expectedErr := fmt.Errorf("the passed connected request " + + "doesn't belong to the current connection or connection pool") + + conn := &Connection{} + stream, _ := conn.NewStream() + req := test_helpers.NewStrangerRequest() + + _, err := stream.Do(req).Get() + if err == nil { + t.Fatalf("nil error has been caught") + } + if err.Error() != expectedErr.Error() { + t.Fatalf("Unexpected error has been caught: %s", err.Error()) + } +} + +func TestStream_DoWithClosedConn(t *testing.T) { + expectedErr := fmt.Errorf("using closed connection") + + test_helpers.SkipIfStreamsUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + + stream, _ := conn.NewStream() + conn.Close() + + // Begin transaction + req := NewBeginRequest() + _, err := stream.Do(req).Get() + if err == nil { + t.Fatalf("nil error has been caught") + } + if !strings.Contains(err.Error(), expectedErr.Error()) { + t.Fatalf("Unexpected error has been caught: %s", err.Error()) + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body // is a separate function, see // https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls func runTestMain(m *testing.M) int { + // Tarantool supports streams and interactive transactions since version 2.10.0 + isStreamUnsupported, err := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil { + log.Fatalf("Could not check the Tarantool version") + } + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ - InitScript: "config.lua", - Listen: server, - WorkDir: "work_dir", - User: opts.User, - Pass: opts.Pass, - WaitStart: 100 * time.Millisecond, - ConnectRetry: 3, - RetryTimeout: 500 * time.Millisecond, + InitScript: "config.lua", + Listen: server, + WorkDir: "work_dir", + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + MemtxUseMvccEngine: !isStreamUnsupported, }) defer test_helpers.StopTarantoolWithCleanup(inst) diff --git a/test_helpers/main.go b/test_helpers/main.go index 5c9d5135e..cdc3c343d 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -72,6 +72,10 @@ type StartOpts struct { // RetryTimeout is a time between tarantool ping retries. RetryTimeout time.Duration + + // MemtxUseMvccEngine is flag to enable transactional + // manager if set to true. + MemtxUseMvccEngine bool } // TarantoolInstance is a data for instance graceful shutdown and cleanup. @@ -190,6 +194,7 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { os.Environ(), fmt.Sprintf("TEST_TNT_WORK_DIR=%s", startOpts.WorkDir), fmt.Sprintf("TEST_TNT_LISTEN=%s", startOpts.Listen), + fmt.Sprintf("TEST_TNT_MEMTX_USE_MVCC_ENGINE=%t", startOpts.MemtxUseMvccEngine), ) // Clean up existing work_dir. diff --git a/test_helpers/utils.go b/test_helpers/utils.go index e07f34bf8..c936e90b3 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -24,6 +24,22 @@ func ConnectWithValidation(t testing.TB, return conn } +func DeleteRecordByKey(t *testing.T, conn tarantool.Connector, + space interface{}, index interface{}, key []interface{}) { + t.Helper() + + req := tarantool.NewDeleteRequest(space). + Index(index). + Key(key) + resp, err := conn.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Delete: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } +} + func SkipIfSQLUnsupported(t testing.TB) { t.Helper() @@ -36,3 +52,17 @@ func SkipIfSQLUnsupported(t testing.TB) { t.Skip() } } + +func SkipIfStreamsUnsupported(t *testing.T) { + t.Helper() + + // Tarantool supports streams and interactive transactions since version 2.10.0 + isLess, err := IsTarantoolVersionLess(2, 10, 0) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + + if isLess { + t.Skip("Skipping test for Tarantool without streams support") + } +} From d95523053648a295d59f235fee41a1358addd2c5 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 27 Jul 2022 13:38:01 +0300 Subject: [PATCH 308/605] Release 1.7.0 Overview This release adds a number of features. The extending of the public API has become possible with a new way of creating requests. New types of requests are created via chain calls: selectReq := NewSelectRequest("space"). Context(ctx). Index(1). Offset(5). Limit(10) future := conn.Do(selectReq) Streams, context and prepared statements support are based on this idea: stream, err := conn.NewStream() beginReq := NewBeginRequest().Context(ctx) if response, err := stream.Do(beginReq).Get(); err != nil { selectFuture := stream.Do(selectReq) commitFuture := stream.Do(NewCommitRequest()) // ... } ``` Breaking changes NewErrorFuture function removed (#190). `IPROTO_*` constants that identify requests renamed from `Request` to `RequestCode` (#126) New features SSL support (#155). IPROTO_PUSH messages support (#67). Public API with request object types (#126). Support decimal type in msgpack (#96). Support datetime type in msgpack (#118). Prepared SQL statements (#117). Context support for request objects (#48). Streams and interactive transactions support (#101). `Call16` method, support build tag `go_tarantool_call_17` to choose default behavior for `Call` method as Call17 (#125) Bugfixes Add `ExecuteAsync` and `ExecuteTyped` to common connector interface (#62). --- CHANGELOG.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97b6e07d8..89d5fe5a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +### Changed + +### Fixed + +## [1.7.0] - 2022-08-02 + +### Added + - SSL support (#155) - IPROTO_PUSH messages support (#67) - Public API with request object types (#126) @@ -18,11 +26,11 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Prepared SQL statements (#117) - Context support for request objects (#48) - Streams and interactive transactions support (#101) +- `Call16` method, support build tag `go_tarantool_call_17` to choose + default behavior for `Call` method as Call17 (#125) ### Changed -- Add `Call16` method, support build tag `go_tarantool_call_17` - to choose behavior for `Call` method (#125) - `IPROTO_*` constants that identify requests renamed from `Request` to `RequestCode` (#126) @@ -32,10 +40,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed -- Build with OpenSSL < 1.1.1 (#194) - Add `ExecuteAsync` and `ExecuteTyped` to common connector interface (#62) -- Race conditions in methods of `Future` type (#195) -- Usage of nil pointer in Connection.peekFuture (#195) ## [1.6.0] - 2022-06-01 From 1ace6b2e4b8744da8f26adb06048ed48a03d7d38 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 17 May 2022 16:04:05 +0300 Subject: [PATCH 309/605] code health: replace deprecated EncodeSliceLen and DecodeSliceLen EncodeSliceLen[1] and DecodeSliceLen[2] are marked as deprecated in msgpack v2.9.2. The patch replaces it with EncodeArrayLen and DecodeArrayLen. 1. https://pkg.go.dev/github.com/vmihailenco/msgpack@v2.9.2+incompatible#Encoder.EncodeSliceLen 2. https://pkg.go.dev/github.com/vmihailenco/msgpack@v2.9.2+incompatible#Decoder.DecodeSliceLen Part of #124 --- client_tools.go | 12 ++++++------ datetime/datetime_test.go | 14 +++++++------- decimal/decimal_test.go | 4 ++-- example_custom_unpacking_test.go | 6 +++--- queue/queue.go | 2 +- queue/queue_test.go | 4 ++-- queue/task.go | 2 +- request.go | 10 +++++----- tarantool_test.go | 4 ++-- uuid/uuid_test.go | 2 +- 10 files changed, 30 insertions(+), 30 deletions(-) diff --git a/client_tools.go b/client_tools.go index 27976a9a7..20bfa722f 100644 --- a/client_tools.go +++ b/client_tools.go @@ -11,7 +11,7 @@ type IntKey struct { } func (k IntKey) EncodeMsgpack(enc *msgpack.Encoder) error { - enc.EncodeSliceLen(1) + enc.EncodeArrayLen(1) enc.EncodeInt(k.I) return nil } @@ -23,7 +23,7 @@ type UintKey struct { } func (k UintKey) EncodeMsgpack(enc *msgpack.Encoder) error { - enc.EncodeSliceLen(1) + enc.EncodeArrayLen(1) enc.EncodeUint(k.I) return nil } @@ -35,7 +35,7 @@ type StringKey struct { } func (k StringKey) EncodeMsgpack(enc *msgpack.Encoder) error { - enc.EncodeSliceLen(1) + enc.EncodeArrayLen(1) enc.EncodeString(k.S) return nil } @@ -47,7 +47,7 @@ type IntIntKey struct { } func (k IntIntKey) EncodeMsgpack(enc *msgpack.Encoder) error { - enc.EncodeSliceLen(2) + enc.EncodeArrayLen(2) enc.EncodeInt(k.I1) enc.EncodeInt(k.I2) return nil @@ -61,7 +61,7 @@ type Op struct { } func (o Op) EncodeMsgpack(enc *msgpack.Encoder) error { - enc.EncodeSliceLen(3) + enc.EncodeArrayLen(3) enc.EncodeString(o.Op) enc.EncodeInt(o.Field) return enc.Encode(o.Arg) @@ -149,7 +149,7 @@ type OpSplice struct { } func (o OpSplice) EncodeMsgpack(enc *msgpack.Encoder) error { - enc.EncodeSliceLen(5) + enc.EncodeArrayLen(5) enc.EncodeString(o.Op) enc.EncodeInt(o.Field) enc.EncodeInt(o.Pos) diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 93d292ecd..2de8888d7 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -271,7 +271,7 @@ type Tuple1 struct { } func (t *Tuple1) EncodeMsgpack(e *msgpack.Encoder) error { - if err := e.EncodeSliceLen(2); err != nil { + if err := e.EncodeArrayLen(2); err != nil { return err } if err := e.Encode(&t.Datetime); err != nil { @@ -283,7 +283,7 @@ func (t *Tuple1) EncodeMsgpack(e *msgpack.Encoder) error { func (t *Tuple1) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - if l, err = d.DecodeSliceLen(); err != nil { + if l, err = d.DecodeArrayLen(); err != nil { return err } if l != 1 { @@ -297,7 +297,7 @@ func (t *Tuple1) DecodeMsgpack(d *msgpack.Decoder) error { } func (ev *Event) EncodeMsgpack(e *msgpack.Encoder) error { - if err := e.EncodeSliceLen(2); err != nil { + if err := e.EncodeArrayLen(2); err != nil { return err } if err := e.EncodeString(ev.Location); err != nil { @@ -312,7 +312,7 @@ func (ev *Event) EncodeMsgpack(e *msgpack.Encoder) error { func (ev *Event) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - if l, err = d.DecodeSliceLen(); err != nil { + if l, err = d.DecodeArrayLen(); err != nil { return err } if l != 2 { @@ -330,7 +330,7 @@ func (ev *Event) DecodeMsgpack(d *msgpack.Decoder) error { } func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { - if err := e.EncodeSliceLen(3); err != nil { + if err := e.EncodeArrayLen(3); err != nil { return err } if err := e.EncodeUint(c.Cid); err != nil { @@ -346,7 +346,7 @@ func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - if l, err = d.DecodeSliceLen(); err != nil { + if l, err = d.DecodeArrayLen(); err != nil { return err } if l != 3 { @@ -358,7 +358,7 @@ func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { if c.Orig, err = d.DecodeString(); err != nil { return err } - if l, err = d.DecodeSliceLen(); err != nil { + if l, err = d.DecodeArrayLen(); err != nil { return err } c.Events = make([]Event, l) diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 1c395b393..499d939c2 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -41,7 +41,7 @@ type TupleDecimal struct { } func (t *TupleDecimal) EncodeMsgpack(e *msgpack.Encoder) error { - if err := e.EncodeSliceLen(1); err != nil { + if err := e.EncodeArrayLen(1); err != nil { return err } return e.EncodeValue(reflect.ValueOf(&t.number)) @@ -50,7 +50,7 @@ func (t *TupleDecimal) EncodeMsgpack(e *msgpack.Encoder) error { func (t *TupleDecimal) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - if l, err = d.DecodeSliceLen(); err != nil { + if l, err = d.DecodeArrayLen(); err != nil { return err } if l != 1 { diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index 2910010cd..d524dfde8 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -25,7 +25,7 @@ type Tuple3 struct { } func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { - if err := e.EncodeSliceLen(3); err != nil { + if err := e.EncodeArrayLen(3); err != nil { return err } if err := e.EncodeUint(c.Cid); err != nil { @@ -41,7 +41,7 @@ func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - if l, err = d.DecodeSliceLen(); err != nil { + if l, err = d.DecodeArrayLen(); err != nil { return err } if l != 3 { @@ -53,7 +53,7 @@ func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { if c.Orig, err = d.DecodeString(); err != nil { return err } - if l, err = d.DecodeSliceLen(); err != nil { + if l, err = d.DecodeArrayLen(); err != nil { return err } c.Members = make([]Member, l) diff --git a/queue/queue.go b/queue/queue.go index 8a3e3e17d..68a98672b 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -339,7 +339,7 @@ type queueData struct { func (qd *queueData) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - if l, err = d.DecodeSliceLen(); err != nil { + if l, err = d.DecodeArrayLen(); err != nil { return err } if l > 1 { diff --git a/queue/queue_test.go b/queue/queue_test.go index 48fb71257..25af17b12 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -186,7 +186,7 @@ type customData struct { func (c *customData) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - if l, err = d.DecodeSliceLen(); err != nil { + if l, err = d.DecodeArrayLen(); err != nil { return err } if l != 1 { @@ -199,7 +199,7 @@ func (c *customData) DecodeMsgpack(d *msgpack.Decoder) error { } func (c *customData) EncodeMsgpack(e *msgpack.Encoder) error { - if err := e.EncodeSliceLen(1); err != nil { + if err := e.EncodeArrayLen(1); err != nil { return err } if err := e.EncodeString(c.customField); err != nil { diff --git a/queue/task.go b/queue/task.go index fc16ffb88..add05efd6 100644 --- a/queue/task.go +++ b/queue/task.go @@ -17,7 +17,7 @@ type Task struct { func (t *Task) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - if l, err = d.DecodeSliceLen(); err != nil { + if l, err = d.DecodeArrayLen(); err != nil { return err } if l < 3 { diff --git a/request.go b/request.go index c708b79b4..a892ca678 100644 --- a/request.go +++ b/request.go @@ -200,7 +200,7 @@ type single struct { func (s *single) DecodeMsgpack(d *msgpack.Decoder) error { var err error var len int - if len, err = d.DecodeSliceLen(); err != nil { + if len, err = d.DecodeArrayLen(); err != nil { return err } if s.found = len >= 1; !s.found { @@ -433,7 +433,7 @@ func encodeSQLBind(enc *msgpack.Encoder, from interface{}) error { } encodeNamedFromMap := func(mp map[string]interface{}) error { - if err := enc.EncodeSliceLen(len(mp)); err != nil { + if err := enc.EncodeArrayLen(len(mp)); err != nil { return err } for k, v := range mp { @@ -445,7 +445,7 @@ func encodeSQLBind(enc *msgpack.Encoder, from interface{}) error { } encodeNamedFromStruct := func(val reflect.Value) error { - if err := enc.EncodeSliceLen(val.NumField()); err != nil { + if err := enc.EncodeArrayLen(val.NumField()); err != nil { return err } cached, ok := lowerCaseNames.Load(val.Type()) @@ -479,7 +479,7 @@ func encodeSQLBind(enc *msgpack.Encoder, from interface{}) error { if !ok { castedKVSlice := from.([]KeyValueBind) t := len(castedKVSlice) - if err := enc.EncodeSliceLen(t); err != nil { + if err := enc.EncodeArrayLen(t); err != nil { return err } for _, v := range castedKVSlice { @@ -490,7 +490,7 @@ func encodeSQLBind(enc *msgpack.Encoder, from interface{}) error { return nil } - if err := enc.EncodeSliceLen(len(castedSlice)); err != nil { + if err := enc.EncodeArrayLen(len(castedSlice)); err != nil { return err } for i := 0; i < len(castedSlice); i++ { diff --git a/tarantool_test.go b/tarantool_test.go index de4062c9e..f3fdbb787 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -27,7 +27,7 @@ type Member struct { } func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { - if err := e.EncodeSliceLen(2); err != nil { + if err := e.EncodeArrayLen(2); err != nil { return err } if err := e.EncodeString(m.Name); err != nil { @@ -42,7 +42,7 @@ func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - if l, err = d.DecodeSliceLen(); err != nil { + if l, err = d.DecodeArrayLen(); err != nil { return err } if l != 2 { diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index 554dadbe1..aed760009 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -36,7 +36,7 @@ type TupleUUID struct { func (t *TupleUUID) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int - if l, err = d.DecodeSliceLen(); err != nil { + if l, err = d.DecodeArrayLen(); err != nil { return err } if l != 1 { From 253c1dac355e178650e5d510ad1c8f02c603ac01 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 17 May 2022 11:06:12 +0300 Subject: [PATCH 310/605] code health: replace EncodeUint64 by EncodeUint We use everywhere EncodeInt instead of EncodeInt64. Also we use everywhere EncodeUint64 instead of EncodeUint. It can be confusing. Although EncodeUint64 and EncodeUint have same logic in msgpack.v2, but different in msgpack.v5. It's good for migration to msgpack.v5 too. Part of #124. --- prepared.go | 12 ++++++------ request.go | 50 +++++++++++++++++++++++++------------------------- stream.go | 2 +- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/prepared.go b/prepared.go index 0f9303344..2546d34b6 100644 --- a/prepared.go +++ b/prepared.go @@ -22,21 +22,21 @@ type Prepared struct { func fillPrepare(enc *msgpack.Encoder, expr string) error { enc.EncodeMapLen(1) - enc.EncodeUint64(KeySQLText) + enc.EncodeUint(KeySQLText) return enc.EncodeString(expr) } func fillUnprepare(enc *msgpack.Encoder, stmt Prepared) error { enc.EncodeMapLen(1) - enc.EncodeUint64(KeyStmtID) - return enc.EncodeUint64(uint64(stmt.StatementID)) + enc.EncodeUint(KeyStmtID) + return enc.EncodeUint(uint(stmt.StatementID)) } func fillExecutePrepared(enc *msgpack.Encoder, stmt Prepared, args interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint64(KeyStmtID) - enc.EncodeUint64(uint64(stmt.StatementID)) - enc.EncodeUint64(KeySQLBind) + enc.EncodeUint(KeyStmtID) + enc.EncodeUint(uint(stmt.StatementID)) + enc.EncodeUint(KeySQLBind) return encodeSQLBind(enc, args) } diff --git a/request.go b/request.go index a892ca678..7d14d3710 100644 --- a/request.go +++ b/request.go @@ -11,28 +11,28 @@ import ( ) func fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { - enc.EncodeUint64(KeySpaceNo) - enc.EncodeUint64(uint64(spaceNo)) - enc.EncodeUint64(KeyIndexNo) - enc.EncodeUint64(uint64(indexNo)) - enc.EncodeUint64(KeyKey) + enc.EncodeUint(KeySpaceNo) + enc.EncodeUint(uint(spaceNo)) + enc.EncodeUint(KeyIndexNo) + enc.EncodeUint(uint(indexNo)) + enc.EncodeUint(KeyKey) return enc.Encode(key) } func fillIterator(enc *msgpack.Encoder, offset, limit, iterator uint32) { - enc.EncodeUint64(KeyIterator) - enc.EncodeUint64(uint64(iterator)) - enc.EncodeUint64(KeyOffset) - enc.EncodeUint64(uint64(offset)) - enc.EncodeUint64(KeyLimit) - enc.EncodeUint64(uint64(limit)) + enc.EncodeUint(KeyIterator) + enc.EncodeUint(uint(iterator)) + enc.EncodeUint(KeyOffset) + enc.EncodeUint(uint(offset)) + enc.EncodeUint(KeyLimit) + enc.EncodeUint(uint(limit)) } func fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint64(KeySpaceNo) - enc.EncodeUint64(uint64(spaceNo)) - enc.EncodeUint64(KeyTuple) + enc.EncodeUint(KeySpaceNo) + enc.EncodeUint(uint(spaceNo)) + enc.EncodeUint(KeyTuple) return enc.Encode(tuple) } @@ -47,19 +47,19 @@ func fillUpdate(enc *msgpack.Encoder, spaceNo, indexNo uint32, key, ops interfac if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { return err } - enc.EncodeUint64(KeyTuple) + enc.EncodeUint(KeyTuple) return enc.Encode(ops) } func fillUpsert(enc *msgpack.Encoder, spaceNo uint32, tuple, ops interface{}) error { enc.EncodeMapLen(3) - enc.EncodeUint64(KeySpaceNo) - enc.EncodeUint64(uint64(spaceNo)) - enc.EncodeUint64(KeyTuple) + enc.EncodeUint(KeySpaceNo) + enc.EncodeUint(uint(spaceNo)) + enc.EncodeUint(KeyTuple) if err := enc.Encode(tuple); err != nil { return err } - enc.EncodeUint64(KeyDefTuple) + enc.EncodeUint(KeyDefTuple) return enc.Encode(ops) } @@ -70,25 +70,25 @@ func fillDelete(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) func fillCall(enc *msgpack.Encoder, functionName string, args interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint64(KeyFunctionName) + enc.EncodeUint(KeyFunctionName) enc.EncodeString(functionName) - enc.EncodeUint64(KeyTuple) + enc.EncodeUint(KeyTuple) return enc.Encode(args) } func fillEval(enc *msgpack.Encoder, expr string, args interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint64(KeyExpression) + enc.EncodeUint(KeyExpression) enc.EncodeString(expr) - enc.EncodeUint64(KeyTuple) + enc.EncodeUint(KeyTuple) return enc.Encode(args) } func fillExecute(enc *msgpack.Encoder, expr string, args interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint64(KeySQLText) + enc.EncodeUint(KeySQLText) enc.EncodeString(expr) - enc.EncodeUint64(KeySQLBind) + enc.EncodeUint(KeySQLBind) return encodeSQLBind(enc, args) } diff --git a/stream.go b/stream.go index fdfe8408c..62ea0adf0 100644 --- a/stream.go +++ b/stream.go @@ -46,7 +46,7 @@ func fillBegin(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, timeout tim } if hasTimeout { - err = enc.EncodeUint64(KeyTimeout) + err = enc.EncodeUint(KeyTimeout) if err != nil { return err } From ec1d3d5df253c1a3d135f51847fa13bf141d25f6 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 17 May 2022 16:00:56 +0300 Subject: [PATCH 311/605] code health: decode schema with msgpack When we decode the schema with msgpack it allows us to handle errors better. For example, incorrect type conversion leads to a runtime error. Now it will be an usual error that we can handle by our code. Also it will help in migration to msgpack.v5, because an interface decoding rules by default have changed in msgpack.v5. Part of #124 Co-authored-by: Oleg Utkin --- schema.go | 388 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 280 insertions(+), 108 deletions(-) diff --git a/schema.go b/schema.go index b1e8562d2..fc3b0793a 100644 --- a/schema.go +++ b/schema.go @@ -1,7 +1,21 @@ package tarantool import ( + "errors" "fmt" + "gopkg.in/vmihailenco/msgpack.v2" + msgpcode "gopkg.in/vmihailenco/msgpack.v2/codes" +) + +//nolint: varcheck,deadcode +const ( + maxSchemas = 10000 + spaceSpId = 280 + vspaceSpId = 281 + indexSpId = 288 + vindexSpId = 289 + vspaceSpTypeFieldNum = 6 + vspaceSpFormatFieldNum = 7 ) // SchemaResolver is an interface for resolving schema details. @@ -37,19 +51,218 @@ type Space struct { IndexesById map[uint32]*Index } +func isUint(code byte) bool { + return code == msgpcode.Uint8 || code == msgpcode.Uint16 || + code == msgpcode.Uint32 || code == msgpcode.Uint64 || + msgpcode.IsFixedNum(code) +} + +func isMap(code byte) bool { + return code == msgpcode.Map16 || code == msgpcode.Map32 || msgpcode.IsFixedMap(code) +} + +func isArray(code byte) bool { + return code == msgpcode.Array16 || code == msgpcode.Array32 || + msgpcode.IsFixedArray(code) +} + +func isString(code byte) bool { + return msgpcode.IsFixedString(code) || code == msgpcode.Str8 || + code == msgpcode.Str16 || code == msgpcode.Str32 +} + +func (space *Space) DecodeMsgpack(d *msgpack.Decoder) error { + arrayLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + if space.Id, err = d.DecodeUint32(); err != nil { + return err + } + if err := d.Skip(); err != nil { + return err + } + if space.Name, err = d.DecodeString(); err != nil { + return err + } + if space.Engine, err = d.DecodeString(); err != nil { + return err + } + if space.FieldsCount, err = d.DecodeUint32(); err != nil { + return err + } + if arrayLen >= vspaceSpTypeFieldNum { + code, err := d.PeekCode() + if err != nil { + return err + } + if isString(code) { + val, err := d.DecodeString() + if err != nil { + return err + } + space.Temporary = val == "temporary" + } else if isMap(code) { + mapLen, err := d.DecodeMapLen() + if err != nil { + return err + } + for i := 0; i < mapLen; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + if key == "temporary" { + if space.Temporary, err = d.DecodeBool(); err != nil { + return err + } + } else { + if err = d.Skip(); err != nil { + return err + } + } + } + } else { + return errors.New("unexpected schema format (space flags)") + } + } + space.FieldsById = make(map[uint32]*Field) + space.Fields = make(map[string]*Field) + space.IndexesById = make(map[uint32]*Index) + space.Indexes = make(map[string]*Index) + if arrayLen >= vspaceSpFormatFieldNum { + fieldCount, err := d.DecodeArrayLen() + if err != nil { + return err + } + for i := 0; i < fieldCount; i++ { + field := &Field{} + if err := field.DecodeMsgpack(d); err != nil { + return err + } + field.Id = uint32(i) + space.FieldsById[field.Id] = field + if field.Name != "" { + space.Fields[field.Name] = field + } + } + } + return nil +} + type Field struct { Id uint32 Name string Type string } +func (field *Field) DecodeMsgpack(d *msgpack.Decoder) error { + l, err := d.DecodeMapLen() + if err != nil { + return err + } + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + switch key { + case "name": + if field.Name, err = d.DecodeString(); err != nil { + return err + } + case "type": + if field.Type, err = d.DecodeString(); err != nil { + return err + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + return nil +} + // Index contains information about index. type Index struct { - Id uint32 - Name string - Type string - Unique bool - Fields []*IndexField + Id uint32 + SpaceId uint32 + Name string + Type string + Unique bool + Fields []*IndexField +} + +func (index *Index) DecodeMsgpack(d *msgpack.Decoder) error { + _, err := d.DecodeArrayLen() + if err != nil { + return err + } + + if index.SpaceId, err = d.DecodeUint32(); err != nil { + return err + } + if index.Id, err = d.DecodeUint32(); err != nil { + return err + } + if index.Name, err = d.DecodeString(); err != nil { + return err + } + if index.Type, err = d.DecodeString(); err != nil { + return err + } + + var code byte + if code, err = d.PeekCode(); err != nil { + return err + } + + if isUint(code) { + optsUint64, err := d.DecodeUint64() + if err != nil { + return nil + } + index.Unique = optsUint64 > 0 + } else { + var optsMap map[string]interface{} + if err := d.Decode(&optsMap); err != nil { + return fmt.Errorf("unexpected schema format (index flags): %w", err) + } + + var ok bool + if index.Unique, ok = optsMap["unique"].(bool); !ok { + /* see bug https://github.com/tarantool/tarantool/issues/2060 */ + index.Unique = true + } + } + + if code, err = d.PeekCode(); err != nil { + return err + } + + if isUint(code) { + fieldCount, err := d.DecodeUint64() + if err != nil { + return err + } + index.Fields = make([]*IndexField, fieldCount) + for i := 0; i < int(fieldCount); i++ { + index.Fields[i] = new(IndexField) + if index.Fields[i].Id, err = d.DecodeUint32(); err != nil { + return err + } + if index.Fields[i].Type, err = d.DecodeString(); err != nil { + return err + } + } + } else { + if err := d.Decode(&index.Fields); err != nil { + return fmt.Errorf("unexpected schema format (index flags): %w", err) + } + } + + return nil } type IndexField struct { @@ -57,128 +270,87 @@ type IndexField struct { Type string } -//nolint: varcheck,deadcode -const ( - maxSchemas = 10000 - spaceSpId = 280 - vspaceSpId = 281 - indexSpId = 288 - vindexSpId = 289 -) - -func (conn *Connection) loadSchema() (err error) { - var resp *Response - - schema := new(Schema) - schema.SpacesById = make(map[uint32]*Space) - schema.Spaces = make(map[string]*Space) - - // Reload spaces. - resp, err = conn.Select(vspaceSpId, 0, 0, maxSchemas, IterAll, []interface{}{}) +func (indexField *IndexField) DecodeMsgpack(d *msgpack.Decoder) error { + code, err := d.PeekCode() if err != nil { return err } - for _, row := range resp.Data { - row := row.([]interface{}) - space := new(Space) - space.Id = uint32(row[0].(uint64)) - space.Name = row[2].(string) - space.Engine = row[3].(string) - space.FieldsCount = uint32(row[4].(uint64)) - if len(row) >= 6 { - switch row5 := row[5].(type) { - case string: - space.Temporary = row5 == "temporary" - case map[interface{}]interface{}: - if temp, ok := row5["temporary"]; ok { - space.Temporary = temp.(bool) - } - default: - panic("unexpected schema format (space flags)") - } + + if isMap(code) { + mapLen, err := d.DecodeMapLen() + if err != nil { + return err } - space.FieldsById = make(map[uint32]*Field) - space.Fields = make(map[string]*Field) - space.IndexesById = make(map[uint32]*Index) - space.Indexes = make(map[string]*Index) - if len(row) >= 7 { - for i, f := range row[6].([]interface{}) { - if f == nil { - continue - } - f := f.(map[interface{}]interface{}) - field := new(Field) - field.Id = uint32(i) - if name, ok := f["name"]; ok && name != nil { - field.Name = name.(string) + for i := 0; i < mapLen; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + switch key { + case "field": + if indexField.Id, err = d.DecodeUint32(); err != nil { + return err } - if type1, ok := f["type"]; ok && type1 != nil { - field.Type = type1.(string) + case "type": + if indexField.Type, err = d.DecodeString(); err != nil { + return err } - space.FieldsById[field.Id] = field - if field.Name != "" { - space.Fields[field.Name] = field + default: + if err := d.Skip(); err != nil { + return err } } } + return nil + } else if isArray(code) { + arrayLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + if indexField.Id, err = d.DecodeUint32(); err != nil { + return err + } + if indexField.Type, err = d.DecodeString(); err != nil { + return err + } + for i := 2; i < arrayLen; i++ { + if err := d.Skip(); err != nil { + return err + } + } + return nil + } + + return errors.New("unexpected schema format (index fields)") +} +func (conn *Connection) loadSchema() (err error) { + schema := new(Schema) + schema.SpacesById = make(map[uint32]*Space) + schema.Spaces = make(map[string]*Space) + + // Reload spaces. + var spaces []*Space + err = conn.SelectTyped(vspaceSpId, 0, 0, maxSchemas, IterAll, []interface{}{}, &spaces) + if err != nil { + return err + } + for _, space := range spaces { schema.SpacesById[space.Id] = space schema.Spaces[space.Name] = space } // Reload indexes. - resp, err = conn.Select(vindexSpId, 0, 0, maxSchemas, IterAll, []interface{}{}) + var indexes []*Index + err = conn.SelectTyped(vindexSpId, 0, 0, maxSchemas, IterAll, []interface{}{}, &indexes) if err != nil { return err } - for _, row := range resp.Data { - row := row.([]interface{}) - index := new(Index) - index.Id = uint32(row[1].(uint64)) - index.Name = row[2].(string) - index.Type = row[3].(string) - switch row[4].(type) { - case uint64: - index.Unique = row[4].(uint64) > 0 - case map[interface{}]interface{}: - opts := row[4].(map[interface{}]interface{}) - var ok bool - if index.Unique, ok = opts["unique"].(bool); !ok { - /* See bug https://github.com/tarantool/tarantool/issues/2060. */ - index.Unique = true - } - default: - panic("unexpected schema format (index flags)") - } - switch fields := row[5].(type) { - case uint64: - cnt := int(fields) - for i := 0; i < cnt; i++ { - field := new(IndexField) - field.Id = uint32(row[6+i*2].(uint64)) - field.Type = row[7+i*2].(string) - index.Fields = append(index.Fields, field) - } - case []interface{}: - for _, f := range fields { - field := new(IndexField) - switch f := f.(type) { - case []interface{}: - field.Id = uint32(f[0].(uint64)) - field.Type = f[1].(string) - case map[interface{}]interface{}: - field.Id = uint32(f["field"].(uint64)) - field.Type = f["type"].(string) - } - index.Fields = append(index.Fields, field) - } - default: - panic("unexpected schema format (index fields)") - } - spaceId := uint32(row[0].(uint64)) - schema.SpacesById[spaceId].IndexesById[index.Id] = index - schema.SpacesById[spaceId].Indexes[index.Name] = index + for _, index := range indexes { + schema.SpacesById[index.SpaceId].IndexesById[index.Id] = index + schema.SpacesById[index.SpaceId].Indexes[index.Name] = index } + conn.Schema = schema return nil } From 460bf884e55b3cedb1f5ad81a0879981c09b4668 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 2 Aug 2022 16:08:36 +0300 Subject: [PATCH 312/605] code health: extract a msgpack code from the code and tests The patch replaces the msgpack code by internal wrappers. The msgpack usage have been extracted to msgpack.go file for the code and to msgpack_helper_test.go for tests. It is the same logic for submodules. Part of #124 --- client_tools.go | 16 +++----- connection.go | 14 +++---- datetime/datetime.go | 9 ----- datetime/datetime_test.go | 17 ++++----- datetime/msgpack.go | 9 +++++ datetime/msgpack_helper_test.go | 16 ++++++++ decimal/decimal.go | 8 ---- decimal/decimal_test.go | 27 +++++++------- decimal/msgpack.go | 9 +++++ decimal/msgpack_helper_test.go | 16 ++++++++ example_custom_unpacking_test.go | 5 +-- export_test.go | 39 +++++++++++--------- msgpack.go | 39 ++++++++++++++++++++ msgpack_helper_test.go | 8 ++++ prepared.go | 14 +++---- queue/example_msgpack_test.go | 5 +-- queue/msgpack.go | 7 ++++ queue/msgpack_helper_test.go | 8 ++++ queue/queue.go | 3 +- queue/queue_test.go | 5 +-- queue/task.go | 4 +- request.go | 52 +++++++++++++------------- request_test.go | 63 ++++++++++++++++---------------- response.go | 16 ++++---- schema.go | 42 +++++---------------- stream.go | 14 +++---- tarantool_test.go | 5 +-- test_helpers/msgpack.go | 7 ++++ test_helpers/request_mock.go | 3 +- uuid/msgpack.go | 16 ++++++++ uuid/msgpack_helper_test.go | 7 ++++ uuid/uuid.go | 10 +---- uuid/uuid_test.go | 3 +- 33 files changed, 296 insertions(+), 220 deletions(-) create mode 100644 datetime/msgpack.go create mode 100644 datetime/msgpack_helper_test.go create mode 100644 decimal/msgpack.go create mode 100644 decimal/msgpack_helper_test.go create mode 100644 msgpack.go create mode 100644 msgpack_helper_test.go create mode 100644 queue/msgpack.go create mode 100644 queue/msgpack_helper_test.go create mode 100644 test_helpers/msgpack.go create mode 100644 uuid/msgpack.go create mode 100644 uuid/msgpack_helper_test.go diff --git a/client_tools.go b/client_tools.go index 20bfa722f..88da9c577 100644 --- a/client_tools.go +++ b/client_tools.go @@ -1,16 +1,12 @@ package tarantool -import ( - "gopkg.in/vmihailenco/msgpack.v2" -) - // IntKey is utility type for passing integer key to Select*, Update* and Delete*. // It serializes to array with single integer element. type IntKey struct { I int } -func (k IntKey) EncodeMsgpack(enc *msgpack.Encoder) error { +func (k IntKey) EncodeMsgpack(enc *encoder) error { enc.EncodeArrayLen(1) enc.EncodeInt(k.I) return nil @@ -22,7 +18,7 @@ type UintKey struct { I uint } -func (k UintKey) EncodeMsgpack(enc *msgpack.Encoder) error { +func (k UintKey) EncodeMsgpack(enc *encoder) error { enc.EncodeArrayLen(1) enc.EncodeUint(k.I) return nil @@ -34,7 +30,7 @@ type StringKey struct { S string } -func (k StringKey) EncodeMsgpack(enc *msgpack.Encoder) error { +func (k StringKey) EncodeMsgpack(enc *encoder) error { enc.EncodeArrayLen(1) enc.EncodeString(k.S) return nil @@ -46,7 +42,7 @@ type IntIntKey struct { I1, I2 int } -func (k IntIntKey) EncodeMsgpack(enc *msgpack.Encoder) error { +func (k IntIntKey) EncodeMsgpack(enc *encoder) error { enc.EncodeArrayLen(2) enc.EncodeInt(k.I1) enc.EncodeInt(k.I2) @@ -60,7 +56,7 @@ type Op struct { Arg interface{} } -func (o Op) EncodeMsgpack(enc *msgpack.Encoder) error { +func (o Op) EncodeMsgpack(enc *encoder) error { enc.EncodeArrayLen(3) enc.EncodeString(o.Op) enc.EncodeInt(o.Field) @@ -148,7 +144,7 @@ type OpSplice struct { Replace string } -func (o OpSplice) EncodeMsgpack(enc *msgpack.Encoder) error { +func (o OpSplice) EncodeMsgpack(enc *encoder) error { enc.EncodeArrayLen(5) enc.EncodeString(o.Op) enc.EncodeInt(o.Field) diff --git a/connection.go b/connection.go index a1e485ba8..957b51506 100644 --- a/connection.go +++ b/connection.go @@ -15,8 +15,6 @@ import ( "sync" "sync/atomic" "time" - - "gopkg.in/vmihailenco/msgpack.v2" ) const requestsMap = 128 @@ -142,7 +140,7 @@ type Connection struct { rlimit chan struct{} opts Opts state uint32 - dec *msgpack.Decoder + dec *decoder lenbuf [PacketLengthBytes]byte lastStreamId uint64 @@ -199,7 +197,7 @@ type connShard struct { requestsWithCtx [requestsMap]futureList bufmut sync.Mutex buf smallWBuf - enc *msgpack.Encoder + enc *encoder } // Greeting is a message sent by Tarantool on connect. @@ -320,7 +318,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { Greeting: &Greeting{}, control: make(chan struct{}), opts: opts, - dec: msgpack.NewDecoder(&smallBuf{}), + dec: newDecoder(&smallBuf{}), } maxprocs := uint32(runtime.GOMAXPROCS(-1)) if conn.opts.Concurrency == 0 || conn.opts.Concurrency > maxprocs*128 { @@ -531,7 +529,7 @@ func (conn *Connection) dial() (err error) { return } -func pack(h *smallWBuf, enc *msgpack.Encoder, reqid uint32, +func pack(h *smallWBuf, enc *encoder, reqid uint32, req Request, streamId uint64, res SchemaResolver) (err error) { hl := h.Len() @@ -569,7 +567,7 @@ func pack(h *smallWBuf, enc *msgpack.Encoder, reqid uint32, func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { var packet smallWBuf req := newAuthRequest(conn.opts.User, string(scramble)) - err = pack(&packet, msgpack.NewEncoder(&packet), 0, req, ignoreStreamId, conn.Schema) + err = pack(&packet, newEncoder(&packet), 0, req, ignoreStreamId, conn.Schema) if err != nil { return errors.New("auth: pack error " + err.Error()) @@ -916,7 +914,7 @@ func (conn *Connection) putFuture(fut *Future, req Request, streamId uint64) { firstWritten := shard.buf.Len() == 0 if shard.buf.Cap() == 0 { shard.buf.b = make([]byte, 0, 128) - shard.enc = msgpack.NewEncoder(&shard.buf) + shard.enc = newEncoder(&shard.buf) } blen := shard.buf.Len() reqid := fut.requestId diff --git a/datetime/datetime.go b/datetime/datetime.go index e9664c7ae..e8f1bdea7 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -13,8 +13,6 @@ import ( "encoding/binary" "fmt" "time" - - "gopkg.in/vmihailenco/msgpack.v2" ) // Datetime MessagePack serialization schema is an MP_EXT extension, which @@ -101,9 +99,6 @@ func (dtime *Datetime) ToTime() time.Time { return dtime.time } -var _ msgpack.Marshaler = (*Datetime)(nil) -var _ msgpack.Unmarshaler = (*Datetime)(nil) - func (dtime *Datetime) MarshalMsgpack() ([]byte, error) { tm := dtime.ToTime() @@ -152,7 +147,3 @@ func (tm *Datetime) UnmarshalMsgpack(b []byte) error { } return err } - -func init() { - msgpack.RegisterExt(datetime_extId, &Datetime{}) -} diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 2de8888d7..035b1025b 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -12,7 +12,6 @@ import ( . "github.com/tarantool/go-tarantool" . "github.com/tarantool/go-tarantool/datetime" "github.com/tarantool/go-tarantool/test_helpers" - "gopkg.in/vmihailenco/msgpack.v2" ) var lesserBoundaryTimes = []time.Time{ @@ -270,7 +269,7 @@ type Tuple1 struct { Datetime Datetime } -func (t *Tuple1) EncodeMsgpack(e *msgpack.Encoder) error { +func (t *Tuple1) EncodeMsgpack(e *encoder) error { if err := e.EncodeArrayLen(2); err != nil { return err } @@ -280,7 +279,7 @@ func (t *Tuple1) EncodeMsgpack(e *msgpack.Encoder) error { return nil } -func (t *Tuple1) DecodeMsgpack(d *msgpack.Decoder) error { +func (t *Tuple1) DecodeMsgpack(d *decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { @@ -296,7 +295,7 @@ func (t *Tuple1) DecodeMsgpack(d *msgpack.Decoder) error { return nil } -func (ev *Event) EncodeMsgpack(e *msgpack.Encoder) error { +func (ev *Event) EncodeMsgpack(e *encoder) error { if err := e.EncodeArrayLen(2); err != nil { return err } @@ -309,7 +308,7 @@ func (ev *Event) EncodeMsgpack(e *msgpack.Encoder) error { return nil } -func (ev *Event) DecodeMsgpack(d *msgpack.Decoder) error { +func (ev *Event) DecodeMsgpack(d *decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { @@ -329,7 +328,7 @@ func (ev *Event) DecodeMsgpack(d *msgpack.Decoder) error { return nil } -func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { +func (c *Tuple2) EncodeMsgpack(e *encoder) error { if err := e.EncodeArrayLen(3); err != nil { return err } @@ -343,7 +342,7 @@ func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { return nil } -func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { +func (c *Tuple2) DecodeMsgpack(d *decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { @@ -525,7 +524,7 @@ func TestMPEncode(t *testing.T) { if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) } - buf, err := msgpack.Marshal(dt) + buf, err := marshal(dt) if err != nil { t.Fatalf("Marshalling failed: %s", err.Error()) } @@ -549,7 +548,7 @@ func TestMPDecode(t *testing.T) { } buf, _ := hex.DecodeString(testcase.mpBuf) var v Datetime - err = msgpack.Unmarshal(buf, &v) + err = unmarshal(buf, &v) if err != nil { t.Fatalf("Unmarshalling failed: %s", err.Error()) } diff --git a/datetime/msgpack.go b/datetime/msgpack.go new file mode 100644 index 000000000..915c541ec --- /dev/null +++ b/datetime/msgpack.go @@ -0,0 +1,9 @@ +package datetime + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +func init() { + msgpack.RegisterExt(datetime_extId, &Datetime{}) +} diff --git a/datetime/msgpack_helper_test.go b/datetime/msgpack_helper_test.go new file mode 100644 index 000000000..ba283db18 --- /dev/null +++ b/datetime/msgpack_helper_test.go @@ -0,0 +1,16 @@ +package datetime_test + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +func marshal(v interface{}) ([]byte, error) { + return msgpack.Marshal(v) +} + +func unmarshal(data []byte, v interface{}) error { + return msgpack.Unmarshal(data, v) +} diff --git a/decimal/decimal.go b/decimal/decimal.go index 66587feec..b92391ac2 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -19,7 +19,6 @@ import ( "fmt" "github.com/shopspring/decimal" - "gopkg.in/vmihailenco/msgpack.v2" ) // Decimal numbers have 38 digits of precision, that is, the total @@ -56,9 +55,6 @@ func NewDecimalFromString(src string) (result *Decimal, err error) { return } -var _ msgpack.Marshaler = (*Decimal)(nil) -var _ msgpack.Unmarshaler = (*Decimal)(nil) - func (decNum *Decimal) MarshalMsgpack() ([]byte, error) { one := decimal.NewFromInt(1) maxSupportedDecimal := decimal.New(1, DecimalPrecision).Sub(one) // 10^DecimalPrecision - 1 @@ -99,7 +95,3 @@ func (decNum *Decimal) UnmarshalMsgpack(b []byte) error { return nil } - -func init() { - msgpack.RegisterExt(decimalExtID, &Decimal{}) -} diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 499d939c2..9eb056ca8 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -13,7 +13,6 @@ import ( . "github.com/tarantool/go-tarantool" . "github.com/tarantool/go-tarantool/decimal" "github.com/tarantool/go-tarantool/test_helpers" - "gopkg.in/vmihailenco/msgpack.v2" ) var isDecimalSupported = false @@ -40,14 +39,14 @@ type TupleDecimal struct { number Decimal } -func (t *TupleDecimal) EncodeMsgpack(e *msgpack.Encoder) error { +func (t *TupleDecimal) EncodeMsgpack(e *encoder) error { if err := e.EncodeArrayLen(1); err != nil { return err } return e.EncodeValue(reflect.ValueOf(&t.number)) } -func (t *TupleDecimal) DecodeMsgpack(d *msgpack.Decoder) error { +func (t *TupleDecimal) DecodeMsgpack(d *decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { @@ -144,11 +143,11 @@ func TestMPEncodeDecode(t *testing.T) { } var buf []byte tuple := TupleDecimal{number: *decNum} - if buf, err = msgpack.Marshal(&tuple); err != nil { + if buf, err = marshal(&tuple); err != nil { t.Fatalf("Failed to encode decimal number '%s' to a MessagePack buffer: %s", testcase.numString, err) } var v TupleDecimal - if err = msgpack.Unmarshal(buf, &v); err != nil { + if err = unmarshal(buf, &v); err != nil { t.Fatalf("Failed to decode MessagePack buffer '%x' to a decimal number: %s", buf, err) } if !decNum.Equal(v.number.Decimal) { @@ -247,7 +246,7 @@ func TestEncodeMaxNumber(t *testing.T) { referenceErrMsg := "msgpack: decimal number is bigger than maximum supported number (10^38 - 1)" decNum := decimal.New(1, DecimalPrecision) // // 10^DecimalPrecision tuple := TupleDecimal{number: *NewDecimal(decNum)} - _, err := msgpack.Marshal(&tuple) + _, err := marshal(&tuple) if err == nil { t.Fatalf("It is possible to encode a number unsupported by Tarantool") } @@ -261,7 +260,7 @@ func TestEncodeMinNumber(t *testing.T) { two := decimal.NewFromInt(2) decNum := decimal.New(1, DecimalPrecision).Neg().Sub(two) // -10^DecimalPrecision - 2 tuple := TupleDecimal{number: *NewDecimal(decNum)} - _, err := msgpack.Marshal(&tuple) + _, err := marshal(&tuple) if err == nil { t.Fatalf("It is possible to encode a number unsupported by Tarantool") } @@ -280,10 +279,10 @@ func benchmarkMPEncodeDecode(b *testing.B, src decimal.Decimal, dst interface{}) var err error for i := 0; i < b.N; i++ { tuple := TupleDecimal{number: *NewDecimal(src)} - if buf, err = msgpack.Marshal(&tuple); err != nil { + if buf, err = marshal(&tuple); err != nil { b.Fatal(err) } - if err = msgpack.Unmarshal(buf, &v); err != nil { + if err = unmarshal(buf, &v); err != nil { b.Fatal(err) } } @@ -310,7 +309,7 @@ func BenchmarkMPEncodeDecimal(b *testing.B) { } b.ResetTimer() for i := 0; i < b.N; i++ { - msgpack.Marshal(decNum) + marshal(decNum) } }) } @@ -324,13 +323,13 @@ func BenchmarkMPDecodeDecimal(b *testing.B) { b.Fatal(err) } var buf []byte - if buf, err = msgpack.Marshal(decNum); err != nil { + if buf, err = marshal(decNum); err != nil { b.Fatal(err) } b.ResetTimer() var v TupleDecimal for i := 0; i < b.N; i++ { - msgpack.Unmarshal(buf, &v) + unmarshal(buf, &v) } }) @@ -417,7 +416,7 @@ func TestMPEncode(t *testing.T) { if err != nil { t.Fatalf("NewDecimalFromString() failed: %s", err.Error()) } - buf, err := msgpack.Marshal(dec) + buf, err := marshal(dec) if err != nil { t.Fatalf("Marshalling failed: %s", err.Error()) } @@ -442,7 +441,7 @@ func TestMPDecode(t *testing.T) { t.Fatalf("hex.DecodeString() failed: %s", err) } var v interface{} - err = msgpack.Unmarshal(mpBuf, &v) + err = unmarshal(mpBuf, &v) if err != nil { t.Fatalf("Unmarshalling failed: %s", err.Error()) } diff --git a/decimal/msgpack.go b/decimal/msgpack.go new file mode 100644 index 000000000..ca360172f --- /dev/null +++ b/decimal/msgpack.go @@ -0,0 +1,9 @@ +package decimal + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +func init() { + msgpack.RegisterExt(decimalExtID, &Decimal{}) +} diff --git a/decimal/msgpack_helper_test.go b/decimal/msgpack_helper_test.go new file mode 100644 index 000000000..3013f4cf0 --- /dev/null +++ b/decimal/msgpack_helper_test.go @@ -0,0 +1,16 @@ +package decimal_test + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +func marshal(v interface{}) ([]byte, error) { + return msgpack.Marshal(v) +} + +func unmarshal(data []byte, v interface{}) error { + return msgpack.Unmarshal(data, v) +} diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index d524dfde8..cff50da50 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -6,7 +6,6 @@ import ( "time" "github.com/tarantool/go-tarantool" - "gopkg.in/vmihailenco/msgpack.v2" ) type Tuple2 struct { @@ -24,7 +23,7 @@ type Tuple3 struct { Members []Member } -func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { +func (c *Tuple2) EncodeMsgpack(e *encoder) error { if err := e.EncodeArrayLen(3); err != nil { return err } @@ -38,7 +37,7 @@ func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { return nil } -func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { +func (c *Tuple2) DecodeMsgpack(d *decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { diff --git a/export_test.go b/export_test.go index 0492c1a5b..cc9a2a594 100644 --- a/export_test.go +++ b/export_test.go @@ -1,10 +1,9 @@ package tarantool import ( + "io" "net" "time" - - "gopkg.in/vmihailenco/msgpack.v2" ) func SslDialTimeout(network, address string, timeout time.Duration, @@ -18,96 +17,100 @@ func SslCreateContext(opts SslOpts) (ctx interface{}, err error) { // RefImplPingBody is reference implementation for filling of a ping // request's body. -func RefImplPingBody(enc *msgpack.Encoder) error { +func RefImplPingBody(enc *encoder) error { return fillPing(enc) } // RefImplSelectBody is reference implementation for filling of a select // request's body. -func RefImplSelectBody(enc *msgpack.Encoder, space, index, offset, limit, iterator uint32, key interface{}) error { +func RefImplSelectBody(enc *encoder, space, index, offset, limit, iterator uint32, key interface{}) error { return fillSelect(enc, space, index, offset, limit, iterator, key) } // RefImplInsertBody is reference implementation for filling of an insert // request's body. -func RefImplInsertBody(enc *msgpack.Encoder, space uint32, tuple interface{}) error { +func RefImplInsertBody(enc *encoder, space uint32, tuple interface{}) error { return fillInsert(enc, space, tuple) } // RefImplReplaceBody is reference implementation for filling of a replace // request's body. -func RefImplReplaceBody(enc *msgpack.Encoder, space uint32, tuple interface{}) error { +func RefImplReplaceBody(enc *encoder, space uint32, tuple interface{}) error { return fillInsert(enc, space, tuple) } // RefImplDeleteBody is reference implementation for filling of a delete // request's body. -func RefImplDeleteBody(enc *msgpack.Encoder, space, index uint32, key interface{}) error { +func RefImplDeleteBody(enc *encoder, space, index uint32, key interface{}) error { return fillDelete(enc, space, index, key) } // RefImplUpdateBody is reference implementation for filling of an update // request's body. -func RefImplUpdateBody(enc *msgpack.Encoder, space, index uint32, key, ops interface{}) error { +func RefImplUpdateBody(enc *encoder, space, index uint32, key, ops interface{}) error { return fillUpdate(enc, space, index, key, ops) } // RefImplUpsertBody is reference implementation for filling of an upsert // request's body. -func RefImplUpsertBody(enc *msgpack.Encoder, space uint32, tuple, ops interface{}) error { +func RefImplUpsertBody(enc *encoder, space uint32, tuple, ops interface{}) error { return fillUpsert(enc, space, tuple, ops) } // RefImplCallBody is reference implementation for filling of a call or call17 // request's body. -func RefImplCallBody(enc *msgpack.Encoder, function string, args interface{}) error { +func RefImplCallBody(enc *encoder, function string, args interface{}) error { return fillCall(enc, function, args) } // RefImplEvalBody is reference implementation for filling of an eval // request's body. -func RefImplEvalBody(enc *msgpack.Encoder, expr string, args interface{}) error { +func RefImplEvalBody(enc *encoder, expr string, args interface{}) error { return fillEval(enc, expr, args) } // RefImplExecuteBody is reference implementation for filling of an execute // request's body. -func RefImplExecuteBody(enc *msgpack.Encoder, expr string, args interface{}) error { +func RefImplExecuteBody(enc *encoder, expr string, args interface{}) error { return fillExecute(enc, expr, args) } // RefImplPrepareBody is reference implementation for filling of an prepare // request's body. -func RefImplPrepareBody(enc *msgpack.Encoder, expr string) error { +func RefImplPrepareBody(enc *encoder, expr string) error { return fillPrepare(enc, expr) } // RefImplUnprepareBody is reference implementation for filling of an execute prepared // request's body. -func RefImplExecutePreparedBody(enc *msgpack.Encoder, stmt Prepared, args interface{}) error { +func RefImplExecutePreparedBody(enc *encoder, stmt Prepared, args interface{}) error { return fillExecutePrepared(enc, stmt, args) } // RefImplUnprepareBody is reference implementation for filling of an unprepare // request's body. -func RefImplUnprepareBody(enc *msgpack.Encoder, stmt Prepared) error { +func RefImplUnprepareBody(enc *encoder, stmt Prepared) error { return fillUnprepare(enc, stmt) } // RefImplBeginBody is reference implementation for filling of an begin // request's body. -func RefImplBeginBody(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, timeout time.Duration) error { +func RefImplBeginBody(enc *encoder, txnIsolation TxnIsolationLevel, timeout time.Duration) error { return fillBegin(enc, txnIsolation, timeout) } // RefImplCommitBody is reference implementation for filling of an commit // request's body. -func RefImplCommitBody(enc *msgpack.Encoder) error { +func RefImplCommitBody(enc *encoder) error { return fillCommit(enc) } // RefImplRollbackBody is reference implementation for filling of an rollback // request's body. -func RefImplRollbackBody(enc *msgpack.Encoder) error { +func RefImplRollbackBody(enc *encoder) error { return fillRollback(enc) } + +func NewEncoder(w io.Writer) *encoder { + return newEncoder(w) +} diff --git a/msgpack.go b/msgpack.go new file mode 100644 index 000000000..bcdcc06ce --- /dev/null +++ b/msgpack.go @@ -0,0 +1,39 @@ +package tarantool + +import ( + "io" + + "gopkg.in/vmihailenco/msgpack.v2" + msgpcode "gopkg.in/vmihailenco/msgpack.v2/codes" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +func newEncoder(w io.Writer) *encoder { + return msgpack.NewEncoder(w) +} + +func newDecoder(r io.Reader) *decoder { + return msgpack.NewDecoder(r) +} + +func msgpackIsUint(code byte) bool { + return code == msgpcode.Uint8 || code == msgpcode.Uint16 || + code == msgpcode.Uint32 || code == msgpcode.Uint64 || + msgpcode.IsFixedNum(code) +} + +func msgpackIsMap(code byte) bool { + return code == msgpcode.Map16 || code == msgpcode.Map32 || msgpcode.IsFixedMap(code) +} + +func msgpackIsArray(code byte) bool { + return code == msgpcode.Array16 || code == msgpcode.Array32 || + msgpcode.IsFixedArray(code) +} + +func msgpackIsString(code byte) bool { + return msgpcode.IsFixedString(code) || code == msgpcode.Str8 || + code == msgpcode.Str16 || code == msgpcode.Str32 +} diff --git a/msgpack_helper_test.go b/msgpack_helper_test.go new file mode 100644 index 000000000..8623059e5 --- /dev/null +++ b/msgpack_helper_test.go @@ -0,0 +1,8 @@ +package tarantool_test + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder diff --git a/prepared.go b/prepared.go index 2546d34b6..91c5238a7 100644 --- a/prepared.go +++ b/prepared.go @@ -3,8 +3,6 @@ package tarantool import ( "context" "fmt" - - "gopkg.in/vmihailenco/msgpack.v2" ) // PreparedID is a type for Prepared Statement ID @@ -20,19 +18,19 @@ type Prepared struct { Conn *Connection } -func fillPrepare(enc *msgpack.Encoder, expr string) error { +func fillPrepare(enc *encoder, expr string) error { enc.EncodeMapLen(1) enc.EncodeUint(KeySQLText) return enc.EncodeString(expr) } -func fillUnprepare(enc *msgpack.Encoder, stmt Prepared) error { +func fillUnprepare(enc *encoder, stmt Prepared) error { enc.EncodeMapLen(1) enc.EncodeUint(KeyStmtID) return enc.EncodeUint(uint(stmt.StatementID)) } -func fillExecutePrepared(enc *msgpack.Encoder, stmt Prepared, args interface{}) error { +func fillExecutePrepared(enc *encoder, stmt Prepared, args interface{}) error { enc.EncodeMapLen(2) enc.EncodeUint(KeyStmtID) enc.EncodeUint(uint(stmt.StatementID)) @@ -75,7 +73,7 @@ func NewPrepareRequest(expr string) *PrepareRequest { } // Body fills an encoder with the execute request body. -func (req *PrepareRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *PrepareRequest) Body(res SchemaResolver, enc *encoder) error { return fillPrepare(enc, req.expr) } @@ -111,7 +109,7 @@ func (req *UnprepareRequest) Conn() *Connection { } // Body fills an encoder with the execute request body. -func (req *UnprepareRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *UnprepareRequest) Body(res SchemaResolver, enc *encoder) error { return fillUnprepare(enc, *req.stmt) } @@ -156,7 +154,7 @@ func (req *ExecutePreparedRequest) Args(args interface{}) *ExecutePreparedReques } // Body fills an encoder with the execute request body. -func (req *ExecutePreparedRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *ExecutePreparedRequest) Body(res SchemaResolver, enc *encoder) error { return fillExecutePrepared(enc, *req.stmt, req.args) } diff --git a/queue/example_msgpack_test.go b/queue/example_msgpack_test.go index 89bdad5a3..2ed7f5542 100644 --- a/queue/example_msgpack_test.go +++ b/queue/example_msgpack_test.go @@ -15,14 +15,13 @@ import ( "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/queue" - "gopkg.in/vmihailenco/msgpack.v2" ) type dummyData struct { Dummy bool } -func (c *dummyData) DecodeMsgpack(d *msgpack.Decoder) error { +func (c *dummyData) DecodeMsgpack(d *decoder) error { var err error if c.Dummy, err = d.DecodeBool(); err != nil { return err @@ -30,7 +29,7 @@ func (c *dummyData) DecodeMsgpack(d *msgpack.Decoder) error { return nil } -func (c *dummyData) EncodeMsgpack(e *msgpack.Encoder) error { +func (c *dummyData) EncodeMsgpack(e *encoder) error { return e.EncodeBool(c.Dummy) } diff --git a/queue/msgpack.go b/queue/msgpack.go new file mode 100644 index 000000000..d614374ce --- /dev/null +++ b/queue/msgpack.go @@ -0,0 +1,7 @@ +package queue + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +type decoder = msgpack.Decoder diff --git a/queue/msgpack_helper_test.go b/queue/msgpack_helper_test.go new file mode 100644 index 000000000..cfb8d8e89 --- /dev/null +++ b/queue/msgpack_helper_test.go @@ -0,0 +1,8 @@ +package queue_test + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder diff --git a/queue/queue.go b/queue/queue.go index 68a98672b..4fe8fe296 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -13,7 +13,6 @@ import ( "time" "github.com/tarantool/go-tarantool" - msgpack "gopkg.in/vmihailenco/msgpack.v2" ) // Queue is a handle to Tarantool queue's tube. @@ -336,7 +335,7 @@ type queueData struct { result interface{} } -func (qd *queueData) DecodeMsgpack(d *msgpack.Decoder) error { +func (qd *queueData) DecodeMsgpack(d *decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { diff --git a/queue/queue_test.go b/queue/queue_test.go index 25af17b12..807ac79e0 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -11,7 +11,6 @@ import ( . "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/queue" "github.com/tarantool/go-tarantool/test_helpers" - "gopkg.in/vmihailenco/msgpack.v2" ) var server = "127.0.0.1:3013" @@ -183,7 +182,7 @@ type customData struct { customField string } -func (c *customData) DecodeMsgpack(d *msgpack.Decoder) error { +func (c *customData) DecodeMsgpack(d *decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { @@ -198,7 +197,7 @@ func (c *customData) DecodeMsgpack(d *msgpack.Decoder) error { return nil } -func (c *customData) EncodeMsgpack(e *msgpack.Encoder) error { +func (c *customData) EncodeMsgpack(e *encoder) error { if err := e.EncodeArrayLen(1); err != nil { return err } diff --git a/queue/task.go b/queue/task.go index add05efd6..2f4242311 100644 --- a/queue/task.go +++ b/queue/task.go @@ -2,8 +2,6 @@ package queue import ( "fmt" - - msgpack "gopkg.in/vmihailenco/msgpack.v2" ) // Task represents a task from Tarantool queue's tube. @@ -14,7 +12,7 @@ type Task struct { q *queue } -func (t *Task) DecodeMsgpack(d *msgpack.Decoder) error { +func (t *Task) DecodeMsgpack(d *decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { diff --git a/request.go b/request.go index 7d14d3710..625380306 100644 --- a/request.go +++ b/request.go @@ -6,11 +6,9 @@ import ( "reflect" "strings" "sync" - - "gopkg.in/vmihailenco/msgpack.v2" ) -func fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { +func fillSearch(enc *encoder, spaceNo, indexNo uint32, key interface{}) error { enc.EncodeUint(KeySpaceNo) enc.EncodeUint(uint(spaceNo)) enc.EncodeUint(KeyIndexNo) @@ -19,7 +17,7 @@ func fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) return enc.Encode(key) } -func fillIterator(enc *msgpack.Encoder, offset, limit, iterator uint32) { +func fillIterator(enc *encoder, offset, limit, iterator uint32) { enc.EncodeUint(KeyIterator) enc.EncodeUint(uint(iterator)) enc.EncodeUint(KeyOffset) @@ -28,7 +26,7 @@ func fillIterator(enc *msgpack.Encoder, offset, limit, iterator uint32) { enc.EncodeUint(uint(limit)) } -func fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { +func fillInsert(enc *encoder, spaceNo uint32, tuple interface{}) error { enc.EncodeMapLen(2) enc.EncodeUint(KeySpaceNo) enc.EncodeUint(uint(spaceNo)) @@ -36,13 +34,13 @@ func fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { return enc.Encode(tuple) } -func fillSelect(enc *msgpack.Encoder, spaceNo, indexNo, offset, limit, iterator uint32, key interface{}) error { +func fillSelect(enc *encoder, spaceNo, indexNo, offset, limit, iterator uint32, key interface{}) error { enc.EncodeMapLen(6) fillIterator(enc, offset, limit, iterator) return fillSearch(enc, spaceNo, indexNo, key) } -func fillUpdate(enc *msgpack.Encoder, spaceNo, indexNo uint32, key, ops interface{}) error { +func fillUpdate(enc *encoder, spaceNo, indexNo uint32, key, ops interface{}) error { enc.EncodeMapLen(4) if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { return err @@ -51,7 +49,7 @@ func fillUpdate(enc *msgpack.Encoder, spaceNo, indexNo uint32, key, ops interfac return enc.Encode(ops) } -func fillUpsert(enc *msgpack.Encoder, spaceNo uint32, tuple, ops interface{}) error { +func fillUpsert(enc *encoder, spaceNo uint32, tuple, ops interface{}) error { enc.EncodeMapLen(3) enc.EncodeUint(KeySpaceNo) enc.EncodeUint(uint(spaceNo)) @@ -63,12 +61,12 @@ func fillUpsert(enc *msgpack.Encoder, spaceNo uint32, tuple, ops interface{}) er return enc.Encode(ops) } -func fillDelete(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { +func fillDelete(enc *encoder, spaceNo, indexNo uint32, key interface{}) error { enc.EncodeMapLen(3) return fillSearch(enc, spaceNo, indexNo, key) } -func fillCall(enc *msgpack.Encoder, functionName string, args interface{}) error { +func fillCall(enc *encoder, functionName string, args interface{}) error { enc.EncodeMapLen(2) enc.EncodeUint(KeyFunctionName) enc.EncodeString(functionName) @@ -76,7 +74,7 @@ func fillCall(enc *msgpack.Encoder, functionName string, args interface{}) error return enc.Encode(args) } -func fillEval(enc *msgpack.Encoder, expr string, args interface{}) error { +func fillEval(enc *encoder, expr string, args interface{}) error { enc.EncodeMapLen(2) enc.EncodeUint(KeyExpression) enc.EncodeString(expr) @@ -84,7 +82,7 @@ func fillEval(enc *msgpack.Encoder, expr string, args interface{}) error { return enc.Encode(args) } -func fillExecute(enc *msgpack.Encoder, expr string, args interface{}) error { +func fillExecute(enc *encoder, expr string, args interface{}) error { enc.EncodeMapLen(2) enc.EncodeUint(KeySQLText) enc.EncodeString(expr) @@ -92,7 +90,7 @@ func fillExecute(enc *msgpack.Encoder, expr string, args interface{}) error { return encodeSQLBind(enc, args) } -func fillPing(enc *msgpack.Encoder) error { +func fillPing(enc *encoder) error { return enc.EncodeMapLen(0) } @@ -197,7 +195,7 @@ type single struct { found bool } -func (s *single) DecodeMsgpack(d *msgpack.Decoder) error { +func (s *single) DecodeMsgpack(d *decoder) error { var err error var len int if len, err = d.DecodeArrayLen(); err != nil { @@ -404,7 +402,7 @@ type KeyValueBind struct { // to avoid extra allocations in heap by calling strings.ToLower() var lowerCaseNames sync.Map -func encodeSQLBind(enc *msgpack.Encoder, from interface{}) error { +func encodeSQLBind(enc *encoder, from interface{}) error { // internal function for encoding single map in msgpack encodeKeyInterface := func(key string, val interface{}) error { if err := enc.EncodeMapLen(1); err != nil { @@ -537,7 +535,7 @@ type Request interface { // Code returns a IPROTO code for the request. Code() int32 // Body fills an encoder with a request body. - Body(resolver SchemaResolver, enc *msgpack.Encoder) error + Body(resolver SchemaResolver, enc *encoder) error // Ctx returns a context of the request. Ctx() context.Context } @@ -597,7 +595,7 @@ func newAuthRequest(user, scramble string) *authRequest { } // Body fills an encoder with the auth request body. -func (req *authRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *authRequest) Body(res SchemaResolver, enc *encoder) error { return enc.Encode(map[uint32]interface{}{ KeyUserName: req.user, KeyTuple: []interface{}{string("chap-sha1"), string(req.scramble)}, @@ -618,7 +616,7 @@ func NewPingRequest() *PingRequest { } // Body fills an encoder with the ping request body. -func (req *PingRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *PingRequest) Body(res SchemaResolver, enc *encoder) error { return fillPing(enc) } @@ -694,7 +692,7 @@ func (req *SelectRequest) Key(key interface{}) *SelectRequest { } // Body fills an encoder with the select request body. -func (req *SelectRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *SelectRequest) Body(res SchemaResolver, enc *encoder) error { spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) if err != nil { return err @@ -738,7 +736,7 @@ func (req *InsertRequest) Tuple(tuple interface{}) *InsertRequest { } // Body fills an encoder with the insert request body. -func (req *InsertRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *InsertRequest) Body(res SchemaResolver, enc *encoder) error { spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) if err != nil { return err @@ -782,7 +780,7 @@ func (req *ReplaceRequest) Tuple(tuple interface{}) *ReplaceRequest { } // Body fills an encoder with the replace request body. -func (req *ReplaceRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *ReplaceRequest) Body(res SchemaResolver, enc *encoder) error { spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) if err != nil { return err @@ -833,7 +831,7 @@ func (req *DeleteRequest) Key(key interface{}) *DeleteRequest { } // Body fills an encoder with the delete request body. -func (req *DeleteRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *DeleteRequest) Body(res SchemaResolver, enc *encoder) error { spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) if err != nil { return err @@ -895,7 +893,7 @@ func (req *UpdateRequest) Operations(ops *Operations) *UpdateRequest { } // Body fills an encoder with the update request body. -func (req *UpdateRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *UpdateRequest) Body(res SchemaResolver, enc *encoder) error { spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) if err != nil { return err @@ -950,7 +948,7 @@ func (req *UpsertRequest) Operations(ops *Operations) *UpsertRequest { } // Body fills an encoder with the upsert request body. -func (req *UpsertRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *UpsertRequest) Body(res SchemaResolver, enc *encoder) error { spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) if err != nil { return err @@ -997,7 +995,7 @@ func (req *CallRequest) Args(args interface{}) *CallRequest { } // Body fills an encoder with the call request body. -func (req *CallRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *CallRequest) Body(res SchemaResolver, enc *encoder) error { return fillCall(enc, req.function, req.args) } @@ -1054,7 +1052,7 @@ func (req *EvalRequest) Args(args interface{}) *EvalRequest { } // Body fills an encoder with the eval request body. -func (req *EvalRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *EvalRequest) Body(res SchemaResolver, enc *encoder) error { return fillEval(enc, req.expr, req.args) } @@ -1094,7 +1092,7 @@ func (req *ExecuteRequest) Args(args interface{}) *ExecuteRequest { } // Body fills an encoder with the execute request body. -func (req *ExecuteRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *ExecuteRequest) Body(res SchemaResolver, enc *encoder) error { return fillExecute(enc, req.expr, req.args) } diff --git a/request_test.go b/request_test.go index b1a558b59..6ba5cecd4 100644 --- a/request_test.go +++ b/request_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/assert" . "github.com/tarantool/go-tarantool" - "gopkg.in/vmihailenco/msgpack.v2" ) const invalidSpaceMsg = "invalid space" @@ -61,7 +60,7 @@ func assertBodyCall(t testing.TB, requests []Request, errorMsg string) { const errBegin = "An unexpected Request.Body() " for _, req := range requests { var reqBuf bytes.Buffer - enc := msgpack.NewEncoder(&reqBuf) + enc := NewEncoder(&reqBuf) err := req.Body(&resolver, enc) if err != nil && errorMsg != "" && err.Error() != errorMsg { @@ -80,7 +79,7 @@ func assertBodyEqual(t testing.TB, reference []byte, req Request) { t.Helper() var reqBuf bytes.Buffer - reqEnc := msgpack.NewEncoder(&reqBuf) + reqEnc := NewEncoder(&reqBuf) err := req.Body(&resolver, reqEnc) if err != nil { @@ -196,7 +195,7 @@ func TestRequestsCodes(t *testing.T) { func TestPingRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplPingBody(refEnc) if err != nil { t.Errorf("An unexpected RefImplPingBody() error: %q", err.Error()) @@ -210,7 +209,7 @@ func TestPingRequestDefaultValues(t *testing.T) { func TestSelectRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterAll, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) @@ -225,7 +224,7 @@ func TestSelectRequestDefaultIteratorEqIfKey(t *testing.T) { var refBuf bytes.Buffer key := []interface{}{uint(18)} - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterEq, key) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) @@ -242,7 +241,7 @@ func TestSelectRequestIteratorNotChangedIfKey(t *testing.T) { key := []interface{}{uint(678)} const iter = IterGe - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, iter, key) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) @@ -262,7 +261,7 @@ func TestSelectRequestSetters(t *testing.T) { key := []interface{}{uint(36)} var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplSelectBody(refEnc, validSpace, validIndex, offset, limit, iter, key) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) @@ -281,7 +280,7 @@ func TestSelectRequestSetters(t *testing.T) { func TestInsertRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplInsertBody(refEnc, validSpace, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplInsertBody() error: %q", err.Error()) @@ -296,7 +295,7 @@ func TestInsertRequestSetters(t *testing.T) { tuple := []interface{}{uint(24)} var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplInsertBody(refEnc, validSpace, tuple) if err != nil { t.Errorf("An unexpected RefImplInsertBody() error: %q", err.Error()) @@ -311,7 +310,7 @@ func TestInsertRequestSetters(t *testing.T) { func TestReplaceRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplReplaceBody(refEnc, validSpace, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplReplaceBody() error: %q", err.Error()) @@ -326,7 +325,7 @@ func TestReplaceRequestSetters(t *testing.T) { tuple := []interface{}{uint(99)} var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplReplaceBody(refEnc, validSpace, tuple) if err != nil { t.Errorf("An unexpected RefImplReplaceBody() error: %q", err.Error()) @@ -341,7 +340,7 @@ func TestReplaceRequestSetters(t *testing.T) { func TestDeleteRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplDeleteBody(refEnc, validSpace, defaultIndex, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplDeleteBody() error: %q", err.Error()) @@ -356,7 +355,7 @@ func TestDeleteRequestSetters(t *testing.T) { key := []interface{}{uint(923)} var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplDeleteBody(refEnc, validSpace, validIndex, key) if err != nil { t.Errorf("An unexpected RefImplDeleteBody() error: %q", err.Error()) @@ -372,7 +371,7 @@ func TestDeleteRequestSetters(t *testing.T) { func TestUpdateRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplUpdateBody(refEnc, validSpace, defaultIndex, []interface{}{}, []Op{}) if err != nil { t.Errorf("An unexpected RefImplUpdateBody() error: %q", err.Error()) @@ -388,7 +387,7 @@ func TestUpdateRequestSetters(t *testing.T) { refOps, reqOps := getTestOps() var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplUpdateBody(refEnc, validSpace, validIndex, key, refOps) if err != nil { t.Errorf("An unexpected RefImplUpdateBody() error: %q", err.Error()) @@ -405,7 +404,7 @@ func TestUpdateRequestSetters(t *testing.T) { func TestUpsertRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplUpsertBody(refEnc, validSpace, []interface{}{}, []Op{}) if err != nil { t.Errorf("An unexpected RefImplUpsertBody() error: %q", err.Error()) @@ -421,7 +420,7 @@ func TestUpsertRequestSetters(t *testing.T) { refOps, reqOps := getTestOps() var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplUpsertBody(refEnc, validSpace, tuple, refOps) if err != nil { t.Errorf("An unexpected RefImplUpsertBody() error: %q", err.Error()) @@ -437,7 +436,7 @@ func TestUpsertRequestSetters(t *testing.T) { func TestCallRequestsDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplCallBody(refEnc, validExpr, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) @@ -456,7 +455,7 @@ func TestCallRequestsSetters(t *testing.T) { args := []interface{}{uint(34)} var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplCallBody(refEnc, validExpr, args) if err != nil { t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) @@ -477,7 +476,7 @@ func TestCallRequestsSetters(t *testing.T) { func TestEvalRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplEvalBody(refEnc, validExpr, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplEvalBody() error: %q", err.Error()) @@ -492,7 +491,7 @@ func TestEvalRequestSetters(t *testing.T) { args := []interface{}{uint(34), int(12)} var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplEvalBody(refEnc, validExpr, args) if err != nil { t.Errorf("An unexpected RefImplEvalBody() error: %q", err.Error()) @@ -507,7 +506,7 @@ func TestEvalRequestSetters(t *testing.T) { func TestExecuteRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplExecuteBody(refEnc, validExpr, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplExecuteBody() error: %q", err.Error()) @@ -522,7 +521,7 @@ func TestExecuteRequestSetters(t *testing.T) { args := []interface{}{uint(11)} var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplExecuteBody(refEnc, validExpr, args) if err != nil { t.Errorf("An unexpected RefImplExecuteBody() error: %q", err.Error()) @@ -537,7 +536,7 @@ func TestExecuteRequestSetters(t *testing.T) { func TestPrepareRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplPrepareBody(refEnc, validExpr) if err != nil { t.Errorf("An unexpected RefImplPrepareBody() error: %q", err.Error()) @@ -551,7 +550,7 @@ func TestPrepareRequestDefaultValues(t *testing.T) { func TestUnprepareRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplUnprepareBody(refEnc, *validStmt) if err != nil { t.Errorf("An unexpected RefImplUnprepareBody() error: %q", err.Error()) @@ -567,7 +566,7 @@ func TestExecutePreparedRequestSetters(t *testing.T) { args := []interface{}{uint(11)} var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplExecutePreparedBody(refEnc, *validStmt, args) if err != nil { t.Errorf("An unexpected RefImplExecutePreparedBody() error: %q", err.Error()) @@ -583,7 +582,7 @@ func TestExecutePreparedRequestSetters(t *testing.T) { func TestExecutePreparedRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplExecutePreparedBody(refEnc, *validStmt, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplExecutePreparedBody() error: %q", err.Error()) @@ -598,7 +597,7 @@ func TestExecutePreparedRequestDefaultValues(t *testing.T) { func TestBeginRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplBeginBody(refEnc, defaultIsolationLevel, defaultTimeout) if err != nil { t.Errorf("An unexpected RefImplBeginBody() error: %q", err.Error()) @@ -612,7 +611,7 @@ func TestBeginRequestDefaultValues(t *testing.T) { func TestBeginRequestSetters(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplBeginBody(refEnc, ReadConfirmedLevel, validTimeout) if err != nil { t.Errorf("An unexpected RefImplBeginBody() error: %q", err.Error()) @@ -626,7 +625,7 @@ func TestBeginRequestSetters(t *testing.T) { func TestCommitRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplCommitBody(refEnc) if err != nil { t.Errorf("An unexpected RefImplCommitBody() error: %q", err.Error()) @@ -640,7 +639,7 @@ func TestCommitRequestDefaultValues(t *testing.T) { func TestRollbackRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := msgpack.NewEncoder(&refBuf) + refEnc := NewEncoder(&refBuf) err := RefImplRollbackBody(refEnc) if err != nil { t.Errorf("An unexpected RefImplRollbackBody() error: %q", err.Error()) diff --git a/response.go b/response.go index 3fd7322b0..96b2c15fa 100644 --- a/response.go +++ b/response.go @@ -2,8 +2,6 @@ package tarantool import ( "fmt" - - "gopkg.in/vmihailenco/msgpack.v2" ) type Response struct { @@ -31,7 +29,7 @@ type SQLInfo struct { InfoAutoincrementIds []uint64 } -func (meta *ColumnMetaData) DecodeMsgpack(d *msgpack.Decoder) error { +func (meta *ColumnMetaData) DecodeMsgpack(d *decoder) error { var err error var l int if l, err = d.DecodeMapLen(); err != nil { @@ -69,7 +67,7 @@ func (meta *ColumnMetaData) DecodeMsgpack(d *msgpack.Decoder) error { return nil } -func (info *SQLInfo) DecodeMsgpack(d *msgpack.Decoder) error { +func (info *SQLInfo) DecodeMsgpack(d *decoder) error { var err error var l int if l, err = d.DecodeMapLen(); err != nil { @@ -99,7 +97,7 @@ func (info *SQLInfo) DecodeMsgpack(d *msgpack.Decoder) error { return nil } -func (resp *Response) smallInt(d *msgpack.Decoder) (i int, err error) { +func (resp *Response) smallInt(d *decoder) (i int, err error) { b, err := resp.buf.ReadByte() if err != nil { return @@ -111,7 +109,7 @@ func (resp *Response) smallInt(d *msgpack.Decoder) (i int, err error) { return d.DecodeInt() } -func (resp *Response) decodeHeader(d *msgpack.Decoder) (err error) { +func (resp *Response) decodeHeader(d *decoder) (err error) { var l int d.Reset(&resp.buf) if l, err = d.DecodeMapLen(); err != nil { @@ -148,7 +146,9 @@ func (resp *Response) decodeBody() (err error) { if resp.buf.Len() > 2 { var l int var stmtID, bindCount uint64 - d := msgpack.NewDecoder(&resp.buf) + + d := newDecoder(&resp.buf) + if l, err = d.DecodeMapLen(); err != nil { return err } @@ -212,7 +212,7 @@ func (resp *Response) decodeBody() (err error) { func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if resp.buf.Len() > 0 { var l int - d := msgpack.NewDecoder(&resp.buf) + d := newDecoder(&resp.buf) if l, err = d.DecodeMapLen(); err != nil { return err } diff --git a/schema.go b/schema.go index fc3b0793a..ecf565762 100644 --- a/schema.go +++ b/schema.go @@ -3,8 +3,6 @@ package tarantool import ( "errors" "fmt" - "gopkg.in/vmihailenco/msgpack.v2" - msgpcode "gopkg.in/vmihailenco/msgpack.v2/codes" ) //nolint: varcheck,deadcode @@ -51,27 +49,7 @@ type Space struct { IndexesById map[uint32]*Index } -func isUint(code byte) bool { - return code == msgpcode.Uint8 || code == msgpcode.Uint16 || - code == msgpcode.Uint32 || code == msgpcode.Uint64 || - msgpcode.IsFixedNum(code) -} - -func isMap(code byte) bool { - return code == msgpcode.Map16 || code == msgpcode.Map32 || msgpcode.IsFixedMap(code) -} - -func isArray(code byte) bool { - return code == msgpcode.Array16 || code == msgpcode.Array32 || - msgpcode.IsFixedArray(code) -} - -func isString(code byte) bool { - return msgpcode.IsFixedString(code) || code == msgpcode.Str8 || - code == msgpcode.Str16 || code == msgpcode.Str32 -} - -func (space *Space) DecodeMsgpack(d *msgpack.Decoder) error { +func (space *Space) DecodeMsgpack(d *decoder) error { arrayLen, err := d.DecodeArrayLen() if err != nil { return err @@ -96,13 +74,13 @@ func (space *Space) DecodeMsgpack(d *msgpack.Decoder) error { if err != nil { return err } - if isString(code) { + if msgpackIsString(code) { val, err := d.DecodeString() if err != nil { return err } space.Temporary = val == "temporary" - } else if isMap(code) { + } else if msgpackIsMap(code) { mapLen, err := d.DecodeMapLen() if err != nil { return err @@ -156,7 +134,7 @@ type Field struct { Type string } -func (field *Field) DecodeMsgpack(d *msgpack.Decoder) error { +func (field *Field) DecodeMsgpack(d *decoder) error { l, err := d.DecodeMapLen() if err != nil { return err @@ -194,7 +172,7 @@ type Index struct { Fields []*IndexField } -func (index *Index) DecodeMsgpack(d *msgpack.Decoder) error { +func (index *Index) DecodeMsgpack(d *decoder) error { _, err := d.DecodeArrayLen() if err != nil { return err @@ -218,7 +196,7 @@ func (index *Index) DecodeMsgpack(d *msgpack.Decoder) error { return err } - if isUint(code) { + if msgpackIsUint(code) { optsUint64, err := d.DecodeUint64() if err != nil { return nil @@ -241,7 +219,7 @@ func (index *Index) DecodeMsgpack(d *msgpack.Decoder) error { return err } - if isUint(code) { + if msgpackIsUint(code) { fieldCount, err := d.DecodeUint64() if err != nil { return err @@ -270,13 +248,13 @@ type IndexField struct { Type string } -func (indexField *IndexField) DecodeMsgpack(d *msgpack.Decoder) error { +func (indexField *IndexField) DecodeMsgpack(d *decoder) error { code, err := d.PeekCode() if err != nil { return err } - if isMap(code) { + if msgpackIsMap(code) { mapLen, err := d.DecodeMapLen() if err != nil { return err @@ -302,7 +280,7 @@ func (indexField *IndexField) DecodeMsgpack(d *msgpack.Decoder) error { } } return nil - } else if isArray(code) { + } else if msgpackIsArray(code) { arrayLen, err := d.DecodeArrayLen() if err != nil { return err diff --git a/stream.go b/stream.go index 62ea0adf0..009f6c0aa 100644 --- a/stream.go +++ b/stream.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "time" - - "gopkg.in/vmihailenco/msgpack.v2" ) type TxnIsolationLevel uint @@ -29,7 +27,7 @@ type Stream struct { Conn *Connection } -func fillBegin(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, timeout time.Duration) error { +func fillBegin(enc *encoder, txnIsolation TxnIsolationLevel, timeout time.Duration) error { hasTimeout := timeout > 0 hasIsolationLevel := txnIsolation != DefaultIsolationLevel mapLen := 0 @@ -72,11 +70,11 @@ func fillBegin(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, timeout tim return err } -func fillCommit(enc *msgpack.Encoder) error { +func fillCommit(enc *encoder) error { return enc.EncodeMapLen(0) } -func fillRollback(enc *msgpack.Encoder) error { +func fillRollback(enc *encoder) error { return enc.EncodeMapLen(0) } @@ -111,7 +109,7 @@ func (req *BeginRequest) Timeout(timeout time.Duration) *BeginRequest { } // Body fills an encoder with the begin request body. -func (req *BeginRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *BeginRequest) Body(res SchemaResolver, enc *encoder) error { return fillBegin(enc, req.txnIsolation, req.timeout) } @@ -141,7 +139,7 @@ func NewCommitRequest() *CommitRequest { } // Body fills an encoder with the commit request body. -func (req *CommitRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *CommitRequest) Body(res SchemaResolver, enc *encoder) error { return fillCommit(enc) } @@ -171,7 +169,7 @@ func NewRollbackRequest() *RollbackRequest { } // Body fills an encoder with the rollback request body. -func (req *RollbackRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req *RollbackRequest) Body(res SchemaResolver, enc *encoder) error { return fillRollback(enc) } diff --git a/tarantool_test.go b/tarantool_test.go index f3fdbb787..ea0bc784d 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/assert" . "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/test_helpers" - "gopkg.in/vmihailenco/msgpack.v2" ) type Member struct { @@ -26,7 +25,7 @@ type Member struct { Val uint } -func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { +func (m *Member) EncodeMsgpack(e *encoder) error { if err := e.EncodeArrayLen(2); err != nil { return err } @@ -39,7 +38,7 @@ func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { return nil } -func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { +func (m *Member) DecodeMsgpack(d *decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { diff --git a/test_helpers/msgpack.go b/test_helpers/msgpack.go new file mode 100644 index 000000000..f18b17f48 --- /dev/null +++ b/test_helpers/msgpack.go @@ -0,0 +1,7 @@ +package test_helpers + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +type encoder = msgpack.Encoder diff --git a/test_helpers/request_mock.go b/test_helpers/request_mock.go index 630d57e66..93551e34a 100644 --- a/test_helpers/request_mock.go +++ b/test_helpers/request_mock.go @@ -4,7 +4,6 @@ import ( "context" "github.com/tarantool/go-tarantool" - "gopkg.in/vmihailenco/msgpack.v2" ) type StrangerRequest struct { @@ -18,7 +17,7 @@ func (sr *StrangerRequest) Code() int32 { return 0 } -func (sr *StrangerRequest) Body(resolver tarantool.SchemaResolver, enc *msgpack.Encoder) error { +func (sr *StrangerRequest) Body(resolver tarantool.SchemaResolver, enc *encoder) error { return nil } diff --git a/uuid/msgpack.go b/uuid/msgpack.go new file mode 100644 index 000000000..1fcbc711b --- /dev/null +++ b/uuid/msgpack.go @@ -0,0 +1,16 @@ +package uuid + +import ( + "reflect" + + "github.com/google/uuid" + "gopkg.in/vmihailenco/msgpack.v2" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +func init() { + msgpack.Register(reflect.TypeOf((*uuid.UUID)(nil)).Elem(), encodeUUID, decodeUUID) + msgpack.RegisterExt(UUID_extId, (*uuid.UUID)(nil)) +} diff --git a/uuid/msgpack_helper_test.go b/uuid/msgpack_helper_test.go new file mode 100644 index 000000000..e8550f050 --- /dev/null +++ b/uuid/msgpack_helper_test.go @@ -0,0 +1,7 @@ +package uuid_test + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +type decoder = msgpack.Decoder diff --git a/uuid/uuid.go b/uuid/uuid.go index 7d362c3d8..a35f33faf 100644 --- a/uuid/uuid.go +++ b/uuid/uuid.go @@ -18,13 +18,12 @@ import ( "reflect" "github.com/google/uuid" - "gopkg.in/vmihailenco/msgpack.v2" ) // UUID external type. const UUID_extId = 2 -func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { +func encodeUUID(e *encoder, v reflect.Value) error { id := v.Interface().(uuid.UUID) bytes, err := id.MarshalBinary() @@ -40,7 +39,7 @@ func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { return nil } -func decodeUUID(d *msgpack.Decoder, v reflect.Value) error { +func decodeUUID(d *decoder, v reflect.Value) error { var bytesCount int = 16 bytes := make([]byte, bytesCount) @@ -60,8 +59,3 @@ func decodeUUID(d *msgpack.Decoder, v reflect.Value) error { v.Set(reflect.ValueOf(id)) return nil } - -func init() { - msgpack.Register(reflect.TypeOf((*uuid.UUID)(nil)).Elem(), encodeUUID, decodeUUID) - msgpack.RegisterExt(UUID_extId, (*uuid.UUID)(nil)) -} diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index aed760009..6224a938b 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -11,7 +11,6 @@ import ( . "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/test_helpers" _ "github.com/tarantool/go-tarantool/uuid" - "gopkg.in/vmihailenco/msgpack.v2" ) // There is no way to skip tests in testing.M, @@ -33,7 +32,7 @@ type TupleUUID struct { id uuid.UUID } -func (t *TupleUUID) DecodeMsgpack(d *msgpack.Decoder) error { +func (t *TupleUUID) DecodeMsgpack(d *decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { From 96f4dd0249f7861373678d41f618213569c1fe42 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 18 May 2022 17:37:21 +0300 Subject: [PATCH 313/605] msgpack: add wrappers for EncodeUint/EncodeInt EncodeUint and EncodeInt have different argument types in msgpack.v2[1][2] and msgpack.v5[3][4]. The wrappers will simplify migration to msgpack.v5. 1. https://pkg.go.dev/github.com/vmihailenco/msgpack@v2.9.2+incompatible#Encoder.EncodeUint 2. https://pkg.go.dev/github.com/vmihailenco/msgpack@v2.9.2+incompatible#Encoder.EncodeInt 3. https://pkg.go.dev/github.com/vmihailenco/msgpack/v5#Encoder.EncodeUint 4. https://pkg.go.dev/github.com/vmihailenco/msgpack/v5#Encoder.EncodeInt Part of #124 --- client_tools.go | 16 +++++----- datetime/datetime_test.go | 2 +- datetime/msgpack_helper_test.go | 4 +++ example_custom_unpacking_test.go | 2 +- msgpack.go | 8 +++++ msgpack_helper_test.go | 4 +++ prepared.go | 12 ++++---- request.go | 50 ++++++++++++++++---------------- stream.go | 6 ++-- tarantool_test.go | 2 +- 10 files changed, 61 insertions(+), 45 deletions(-) diff --git a/client_tools.go b/client_tools.go index 88da9c577..a5d6fbba9 100644 --- a/client_tools.go +++ b/client_tools.go @@ -8,7 +8,7 @@ type IntKey struct { func (k IntKey) EncodeMsgpack(enc *encoder) error { enc.EncodeArrayLen(1) - enc.EncodeInt(k.I) + encodeInt(enc, int64(k.I)) return nil } @@ -20,7 +20,7 @@ type UintKey struct { func (k UintKey) EncodeMsgpack(enc *encoder) error { enc.EncodeArrayLen(1) - enc.EncodeUint(k.I) + encodeUint(enc, uint64(k.I)) return nil } @@ -44,8 +44,8 @@ type IntIntKey struct { func (k IntIntKey) EncodeMsgpack(enc *encoder) error { enc.EncodeArrayLen(2) - enc.EncodeInt(k.I1) - enc.EncodeInt(k.I2) + encodeInt(enc, int64(k.I1)) + encodeInt(enc, int64(k.I2)) return nil } @@ -59,7 +59,7 @@ type Op struct { func (o Op) EncodeMsgpack(enc *encoder) error { enc.EncodeArrayLen(3) enc.EncodeString(o.Op) - enc.EncodeInt(o.Field) + encodeInt(enc, int64(o.Field)) return enc.Encode(o.Arg) } @@ -147,9 +147,9 @@ type OpSplice struct { func (o OpSplice) EncodeMsgpack(enc *encoder) error { enc.EncodeArrayLen(5) enc.EncodeString(o.Op) - enc.EncodeInt(o.Field) - enc.EncodeInt(o.Pos) - enc.EncodeInt(o.Len) + encodeInt(enc, int64(o.Field)) + encodeInt(enc, int64(o.Pos)) + encodeInt(enc, int64(o.Len)) enc.EncodeString(o.Replace) return nil } diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 035b1025b..176f8a40c 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -332,7 +332,7 @@ func (c *Tuple2) EncodeMsgpack(e *encoder) error { if err := e.EncodeArrayLen(3); err != nil { return err } - if err := e.EncodeUint(c.Cid); err != nil { + if err := encodeUint(e, uint64(c.Cid)); err != nil { return err } if err := e.EncodeString(c.Orig); err != nil { diff --git a/datetime/msgpack_helper_test.go b/datetime/msgpack_helper_test.go index ba283db18..7b045a863 100644 --- a/datetime/msgpack_helper_test.go +++ b/datetime/msgpack_helper_test.go @@ -7,6 +7,10 @@ import ( type encoder = msgpack.Encoder type decoder = msgpack.Decoder +func encodeUint(e *encoder, v uint64) error { + return e.EncodeUint(uint(v)) +} + func marshal(v interface{}) ([]byte, error) { return msgpack.Marshal(v) } diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index cff50da50..4087c5620 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -27,7 +27,7 @@ func (c *Tuple2) EncodeMsgpack(e *encoder) error { if err := e.EncodeArrayLen(3); err != nil { return err } - if err := e.EncodeUint(c.Cid); err != nil { + if err := encodeUint(e, uint64(c.Cid)); err != nil { return err } if err := e.EncodeString(c.Orig); err != nil { diff --git a/msgpack.go b/msgpack.go index bcdcc06ce..e48e14a21 100644 --- a/msgpack.go +++ b/msgpack.go @@ -18,6 +18,14 @@ func newDecoder(r io.Reader) *decoder { return msgpack.NewDecoder(r) } +func encodeUint(e *encoder, v uint64) error { + return e.EncodeUint(uint(v)) +} + +func encodeInt(e *encoder, v int64) error { + return e.EncodeInt(int(v)) +} + func msgpackIsUint(code byte) bool { return code == msgpcode.Uint8 || code == msgpcode.Uint16 || code == msgpcode.Uint32 || code == msgpcode.Uint64 || diff --git a/msgpack_helper_test.go b/msgpack_helper_test.go index 8623059e5..fcadbc573 100644 --- a/msgpack_helper_test.go +++ b/msgpack_helper_test.go @@ -6,3 +6,7 @@ import ( type encoder = msgpack.Encoder type decoder = msgpack.Decoder + +func encodeUint(e *encoder, v uint64) error { + return e.EncodeUint(uint(v)) +} diff --git a/prepared.go b/prepared.go index 91c5238a7..6f63e3bb1 100644 --- a/prepared.go +++ b/prepared.go @@ -20,21 +20,21 @@ type Prepared struct { func fillPrepare(enc *encoder, expr string) error { enc.EncodeMapLen(1) - enc.EncodeUint(KeySQLText) + encodeUint(enc, KeySQLText) return enc.EncodeString(expr) } func fillUnprepare(enc *encoder, stmt Prepared) error { enc.EncodeMapLen(1) - enc.EncodeUint(KeyStmtID) - return enc.EncodeUint(uint(stmt.StatementID)) + encodeUint(enc, KeyStmtID) + return encodeUint(enc, uint64(stmt.StatementID)) } func fillExecutePrepared(enc *encoder, stmt Prepared, args interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint(KeyStmtID) - enc.EncodeUint(uint(stmt.StatementID)) - enc.EncodeUint(KeySQLBind) + encodeUint(enc, KeyStmtID) + encodeUint(enc, uint64(stmt.StatementID)) + encodeUint(enc, KeySQLBind) return encodeSQLBind(enc, args) } diff --git a/request.go b/request.go index 625380306..cfa40e522 100644 --- a/request.go +++ b/request.go @@ -9,28 +9,28 @@ import ( ) func fillSearch(enc *encoder, spaceNo, indexNo uint32, key interface{}) error { - enc.EncodeUint(KeySpaceNo) - enc.EncodeUint(uint(spaceNo)) - enc.EncodeUint(KeyIndexNo) - enc.EncodeUint(uint(indexNo)) - enc.EncodeUint(KeyKey) + encodeUint(enc, KeySpaceNo) + encodeUint(enc, uint64(spaceNo)) + encodeUint(enc, KeyIndexNo) + encodeUint(enc, uint64(indexNo)) + encodeUint(enc, KeyKey) return enc.Encode(key) } func fillIterator(enc *encoder, offset, limit, iterator uint32) { - enc.EncodeUint(KeyIterator) - enc.EncodeUint(uint(iterator)) - enc.EncodeUint(KeyOffset) - enc.EncodeUint(uint(offset)) - enc.EncodeUint(KeyLimit) - enc.EncodeUint(uint(limit)) + encodeUint(enc, KeyIterator) + encodeUint(enc, uint64(iterator)) + encodeUint(enc, KeyOffset) + encodeUint(enc, uint64(offset)) + encodeUint(enc, KeyLimit) + encodeUint(enc, uint64(limit)) } func fillInsert(enc *encoder, spaceNo uint32, tuple interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint(KeySpaceNo) - enc.EncodeUint(uint(spaceNo)) - enc.EncodeUint(KeyTuple) + encodeUint(enc, KeySpaceNo) + encodeUint(enc, uint64(spaceNo)) + encodeUint(enc, KeyTuple) return enc.Encode(tuple) } @@ -45,19 +45,19 @@ func fillUpdate(enc *encoder, spaceNo, indexNo uint32, key, ops interface{}) err if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { return err } - enc.EncodeUint(KeyTuple) + encodeUint(enc, KeyTuple) return enc.Encode(ops) } func fillUpsert(enc *encoder, spaceNo uint32, tuple, ops interface{}) error { enc.EncodeMapLen(3) - enc.EncodeUint(KeySpaceNo) - enc.EncodeUint(uint(spaceNo)) - enc.EncodeUint(KeyTuple) + encodeUint(enc, KeySpaceNo) + encodeUint(enc, uint64(spaceNo)) + encodeUint(enc, KeyTuple) if err := enc.Encode(tuple); err != nil { return err } - enc.EncodeUint(KeyDefTuple) + encodeUint(enc, KeyDefTuple) return enc.Encode(ops) } @@ -68,25 +68,25 @@ func fillDelete(enc *encoder, spaceNo, indexNo uint32, key interface{}) error { func fillCall(enc *encoder, functionName string, args interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint(KeyFunctionName) + encodeUint(enc, KeyFunctionName) enc.EncodeString(functionName) - enc.EncodeUint(KeyTuple) + encodeUint(enc, KeyTuple) return enc.Encode(args) } func fillEval(enc *encoder, expr string, args interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint(KeyExpression) + encodeUint(enc, KeyExpression) enc.EncodeString(expr) - enc.EncodeUint(KeyTuple) + encodeUint(enc, KeyTuple) return enc.Encode(args) } func fillExecute(enc *encoder, expr string, args interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint(KeySQLText) + encodeUint(enc, KeySQLText) enc.EncodeString(expr) - enc.EncodeUint(KeySQLBind) + encodeUint(enc, KeySQLBind) return encodeSQLBind(enc, args) } diff --git a/stream.go b/stream.go index 009f6c0aa..3a03ec68f 100644 --- a/stream.go +++ b/stream.go @@ -44,7 +44,7 @@ func fillBegin(enc *encoder, txnIsolation TxnIsolationLevel, timeout time.Durati } if hasTimeout { - err = enc.EncodeUint(KeyTimeout) + err = encodeUint(enc, KeyTimeout) if err != nil { return err } @@ -56,12 +56,12 @@ func fillBegin(enc *encoder, txnIsolation TxnIsolationLevel, timeout time.Durati } if hasIsolationLevel { - err = enc.EncodeUint(KeyTxnIsolation) + err = encodeUint(enc, KeyTxnIsolation) if err != nil { return err } - err = enc.Encode(txnIsolation) + err = encodeUint(enc, uint64(txnIsolation)) if err != nil { return err } diff --git a/tarantool_test.go b/tarantool_test.go index ea0bc784d..18ebb0506 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -32,7 +32,7 @@ func (m *Member) EncodeMsgpack(e *encoder) error { if err := e.EncodeString(m.Name); err != nil { return err } - if err := e.EncodeUint(m.Val); err != nil { + if err := encodeUint(e, uint64(m.Val)); err != nil { return err } return nil From 33530e721efac3e411c2d5f9ce935f5208f227a2 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 18 May 2022 18:00:12 +0300 Subject: [PATCH 314/605] msgpack: add optional msgpack.v5 support The msgpack.v5 code located in msgpack_v5.go and msgpack_v5_helper_test.go for tests. It is the same logic for submodules. An user can use msgpack.v5 with a build tag: go_tarantool_msgpack_v5 The commit also unify typo conversions after decoding in tests. This is necessary because msgpack v2.9.2 decodes all numbers as uint[1] or int[2], but msgpack.v5 decodes numbers as a MessagePack type[3]. There are additional differences when decoding types. 1. https://github.com/vmihailenco/msgpack/blob/23644a15054d8b9f8a9fca3e041f3419504b9c21/decode.go#L283 2. https://github.com/vmihailenco/msgpack/blob/23644a15054d8b9f8a9fca3e041f3419504b9c21/decode.go#L285 3. https://github.com/vmihailenco/msgpack/blob/233c977ae92b215a9aa7eb420bbe67da99f05ab6/decode.go#L410 Part of #124 --- CHANGELOG.md | 2 + README.md | 36 +++++++- call_16_test.go | 10 +-- call_17_test.go | 10 +-- config.lua | 8 +- datetime/datetime_test.go | 17 ++-- datetime/msgpack.go | 3 + datetime/msgpack_helper_test.go | 10 ++- datetime/msgpack_v5.go | 12 +++ datetime/msgpack_v5_helper_test.go | 28 +++++++ decimal/decimal_test.go | 13 ++- decimal/msgpack.go | 3 + decimal/msgpack_helper_test.go | 10 +++ decimal/msgpack_v5.go | 12 +++ decimal/msgpack_v5_helper_test.go | 28 +++++++ example_test.go | 14 ++-- go.mod | 1 + go.sum | 5 ++ msgpack.go | 3 + msgpack_helper_test.go | 3 + msgpack_v5.go | 54 ++++++++++++ msgpack_v5_helper_test.go | 15 ++++ multi/call_16_test.go | 4 +- multi/call_17_test.go | 4 +- multi/config.lua | 8 +- multi/multi_test.go | 22 ++--- queue/msgpack.go | 3 + queue/msgpack_helper_test.go | 3 + queue/msgpack_v5.go | 10 +++ queue/msgpack_v5_helper_test.go | 11 +++ queue/queue.go | 25 ++++-- tarantool_test.go | 127 ++++++++++++++++++----------- test_helpers/msgpack.go | 3 + test_helpers/msgpack_v5.go | 10 +++ uuid/msgpack.go | 3 + uuid/msgpack_helper_test.go | 3 + uuid/msgpack_v5.go | 27 ++++++ uuid/msgpack_v5_helper_test.go | 10 +++ 38 files changed, 461 insertions(+), 109 deletions(-) create mode 100644 datetime/msgpack_v5.go create mode 100644 datetime/msgpack_v5_helper_test.go create mode 100644 decimal/msgpack_v5.go create mode 100644 decimal/msgpack_v5_helper_test.go create mode 100644 msgpack_v5.go create mode 100644 msgpack_v5_helper_test.go create mode 100644 queue/msgpack_v5.go create mode 100644 queue/msgpack_v5_helper_test.go create mode 100644 test_helpers/msgpack_v5.go create mode 100644 uuid/msgpack_v5.go create mode 100644 uuid/msgpack_v5_helper_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 89d5fe5a7..6381b3d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +- Optional msgpack.v5 usage (#124) + ### Changed ### Fixed diff --git a/README.md b/README.md index 0d992a344..769acea21 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ faster than other packages according to public benchmarks. * [Documentation](#documentation) * [API reference](#api-reference) * [Walking\-through example](#walking-through-example) + * [msgpack.v5 migration](#msgpackv5-migration) * [Contributing](#contributing) * [Alternative connectors](#alternative-connectors) @@ -68,7 +69,15 @@ This allows us to introduce new features without losing backward compatibility. go_tarantool_call_17 ``` **Note:** In future releases, `Call17` may be used as default `Call` behavior. -3. To run fuzz tests with decimals, you can use the build tag: +3. To replace usage of `msgpack.v2` with `msgpack.v5`, you can use the build + tag: + ``` + go_tarantool_msgpack_v5 + ``` + **Note:** In future releases, `msgpack.v5` may be used by default. We recommend + to read [msgpack.v5 migration notes](#msgpackv5-migration) and try to + use msgpack.v5 before the changes. +4. To run fuzz tests with decimals, you can use the build tag: ``` go_tarantool_decimal_fuzzing ``` @@ -144,6 +153,31 @@ There are two parameters: * a space number (it could just as easily have been a space name), and * a tuple. +### msgpack.v5 migration + +Most function names and argument types in `msgpack.v5` and `msgpack.v2` +have not changed (in our code, we noticed changes in `EncodeInt`, `EncodeUint` +and `RegisterExt`). But there are a lot of changes in a logic of encoding and +decoding. On the plus side the migration seems easy, but on the minus side you +need to be very careful. + +First of all, `EncodeInt8`, `EncodeInt16`, `EncodeInt32`, `EncodeInt64` +and `EncodeUint*` analogues at `msgpack.v5` encode numbers as is without loss of +type. In `msgpack.v2` the type of a number is reduced to a value. + +Secondly, a base decoding function does not convert numbers to `int64` or +`uint64`. It converts numbers to an exact type defined by MessagePack. The +change makes manual type conversions much more difficult and can lead to +runtime errors with an old code. We do not recommend to use type conversions +and give preference to `*Typed` functions (besides, it's faster). + +There are also changes in the logic that can lead to errors in the old code, +[as example](https://github.com/vmihailenco/msgpack/issues/327). Although in +`msgpack.v5` some functions for the logic tuning were added (see +[UseLooseInterfaceDecoding](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5#Decoder.UseLooseInterfaceDecoding), [UseCompactInts](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5#Encoder.UseCompactInts) etc), it is still impossible +to achieve full compliance of behavior between `msgpack.v5` and `msgpack.v2`. So +we don't go this way. We use standart settings if it possible. + ## Contributing See [the contributing guide](CONTRIBUTING.md) for detailed instructions on how diff --git a/call_16_test.go b/call_16_test.go index 1dab3079d..f23f0f783 100644 --- a/call_16_test.go +++ b/call_16_test.go @@ -18,11 +18,11 @@ func TestConnection_Call(t *testing.T) { defer conn.Close() // Call16 - resp, err = conn.Call("simple_incr", []interface{}{1}) + resp, err = conn.Call("simple_concat", []interface{}{"1"}) if err != nil { t.Errorf("Failed to use Call") } - if resp.Data[0].([]interface{})[0].(uint64) != 2 { + if val, ok := resp.Data[0].([]interface{})[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } } @@ -34,18 +34,18 @@ func TestCallRequest(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - req := NewCallRequest("simple_incr").Args([]interface{}{1}) + req := NewCallRequest("simple_concat").Args([]interface{}{"1"}) resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to use Call") } - if resp.Data[0].([]interface{})[0].(uint64) != 2 { + if val, ok := resp.Data[0].([]interface{})[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } } func TestCallRequestCode(t *testing.T) { - req := NewCallRequest("simple_incrt") + req := NewCallRequest("simple_concat") code := req.Code() expected := Call16RequestCode if code != int32(expected) { diff --git a/call_17_test.go b/call_17_test.go index 400c69693..824ed850b 100644 --- a/call_17_test.go +++ b/call_17_test.go @@ -18,11 +18,11 @@ func TestConnection_Call(t *testing.T) { defer conn.Close() // Call17 - resp, err = conn.Call17("simple_incr", []interface{}{1}) + resp, err = conn.Call17("simple_concat", []interface{}{"1"}) if err != nil { t.Errorf("Failed to use Call") } - if resp.Data[0].(uint64) != 2 { + if val, ok := resp.Data[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } } @@ -34,18 +34,18 @@ func TestCallRequest(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - req := NewCallRequest("simple_incr").Args([]interface{}{1}) + req := NewCallRequest("simple_concat").Args([]interface{}{"1"}) resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to use Call") } - if resp.Data[0].(uint64) != 2 { + if val, ok := resp.Data[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } } func TestCallRequestCode(t *testing.T) { - req := NewCallRequest("simple_incrt") + req := NewCallRequest("simple_concat") code := req.Code() expected := Call17RequestCode if code != int32(expected) { diff --git a/config.lua b/config.lua index 9ec12f7d5..db4aed6eb 100644 --- a/config.lua +++ b/config.lua @@ -80,7 +80,7 @@ box.once("init", function() --box.schema.user.grant('guest', 'read,write,execute', 'universe') box.schema.func.create('box.info') - box.schema.func.create('simple_incr') + box.schema.func.create('simple_concat') -- auth testing: access control box.schema.user.create('test', {password = 'test'}) @@ -106,10 +106,10 @@ local function func_name() end rawset(_G, 'func_name', func_name) -local function simple_incr(a) - return a + 1 +local function simple_concat(a) + return a .. a end -rawset(_G, 'simple_incr', simple_incr) +rawset(_G, 'simple_concat', simple_concat) local function push_func(cnt) for i = 1, cnt do diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 176f8a40c..7d9dbe29a 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -64,10 +64,10 @@ func assertDatetimeIsEqual(t *testing.T, tuples []interface{}, tm time.Time) { if len(tpl) != 2 { t.Fatalf("Unexpected return value body (tuple len = %d)", len(tpl)) } - if val, ok := tpl[dtIndex].(Datetime); !ok || !val.ToTime().Equal(tm) { + if val, ok := toDatetime(tpl[dtIndex]); !ok || !val.ToTime().Equal(tm) { t.Fatalf("Unexpected tuple %d field %v, expected %v", dtIndex, - val.ToTime(), + val, tm) } } @@ -324,7 +324,10 @@ func (ev *Event) DecodeMsgpack(d *decoder) error { if err != nil { return err } - ev.Datetime = res.(Datetime) + var ok bool + if ev.Datetime, ok = toDatetime(res); !ok { + return fmt.Errorf("datetime doesn't match") + } return nil } @@ -332,7 +335,7 @@ func (c *Tuple2) EncodeMsgpack(e *encoder) error { if err := e.EncodeArrayLen(3); err != nil { return err } - if err := encodeUint(e, uint64(c.Cid)); err != nil { + if err := e.EncodeUint64(uint64(c.Cid)); err != nil { return err } if err := e.EncodeString(c.Orig); err != nil { @@ -428,8 +431,8 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { } for i, tv := range []time.Time{tm1, tm2} { - dt := events[i].([]interface{})[1].(Datetime) - if !dt.ToTime().Equal(tv) { + dt, ok := toDatetime(events[i].([]interface{})[1]) + if !ok || !dt.ToTime().Equal(tv) { t.Fatalf("%v != %v", dt.ToTime(), tv) } } @@ -501,7 +504,7 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if val, ok := tpl[0].(Datetime); !ok || !val.ToTime().Equal(tm) { + if val, ok := toDatetime(tpl[0]); !ok || !val.ToTime().Equal(tm) { t.Fatalf("Unexpected body of Select") } } diff --git a/datetime/msgpack.go b/datetime/msgpack.go index 915c541ec..4f48f1d3e 100644 --- a/datetime/msgpack.go +++ b/datetime/msgpack.go @@ -1,3 +1,6 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + package datetime import ( diff --git a/datetime/msgpack_helper_test.go b/datetime/msgpack_helper_test.go index 7b045a863..7af2ee6ad 100644 --- a/datetime/msgpack_helper_test.go +++ b/datetime/msgpack_helper_test.go @@ -1,14 +1,20 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + package datetime_test import ( + . "github.com/tarantool/go-tarantool/datetime" + "gopkg.in/vmihailenco/msgpack.v2" ) type encoder = msgpack.Encoder type decoder = msgpack.Decoder -func encodeUint(e *encoder, v uint64) error { - return e.EncodeUint(uint(v)) +func toDatetime(i interface{}) (dt Datetime, ok bool) { + dt, ok = i.(Datetime) + return } func marshal(v interface{}) ([]byte, error) { diff --git a/datetime/msgpack_v5.go b/datetime/msgpack_v5.go new file mode 100644 index 000000000..a69a81aa3 --- /dev/null +++ b/datetime/msgpack_v5.go @@ -0,0 +1,12 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package datetime + +import ( + "github.com/vmihailenco/msgpack/v5" +) + +func init() { + msgpack.RegisterExt(datetime_extId, (*Datetime)(nil)) +} diff --git a/datetime/msgpack_v5_helper_test.go b/datetime/msgpack_v5_helper_test.go new file mode 100644 index 000000000..d750ef006 --- /dev/null +++ b/datetime/msgpack_v5_helper_test.go @@ -0,0 +1,28 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package datetime_test + +import ( + . "github.com/tarantool/go-tarantool/datetime" + "github.com/vmihailenco/msgpack/v5" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +func toDatetime(i interface{}) (dt Datetime, ok bool) { + var ptr *Datetime + if ptr, ok = i.(*Datetime); ok { + dt = *ptr + } + return +} + +func marshal(v interface{}) ([]byte, error) { + return msgpack.Marshal(v) +} + +func unmarshal(data []byte, v interface{}) error { + return msgpack.Unmarshal(data, v) +} diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 9eb056ca8..6fba0cc8f 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -60,8 +60,10 @@ func (t *TupleDecimal) DecodeMsgpack(d *decoder) error { if err != nil { return err } - t.number = res.(Decimal) - + var ok bool + if t.number, ok = toDecimal(res); !ok { + return fmt.Errorf("decimal doesn't match") + } return nil } @@ -347,7 +349,7 @@ func tupleValueIsDecimal(t *testing.T, tuples []interface{}, number decimal.Deci if len(tpl) != 1 { t.Fatalf("Unexpected return value body (tuple len)") } - if val, ok := tpl[0].(Decimal); !ok || !val.Equal(number) { + if val, ok := toDecimal(tpl[0]); !ok || !val.Equal(number) { t.Fatalf("Unexpected return value body (tuple 0 field)") } } @@ -445,7 +447,10 @@ func TestMPDecode(t *testing.T) { if err != nil { t.Fatalf("Unmarshalling failed: %s", err.Error()) } - decActual := v.(Decimal) + decActual, ok := toDecimal(v) + if !ok { + t.Fatalf("Unable to convert to Decimal") + } decExpected, err := decimal.NewFromString(testcase.numString) if err != nil { diff --git a/decimal/msgpack.go b/decimal/msgpack.go index ca360172f..5a455ae59 100644 --- a/decimal/msgpack.go +++ b/decimal/msgpack.go @@ -1,3 +1,6 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + package decimal import ( diff --git a/decimal/msgpack_helper_test.go b/decimal/msgpack_helper_test.go index 3013f4cf0..3824f70b8 100644 --- a/decimal/msgpack_helper_test.go +++ b/decimal/msgpack_helper_test.go @@ -1,12 +1,22 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + package decimal_test import ( + . "github.com/tarantool/go-tarantool/decimal" + "gopkg.in/vmihailenco/msgpack.v2" ) type encoder = msgpack.Encoder type decoder = msgpack.Decoder +func toDecimal(i interface{}) (dec Decimal, ok bool) { + dec, ok = i.(Decimal) + return +} + func marshal(v interface{}) ([]byte, error) { return msgpack.Marshal(v) } diff --git a/decimal/msgpack_v5.go b/decimal/msgpack_v5.go new file mode 100644 index 000000000..59fa713d0 --- /dev/null +++ b/decimal/msgpack_v5.go @@ -0,0 +1,12 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package decimal + +import ( + "github.com/vmihailenco/msgpack/v5" +) + +func init() { + msgpack.RegisterExt(decimalExtID, (*Decimal)(nil)) +} diff --git a/decimal/msgpack_v5_helper_test.go b/decimal/msgpack_v5_helper_test.go new file mode 100644 index 000000000..6bb78168a --- /dev/null +++ b/decimal/msgpack_v5_helper_test.go @@ -0,0 +1,28 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package decimal_test + +import ( + . "github.com/tarantool/go-tarantool/decimal" + "github.com/vmihailenco/msgpack/v5" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +func toDecimal(i interface{}) (dec Decimal, ok bool) { + var ptr *Decimal + if ptr, ok = i.(*Decimal); ok { + dec = *ptr + } + return +} + +func marshal(v interface{}) ([]byte, error) { + return msgpack.Marshal(v) +} + +func unmarshal(data []byte, v interface{}) error { + return msgpack.Unmarshal(data, v) +} diff --git a/example_test.go b/example_test.go index df7dad770..88779bc47 100644 --- a/example_test.go +++ b/example_test.go @@ -471,10 +471,10 @@ func ExampleFuture_GetIterator() { resp := it.Value() if resp.Code == tarantool.PushCode { // It is a push message. - fmt.Printf("push message: %d\n", resp.Data[0].(uint64)) + fmt.Printf("push message: %v\n", resp.Data[0]) } else if resp.Code == tarantool.OkCode { // It is a regular response. - fmt.Printf("response: %d", resp.Data[0].(uint64)) + fmt.Printf("response: %v", resp.Data[0]) } else { fmt.Printf("an unexpected response code %d", resp.Code) } @@ -645,17 +645,17 @@ func ExampleConnection_Call() { conn := example_connect() defer conn.Close() - // Call a function 'simple_incr' with arguments. - resp, err := conn.Call17("simple_incr", []interface{}{1}) - fmt.Println("Call simple_incr()") + // Call a function 'simple_concat' with arguments. + resp, err := conn.Call17("simple_concat", []interface{}{"1"}) + fmt.Println("Call simple_concat()") fmt.Println("Error", err) fmt.Println("Code", resp.Code) fmt.Println("Data", resp.Data) // Output: - // Call simple_incr() + // Call simple_concat() // Error // Code 0 - // Data [2] + // Data [11] } func ExampleConnection_Eval() { diff --git a/go.mod b/go.mod index c10d60795..2f3b8c226 100644 --- a/go.mod +++ b/go.mod @@ -14,4 +14,5 @@ require ( gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/vmihailenco/msgpack.v2 v2.9.2 gotest.tools/v3 v3.2.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 ) diff --git a/go.sum b/go.sum index fb3474afc..38e70ec44 100644 --- a/go.sum +++ b/go.sum @@ -24,10 +24,15 @@ github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+ github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tarantool/go-openssl v0.0.8-0.20220711094538-d93c1eff4f49 h1:rZYYi1cI3QXZ3yRFZd2ItYM1XA2BaJqP0buDroMbjNo= github.com/tarantool/go-openssl v0.0.8-0.20220711094538-d93c1eff4f49/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/msgpack.go b/msgpack.go index e48e14a21..34ecc4b3b 100644 --- a/msgpack.go +++ b/msgpack.go @@ -1,3 +1,6 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + package tarantool import ( diff --git a/msgpack_helper_test.go b/msgpack_helper_test.go index fcadbc573..fa47c2fda 100644 --- a/msgpack_helper_test.go +++ b/msgpack_helper_test.go @@ -1,3 +1,6 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + package tarantool_test import ( diff --git a/msgpack_v5.go b/msgpack_v5.go new file mode 100644 index 000000000..806dd1632 --- /dev/null +++ b/msgpack_v5.go @@ -0,0 +1,54 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package tarantool + +import ( + "io" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +func newEncoder(w io.Writer) *encoder { + return msgpack.NewEncoder(w) +} + +func newDecoder(r io.Reader) *decoder { + dec := msgpack.NewDecoder(r) + dec.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { + return dec.DecodeUntypedMap() + }) + return dec +} + +func encodeUint(e *encoder, v uint64) error { + return e.EncodeUint(v) +} + +func encodeInt(e *encoder, v int64) error { + return e.EncodeInt(v) +} + +func msgpackIsUint(code byte) bool { + return code == msgpcode.Uint8 || code == msgpcode.Uint16 || + code == msgpcode.Uint32 || code == msgpcode.Uint64 || + msgpcode.IsFixedNum(code) +} + +func msgpackIsMap(code byte) bool { + return code == msgpcode.Map16 || code == msgpcode.Map32 || msgpcode.IsFixedMap(code) +} + +func msgpackIsArray(code byte) bool { + return code == msgpcode.Array16 || code == msgpcode.Array32 || + msgpcode.IsFixedArray(code) +} + +func msgpackIsString(code byte) bool { + return msgpcode.IsFixedString(code) || code == msgpcode.Str8 || + code == msgpcode.Str16 || code == msgpcode.Str32 +} diff --git a/msgpack_v5_helper_test.go b/msgpack_v5_helper_test.go new file mode 100644 index 000000000..347c1ba95 --- /dev/null +++ b/msgpack_v5_helper_test.go @@ -0,0 +1,15 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package tarantool_test + +import ( + "github.com/vmihailenco/msgpack/v5" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +func encodeUint(e *encoder, v uint64) error { + return e.EncodeUint(v) +} diff --git a/multi/call_16_test.go b/multi/call_16_test.go index 11a1a766d..35bcd20fb 100644 --- a/multi/call_16_test.go +++ b/multi/call_16_test.go @@ -23,11 +23,11 @@ func TestCall(t *testing.T) { defer multiConn.Close() // Call16 - resp, err = multiConn.Call("simple_incr", []interface{}{1}) + resp, err = multiConn.Call("simple_concat", []interface{}{"t"}) if err != nil { t.Fatalf("Failed to use Call: %s", err.Error()) } - if resp.Data[0].([]interface{})[0].(uint64) != 2 { + if resp.Data[0].([]interface{})[0].(string) != "tt" { t.Fatalf("result is not {{1}} : %v", resp.Data) } } diff --git a/multi/call_17_test.go b/multi/call_17_test.go index 86a3cfc13..378961fda 100644 --- a/multi/call_17_test.go +++ b/multi/call_17_test.go @@ -23,11 +23,11 @@ func TestCall(t *testing.T) { defer multiConn.Close() // Call17 - resp, err = multiConn.Call("simple_incr", []interface{}{1}) + resp, err = multiConn.Call("simple_concat", []interface{}{"t"}) if err != nil { t.Fatalf("Failed to use Call: %s", err.Error()) } - if resp.Data[0].(uint64) != 2 { + if resp.Data[0].(string) != "tt" { t.Fatalf("result is not {{1}} : %v", resp.Data) } } diff --git a/multi/config.lua b/multi/config.lua index 0f032b204..aca4db9b3 100644 --- a/multi/config.lua +++ b/multi/config.lua @@ -16,7 +16,7 @@ box.once("init", function() id = 517, if_not_exists = true, }) - s:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) + s:create_index('primary', {type = 'tree', parts = {1, 'string'}, if_not_exists = true}) box.schema.user.create('test', { password = 'test' }) box.schema.user.grant('test', 'read,write,execute', 'universe') @@ -37,11 +37,11 @@ box.once("init", function() box.schema.user.grant('test', 'create', 'sequence') end) -local function simple_incr(a) - return a + 1 +local function simple_concat(a) + return a .. a end -rawset(_G, 'simple_incr', simple_incr) +rawset(_G, 'simple_concat', simple_concat) -- Set listen only when every other thing is configured. box.cfg{ diff --git a/multi/multi_test.go b/multi/multi_test.go index b4cdf18af..d95a51f03 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -229,11 +229,11 @@ func TestCall17(t *testing.T) { defer multiConn.Close() // Call17 - resp, err = multiConn.Call17("simple_incr", []interface{}{1}) + resp, err = multiConn.Call17("simple_concat", []interface{}{"s"}) if err != nil { t.Fatalf("Failed to use Call: %s", err.Error()) } - if resp.Data[0].(uint64) != 2 { + if resp.Data[0].(string) != "ss" { t.Fatalf("result is not {{1}} : %v", resp.Data) } } @@ -351,7 +351,7 @@ func TestStream_Commit(t *testing.T) { // Insert in stream req = tarantool.NewInsertRequest(spaceName). - Tuple([]interface{}{uint(1001), "hello2", "world2"}) + Tuple([]interface{}{"1001", "hello2", "world2"}) resp, err = stream.Do(req).Get() if err != nil { t.Fatalf("Failed to Insert: %s", err.Error()) @@ -359,7 +359,7 @@ func TestStream_Commit(t *testing.T) { if resp.Code != tarantool.OkCode { t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) } - defer test_helpers.DeleteRecordByKey(t, multiConn, spaceNo, indexNo, []interface{}{uint(1001)}) + defer test_helpers.DeleteRecordByKey(t, multiConn, spaceNo, indexNo, []interface{}{"1001"}) // Select not related to the transaction // while transaction is not committed @@ -368,7 +368,7 @@ func TestStream_Commit(t *testing.T) { Index(indexNo). Limit(1). Iterator(tarantool.IterEq). - Key([]interface{}{uint(1001)}) + Key([]interface{}{"1001"}) resp, err = multiConn.Do(selectReq).Get() if err != nil { t.Fatalf("Failed to Select: %s", err.Error()) @@ -394,7 +394,7 @@ func TestStream_Commit(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { - if id, ok := tpl[0].(uint64); !ok || id != 1001 { + if id, ok := tpl[0].(string); !ok || id != "1001" { t.Fatalf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "hello2" { @@ -429,7 +429,7 @@ func TestStream_Commit(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { - if id, ok := tpl[0].(uint64); !ok || id != 1001 { + if id, ok := tpl[0].(string); !ok || id != "1001" { t.Fatalf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "hello2" { @@ -471,7 +471,7 @@ func TestStream_Rollback(t *testing.T) { // Insert in stream req = tarantool.NewInsertRequest(spaceName). - Tuple([]interface{}{uint(1001), "hello2", "world2"}) + Tuple([]interface{}{"1001", "hello2", "world2"}) resp, err = stream.Do(req).Get() if err != nil { t.Fatalf("Failed to Insert: %s", err.Error()) @@ -479,7 +479,7 @@ func TestStream_Rollback(t *testing.T) { if resp.Code != tarantool.OkCode { t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) } - defer test_helpers.DeleteRecordByKey(t, multiConn, spaceNo, indexNo, []interface{}{uint(1001)}) + defer test_helpers.DeleteRecordByKey(t, multiConn, spaceNo, indexNo, []interface{}{"1001"}) // Select not related to the transaction // while transaction is not committed @@ -488,7 +488,7 @@ func TestStream_Rollback(t *testing.T) { Index(indexNo). Limit(1). Iterator(tarantool.IterEq). - Key([]interface{}{uint(1001)}) + Key([]interface{}{"1001"}) resp, err = multiConn.Do(selectReq).Get() if err != nil { t.Fatalf("Failed to Select: %s", err.Error()) @@ -514,7 +514,7 @@ func TestStream_Rollback(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { - if id, ok := tpl[0].(uint64); !ok || id != 1001 { + if id, ok := tpl[0].(string); !ok || id != "1001" { t.Fatalf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "hello2" { diff --git a/queue/msgpack.go b/queue/msgpack.go index d614374ce..d9e0b58db 100644 --- a/queue/msgpack.go +++ b/queue/msgpack.go @@ -1,3 +1,6 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + package queue import ( diff --git a/queue/msgpack_helper_test.go b/queue/msgpack_helper_test.go index cfb8d8e89..49b61240c 100644 --- a/queue/msgpack_helper_test.go +++ b/queue/msgpack_helper_test.go @@ -1,3 +1,6 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + package queue_test import ( diff --git a/queue/msgpack_v5.go b/queue/msgpack_v5.go new file mode 100644 index 000000000..b5037caaf --- /dev/null +++ b/queue/msgpack_v5.go @@ -0,0 +1,10 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package queue + +import ( + "github.com/vmihailenco/msgpack/v5" +) + +type decoder = msgpack.Decoder diff --git a/queue/msgpack_v5_helper_test.go b/queue/msgpack_v5_helper_test.go new file mode 100644 index 000000000..ea2991f34 --- /dev/null +++ b/queue/msgpack_v5_helper_test.go @@ -0,0 +1,11 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package queue_test + +import ( + "github.com/vmihailenco/msgpack/v5" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder diff --git a/queue/queue.go b/queue/queue.go index 4fe8fe296..ef40154b0 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -283,14 +283,27 @@ func (q *queue) produce(cmd string, params ...interface{}) (string, error) { return qd.task.status, nil } +type kickResult struct { + id uint64 +} + +func (r *kickResult) DecodeMsgpack(d *decoder) (err error) { + var l int + if l, err = d.DecodeArrayLen(); err != nil { + return err + } + if l > 1 { + return fmt.Errorf("array len doesn't match for queue kick data: %d", l) + } + r.id, err = d.DecodeUint64() + return +} + // Reverse the effect of a bury request on one or more tasks. func (q *queue) Kick(count uint64) (uint64, error) { - resp, err := q.conn.Call17(q.cmds.kick, []interface{}{count}) - var id uint64 - if err == nil { - id = resp.Data[0].(uint64) - } - return id, err + var r kickResult + err := q.conn.Call17Typed(q.cmds.kick, []interface{}{count}, &r) + return r.id, err } // Delete the task identified by its id. diff --git a/tarantool_test.go b/tarantool_test.go index 18ebb0506..c1d49f12e 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -56,6 +56,36 @@ func (m *Member) DecodeMsgpack(d *decoder) error { return nil } +// msgpack.v2 and msgpack.v5 return different uint types in responses. The +// function helps to unify a result. +func convertUint64(v interface{}) (result uint64, err error) { + switch v := v.(type) { + case uint: + result = uint64(v) + case uint8: + result = uint64(v) + case uint16: + result = uint64(v) + case uint32: + result = uint64(v) + case uint64: + result = uint64(v) + case int: + result = uint64(v) + case int8: + result = uint64(v) + case int16: + result = uint64(v) + case int32: + result = uint64(v) + case int64: + result = uint64(v) + default: + err = fmt.Errorf("Non-number value %T", v) + } + return +} + var server = "127.0.0.1:3013" var spaceNo = uint32(517) var spaceName = "test" @@ -726,7 +756,7 @@ func TestClient(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Insert (tuple len)") } - if id, ok := tpl[0].(uint64); !ok || id != 1 { + if id, err := convertUint64(tpl[0]); err != nil || id != 1 { t.Errorf("Unexpected body of Insert (0)") } if h, ok := tpl[1].(string); !ok || h != "hello" { @@ -759,7 +789,7 @@ func TestClient(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Delete (tuple len)") } - if id, ok := tpl[0].(uint64); !ok || id != 1 { + if id, err := convertUint64(tpl[0]); err != nil || id != 1 { t.Errorf("Unexpected body of Delete (0)") } if h, ok := tpl[1].(string); !ok || h != "hello" { @@ -801,7 +831,7 @@ func TestClient(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Replace (tuple len)") } - if id, ok := tpl[0].(uint64); !ok || id != 2 { + if id, err := convertUint64(tpl[0]); err != nil || id != 2 { t.Errorf("Unexpected body of Replace (0)") } if h, ok := tpl[1].(string); !ok || h != "hi" { @@ -826,7 +856,7 @@ func TestClient(t *testing.T) { if len(tpl) != 2 { t.Errorf("Unexpected body of Update (tuple len)") } - if id, ok := tpl[0].(uint64); !ok || id != 2 { + if id, err := convertUint64(tpl[0]); err != nil || id != 2 { t.Errorf("Unexpected body of Update (0)") } if h, ok := tpl[1].(string); !ok || h != "bye" { @@ -875,7 +905,7 @@ func TestClient(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if id, ok := tpl[0].(uint64); !ok || id != 10 { + if id, err := convertUint64(tpl[0]); err != nil || id != 10 { t.Errorf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "val 10" { @@ -966,19 +996,19 @@ func TestClient(t *testing.T) { } // Call16 vs Call17 - resp, err = conn.Call16("simple_incr", []interface{}{1}) + resp, err = conn.Call16("simple_concat", []interface{}{"1"}) if err != nil { t.Errorf("Failed to use Call16") } - if resp.Data[0].([]interface{})[0].(uint64) != 2 { + if val, ok := resp.Data[0].([]interface{})[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } - resp, err = conn.Call17("simple_incr", []interface{}{1}) + resp, err = conn.Call17("simple_concat", []interface{}{"1"}) if err != nil { t.Errorf("Failed to use Call") } - if resp.Data[0].(uint64) != 2 { + if val, ok := resp.Data[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } @@ -993,8 +1023,7 @@ func TestClient(t *testing.T) { if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } - val := resp.Data[0].(uint64) - if val != 11 { + if val, err := convertUint64(resp.Data[0]); err != nil || val != 11 { t.Errorf("5 + 6 == 11, but got %v", val) } } @@ -1039,7 +1068,7 @@ func TestClientSessionPush(t *testing.T) { t.Errorf("Response is nil after CallAsync") } else if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Call17Async") - } else if resp.Data[0].(uint64) != pushMax { + } else if val, err := convertUint64(resp.Data[0]); err != nil || val != pushMax { t.Errorf("result is not {{1}} : %v", resp.Data) } @@ -1068,12 +1097,12 @@ func TestClientSessionPush(t *testing.T) { } if resp.Code == PushCode { pushCnt += 1 - if resp.Data[0].(uint64) != pushCnt { + if val, err := convertUint64(resp.Data[0]); err != nil || val != pushCnt { t.Errorf("Unexpected push data = %v", resp.Data) } } else { respCnt += 1 - if resp.Data[0].(uint64) != pushMax { + if val, err := convertUint64(resp.Data[0]); err != nil || val != pushMax { t.Errorf("result is not {{1}} : %v", resp.Data) } } @@ -1094,13 +1123,13 @@ func TestClientSessionPush(t *testing.T) { } const ( - createTableQuery = "CREATE TABLE SQL_SPACE (id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING COLLATE \"unicode\" DEFAULT NULL);" + createTableQuery = "CREATE TABLE SQL_SPACE (id STRING PRIMARY KEY, name STRING COLLATE \"unicode\" DEFAULT NULL);" insertQuery = "INSERT INTO SQL_SPACE VALUES (?, ?);" selectNamedQuery = "SELECT id, name FROM SQL_SPACE WHERE id=:id AND name=:name;" selectPosQuery = "SELECT id, name FROM SQL_SPACE WHERE id=? AND name=?;" updateQuery = "UPDATE SQL_SPACE SET name=? WHERE id=?;" enableFullMetaDataQuery = "SET SESSION \"sql_full_metadata\" = true;" - selectSpanDifQuery = "SELECT id*2, name, id FROM SQL_SPACE WHERE name=?;" + selectSpanDifQuery = "SELECT id||id, name, id FROM SQL_SPACE WHERE name=?;" alterTableQuery = "ALTER TABLE SQL_SPACE RENAME TO SQL_SPACE2;" insertIncrQuery = "INSERT INTO SQL_SPACE2 VALUES (?, ?);" deleteQuery = "DELETE FROM SQL_SPACE2 WHERE name=?;" @@ -1135,7 +1164,7 @@ func TestSQL(t *testing.T) { }, { insertQuery, - []interface{}{1, "test"}, + []interface{}{"1", "test"}, Response{ SQLInfo: SQLInfo{AffectedCount: 1}, Data: []interface{}{}, @@ -1145,31 +1174,31 @@ func TestSQL(t *testing.T) { { selectNamedQuery, map[string]interface{}{ - "id": 1, + "id": "1", "name": "test", }, Response{ SQLInfo: SQLInfo{AffectedCount: 0}, - Data: []interface{}{[]interface{}{uint64(1), "test"}}, + Data: []interface{}{[]interface{}{"1", "test"}}, MetaData: []ColumnMetaData{ - {FieldType: "integer", FieldName: "ID"}, + {FieldType: "string", FieldName: "ID"}, {FieldType: "string", FieldName: "NAME"}}, }, }, { selectPosQuery, - []interface{}{1, "test"}, + []interface{}{"1", "test"}, Response{ SQLInfo: SQLInfo{AffectedCount: 0}, - Data: []interface{}{[]interface{}{uint64(1), "test"}}, + Data: []interface{}{[]interface{}{"1", "test"}}, MetaData: []ColumnMetaData{ - {FieldType: "integer", FieldName: "ID"}, + {FieldType: "string", FieldName: "ID"}, {FieldType: "string", FieldName: "NAME"}}, }, }, { updateQuery, - []interface{}{"test_test", 1}, + []interface{}{"test_test", "1"}, Response{ SQLInfo: SQLInfo{AffectedCount: 1}, Data: []interface{}{}, @@ -1189,14 +1218,14 @@ func TestSQL(t *testing.T) { selectSpanDifQuery, []interface{}{"test_test"}, Response{ - SQLInfo: SQLInfo{AffectedCount: 0}, Data: []interface{}{[]interface{}{uint64(2), "test_test", uint64(1)}}, + SQLInfo: SQLInfo{AffectedCount: 0}, Data: []interface{}{[]interface{}{"11", "test_test", "1"}}, MetaData: []ColumnMetaData{ { - FieldType: "integer", + FieldType: "string", FieldName: "COLUMN_1", FieldIsNullable: false, FieldIsAutoincrement: false, - FieldSpan: "id*2", + FieldSpan: "id||id", }, { FieldType: "string", @@ -1207,11 +1236,12 @@ func TestSQL(t *testing.T) { FieldCollation: "unicode", }, { - FieldType: "integer", + FieldType: "string", FieldName: "ID", FieldIsNullable: false, - FieldIsAutoincrement: true, + FieldIsAutoincrement: false, FieldSpan: "id", + FieldCollation: "", }, }}, }, @@ -1226,7 +1256,7 @@ func TestSQL(t *testing.T) { }, { insertIncrQuery, - []interface{}{2, "test_2"}, + []interface{}{"2", "test_2"}, Response{ SQLInfo: SQLInfo{AffectedCount: 1, InfoAutoincrementIds: []uint64{1}}, Data: []interface{}{}, @@ -1944,7 +1974,7 @@ func TestClientRequestObjects(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Insert (tuple len)") } - if id, ok := tpl[0].(uint64); !ok || id != uint64(i) { + if id, err := convertUint64(tpl[0]); err != nil || id != uint64(i) { t.Errorf("Unexpected body of Insert (0)") } if h, ok := tpl[1].(string); !ok || h != fmt.Sprintf("val %d", i) { @@ -1979,7 +2009,7 @@ func TestClientRequestObjects(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Replace (tuple len)") } - if id, ok := tpl[0].(uint64); !ok || id != uint64(i) { + if id, err := convertUint64(tpl[0]); err != nil || id != uint64(i) { t.Errorf("Unexpected body of Replace (0)") } if h, ok := tpl[1].(string); !ok || h != fmt.Sprintf("val %d", i) { @@ -2013,7 +2043,7 @@ func TestClientRequestObjects(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Delete (tuple len)") } - if id, ok := tpl[0].(uint64); !ok || id != uint64(1016) { + if id, err := convertUint64(tpl[0]); err != nil || id != uint64(1016) { t.Errorf("Unexpected body of Delete (0)") } if h, ok := tpl[1].(string); !ok || h != "val 1016" { @@ -2044,7 +2074,7 @@ func TestClientRequestObjects(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Update") } else { - if id, ok := tpl[0].(uint64); !ok || id != 1010 { + if id, err := convertUint64(tpl[0]); err != nil || id != uint64(1010) { t.Errorf("Unexpected body of Update (0)") } if h, ok := tpl[1].(string); !ok || h != "val 1010" { @@ -2076,13 +2106,13 @@ func TestClientRequestObjects(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if id, ok := tpl[0].(uint64); !ok || id != 1010 { + if id, err := convertUint64(tpl[0]); err != nil || id != 1010 { t.Errorf("Unexpected body of Update (0)") } if h, ok := tpl[1].(string); !ok || h != "bye" { t.Errorf("Unexpected body of Update (1)") } - if h, ok := tpl[2].(uint64); !ok || h != 1 { + if h, err := convertUint64(tpl[2]); err != nil || h != 1 { t.Errorf("Unexpected body of Update (2)") } } @@ -2142,8 +2172,8 @@ func TestClientRequestObjects(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if id, ok := tpl[0].(uint64); !ok || id != 1010 { - t.Errorf("Unexpected body of Select (0) %d, expected %d", tpl[0].(uint64), 1010) + if id, err := convertUint64(tpl[0]); err != nil || id != 1010 { + t.Errorf("Unexpected body of Select (0) %v, expected %d", tpl[0], 1010) } if h, ok := tpl[1].(string); !ok || h != "bye" { t.Errorf("Unexpected body of Select (1) %q, expected %q", tpl[1].(string), "bye") @@ -2154,22 +2184,22 @@ func TestClientRequestObjects(t *testing.T) { } // Call16 vs Call17 - req = NewCall16Request("simple_incr").Args([]interface{}{1}) + req = NewCall16Request("simple_concat").Args([]interface{}{"1"}) resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to use Call") } - if resp.Data[0].([]interface{})[0].(uint64) != 2 { + if val, ok := resp.Data[0].([]interface{})[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } // Call17 - req = NewCall17Request("simple_incr").Args([]interface{}{1}) + req = NewCall17Request("simple_concat").Args([]interface{}{"1"}) resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to use Call17") } - if resp.Data[0].(uint64) != 2 { + if val, ok := resp.Data[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } @@ -2185,8 +2215,7 @@ func TestClientRequestObjects(t *testing.T) { if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } - val := resp.Data[0].(uint64) - if val != 11 { + if val, err := convertUint64(resp.Data[0]); err != nil || val != 11 { t.Errorf("5 + 6 == 11, but got %v", val) } @@ -2386,7 +2415,7 @@ func TestStream_Commit(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { - if id, ok := tpl[0].(uint64); !ok || id != 1001 { + if id, err := convertUint64(tpl[0]); err != nil || id != 1001 { t.Fatalf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "hello2" { @@ -2421,7 +2450,7 @@ func TestStream_Commit(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { - if id, ok := tpl[0].(uint64); !ok || id != 1001 { + if id, err := convertUint64(tpl[0]); err != nil || id != 1001 { t.Fatalf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "hello2" { @@ -2501,7 +2530,7 @@ func TestStream_Rollback(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { - if id, ok := tpl[0].(uint64); !ok || id != 1001 { + if id, err := convertUint64(tpl[0]); err != nil || id != 1001 { t.Fatalf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "hello2" { @@ -2606,8 +2635,8 @@ func TestStream_TxnIsolationLevel(t *testing.T) { require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 3, len(tpl), "unexpected body of Select") - key, ok := tpl[0].(uint64) - require.Truef(t, ok, "unexpected body of Select (0)") + key, err := convertUint64(tpl[0]) + require.Nilf(t, err, "unexpected body of Select (0)") require.Equalf(t, uint64(1001), key, "unexpected body of Select (0)") value1, ok := tpl[1].(string) diff --git a/test_helpers/msgpack.go b/test_helpers/msgpack.go index f18b17f48..1ea712b38 100644 --- a/test_helpers/msgpack.go +++ b/test_helpers/msgpack.go @@ -1,3 +1,6 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + package test_helpers import ( diff --git a/test_helpers/msgpack_v5.go b/test_helpers/msgpack_v5.go new file mode 100644 index 000000000..37f85ef31 --- /dev/null +++ b/test_helpers/msgpack_v5.go @@ -0,0 +1,10 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package test_helpers + +import ( + "github.com/vmihailenco/msgpack/v5" +) + +type encoder = msgpack.Encoder diff --git a/uuid/msgpack.go b/uuid/msgpack.go index 1fcbc711b..62504e5f9 100644 --- a/uuid/msgpack.go +++ b/uuid/msgpack.go @@ -1,3 +1,6 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + package uuid import ( diff --git a/uuid/msgpack_helper_test.go b/uuid/msgpack_helper_test.go index e8550f050..d5a1cb70e 100644 --- a/uuid/msgpack_helper_test.go +++ b/uuid/msgpack_helper_test.go @@ -1,3 +1,6 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + package uuid_test import ( diff --git a/uuid/msgpack_v5.go b/uuid/msgpack_v5.go new file mode 100644 index 000000000..951c437dd --- /dev/null +++ b/uuid/msgpack_v5.go @@ -0,0 +1,27 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package uuid + +import ( + "reflect" + + "github.com/google/uuid" + "github.com/vmihailenco/msgpack/v5" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +func init() { + msgpack.Register(reflect.TypeOf((*uuid.UUID)(nil)).Elem(), encodeUUID, decodeUUID) + msgpack.RegisterExtEncoder(UUID_extId, uuid.UUID{}, + func(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { + uuid := v.Interface().(uuid.UUID) + return uuid.MarshalBinary() + }) + msgpack.RegisterExtDecoder(UUID_extId, uuid.UUID{}, + func(d *msgpack.Decoder, v reflect.Value, extLen int) error { + return decodeUUID(d, v) + }) +} diff --git a/uuid/msgpack_v5_helper_test.go b/uuid/msgpack_v5_helper_test.go new file mode 100644 index 000000000..c2356ef1a --- /dev/null +++ b/uuid/msgpack_v5_helper_test.go @@ -0,0 +1,10 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package uuid_test + +import ( + "github.com/vmihailenco/msgpack/v5" +) + +type decoder = msgpack.Decoder From 65a6de4bdb6cd31cfd68ee1af9ae26eb0446d215 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 19 May 2022 11:32:23 +0300 Subject: [PATCH 315/605] github-ci: add tests for msgpack.v5 Closes #124 --- .github/workflows/testing.yml | 26 +++++++++++++++++++++++--- CONTRIBUTING.md | 5 +++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 978073afd..eee435bef 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -71,9 +71,15 @@ jobs: - name: Run regression tests run: make test - - name: Run tests with call_17 + - name: Run regression tests with call_17 run: make test TAGS="go_tarantool_call_17" + - name: Run regression tests with msgpack.v5 + run: make test TAGS="go_tarantool_msgpack_v5" + + - name: Run regression tests with msgpack.v5 and call_17 + run: make test TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" + - name: Run fuzzing tests if: ${{ matrix.fuzzing }} run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" @@ -146,20 +152,34 @@ jobs: source tarantool-enterprise/env.sh make deps - - name: Run tests + - name: Run regression tests run: | source tarantool-enterprise/env.sh make test env: TEST_TNT_SSL: ${{matrix.ssl}} - - name: Run tests with call_17 + - name: Run regression tests with call_17 run: | source tarantool-enterprise/env.sh make test TAGS="go_tarantool_call_17" env: TEST_TNT_SSL: ${{matrix.ssl}} + - name: Run regression tests with msgpack.v5 + run: | + source tarantool-enterprise/env.sh + make test TAGS="go_tarantool_msgpack_v5" + env: + TEST_TNT_SSL: ${{matrix.ssl}} + + - name: Run regression tests with msgpack.v5 and call_17 + run: | + source tarantool-enterprise/env.sh + make test TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" + env: + TEST_TNT_SSL: ${{matrix.ssl}} + - name: Run fuzzing tests if: ${{ matrix.fuzzing }} run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8d493e40..15bccd3a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,11 @@ make test The tests set up all required `tarantool` processes before run and clean up afterwards. +If you want to run the tests with specific build tags: +```bash +make test TAGS=go_tarantool_ssl_disable,go_tarantool_msgpack_v5 +``` + If you have Tarantool Enterprise Edition 2.10 or newer, you can run additional SSL tests. To do this, you need to set an environment variable 'TEST_TNT_SSL': From f5cbb119373c27bf170e622bacf011f5499ad54f Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 5 Aug 2022 16:21:37 +0300 Subject: [PATCH 316/605] code health: run gofmt from GoLang 1.19 golangci-lint on GitHub Actions uses a latest gofmt from GoLang 1.19. The patch contains changes from gofmt 1.19 run to fix golangci-lint check on CI. --- connection_pool/const.go | 24 ++++++++++++------------ decimal/bcd.go | 8 ++++---- future.go | 3 +-- queue/queue.go | 2 +- schema.go | 2 +- ssl_test.go | 28 ++++++++++++++-------------- uuid/uuid.go | 2 +- 7 files changed, 34 insertions(+), 35 deletions(-) diff --git a/connection_pool/const.go b/connection_pool/const.go index eb41a47ea..334806437 100644 --- a/connection_pool/const.go +++ b/connection_pool/const.go @@ -17,18 +17,18 @@ Mode parameter: - PREFER_RW (prefer write only instance (master)) - if there is one, otherwise fallback to a read only one (replica). - Request Default mode - ---------- -------------- - | call | no default | - | eval | no default | - | ping | no default | - | insert | RW | - | delete | RW | - | replace | RW | - | update | RW | - | upsert | RW | - | select | ANY | - | get | ANY | + Request Default mode + ---------- -------------- + | call | no default | + | eval | no default | + | ping | no default | + | insert | RW | + | delete | RW | + | replace | RW | + | update | RW | + | upsert | RW | + | select | ANY | + | get | ANY | */ const ( ANY = iota diff --git a/decimal/bcd.go b/decimal/bcd.go index 4cc57aa45..cf95ccf7c 100644 --- a/decimal/bcd.go +++ b/decimal/bcd.go @@ -9,15 +9,15 @@ // The first byte of the BCD array contains the first digit of the number, // represented as follows: // -// | 4 bits | 4 bits | -// = 0x = the 1st digit +// | 4 bits | 4 bits | +// = 0x = the 1st digit // // (The first nibble contains 0 if the decimal number has an even number of // digits). The last byte of the BCD array contains the last digit of the // number and the final nibble, represented as follows: // -// | 4 bits | 4 bits | -// = the last digit = nibble +// | 4 bits | 4 bits | +// = the last digit = nibble // // The final nibble represents the number's sign: 0x0a, 0x0c, 0x0e, 0x0f stand // for plus, 0x0b and 0x0d stand for minus. diff --git a/future.go b/future.go index e34bc2f65..a512d244c 100644 --- a/future.go +++ b/future.go @@ -210,10 +210,9 @@ func (fut *Future) GetTyped(result interface{}) error { // and a response. Push messages and the response will contain deserialized // result in Data field as for the Get() function. // -// See also +// # See also // // * box.session.push() https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_session/push/ -// func (fut *Future) GetIterator() (it TimeoutResponseIterator) { futit := &asyncResponseIterator{ fut: fut, diff --git a/queue/queue.go b/queue/queue.go index ef40154b0..c40b8a0df 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -3,7 +3,7 @@ // // Since: 1.5. // -// See also +// # See also // // * Tarantool queue module https://github.com/tarantool/queue package queue diff --git a/schema.go b/schema.go index ecf565762..35f65eb28 100644 --- a/schema.go +++ b/schema.go @@ -5,7 +5,7 @@ import ( "fmt" ) -//nolint: varcheck,deadcode +// nolint: varcheck,deadcode const ( maxSchemas = 10000 spaceSpId = 280 diff --git a/ssl_test.go b/ssl_test.go index 3bf9d8ba3..36ce4905f 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -198,20 +198,20 @@ type test struct { } /* - Requirements from Tarantool Enterprise Edition manual: - https://www.tarantool.io/ru/enterprise_doc/security/#configuration - - For a server: - KeyFile - mandatory - CertFile - mandatory - CaFile - optional - Ciphers - optional - - For a client: - KeyFile - optional, mandatory if server.CaFile set - CertFile - optional, mandatory if server.CaFile set - CaFile - optional, - Ciphers - optional +Requirements from Tarantool Enterprise Edition manual: +https://www.tarantool.io/ru/enterprise_doc/security/#configuration + +For a server: +KeyFile - mandatory +CertFile - mandatory +CaFile - optional +Ciphers - optional + +For a client: +KeyFile - optional, mandatory if server.CaFile set +CertFile - optional, mandatory if server.CaFile set +CaFile - optional, +Ciphers - optional */ var tests = []test{ { diff --git a/uuid/uuid.go b/uuid/uuid.go index a35f33faf..a72785c0e 100644 --- a/uuid/uuid.go +++ b/uuid/uuid.go @@ -4,7 +4,7 @@ // // Since: 1.6.0. // -// See also +// # See also // // * Tarantool commit with UUID support https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5 // From 6bac6060c5d5f1af2c6cfa76e864908054a40e3d Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 11 Aug 2022 12:11:27 +0300 Subject: [PATCH 317/605] code health: unify messages format in datetime After the patch all messages in datetime tests start with a capital letter. All errors start with a lowecase letter [1]. 1. https://github.com/golang/go/wiki/CodeReviewComments#error-strings --- datetime/datetime.go | 2 +- datetime/datetime_test.go | 8 ++++---- datetime/example_test.go | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/datetime/datetime.go b/datetime/datetime.go index e8f1bdea7..abd4d51d2 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -86,7 +86,7 @@ func NewDatetime(t time.Time) (*Datetime, error) { seconds := t.Unix() if seconds < minSeconds || seconds > maxSeconds { - return nil, fmt.Errorf("Time %s is out of supported range.", t) + return nil, fmt.Errorf("time %s is out of supported range", t) } dt := new(Datetime) diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 7d9dbe29a..7b51ba348 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -286,7 +286,7 @@ func (t *Tuple1) DecodeMsgpack(d *decoder) error { return err } if l != 1 { - return fmt.Errorf("array len doesn't match: %d", l) + return fmt.Errorf("Array len doesn't match: %d", l) } err = d.Decode(&t.Datetime) if err != nil { @@ -315,7 +315,7 @@ func (ev *Event) DecodeMsgpack(d *decoder) error { return err } if l != 2 { - return fmt.Errorf("array len doesn't match: %d", l) + return fmt.Errorf("Array len doesn't match: %d", l) } if ev.Location, err = d.DecodeString(); err != nil { return err @@ -326,7 +326,7 @@ func (ev *Event) DecodeMsgpack(d *decoder) error { } var ok bool if ev.Datetime, ok = toDatetime(res); !ok { - return fmt.Errorf("datetime doesn't match") + return fmt.Errorf("Datetime doesn't match") } return nil } @@ -352,7 +352,7 @@ func (c *Tuple2) DecodeMsgpack(d *decoder) error { return err } if l != 3 { - return fmt.Errorf("array len doesn't match: %d", l) + return fmt.Errorf("Array len doesn't match: %d", l) } if c.Cid, err = d.DecodeUint(); err != nil { return err diff --git a/datetime/example_test.go b/datetime/example_test.go index 97359f43c..cabaf26bc 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -25,14 +25,14 @@ func Example() { } conn, err := tarantool.Connect("127.0.0.1:3013", opts) if err != nil { - fmt.Printf("error in connect is %v", err) + fmt.Printf("Error in connect is %v", err) return } var datetime = "2013-10-28T17:51:56.000000009Z" tm, err := time.Parse(time.RFC3339, datetime) if err != nil { - fmt.Printf("error in time.Parse() is %v", err) + fmt.Printf("Error in time.Parse() is %v", err) return } dt, err := NewDatetime(tm) @@ -47,7 +47,7 @@ func Example() { // Replace a tuple with datetime. resp, err := conn.Replace(space, []interface{}{dt}) if err != nil { - fmt.Printf("error in replace is %v", err) + fmt.Printf("Error in replace is %v", err) return } respDt := resp.Data[0].([]interface{})[0].(Datetime) @@ -60,7 +60,7 @@ func Example() { var limit uint32 = 1 resp, err = conn.Select(space, index, offset, limit, tarantool.IterEq, []interface{}{dt}) if err != nil { - fmt.Printf("error in select is %v", err) + fmt.Printf("Error in select is %v", err) return } respDt = resp.Data[0].([]interface{})[0].(Datetime) @@ -71,7 +71,7 @@ func Example() { // Delete a tuple with datetime. resp, err = conn.Delete(space, index, []interface{}{dt}) if err != nil { - fmt.Printf("error in delete is %v", err) + fmt.Printf("Error in delete is %v", err) return } respDt = resp.Data[0].([]interface{})[0].(Datetime) From 913bf30fef2976b22fb3af3efe3801f16cfa84b9 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 28 Jul 2022 15:39:45 +0300 Subject: [PATCH 318/605] datetime: add TZ support The patch adds timezone support [1] for datetime. For the purpose of compatibility we got constants for timezones from Tarantool [2]. The patch adds a script to generate the data according to the instructions [3]. 1. https://github.com/tarantool/tarantool/wiki/Datetime-Internals#timezone-support 2. https://github.com/tarantool/tarantool/blob/9ee45289e01232b8df1413efea11db170ae3b3b4/src/lib/tzcode/timezones.h 3. https://github.com/tarantool/tarantool/blob/9ee45289e01232b8df1413efea11db170ae3b3b4/src/lib/tzcode/gen-zone-abbrevs.pl#L35-L39 Closes #163 --- CHANGELOG.md | 1 + Makefile | 4 + datetime/datetime.go | 70 +- datetime/datetime_test.go | 278 +++++-- datetime/example_test.go | 21 + datetime/export_test.go | 5 + datetime/gen-timezones.sh | 54 ++ datetime/timezones.go | 1484 +++++++++++++++++++++++++++++++++++++ 8 files changed, 1850 insertions(+), 67 deletions(-) create mode 100644 datetime/export_test.go create mode 100755 datetime/gen-timezones.sh create mode 100644 datetime/timezones.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6381b3d13..25d0ed32e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added - Optional msgpack.v5 usage (#124) +- TZ support for datetime (#163) ### Changed diff --git a/Makefile b/Makefile index 7bc6411e4..418b3c89f 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,10 @@ clean: deps: clean ( cd ./queue; tarantoolctl rocks install queue 1.1.0 ) +.PHONY: datetime-timezones +datetime-timezones: + (cd ./datetime; ./gen-timezones.sh) + .PHONY: format format: goimports -l -w . diff --git a/datetime/datetime.go b/datetime/datetime.go index abd4d51d2..a73550b23 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -47,13 +47,11 @@ type datetime struct { // Nanoseconds, fractional part of seconds. Tarantool uses int32_t, see // a definition in src/lib/core/datetime.h. nsec int32 - // Timezone offset in minutes from UTC (not implemented in Tarantool, - // see gh-163). Tarantool uses a int16_t type, see a structure - // definition in src/lib/core/datetime.h. + // Timezone offset in minutes from UTC. Tarantool uses a int16_t type, + // see a structure definition in src/lib/core/datetime.h. tzOffset int16 - // Olson timezone id (not implemented in Tarantool, see gh-163). - // Tarantool uses a int16_t type, see a structure definition in - // src/lib/core/datetime.h. + // Olson timezone id. Tarantool uses a int16_t type, see a structure + // definition in src/lib/core/datetime.h. tzIndex int16 } @@ -79,9 +77,25 @@ type Datetime struct { time time.Time } +const ( + // NoTimezone allows to create a datetime without UTC timezone for + // Tarantool. The problem is that Golang by default creates a time value + // with UTC timezone. So it is a way to create a datetime without timezone. + NoTimezone = "" +) + +var noTimezoneLoc = time.FixedZone(NoTimezone, 0) + +const ( + offsetMin = -12 * 60 * 60 + offsetMax = 14 * 60 * 60 +) + // NewDatetime returns a pointer to a new datetime.Datetime that contains a -// specified time.Time. It may returns an error if the Time value is out of -// supported range: [-5879610-06-22T00:00Z .. 5879611-07-11T00:00Z] +// specified time.Time. It may return an error if the Time value is out of +// supported range: [-5879610-06-22T00:00Z .. 5879611-07-11T00:00Z] or +// an invalid timezone or offset value is out of supported range: +// [-12 * 60 * 60, 14 * 60 * 60]. func NewDatetime(t time.Time) (*Datetime, error) { seconds := t.Unix() @@ -89,6 +103,18 @@ func NewDatetime(t time.Time) (*Datetime, error) { return nil, fmt.Errorf("time %s is out of supported range", t) } + zone, offset := t.Zone() + if zone != NoTimezone { + if _, ok := timezoneToIndex[zone]; !ok { + return nil, fmt.Errorf("unknown timezone %s with offset %d", + zone, offset) + } + } + if offset < offsetMin || offset > offsetMax { + return nil, fmt.Errorf("offset must be between %d and %d hours", + offsetMin, offsetMax) + } + dt := new(Datetime) dt.time = t return dt, nil @@ -105,8 +131,14 @@ func (dtime *Datetime) MarshalMsgpack() ([]byte, error) { var dt datetime dt.seconds = tm.Unix() dt.nsec = int32(tm.Nanosecond()) - dt.tzIndex = 0 // It is not implemented, see gh-163. - dt.tzOffset = 0 // It is not implemented, see gh-163. + + zone, offset := tm.Zone() + if zone != NoTimezone { + // The zone value already checked in NewDatetime() or + // UnmarshalMsgpack() calls. + dt.tzIndex = int16(timezoneToIndex[zone]) + } + dt.tzOffset = int16(offset / 60) var bytesSize = secondsSize if dt.nsec != 0 || dt.tzOffset != 0 || dt.tzIndex != 0 { @@ -140,7 +172,23 @@ func (tm *Datetime) UnmarshalMsgpack(b []byte) error { dt.tzIndex = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize+tzOffsetSize:])) } - tt := time.Unix(dt.seconds, int64(dt.nsec)).UTC() + tt := time.Unix(dt.seconds, int64(dt.nsec)) + + loc := noTimezoneLoc + if dt.tzIndex != 0 || dt.tzOffset != 0 { + zone := NoTimezone + offset := int(dt.tzOffset) * 60 + + if dt.tzIndex != 0 { + if _, ok := indexToTimezone[int(dt.tzIndex)]; !ok { + return fmt.Errorf("unknown timezone index %d", dt.tzIndex) + } + zone = indexToTimezone[int(dt.tzIndex)] + } + loc = time.FixedZone(zone, offset) + } + tt = tt.In(loc) + dtp, err := NewDatetime(tt) if dtp != nil { *tm = *dtp diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 7b51ba348..77153b9fc 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -14,6 +14,8 @@ import ( "github.com/tarantool/go-tarantool/test_helpers" ) +var noTimezoneLoc = time.FixedZone(NoTimezone, 0) + var lesserBoundaryTimes = []time.Time{ time.Date(-5879610, 06, 22, 0, 0, 1, 0, time.UTC), time.Date(-5879610, 06, 22, 0, 0, 0, 1, time.UTC), @@ -73,6 +75,128 @@ func assertDatetimeIsEqual(t *testing.T, tuples []interface{}, tm time.Time) { } } +func TestTimezonesIndexMapping(t *testing.T) { + for _, index := range TimezoneToIndex { + if _, ok := IndexToTimezone[index]; !ok { + t.Errorf("Index %d not found", index) + } + } +} + +func TestTimezonesZonesMapping(t *testing.T) { + for _, zone := range IndexToTimezone { + if _, ok := TimezoneToIndex[zone]; !ok { + t.Errorf("Zone %s not found", zone) + } + } +} + +func TestInvalidTimezone(t *testing.T) { + invalidLoc := time.FixedZone("AnyInvalid", 0) + tm, err := time.Parse(time.RFC3339, "2010-08-12T11:39:14Z") + if err != nil { + t.Fatalf("Time parse failed: %s", err) + } + tm = tm.In(invalidLoc) + dt, err := NewDatetime(tm) + if err == nil { + t.Fatalf("Unexpected success: %v", dt) + } + if err.Error() != "unknown timezone AnyInvalid with offset 0" { + t.Fatalf("Unexpected error: %s", err.Error()) + } +} + +func TestInvalidOffset(t *testing.T) { + tests := []struct { + ok bool + offset int + }{ + {ok: true, offset: -12 * 60 * 60}, + {ok: true, offset: -12*60*60 + 1}, + {ok: true, offset: 14*60*60 - 1}, + {ok: true, offset: 14 * 60 * 60}, + {ok: false, offset: -12*60*60 - 1}, + {ok: false, offset: 14*60*60 + 1}, + } + + for _, testcase := range tests { + name := "" + if testcase.ok { + name = fmt.Sprintf("in_boundary_%d", testcase.offset) + } else { + name = fmt.Sprintf("out_of_boundary_%d", testcase.offset) + } + t.Run(name, func(t *testing.T) { + loc := time.FixedZone("MSK", testcase.offset) + tm, err := time.Parse(time.RFC3339, "2010-08-12T11:39:14Z") + if err != nil { + t.Fatalf("Time parse failed: %s", err) + } + tm = tm.In(loc) + dt, err := NewDatetime(tm) + if testcase.ok && err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + if !testcase.ok && err == nil { + t.Fatalf("Unexpected success: %v", dt) + } + if testcase.ok && isDatetimeSupported { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + tupleInsertSelectDelete(t, conn, tm) + } + }) + } +} + +func TestCustomTimezone(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + customZone := "Europe/Moscow" + customOffset := 180 * 60 + + customLoc := time.FixedZone(customZone, customOffset) + tm, err := time.Parse(time.RFC3339, "2010-08-12T11:44:14Z") + if err != nil { + t.Fatalf("Time parse failed: %s", err) + } + tm = tm.In(customLoc) + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Unable to create datetime: %s", err.Error()) + } + + resp, err := conn.Replace(spaceTuple1, []interface{}{dt, "payload"}) + if err != nil { + t.Fatalf("Datetime replace failed %s", err.Error()) + } + assertDatetimeIsEqual(t, resp.Data, tm) + + tpl := resp.Data[0].([]interface{}) + if respDt, ok := toDatetime(tpl[0]); ok { + zone, offset := respDt.ToTime().Zone() + if zone != customZone { + t.Fatalf("Expected zone %s instead of %s", customZone, zone) + } + if offset != customOffset { + t.Fatalf("Expected offset %d instead of %d", customOffset, offset) + } + + _, err = conn.Delete(spaceTuple1, 0, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime delete failed: %s", err.Error()) + } + } else { + t.Fatalf("Datetime doesn't match") + } + +} + func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) { t.Helper() @@ -111,61 +235,65 @@ func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) { } var datetimeSample = []struct { + fmt string dt string mpBuf string // MessagePack buffer. }{ - {"2012-01-31T23:59:59.000000010Z", "d8047f80284f000000000a00000000000000"}, - {"1970-01-01T00:00:00.000000010Z", "d80400000000000000000a00000000000000"}, - {"2010-08-12T11:39:14Z", "d70462dd634c00000000"}, - {"1984-03-24T18:04:05Z", "d7041530c31a00000000"}, - {"2010-01-12T00:00:00Z", "d70480bb4b4b00000000"}, - {"1970-01-01T00:00:00Z", "d7040000000000000000"}, - {"1970-01-01T00:00:00.123456789Z", "d804000000000000000015cd5b0700000000"}, - {"1970-01-01T00:00:00.12345678Z", "d80400000000000000000ccd5b0700000000"}, - {"1970-01-01T00:00:00.1234567Z", "d8040000000000000000bccc5b0700000000"}, - {"1970-01-01T00:00:00.123456Z", "d804000000000000000000ca5b0700000000"}, - {"1970-01-01T00:00:00.12345Z", "d804000000000000000090b25b0700000000"}, - {"1970-01-01T00:00:00.1234Z", "d804000000000000000040ef5a0700000000"}, - {"1970-01-01T00:00:00.123Z", "d8040000000000000000c0d4540700000000"}, - {"1970-01-01T00:00:00.12Z", "d8040000000000000000000e270700000000"}, - {"1970-01-01T00:00:00.1Z", "d804000000000000000000e1f50500000000"}, - {"1970-01-01T00:00:00.01Z", "d80400000000000000008096980000000000"}, - {"1970-01-01T00:00:00.001Z", "d804000000000000000040420f0000000000"}, - {"1970-01-01T00:00:00.0001Z", "d8040000000000000000a086010000000000"}, - {"1970-01-01T00:00:00.00001Z", "d80400000000000000001027000000000000"}, - {"1970-01-01T00:00:00.000001Z", "d8040000000000000000e803000000000000"}, - {"1970-01-01T00:00:00.0000001Z", "d80400000000000000006400000000000000"}, - {"1970-01-01T00:00:00.00000001Z", "d80400000000000000000a00000000000000"}, - {"1970-01-01T00:00:00.000000001Z", "d80400000000000000000100000000000000"}, - {"1970-01-01T00:00:00.000000009Z", "d80400000000000000000900000000000000"}, - {"1970-01-01T00:00:00.00000009Z", "d80400000000000000005a00000000000000"}, - {"1970-01-01T00:00:00.0000009Z", "d80400000000000000008403000000000000"}, - {"1970-01-01T00:00:00.000009Z", "d80400000000000000002823000000000000"}, - {"1970-01-01T00:00:00.00009Z", "d8040000000000000000905f010000000000"}, - {"1970-01-01T00:00:00.0009Z", "d8040000000000000000a0bb0d0000000000"}, - {"1970-01-01T00:00:00.009Z", "d80400000000000000004054890000000000"}, - {"1970-01-01T00:00:00.09Z", "d8040000000000000000804a5d0500000000"}, - {"1970-01-01T00:00:00.9Z", "d804000000000000000000e9a43500000000"}, - {"1970-01-01T00:00:00.99Z", "d80400000000000000008033023b00000000"}, - {"1970-01-01T00:00:00.999Z", "d8040000000000000000c0878b3b00000000"}, - {"1970-01-01T00:00:00.9999Z", "d80400000000000000006043993b00000000"}, - {"1970-01-01T00:00:00.99999Z", "d8040000000000000000f0a29a3b00000000"}, - {"1970-01-01T00:00:00.999999Z", "d804000000000000000018c69a3b00000000"}, - {"1970-01-01T00:00:00.9999999Z", "d80400000000000000009cc99a3b00000000"}, - {"1970-01-01T00:00:00.99999999Z", "d8040000000000000000f6c99a3b00000000"}, - {"1970-01-01T00:00:00.999999999Z", "d8040000000000000000ffc99a3b00000000"}, - {"1970-01-01T00:00:00.0Z", "d7040000000000000000"}, - {"1970-01-01T00:00:00.00Z", "d7040000000000000000"}, - {"1970-01-01T00:00:00.000Z", "d7040000000000000000"}, - {"1970-01-01T00:00:00.0000Z", "d7040000000000000000"}, - {"1970-01-01T00:00:00.00000Z", "d7040000000000000000"}, - {"1970-01-01T00:00:00.000000Z", "d7040000000000000000"}, - {"1970-01-01T00:00:00.0000000Z", "d7040000000000000000"}, - {"1970-01-01T00:00:00.00000000Z", "d7040000000000000000"}, - {"1970-01-01T00:00:00.000000000Z", "d7040000000000000000"}, - {"1973-11-29T21:33:09Z", "d70415cd5b0700000000"}, - {"2013-10-28T17:51:56Z", "d7043ca46e5200000000"}, - {"9999-12-31T23:59:59Z", "d7047f41f4ff3a000000"}, + /* Cases for base encoding without a timezone. */ + {time.RFC3339, "2012-01-31T23:59:59.000000010Z", "d8047f80284f000000000a00000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.000000010Z", "d80400000000000000000a00000000000000"}, + {time.RFC3339, "2010-08-12T11:39:14Z", "d70462dd634c00000000"}, + {time.RFC3339, "1984-03-24T18:04:05Z", "d7041530c31a00000000"}, + {time.RFC3339, "2010-01-12T00:00:00Z", "d70480bb4b4b00000000"}, + {time.RFC3339, "1970-01-01T00:00:00Z", "d7040000000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.123456789Z", "d804000000000000000015cd5b0700000000"}, + {time.RFC3339, "1970-01-01T00:00:00.12345678Z", "d80400000000000000000ccd5b0700000000"}, + {time.RFC3339, "1970-01-01T00:00:00.1234567Z", "d8040000000000000000bccc5b0700000000"}, + {time.RFC3339, "1970-01-01T00:00:00.123456Z", "d804000000000000000000ca5b0700000000"}, + {time.RFC3339, "1970-01-01T00:00:00.12345Z", "d804000000000000000090b25b0700000000"}, + {time.RFC3339, "1970-01-01T00:00:00.1234Z", "d804000000000000000040ef5a0700000000"}, + {time.RFC3339, "1970-01-01T00:00:00.123Z", "d8040000000000000000c0d4540700000000"}, + {time.RFC3339, "1970-01-01T00:00:00.12Z", "d8040000000000000000000e270700000000"}, + {time.RFC3339, "1970-01-01T00:00:00.1Z", "d804000000000000000000e1f50500000000"}, + {time.RFC3339, "1970-01-01T00:00:00.01Z", "d80400000000000000008096980000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.001Z", "d804000000000000000040420f0000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.0001Z", "d8040000000000000000a086010000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.00001Z", "d80400000000000000001027000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.000001Z", "d8040000000000000000e803000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.0000001Z", "d80400000000000000006400000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.00000001Z", "d80400000000000000000a00000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.000000001Z", "d80400000000000000000100000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.000000009Z", "d80400000000000000000900000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.00000009Z", "d80400000000000000005a00000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.0000009Z", "d80400000000000000008403000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.000009Z", "d80400000000000000002823000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.00009Z", "d8040000000000000000905f010000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.0009Z", "d8040000000000000000a0bb0d0000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.009Z", "d80400000000000000004054890000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.09Z", "d8040000000000000000804a5d0500000000"}, + {time.RFC3339, "1970-01-01T00:00:00.9Z", "d804000000000000000000e9a43500000000"}, + {time.RFC3339, "1970-01-01T00:00:00.99Z", "d80400000000000000008033023b00000000"}, + {time.RFC3339, "1970-01-01T00:00:00.999Z", "d8040000000000000000c0878b3b00000000"}, + {time.RFC3339, "1970-01-01T00:00:00.9999Z", "d80400000000000000006043993b00000000"}, + {time.RFC3339, "1970-01-01T00:00:00.99999Z", "d8040000000000000000f0a29a3b00000000"}, + {time.RFC3339, "1970-01-01T00:00:00.999999Z", "d804000000000000000018c69a3b00000000"}, + {time.RFC3339, "1970-01-01T00:00:00.9999999Z", "d80400000000000000009cc99a3b00000000"}, + {time.RFC3339, "1970-01-01T00:00:00.99999999Z", "d8040000000000000000f6c99a3b00000000"}, + {time.RFC3339, "1970-01-01T00:00:00.999999999Z", "d8040000000000000000ffc99a3b00000000"}, + {time.RFC3339, "1970-01-01T00:00:00.0Z", "d7040000000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.00Z", "d7040000000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.000Z", "d7040000000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.0000Z", "d7040000000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.00000Z", "d7040000000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.000000Z", "d7040000000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.0000000Z", "d7040000000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.00000000Z", "d7040000000000000000"}, + {time.RFC3339, "1970-01-01T00:00:00.000000000Z", "d7040000000000000000"}, + {time.RFC3339, "1973-11-29T21:33:09Z", "d70415cd5b0700000000"}, + {time.RFC3339, "2013-10-28T17:51:56Z", "d7043ca46e5200000000"}, + {time.RFC3339, "9999-12-31T23:59:59Z", "d7047f41f4ff3a000000"}, + /* Cases for encoding with a timezone. */ + {time.RFC3339 + " MST", "2006-01-02T15:04:00+03:00 MSK", "d804b016b9430000000000000000b400ee00"}, } func TestDatetimeInsertSelectDelete(t *testing.T) { @@ -176,7 +304,10 @@ func TestDatetimeInsertSelectDelete(t *testing.T) { for _, testcase := range datetimeSample { t.Run(testcase.dt, func(t *testing.T) { - tm, err := time.Parse(time.RFC3339, testcase.dt) + tm, err := time.Parse(testcase.fmt, testcase.dt) + if testcase.fmt == time.RFC3339 { + tm = tm.In(noTimezoneLoc) + } if err != nil { t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) } @@ -519,7 +650,10 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) { func TestMPEncode(t *testing.T) { for _, testcase := range datetimeSample { t.Run(testcase.dt, func(t *testing.T) { - tm, err := time.Parse(time.RFC3339, testcase.dt) + tm, err := time.Parse(testcase.fmt, testcase.dt) + if testcase.fmt == time.RFC3339 { + tm = tm.In(noTimezoneLoc) + } if err != nil { t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) } @@ -533,7 +667,7 @@ func TestMPEncode(t *testing.T) { } refBuf, _ := hex.DecodeString(testcase.mpBuf) if reflect.DeepEqual(buf, refBuf) != true { - t.Fatalf("Failed to encode datetime '%s', actual %v, expected %v", + t.Fatalf("Failed to encode datetime '%s', actual %x, expected %x", testcase.dt, buf, refBuf) @@ -545,7 +679,10 @@ func TestMPEncode(t *testing.T) { func TestMPDecode(t *testing.T) { for _, testcase := range datetimeSample { t.Run(testcase.dt, func(t *testing.T) { - tm, err := time.Parse(time.RFC3339, testcase.dt) + tm, err := time.Parse(testcase.fmt, testcase.dt) + if testcase.fmt == time.RFC3339 { + tm = tm.In(noTimezoneLoc) + } if err != nil { t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) } @@ -565,6 +702,35 @@ func TestMPDecode(t *testing.T) { } } +func TestUnmarshalMsgpackInvalidLength(t *testing.T) { + var v Datetime + + err := v.UnmarshalMsgpack([]byte{0x04}) + if err == nil { + t.Fatalf("Unexpected success %v", v) + } + if err.Error() != "invalid data length: got 1, wanted 8 or 16" { + t.Fatalf("Unexpected error: %s", err.Error()) + } +} + +func TestUnmarshalMsgpackInvalidZone(t *testing.T) { + var v Datetime + + // The original value from datetimeSample array: + // {time.RFC3339 + " MST", + // "2006-01-02T15:04:00+03:00 MSK", + // "d804b016b9430000000000000000b400ee00"} + buf, _ := hex.DecodeString("b016b9430000000000000000b400ee01") + err := v.UnmarshalMsgpack(buf) + if err == nil { + t.Fatalf("Unexpected success %v", v) + } + if err.Error() != "unknown timezone index 494" { + t.Fatalf("Unexpected error: %s", err.Error()) + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/datetime/example_test.go b/datetime/example_test.go index cabaf26bc..ce8cfb800 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -79,3 +79,24 @@ func Example() { fmt.Printf("Code: %d\n", resp.Code) fmt.Printf("Data: %v\n", respDt.ToTime()) } + +// Example demonstrates how to create a datetime for Tarantool without UTC +// timezone in datetime. +func ExampleNewDatetime_noTimezone() { + var datetime = "2013-10-28T17:51:56.000000009Z" + tm, err := time.Parse(time.RFC3339, datetime) + if err != nil { + fmt.Printf("Error in time.Parse() is %v", err) + return + } + + tm = tm.In(time.FixedZone(NoTimezone, 0)) + + dt, err := NewDatetime(tm) + if err != nil { + fmt.Printf("Unable to create Datetime from %s: %s", tm, err) + return + } + + fmt.Printf("Time value: %v\n", dt.ToTime()) +} diff --git a/datetime/export_test.go b/datetime/export_test.go new file mode 100644 index 000000000..f138b7a7a --- /dev/null +++ b/datetime/export_test.go @@ -0,0 +1,5 @@ +package datetime + +/* It's kind of an integration test data from an external data source. */ +var IndexToTimezone = indexToTimezone +var TimezoneToIndex = timezoneToIndex diff --git a/datetime/gen-timezones.sh b/datetime/gen-timezones.sh new file mode 100755 index 000000000..e251d7db6 --- /dev/null +++ b/datetime/gen-timezones.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -xeuo pipefail + +SRC_COMMIT="9ee45289e01232b8df1413efea11db170ae3b3b4" +SRC_FILE=timezones.h +DST_FILE=timezones.go + +[ -e ${SRC_FILE} ] && rm ${SRC_FILE} +wget -O ${SRC_FILE} \ + https://raw.githubusercontent.com/tarantool/tarantool/${SRC_COMMIT}/src/lib/tzcode/timezones.h + +# We don't need aliases in indexToTimezone because Tarantool always replace it: +# +# tarantool> T = date.parse '2022-01-01T00:00 Pacific/Enderbury' +# --- +# ... +# tarantool> T +# --- +# - 2022-01-01T00:00:00 Pacific/Kanton +# ... +# +# So we can do the same and don't worry, be happy. + +cat < ${DST_FILE} +package datetime + +/* Automatically generated by gen-timezones.sh */ + +var indexToTimezone = map[int]string{ +EOF + +grep ZONE_ABBREV ${SRC_FILE} | sed "s/ZONE_ABBREV( *//g" | sed "s/[),]//g" \ + | awk '{printf("\t%s : %s,\n", $1, $3)}' >> ${DST_FILE} +grep ZONE_UNIQUE ${SRC_FILE} | sed "s/ZONE_UNIQUE( *//g" | sed "s/[),]//g" \ + | awk '{printf("\t%s : %s,\n", $1, $2)}' >> ${DST_FILE} + +cat <> ${DST_FILE} +} + +var timezoneToIndex = map[string]int{ +EOF + +grep ZONE_ABBREV ${SRC_FILE} | sed "s/ZONE_ABBREV( *//g" | sed "s/[),]//g" \ + | awk '{printf("\t%s : %s,\n", $3, $1)}' >> ${DST_FILE} +grep ZONE_UNIQUE ${SRC_FILE} | sed "s/ZONE_UNIQUE( *//g" | sed "s/[),]//g" \ + | awk '{printf("\t%s : %s,\n", $2, $1)}' >> ${DST_FILE} +grep ZONE_ALIAS ${SRC_FILE} | sed "s/ZONE_ALIAS( *//g" | sed "s/[),]//g" \ + | awk '{printf("\t%s : %s,\n", $2, $1)}' >> ${DST_FILE} + +echo "}" >> ${DST_FILE} + +rm timezones.h + +gofmt -s -w ${DST_FILE} diff --git a/datetime/timezones.go b/datetime/timezones.go new file mode 100644 index 000000000..ca4234fd1 --- /dev/null +++ b/datetime/timezones.go @@ -0,0 +1,1484 @@ +package datetime + +/* Automatically generated by gen-timezones.sh */ + +var indexToTimezone = map[int]string{ + 1: "A", + 2: "B", + 3: "C", + 4: "D", + 5: "E", + 6: "F", + 7: "G", + 8: "H", + 9: "I", + 10: "K", + 11: "L", + 12: "M", + 13: "N", + 14: "O", + 15: "P", + 16: "Q", + 17: "R", + 18: "S", + 19: "T", + 20: "U", + 21: "V", + 22: "W", + 23: "X", + 24: "Y", + 25: "Z", + 32: "AT", + 40: "BT", + 48: "CT", + 56: "ET", + 64: "GT", + 72: "IT", + 80: "KT", + 88: "MT", + 96: "PT", + 104: "ST", + 112: "UT", + 120: "WT", + 128: "ACT", + 129: "ADT", + 130: "AET", + 131: "AFT", + 132: "AMT", + 133: "AoE", + 134: "ART", + 135: "AST", + 136: "AZT", + 144: "BDT", + 145: "BNT", + 146: "BOT", + 147: "BRT", + 148: "BST", + 149: "BTT", + 152: "CAT", + 153: "CCT", + 154: "CDT", + 155: "CET", + 156: "CIT", + 157: "CKT", + 158: "CLT", + 159: "COT", + 160: "CST", + 161: "CVT", + 162: "CXT", + 168: "EAT", + 169: "ECT", + 170: "EDT", + 171: "EET", + 172: "EGT", + 173: "EST", + 176: "FET", + 177: "FJT", + 178: "FKT", + 179: "FNT", + 184: "GET", + 185: "GFT", + 186: "GMT", + 187: "GST", + 188: "GYT", + 192: "HAA", + 193: "HAC", + 194: "HAE", + 195: "HAP", + 196: "HAR", + 197: "HAT", + 198: "HDT", + 199: "HKT", + 200: "HLV", + 201: "HNA", + 202: "HNC", + 203: "HNE", + 204: "HNP", + 205: "HNR", + 206: "HNT", + 207: "HST", + 208: "ICT", + 209: "IDT", + 210: "IOT", + 211: "IST", + 216: "JST", + 224: "KGT", + 225: "KIT", + 226: "KST", + 232: "MCK", + 233: "MDT", + 234: "MEZ", + 235: "MHT", + 236: "MMT", + 237: "MSD", + 238: "MSK", + 239: "MST", + 240: "MUT", + 241: "MVT", + 242: "MYT", + 248: "NCT", + 249: "NDT", + 250: "NFT", + 251: "NPT", + 252: "NRT", + 253: "NST", + 254: "NUT", + 256: "OEZ", + 264: "PDT", + 265: "PET", + 266: "PGT", + 267: "PHT", + 268: "PKT", + 269: "PST", + 270: "PWT", + 271: "PYT", + 272: "RET", + 280: "SBT", + 281: "SCT", + 282: "SGT", + 283: "SRT", + 284: "SST", + 288: "TFT", + 289: "TJT", + 290: "TKT", + 291: "TLT", + 292: "TMT", + 293: "TOT", + 294: "TRT", + 295: "TVT", + 296: "UTC", + 297: "UYT", + 298: "UZT", + 304: "VET", + 305: "VUT", + 312: "WAT", + 313: "WDT", + 314: "WET", + 315: "WEZ", + 316: "WFT", + 317: "WGT", + 318: "WIB", + 319: "WIT", + 320: "WST", + 328: "ACDT", + 329: "ACST", + 330: "ADST", + 331: "AEDT", + 332: "AEST", + 333: "AKDT", + 334: "AKST", + 335: "ALMT", + 336: "AMDT", + 337: "AMST", + 338: "ANAT", + 339: "AQTT", + 340: "AWDT", + 341: "AWST", + 342: "AZOT", + 343: "AZST", + 344: "BDST", + 345: "BRST", + 352: "CAST", + 353: "CDST", + 354: "CEDT", + 355: "CEST", + 356: "CHOT", + 357: "ChST", + 358: "CHUT", + 359: "CIST", + 360: "CLDT", + 361: "CLST", + 368: "DAVT", + 369: "DDUT", + 376: "EADT", + 377: "EAST", + 378: "ECST", + 379: "EDST", + 380: "EEDT", + 381: "EEST", + 382: "EGST", + 384: "FJDT", + 385: "FJST", + 386: "FKDT", + 387: "FKST", + 392: "GALT", + 393: "GAMT", + 394: "GILT", + 400: "HADT", + 401: "HAST", + 402: "HOVT", + 408: "IRDT", + 409: "IRKT", + 410: "IRST", + 416: "KOST", + 417: "KRAT", + 418: "KUYT", + 424: "LHDT", + 425: "LHST", + 426: "LINT", + 432: "MAGT", + 433: "MART", + 434: "MAWT", + 435: "MDST", + 436: "MESZ", + 440: "NFDT", + 441: "NOVT", + 442: "NZDT", + 443: "NZST", + 448: "OESZ", + 449: "OMST", + 450: "ORAT", + 456: "PDST", + 457: "PETT", + 458: "PHOT", + 459: "PMDT", + 460: "PMST", + 461: "PONT", + 462: "PYST", + 464: "QYZT", + 472: "ROTT", + 480: "SAKT", + 481: "SAMT", + 482: "SAST", + 483: "SRET", + 484: "SYOT", + 488: "TAHT", + 489: "TOST", + 496: "ULAT", + 497: "UYST", + 504: "VLAT", + 505: "VOST", + 512: "WAKT", + 513: "WAST", + 514: "WEDT", + 515: "WEST", + 516: "WESZ", + 517: "WGST", + 518: "WITA", + 520: "YAKT", + 521: "YAPT", + 522: "YEKT", + 528: "ACWST", + 529: "ANAST", + 530: "AZODT", + 531: "AZOST", + 536: "CHADT", + 537: "CHAST", + 538: "CHODT", + 539: "CHOST", + 540: "CIDST", + 544: "EASST", + 545: "EFATE", + 552: "HOVDT", + 553: "HOVST", + 560: "IRKST", + 568: "KRAST", + 576: "MAGST", + 584: "NACDT", + 585: "NACST", + 586: "NAEDT", + 587: "NAEST", + 588: "NAMDT", + 589: "NAMST", + 590: "NAPDT", + 591: "NAPST", + 592: "NOVST", + 600: "OMSST", + 608: "PETST", + 616: "SAMST", + 624: "ULAST", + 632: "VLAST", + 640: "WARST", + 648: "YAKST", + 649: "YEKST", + 656: "CHODST", + 664: "HOVDST", + 672: "Africa/Abidjan", + 673: "Africa/Algiers", + 674: "Africa/Bissau", + 675: "Africa/Cairo", + 676: "Africa/Casablanca", + 677: "Africa/Ceuta", + 678: "Africa/El_Aaiun", + 679: "Africa/Johannesburg", + 680: "Africa/Juba", + 681: "Africa/Khartoum", + 682: "Africa/Lagos", + 683: "Africa/Maputo", + 684: "Africa/Monrovia", + 685: "Africa/Nairobi", + 686: "Africa/Ndjamena", + 687: "Africa/Sao_Tome", + 688: "Africa/Tripoli", + 689: "Africa/Tunis", + 690: "Africa/Windhoek", + 691: "America/Adak", + 692: "America/Anchorage", + 693: "America/Araguaina", + 694: "America/Argentina/Buenos_Aires", + 695: "America/Argentina/Catamarca", + 696: "America/Argentina/Cordoba", + 697: "America/Argentina/Jujuy", + 698: "America/Argentina/La_Rioja", + 699: "America/Argentina/Mendoza", + 700: "America/Argentina/Rio_Gallegos", + 701: "America/Argentina/Salta", + 702: "America/Argentina/San_Juan", + 703: "America/Argentina/San_Luis", + 704: "America/Argentina/Tucuman", + 705: "America/Argentina/Ushuaia", + 706: "America/Asuncion", + 707: "America/Bahia", + 708: "America/Bahia_Banderas", + 709: "America/Barbados", + 710: "America/Belem", + 711: "America/Belize", + 712: "America/Boa_Vista", + 713: "America/Bogota", + 714: "America/Boise", + 715: "America/Cambridge_Bay", + 716: "America/Campo_Grande", + 717: "America/Cancun", + 718: "America/Caracas", + 719: "America/Cayenne", + 720: "America/Chicago", + 721: "America/Chihuahua", + 722: "America/Costa_Rica", + 723: "America/Cuiaba", + 724: "America/Danmarkshavn", + 725: "America/Dawson", + 726: "America/Dawson_Creek", + 727: "America/Denver", + 728: "America/Detroit", + 729: "America/Edmonton", + 730: "America/Eirunepe", + 731: "America/El_Salvador", + 732: "America/Fort_Nelson", + 733: "America/Fortaleza", + 734: "America/Glace_Bay", + 735: "America/Goose_Bay", + 736: "America/Grand_Turk", + 737: "America/Guatemala", + 738: "America/Guayaquil", + 739: "America/Guyana", + 740: "America/Halifax", + 741: "America/Havana", + 742: "America/Hermosillo", + 743: "America/Indiana/Indianapolis", + 744: "America/Indiana/Knox", + 745: "America/Indiana/Marengo", + 746: "America/Indiana/Petersburg", + 747: "America/Indiana/Tell_City", + 748: "America/Indiana/Vevay", + 749: "America/Indiana/Vincennes", + 750: "America/Indiana/Winamac", + 751: "America/Inuvik", + 752: "America/Iqaluit", + 753: "America/Jamaica", + 754: "America/Juneau", + 755: "America/Kentucky/Louisville", + 756: "America/Kentucky/Monticello", + 757: "America/La_Paz", + 758: "America/Lima", + 759: "America/Los_Angeles", + 760: "America/Maceio", + 761: "America/Managua", + 762: "America/Manaus", + 763: "America/Martinique", + 764: "America/Matamoros", + 765: "America/Mazatlan", + 766: "America/Menominee", + 767: "America/Merida", + 768: "America/Metlakatla", + 769: "America/Mexico_City", + 770: "America/Miquelon", + 771: "America/Moncton", + 772: "America/Monterrey", + 773: "America/Montevideo", + 774: "America/New_York", + 775: "America/Nipigon", + 776: "America/Nome", + 777: "America/Noronha", + 778: "America/North_Dakota/Beulah", + 779: "America/North_Dakota/Center", + 780: "America/North_Dakota/New_Salem", + 781: "America/Nuuk", + 782: "America/Ojinaga", + 783: "America/Panama", + 784: "America/Pangnirtung", + 785: "America/Paramaribo", + 786: "America/Phoenix", + 787: "America/Port-au-Prince", + 788: "America/Porto_Velho", + 789: "America/Puerto_Rico", + 790: "America/Punta_Arenas", + 791: "America/Rainy_River", + 792: "America/Rankin_Inlet", + 793: "America/Recife", + 794: "America/Regina", + 795: "America/Resolute", + 796: "America/Rio_Branco", + 797: "America/Santarem", + 798: "America/Santiago", + 799: "America/Santo_Domingo", + 800: "America/Sao_Paulo", + 801: "America/Scoresbysund", + 802: "America/Sitka", + 803: "America/St_Johns", + 804: "America/Swift_Current", + 805: "America/Tegucigalpa", + 806: "America/Thule", + 807: "America/Thunder_Bay", + 808: "America/Tijuana", + 809: "America/Toronto", + 810: "America/Vancouver", + 811: "America/Whitehorse", + 812: "America/Winnipeg", + 813: "America/Yakutat", + 814: "America/Yellowknife", + 815: "Antarctica/Casey", + 816: "Antarctica/Davis", + 817: "Antarctica/Macquarie", + 818: "Antarctica/Mawson", + 819: "Antarctica/Palmer", + 820: "Antarctica/Rothera", + 821: "Antarctica/Troll", + 822: "Antarctica/Vostok", + 823: "Asia/Almaty", + 824: "Asia/Amman", + 825: "Asia/Anadyr", + 826: "Asia/Aqtau", + 827: "Asia/Aqtobe", + 828: "Asia/Ashgabat", + 829: "Asia/Atyrau", + 830: "Asia/Baghdad", + 831: "Asia/Baku", + 832: "Asia/Bangkok", + 833: "Asia/Barnaul", + 834: "Asia/Beirut", + 835: "Asia/Bishkek", + 836: "Asia/Brunei", + 837: "Asia/Chita", + 838: "Asia/Choibalsan", + 839: "Asia/Colombo", + 840: "Asia/Damascus", + 841: "Asia/Dhaka", + 842: "Asia/Dili", + 843: "Asia/Dubai", + 844: "Asia/Dushanbe", + 845: "Asia/Famagusta", + 846: "Asia/Gaza", + 847: "Asia/Hebron", + 848: "Asia/Ho_Chi_Minh", + 849: "Asia/Hong_Kong", + 850: "Asia/Hovd", + 851: "Asia/Irkutsk", + 852: "Asia/Jakarta", + 853: "Asia/Jayapura", + 854: "Asia/Jerusalem", + 855: "Asia/Kabul", + 856: "Asia/Kamchatka", + 857: "Asia/Karachi", + 858: "Asia/Kathmandu", + 859: "Asia/Khandyga", + 860: "Asia/Kolkata", + 861: "Asia/Krasnoyarsk", + 862: "Asia/Kuala_Lumpur", + 863: "Asia/Kuching", + 864: "Asia/Macau", + 865: "Asia/Magadan", + 866: "Asia/Makassar", + 867: "Asia/Manila", + 868: "Asia/Nicosia", + 869: "Asia/Novokuznetsk", + 870: "Asia/Novosibirsk", + 871: "Asia/Omsk", + 872: "Asia/Oral", + 873: "Asia/Pontianak", + 874: "Asia/Pyongyang", + 875: "Asia/Qatar", + 876: "Asia/Qostanay", + 877: "Asia/Qyzylorda", + 878: "Asia/Riyadh", + 879: "Asia/Sakhalin", + 880: "Asia/Samarkand", + 881: "Asia/Seoul", + 882: "Asia/Shanghai", + 883: "Asia/Singapore", + 884: "Asia/Srednekolymsk", + 885: "Asia/Taipei", + 886: "Asia/Tashkent", + 887: "Asia/Tbilisi", + 888: "Asia/Tehran", + 889: "Asia/Thimphu", + 890: "Asia/Tokyo", + 891: "Asia/Tomsk", + 892: "Asia/Ulaanbaatar", + 893: "Asia/Urumqi", + 894: "Asia/Ust-Nera", + 895: "Asia/Vladivostok", + 896: "Asia/Yakutsk", + 897: "Asia/Yangon", + 898: "Asia/Yekaterinburg", + 899: "Asia/Yerevan", + 900: "Atlantic/Azores", + 901: "Atlantic/Bermuda", + 902: "Atlantic/Canary", + 903: "Atlantic/Cape_Verde", + 904: "Atlantic/Faroe", + 905: "Atlantic/Madeira", + 906: "Atlantic/Reykjavik", + 907: "Atlantic/South_Georgia", + 908: "Atlantic/Stanley", + 909: "Australia/Adelaide", + 910: "Australia/Brisbane", + 911: "Australia/Broken_Hill", + 912: "Australia/Darwin", + 913: "Australia/Eucla", + 914: "Australia/Hobart", + 915: "Australia/Lindeman", + 916: "Australia/Lord_Howe", + 917: "Australia/Melbourne", + 918: "Australia/Perth", + 919: "Australia/Sydney", + 920: "Etc/GMT", + 921: "Etc/UTC", + 922: "Europe/Amsterdam", + 923: "Europe/Andorra", + 924: "Europe/Astrakhan", + 925: "Europe/Athens", + 926: "Europe/Belgrade", + 927: "Europe/Berlin", + 928: "Europe/Brussels", + 929: "Europe/Bucharest", + 930: "Europe/Budapest", + 931: "Europe/Chisinau", + 932: "Europe/Copenhagen", + 933: "Europe/Dublin", + 934: "Europe/Gibraltar", + 935: "Europe/Helsinki", + 936: "Europe/Istanbul", + 937: "Europe/Kaliningrad", + 938: "Europe/Kiev", + 939: "Europe/Kirov", + 940: "Europe/Lisbon", + 941: "Europe/London", + 942: "Europe/Luxembourg", + 943: "Europe/Madrid", + 944: "Europe/Malta", + 945: "Europe/Minsk", + 946: "Europe/Monaco", + 947: "Europe/Moscow", + 948: "Europe/Oslo", + 949: "Europe/Paris", + 950: "Europe/Prague", + 951: "Europe/Riga", + 952: "Europe/Rome", + 953: "Europe/Samara", + 954: "Europe/Saratov", + 955: "Europe/Simferopol", + 956: "Europe/Sofia", + 957: "Europe/Stockholm", + 958: "Europe/Tallinn", + 959: "Europe/Tirane", + 960: "Europe/Ulyanovsk", + 961: "Europe/Uzhgorod", + 962: "Europe/Vienna", + 963: "Europe/Vilnius", + 964: "Europe/Volgograd", + 965: "Europe/Warsaw", + 966: "Europe/Zaporozhye", + 967: "Europe/Zurich", + 968: "Indian/Chagos", + 969: "Indian/Christmas", + 970: "Indian/Cocos", + 971: "Indian/Kerguelen", + 972: "Indian/Mahe", + 973: "Indian/Maldives", + 974: "Indian/Mauritius", + 975: "Indian/Reunion", + 976: "Pacific/Apia", + 977: "Pacific/Auckland", + 978: "Pacific/Bougainville", + 979: "Pacific/Chatham", + 980: "Pacific/Chuuk", + 981: "Pacific/Easter", + 982: "Pacific/Efate", + 983: "Pacific/Fakaofo", + 984: "Pacific/Fiji", + 985: "Pacific/Funafuti", + 986: "Pacific/Galapagos", + 987: "Pacific/Gambier", + 988: "Pacific/Guadalcanal", + 989: "Pacific/Guam", + 990: "Pacific/Honolulu", + 991: "Pacific/Kanton", + 992: "Pacific/Kiritimati", + 993: "Pacific/Kosrae", + 994: "Pacific/Kwajalein", + 995: "Pacific/Majuro", + 996: "Pacific/Marquesas", + 997: "Pacific/Nauru", + 998: "Pacific/Niue", + 999: "Pacific/Norfolk", + 1000: "Pacific/Noumea", + 1001: "Pacific/Pago_Pago", + 1002: "Pacific/Palau", + 1003: "Pacific/Pitcairn", + 1004: "Pacific/Pohnpei", + 1005: "Pacific/Port_Moresby", + 1006: "Pacific/Rarotonga", + 1007: "Pacific/Tahiti", + 1008: "Pacific/Tarawa", + 1009: "Pacific/Tongatapu", + 1010: "Pacific/Wake", + 1011: "Pacific/Wallis", +} + +var timezoneToIndex = map[string]int{ + "A": 1, + "B": 2, + "C": 3, + "D": 4, + "E": 5, + "F": 6, + "G": 7, + "H": 8, + "I": 9, + "K": 10, + "L": 11, + "M": 12, + "N": 13, + "O": 14, + "P": 15, + "Q": 16, + "R": 17, + "S": 18, + "T": 19, + "U": 20, + "V": 21, + "W": 22, + "X": 23, + "Y": 24, + "Z": 25, + "AT": 32, + "BT": 40, + "CT": 48, + "ET": 56, + "GT": 64, + "IT": 72, + "KT": 80, + "MT": 88, + "PT": 96, + "ST": 104, + "UT": 112, + "WT": 120, + "ACT": 128, + "ADT": 129, + "AET": 130, + "AFT": 131, + "AMT": 132, + "AoE": 133, + "ART": 134, + "AST": 135, + "AZT": 136, + "BDT": 144, + "BNT": 145, + "BOT": 146, + "BRT": 147, + "BST": 148, + "BTT": 149, + "CAT": 152, + "CCT": 153, + "CDT": 154, + "CET": 155, + "CIT": 156, + "CKT": 157, + "CLT": 158, + "COT": 159, + "CST": 160, + "CVT": 161, + "CXT": 162, + "EAT": 168, + "ECT": 169, + "EDT": 170, + "EET": 171, + "EGT": 172, + "EST": 173, + "FET": 176, + "FJT": 177, + "FKT": 178, + "FNT": 179, + "GET": 184, + "GFT": 185, + "GMT": 186, + "GST": 187, + "GYT": 188, + "HAA": 192, + "HAC": 193, + "HAE": 194, + "HAP": 195, + "HAR": 196, + "HAT": 197, + "HDT": 198, + "HKT": 199, + "HLV": 200, + "HNA": 201, + "HNC": 202, + "HNE": 203, + "HNP": 204, + "HNR": 205, + "HNT": 206, + "HST": 207, + "ICT": 208, + "IDT": 209, + "IOT": 210, + "IST": 211, + "JST": 216, + "KGT": 224, + "KIT": 225, + "KST": 226, + "MCK": 232, + "MDT": 233, + "MEZ": 234, + "MHT": 235, + "MMT": 236, + "MSD": 237, + "MSK": 238, + "MST": 239, + "MUT": 240, + "MVT": 241, + "MYT": 242, + "NCT": 248, + "NDT": 249, + "NFT": 250, + "NPT": 251, + "NRT": 252, + "NST": 253, + "NUT": 254, + "OEZ": 256, + "PDT": 264, + "PET": 265, + "PGT": 266, + "PHT": 267, + "PKT": 268, + "PST": 269, + "PWT": 270, + "PYT": 271, + "RET": 272, + "SBT": 280, + "SCT": 281, + "SGT": 282, + "SRT": 283, + "SST": 284, + "TFT": 288, + "TJT": 289, + "TKT": 290, + "TLT": 291, + "TMT": 292, + "TOT": 293, + "TRT": 294, + "TVT": 295, + "UTC": 296, + "UYT": 297, + "UZT": 298, + "VET": 304, + "VUT": 305, + "WAT": 312, + "WDT": 313, + "WET": 314, + "WEZ": 315, + "WFT": 316, + "WGT": 317, + "WIB": 318, + "WIT": 319, + "WST": 320, + "ACDT": 328, + "ACST": 329, + "ADST": 330, + "AEDT": 331, + "AEST": 332, + "AKDT": 333, + "AKST": 334, + "ALMT": 335, + "AMDT": 336, + "AMST": 337, + "ANAT": 338, + "AQTT": 339, + "AWDT": 340, + "AWST": 341, + "AZOT": 342, + "AZST": 343, + "BDST": 344, + "BRST": 345, + "CAST": 352, + "CDST": 353, + "CEDT": 354, + "CEST": 355, + "CHOT": 356, + "ChST": 357, + "CHUT": 358, + "CIST": 359, + "CLDT": 360, + "CLST": 361, + "DAVT": 368, + "DDUT": 369, + "EADT": 376, + "EAST": 377, + "ECST": 378, + "EDST": 379, + "EEDT": 380, + "EEST": 381, + "EGST": 382, + "FJDT": 384, + "FJST": 385, + "FKDT": 386, + "FKST": 387, + "GALT": 392, + "GAMT": 393, + "GILT": 394, + "HADT": 400, + "HAST": 401, + "HOVT": 402, + "IRDT": 408, + "IRKT": 409, + "IRST": 410, + "KOST": 416, + "KRAT": 417, + "KUYT": 418, + "LHDT": 424, + "LHST": 425, + "LINT": 426, + "MAGT": 432, + "MART": 433, + "MAWT": 434, + "MDST": 435, + "MESZ": 436, + "NFDT": 440, + "NOVT": 441, + "NZDT": 442, + "NZST": 443, + "OESZ": 448, + "OMST": 449, + "ORAT": 450, + "PDST": 456, + "PETT": 457, + "PHOT": 458, + "PMDT": 459, + "PMST": 460, + "PONT": 461, + "PYST": 462, + "QYZT": 464, + "ROTT": 472, + "SAKT": 480, + "SAMT": 481, + "SAST": 482, + "SRET": 483, + "SYOT": 484, + "TAHT": 488, + "TOST": 489, + "ULAT": 496, + "UYST": 497, + "VLAT": 504, + "VOST": 505, + "WAKT": 512, + "WAST": 513, + "WEDT": 514, + "WEST": 515, + "WESZ": 516, + "WGST": 517, + "WITA": 518, + "YAKT": 520, + "YAPT": 521, + "YEKT": 522, + "ACWST": 528, + "ANAST": 529, + "AZODT": 530, + "AZOST": 531, + "CHADT": 536, + "CHAST": 537, + "CHODT": 538, + "CHOST": 539, + "CIDST": 540, + "EASST": 544, + "EFATE": 545, + "HOVDT": 552, + "HOVST": 553, + "IRKST": 560, + "KRAST": 568, + "MAGST": 576, + "NACDT": 584, + "NACST": 585, + "NAEDT": 586, + "NAEST": 587, + "NAMDT": 588, + "NAMST": 589, + "NAPDT": 590, + "NAPST": 591, + "NOVST": 592, + "OMSST": 600, + "PETST": 608, + "SAMST": 616, + "ULAST": 624, + "VLAST": 632, + "WARST": 640, + "YAKST": 648, + "YEKST": 649, + "CHODST": 656, + "HOVDST": 664, + "Africa/Abidjan": 672, + "Africa/Algiers": 673, + "Africa/Bissau": 674, + "Africa/Cairo": 675, + "Africa/Casablanca": 676, + "Africa/Ceuta": 677, + "Africa/El_Aaiun": 678, + "Africa/Johannesburg": 679, + "Africa/Juba": 680, + "Africa/Khartoum": 681, + "Africa/Lagos": 682, + "Africa/Maputo": 683, + "Africa/Monrovia": 684, + "Africa/Nairobi": 685, + "Africa/Ndjamena": 686, + "Africa/Sao_Tome": 687, + "Africa/Tripoli": 688, + "Africa/Tunis": 689, + "Africa/Windhoek": 690, + "America/Adak": 691, + "America/Anchorage": 692, + "America/Araguaina": 693, + "America/Argentina/Buenos_Aires": 694, + "America/Argentina/Catamarca": 695, + "America/Argentina/Cordoba": 696, + "America/Argentina/Jujuy": 697, + "America/Argentina/La_Rioja": 698, + "America/Argentina/Mendoza": 699, + "America/Argentina/Rio_Gallegos": 700, + "America/Argentina/Salta": 701, + "America/Argentina/San_Juan": 702, + "America/Argentina/San_Luis": 703, + "America/Argentina/Tucuman": 704, + "America/Argentina/Ushuaia": 705, + "America/Asuncion": 706, + "America/Bahia": 707, + "America/Bahia_Banderas": 708, + "America/Barbados": 709, + "America/Belem": 710, + "America/Belize": 711, + "America/Boa_Vista": 712, + "America/Bogota": 713, + "America/Boise": 714, + "America/Cambridge_Bay": 715, + "America/Campo_Grande": 716, + "America/Cancun": 717, + "America/Caracas": 718, + "America/Cayenne": 719, + "America/Chicago": 720, + "America/Chihuahua": 721, + "America/Costa_Rica": 722, + "America/Cuiaba": 723, + "America/Danmarkshavn": 724, + "America/Dawson": 725, + "America/Dawson_Creek": 726, + "America/Denver": 727, + "America/Detroit": 728, + "America/Edmonton": 729, + "America/Eirunepe": 730, + "America/El_Salvador": 731, + "America/Fort_Nelson": 732, + "America/Fortaleza": 733, + "America/Glace_Bay": 734, + "America/Goose_Bay": 735, + "America/Grand_Turk": 736, + "America/Guatemala": 737, + "America/Guayaquil": 738, + "America/Guyana": 739, + "America/Halifax": 740, + "America/Havana": 741, + "America/Hermosillo": 742, + "America/Indiana/Indianapolis": 743, + "America/Indiana/Knox": 744, + "America/Indiana/Marengo": 745, + "America/Indiana/Petersburg": 746, + "America/Indiana/Tell_City": 747, + "America/Indiana/Vevay": 748, + "America/Indiana/Vincennes": 749, + "America/Indiana/Winamac": 750, + "America/Inuvik": 751, + "America/Iqaluit": 752, + "America/Jamaica": 753, + "America/Juneau": 754, + "America/Kentucky/Louisville": 755, + "America/Kentucky/Monticello": 756, + "America/La_Paz": 757, + "America/Lima": 758, + "America/Los_Angeles": 759, + "America/Maceio": 760, + "America/Managua": 761, + "America/Manaus": 762, + "America/Martinique": 763, + "America/Matamoros": 764, + "America/Mazatlan": 765, + "America/Menominee": 766, + "America/Merida": 767, + "America/Metlakatla": 768, + "America/Mexico_City": 769, + "America/Miquelon": 770, + "America/Moncton": 771, + "America/Monterrey": 772, + "America/Montevideo": 773, + "America/New_York": 774, + "America/Nipigon": 775, + "America/Nome": 776, + "America/Noronha": 777, + "America/North_Dakota/Beulah": 778, + "America/North_Dakota/Center": 779, + "America/North_Dakota/New_Salem": 780, + "America/Nuuk": 781, + "America/Ojinaga": 782, + "America/Panama": 783, + "America/Pangnirtung": 784, + "America/Paramaribo": 785, + "America/Phoenix": 786, + "America/Port-au-Prince": 787, + "America/Porto_Velho": 788, + "America/Puerto_Rico": 789, + "America/Punta_Arenas": 790, + "America/Rainy_River": 791, + "America/Rankin_Inlet": 792, + "America/Recife": 793, + "America/Regina": 794, + "America/Resolute": 795, + "America/Rio_Branco": 796, + "America/Santarem": 797, + "America/Santiago": 798, + "America/Santo_Domingo": 799, + "America/Sao_Paulo": 800, + "America/Scoresbysund": 801, + "America/Sitka": 802, + "America/St_Johns": 803, + "America/Swift_Current": 804, + "America/Tegucigalpa": 805, + "America/Thule": 806, + "America/Thunder_Bay": 807, + "America/Tijuana": 808, + "America/Toronto": 809, + "America/Vancouver": 810, + "America/Whitehorse": 811, + "America/Winnipeg": 812, + "America/Yakutat": 813, + "America/Yellowknife": 814, + "Antarctica/Casey": 815, + "Antarctica/Davis": 816, + "Antarctica/Macquarie": 817, + "Antarctica/Mawson": 818, + "Antarctica/Palmer": 819, + "Antarctica/Rothera": 820, + "Antarctica/Troll": 821, + "Antarctica/Vostok": 822, + "Asia/Almaty": 823, + "Asia/Amman": 824, + "Asia/Anadyr": 825, + "Asia/Aqtau": 826, + "Asia/Aqtobe": 827, + "Asia/Ashgabat": 828, + "Asia/Atyrau": 829, + "Asia/Baghdad": 830, + "Asia/Baku": 831, + "Asia/Bangkok": 832, + "Asia/Barnaul": 833, + "Asia/Beirut": 834, + "Asia/Bishkek": 835, + "Asia/Brunei": 836, + "Asia/Chita": 837, + "Asia/Choibalsan": 838, + "Asia/Colombo": 839, + "Asia/Damascus": 840, + "Asia/Dhaka": 841, + "Asia/Dili": 842, + "Asia/Dubai": 843, + "Asia/Dushanbe": 844, + "Asia/Famagusta": 845, + "Asia/Gaza": 846, + "Asia/Hebron": 847, + "Asia/Ho_Chi_Minh": 848, + "Asia/Hong_Kong": 849, + "Asia/Hovd": 850, + "Asia/Irkutsk": 851, + "Asia/Jakarta": 852, + "Asia/Jayapura": 853, + "Asia/Jerusalem": 854, + "Asia/Kabul": 855, + "Asia/Kamchatka": 856, + "Asia/Karachi": 857, + "Asia/Kathmandu": 858, + "Asia/Khandyga": 859, + "Asia/Kolkata": 860, + "Asia/Krasnoyarsk": 861, + "Asia/Kuala_Lumpur": 862, + "Asia/Kuching": 863, + "Asia/Macau": 864, + "Asia/Magadan": 865, + "Asia/Makassar": 866, + "Asia/Manila": 867, + "Asia/Nicosia": 868, + "Asia/Novokuznetsk": 869, + "Asia/Novosibirsk": 870, + "Asia/Omsk": 871, + "Asia/Oral": 872, + "Asia/Pontianak": 873, + "Asia/Pyongyang": 874, + "Asia/Qatar": 875, + "Asia/Qostanay": 876, + "Asia/Qyzylorda": 877, + "Asia/Riyadh": 878, + "Asia/Sakhalin": 879, + "Asia/Samarkand": 880, + "Asia/Seoul": 881, + "Asia/Shanghai": 882, + "Asia/Singapore": 883, + "Asia/Srednekolymsk": 884, + "Asia/Taipei": 885, + "Asia/Tashkent": 886, + "Asia/Tbilisi": 887, + "Asia/Tehran": 888, + "Asia/Thimphu": 889, + "Asia/Tokyo": 890, + "Asia/Tomsk": 891, + "Asia/Ulaanbaatar": 892, + "Asia/Urumqi": 893, + "Asia/Ust-Nera": 894, + "Asia/Vladivostok": 895, + "Asia/Yakutsk": 896, + "Asia/Yangon": 897, + "Asia/Yekaterinburg": 898, + "Asia/Yerevan": 899, + "Atlantic/Azores": 900, + "Atlantic/Bermuda": 901, + "Atlantic/Canary": 902, + "Atlantic/Cape_Verde": 903, + "Atlantic/Faroe": 904, + "Atlantic/Madeira": 905, + "Atlantic/Reykjavik": 906, + "Atlantic/South_Georgia": 907, + "Atlantic/Stanley": 908, + "Australia/Adelaide": 909, + "Australia/Brisbane": 910, + "Australia/Broken_Hill": 911, + "Australia/Darwin": 912, + "Australia/Eucla": 913, + "Australia/Hobart": 914, + "Australia/Lindeman": 915, + "Australia/Lord_Howe": 916, + "Australia/Melbourne": 917, + "Australia/Perth": 918, + "Australia/Sydney": 919, + "Etc/GMT": 920, + "Etc/UTC": 921, + "Europe/Amsterdam": 922, + "Europe/Andorra": 923, + "Europe/Astrakhan": 924, + "Europe/Athens": 925, + "Europe/Belgrade": 926, + "Europe/Berlin": 927, + "Europe/Brussels": 928, + "Europe/Bucharest": 929, + "Europe/Budapest": 930, + "Europe/Chisinau": 931, + "Europe/Copenhagen": 932, + "Europe/Dublin": 933, + "Europe/Gibraltar": 934, + "Europe/Helsinki": 935, + "Europe/Istanbul": 936, + "Europe/Kaliningrad": 937, + "Europe/Kiev": 938, + "Europe/Kirov": 939, + "Europe/Lisbon": 940, + "Europe/London": 941, + "Europe/Luxembourg": 942, + "Europe/Madrid": 943, + "Europe/Malta": 944, + "Europe/Minsk": 945, + "Europe/Monaco": 946, + "Europe/Moscow": 947, + "Europe/Oslo": 948, + "Europe/Paris": 949, + "Europe/Prague": 950, + "Europe/Riga": 951, + "Europe/Rome": 952, + "Europe/Samara": 953, + "Europe/Saratov": 954, + "Europe/Simferopol": 955, + "Europe/Sofia": 956, + "Europe/Stockholm": 957, + "Europe/Tallinn": 958, + "Europe/Tirane": 959, + "Europe/Ulyanovsk": 960, + "Europe/Uzhgorod": 961, + "Europe/Vienna": 962, + "Europe/Vilnius": 963, + "Europe/Volgograd": 964, + "Europe/Warsaw": 965, + "Europe/Zaporozhye": 966, + "Europe/Zurich": 967, + "Indian/Chagos": 968, + "Indian/Christmas": 969, + "Indian/Cocos": 970, + "Indian/Kerguelen": 971, + "Indian/Mahe": 972, + "Indian/Maldives": 973, + "Indian/Mauritius": 974, + "Indian/Reunion": 975, + "Pacific/Apia": 976, + "Pacific/Auckland": 977, + "Pacific/Bougainville": 978, + "Pacific/Chatham": 979, + "Pacific/Chuuk": 980, + "Pacific/Easter": 981, + "Pacific/Efate": 982, + "Pacific/Fakaofo": 983, + "Pacific/Fiji": 984, + "Pacific/Funafuti": 985, + "Pacific/Galapagos": 986, + "Pacific/Gambier": 987, + "Pacific/Guadalcanal": 988, + "Pacific/Guam": 989, + "Pacific/Honolulu": 990, + "Pacific/Kanton": 991, + "Pacific/Kiritimati": 992, + "Pacific/Kosrae": 993, + "Pacific/Kwajalein": 994, + "Pacific/Majuro": 995, + "Pacific/Marquesas": 996, + "Pacific/Nauru": 997, + "Pacific/Niue": 998, + "Pacific/Norfolk": 999, + "Pacific/Noumea": 1000, + "Pacific/Pago_Pago": 1001, + "Pacific/Palau": 1002, + "Pacific/Pitcairn": 1003, + "Pacific/Pohnpei": 1004, + "Pacific/Port_Moresby": 1005, + "Pacific/Rarotonga": 1006, + "Pacific/Tahiti": 1007, + "Pacific/Tarawa": 1008, + "Pacific/Tongatapu": 1009, + "Pacific/Wake": 1010, + "Pacific/Wallis": 1011, + "Africa/Accra": 672, + "Africa/Addis_Ababa": 685, + "Africa/Asmara": 685, + "Africa/Asmera": 685, + "Africa/Bamako": 672, + "Africa/Bangui": 682, + "Africa/Banjul": 672, + "Africa/Blantyre": 683, + "Africa/Brazzaville": 682, + "Africa/Bujumbura": 683, + "Africa/Conakry": 672, + "Africa/Dakar": 672, + "Africa/Dar_es_Salaam": 685, + "Africa/Djibouti": 685, + "Africa/Douala": 682, + "Africa/Freetown": 672, + "Africa/Gaborone": 683, + "Africa/Harare": 683, + "Africa/Kampala": 685, + "Africa/Kigali": 683, + "Africa/Kinshasa": 682, + "Africa/Libreville": 682, + "Africa/Lome": 672, + "Africa/Luanda": 682, + "Africa/Lubumbashi": 683, + "Africa/Lusaka": 683, + "Africa/Malabo": 682, + "Africa/Maseru": 679, + "Africa/Mbabane": 679, + "Africa/Mogadishu": 685, + "Africa/Niamey": 682, + "Africa/Nouakchott": 672, + "Africa/Ouagadougou": 672, + "Africa/Porto-Novo": 682, + "Africa/Timbuktu": 672, + "America/Anguilla": 789, + "America/Antigua": 789, + "America/Argentina/ComodRivadavia": 695, + "America/Aruba": 789, + "America/Atikokan": 783, + "America/Atka": 691, + "America/Blanc-Sablon": 789, + "America/Buenos_Aires": 694, + "America/Catamarca": 695, + "America/Cayman": 783, + "America/Coral_Harbour": 783, + "America/Cordoba": 696, + "America/Creston": 786, + "America/Curacao": 789, + "America/Dominica": 789, + "America/Ensenada": 808, + "America/Fort_Wayne": 743, + "America/Godthab": 781, + "America/Grenada": 789, + "America/Guadeloupe": 789, + "America/Indianapolis": 743, + "America/Jujuy": 697, + "America/Knox_IN": 744, + "America/Kralendijk": 789, + "America/Louisville": 755, + "America/Lower_Princes": 789, + "America/Marigot": 789, + "America/Mendoza": 699, + "America/Montreal": 809, + "America/Montserrat": 789, + "America/Nassau": 809, + "America/Port_of_Spain": 789, + "America/Porto_Acre": 796, + "America/Rosario": 696, + "America/Santa_Isabel": 808, + "America/Shiprock": 727, + "America/St_Barthelemy": 789, + "America/St_Kitts": 789, + "America/St_Lucia": 789, + "America/St_Thomas": 789, + "America/St_Vincent": 789, + "America/Tortola": 789, + "America/Virgin": 789, + "Antarctica/DumontDUrville": 1005, + "Antarctica/McMurdo": 977, + "Antarctica/South_Pole": 977, + "Antarctica/Syowa": 878, + "Arctic/Longyearbyen": 948, + "Asia/Aden": 878, + "Asia/Ashkhabad": 828, + "Asia/Bahrain": 875, + "Asia/Calcutta": 860, + "Asia/Chongqing": 882, + "Asia/Chungking": 882, + "Asia/Dacca": 841, + "Asia/Harbin": 882, + "Asia/Istanbul": 936, + "Asia/Kashgar": 893, + "Asia/Katmandu": 858, + "Asia/Kuwait": 878, + "Asia/Macao": 864, + "Asia/Muscat": 843, + "Asia/Phnom_Penh": 832, + "Asia/Rangoon": 897, + "Asia/Saigon": 848, + "Asia/Tel_Aviv": 854, + "Asia/Thimbu": 889, + "Asia/Ujung_Pandang": 866, + "Asia/Ulan_Bator": 892, + "Asia/Vientiane": 832, + "Atlantic/Faeroe": 904, + "Atlantic/Jan_Mayen": 948, + "Atlantic/St_Helena": 672, + "Australia/ACT": 919, + "Australia/Canberra": 919, + "Australia/Currie": 914, + "Australia/LHI": 916, + "Australia/NSW": 919, + "Australia/North": 912, + "Australia/Queensland": 910, + "Australia/South": 909, + "Australia/Tasmania": 914, + "Australia/Victoria": 917, + "Australia/West": 918, + "Australia/Yancowinna": 911, + "Brazil/Acre": 796, + "Brazil/DeNoronha": 777, + "Brazil/East": 800, + "Brazil/West": 762, + "Canada/Atlantic": 740, + "Canada/Central": 812, + "Canada/Eastern": 809, + "Canada/Mountain": 729, + "Canada/Newfoundland": 803, + "Canada/Pacific": 810, + "Canada/Saskatchewan": 794, + "Canada/Yukon": 811, + "Chile/Continental": 798, + "Chile/EasterIsland": 981, + "Cuba": 741, + "Egypt": 675, + "Eire": 933, + "Etc/GMT+0": 920, + "Etc/GMT-0": 920, + "Etc/GMT0": 920, + "Etc/Greenwich": 920, + "Etc/UCT": 921, + "Etc/Universal": 921, + "Etc/Zulu": 921, + "Europe/Belfast": 941, + "Europe/Bratislava": 950, + "Europe/Busingen": 967, + "Europe/Guernsey": 941, + "Europe/Isle_of_Man": 941, + "Europe/Jersey": 941, + "Europe/Ljubljana": 926, + "Europe/Mariehamn": 935, + "Europe/Nicosia": 868, + "Europe/Podgorica": 926, + "Europe/San_Marino": 952, + "Europe/Sarajevo": 926, + "Europe/Skopje": 926, + "Europe/Tiraspol": 931, + "Europe/Vaduz": 967, + "Europe/Vatican": 952, + "Europe/Zagreb": 926, + "GB": 941, + "GB-Eire": 941, + "GMT+0": 920, + "GMT-0": 920, + "GMT0": 920, + "Greenwich": 920, + "Hongkong": 849, + "Iceland": 906, + "Indian/Antananarivo": 685, + "Indian/Comoro": 685, + "Indian/Mayotte": 685, + "Iran": 888, + "Israel": 854, + "Jamaica": 753, + "Japan": 890, + "Kwajalein": 994, + "Libya": 688, + "Mexico/BajaNorte": 808, + "Mexico/BajaSur": 765, + "Mexico/General": 769, + "NZ": 977, + "NZ-CHAT": 979, + "Navajo": 727, + "PRC": 882, + "Pacific/Enderbury": 991, + "Pacific/Johnston": 990, + "Pacific/Midway": 1001, + "Pacific/Ponape": 1004, + "Pacific/Saipan": 989, + "Pacific/Samoa": 1001, + "Pacific/Truk": 980, + "Pacific/Yap": 980, + "Poland": 965, + "Portugal": 940, + "ROC": 885, + "ROK": 881, + "Singapore": 883, + "Turkey": 936, + "UCT": 921, + "US/Alaska": 692, + "US/Aleutian": 691, + "US/Arizona": 786, + "US/Central": 720, + "US/East-Indiana": 743, + "US/Eastern": 774, + "US/Hawaii": 990, + "US/Indiana-Starke": 744, + "US/Michigan": 728, + "US/Mountain": 727, + "US/Pacific": 759, + "US/Samoa": 1001, + "Universal": 921, + "W-SU": 947, + "Zulu": 921, +} From ab1fb930328e8a680ef2537b25be77f8b94bf5a0 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 5 Aug 2022 16:21:37 +0300 Subject: [PATCH 319/605] datetime: add interval support The patch adds interval [1] support for datetime. Except encoding/decoding interval values from MessagePack, it adds a several functions for addition and substraction Interval and Datetime types in GoLang. Reproducing, thus, arithmetic operations from the Lua implementation [2]. 1. https://github.com/tarantool/tarantool/wiki/Datetime-Internals#interval-arithmetic 2. https://github.com/tarantool/tarantool/wiki/Datetime-Internals#arithmetic-operations Closes #165 --- CHANGELOG.md | 1 + datetime/adjust.go | 31 ++++ datetime/config.lua | 10 ++ datetime/datetime.go | 98 +++++++++++ datetime/datetime_test.go | 348 ++++++++++++++++++++++++++++++++++++++ datetime/example_test.go | 135 +++++++++++++++ datetime/interval.go | 179 ++++++++++++++++++++ datetime/interval_test.go | 132 +++++++++++++++ datetime/msgpack.go | 16 ++ datetime/msgpack_v5.go | 30 ++++ 10 files changed, 980 insertions(+) create mode 100644 datetime/adjust.go create mode 100644 datetime/interval.go create mode 100644 datetime/interval_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 25d0ed32e..f4b57c82c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Optional msgpack.v5 usage (#124) - TZ support for datetime (#163) +- Interval support for datetime (#165) ### Changed diff --git a/datetime/adjust.go b/datetime/adjust.go new file mode 100644 index 000000000..35812f45f --- /dev/null +++ b/datetime/adjust.go @@ -0,0 +1,31 @@ +package datetime + +// An Adjust is used as a parameter for date adjustions, see: +// https://github.com/tarantool/tarantool/wiki/Datetime-Internals#date-adjustions-and-leap-years +type Adjust int + +const ( + NoneAdjust Adjust = 0 // adjust = "none" in Tarantool + ExcessAdjust Adjust = 1 // adjust = "excess" in Tarantool + LastAdjust Adjust = 2 // adjust = "last" in Tarantool +) + +// We need the mappings to make NoneAdjust as a default value instead of +// dtExcess. +const ( + dtExcess = 0 // DT_EXCESS from dt-c/dt_arithmetic.h + dtLimit = 1 // DT_LIMIT + dtSnap = 2 // DT_SNAP +) + +var adjustToDt = map[Adjust]int64{ + NoneAdjust: dtLimit, + ExcessAdjust: dtExcess, + LastAdjust: dtSnap, +} + +var dtToAdjust = map[int64]Adjust{ + dtExcess: ExcessAdjust, + dtLimit: NoneAdjust, + dtSnap: LastAdjust, +} diff --git a/datetime/config.lua b/datetime/config.lua index 8b1ba2316..9b5baf719 100644 --- a/datetime/config.lua +++ b/datetime/config.lua @@ -61,6 +61,16 @@ local function call_datetime_testdata() end rawset(_G, 'call_datetime_testdata', call_datetime_testdata) +local function call_interval_testdata(interval) + return interval +end +rawset(_G, 'call_interval_testdata', call_interval_testdata) + +local function call_datetime_interval(dtleft, dtright) + return dtright - dtleft +end +rawset(_G, 'call_datetime_interval', call_datetime_interval) + -- Set listen only when every other thing is configured. box.cfg{ listen = os.getenv("TEST_TNT_LISTEN"), diff --git a/datetime/datetime.go b/datetime/datetime.go index a73550b23..b9fbc1dbe 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -120,6 +120,104 @@ func NewDatetime(t time.Time) (*Datetime, error) { return dt, nil } +func intervalFromDatetime(dtime *Datetime) (ival Interval) { + ival.Year = int64(dtime.time.Year()) + ival.Month = int64(dtime.time.Month()) + ival.Day = int64(dtime.time.Day()) + ival.Hour = int64(dtime.time.Hour()) + ival.Min = int64(dtime.time.Minute()) + ival.Sec = int64(dtime.time.Second()) + ival.Nsec = int64(dtime.time.Nanosecond()) + ival.Adjust = NoneAdjust + + return ival +} + +func daysInMonth(year int64, month int64) int64 { + if month == 12 { + year++ + month = 1 + } else { + month += 1 + } + + // We use the fact that time.Date accepts values outside their usual + // ranges - the values are normalized during the conversion. + // + // So we got a day (year, month - 1, last day of the month) before + // (year, month, 1) because we pass (year, month, 0). + return int64(time.Date(int(year), time.Month(month), 0, 0, 0, 0, 0, time.UTC).Day()) +} + +// C imlementation: +// https://github.com/tarantool/c-dt/blob/cec6acebb54d9e73ea0b99c63898732abd7683a6/dt_arithmetic.c#L74-L98 +func addMonth(ival *Interval, delta int64, adjust Adjust) { + oldYear := ival.Year + oldMonth := ival.Month + + ival.Month += delta + if ival.Month < 1 || ival.Month > 12 { + ival.Year += ival.Month / 12 + ival.Month %= 12 + if ival.Month < 1 { + ival.Year-- + ival.Month += 12 + } + } + if adjust == ExcessAdjust || ival.Day < 28 { + return + } + + dim := daysInMonth(ival.Year, ival.Month) + if ival.Day > dim || (adjust == LastAdjust && ival.Day == daysInMonth(oldYear, oldMonth)) { + ival.Day = dim + } +} + +func (dtime *Datetime) add(ival Interval, positive bool) (*Datetime, error) { + newVal := intervalFromDatetime(dtime) + + var direction int64 + if positive { + direction = 1 + } else { + direction = -1 + } + + addMonth(&newVal, direction*ival.Year*12+direction*ival.Month, ival.Adjust) + newVal.Day += direction * 7 * ival.Week + newVal.Day += direction * ival.Day + newVal.Hour += direction * ival.Hour + newVal.Min += direction * ival.Min + newVal.Sec += direction * ival.Sec + newVal.Nsec += direction * ival.Nsec + + tm := time.Date(int(newVal.Year), time.Month(newVal.Month), + int(newVal.Day), int(newVal.Hour), int(newVal.Min), + int(newVal.Sec), int(newVal.Nsec), dtime.time.Location()) + + return NewDatetime(tm) +} + +// Add creates a new Datetime as addition of the Datetime and Interval. It may +// return an error if a new Datetime is out of supported range. +func (dtime *Datetime) Add(ival Interval) (*Datetime, error) { + return dtime.add(ival, true) +} + +// Sub creates a new Datetime as subtraction of the Datetime and Interval. It +// may return an error if a new Datetime is out of supported range. +func (dtime *Datetime) Sub(ival Interval) (*Datetime, error) { + return dtime.add(ival, false) +} + +// Interval returns an Interval value to a next Datetime value. +func (dtime *Datetime) Interval(next *Datetime) Interval { + curIval := intervalFromDatetime(dtime) + nextIval := intervalFromDatetime(next) + return nextIval.Sub(curIval) +} + // ToTime returns a time.Time that Datetime contains. func (dtime *Datetime) ToTime() time.Time { return dtime.time diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 77153b9fc..c128a82d0 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -54,6 +54,354 @@ func skipIfDatetimeUnsupported(t *testing.T) { } } +func TestDatetimeAdd(t *testing.T) { + tm := time.Unix(0, 0).UTC() + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + newdt, err := dt.Add(Interval{ + Year: 1, + Month: -3, + Week: 3, + Day: 4, + Hour: -5, + Min: 5, + Sec: 6, + Nsec: -3, + }) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + expected := "1970-10-25 19:05:05.999999997 +0000 UTC" + if newdt.ToTime().String() != expected { + t.Fatalf("Unexpected result: %s, expected: %s", newdt.ToTime().String(), expected) + } +} + +func TestDatetimeAddAdjust(t *testing.T) { + /* + How-to test in Tarantool: + > date = require("datetime") + > date.parse("2012-12-31T00:00:00Z") + {month = -1, adjust = "excess"} + */ + cases := []struct { + year int64 + month int64 + adjust Adjust + date string + want string + }{ + { + year: 0, + month: 1, + adjust: NoneAdjust, + date: "2013-02-28T00:00:00Z", + want: "2013-03-28T00:00:00Z", + }, + { + year: 0, + month: 1, + adjust: LastAdjust, + date: "2013-02-28T00:00:00Z", + want: "2013-03-31T00:00:00Z", + }, + { + year: 0, + month: 1, + adjust: ExcessAdjust, + date: "2013-02-28T00:00:00Z", + want: "2013-03-28T00:00:00Z", + }, + { + year: 0, + month: 1, + adjust: NoneAdjust, + date: "2013-01-31T00:00:00Z", + want: "2013-02-28T00:00:00Z", + }, + { + year: 0, + month: 1, + adjust: LastAdjust, + date: "2013-01-31T00:00:00Z", + want: "2013-02-28T00:00:00Z", + }, + { + year: 0, + month: 1, + adjust: ExcessAdjust, + date: "2013-01-31T00:00:00Z", + want: "2013-03-03T00:00:00Z", + }, + { + year: 2, + month: 2, + adjust: NoneAdjust, + date: "2011-12-31T00:00:00Z", + want: "2014-02-28T00:00:00Z", + }, + { + year: 2, + month: 2, + adjust: LastAdjust, + date: "2011-12-31T00:00:00Z", + want: "2014-02-28T00:00:00Z", + }, + { + year: 2, + month: 2, + adjust: ExcessAdjust, + date: "2011-12-31T00:00:00Z", + want: "2014-03-03T00:00:00Z", + }, + { + year: 0, + month: -1, + adjust: NoneAdjust, + date: "2013-02-28T00:00:00Z", + want: "2013-01-28T00:00:00Z", + }, + { + year: 0, + month: -1, + adjust: LastAdjust, + date: "2013-02-28T00:00:00Z", + want: "2013-01-31T00:00:00Z", + }, + { + year: 0, + month: -1, + adjust: ExcessAdjust, + date: "2013-02-28T00:00:00Z", + want: "2013-01-28T00:00:00Z", + }, + { + year: 0, + month: -1, + adjust: NoneAdjust, + date: "2012-12-31T00:00:00Z", + want: "2012-11-30T00:00:00Z", + }, + { + year: 0, + month: -1, + adjust: LastAdjust, + date: "2012-12-31T00:00:00Z", + want: "2012-11-30T00:00:00Z", + }, + { + year: 0, + month: -1, + adjust: ExcessAdjust, + date: "2012-12-31T00:00:00Z", + want: "2012-12-01T00:00:00Z", + }, + { + year: -2, + month: -2, + adjust: NoneAdjust, + date: "2011-01-31T00:00:00Z", + want: "2008-11-30T00:00:00Z", + }, + { + year: -2, + month: -2, + adjust: LastAdjust, + date: "2011-12-31T00:00:00Z", + want: "2009-10-31T00:00:00Z", + }, + { + year: -2, + month: -2, + adjust: ExcessAdjust, + date: "2011-12-31T00:00:00Z", + want: "2009-10-31T00:00:00Z", + }, + } + + for _, tc := range cases { + tm, err := time.Parse(time.RFC3339, tc.date) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + t.Run(fmt.Sprintf("%d_%d_%d_%s", tc.year, tc.month, tc.adjust, tc.date), + func(t *testing.T) { + newdt, err := dt.Add(Interval{ + Year: tc.year, + Month: tc.month, + Adjust: tc.adjust, + }) + if err != nil { + t.Fatalf("Unable to add: %s", err.Error()) + } + if newdt == nil { + t.Fatalf("Unexpected nil value") + } + res := newdt.ToTime().Format(time.RFC3339) + if res != tc.want { + t.Fatalf("Unexpected result %s, expected %s", res, tc.want) + } + }) + } +} + +func TestDatetimeAddSubSymmetric(t *testing.T) { + tm := time.Unix(0, 0).UTC() + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + newdtadd, err := dt.Add(Interval{ + Year: 1, + Month: -3, + Week: 3, + Day: 4, + Hour: -5, + Min: 5, + Sec: 6, + Nsec: -3, + }) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + newdtsub, err := dt.Sub(Interval{ + Year: -1, + Month: 3, + Week: -3, + Day: -4, + Hour: 5, + Min: -5, + Sec: -6, + Nsec: 3, + }) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + expected := "1970-10-25 19:05:05.999999997 +0000 UTC" + addstr := newdtadd.ToTime().String() + substr := newdtsub.ToTime().String() + + if addstr != expected { + t.Fatalf("Unexpected Add result: %s, expected: %s", addstr, expected) + } + if substr != expected { + t.Fatalf("Unexpected Sub result: %s, expected: %s", substr, expected) + } +} + +// We have a separate test for accurate Datetime boundaries. +func TestDatetimeAddOutOfRange(t *testing.T) { + tm := time.Unix(0, 0).UTC() + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + newdt, err := dt.Add(Interval{Year: 1000000000}) + if err == nil { + t.Fatalf("Unexpected success: %v", newdt) + } + expected := "time 1000001970-01-01 00:00:00 +0000 UTC is out of supported range" + if err.Error() != expected { + t.Fatalf("Unexpected error: %s", err.Error()) + } + if newdt != nil { + t.Fatalf("Unexpected result: %v", newdt) + } +} + +func TestDatetimeInterval(t *testing.T) { + var first = "2015-03-20T17:50:56.000000009Z" + var second = "2013-01-31T17:51:56.000000009Z" + + tmFirst, err := time.Parse(time.RFC3339, first) + if err != nil { + t.Fatalf("Error in time.Parse(): %s", err) + } + tmSecond, err := time.Parse(time.RFC3339, second) + if err != nil { + t.Fatalf("Error in time.Parse(): %s", err) + } + + dtFirst, err := NewDatetime(tmFirst) + if err != nil { + t.Fatalf("Unable to create Datetime from %s: %s", tmFirst, err) + } + dtSecond, err := NewDatetime(tmSecond) + if err != nil { + t.Fatalf("Unable to create Datetime from %s: %s", tmSecond, err) + } + + ivalFirst := dtFirst.Interval(dtSecond) + ivalSecond := dtSecond.Interval(dtFirst) + + expectedFirst := Interval{-2, -2, 0, 11, 0, 1, 0, 0, NoneAdjust} + expectedSecond := Interval{2, 2, 0, -11, 0, -1, 0, 0, NoneAdjust} + + if !reflect.DeepEqual(ivalFirst, expectedFirst) { + t.Errorf("Unexpected interval %v, expected %v", ivalFirst, expectedFirst) + } + if !reflect.DeepEqual(ivalSecond, expectedSecond) { + t.Errorf("Unexpected interval %v, expected %v", ivalFirst, expectedSecond) + } +} + +func TestDatetimeTarantoolInterval(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + dates := []string{ + "2015-03-20T17:50:56.000000009+01:00", + "2015-12-21T17:50:53Z", + "2010-02-24T23:03:56.0000013-04:00", + "1980-03-28T13:18:39.000099Z", + "2025-08-01T00:00:00.000000003+11:00", + "2020-01-01T01:01:01+11:30", + } + datetimes := []*Datetime{} + for _, date := range dates { + tm, err := time.Parse(time.RFC3339, date) + if err != nil { + t.Fatalf("Error in time.Parse(%s): %s", date, err) + } + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Error in NewDatetime(%s): %s", tm, err) + } + datetimes = append(datetimes, dt) + } + + for _, dti := range datetimes { + for _, dtj := range datetimes { + t.Run(fmt.Sprintf("%s_to_%s", dti.ToTime(), dtj.ToTime()), + func(t *testing.T) { + resp, err := conn.Call17("call_datetime_interval", + []interface{}{dti, dtj}) + if err != nil { + t.Fatalf("Unable to call call_datetime_interval: %s", err) + } + ival := dti.Interval(dtj) + ret := resp.Data[0].(Interval) + if !reflect.DeepEqual(ival, ret) { + t.Fatalf("%v != %v", ival, ret) + } + }) + } + } +} + // Expect that first element of tuple is time.Time. Compare extracted actual // and expected datetime values. func assertDatetimeIsEqual(t *testing.T, tuples []interface{}, tm time.Time) { diff --git a/datetime/example_test.go b/datetime/example_test.go index ce8cfb800..ca1603ea8 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -100,3 +100,138 @@ func ExampleNewDatetime_noTimezone() { fmt.Printf("Time value: %v\n", dt.ToTime()) } + +// ExampleDatetime_Interval demonstrates how to get an Interval value between +// two Datetime values. +func ExampleDatetime_Interval() { + var first = "2013-01-31T17:51:56.000000009Z" + var second = "2015-03-20T17:50:56.000000009Z" + + tmFirst, err := time.Parse(time.RFC3339, first) + if err != nil { + fmt.Printf("Error in time.Parse() is %v", err) + return + } + tmSecond, err := time.Parse(time.RFC3339, second) + if err != nil { + fmt.Printf("Error in time.Parse() is %v", err) + return + } + + dtFirst, err := NewDatetime(tmFirst) + if err != nil { + fmt.Printf("Unable to create Datetime from %s: %s", tmFirst, err) + return + } + dtSecond, err := NewDatetime(tmSecond) + if err != nil { + fmt.Printf("Unable to create Datetime from %s: %s", tmSecond, err) + return + } + + ival := dtFirst.Interval(dtSecond) + fmt.Printf("%v", ival) + // Output: + // {2 2 0 -11 0 -1 0 0 0} +} + +// ExampleDatetime_Add demonstrates how to add an Interval to a Datetime value. +func ExampleDatetime_Add() { + var datetime = "2013-01-31T17:51:56.000000009Z" + tm, err := time.Parse(time.RFC3339, datetime) + if err != nil { + fmt.Printf("Error in time.Parse() is %s", err) + return + } + dt, err := NewDatetime(tm) + if err != nil { + fmt.Printf("Unable to create Datetime from %s: %s", tm, err) + return + } + + newdt, err := dt.Add(Interval{ + Year: 1, + Month: 1, + Sec: 333, + Adjust: LastAdjust, + }) + if err != nil { + fmt.Printf("Unable to add to Datetime: %s", err) + return + } + + fmt.Printf("New time: %s\n", newdt.ToTime().String()) + // Output: + // New time: 2014-02-28 17:57:29.000000009 +0000 UTC +} + +// ExampleDatetime_Sub demonstrates how to subtract an Interval from a +// Datetime value. +func ExampleDatetime_Sub() { + var datetime = "2013-01-31T17:51:56.000000009Z" + tm, err := time.Parse(time.RFC3339, datetime) + if err != nil { + fmt.Printf("Error in time.Parse() is %s", err) + return + } + dt, err := NewDatetime(tm) + if err != nil { + fmt.Printf("Unable to create Datetime from %s: %s", tm, err) + return + } + + newdt, err := dt.Sub(Interval{ + Year: 1, + Month: 1, + Sec: 333, + Adjust: LastAdjust, + }) + if err != nil { + fmt.Printf("Unable to sub from Datetime: %s", err) + return + } + + fmt.Printf("New time: %s\n", newdt.ToTime().String()) + // Output: + // New time: 2011-12-31 17:46:23.000000009 +0000 UTC +} + +// ExampleInterval_Add demonstrates how to add two intervals. +func ExampleInterval_Add() { + orig := Interval{ + Year: 1, + Month: 2, + Week: 3, + Sec: 10, + Adjust: ExcessAdjust, + } + ival := orig.Add(Interval{ + Year: 10, + Min: 30, + Adjust: LastAdjust, + }) + + fmt.Printf("%v", ival) + // Output: + // {11 2 3 0 0 30 10 0 1} +} + +// ExampleInterval_Sub demonstrates how to subtract two intervals. +func ExampleInterval_Sub() { + orig := Interval{ + Year: 1, + Month: 2, + Week: 3, + Sec: 10, + Adjust: ExcessAdjust, + } + ival := orig.Sub(Interval{ + Year: 10, + Min: 30, + Adjust: LastAdjust, + }) + + fmt.Printf("%v", ival) + // Output: + // {-9 2 3 0 0 -30 10 0 1} +} diff --git a/datetime/interval.go b/datetime/interval.go new file mode 100644 index 000000000..eee6b2d97 --- /dev/null +++ b/datetime/interval.go @@ -0,0 +1,179 @@ +package datetime + +import ( + "fmt" + "reflect" +) + +const interval_extId = 6 + +const ( + fieldYear = 0 + fieldMonth = 1 + fieldWeek = 2 + fieldDay = 3 + fieldHour = 4 + fieldMin = 5 + fieldSec = 6 + fieldNSec = 7 + fieldAdjust = 8 +) + +// Interval type is GoLang implementation of Tarantool intervals. +type Interval struct { + Year int64 + Month int64 + Week int64 + Day int64 + Hour int64 + Min int64 + Sec int64 + Nsec int64 + Adjust Adjust +} + +// We use int64 for every field to avoid changes in the future, see: +// https://github.com/tarantool/tarantool/blob/943ce3caf8401510ced4f074bca7006c3d73f9b3/src/lib/core/datetime.h#L106 + +// Add creates a new Interval as addition of intervals. +func (ival Interval) Add(add Interval) Interval { + ival.Year += add.Year + ival.Month += add.Month + ival.Week += add.Week + ival.Day += add.Day + ival.Hour += add.Hour + ival.Min += add.Min + ival.Sec += add.Sec + ival.Nsec += add.Nsec + + return ival +} + +// Sub creates a new Interval as subtraction of intervals. +func (ival Interval) Sub(sub Interval) Interval { + ival.Year -= sub.Year + ival.Month -= sub.Month + ival.Week -= sub.Week + ival.Day -= sub.Day + ival.Hour -= sub.Hour + ival.Min -= sub.Min + ival.Sec -= sub.Sec + ival.Nsec -= sub.Nsec + + return ival +} + +func encodeIntervalValue(e *encoder, typ uint64, value int64) (err error) { + if value == 0 { + return + } + err = encodeUint(e, typ) + if err == nil { + if value > 0 { + err = encodeUint(e, uint64(value)) + } else if value < 0 { + err = encodeInt(e, int64(value)) + } + } + return +} + +func encodeInterval(e *encoder, v reflect.Value) (err error) { + val := v.Interface().(Interval) + + var fieldNum uint64 + for _, val := range []int64{val.Year, val.Month, val.Week, val.Day, + val.Hour, val.Min, val.Sec, val.Nsec, + adjustToDt[val.Adjust]} { + if val != 0 { + fieldNum++ + } + } + if err = encodeUint(e, fieldNum); err != nil { + return + } + + if err = encodeIntervalValue(e, fieldYear, val.Year); err != nil { + return + } + if err = encodeIntervalValue(e, fieldMonth, val.Month); err != nil { + return + } + if err = encodeIntervalValue(e, fieldWeek, val.Week); err != nil { + return + } + if err = encodeIntervalValue(e, fieldDay, val.Day); err != nil { + return + } + if err = encodeIntervalValue(e, fieldHour, val.Hour); err != nil { + return + } + if err = encodeIntervalValue(e, fieldMin, val.Min); err != nil { + return + } + if err = encodeIntervalValue(e, fieldSec, val.Sec); err != nil { + return + } + if err = encodeIntervalValue(e, fieldNSec, val.Nsec); err != nil { + return + } + if err = encodeIntervalValue(e, fieldAdjust, adjustToDt[val.Adjust]); err != nil { + return + } + return nil +} + +func decodeInterval(d *decoder, v reflect.Value) (err error) { + var fieldNum uint + if fieldNum, err = d.DecodeUint(); err != nil { + return + } + + var val Interval + + hasAdjust := false + for i := 0; i < int(fieldNum); i++ { + var fieldType uint + if fieldType, err = d.DecodeUint(); err != nil { + return + } + var fieldVal int64 + if fieldVal, err = d.DecodeInt64(); err != nil { + return + } + switch fieldType { + case fieldYear: + val.Year = fieldVal + case fieldMonth: + val.Month = fieldVal + case fieldWeek: + val.Week = fieldVal + case fieldDay: + val.Day = fieldVal + case fieldHour: + val.Hour = fieldVal + case fieldMin: + val.Min = fieldVal + case fieldSec: + val.Sec = fieldVal + case fieldNSec: + val.Nsec = fieldVal + case fieldAdjust: + hasAdjust = true + if adjust, ok := dtToAdjust[fieldVal]; ok { + val.Adjust = adjust + } else { + return fmt.Errorf("unsupported Adjust: %d", fieldVal) + } + default: + return fmt.Errorf("unsupported interval field type: %d", fieldType) + } + } + + if !hasAdjust { + val.Adjust = dtToAdjust[0] + } + + v.Set(reflect.ValueOf(val)) + return nil +} diff --git a/datetime/interval_test.go b/datetime/interval_test.go new file mode 100644 index 000000000..2d1ad41f9 --- /dev/null +++ b/datetime/interval_test.go @@ -0,0 +1,132 @@ +package datetime_test + +import ( + "fmt" + "reflect" + "testing" + + . "github.com/tarantool/go-tarantool/datetime" + "github.com/tarantool/go-tarantool/test_helpers" +) + +func TestIntervalAdd(t *testing.T) { + orig := Interval{ + Year: 1, + Month: 2, + Week: 3, + Day: 4, + Hour: -5, + Min: 6, + Sec: -7, + Nsec: 8, + Adjust: LastAdjust, + } + cpyOrig := orig + add := Interval{ + Year: 2, + Month: 3, + Week: -4, + Day: 5, + Hour: -6, + Min: 7, + Sec: -8, + Nsec: 0, + Adjust: ExcessAdjust, + } + expected := Interval{ + Year: orig.Year + add.Year, + Month: orig.Month + add.Month, + Week: orig.Week + add.Week, + Day: orig.Day + add.Day, + Hour: orig.Hour + add.Hour, + Min: orig.Min + add.Min, + Sec: orig.Sec + add.Sec, + Nsec: orig.Nsec + add.Nsec, + Adjust: orig.Adjust, + } + + ival := orig.Add(add) + + if !reflect.DeepEqual(ival, expected) { + t.Fatalf("Unexpected %v, expected %v", ival, expected) + } + if !reflect.DeepEqual(cpyOrig, orig) { + t.Fatalf("Original value changed %v, expected %v", orig, cpyOrig) + } +} + +func TestIntervalSub(t *testing.T) { + orig := Interval{ + Year: 1, + Month: 2, + Week: 3, + Day: 4, + Hour: -5, + Min: 6, + Sec: -7, + Nsec: 8, + Adjust: LastAdjust, + } + cpyOrig := orig + sub := Interval{ + Year: 2, + Month: 3, + Week: -4, + Day: 5, + Hour: -6, + Min: 7, + Sec: -8, + Nsec: 0, + Adjust: ExcessAdjust, + } + expected := Interval{ + Year: orig.Year - sub.Year, + Month: orig.Month - sub.Month, + Week: orig.Week - sub.Week, + Day: orig.Day - sub.Day, + Hour: orig.Hour - sub.Hour, + Min: orig.Min - sub.Min, + Sec: orig.Sec - sub.Sec, + Nsec: orig.Nsec - sub.Nsec, + Adjust: orig.Adjust, + } + + ival := orig.Sub(sub) + + if !reflect.DeepEqual(ival, expected) { + t.Fatalf("Unexpected %v, expected %v", ival, expected) + } + if !reflect.DeepEqual(cpyOrig, orig) { + t.Fatalf("Original value changed %v, expected %v", orig, cpyOrig) + } +} + +func TestIntervalTarantoolEncoding(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + cases := []Interval{ + {}, + {1, 2, 3, 4, -5, 6, -7, 8, LastAdjust}, + {1, 2, 3, 4, -5, 6, -7, 8, ExcessAdjust}, + {1, 2, 3, 4, -5, 6, -7, 8, LastAdjust}, + {0, 2, 3, 4, -5, 0, -7, 8, LastAdjust}, + {0, 0, 3, 0, -5, 6, -7, 8, ExcessAdjust}, + {0, 0, 0, 4, 0, 0, 0, 8, LastAdjust}, + } + for _, tc := range cases { + t.Run(fmt.Sprintf("%v", tc), func(t *testing.T) { + resp, err := conn.Call17("call_interval_testdata", []interface{}{tc}) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + ret := resp.Data[0].(Interval) + if !reflect.DeepEqual(ret, tc) { + t.Fatalf("Unexpected response: %v, expected %v", ret, tc) + } + }) + } +} diff --git a/datetime/msgpack.go b/datetime/msgpack.go index 4f48f1d3e..b5bc0d7c5 100644 --- a/datetime/msgpack.go +++ b/datetime/msgpack.go @@ -4,9 +4,25 @@ package datetime import ( + "reflect" + "gopkg.in/vmihailenco/msgpack.v2" ) +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +func encodeUint(e *encoder, v uint64) error { + return e.EncodeUint(uint(v)) +} + +func encodeInt(e *encoder, v int64) error { + return e.EncodeInt(int(v)) +} + func init() { msgpack.RegisterExt(datetime_extId, &Datetime{}) + + msgpack.Register(reflect.TypeOf((*Interval)(nil)).Elem(), encodeInterval, decodeInterval) + msgpack.RegisterExt(interval_extId, (*Interval)(nil)) } diff --git a/datetime/msgpack_v5.go b/datetime/msgpack_v5.go index a69a81aa3..69285576e 100644 --- a/datetime/msgpack_v5.go +++ b/datetime/msgpack_v5.go @@ -4,9 +4,39 @@ package datetime import ( + "bytes" + "reflect" + "github.com/vmihailenco/msgpack/v5" ) +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +func encodeUint(e *encoder, v uint64) error { + return e.EncodeUint(v) +} + +func encodeInt(e *encoder, v int64) error { + return e.EncodeInt(v) +} + func init() { msgpack.RegisterExt(datetime_extId, (*Datetime)(nil)) + + msgpack.RegisterExtEncoder(interval_extId, Interval{}, + func(e *msgpack.Encoder, v reflect.Value) (ret []byte, err error) { + var b bytes.Buffer + + enc := msgpack.NewEncoder(&b) + if err = encodeInterval(enc, v); err == nil { + ret = b.Bytes() + } + + return + }) + msgpack.RegisterExtDecoder(interval_extId, Interval{}, + func(d *msgpack.Decoder, v reflect.Value, extLen int) error { + return decodeInterval(d, v) + }) } From e9486838e104a1ad84e790b4980aa9352e5c0620 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 4 Aug 2022 15:21:49 +0300 Subject: [PATCH 320/605] doc: fix markdown for the decimal subpackage The patch hides the implementation details of the Decimal type from documentation. It provides comments for Decimal.MarshalMsgpack and Decimal.UnmarshalMsgpack. Closes #201 --- CHANGELOG.md | 2 ++ decimal/bcd.go | 11 ++++++----- decimal/decimal.go | 19 +++++++++++-------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4b57c82c..6185dc883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +- Markdown of documentation for the decimal subpackage (#201) + ## [1.7.0] - 2022-08-02 ### Added diff --git a/decimal/bcd.go b/decimal/bcd.go index cf95ccf7c..2cdecb5f0 100644 --- a/decimal/bcd.go +++ b/decimal/bcd.go @@ -1,3 +1,5 @@ +package decimal + // Package decimal implements methods to encode and decode BCD. // // BCD (Binary-Coded Decimal) is a sequence of bytes representing decimal @@ -26,21 +28,20 @@ // // The decimal -12.34 will be encoded as 0xd6, 0x01, 0x02, 0x01, 0x23, 0x4d: // -// | MP_EXT (fixext 4) | MP_DECIMAL | scale | 1 | 2,3 | 4 (minus) | -// | 0xd6 | 0x01 | 0x02 | 0x01 | 0x23 | 0x4d | +// | MP_EXT (fixext 4) | MP_DECIMAL | scale | 1 | 2,3 | 4 (minus) | +// | 0xd6 | 0x01 | 0x02 | 0x01 | 0x23 | 0x4d | // // The decimal 0.000000000000000000000000000000000010 will be encoded as // 0xc7, 0x03, 0x01, 0x24, 0x01, 0x0c: // -// | MP_EXT (ext 8) | length | MP_DECIMAL | scale | 1 | 0 (plus) | -// | 0xc7 | 0x03 | 0x01 | 0x24 | 0x01 | 0x0c | +// | MP_EXT (ext 8) | length | MP_DECIMAL | scale | 1 | 0 (plus) | +// | 0xc7 | 0x03 | 0x01 | 0x24 | 0x01 | 0x0c | // // See also: // // * MessagePack extensions https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/ // // * An implementation in C language https://github.com/tarantool/decNumber/blob/master/decPacked.c -package decimal import ( "fmt" diff --git a/decimal/decimal.go b/decimal/decimal.go index b92391ac2..44e192efc 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -55,6 +55,7 @@ func NewDecimalFromString(src string) (result *Decimal, err error) { return } +// MarshalMsgpack serializes the Decimal into a MessagePack representation. func (decNum *Decimal) MarshalMsgpack() ([]byte, error) { one := decimal.NewFromInt(1) maxSupportedDecimal := decimal.New(1, DecimalPrecision).Sub(one) // 10^DecimalPrecision - 1 @@ -74,15 +75,17 @@ func (decNum *Decimal) MarshalMsgpack() ([]byte, error) { return bcdBuf, nil } -// Decimal values can be encoded to fixext MessagePack, where buffer -// has a fixed length encoded by first byte, and ext MessagePack, where -// buffer length is not fixed and encoded by a number in a separate -// field: -// -// +--------+-------------------+------------+===============+ -// | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal | -// +--------+-------------------+------------+===============+ +// UnmarshalMsgpack deserializes a Decimal value from a MessagePack +// representation. func (decNum *Decimal) UnmarshalMsgpack(b []byte) error { + // Decimal values can be encoded to fixext MessagePack, where buffer + // has a fixed length encoded by first byte, and ext MessagePack, where + // buffer length is not fixed and encoded by a number in a separate + // field: + // + // +--------+-------------------+------------+===============+ + // | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal | + // +--------+-------------------+------------+===============+ digits, err := decodeStringFromBCD(b) if err != nil { return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", b, err) From a8e03440f3c74ad6e593cc327e36ce9aba8f2a90 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 17 Aug 2022 13:45:19 +0300 Subject: [PATCH 321/605] Release 1.8.0 Overview The minor release with time zones and interval support for datetime. Also now you can use go-tarantool with `msgpack.v5`. To do this, add `go_tarantool_msgpack_v5` to your build tags: $ go build -tags=go_tarantool_msgpack_v5 . Breaking changes There are no breaking changes in the release. New features Optional msgpack.v5 usage (#124). TZ support for datetime (#163). Interval support for datetime (#165). Bugfixes Markdown of documentation for the decimal subpackage (#201). --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6185dc883..1177e4a8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +### Changed + +### Fixed + +## [1.8.0] - 2022-08-17 + +### Added + - Optional msgpack.v5 usage (#124) - TZ support for datetime (#163) - Interval support for datetime (#165) From 8f16c973da12e2697162acde4a3211e4a614ab7c Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 15 Aug 2022 17:04:23 +0300 Subject: [PATCH 322/605] code health: queue tests improvments Non-critical tests improvements for queue subpackage. It does not affect a logic of the tests: * A duplicate code that creates a queue is extracted into a separate function. * A duplicate code that drops a queue is extraced into a separate function. * `t.Errorf() + return` replaced by `t.Fatalf()`. * `err.Error()` replaced by just `err` in formatted messages. --- queue/queue_test.go | 328 +++++++++++++------------------------------- 1 file changed, 94 insertions(+), 234 deletions(-) diff --git a/queue/queue_test.go b/queue/queue_test.go index 807ac79e0..c60ddcd57 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -22,6 +22,25 @@ var opts = Opts{ //RateLimit: 4*1024, } +func createQueue(t *testing.T, conn *Connection, name string, cfg queue.Cfg) queue.Queue { + t.Helper() + + q := queue.New(conn, name) + if err := q.Create(cfg); err != nil { + t.Fatalf("Failed to create queue: %s", err) + } + + return q +} + +func dropQueue(t *testing.T, q queue.Queue) { + t.Helper() + + if err := q.Drop(); err != nil { + t.Fatalf("Failed to drop queue: %s", err) + } +} + /////////QUEUE///////// func TestFifoQueue(t *testing.T) { @@ -29,16 +48,8 @@ func TestFifoQueue(t *testing.T) { defer conn.Close() name := "test_queue" - q := queue.New(conn, name) - if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - //Drop - if err := q.Drop(); err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } + q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + defer dropQueue(t, q) } func TestFifoQueue_GetExist_Statistic(t *testing.T) { @@ -46,39 +57,26 @@ func TestFifoQueue_GetExist_Statistic(t *testing.T) { defer conn.Close() name := "test_queue" - q := queue.New(conn, name) - if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() + q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + defer dropQueue(t, q) ok, err := q.Exists() if err != nil { - t.Errorf("Failed to get exist queue: %s", err.Error()) - return + t.Fatalf("Failed to get exist queue: %s", err) } if !ok { - t.Error("Queue is not found") - return + t.Fatal("Queue is not found") } putData := "put_data" _, err = q.Put(putData) if err != nil { - t.Errorf("Failed to put queue: %s", err.Error()) - return + t.Fatalf("Failed to put queue: %s", err) } stat, err := q.Statistic() if err != nil { - t.Errorf("Failed to get statistic queue: %s", err.Error()) + t.Errorf("Failed to get statistic queue: %s", err) } else if stat == nil { t.Error("Statistic is nil") } @@ -89,29 +87,16 @@ func TestFifoQueue_Put(t *testing.T) { defer conn.Close() name := "test_queue" - q := queue.New(conn, name) - if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() + q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + defer dropQueue(t, q) //Put putData := "put_data" task, err := q.Put(putData) if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return + t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return + t.Fatalf("Task is nil after put") } else { if task.Data() != putData { t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) @@ -124,29 +109,16 @@ func TestFifoQueue_Take(t *testing.T) { defer conn.Close() name := "test_queue" - q := queue.New(conn, name) - if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() + q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + defer dropQueue(t, q) //Put putData := "put_data" task, err := q.Put(putData) if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return + t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return + t.Fatalf("Task is nil after put") } else { if task.Data() != putData { t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) @@ -156,7 +128,7 @@ func TestFifoQueue_Take(t *testing.T) { //Take task, err = q.TakeTimeout(2 * time.Second) if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) + t.Errorf("Failed take from queue: %s", err) } else if task == nil { t.Errorf("Task is nil after take") } else { @@ -171,7 +143,7 @@ func TestFifoQueue_Take(t *testing.T) { err = task.Ack() if err != nil { - t.Errorf("Failed ack %s", err.Error()) + t.Errorf("Failed ack %s", err) } else if !task.IsDone() { t.Errorf("Task status after take is not done. Status = %s", task.Status()) } @@ -212,29 +184,16 @@ func TestFifoQueue_TakeTyped(t *testing.T) { defer conn.Close() name := "test_queue" - q := queue.New(conn, name) - if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() + q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + defer dropQueue(t, q) //Put putData := &customData{customField: "put_data"} task, err := q.Put(putData) if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return + t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return + t.Fatalf("Task is nil after put") } else { typedData, ok := task.Data().(*customData) if !ok { @@ -249,7 +208,7 @@ func TestFifoQueue_TakeTyped(t *testing.T) { takeData := &customData{} task, err = q.TakeTypedTimeout(2*time.Second, takeData) if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) + t.Errorf("Failed take from queue: %s", err) } else if task == nil { t.Errorf("Task is nil after take") } else { @@ -269,7 +228,7 @@ func TestFifoQueue_TakeTyped(t *testing.T) { err = task.Ack() if err != nil { - t.Errorf("Failed ack %s", err.Error()) + t.Errorf("Failed ack %s", err) } else if !task.IsDone() { t.Errorf("Task status after take is not done. Status = %s", task.Status()) } @@ -281,29 +240,16 @@ func TestFifoQueue_Peek(t *testing.T) { defer conn.Close() name := "test_queue" - q := queue.New(conn, name) - if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() + q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + defer dropQueue(t, q) //Put putData := "put_data" task, err := q.Put(putData) if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return + t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return + t.Fatalf("Task is nil after put") } else { if task.Data() != putData { t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) @@ -313,7 +259,7 @@ func TestFifoQueue_Peek(t *testing.T) { //Peek task, err = q.Peek(task.Id()) if err != nil { - t.Errorf("Failed peek from queue: %s", err.Error()) + t.Errorf("Failed peek from queue: %s", err) } else if task == nil { t.Errorf("Task is nil after peek") } else { @@ -332,29 +278,16 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { defer conn.Close() name := "test_queue" - q := queue.New(conn, name) - if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() + q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + defer dropQueue(t, q) //Put putData := "put_data" task, err := q.Put(putData) if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return + t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return + t.Fatalf("Task is nil after put") } else { if task.Data() != putData { t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) @@ -364,8 +297,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { //Bury err = task.Bury() if err != nil { - t.Errorf("Failed bury task %s", err.Error()) - return + t.Fatalf("Failed bury task %s", err) } else if !task.IsBuried() { t.Errorf("Task status after bury is not buried. Status = %s", task.Status()) } @@ -373,17 +305,15 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { //Kick count, err := q.Kick(1) if err != nil { - t.Errorf("Failed kick task %s", err.Error()) - return + t.Fatalf("Failed kick task %s", err) } else if count != 1 { - t.Errorf("Kick result != 1") - return + t.Fatalf("Kick result != 1") } //Take task, err = q.TakeTimeout(2 * time.Second) if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) + t.Errorf("Failed take from queue: %s", err) } else if task == nil { t.Errorf("Task is nil after take") } else { @@ -397,7 +327,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { err = task.Ack() if err != nil { - t.Errorf("Failed ack %s", err.Error()) + t.Errorf("Failed ack %s", err) } else if !task.IsDone() { t.Errorf("Task status after take is not done. Status = %s", task.Status()) } @@ -411,19 +341,8 @@ func TestFifoQueue_Delete(t *testing.T) { defer conn.Close() name := "test_queue" - q := queue.New(conn, name) - if err = q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() + q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + defer dropQueue(t, q) //Put var putData = "put_data" @@ -432,11 +351,9 @@ func TestFifoQueue_Delete(t *testing.T) { for i := 0; i < 2; i++ { tasks[i], err = q.Put(putData) if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return + t.Fatalf("Failed put to queue: %s", err) } else if err == nil && tasks[i] == nil { - t.Errorf("Task is nil after put") - return + t.Fatalf("Task is nil after put") } else { if tasks[i].Data() != putData { t.Errorf("Task data after put not equal with example. %s != %s", tasks[i].Data(), putData) @@ -447,8 +364,7 @@ func TestFifoQueue_Delete(t *testing.T) { //Delete by task method err = tasks[0].Delete() if err != nil { - t.Errorf("Failed bury task %s", err.Error()) - return + t.Fatalf("Failed bury task %s", err) } else if !tasks[0].IsDone() { t.Errorf("Task status after delete is not done. Status = %s", tasks[0].Status()) } @@ -456,8 +372,7 @@ func TestFifoQueue_Delete(t *testing.T) { //Delete by task ID err = q.Delete(tasks[1].Id()) if err != nil { - t.Errorf("Failed bury task %s", err.Error()) - return + t.Fatalf("Failed bury task %s", err) } else if !tasks[0].IsDone() { t.Errorf("Task status after delete is not done. Status = %s", tasks[0].Status()) } @@ -466,7 +381,7 @@ func TestFifoQueue_Delete(t *testing.T) { for i := 0; i < 2; i++ { tasks[i], err = q.TakeTimeout(2 * time.Second) if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) + t.Errorf("Failed take from queue: %s", err) } else if tasks[i] != nil { t.Errorf("Task is not nil after take. Task is %d", tasks[i].Id()) } @@ -478,28 +393,15 @@ func TestFifoQueue_Release(t *testing.T) { defer conn.Close() name := "test_queue" - q := queue.New(conn, name) - if err := q.Create(queue.Cfg{Temporary: true, Kind: queue.FIFO}); err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() + q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + defer dropQueue(t, q) putData := "put_data" task, err := q.Put(putData) if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return + t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return + t.Fatalf("Task is nil after put") } else { if task.Data() != putData { t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) @@ -509,33 +411,27 @@ func TestFifoQueue_Release(t *testing.T) { //Take task, err = q.Take() if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) - return + t.Fatalf("Failed take from queue: %s", err) } else if task == nil { - t.Error("Task is nil after take") - return + t.Fatal("Task is nil after take") } //Release err = task.Release() if err != nil { - t.Errorf("Failed release task %s", err.Error()) - return + t.Fatalf("Failed release task %s", err) } if !task.IsReady() { - t.Errorf("Task status is not ready, but %s", task.Status()) - return + t.Fatalf("Task status is not ready, but %s", task.Status()) } //Take task, err = q.Take() if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) - return + t.Fatalf("Failed take from queue: %s", err) } else if task == nil { - t.Error("Task is nil after take") - return + t.Fatal("Task is nil after take") } else { if task.Data() != putData { t.Errorf("Task data after take not equal with example. %s != %s", task.Data(), putData) @@ -547,7 +443,7 @@ func TestFifoQueue_Release(t *testing.T) { err = task.Ack() if err != nil { - t.Errorf("Failed ack %s", err.Error()) + t.Errorf("Failed ack %s", err) } else if !task.IsDone() { t.Errorf("Task status after take is not done. Status = %s", task.Status()) } @@ -564,28 +460,15 @@ func TestTtlQueue(t *testing.T) { Kind: queue.FIFO_TTL, Opts: queue.Opts{Ttl: 5 * time.Second}, } - q := queue.New(conn, name) - if err := q.Create(cfg); err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() + q := createQueue(t, conn, name, cfg) + defer dropQueue(t, q) putData := "put_data" task, err := q.Put(putData) if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return + t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return + t.Fatalf("Task is nil after put") } else { if task.Data() != putData { t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) @@ -597,7 +480,7 @@ func TestTtlQueue(t *testing.T) { //Take task, err = q.TakeTimeout(2 * time.Second) if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) + t.Errorf("Failed take from queue: %s", err) } else if task != nil { t.Errorf("Task is not nil after sleep") } @@ -613,28 +496,15 @@ func TestTtlQueue_Put(t *testing.T) { Kind: queue.FIFO_TTL, Opts: queue.Opts{Ttl: 5 * time.Second}, } - q := queue.New(conn, name) - if err := q.Create(cfg); err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() + q := createQueue(t, conn, name, cfg) + defer dropQueue(t, q) putData := "put_data" task, err := q.PutWithOpts(putData, queue.Opts{Ttl: 10 * time.Second}) if err != nil { - t.Errorf("Failed put to queue: %s", err.Error()) - return + t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { - t.Errorf("Task is nil after put") - return + t.Fatalf("Task is nil after put") } else { if task.Data() != putData { t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) @@ -646,7 +516,7 @@ func TestTtlQueue_Put(t *testing.T) { //Take task, err = q.TakeTimeout(2 * time.Second) if err != nil { - t.Errorf("Failed take from queue: %s", err.Error()) + t.Errorf("Failed take from queue: %s", err) } else if task == nil { t.Errorf("Task is nil after sleep") } else { @@ -660,7 +530,7 @@ func TestTtlQueue_Put(t *testing.T) { err = task.Ack() if err != nil { - t.Errorf("Failed ack %s", err.Error()) + t.Errorf("Failed ack %s", err) } else if !task.IsDone() { t.Errorf("Task status after take is not done. Status = %s", task.Status()) } @@ -677,42 +547,32 @@ func TestUtube_Put(t *testing.T) { Kind: queue.UTUBE, IfNotExists: true, } - q := queue.New(conn, name) - if err := q.Create(cfg); err != nil { - t.Errorf("Failed to create queue: %s", err.Error()) - return - } - defer func() { - //Drop - err := q.Drop() - if err != nil { - t.Errorf("Failed drop queue: %s", err.Error()) - } - }() + q := createQueue(t, conn, name, cfg) + defer dropQueue(t, q) data1 := &customData{"test-data-0"} _, err := q.PutWithOpts(data1, queue.Opts{Utube: "test-utube-consumer-key"}) if err != nil { - t.Fatalf("Failed put task to queue: %s", err.Error()) + t.Fatalf("Failed put task to queue: %s", err) } data2 := &customData{"test-data-1"} _, err = q.PutWithOpts(data2, queue.Opts{Utube: "test-utube-consumer-key"}) if err != nil { - t.Fatalf("Failed put task to queue: %s", err.Error()) + t.Fatalf("Failed put task to queue: %s", err) } errChan := make(chan struct{}) go func() { t1, err := q.TakeTimeout(2 * time.Second) if err != nil { - t.Errorf("Failed to take task from utube: %s", err.Error()) + t.Errorf("Failed to take task from utube: %s", err) errChan <- struct{}{} return } time.Sleep(2 * time.Second) if err := t1.Ack(); err != nil { - t.Errorf("Failed to ack task: %s", err.Error()) + t.Errorf("Failed to ack task: %s", err) errChan <- struct{}{} return } @@ -724,10 +584,10 @@ func TestUtube_Put(t *testing.T) { start := time.Now() t2, err := q.TakeTimeout(2 * time.Second) if err != nil { - t.Fatalf("Failed to take task from utube: %s", err.Error()) + t.Fatalf("Failed to take task from utube: %s", err) } if err := t2.Ack(); err != nil { - t.Fatalf("Failed to ack task: %s", err.Error()) + t.Fatalf("Failed to ack task: %s", err) } end := time.Now() if _, ok := <-errChan; ok { From bda4442829806765d0c7e8d7d2e6302bea94b470 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 16 Aug 2022 11:38:18 +0300 Subject: [PATCH 323/605] api: support queue 1.2.0 * bump queue package version to 1.2.0 [1] * add Task.Touch(): increases TTR and/or TTL for tasks [2] * add Queue.Cfg(): set queue settings [3] * add Queue.ReleaseAll(): releases all taken tasks [4] * add Queue.State(): returns a current queue state [5] * add Queue.Identify(): identifies a shared session [6] 1. https://github.com/tarantool/queue/releases/tag/1.2.0 2. https://github.com/tarantool/queue/blob/1.2.0/README.md?plain=1#L562-L576 3. https://github.com/tarantool/queue/blob/1.2.0/README.md?plain=1#L450-L463 4. https://github.com/tarantool/queue/blob/1.2.0/README.md?plain=1#L698-L704 5. https://github.com/tarantool/queue/blob/1.2.0/README.md?plain=1#L377-L391 6. https://github.com/tarantool/queue/blob/1.2.0/README.md?plain=1#L465-L494 Closes #110 Closes #177 --- CHANGELOG.md | 2 + Makefile | 2 +- queue/config.lua | 5 + queue/const.go | 19 +++ queue/queue.go | 105 ++++++++++++++++ queue/queue_test.go | 285 ++++++++++++++++++++++++++++++++++++++++++++ queue/task.go | 6 + 7 files changed, 423 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1177e4a8e..11b0e54e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +- Support queue 1.2.0 (#177) + ### Changed ### Fixed diff --git a/Makefile b/Makefile index 418b3c89f..7149e05ed 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ clean: .PHONY: deps deps: clean - ( cd ./queue; tarantoolctl rocks install queue 1.1.0 ) + ( cd ./queue; tarantoolctl rocks install queue 1.2.0 ) .PHONY: datetime-timezones datetime-timezones: diff --git a/queue/config.lua b/queue/config.lua index cb64f4df8..df28496a3 100644 --- a/queue/config.lua +++ b/queue/config.lua @@ -9,6 +9,7 @@ box.cfg{ box.once("init", function() box.schema.user.create('test', {password = 'test'}) + box.schema.func.create('queue.tube.test_queue:touch') box.schema.func.create('queue.tube.test_queue:ack') box.schema.func.create('queue.tube.test_queue:put') box.schema.func.create('queue.tube.test_queue:drop') @@ -17,7 +18,10 @@ box.cfg{ box.schema.func.create('queue.tube.test_queue:take') box.schema.func.create('queue.tube.test_queue:delete') box.schema.func.create('queue.tube.test_queue:release') + box.schema.func.create('queue.tube.test_queue:release_all') box.schema.func.create('queue.tube.test_queue:bury') + box.schema.func.create('queue.identify') + box.schema.func.create('queue.state') box.schema.func.create('queue.statistics') box.schema.user.grant('test', 'create', 'space') box.schema.user.grant('test', 'write', 'space', '_schema') @@ -33,6 +37,7 @@ box.cfg{ box.schema.user.grant('test', 'read,write', 'space', '_queue_consumers') box.schema.user.grant('test', 'read,write', 'space', '_priv') box.schema.user.grant('test', 'read,write', 'space', '_queue_taken_2') + box.schema.user.grant('test', 'read,write', 'space', '_queue_shared_sessions') if box.space._trigger ~= nil then box.schema.user.grant('test', 'read', 'space', '_trigger') end diff --git a/queue/const.go b/queue/const.go index f984fac62..0e0eadcc7 100644 --- a/queue/const.go +++ b/queue/const.go @@ -16,3 +16,22 @@ const ( UTUBE queueType = "utube" UTUBE_TTL queueType = "utubettl" ) + +type State int + +const ( + UnknownState State = iota + InitState + StartupState + RunningState + EndingState + WaitingState +) + +var strToState = map[string]State{ + "INIT": InitState, + "STARTUP": StartupState, + "RUNNING": RunningState, + "ENDING": EndingState, + "WAITING": WaitingState, +} diff --git a/queue/queue.go b/queue/queue.go index c40b8a0df..cbe80e057 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -12,14 +12,23 @@ import ( "fmt" "time" + "github.com/google/uuid" "github.com/tarantool/go-tarantool" ) // Queue is a handle to Tarantool queue's tube. type Queue interface { + // Set queue settings. + Cfg(opts CfgOpts) error // Exists checks tube for existence. // Note: it uses Eval, so user needs 'execute universe' privilege. Exists() (bool, error) + // Identify to a shared session. + // In the queue the session has a unique UUID and many connections may + // share one logical session. Also, the consumer can reconnect to the + // existing session during the ttr time. + // To get the UUID of the current session, call the Queue.Identify(nil). + Identify(u *uuid.UUID) (uuid.UUID, error) // Create creates new tube with configuration. // Note: it uses Eval, so user needs 'execute universe' privilege // Note: you'd better not use this function in your application, cause it is @@ -29,6 +38,8 @@ type Queue interface { // Note: you'd better not use this function in your application, cause it is // administrative task to create or delete queue. Drop() error + // ReleaseAll forcibly returns all taken tasks to a ready state. + ReleaseAll() error // Put creates new task in a tube. Put(data interface{}) (*Task, error) // PutWithOpts creates new task with options different from tube's defaults. @@ -64,6 +75,8 @@ type Queue interface { Kick(count uint64) (uint64, error) // Delete the task identified by its id. Delete(taskId uint64) error + // State returns a current queue state. + State() (State, error) // Statistic returns some statistic about queue. Statistic() (interface{}, error) } @@ -79,11 +92,16 @@ type cmd struct { take string drop string peek string + touch string ack string delete string bury string kick string release string + releaseAll string + cfg string + identify string + state string statistics string } @@ -110,6 +128,26 @@ func (cfg Cfg) getType() string { return kind } +// CfgOpts is argument type for the Queue.Cfg() call. +type CfgOpts struct { + // Enable replication mode. Must be true if the queue is used in master and + // replica mode. With replication mode enabled, the potential loss of + // performance can be ~20% compared to single mode. Default value is false. + InReplicaset bool + // Time to release in seconds. The time after which, if there is no active + // connection in the session, it will be released with all its tasks. + Ttr time.Duration +} + +func (opts CfgOpts) toMap() map[string]interface{} { + ret := make(map[string]interface{}) + ret["in_replicaset"] = opts.InReplicaset + if opts.Ttr != 0 { + ret["ttr"] = opts.Ttr + } + return ret +} + type Opts struct { Pri int // Task priorities. Ttl time.Duration // Task time to live. @@ -161,6 +199,12 @@ func (q *queue) Create(cfg Cfg) error { return err } +// Set queue settings. +func (q *queue) Cfg(opts CfgOpts) error { + _, err := q.conn.Call17(q.cmds.cfg, []interface{}{opts.toMap()}) + return err +} + // Exists checks existance of a tube. func (q *queue) Exists() (bool, error) { cmd := "local name = ... ; return queue.tube[name] ~= nil" @@ -173,6 +217,36 @@ func (q *queue) Exists() (bool, error) { return exist, nil } +// Identify to a shared session. +// In the queue the session has a unique UUID and many connections may share +// one logical session. Also, the consumer can reconnect to the existing +// session during the ttr time. +// To get the UUID of the current session, call the Queue.Identify(nil). +func (q *queue) Identify(u *uuid.UUID) (uuid.UUID, error) { + // Unfortunately we can't use go-tarantool/uuid here: + // https://github.com/tarantool/queue/issues/182 + var args []interface{} + if u == nil { + args = []interface{}{} + } else { + if bytes, err := u.MarshalBinary(); err != nil { + return uuid.UUID{}, err + } else { + args = []interface{}{bytes} + } + } + + if resp, err := q.conn.Call17(q.cmds.identify, args); err == nil { + if us, ok := resp.Data[0].(string); ok { + return uuid.FromBytes([]byte(us)) + } else { + return uuid.UUID{}, fmt.Errorf("unexpected response: %v", resp.Data) + } + } else { + return uuid.UUID{}, err + } +} + // Put data to queue. Returns task. func (q *queue) Put(data interface{}) (*Task, error) { return q.put(data) @@ -251,6 +325,12 @@ func (q *queue) Drop() error { return err } +// ReleaseAll forcibly returns all taken tasks to a ready state. +func (q *queue) ReleaseAll() error { + _, err := q.conn.Call17(q.cmds.releaseAll, []interface{}{}) + return err +} + // Look at a task without changing its state. func (q *queue) Peek(taskId uint64) (*Task, error) { qd := queueData{q: q} @@ -260,6 +340,10 @@ func (q *queue) Peek(taskId uint64) (*Task, error) { return qd.task, nil } +func (q *queue) _touch(taskId uint64, increment time.Duration) (string, error) { + return q.produce(q.cmds.touch, taskId, increment.Seconds()) +} + func (q *queue) _ack(taskId uint64) (string, error) { return q.produce(q.cmds.ack, taskId) } @@ -312,6 +396,22 @@ func (q *queue) Delete(taskId uint64) error { return err } +// State returns a current queue state. +func (q *queue) State() (State, error) { + resp, err := q.conn.Call17(q.cmds.state, []interface{}{}) + if err != nil { + return UnknownState, err + } + + if respState, ok := resp.Data[0].(string); ok { + if state, ok := strToState[respState]; ok { + return state, nil + } + return UnknownState, fmt.Errorf("unknown state: %v", resp.Data[0]) + } + return UnknownState, fmt.Errorf("unexpected response: %v", resp.Data) +} + // Return the number of tasks in a queue broken down by task_state, and the // number of requests broken down by the type of request. func (q *queue) Statistic() (interface{}, error) { @@ -333,11 +433,16 @@ func makeCmd(q *queue) { take: "queue.tube." + q.name + ":take", drop: "queue.tube." + q.name + ":drop", peek: "queue.tube." + q.name + ":peek", + touch: "queue.tube." + q.name + ":touch", ack: "queue.tube." + q.name + ":ack", delete: "queue.tube." + q.name + ":delete", bury: "queue.tube." + q.name + ":bury", kick: "queue.tube." + q.name + ":kick", release: "queue.tube." + q.name + ":release", + releaseAll: "queue.tube." + q.name + ":release_all", + cfg: "queue.cfg", + identify: "queue.identify", + state: "queue.state", statistics: "queue.statistics", } } diff --git a/queue/queue_test.go b/queue/queue_test.go index c60ddcd57..ef59156da 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -52,6 +52,145 @@ func TestFifoQueue(t *testing.T) { defer dropQueue(t, q) } +func TestQueue_Cfg(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + name := "test_queue" + q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + defer dropQueue(t, q) + + err := q.Cfg(queue.CfgOpts{InReplicaset: false, Ttr: 5 * time.Second}) + if err != nil { + t.Fatalf("Unexpected q.Cfg() error: %s", err) + } +} + +func TestQueue_Identify(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + name := "test_queue" + q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + defer dropQueue(t, q) + + uuid, err := q.Identify(nil) + if err != nil { + t.Fatalf("Failed to identify: %s", err) + } + cpy := uuid + + uuid, err = q.Identify(&cpy) + if err != nil { + t.Fatalf("Failed to identify with uuid %s: %s", cpy, err) + } + if cpy.String() != uuid.String() { + t.Fatalf("Unequal UUIDs after re-identify: %s, expected %s", uuid, cpy) + } +} + +func TestQueue_ReIdentify(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer func() { + if conn != nil { + conn.Close() + } + }() + + name := "test_queue" + cfg := queue.Cfg{ + Temporary: true, + Kind: queue.FIFO_TTL, + Opts: queue.Opts{Ttl: 5 * time.Second}, + } + q := createQueue(t, conn, name, cfg) + q.Cfg(queue.CfgOpts{InReplicaset: false, Ttr: 5 * time.Second}) + defer func() { + dropQueue(t, q) + }() + + uuid, err := q.Identify(nil) + if err != nil { + t.Fatalf("Failed to identify: %s", err) + } + newuuid, err := q.Identify(&uuid) + if err != nil { + t.Fatalf("Failed to identify: %s", err) + } + if newuuid.String() != uuid.String() { + t.Fatalf("Unequal UUIDs after re-identify: %s, expected %s", newuuid, uuid) + } + //Put + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + conn.Close() + t.Fatalf("Failed put to queue: %s", err) + } else if err == nil && task == nil { + t.Fatalf("Task is nil after put") + } else { + if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) + } + } + + //Take + task, err = q.TakeTimeout(2 * time.Second) + if err != nil { + t.Fatalf("Failed take from queue: %s", err) + } else if task == nil { + t.Fatalf("Task is nil after take") + } + + conn.Close() + conn = nil + + conn = test_helpers.ConnectWithValidation(t, server, opts) + q = queue.New(conn, name) + + //Identify in another connection + newuuid, err = q.Identify(&uuid) + if err != nil { + t.Fatalf("Failed to identify: %s", err) + } + if newuuid.String() != uuid.String() { + t.Fatalf("Unequal UUIDs after re-identify: %s, expected %s", newuuid, uuid) + } + + //Peek in another connection + task, err = q.Peek(task.Id()) + if err != nil { + t.Fatalf("Failed take from queue: %s", err) + } else if task == nil { + t.Fatalf("Task is nil after take") + } + + //Ack in another connection + err = task.Ack() + if err != nil { + t.Errorf("Failed ack %s", err) + } else if !task.IsDone() { + t.Errorf("Task status after take is not done. Status = %s", task.Status()) + } +} + +func TestQueue_State(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + name := "test_queue" + q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + defer dropQueue(t, q) + + state, err := q.State() + if err != nil { + t.Fatalf("Failed to get queue state: %s", err) + } + if state != queue.InitState && state != queue.RunningState { + t.Fatalf("Unexpected state: %d", state) + } +} + func TestFifoQueue_GetExist_Statistic(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() @@ -450,6 +589,72 @@ func TestFifoQueue_Release(t *testing.T) { } } +func TestQueue_ReleaseAll(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + name := "test_queue" + q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) + defer dropQueue(t, q) + + putData := "put_data" + task, err := q.Put(putData) + if err != nil { + t.Fatalf("Failed put to queue: %s", err) + } else if err == nil && task == nil { + t.Fatalf("Task is nil after put") + } else { + if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) + } + } + + //Take + task, err = q.Take() + if err != nil { + t.Fatalf("Failed take from queue: %s", err) + } else if task == nil { + t.Fatal("Task is nil after take") + } + + //ReleaseAll + err = q.ReleaseAll() + if err != nil { + t.Fatalf("Failed release task %s", err) + } + + task, err = q.Peek(task.Id()) + if err != nil { + t.Fatalf("Failed to peek task %s", err) + } + if !task.IsReady() { + t.Fatalf("Task status is not ready, but %s", task.Status()) + } + + //Take + task, err = q.Take() + if err != nil { + t.Fatalf("Failed take from queue: %s", err) + } else if task == nil { + t.Fatal("Task is nil after take") + } else { + if task.Data() != putData { + t.Errorf("Task data after take not equal with example. %s != %s", task.Data(), putData) + } + + if !task.IsTaken() { + t.Errorf("Task status after take is not taken. Status = %s", task.Status()) + } + + err = task.Ack() + if err != nil { + t.Errorf("Failed ack %s", err) + } else if !task.IsDone() { + t.Errorf("Task status after take is not done. Status = %s", task.Status()) + } + } +} + func TestTtlQueue(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() @@ -598,6 +803,86 @@ func TestUtube_Put(t *testing.T) { } } +func TestTask_Touch(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + tests := []struct { + name string + cfg queue.Cfg + ok bool + }{ + {"test_queue", + queue.Cfg{ + Temporary: true, + Kind: queue.FIFO, + }, + false, + }, + {"test_queue_ttl", + queue.Cfg{ + Temporary: true, + Kind: queue.FIFO_TTL, + Opts: queue.Opts{Ttl: 5 * time.Second}, + }, + true, + }, + {"test_utube", + queue.Cfg{ + Temporary: true, + Kind: queue.UTUBE, + }, + false, + }, + {"test_utube_ttl", + queue.Cfg{ + Temporary: true, + Kind: queue.UTUBE_TTL, + Opts: queue.Opts{Ttl: 5 * time.Second}, + }, + true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var task *queue.Task + + q := createQueue(t, conn, tc.name, tc.cfg) + defer func() { + if task != nil { + if err := task.Ack(); err != nil { + t.Fatalf("Failed to Ack: %s", err) + } + } + dropQueue(t, q) + }() + + putData := "put_data" + _, err := q.PutWithOpts(putData, + queue.Opts{ + Ttl: 10 * time.Second, + Utube: "test_utube", + }) + if err != nil { + t.Fatalf("Failed put a task: %s", err) + } + + task, err = q.TakeTimeout(2 * time.Second) + if err != nil { + t.Fatalf("Failed to take task from utube: %s", err) + } + + err = task.Touch(1 * time.Second) + if tc.ok && err != nil { + t.Fatalf("Failed to touch: %s", err) + } else if !tc.ok && err == nil { + t.Fatalf("Unexpected success") + } + }) + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/queue/task.go b/queue/task.go index 2f4242311..01ab26da6 100644 --- a/queue/task.go +++ b/queue/task.go @@ -2,6 +2,7 @@ package queue import ( "fmt" + "time" ) // Task represents a task from Tarantool queue's tube. @@ -54,6 +55,11 @@ func (t *Task) Status() string { return t.status } +// Touch increases ttr of running task. +func (t *Task) Touch(increment time.Duration) error { + return t.accept(t.q._touch(t.id, increment)) +} + // Ack signals about task completion. func (t *Task) Ack() error { return t.accept(t.q._ack(t.id)) From de95f01e83ea0b752a6d8114b97867a2b7b948c1 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 18 Aug 2022 09:10:51 +0300 Subject: [PATCH 324/605] doc: fix connection_pool Mode description The patch fixes PREFER_RW/PREFER_RO usage. It also make comments in Go-style. See `Const` section in `Go Doc Comments` guide [1]. 1. https://go.dev/doc/comment Part of #208 --- CHANGELOG.md | 2 ++ connection_pool/const.go | 32 ++++++++++++-------------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b0e54e9..fd2b8876b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +- Mode type description in the connection_pool subpackage (#208) + ## [1.8.0] - 2022-08-17 ### Added diff --git a/connection_pool/const.go b/connection_pool/const.go index 334806437..c93d4ee3e 100644 --- a/connection_pool/const.go +++ b/connection_pool/const.go @@ -1,21 +1,7 @@ package connection_pool -type Mode uint32 -type Role uint32 -type State uint32 - /* -Mode parameter: - -- ANY (use any instance) - the request can be executed on any instance (master or replica). - -- RW (writeable instance (master)) - the request can only be executed on master. - -- RO (read only instance (replica)) - the request can only be executed on replica. - -- PREFER_RO (prefer read only instance (replica)) - if there is one, otherwise fallback to a writeable one (master). - -- PREFER_RW (prefer write only instance (master)) - if there is one, otherwise fallback to a read only one (replica). +Default mode for each request table: Request Default mode ---------- -------------- @@ -30,14 +16,18 @@ Mode parameter: | select | ANY | | get | ANY | */ +type Mode uint32 + const ( - ANY = iota - RW - RO - PreferRW - PreferRO + ANY Mode = iota // The request can be executed on any instance (master or replica). + RW // The request can only be executed on master. + RO // The request can only be executed on replica. + PreferRW // If there is one, otherwise fallback to a writeable one (master). + PreferRO // If there is one, otherwise fallback to a read only one (replica). ) +type Role uint32 + // master/replica role const ( unknown = iota @@ -45,6 +35,8 @@ const ( replica ) +type State uint32 + // pool state const ( connConnected = iota From 68356908bc172a0bc74fbd302da107ceb18f3ba6 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 18 Aug 2022 13:10:48 +0300 Subject: [PATCH 325/605] code health: move NewPrepared to public code block Part of #208 --- connection_pool/connection_pool.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 1b080fc1f..fb46c8101 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -558,6 +558,15 @@ func (connPool *ConnectionPool) NewStream(userMode Mode) (*tarantool.Stream, err return conn.NewStream() } +// NewPrepared passes a sql statement to Tarantool for preparation synchronously. +func (connPool *ConnectionPool) NewPrepared(expr string, userMode Mode) (*tarantool.Prepared, error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return nil, err + } + return conn.NewPrepared(expr) +} + // // private // @@ -812,12 +821,3 @@ func newErrorFuture(err error) *tarantool.Future { fut.SetError(err) return fut } - -// NewPrepared passes a sql statement to Tarantool for preparation synchronously. -func (connPool *ConnectionPool) NewPrepared(expr string, userMode Mode) (*tarantool.Prepared, error) { - conn, err := connPool.getNextConnection(userMode) - if err != nil { - return nil, err - } - return conn.NewPrepared(expr) -} From 22dfe175de6270903b9f0a0891853b2c05e63eab Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 16 Aug 2022 17:30:18 +0300 Subject: [PATCH 326/605] code health: remove duplicate box.info check It's enough to get `box.info` table once in ConnectionPool.getConnectionRole(). Part of #208 --- connection_pool/connection_pool.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index fb46c8101..d3b7b5d1b 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -591,17 +591,6 @@ func (connPool *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (R return unknown, ErrIncorrectStatus } - resp, err = conn.Call17("box.info", []interface{}{}) - if err != nil { - return unknown, err - } - if resp == nil { - return unknown, ErrIncorrectResponse - } - if len(resp.Data) < 1 { - return unknown, ErrIncorrectResponse - } - replicaRole, ok := resp.Data[0].(map[interface{}]interface{})["ro"] if !ok { return unknown, ErrIncorrectResponse From 1767bbd77f611238866d1009d67665f8ea823d96 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 16 Aug 2022 17:39:21 +0300 Subject: [PATCH 327/605] code health: unify error format in connection_pool After the patch all errors in the connection_pool subpackage start with a lowercase letter [1]. 1. https://github.com/golang/go/wiki/CodeReviewComments#error-strings Part of #208 --- connection_pool/connection_pool.go | 10 +++++----- connection_pool/connection_pool_test.go | 4 ++-- connection_pool/example_test.go | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index d3b7b5d1b..ed8cc38fb 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -25,11 +25,11 @@ var ( ErrWrongCheckTimeout = errors.New("wrong check timeout, must be greater than 0") ErrNoConnection = errors.New("no active connections") ErrTooManyArgs = errors.New("too many arguments") - ErrIncorrectResponse = errors.New("Incorrect response format") - ErrIncorrectStatus = errors.New("Incorrect instance status: status should be `running`") - ErrNoRwInstance = errors.New("Can't find rw instance in pool") - ErrNoRoInstance = errors.New("Can't find ro instance in pool") - ErrNoHealthyInstance = errors.New("Can't find healthy instance in pool") + ErrIncorrectResponse = errors.New("incorrect response format") + ErrIncorrectStatus = errors.New("incorrect instance status: status should be `running`") + ErrNoRwInstance = errors.New("can't find rw instance in pool") + ErrNoRoInstance = errors.New("can't find ro instance in pool") + ErrNoHealthyInstance = errors.New("can't find healthy instance in pool") ) /* diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 4dc3ac12c..0e34a6b95 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -431,7 +431,7 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { // RO _, err = connPool.Eval("return box.cfg.listen", []interface{}{}, connection_pool.RO) require.NotNilf(t, err, "expected to fail after Eval, but error is nil") - require.Equal(t, "Can't find ro instance in pool", err.Error()) + require.Equal(t, "can't find ro instance in pool", err.Error()) // ANY args := test_helpers.ListenOnInstanceArgs{ @@ -502,7 +502,7 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { // RW _, err = connPool.Eval("return box.cfg.listen", []interface{}{}, connection_pool.RW) require.NotNilf(t, err, "expected to fail after Eval, but error is nil") - require.Equal(t, "Can't find rw instance in pool", err.Error()) + require.Equal(t, "can't find rw instance in pool", err.Error()) // ANY args := test_helpers.ListenOnInstanceArgs{ diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go index 7f6c34700..65eff2cca 100644 --- a/connection_pool/example_test.go +++ b/connection_pool/example_test.go @@ -254,7 +254,7 @@ func ExampleConnectionPool_SelectAsync_err() { fmt.Println("Future", 0, "Error", err) // Output: - // Future 0 Error Can't find rw instance in pool + // Future 0 Error can't find rw instance in pool } func ExampleConnectionPool_Ping() { From 844f523225042b07caaccca6db291f3bd4140dcd Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 18 Aug 2022 09:36:26 +0300 Subject: [PATCH 328/605] bugfix: move Role constants into the public API We return a role of an instance from `GetPoolInfo()` call. It is unexpected that Role constants are not a part of the public API. Part of #208 --- CHANGELOG.md | 1 + connection_pool/connection_pool.go | 34 ++++++++++++------------- connection_pool/connection_pool_test.go | 2 +- connection_pool/const.go | 8 +++--- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd2b8876b..9ed7967b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed - Mode type description in the connection_pool subpackage (#208) +- Missed Role type constants in the connection_pool subpackage (#208) ## [1.8.0] - 2022-08-17 diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index ed8cc38fb..28aca1989 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -574,50 +574,48 @@ func (connPool *ConnectionPool) NewPrepared(expr string, userMode Mode) (*tarant func (connPool *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (Role, error) { resp, err := conn.Call17("box.info", []interface{}{}) if err != nil { - return unknown, err + return UnknownRole, err } if resp == nil { - return unknown, ErrIncorrectResponse + return UnknownRole, ErrIncorrectResponse } if len(resp.Data) < 1 { - return unknown, ErrIncorrectResponse + return UnknownRole, ErrIncorrectResponse } instanceStatus, ok := resp.Data[0].(map[interface{}]interface{})["status"] if !ok { - return unknown, ErrIncorrectResponse + return UnknownRole, ErrIncorrectResponse } if instanceStatus != "running" { - return unknown, ErrIncorrectStatus + return UnknownRole, ErrIncorrectStatus } replicaRole, ok := resp.Data[0].(map[interface{}]interface{})["ro"] if !ok { - return unknown, ErrIncorrectResponse + return UnknownRole, ErrIncorrectResponse } switch replicaRole { case false: - return master, nil + return MasterRole, nil case true: - return replica, nil + return ReplicaRole, nil } - return unknown, nil + return UnknownRole, nil } func (connPool *ConnectionPool) getConnectionFromPool(addr string) (*tarantool.Connection, Role) { - conn := connPool.rwPool.GetConnByAddr(addr) - if conn != nil { - return conn, master + if conn := connPool.rwPool.GetConnByAddr(addr); conn != nil { + return conn, MasterRole } - conn = connPool.roPool.GetConnByAddr(addr) - if conn != nil { - return conn, replica + if conn := connPool.roPool.GetConnByAddr(addr); conn != nil { + return conn, ReplicaRole } - return connPool.anyPool.GetConnByAddr(addr), unknown + return connPool.anyPool.GetConnByAddr(addr), UnknownRole } func (connPool *ConnectionPool) deleteConnectionFromPool(addr string) { @@ -639,9 +637,9 @@ func (connPool *ConnectionPool) setConnectionToPool(addr string, conn *tarantool connPool.anyPool.AddConn(addr, conn) switch role { - case master: + case MasterRole: connPool.rwPool.AddConn(addr, conn) - case replica: + case ReplicaRole: connPool.roPool.AddConn(addr, conn) } diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 0e34a6b95..b6380219d 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -1295,7 +1295,7 @@ func TestNewPrepared(t *testing.T) { stmt, err := connPool.NewPrepared("SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:id AND NAME1=:name;", connection_pool.RO) require.Nilf(t, err, "fail to prepare statement: %v", err) - if connPool.GetPoolInfo()[stmt.Conn.Addr()].ConnRole != connection_pool.RO { + if connPool.GetPoolInfo()[stmt.Conn.Addr()].ConnRole != connection_pool.ReplicaRole { t.Errorf("wrong role for the statement's connection") } diff --git a/connection_pool/const.go b/connection_pool/const.go index c93d4ee3e..2cfcbcb00 100644 --- a/connection_pool/const.go +++ b/connection_pool/const.go @@ -26,13 +26,13 @@ const ( PreferRO // If there is one, otherwise fallback to a read only one (replica). ) +// Role describes a role of an instance by its mode. type Role uint32 -// master/replica role const ( - unknown = iota - master - replica + UnknownRole Role = iota // A connection pool failed to discover mode of the instance. + MasterRole // The instance is read-write mode. + ReplicaRole // The instance is in read-only mode. ) type State uint32 From 579cebd4bcb2ec5acdcded09e8a151ab8e46a597 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 17 Aug 2022 10:14:15 +0300 Subject: [PATCH 329/605] bugfix: close `UnknownRole` connections from a pool We add all connections into ConnectionPool.anyPool, but `MasterRole` connection only in ConnectionPool.rwPool and `ReplicaRole` connections only in ConnectionPool.roPool. As a result `UnknownRole` connections appears only in the `anyPool`. See `setConnectionToPool` implementation. So we need to close connections from the `anyPool` instead of `roPool` + `rwPool`. Part of #208 --- CHANGELOG.md | 1 + connection_pool/connection_pool.go | 15 +++++++++++---- connection_pool/round_robin.go | 13 ------------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ed7967b3..6f3be2de8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Mode type description in the connection_pool subpackage (#208) - Missed Role type constants in the connection_pool subpackage (#208) +- ConnectionPool does not close UnknownRole connections (#208) ## [1.8.0] - 2022-08-17 diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 28aca1989..e424fe85e 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -154,12 +154,19 @@ func (connPool *ConnectionPool) Close() []error { close(connPool.control) connPool.state = connClosed - rwErrs := connPool.rwPool.CloseConns() - roErrs := connPool.roPool.CloseConns() + errs := make([]error, 0, len(connPool.addrs)) - allErrs := append(rwErrs, roErrs...) + for _, addr := range connPool.addrs { + if conn := connPool.anyPool.DeleteConnByAddr(addr); conn != nil { + if err := conn.Close(); err != nil { + errs = append(errs, err) + } + } + connPool.rwPool.DeleteConnByAddr(addr) + connPool.roPool.DeleteConnByAddr(addr) + } - return allErrs + return errs } // GetAddrs gets addresses of connections in pool. diff --git a/connection_pool/round_robin.go b/connection_pool/round_robin.go index 7f3f0d098..63afaeb12 100644 --- a/connection_pool/round_robin.go +++ b/connection_pool/round_robin.go @@ -60,19 +60,6 @@ func (r *RoundRobinStrategy) IsEmpty() bool { return r.size == 0 } -func (r *RoundRobinStrategy) CloseConns() []error { - r.mutex.Lock() - defer r.mutex.Unlock() - - errs := make([]error, len(r.conns)) - - for i, conn := range r.conns { - errs[i] = conn.Close() - } - - return errs -} - func (r *RoundRobinStrategy) GetNextConnection() *tarantool.Connection { r.mutex.RLock() defer r.mutex.RUnlock() From fad033e94a9fbd8e0cd843bd2519068ae4a26f6f Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 18 Aug 2022 08:30:16 +0300 Subject: [PATCH 330/605] bugfix: logic of add/delete in round robin strategy * Duplicates could be added to a list of connections, which then cannot be deleted. * The delete function uses an address from a connection instead of argument value. It may lead to unexpected errors. Part of #208 --- connection_pool/round_robin.go | 16 ++++++--- connection_pool/round_robin_test.go | 55 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 connection_pool/round_robin_test.go diff --git a/connection_pool/round_robin.go b/connection_pool/round_robin.go index 63afaeb12..9e512bfbb 100644 --- a/connection_pool/round_robin.go +++ b/connection_pool/round_robin.go @@ -46,8 +46,10 @@ func (r *RoundRobinStrategy) DeleteConnByAddr(addr string) *tarantool.Connection r.conns = append(r.conns[:index], r.conns[index+1:]...) r.size -= 1 - for index, conn := range r.conns { - r.indexByAddr[conn.Addr()] = index + for k, v := range r.indexByAddr { + if v > index { + r.indexByAddr[k] = v - 1 + } } return conn @@ -94,9 +96,13 @@ func (r *RoundRobinStrategy) AddConn(addr string, conn *tarantool.Connection) { r.mutex.Lock() defer r.mutex.Unlock() - r.conns = append(r.conns, conn) - r.indexByAddr[addr] = r.size - r.size += 1 + if idx, ok := r.indexByAddr[addr]; ok { + r.conns[idx] = conn + } else { + r.conns = append(r.conns, conn) + r.indexByAddr[addr] = r.size + r.size += 1 + } } func (r *RoundRobinStrategy) nextIndex() int { diff --git a/connection_pool/round_robin_test.go b/connection_pool/round_robin_test.go new file mode 100644 index 000000000..2ca8d19b9 --- /dev/null +++ b/connection_pool/round_robin_test.go @@ -0,0 +1,55 @@ +package connection_pool_test + +import ( + "testing" + + "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/connection_pool" +) + +const ( + validAddr1 = "x" + validAddr2 = "y" +) + +func TestRoundRobinAddDelete(t *testing.T) { + rr := NewEmptyRoundRobin(10) + + addrs := []string{validAddr1, validAddr2} + conns := []*tarantool.Connection{&tarantool.Connection{}, &tarantool.Connection{}} + + for i, addr := range addrs { + rr.AddConn(addr, conns[i]) + } + + for i, addr := range addrs { + if conn := rr.DeleteConnByAddr(addr); conn != conns[i] { + t.Errorf("Unexpected connection on address %s", addr) + } + } + if !rr.IsEmpty() { + t.Errorf("RoundRobin does not empty") + } +} + +func TestRoundRobinAddDuplicateDelete(t *testing.T) { + rr := NewEmptyRoundRobin(10) + + conn1 := &tarantool.Connection{} + conn2 := &tarantool.Connection{} + + rr.AddConn(validAddr1, conn1) + rr.AddConn(validAddr1, conn2) + + if rr.DeleteConnByAddr(validAddr1) != conn2 { + t.Errorf("Unexpected deleted connection") + } + if !rr.IsEmpty() { + t.Errorf("RoundRobin does not empty") + } + if rr.DeleteConnByAddr(validAddr1) != nil { + t.Errorf("Unexpected value after second deletion") + } +} + + From 1fe40823b022ff35c24ef3f1eb380c37f1adc0ac Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 18 Aug 2022 14:06:16 +0300 Subject: [PATCH 331/605] bugfix: prevent segfaults in connection_pool After the patch ConnectionPool.getNextConnection() does not return (nil, nil). It always return a connection or an error. The check Connection.ConnectedNow() does not have sence because the connection may be closed right after the call. The code just complicates the logic and does not protect against anything. A chain of two atomic operations IsEmpty() + GetNextConnection() wrong because it leads too a race condition. Part of #208 --- CHANGELOG.md | 1 + connection_pool/connection_pool.go | 66 +++++++++++-------------- connection_pool/connection_pool_test.go | 36 ++++++++++++++ connection_pool/round_robin.go | 34 +++++-------- connection_pool/round_robin_test.go | 16 ++++++ 5 files changed, 94 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f3be2de8..125081630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Mode type description in the connection_pool subpackage (#208) - Missed Role type constants in the connection_pool subpackage (#208) - ConnectionPool does not close UnknownRole connections (#208) +- Segmentation faults in ConnectionPool requests after disconnect (#208) ## [1.8.0] - 2022-08-17 diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index e424fe85e..0fcd88d07 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -130,13 +130,20 @@ func (connPool *ConnectionPool) ConnectedNow(mode Mode) (bool, error) { if connPool.getState() != connConnected { return false, nil } - - conn, err := connPool.getNextConnection(mode) - if err != nil || conn == nil { - return false, err + switch mode { + case ANY: + return !connPool.anyPool.IsEmpty(), nil + case RW: + return !connPool.rwPool.IsEmpty(), nil + case RO: + return !connPool.roPool.IsEmpty(), nil + case PreferRW: + fallthrough + case PreferRO: + return !connPool.rwPool.IsEmpty() || !connPool.roPool.IsEmpty(), nil + default: + return false, ErrNoHealthyInstance } - - return conn.ConnectedNow(), nil } // ConfiguredTimeout gets timeout of current connection. @@ -751,49 +758,34 @@ func (connPool *ConnectionPool) getNextConnection(mode Mode) (*tarantool.Connect switch mode { case ANY: - if connPool.anyPool.IsEmpty() { - return nil, ErrNoHealthyInstance + if next := connPool.anyPool.GetNextConnection(); next != nil { + return next, nil } - - return connPool.anyPool.GetNextConnection(), nil - case RW: - if connPool.rwPool.IsEmpty() { - return nil, ErrNoRwInstance + if next := connPool.rwPool.GetNextConnection(); next != nil { + return next, nil } - - return connPool.rwPool.GetNextConnection(), nil - + return nil, ErrNoRwInstance case RO: - if connPool.roPool.IsEmpty() { - return nil, ErrNoRoInstance + if next := connPool.roPool.GetNextConnection(); next != nil { + return next, nil } - - return connPool.roPool.GetNextConnection(), nil - + return nil, ErrNoRoInstance case PreferRW: - if !connPool.rwPool.IsEmpty() { - return connPool.rwPool.GetNextConnection(), nil + if next := connPool.rwPool.GetNextConnection(); next != nil { + return next, nil } - - if !connPool.roPool.IsEmpty() { - return connPool.roPool.GetNextConnection(), nil + if next := connPool.roPool.GetNextConnection(); next != nil { + return next, nil } - - return nil, ErrNoHealthyInstance - case PreferRO: - if !connPool.roPool.IsEmpty() { - return connPool.roPool.GetNextConnection(), nil + if next := connPool.roPool.GetNextConnection(); next != nil { + return next, nil } - - if !connPool.rwPool.IsEmpty() { - return connPool.rwPool.GetNextConnection(), nil + if next := connPool.rwPool.GetNextConnection(); next != nil { + return next, nil } - - return nil, ErrNoHealthyInstance } - return nil, ErrNoHealthyInstance } diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index b6380219d..a01bf5fd2 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -208,6 +208,42 @@ func TestClose(t *testing.T) { require.Nil(t, err) } +func TestRequestOnClosed(t *testing.T) { + server1 := servers[0] + server2 := servers[1] + + connPool, err := connection_pool.Connect([]string{server1, server2}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + test_helpers.StopTarantoolWithCleanup(instances[0]) + test_helpers.StopTarantoolWithCleanup(instances[1]) + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server1, server2}, + ExpectedPoolStatus: false, + ExpectedStatuses: map[string]bool{ + server1: false, + server2: false, + }, + } + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) + + _, err = connPool.Ping(connection_pool.ANY) + require.NotNilf(t, err, "err is nil after Ping") + + err = test_helpers.RestartTarantool(&instances[0]) + require.Nilf(t, err, "failed to restart tarantool") + + err = test_helpers.RestartTarantool(&instances[1]) + require.Nilf(t, err, "failed to restart tarantool") +} + func TestCall17(t *testing.T) { roles := []bool{false, true, false, false, true} diff --git a/connection_pool/round_robin.go b/connection_pool/round_robin.go index 9e512bfbb..b83d877d9 100644 --- a/connection_pool/round_robin.go +++ b/connection_pool/round_robin.go @@ -2,17 +2,16 @@ package connection_pool import ( "sync" - "sync/atomic" "github.com/tarantool/go-tarantool" ) type RoundRobinStrategy struct { conns []*tarantool.Connection - indexByAddr map[string]int + indexByAddr map[string]uint mutex sync.RWMutex - size int - current uint64 + size uint + current uint } func (r *RoundRobinStrategy) GetConnByAddr(addr string) *tarantool.Connection { @@ -66,29 +65,18 @@ func (r *RoundRobinStrategy) GetNextConnection() *tarantool.Connection { r.mutex.RLock() defer r.mutex.RUnlock() - // We want to iterate through the elements in a circular order - // so the first element in cycle is connections[next] - // and the last one is connections[next + length]. - next := r.nextIndex() - cycleLen := len(r.conns) + next - for i := next; i < cycleLen; i++ { - idx := i % len(r.conns) - if r.conns[idx].ConnectedNow() { - if i != next { - atomic.StoreUint64(&r.current, uint64(idx)) - } - return r.conns[idx] - } + if r.size == 0 { + return nil } - - return nil + return r.conns[r.nextIndex()] } func NewEmptyRoundRobin(size int) *RoundRobinStrategy { return &RoundRobinStrategy{ conns: make([]*tarantool.Connection, 0, size), - indexByAddr: make(map[string]int), + indexByAddr: make(map[string]uint), size: 0, + current: 0, } } @@ -105,6 +93,8 @@ func (r *RoundRobinStrategy) AddConn(addr string, conn *tarantool.Connection) { } } -func (r *RoundRobinStrategy) nextIndex() int { - return int(atomic.AddUint64(&r.current, uint64(1)) % uint64(len(r.conns))) +func (r *RoundRobinStrategy) nextIndex() uint { + ret := r.current % r.size + r.current++ + return ret } diff --git a/connection_pool/round_robin_test.go b/connection_pool/round_robin_test.go index 2ca8d19b9..6b54ecfd8 100644 --- a/connection_pool/round_robin_test.go +++ b/connection_pool/round_robin_test.go @@ -52,4 +52,20 @@ func TestRoundRobinAddDuplicateDelete(t *testing.T) { } } +func TestRoundRobinGetNextConnection(t *testing.T) { + rr := NewEmptyRoundRobin(10) + + addrs := []string{validAddr1, validAddr2} + conns := []*tarantool.Connection{&tarantool.Connection{}, &tarantool.Connection{}} + for i, addr := range addrs { + rr.AddConn(addr, conns[i]) + } + + expectedConns := []*tarantool.Connection{conns[0], conns[1], conns[0], conns[1]} + for i, expected := range expectedConns { + if rr.GetNextConnection() != expected { + t.Errorf("Unexpected connection on %d call", i) + } + } +} From 5c0de20dbafbea02585dc46c736bbfee12535230 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 18 Aug 2022 10:01:27 +0300 Subject: [PATCH 332/605] bugfix: protect addresses from external changes We need to use copies of slices, not just pointers to them. It helps to avoid unexpected changes. Part of #208 --- CHANGELOG.md | 1 + connection_pool/connection_pool.go | 7 +++++-- connection_pool/connection_pool_test.go | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 125081630..51551325d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Missed Role type constants in the connection_pool subpackage (#208) - ConnectionPool does not close UnknownRole connections (#208) - Segmentation faults in ConnectionPool requests after disconnect (#208) +- Addresses in ConnectionPool may be changed from an external code (#208) ## [1.8.0] - 2022-08-17 diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 0fcd88d07..68fcf7155 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -96,7 +96,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co anyPool := NewEmptyRoundRobin(size) connPool = &ConnectionPool{ - addrs: addrs, + addrs: make([]string, len(addrs)), connOpts: connOpts, opts: opts, notify: notify, @@ -105,6 +105,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co roPool: roPool, anyPool: anyPool, } + copy(connPool.addrs, addrs) somebodyAlive := connPool.fillPools() if !somebodyAlive { @@ -178,7 +179,9 @@ func (connPool *ConnectionPool) Close() []error { // GetAddrs gets addresses of connections in pool. func (connPool *ConnectionPool) GetAddrs() []string { - return connPool.addrs + cpy := make([]string, len(connPool.addrs)) + copy(cpy, connPool.addrs) + return cpy } // GetPoolInfo gets information of connections (connected status, ro/rw role). diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index a01bf5fd2..ccf1f14ab 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -244,6 +244,25 @@ func TestRequestOnClosed(t *testing.T) { require.Nilf(t, err, "failed to restart tarantool") } +func TestGetPoolInfo(t *testing.T) { + server1 := servers[0] + server2 := servers[1] + + srvs := []string{server1, server2} + expected := []string{server1, server2} + connPool, err := connection_pool.Connect(srvs, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + srvs[0] = "x" + connPool.GetAddrs()[1] = "y" + for i, addr := range connPool.GetAddrs() { + require.Equal(t, expected[i], addr) + } +} + func TestCall17(t *testing.T) { roles := []bool{false, true, false, false, true} From 16b49b669cf4d874468efe69072bb5f2a5104db3 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 18 Aug 2022 11:43:50 +0300 Subject: [PATCH 333/605] bugfix: do not recreate a connection immediately If we create a connection immediately after closing previous, then it can to lead to too frequent connection creation under some configurations [1] and high CPU load. It will be expected to recreate connection with OptsPool.CheckTimeout frequency. 1. https://github.com/tarantool/go-tarantool/issues/136 Part of #208 --- CHANGELOG.md | 1 + connection_pool/connection_pool.go | 15 +-------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51551325d..b638eed29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - ConnectionPool does not close UnknownRole connections (#208) - Segmentation faults in ConnectionPool requests after disconnect (#208) - Addresses in ConnectionPool may be changed from an external code (#208) +- ConnectionPool recreates connections too often (#208) ## [1.8.0] - 2022-08-17 diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 68fcf7155..83959141a 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -702,20 +702,7 @@ func (connPool *ConnectionPool) checker() { return } if e.Conn.ClosedNow() { - addr := e.Conn.Addr() - if conn, _ := connPool.getConnectionFromPool(addr); conn == nil { - continue - } - conn, _ := tarantool.Connect(addr, connPool.connOpts) - if conn != nil { - err := connPool.setConnectionToPool(addr, conn) - if err != nil { - conn.Close() - log.Printf("tarantool: storing connection to %s failed: %s\n", addr, err.Error()) - } - } else { - connPool.deleteConnectionFromPool(addr) - } + connPool.deleteConnectionFromPool(e.Conn.Addr()) } case <-timer.C: for _, addr := range connPool.addrs { From 3d16de6c75ecc8c81a74e1e4362d3040f64c063c Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 17 Aug 2022 13:16:01 +0300 Subject: [PATCH 334/605] bugfix: prevent recreate connection after Close() The ConnectionPool.checker() goroutine may still work some time after ConnectionPool.Close() call. It may lead to re-open connection in a concurrent closing pool. The connection still opened after the pool is closed. The patch adds RWLock to protect blocks which work with anyPool, roPool and rwPool. We don't need to protect regular requests because in the worst case, we will send a request into a closed connection. It can happen for other reasons and it seems like we can't avoid it. So it is an expected behavior. Part of #208 --- CHANGELOG.md | 1 + connection_pool/connection_pool.go | 85 ++++++++++++++++++------------ connection_pool/const.go | 8 --- connection_pool/state.go | 26 +++++++++ 4 files changed, 78 insertions(+), 42 deletions(-) create mode 100644 connection_pool/state.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b638eed29..9ba11e011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Segmentation faults in ConnectionPool requests after disconnect (#208) - Addresses in ConnectionPool may be changed from an external code (#208) - ConnectionPool recreates connections too often (#208) +- A connection is still opened after ConnectionPool.Close() (#208) ## [1.8.0] - 2022-08-17 diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 83959141a..673a7d63d 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -14,7 +14,7 @@ import ( "errors" "fmt" "log" - "sync/atomic" + "sync" "time" "github.com/tarantool/go-tarantool" @@ -69,12 +69,13 @@ type ConnectionPool struct { connOpts tarantool.Opts opts OptsPool - notify chan tarantool.ConnEvent - state State - control chan struct{} - roPool *RoundRobinStrategy - rwPool *RoundRobinStrategy - anyPool *RoundRobinStrategy + notify chan tarantool.ConnEvent + state state + control chan struct{} + roPool *RoundRobinStrategy + rwPool *RoundRobinStrategy + anyPool *RoundRobinStrategy + poolsMutex sync.RWMutex } // ConnectWithOpts creates pool for instances with addresses addrs @@ -100,6 +101,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co connOpts: connOpts, opts: opts, notify: notify, + state: unknownState, control: make(chan struct{}), rwPool: rwPool, roPool: roPool, @@ -109,10 +111,12 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co somebodyAlive := connPool.fillPools() if !somebodyAlive { - connPool.Close() + connPool.state.set(closedState) + connPool.closeImpl() return nil, ErrNoConnection } + connPool.state.set(connectedState) go connPool.checker() return connPool, nil @@ -128,7 +132,10 @@ func Connect(addrs []string, connOpts tarantool.Opts) (connPool *ConnectionPool, // ConnectedNow gets connected status of pool. func (connPool *ConnectionPool) ConnectedNow(mode Mode) (bool, error) { - if connPool.getState() != connConnected { + connPool.poolsMutex.RLock() + defer connPool.poolsMutex.RUnlock() + + if connPool.state.get() != connectedState { return false, nil } switch mode { @@ -157,10 +164,8 @@ func (connPool *ConnectionPool) ConfiguredTimeout(mode Mode) (time.Duration, err return conn.ConfiguredTimeout(), nil } -// Close closes connections in pool. -func (connPool *ConnectionPool) Close() []error { +func (connPool *ConnectionPool) closeImpl() []error { close(connPool.control) - connPool.state = connClosed errs := make([]error, 0, len(connPool.addrs)) @@ -177,6 +182,17 @@ func (connPool *ConnectionPool) Close() []error { return errs } +// Close closes connections in pool. +func (connPool *ConnectionPool) Close() []error { + if connPool.state.cas(connectedState, closedState) { + connPool.poolsMutex.Lock() + defer connPool.poolsMutex.Unlock() + + return connPool.closeImpl() + } + return nil +} + // GetAddrs gets addresses of connections in pool. func (connPool *ConnectionPool) GetAddrs() []string { cpy := make([]string, len(connPool.addrs)) @@ -188,6 +204,13 @@ func (connPool *ConnectionPool) GetAddrs() []string { func (connPool *ConnectionPool) GetPoolInfo() map[string]*ConnectionInfo { info := make(map[string]*ConnectionInfo) + connPool.poolsMutex.RLock() + defer connPool.poolsMutex.RUnlock() + + if connPool.state.get() != connectedState { + return info + } + for _, addr := range connPool.addrs { conn, role := connPool.getConnectionFromPool(addr) if conn != nil { @@ -638,11 +661,9 @@ func (connPool *ConnectionPool) getConnectionFromPool(addr string) (*tarantool.C func (connPool *ConnectionPool) deleteConnectionFromPool(addr string) { _ = connPool.anyPool.DeleteConnByAddr(addr) conn := connPool.rwPool.DeleteConnByAddr(addr) - if conn != nil { - return + if conn == nil { + connPool.roPool.DeleteConnByAddr(addr) } - - connPool.roPool.DeleteConnByAddr(addr) } func (connPool *ConnectionPool) setConnectionToPool(addr string, conn *tarantool.Connection) error { @@ -689,32 +710,30 @@ func (connPool *ConnectionPool) refreshConnection(addr string) { } func (connPool *ConnectionPool) checker() { - timer := time.NewTicker(connPool.opts.CheckTimeout) defer timer.Stop() - for connPool.getState() != connClosed { + for connPool.state.get() != closedState { select { case <-connPool.control: return case e := <-connPool.notify: - if connPool.getState() == connClosed { - return - } - if e.Conn.ClosedNow() { + connPool.poolsMutex.Lock() + if connPool.state.get() == connectedState && e.Conn.ClosedNow() { connPool.deleteConnectionFromPool(e.Conn.Addr()) } + connPool.poolsMutex.Unlock() case <-timer.C: - for _, addr := range connPool.addrs { - if connPool.getState() == connClosed { - return + connPool.poolsMutex.Lock() + if connPool.state.get() == connectedState { + for _, addr := range connPool.addrs { + // Reopen connection + // Relocate connection between subpools + // if ro/rw was updated + connPool.refreshConnection(addr) } - - // Reopen connection - // Relocate connection between subpools - // if ro/rw was updated - connPool.refreshConnection(addr) } + connPool.poolsMutex.Unlock() } } } @@ -722,6 +741,8 @@ func (connPool *ConnectionPool) checker() { func (connPool *ConnectionPool) fillPools() bool { somebodyAlive := false + // It is called before checker() goroutine and before closeImpl() may be + // called so we don't expect concurrency issues here. for _, addr := range connPool.addrs { conn, err := tarantool.Connect(addr, connPool.connOpts) if err != nil { @@ -740,10 +761,6 @@ func (connPool *ConnectionPool) fillPools() bool { return somebodyAlive } -func (connPool *ConnectionPool) getState() uint32 { - return atomic.LoadUint32((*uint32)(&connPool.state)) -} - func (connPool *ConnectionPool) getNextConnection(mode Mode) (*tarantool.Connection, error) { switch mode { diff --git a/connection_pool/const.go b/connection_pool/const.go index 2cfcbcb00..d77a55044 100644 --- a/connection_pool/const.go +++ b/connection_pool/const.go @@ -34,11 +34,3 @@ const ( MasterRole // The instance is read-write mode. ReplicaRole // The instance is in read-only mode. ) - -type State uint32 - -// pool state -const ( - connConnected = iota - connClosed -) diff --git a/connection_pool/state.go b/connection_pool/state.go new file mode 100644 index 000000000..a9d20392e --- /dev/null +++ b/connection_pool/state.go @@ -0,0 +1,26 @@ +package connection_pool + +import ( + "sync/atomic" +) + +// pool state +type state uint32 + +const ( + unknownState state = iota + connectedState + closedState +) + +func (s *state) set(news state) { + atomic.StoreUint32((*uint32)(s), uint32(news)) +} + +func (s *state) cas(olds, news state) bool { + return atomic.CompareAndSwapUint32((*uint32)(s), uint32(olds), uint32(news)) +} + +func (s *state) get() state { + return state(atomic.LoadUint32((*uint32)(s))) +} From 5801dc6f5ce69db7c8bc0c0d0fe4fb6042d5ecbc Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 25 Aug 2022 11:39:41 +0300 Subject: [PATCH 335/605] bugfix: prevent duplicate connections in pool An user can specify duplicate addresses for a ConnectionPool. We cannot support multiple connections to the same address due to the ConnectionPool.GetPoolInfo() implementation without breaking backward compatibility. So we need to skip duplicates. Closes #208 --- connection_pool/connection_pool.go | 11 +++++++++-- connection_pool/connection_pool_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 673a7d63d..e0c862386 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -97,7 +97,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co anyPool := NewEmptyRoundRobin(size) connPool = &ConnectionPool{ - addrs: make([]string, len(addrs)), + addrs: make([]string, 0, len(addrs)), connOpts: connOpts, opts: opts, notify: notify, @@ -107,7 +107,14 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co roPool: roPool, anyPool: anyPool, } - copy(connPool.addrs, addrs) + + m := make(map[string]bool) + for _, addr := range addrs { + if _, ok := m[addr]; !ok { + m[addr] = true + connPool.addrs = append(connPool.addrs, addr) + } + } somebodyAlive := connPool.fillPools() if !somebodyAlive { diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index ccf1f14ab..326604b51 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -80,6 +80,31 @@ func TestConnSuccessfully(t *testing.T) { require.Nil(t, err) } +func TestConnSuccessfullyDuplicates(t *testing.T) { + server := servers[0] + connPool, err := connection_pool.Connect([]string{server, server, server, server}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server}, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + server: true, + }, + } + + err = test_helpers.CheckPoolStatuses(args) + require.Nil(t, err) + + addrs := connPool.GetAddrs() + require.Equalf(t, []string{server}, addrs, "should be only one address") +} + func TestReconnect(t *testing.T) { server := servers[0] From 3d3ec7092bd243a61b6974259794ac79feffc755 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 22 Aug 2022 16:40:22 +0300 Subject: [PATCH 336/605] api: add ConnectionHandler to the connection_pool ConnectionHandler provides callbacks for components interested in handling changes of connections in a ConnectionPool. We have to take into account that user callbacks can take an indefinite amount of time. The simplest solution is to process each connection in a separate goroutine. This should not affect to performance because most of the time these goroutines are blocked. Closes #178 --- CHANGELOG.md | 2 + connection_pool/connection_pool.go | 348 ++++++++++++++++++------ connection_pool/connection_pool_test.go | 244 +++++++++++++++++ 3 files changed, 507 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ba11e011..8e1602a03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added - Support queue 1.2.0 (#177) +- ConnectionHandler interface for handling changes of connections in + ConnectionPool (#178) ### Changed diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index e0c862386..79667ae7c 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -32,17 +32,39 @@ var ( ErrNoHealthyInstance = errors.New("can't find healthy instance in pool") ) -/* -Additional options (configurable via ConnectWithOpts): - -- CheckTimeout - time interval to check for connection timeout and try to switch connection. -*/ +// ConnectionHandler provides callbacks for components interested in handling +// changes of connections in a ConnectionPool. +type ConnectionHandler interface { + // Discovered is called when a connection with a role has been detected + // (for the first time or when a role of a connection has been changed), + // but is not yet available to send requests. It allows for a client to + // initialize the connection before using it in a pool. + // + // The client code may cancel adding a connection to the pool. The client + // need to return an error from the Discovered call for that. In this case + // the pool will close connection and will try to reopen it later. + Discovered(conn *tarantool.Connection, role Role) error + // Deactivated is called when a connection with a role has become + // unavaileble to send requests. It happens if the connection is closed or + // the connection role is switched. + // + // So if a connection switches a role, a pool calls: + // Deactivated() + Discovered(). + // + // Deactivated will not be called if a previous Discovered() call returns + // an error. Because in this case, the connection does not become available + // for sending requests. + Deactivated(conn *tarantool.Connection, role Role) error +} + +// OptsPool provides additional options (configurable via ConnectWithOpts). type OptsPool struct { - // timeout for timer to reopen connections - // that have been closed by some events and - // to relocate connection between subpools - // if ro/rw role has been updated + // Timeout for timer to reopen connections that have been closed by some + // events and to relocate connection between subpools if ro/rw role has + // been updated. CheckTimeout time.Duration + // ConnectionHandler provides an ability to handle connection updates. + ConnectionHandler ConnectionHandler } /* @@ -69,15 +91,21 @@ type ConnectionPool struct { connOpts tarantool.Opts opts OptsPool - notify chan tarantool.ConnEvent state state - control chan struct{} + done chan struct{} roPool *RoundRobinStrategy rwPool *RoundRobinStrategy anyPool *RoundRobinStrategy poolsMutex sync.RWMutex } +type connState struct { + addr string + notify chan tarantool.ConnEvent + conn *tarantool.Connection + role Role +} + // ConnectWithOpts creates pool for instances with addresses addrs // with options opts. func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (connPool *ConnectionPool, err error) { @@ -88,9 +116,6 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co return nil, ErrWrongCheckTimeout } - notify := make(chan tarantool.ConnEvent, 10*len(addrs)) // x10 to accept disconnected and closed event (with a margin) - connOpts.Notify = notify - size := len(addrs) rwPool := NewEmptyRoundRobin(size) roPool := NewEmptyRoundRobin(size) @@ -100,9 +125,8 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co addrs: make([]string, 0, len(addrs)), connOpts: connOpts, opts: opts, - notify: notify, state: unknownState, - control: make(chan struct{}), + done: make(chan struct{}), rwPool: rwPool, roPool: roPool, anyPool: anyPool, @@ -116,15 +140,21 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co } } - somebodyAlive := connPool.fillPools() + states, somebodyAlive := connPool.fillPools() if !somebodyAlive { connPool.state.set(closedState) connPool.closeImpl() + for _, s := range states { + close(s.notify) + } return nil, ErrNoConnection } connPool.state.set(connectedState) - go connPool.checker() + + for _, s := range states { + go connPool.checker(s) + } return connPool, nil } @@ -172,8 +202,6 @@ func (connPool *ConnectionPool) ConfiguredTimeout(mode Mode) (time.Duration, err } func (connPool *ConnectionPool) closeImpl() []error { - close(connPool.control) - errs := make([]error, 0, len(connPool.addrs)) for _, addr := range connPool.addrs { @@ -181,11 +209,18 @@ func (connPool *ConnectionPool) closeImpl() []error { if err := conn.Close(); err != nil { errs = append(errs, err) } + + role := UnknownRole + if conn := connPool.rwPool.DeleteConnByAddr(addr); conn != nil { + role = MasterRole + } else if conn := connPool.roPool.DeleteConnByAddr(addr); conn != nil { + role = ReplicaRole + } + connPool.handlerDeactivated(conn, role) } - connPool.rwPool.DeleteConnByAddr(addr) - connPool.roPool.DeleteConnByAddr(addr) } + close(connPool.done) return errs } @@ -665,19 +700,17 @@ func (connPool *ConnectionPool) getConnectionFromPool(addr string) (*tarantool.C return connPool.anyPool.GetConnByAddr(addr), UnknownRole } -func (connPool *ConnectionPool) deleteConnectionFromPool(addr string) { - _ = connPool.anyPool.DeleteConnByAddr(addr) - conn := connPool.rwPool.DeleteConnByAddr(addr) - if conn == nil { +func (connPool *ConnectionPool) deleteConnection(addr string) { + if conn := connPool.anyPool.DeleteConnByAddr(addr); conn != nil { + if conn := connPool.rwPool.DeleteConnByAddr(addr); conn != nil { + return + } connPool.roPool.DeleteConnByAddr(addr) } } -func (connPool *ConnectionPool) setConnectionToPool(addr string, conn *tarantool.Connection) error { - role, err := connPool.getConnectionRole(conn) - if err != nil { - return err - } +func (connPool *ConnectionPool) addConnection(addr string, + conn *tarantool.Connection, role Role) { connPool.anyPool.AddConn(addr, conn) @@ -687,85 +720,226 @@ func (connPool *ConnectionPool) setConnectionToPool(addr string, conn *tarantool case ReplicaRole: connPool.roPool.AddConn(addr, conn) } +} - return nil +func (connPool *ConnectionPool) handlerDiscovered(conn *tarantool.Connection, + role Role) bool { + var err error + if connPool.opts.ConnectionHandler != nil { + err = connPool.opts.ConnectionHandler.Discovered(conn, role) + } + + if err != nil { + addr := conn.Addr() + log.Printf("tarantool: storing connection to %s canceled: %s\n", addr, err) + return false + } + return true } -func (connPool *ConnectionPool) refreshConnection(addr string) { - if conn, oldRole := connPool.getConnectionFromPool(addr); conn != nil { - if !conn.ClosedNow() { - curRole, _ := connPool.getConnectionRole(conn) - if oldRole != curRole { - connPool.deleteConnectionFromPool(addr) - err := connPool.setConnectionToPool(addr, conn) - if err != nil { - conn.Close() - log.Printf("tarantool: storing connection to %s failed: %s\n", addr, err.Error()) - } - } +func (connPool *ConnectionPool) handlerDeactivated(conn *tarantool.Connection, + role Role) { + var err error + if connPool.opts.ConnectionHandler != nil { + err = connPool.opts.ConnectionHandler.Deactivated(conn, role) + } + + if err != nil { + addr := conn.Addr() + log.Printf("tarantool: deactivating connection to %s by user failed: %s\n", addr, err) + } +} + +func (connPool *ConnectionPool) fillPools() ([]connState, bool) { + states := make([]connState, len(connPool.addrs)) + somebodyAlive := false + + // It is called before checker() goroutines and before closeImpl() may be + // called so we don't expect concurrency issues here. + for i, addr := range connPool.addrs { + states[i] = connState{ + addr: addr, + notify: make(chan tarantool.ConnEvent, 10), + conn: nil, + role: UnknownRole, } - } else { - conn, _ := tarantool.Connect(addr, connPool.connOpts) - if conn != nil { - err := connPool.setConnectionToPool(addr, conn) + connOpts := connPool.connOpts + connOpts.Notify = states[i].notify + + conn, err := tarantool.Connect(addr, connOpts) + if err != nil { + log.Printf("tarantool: connect to %s failed: %s\n", addr, err.Error()) + } else if conn != nil { + role, err := connPool.getConnectionRole(conn) if err != nil { conn.Close() - log.Printf("tarantool: storing connection to %s failed: %s\n", addr, err.Error()) + log.Printf("tarantool: storing connection to %s failed: %s\n", addr, err) + continue + } + + if connPool.handlerDiscovered(conn, role) { + connPool.addConnection(addr, conn, role) + + if conn.ConnectedNow() { + states[i].conn = conn + states[i].role = role + somebodyAlive = true + } else { + connPool.deleteConnection(addr) + conn.Close() + connPool.handlerDeactivated(conn, role) + } + } else { + conn.Close() } } } + + return states, somebodyAlive } -func (connPool *ConnectionPool) checker() { - timer := time.NewTicker(connPool.opts.CheckTimeout) - defer timer.Stop() +func (pool *ConnectionPool) updateConnection(s connState) connState { + pool.poolsMutex.Lock() - for connPool.state.get() != closedState { - select { - case <-connPool.control: - return - case e := <-connPool.notify: - connPool.poolsMutex.Lock() - if connPool.state.get() == connectedState && e.Conn.ClosedNow() { - connPool.deleteConnectionFromPool(e.Conn.Addr()) + if pool.state.get() != connectedState { + pool.poolsMutex.Unlock() + return s + } + + if role, err := pool.getConnectionRole(s.conn); err == nil { + if s.role != role { + pool.deleteConnection(s.addr) + pool.poolsMutex.Unlock() + + pool.handlerDeactivated(s.conn, s.role) + opened := pool.handlerDiscovered(s.conn, role) + if !opened { + s.conn.Close() + s.conn = nil + s.role = UnknownRole + return s } - connPool.poolsMutex.Unlock() - case <-timer.C: - connPool.poolsMutex.Lock() - if connPool.state.get() == connectedState { - for _, addr := range connPool.addrs { - // Reopen connection - // Relocate connection between subpools - // if ro/rw was updated - connPool.refreshConnection(addr) - } + + pool.poolsMutex.Lock() + if pool.state.get() != connectedState { + pool.poolsMutex.Unlock() + + s.conn.Close() + pool.handlerDeactivated(s.conn, role) + s.conn = nil + s.role = UnknownRole + return s } - connPool.poolsMutex.Unlock() + + pool.addConnection(s.addr, s.conn, role) + s.role = role } } + + pool.poolsMutex.Unlock() + return s } -func (connPool *ConnectionPool) fillPools() bool { - somebodyAlive := false +func (pool *ConnectionPool) tryConnect(s connState) connState { + pool.poolsMutex.Lock() + + if pool.state.get() != connectedState { + pool.poolsMutex.Unlock() + return s + } + + s.conn = nil + s.role = UnknownRole + + connOpts := pool.connOpts + connOpts.Notify = s.notify + conn, _ := tarantool.Connect(s.addr, connOpts) + if conn != nil { + role, err := pool.getConnectionRole(conn) + pool.poolsMutex.Unlock() - // It is called before checker() goroutine and before closeImpl() may be - // called so we don't expect concurrency issues here. - for _, addr := range connPool.addrs { - conn, err := tarantool.Connect(addr, connPool.connOpts) if err != nil { - log.Printf("tarantool: connect to %s failed: %s\n", addr, err.Error()) - } else if conn != nil { - err = connPool.setConnectionToPool(addr, conn) - if err != nil { - conn.Close() - log.Printf("tarantool: storing connection to %s failed: %s\n", addr, err.Error()) - } else if conn.ConnectedNow() { - somebodyAlive = true - } + conn.Close() + log.Printf("tarantool: storing connection to %s failed: %s\n", s.addr, err) + return s } + + opened := pool.handlerDiscovered(conn, role) + if !opened { + conn.Close() + return s + } + + pool.poolsMutex.Lock() + if pool.state.get() != connectedState { + pool.poolsMutex.Unlock() + conn.Close() + pool.handlerDeactivated(conn, role) + return s + } + + pool.addConnection(s.addr, conn, role) + s.conn = conn + s.role = role + } + + pool.poolsMutex.Unlock() + return s +} + +func (pool *ConnectionPool) reconnect(s connState) connState { + pool.poolsMutex.Lock() + + if pool.state.get() != connectedState { + pool.poolsMutex.Unlock() + return s } - return somebodyAlive + pool.deleteConnection(s.addr) + pool.poolsMutex.Unlock() + + pool.handlerDeactivated(s.conn, s.role) + s.conn = nil + s.role = UnknownRole + + return pool.tryConnect(s) +} + +func (pool *ConnectionPool) checker(s connState) { + timer := time.NewTicker(pool.opts.CheckTimeout) + defer timer.Stop() + + for { + select { + case <-pool.done: + close(s.notify) + return + case <-s.notify: + if s.conn != nil && s.conn.ClosedNow() { + pool.poolsMutex.Lock() + if pool.state.get() == connectedState { + pool.deleteConnection(s.addr) + pool.poolsMutex.Unlock() + pool.handlerDeactivated(s.conn, s.role) + s.conn = nil + s.role = UnknownRole + } else { + pool.poolsMutex.Unlock() + } + } + case <-timer.C: + // Reopen connection + // Relocate connection between subpools + // if ro/rw was updated + if s.conn == nil { + s = pool.tryConnect(s) + } else if !s.conn.ClosedNow() { + s = pool.updateConnection(s) + } else { + s = pool.reconnect(s) + } + } + } } func (connPool *ConnectionPool) getNextConnection(mode Mode) (*tarantool.Connection, error) { diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 326604b51..f009ff079 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -6,6 +6,7 @@ import ( "os" "reflect" "strings" + "sync" "testing" "time" @@ -233,6 +234,249 @@ func TestClose(t *testing.T) { require.Nil(t, err) } +type testHandler struct { + discovered, deactivated int + errs []error + mutex sync.Mutex +} + +func (h *testHandler) addErr(err error) { + h.errs = append(h.errs, err) +} + +func (h *testHandler) Discovered(conn *tarantool.Connection, + role connection_pool.Role) error { + h.mutex.Lock() + defer h.mutex.Unlock() + + h.discovered++ + + if conn == nil { + h.addErr(fmt.Errorf("discovered conn == nil")) + return nil + } + + // discovered < 3 - initial open of connections + // discovered >= 3 - update a connection after a role update + addr := conn.Addr() + if addr == servers[0] { + if h.discovered < 3 && role != connection_pool.MasterRole { + h.addErr(fmt.Errorf("unexpected init role %d for addr %s", role, addr)) + } + if h.discovered >= 3 && role != connection_pool.ReplicaRole { + h.addErr(fmt.Errorf("unexpected updated role %d for addr %s", role, addr)) + } + } else if addr == servers[1] { + if h.discovered >= 3 { + h.addErr(fmt.Errorf("unexpected discovery for addr %s", addr)) + } + if role != connection_pool.ReplicaRole { + h.addErr(fmt.Errorf("unexpected role %d for addr %s", role, addr)) + } + } else { + h.addErr(fmt.Errorf("unexpected discovered addr %s", addr)) + } + + return nil +} + +func (h *testHandler) Deactivated(conn *tarantool.Connection, + role connection_pool.Role) error { + h.mutex.Lock() + defer h.mutex.Unlock() + + h.deactivated++ + + if conn == nil { + h.addErr(fmt.Errorf("removed conn == nil")) + return nil + } + + addr := conn.Addr() + if h.deactivated == 1 && addr == servers[0] { + // A first close is a role update. + if role != connection_pool.MasterRole { + h.addErr(fmt.Errorf("unexpected removed role %d for addr %s", role, addr)) + } + return nil + } + + if addr == servers[0] || addr == servers[1] { + // Close. + if role != connection_pool.ReplicaRole { + h.addErr(fmt.Errorf("unexpected removed role %d for addr %s", role, addr)) + } + } else { + h.addErr(fmt.Errorf("unexpected removed addr %s", addr)) + } + + return nil +} + +func TestConnectionHandlerOpenUpdateClose(t *testing.T) { + poolServers := []string{servers[0], servers[1]} + roles := []bool{false, true} + + err := test_helpers.SetClusterRO(poolServers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + h := &testHandler{} + poolOpts := connection_pool.OptsPool{ + CheckTimeout: 100 * time.Microsecond, + ConnectionHandler: h, + } + pool, err := connection_pool.ConnectWithOpts(poolServers, connOpts, poolOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, pool, "conn is nil after Connect") + + _, err = pool.Call17("box.cfg", []interface{}{map[string]bool{ + "read_only": true, + }}, connection_pool.RW) + require.Nilf(t, err, "failed to make ro") + + for i := 0; i < 100; i++ { + // Wait for read_only update, it should report about close connection + // with old role. + if h.deactivated >= 1 { + break + } + time.Sleep(poolOpts.CheckTimeout) + } + require.Equalf(t, h.deactivated, 1, "updated not reported as deactivated") + require.Equalf(t, h.discovered, 3, "updated not reported as discovered") + + pool.Close() + + for i := 0; i < 100; i++ { + // Wait for close of all connections. + if h.deactivated >= 3 { + break + } + time.Sleep(poolOpts.CheckTimeout) + } + + for _, err := range h.errs { + t.Errorf("Unexpected error: %s", err) + } + connected, err := pool.ConnectedNow(connection_pool.ANY) + require.Nilf(t, err, "failed to get connected state") + require.Falsef(t, connected, "connection pool still be connected") + require.Equalf(t, len(poolServers)+1, h.discovered, "unexpected discovered count") + require.Equalf(t, len(poolServers)+1, h.deactivated, "unexpected deactivated count") +} + +type testAddErrorHandler struct { + discovered, deactivated int +} + +func (h *testAddErrorHandler) Discovered(conn *tarantool.Connection, + role connection_pool.Role) error { + h.discovered++ + return fmt.Errorf("any error") +} + +func (h *testAddErrorHandler) Deactivated(conn *tarantool.Connection, + role connection_pool.Role) error { + h.deactivated++ + return nil +} + +func TestConnectionHandlerOpenError(t *testing.T) { + poolServers := []string{servers[0], servers[1]} + + h := &testAddErrorHandler{} + poolOpts := connection_pool.OptsPool{ + CheckTimeout: 100 * time.Microsecond, + ConnectionHandler: h, + } + connPool, err := connection_pool.ConnectWithOpts(poolServers, connOpts, poolOpts) + if err == nil { + defer connPool.Close() + } + require.NotNilf(t, err, "success to connect") + require.Equalf(t, 2, h.discovered, "unexpected discovered count") + require.Equalf(t, 0, h.deactivated, "unexpected deactivated count") +} + +type testUpdateErrorHandler struct { + discovered, deactivated int + mutex sync.Mutex +} + +func (h *testUpdateErrorHandler) Discovered(conn *tarantool.Connection, + role connection_pool.Role) error { + h.mutex.Lock() + defer h.mutex.Unlock() + + h.discovered++ + + if h.deactivated != 0 { + // Don't add a connection into a pool again after it was deleted. + return fmt.Errorf("any error") + } + return nil +} + +func (h *testUpdateErrorHandler) Deactivated(conn *tarantool.Connection, + role connection_pool.Role) error { + h.mutex.Lock() + defer h.mutex.Unlock() + + h.deactivated++ + return nil +} + +func TestConnectionHandlerUpdateError(t *testing.T) { + poolServers := []string{servers[0], servers[1]} + roles := []bool{false, false} + + err := test_helpers.SetClusterRO(poolServers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + h := &testUpdateErrorHandler{} + poolOpts := connection_pool.OptsPool{ + CheckTimeout: 100 * time.Microsecond, + ConnectionHandler: h, + } + connPool, err := connection_pool.ConnectWithOpts(poolServers, connOpts, poolOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + defer connPool.Close() + + connected, err := connPool.ConnectedNow(connection_pool.ANY) + require.Nilf(t, err, "failed to get ConnectedNow()") + require.Truef(t, connected, "should be connected") + + for i := 0; i < len(poolServers); i++ { + _, err = connPool.Call17("box.cfg", []interface{}{map[string]bool{ + "read_only": true, + }}, connection_pool.RW) + require.Nilf(t, err, "failed to make ro") + } + + for i := 0; i < 100; i++ { + // Wait for updates done. + connected, err = connPool.ConnectedNow(connection_pool.ANY) + if !connected || err != nil { + break + } + time.Sleep(poolOpts.CheckTimeout) + } + connected, err = connPool.ConnectedNow(connection_pool.ANY) + + require.Nilf(t, err, "failed to get ConnectedNow()") + require.Falsef(t, connected, "should not be any active connection") + + connPool.Close() + + connected, err = connPool.ConnectedNow(connection_pool.ANY) + + require.Nilf(t, err, "failed to get ConnectedNow()") + require.Falsef(t, connected, "should be deactivated") + require.GreaterOrEqualf(t, h.discovered, h.deactivated, "discovered < deactivated") + require.Nilf(t, err, "failed to get ConnectedNow()") +} + func TestRequestOnClosed(t *testing.T) { server1 := servers[0] server2 := servers[1] From 5b29f65eab815086d9d19f28371efd0c064a680c Mon Sep 17 00:00:00 2001 From: Rianov Viacheslav Date: Thu, 15 Sep 2022 15:05:45 +0300 Subject: [PATCH 337/605] Makefile: add codespell target Added codespell target for checking typos in repo. Added codespell config file. --- .codespellrc | 5 +++++ Makefile | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .codespellrc diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 000000000..0fcbfb7be --- /dev/null +++ b/.codespellrc @@ -0,0 +1,5 @@ +[codespell] +skip = */testdata,./LICENSE,./datetime/timezones.go +ignore-words-list = ro,gost,warmup +count = +quiet-level = 3 diff --git a/Makefile b/Makefile index 7149e05ed..efe0d668c 100644 --- a/Makefile +++ b/Makefile @@ -134,3 +134,8 @@ fuzzing: @echo "Running fuzzing tests" go clean -testcache go test -tags "$(TAGS)" ./... -run=^Fuzz -v -p 1 + +.PHONY: codespell +codespell: + @echo "Running codespell" + codespell From cc0a3766551195fccf3e7dbf4801dbb90e91c86d Mon Sep 17 00:00:00 2001 From: Rianov Viacheslav Date: Thu, 15 Sep 2022 15:05:59 +0300 Subject: [PATCH 338/605] CI: add codespell job Codespell job will check all typos in repo after push and pull request event. --- .github/workflows/check.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index aa6e7b139..8769f088f 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -48,3 +48,18 @@ jobs: # cause those comments get rendered in documentation by godoc. # See https://github.com/tarantool/go-tarantool/pull/160#discussion_r858608221 args: -E goimports -D errcheck + + codespell: + runs-on: ubuntu-latest + if: | + github.event_name == 'push' || + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.repository + steps: + - uses: actions/checkout@master + + - name: Install codespell + run: pip3 install codespell + + - name: Run codespell + run: make codespell From 8b58928c5e28d0cd59de2769a42d26a77ec7926e Mon Sep 17 00:00:00 2001 From: Rianov Viacheslav Date: Thu, 15 Sep 2022 15:06:26 +0300 Subject: [PATCH 339/605] code health: fix all typos Fixed all typos detected by codespell. --- CHANGELOG.md | 2 +- README.md | 2 +- connection.go | 8 ++++---- connection_pool/connection_pool_test.go | 4 ++-- connection_pool/example_test.go | 6 +++--- datetime/datetime.go | 2 +- errors.go | 2 +- example_test.go | 6 +++--- future.go | 4 ++-- multi/multi_test.go | 14 +++++++------- prepared.go | 2 +- queue/queue.go | 2 +- queue/queue_test.go | 4 ++-- queue/task.go | 2 +- request_test.go | 2 +- tarantool_test.go | 12 ++++++------ 16 files changed, 37 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e1602a03..0f560ad1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,7 +88,7 @@ CI and documentation. - Handle everything with `go test` (#115) - Use plain package instead of module for UUID submodule (#134) -- Reset buffer if its average use size smaller than quater of capacity (#95) +- Reset buffer if its average use size smaller than quarter of capacity (#95) - Update API documentation: comments and examples (#123) ### Fixed diff --git a/README.md b/README.md index 769acea21..2bd3f271c 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ There are also changes in the logic that can lead to errors in the old code, `msgpack.v5` some functions for the logic tuning were added (see [UseLooseInterfaceDecoding](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5#Decoder.UseLooseInterfaceDecoding), [UseCompactInts](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5#Encoder.UseCompactInts) etc), it is still impossible to achieve full compliance of behavior between `msgpack.v5` and `msgpack.v2`. So -we don't go this way. We use standart settings if it possible. +we don't go this way. We use standard settings if it possible. ## Contributing diff --git a/connection.go b/connection.go index 957b51506..3fe6fec26 100644 --- a/connection.go +++ b/connection.go @@ -240,7 +240,7 @@ type Opts struct { // See RLimitAction for possible actions when RateLimit.reached. RateLimit uint // RLimitAction tells what to do when RateLimit reached: - // RLimitDrop - immediatly abort request, + // RLimitDrop - immediately abort request, // RLimitWait - wait during timeout period for some request to be answered. // If no request answered during timeout period, this request // is aborted. @@ -249,7 +249,7 @@ type Opts struct { RLimitAction uint // Concurrency is amount of separate mutexes for request // queues and buffers inside of connection. - // It is rounded upto nearest power of 2. + // It is rounded up to nearest power of 2. // By default it is runtime.GOMAXPROCS(-1) * 4 Concurrency uint32 // SkipSchema disables schema loading. Without disabling schema loading, @@ -273,7 +273,7 @@ type Opts struct { type SslOpts struct { // KeyFile is a path to a private SSL key file. KeyFile string - // CertFile is a path to an SSL sertificate file. + // CertFile is a path to an SSL certificate file. CertFile string // CaFile is a path to a trusted certificate authorities (CA) file. CaFile string @@ -1060,7 +1060,7 @@ func (conn *Connection) read(r io.Reader) (response []byte, err error) { return } if conn.lenbuf[0] != 0xce { - err = errors.New("Wrong reponse header") + err = errors.New("Wrong response header") return } length = (int(conn.lenbuf[1]) << 24) + diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index f009ff079..a41495d8d 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -1686,10 +1686,10 @@ func TestDoWithStrangerConn(t *testing.T) { _, err = connPool.Do(req, connection_pool.ANY).Get() if err == nil { - t.Fatalf("nil error catched") + t.Fatalf("nil error caught") } if err.Error() != expectedErr.Error() { - t.Fatalf("Unexpected error catched") + t.Fatalf("Unexpected error caught") } } diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go index 65eff2cca..9328c616a 100644 --- a/connection_pool/example_test.go +++ b/connection_pool/example_test.go @@ -620,7 +620,7 @@ func ExampleCommitRequest() { fmt.Printf("Insert in stream: response is %#v\n", resp.Code) // Select not related to the transaction - // while transaction is not commited + // while transaction is not committed // result of select is empty selectReq := tarantool.NewSelectRequest(spaceNo). Index(indexNo). @@ -706,7 +706,7 @@ func ExampleRollbackRequest() { fmt.Printf("Insert in stream: response is %#v\n", resp.Code) // Select not related to the transaction - // while transaction is not commited + // while transaction is not committed // result of select is empty selectReq := tarantool.NewSelectRequest(spaceNo). Index(indexNo). @@ -794,7 +794,7 @@ func ExampleBeginRequest_TxnIsolation() { fmt.Printf("Insert in stream: response is %#v\n", resp.Code) // Select not related to the transaction - // while transaction is not commited + // while transaction is not committed // result of select is empty selectReq := tarantool.NewSelectRequest(spaceNo). Index(indexNo). diff --git a/datetime/datetime.go b/datetime/datetime.go index b9fbc1dbe..cf3ac8758 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -149,7 +149,7 @@ func daysInMonth(year int64, month int64) int64 { return int64(time.Date(int(year), time.Month(month), 0, 0, 0, 0, 0, time.UTC).Day()) } -// C imlementation: +// C implementation: // https://github.com/tarantool/c-dt/blob/cec6acebb54d9e73ea0b99c63898732abd7683a6/dt_arithmetic.c#L74-L98 func addMonth(ival *Interval, delta int64, adjust Adjust) { oldYear := ival.Year diff --git a/errors.go b/errors.go index 760a59c5e..5677d07fc 100644 --- a/errors.go +++ b/errors.go @@ -25,7 +25,7 @@ func (clierr ClientError) Error() string { return fmt.Sprintf("%s (0x%x)", clierr.Msg, clierr.Code) } -// Temporary returns true if next attempt to perform request may succeeed. +// Temporary returns true if next attempt to perform request may succeeded. // // Currently it returns true when: // diff --git a/example_test.go b/example_test.go index 88779bc47..cfc30b162 100644 --- a/example_test.go +++ b/example_test.go @@ -264,7 +264,7 @@ func ExampleCommitRequest() { fmt.Printf("Insert in stream: response is %#v\n", resp.Code) // Select not related to the transaction - // while transaction is not commited + // while transaction is not committed // result of select is empty selectReq := tarantool.NewSelectRequest(spaceNo). Index(indexNo). @@ -340,7 +340,7 @@ func ExampleRollbackRequest() { fmt.Printf("Insert in stream: response is %#v\n", resp.Code) // Select not related to the transaction - // while transaction is not commited + // while transaction is not committed // result of select is empty selectReq := tarantool.NewSelectRequest(spaceNo). Index(indexNo). @@ -418,7 +418,7 @@ func ExampleBeginRequest_TxnIsolation() { fmt.Printf("Insert in stream: response is %#v\n", resp.Code) // Select not related to the transaction - // while transaction is not commited + // while transaction is not committed // result of select is empty selectReq := tarantool.NewSelectRequest(spaceNo). Index(indexNo). diff --git a/future.go b/future.go index a512d244c..4cb13e37f 100644 --- a/future.go +++ b/future.go @@ -172,7 +172,7 @@ func (fut *Future) SetError(err error) { // Get waits for Future to be filled and returns Response and error. // // Response will contain deserialized result in Data field. -// It will be []interface{}, so if you want more performace, use GetTyped method. +// It will be []interface{}, so if you want more performance, use GetTyped method. // // Note: Response could be equal to nil if ClientError is returned in error. // @@ -226,7 +226,7 @@ func init() { close(closedChan) } -// WaitChan returns channel which becomes closed when response arrived or error occured. +// WaitChan returns channel which becomes closed when response arrived or error occurred. func (fut *Future) WaitChan() <-chan struct{} { if fut.done == nil { return closedChan diff --git a/multi/multi_test.go b/multi/multi_test.go index d95a51f03..643ea186d 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -106,14 +106,14 @@ func TestReconnect(t *testing.T) { t.Errorf("conn has incorrect addr: %s after disconnect server1", multiConn.getCurrentConnection().Addr()) } if !multiConn.ConnectedNow() { - t.Errorf("incorrect multiConn status after reconecting") + t.Errorf("incorrect multiConn status after reconnecting") } timer = time.NewTimer(100 * time.Millisecond) <-timer.C conn, _ = multiConn.getConnectionFromPool(server1) if !conn.ConnectedNow() { - t.Errorf("incorrect conn status after reconecting") + t.Errorf("incorrect conn status after reconnecting") } } @@ -139,15 +139,15 @@ func TestDisconnectAll(t *testing.T) { timer = time.NewTimer(100 * time.Millisecond) <-timer.C if !multiConn.ConnectedNow() { - t.Errorf("incorrect multiConn status after reconecting") + t.Errorf("incorrect multiConn status after reconnecting") } conn, _ = multiConn.getConnectionFromPool(server1) if !conn.ConnectedNow() { - t.Errorf("incorrect server1 conn status after reconecting") + t.Errorf("incorrect server1 conn status after reconnecting") } conn, _ = multiConn.getConnectionFromPool(server2) if !conn.ConnectedNow() { - t.Errorf("incorrect server2 conn status after reconecting") + t.Errorf("incorrect server2 conn status after reconnecting") } } @@ -314,10 +314,10 @@ func TestDoWithStrangerConn(t *testing.T) { _, err = multiConn.Do(req).Get() if err == nil { - t.Fatalf("nil error catched") + t.Fatalf("nil error caught") } if err.Error() != expectedErr.Error() { - t.Fatalf("Unexpected error catched") + t.Fatalf("Unexpected error caught") } } diff --git a/prepared.go b/prepared.go index 6f63e3bb1..9019a81f3 100644 --- a/prepared.go +++ b/prepared.go @@ -41,7 +41,7 @@ func fillExecutePrepared(enc *encoder, stmt Prepared, args interface{}) error { // NewPreparedFromResponse constructs a Prepared object. func NewPreparedFromResponse(conn *Connection, resp *Response) (*Prepared, error) { if resp == nil { - return nil, fmt.Errorf("pased nil response") + return nil, fmt.Errorf("passed nil response") } if resp.Data == nil { return nil, fmt.Errorf("response Data is nil") diff --git a/queue/queue.go b/queue/queue.go index cbe80e057..a4e9af029 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -205,7 +205,7 @@ func (q *queue) Cfg(opts CfgOpts) error { return err } -// Exists checks existance of a tube. +// Exists checks existence of a tube. func (q *queue) Exists() (bool, error) { cmd := "local name = ... ; return queue.tube[name] ~= nil" resp, err := q.conn.Eval(cmd, []string{q.name}) diff --git a/queue/queue_test.go b/queue/queue_test.go index ef59156da..7b19c0dd3 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -336,7 +336,7 @@ func TestFifoQueue_TakeTyped(t *testing.T) { } else { typedData, ok := task.Data().(*customData) if !ok { - t.Errorf("Task data after put has diferent type. %#v != %#v", task.Data(), putData) + t.Errorf("Task data after put has different type. %#v != %#v", task.Data(), putData) } if *typedData != *putData { t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) @@ -353,7 +353,7 @@ func TestFifoQueue_TakeTyped(t *testing.T) { } else { typedData, ok := task.Data().(*customData) if !ok { - t.Errorf("Task data after put has diferent type. %#v != %#v", task.Data(), putData) + t.Errorf("Task data after put has different type. %#v != %#v", task.Data(), putData) } if *typedData != *putData { t.Errorf("Task data after take not equal with example. %#v != %#v", task.Data(), putData) diff --git a/queue/task.go b/queue/task.go index 01ab26da6..348d8610d 100644 --- a/queue/task.go +++ b/queue/task.go @@ -79,7 +79,7 @@ func (t *Task) Bury() error { } // Release returns task back in the queue without making it complete. -// In outher words, this worker failed to complete the task, and +// In other words, this worker failed to complete the task, and // it, so other worker could try to do that again. func (t *Task) Release() error { return t.accept(t.q._release(t.id, Opts{})) diff --git a/request_test.go b/request_test.go index 6ba5cecd4..89d1d8884 100644 --- a/request_test.go +++ b/request_test.go @@ -70,7 +70,7 @@ func assertBodyCall(t testing.TB, requests []Request, errorMsg string) { t.Errorf(errBegin+"error %q", err.Error()) } if err == nil && errorMsg != "" { - t.Errorf(errBegin+"result, expexted error %q", errorMsg) + t.Errorf(errBegin+"result, expected error %q", errorMsg) } } } diff --git a/tarantool_test.go b/tarantool_test.go index c1d49f12e..944e8bd0e 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -1626,16 +1626,16 @@ func TestConnection_DoWithStrangerConn(t *testing.T) { _, err := conn1.Do(req).Get() if err == nil { - t.Fatalf("nil error catched") + t.Fatalf("nil error caught") } if err.Error() != expectedErr.Error() { - t.Fatalf("Unexpected error catched") + t.Fatalf("Unexpected error caught") } } func TestNewPreparedFromResponse(t *testing.T) { var ( - ErrNilResponsePassed = fmt.Errorf("pased nil response") + ErrNilResponsePassed = fmt.Errorf("passed nil response") ErrNilResponseData = fmt.Errorf("response Data is nil") ErrWrongDataFormat = fmt.Errorf("response Data format is wrong") ) @@ -2293,7 +2293,7 @@ func TestClientRequestObjectsWithPassedCanceledContext(t *testing.T) { t.Fatalf("Failed to catch an error from done context") } if resp != nil { - t.Fatalf("Response is not nil after the occured error") + t.Fatalf("Response is not nil after the occurred error") } } @@ -2311,10 +2311,10 @@ func TestClientRequestObjectsWithContext(t *testing.T) { t.Fatalf("response must be nil") } if err == nil { - t.Fatalf("catched nil error") + t.Fatalf("caught nil error") } if err.Error() != "context is done" { - t.Fatalf("wrong error catched: %v", err) + t.Fatalf("wrong error caught: %v", err) } } From d4905f5de8300d4623e3468f4c6d4ab521b4756e Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 9 Sep 2022 10:54:48 +0300 Subject: [PATCH 340/605] bugfix: allow multiple Future.Get*() After the patch, a user can call Future.GetTyped() or Future.Get() multiple times in any order. Needed for https://github.com/tarantool/tt/issues/54 --- CHANGELOG.md | 2 + future.go | 6 --- response.go | 6 +++ smallbuf.go | 15 +++++++ tarantool_test.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 120 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f560ad1e..333c09554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Addresses in ConnectionPool may be changed from an external code (#208) - ConnectionPool recreates connections too often (#208) - A connection is still opened after ConnectionPool.Close() (#208) +- Future.GetTyped() after Future.Get() does not decode response + correctly (#213) ## [1.8.0] - 2022-08-17 diff --git a/future.go b/future.go index 4cb13e37f..a92a4c091 100644 --- a/future.go +++ b/future.go @@ -184,9 +184,6 @@ func (fut *Future) Get() (*Response, error) { return fut.resp, fut.err } err := fut.resp.decodeBody() - if err != nil { - fut.err = err - } return fut.resp, err } @@ -200,9 +197,6 @@ func (fut *Future) GetTyped(result interface{}) error { return fut.err } err := fut.resp.decodeBodyTyped(result) - if err != nil { - fut.err = err - } return err } diff --git a/response.go b/response.go index 96b2c15fa..7b203bc54 100644 --- a/response.go +++ b/response.go @@ -144,6 +144,9 @@ func (resp *Response) decodeHeader(d *decoder) (err error) { func (resp *Response) decodeBody() (err error) { if resp.buf.Len() > 2 { + offset := resp.buf.Offset() + defer resp.buf.Seek(offset) + var l int var stmtID, bindCount uint64 @@ -211,6 +214,9 @@ func (resp *Response) decodeBody() (err error) { func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if resp.buf.Len() > 0 { + offset := resp.buf.Offset() + defer resp.buf.Seek(offset) + var l int d := newDecoder(&resp.buf) if l, err = d.DecodeMapLen(); err != nil { diff --git a/smallbuf.go b/smallbuf.go index 27b79e864..a5b926835 100644 --- a/smallbuf.go +++ b/smallbuf.go @@ -51,6 +51,21 @@ func (s *smallBuf) Bytes() []byte { return nil } +func (s *smallBuf) Offset() int { + return s.p +} + +func (s *smallBuf) Seek(offset int) error { + if offset < 0 { + return errors.New("too small offset") + } + if offset > len(s.b) { + return errors.New("too big offset") + } + s.p = offset + return nil +} + type smallWBuf struct { b []byte sum uint diff --git a/tarantool_test.go b/tarantool_test.go index 944e8bd0e..a86fbb716 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -721,6 +721,81 @@ func BenchmarkSQLSerial(b *testing.B) { } } +func TestFutureMultipleGetGetTyped(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + fut := conn.Call17Async("simple_concat", []interface{}{"1"}) + + for i := 0; i < 30; i++ { + // [0, 10) fut.Get() + // [10, 20) fut.GetTyped() + // [20, 30) Mix + get := false + if (i < 10) || (i >= 20 && i%2 == 0) { + get = true + } + + if get { + resp, err := fut.Get() + if err != nil { + t.Errorf("Failed to call Get(): %s", err) + } + if val, ok := resp.Data[0].(string); !ok || val != "11" { + t.Errorf("Wrong Get() result: %v", resp.Data) + } + } else { + tpl := struct { + Val string + }{} + err := fut.GetTyped(&tpl) + if err != nil { + t.Errorf("Failed to call GetTyped(): %s", err) + } + if tpl.Val != "11" { + t.Errorf("Wrong GetTyped() result: %v", tpl) + } + } + } +} + +func TestFutureMultipleGetWithError(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + fut := conn.Call17Async("non_exist", []interface{}{"1"}) + + for i := 0; i < 2; i++ { + if _, err := fut.Get(); err == nil { + t.Fatalf("An error expected") + } + } +} + +func TestFutureMultipleGetTypedWithError(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + fut := conn.Call17Async("simple_concat", []interface{}{"1"}) + + wrongTpl := struct { + Val int + }{} + goodTpl := struct { + Val string + }{} + + if err := fut.GetTyped(&wrongTpl); err == nil { + t.Fatalf("An error expected") + } + if err := fut.GetTyped(&goodTpl); err != nil { + t.Fatalf("Unexpected error: %s", err) + } + if goodTpl.Val != "11" { + t.Fatalf("Wrong result: %s", goodTpl.Val) + } +} + /////////////////// func TestClient(t *testing.T) { @@ -1069,7 +1144,7 @@ func TestClientSessionPush(t *testing.T) { } else if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Call17Async") } else if val, err := convertUint64(resp.Data[0]); err != nil || val != pushMax { - t.Errorf("result is not {{1}} : %v", resp.Data) + t.Errorf("Result is not %d: %v", pushMax, resp.Data) } // It will will be iterated with a timeout. @@ -1103,7 +1178,7 @@ func TestClientSessionPush(t *testing.T) { } else { respCnt += 1 if val, err := convertUint64(resp.Data[0]); err != nil || val != pushMax { - t.Errorf("result is not {{1}} : %v", resp.Data) + t.Errorf("Result is not %d: %v", pushMax, resp.Data) } } } @@ -1120,6 +1195,26 @@ func TestClientSessionPush(t *testing.T) { t.Errorf("Expect %d responses but got %d", 1, respCnt) } } + + // We can collect original responses after iterations. + for _, fut := range []*Future{fut0, fut1, fut2} { + resp, err := fut.Get() + if err != nil { + t.Errorf("Unable to call fut.Get(): %s", err) + } else if val, err := convertUint64(resp.Data[0]); err != nil || val != pushMax { + t.Errorf("Result is not %d: %v", pushMax, resp.Data) + } + + tpl := struct { + Val int + }{} + err = fut.GetTyped(&tpl) + if err != nil { + t.Errorf("Unable to call fut.GetTyped(): %s", err) + } else if tpl.Val != pushMax { + t.Errorf("Result is not %d: %d", pushMax, tpl.Val) + } + } } const ( From c783152a29e5c7eadbc8b9ef6278098a71753aed Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 23 Aug 2022 17:59:17 +0300 Subject: [PATCH 341/605] api: add Execute methods to ConnectionPool The patch adds missed Execute, ExecuteTyped and ExecuteAsync methods to the ConnectionPool type. Part of #176 --- CHANGELOG.md | 1 + connection_pool/connection_pool.go | 31 ++++++++++++ connection_pool/connection_pool_test.go | 61 +++++++++++++++++++++++ connection_pool/const.go | 1 + connection_pool/msgpack_helper_test.go | 10 ++++ connection_pool/msgpack_v5_helper_test.go | 10 ++++ 6 files changed, 114 insertions(+) create mode 100644 connection_pool/msgpack_helper_test.go create mode 100644 connection_pool/msgpack_v5_helper_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 333c09554..002bd758a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support queue 1.2.0 (#177) - ConnectionHandler interface for handling changes of connections in ConnectionPool (#178) +- Execute, ExecuteTyped and ExecuteAsync methods to ConnectionPool (#176) ### Changed diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 79667ae7c..d9d41edef 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -385,6 +385,16 @@ func (connPool *ConnectionPool) Eval(expr string, args interface{}, userMode Mod return conn.Eval(expr, args) } +// Execute passes sql expression to Tarantool for execution. +func (connPool *ConnectionPool) Execute(expr string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return nil, err + } + + return conn.Execute(expr, args) +} + // GetTyped performs select (with limit = 1 and offset = 0) // to box space and fills typed result. func (connPool *ConnectionPool) GetTyped(space, index interface{}, key interface{}, result interface{}, userMode ...Mode) (err error) { @@ -495,6 +505,16 @@ func (connPool *ConnectionPool) EvalTyped(expr string, args interface{}, result return conn.EvalTyped(expr, args, result) } +// ExecuteTyped passes sql expression to Tarantool for execution. +func (connPool *ConnectionPool) ExecuteTyped(expr string, args interface{}, result interface{}, userMode Mode) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return tarantool.SQLInfo{}, nil, err + } + + return conn.ExecuteTyped(expr, args, result) +} + // SelectAsync sends select request to Tarantool and returns Future. func (connPool *ConnectionPool) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}, userMode ...Mode) *tarantool.Future { conn, err := connPool.getConnByMode(ANY, userMode) @@ -607,6 +627,17 @@ func (connPool *ConnectionPool) EvalAsync(expr string, args interface{}, userMod return conn.EvalAsync(expr, args) } +// ExecuteAsync sends sql expression to Tarantool for execution and returns +// Future. +func (connPool *ConnectionPool) ExecuteAsync(expr string, args interface{}, userMode Mode) *tarantool.Future { + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return newErrorFuture(err) + } + + return conn.ExecuteAsync(expr, args) +} + // Do sends the request and returns a future. // For requests that belong to an only one connection (e.g. Unprepare or ExecutePrepared) // the argument of type Mode is unused. diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index a41495d8d..b3aee615d 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -642,6 +642,67 @@ func TestEval(t *testing.T) { require.Falsef(t, val, "expected `false` with mode `RW`") } +type Member struct { + id uint + val string +} + +func (m *Member) DecodeMsgpack(d *decoder) error { + var err error + var l int + if l, err = d.DecodeArrayLen(); err != nil { + return err + } + if l != 2 { + return fmt.Errorf("array len doesn't match: %d", l) + } + if m.id, err = d.DecodeUint(); err != nil { + return err + } + if m.val, err = d.DecodeString(); err != nil { + return err + } + return nil +} + +func TestExecute(t *testing.T) { + test_helpers.SkipIfSQLUnsupported(t) + + roles := []bool{false, true, false, false, true} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + request := "SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0 == 1;" + // Execute + resp, err := connPool.Execute(request, []interface{}{}, connection_pool.ANY) + require.Nilf(t, err, "failed to Execute") + require.NotNilf(t, resp, "response is nil after Execute") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Execute") + require.Equalf(t, len(resp.Data[0].([]interface{})), 2, "unexpected response") + + // ExecuteTyped + mem := []Member{} + info, _, err := connPool.ExecuteTyped(request, []interface{}{}, &mem, connection_pool.ANY) + require.Nilf(t, err, "failed to ExecuteTyped") + require.Equalf(t, info.AffectedCount, uint64(0), "unexpected info.AffectedCount") + require.Equalf(t, len(mem), 1, "wrong count of results") + + // ExecuteAsync + fut := connPool.ExecuteAsync(request, []interface{}{}, connection_pool.ANY) + resp, err = fut.Get() + require.Nilf(t, err, "failed to ExecuteAsync") + require.NotNilf(t, resp, "response is nil after ExecuteAsync") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after ExecuteAsync") + require.Equalf(t, len(resp.Data[0].([]interface{})), 2, "unexpected response") +} + func TestRoundRobinStrategy(t *testing.T) { roles := []bool{false, true, false, false, true} diff --git a/connection_pool/const.go b/connection_pool/const.go index d77a55044..26b028f5a 100644 --- a/connection_pool/const.go +++ b/connection_pool/const.go @@ -7,6 +7,7 @@ Default mode for each request table: ---------- -------------- | call | no default | | eval | no default | + | execute | no default | | ping | no default | | insert | RW | | delete | RW | diff --git a/connection_pool/msgpack_helper_test.go b/connection_pool/msgpack_helper_test.go new file mode 100644 index 000000000..d60c7d84d --- /dev/null +++ b/connection_pool/msgpack_helper_test.go @@ -0,0 +1,10 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + +package connection_pool_test + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +type decoder = msgpack.Decoder diff --git a/connection_pool/msgpack_v5_helper_test.go b/connection_pool/msgpack_v5_helper_test.go new file mode 100644 index 000000000..7c449bec5 --- /dev/null +++ b/connection_pool/msgpack_v5_helper_test.go @@ -0,0 +1,10 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package connection_pool_test + +import ( + "github.com/vmihailenco/msgpack/v5" +) + +type decoder = msgpack.Decoder From 9132e2b57e0a045f3189ee18bf64e110065ade98 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 23 Aug 2022 19:09:16 +0300 Subject: [PATCH 342/605] api: add ConnectorAdapter to connection_pool ConnectorAdapter allows to use ConnectionPool as Connector. All requests to a pool will be executed in a specified mode. Part of #176 --- CHANGELOG.md | 1 + connection_pool/connection_pool.go | 2 + connection_pool/connector.go | 305 ++++++++ connection_pool/connector_test.go | 1168 ++++++++++++++++++++++++++++ connection_pool/example_test.go | 21 + connection_pool/pooler.go | 89 +++ 6 files changed, 1586 insertions(+) create mode 100644 connection_pool/connector.go create mode 100644 connection_pool/connector_test.go create mode 100644 connection_pool/pooler.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 002bd758a..9e0303af8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - ConnectionHandler interface for handling changes of connections in ConnectionPool (#178) - Execute, ExecuteTyped and ExecuteAsync methods to ConnectionPool (#176) +- ConnectorAdapter type to use ConnectionPool as Connector interface (#176) ### Changed diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index d9d41edef..6597e2dd0 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -99,6 +99,8 @@ type ConnectionPool struct { poolsMutex sync.RWMutex } +var _ Pooler = (*ConnectionPool)(nil) + type connState struct { addr string notify chan tarantool.ConnEvent diff --git a/connection_pool/connector.go b/connection_pool/connector.go new file mode 100644 index 000000000..e52109d92 --- /dev/null +++ b/connection_pool/connector.go @@ -0,0 +1,305 @@ +package connection_pool + +import ( + "errors" + "fmt" + "time" + + "github.com/tarantool/go-tarantool" +) + +// ConnectorAdapter allows to use Pooler as Connector. +type ConnectorAdapter struct { + pool Pooler + mode Mode +} + +var _ tarantool.Connector = (*ConnectorAdapter)(nil) + +// NewConnectorAdapter creates a new ConnectorAdapter object for a pool +// and with a mode. All requests to the pool will be executed in the +// specified mode. +func NewConnectorAdapter(pool Pooler, mode Mode) *ConnectorAdapter { + return &ConnectorAdapter{pool: pool, mode: mode} +} + +// ConnectedNow reports if connections is established at the moment. +func (c *ConnectorAdapter) ConnectedNow() bool { + ret, err := c.pool.ConnectedNow(c.mode) + if err != nil { + return false + } + return ret +} + +// ClosedNow reports if the connector is closed by user or all connections +// in the specified mode closed. +func (c *ConnectorAdapter) Close() error { + errs := c.pool.Close() + if len(errs) == 0 { + return nil + } + + err := errors.New("failed to close connection pool") + for _, e := range errs { + err = fmt.Errorf("%s: %w", err.Error(), e) + } + return err +} + +// Ping sends empty request to Tarantool to check connection. +func (c *ConnectorAdapter) Ping() (*tarantool.Response, error) { + return c.pool.Ping(c.mode) +} + +// ConfiguredTimeout returns a timeout from connections config. +func (c *ConnectorAdapter) ConfiguredTimeout() time.Duration { + ret, err := c.pool.ConfiguredTimeout(c.mode) + if err != nil { + return 0 * time.Second + } + return ret +} + +// Select performs select to box space. +func (c *ConnectorAdapter) Select(space, index interface{}, + offset, limit, iterator uint32, + key interface{}) (*tarantool.Response, error) { + return c.pool.Select(space, index, offset, limit, iterator, key, c.mode) +} + +// Insert performs insertion to box space. +func (c *ConnectorAdapter) Insert(space interface{}, + tuple interface{}) (*tarantool.Response, error) { + return c.pool.Insert(space, tuple, c.mode) +} + +// Replace performs "insert or replace" action to box space. +func (c *ConnectorAdapter) Replace(space interface{}, + tuple interface{}) (*tarantool.Response, error) { + return c.pool.Replace(space, tuple, c.mode) +} + +// Delete performs deletion of a tuple by key. +func (c *ConnectorAdapter) Delete(space, index interface{}, + key interface{}) (*tarantool.Response, error) { + return c.pool.Delete(space, index, key, c.mode) +} + +// Update performs update of a tuple by key. +func (c *ConnectorAdapter) Update(space, index interface{}, + key, ops interface{}) (*tarantool.Response, error) { + return c.pool.Update(space, index, key, ops, c.mode) +} + +// Upsert performs "update or insert" action of a tuple by key. +func (c *ConnectorAdapter) Upsert(space interface{}, + tuple, ops interface{}) (*tarantool.Response, error) { + return c.pool.Upsert(space, tuple, ops, c.mode) +} + +// Call calls registered Tarantool function. +// It uses request code for Tarantool >= 1.7 if go-tarantool +// was build with go_tarantool_call_17 tag. +// Otherwise, uses request code for Tarantool 1.6. +func (c *ConnectorAdapter) Call(functionName string, + args interface{}) (*tarantool.Response, error) { + return c.pool.Call(functionName, args, c.mode) +} + +// Call16 calls registered Tarantool function. +// It uses request code for Tarantool 1.6, so result is converted to array of arrays +// Deprecated since Tarantool 1.7.2. +func (c *ConnectorAdapter) Call16(functionName string, + args interface{}) (*tarantool.Response, error) { + return c.pool.Call16(functionName, args, c.mode) +} + +// Call17 calls registered Tarantool function. +// It uses request code for Tarantool >= 1.7, so result is not converted +// (though, keep in mind, result is always array) +func (c *ConnectorAdapter) Call17(functionName string, + args interface{}) (*tarantool.Response, error) { + return c.pool.Call17(functionName, args, c.mode) +} + +// Eval passes Lua expression for evaluation. +func (c *ConnectorAdapter) Eval(expr string, + args interface{}) (*tarantool.Response, error) { + return c.pool.Eval(expr, args, c.mode) +} + +// Execute passes sql expression to Tarantool for execution. +func (c *ConnectorAdapter) Execute(expr string, + args interface{}) (*tarantool.Response, error) { + return c.pool.Execute(expr, args, c.mode) +} + +// GetTyped performs select (with limit = 1 and offset = 0) +// to box space and fills typed result. +func (c *ConnectorAdapter) GetTyped(space, index interface{}, + key interface{}, result interface{}) error { + return c.pool.GetTyped(space, index, key, result, c.mode) +} + +// SelectTyped performs select to box space and fills typed result. +func (c *ConnectorAdapter) SelectTyped(space, index interface{}, + offset, limit, iterator uint32, + key interface{}, result interface{}) error { + return c.pool.SelectTyped(space, index, offset, limit, iterator, key, result, c.mode) +} + +// InsertTyped performs insertion to box space. +func (c *ConnectorAdapter) InsertTyped(space interface{}, + tuple interface{}, result interface{}) error { + return c.pool.InsertTyped(space, tuple, result, c.mode) +} + +// ReplaceTyped performs "insert or replace" action to box space. +func (c *ConnectorAdapter) ReplaceTyped(space interface{}, + tuple interface{}, result interface{}) error { + return c.pool.ReplaceTyped(space, tuple, result, c.mode) +} + +// DeleteTyped performs deletion of a tuple by key and fills result with deleted tuple. +func (c *ConnectorAdapter) DeleteTyped(space, index interface{}, + key interface{}, result interface{}) error { + return c.pool.DeleteTyped(space, index, key, result, c.mode) +} + +// UpdateTyped performs update of a tuple by key and fills result with updated tuple. +func (c *ConnectorAdapter) UpdateTyped(space, index interface{}, + key, ops interface{}, result interface{}) error { + return c.pool.UpdateTyped(space, index, key, ops, result, c.mode) +} + +// CallTyped calls registered function. +// It uses request code for Tarantool >= 1.7 if go-tarantool +// was build with go_tarantool_call_17 tag. +// Otherwise, uses request code for Tarantool 1.6. +func (c *ConnectorAdapter) CallTyped(functionName string, + args interface{}, result interface{}) error { + return c.pool.CallTyped(functionName, args, result, c.mode) +} + +// Call16Typed calls registered function. +// It uses request code for Tarantool 1.6, so result is converted to array of arrays +// Deprecated since Tarantool 1.7.2. +func (c *ConnectorAdapter) Call16Typed(functionName string, + args interface{}, result interface{}) error { + return c.pool.Call16Typed(functionName, args, result, c.mode) +} + +// Call17Typed calls registered function. +// It uses request code for Tarantool >= 1.7, so result is not converted +// (though, keep in mind, result is always array) +func (c *ConnectorAdapter) Call17Typed(functionName string, + args interface{}, result interface{}) error { + return c.pool.Call17Typed(functionName, args, result, c.mode) +} + +// EvalTyped passes Lua expression for evaluation. +func (c *ConnectorAdapter) EvalTyped(expr string, args interface{}, + result interface{}) error { + return c.pool.EvalTyped(expr, args, result, c.mode) +} + +// ExecuteTyped passes sql expression to Tarantool for execution. +func (c *ConnectorAdapter) ExecuteTyped(expr string, args interface{}, + result interface{}) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) { + return c.pool.ExecuteTyped(expr, args, result, c.mode) +} + +// SelectAsync sends select request to Tarantool and returns Future. +func (c *ConnectorAdapter) SelectAsync(space, index interface{}, + offset, limit, iterator uint32, key interface{}) *tarantool.Future { + return c.pool.SelectAsync(space, index, offset, limit, iterator, key, c.mode) +} + +// InsertAsync sends insert action to Tarantool and returns Future. +func (c *ConnectorAdapter) InsertAsync(space interface{}, + tuple interface{}) *tarantool.Future { + return c.pool.InsertAsync(space, tuple, c.mode) +} + +// ReplaceAsync sends "insert or replace" action to Tarantool and returns Future. +func (c *ConnectorAdapter) ReplaceAsync(space interface{}, + tuple interface{}) *tarantool.Future { + return c.pool.ReplaceAsync(space, tuple, c.mode) +} + +// DeleteAsync sends deletion action to Tarantool and returns Future. +func (c *ConnectorAdapter) DeleteAsync(space, index interface{}, + key interface{}) *tarantool.Future { + return c.pool.DeleteAsync(space, index, key, c.mode) +} + +// Update sends deletion of a tuple by key and returns Future. +func (c *ConnectorAdapter) UpdateAsync(space, index interface{}, + key, ops interface{}) *tarantool.Future { + return c.pool.UpdateAsync(space, index, key, ops, c.mode) +} + +// UpsertAsync sends "update or insert" action to Tarantool and returns Future. +func (c *ConnectorAdapter) UpsertAsync(space interface{}, tuple interface{}, + ops interface{}) *tarantool.Future { + return c.pool.UpsertAsync(space, tuple, ops, c.mode) +} + +// CallAsync sends a call to registered Tarantool function and returns Future. +// It uses request code for Tarantool >= 1.7 if go-tarantool +// was build with go_tarantool_call_17 tag. +// Otherwise, uses request code for Tarantool 1.6. +func (c *ConnectorAdapter) CallAsync(functionName string, + args interface{}) *tarantool.Future { + return c.pool.CallAsync(functionName, args, c.mode) +} + +// Call16Async sends a call to registered Tarantool function and returns Future. +// It uses request code for Tarantool 1.6, so future's result is always array of arrays. +// Deprecated since Tarantool 1.7.2. +func (c *ConnectorAdapter) Call16Async(functionName string, + args interface{}) *tarantool.Future { + return c.pool.Call16Async(functionName, args, c.mode) +} + +// Call17Async sends a call to registered Tarantool function and returns Future. +// It uses request code for Tarantool >= 1.7, so future's result will not be converted +// (though, keep in mind, result is always array) +func (c *ConnectorAdapter) Call17Async(functionName string, + args interface{}) *tarantool.Future { + return c.pool.Call17Async(functionName, args, c.mode) +} + +// EvalAsync sends a Lua expression for evaluation and returns Future. +func (c *ConnectorAdapter) EvalAsync(expr string, + args interface{}) *tarantool.Future { + return c.pool.EvalAsync(expr, args, c.mode) +} + +// ExecuteAsync sends a sql expression for execution and returns Future. +func (c *ConnectorAdapter) ExecuteAsync(expr string, + args interface{}) *tarantool.Future { + return c.pool.ExecuteAsync(expr, args, c.mode) +} + +// NewPrepared passes a sql statement to Tarantool for preparation +// synchronously. +func (c *ConnectorAdapter) NewPrepared(expr string) (*tarantool.Prepared, error) { + return c.pool.NewPrepared(expr, c.mode) +} + +// NewStream creates new Stream object for connection. +// +// Since v. 2.10.0, Tarantool supports streams and interactive transactions over +// them. To use interactive transactions, memtx_use_mvcc_engine box option +// should be set to true. +// Since 1.7.0 +func (c *ConnectorAdapter) NewStream() (*tarantool.Stream, error) { + return c.pool.NewStream(c.mode) +} + +// Do performs a request asynchronously on the connection. +func (c *ConnectorAdapter) Do(req tarantool.Request) *tarantool.Future { + return c.pool.Do(req, c.mode) +} diff --git a/connection_pool/connector_test.go b/connection_pool/connector_test.go new file mode 100644 index 000000000..fa7cf06ba --- /dev/null +++ b/connection_pool/connector_test.go @@ -0,0 +1,1168 @@ +package connection_pool_test + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/connection_pool" +) + +var testMode Mode = RW + +type connectedNowMock struct { + Pooler + called int + mode Mode + retErr bool +} + +// Tests for different logic. + +func (m *connectedNowMock) ConnectedNow(mode Mode) (bool, error) { + m.called++ + m.mode = mode + + if m.retErr { + return true, errors.New("mock error") + } + return true, nil +} + +func TestConnectorConnectedNow(t *testing.T) { + m := &connectedNowMock{retErr: false} + c := NewConnectorAdapter(m, testMode) + + require.Truef(t, c.ConnectedNow(), "unexpected result") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +func TestConnectorConnectedNowWithError(t *testing.T) { + m := &connectedNowMock{retErr: true} + c := NewConnectorAdapter(m, testMode) + + require.Falsef(t, c.ConnectedNow(), "unexpected result") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type closeMock struct { + Pooler + called int + retErr bool +} + +func (m *closeMock) Close() []error { + m.called++ + if m.retErr { + return []error{errors.New("err1"), errors.New("err2")} + } + return nil +} + +func TestConnectorClose(t *testing.T) { + m := &closeMock{retErr: false} + c := NewConnectorAdapter(m, testMode) + + require.Nilf(t, c.Close(), "unexpected result") + require.Equalf(t, 1, m.called, "should be called only once") +} + +func TestConnectorCloseWithError(t *testing.T) { + m := &closeMock{retErr: true} + c := NewConnectorAdapter(m, testMode) + + err := c.Close() + require.NotNilf(t, err, "unexpected result") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equal(t, "failed to close connection pool: err1: err2", err.Error()) +} + +type configuredTimeoutMock struct { + Pooler + called int + timeout time.Duration + mode Mode + retErr bool +} + +func (m *configuredTimeoutMock) ConfiguredTimeout(mode Mode) (time.Duration, error) { + m.called++ + m.mode = mode + m.timeout = 5 * time.Second + if m.retErr { + return m.timeout, fmt.Errorf("err") + } + return m.timeout, nil +} + +func TestConnectorConfiguredTimeout(t *testing.T) { + m := &configuredTimeoutMock{retErr: false} + c := NewConnectorAdapter(m, testMode) + + require.Equalf(t, c.ConfiguredTimeout(), m.timeout, "unexpected result") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +func TestConnectorConfiguredTimeoutWithError(t *testing.T) { + m := &configuredTimeoutMock{retErr: true} + c := NewConnectorAdapter(m, testMode) + + ret := c.ConfiguredTimeout() + + require.NotEqualf(t, ret, m.timeout, "unexpected result") + require.Equalf(t, ret, time.Duration(0), "unexpected result") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +// Tests for that ConnectorAdapter is just a proxy for requests. + +type baseRequestMock struct { + Pooler + called int + functionName string + offset, limit, iterator uint32 + space, index interface{} + args, tuple, key, ops interface{} + result interface{} + mode Mode +} + +var reqResp *tarantool.Response = &tarantool.Response{} +var reqErr error = errors.New("response error") +var reqFuture *tarantool.Future = &tarantool.Future{} + +var reqFunctionName string = "any_name" +var reqOffset uint32 = 1 +var reqLimit uint32 = 2 +var reqIterator uint32 = 3 +var reqSpace interface{} = []interface{}{1} +var reqIndex interface{} = []interface{}{2} +var reqArgs interface{} = []interface{}{3} +var reqTuple interface{} = []interface{}{4} +var reqKey interface{} = []interface{}{5} +var reqOps interface{} = []interface{}{6} + +var reqResult interface{} = []interface{}{7} +var reqSqlInfo = tarantool.SQLInfo{AffectedCount: 3} +var reqMeta = []tarantool.ColumnMetaData{{FieldIsNullable: false}} + +type getTypedMock struct { + baseRequestMock +} + +func (m *getTypedMock) GetTyped(space, index, key interface{}, + result interface{}, mode ...Mode) error { + m.called++ + m.space = space + m.index = index + m.key = key + m.result = result + m.mode = mode[0] + return reqErr +} + +func TestConnectorGetTyped(t *testing.T) { + m := &getTypedMock{} + c := NewConnectorAdapter(m, testMode) + + err := c.GetTyped(reqSpace, reqIndex, reqKey, reqResult) + + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqIndex, m.index, "unexpected index was passed") + require.Equalf(t, reqKey, m.key, "unexpected key was passed") + require.Equalf(t, reqResult, m.result, "unexpected result was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type selectMock struct { + baseRequestMock +} + +func (m *selectMock) Select(space, index interface{}, + offset, limit, iterator uint32, key interface{}, + mode ...Mode) (*tarantool.Response, error) { + m.called++ + m.space = space + m.index = index + m.offset = offset + m.limit = limit + m.iterator = iterator + m.key = key + m.mode = mode[0] + return reqResp, reqErr +} + +func TestConnectorSelect(t *testing.T) { + m := &selectMock{} + c := NewConnectorAdapter(m, testMode) + + resp, err := c.Select(reqSpace, reqIndex, reqOffset, reqLimit, reqIterator, reqKey) + + require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqIndex, m.index, "unexpected index was passed") + require.Equalf(t, reqOffset, m.offset, "unexpected offset was passed") + require.Equalf(t, reqLimit, m.limit, "unexpected limit was passed") + require.Equalf(t, reqIterator, m.iterator, "unexpected iterator was passed") + require.Equalf(t, reqKey, m.key, "unexpected key was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type selectTypedMock struct { + baseRequestMock +} + +func (m *selectTypedMock) SelectTyped(space, index interface{}, + offset, limit, iterator uint32, key interface{}, + result interface{}, mode ...Mode) error { + m.called++ + m.space = space + m.index = index + m.offset = offset + m.limit = limit + m.iterator = iterator + m.key = key + m.result = result + m.mode = mode[0] + return reqErr +} + +func TestConnectorSelectTyped(t *testing.T) { + m := &selectTypedMock{} + c := NewConnectorAdapter(m, testMode) + + err := c.SelectTyped(reqSpace, reqIndex, reqOffset, reqLimit, + reqIterator, reqKey, reqResult) + + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqIndex, m.index, "unexpected index was passed") + require.Equalf(t, reqOffset, m.offset, "unexpected offset was passed") + require.Equalf(t, reqLimit, m.limit, "unexpected limit was passed") + require.Equalf(t, reqIterator, m.iterator, "unexpected iterator was passed") + require.Equalf(t, reqKey, m.key, "unexpected key was passed") + require.Equalf(t, reqResult, m.result, "unexpected result was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type selectAsyncMock struct { + baseRequestMock +} + +func (m *selectAsyncMock) SelectAsync(space, index interface{}, + offset, limit, iterator uint32, key interface{}, + mode ...Mode) *tarantool.Future { + m.called++ + m.space = space + m.index = index + m.offset = offset + m.limit = limit + m.iterator = iterator + m.key = key + m.mode = mode[0] + return reqFuture +} + +func TestConnectorSelectAsync(t *testing.T) { + m := &selectAsyncMock{} + c := NewConnectorAdapter(m, testMode) + + fut := c.SelectAsync(reqSpace, reqIndex, reqOffset, reqLimit, + reqIterator, reqKey) + + require.Equalf(t, reqFuture, fut, "unexpected future") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqIndex, m.index, "unexpected index was passed") + require.Equalf(t, reqOffset, m.offset, "unexpected offset was passed") + require.Equalf(t, reqLimit, m.limit, "unexpected limit was passed") + require.Equalf(t, reqIterator, m.iterator, "unexpected iterator was passed") + require.Equalf(t, reqKey, m.key, "unexpected key was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type insertMock struct { + baseRequestMock +} + +func (m *insertMock) Insert(space, tuple interface{}, + mode ...Mode) (*tarantool.Response, error) { + m.called++ + m.space = space + m.tuple = tuple + m.mode = mode[0] + return reqResp, reqErr +} + +func TestConnectorInsert(t *testing.T) { + m := &insertMock{} + c := NewConnectorAdapter(m, testMode) + + resp, err := c.Insert(reqSpace, reqTuple) + + require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type insertTypedMock struct { + baseRequestMock +} + +func (m *insertTypedMock) InsertTyped(space, tuple interface{}, + result interface{}, mode ...Mode) error { + m.called++ + m.space = space + m.tuple = tuple + m.result = result + m.mode = mode[0] + return reqErr +} + +func TestConnectorInsertTyped(t *testing.T) { + m := &insertTypedMock{} + c := NewConnectorAdapter(m, testMode) + + err := c.InsertTyped(reqSpace, reqTuple, reqResult) + + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") + require.Equalf(t, reqResult, m.result, "unexpected result was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type insertAsyncMock struct { + baseRequestMock +} + +func (m *insertAsyncMock) InsertAsync(space, tuple interface{}, + mode ...Mode) *tarantool.Future { + m.called++ + m.space = space + m.tuple = tuple + m.mode = mode[0] + return reqFuture +} + +func TestConnectorInsertAsync(t *testing.T) { + m := &insertAsyncMock{} + c := NewConnectorAdapter(m, testMode) + + fut := c.InsertAsync(reqSpace, reqTuple) + + require.Equalf(t, reqFuture, fut, "unexpected future") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type replaceMock struct { + baseRequestMock +} + +func (m *replaceMock) Replace(space, tuple interface{}, + mode ...Mode) (*tarantool.Response, error) { + m.called++ + m.space = space + m.tuple = tuple + m.mode = mode[0] + return reqResp, reqErr +} + +func TestConnectorReplace(t *testing.T) { + m := &replaceMock{} + c := NewConnectorAdapter(m, testMode) + + resp, err := c.Replace(reqSpace, reqTuple) + + require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type replaceTypedMock struct { + baseRequestMock +} + +func (m *replaceTypedMock) ReplaceTyped(space, tuple interface{}, + result interface{}, mode ...Mode) error { + m.called++ + m.space = space + m.tuple = tuple + m.result = result + m.mode = mode[0] + return reqErr +} + +func TestConnectorReplaceTyped(t *testing.T) { + m := &replaceTypedMock{} + c := NewConnectorAdapter(m, testMode) + + err := c.ReplaceTyped(reqSpace, reqTuple, reqResult) + + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") + require.Equalf(t, reqResult, m.result, "unexpected result was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type replaceAsyncMock struct { + baseRequestMock +} + +func (m *replaceAsyncMock) ReplaceAsync(space, tuple interface{}, + mode ...Mode) *tarantool.Future { + m.called++ + m.space = space + m.tuple = tuple + m.mode = mode[0] + return reqFuture +} + +func TestConnectorReplaceAsync(t *testing.T) { + m := &replaceAsyncMock{} + c := NewConnectorAdapter(m, testMode) + + fut := c.ReplaceAsync(reqSpace, reqTuple) + + require.Equalf(t, reqFuture, fut, "unexpected future") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type deleteMock struct { + baseRequestMock +} + +func (m *deleteMock) Delete(space, index, key interface{}, + mode ...Mode) (*tarantool.Response, error) { + m.called++ + m.space = space + m.index = index + m.key = key + m.mode = mode[0] + return reqResp, reqErr +} + +func TestConnectorDelete(t *testing.T) { + m := &deleteMock{} + c := NewConnectorAdapter(m, testMode) + + resp, err := c.Delete(reqSpace, reqIndex, reqKey) + + require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqIndex, m.index, "unexpected index was passed") + require.Equalf(t, reqKey, m.key, "unexpected key was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type deleteTypedMock struct { + baseRequestMock +} + +func (m *deleteTypedMock) DeleteTyped(space, index, key interface{}, + result interface{}, mode ...Mode) error { + m.called++ + m.space = space + m.index = index + m.key = key + m.result = result + m.mode = mode[0] + return reqErr +} + +func TestConnectorDeleteTyped(t *testing.T) { + m := &deleteTypedMock{} + c := NewConnectorAdapter(m, testMode) + + err := c.DeleteTyped(reqSpace, reqIndex, reqKey, reqResult) + + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqIndex, m.index, "unexpected index was passed") + require.Equalf(t, reqKey, m.key, "unexpected key was passed") + require.Equalf(t, reqResult, m.result, "unexpected result was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type deleteAsyncMock struct { + baseRequestMock +} + +func (m *deleteAsyncMock) DeleteAsync(space, index, key interface{}, + mode ...Mode) *tarantool.Future { + m.called++ + m.space = space + m.index = index + m.key = key + m.mode = mode[0] + return reqFuture +} + +func TestConnectorDeleteAsync(t *testing.T) { + m := &deleteAsyncMock{} + c := NewConnectorAdapter(m, testMode) + + fut := c.DeleteAsync(reqSpace, reqIndex, reqKey) + + require.Equalf(t, reqFuture, fut, "unexpected future") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqIndex, m.index, "unexpected index was passed") + require.Equalf(t, reqKey, m.key, "unexpected key was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type updateMock struct { + baseRequestMock +} + +func (m *updateMock) Update(space, index, key, ops interface{}, + mode ...Mode) (*tarantool.Response, error) { + m.called++ + m.space = space + m.index = index + m.key = key + m.ops = ops + m.mode = mode[0] + return reqResp, reqErr +} + +func TestConnectorUpdate(t *testing.T) { + m := &updateMock{} + c := NewConnectorAdapter(m, testMode) + + resp, err := c.Update(reqSpace, reqIndex, reqKey, reqOps) + + require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqIndex, m.index, "unexpected index was passed") + require.Equalf(t, reqKey, m.key, "unexpected key was passed") + require.Equalf(t, reqOps, m.ops, "unexpected ops was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type updateTypedMock struct { + baseRequestMock +} + +func (m *updateTypedMock) UpdateTyped(space, index, key, ops interface{}, + result interface{}, mode ...Mode) error { + m.called++ + m.space = space + m.index = index + m.key = key + m.ops = ops + m.result = result + m.mode = mode[0] + return reqErr +} + +func TestConnectorUpdateTyped(t *testing.T) { + m := &updateTypedMock{} + c := NewConnectorAdapter(m, testMode) + + err := c.UpdateTyped(reqSpace, reqIndex, reqKey, reqOps, reqResult) + + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqIndex, m.index, "unexpected index was passed") + require.Equalf(t, reqKey, m.key, "unexpected key was passed") + require.Equalf(t, reqOps, m.ops, "unexpected ops was passed") + require.Equalf(t, reqResult, m.result, "unexpected result was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type updateAsyncMock struct { + baseRequestMock +} + +func (m *updateAsyncMock) UpdateAsync(space, index, key, ops interface{}, + mode ...Mode) *tarantool.Future { + m.called++ + m.space = space + m.index = index + m.key = key + m.ops = ops + m.mode = mode[0] + return reqFuture +} + +func TestConnectorUpdateAsync(t *testing.T) { + m := &updateAsyncMock{} + c := NewConnectorAdapter(m, testMode) + + fut := c.UpdateAsync(reqSpace, reqIndex, reqKey, reqOps) + + require.Equalf(t, reqFuture, fut, "unexpected future") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqIndex, m.index, "unexpected index was passed") + require.Equalf(t, reqKey, m.key, "unexpected key was passed") + require.Equalf(t, reqOps, m.ops, "unexpected ops was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type upsertMock struct { + baseRequestMock +} + +func (m *upsertMock) Upsert(space, tuple, ops interface{}, + mode ...Mode) (*tarantool.Response, error) { + m.called++ + m.space = space + m.tuple = tuple + m.ops = ops + m.mode = mode[0] + return reqResp, reqErr +} + +func TestConnectorUpsert(t *testing.T) { + m := &upsertMock{} + c := NewConnectorAdapter(m, testMode) + + resp, err := c.Upsert(reqSpace, reqTuple, reqOps) + + require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") + require.Equalf(t, reqOps, m.ops, "unexpected ops was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type upsertAsyncMock struct { + baseRequestMock +} + +func (m *upsertAsyncMock) UpsertAsync(space, tuple, ops interface{}, + mode ...Mode) *tarantool.Future { + m.called++ + m.space = space + m.tuple = tuple + m.ops = ops + m.mode = mode[0] + return reqFuture +} + +func TestConnectorUpsertAsync(t *testing.T) { + m := &upsertAsyncMock{} + c := NewConnectorAdapter(m, testMode) + + fut := c.UpsertAsync(reqSpace, reqTuple, reqOps) + + require.Equalf(t, reqFuture, fut, "unexpected future") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqSpace, m.space, "unexpected space was passed") + require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") + require.Equalf(t, reqOps, m.ops, "unexpected ops was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type baseCallMock struct { + baseRequestMock +} + +func (m *baseCallMock) call(functionName string, args interface{}, + mode Mode) (*tarantool.Response, error) { + m.called++ + m.functionName = functionName + m.args = args + m.mode = mode + return reqResp, reqErr +} + +func (m *baseCallMock) callTyped(functionName string, args interface{}, + result interface{}, mode Mode) error { + m.called++ + m.functionName = functionName + m.args = args + m.result = result + m.mode = mode + return reqErr +} + +func (m *baseCallMock) callAsync(functionName string, args interface{}, + mode Mode) *tarantool.Future { + m.called++ + m.functionName = functionName + m.args = args + m.mode = mode + return reqFuture +} + +type callMock struct { + baseCallMock +} + +func (m *callMock) Call(functionName string, args interface{}, + mode Mode) (*tarantool.Response, error) { + return m.call(functionName, args, mode) +} + +func TestConnectorCall(t *testing.T) { + m := &callMock{} + c := NewConnectorAdapter(m, testMode) + + resp, err := c.Call(reqFunctionName, reqArgs) + + require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected functionName was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type callTypedMock struct { + baseCallMock +} + +func (m *callTypedMock) CallTyped(functionName string, args interface{}, + result interface{}, mode Mode) error { + return m.callTyped(functionName, args, result, mode) +} + +func TestConnectorCallTyped(t *testing.T) { + m := &callTypedMock{} + c := NewConnectorAdapter(m, testMode) + + err := c.CallTyped(reqFunctionName, reqArgs, reqResult) + + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected functionName was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, reqResult, m.result, "unexpected result was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type callAsyncMock struct { + baseCallMock +} + +func (m *callAsyncMock) CallAsync(functionName string, args interface{}, + mode Mode) *tarantool.Future { + return m.callAsync(functionName, args, mode) +} + +func TestConnectorCallAsync(t *testing.T) { + m := &callAsyncMock{} + c := NewConnectorAdapter(m, testMode) + + fut := c.CallAsync(reqFunctionName, reqArgs) + + require.Equalf(t, reqFuture, fut, "unexpected future") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected functionName was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type call16Mock struct { + baseCallMock +} + +func (m *call16Mock) Call16(functionName string, args interface{}, + mode Mode) (*tarantool.Response, error) { + return m.call(functionName, args, mode) +} + +func TestConnectorCall16(t *testing.T) { + m := &call16Mock{} + c := NewConnectorAdapter(m, testMode) + + resp, err := c.Call16(reqFunctionName, reqArgs) + + require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected functionName was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type call16TypedMock struct { + baseCallMock +} + +func (m *call16TypedMock) Call16Typed(functionName string, args interface{}, + result interface{}, mode Mode) error { + return m.callTyped(functionName, args, result, mode) +} + +func TestConnectorCall16Typed(t *testing.T) { + m := &call16TypedMock{} + c := NewConnectorAdapter(m, testMode) + + err := c.Call16Typed(reqFunctionName, reqArgs, reqResult) + + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected functionName was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, reqResult, m.result, "unexpected result was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type call16AsyncMock struct { + baseCallMock +} + +func (m *call16AsyncMock) Call16Async(functionName string, args interface{}, + mode Mode) *tarantool.Future { + return m.callAsync(functionName, args, mode) +} + +func TestConnectorCall16Async(t *testing.T) { + m := &call16AsyncMock{} + c := NewConnectorAdapter(m, testMode) + + fut := c.Call16Async(reqFunctionName, reqArgs) + + require.Equalf(t, reqFuture, fut, "unexpected future") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected functionName was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type call17Mock struct { + baseCallMock +} + +func (m *call17Mock) Call17(functionName string, args interface{}, + mode Mode) (*tarantool.Response, error) { + return m.call(functionName, args, mode) +} + +func TestConnectorCall17(t *testing.T) { + m := &call17Mock{} + c := NewConnectorAdapter(m, testMode) + + resp, err := c.Call17(reqFunctionName, reqArgs) + + require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected functionName was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type call17TypedMock struct { + baseCallMock +} + +func (m *call17TypedMock) Call17Typed(functionName string, args interface{}, + result interface{}, mode Mode) error { + return m.callTyped(functionName, args, result, mode) +} + +func TestConnectorCall17Typed(t *testing.T) { + m := &call17TypedMock{} + c := NewConnectorAdapter(m, testMode) + + err := c.Call17Typed(reqFunctionName, reqArgs, reqResult) + + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected functionName was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, reqResult, m.result, "unexpected result was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type call17AsyncMock struct { + baseCallMock +} + +func (m *call17AsyncMock) Call17Async(functionName string, args interface{}, + mode Mode) *tarantool.Future { + return m.callAsync(functionName, args, mode) +} + +func TestConnectorCall17Async(t *testing.T) { + m := &call17AsyncMock{} + c := NewConnectorAdapter(m, testMode) + + fut := c.Call17Async(reqFunctionName, reqArgs) + + require.Equalf(t, reqFuture, fut, "unexpected future") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected functionName was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type evalMock struct { + baseCallMock +} + +func (m *evalMock) Eval(functionName string, args interface{}, + mode Mode) (*tarantool.Response, error) { + return m.call(functionName, args, mode) +} + +func TestConnectorEval(t *testing.T) { + m := &evalMock{} + c := NewConnectorAdapter(m, testMode) + + resp, err := c.Eval(reqFunctionName, reqArgs) + + require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected expr was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type evalTypedMock struct { + baseCallMock +} + +func (m *evalTypedMock) EvalTyped(functionName string, args interface{}, + result interface{}, mode Mode) error { + return m.callTyped(functionName, args, result, mode) +} + +func TestConnectorEvalTyped(t *testing.T) { + m := &evalTypedMock{} + c := NewConnectorAdapter(m, testMode) + + err := c.EvalTyped(reqFunctionName, reqArgs, reqResult) + + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected expr was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, reqResult, m.result, "unexpected result was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type evalAsyncMock struct { + baseCallMock +} + +func (m *evalAsyncMock) EvalAsync(functionName string, args interface{}, + mode Mode) *tarantool.Future { + return m.callAsync(functionName, args, mode) +} + +func TestConnectorEvalAsync(t *testing.T) { + m := &evalAsyncMock{} + c := NewConnectorAdapter(m, testMode) + + fut := c.EvalAsync(reqFunctionName, reqArgs) + + require.Equalf(t, reqFuture, fut, "unexpected future") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected expr was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type executeMock struct { + baseCallMock +} + +func (m *executeMock) Execute(functionName string, args interface{}, + mode Mode) (*tarantool.Response, error) { + return m.call(functionName, args, mode) +} + +func TestConnectorExecute(t *testing.T) { + m := &executeMock{} + c := NewConnectorAdapter(m, testMode) + + resp, err := c.Execute(reqFunctionName, reqArgs) + + require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected expr was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type executeTypedMock struct { + baseCallMock +} + +func (m *executeTypedMock) ExecuteTyped(functionName string, args, result interface{}, + mode Mode) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) { + m.callTyped(functionName, args, result, mode) + return reqSqlInfo, reqMeta, reqErr +} + +func TestConnectorExecuteTyped(t *testing.T) { + m := &executeTypedMock{} + c := NewConnectorAdapter(m, testMode) + + info, meta, err := c.ExecuteTyped(reqFunctionName, reqArgs, reqResult) + + require.Equalf(t, reqSqlInfo, info, "unexpected info") + require.Equalf(t, reqMeta, meta, "unexpected meta") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected expr was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, reqResult, m.result, "unexpected result was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +type executeAsyncMock struct { + baseCallMock +} + +func (m *executeAsyncMock) ExecuteAsync(functionName string, args interface{}, + mode Mode) *tarantool.Future { + return m.callAsync(functionName, args, mode) +} + +func TestConnectorExecuteAsync(t *testing.T) { + m := &executeAsyncMock{} + c := NewConnectorAdapter(m, testMode) + + fut := c.ExecuteAsync(reqFunctionName, reqArgs) + + require.Equalf(t, reqFuture, fut, "unexpected future") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.functionName, + "unexpected expr was passed") + require.Equalf(t, reqArgs, m.args, "unexpected args was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +var reqPrepared *tarantool.Prepared = &tarantool.Prepared{} + +type newPreparedMock struct { + Pooler + called int + expr string + mode Mode +} + +func (m *newPreparedMock) NewPrepared(expr string, + mode Mode) (*tarantool.Prepared, error) { + m.called++ + m.expr = expr + m.mode = mode + return reqPrepared, reqErr +} + +func TestConnectorNewPrepared(t *testing.T) { + m := &newPreparedMock{} + c := NewConnectorAdapter(m, testMode) + + p, err := c.NewPrepared(reqFunctionName) + + require.Equalf(t, reqPrepared, p, "unexpected prepared") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqFunctionName, m.expr, + "unexpected expr was passed") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +var reqStream *tarantool.Stream = &tarantool.Stream{} + +type newStreamMock struct { + Pooler + called int + mode Mode +} + +func (m *newStreamMock) NewStream(mode Mode) (*tarantool.Stream, error) { + m.called++ + m.mode = mode + return reqStream, reqErr +} + +func TestConnectorNewStream(t *testing.T) { + m := &newStreamMock{} + c := NewConnectorAdapter(m, testMode) + + s, err := c.NewStream() + + require.Equalf(t, reqStream, s, "unexpected stream") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + +var reqRequest tarantool.Request = tarantool.NewPingRequest() + +type doMock struct { + Pooler + called int + req tarantool.Request + mode Mode +} + +func (m *doMock) Do(req tarantool.Request, mode Mode) *tarantool.Future { + m.called++ + m.req = req + m.mode = mode + return reqFuture +} + +func TestConnectorDo(t *testing.T) { + m := &doMock{} + c := NewConnectorAdapter(m, testMode) + + fut := c.Do(reqRequest) + + require.Equalf(t, reqFuture, fut, "unexpected future") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqRequest, m.req, "unexpected request") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go index 9328c616a..9a486924a 100644 --- a/connection_pool/example_test.go +++ b/connection_pool/example_test.go @@ -834,3 +834,24 @@ func ExampleBeginRequest_TxnIsolation() { } fmt.Printf("Select after Rollback: response is %#v\n", resp.Data) } + +func ExampleConnectorAdapter() { + pool, err := examplePool(testRoles) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + adapter := connection_pool.NewConnectorAdapter(pool, connection_pool.RW) + var connector tarantool.Connector = adapter + + // Ping an RW instance to check connection. + resp, err := connector.Ping() + fmt.Println("Ping Code", resp.Code) + fmt.Println("Ping Data", resp.Data) + fmt.Println("Ping Error", err) + // Output: + // Ping Code 0 + // Ping Data [] + // Ping Error +} diff --git a/connection_pool/pooler.go b/connection_pool/pooler.go new file mode 100644 index 000000000..a9dbe09f9 --- /dev/null +++ b/connection_pool/pooler.go @@ -0,0 +1,89 @@ +package connection_pool + +import ( + "time" + + "github.com/tarantool/go-tarantool" +) + +// Pooler is the interface that must be implemented by a connection pool. +type Pooler interface { + ConnectedNow(mode Mode) (bool, error) + Close() []error + Ping(mode Mode) (*tarantool.Response, error) + ConfiguredTimeout(mode Mode) (time.Duration, error) + + Select(space, index interface{}, offset, limit, iterator uint32, + key interface{}, mode ...Mode) (*tarantool.Response, error) + Insert(space interface{}, tuple interface{}, + mode ...Mode) (*tarantool.Response, error) + Replace(space interface{}, tuple interface{}, + mode ...Mode) (*tarantool.Response, error) + Delete(space, index interface{}, key interface{}, + mode ...Mode) (*tarantool.Response, error) + Update(space, index interface{}, key, ops interface{}, + mode ...Mode) (*tarantool.Response, error) + Upsert(space interface{}, tuple, ops interface{}, + mode ...Mode) (*tarantool.Response, error) + Call(functionName string, args interface{}, + mode Mode) (*tarantool.Response, error) + Call16(functionName string, args interface{}, + mode Mode) (*tarantool.Response, error) + Call17(functionName string, args interface{}, + mode Mode) (*tarantool.Response, error) + Eval(expr string, args interface{}, + mode Mode) (*tarantool.Response, error) + Execute(expr string, args interface{}, + mode Mode) (*tarantool.Response, error) + + GetTyped(space, index interface{}, key interface{}, result interface{}, + mode ...Mode) error + SelectTyped(space, index interface{}, offset, limit, iterator uint32, + key interface{}, result interface{}, mode ...Mode) error + InsertTyped(space interface{}, tuple interface{}, result interface{}, + mode ...Mode) error + ReplaceTyped(space interface{}, tuple interface{}, result interface{}, + mode ...Mode) error + DeleteTyped(space, index interface{}, key interface{}, result interface{}, + mode ...Mode) error + UpdateTyped(space, index interface{}, key, ops interface{}, + result interface{}, mode ...Mode) error + CallTyped(functionName string, args interface{}, result interface{}, + mode Mode) error + Call16Typed(functionName string, args interface{}, result interface{}, + mode Mode) error + Call17Typed(functionName string, args interface{}, result interface{}, + mode Mode) error + EvalTyped(expr string, args interface{}, result interface{}, + mode Mode) error + ExecuteTyped(expr string, args interface{}, result interface{}, + mode Mode) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) + + SelectAsync(space, index interface{}, offset, limit, iterator uint32, + key interface{}, mode ...Mode) *tarantool.Future + InsertAsync(space interface{}, tuple interface{}, + mode ...Mode) *tarantool.Future + ReplaceAsync(space interface{}, tuple interface{}, + mode ...Mode) *tarantool.Future + DeleteAsync(space, index interface{}, key interface{}, + mode ...Mode) *tarantool.Future + UpdateAsync(space, index interface{}, key, ops interface{}, + mode ...Mode) *tarantool.Future + UpsertAsync(space interface{}, tuple interface{}, ops interface{}, + mode ...Mode) *tarantool.Future + CallAsync(functionName string, args interface{}, + mode Mode) *tarantool.Future + Call16Async(functionName string, args interface{}, + mode Mode) *tarantool.Future + Call17Async(functionName string, args interface{}, + mode Mode) *tarantool.Future + EvalAsync(expr string, args interface{}, + mode Mode) *tarantool.Future + ExecuteAsync(expr string, args interface{}, + mode Mode) *tarantool.Future + + NewPrepared(expr string, mode Mode) (*tarantool.Prepared, error) + NewStream(mode Mode) (*tarantool.Stream, error) + + Do(req tarantool.Request, mode Mode) (fut *tarantool.Future) +} From 05c418fbd97a552ddb070c1c325da6f1bc20539b Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 12 Sep 2022 16:03:08 +0300 Subject: [PATCH 343/605] queue: bump package version to 1.2.1 It fixes queue.cfg({in_replicaset = true}) for Tarantool 1.10 [1]. 1. https://github.com/tarantool/queue/issues/185 Part of #176 --- CHANGELOG.md | 2 ++ Makefile | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e0303af8..fe991b84b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Changed +- Bump queue package version to 1.2.1 (#176) + ### Fixed - Mode type description in the connection_pool subpackage (#208) diff --git a/Makefile b/Makefile index efe0d668c..59961774b 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ clean: .PHONY: deps deps: clean - ( cd ./queue; tarantoolctl rocks install queue 1.2.0 ) + ( cd ./queue; tarantoolctl rocks install queue 1.2.1 ) .PHONY: datetime-timezones datetime-timezones: From 2603eda2823ae7a506c900de3640291401ad569c Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 25 Aug 2022 13:49:13 +0300 Subject: [PATCH 344/605] queue: add an example with connection_pool The example demonstrates how to use the queue package with the connection_pool package. Closes #176 --- CHANGELOG.md | 1 + connection_pool/connection_pool_test.go | 2 +- multi/multi_test.go | 2 +- queue/example_connection_pool_test.go | 206 ++++++++++++++++++++++++ queue/queue_test.go | 39 ++++- queue/{ => testdata}/config.lua | 14 +- queue/testdata/pool.lua | 56 +++++++ test_helpers/main.go | 7 +- 8 files changed, 311 insertions(+), 16 deletions(-) create mode 100644 queue/example_connection_pool_test.go rename queue/{ => testdata}/config.lua (81%) create mode 100644 queue/testdata/pool.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index fe991b84b..f69e97128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ConnectionPool (#178) - Execute, ExecuteTyped and ExecuteAsync methods to ConnectionPool (#176) - ConnectorAdapter type to use ConnectionPool as Connector interface (#176) +- An example how to use queue and connection_pool subpackages together (#176) ### Changed diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index b3aee615d..091d5216c 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -2056,7 +2056,7 @@ func TestStream_TxnIsolationLevel(t *testing.T) { func runTestMain(m *testing.M) int { initScript := "config.lua" waitStart := 100 * time.Millisecond - var connectRetry uint = 3 + connectRetry := 3 retryTimeout := 500 * time.Millisecond workDirs := []string{ "work_dir1", "work_dir2", diff --git a/multi/multi_test.go b/multi/multi_test.go index 643ea186d..2d43bb179 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -556,7 +556,7 @@ func TestStream_Rollback(t *testing.T) { func runTestMain(m *testing.M) int { initScript := "config.lua" waitStart := 100 * time.Millisecond - var connectRetry uint = 3 + connectRetry := 3 retryTimeout := 500 * time.Millisecond // Tarantool supports streams and interactive transactions since version 2.10.0 diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go new file mode 100644 index 000000000..59f5020ea --- /dev/null +++ b/queue/example_connection_pool_test.go @@ -0,0 +1,206 @@ +package queue_test + +import ( + "fmt" + "sync" + "time" + + "github.com/google/uuid" + "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/connection_pool" + "github.com/tarantool/go-tarantool/queue" + "github.com/tarantool/go-tarantool/test_helpers" +) + +// QueueConnectionHandler handles new connections in a ConnectionPool. +type QueueConnectionHandler struct { + name string + cfg queue.Cfg + + uuid uuid.UUID + registered bool + err error + mutex sync.Mutex + masterUpdated chan struct{} +} + +// QueueConnectionHandler implements the ConnectionHandler interface. +var _ connection_pool.ConnectionHandler = &QueueConnectionHandler{} + +// NewQueueConnectionHandler creates a QueueConnectionHandler object. +func NewQueueConnectionHandler(name string, cfg queue.Cfg) *QueueConnectionHandler { + return &QueueConnectionHandler{ + name: name, + cfg: cfg, + masterUpdated: make(chan struct{}, 10), + } +} + +// Discovered configures a queue for an instance and identifies a shared queue +// session on master instances. +// +// NOTE: the Queue supports only a master-replica cluster configuration. It +// does not support a master-master configuration. +func (h *QueueConnectionHandler) Discovered(conn *tarantool.Connection, + role connection_pool.Role) error { + h.mutex.Lock() + defer h.mutex.Unlock() + + if h.err != nil { + return h.err + } + + master := role == connection_pool.MasterRole + if master { + defer func() { + h.masterUpdated <- struct{}{} + }() + } + + // Set up a queue module configuration for an instance. + q := queue.New(conn, h.name) + opts := queue.CfgOpts{InReplicaset: true, Ttr: 60 * time.Second} + + if h.err = q.Cfg(opts); h.err != nil { + return fmt.Errorf("unable to configure queue: %w", h.err) + } + + // The queue only works with a master instance. + if !master { + return nil + } + + if h.err = q.Create(h.cfg); h.err != nil { + return h.err + } + + if !h.registered { + // We register a shared session at the first time. + if h.uuid, h.err = q.Identify(nil); h.err != nil { + return h.err + } + h.registered = true + } else { + // We re-identify as the shared session. + if _, h.err = q.Identify(&h.uuid); h.err != nil { + return h.err + } + } + + fmt.Printf("Master %s is ready to work!\n", conn.Addr()) + + return nil +} + +// Deactivated doesn't do anything useful for the example. +func (h *QueueConnectionHandler) Deactivated(conn *tarantool.Connection, + role connection_pool.Role) error { + return nil +} + +// Closes closes a QueueConnectionHandler object. +func (h *QueueConnectionHandler) Close() { + close(h.masterUpdated) +} + +// Example demonstrates how to use the queue package with the connection_pool +// package. First of all, you need to create a ConnectionHandler implementation +// for the a ConnectionPool object to process new connections from +// RW-instances. +// +// You need to register a shared session UUID at a first master connection. +// It needs to be used to re-identify as the shared session on new +// RW-instances. See QueueConnectionHandler.Discovered() implementation. +// +// After that, you need to create a ConnectorAdapter object with RW mode for +// the ConnectionPool to send requests into RW-instances. This adapter can +// be used to create a ready-to-work queue object. +func Example_connectionPool() { + // Create a ConnectionHandler object. + cfg := queue.Cfg{ + Temporary: false, + IfNotExists: true, + Kind: queue.FIFO, + Opts: queue.Opts{ + Ttl: 10 * time.Second, + }, + } + h := NewQueueConnectionHandler("test_queue", cfg) + defer h.Close() + + // Create a ConnectionPool object. + servers := []string{ + "127.0.0.1:3014", + "127.0.0.1:3015", + } + connOpts := tarantool.Opts{ + Timeout: 1 * time.Second, + User: "test", + Pass: "test", + } + poolOpts := connection_pool.OptsPool{ + CheckTimeout: 1 * time.Second, + ConnectionHandler: h, + } + connPool, err := connection_pool.ConnectWithOpts(servers, connOpts, poolOpts) + if err != nil { + fmt.Printf("Unable to connect to the pool: %s", err) + return + } + defer connPool.Close() + + // Wait for a master instance identification in the queue. + <-h.masterUpdated + if h.err != nil { + fmt.Printf("Unable to identify in the pool: %s", h.err) + return + } + + // Create a Queue object from the ConnectionPool object via + // a ConnectorAdapter. + rw := connection_pool.NewConnectorAdapter(connPool, connection_pool.RW) + q := queue.New(rw, "test_queue") + fmt.Println("A Queue object is ready to work.") + + testData := "test_data" + fmt.Println("Send data:", testData) + if _, err = q.Put(testData); err != nil { + fmt.Printf("Unable to put data into the queue: %s", err) + return + } + + // Switch a master instance in the pool. + roles := []bool{true, false} + err = test_helpers.SetClusterRO(servers, connOpts, roles) + if err != nil { + fmt.Printf("Unable to set cluster roles: %s", err) + return + } + + // Wait for a new master instance re-identification. + <-h.masterUpdated + if h.err != nil { + fmt.Printf("Unable to re-identify in the pool: %s", h.err) + return + } + + // Take a data from the new master instance. + task, err := q.TakeTimeout(1 * time.Second) + if err != nil { + fmt.Println("Unable to got task:", err) + } else if task == nil { + fmt.Println("task == nil") + } else if task.Data() == nil { + fmt.Println("task.Data() == nil") + } else { + task.Ack() + fmt.Println("Got data:", task.Data()) + } + + // Output: + // Master 127.0.0.1:3014 is ready to work! + // A Queue object is ready to work. + // Send data: test_data + // Master 127.0.0.1:3015 is ready to work! + // Got data: test_data +} diff --git a/queue/queue_test.go b/queue/queue_test.go index 7b19c0dd3..85276e4a8 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -14,6 +14,13 @@ import ( ) var server = "127.0.0.1:3013" +var serversPool = []string{ + "127.0.0.1:3014", + "127.0.0.1:3015", +} + +var instances []test_helpers.TarantoolInstance + var opts = Opts{ Timeout: 2500 * time.Millisecond, User: "test", @@ -890,7 +897,7 @@ func TestTask_Touch(t *testing.T) { // https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls func runTestMain(m *testing.M) int { inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ - InitScript: "config.lua", + InitScript: "testdata/config.lua", Listen: server, WorkDir: "work_dir", User: opts.User, @@ -899,12 +906,40 @@ func runTestMain(m *testing.M) int { ConnectRetry: 3, RetryTimeout: 500 * time.Millisecond, }) - defer test_helpers.StopTarantoolWithCleanup(inst) if err != nil { log.Fatalf("Failed to prepare test tarantool: %s", err) } + defer test_helpers.StopTarantoolWithCleanup(inst) + + workDirs := []string{"work_dir1", "work_dir2"} + poolOpts := test_helpers.StartOpts{ + InitScript: "testdata/pool.lua", + User: opts.User, + Pass: opts.Pass, + WaitStart: 3 * time.Second, // replication_timeout * 3 + ConnectRetry: -1, + } + instances, err = test_helpers.StartTarantoolInstances(serversPool, workDirs, poolOpts) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool pool: %s", err) + } + + defer test_helpers.StopTarantoolInstances(instances) + + roles := []bool{false, true} + connOpts := Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", + } + err = test_helpers.SetClusterRO(serversPool, connOpts, roles) + + if err != nil { + log.Fatalf("Failed to set roles in tarantool pool: %s", err) + } return m.Run() } diff --git a/queue/config.lua b/queue/testdata/config.lua similarity index 81% rename from queue/config.lua rename to queue/testdata/config.lua index df28496a3..eccb19a68 100644 --- a/queue/config.lua +++ b/queue/testdata/config.lua @@ -7,7 +7,7 @@ box.cfg{ work_dir = os.getenv("TEST_TNT_WORK_DIR"), } - box.once("init", function() +box.once("init", function() box.schema.user.create('test', {password = 'test'}) box.schema.func.create('queue.tube.test_queue:touch') box.schema.func.create('queue.tube.test_queue:ack') @@ -23,21 +23,15 @@ box.cfg{ box.schema.func.create('queue.identify') box.schema.func.create('queue.state') box.schema.func.create('queue.statistics') - box.schema.user.grant('test', 'create', 'space') - box.schema.user.grant('test', 'write', 'space', '_schema') - box.schema.user.grant('test', 'write', 'space', '_space') - box.schema.user.grant('test', 'read,write', 'space', '_space_sequence') - box.schema.user.grant('test', 'write', 'space', '_index') + box.schema.user.grant('test', 'create,read,write,drop', 'space') box.schema.user.grant('test', 'read, write', 'space', '_queue_session_ids') box.schema.user.grant('test', 'execute', 'universe') box.schema.user.grant('test', 'read,write', 'space', '_queue') box.schema.user.grant('test', 'read,write', 'space', '_schema') + box.schema.user.grant('test', 'read,write', 'space', '_space_sequence') box.schema.user.grant('test', 'read,write', 'space', '_space') box.schema.user.grant('test', 'read,write', 'space', '_index') - box.schema.user.grant('test', 'read,write', 'space', '_queue_consumers') box.schema.user.grant('test', 'read,write', 'space', '_priv') - box.schema.user.grant('test', 'read,write', 'space', '_queue_taken_2') - box.schema.user.grant('test', 'read,write', 'space', '_queue_shared_sessions') if box.space._trigger ~= nil then box.schema.user.grant('test', 'read', 'space', '_trigger') end @@ -56,3 +50,5 @@ end) box.cfg{ listen = os.getenv("TEST_TNT_LISTEN"), } + +require('console').start() diff --git a/queue/testdata/pool.lua b/queue/testdata/pool.lua new file mode 100644 index 000000000..7c63aa787 --- /dev/null +++ b/queue/testdata/pool.lua @@ -0,0 +1,56 @@ +local queue = require('queue') +rawset(_G, 'queue', queue) + +local listen = os.getenv("TEST_TNT_LISTEN") +box.cfg{ + work_dir = os.getenv("TEST_TNT_WORK_DIR"), + listen = listen, + replication = { + "test:test@127.0.0.1:3014", + "test:test@127.0.0.1:3015", + }, + read_only = listen == "127.0.0.1:3015" +} + +box.once("schema", function() + box.schema.user.create('test', {password = 'test'}) + box.schema.user.grant('test', 'replication') + + box.schema.func.create('queue.tube.test_queue:touch') + box.schema.func.create('queue.tube.test_queue:ack') + box.schema.func.create('queue.tube.test_queue:put') + box.schema.func.create('queue.tube.test_queue:drop') + box.schema.func.create('queue.tube.test_queue:peek') + box.schema.func.create('queue.tube.test_queue:kick') + box.schema.func.create('queue.tube.test_queue:take') + box.schema.func.create('queue.tube.test_queue:delete') + box.schema.func.create('queue.tube.test_queue:release') + box.schema.func.create('queue.tube.test_queue:release_all') + box.schema.func.create('queue.tube.test_queue:bury') + box.schema.func.create('queue.identify') + box.schema.func.create('queue.state') + box.schema.func.create('queue.statistics') + box.schema.user.grant('test', 'create,read,write,drop', 'space') + box.schema.user.grant('test', 'read, write', 'space', '_queue_session_ids') + box.schema.user.grant('test', 'execute', 'universe') + box.schema.user.grant('test', 'read,write', 'space', '_queue') + box.schema.user.grant('test', 'read,write', 'space', '_schema') + box.schema.user.grant('test', 'read,write', 'space', '_space_sequence') + box.schema.user.grant('test', 'read,write', 'space', '_space') + box.schema.user.grant('test', 'read,write', 'space', '_index') + box.schema.user.grant('test', 'read,write', 'space', '_priv') + if box.space._trigger ~= nil then + box.schema.user.grant('test', 'read', 'space', '_trigger') + end + if box.space._fk_constraint ~= nil then + box.schema.user.grant('test', 'read', 'space', '_fk_constraint') + end + if box.space._ck_constraint ~= nil then + box.schema.user.grant('test', 'read', 'space', '_ck_constraint') + end + if box.space._func_index ~= nil then + box.schema.user.grant('test', 'read', 'space', '_func_index') + end +end) + +require('console').start() diff --git a/test_helpers/main.go b/test_helpers/main.go index cdc3c343d..77e22c535 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -67,8 +67,9 @@ type StartOpts struct { // WaitStart is a time to wait before starting to ping tarantool. WaitStart time.Duration - // ConnectRetry is a count of attempts to ping tarantool. - ConnectRetry uint + // ConnectRetry is a count of retry attempts to ping tarantool. If the + // value < 0 then there will be no ping tarantool at all. + ConnectRetry int // RetryTimeout is a time between tarantool ping retries. RetryTimeout time.Duration @@ -240,7 +241,7 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { Ssl: startOpts.ClientSsl, } - var i uint + var i int var server string if startOpts.ClientServer != "" { server = startOpts.ClientServer From 49fbabbb0c618b3d039cef7b5b4bf5fd71d87f0a Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 28 Sep 2022 11:20:03 +0300 Subject: [PATCH 345/605] bugfix: decimal use a test function The patch replaces usage of a test function GetNumberLength by a package-level function getNumberLength in the decimal package code. --- CHANGELOG.md | 2 ++ decimal/bcd.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f69e97128..a058a81ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - A connection is still opened after ConnectionPool.Close() (#208) - Future.GetTyped() after Future.Get() does not decode response correctly (#213) +- Decimal package use a test function GetNumberLength instead of a + package-level function getNumberLength (#219) ## [1.8.0] - 2022-08-17 diff --git a/decimal/bcd.go b/decimal/bcd.go index 2cdecb5f0..2b5a4b258 100644 --- a/decimal/bcd.go +++ b/decimal/bcd.go @@ -117,7 +117,7 @@ func encodeStringToBCD(buf string) ([]byte, error) { // number of digits. Therefore highNibble is false when decimal number // is even. highNibble := true - l := GetNumberLength(buf) + l := getNumberLength(buf) if l%2 == 0 { highNibble = false } From 64e41c5798b0de75ab97d527afb67abdc40760d1 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 15 Sep 2022 18:11:22 +0300 Subject: [PATCH 346/605] bugfix: unequal timezone after decoding The patch fixes unequal timezones after Datetime encoding/decoding. It does two things for the fix: 1. After the patch, a location name is picked from Time.Location().String() instead of Time.Zone(). It allows us to encode a timezone from the original name. 2. A decoder function tries to load a location from system location. It allows to handle unfixed timezones properly. Closes #217 --- CHANGELOG.md | 1 + datetime/datetime.go | 29 +++++++- datetime/datetime_test.go | 143 ++++++++++++++++++++++---------------- datetime/example_test.go | 52 ++++++++++++++ 4 files changed, 162 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a058a81ae..f2636e12a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. correctly (#213) - Decimal package use a test function GetNumberLength instead of a package-level function getNumberLength (#219) +- Datetime location after encode + decode is unequal (#217) ## [1.8.0] - 2022-08-17 diff --git a/datetime/datetime.go b/datetime/datetime.go index cf3ac8758..0fe20f572 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -96,6 +96,9 @@ const ( // supported range: [-5879610-06-22T00:00Z .. 5879611-07-11T00:00Z] or // an invalid timezone or offset value is out of supported range: // [-12 * 60 * 60, 14 * 60 * 60]. +// +// NOTE: Tarantool's datetime.tz value is picked from t.Location().String(). +// "Local" location is unsupported, see ExampleNewDatetime_localUnsupported. func NewDatetime(t time.Time) (*Datetime, error) { seconds := t.Unix() @@ -103,13 +106,15 @@ func NewDatetime(t time.Time) (*Datetime, error) { return nil, fmt.Errorf("time %s is out of supported range", t) } - zone, offset := t.Zone() + zone := t.Location().String() + _, offset := t.Zone() if zone != NoTimezone { if _, ok := timezoneToIndex[zone]; !ok { return nil, fmt.Errorf("unknown timezone %s with offset %d", zone, offset) } } + if offset < offsetMin || offset > offsetMax { return nil, fmt.Errorf("offset must be between %d and %d hours", offsetMin, offsetMax) @@ -219,6 +224,13 @@ func (dtime *Datetime) Interval(next *Datetime) Interval { } // ToTime returns a time.Time that Datetime contains. +// +// If a Datetime created from time.Time value then an original location is used +// for the time value. +// +// If a Datetime created via unmarshaling Tarantool's datetime then we try to +// create a location with time.LoadLocation() first. In case of failure, we use +// a location created with time.FixedZone(). func (dtime *Datetime) ToTime() time.Time { return dtime.time } @@ -230,7 +242,8 @@ func (dtime *Datetime) MarshalMsgpack() ([]byte, error) { dt.seconds = tm.Unix() dt.nsec = int32(tm.Nanosecond()) - zone, offset := tm.Zone() + zone := tm.Location().String() + _, offset := tm.Zone() if zone != NoTimezone { // The zone value already checked in NewDatetime() or // UnmarshalMsgpack() calls. @@ -283,7 +296,17 @@ func (tm *Datetime) UnmarshalMsgpack(b []byte) error { } zone = indexToTimezone[int(dt.tzIndex)] } - loc = time.FixedZone(zone, offset) + if zone != NoTimezone { + if loadLoc, err := time.LoadLocation(zone); err == nil { + loc = loadLoc + } else { + // Unable to load location. + loc = time.FixedZone(zone, offset) + } + } else { + // Only offset. + loc = time.FixedZone(zone, offset) + } } tt = tt.In(loc) diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index c128a82d0..60afa4077 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -507,6 +507,9 @@ func TestCustomTimezone(t *testing.T) { customZone := "Europe/Moscow" customOffset := 180 * 60 + // Tarantool does not use a custom offset value if a time zone is provided. + // So it will change to an actual one. + zoneOffset := 240 * 60 customLoc := time.FixedZone(customZone, customOffset) tm, err := time.Parse(time.RFC3339, "2010-08-12T11:44:14Z") @@ -527,11 +530,12 @@ func TestCustomTimezone(t *testing.T) { tpl := resp.Data[0].([]interface{}) if respDt, ok := toDatetime(tpl[0]); ok { - zone, offset := respDt.ToTime().Zone() + zone := respDt.ToTime().Location().String() + _, offset := respDt.ToTime().Zone() if zone != customZone { t.Fatalf("Expected zone %s instead of %s", customZone, zone) } - if offset != customOffset { + if offset != zoneOffset { t.Fatalf("Expected offset %d instead of %d", customOffset, offset) } @@ -586,62 +590,63 @@ var datetimeSample = []struct { fmt string dt string mpBuf string // MessagePack buffer. + zone string }{ /* Cases for base encoding without a timezone. */ - {time.RFC3339, "2012-01-31T23:59:59.000000010Z", "d8047f80284f000000000a00000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.000000010Z", "d80400000000000000000a00000000000000"}, - {time.RFC3339, "2010-08-12T11:39:14Z", "d70462dd634c00000000"}, - {time.RFC3339, "1984-03-24T18:04:05Z", "d7041530c31a00000000"}, - {time.RFC3339, "2010-01-12T00:00:00Z", "d70480bb4b4b00000000"}, - {time.RFC3339, "1970-01-01T00:00:00Z", "d7040000000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.123456789Z", "d804000000000000000015cd5b0700000000"}, - {time.RFC3339, "1970-01-01T00:00:00.12345678Z", "d80400000000000000000ccd5b0700000000"}, - {time.RFC3339, "1970-01-01T00:00:00.1234567Z", "d8040000000000000000bccc5b0700000000"}, - {time.RFC3339, "1970-01-01T00:00:00.123456Z", "d804000000000000000000ca5b0700000000"}, - {time.RFC3339, "1970-01-01T00:00:00.12345Z", "d804000000000000000090b25b0700000000"}, - {time.RFC3339, "1970-01-01T00:00:00.1234Z", "d804000000000000000040ef5a0700000000"}, - {time.RFC3339, "1970-01-01T00:00:00.123Z", "d8040000000000000000c0d4540700000000"}, - {time.RFC3339, "1970-01-01T00:00:00.12Z", "d8040000000000000000000e270700000000"}, - {time.RFC3339, "1970-01-01T00:00:00.1Z", "d804000000000000000000e1f50500000000"}, - {time.RFC3339, "1970-01-01T00:00:00.01Z", "d80400000000000000008096980000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.001Z", "d804000000000000000040420f0000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.0001Z", "d8040000000000000000a086010000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.00001Z", "d80400000000000000001027000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.000001Z", "d8040000000000000000e803000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.0000001Z", "d80400000000000000006400000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.00000001Z", "d80400000000000000000a00000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.000000001Z", "d80400000000000000000100000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.000000009Z", "d80400000000000000000900000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.00000009Z", "d80400000000000000005a00000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.0000009Z", "d80400000000000000008403000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.000009Z", "d80400000000000000002823000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.00009Z", "d8040000000000000000905f010000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.0009Z", "d8040000000000000000a0bb0d0000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.009Z", "d80400000000000000004054890000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.09Z", "d8040000000000000000804a5d0500000000"}, - {time.RFC3339, "1970-01-01T00:00:00.9Z", "d804000000000000000000e9a43500000000"}, - {time.RFC3339, "1970-01-01T00:00:00.99Z", "d80400000000000000008033023b00000000"}, - {time.RFC3339, "1970-01-01T00:00:00.999Z", "d8040000000000000000c0878b3b00000000"}, - {time.RFC3339, "1970-01-01T00:00:00.9999Z", "d80400000000000000006043993b00000000"}, - {time.RFC3339, "1970-01-01T00:00:00.99999Z", "d8040000000000000000f0a29a3b00000000"}, - {time.RFC3339, "1970-01-01T00:00:00.999999Z", "d804000000000000000018c69a3b00000000"}, - {time.RFC3339, "1970-01-01T00:00:00.9999999Z", "d80400000000000000009cc99a3b00000000"}, - {time.RFC3339, "1970-01-01T00:00:00.99999999Z", "d8040000000000000000f6c99a3b00000000"}, - {time.RFC3339, "1970-01-01T00:00:00.999999999Z", "d8040000000000000000ffc99a3b00000000"}, - {time.RFC3339, "1970-01-01T00:00:00.0Z", "d7040000000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.00Z", "d7040000000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.000Z", "d7040000000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.0000Z", "d7040000000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.00000Z", "d7040000000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.000000Z", "d7040000000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.0000000Z", "d7040000000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.00000000Z", "d7040000000000000000"}, - {time.RFC3339, "1970-01-01T00:00:00.000000000Z", "d7040000000000000000"}, - {time.RFC3339, "1973-11-29T21:33:09Z", "d70415cd5b0700000000"}, - {time.RFC3339, "2013-10-28T17:51:56Z", "d7043ca46e5200000000"}, - {time.RFC3339, "9999-12-31T23:59:59Z", "d7047f41f4ff3a000000"}, + {time.RFC3339, "2012-01-31T23:59:59.000000010Z", "d8047f80284f000000000a00000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.000000010Z", "d80400000000000000000a00000000000000", ""}, + {time.RFC3339, "2010-08-12T11:39:14Z", "d70462dd634c00000000", ""}, + {time.RFC3339, "1984-03-24T18:04:05Z", "d7041530c31a00000000", ""}, + {time.RFC3339, "2010-01-12T00:00:00Z", "d70480bb4b4b00000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00Z", "d7040000000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.123456789Z", "d804000000000000000015cd5b0700000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.12345678Z", "d80400000000000000000ccd5b0700000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.1234567Z", "d8040000000000000000bccc5b0700000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.123456Z", "d804000000000000000000ca5b0700000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.12345Z", "d804000000000000000090b25b0700000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.1234Z", "d804000000000000000040ef5a0700000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.123Z", "d8040000000000000000c0d4540700000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.12Z", "d8040000000000000000000e270700000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.1Z", "d804000000000000000000e1f50500000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.01Z", "d80400000000000000008096980000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.001Z", "d804000000000000000040420f0000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.0001Z", "d8040000000000000000a086010000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.00001Z", "d80400000000000000001027000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.000001Z", "d8040000000000000000e803000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.0000001Z", "d80400000000000000006400000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.00000001Z", "d80400000000000000000a00000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.000000001Z", "d80400000000000000000100000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.000000009Z", "d80400000000000000000900000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.00000009Z", "d80400000000000000005a00000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.0000009Z", "d80400000000000000008403000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.000009Z", "d80400000000000000002823000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.00009Z", "d8040000000000000000905f010000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.0009Z", "d8040000000000000000a0bb0d0000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.009Z", "d80400000000000000004054890000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.09Z", "d8040000000000000000804a5d0500000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.9Z", "d804000000000000000000e9a43500000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.99Z", "d80400000000000000008033023b00000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.999Z", "d8040000000000000000c0878b3b00000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.9999Z", "d80400000000000000006043993b00000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.99999Z", "d8040000000000000000f0a29a3b00000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.999999Z", "d804000000000000000018c69a3b00000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.9999999Z", "d80400000000000000009cc99a3b00000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.99999999Z", "d8040000000000000000f6c99a3b00000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.999999999Z", "d8040000000000000000ffc99a3b00000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.0Z", "d7040000000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.00Z", "d7040000000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.000Z", "d7040000000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.0000Z", "d7040000000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.00000Z", "d7040000000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.000000Z", "d7040000000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.0000000Z", "d7040000000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.00000000Z", "d7040000000000000000", ""}, + {time.RFC3339, "1970-01-01T00:00:00.000000000Z", "d7040000000000000000", ""}, + {time.RFC3339, "1973-11-29T21:33:09Z", "d70415cd5b0700000000", ""}, + {time.RFC3339, "2013-10-28T17:51:56Z", "d7043ca46e5200000000", ""}, + {time.RFC3339, "9999-12-31T23:59:59Z", "d7047f41f4ff3a000000", ""}, /* Cases for encoding with a timezone. */ - {time.RFC3339 + " MST", "2006-01-02T15:04:00+03:00 MSK", "d804b016b9430000000000000000b400ee00"}, + {time.RFC3339, "2006-01-02T15:04:00Z", "d804e040b9430000000000000000b400b303", "Europe/Moscow"}, } func TestDatetimeInsertSelectDelete(t *testing.T) { @@ -653,8 +658,14 @@ func TestDatetimeInsertSelectDelete(t *testing.T) { for _, testcase := range datetimeSample { t.Run(testcase.dt, func(t *testing.T) { tm, err := time.Parse(testcase.fmt, testcase.dt) - if testcase.fmt == time.RFC3339 { + if testcase.zone == "" { tm = tm.In(noTimezoneLoc) + } else { + loc, err := time.LoadLocation(testcase.zone) + if err != nil { + t.Fatalf("Unable to load location: %s", err) + } + tm = tm.In(loc) } if err != nil { t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) @@ -966,7 +977,7 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - tm := time.Unix(500, 1000) + tm := time.Unix(500, 1000).In(time.FixedZone(NoTimezone, 0)) dt, err := NewDatetime(tm) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) @@ -999,8 +1010,14 @@ func TestMPEncode(t *testing.T) { for _, testcase := range datetimeSample { t.Run(testcase.dt, func(t *testing.T) { tm, err := time.Parse(testcase.fmt, testcase.dt) - if testcase.fmt == time.RFC3339 { + if testcase.zone == "" { tm = tm.In(noTimezoneLoc) + } else { + loc, err := time.LoadLocation(testcase.zone) + if err != nil { + t.Fatalf("Unable to load location: %s", err) + } + tm = tm.In(loc) } if err != nil { t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) @@ -1016,7 +1033,7 @@ func TestMPEncode(t *testing.T) { refBuf, _ := hex.DecodeString(testcase.mpBuf) if reflect.DeepEqual(buf, refBuf) != true { t.Fatalf("Failed to encode datetime '%s', actual %x, expected %x", - testcase.dt, + tm, buf, refBuf) } @@ -1028,8 +1045,14 @@ func TestMPDecode(t *testing.T) { for _, testcase := range datetimeSample { t.Run(testcase.dt, func(t *testing.T) { tm, err := time.Parse(testcase.fmt, testcase.dt) - if testcase.fmt == time.RFC3339 { + if testcase.zone == "" { tm = tm.In(noTimezoneLoc) + } else { + loc, err := time.LoadLocation(testcase.zone) + if err != nil { + t.Fatalf("Unable to load location: %s", err) + } + tm = tm.In(loc) } if err != nil { t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) diff --git a/datetime/example_test.go b/datetime/example_test.go index ca1603ea8..50725e6a3 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -80,6 +80,22 @@ func Example() { fmt.Printf("Data: %v\n", respDt.ToTime()) } +// ExampleNewDatetime_localUnsupported demonstrates that "Local" location is +// unsupported. +func ExampleNewDatetime_localUnsupported() { + tm := time.Now().Local() + loc := tm.Location() + fmt.Println("Location:", loc) + if _, err := NewDatetime(tm); err != nil { + fmt.Printf("Could not create a Datetime with %s location.\n", loc) + } else { + fmt.Printf("A Datetime with %s location created.\n", loc) + } + // Output: + // Location: Local + // Could not create a Datetime with Local location. +} + // Example demonstrates how to create a datetime for Tarantool without UTC // timezone in datetime. func ExampleNewDatetime_noTimezone() { @@ -165,6 +181,42 @@ func ExampleDatetime_Add() { // New time: 2014-02-28 17:57:29.000000009 +0000 UTC } +// ExampleDatetime_Add_dst demonstrates how to add an Interval to a +// Datetime value with a DST location. +func ExampleDatetime_Add_dst() { + loc, err := time.LoadLocation("Europe/Moscow") + if err != nil { + fmt.Printf("Unable to load location: %s", err) + return + } + tm := time.Date(2008, 1, 1, 1, 1, 1, 1, loc) + dt, err := NewDatetime(tm) + if err != nil { + fmt.Printf("Unable to create Datetime: %s", err) + return + } + + fmt.Printf("Datetime time:\n") + fmt.Printf("%s\n", dt.ToTime()) + fmt.Printf("Datetime time + 6 month:\n") + fmt.Printf("%s\n", dt.ToTime().AddDate(0, 6, 0)) + dt, err = dt.Add(Interval{Month: 6}) + if err != nil { + fmt.Printf("Unable to add 6 month: %s", err) + return + } + fmt.Printf("Datetime + 6 month time:\n") + fmt.Printf("%s\n", dt.ToTime()) + + // Output: + // Datetime time: + // 2008-01-01 01:01:01.000000001 +0300 MSK + // Datetime time + 6 month: + // 2008-07-01 01:01:01.000000001 +0400 MSD + // Datetime + 6 month time: + // 2008-07-01 01:01:01.000000001 +0400 MSD +} + // ExampleDatetime_Sub demonstrates how to subtract an Interval from a // Datetime value. func ExampleDatetime_Sub() { From a156f4e3d641b4585674755d78af1da50ba36fff Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 6 Oct 2022 10:39:37 +0300 Subject: [PATCH 347/605] datetime: fix interval arithmetic with timezones Tarantool used to ignore timezone difference (offset) for a datetime interval calculations due to a bug [1]. We have to fix it too since we relied on the wrong behavior. 1. https://github.com/tarantool/tarantool/issues/7698 --- CHANGELOG.md | 1 + datetime/datetime.go | 3 +++ datetime/datetime_test.go | 35 ++++++++++++++++++++++++++--------- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2636e12a..3b59b9da9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Decimal package use a test function GetNumberLength instead of a package-level function getNumberLength (#219) - Datetime location after encode + decode is unequal (#217) +- Wrong interval arithmetic with timezones (#221) ## [1.8.0] - 2022-08-17 diff --git a/datetime/datetime.go b/datetime/datetime.go index 0fe20f572..105d2cca5 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -220,6 +220,9 @@ func (dtime *Datetime) Sub(ival Interval) (*Datetime, error) { func (dtime *Datetime) Interval(next *Datetime) Interval { curIval := intervalFromDatetime(dtime) nextIval := intervalFromDatetime(next) + _, curOffset := dtime.time.Zone() + _, nextOffset := next.time.Zone() + curIval.Min -= int64(curOffset-nextOffset) / 60 return nextIval.Sub(curIval) } diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 60afa4077..5c63d0f4c 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -321,8 +321,8 @@ func TestDatetimeAddOutOfRange(t *testing.T) { } func TestDatetimeInterval(t *testing.T) { - var first = "2015-03-20T17:50:56.000000009Z" - var second = "2013-01-31T17:51:56.000000009Z" + var first = "2015-03-20T17:50:56.000000009+02:00" + var second = "2013-01-31T11:51:58.00000009+01:00" tmFirst, err := time.Parse(time.RFC3339, first) if err != nil { @@ -345,14 +345,23 @@ func TestDatetimeInterval(t *testing.T) { ivalFirst := dtFirst.Interval(dtSecond) ivalSecond := dtSecond.Interval(dtFirst) - expectedFirst := Interval{-2, -2, 0, 11, 0, 1, 0, 0, NoneAdjust} - expectedSecond := Interval{2, 2, 0, -11, 0, -1, 0, 0, NoneAdjust} + expectedFirst := Interval{-2, -2, 0, 11, -6, 61, 2, 81, NoneAdjust} + expectedSecond := Interval{2, 2, 0, -11, 6, -61, -2, -81, NoneAdjust} if !reflect.DeepEqual(ivalFirst, expectedFirst) { t.Errorf("Unexpected interval %v, expected %v", ivalFirst, expectedFirst) } if !reflect.DeepEqual(ivalSecond, expectedSecond) { - t.Errorf("Unexpected interval %v, expected %v", ivalFirst, expectedSecond) + t.Errorf("Unexpected interval %v, expected %v", ivalSecond, expectedSecond) + } + + dtFirst, err = dtFirst.Add(ivalFirst) + if err != nil { + t.Fatalf("Unable to add an interval: %s", err) + } + if !dtFirst.ToTime().Equal(dtSecond.ToTime()) { + t.Errorf("Incorrect add an interval result: %s, expected %s", + dtFirst.ToTime(), dtSecond.ToTime()) } } @@ -363,12 +372,20 @@ func TestDatetimeTarantoolInterval(t *testing.T) { defer conn.Close() dates := []string{ - "2015-03-20T17:50:56.000000009+01:00", + // We could return tests with timezones after a release with a fix of + // the bug: + // https://github.com/tarantool/tarantool/issues/7698 + // + // "2010-02-24T23:03:56.0000013-04:00", + // "2015-03-20T17:50:56.000000009+01:00", + // "2020-01-01T01:01:01+11:30", + // "2025-08-01T00:00:00.000000003+11:00", + "2010-02-24T23:03:56.0000013Z", + "2015-03-20T17:50:56.000000009Z", + "2020-01-01T01:01:01Z", + "2025-08-01T00:00:00.000000003Z", "2015-12-21T17:50:53Z", - "2010-02-24T23:03:56.0000013-04:00", "1980-03-28T13:18:39.000099Z", - "2025-08-01T00:00:00.000000003+11:00", - "2020-01-01T01:01:01+11:30", } datetimes := []*Datetime{} for _, date := range dates { From ef1dd3b971c53779706d3acfb56d5c28290f9c49 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 19 Oct 2022 10:02:50 +0300 Subject: [PATCH 348/605] bugfix: Invalid MsgPack if STREAM_ID > 127 The patch fixes an error if a stream id value > 127: Invalid MsgPack - packet header (0x14) Closes #223 --- CHANGELOG.md | 1 + connection.go | 31 ++++++++++++++++++++++++------- tarantool_test.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b59b9da9..c2d03b21f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. package-level function getNumberLength (#219) - Datetime location after encode + decode is unequal (#217) - Wrong interval arithmetic with timezones (#221) +- Invalid MsgPack if STREAM_ID > 127 (#224) ## [1.8.0] - 2022-08-17 diff --git a/connection.go b/connection.go index 3fe6fec26..a4ae8cc36 100644 --- a/connection.go +++ b/connection.go @@ -6,10 +6,12 @@ import ( "bufio" "bytes" "context" + "encoding/binary" "errors" "fmt" "io" "log" + "math" "net" "runtime" "sync" @@ -531,23 +533,38 @@ func (conn *Connection) dial() (err error) { func pack(h *smallWBuf, enc *encoder, reqid uint32, req Request, streamId uint64, res SchemaResolver) (err error) { + const uint32Code = 0xce + const uint64Code = 0xcf + const streamBytesLenUint64 = 10 + const streamBytesLenUint32 = 6 + hl := h.Len() + var streamBytesLen = 0 + var streamBytes [streamBytesLenUint64]byte hMapLen := byte(0x82) // 2 element map. if streamId != ignoreStreamId { hMapLen = byte(0x83) // 3 element map. + streamBytes[0] = KeyStreamId + if streamId > math.MaxUint32 { + streamBytesLen = streamBytesLenUint64 + streamBytes[1] = uint64Code + binary.BigEndian.PutUint64(streamBytes[2:], streamId) + } else { + streamBytesLen = streamBytesLenUint32 + streamBytes[1] = uint32Code + binary.BigEndian.PutUint32(streamBytes[2:], uint32(streamId)) + } } - hBytes := []byte{ - 0xce, 0, 0, 0, 0, // Length. + + hBytes := append([]byte{ + uint32Code, 0, 0, 0, 0, // Length. hMapLen, KeyCode, byte(req.Code()), // Request code. - KeySync, 0xce, + KeySync, uint32Code, byte(reqid >> 24), byte(reqid >> 16), byte(reqid >> 8), byte(reqid), - } - if streamId != ignoreStreamId { - hBytes = append(hBytes, KeyStreamId, byte(streamId)) - } + }, streamBytes[:streamBytesLen]...) h.Write(hBytes) diff --git a/tarantool_test.go b/tarantool_test.go index a86fbb716..0037aea8c 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "math" "os" "reflect" "runtime" @@ -2442,6 +2443,38 @@ func TestComplexStructs(t *testing.T) { } } +func TestStream_IdValues(t *testing.T) { + test_helpers.SkipIfStreamsUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + cases := []uint64{ + 1, + 128, + math.MaxUint8, + math.MaxUint8 + 1, + math.MaxUint16, + math.MaxUint16 + 1, + math.MaxUint32, + math.MaxUint32 + 1, + math.MaxUint64, + } + + stream, _ := conn.NewStream() + req := NewPingRequest() + + for _, id := range cases { + t.Run(fmt.Sprintf("%d", id), func(t *testing.T) { + stream.Id = id + _, err := stream.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Ping: %s", err.Error()) + } + }) + } +} + func TestStream_Commit(t *testing.T) { var req Request var resp *Response From acc2a6bcbed13e9dfa1aa6a6c1a4b2d0d938b3a6 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 7 Oct 2022 11:38:14 +0300 Subject: [PATCH 349/605] queue: queue.Take() returns an invalid task The patch fixes the return an invalid task object from queue.Take(). It may happen if an encoded task data has a nil value [1]. After the fix queue.Take() returns a nil value in the case. 1. https://github.com/msgpack/msgpack/blob/master/spec.md#nil-format --- CHANGELOG.md | 1 + queue/queue.go | 10 +++++++++- queue/task.go | 10 +++------- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2d03b21f..94c20c265 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Datetime location after encode + decode is unequal (#217) - Wrong interval arithmetic with timezones (#221) - Invalid MsgPack if STREAM_ID > 127 (#224) +- queue.Take() returns an invalid task (#222) ## [1.8.0] - 2022-08-17 diff --git a/queue/queue.go b/queue/queue.go index a4e9af029..df13f09bb 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -467,6 +467,14 @@ func (qd *queueData) DecodeMsgpack(d *decoder) error { } qd.task = &Task{data: qd.result, q: qd.q} - d.Decode(&qd.task) + if err = d.Decode(&qd.task); err != nil { + return err + } + + if qd.task.Data() == nil { + // It may happen if the decoder has a code.Nil value inside. As a + // result, the task will not be decoded. + qd.task = nil + } return nil } diff --git a/queue/task.go b/queue/task.go index 348d8610d..c1b0aad98 100644 --- a/queue/task.go +++ b/queue/task.go @@ -29,13 +29,9 @@ func (t *Task) DecodeMsgpack(d *decoder) error { return err } if t.data != nil { - if err = d.Decode(t.data); err != nil { - return fmt.Errorf("fffuuuu: %s", err) - } - } else { - if t.data, err = d.DecodeInterface(); err != nil { - return err - } + d.Decode(t.data) + } else if t.data, err = d.DecodeInterface(); err != nil { + return err } return nil } From 12cdc0cc089298ee9822d8328817853da8c20809 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 7 Oct 2022 12:11:42 +0300 Subject: [PATCH 350/605] queue: fix flaky Example_connectionPool We get a nil task if a ConnectionPool does not yet detect master->replica role switch. We need to add additional check for the case to make the test output deterministic. --- queue/example_connection_pool_test.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go index 59f5020ea..214e67274 100644 --- a/queue/example_connection_pool_test.go +++ b/queue/example_connection_pool_test.go @@ -3,6 +3,7 @@ package queue_test import ( "fmt" "sync" + "sync/atomic" "time" "github.com/google/uuid" @@ -22,6 +23,7 @@ type QueueConnectionHandler struct { err error mutex sync.Mutex masterUpdated chan struct{} + masterCnt int32 } // QueueConnectionHandler implements the ConnectionHandler interface. @@ -87,6 +89,7 @@ func (h *QueueConnectionHandler) Discovered(conn *tarantool.Connection, } } + atomic.AddInt32(&h.masterCnt, 1) fmt.Printf("Master %s is ready to work!\n", conn.Addr()) return nil @@ -95,6 +98,9 @@ func (h *QueueConnectionHandler) Discovered(conn *tarantool.Connection, // Deactivated doesn't do anything useful for the example. func (h *QueueConnectionHandler) Deactivated(conn *tarantool.Connection, role connection_pool.Role) error { + if role == connection_pool.MasterRole { + atomic.AddInt32(&h.masterCnt, -1) + } return nil } @@ -184,8 +190,18 @@ func Example_connectionPool() { return } + for i := 0; i < 2 && atomic.LoadInt32(&h.masterCnt) != 1; i++ { + // The pool does not immediately detect role switching. It may happen + // that requests will be sent to RO instances. In that case q.Take() + // method will return a nil value. + // + // We need to make the example test output deterministic so we need to + // avoid it here. But in real life, you need to take this into account. + time.Sleep(poolOpts.CheckTimeout) + } + // Take a data from the new master instance. - task, err := q.TakeTimeout(1 * time.Second) + task, err := q.Take() if err != nil { fmt.Println("Unable to got task:", err) } else if task == nil { From 1cb4e0beda6a3535e72924cc95693730d319b6f2 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 31 Oct 2022 13:46:10 +0300 Subject: [PATCH 351/605] pool: fix flaky TestConnectionHandlerOpenUpdateClose It's better to wait for a discovered event instead of deactivated to handle finish of a role update. --- connection_pool/connection_pool_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 091d5216c..60c9b4f91 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -337,7 +337,7 @@ func TestConnectionHandlerOpenUpdateClose(t *testing.T) { for i := 0; i < 100; i++ { // Wait for read_only update, it should report about close connection // with old role. - if h.deactivated >= 1 { + if h.discovered >= 3 { break } time.Sleep(poolOpts.CheckTimeout) From 9b68b88f7fe9a6ffb475218460b79bdbec501a4e Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 5 Oct 2022 10:05:40 +0300 Subject: [PATCH 352/605] changelog: add missing summary messages Summary messages for versions 1.7.0 and 1.8.0 were missed. The patch adds messages. Related to #220 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94c20c265..13098e497 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ## [1.8.0] - 2022-08-17 +The minor release with time zones and interval support for datetime. + ### Added - Optional msgpack.v5 usage (#124) @@ -55,6 +57,11 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ## [1.7.0] - 2022-08-02 +This release adds a number of features. The extending of the public API has +become possible with a new way of creating requests. New types of requests are +created via chain calls. Streams, context and prepared statements support are +based on this idea. + ### Added - SSL support (#155) From 679e9c9180311cf22a386d56a51e2b80e2ae069a Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 4 Oct 2022 18:36:39 +0300 Subject: [PATCH 353/605] Release 1.9.0 Overview The release adds support for the latest version of the queue package with master-replica switching. Breaking changes There are no breaking changes in the release. New features Support the queue 1.2.1 (#177). ConnectionHandler interface for handling changes of connections in ConnectionPool (#178). Execute, ExecuteTyped and ExecuteAsync methods to ConnectionPool (#176). ConnectorAdapter type to use ConnectionPool as Connector interface (#176). An example how to use queue and connection_pool subpackages together (#176). Bugfixes Mode type description in the connection_pool subpackage (#208). Missed Role type constants in the connection_pool subpackage (#208). ConnectionPool does not close UnknownRole connections (#208). Segmentation faults in ConnectionPool requests after disconnect (#208). Addresses in ConnectionPool may be changed from an external code (#208). ConnectionPool recreates connections too often (#208). A connection is still opened after ConnectionPool.Close() (#208). Future.GetTyped() after Future.Get() does not decode response correctly (#213). Decimal package use a test function GetNumberLength instead of a package-level function getNumberLength (#219). Datetime location after encode + decode is unequal (#217). Wrong interval arithmetic with timezones (#221). Invalid MsgPack if STREAM_ID > 127 (#224). queue.Take() returns an invalid task (#222). --- CHANGELOG.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13098e497..4fb75664b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,17 +10,25 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added -- Support queue 1.2.0 (#177) +### Changed + +### Fixed + +## [1.9.0] - 2022-11-02 + +The release adds support for the latest version of the +[queue package](https://github.com/tarantool/queue) with master-replica +switching. + +### Added + +- Support the queue 1.2.1 (#177) - ConnectionHandler interface for handling changes of connections in ConnectionPool (#178) - Execute, ExecuteTyped and ExecuteAsync methods to ConnectionPool (#176) - ConnectorAdapter type to use ConnectionPool as Connector interface (#176) - An example how to use queue and connection_pool subpackages together (#176) -### Changed - -- Bump queue package version to 1.2.1 (#176) - ### Fixed - Mode type description in the connection_pool subpackage (#208) From 15391b2f2db8794e117f3d773e06f2c57af3f1ad Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 1 Nov 2022 13:18:37 +0300 Subject: [PATCH 354/605] code health: unify create spaces in config.lua Part of #196 --- config.lua | 64 ++++++++++++++++++++++++++++++----------------- tarantool_test.go | 16 ++++-------- 2 files changed, 46 insertions(+), 34 deletions(-) diff --git a/config.lua b/config.lua index db4aed6eb..eb6989061 100644 --- a/config.lua +++ b/config.lua @@ -6,24 +6,6 @@ box.cfg{ } box.once("init", function() - local s = box.schema.space.create('test', { - id = 517, - if_not_exists = true, - }) - s:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) - - local sp = box.schema.space.create('SQL_TEST', { - id = 519, - if_not_exists = true, - format = { - {name = "NAME0", type = "unsigned"}, - {name = "NAME1", type = "string"}, - {name = "NAME2", type = "string"}, - } - }) - sp:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) - sp:insert{1, "test", "test"} - local st = box.schema.space.create('schematest', { id = 516, temporary = true, @@ -53,8 +35,34 @@ box.once("init", function() }) st:truncate() - local s2 = box.schema.space.create('test_perf', { - id = 520, + local s = box.schema.space.create('test', { + id = 517, + if_not_exists = true, + }) + s:create_index('primary', { + type = 'tree', + parts = {1, 'uint'}, + if_not_exists = true + }) + + local s = box.schema.space.create('SQL_TEST', { + id = 518, + if_not_exists = true, + format = { + {name = "NAME0", type = "unsigned"}, + {name = "NAME1", type = "string"}, + {name = "NAME2", type = "string"}, + } + }) + s:create_index('primary', { + type = 'tree', + parts = {1, 'uint'}, + if_not_exists = true + }) + s:insert{1, "test", "test"} + + local s = box.schema.space.create('test_perf', { + id = 519, temporary = true, if_not_exists = true, field_count = 3, @@ -64,14 +72,24 @@ box.once("init", function() {name = "arr1", type = "array"}, }, }) - s2:create_index('primary', {type = 'tree', unique = true, parts = {1, 'unsigned'}, if_not_exists = true}) - s2:create_index('secondary', {id = 5, type = 'tree', unique = false, parts = {2, 'string'}, if_not_exists = true}) + s:create_index('primary', { + type = 'tree', + unique = true, + parts = {1, 'unsigned'}, + if_not_exists = true + }) + s:create_index('secondary', { + id = 5, type = 'tree', + unique = false, + parts = {2, 'string'}, + if_not_exists = true + }) local arr_data = {} for i = 1,100 do arr_data[i] = i end for i = 1,1000 do - s2:insert{ + s:insert{ i, 'test_name', arr_data, diff --git a/tarantool_test.go b/tarantool_test.go index 0037aea8c..1350390f9 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -205,8 +205,7 @@ func BenchmarkClientSerialSQL(b *testing.B) { conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() - spaceNo := 519 - _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("Failed to replace: %s", err) } @@ -227,8 +226,7 @@ func BenchmarkClientSerialSQLPrepared(b *testing.B) { conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() - spaceNo := 519 - _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("Failed to replace: %s", err) } @@ -601,7 +599,6 @@ func BenchmarkClientParallelMassiveUntyped(b *testing.B) { func BenchmarkClientReplaceParallel(b *testing.B) { conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() - spaceNo = 520 rSpaceNo, _, err := conn.Schema.ResolveSpaceIndex("test_perf", "secondary") if err != nil { @@ -647,8 +644,7 @@ func BenchmarkClientParallelSQL(b *testing.B) { conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() - spaceNo := 519 - _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("No connection available") } @@ -671,8 +667,7 @@ func BenchmarkClientParallelSQLPrepared(b *testing.B) { conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() - spaceNo := 519 - _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("No connection available") } @@ -706,8 +701,7 @@ func BenchmarkSQLSerial(b *testing.B) { conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() - spaceNo := 519 - _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) if err != nil { b.Errorf("Failed to replace: %s", err) } From 48cf0c75b3869e54fc47881f0a1323612c3694fe Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 1 Nov 2022 12:37:16 +0300 Subject: [PATCH 355/605] doc: add examples for GetTyped() and custom keys The patch adds examples for: * Connection.GetTyped() method * IntKey type * UintKey type * StringKey type * IntIntKey type Closes #196 --- client_tools.go | 17 ++++----- config.lua | 24 +++++++++++-- example_test.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 10 deletions(-) diff --git a/client_tools.go b/client_tools.go index a5d6fbba9..9c439ec21 100644 --- a/client_tools.go +++ b/client_tools.go @@ -1,7 +1,7 @@ package tarantool -// IntKey is utility type for passing integer key to Select*, Update* and Delete*. -// It serializes to array with single integer element. +// IntKey is utility type for passing integer key to Select*, Update*, +// Delete* and GetTyped. It serializes to array with single integer element. type IntKey struct { I int } @@ -12,8 +12,9 @@ func (k IntKey) EncodeMsgpack(enc *encoder) error { return nil } -// UintKey is utility type for passing unsigned integer key to Select*, Update* and Delete*. -// It serializes to array with single integer element. +// UintKey is utility type for passing unsigned integer key to Select*, +// Update*, Delete* and GetTyped. It serializes to array with single unsigned +// integer element. type UintKey struct { I uint } @@ -24,8 +25,8 @@ func (k UintKey) EncodeMsgpack(enc *encoder) error { return nil } -// UintKey is utility type for passing string key to Select*, Update* and Delete*. -// It serializes to array with single string element. +// StringKey is utility type for passing string key to Select*, Update*, +// Delete* and GetTyped. It serializes to array with single string element. type StringKey struct { S string } @@ -36,8 +37,8 @@ func (k StringKey) EncodeMsgpack(enc *encoder) error { return nil } -// IntIntKey is utility type for passing two integer keys to Select*, Update* and Delete*. -// It serializes to array with two integer elements. +// IntIntKey is utility type for passing two integer keys to Select*, Update*, +// Delete* and GetTyped. It serializes to array with two integer elements. type IntIntKey struct { I1, I2 int } diff --git a/config.lua b/config.lua index eb6989061..5ab98cdfe 100644 --- a/config.lua +++ b/config.lua @@ -45,9 +45,29 @@ box.once("init", function() if_not_exists = true }) - local s = box.schema.space.create('SQL_TEST', { + local s = box.schema.space.create('teststring', { id = 518, if_not_exists = true, + }) + s:create_index('primary', { + type = 'tree', + parts = {1, 'string'}, + if_not_exists = true + }) + + local s = box.schema.space.create('testintint', { + id = 519, + if_not_exists = true, + }) + s:create_index('primary', { + type = 'tree', + parts = {1, 'int', 2, 'int'}, + if_not_exists = true + }) + + local s = box.schema.space.create('SQL_TEST', { + id = 520, + if_not_exists = true, format = { {name = "NAME0", type = "unsigned"}, {name = "NAME1", type = "string"}, @@ -62,7 +82,7 @@ box.once("init", function() s:insert{1, "test", "test"} local s = box.schema.space.create('test_perf', { - id = 519, + id = 521, temporary = true, if_not_exists = true, field_count = 3, diff --git a/example_test.go b/example_test.go index cfc30b162..37939a268 100644 --- a/example_test.go +++ b/example_test.go @@ -127,6 +127,98 @@ func ExampleConnection_SelectAsync() { // Future 2 Data [[18 val 18 bla]] } +func ExampleConnection_GetTyped() { + conn := example_connect() + defer conn.Close() + + const space = "test" + const index = "primary" + conn.Replace(space, []interface{}{uint(1111), "hello", "world"}) + + var t Tuple + err := conn.GetTyped(space, index, []interface{}{1111}, &t) + fmt.Println("Error", err) + fmt.Println("Data", t) + // Output: + // Error + // Data {{} 1111 hello world} +} + +func ExampleIntKey() { + conn := example_connect() + defer conn.Close() + + const space = "test" + const index = "primary" + conn.Replace(space, []interface{}{int(1111), "hello", "world"}) + + var t Tuple + err := conn.GetTyped(space, index, tarantool.IntKey{1111}, &t) + fmt.Println("Error", err) + fmt.Println("Data", t) + // Output: + // Error + // Data {{} 1111 hello world} +} + +func ExampleUintKey() { + conn := example_connect() + defer conn.Close() + + const space = "test" + const index = "primary" + conn.Replace(space, []interface{}{uint(1111), "hello", "world"}) + + var t Tuple + err := conn.GetTyped(space, index, tarantool.UintKey{1111}, &t) + fmt.Println("Error", err) + fmt.Println("Data", t) + // Output: + // Error + // Data {{} 1111 hello world} +} + +func ExampleStringKey() { + conn := example_connect() + defer conn.Close() + + const space = "teststring" + const index = "primary" + conn.Replace(space, []interface{}{"any", []byte{0x01, 0x02}}) + + t := struct { + Key string + Value []byte + }{} + err := conn.GetTyped(space, index, tarantool.StringKey{"any"}, &t) + fmt.Println("Error", err) + fmt.Println("Data", t) + // Output: + // Error + // Data {any [1 2]} +} + +func ExampleIntIntKey() { + conn := example_connect() + defer conn.Close() + + const space = "testintint" + const index = "primary" + conn.Replace(space, []interface{}{1, 2, "foo"}) + + t := struct { + Key1 int + Key2 int + Value string + }{} + err := conn.GetTyped(space, index, tarantool.IntIntKey{1, 2}, &t) + fmt.Println("Error", err) + fmt.Println("Data", t) + // Output: + // Error + // Data {1 2 foo} +} + func ExampleSelectRequest() { conn := example_connect() defer conn.Close() From 7d4b3cc417a4b81e134e069f5e5467f1cf6859f3 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 18 Nov 2022 14:04:50 +0300 Subject: [PATCH 356/605] ci: use merge commit for pull_request_target It would be more correct to use a merge request commit instead of original commit for pull_request_target. --- .github/workflows/testing.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index eee435bef..e6d3c12c3 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -126,14 +126,16 @@ jobs: steps: - name: Clone the connector + # `ref` as merge request is needed for pull_request_target because this + # target runs in the context of the base commit of the pull request. uses: actions/checkout@v2 - # This is needed for pull_request_target because this event runs in the - # context of the base commit of the pull request. It works fine for - # `push` and `workflow_dispatch` because the default behavior is used - # if `ref` and `repository` are empty. + if: github.event_name == 'pull_request_target' with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} + ref: refs/pull/${{ github.event.pull_request.number }}/merge + + - name: Clone the connector + if: github.event_name != 'pull_request_target' + uses: actions/checkout@v2 - name: Setup Tarantool ${{ matrix.sdk-version }} run: | From 7e86795514b0e0637372bee473deb8eaefb51401 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 18 Nov 2022 17:36:31 +0300 Subject: [PATCH 357/605] bugfix: decimal uses a test variable The patch replaces usage of a test variable DecimalPrecision by a package-level variable decimalPrecision in the decimal package code. --- CHANGELOG.md | 5 ++++- decimal/decimal.go | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fb75664b..2393216de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +- Decimal package uses a test variable DecimalPrecision instead of a + package-level variable decimalPrecision (#233) + ## [1.9.0] - 2022-11-02 The release adds support for the latest version of the @@ -40,7 +43,7 @@ switching. - A connection is still opened after ConnectionPool.Close() (#208) - Future.GetTyped() after Future.Get() does not decode response correctly (#213) -- Decimal package use a test function GetNumberLength instead of a +- Decimal package uses a test function GetNumberLength instead of a package-level function getNumberLength (#219) - Datetime location after encode + decode is unequal (#217) - Wrong interval arithmetic with timezones (#221) diff --git a/decimal/decimal.go b/decimal/decimal.go index 44e192efc..cda08c12e 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -58,13 +58,13 @@ func NewDecimalFromString(src string) (result *Decimal, err error) { // MarshalMsgpack serializes the Decimal into a MessagePack representation. func (decNum *Decimal) MarshalMsgpack() ([]byte, error) { one := decimal.NewFromInt(1) - maxSupportedDecimal := decimal.New(1, DecimalPrecision).Sub(one) // 10^DecimalPrecision - 1 - minSupportedDecimal := maxSupportedDecimal.Neg().Sub(one) // -10^DecimalPrecision - 1 + maxSupportedDecimal := decimal.New(1, decimalPrecision).Sub(one) // 10^decimalPrecision - 1 + minSupportedDecimal := maxSupportedDecimal.Neg().Sub(one) // -10^decimalPrecision - 1 if decNum.GreaterThan(maxSupportedDecimal) { - return nil, fmt.Errorf("msgpack: decimal number is bigger than maximum supported number (10^%d - 1)", DecimalPrecision) + return nil, fmt.Errorf("msgpack: decimal number is bigger than maximum supported number (10^%d - 1)", decimalPrecision) } if decNum.LessThan(minSupportedDecimal) { - return nil, fmt.Errorf("msgpack: decimal number is lesser than minimum supported number (-10^%d - 1)", DecimalPrecision) + return nil, fmt.Errorf("msgpack: decimal number is lesser than minimum supported number (-10^%d - 1)", decimalPrecision) } strBuf := decNum.String() From 47871bd4bc1ea4051d1b8932cfaaccb696da7cde Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 15 Nov 2022 10:13:29 +0300 Subject: [PATCH 358/605] internal: raw request sending functions Auth requests are sent before connection event loop initialization. It would be useful to be able to send IPROTO_ID requests in the same way. Part of #120 --- connection.go | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/connection.go b/connection.go index a4ae8cc36..aa27953b1 100644 --- a/connection.go +++ b/connection.go @@ -581,43 +581,63 @@ func pack(h *smallWBuf, enc *encoder, reqid uint32, return } -func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { +func (conn *Connection) writeRequest(w *bufio.Writer, req Request) error { var packet smallWBuf - req := newAuthRequest(conn.opts.User, string(scramble)) - err = pack(&packet, newEncoder(&packet), 0, req, ignoreStreamId, conn.Schema) + err := pack(&packet, newEncoder(&packet), 0, req, ignoreStreamId, conn.Schema) if err != nil { - return errors.New("auth: pack error " + err.Error()) + return fmt.Errorf("pack error: %w", err) } - if err := write(w, packet.b); err != nil { - return errors.New("auth: write error " + err.Error()) + if err = write(w, packet.b); err != nil { + return fmt.Errorf("write error: %w", err) } if err = w.Flush(); err != nil { - return errors.New("auth: flush error " + err.Error()) + return fmt.Errorf("flush error: %w", err) } - return + return err +} + +func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) error { + req := newAuthRequest(conn.opts.User, string(scramble)) + + err := conn.writeRequest(w, req) + if err != nil { + return fmt.Errorf("auth: %w", err) + } + + return nil } -func (conn *Connection) readAuthResponse(r io.Reader) (err error) { +func (conn *Connection) readResponse(r io.Reader) (Response, error) { respBytes, err := conn.read(r) if err != nil { - return errors.New("auth: read error " + err.Error()) + return Response{}, fmt.Errorf("read error: %w", err) } + resp := Response{buf: smallBuf{b: respBytes}} err = resp.decodeHeader(conn.dec) if err != nil { - return errors.New("auth: decode response header error " + err.Error()) + return resp, fmt.Errorf("decode response header error: %w", err) } err = resp.decodeBody() if err != nil { switch err.(type) { case Error: - return err + return resp, err default: - return errors.New("auth: decode response body error " + err.Error()) + return resp, fmt.Errorf("decode response body error: %w", err) } } - return + return resp, nil +} + +func (conn *Connection) readAuthResponse(r io.Reader) error { + _, err := conn.readResponse(r) + if err != nil { + return fmt.Errorf("auth: %w", err) + } + + return nil } func (conn *Connection) createConnection(reconnect bool) (err error) { From c34bd354f0fd4f2ae24601cf5ca330a2d60d4572 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Thu, 17 Nov 2022 19:13:45 +0300 Subject: [PATCH 359/605] internal: copy opts on Connect The copy is not an honest deepcopy because, for example, copying logger or channel will break the logic. Part of #120 --- connection.go | 15 +++++++++++---- connection_pool/connection_pool.go | 2 +- multi/multi.go | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/connection.go b/connection.go index aa27953b1..561968849 100644 --- a/connection.go +++ b/connection.go @@ -293,6 +293,13 @@ type SslOpts struct { Ciphers string } +// Clone returns a copy of the Opts object. +func (opts Opts) Clone() Opts { + optsCopy := opts + + return optsCopy +} + // Connect creates and configures a new Connection. // // Address could be specified in following ways: @@ -319,7 +326,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { contextRequestId: 1, Greeting: &Greeting{}, control: make(chan struct{}), - opts: opts, + opts: opts.Clone(), dec: newDecoder(&smallBuf{}), } maxprocs := uint32(runtime.GOMAXPROCS(-1)) @@ -344,9 +351,9 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { } } - if opts.RateLimit > 0 { - conn.rlimit = make(chan struct{}, opts.RateLimit) - if opts.RLimitAction != RLimitDrop && opts.RLimitAction != RLimitWait { + if conn.opts.RateLimit > 0 { + conn.rlimit = make(chan struct{}, conn.opts.RateLimit) + if conn.opts.RLimitAction != RLimitDrop && conn.opts.RLimitAction != RLimitWait { return nil, errors.New("RLimitAction should be specified to RLimitDone nor RLimitWait") } } diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 6597e2dd0..2891a88ab 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -125,7 +125,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co connPool = &ConnectionPool{ addrs: make([]string, 0, len(addrs)), - connOpts: connOpts, + connOpts: connOpts.Clone(), opts: opts, state: unknownState, done: make(chan struct{}), diff --git a/multi/multi.go b/multi/multi.go index 67f450c5c..390186f27 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -88,7 +88,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsMulti) (c connOpts.Notify = notify connMulti = &ConnectionMulti{ addrs: addrs, - connOpts: connOpts, + connOpts: connOpts.Clone(), opts: opts, notify: notify, control: make(chan struct{}), From d6d0031d7a0ce9959edf57f9407bba9652a37506 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 15 Nov 2022 10:40:09 +0300 Subject: [PATCH 360/605] 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. https://github.com/tarantool/tarantool/issues/6253 2. https://www.tarantool.io/en/doc/latest/reference/reference_lua/net_box/#lua-function.net_box.new 3. https://github.com/tarantool/tarantool/issues/7953 4. https://stackoverflow.com/a/52710077/11646599 5. https://github.com/tarantool/tarantool-python/issues/262 6. https://github.com/tarantool/tarantool/commit/948e5cdce18b081a8f7b03ebd43e34a029b7aefe Closes #120 --- CHANGELOG.md | 2 + connection.go | 128 ++++++++++++ connection_pool/example_test.go | 54 ++++-- connection_test.go | 31 +++ const.go | 3 + example_test.go | 82 +++++--- export_test.go | 6 + protocol.go | 139 +++++++++++++ protocol_test.go | 37 ++++ response.go | 32 ++- tarantool_test.go | 332 ++++++++++++++++++++++++++++++-- test_helpers/utils.go | 54 ++++++ 12 files changed, 844 insertions(+), 56 deletions(-) create mode 100644 connection_test.go create mode 100644 protocol.go create mode 100644 protocol_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2393216de..3019df4e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/connection.go b/connection.go index 561968849..a2251aaff 100644 --- a/connection.go +++ b/connection.go @@ -14,6 +14,7 @@ import ( "math" "net" "runtime" + "strings" "sync" "sync/atomic" "time" @@ -146,6 +147,8 @@ type Connection struct { lenbuf [PacketLengthBytes]byte lastStreamId uint64 + + serverProtocolInfo ProtocolInfo } var _ = Connector(&Connection{}) // Check compatibility with connector interface. @@ -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. @@ -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 } @@ -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) @@ -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 { @@ -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 { @@ -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() +} diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go index 9a486924a..02715a2bc 100644 --- a/connection_pool/example_test.go +++ b/connection_pool/example_test.go @@ -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") @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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 @@ -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 @@ -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 @@ -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 @@ -836,7 +854,7 @@ func ExampleBeginRequest_TxnIsolation() { } func ExampleConnectorAdapter() { - pool, err := examplePool(testRoles) + pool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } diff --git a/connection_test.go b/connection_test.go new file mode 100644 index 000000000..05f29b093 --- /dev/null +++ b/connection_test.go @@ -0,0 +1,31 @@ +package tarantool_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + . "github.com/tarantool/go-tarantool" +) + +func TestOptsClonePreservesRequiredProtocolFeatures(t *testing.T) { + original := Opts{ + RequiredProtocolInfo: ProtocolInfo{ + Version: ProtocolVersion(100), + Features: []ProtocolFeature{ProtocolFeature(99), ProtocolFeature(100)}, + }, + } + + origCopy := original.Clone() + + original.RequiredProtocolInfo.Features[1] = ProtocolFeature(98) + + require.Equal(t, + origCopy, + Opts{ + RequiredProtocolInfo: ProtocolInfo{ + Version: ProtocolVersion(100), + Features: []ProtocolFeature{ProtocolFeature(99), ProtocolFeature(100)}, + }, + }) +} diff --git a/const.go b/const.go index 4a3cb6833..35ec83380 100644 --- a/const.go +++ b/const.go @@ -18,6 +18,7 @@ const ( RollbackRequestCode = 16 PingRequestCode = 64 SubscribeRequestCode = 66 + IdRequestCode = 73 KeyCode = 0x00 KeySync = 0x01 @@ -41,6 +42,8 @@ const ( KeySQLBind = 0x41 KeySQLInfo = 0x42 KeyStmtID = 0x43 + KeyVersion = 0x54 + KeyFeatures = 0x55 KeyTimeout = 0x56 KeyTxnIsolation = 0x59 diff --git a/example_test.go b/example_test.go index 37939a268..15574d099 100644 --- a/example_test.go +++ b/example_test.go @@ -18,7 +18,7 @@ type Tuple struct { Name string } -func example_connect() *tarantool.Connection { +func example_connect(opts tarantool.Opts) *tarantool.Connection { conn, err := tarantool.Connect(server, opts) if err != nil { panic("Connection is not established: " + err.Error()) @@ -45,7 +45,7 @@ func ExampleSslOpts() { } func ExampleConnection_Select() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -71,7 +71,7 @@ func ExampleConnection_Select() { } func ExampleConnection_SelectTyped() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() var res []Tuple @@ -94,7 +94,7 @@ func ExampleConnection_SelectTyped() { } func ExampleConnection_SelectAsync() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() spaceNo := uint32(517) @@ -128,7 +128,7 @@ func ExampleConnection_SelectAsync() { } func ExampleConnection_GetTyped() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() const space = "test" @@ -145,7 +145,7 @@ func ExampleConnection_GetTyped() { } func ExampleIntKey() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() const space = "test" @@ -162,7 +162,7 @@ func ExampleIntKey() { } func ExampleUintKey() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() const space = "test" @@ -179,7 +179,7 @@ func ExampleUintKey() { } func ExampleStringKey() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() const space = "teststring" @@ -199,7 +199,7 @@ func ExampleStringKey() { } func ExampleIntIntKey() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() const space = "testintint" @@ -220,7 +220,7 @@ func ExampleIntIntKey() { } func ExampleSelectRequest() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() req := tarantool.NewSelectRequest(517). @@ -250,7 +250,7 @@ func ExampleSelectRequest() { } func ExampleUpdateRequest() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() req := tarantool.NewUpdateRequest(517). @@ -280,7 +280,7 @@ func ExampleUpdateRequest() { } func ExampleUpsertRequest() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() var req tarantool.Request @@ -320,6 +320,33 @@ func ExampleUpsertRequest() { // response is []interface {}{[]interface {}{0x459, "first", "updated"}} } +func ExampleProtocolVersion() { + conn := example_connect(opts) + defer conn.Close() + + clientProtocolInfo := conn.ClientProtocolInfo() + fmt.Println("Connector client protocol version:", clientProtocolInfo.Version) + fmt.Println("Connector client protocol features:", clientProtocolInfo.Features) + // Output: + // Connector client protocol version: 4 + // Connector client protocol features: [StreamsFeature TransactionsFeature] +} + +func getTestTxnOpts() tarantool.Opts { + txnOpts := opts.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 @@ -331,7 +358,8 @@ func ExampleCommitRequest() { return } - conn := example_connect() + txnOpts := getTestTxnOpts() + conn := example_connect(txnOpts) defer conn.Close() stream, _ := conn.NewStream() @@ -407,7 +435,8 @@ func ExampleRollbackRequest() { return } - conn := example_connect() + txnOpts := getTestTxnOpts() + conn := example_connect(txnOpts) defer conn.Close() stream, _ := conn.NewStream() @@ -483,7 +512,8 @@ func ExampleBeginRequest_TxnIsolation() { return } - conn := example_connect() + txnOpts := getTestTxnOpts() + conn := example_connect(txnOpts) defer conn.Close() stream, _ := conn.NewStream() @@ -551,7 +581,7 @@ func ExampleBeginRequest_TxnIsolation() { } func ExampleFuture_GetIterator() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() const timeout = 3 * time.Second @@ -584,7 +614,7 @@ func ExampleFuture_GetIterator() { } func ExampleConnection_Ping() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() // Ping a Tarantool instance to check connection. @@ -599,7 +629,7 @@ func ExampleConnection_Ping() { } func ExampleConnection_Insert() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() // Insert a new tuple { 31, 1 }. @@ -632,7 +662,7 @@ func ExampleConnection_Insert() { } func ExampleConnection_Delete() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() // Insert a new tuple { 35, 1 }. @@ -665,7 +695,7 @@ func ExampleConnection_Delete() { } func ExampleConnection_Replace() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() // Insert a new tuple { 13, 1 }. @@ -714,7 +744,7 @@ func ExampleConnection_Replace() { } func ExampleConnection_Update() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() // Insert a new tuple { 14, 1 }. @@ -734,7 +764,7 @@ func ExampleConnection_Update() { } func ExampleConnection_Call() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() // Call a function 'simple_concat' with arguments. @@ -751,7 +781,7 @@ func ExampleConnection_Call() { } func ExampleConnection_Eval() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() // Run raw Lua code. @@ -788,7 +818,7 @@ func ExampleConnect() { // Example demonstrates how to retrieve information with space schema. func ExampleSchema() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() schema := conn.Schema @@ -810,7 +840,7 @@ func ExampleSchema() { // Example demonstrates how to retrieve information with space schema. func ExampleSpace() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() // Save Schema to a local variable to avoid races @@ -1021,7 +1051,7 @@ func ExampleConnection_NewPrepared() { // of the request. For those purposes use context.WithTimeout() as // the root context. func ExamplePingRequest_Context() { - conn := example_connect() + conn := example_connect(opts) defer conn.Close() timeout := time.Nanosecond diff --git a/export_test.go b/export_test.go index cc9a2a594..464a85844 100644 --- a/export_test.go +++ b/export_test.go @@ -111,6 +111,12 @@ func RefImplRollbackBody(enc *encoder) error { return fillRollback(enc) } +// RefImplIdBody is reference implementation for filling of an id +// request's body. +func RefImplIdBody(enc *encoder, protocolInfo ProtocolInfo) error { + return fillId(enc, protocolInfo) +} + func NewEncoder(w io.Writer) *encoder { return newEncoder(w) } diff --git a/protocol.go b/protocol.go new file mode 100644 index 000000000..1eaf60e2b --- /dev/null +++ b/protocol.go @@ -0,0 +1,139 @@ +package tarantool + +import ( + "context" + "fmt" +) + +// ProtocolVersion type stores Tarantool protocol version. +type ProtocolVersion uint64 + +// ProtocolVersion type stores a Tarantool protocol feature. +type ProtocolFeature uint64 + +// ProtocolInfo type aggregates Tarantool protocol version and features info. +type ProtocolInfo struct { + // Version is the supported protocol version. + Version ProtocolVersion + // Features are supported protocol features. + Features []ProtocolFeature +} + +// Clone returns an exact copy of the ProtocolInfo object. +// Any changes in copy will not affect the original values. +func (info ProtocolInfo) Clone() ProtocolInfo { + infoCopy := info + + if info.Features != nil { + infoCopy.Features = make([]ProtocolFeature, len(info.Features)) + copy(infoCopy.Features, info.Features) + } + + return infoCopy +} + +const ( + // StreamsFeature represents streams support (supported by connector). + StreamsFeature ProtocolFeature = 0 + // TransactionsFeature represents interactive transactions support. + // (supported by connector). + TransactionsFeature ProtocolFeature = 1 + // ErrorExtensionFeature represents support of MP_ERROR objects over MessagePack + // (unsupported by connector). + ErrorExtensionFeature ProtocolFeature = 2 + // WatchersFeature represents support of watchers + // (unsupported by connector). + WatchersFeature ProtocolFeature = 3 + // PaginationFeature represents support of pagination + // (unsupported by connector). + PaginationFeature ProtocolFeature = 4 +) + +// String returns the name of a Tarantool feature. +// If value X is not a known feature, returns "Unknown feature (code X)" string. +func (ftr ProtocolFeature) String() string { + switch ftr { + case StreamsFeature: + return "StreamsFeature" + case TransactionsFeature: + return "TransactionsFeature" + case ErrorExtensionFeature: + return "ErrorExtensionFeature" + case WatchersFeature: + return "WatchersFeature" + case PaginationFeature: + return "PaginationFeature" + default: + return fmt.Sprintf("Unknown feature (code %d)", ftr) + } +} + +var clientProtocolInfo ProtocolInfo = ProtocolInfo{ + // Protocol version supported by connector. Version 3 + // was introduced in Tarantool 2.10.0, version 4 was + // introduced in master 948e5cd (possible 2.10.5 or 2.11.0). + // Support of protocol version on connector side was introduced in + // 1.10.0. + Version: ProtocolVersion(4), + // Streams and transactions were introduced in protocol version 1 + // (Tarantool 2.10.0), in connector since 1.7.0. + Features: []ProtocolFeature{ + StreamsFeature, + TransactionsFeature, + }, +} + +// IdRequest informs the server about supported protocol +// version and protocol features. +type IdRequest struct { + baseRequest + protocolInfo ProtocolInfo +} + +func fillId(enc *encoder, protocolInfo ProtocolInfo) error { + enc.EncodeMapLen(2) + + encodeUint(enc, KeyVersion) + if err := enc.Encode(protocolInfo.Version); err != nil { + return err + } + + encodeUint(enc, KeyFeatures) + + t := len(protocolInfo.Features) + if err := enc.EncodeArrayLen(t); err != nil { + return err + } + + for _, feature := range protocolInfo.Features { + if err := enc.Encode(feature); err != nil { + return err + } + } + + return nil +} + +// NewIdRequest returns a new IdRequest. +func NewIdRequest(protocolInfo ProtocolInfo) *IdRequest { + req := new(IdRequest) + req.requestCode = IdRequestCode + req.protocolInfo = protocolInfo.Clone() + return req +} + +// Body fills an encoder with the id request body. +func (req *IdRequest) Body(res SchemaResolver, enc *encoder) error { + return fillId(enc, req.protocolInfo) +} + +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (req *IdRequest) Context(ctx context.Context) *IdRequest { + req.ctx = ctx + return req +} diff --git a/protocol_test.go b/protocol_test.go new file mode 100644 index 000000000..c747d9bff --- /dev/null +++ b/protocol_test.go @@ -0,0 +1,37 @@ +package tarantool_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + . "github.com/tarantool/go-tarantool" +) + +func TestProtocolInfoClonePreservesFeatures(t *testing.T) { + original := ProtocolInfo{ + Version: ProtocolVersion(100), + Features: []ProtocolFeature{ProtocolFeature(99), ProtocolFeature(100)}, + } + + origCopy := original.Clone() + + original.Features[1] = ProtocolFeature(98) + + require.Equal(t, + origCopy, + ProtocolInfo{ + Version: ProtocolVersion(100), + Features: []ProtocolFeature{ProtocolFeature(99), ProtocolFeature(100)}, + }) +} + +func TestFeatureStringRepresentation(t *testing.T) { + require.Equal(t, StreamsFeature.String(), "StreamsFeature") + require.Equal(t, TransactionsFeature.String(), "TransactionsFeature") + require.Equal(t, ErrorExtensionFeature.String(), "ErrorExtensionFeature") + require.Equal(t, WatchersFeature.String(), "WatchersFeature") + require.Equal(t, PaginationFeature.String(), "PaginationFeature") + + require.Equal(t, ProtocolFeature(15532).String(), "Unknown feature (code 15532)") +} diff --git a/response.go b/response.go index 7b203bc54..9e38e970d 100644 --- a/response.go +++ b/response.go @@ -147,8 +147,10 @@ func (resp *Response) decodeBody() (err error) { offset := resp.buf.Offset() defer resp.buf.Seek(offset) - var l int + var l, larr int var stmtID, bindCount uint64 + var serverProtocolInfo ProtocolInfo + var feature ProtocolFeature d := newDecoder(&resp.buf) @@ -190,6 +192,22 @@ func (resp *Response) decodeBody() (err error) { if bindCount, err = d.DecodeUint64(); err != nil { return err } + case KeyVersion: + if err = d.Decode(&serverProtocolInfo.Version); err != nil { + return err + } + case KeyFeatures: + if larr, err = d.DecodeArrayLen(); err != nil { + return err + } + + serverProtocolInfo.Features = make([]ProtocolFeature, larr) + for i := 0; i < larr; i++ { + if err = d.Decode(&feature); err != nil { + return err + } + serverProtocolInfo.Features[i] = feature + } default: if err = d.Skip(); err != nil { return err @@ -204,6 +222,18 @@ func (resp *Response) decodeBody() (err error) { } resp.Data = []interface{}{stmt} } + + // Tarantool may send only version >= 1 + if (serverProtocolInfo.Version != ProtocolVersion(0)) || (serverProtocolInfo.Features != nil) { + if serverProtocolInfo.Version == ProtocolVersion(0) { + return fmt.Errorf("No protocol version provided in Id response") + } + if serverProtocolInfo.Features == nil { + return fmt.Errorf("No features provided in Id response") + } + resp.Data = []interface{}{serverProtocolInfo} + } + if resp.Code != OkCode && resp.Code != PushCode { resp.Code &^= ErrorCodeBit err = Error{resp.Code, resp.Error} diff --git a/tarantool_test.go b/tarantool_test.go index 1350390f9..31d287272 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -20,6 +20,17 @@ import ( "github.com/tarantool/go-tarantool/test_helpers" ) +var startOpts test_helpers.StartOpts = test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: server, + WorkDir: "work_dir", + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, +} + type Member struct { Name string Nonce string @@ -2830,6 +2841,313 @@ func TestStream_DoWithClosedConn(t *testing.T) { } } +func TestConnectionProtocolInfoSupported(t *testing.T) { + test_helpers.SkipIfIdUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // First Tarantool protocol version (1, StreamsFeature and TransactionsFeature) + // was introduced between 2.10.0-beta1 and 2.10.0-beta2. + // Versions 2 (ErrorExtensionFeature) and 3 (WatchersFeature) were also + // introduced between 2.10.0-beta1 and 2.10.0-beta2. Version 4 + // (PaginationFeature) was introduced in master 948e5cd (possible 2.10.5 or + // 2.11.0). So each release Tarantool >= 2.10 (same as each Tarantool with + // id support) has protocol version >= 3 and first four features. + tarantool210ProtocolInfo := ProtocolInfo{ + Version: ProtocolVersion(3), + Features: []ProtocolFeature{ + StreamsFeature, + TransactionsFeature, + ErrorExtensionFeature, + WatchersFeature, + }, + } + + clientProtocolInfo := conn.ClientProtocolInfo() + require.Equal(t, + clientProtocolInfo, + ProtocolInfo{ + Version: ProtocolVersion(4), + Features: []ProtocolFeature{StreamsFeature, TransactionsFeature}, + }) + + serverProtocolInfo := conn.ServerProtocolInfo() + require.GreaterOrEqual(t, + serverProtocolInfo.Version, + tarantool210ProtocolInfo.Version) + require.Subset(t, + serverProtocolInfo.Features, + tarantool210ProtocolInfo.Features) +} + +func TestClientIdRequestObject(t *testing.T) { + test_helpers.SkipIfIdUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + tarantool210ProtocolInfo := ProtocolInfo{ + Version: ProtocolVersion(3), + Features: []ProtocolFeature{ + StreamsFeature, + TransactionsFeature, + ErrorExtensionFeature, + WatchersFeature, + }, + } + + req := NewIdRequest(ProtocolInfo{ + Version: ProtocolVersion(1), + Features: []ProtocolFeature{StreamsFeature}, + }) + resp, err := conn.Do(req).Get() + require.Nilf(t, err, "No errors on Id request execution") + require.NotNilf(t, resp, "Response not empty") + require.NotNilf(t, resp.Data, "Response data not empty") + require.Equal(t, len(resp.Data), 1, "Response data contains exactly one object") + + serverProtocolInfo, ok := resp.Data[0].(ProtocolInfo) + require.Truef(t, ok, "Response Data object is an ProtocolInfo object") + require.GreaterOrEqual(t, + serverProtocolInfo.Version, + tarantool210ProtocolInfo.Version) + require.Subset(t, + serverProtocolInfo.Features, + tarantool210ProtocolInfo.Features) +} + +func TestClientIdRequestObjectWithNilContext(t *testing.T) { + test_helpers.SkipIfIdUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + tarantool210ProtocolInfo := ProtocolInfo{ + Version: ProtocolVersion(3), + Features: []ProtocolFeature{ + StreamsFeature, + TransactionsFeature, + ErrorExtensionFeature, + WatchersFeature, + }, + } + + req := NewIdRequest(ProtocolInfo{ + Version: ProtocolVersion(1), + Features: []ProtocolFeature{StreamsFeature}, + }).Context(nil) //nolint + resp, err := conn.Do(req).Get() + require.Nilf(t, err, "No errors on Id request execution") + require.NotNilf(t, resp, "Response not empty") + require.NotNilf(t, resp.Data, "Response data not empty") + require.Equal(t, len(resp.Data), 1, "Response data contains exactly one object") + + serverProtocolInfo, ok := resp.Data[0].(ProtocolInfo) + require.Truef(t, ok, "Response Data object is an ProtocolInfo object") + require.GreaterOrEqual(t, + serverProtocolInfo.Version, + tarantool210ProtocolInfo.Version) + require.Subset(t, + serverProtocolInfo.Features, + tarantool210ProtocolInfo.Features) +} + +func TestClientIdRequestObjectWithPassedCanceledContext(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + ctx, cancel := context.WithCancel(context.Background()) + req := NewIdRequest(ProtocolInfo{ + Version: ProtocolVersion(1), + Features: []ProtocolFeature{StreamsFeature}, + }).Context(ctx) //nolint + cancel() + resp, err := conn.Do(req).Get() + require.Nilf(t, resp, "Response is empty") + require.NotNilf(t, err, "Error is not empty") + require.Equal(t, err.Error(), "context is done") +} + +func TestClientIdRequestObjectWithContext(t *testing.T) { + var err error + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + ctx, cancel := context.WithCancel(context.Background()) + req := NewIdRequest(ProtocolInfo{ + Version: ProtocolVersion(1), + Features: []ProtocolFeature{StreamsFeature}, + }).Context(ctx) //nolint + fut := conn.Do(req) + cancel() + resp, err := fut.Get() + require.Nilf(t, resp, "Response is empty") + require.NotNilf(t, err, "Error is not empty") + require.Equal(t, err.Error(), "context is done") +} + +func TestConnectionProtocolInfoUnsupported(t *testing.T) { + test_helpers.SkipIfIdSupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + clientProtocolInfo := conn.ClientProtocolInfo() + require.Equal(t, + clientProtocolInfo, + ProtocolInfo{ + Version: ProtocolVersion(4), + Features: []ProtocolFeature{StreamsFeature, TransactionsFeature}, + }) + + serverProtocolInfo := conn.ServerProtocolInfo() + require.Equal(t, serverProtocolInfo, ProtocolInfo{}) +} + +func TestConnectionClientFeaturesUmmutable(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + info := conn.ClientProtocolInfo() + infoOrig := info.Clone() + info.Features[0] = ProtocolFeature(15532) + + require.Equal(t, conn.ClientProtocolInfo(), infoOrig) + require.NotEqual(t, conn.ClientProtocolInfo(), info) +} + +func TestConnectionServerFeaturesUmmutable(t *testing.T) { + test_helpers.SkipIfIdUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + info := conn.ServerProtocolInfo() + infoOrig := info.Clone() + info.Features[0] = ProtocolFeature(15532) + + require.Equal(t, conn.ServerProtocolInfo(), infoOrig) + require.NotEqual(t, conn.ServerProtocolInfo(), info) +} + +func TestConnectionProtocolVersionRequirementSuccess(t *testing.T) { + test_helpers.SkipIfIdUnsupported(t) + + connOpts := opts.Clone() + connOpts.RequiredProtocolInfo = ProtocolInfo{ + Version: ProtocolVersion(3), + } + + conn, err := Connect(server, connOpts) + + require.Nilf(t, err, "No errors on connect") + require.NotNilf(t, conn, "Connect success") + + conn.Close() +} + +func TestConnectionProtocolVersionRequirementFail(t *testing.T) { + test_helpers.SkipIfIdSupported(t) + + connOpts := opts.Clone() + connOpts.RequiredProtocolInfo = ProtocolInfo{ + Version: ProtocolVersion(3), + } + + conn, err := Connect(server, connOpts) + + require.Nilf(t, conn, "Connect fail") + require.NotNilf(t, err, "Got error on connect") + require.Contains(t, err.Error(), "identify: protocol version 3 is not supported") +} + +func TestConnectionProtocolFeatureRequirementSuccess(t *testing.T) { + test_helpers.SkipIfIdUnsupported(t) + + connOpts := opts.Clone() + connOpts.RequiredProtocolInfo = ProtocolInfo{ + Features: []ProtocolFeature{TransactionsFeature}, + } + + conn, err := Connect(server, connOpts) + + require.NotNilf(t, conn, "Connect success") + require.Nilf(t, err, "No errors on connect") + + conn.Close() +} + +func TestConnectionProtocolFeatureRequirementFail(t *testing.T) { + test_helpers.SkipIfIdSupported(t) + + connOpts := opts.Clone() + connOpts.RequiredProtocolInfo = ProtocolInfo{ + Features: []ProtocolFeature{TransactionsFeature}, + } + + conn, err := Connect(server, connOpts) + + require.Nilf(t, conn, "Connect fail") + require.NotNilf(t, err, "Got error on connect") + require.Contains(t, err.Error(), "identify: protocol feature TransactionsFeature is not supported") +} + +func TestConnectionProtocolFeatureRequirementManyFail(t *testing.T) { + test_helpers.SkipIfIdSupported(t) + + connOpts := opts.Clone() + connOpts.RequiredProtocolInfo = ProtocolInfo{ + Features: []ProtocolFeature{TransactionsFeature, ProtocolFeature(15532)}, + } + + conn, err := Connect(server, connOpts) + + require.Nilf(t, conn, "Connect fail") + require.NotNilf(t, err, "Got error on connect") + require.Contains(t, + err.Error(), + "identify: protocol features TransactionsFeature, Unknown feature (code 15532) are not supported") +} + +func TestConnectionFeatureOptsImmutable(t *testing.T) { + test_helpers.SkipIfIdUnsupported(t) + + restartOpts := startOpts + restartOpts.Listen = "127.0.0.1:3014" + inst, err := test_helpers.StartTarantool(restartOpts) + defer test_helpers.StopTarantoolWithCleanup(inst) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + retries := uint(10) + timeout := 100 * time.Millisecond + + connOpts := opts.Clone() + connOpts.Reconnect = timeout + connOpts.MaxReconnects = retries + connOpts.RequiredProtocolInfo = ProtocolInfo{ + Features: []ProtocolFeature{TransactionsFeature}, + } + + // Connect with valid opts + conn := test_helpers.ConnectWithValidation(t, server, connOpts) + defer conn.Close() + + // Change opts outside + connOpts.RequiredProtocolInfo.Features[0] = ProtocolFeature(15532) + + // Trigger reconnect with opts re-check + test_helpers.StopTarantool(inst) + err = test_helpers.RestartTarantool(&inst) + require.Nilf(t, err, "Failed to restart tarantool") + + connected := test_helpers.WaitUntilReconnected(conn, retries, timeout) + require.True(t, connected, "Reconnect success") +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body @@ -2842,17 +3160,9 @@ func runTestMain(m *testing.M) int { log.Fatalf("Could not check the Tarantool version") } - inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ - InitScript: "config.lua", - Listen: server, - WorkDir: "work_dir", - User: opts.User, - Pass: opts.Pass, - WaitStart: 100 * time.Millisecond, - ConnectRetry: 3, - RetryTimeout: 500 * time.Millisecond, - MemtxUseMvccEngine: !isStreamUnsupported, - }) + startOpts.MemtxUseMvccEngine = !isStreamUnsupported + + inst, err := test_helpers.StartTarantool(startOpts) defer test_helpers.StopTarantoolWithCleanup(inst) if err != nil { diff --git a/test_helpers/utils.go b/test_helpers/utils.go index c936e90b3..dff0bb357 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -2,6 +2,7 @@ package test_helpers import ( "testing" + "time" "github.com/tarantool/go-tarantool" ) @@ -40,6 +41,26 @@ func DeleteRecordByKey(t *testing.T, conn tarantool.Connector, } } +// WaitUntilReconnected waits until connection is reestablished. +// Returns false in case of connection is not in the connected state +// after specified retries count, true otherwise. +func WaitUntilReconnected(conn *tarantool.Connection, retries uint, timeout time.Duration) bool { + for i := uint(0); ; i++ { + connected := conn.ConnectedNow() + if connected { + return true + } + + if i == retries { + break + } + + time.Sleep(timeout) + } + + return false +} + func SkipIfSQLUnsupported(t testing.TB) { t.Helper() @@ -66,3 +87,36 @@ func SkipIfStreamsUnsupported(t *testing.T) { t.Skip("Skipping test for Tarantool without streams support") } } + +// SkipIfIdUnsupported skips test run if Tarantool without +// IPROTO_ID support is used. +func SkipIfIdUnsupported(t *testing.T) { + t.Helper() + + // Tarantool supports Id requests since version 2.10.0 + isLess, err := IsTarantoolVersionLess(2, 10, 0) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + + if isLess { + t.Skip("Skipping test for Tarantool without id requests support") + } +} + +// SkipIfIdSupported skips test run if Tarantool with +// IPROTO_ID support is used. Skip is useful for tests validating +// that protocol info is processed as expected even for pre-IPROTO_ID instances. +func SkipIfIdSupported(t *testing.T) { + t.Helper() + + // Tarantool supports Id requests since version 2.10.0 + isLess, err := IsTarantoolVersionLess(2, 10, 0) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + + if !isLess { + t.Skip("Skipping test for Tarantool with non-zero protocol version and features") + } +} From c5ebadf929b26123f527b0dd31b081188a83eea8 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 5 Dec 2022 12:29:59 +0300 Subject: [PATCH 361/605] changelog: remove a dot from the end of an entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3019df4e0..7b2c49dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added -- Support iproto feature discovery (#120). +- Support iproto feature discovery (#120) ### Changed From 952332b3d7f3a721ecc9005025590ed4b73f7c81 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Fri, 11 Nov 2022 18:46:07 +0300 Subject: [PATCH 362/605] code health: rename error iproto code Since 2.4.1 IPROTO_ERROR (0x31) is renamed to IPROTO_ERROR_24 and IPROTO_ERROR name is used for 0x52 constant describing extended error info [1]. 1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-error-type Part of #209 --- const.go | 2 +- response.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/const.go b/const.go index 35ec83380..8cd9bc490 100644 --- a/const.go +++ b/const.go @@ -35,7 +35,7 @@ const ( KeyExpression = 0x27 KeyDefTuple = 0x28 KeyData = 0x30 - KeyError = 0x31 + KeyError24 = 0x31 KeyMetaData = 0x32 KeyBindCount = 0x34 KeySQLText = 0x40 diff --git a/response.go b/response.go index 9e38e970d..df0b0eca7 100644 --- a/response.go +++ b/response.go @@ -172,7 +172,7 @@ func (resp *Response) decodeBody() (err error) { if resp.Data, ok = res.([]interface{}); !ok { return fmt.Errorf("result is not array: %v", res) } - case KeyError: + case KeyError24: if resp.Error, err = d.DecodeString(); err != nil { return err } @@ -262,7 +262,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if err = d.Decode(res); err != nil { return err } - case KeyError: + case KeyError24: if resp.Error, err = d.DecodeString(); err != nil { return err } From 4ae249cfff0d0f268156a48a9223c063ebb828f0 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Wed, 23 Nov 2022 15:38:00 +0300 Subject: [PATCH 363/605] api: support errors extended information Since Tarantool 2.4.1, iproto error responses contain extended info with backtrace [1]. After this patch, Error would contain ExtendedInfo field (BoxError object), if it was provided. Error() handle now will print extended info, if possible. 1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/#responses-for-errors Part of #209 --- CHANGELOG.md | 1 + box_error.go | 186 +++++++++++++++++++++++++++++++++++++++ box_error_test.go | 200 ++++++++++++++++++++++++++++++++++++++++++ config.lua | 37 ++++++++ const.go | 3 +- errors.go | 9 +- response.go | 15 +++- tarantool_test.go | 97 ++++++++++++++++++++ test_helpers/utils.go | 56 ++++++++++++ 9 files changed, 599 insertions(+), 5 deletions(-) create mode 100644 box_error.go create mode 100644 box_error_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b2c49dab..e870c6a9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added - Support iproto feature discovery (#120) +- Support errors extended information (#209) ### Changed diff --git a/box_error.go b/box_error.go new file mode 100644 index 000000000..97c5c1774 --- /dev/null +++ b/box_error.go @@ -0,0 +1,186 @@ +package tarantool + +import ( + "bytes" + "fmt" +) + +const ( + keyErrorStack = 0x00 + keyErrorType = 0x00 + keyErrorFile = 0x01 + keyErrorLine = 0x02 + keyErrorMessage = 0x03 + keyErrorErrno = 0x04 + keyErrorErrcode = 0x05 + keyErrorFields = 0x06 +) + +// BoxError is a type representing Tarantool `box.error` object: a single +// MP_ERROR_STACK object with a link to the previous stack error. +// See https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_error/error/ +// +// Since 1.10.0 +type BoxError struct { + // Type is error type that implies its source (for example, "ClientError"). + Type string + // File is a source code file where the error was caught. + File string + // Line is a number of line in the source code file where the error was caught. + Line uint64 + // Msg is the text of reason. + Msg string + // Errno is the ordinal number of the error. + Errno uint64 + // Code is the number of the error as defined in `errcode.h`. + Code uint64 + // Fields are additional fields depending on error type. For example, if + // type is "AccessDeniedError", then it will include "object_type", + // "object_name", "access_type". + Fields map[string]interface{} + // Prev is the previous error in stack. + Prev *BoxError +} + +// Error converts a BoxError to a string. +func (e *BoxError) Error() string { + s := fmt.Sprintf("%s (%s, code 0x%x), see %s line %d", + e.Msg, e.Type, e.Code, e.File, e.Line) + + if e.Prev != nil { + return fmt.Sprintf("%s: %s", s, e.Prev) + } + + return s +} + +// Depth computes the count of errors in stack, including the current one. +func (e *BoxError) Depth() int { + depth := int(0) + + cur := e + for cur != nil { + cur = cur.Prev + depth++ + } + + return depth +} + +func decodeBoxError(d *decoder) (*BoxError, error) { + var l, larr, l1, l2 int + var errorStack []BoxError + var err error + + if l, err = d.DecodeMapLen(); err != nil { + return nil, err + } + + for ; l > 0; l-- { + var cd int + if cd, err = d.DecodeInt(); err != nil { + return nil, err + } + switch cd { + case keyErrorStack: + if larr, err = d.DecodeArrayLen(); err != nil { + return nil, err + } + + errorStack = make([]BoxError, larr) + + for i := 0; i < larr; i++ { + if l1, err = d.DecodeMapLen(); err != nil { + return nil, err + } + + for ; l1 > 0; l1-- { + var cd1 int + if cd1, err = d.DecodeInt(); err != nil { + return nil, err + } + switch cd1 { + case keyErrorType: + if errorStack[i].Type, err = d.DecodeString(); err != nil { + return nil, err + } + case keyErrorFile: + if errorStack[i].File, err = d.DecodeString(); err != nil { + return nil, err + } + case keyErrorLine: + if errorStack[i].Line, err = d.DecodeUint64(); err != nil { + return nil, err + } + case keyErrorMessage: + if errorStack[i].Msg, err = d.DecodeString(); err != nil { + return nil, err + } + case keyErrorErrno: + if errorStack[i].Errno, err = d.DecodeUint64(); err != nil { + return nil, err + } + case keyErrorErrcode: + if errorStack[i].Code, err = d.DecodeUint64(); err != nil { + return nil, err + } + case keyErrorFields: + var mapk string + var mapv interface{} + + errorStack[i].Fields = make(map[string]interface{}) + + if l2, err = d.DecodeMapLen(); err != nil { + return nil, err + } + for ; l2 > 0; l2-- { + if mapk, err = d.DecodeString(); err != nil { + return nil, err + } + if mapv, err = d.DecodeInterface(); err != nil { + return nil, err + } + errorStack[i].Fields[mapk] = mapv + } + default: + if err = d.Skip(); err != nil { + return nil, err + } + } + } + + if i > 0 { + errorStack[i-1].Prev = &errorStack[i] + } + } + default: + if err = d.Skip(); err != nil { + return nil, err + } + } + } + + if len(errorStack) == 0 { + return nil, fmt.Errorf("msgpack: unexpected empty BoxError stack on decode") + } + + return &errorStack[0], nil +} + +// UnmarshalMsgpack deserializes a BoxError value from a MessagePack +// representation. +func (e *BoxError) UnmarshalMsgpack(b []byte) error { + if e == nil { + panic("cannot unmarshal to a nil pointer") + } + + buf := bytes.NewBuffer(b) + dec := newDecoder(buf) + + if val, err := decodeBoxError(dec); err != nil { + return err + } else { + *e = *val + return nil + } +} diff --git a/box_error_test.go b/box_error_test.go new file mode 100644 index 000000000..76ba3290d --- /dev/null +++ b/box_error_test.go @@ -0,0 +1,200 @@ +package tarantool_test + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/require" + . "github.com/tarantool/go-tarantool" +) + +var samples = map[string]BoxError{ + "SimpleError": { + Type: "ClientError", + File: "config.lua", + Line: uint64(202), + Msg: "Unknown error", + Errno: uint64(0), + Code: uint64(0), + }, + "AccessDeniedError": { + Type: "AccessDeniedError", + File: "/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c", + Line: uint64(535), + Msg: "Execute access to function 'forbidden_function' is denied for user 'no_grants'", + Errno: uint64(0), + Code: uint64(42), + Fields: map[string]interface{}{ + "object_type": "function", + "object_name": "forbidden_function", + "access_type": "Execute", + }, + }, + "ChainedError": { + Type: "ClientError", + File: "config.lua", + Line: uint64(205), + Msg: "Timeout exceeded", + Errno: uint64(0), + Code: uint64(78), + Prev: &BoxError{ + Type: "ClientError", + File: "config.lua", + Line: uint64(202), + Msg: "Unknown error", + Errno: uint64(0), + Code: uint64(0), + }, + }, +} + +var stringCases = map[string]struct { + e BoxError + s string +}{ + "SimpleError": { + samples["SimpleError"], + "Unknown error (ClientError, code 0x0), see config.lua line 202", + }, + "AccessDeniedError": { + samples["AccessDeniedError"], + "Execute access to function 'forbidden_function' is denied for user " + + "'no_grants' (AccessDeniedError, code 0x2a), see " + + "/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c line 535", + }, + "ChainedError": { + samples["ChainedError"], + "Timeout exceeded (ClientError, code 0x4e), see config.lua line 205: " + + "Unknown error (ClientError, code 0x0), see config.lua line 202", + }, +} + +func TestBoxErrorStringRepr(t *testing.T) { + for name, testcase := range stringCases { + t.Run(name, func(t *testing.T) { + require.Equal(t, testcase.s, testcase.e.Error()) + }) + } +} + +var mpDecodeSamples = map[string]struct { + b []byte + ok bool + err *regexp.Regexp +}{ + "OuterMapInvalidLen": { + []byte{0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding map length`), + }, + "OuterMapInvalidKey": { + []byte{0x81, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding int64`), + }, + "OuterMapExtraKey": { + []byte{0x82, 0x00, 0x91, 0x81, 0x02, 0x01, 0x11, 0x00}, + true, + regexp.MustCompile(``), + }, + "OuterMapExtraInvalidKey": { + []byte{0x81, 0x11, 0x81}, + false, + regexp.MustCompile(`EOF`), + }, + "ArrayInvalidLen": { + []byte{0x81, 0x00, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding array length`), + }, + "ArrayZeroLen": { + []byte{0x81, 0x00, 0x90}, + false, + regexp.MustCompile(`msgpack: unexpected empty BoxError stack on decode`), + }, + "InnerMapInvalidLen": { + []byte{0x81, 0x00, 0x91, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding map length`), + }, + "InnerMapInvalidKey": { + []byte{0x81, 0x00, 0x91, 0x81, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding int64`), + }, + "InnerMapInvalidErrorType": { + []byte{0x81, 0x00, 0x91, 0x81, 0x00, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`), + }, + "InnerMapInvalidErrorFile": { + []byte{0x81, 0x00, 0x91, 0x81, 0x01, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`), + }, + "InnerMapInvalidErrorLine": { + []byte{0x81, 0x00, 0x91, 0x81, 0x02, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding uint64`), + }, + "InnerMapInvalidErrorMessage": { + []byte{0x81, 0x00, 0x91, 0x81, 0x03, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`), + }, + "InnerMapInvalidErrorErrno": { + []byte{0x81, 0x00, 0x91, 0x81, 0x04, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding uint64`), + }, + "InnerMapInvalidErrorErrcode": { + []byte{0x81, 0x00, 0x91, 0x81, 0x05, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding uint64`), + }, + "InnerMapInvalidErrorFields": { + []byte{0x81, 0x00, 0x91, 0x81, 0x06, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding map length`), + }, + "InnerMapInvalidErrorFieldsKey": { + []byte{0x81, 0x00, 0x91, 0x81, 0x06, 0x81, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`), + }, + "InnerMapInvalidErrorFieldsValue": { + []byte{0x81, 0x00, 0x91, 0x81, 0x06, 0x81, 0xa3, 0x6b, 0x65, 0x79, 0xc1}, + false, + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding interface{}`), + }, + "InnerMapExtraKey": { + []byte{0x81, 0x00, 0x91, 0x81, 0x21, 0x00}, + true, + regexp.MustCompile(``), + }, + "InnerMapExtraInvalidKey": { + []byte{0x81, 0x00, 0x91, 0x81, 0x21, 0x81}, + false, + regexp.MustCompile(`EOF`), + }, +} + +func TestMessagePackDecode(t *testing.T) { + for name, testcase := range mpDecodeSamples { + t.Run(name, func(t *testing.T) { + var val *BoxError = &BoxError{} + err := val.UnmarshalMsgpack(testcase.b) + if testcase.ok { + require.Nilf(t, err, "No errors on decode") + } else { + require.Regexp(t, testcase.err, err.Error()) + } + }) + } +} + +func TestMessagePackUnmarshalToNil(t *testing.T) { + var val *BoxError = nil + require.PanicsWithValue(t, "cannot unmarshal to a nil pointer", + func() { val.UnmarshalMsgpack(mpDecodeSamples["InnerMapExtraKey"].b) }) +} diff --git a/config.lua b/config.lua index 5ab98cdfe..c2d52d209 100644 --- a/config.lua +++ b/config.lua @@ -130,6 +130,8 @@ box.once("init", function() -- grants for sql tests box.schema.user.grant('test', 'create,read,write,drop,alter', 'space') box.schema.user.grant('test', 'create', 'sequence') + + box.schema.user.create('no_grants') end) local function func_name() @@ -157,6 +159,41 @@ local function push_func(cnt) end rawset(_G, 'push_func', push_func) +local function tarantool_version_at_least(wanted_major, wanted_minor, wanted_patch) + -- https://github.com/tarantool/crud/blob/733528be02c1ffa3dacc12c034ee58c9903127fc/test/helper.lua#L316-L337 + local major_minor_patch = _TARANTOOL:split('-', 1)[1] + local major_minor_patch_parts = major_minor_patch:split('.', 2) + + local major = tonumber(major_minor_patch_parts[1]) + local minor = tonumber(major_minor_patch_parts[2]) + local patch = tonumber(major_minor_patch_parts[3]) + + if major < (wanted_major or 0) then return false end + if major > (wanted_major or 0) then return true end + + if minor < (wanted_minor or 0) then return false end + if minor > (wanted_minor or 0) then return true end + + if patch < (wanted_patch or 0) then return false end + if patch > (wanted_patch or 0) then return true end + + return true +end + +if tarantool_version_at_least(2, 4, 1) then + local e1 = box.error.new(box.error.UNKNOWN) + local e2 = box.error.new(box.error.TIMEOUT) + e2:set_prev(e1) + rawset(_G, 'chained_error', e2) + + local user = box.session.user() + box.schema.func.create('forbidden_function', {body = 'function() end'}) + box.session.su('no_grants') + local _, access_denied_error = pcall(function() box.func.forbidden_function:call() end) + box.session.su(user) + rawset(_G, 'access_denied_error', access_denied_error) +end + box.space.test:truncate() --box.schema.user.revoke('guest', 'read,write,execute', 'universe') diff --git a/const.go b/const.go index 8cd9bc490..0dd8d708d 100644 --- a/const.go +++ b/const.go @@ -35,13 +35,14 @@ const ( KeyExpression = 0x27 KeyDefTuple = 0x28 KeyData = 0x30 - KeyError24 = 0x31 + KeyError24 = 0x31 /* Error in pre-2.4 format. */ KeyMetaData = 0x32 KeyBindCount = 0x34 KeySQLText = 0x40 KeySQLBind = 0x41 KeySQLInfo = 0x42 KeyStmtID = 0x43 + KeyError = 0x52 /* Extended error in >= 2.4 format. */ KeyVersion = 0x54 KeyFeatures = 0x55 KeyTimeout = 0x56 diff --git a/errors.go b/errors.go index 5677d07fc..02e4635bb 100644 --- a/errors.go +++ b/errors.go @@ -4,12 +4,17 @@ import "fmt" // Error is wrapper around error returned by Tarantool. type Error struct { - Code uint32 - Msg string + Code uint32 + Msg string + ExtendedInfo *BoxError } // Error converts an Error to a string. func (tnterr Error) Error() string { + if tnterr.ExtendedInfo != nil { + return tnterr.ExtendedInfo.Error() + } + return fmt.Sprintf("%s (0x%x)", tnterr.Msg, tnterr.Code) } diff --git a/response.go b/response.go index df0b0eca7..6c3f69c99 100644 --- a/response.go +++ b/response.go @@ -151,6 +151,7 @@ func (resp *Response) decodeBody() (err error) { var stmtID, bindCount uint64 var serverProtocolInfo ProtocolInfo var feature ProtocolFeature + var errorExtendedInfo *BoxError = nil d := newDecoder(&resp.buf) @@ -172,6 +173,10 @@ func (resp *Response) decodeBody() (err error) { if resp.Data, ok = res.([]interface{}); !ok { return fmt.Errorf("result is not array: %v", res) } + case KeyError: + if errorExtendedInfo, err = decodeBoxError(d); err != nil { + return err + } case KeyError24: if resp.Error, err = d.DecodeString(); err != nil { return err @@ -236,7 +241,7 @@ func (resp *Response) decodeBody() (err error) { if resp.Code != OkCode && resp.Code != PushCode { resp.Code &^= ErrorCodeBit - err = Error{resp.Code, resp.Error} + err = Error{resp.Code, resp.Error, errorExtendedInfo} } } return @@ -247,6 +252,8 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { offset := resp.buf.Offset() defer resp.buf.Seek(offset) + var errorExtendedInfo *BoxError = nil + var l int d := newDecoder(&resp.buf) if l, err = d.DecodeMapLen(); err != nil { @@ -262,6 +269,10 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if err = d.Decode(res); err != nil { return err } + case KeyError: + if errorExtendedInfo, err = decodeBoxError(d); err != nil { + return err + } case KeyError24: if resp.Error, err = d.DecodeString(); err != nil { return err @@ -282,7 +293,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { } if resp.Code != OkCode && resp.Code != PushCode { resp.Code &^= ErrorCodeBit - err = Error{resp.Code, resp.Error} + err = Error{resp.Code, resp.Error, errorExtendedInfo} } } return diff --git a/tarantool_test.go b/tarantool_test.go index 31d287272..0accac5e7 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3148,6 +3148,103 @@ func TestConnectionFeatureOptsImmutable(t *testing.T) { require.True(t, connected, "Reconnect success") } +func TestErrorExtendedInfoBasic(t *testing.T) { + test_helpers.SkipIfErrorExtendedInfoUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + _, err := conn.Eval("not a Lua code", []interface{}{}) + require.NotNilf(t, err, "expected error on invalid Lua code") + + ttErr, ok := err.(Error) + require.Equalf(t, ok, true, "error is built from a Tarantool error") + + expected := BoxError{ + Type: "LuajitError", + File: "eval", + Line: uint64(1), + Msg: "eval:1: unexpected symbol near 'not'", + Errno: uint64(0), + Code: uint64(32), + } + + // In fact, CheckEqualBoxErrors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + test_helpers.CheckEqualBoxErrors(t, expected, *ttErr.ExtendedInfo) +} + +func TestErrorExtendedInfoStack(t *testing.T) { + test_helpers.SkipIfErrorExtendedInfoUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + _, err := conn.Eval("error(chained_error)", []interface{}{}) + require.NotNilf(t, err, "expected error on explicit error raise") + + ttErr, ok := err.(Error) + require.Equalf(t, ok, true, "error is built from a Tarantool error") + + expected := BoxError{ + Type: "ClientError", + File: "config.lua", + Line: uint64(214), + Msg: "Timeout exceeded", + Errno: uint64(0), + Code: uint64(78), + Prev: &BoxError{ + Type: "ClientError", + File: "config.lua", + Line: uint64(213), + Msg: "Unknown error", + Errno: uint64(0), + Code: uint64(0), + }, + } + + // In fact, CheckEqualBoxErrors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + test_helpers.CheckEqualBoxErrors(t, expected, *ttErr.ExtendedInfo) +} + +func TestErrorExtendedInfoFields(t *testing.T) { + test_helpers.SkipIfErrorExtendedInfoUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + _, err := conn.Eval("error(access_denied_error)", []interface{}{}) + require.NotNilf(t, err, "expected error on forbidden action") + + ttErr, ok := err.(Error) + require.Equalf(t, ok, true, "error is built from a Tarantool error") + + expected := BoxError{ + Type: "AccessDeniedError", + File: "/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c", + Line: uint64(535), + Msg: "Execute access to function 'forbidden_function' is denied for user 'no_grants'", + Errno: uint64(0), + Code: uint64(42), + Fields: map[string]interface{}{ + "object_type": "function", + "object_name": "forbidden_function", + "access_type": "Execute", + }, + } + + // In fact, CheckEqualBoxErrors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + test_helpers.CheckEqualBoxErrors(t, expected, *ttErr.ExtendedInfo) +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/test_helpers/utils.go b/test_helpers/utils.go index dff0bb357..be25b5804 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" "github.com/tarantool/go-tarantool" ) @@ -120,3 +121,58 @@ func SkipIfIdSupported(t *testing.T) { t.Skip("Skipping test for Tarantool with non-zero protocol version and features") } } + +// CheckEqualBoxErrors checks equivalence of tarantool.BoxError objects. +// +// Tarantool errors are not comparable by nature: +// +// tarantool> msgpack.decode(mp_error_repr) == msgpack.decode(mp_error_repr) +// --- +// - false +// ... +// +// Tarantool error file and line could differ even between +// different patches. +// +// So we check equivalence of all attributes except for Line and File. +// For Line and File, we check that they are filled with some non-default values +// (lines are counted starting with 1 and empty file path is not expected too). +func CheckEqualBoxErrors(t *testing.T, expected tarantool.BoxError, actual tarantool.BoxError) { + t.Helper() + + require.Equalf(t, expected.Depth(), actual.Depth(), "Error stack depth is the same") + + for { + require.Equal(t, expected.Type, actual.Type) + require.Greater(t, len(expected.File), 0) + require.Greater(t, expected.Line, uint64(0)) + require.Equal(t, expected.Msg, actual.Msg) + require.Equal(t, expected.Errno, actual.Errno) + require.Equal(t, expected.Code, actual.Code) + require.Equal(t, expected.Fields, actual.Fields) + + if expected.Prev != nil { + // Stack depth is the same + expected = *expected.Prev + actual = *actual.Prev + } else { + break + } + } +} + +// SkipIfErrorExtendedInfoUnsupported skips test run if Tarantool without +// IPROTO_ERROR (0x52) support is used. +func SkipIfErrorExtendedInfoUnsupported(t *testing.T) { + t.Helper() + + // Tarantool provides extended error info only since 2.4.1 version. + isLess, err := IsTarantoolVersionLess(2, 4, 1) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + + if isLess { + t.Skip("Skipping test for Tarantool without error extended info support") + } +} From 319c93be53a7c75ecf0b4d5d6f72f7179511b9e4 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Mon, 28 Nov 2022 17:02:32 +0300 Subject: [PATCH 364/605] api: support error type in MessagePack Tarantool supports error extension type since version 2.4.1 [1], encoding was introduced in Tarantool 2.10.0 [2]. This patch introduces the support of Tarantool error extension type in msgpack decoders and encoders. Tarantool error extension type objects are decoded to `*tarantool.BoxError` type. `*tarantool.BoxError` may be encoded to Tarantool error extension type objects. Error extension type internals are the same as errors extended information: the only difference is that extra information is encoded as a separate error dictionary field and error extension type objects are encoded as MessagePack extension type objects. The only way to receive an error extension type object from Tarantool is to receive an explicitly built `box.error` object: either from `return box.error.new(...)` or a tuple with it. All errors raised within Tarantool (including those raised with `box.error(...)`) are encoded based on the same rules as simple errors due to backward compatibility. It is possible to create error extension type objects with Go code, but it not likely to be really useful since most of their fields is computed on error initialization on the server side (even for custom error types). This patch also adds ErrorExtensionFeature flag to client protocol features list. Without this flag, all `box.error` object sent over iproto are encoded to string. We behave like Tarantool `net.box` here: if we support the feature, we provide the feature flag. Since it may become too complicated to enable/disable feature flag through import, error extension type is available as a part of the base package, in contrary to Decimal, UUID, Datetime and Interval types which are enabled by importing underscore subpackage. 1. tarantool/tarantool#4398 2. tarantool/tarantool#6433 Closes #209 --- CHANGELOG.md | 1 + box_error.go | 113 +++++++++++++++ box_error_test.go | 293 ++++++++++++++++++++++++++++++++++++++ config.lua | 67 +++++++++ example_test.go | 2 +- msgpack.go | 4 + msgpack_helper_test.go | 14 ++ msgpack_v5.go | 4 + msgpack_v5_helper_test.go | 17 +++ protocol.go | 7 +- tarantool_test.go | 16 ++- test_helpers/utils.go | 17 +++ 12 files changed, 548 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e870c6a9d..0585e129f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support iproto feature discovery (#120) - Support errors extended information (#209) +- Error type support in MessagePack (#209) ### Changed diff --git a/box_error.go b/box_error.go index 97c5c1774..ab8981b0b 100644 --- a/box_error.go +++ b/box_error.go @@ -5,6 +5,8 @@ import ( "fmt" ) +const errorExtID = 3 + const ( keyErrorStack = 0x00 keyErrorType = 0x00 @@ -167,6 +169,105 @@ func decodeBoxError(d *decoder) (*BoxError, error) { return &errorStack[0], nil } +func encodeBoxError(enc *encoder, boxError *BoxError) error { + if boxError == nil { + return fmt.Errorf("msgpack: unexpected nil BoxError on encode") + } + + if err := enc.EncodeMapLen(1); err != nil { + return err + } + if err := encodeUint(enc, keyErrorStack); err != nil { + return err + } + + var stackDepth = boxError.Depth() + if err := enc.EncodeArrayLen(stackDepth); err != nil { + return err + } + + for ; stackDepth > 0; stackDepth-- { + fieldsLen := len(boxError.Fields) + + if fieldsLen > 0 { + if err := enc.EncodeMapLen(7); err != nil { + return err + } + } else { + if err := enc.EncodeMapLen(6); err != nil { + return err + } + } + + if err := encodeUint(enc, keyErrorType); err != nil { + return err + } + if err := enc.EncodeString(boxError.Type); err != nil { + return err + } + + if err := encodeUint(enc, keyErrorFile); err != nil { + return err + } + if err := enc.EncodeString(boxError.File); err != nil { + return err + } + + if err := encodeUint(enc, keyErrorLine); err != nil { + return err + } + if err := enc.EncodeUint64(boxError.Line); err != nil { + return err + } + + if err := encodeUint(enc, keyErrorMessage); err != nil { + return err + } + if err := enc.EncodeString(boxError.Msg); err != nil { + return err + } + + if err := encodeUint(enc, keyErrorErrno); err != nil { + return err + } + if err := enc.EncodeUint64(boxError.Errno); err != nil { + return err + } + + if err := encodeUint(enc, keyErrorErrcode); err != nil { + return err + } + if err := enc.EncodeUint64(boxError.Code); err != nil { + return err + } + + if fieldsLen > 0 { + if err := encodeUint(enc, keyErrorFields); err != nil { + return err + } + + if err := enc.EncodeMapLen(fieldsLen); err != nil { + return err + } + + for k, v := range boxError.Fields { + if err := enc.EncodeString(k); err != nil { + return err + } + if err := enc.Encode(v); err != nil { + return err + } + } + } + + if stackDepth > 1 { + boxError = boxError.Prev + } + } + + return nil +} + // UnmarshalMsgpack deserializes a BoxError value from a MessagePack // representation. func (e *BoxError) UnmarshalMsgpack(b []byte) error { @@ -184,3 +285,15 @@ func (e *BoxError) UnmarshalMsgpack(b []byte) error { return nil } } + +// MarshalMsgpack serializes the BoxError into a MessagePack representation. +func (e *BoxError) MarshalMsgpack() ([]byte, error) { + var buf bytes.Buffer + + enc := newEncoder(&buf) + if err := encodeBoxError(enc, e); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/box_error_test.go b/box_error_test.go index 76ba3290d..276ca2cf8 100644 --- a/box_error_test.go +++ b/box_error_test.go @@ -1,11 +1,13 @@ package tarantool_test import ( + "fmt" "regexp" "testing" "github.com/stretchr/testify/require" . "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" ) var samples = map[string]BoxError{ @@ -198,3 +200,294 @@ func TestMessagePackUnmarshalToNil(t *testing.T) { require.PanicsWithValue(t, "cannot unmarshal to a nil pointer", func() { val.UnmarshalMsgpack(mpDecodeSamples["InnerMapExtraKey"].b) }) } + +func TestMessagePackEncodeNil(t *testing.T) { + var val *BoxError + + _, err := val.MarshalMsgpack() + require.NotNil(t, err) + require.Equal(t, "msgpack: unexpected nil BoxError on encode", err.Error()) +} + +var space = "test_error_type" +var index = "primary" + +type TupleBoxError struct { + pk string // BoxError cannot be used as a primary key. + val BoxError +} + +func (t *TupleBoxError) EncodeMsgpack(e *encoder) error { + if err := e.EncodeArrayLen(2); err != nil { + return err + } + + if err := e.EncodeString(t.pk); err != nil { + return err + } + + return e.Encode(&t.val) +} + +func (t *TupleBoxError) DecodeMsgpack(d *decoder) error { + var err error + var l int + if l, err = d.DecodeArrayLen(); err != nil { + return err + } + if l != 2 { + return fmt.Errorf("Array length doesn't match: %d", l) + } + + if t.pk, err = d.DecodeString(); err != nil { + return err + } + + return d.Decode(&t.val) +} + +// Raw bytes encoding test is impossible for +// object with Fields since map iterating is random. +var tupleCases = map[string]struct { + tuple TupleBoxError + ttObj string +}{ + "SimpleError": { + TupleBoxError{ + "simple_error_pk", + samples["SimpleError"], + }, + "simple_error", + }, + "AccessDeniedError": { + TupleBoxError{ + "access_denied_error_pk", + samples["AccessDeniedError"], + }, + "access_denied_error", + }, + "ChainedError": { + TupleBoxError{ + "chained_error_pk", + samples["ChainedError"], + }, + "chained_error", + }, +} + +func TestErrorTypeMPEncodeDecode(t *testing.T) { + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + buf, err := marshal(&testcase.tuple) + require.Nil(t, err) + + var res TupleBoxError + err = unmarshal(buf, &res) + require.Nil(t, err) + + require.Equal(t, testcase.tuple, res) + }) + } +} + +func TestErrorTypeEval(t *testing.T) { + test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + resp, err := conn.Eval("return ...", []interface{}{&testcase.tuple.val}) + require.Nil(t, err) + require.NotNil(t, resp.Data) + require.Equal(t, len(resp.Data), 1) + actual, ok := toBoxError(resp.Data[0]) + require.Truef(t, ok, "Response data has valid type") + require.Equal(t, testcase.tuple.val, actual) + }) + } +} + +func TestErrorTypeEvalTyped(t *testing.T) { + test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + var res []BoxError + err := conn.EvalTyped("return ...", []interface{}{&testcase.tuple.val}, &res) + require.Nil(t, err) + require.NotNil(t, res) + require.Equal(t, len(res), 1) + require.Equal(t, testcase.tuple.val, res[0]) + }) + } +} + +func TestErrorTypeInsert(t *testing.T) { + test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + truncateEval := fmt.Sprintf("box.space[%q]:truncate()", space) + _, err := conn.Eval(truncateEval, []interface{}{}) + require.Nil(t, err) + + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + _, err = conn.Insert(space, &testcase.tuple) + require.Nil(t, err) + + checkEval := fmt.Sprintf(` + local err = rawget(_G, %q) + assert(err ~= nil) + + local tuple = box.space[%q]:get(%q) + assert(tuple ~= nil) + + local tuple_err = tuple[2] + assert(tuple_err ~= nil) + + return compare_box_errors(err, tuple_err) + `, testcase.ttObj, space, testcase.tuple.pk) + + // In fact, compare_box_errors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + _, err := conn.Eval(checkEval, []interface{}{}) + require.Nilf(t, err, "Tuple has been successfully inserted") + }) + } +} + +func TestErrorTypeInsertTyped(t *testing.T) { + test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + truncateEval := fmt.Sprintf("box.space[%q]:truncate()", space) + _, err := conn.Eval(truncateEval, []interface{}{}) + require.Nil(t, err) + + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + var res []TupleBoxError + err = conn.InsertTyped(space, &testcase.tuple, &res) + require.Nil(t, err) + require.NotNil(t, res) + require.Equal(t, len(res), 1) + require.Equal(t, testcase.tuple, res[0]) + + checkEval := fmt.Sprintf(` + local err = rawget(_G, %q) + assert(err ~= nil) + + local tuple = box.space[%q]:get(%q) + assert(tuple ~= nil) + + local tuple_err = tuple[2] + assert(tuple_err ~= nil) + + return compare_box_errors(err, tuple_err) + `, testcase.ttObj, space, testcase.tuple.pk) + + // In fact, compare_box_errors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + _, err := conn.Eval(checkEval, []interface{}{}) + require.Nilf(t, err, "Tuple has been successfully inserted") + }) + } +} + +func TestErrorTypeSelect(t *testing.T) { + test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + truncateEval := fmt.Sprintf("box.space[%q]:truncate()", space) + _, err := conn.Eval(truncateEval, []interface{}{}) + require.Nil(t, err) + + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + insertEval := fmt.Sprintf(` + local err = rawget(_G, %q) + assert(err ~= nil) + + local tuple = box.space[%q]:insert{%q, err} + assert(tuple ~= nil) + `, testcase.ttObj, space, testcase.tuple.pk) + + _, err := conn.Eval(insertEval, []interface{}{}) + require.Nilf(t, err, "Tuple has been successfully inserted") + + var resp *Response + var offset uint32 = 0 + var limit uint32 = 1 + resp, err = conn.Select(space, index, offset, limit, IterEq, []interface{}{testcase.tuple.pk}) + require.Nil(t, err) + require.NotNil(t, resp.Data) + require.Equalf(t, len(resp.Data), 1, "Exactly one tuple had been found") + tpl, ok := resp.Data[0].([]interface{}) + require.Truef(t, ok, "Tuple has valid type") + require.Equal(t, testcase.tuple.pk, tpl[0]) + var actual BoxError + actual, ok = toBoxError(tpl[1]) + require.Truef(t, ok, "BoxError tuple field has valid type") + // In fact, CheckEqualBoxErrors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + test_helpers.CheckEqualBoxErrors(t, testcase.tuple.val, actual) + }) + } +} + +func TestErrorTypeSelectTyped(t *testing.T) { + test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + truncateEval := fmt.Sprintf("box.space[%q]:truncate()", space) + _, err := conn.Eval(truncateEval, []interface{}{}) + require.Nil(t, err) + + for name, testcase := range tupleCases { + t.Run(name, func(t *testing.T) { + insertEval := fmt.Sprintf(` + local err = rawget(_G, %q) + assert(err ~= nil) + + local tuple = box.space[%q]:insert{%q, err} + assert(tuple ~= nil) + `, testcase.ttObj, space, testcase.tuple.pk) + + _, err := conn.Eval(insertEval, []interface{}{}) + require.Nilf(t, err, "Tuple has been successfully inserted") + + var offset uint32 = 0 + var limit uint32 = 1 + var resp []TupleBoxError + err = conn.SelectTyped(space, index, offset, limit, IterEq, []interface{}{testcase.tuple.pk}, &resp) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equalf(t, len(resp), 1, "Exactly one tuple had been found") + require.Equal(t, testcase.tuple.pk, resp[0].pk) + // In fact, CheckEqualBoxErrors does not check than File and Line + // of connector BoxError are equal to the Tarantool ones + // since they may differ between different Tarantool versions + // and editions. + test_helpers.CheckEqualBoxErrors(t, testcase.tuple.val, resp[0].val) + }) + } +} diff --git a/config.lua b/config.lua index c2d52d209..bb976ef43 100644 --- a/config.lua +++ b/config.lua @@ -116,6 +116,21 @@ box.once("init", function() } end + local s = box.schema.space.create('test_error_type', { + id = 522, + temporary = true, + if_not_exists = true, + field_count = 2, + -- You can't specify box.error as format type, + -- but can put box.error objects. + }) + s:create_index('primary', { + type = 'tree', + unique = true, + parts = {1, 'string'}, + if_not_exists = true + }) + --box.schema.user.grant('guest', 'read,write,execute', 'universe') box.schema.func.create('box.info') box.schema.func.create('simple_concat') @@ -126,6 +141,7 @@ box.once("init", function() box.schema.user.grant('test', 'read,write', 'space', 'test') box.schema.user.grant('test', 'read,write', 'space', 'schematest') box.schema.user.grant('test', 'read,write', 'space', 'test_perf') + box.schema.user.grant('test', 'read,write', 'space', 'test_error_type') -- grants for sql tests box.schema.user.grant('test', 'create,read,write,drop,alter', 'space') @@ -182,6 +198,8 @@ end if tarantool_version_at_least(2, 4, 1) then local e1 = box.error.new(box.error.UNKNOWN) + rawset(_G, 'simple_error', e1) + local e2 = box.error.new(box.error.TIMEOUT) e2:set_prev(e1) rawset(_G, 'chained_error', e2) @@ -192,6 +210,55 @@ if tarantool_version_at_least(2, 4, 1) then local _, access_denied_error = pcall(function() box.func.forbidden_function:call() end) box.session.su(user) rawset(_G, 'access_denied_error', access_denied_error) + + -- cdata structure is as follows: + -- + -- tarantool> err:unpack() + -- - code: val + -- base_type: val + -- type: val + -- message: val + -- field1: val + -- field2: val + -- trace: + -- - file: val + -- line: val + + local function compare_box_error_attributes(expected, actual, attr_provider) + for attr, _ in pairs(attr_provider:unpack()) do + if (attr ~= 'prev') and (attr ~= 'trace') then + if expected[attr] ~= actual[attr] then + error(('%s expected %s is not equal to actual %s'):format( + attr, expected[attr], actual[attr])) + end + end + end + end + + local function compare_box_errors(expected, actual) + if (expected == nil) and (actual ~= nil) then + error(('Expected error stack is empty, but actual error ' .. + 'has previous %s (%s) error'):format( + actual.type, actual.message)) + end + + if (expected ~= nil) and (actual == nil) then + error(('Actual error stack is empty, but expected error ' .. + 'has previous %s (%s) error'):format( + expected.type, expected.message)) + end + + compare_box_error_attributes(expected, actual, expected) + compare_box_error_attributes(expected, actual, actual) + + if (expected.prev ~= nil) or (actual.prev ~= nil) then + return compare_box_errors(expected.prev, actual.prev) + end + + return true + end + + rawset(_G, 'compare_box_errors', compare_box_errors) end box.space.test:truncate() diff --git a/example_test.go b/example_test.go index 15574d099..54202eb46 100644 --- a/example_test.go +++ b/example_test.go @@ -329,7 +329,7 @@ func ExampleProtocolVersion() { fmt.Println("Connector client protocol features:", clientProtocolInfo.Features) // Output: // Connector client protocol version: 4 - // Connector client protocol features: [StreamsFeature TransactionsFeature] + // Connector client protocol features: [StreamsFeature TransactionsFeature ErrorExtensionFeature] } func getTestTxnOpts() tarantool.Opts { diff --git a/msgpack.go b/msgpack.go index 34ecc4b3b..9977e9399 100644 --- a/msgpack.go +++ b/msgpack.go @@ -48,3 +48,7 @@ func msgpackIsString(code byte) bool { return msgpcode.IsFixedString(code) || code == msgpcode.Str8 || code == msgpcode.Str16 || code == msgpcode.Str32 } + +func init() { + msgpack.RegisterExt(errorExtID, &BoxError{}) +} diff --git a/msgpack_helper_test.go b/msgpack_helper_test.go index fa47c2fda..896c105d3 100644 --- a/msgpack_helper_test.go +++ b/msgpack_helper_test.go @@ -4,6 +4,7 @@ package tarantool_test import ( + "github.com/tarantool/go-tarantool" "gopkg.in/vmihailenco/msgpack.v2" ) @@ -13,3 +14,16 @@ type decoder = msgpack.Decoder func encodeUint(e *encoder, v uint64) error { return e.EncodeUint(uint(v)) } + +func toBoxError(i interface{}) (v tarantool.BoxError, ok bool) { + v, ok = i.(tarantool.BoxError) + return +} + +func marshal(v interface{}) ([]byte, error) { + return msgpack.Marshal(v) +} + +func unmarshal(data []byte, v interface{}) error { + return msgpack.Unmarshal(data, v) +} diff --git a/msgpack_v5.go b/msgpack_v5.go index 806dd1632..e8cd9aa29 100644 --- a/msgpack_v5.go +++ b/msgpack_v5.go @@ -52,3 +52,7 @@ func msgpackIsString(code byte) bool { return msgpcode.IsFixedString(code) || code == msgpcode.Str8 || code == msgpcode.Str16 || code == msgpcode.Str32 } + +func init() { + msgpack.RegisterExt(errorExtID, (*BoxError)(nil)) +} diff --git a/msgpack_v5_helper_test.go b/msgpack_v5_helper_test.go index 347c1ba95..88154c26f 100644 --- a/msgpack_v5_helper_test.go +++ b/msgpack_v5_helper_test.go @@ -4,6 +4,7 @@ package tarantool_test import ( + "github.com/tarantool/go-tarantool" "github.com/vmihailenco/msgpack/v5" ) @@ -13,3 +14,19 @@ type decoder = msgpack.Decoder func encodeUint(e *encoder, v uint64) error { return e.EncodeUint(v) } + +func toBoxError(i interface{}) (v tarantool.BoxError, ok bool) { + var ptr *tarantool.BoxError + if ptr, ok = i.(*tarantool.BoxError); ok { + v = *ptr + } + return +} + +func marshal(v interface{}) ([]byte, error) { + return msgpack.Marshal(v) +} + +func unmarshal(data []byte, v interface{}) error { + return msgpack.Unmarshal(data, v) +} diff --git a/protocol.go b/protocol.go index 1eaf60e2b..fa890fdd4 100644 --- a/protocol.go +++ b/protocol.go @@ -42,7 +42,7 @@ const ( // (unsupported by connector). ErrorExtensionFeature ProtocolFeature = 2 // WatchersFeature represents support of watchers - // (unsupported by connector). + // (supported by connector). WatchersFeature ProtocolFeature = 3 // PaginationFeature represents support of pagination // (unsupported by connector). @@ -76,10 +76,13 @@ var clientProtocolInfo ProtocolInfo = ProtocolInfo{ // 1.10.0. Version: ProtocolVersion(4), // Streams and transactions were introduced in protocol version 1 - // (Tarantool 2.10.0), in connector since 1.7.0. + // (Tarantool 2.10.0), in connector since 1.7.0. Error extension + // type was introduced in protocol version 2 (Tarantool 2.10.0), + // in connector since 1.10.0. Features: []ProtocolFeature{ StreamsFeature, TransactionsFeature, + ErrorExtensionFeature, }, } diff --git a/tarantool_test.go b/tarantool_test.go index 0accac5e7..202c10b17 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2868,8 +2868,12 @@ func TestConnectionProtocolInfoSupported(t *testing.T) { require.Equal(t, clientProtocolInfo, ProtocolInfo{ - Version: ProtocolVersion(4), - Features: []ProtocolFeature{StreamsFeature, TransactionsFeature}, + Version: ProtocolVersion(4), + Features: []ProtocolFeature{ + StreamsFeature, + TransactionsFeature, + ErrorExtensionFeature, + }, }) serverProtocolInfo := conn.ServerProtocolInfo() @@ -2997,8 +3001,12 @@ func TestConnectionProtocolInfoUnsupported(t *testing.T) { require.Equal(t, clientProtocolInfo, ProtocolInfo{ - Version: ProtocolVersion(4), - Features: []ProtocolFeature{StreamsFeature, TransactionsFeature}, + Version: ProtocolVersion(4), + Features: []ProtocolFeature{ + StreamsFeature, + TransactionsFeature, + ErrorExtensionFeature, + }, }) serverProtocolInfo := conn.ServerProtocolInfo() diff --git a/test_helpers/utils.go b/test_helpers/utils.go index be25b5804..5081de6c2 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -176,3 +176,20 @@ func SkipIfErrorExtendedInfoUnsupported(t *testing.T) { t.Skip("Skipping test for Tarantool without error extended info support") } } + +// SkipIfErrorExtendedInfoUnsupported skips test run if Tarantool without +// MP_ERROR type over iproto support is used. +func SkipIfErrorMessagePackTypeUnsupported(t *testing.T) { + t.Helper() + + // Tarantool error type over MessagePack supported only since 2.10.0 version. + isLess, err := IsTarantoolVersionLess(2, 10, 0) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + + if isLess { + t.Skip("Skipping test for Tarantool without support of error type over MessagePack") + t.Skip("Skipping test for Tarantool without error extended info support") + } +} From 519b07b34e6e02cf29fae5e1c56d9485c64914a2 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 5 Dec 2022 13:03:44 +0300 Subject: [PATCH 365/605] ci: bump setup-tarantool --- .github/workflows/check.yaml | 2 +- .github/workflows/testing.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 8769f088f..d6a72e55b 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@master - name: Setup Tarantool - uses: tarantool/setup-tarantool@v1 + uses: tarantool/setup-tarantool@v2 with: tarantool-version: '2.8' diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index e6d3c12c3..093f15fda 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -50,7 +50,7 @@ jobs: - name: Setup Tarantool ${{ matrix.tarantool }} if: matrix.tarantool != '2.x-latest' - uses: tarantool/setup-tarantool@v1 + uses: tarantool/setup-tarantool@v2 with: tarantool-version: ${{ matrix.tarantool }} From 525b294ef4fc696f0f52ece2591f9ac5fac49ebc Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 7 Dec 2022 10:42:45 +0300 Subject: [PATCH 366/605] ci: run ce tests on ubuntu-20.04 Due to a bug [1], our test workflow fails on Tarantool 1.10. It is a quick fix until the problem is fixed. 1. https://github.com/tarantool/setup-tarantool/issues/37 --- .github/workflows/testing.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 093f15fda..d06549566 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -21,7 +21,9 @@ jobs: github.event.pull_request.head.repo.full_name != github.repository) || (github.event_name == 'workflow_dispatch') - runs-on: ubuntu-latest + # We could replace it with ubuntu-latest after fixing the bug: + # https://github.com/tarantool/setup-tarantool/issues/37 + runs-on: ubuntu-20.04 strategy: fail-fast: false From a78709dbd1c3c3ecadcf358fd118bb32b8257349 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 30 Nov 2022 12:11:09 +0300 Subject: [PATCH 367/605] code health: unify log messages format The patch removes a last newline character from log messages because the character will be added anyway [1][2]. 1. https://pkg.go.dev/log#Logger.Printf 2. https://pkg.go.dev/log#Output Part of #119 --- connection.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connection.go b/connection.go index a2251aaff..7255d2435 100644 --- a/connection.go +++ b/connection.go @@ -77,10 +77,10 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac case LogReconnectFailed: reconnects := v[0].(uint) err := v[1].(error) - log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s\n", reconnects, conn.opts.MaxReconnects, conn.addr, err.Error()) + log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s", reconnects, conn.opts.MaxReconnects, conn.addr, err) case LogLastReconnectFailed: err := v[0].(error) - log.Printf("tarantool: last reconnect to %s failed: %s, giving it up.\n", conn.addr, err.Error()) + log.Printf("tarantool: last reconnect to %s failed: %s, giving it up", conn.addr, err) case LogUnexpectedResultId: resp := v[0].(*Response) log.Printf("tarantool: connection %s got unexpected resultId (%d) in response", conn.addr, resp.RequestId) From 265f96c9ff5f39e436e7645845fea4589762174d Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 21 Nov 2022 19:00:47 +0300 Subject: [PATCH 368/605] api: add events subscription support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A user can create watcher by the Connection.NewWatcher() call: watcher = conn.NewWatcker("key", func(event WatchEvent) { // The callback code. }) After that, the watcher callback is invoked for the first time. In this case, the callback is triggered whether or not the key has already been broadcast. All subsequent invocations are triggered with box.broadcast() called on the remote host. If a watcher is subscribed for a key that has not been broadcast yet, the callback is triggered only once, after the registration of the watcher. If the key is updated while the watcher callback is running, the callback will be invoked again with the latest value as soon as it returns. Multiple watchers can be created for one key. If you don’t need the watcher anymore, you can unregister it using the Unregister method: watcher.Unregister() The api is similar to net.box implementation [1]. It also adds a BroadcastRequest to make it easier to send broadcast messages. 1. https://www.tarantool.io/en/doc/latest/reference/reference_lua/net_box/#conn-watch Closes #119 --- CHANGELOG.md | 1 + connection.go | 354 +++++++++++++++++++++- connection_pool/connection_pool.go | 195 +++++++++--- connection_pool/connection_pool_test.go | 321 ++++++++++++++++++++ connection_pool/connector.go | 8 + connection_pool/connector_test.go | 39 +++ connection_pool/example_test.go | 53 ++++ connection_pool/pooler.go | 2 + connection_pool/round_robin.go | 24 +- connection_pool/round_robin_test.go | 19 ++ connection_pool/watcher.go | 133 +++++++++ connector.go | 1 + const.go | 5 + example_test.go | 47 ++- multi/multi.go | 10 + multi/multi_test.go | 24 ++ protocol.go | 9 +- request.go | 8 + request_test.go | 65 ++++ tarantool_test.go | 381 ++++++++++++++++++++++++ test_helpers/request_mock.go | 4 + test_helpers/utils.go | 60 ++-- watch.go | 138 +++++++++ 23 files changed, 1827 insertions(+), 74 deletions(-) create mode 100644 connection_pool/watcher.go create mode 100644 watch.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0585e129f..9c2c49d39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support iproto feature discovery (#120) - Support errors extended information (#209) - Error type support in MessagePack (#209) +- Event subscription support (#119) ### Changed diff --git a/connection.go b/connection.go index 7255d2435..b657dc2ae 100644 --- a/connection.go +++ b/connection.go @@ -54,6 +54,8 @@ const ( // LogUnexpectedResultId is logged when response with unknown id was received. // Most probably it is due to request timeout. LogUnexpectedResultId + // LogWatchEventReadFailed is logged when failed to read a watch event. + LogWatchEventReadFailed ) // ConnEvent is sent throw Notify channel specified in Opts. @@ -63,6 +65,12 @@ type ConnEvent struct { When time.Time } +// A raw watch event. +type connWatchEvent struct { + key string + value interface{} +} + var epoch = time.Now() // Logger is logger type expected to be passed in options. @@ -84,6 +92,9 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac case LogUnexpectedResultId: resp := v[0].(*Response) log.Printf("tarantool: connection %s got unexpected resultId (%d) in response", conn.addr, resp.RequestId) + case LogWatchEventReadFailed: + err := v[0].(error) + log.Printf("tarantool: unable to parse watch event: %s", err) default: args := append([]interface{}{"tarantool: unexpected event ", event, conn}, v...) log.Print(args...) @@ -149,6 +160,8 @@ type Connection struct { lastStreamId uint64 serverProtocolInfo ProtocolInfo + // watchMap is a map of key -> chan watchState. + watchMap sync.Map } var _ = Connector(&Connection{}) // Check compatibility with connector interface. @@ -531,7 +544,7 @@ func (conn *Connection) dial() (err error) { return fmt.Errorf("identify: %w", err) } - // Auth + // Auth. if opts.User != "" { scr, err := scramble(conn.Greeting.auth, opts.Pass) if err != nil { @@ -549,7 +562,30 @@ func (conn *Connection) dial() (err error) { } } - // Only if connected and authenticated. + // Watchers. + conn.watchMap.Range(func(key, value interface{}) bool { + st := value.(chan watchState) + state := <-st + if state.unready != nil { + return true + } + + req := newWatchRequest(key.(string)) + if err = conn.writeRequest(w, req); err != nil { + st <- state + return false + } + state.ack = true + + st <- state + return true + }) + + if err != nil { + return fmt.Errorf("unable to register watch: %w", err) + } + + // Only if connected and fully initialized. conn.lockShards() conn.c = connection atomic.StoreUint32(&conn.state, connConnected) @@ -843,7 +879,52 @@ func (conn *Connection) writer(w *bufio.Writer, c net.Conn) { } } +func readWatchEvent(reader io.Reader) (connWatchEvent, error) { + keyExist := false + event := connWatchEvent{} + d := newDecoder(reader) + + l, err := d.DecodeMapLen() + if err != nil { + return event, err + } + + for ; l > 0; l-- { + cd, err := d.DecodeInt() + if err != nil { + return event, err + } + + switch cd { + case KeyEvent: + if event.key, err = d.DecodeString(); err != nil { + return event, err + } + keyExist = true + case KeyEventData: + if event.value, err = d.DecodeInterface(); err != nil { + return event, err + } + default: + if err = d.Skip(); err != nil { + return event, err + } + } + } + + if !keyExist { + return event, errors.New("watch event does not have a key") + } + + return event, nil +} + func (conn *Connection) reader(r *bufio.Reader, c net.Conn) { + events := make(chan connWatchEvent, 1024) + defer close(events) + + go conn.eventer(events) + for atomic.LoadUint32(&conn.state) != connClosed { respBytes, err := conn.read(r) if err != nil { @@ -858,7 +939,14 @@ func (conn *Connection) reader(r *bufio.Reader, c net.Conn) { } var fut *Future = nil - if resp.Code == PushCode { + if resp.Code == EventCode { + if event, err := readWatchEvent(&resp.buf); err == nil { + events <- event + } else { + conn.opts.Logger.Report(LogWatchEventReadFailed, conn, err) + } + continue + } else if resp.Code == PushCode { if fut = conn.peekFuture(resp.RequestId); fut != nil { fut.AppendPush(resp) } @@ -868,12 +956,38 @@ func (conn *Connection) reader(r *bufio.Reader, c net.Conn) { conn.markDone(fut) } } + if fut == nil { conn.opts.Logger.Report(LogUnexpectedResultId, conn, resp) } } } +// eventer goroutine gets watch events and updates values for watchers. +func (conn *Connection) eventer(events <-chan connWatchEvent) { + for event := range events { + if value, ok := conn.watchMap.Load(event.key); ok { + st := value.(chan watchState) + state := <-st + state.value = event.value + if state.version == math.MaxUint64 { + state.version = initWatchEventVersion + 1 + } else { + state.version += 1 + } + state.ack = false + if state.changed != nil { + close(state.changed) + state.changed = nil + } + st <- state + } + // It is possible to get IPROTO_EVENT after we already send + // IPROTO_UNWATCH due to processing on a Tarantool side or slow + // read from the network, so it looks like an expected behavior. + } +} + func (conn *Connection) newFuture(ctx context.Context) (fut *Future) { fut = NewFuture() if conn.rlimit != nil && conn.opts.RLimitAction == RLimitDrop { @@ -1029,6 +1143,18 @@ func (conn *Connection) putFuture(fut *Future, req Request, streamId uint64) { return } shard.bufmut.Unlock() + + if req.Async() { + if fut = conn.fetchFuture(reqid); fut != nil { + resp := &Response{ + RequestId: reqid, + Code: OkCode, + } + fut.SetResponse(resp) + conn.markDone(fut) + } + } + if firstWritten { conn.dirtyShard <- shardn } @@ -1233,6 +1359,228 @@ func (conn *Connection) NewStream() (*Stream, error) { }, nil } +// watchState is the current state of the watcher. See the idea at p. 70, 105: +// https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view +type watchState struct { + // value is a current value. + value interface{} + // version is a current version of the value. The only reason for uint64: + // go 1.13 has no math.Uint. + version uint64 + // ack true if the acknowledge is already sent. + ack bool + // cnt is a count of active watchers for the key. + cnt int + // changed is a channel for broadcast the value changes. + changed chan struct{} + // unready channel exists if a state is not ready to work (subscription + // or unsubscription in progress). + unready chan struct{} +} + +// initWatchEventVersion is an initial version until no events from Tarantool. +const initWatchEventVersion uint64 = 0 + +// connWatcher is an internal implementation of the Watcher interface. +type connWatcher struct { + unregister sync.Once + // done is closed when the watcher is unregistered, but the watcher + // goroutine is not yet finished. + done chan struct{} + // finished is closed when the watcher is unregistered and the watcher + // goroutine is finished. + finished chan struct{} +} + +// Unregister unregisters the connection watcher. +func (w *connWatcher) Unregister() { + w.unregister.Do(func() { + close(w.done) + }) + <-w.finished +} + +// subscribeWatchChannel returns an existing one or a new watch state channel +// for the key. It also increases a counter of active watchers for the channel. +func subscribeWatchChannel(conn *Connection, key string) (chan watchState, error) { + var st chan watchState + + for st == nil { + if val, ok := conn.watchMap.Load(key); !ok { + st = make(chan watchState, 1) + state := watchState{ + value: nil, + version: initWatchEventVersion, + ack: false, + cnt: 0, + changed: nil, + unready: make(chan struct{}), + } + st <- state + + if val, loaded := conn.watchMap.LoadOrStore(key, st); !loaded { + if _, err := conn.Do(newWatchRequest(key)).Get(); err != nil { + conn.watchMap.Delete(key) + close(state.unready) + return nil, err + } + // It is a successful subsctiption to a watch events by itself. + state = <-st + state.cnt = 1 + close(state.unready) + state.unready = nil + st <- state + continue + } else { + close(state.unready) + close(st) + st = val.(chan watchState) + } + } else { + st = val.(chan watchState) + } + + // It is an existing channel created outside. It may be in the + // unready state. + state := <-st + if state.unready == nil { + state.cnt += 1 + } + st <- state + + if state.unready != nil { + // Wait for an update and retry. + <-state.unready + st = nil + } + } + + return st, nil +} + +// NewWatcher creates a new Watcher object for the connection. +// +// You need to require WatchersFeature to use watchers, see examples for the +// function. +// +// After watcher creation, the watcher callback is invoked for the first time. +// In this case, the callback is triggered whether or not the key has already +// been broadcast. All subsequent invocations are triggered with +// box.broadcast() called on the remote host. If a watcher is subscribed for a +// key that has not been broadcast yet, the callback is triggered only once, +// after the registration of the watcher. +// +// The watcher callbacks are always invoked in a separate goroutine. A watcher +// callback is never executed in parallel with itself, but they can be executed +// in parallel to other watchers. +// +// If the key is updated while the watcher callback is running, the callback +// will be invoked again with the latest value as soon as it returns. +// +// Watchers survive reconnection. All registered watchers are automatically +// resubscribed when the connection is reestablished. +// +// Keep in mind that garbage collection of a watcher handle doesn’t lead to the +// watcher’s destruction. In this case, the watcher remains registered. You +// need to call Unregister() directly. +// +// Unregister() guarantees that there will be no the watcher's callback calls +// after it, but Unregister() call from the callback leads to a deadlock. +// +// See: +// https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_events/#box-watchers +// +// Since 1.10.0 +func (conn *Connection) NewWatcher(key string, callback WatchCallback) (Watcher, error) { + // We need to check the feature because the IPROTO_WATCH request is + // asynchronous. We do not expect any response from a Tarantool instance + // That's why we can't just check the Tarantool response for an unsupported + // request error. + watchersRequired := false + for _, feature := range conn.opts.RequiredProtocolInfo.Features { + if feature == WatchersFeature { + watchersRequired = true + break + } + } + + if !watchersRequired { + err := fmt.Errorf("the feature %s must be required by connection "+ + "options to create a watcher", WatchersFeature) + return nil, err + } + + st, err := subscribeWatchChannel(conn, key) + if err != nil { + return nil, err + } + + // Start the watcher goroutine. + done := make(chan struct{}) + finished := make(chan struct{}) + + go func() { + version := initWatchEventVersion + for { + state := <-st + if state.changed == nil { + state.changed = make(chan struct{}) + } + st <- state + + if state.version != version { + callback(WatchEvent{ + Conn: conn, + Key: key, + Value: state.value, + }) + version = state.version + + // Do we need to acknowledge the notification? + state = <-st + sendAck := !state.ack && version == state.version + if sendAck { + state.ack = true + } + st <- state + + if sendAck { + conn.Do(newWatchRequest(key)).Get() + // We expect a reconnect and re-subscribe if it fails to + // send the watch request. So it looks ok do not check a + // result. + } + } + + select { + case <-done: + state := <-st + state.cnt -= 1 + if state.cnt == 0 { + state.unready = make(chan struct{}) + } + st <- state + + if state.cnt == 0 { + // The last one sends IPROTO_UNWATCH. + conn.Do(newUnwatchRequest(key)).Get() + conn.watchMap.Delete(key) + close(state.unready) + } + + close(finished) + return + case <-state.changed: + } + } + }() + + return &connWatcher{ + done: done, + finished: finished, + }, nil +} + // checkProtocolInfo checks that expected protocol version is // and protocol features are supported. func checkProtocolInfo(expected ProtocolInfo, actual ProtocolInfo) error { diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 2891a88ab..569669be4 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -91,12 +91,13 @@ type ConnectionPool struct { connOpts tarantool.Opts opts OptsPool - state state - done chan struct{} - roPool *RoundRobinStrategy - rwPool *RoundRobinStrategy - anyPool *RoundRobinStrategy - poolsMutex sync.RWMutex + state state + done chan struct{} + roPool *RoundRobinStrategy + rwPool *RoundRobinStrategy + anyPool *RoundRobinStrategy + poolsMutex sync.RWMutex + watcherContainer watcherContainer } var _ Pooler = (*ConnectionPool)(nil) @@ -640,25 +641,6 @@ func (connPool *ConnectionPool) ExecuteAsync(expr string, args interface{}, user return conn.ExecuteAsync(expr, args) } -// Do sends the request and returns a future. -// For requests that belong to an only one connection (e.g. Unprepare or ExecutePrepared) -// the argument of type Mode is unused. -func (connPool *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarantool.Future { - if connectedReq, ok := req.(tarantool.ConnectedRequest); ok { - conn, _ := connPool.getConnectionFromPool(connectedReq.Conn().Addr()) - if conn == nil { - return newErrorFuture(fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool")) - } - return connectedReq.Conn().Do(req) - } - conn, err := connPool.getNextConnection(userMode) - if err != nil { - return newErrorFuture(err) - } - - return conn.Do(req) -} - // NewStream creates new Stream object for connection selected // by userMode from connPool. // @@ -682,6 +664,86 @@ func (connPool *ConnectionPool) NewPrepared(expr string, userMode Mode) (*tarant return conn.NewPrepared(expr) } +// NewWatcher creates a new Watcher object for the connection pool. +// +// You need to require WatchersFeature to use watchers, see examples for the +// function. +// +// The behavior is same as if Connection.NewWatcher() called for each +// connection with a suitable role. +// +// Keep in mind that garbage collection of a watcher handle doesn’t lead to the +// watcher’s destruction. In this case, the watcher remains registered. You +// need to call Unregister() directly. +// +// Unregister() guarantees that there will be no the watcher's callback calls +// after it, but Unregister() call from the callback leads to a deadlock. +// +// See: +// https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_events/#box-watchers +// +// Since 1.10.0 +func (pool *ConnectionPool) NewWatcher(key string, + callback tarantool.WatchCallback, mode Mode) (tarantool.Watcher, error) { + watchersRequired := false + for _, feature := range pool.connOpts.RequiredProtocolInfo.Features { + if tarantool.WatchersFeature == feature { + watchersRequired = true + break + } + } + if !watchersRequired { + return nil, errors.New("the feature WatchersFeature must be " + + "required by connection options to create a watcher") + } + + watcher := &poolWatcher{ + container: &pool.watcherContainer, + mode: mode, + key: key, + callback: callback, + watchers: make(map[string]tarantool.Watcher), + unregistered: false, + } + + watcher.container.add(watcher) + + rr := pool.anyPool + if mode == RW { + rr = pool.rwPool + } else if mode == RO { + rr = pool.roPool + } + + conns := rr.GetConnections() + for _, conn := range conns { + if err := watcher.watch(conn); err != nil { + conn.Close() + } + } + + return watcher, nil +} + +// Do sends the request and returns a future. +// For requests that belong to the only one connection (e.g. Unprepare or ExecutePrepared) +// the argument of type Mode is unused. +func (connPool *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarantool.Future { + if connectedReq, ok := req.(tarantool.ConnectedRequest); ok { + conn, _ := connPool.getConnectionFromPool(connectedReq.Conn().Addr()) + if conn == nil { + return newErrorFuture(fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool")) + } + return connectedReq.Conn().Do(req) + } + conn, err := connPool.getNextConnection(userMode) + if err != nil { + return newErrorFuture(err) + } + + return conn.Do(req) +} + // // private // @@ -733,26 +795,63 @@ func (connPool *ConnectionPool) getConnectionFromPool(addr string) (*tarantool.C return connPool.anyPool.GetConnByAddr(addr), UnknownRole } -func (connPool *ConnectionPool) deleteConnection(addr string) { - if conn := connPool.anyPool.DeleteConnByAddr(addr); conn != nil { - if conn := connPool.rwPool.DeleteConnByAddr(addr); conn != nil { - return +func (pool *ConnectionPool) deleteConnection(addr string) { + if conn := pool.anyPool.DeleteConnByAddr(addr); conn != nil { + if conn := pool.rwPool.DeleteConnByAddr(addr); conn == nil { + pool.roPool.DeleteConnByAddr(addr) + } + // The internal connection deinitialization. + pool.watcherContainer.mutex.RLock() + defer pool.watcherContainer.mutex.RUnlock() + + pool.watcherContainer.foreach(func(watcher *poolWatcher) error { + watcher.unwatch(conn) + return nil + }) + } +} + +func (pool *ConnectionPool) addConnection(addr string, + conn *tarantool.Connection, role Role) error { + // The internal connection initialization. + pool.watcherContainer.mutex.RLock() + defer pool.watcherContainer.mutex.RUnlock() + + watched := []*poolWatcher{} + err := pool.watcherContainer.foreach(func(watcher *poolWatcher) error { + watch := false + if watcher.mode == RW { + watch = role == MasterRole + } else if watcher.mode == RO { + watch = role == ReplicaRole + } else { + watch = true + } + if watch { + if err := watcher.watch(conn); err != nil { + return err + } + watched = append(watched, watcher) + } + return nil + }) + if err != nil { + for _, watcher := range watched { + watcher.unwatch(conn) } - connPool.roPool.DeleteConnByAddr(addr) + log.Printf("tarantool: failed initialize watchers for %s: %s", addr, err) + return err } -} - -func (connPool *ConnectionPool) addConnection(addr string, - conn *tarantool.Connection, role Role) { - connPool.anyPool.AddConn(addr, conn) + pool.anyPool.AddConn(addr, conn) switch role { case MasterRole: - connPool.rwPool.AddConn(addr, conn) + pool.rwPool.AddConn(addr, conn) case ReplicaRole: - connPool.roPool.AddConn(addr, conn) + pool.roPool.AddConn(addr, conn) } + return nil } func (connPool *ConnectionPool) handlerDiscovered(conn *tarantool.Connection, @@ -811,7 +910,10 @@ func (connPool *ConnectionPool) fillPools() ([]connState, bool) { } if connPool.handlerDiscovered(conn, role) { - connPool.addConnection(addr, conn, role) + if connPool.addConnection(addr, conn, role) != nil { + conn.Close() + connPool.handlerDeactivated(conn, role) + } if conn.ConnectedNow() { states[i].conn = conn @@ -864,7 +966,15 @@ func (pool *ConnectionPool) updateConnection(s connState) connState { return s } - pool.addConnection(s.addr, s.conn, role) + if pool.addConnection(s.addr, s.conn, role) != nil { + pool.poolsMutex.Unlock() + + s.conn.Close() + pool.handlerDeactivated(s.conn, role) + s.conn = nil + s.role = UnknownRole + return s + } s.role = role } } @@ -911,7 +1021,12 @@ func (pool *ConnectionPool) tryConnect(s connState) connState { return s } - pool.addConnection(s.addr, conn, role) + if pool.addConnection(s.addr, conn, role) != nil { + pool.poolsMutex.Unlock() + conn.Close() + pool.handlerDeactivated(conn, role) + return s + } s.conn = conn s.role = role } diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 60c9b4f91..8dc8cc9da 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -2048,6 +2048,327 @@ func TestStream_TxnIsolationLevel(t *testing.T) { } } +func TestConnectionPool_NewWatcher_noWatchersFeature(t *testing.T) { + const key = "TestConnectionPool_NewWatcher_noWatchersFeature" + + roles := []bool{true, false, false, true, true} + + opts := connOpts.Clone() + opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{} + err := test_helpers.SetClusterRO(servers, opts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + pool, err := connection_pool.Connect(servers, opts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, pool, "conn is nil after Connect") + defer pool.Close() + + watcher, err := pool.NewWatcher(key, + func(event tarantool.WatchEvent) {}, connection_pool.ANY) + require.Nilf(t, watcher, "watcher must not be created") + require.NotNilf(t, err, "an error is expected") + expected := "the feature WatchersFeature must be required by connection " + + "options to create a watcher" + require.Equal(t, expected, err.Error()) +} + +func TestConnectionPool_NewWatcher_modes(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const key = "TestConnectionPool_NewWatcher_modes" + + roles := []bool{true, false, false, true, true} + + opts := connOpts.Clone() + opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{ + tarantool.WatchersFeature, + } + err := test_helpers.SetClusterRO(servers, opts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + pool, err := connection_pool.Connect(servers, opts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, pool, "conn is nil after Connect") + defer pool.Close() + + modes := []connection_pool.Mode{ + connection_pool.ANY, + connection_pool.RW, + connection_pool.RO, + connection_pool.PreferRW, + connection_pool.PreferRO, + } + for _, mode := range modes { + t.Run(fmt.Sprintf("%d", mode), func(t *testing.T) { + expectedServers := []string{} + for i, server := range servers { + if roles[i] && mode == connection_pool.RW { + continue + } else if !roles[i] && mode == connection_pool.RO { + continue + } + expectedServers = append(expectedServers, server) + } + + events := make(chan tarantool.WatchEvent, 1024) + defer close(events) + + watcher, err := pool.NewWatcher(key, func(event tarantool.WatchEvent) { + require.Equal(t, key, event.Key) + require.Equal(t, nil, event.Value) + events <- event + }, mode) + require.Nilf(t, err, "failed to register a watcher") + defer watcher.Unregister() + + testMap := make(map[string]int) + + for i := 0; i < len(expectedServers); i++ { + select { + case event := <-events: + require.NotNil(t, event.Conn) + addr := event.Conn.Addr() + if val, ok := testMap[addr]; ok { + testMap[addr] = val + 1 + } else { + testMap[addr] = 1 + } + case <-time.After(time.Second): + t.Errorf("Failed to get a watch event.") + break + } + } + + for _, server := range expectedServers { + if val, ok := testMap[server]; !ok { + t.Errorf("Server not found: %s", server) + } else if val != 1 { + t.Errorf("Too many events %d for server %s", val, server) + } + } + }) + } +} + +func TestConnectionPool_NewWatcher_update(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const key = "TestConnectionPool_NewWatcher_update" + const mode = connection_pool.RW + const initCnt = 2 + roles := []bool{true, false, false, true, true} + + opts := connOpts.Clone() + opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{ + tarantool.WatchersFeature, + } + err := test_helpers.SetClusterRO(servers, opts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + pool, err := connection_pool.Connect(servers, opts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, pool, "conn is nil after Connect") + defer pool.Close() + + events := make(chan tarantool.WatchEvent, 1024) + defer close(events) + + watcher, err := pool.NewWatcher(key, func(event tarantool.WatchEvent) { + require.Equal(t, key, event.Key) + require.Equal(t, nil, event.Value) + events <- event + }, mode) + require.Nilf(t, err, "failed to create a watcher") + defer watcher.Unregister() + + // Wait for all initial events. + testMap := make(map[string]int) + for i := 0; i < initCnt; i++ { + select { + case event := <-events: + require.NotNil(t, event.Conn) + addr := event.Conn.Addr() + if val, ok := testMap[addr]; ok { + testMap[addr] = val + 1 + } else { + testMap[addr] = 1 + } + case <-time.After(time.Second): + t.Errorf("Failed to get a watch init event.") + break + } + } + + // Just invert roles for simplify the test. + for i, role := range roles { + roles[i] = !role + } + err = test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + // Wait for all updated events. + for i := 0; i < len(servers)-initCnt; i++ { + select { + case event := <-events: + require.NotNil(t, event.Conn) + addr := event.Conn.Addr() + if val, ok := testMap[addr]; ok { + testMap[addr] = val + 1 + } else { + testMap[addr] = 1 + } + case <-time.After(time.Second): + t.Errorf("Failed to get a watch update event.") + break + } + } + + // Check that all an event happen for an each connection. + for _, server := range servers { + if val, ok := testMap[server]; !ok { + t.Errorf("Server not found: %s", server) + } else { + require.Equal(t, val, 1, fmt.Sprintf("for server %s", server)) + } + } +} + +func TestWatcher_Unregister(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const key = "TestWatcher_Unregister" + const mode = connection_pool.RW + const expectedCnt = 2 + roles := []bool{true, false, false, true, true} + + opts := connOpts.Clone() + opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{ + tarantool.WatchersFeature, + } + err := test_helpers.SetClusterRO(servers, opts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + pool, err := connection_pool.Connect(servers, opts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, pool, "conn is nil after Connect") + defer pool.Close() + + events := make(chan tarantool.WatchEvent, 1024) + defer close(events) + + watcher, err := pool.NewWatcher(key, func(event tarantool.WatchEvent) { + require.Equal(t, key, event.Key) + require.Equal(t, nil, event.Value) + events <- event + }, mode) + require.Nilf(t, err, "failed to create a watcher") + + for i := 0; i < expectedCnt; i++ { + select { + case <-events: + case <-time.After(time.Second): + t.Fatalf("Failed to skip initial events.") + } + } + watcher.Unregister() + + broadcast := tarantool.NewBroadcastRequest(key).Value("foo") + for i := 0; i < expectedCnt; i++ { + _, err := pool.Do(broadcast, mode).Get() + require.Nilf(t, err, "failed to send a broadcast request") + } + + select { + case event := <-events: + t.Fatalf("Get unexpected event: %v", event) + case <-time.After(time.Second): + } + + // Reset to the initial state. + broadcast = tarantool.NewBroadcastRequest(key) + for i := 0; i < expectedCnt; i++ { + _, err := pool.Do(broadcast, mode).Get() + require.Nilf(t, err, "failed to send a broadcast request") + } +} + +func TestConnectionPool_NewWatcher_concurrent(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const testConcurrency = 1000 + const key = "TestConnectionPool_NewWatcher_concurrent" + + roles := []bool{true, false, false, true, true} + + opts := connOpts.Clone() + opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{ + tarantool.WatchersFeature, + } + err := test_helpers.SetClusterRO(servers, opts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + pool, err := connection_pool.Connect(servers, opts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, pool, "conn is nil after Connect") + defer pool.Close() + + var wg sync.WaitGroup + wg.Add(testConcurrency) + + mode := connection_pool.ANY + callback := func(event tarantool.WatchEvent) {} + for i := 0; i < testConcurrency; i++ { + go func(i int) { + defer wg.Done() + + watcher, err := pool.NewWatcher(key, callback, mode) + if err != nil { + t.Errorf("Failed to create a watcher: %s", err) + } else { + watcher.Unregister() + } + }(i) + } + wg.Wait() +} + +func TestWatcher_Unregister_concurrent(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const testConcurrency = 1000 + const key = "TestWatcher_Unregister_concurrent" + + roles := []bool{true, false, false, true, true} + + opts := connOpts.Clone() + opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{ + tarantool.WatchersFeature, + } + err := test_helpers.SetClusterRO(servers, opts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + pool, err := connection_pool.Connect(servers, opts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, pool, "conn is nil after Connect") + defer pool.Close() + + mode := connection_pool.ANY + watcher, err := pool.NewWatcher(key, func(event tarantool.WatchEvent) { + }, mode) + require.Nilf(t, err, "failed to create a watcher") + + var wg sync.WaitGroup + wg.Add(testConcurrency) + + for i := 0; i < testConcurrency; i++ { + go func() { + defer wg.Done() + watcher.Unregister() + }() + } + wg.Wait() +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/connection_pool/connector.go b/connection_pool/connector.go index e52109d92..c108aba0b 100644 --- a/connection_pool/connector.go +++ b/connection_pool/connector.go @@ -299,6 +299,14 @@ func (c *ConnectorAdapter) NewStream() (*tarantool.Stream, error) { return c.pool.NewStream(c.mode) } +// NewWatcher creates new Watcher object for the pool +// +// Since 1.10.0 +func (c *ConnectorAdapter) NewWatcher(key string, + callback tarantool.WatchCallback) (tarantool.Watcher, error) { + return c.pool.NewWatcher(key, callback, c.mode) +} + // Do performs a request asynchronously on the connection. func (c *ConnectorAdapter) Do(req tarantool.Request) *tarantool.Future { return c.pool.Do(req, c.mode) diff --git a/connection_pool/connector_test.go b/connection_pool/connector_test.go index fa7cf06ba..f53a05b22 100644 --- a/connection_pool/connector_test.go +++ b/connection_pool/connector_test.go @@ -1139,6 +1139,45 @@ func TestConnectorNewStream(t *testing.T) { require.Equalf(t, testMode, m.mode, "unexpected proxy mode") } +type watcherMock struct{} + +func (w *watcherMock) Unregister() {} + +const reqWatchKey = "foo" + +var reqWatcher tarantool.Watcher = &watcherMock{} + +type newWatcherMock struct { + Pooler + key string + callback tarantool.WatchCallback + called int + mode Mode +} + +func (m *newWatcherMock) NewWatcher(key string, + callback tarantool.WatchCallback, mode Mode) (tarantool.Watcher, error) { + m.called++ + m.key = key + m.callback = callback + m.mode = mode + return reqWatcher, reqErr +} + +func TestConnectorNewWatcher(t *testing.T) { + m := &newWatcherMock{} + c := NewConnectorAdapter(m, testMode) + + w, err := c.NewWatcher(reqWatchKey, func(event tarantool.WatchEvent) {}) + + require.Equalf(t, reqWatcher, w, "unexpected watcher") + require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, 1, m.called, "should be called only once") + require.Equalf(t, reqWatchKey, m.key, "unexpected key") + require.NotNilf(t, m.callback, "callback must be set") + require.Equalf(t, testMode, m.mode, "unexpected proxy mode") +} + var reqRequest tarantool.Request = tarantool.NewPingRequest() type doMock struct { diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go index 02715a2bc..cf59455ca 100644 --- a/connection_pool/example_test.go +++ b/connection_pool/example_test.go @@ -575,6 +575,59 @@ func ExampleConnectionPool_NewPrepared() { } } +func ExampleConnectionPool_NewWatcher() { + const key = "foo" + const value = "bar" + + opts := connOpts.Clone() + opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{ + tarantool.WatchersFeature, + } + + pool, err := examplePool(testRoles, connOpts) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + callback := func(event tarantool.WatchEvent) { + fmt.Printf("event connection: %s\n", event.Conn.Addr()) + fmt.Printf("event key: %s\n", event.Key) + fmt.Printf("event value: %v\n", event.Value) + } + mode := connection_pool.ANY + watcher, err := pool.NewWatcher(key, callback, mode) + if err != nil { + fmt.Printf("Unexpected error: %s\n", err) + return + } + defer watcher.Unregister() + + pool.Do(tarantool.NewBroadcastRequest(key).Value(value), mode).Get() + time.Sleep(time.Second) +} + +func ExampleConnectionPool_NewWatcher_noWatchersFeature() { + const key = "foo" + + opts := connOpts.Clone() + opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{} + + pool, err := examplePool(testRoles, connOpts) + if err != nil { + fmt.Println(err) + } + defer pool.Close() + + callback := func(event tarantool.WatchEvent) {} + watcher, err := pool.NewWatcher(key, callback, connection_pool.ANY) + fmt.Println(watcher) + fmt.Println(err) + // Output: + // + // the feature WatchersFeature must be required by connection options to create a watcher +} + func getTestTxnOpts() tarantool.Opts { txnOpts := connOpts.Clone() diff --git a/connection_pool/pooler.go b/connection_pool/pooler.go index a9dbe09f9..856f5d5be 100644 --- a/connection_pool/pooler.go +++ b/connection_pool/pooler.go @@ -84,6 +84,8 @@ type Pooler interface { NewPrepared(expr string, mode Mode) (*tarantool.Prepared, error) NewStream(mode Mode) (*tarantool.Stream, error) + NewWatcher(key string, callback tarantool.WatchCallback, + mode Mode) (tarantool.Watcher, error) Do(req tarantool.Request, mode Mode) (fut *tarantool.Future) } diff --git a/connection_pool/round_robin.go b/connection_pool/round_robin.go index b83d877d9..a7fb73e18 100644 --- a/connection_pool/round_robin.go +++ b/connection_pool/round_robin.go @@ -14,6 +14,15 @@ type RoundRobinStrategy struct { current uint } +func NewEmptyRoundRobin(size int) *RoundRobinStrategy { + return &RoundRobinStrategy{ + conns: make([]*tarantool.Connection, 0, size), + indexByAddr: make(map[string]uint), + size: 0, + current: 0, + } +} + func (r *RoundRobinStrategy) GetConnByAddr(addr string) *tarantool.Connection { r.mutex.RLock() defer r.mutex.RUnlock() @@ -71,13 +80,14 @@ func (r *RoundRobinStrategy) GetNextConnection() *tarantool.Connection { return r.conns[r.nextIndex()] } -func NewEmptyRoundRobin(size int) *RoundRobinStrategy { - return &RoundRobinStrategy{ - conns: make([]*tarantool.Connection, 0, size), - indexByAddr: make(map[string]uint), - size: 0, - current: 0, - } +func (r *RoundRobinStrategy) GetConnections() []*tarantool.Connection { + r.mutex.RLock() + defer r.mutex.RUnlock() + + ret := make([]*tarantool.Connection, len(r.conns)) + copy(ret, r.conns) + + return ret } func (r *RoundRobinStrategy) AddConn(addr string, conn *tarantool.Connection) { diff --git a/connection_pool/round_robin_test.go b/connection_pool/round_robin_test.go index 6b54ecfd8..03038eada 100644 --- a/connection_pool/round_robin_test.go +++ b/connection_pool/round_robin_test.go @@ -69,3 +69,22 @@ func TestRoundRobinGetNextConnection(t *testing.T) { } } } + +func TestRoundRobinStrategy_GetConnections(t *testing.T) { + rr := NewEmptyRoundRobin(10) + + addrs := []string{validAddr1, validAddr2} + conns := []*tarantool.Connection{&tarantool.Connection{}, &tarantool.Connection{}} + + for i, addr := range addrs { + rr.AddConn(addr, conns[i]) + } + + rr.GetConnections()[1] = conns[0] // GetConnections() returns a copy. + rrConns := rr.GetConnections() + for i, expected := range conns { + if expected != rrConns[i] { + t.Errorf("Unexpected connection on %d call", i) + } + } +} diff --git a/connection_pool/watcher.go b/connection_pool/watcher.go new file mode 100644 index 000000000..6a1fde8f4 --- /dev/null +++ b/connection_pool/watcher.go @@ -0,0 +1,133 @@ +package connection_pool + +import ( + "sync" + + "github.com/tarantool/go-tarantool" +) + +// watcherContainer is a very simple implementation of a thread-safe container +// for watchers. It is not expected that there will be too many watchers and +// they will registered/unregistered too frequently. +// +// Otherwise, the implementation will need to be optimized. +type watcherContainer struct { + head *poolWatcher + mutex sync.RWMutex +} + +// add adds a watcher to the container. +func (c *watcherContainer) add(watcher *poolWatcher) { + c.mutex.Lock() + defer c.mutex.Unlock() + + watcher.next = c.head + c.head = watcher +} + +// remove removes a watcher from the container. +func (c *watcherContainer) remove(watcher *poolWatcher) { + c.mutex.Lock() + defer c.mutex.Unlock() + + if watcher == c.head { + c.head = watcher.next + } else { + cur := c.head + for cur.next != nil { + if cur.next == watcher { + cur.next = watcher.next + break + } + cur = cur.next + } + } +} + +// foreach iterates over the container to the end or until the call returns +// false. +func (c *watcherContainer) foreach(call func(watcher *poolWatcher) error) error { + cur := c.head + for cur != nil { + if err := call(cur); err != nil { + return err + } + cur = cur.next + } + return nil +} + +// poolWatcher is an internal implementation of the tarantool.Watcher interface. +type poolWatcher struct { + // The watcher container data. We can split the structure into two parts + // in the future: a watcher data and a watcher container data, but it looks + // simple at now. + + // next item in the watcher container. + next *poolWatcher + // container is the container for all active poolWatcher objects. + container *watcherContainer + + // The watcher data. + // mode of the watcher. + mode Mode + key string + callback tarantool.WatchCallback + // watchers is a map connection -> connection watcher. + watchers map[string]tarantool.Watcher + // unregistered is true if the watcher already unregistered. + unregistered bool + // mutex for the pool watcher. + mutex sync.Mutex +} + +// Unregister unregisters the pool watcher. +func (w *poolWatcher) Unregister() { + w.mutex.Lock() + defer w.mutex.Unlock() + + if !w.unregistered { + w.container.remove(w) + w.unregistered = true + for _, watcher := range w.watchers { + watcher.Unregister() + } + } +} + +// watch adds a watcher for the connection. +func (w *poolWatcher) watch(conn *tarantool.Connection) error { + addr := conn.Addr() + + w.mutex.Lock() + defer w.mutex.Unlock() + + if !w.unregistered { + if _, ok := w.watchers[addr]; ok { + return nil + } + + if watcher, err := conn.NewWatcher(w.key, w.callback); err == nil { + w.watchers[addr] = watcher + return nil + } else { + return err + } + } + return nil +} + +// unwatch removes a watcher for the connection. +func (w *poolWatcher) unwatch(conn *tarantool.Connection) { + addr := conn.Addr() + + w.mutex.Lock() + defer w.mutex.Unlock() + + if !w.unregistered { + if watcher, ok := w.watchers[addr]; ok { + watcher.Unregister() + delete(w.watchers, addr) + } + } +} diff --git a/connector.go b/connector.go index d6c44c8dd..d93c69ec8 100644 --- a/connector.go +++ b/connector.go @@ -46,6 +46,7 @@ type Connector interface { NewPrepared(expr string) (*Prepared, error) NewStream() (*Stream, error) + NewWatcher(key string, callback WatchCallback) (Watcher, error) Do(req Request) (fut *Future) } diff --git a/const.go b/const.go index 0dd8d708d..acd6a4861 100644 --- a/const.go +++ b/const.go @@ -19,6 +19,8 @@ const ( PingRequestCode = 64 SubscribeRequestCode = 66 IdRequestCode = 73 + WatchRequestCode = 74 + UnwatchRequestCode = 75 KeyCode = 0x00 KeySync = 0x01 @@ -46,6 +48,8 @@ const ( KeyVersion = 0x54 KeyFeatures = 0x55 KeyTimeout = 0x56 + KeyEvent = 0x57 + KeyEventData = 0x58 KeyTxnIsolation = 0x59 KeyFieldName = 0x00 @@ -74,6 +78,7 @@ const ( RLimitWait = 2 OkCode = uint32(0) + EventCode = uint32(0x4c) PushCode = uint32(0x80) ErrorCodeBit = 0x8000 PacketLengthBytes = 5 diff --git a/example_test.go b/example_test.go index 54202eb46..538fa93b1 100644 --- a/example_test.go +++ b/example_test.go @@ -329,7 +329,7 @@ func ExampleProtocolVersion() { fmt.Println("Connector client protocol features:", clientProtocolInfo.Features) // Output: // Connector client protocol version: 4 - // Connector client protocol features: [StreamsFeature TransactionsFeature ErrorExtensionFeature] + // Connector client protocol features: [StreamsFeature TransactionsFeature ErrorExtensionFeature WatchersFeature] } func getTestTxnOpts() tarantool.Opts { @@ -1045,6 +1045,51 @@ func ExampleConnection_NewPrepared() { } } +func ExampleConnection_NewWatcher() { + const key = "foo" + const value = "bar" + + // Tarantool watchers since version 2.10 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil || isLess { + return + } + + server := "127.0.0.1:3013" + opts := tarantool.Opts{ + Timeout: 500 * time.Millisecond, + Reconnect: 1 * time.Second, + MaxReconnects: 3, + User: "test", + Pass: "test", + // You need to require the feature to create a watcher. + RequiredProtocolInfo: tarantool.ProtocolInfo{ + Features: []tarantool.ProtocolFeature{tarantool.WatchersFeature}, + }, + } + conn, err := tarantool.Connect(server, opts) + if err != nil { + fmt.Printf("Failed to connect: %s\n", err) + return + } + defer conn.Close() + + callback := func(event tarantool.WatchEvent) { + fmt.Printf("event connection: %s\n", event.Conn.Addr()) + fmt.Printf("event key: %s\n", event.Key) + fmt.Printf("event value: %v\n", event.Value) + } + watcher, err := conn.NewWatcher(key, callback) + if err != nil { + fmt.Printf("Failed to connect: %s\n", err) + return + } + defer watcher.Unregister() + + conn.Do(tarantool.NewBroadcastRequest(key).Value(value)).Get() + time.Sleep(time.Second) +} + // To pass contexts to request objects, use the Context() method. // Pay attention that when using context with request objects, // the timeout option for Connection will not affect the lifetime diff --git a/multi/multi.go b/multi/multi.go index 390186f27..ea950cfdf 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -507,6 +507,16 @@ func (connMulti *ConnectionMulti) NewStream() (*tarantool.Stream, error) { return connMulti.getCurrentConnection().NewStream() } +// NewWatcher does not supported by the ConnectionMulti. The ConnectionMulti is +// deprecated: use ConnectionPool instead. +// +// Since 1.10.0 +func (connMulti *ConnectionMulti) NewWatcher(key string, + callback tarantool.WatchCallback) (tarantool.Watcher, error) { + return nil, errors.New("ConnectionMulti is deprecated " + + "use ConnectionPool") +} + // Do sends the request and returns a future. func (connMulti *ConnectionMulti) Do(req tarantool.Request) *tarantool.Future { if connectedReq, ok := req.(tarantool.ConnectedRequest); ok { diff --git a/multi/multi_test.go b/multi/multi_test.go index 2d43bb179..ef07d629b 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -548,6 +548,30 @@ func TestStream_Rollback(t *testing.T) { } } +func TestConnectionMulti_NewWatcher(t *testing.T) { + test_helpers.SkipIfStreamsUnsupported(t) + + multiConn, err := Connect([]string{server1, server2}, connOpts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if multiConn == nil { + t.Fatalf("conn is nil after Connect") + } + defer multiConn.Close() + + watcher, err := multiConn.NewWatcher("foo", func(event tarantool.WatchEvent) {}) + if watcher != nil { + t.Errorf("Unexpected watcher") + } + if err == nil { + t.Fatalf("Unexpected success") + } + if err.Error() != "ConnectionMulti is deprecated use ConnectionPool" { + t.Fatalf("Unexpected error: %s", err) + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/protocol.go b/protocol.go index fa890fdd4..ae6142b4d 100644 --- a/protocol.go +++ b/protocol.go @@ -76,13 +76,16 @@ var clientProtocolInfo ProtocolInfo = ProtocolInfo{ // 1.10.0. Version: ProtocolVersion(4), // Streams and transactions were introduced in protocol version 1 - // (Tarantool 2.10.0), in connector since 1.7.0. Error extension - // type was introduced in protocol version 2 (Tarantool 2.10.0), - // in connector since 1.10.0. + // (Tarantool 2.10.0), in connector since 1.7.0. + // Error extension type was introduced in protocol + // version 2 (Tarantool 2.10.0), in connector since 1.10.0. + // Watchers were introduced in protocol version 3 (Tarantool 2.10.0), in + // connector since 1.10.0. Features: []ProtocolFeature{ StreamsFeature, TransactionsFeature, ErrorExtensionFeature, + WatchersFeature, }, } diff --git a/request.go b/request.go index cfa40e522..66eb4be41 100644 --- a/request.go +++ b/request.go @@ -538,6 +538,8 @@ type Request interface { Body(resolver SchemaResolver, enc *encoder) error // Ctx returns a context of the request. Ctx() context.Context + // Async returns true if the request does not expect response. + Async() bool } // ConnectedRequest is an interface that provides the info about a Connection @@ -550,6 +552,7 @@ type ConnectedRequest interface { type baseRequest struct { requestCode int32 + async bool ctx context.Context } @@ -558,6 +561,11 @@ func (req *baseRequest) Code() int32 { return req.requestCode } +// Async returns true if the request does not require a response. +func (req *baseRequest) Async() bool { + return req.async +} + // Ctx returns a context of the request. func (req *baseRequest) Ctx() context.Context { return req.ctx diff --git a/request_test.go b/request_test.go index 89d1d8884..a078f6514 100644 --- a/request_test.go +++ b/request_test.go @@ -19,6 +19,7 @@ const invalidIndex = 2 const validSpace = 1 // Any valid value != default. const validIndex = 3 // Any valid value != default. const validExpr = "any string" // We don't check the value here. +const validKey = "foo" // Any string. const defaultSpace = 0 // And valid too. const defaultIndex = 0 // And valid too. @@ -183,6 +184,7 @@ func TestRequestsCodes(t *testing.T) { {req: NewBeginRequest(), code: BeginRequestCode}, {req: NewCommitRequest(), code: CommitRequestCode}, {req: NewRollbackRequest(), code: RollbackRequestCode}, + {req: NewBroadcastRequest(validKey), code: CallRequestCode}, } for _, test := range tests { @@ -192,6 +194,38 @@ func TestRequestsCodes(t *testing.T) { } } +func TestRequestsAsync(t *testing.T) { + tests := []struct { + req Request + async bool + }{ + {req: NewSelectRequest(validSpace), async: false}, + {req: NewUpdateRequest(validSpace), async: false}, + {req: NewUpsertRequest(validSpace), async: false}, + {req: NewInsertRequest(validSpace), async: false}, + {req: NewReplaceRequest(validSpace), async: false}, + {req: NewDeleteRequest(validSpace), async: false}, + {req: NewCall16Request(validExpr), async: false}, + {req: NewCall17Request(validExpr), async: false}, + {req: NewEvalRequest(validExpr), async: false}, + {req: NewExecuteRequest(validExpr), async: false}, + {req: NewPingRequest(), async: false}, + {req: NewPrepareRequest(validExpr), async: false}, + {req: NewUnprepareRequest(validStmt), async: false}, + {req: NewExecutePreparedRequest(validStmt), async: false}, + {req: NewBeginRequest(), async: false}, + {req: NewCommitRequest(), async: false}, + {req: NewRollbackRequest(), async: false}, + {req: NewBroadcastRequest(validKey), async: false}, + } + + for _, test := range tests { + if async := test.req.Async(); async != test.async { + t.Errorf("An invalid async %t, expected %t", async, test.async) + } + } +} + func TestPingRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer @@ -649,3 +683,34 @@ func TestRollbackRequestDefaultValues(t *testing.T) { req := NewRollbackRequest() assertBodyEqual(t, refBuf.Bytes(), req) } + +func TestBroadcastRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := NewEncoder(&refBuf) + expectedArgs := []interface{}{validKey} + err := RefImplCallBody(refEnc, "box.broadcast", expectedArgs) + if err != nil { + t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) + return + } + + req := NewBroadcastRequest(validKey) + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestBroadcastRequestSetters(t *testing.T) { + value := []interface{}{uint(34), int(12)} + var refBuf bytes.Buffer + + refEnc := NewEncoder(&refBuf) + expectedArgs := []interface{}{validKey, value} + err := RefImplCallBody(refEnc, "box.broadcast", expectedArgs) + if err != nil { + t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) + return + } + + req := NewBroadcastRequest(validKey).Value(value) + assertBodyEqual(t, refBuf.Bytes(), req) +} diff --git a/tarantool_test.go b/tarantool_test.go index 202c10b17..de7af9e91 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2873,6 +2873,7 @@ func TestConnectionProtocolInfoSupported(t *testing.T) { StreamsFeature, TransactionsFeature, ErrorExtensionFeature, + WatchersFeature, }, }) @@ -3006,6 +3007,7 @@ func TestConnectionProtocolInfoUnsupported(t *testing.T) { StreamsFeature, TransactionsFeature, ErrorExtensionFeature, + WatchersFeature, }, }) @@ -3253,6 +3255,385 @@ func TestErrorExtendedInfoFields(t *testing.T) { test_helpers.CheckEqualBoxErrors(t, expected, *ttErr.ExtendedInfo) } +func TestConnection_NewWatcher(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const key = "TestConnection_NewWatcher" + connOpts := opts.Clone() + connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ + WatchersFeature, + } + conn := test_helpers.ConnectWithValidation(t, server, connOpts) + defer conn.Close() + + events := make(chan WatchEvent) + defer close(events) + + watcher, err := conn.NewWatcher(key, func(event WatchEvent) { + events <- event + }) + if err != nil { + t.Fatalf("Failed to create a watch: %s", err) + } + defer watcher.Unregister() + + select { + case event := <-events: + if event.Conn != conn { + t.Errorf("Unexpected event connection: %v", event.Conn) + } + if event.Key != key { + t.Errorf("Unexpected event key: %s", event.Key) + } + if event.Value != nil { + t.Errorf("Unexpected event value: %v", event.Value) + } + case <-time.After(time.Second): + t.Fatalf("Failed to get watch event.") + } +} + +func TestConnection_NewWatcher_noWatchersFeature(t *testing.T) { + const key = "TestConnection_NewWatcher_noWatchersFeature" + connOpts := opts.Clone() + connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{} + conn := test_helpers.ConnectWithValidation(t, server, connOpts) + defer conn.Close() + + watcher, err := conn.NewWatcher(key, func(event WatchEvent) {}) + require.Nilf(t, watcher, "watcher must not be created") + require.NotNilf(t, err, "an error is expected") + expected := "the feature WatchersFeature must be required by connection " + + "options to create a watcher" + require.Equal(t, expected, err.Error()) +} + +func TestConnection_NewWatcher_reconnect(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const key = "TestConnection_NewWatcher_reconnect" + const server = "127.0.0.1:3014" + + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: server, + WorkDir: "work_dir", + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(inst) + if err != nil { + t.Fatalf("Unable to start Tarantool: %s", err) + } + + reconnectOpts := opts + reconnectOpts.Reconnect = 100 * time.Millisecond + reconnectOpts.MaxReconnects = 10 + reconnectOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ + WatchersFeature, + } + conn := test_helpers.ConnectWithValidation(t, server, reconnectOpts) + defer conn.Close() + + events := make(chan WatchEvent) + defer close(events) + watcher, err := conn.NewWatcher(key, func(event WatchEvent) { + events <- event + }) + if err != nil { + t.Fatalf("Failed to create a watch: %s", err) + } + defer watcher.Unregister() + + <-events + + test_helpers.StopTarantool(inst) + if err := test_helpers.RestartTarantool(&inst); err != nil { + t.Fatalf("Unable to restart Tarantool: %s", err) + } + + maxTime := reconnectOpts.Reconnect * time.Duration(reconnectOpts.MaxReconnects) + select { + case <-events: + case <-time.After(maxTime): + t.Fatalf("Failed to get watch event.") + } +} + +func TestBroadcastRequest(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const key = "TestBroadcastRequest" + const value = "bar" + + connOpts := opts.Clone() + connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ + WatchersFeature, + } + conn := test_helpers.ConnectWithValidation(t, server, connOpts) + defer conn.Close() + + resp, err := conn.Do(NewBroadcastRequest(key).Value(value)).Get() + if err != nil { + t.Fatalf("Got broadcast error: %s", err) + } + if resp.Code != OkCode { + t.Errorf("Got unexpected broadcast response code: %d", resp.Code) + } + if !reflect.DeepEqual(resp.Data, []interface{}{}) { + t.Errorf("Got unexpected broadcast response data: %v", resp.Data) + } + + events := make(chan WatchEvent) + defer close(events) + + watcher, err := conn.NewWatcher(key, func(event WatchEvent) { + events <- event + }) + if err != nil { + t.Fatalf("Failed to create a watch: %s", err) + } + defer watcher.Unregister() + + select { + case event := <-events: + if event.Conn != conn { + t.Errorf("Unexpected event connection: %v", event.Conn) + } + if event.Key != key { + t.Errorf("Unexpected event key: %s", event.Key) + } + if event.Value != value { + t.Errorf("Unexpected event value: %v", event.Value) + } + case <-time.After(time.Second): + t.Fatalf("Failed to get watch event.") + } +} + +func TestBroadcastRequest_multi(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const key = "TestBroadcastRequest_multi" + + connOpts := opts.Clone() + connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ + WatchersFeature, + } + conn := test_helpers.ConnectWithValidation(t, server, connOpts) + defer conn.Close() + + events := make(chan WatchEvent) + defer close(events) + + watcher, err := conn.NewWatcher(key, func(event WatchEvent) { + events <- event + }) + if err != nil { + t.Fatalf("Failed to create a watch: %s", err) + } + defer watcher.Unregister() + + <-events // Skip an initial event. + for i := 0; i < 10; i++ { + val := fmt.Sprintf("%d", i) + _, err := conn.Do(NewBroadcastRequest(key).Value(val)).Get() + if err != nil { + t.Fatalf("Failed to send a broadcast request: %s", err) + } + select { + case event := <-events: + if event.Conn != conn { + t.Errorf("Unexpected event connection: %v", event.Conn) + } + if event.Key != key { + t.Errorf("Unexpected event key: %s", event.Key) + } + if event.Value.(string) != val { + t.Errorf("Unexpected event value: %v", event.Value) + } + case <-time.After(time.Second): + t.Fatalf("Failed to get watch event %d", i) + } + } +} + +func TestConnection_NewWatcher_multiOnKey(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const key = "TestConnection_NewWatcher_multiOnKey" + const value = "bar" + + connOpts := opts.Clone() + connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ + WatchersFeature, + } + conn := test_helpers.ConnectWithValidation(t, server, connOpts) + defer conn.Close() + + events := []chan WatchEvent{ + make(chan WatchEvent), + make(chan WatchEvent), + } + for _, ch := range events { + defer close(ch) + } + + for _, ch := range events { + channel := ch + watcher, err := conn.NewWatcher(key, func(event WatchEvent) { + channel <- event + }) + if err != nil { + t.Fatalf("Failed to create a watch: %s", err) + } + defer watcher.Unregister() + } + + for i, ch := range events { + select { + case <-ch: // Skip an initial event. + case <-time.After(2 * time.Second): + t.Fatalf("Failed to skip watch event for %d callback", i) + } + } + + _, err := conn.Do(NewBroadcastRequest(key).Value(value)).Get() + if err != nil { + t.Fatalf("Failed to send a broadcast request: %s", err) + } + + for i, ch := range events { + select { + case event := <-ch: + if event.Conn != conn { + t.Errorf("Unexpected event connection: %v", event.Conn) + } + if event.Key != key { + t.Errorf("Unexpected event key: %s", event.Key) + } + if event.Value.(string) != value { + t.Errorf("Unexpected event value: %v", event.Value) + } + case <-time.After(2 * time.Second): + t.Fatalf("Failed to get watch event from callback %d", i) + } + } +} + +func TestWatcher_Unregister(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const key = "TestWatcher_Unregister" + const value = "bar" + + connOpts := opts.Clone() + connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ + WatchersFeature, + } + conn := test_helpers.ConnectWithValidation(t, server, connOpts) + defer conn.Close() + + events := make(chan WatchEvent) + defer close(events) + watcher, err := conn.NewWatcher(key, func(event WatchEvent) { + events <- event + }) + if err != nil { + t.Fatalf("Failed to create a watch: %s", err) + } + + <-events + watcher.Unregister() + + _, err = conn.Do(NewBroadcastRequest(key).Value(value)).Get() + if err != nil { + t.Fatalf("Got broadcast error: %s", err) + } + + select { + case event := <-events: + t.Fatalf("Get unexpected events: %v", event) + case <-time.After(time.Second): + } +} + +func TestConnection_NewWatcher_concurrent(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const testConcurrency = 1000 + const key = "TestConnection_NewWatcher_concurrent" + connOpts := opts.Clone() + connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ + WatchersFeature, + } + conn := test_helpers.ConnectWithValidation(t, server, connOpts) + defer conn.Close() + + var wg sync.WaitGroup + wg.Add(testConcurrency) + + var ret error + for i := 0; i < testConcurrency; i++ { + go func(i int) { + defer wg.Done() + + events := make(chan struct{}) + + watcher, err := conn.NewWatcher(key, func(event WatchEvent) { + close(events) + }) + if err != nil { + ret = err + } else { + select { + case <-events: + case <-time.After(time.Second): + ret = fmt.Errorf("Unable to get an event %d", i) + } + watcher.Unregister() + } + }(i) + } + wg.Wait() + + if ret != nil { + t.Fatalf("An error found: %s", ret) + } +} + +func TestWatcher_Unregister_concurrent(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + const testConcurrency = 1000 + const key = "TestWatcher_Unregister_concurrent" + connOpts := opts.Clone() + connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ + WatchersFeature, + } + conn := test_helpers.ConnectWithValidation(t, server, connOpts) + defer conn.Close() + + watcher, err := conn.NewWatcher(key, func(event WatchEvent) {}) + if err != nil { + t.Fatalf("Failed to create a watch: %s", err) + } + + var wg sync.WaitGroup + wg.Add(testConcurrency) + + for i := 0; i < testConcurrency; i++ { + go func() { + defer wg.Done() + watcher.Unregister() + }() + } + wg.Wait() +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/test_helpers/request_mock.go b/test_helpers/request_mock.go index 93551e34a..19c18545e 100644 --- a/test_helpers/request_mock.go +++ b/test_helpers/request_mock.go @@ -17,6 +17,10 @@ func (sr *StrangerRequest) Code() int32 { return 0 } +func (sr *StrangerRequest) Async() bool { + return false +} + func (sr *StrangerRequest) Body(resolver tarantool.SchemaResolver, enc *encoder) error { return nil } diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 5081de6c2..8cf8e2b4b 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -75,51 +75,71 @@ func SkipIfSQLUnsupported(t testing.TB) { } } -func SkipIfStreamsUnsupported(t *testing.T) { +func skipIfLess(t *testing.T, feature string, major, minor, patch uint64) { t.Helper() - // Tarantool supports streams and interactive transactions since version 2.10.0 - isLess, err := IsTarantoolVersionLess(2, 10, 0) + isLess, err := IsTarantoolVersionLess(major, minor, patch) if err != nil { t.Fatalf("Could not check the Tarantool version") } if isLess { - t.Skip("Skipping test for Tarantool without streams support") + t.Skipf("Skipping test for Tarantool without %s support", feature) } } -// SkipIfIdUnsupported skips test run if Tarantool without -// IPROTO_ID support is used. -func SkipIfIdUnsupported(t *testing.T) { +func skipIfGreaterOrEqual(t *testing.T, feature string, major, minor, patch uint64) { t.Helper() - // Tarantool supports Id requests since version 2.10.0 - isLess, err := IsTarantoolVersionLess(2, 10, 0) + isLess, err := IsTarantoolVersionLess(major, minor, patch) if err != nil { t.Fatalf("Could not check the Tarantool version") } - if isLess { - t.Skip("Skipping test for Tarantool without id requests support") + if !isLess { + t.Skipf("Skipping test for Tarantool with %s support", feature) } } +// SkipOfStreamsUnsupported skips test run if Tarantool without streams +// support is used. +func SkipIfStreamsUnsupported(t *testing.T) { + t.Helper() + + skipIfLess(t, "streams", 2, 10, 0) +} + +// SkipOfStreamsUnsupported skips test run if Tarantool without watchers +// support is used. +func SkipIfWatchersUnsupported(t *testing.T) { + t.Helper() + + skipIfLess(t, "watchers", 2, 10, 0) +} + +// SkipIfWatchersSupported skips test run if Tarantool with watchers +// support is used. +func SkipIfWatchersSupported(t *testing.T) { + t.Helper() + + skipIfGreaterOrEqual(t, "watchers", 2, 10, 0) +} + +// SkipIfIdUnsupported skips test run if Tarantool without +// IPROTO_ID support is used. +func SkipIfIdUnsupported(t *testing.T) { + t.Helper() + + skipIfLess(t, "id requests", 2, 10, 0) +} + // SkipIfIdSupported skips test run if Tarantool with // IPROTO_ID support is used. Skip is useful for tests validating // that protocol info is processed as expected even for pre-IPROTO_ID instances. func SkipIfIdSupported(t *testing.T) { t.Helper() - // Tarantool supports Id requests since version 2.10.0 - isLess, err := IsTarantoolVersionLess(2, 10, 0) - if err != nil { - t.Fatalf("Could not check the Tarantool version") - } - - if !isLess { - t.Skip("Skipping test for Tarantool with non-zero protocol version and features") - } + skipIfGreaterOrEqual(t, "id requests", 2, 10, 0) } // CheckEqualBoxErrors checks equivalence of tarantool.BoxError objects. diff --git a/watch.go b/watch.go new file mode 100644 index 000000000..61631657c --- /dev/null +++ b/watch.go @@ -0,0 +1,138 @@ +package tarantool + +import ( + "context" +) + +// BroadcastRequest helps to send broadcast messages. See: +// https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_events/broadcast/ +type BroadcastRequest struct { + call *CallRequest + key string +} + +// NewBroadcastRequest returns a new broadcast request for a specified key. +func NewBroadcastRequest(key string) *BroadcastRequest { + req := new(BroadcastRequest) + req.key = key + req.call = NewCallRequest("box.broadcast").Args([]interface{}{key}) + return req +} + +// Value sets the value for the broadcast request. +// Note: default value is nil. +func (req *BroadcastRequest) Value(value interface{}) *BroadcastRequest { + req.call = req.call.Args([]interface{}{req.key, value}) + return req +} + +// Context sets a passed context to the broadcast request. +func (req *BroadcastRequest) Context(ctx context.Context) *BroadcastRequest { + req.call = req.call.Context(ctx) + return req +} + +// Code returns IPROTO code for the broadcast request. +func (req *BroadcastRequest) Code() int32 { + return req.call.Code() +} + +// Body fills an encoder with the broadcast request body. +func (req *BroadcastRequest) Body(res SchemaResolver, enc *encoder) error { + return req.call.Body(res, enc) +} + +// Ctx returns a context of the broadcast request. +func (req *BroadcastRequest) Ctx() context.Context { + return req.call.Ctx() +} + +// Async returns is the broadcast request expects a response. +func (req *BroadcastRequest) Async() bool { + return req.call.Async() +} + +// watchRequest subscribes to the updates of a specified key defined on the +// server. After receiving the notification, you should send a new +// watchRequest to acknowledge the notification. +type watchRequest struct { + baseRequest + key string + ctx context.Context +} + +// newWatchRequest returns a new watchRequest. +func newWatchRequest(key string) *watchRequest { + req := new(watchRequest) + req.requestCode = WatchRequestCode + req.async = true + req.key = key + return req +} + +// Body fills an encoder with the watch request body. +func (req *watchRequest) Body(res SchemaResolver, enc *encoder) error { + if err := enc.EncodeMapLen(1); err != nil { + return err + } + if err := encodeUint(enc, KeyEvent); err != nil { + return err + } + return enc.EncodeString(req.key) +} + +// Context sets a passed context to the request. +func (req *watchRequest) Context(ctx context.Context) *watchRequest { + req.ctx = ctx + return req +} + +// unwatchRequest unregisters a watcher subscribed to the given notification +// key. +type unwatchRequest struct { + baseRequest + key string + ctx context.Context +} + +// newUnwatchRequest returns a new unwatchRequest. +func newUnwatchRequest(key string) *unwatchRequest { + req := new(unwatchRequest) + req.requestCode = UnwatchRequestCode + req.async = true + req.key = key + return req +} + +// Body fills an encoder with the unwatch request body. +func (req *unwatchRequest) Body(res SchemaResolver, enc *encoder) error { + if err := enc.EncodeMapLen(1); err != nil { + return err + } + if err := encodeUint(enc, KeyEvent); err != nil { + return err + } + return enc.EncodeString(req.key) +} + +// Context sets a passed context to the request. +func (req *unwatchRequest) Context(ctx context.Context) *unwatchRequest { + req.ctx = ctx + return req +} + +// WatchEvent is a watch notification event received from a server. +type WatchEvent struct { + Conn *Connection // A source connection. + Key string // A key. + Value interface{} // A value. +} + +// Watcher is a subscription to broadcast events. +type Watcher interface { + // Unregister unregisters the watcher. + Unregister() +} + +// WatchCallback is a callback to invoke when the key value is updated. +type WatchCallback func(event WatchEvent) From 1a0108994dcaff45e09b5ddf561f21679e8f3021 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 13 Dec 2022 11:26:53 +0300 Subject: [PATCH 369/605] code health: rewrite ext error test skip Use skipIfLess helpers like in other test conditions. Follows #119 --- test_helpers/utils.go | 49 ++++++++++++++----------------------------- 1 file changed, 16 insertions(+), 33 deletions(-) diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 8cf8e2b4b..5747aeeec 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -142,6 +142,22 @@ func SkipIfIdSupported(t *testing.T) { skipIfGreaterOrEqual(t, "id requests", 2, 10, 0) } +// SkipIfErrorExtendedInfoUnsupported skips test run if Tarantool without +// IPROTO_ERROR (0x52) support is used. +func SkipIfErrorExtendedInfoUnsupported(t *testing.T) { + t.Helper() + + skipIfLess(t, "error extended info", 2, 4, 1) +} + +// SkipIfErrorMessagePackTypeUnsupported skips test run if Tarantool without +// MP_ERROR type over iproto support is used. +func SkipIfErrorMessagePackTypeUnsupported(t *testing.T) { + t.Helper() + + skipIfLess(t, "error type in MessagePack", 2, 10, 0) +} + // CheckEqualBoxErrors checks equivalence of tarantool.BoxError objects. // // Tarantool errors are not comparable by nature: @@ -180,36 +196,3 @@ func CheckEqualBoxErrors(t *testing.T, expected tarantool.BoxError, actual taran } } } - -// SkipIfErrorExtendedInfoUnsupported skips test run if Tarantool without -// IPROTO_ERROR (0x52) support is used. -func SkipIfErrorExtendedInfoUnsupported(t *testing.T) { - t.Helper() - - // Tarantool provides extended error info only since 2.4.1 version. - isLess, err := IsTarantoolVersionLess(2, 4, 1) - if err != nil { - t.Fatalf("Could not check the Tarantool version") - } - - if isLess { - t.Skip("Skipping test for Tarantool without error extended info support") - } -} - -// SkipIfErrorExtendedInfoUnsupported skips test run if Tarantool without -// MP_ERROR type over iproto support is used. -func SkipIfErrorMessagePackTypeUnsupported(t *testing.T) { - t.Helper() - - // Tarantool error type over MessagePack supported only since 2.10.0 version. - isLess, err := IsTarantoolVersionLess(2, 10, 0) - if err != nil { - t.Fatalf("Could not check the Tarantool version") - } - - if isLess { - t.Skip("Skipping test for Tarantool without support of error type over MessagePack") - t.Skip("Skipping test for Tarantool without error extended info support") - } -} From 61ebbeb9b335ab91b597f57292b3fa2586089348 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Mon, 12 Dec 2022 17:14:09 +0300 Subject: [PATCH 370/605] test: change space id range In Tarantool, user space ids starts from 512. You can set arbitrary id or use autoincrement (sequence also starts from 512). Unfortunately, mixing spaces with autoincremented ids and spaces with explicit ids may cause id conflict [1]. Since there are cases when we cannot explicitly set space id (creating a space with SQL), a short range of free ids (from 512 to 515) causes problems. This patch increases range of free ids (now it's from 512 to 615) so it should be ok until [1] is resolved. 1. https://github.com/tarantool/tarantool/issues/8036 Part of #215 --- config.lua | 14 +++++++------- example_custom_unpacking_test.go | 2 +- example_test.go | 24 ++++++++++++------------ multi/config.lua | 4 ++-- multi/multi_test.go | 2 +- tarantool_test.go | 32 ++++++++++++++++---------------- 6 files changed, 39 insertions(+), 39 deletions(-) diff --git a/config.lua b/config.lua index bb976ef43..c8a853ff4 100644 --- a/config.lua +++ b/config.lua @@ -7,7 +7,7 @@ box.cfg{ box.once("init", function() local st = box.schema.space.create('schematest', { - id = 516, + id = 616, temporary = true, if_not_exists = true, field_count = 7, @@ -36,7 +36,7 @@ box.once("init", function() st:truncate() local s = box.schema.space.create('test', { - id = 517, + id = 617, if_not_exists = true, }) s:create_index('primary', { @@ -46,7 +46,7 @@ box.once("init", function() }) local s = box.schema.space.create('teststring', { - id = 518, + id = 618, if_not_exists = true, }) s:create_index('primary', { @@ -56,7 +56,7 @@ box.once("init", function() }) local s = box.schema.space.create('testintint', { - id = 519, + id = 619, if_not_exists = true, }) s:create_index('primary', { @@ -66,7 +66,7 @@ box.once("init", function() }) local s = box.schema.space.create('SQL_TEST', { - id = 520, + id = 620, if_not_exists = true, format = { {name = "NAME0", type = "unsigned"}, @@ -82,7 +82,7 @@ box.once("init", function() s:insert{1, "test", "test"} local s = box.schema.space.create('test_perf', { - id = 521, + id = 621, temporary = true, if_not_exists = true, field_count = 3, @@ -117,7 +117,7 @@ box.once("init", function() end local s = box.schema.space.create('test_error_type', { - id = 522, + id = 622, temporary = true, if_not_exists = true, field_count = 2, diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index 4087c5620..26d19f3af 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -87,7 +87,7 @@ func Example_customUnpacking() { log.Fatalf("Failed to connect: %s", err.Error()) } - spaceNo := uint32(517) + spaceNo := uint32(617) indexNo := uint32(0) tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} diff --git a/example_test.go b/example_test.go index 538fa93b1..27a962da0 100644 --- a/example_test.go +++ b/example_test.go @@ -51,7 +51,7 @@ func ExampleConnection_Select() { conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) conn.Replace(spaceNo, []interface{}{uint(1112), "hallo", "werld"}) - resp, err := conn.Select(517, 0, 0, 100, tarantool.IterEq, []interface{}{uint(1111)}) + resp, err := conn.Select(617, 0, 0, 100, tarantool.IterEq, []interface{}{uint(1111)}) if err != nil { fmt.Printf("error in select is %v", err) @@ -75,7 +75,7 @@ func ExampleConnection_SelectTyped() { defer conn.Close() var res []Tuple - err := conn.SelectTyped(517, 0, 0, 100, tarantool.IterEq, tarantool.IntKey{1111}, &res) + err := conn.SelectTyped(617, 0, 0, 100, tarantool.IterEq, tarantool.IntKey{1111}, &res) if err != nil { fmt.Printf("error in select is %v", err) @@ -96,7 +96,7 @@ func ExampleConnection_SelectTyped() { func ExampleConnection_SelectAsync() { conn := example_connect(opts) defer conn.Close() - spaceNo := uint32(517) + spaceNo := uint32(617) conn.Insert(spaceNo, []interface{}{uint(16), "test", "one"}) conn.Insert(spaceNo, []interface{}{uint(17), "test", "one"}) @@ -223,7 +223,7 @@ func ExampleSelectRequest() { conn := example_connect(opts) defer conn.Close() - req := tarantool.NewSelectRequest(517). + req := tarantool.NewSelectRequest(617). Limit(100). Key(tarantool.IntKey{1111}) resp, err := conn.Do(req).Get() @@ -253,7 +253,7 @@ func ExampleUpdateRequest() { conn := example_connect(opts) defer conn.Close() - req := tarantool.NewUpdateRequest(517). + req := tarantool.NewUpdateRequest(617). Key(tarantool.IntKey{1111}). Operations(tarantool.NewOperations().Assign(1, "bye")) resp, err := conn.Do(req).Get() @@ -284,7 +284,7 @@ func ExampleUpsertRequest() { defer conn.Close() var req tarantool.Request - req = tarantool.NewUpsertRequest(517). + req = tarantool.NewUpsertRequest(617). Tuple([]interface{}{uint(1113), "first", "first"}). Operations(tarantool.NewOperations().Assign(1, "updated")) resp, err := conn.Do(req).Get() @@ -305,7 +305,7 @@ func ExampleUpsertRequest() { } fmt.Printf("response is %#v\n", resp.Data) - req = tarantool.NewSelectRequest(517). + req = tarantool.NewSelectRequest(617). Limit(100). Key(tarantool.IntKey{1113}) resp, err = conn.Do(req).Get() @@ -830,12 +830,12 @@ func ExampleSchema() { } space1 := schema.Spaces["test"] - space2 := schema.SpacesById[516] + space2 := schema.SpacesById[616] fmt.Printf("Space 1 ID %d %s\n", space1.Id, space1.Name) fmt.Printf("Space 2 ID %d %s\n", space2.Id, space2.Name) // Output: - // Space 1 ID 517 test - // Space 2 ID 516 schematest + // Space 1 ID 617 test + // Space 2 ID 616 schematest } // Example demonstrates how to retrieve information with space schema. @@ -854,7 +854,7 @@ func ExampleSpace() { // Access Space objects by name or ID. space1 := schema.Spaces["test"] - space2 := schema.SpacesById[516] // It's a map. + space2 := schema.SpacesById[616] // It's a map. fmt.Printf("Space 1 ID %d %s %s\n", space1.Id, space1.Name, space1.Engine) fmt.Printf("Space 1 ID %d %t\n", space1.FieldsCount, space1.Temporary) @@ -875,7 +875,7 @@ func ExampleSpace() { fmt.Printf("SpaceField 2 %s %s\n", spaceField2.Name, spaceField2.Type) // Output: - // Space 1 ID 517 test memtx + // Space 1 ID 617 test memtx // Space 1 ID 0 false // Index 0 primary // &{0 unsigned} &{2 string} diff --git a/multi/config.lua b/multi/config.lua index aca4db9b3..7364c0bd6 100644 --- a/multi/config.lua +++ b/multi/config.lua @@ -13,7 +13,7 @@ rawset(_G, 'get_cluster_nodes', get_cluster_nodes) box.once("init", function() local s = box.schema.space.create('test', { - id = 517, + id = 617, if_not_exists = true, }) s:create_index('primary', {type = 'tree', parts = {1, 'string'}, if_not_exists = true}) @@ -22,7 +22,7 @@ box.once("init", function() box.schema.user.grant('test', 'read,write,execute', 'universe') local sp = box.schema.space.create('SQL_TEST', { - id = 521, + id = 621, if_not_exists = true, format = { {name = "NAME0", type = "unsigned"}, diff --git a/multi/multi_test.go b/multi/multi_test.go index ef07d629b..d3a111a9f 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -16,7 +16,7 @@ import ( var server1 = "127.0.0.1:3013" var server2 = "127.0.0.1:3014" -var spaceNo = uint32(517) +var spaceNo = uint32(617) var spaceName = "test" var indexNo = uint32(0) var connOpts = tarantool.Opts{ diff --git a/tarantool_test.go b/tarantool_test.go index de7af9e91..fdb129d07 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -99,7 +99,7 @@ func convertUint64(v interface{}) (result uint64, err error) { } var server = "127.0.0.1:3013" -var spaceNo = uint32(517) +var spaceNo = uint32(617) var spaceName = "test" var indexNo = uint32(0) var indexName = "primary" @@ -1776,29 +1776,29 @@ func TestSchema(t *testing.T) { } var space, space2 *Space var ok bool - if space, ok = schema.SpacesById[516]; !ok { - t.Errorf("space with id = 516 was not found in schema.SpacesById") + if space, ok = schema.SpacesById[616]; !ok { + t.Errorf("space with id = 616 was not found in schema.SpacesById") } if space2, ok = schema.Spaces["schematest"]; !ok { t.Errorf("space with name 'schematest' was not found in schema.SpacesById") } if space != space2 { - t.Errorf("space with id = 516 and space with name schematest are different") + t.Errorf("space with id = 616 and space with name schematest are different") } - if space.Id != 516 { - t.Errorf("space 516 has incorrect Id") + if space.Id != 616 { + t.Errorf("space 616 has incorrect Id") } if space.Name != "schematest" { - t.Errorf("space 516 has incorrect Name") + t.Errorf("space 616 has incorrect Name") } if !space.Temporary { - t.Errorf("space 516 should be temporary") + t.Errorf("space 616 should be temporary") } if space.Engine != "memtx" { - t.Errorf("space 516 engine should be memtx") + t.Errorf("space 616 engine should be memtx") } if space.FieldsCount != 7 { - t.Errorf("space 516 has incorrect fields count") + t.Errorf("space 616 has incorrect fields count") } if space.FieldsById == nil { @@ -1908,20 +1908,20 @@ func TestSchema(t *testing.T) { } var rSpaceNo, rIndexNo uint32 - rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex(516, 3) - if err != nil || rSpaceNo != 516 || rIndexNo != 3 { + rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex(616, 3) + if err != nil || rSpaceNo != 616 || rIndexNo != 3 { t.Errorf("numeric space and index params not resolved as-is") } - rSpaceNo, _, err = schema.ResolveSpaceIndex(516, nil) - if err != nil || rSpaceNo != 516 { + rSpaceNo, _, err = schema.ResolveSpaceIndex(616, nil) + if err != nil || rSpaceNo != 616 { t.Errorf("numeric space param not resolved as-is") } rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex("schematest", "secondary") - if err != nil || rSpaceNo != 516 || rIndexNo != 3 { + if err != nil || rSpaceNo != 616 || rIndexNo != 3 { t.Errorf("symbolic space and index params not resolved") } rSpaceNo, _, err = schema.ResolveSpaceIndex("schematest", nil) - if err != nil || rSpaceNo != 516 { + if err != nil || rSpaceNo != 616 { t.Errorf("symbolic space param not resolved") } _, _, err = schema.ResolveSpaceIndex("schematest22", "secondary") From 3dc36de627e3473b0c48c60f63fa9e9c71361f93 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 13 Dec 2022 16:09:09 +0300 Subject: [PATCH 371/605] test: configure module paths for queue init Set up package paths so queue test instances could be run with arbitrary work_dir. Part of #215 --- Makefile | 2 +- queue/testdata/config.lua | 25 +++++++++++++++++++++++++ queue/testdata/pool.lua | 25 +++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 59961774b..7b6f8ec5f 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ clean: .PHONY: deps deps: clean - ( cd ./queue; tarantoolctl rocks install queue 1.2.1 ) + ( cd ./queue/testdata; tarantoolctl rocks install queue 1.2.1 ) .PHONY: datetime-timezones datetime-timezones: diff --git a/queue/testdata/config.lua b/queue/testdata/config.lua index eccb19a68..e0adc069c 100644 --- a/queue/testdata/config.lua +++ b/queue/testdata/config.lua @@ -1,3 +1,28 @@ +-- configure path so that you can run application +-- from outside the root directory +if package.setsearchroot ~= nil then + package.setsearchroot() +else + -- Workaround for rocks loading in tarantool 1.10 + -- It can be removed in tarantool > 2.2 + -- By default, when you do require('mymodule'), tarantool looks into + -- the current working directory and whatever is specified in + -- package.path and package.cpath. If you run your app while in the + -- root directory of that app, everything goes fine, but if you try to + -- start your app with "tarantool myapp/init.lua", it will fail to load + -- its modules, and modules from myapp/.rocks. + local fio = require('fio') + local app_dir = fio.abspath(fio.dirname(arg[0])) + package.path = app_dir .. '/?.lua;' .. package.path + package.path = app_dir .. '/?/init.lua;' .. package.path + package.path = app_dir .. '/.rocks/share/tarantool/?.lua;' .. package.path + package.path = app_dir .. '/.rocks/share/tarantool/?/init.lua;' .. package.path + package.cpath = app_dir .. '/?.so;' .. package.cpath + package.cpath = app_dir .. '/?.dylib;' .. package.cpath + package.cpath = app_dir .. '/.rocks/lib/tarantool/?.so;' .. package.cpath + package.cpath = app_dir .. '/.rocks/lib/tarantool/?.dylib;' .. package.cpath +end + local queue = require('queue') rawset(_G, 'queue', queue) diff --git a/queue/testdata/pool.lua b/queue/testdata/pool.lua index 7c63aa787..1bcc11654 100644 --- a/queue/testdata/pool.lua +++ b/queue/testdata/pool.lua @@ -1,3 +1,28 @@ +-- configure path so that you can run application +-- from outside the root directory +if package.setsearchroot ~= nil then + package.setsearchroot() +else + -- Workaround for rocks loading in tarantool 1.10 + -- It can be removed in tarantool > 2.2 + -- By default, when you do require('mymodule'), tarantool looks into + -- the current working directory and whatever is specified in + -- package.path and package.cpath. If you run your app while in the + -- root directory of that app, everything goes fine, but if you try to + -- start your app with "tarantool myapp/init.lua", it will fail to load + -- its modules, and modules from myapp/.rocks. + local fio = require('fio') + local app_dir = fio.abspath(fio.dirname(arg[0])) + package.path = app_dir .. '/?.lua;' .. package.path + package.path = app_dir .. '/?/init.lua;' .. package.path + package.path = app_dir .. '/.rocks/share/tarantool/?.lua;' .. package.path + package.path = app_dir .. '/.rocks/share/tarantool/?/init.lua;' .. package.path + package.cpath = app_dir .. '/?.so;' .. package.cpath + package.cpath = app_dir .. '/?.dylib;' .. package.cpath + package.cpath = app_dir .. '/.rocks/lib/tarantool/?.so;' .. package.cpath + package.cpath = app_dir .. '/.rocks/lib/tarantool/?.dylib;' .. package.cpath +end + local queue = require('queue') rawset(_G, 'queue', queue) From 770ad3a8aaf024a45dd7218a0237ba11145173d0 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Mon, 12 Dec 2022 18:01:31 +0300 Subject: [PATCH 372/605] test: generate tmp work dirs Generate tmp work directories for each new Tarantool instance, if not specified. It prevents possible directory clash issues. Later `ioutil.TempDir` (deprecated since Go 1.17) must be replaced with `os.MkdirTemp` (introduced in Go 1.16 as a replacement) when we drop support of Go 1.16 an older. Part of #215 --- connection_pool/connection_pool_test.go | 6 +--- datetime/datetime_test.go | 1 - decimal/decimal_test.go | 1 - multi/multi_test.go | 2 -- queue/queue_test.go | 4 +-- ssl_test.go | 1 - tarantool_test.go | 2 -- test_helpers/main.go | 45 ++++++++++++++++--------- test_helpers/pool_helper.go | 9 +++-- uuid/uuid_test.go | 1 - 10 files changed, 39 insertions(+), 33 deletions(-) diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 8dc8cc9da..970612b5e 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -2379,10 +2379,6 @@ func runTestMain(m *testing.M) int { waitStart := 100 * time.Millisecond connectRetry := 3 retryTimeout := 500 * time.Millisecond - workDirs := []string{ - "work_dir1", "work_dir2", - "work_dir3", "work_dir4", - "work_dir5"} // Tarantool supports streams and interactive transactions since version 2.10.0 isStreamUnsupported, err := test_helpers.IsTarantoolVersionLess(2, 10, 0) @@ -2390,7 +2386,7 @@ func runTestMain(m *testing.M) int { log.Fatalf("Could not check the Tarantool version") } - instances, err = test_helpers.StartTarantoolInstances(servers, workDirs, test_helpers.StartOpts{ + instances, err = test_helpers.StartTarantoolInstances(servers, nil, test_helpers.StartOpts{ InitScript: initScript, User: connOpts.User, Pass: connOpts.Pass, diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 5c63d0f4c..f4cc8b2a1 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -1141,7 +1141,6 @@ func runTestMain(m *testing.M) int { instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{ InitScript: "config.lua", Listen: server, - WorkDir: "work_dir", User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 6fba0cc8f..d81e6c488 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -613,7 +613,6 @@ func runTestMain(m *testing.M) int { instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{ InitScript: "config.lua", Listen: server, - WorkDir: "work_dir", User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, diff --git a/multi/multi_test.go b/multi/multi_test.go index d3a111a9f..3b395864c 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -592,7 +592,6 @@ func runTestMain(m *testing.M) int { inst1, err := test_helpers.StartTarantool(test_helpers.StartOpts{ InitScript: initScript, Listen: server1, - WorkDir: "work_dir1", User: connOpts.User, Pass: connOpts.Pass, WaitStart: waitStart, @@ -609,7 +608,6 @@ func runTestMain(m *testing.M) int { inst2, err := test_helpers.StartTarantool(test_helpers.StartOpts{ InitScript: initScript, Listen: server2, - WorkDir: "work_dir2", User: connOpts.User, Pass: connOpts.Pass, WaitStart: waitStart, diff --git a/queue/queue_test.go b/queue/queue_test.go index 85276e4a8..905fefc32 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -899,7 +899,6 @@ func runTestMain(m *testing.M) int { inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ InitScript: "testdata/config.lua", Listen: server, - WorkDir: "work_dir", User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, @@ -913,7 +912,6 @@ func runTestMain(m *testing.M) int { defer test_helpers.StopTarantoolWithCleanup(inst) - workDirs := []string{"work_dir1", "work_dir2"} poolOpts := test_helpers.StartOpts{ InitScript: "testdata/pool.lua", User: opts.User, @@ -921,7 +919,7 @@ func runTestMain(m *testing.M) int { WaitStart: 3 * time.Second, // replication_timeout * 3 ConnectRetry: -1, } - instances, err = test_helpers.StartTarantoolInstances(serversPool, workDirs, poolOpts) + instances, err = test_helpers.StartTarantoolInstances(serversPool, nil, poolOpts) if err != nil { log.Fatalf("Failed to prepare test tarantool pool: %s", err) diff --git a/ssl_test.go b/ssl_test.go index 36ce4905f..769508284 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -126,7 +126,6 @@ func serverTnt(serverOpts, clientOpts SslOpts) (test_helpers.TarantoolInstance, ClientServer: tntHost, ClientTransport: "ssl", ClientSsl: clientOpts, - WorkDir: "work_dir_ssl", User: "test", Pass: "test", WaitStart: 100 * time.Millisecond, diff --git a/tarantool_test.go b/tarantool_test.go index fdb129d07..96bf02254 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -23,7 +23,6 @@ import ( var startOpts test_helpers.StartOpts = test_helpers.StartOpts{ InitScript: "config.lua", Listen: server, - WorkDir: "work_dir", User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, @@ -3317,7 +3316,6 @@ func TestConnection_NewWatcher_reconnect(t *testing.T) { inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ InitScript: "config.lua", Listen: server, - WorkDir: "work_dir", User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, diff --git a/test_helpers/main.go b/test_helpers/main.go index 77e22c535..f6e745617 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -47,9 +47,10 @@ type StartOpts struct { // a Tarantool instance. ClientSsl tarantool.SslOpts - // WorkDir is box.cfg work_dir parameter for tarantool. - // Specify folder to store tarantool data files. - // Folder must be unique for each tarantool process used simultaneously. + // WorkDir is box.cfg work_dir parameter for a Tarantool instance: + // a folder to store data files. If not specified, helpers create a + // new temporary directory. + // Folder must be unique for each Tarantool process used simultaneously. // https://www.tarantool.io/en/doc/latest/reference/configuration/#confval-work_dir WorkDir string @@ -189,6 +190,32 @@ func RestartTarantool(inst *TarantoolInstance) error { func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { // Prepare tarantool command. var inst TarantoolInstance + var dir string + var err error + + if startOpts.WorkDir == "" { + // Create work_dir for a new instance. + // TO DO: replace with `os.MkdirTemp` when we drop support of + // Go 1.16 an older + dir, err = ioutil.TempDir("", "work_dir") + if err != nil { + return inst, err + } + startOpts.WorkDir = dir + } else { + // Clean up existing work_dir. + err = os.RemoveAll(startOpts.WorkDir) + if err != nil { + return inst, err + } + + // Create work_dir. + err = os.Mkdir(startOpts.WorkDir, 0755) + if err != nil { + return inst, err + } + } + inst.Cmd = exec.Command("tarantool", startOpts.InitScript) inst.Cmd.Env = append( @@ -198,18 +225,6 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { fmt.Sprintf("TEST_TNT_MEMTX_USE_MVCC_ENGINE=%t", startOpts.MemtxUseMvccEngine), ) - // Clean up existing work_dir. - err := os.RemoveAll(startOpts.WorkDir) - if err != nil { - return inst, err - } - - // Create work_dir. - err = os.Mkdir(startOpts.WorkDir, 0755) - if err != nil { - return inst, err - } - // Copy SSL certificates. if startOpts.SslCertsDir != "" { err = copySslCerts(startOpts.WorkDir, startOpts.SslCertsDir) diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index 0061870de..8c3a7e6ff 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -219,7 +219,8 @@ func SetClusterRO(servers []string, connOpts tarantool.Opts, roles []bool) error } func StartTarantoolInstances(servers []string, workDirs []string, opts StartOpts) ([]TarantoolInstance, error) { - if len(servers) != len(workDirs) { + isUserWorkDirs := (workDirs != nil) + if isUserWorkDirs && (len(servers) != len(workDirs)) { return nil, fmt.Errorf("number of servers should be equal to number of workDirs") } @@ -227,7 +228,11 @@ func StartTarantoolInstances(servers []string, workDirs []string, opts StartOpts for i, server := range servers { opts.Listen = server - opts.WorkDir = workDirs[i] + if isUserWorkDirs { + opts.WorkDir = workDirs[i] + } else { + opts.WorkDir = "" + } instance, err := StartTarantool(opts) if err != nil { diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index 6224a938b..e04ab5ab9 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -155,7 +155,6 @@ func runTestMain(m *testing.M) int { inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ InitScript: "config.lua", Listen: server, - WorkDir: "work_dir", User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, From b2a14637e9b6171d32ac1977d3eef9f5c4852f58 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 13 Dec 2022 16:15:11 +0300 Subject: [PATCH 373/605] test: improve skip helpers Make skip helpers public. Add more wrappers to reduce code duplication. Part of #215 --- test_helpers/utils.go | 50 +++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 5747aeeec..4862d90d8 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -1,6 +1,7 @@ package test_helpers import ( + "fmt" "testing" "time" @@ -75,7 +76,8 @@ func SkipIfSQLUnsupported(t testing.TB) { } } -func skipIfLess(t *testing.T, feature string, major, minor, patch uint64) { +// SkipIfLess skips test run if Tarantool version is less than expected. +func SkipIfLess(t *testing.T, reason string, major, minor, patch uint64) { t.Helper() isLess, err := IsTarantoolVersionLess(major, minor, patch) @@ -84,11 +86,13 @@ func skipIfLess(t *testing.T, feature string, major, minor, patch uint64) { } if isLess { - t.Skipf("Skipping test for Tarantool without %s support", feature) + t.Skipf("Skipping test for Tarantool %s", reason) } } -func skipIfGreaterOrEqual(t *testing.T, feature string, major, minor, patch uint64) { +// SkipIfGreaterOrEqual skips test run if Tarantool version is greater or equal +// than expected. +func SkipIfGreaterOrEqual(t *testing.T, reason string, major, minor, patch uint64) { t.Helper() isLess, err := IsTarantoolVersionLess(major, minor, patch) @@ -97,16 +101,40 @@ func skipIfGreaterOrEqual(t *testing.T, feature string, major, minor, patch uint } if !isLess { - t.Skipf("Skipping test for Tarantool with %s support", feature) + t.Skipf("Skipping test for Tarantool %s", reason) } } +// SkipIfFeatureUnsupported skips test run if Tarantool does not yet support a feature. +func SkipIfFeatureUnsupported(t *testing.T, feature string, major, minor, patch uint64) { + t.Helper() + + SkipIfLess(t, fmt.Sprintf("without %s support", feature), major, minor, patch) +} + +// SkipIfFeatureSupported skips test run if Tarantool supports a feature. +// Helper if useful when we want to test if everything is alright +// on older versions. +func SkipIfFeatureSupported(t *testing.T, feature string, major, minor, patch uint64) { + t.Helper() + + SkipIfGreaterOrEqual(t, fmt.Sprintf("with %s support", feature), major, minor, patch) +} + +// SkipIfFeatureDropped skips test run if Tarantool had dropped +// support of a feature. +func SkipIfFeatureDropped(t *testing.T, feature string, major, minor, patch uint64) { + t.Helper() + + SkipIfGreaterOrEqual(t, fmt.Sprintf("with %s support dropped", feature), major, minor, patch) +} + // SkipOfStreamsUnsupported skips test run if Tarantool without streams // support is used. func SkipIfStreamsUnsupported(t *testing.T) { t.Helper() - skipIfLess(t, "streams", 2, 10, 0) + SkipIfFeatureUnsupported(t, "streams", 2, 10, 0) } // SkipOfStreamsUnsupported skips test run if Tarantool without watchers @@ -114,7 +142,7 @@ func SkipIfStreamsUnsupported(t *testing.T) { func SkipIfWatchersUnsupported(t *testing.T) { t.Helper() - skipIfLess(t, "watchers", 2, 10, 0) + SkipIfFeatureUnsupported(t, "watchers", 2, 10, 0) } // SkipIfWatchersSupported skips test run if Tarantool with watchers @@ -122,7 +150,7 @@ func SkipIfWatchersUnsupported(t *testing.T) { func SkipIfWatchersSupported(t *testing.T) { t.Helper() - skipIfGreaterOrEqual(t, "watchers", 2, 10, 0) + SkipIfFeatureSupported(t, "watchers", 2, 10, 0) } // SkipIfIdUnsupported skips test run if Tarantool without @@ -130,7 +158,7 @@ func SkipIfWatchersSupported(t *testing.T) { func SkipIfIdUnsupported(t *testing.T) { t.Helper() - skipIfLess(t, "id requests", 2, 10, 0) + SkipIfFeatureUnsupported(t, "id requests", 2, 10, 0) } // SkipIfIdSupported skips test run if Tarantool with @@ -139,7 +167,7 @@ func SkipIfIdUnsupported(t *testing.T) { func SkipIfIdSupported(t *testing.T) { t.Helper() - skipIfGreaterOrEqual(t, "id requests", 2, 10, 0) + SkipIfFeatureSupported(t, "id requests", 2, 10, 0) } // SkipIfErrorExtendedInfoUnsupported skips test run if Tarantool without @@ -147,7 +175,7 @@ func SkipIfIdSupported(t *testing.T) { func SkipIfErrorExtendedInfoUnsupported(t *testing.T) { t.Helper() - skipIfLess(t, "error extended info", 2, 4, 1) + SkipIfFeatureUnsupported(t, "error extended info", 2, 4, 1) } // SkipIfErrorMessagePackTypeUnsupported skips test run if Tarantool without @@ -155,7 +183,7 @@ func SkipIfErrorExtendedInfoUnsupported(t *testing.T) { func SkipIfErrorMessagePackTypeUnsupported(t *testing.T) { t.Helper() - skipIfLess(t, "error type in MessagePack", 2, 10, 0) + SkipIfFeatureUnsupported(t, "error type in MessagePack", 2, 10, 0) } // CheckEqualBoxErrors checks equivalence of tarantool.BoxError objects. From a1f28a6cd204d9ab6c3ea99f3192235cf3856f74 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 13 Dec 2022 14:14:47 +0300 Subject: [PATCH 374/605] api: support session settings Support session settings in a separate subpackage "settings" [1]. It allows to create a specific requests to get or set session settings. Settings are independent for each connection. 1. https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/_session_settings/ Closes #215 --- CHANGELOG.md | 1 + Makefile | 6 + settings/const.go | 21 + settings/example_test.go | 78 ++++ settings/msgpack.go | 10 + settings/msgpack_helper_test.go | 22 + settings/msgpack_v5.go | 10 + settings/msgpack_v5_helper_test.go | 25 ++ settings/request.go | 273 ++++++++++++ settings/request_test.go | 118 ++++++ settings/tarantool_test.go | 656 +++++++++++++++++++++++++++++ settings/testdata/config.lua | 15 + 12 files changed, 1235 insertions(+) create mode 100644 settings/const.go create mode 100644 settings/example_test.go create mode 100644 settings/msgpack.go create mode 100644 settings/msgpack_helper_test.go create mode 100644 settings/msgpack_v5.go create mode 100644 settings/msgpack_v5_helper_test.go create mode 100644 settings/request.go create mode 100644 settings/request_test.go create mode 100644 settings/tarantool_test.go create mode 100644 settings/testdata/config.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2c49d39..7930b34d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support errors extended information (#209) - Error type support in MessagePack (#209) - Event subscription support (#119) +- Session settings support (#215) ### Changed diff --git a/Makefile b/Makefile index 7b6f8ec5f..718139967 100644 --- a/Makefile +++ b/Makefile @@ -81,6 +81,12 @@ test-uuid: go clean -testcache go test -tags "$(TAGS)" ./uuid/ -v -p 1 +.PHONY: test-settings +test-settings: + @echo "Running tests in settings package" + go clean -testcache + go test -tags "$(TAGS)" ./settings/ -v -p 1 + .PHONY: test-main test-main: @echo "Running tests in main package" diff --git a/settings/const.go b/settings/const.go new file mode 100644 index 000000000..cc980cd7a --- /dev/null +++ b/settings/const.go @@ -0,0 +1,21 @@ +package settings + +const sessionSettingsSpace string = "_session_settings" + +// In Go and IPROTO_UPDATE count starts with 0. +const sessionSettingValueField int = 1 + +const ( + errorMarshalingEnabled string = "error_marshaling_enabled" + sqlDefaultEngine string = "sql_default_engine" + sqlDeferForeignKeys string = "sql_defer_foreign_keys" + sqlFullColumnNames string = "sql_full_column_names" + sqlFullMetadata string = "sql_full_metadata" + sqlParserDebug string = "sql_parser_debug" + sqlRecursiveTriggers string = "sql_recursive_triggers" + sqlReverseUnorderedSelects string = "sql_reverse_unordered_selects" + sqlSelectDebug string = "sql_select_debug" + sqlVDBEDebug string = "sql_vdbe_debug" +) + +const selectAllLimit uint32 = 1000 diff --git a/settings/example_test.go b/settings/example_test.go new file mode 100644 index 000000000..a2391328f --- /dev/null +++ b/settings/example_test.go @@ -0,0 +1,78 @@ +package settings_test + +import ( + "fmt" + + "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/settings" + "github.com/tarantool/go-tarantool/test_helpers" +) + +func example_connect(opts tarantool.Opts) *tarantool.Connection { + conn, err := tarantool.Connect(server, opts) + if err != nil { + panic("Connection is not established: " + err.Error()) + } + return conn +} + +func Example_sqlFullColumnNames() { + var resp *tarantool.Response + var err error + var isLess bool + + conn := example_connect(opts) + defer conn.Close() + + // Tarantool supports session settings since version 2.3.1 + isLess, err = test_helpers.IsTarantoolVersionLess(2, 3, 1) + if err != nil || isLess { + return + } + + // Create a space. + _, err = conn.Execute("CREATE TABLE example(id INT PRIMARY KEY, x INT);", []interface{}{}) + if err != nil { + fmt.Printf("error in create table: %v\n", err) + return + } + + // Insert some tuple into space. + _, err = conn.Execute("INSERT INTO example VALUES (1, 1);", []interface{}{}) + if err != nil { + fmt.Printf("error on insert: %v\n", err) + return + } + + // Enable showing full column names in SQL responses. + _, err = conn.Do(settings.NewSQLFullColumnNamesSetRequest(true)).Get() + if err != nil { + fmt.Printf("error on setting setup: %v\n", err) + return + } + + // Get some data with SQL query. + resp, err = conn.Execute("SELECT x FROM example WHERE id = 1;", []interface{}{}) + if err != nil { + fmt.Printf("error on select: %v\n", err) + return + } + // Show response metadata. + fmt.Printf("full column name: %v\n", resp.MetaData[0].FieldName) + + // Disable showing full column names in SQL responses. + _, err = conn.Do(settings.NewSQLFullColumnNamesSetRequest(false)).Get() + if err != nil { + fmt.Printf("error on setting setup: %v\n", err) + return + } + + // Get some data with SQL query. + resp, err = conn.Execute("SELECT x FROM example WHERE id = 1;", []interface{}{}) + if err != nil { + fmt.Printf("error on select: %v\n", err) + return + } + // Show response metadata. + fmt.Printf("short column name: %v\n", resp.MetaData[0].FieldName) +} diff --git a/settings/msgpack.go b/settings/msgpack.go new file mode 100644 index 000000000..295620aba --- /dev/null +++ b/settings/msgpack.go @@ -0,0 +1,10 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + +package settings + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +type encoder = msgpack.Encoder diff --git a/settings/msgpack_helper_test.go b/settings/msgpack_helper_test.go new file mode 100644 index 000000000..0c002213a --- /dev/null +++ b/settings/msgpack_helper_test.go @@ -0,0 +1,22 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + +package settings_test + +import ( + "io" + + "github.com/tarantool/go-tarantool" + "gopkg.in/vmihailenco/msgpack.v2" +) + +type encoder = msgpack.Encoder + +func NewEncoder(w io.Writer) *encoder { + return msgpack.NewEncoder(w) +} + +func toBoxError(i interface{}) (v tarantool.BoxError, ok bool) { + v, ok = i.(tarantool.BoxError) + return +} diff --git a/settings/msgpack_v5.go b/settings/msgpack_v5.go new file mode 100644 index 000000000..288418ec6 --- /dev/null +++ b/settings/msgpack_v5.go @@ -0,0 +1,10 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package settings + +import ( + "github.com/vmihailenco/msgpack/v5" +) + +type encoder = msgpack.Encoder diff --git a/settings/msgpack_v5_helper_test.go b/settings/msgpack_v5_helper_test.go new file mode 100644 index 000000000..96df6bae1 --- /dev/null +++ b/settings/msgpack_v5_helper_test.go @@ -0,0 +1,25 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package settings_test + +import ( + "io" + + "github.com/tarantool/go-tarantool" + "github.com/vmihailenco/msgpack/v5" +) + +type encoder = msgpack.Encoder + +func NewEncoder(w io.Writer) *encoder { + return msgpack.NewEncoder(w) +} + +func toBoxError(i interface{}) (v tarantool.BoxError, ok bool) { + var ptr *tarantool.BoxError + if ptr, ok = i.(*tarantool.BoxError); ok { + v = *ptr + } + return +} diff --git a/settings/request.go b/settings/request.go new file mode 100644 index 000000000..84723bb2d --- /dev/null +++ b/settings/request.go @@ -0,0 +1,273 @@ +// Package settings is a collection of requests to set a connection session setting +// or get current session configuration. +// +// +============================+=========================+=========+===========================+ +// | Setting | Meaning | Default | Supported in | +// | | | | Tarantool versions | +// +============================+=========================+=========+===========================+ +// | ErrorMarshalingEnabled | Defines whether error | false | Since 2.4.1 till 2.10.0, | +// | | objectshave a special | | replaced with IPROTO_ID | +// | | structure. | | feature flag. | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLDefaultEngine | Defines default storage | "memtx" | Since 2.3.1. | +// | | engine for new SQL | | | +// | | tables. | | | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLDeferForeignKeys | Defines whether | false | Since 2.3.1 till master | +// | | foreign-key checks can | | commit 14618c4 (possible | +// | | wait till commit. | | 2.10.5 or 2.11.0) | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLFullColumnNames | Defines whether full | false | Since 2.3.1. | +// | | column names is | | | +// | | displayed in SQL result | | | +// | | set metadata. | | | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLFullMetadata | Defines whether SQL | false | Since 2.3.1. | +// | | result set metadata | | | +// | | will have more than | | | +// | | just name and type. | | | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLParserDebug | Defines whether to show | false | Since 2.3.1 (only if | +// | | parser steps for | | built with | +// | | following statements. | | -DCMAKE_BUILD_TYPE=Debug) | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLRecursiveTriggers | Defines whether a | true | Since 2.3.1. | +// | | triggered statement can | | | +// | | activate a trigger. | | | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLReverseUnorderedSelects | Defines defines whether | false | Since 2.3.1. | +// | | result rows are usually | | | +// | | in reverse order if | | | +// | | there is no ORDER BY | | | +// | | clause. | | | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLSelectDebug | Defines whether to show | false | Since 2.3.1 (only if | +// | | to show execution steps | | built with | +// | | during SELECT. | | -DCMAKE_BUILD_TYPE=Debug) | +// +----------------------------+-------------------------+---------+---------------------------+ +// | SQLVDBEDebug | Defines whether VDBE | false | Since 2.3.1 (only if | +// | | debug mode is enabled. | | built with | +// | | | | -DCMAKE_BUILD_TYPE=Debug) | +// +----------------------------+-------------------------+---------+---------------------------+ +// +// Since: 1.10.0 +// +// See also: +// +// * Session settings https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/_session_settings/ +package settings + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// SetRequest helps to set session settings. +type SetRequest struct { + impl *tarantool.UpdateRequest +} + +func newSetRequest(setting string, value interface{}) *SetRequest { + return &SetRequest{ + impl: tarantool.NewUpdateRequest(sessionSettingsSpace). + Key(tarantool.StringKey{S: setting}). + Operations(tarantool.NewOperations().Assign(sessionSettingValueField, value)), + } +} + +// Context sets a passed context to set session settings request. +func (req *SetRequest) Context(ctx context.Context) *SetRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// Code returns IPROTO code for set session settings request. +func (req *SetRequest) Code() int32 { + return req.impl.Code() +} + +// Body fills an encoder with set session settings request body. +func (req *SetRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + return req.impl.Body(res, enc) +} + +// Ctx returns a context of set session settings request. +func (req *SetRequest) Ctx() context.Context { + return req.impl.Ctx() +} + +// Async returns is set session settings request expects a response. +func (req *SetRequest) Async() bool { + return req.impl.Async() +} + +// GetRequest helps to get session settings. +type GetRequest struct { + impl *tarantool.SelectRequest +} + +func newGetRequest(setting string) *GetRequest { + return &GetRequest{ + impl: tarantool.NewSelectRequest(sessionSettingsSpace). + Key(tarantool.StringKey{S: setting}). + Limit(1), + } +} + +// Context sets a passed context to get session settings request. +func (req *GetRequest) Context(ctx context.Context) *GetRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// Code returns IPROTO code for get session settings request. +func (req *GetRequest) Code() int32 { + return req.impl.Code() +} + +// Body fills an encoder with get session settings request body. +func (req *GetRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + return req.impl.Body(res, enc) +} + +// Ctx returns a context of get session settings request. +func (req *GetRequest) Ctx() context.Context { + return req.impl.Ctx() +} + +// Async returns is get session settings request expects a response. +func (req *GetRequest) Async() bool { + return req.impl.Async() +} + +// NewErrorMarshalingEnabledSetRequest creates a request to +// update current session ErrorMarshalingEnabled setting. +func NewErrorMarshalingEnabledSetRequest(value bool) *SetRequest { + return newSetRequest(errorMarshalingEnabled, value) +} + +// NewErrorMarshalingEnabledGetRequest creates a request to get +// current session ErrorMarshalingEnabled setting in tuple format. +func NewErrorMarshalingEnabledGetRequest() *GetRequest { + return newGetRequest(errorMarshalingEnabled) +} + +// NewSQLDefaultEngineSetRequest creates a request to +// update current session SQLDefaultEngine setting. +func NewSQLDefaultEngineSetRequest(value string) *SetRequest { + return newSetRequest(sqlDefaultEngine, value) +} + +// NewSQLDefaultEngineGetRequest creates a request to get +// current session SQLDefaultEngine setting in tuple format. +func NewSQLDefaultEngineGetRequest() *GetRequest { + return newGetRequest(sqlDefaultEngine) +} + +// NewSQLDeferForeignKeysSetRequest creates a request to +// update current session SQLDeferForeignKeys setting. +func NewSQLDeferForeignKeysSetRequest(value bool) *SetRequest { + return newSetRequest(sqlDeferForeignKeys, value) +} + +// NewSQLDeferForeignKeysGetRequest creates a request to get +// current session SQLDeferForeignKeys setting in tuple format. +func NewSQLDeferForeignKeysGetRequest() *GetRequest { + return newGetRequest(sqlDeferForeignKeys) +} + +// NewSQLFullColumnNamesSetRequest creates a request to +// update current session SQLFullColumnNames setting. +func NewSQLFullColumnNamesSetRequest(value bool) *SetRequest { + return newSetRequest(sqlFullColumnNames, value) +} + +// NewSQLFullColumnNamesGetRequest creates a request to get +// current session SQLFullColumnNames setting in tuple format. +func NewSQLFullColumnNamesGetRequest() *GetRequest { + return newGetRequest(sqlFullColumnNames) +} + +// NewSQLFullMetadataSetRequest creates a request to +// update current session SQLFullMetadata setting. +func NewSQLFullMetadataSetRequest(value bool) *SetRequest { + return newSetRequest(sqlFullMetadata, value) +} + +// NewSQLFullMetadataGetRequest creates a request to get +// current session SQLFullMetadata setting in tuple format. +func NewSQLFullMetadataGetRequest() *GetRequest { + return newGetRequest(sqlFullMetadata) +} + +// NewSQLParserDebugSetRequest creates a request to +// update current session SQLParserDebug setting. +func NewSQLParserDebugSetRequest(value bool) *SetRequest { + return newSetRequest(sqlParserDebug, value) +} + +// NewSQLParserDebugGetRequest creates a request to get +// current session SQLParserDebug setting in tuple format. +func NewSQLParserDebugGetRequest() *GetRequest { + return newGetRequest(sqlParserDebug) +} + +// NewSQLRecursiveTriggersSetRequest creates a request to +// update current session SQLRecursiveTriggers setting. +func NewSQLRecursiveTriggersSetRequest(value bool) *SetRequest { + return newSetRequest(sqlRecursiveTriggers, value) +} + +// NewSQLRecursiveTriggersGetRequest creates a request to get +// current session SQLRecursiveTriggers setting in tuple format. +func NewSQLRecursiveTriggersGetRequest() *GetRequest { + return newGetRequest(sqlRecursiveTriggers) +} + +// NewSQLReverseUnorderedSelectsSetRequest creates a request to +// update current session SQLReverseUnorderedSelects setting. +func NewSQLReverseUnorderedSelectsSetRequest(value bool) *SetRequest { + return newSetRequest(sqlReverseUnorderedSelects, value) +} + +// NewSQLReverseUnorderedSelectsGetRequest creates a request to get +// current session SQLReverseUnorderedSelects setting in tuple format. +func NewSQLReverseUnorderedSelectsGetRequest() *GetRequest { + return newGetRequest(sqlReverseUnorderedSelects) +} + +// NewSQLSelectDebugSetRequest creates a request to +// update current session SQLSelectDebug setting. +func NewSQLSelectDebugSetRequest(value bool) *SetRequest { + return newSetRequest(sqlSelectDebug, value) +} + +// NewSQLSelectDebugGetRequest creates a request to get +// current session SQLSelectDebug setting in tuple format. +func NewSQLSelectDebugGetRequest() *GetRequest { + return newGetRequest(sqlSelectDebug) +} + +// NewSQLVDBEDebugSetRequest creates a request to +// update current session SQLVDBEDebug setting. +func NewSQLVDBEDebugSetRequest(value bool) *SetRequest { + return newSetRequest(sqlVDBEDebug, value) +} + +// NewSQLVDBEDebugGetRequest creates a request to get +// current session SQLVDBEDebug setting in tuple format. +func NewSQLVDBEDebugGetRequest() *GetRequest { + return newGetRequest(sqlVDBEDebug) +} + +// NewSessionSettingsGetRequest creates a request to get all +// current session settings in tuple format. +func NewSessionSettingsGetRequest() *GetRequest { + return &GetRequest{ + impl: tarantool.NewSelectRequest(sessionSettingsSpace). + Limit(selectAllLimit), + } +} diff --git a/settings/request_test.go b/settings/request_test.go new file mode 100644 index 000000000..bc26ff058 --- /dev/null +++ b/settings/request_test.go @@ -0,0 +1,118 @@ +package settings_test + +import ( + "bytes" + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/settings" +) + +type ValidSchemeResolver struct { +} + +func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (spaceNo, indexNo uint32, err error) { + if s == nil { + if s == "_session_settings" { + spaceNo = 380 + } else { + spaceNo = uint32(s.(int)) + } + } else { + spaceNo = 0 + } + if i != nil { + indexNo = uint32(i.(int)) + } else { + indexNo = 0 + } + + return spaceNo, indexNo, nil +} + +var resolver ValidSchemeResolver + +func TestRequestsAPI(t *testing.T) { + tests := []struct { + req tarantool.Request + async bool + code int32 + }{ + {req: NewErrorMarshalingEnabledSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, + {req: NewErrorMarshalingEnabledGetRequest(), async: false, code: tarantool.SelectRequestCode}, + {req: NewSQLDefaultEngineSetRequest("memtx"), async: false, code: tarantool.UpdateRequestCode}, + {req: NewSQLDefaultEngineGetRequest(), async: false, code: tarantool.SelectRequestCode}, + {req: NewSQLDeferForeignKeysSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, + {req: NewSQLDeferForeignKeysGetRequest(), async: false, code: tarantool.SelectRequestCode}, + {req: NewSQLFullColumnNamesSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, + {req: NewSQLFullColumnNamesGetRequest(), async: false, code: tarantool.SelectRequestCode}, + {req: NewSQLFullMetadataSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, + {req: NewSQLFullMetadataGetRequest(), async: false, code: tarantool.SelectRequestCode}, + {req: NewSQLParserDebugSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, + {req: NewSQLParserDebugGetRequest(), async: false, code: tarantool.SelectRequestCode}, + {req: NewSQLRecursiveTriggersSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, + {req: NewSQLRecursiveTriggersGetRequest(), async: false, code: tarantool.SelectRequestCode}, + {req: NewSQLReverseUnorderedSelectsSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, + {req: NewSQLReverseUnorderedSelectsGetRequest(), async: false, code: tarantool.SelectRequestCode}, + {req: NewSQLSelectDebugSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, + {req: NewSQLSelectDebugGetRequest(), async: false, code: tarantool.SelectRequestCode}, + {req: NewSQLVDBEDebugSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, + {req: NewSQLVDBEDebugGetRequest(), async: false, code: tarantool.SelectRequestCode}, + {req: NewSessionSettingsGetRequest(), async: false, code: tarantool.SelectRequestCode}, + } + + for _, test := range tests { + require.Equal(t, test.async, test.req.Async()) + require.Equal(t, test.code, test.req.Code()) + + var reqBuf bytes.Buffer + enc := NewEncoder(&reqBuf) + require.Nilf(t, test.req.Body(&resolver, enc), "No errors on fill") + } +} + +func TestRequestsCtx(t *testing.T) { + // tarantool.Request interface doesn't have Context() + getTests := []struct { + req *GetRequest + }{ + {req: NewErrorMarshalingEnabledGetRequest()}, + {req: NewSQLDefaultEngineGetRequest()}, + {req: NewSQLDeferForeignKeysGetRequest()}, + {req: NewSQLFullColumnNamesGetRequest()}, + {req: NewSQLFullMetadataGetRequest()}, + {req: NewSQLParserDebugGetRequest()}, + {req: NewSQLRecursiveTriggersGetRequest()}, + {req: NewSQLReverseUnorderedSelectsGetRequest()}, + {req: NewSQLSelectDebugGetRequest()}, + {req: NewSQLVDBEDebugGetRequest()}, + {req: NewSessionSettingsGetRequest()}, + } + + for _, test := range getTests { + var ctx context.Context + require.Equal(t, ctx, test.req.Context(ctx).Ctx()) + } + + setTests := []struct { + req *SetRequest + }{ + {req: NewErrorMarshalingEnabledSetRequest(false)}, + {req: NewSQLDefaultEngineSetRequest("memtx")}, + {req: NewSQLDeferForeignKeysSetRequest(false)}, + {req: NewSQLFullColumnNamesSetRequest(false)}, + {req: NewSQLFullMetadataSetRequest(false)}, + {req: NewSQLParserDebugSetRequest(false)}, + {req: NewSQLRecursiveTriggersSetRequest(false)}, + {req: NewSQLReverseUnorderedSelectsSetRequest(false)}, + {req: NewSQLSelectDebugSetRequest(false)}, + {req: NewSQLVDBEDebugSetRequest(false)}, + } + + for _, test := range setTests { + var ctx context.Context + require.Equal(t, ctx, test.req.Context(ctx).Ctx()) + } +} diff --git a/settings/tarantool_test.go b/settings/tarantool_test.go new file mode 100644 index 000000000..d32767116 --- /dev/null +++ b/settings/tarantool_test.go @@ -0,0 +1,656 @@ +package settings_test + +import ( + "log" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/settings" + "github.com/tarantool/go-tarantool/test_helpers" +) + +// There is no way to skip tests in testing.M, +// so we use this variable to pass info +// to each testing.T that it should skip. +var isSettingsSupported = false + +var server = "127.0.0.1:3013" +var opts = tarantool.Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +func skipIfSettingsUnsupported(t *testing.T) { + t.Helper() + + if isSettingsSupported == false { + t.Skip("Skipping test for Tarantool without session settings support") + } +} + +func skipIfErrorMarshalingEnabledSettingUnsupported(t *testing.T) { + t.Helper() + + test_helpers.SkipIfFeatureUnsupported(t, "error_marshaling_enabled session setting", 2, 4, 1) + test_helpers.SkipIfFeatureDropped(t, "error_marshaling_enabled session setting", 2, 10, 0) +} + +func skipIfSQLDeferForeignKeysSettingUnsupported(t *testing.T) { + t.Helper() + + test_helpers.SkipIfFeatureUnsupported(t, "sql_defer_foreign_keys session setting", 2, 3, 1) + test_helpers.SkipIfFeatureDropped(t, "sql_defer_foreign_keys session setting", 2, 10, 5) +} + +func TestErrorMarshalingEnabledSetting(t *testing.T) { + skipIfErrorMarshalingEnabledSettingUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Disable receiving box.error as MP_EXT 3. + resp, err = conn.Do(NewErrorMarshalingEnabledSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewErrorMarshalingEnabledGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", false}}, resp.Data) + + // Get a box.Error value. + resp, err = conn.Eval("return box.error.new(box.error.UNKNOWN)", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.IsType(t, "string", resp.Data[0]) + + // Enable receiving box.error as MP_EXT 3. + resp, err = conn.Do(NewErrorMarshalingEnabledSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewErrorMarshalingEnabledGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", true}}, resp.Data) + + // Get a box.Error value. + resp, err = conn.Eval("return box.error.new(box.error.UNKNOWN)", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + _, ok := toBoxError(resp.Data[0]) + require.True(t, ok) +} + +func TestSQLDefaultEngineSetting(t *testing.T) { + // https://github.com/tarantool/tarantool/blob/680990a082374e4790539215f69d9e9ee39c3307/test/sql/engine.test.lua + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Set default SQL "CREATE TABLE" engine to "vinyl". + resp, err = conn.Do(NewSQLDefaultEngineSetRequest("vinyl")).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.EqualValues(t, []interface{}{[]interface{}{"sql_default_engine", "vinyl"}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLDefaultEngineGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "vinyl"}}, resp.Data) + + // Create a space with "CREATE TABLE". + resp, err = conn.Execute("CREATE TABLE t1_vinyl(a INT PRIMARY KEY, b INT, c INT);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Check new space engine. + resp, err = conn.Eval("return box.space['T1_VINYL'].engine", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, "vinyl", resp.Data[0]) + + // Set default SQL "CREATE TABLE" engine to "memtx". + resp, err = conn.Do(NewSQLDefaultEngineSetRequest("memtx")).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLDefaultEngineGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, resp.Data) + + // Create a space with "CREATE TABLE". + resp, err = conn.Execute("CREATE TABLE t2_memtx(a INT PRIMARY KEY, b INT, c INT);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Check new space engine. + resp, err = conn.Eval("return box.space['T2_MEMTX'].engine", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, "memtx", resp.Data[0]) +} + +func TestSQLDeferForeignKeysSetting(t *testing.T) { + // https://github.com/tarantool/tarantool/blob/eafadc13425f14446d7aaa49dea67dfc1d5f45e9/test/sql/transitive-transactions.result + skipIfSQLDeferForeignKeysSettingUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Create a parent space. + resp, err = conn.Execute("CREATE TABLE parent(id INT PRIMARY KEY, y INT UNIQUE);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Create a space with reference to the parent space. + resp, err = conn.Execute("CREATE TABLE child(id INT PRIMARY KEY, x INT REFERENCES parent(y));", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + deferEval := ` + box.begin() + local _, err = box.execute('INSERT INTO child VALUES (2, 2);') + if err ~= nil then + box.rollback() + error(err) + end + box.execute('INSERT INTO parent VALUES (2, 2);') + box.commit() + return true + ` + + // Disable foreign key constraint checks before commit. + resp, err = conn.Do(NewSQLDeferForeignKeysSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLDeferForeignKeysGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", false}}, resp.Data) + + // Evaluate a scenario when foreign key not exists + // on INSERT, but exists on commit. + _, err = conn.Eval(deferEval, []interface{}{}) + require.NotNil(t, err) + require.ErrorContains(t, err, "Failed to execute SQL statement: FOREIGN KEY constraint failed") + + resp, err = conn.Do(NewSQLDeferForeignKeysSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLDeferForeignKeysGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", true}}, resp.Data) + + // Evaluate a scenario when foreign key not exists + // on INSERT, but exists on commit. + resp, err = conn.Eval(deferEval, []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, true, resp.Data[0]) +} + +func TestSQLFullColumnNamesSetting(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Create a space. + resp, err = conn.Execute("CREATE TABLE fkname(id INT PRIMARY KEY, x INT);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Fill it with some data. + resp, err = conn.Execute("INSERT INTO fkname VALUES (1, 1);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Disable displaying full column names in metadata. + resp, err = conn.Do(NewSQLFullColumnNamesSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLFullColumnNamesGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", false}}, resp.Data) + + // Get a data with short column names in metadata. + resp, err = conn.Execute("SELECT x FROM fkname WHERE id = 1;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, "X", resp.MetaData[0].FieldName) + + // Enable displaying full column names in metadata. + resp, err = conn.Do(NewSQLFullColumnNamesSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLFullColumnNamesGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, resp.Data) + + // Get a data with full column names in metadata. + resp, err = conn.Execute("SELECT x FROM fkname WHERE id = 1;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, "FKNAME.X", resp.MetaData[0].FieldName) +} + +func TestSQLFullMetadataSetting(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Create a space. + resp, err = conn.Execute("CREATE TABLE fmt(id INT PRIMARY KEY, x INT);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Fill it with some data. + resp, err = conn.Execute("INSERT INTO fmt VALUES (1, 1);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Disable displaying additional fields in metadata. + resp, err = conn.Do(NewSQLFullMetadataSetRequest(false)).Get() + require.Nil(t, err) + require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLFullMetadataGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", false}}, resp.Data) + + // Get a data without additional fields in metadata. + resp, err = conn.Execute("SELECT x FROM fmt WHERE id = 1;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, "", resp.MetaData[0].FieldSpan) + + // Enable displaying full column names in metadata. + resp, err = conn.Do(NewSQLFullMetadataSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLFullMetadataGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", true}}, resp.Data) + + // Get a data with additional fields in metadata. + resp, err = conn.Execute("SELECT x FROM fmt WHERE id = 1;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, "x", resp.MetaData[0].FieldSpan) +} + +func TestSQLParserDebugSetting(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Disable parser debug mode. + resp, err = conn.Do(NewSQLParserDebugSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLParserDebugGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", false}}, resp.Data) + + // Enable parser debug mode. + resp, err = conn.Do(NewSQLParserDebugSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLParserDebugGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", true}}, resp.Data) + + // To test real effect we need a Tarantool instance built with + // `-DCMAKE_BUILD_TYPE=Debug`. +} + +func TestSQLRecursiveTriggersSetting(t *testing.T) { + // https://github.com/tarantool/tarantool/blob/d11fb3061e15faf4e0eb5375fb8056b4e64348ae/test/sql-tap/triggerC.test.lua + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Create a space. + resp, err = conn.Execute("CREATE TABLE rec(id INTEGER PRIMARY KEY, a INT, b INT);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Fill it with some data. + resp, err = conn.Execute("INSERT INTO rec VALUES(1, 1, 2);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Create a recursive trigger (with infinite depth). + resp, err = conn.Execute(` + CREATE TRIGGER tr12 AFTER UPDATE ON rec FOR EACH ROW BEGIN + UPDATE rec SET a=new.a+1, b=new.b+1; + END;`, []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Enable SQL recursive triggers. + resp, err = conn.Do(NewSQLRecursiveTriggersSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLRecursiveTriggersGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", true}}, resp.Data) + + // Trigger the recursion. + _, err = conn.Execute("UPDATE rec SET a=a+1, b=b+1;", []interface{}{}) + require.NotNil(t, err) + require.ErrorContains(t, err, "Failed to execute SQL statement: too many levels of trigger recursion") + + // Disable SQL recursive triggers. + resp, err = conn.Do(NewSQLRecursiveTriggersSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLRecursiveTriggersGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", false}}, resp.Data) + + // Trigger the recursion. + resp, err = conn.Execute("UPDATE rec SET a=a+1, b=b+1;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) +} + +func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Create a space. + resp, err = conn.Execute("CREATE TABLE data(id STRING PRIMARY KEY);", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Fill it with some data. + resp, err = conn.Execute("INSERT INTO data VALUES('1');", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + resp, err = conn.Execute("INSERT INTO data VALUES('2');", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + + // Disable reverse order in unordered selects. + resp, err = conn.Do(NewSQLReverseUnorderedSelectsSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLReverseUnorderedSelectsGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", false}}, resp.Data) + + // Select multiple records. + resp, err = conn.Execute("SELECT * FROM data;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.EqualValues(t, []interface{}{"1"}, resp.Data[0]) + require.EqualValues(t, []interface{}{"2"}, resp.Data[1]) + + // Enable reverse order in unordered selects. + resp, err = conn.Do(NewSQLReverseUnorderedSelectsSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLReverseUnorderedSelectsGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", true}}, resp.Data) + + // Select multiple records. + resp, err = conn.Execute("SELECT * FROM data;", []interface{}{}) + require.Nil(t, err) + require.NotNil(t, resp) + require.EqualValues(t, []interface{}{"2"}, resp.Data[0]) + require.EqualValues(t, []interface{}{"1"}, resp.Data[1]) +} + +func TestSQLSelectDebugSetting(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Disable select debug mode. + resp, err = conn.Do(NewSQLSelectDebugSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLSelectDebugGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", false}}, resp.Data) + + // Enable select debug mode. + resp, err = conn.Do(NewSQLSelectDebugSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLSelectDebugGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", true}}, resp.Data) + + // To test real effect we need a Tarantool instance built with + // `-DCMAKE_BUILD_TYPE=Debug`. +} + +func TestSQLVDBEDebugSetting(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Disable VDBE debug mode. + resp, err = conn.Do(NewSQLVDBEDebugSetRequest(false)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", false}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLVDBEDebugGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", false}}, resp.Data) + + // Enable VDBE debug mode. + resp, err = conn.Do(NewSQLVDBEDebugSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", true}}, resp.Data) + + // Fetch current setting value. + resp, err = conn.Do(NewSQLVDBEDebugGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", true}}, resp.Data) + + // To test real effect we need a Tarantool instance built with + // `-DCMAKE_BUILD_TYPE=Debug`. +} + +func TestSessionSettings(t *testing.T) { + skipIfSettingsUnsupported(t) + + var resp *tarantool.Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Set some settings values. + resp, err = conn.Do(NewSQLDefaultEngineSetRequest("memtx")).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, resp.Data) + + resp, err = conn.Do(NewSQLFullColumnNamesSetRequest(true)).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, resp.Data) + + // Fetch current settings values. + resp, err = conn.Do(NewSessionSettingsGetRequest()).Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Subset(t, resp.Data, + []interface{}{ + []interface{}{"sql_default_engine", "memtx"}, + []interface{}{"sql_full_column_names", true}, + }) +} + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + isLess, err := test_helpers.IsTarantoolVersionLess(2, 3, 1) + if err != nil { + log.Fatalf("Failed to extract tarantool version: %s", err) + } + + if isLess { + log.Println("Skipping session settings tests...") + isSettingsSupported = false + return m.Run() + } + + isSettingsSupported = true + + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "testdata/config.lua", + Listen: server, + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + defer test_helpers.StopTarantoolWithCleanup(inst) + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/settings/testdata/config.lua b/settings/testdata/config.lua new file mode 100644 index 000000000..7f6af1db2 --- /dev/null +++ b/settings/testdata/config.lua @@ -0,0 +1,15 @@ +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +box.cfg{ + work_dir = os.getenv("TEST_TNT_WORK_DIR"), +} + +box.schema.user.create('test', { password = 'test' , if_not_exists = true }) +box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) +box.schema.user.grant('test', 'create,read,write,drop,alter', 'space', nil, { if_not_exists = true }) +box.schema.user.grant('test', 'create', 'sequence', nil, { if_not_exists = true }) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} From d72a10c8ed6e4bd41e4caecc637f89b7dfcf45d7 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 21 Dec 2022 17:39:59 +0300 Subject: [PATCH 375/605] bugfix: deadlock poolWatcher.Unregister() We need to lock mutexes everywhere in the same order to avoid the deadlock: 1. Container with watchers. 2. Watcher. --- connection_pool/watcher.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/connection_pool/watcher.go b/connection_pool/watcher.go index 6a1fde8f4..2876f90bc 100644 --- a/connection_pool/watcher.go +++ b/connection_pool/watcher.go @@ -26,22 +26,24 @@ func (c *watcherContainer) add(watcher *poolWatcher) { } // remove removes a watcher from the container. -func (c *watcherContainer) remove(watcher *poolWatcher) { +func (c *watcherContainer) remove(watcher *poolWatcher) bool { c.mutex.Lock() defer c.mutex.Unlock() if watcher == c.head { c.head = watcher.next + return true } else { cur := c.head for cur.next != nil { if cur.next == watcher { cur.next = watcher.next - break + return true } cur = cur.next } } + return false } // foreach iterates over the container to the end or until the call returns @@ -83,15 +85,13 @@ type poolWatcher struct { // Unregister unregisters the pool watcher. func (w *poolWatcher) Unregister() { - w.mutex.Lock() - defer w.mutex.Unlock() - - if !w.unregistered { - w.container.remove(w) + if !w.unregistered && w.container.remove(w) { + w.mutex.Lock() w.unregistered = true for _, watcher := range w.watchers { watcher.Unregister() } + w.mutex.Unlock() } } From fb93f9ccee9220c728bfc16dd733b038e66a2885 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 21 Dec 2022 17:40:34 +0300 Subject: [PATCH 376/605] api: add pap-sha256 auth support The authentication method works only with Tarantool EE [1]. 1. https://github.com/tarantool/enterprise_doc/issues/206 Closes #243 --- .github/workflows/testing.yml | 13 +++-- CHANGELOG.md | 1 + auth.go | 38 +++++++++++++++ auth_test.go | 28 +++++++++++ config.lua | 7 +++ connection.go | 92 +++++++++++++++-------------------- const.go | 1 + protocol.go | 2 + request.go | 25 ++++++++-- response.go | 15 ++++++ ssl_test.go | 55 ++++++++++++++++++--- tarantool_test.go | 32 ++++++++++++ test_helpers/main.go | 5 ++ 13 files changed, 244 insertions(+), 70 deletions(-) create mode 100644 auth_test.go diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index d06549566..698f23f87 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -116,13 +116,16 @@ jobs: fail-fast: false matrix: sdk-version: - - '1.10.11-0-gf0b0e7ecf-r470' - - '2.8.3-21-g7d35cd2be-r470' + - 'bundle-1.10.11-0-gf0b0e7ecf-r470' coveralls: [false] fuzzing: [false] ssl: [false] include: - - sdk-version: '2.10.0-1-gfa775b383-r486-linux-x86_64' + - sdk-version: 'bundle-2.10.0-1-gfa775b383-r486-linux-x86_64' + coveralls: false + ssl: true + - sdk-path: 'dev/linux/x86_64/master/' + sdk-version: 'sdk-gc64-2.11.0-entrypoint-113-g803baaffe-r529.linux.x86_64' coveralls: true ssl: true @@ -141,8 +144,8 @@ jobs: - name: Setup Tarantool ${{ matrix.sdk-version }} run: | - ARCHIVE_NAME=tarantool-enterprise-bundle-${{ matrix.sdk-version }}.tar.gz - curl -O -L https://${{ secrets.SDK_DOWNLOAD_TOKEN }}@download.tarantool.io/enterprise/${ARCHIVE_NAME} + ARCHIVE_NAME=tarantool-enterprise-${{ matrix.sdk-version }}.tar.gz + curl -O -L https://${{ secrets.SDK_DOWNLOAD_TOKEN }}@download.tarantool.io/enterprise/${{ matrix.sdk-path }}${ARCHIVE_NAME} tar -xzf ${ARCHIVE_NAME} rm -f ${ARCHIVE_NAME} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7930b34d1..ca81649e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Error type support in MessagePack (#209) - Event subscription support (#119) - Session settings support (#215) +- pap-sha256 authorization method support (Tarantool EE feature) (#243) ### Changed diff --git a/auth.go b/auth.go index 60c219d69..2e5ddc4c4 100644 --- a/auth.go +++ b/auth.go @@ -3,8 +3,46 @@ package tarantool import ( "crypto/sha1" "encoding/base64" + "fmt" ) +const ( + chapSha1 = "chap-sha1" + papSha256 = "pap-sha256" +) + +// Auth is used as a parameter to set up an authentication method. +type Auth int + +const ( + // AutoAuth does not force any authentication method. A method will be + // selected automatically (a value from IPROTO_ID response or + // ChapSha1Auth). + AutoAuth Auth = iota + // ChapSha1Auth forces chap-sha1 authentication method. The method is + // available both in the Tarantool Community Edition (CE) and the + // Tarantool Enterprise Edition (EE) + ChapSha1Auth + // PapSha256Auth forces pap-sha256 authentication method. The method is + // available only for the Tarantool Enterprise Edition (EE) with + // SSL transport. + PapSha256Auth +) + +// String returns a string representation of an authentication method. +func (a Auth) String() string { + switch a { + case AutoAuth: + return "auto" + case ChapSha1Auth: + return chapSha1 + case PapSha256Auth: + return papSha256 + default: + return fmt.Sprintf("unknown auth type (code %d)", a) + } +} + func scramble(encodedSalt, pass string) (scramble []byte, err error) { /* ================================================================== According to: http://tarantool.org/doc/dev_guide/box-protocol.html diff --git a/auth_test.go b/auth_test.go new file mode 100644 index 000000000..6964f2552 --- /dev/null +++ b/auth_test.go @@ -0,0 +1,28 @@ +package tarantool_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + . "github.com/tarantool/go-tarantool" +) + +func TestAuth_String(t *testing.T) { + unknownId := int(PapSha256Auth) + 1 + tests := []struct { + auth Auth + expected string + }{ + {AutoAuth, "auto"}, + {ChapSha1Auth, "chap-sha1"}, + {PapSha256Auth, "pap-sha256"}, + {Auth(unknownId), fmt.Sprintf("unknown auth type (code %d)", unknownId)}, + } + + for _, tc := range tests { + t.Run(tc.expected, func(t *testing.T) { + assert.Equal(t, tc.auth.String(), tc.expected) + }) + } +} diff --git a/config.lua b/config.lua index c8a853ff4..eadfb3825 100644 --- a/config.lua +++ b/config.lua @@ -1,6 +1,12 @@ -- Do not set listen for now so connector won't be -- able to send requests until everything is configured. +local auth_type = os.getenv("TEST_TNT_AUTH_TYPE") +if auth_type == "auto" then + auth_type = nil +end + box.cfg{ + auth_type = auth_type, work_dir = os.getenv("TEST_TNT_WORK_DIR"), memtx_use_mvcc_engine = os.getenv("TEST_TNT_MEMTX_USE_MVCC_ENGINE") == 'true' or nil, } @@ -267,5 +273,6 @@ box.space.test:truncate() -- Set listen only when every other thing is configured. box.cfg{ + auth_type = auth_type, listen = os.getenv("TEST_TNT_LISTEN"), } diff --git a/connection.go b/connection.go index b657dc2ae..c52d2e0a7 100644 --- a/connection.go +++ b/connection.go @@ -226,6 +226,8 @@ type Greeting struct { // Opts is a way to configure Connection type Opts struct { + // Auth is an authentication method. + Auth Auth // Timeout for response to a particular request. The timeout is reset when // push messages are received. If Timeout is zero, any request can be // blocked infinitely. @@ -546,19 +548,40 @@ func (conn *Connection) dial() (err error) { // Auth. if opts.User != "" { - scr, err := scramble(conn.Greeting.auth, opts.Pass) - if err != nil { - err = errors.New("auth: scrambling failure " + err.Error()) + auth := opts.Auth + if opts.Auth == AutoAuth { + if conn.serverProtocolInfo.Auth != AutoAuth { + auth = conn.serverProtocolInfo.Auth + } else { + auth = ChapSha1Auth + } + } + + var req Request + if auth == ChapSha1Auth { + salt := conn.Greeting.auth + req, err = newChapSha1AuthRequest(conn.opts.User, salt, opts.Pass) + if err != nil { + return fmt.Errorf("auth: %w", err) + } + } else if auth == PapSha256Auth { + if opts.Transport != connTransportSsl { + return errors.New("auth: forbidden to use " + auth.String() + + " unless SSL is enabled for the connection") + } + req = newPapSha256AuthRequest(conn.opts.User, opts.Pass) + } else { connection.Close() - return err + return errors.New("auth: " + auth.String()) } - if err = conn.writeAuthRequest(w, scr); err != nil { + + if err = conn.writeRequest(w, req); err != nil { connection.Close() - return err + return fmt.Errorf("auth: %w", err) } - if err = conn.readAuthResponse(r); err != nil { + if _, err = conn.readResponse(r); err != nil { connection.Close() - return err + return fmt.Errorf("auth: %w", err) } } @@ -662,28 +685,6 @@ func (conn *Connection) writeRequest(w *bufio.Writer, req Request) error { return err } -func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) error { - req := newAuthRequest(conn.opts.User, string(scramble)) - - err := conn.writeRequest(w, req) - if err != nil { - return fmt.Errorf("auth: %w", err) - } - - 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 { @@ -707,24 +708,6 @@ func (conn *Connection) readResponse(r io.Reader) (Response, error) { return resp, nil } -func (conn *Connection) readAuthResponse(r io.Reader) error { - _, err := conn.readResponse(r) - if err != nil { - return fmt.Errorf("auth: %w", err) - } - - 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 { @@ -1625,19 +1608,20 @@ func checkProtocolInfo(expected ProtocolInfo, actual ProtocolInfo) error { func (conn *Connection) identify(w *bufio.Writer, r *bufio.Reader) error { var ok bool - werr := conn.writeIdRequest(w, clientProtocolInfo) + req := NewIdRequest(clientProtocolInfo) + werr := conn.writeRequest(w, req) if werr != nil { - return werr + return fmt.Errorf("identify: %w", werr) } - resp, rerr := conn.readIdResponse(r) + resp, rerr := conn.readResponse(r) if rerr != nil { if resp.Code == ErrUnknownRequestType { // IPROTO_ID requests are not supported by server. return nil } - return rerr + return fmt.Errorf("identify: %w", rerr) } if len(resp.Data) == 0 { @@ -1664,5 +1648,7 @@ func (conn *Connection) ServerProtocolInfo() ProtocolInfo { // supported by Go connection client. // Since 1.10.0 func (conn *Connection) ClientProtocolInfo() ProtocolInfo { - return clientProtocolInfo.Clone() + info := clientProtocolInfo.Clone() + info.Auth = conn.opts.Auth + return info } diff --git a/const.go b/const.go index acd6a4861..ead151878 100644 --- a/const.go +++ b/const.go @@ -51,6 +51,7 @@ const ( KeyEvent = 0x57 KeyEventData = 0x58 KeyTxnIsolation = 0x59 + KeyAuthType = 0x5b KeyFieldName = 0x00 KeyFieldType = 0x01 diff --git a/protocol.go b/protocol.go index ae6142b4d..06506ee5a 100644 --- a/protocol.go +++ b/protocol.go @@ -13,6 +13,8 @@ type ProtocolFeature uint64 // ProtocolInfo type aggregates Tarantool protocol version and features info. type ProtocolInfo struct { + // Auth is an authentication method. + Auth Auth // Version is the supported protocol version. Version ProtocolVersion // Features are supported protocol features. diff --git a/request.go b/request.go index 66eb4be41..1b135eaa6 100644 --- a/request.go +++ b/request.go @@ -3,6 +3,7 @@ package tarantool import ( "context" "errors" + "fmt" "reflect" "strings" "sync" @@ -591,14 +592,30 @@ func (req *spaceIndexRequest) setIndex(index interface{}) { type authRequest struct { baseRequest - user, scramble string + auth Auth + user, pass string } -func newAuthRequest(user, scramble string) *authRequest { +func newChapSha1AuthRequest(user, salt, password string) (*authRequest, error) { + scr, err := scramble(salt, password) + if err != nil { + return nil, fmt.Errorf("scrambling failure: %w", err) + } + + req := new(authRequest) + req.requestCode = AuthRequestCode + req.auth = ChapSha1Auth + req.user = user + req.pass = string(scr) + return req, nil +} + +func newPapSha256AuthRequest(user, password string) *authRequest { req := new(authRequest) req.requestCode = AuthRequestCode + req.auth = PapSha256Auth req.user = user - req.scramble = scramble + req.pass = password return req } @@ -606,7 +623,7 @@ func newAuthRequest(user, scramble string) *authRequest { func (req *authRequest) Body(res SchemaResolver, enc *encoder) error { return enc.Encode(map[uint32]interface{}{ KeyUserName: req.user, - KeyTuple: []interface{}{string("chap-sha1"), string(req.scramble)}, + KeyTuple: []interface{}{req.auth.String(), req.pass}, }) } diff --git a/response.go b/response.go index 6c3f69c99..dc747c852 100644 --- a/response.go +++ b/response.go @@ -213,6 +213,21 @@ func (resp *Response) decodeBody() (err error) { } serverProtocolInfo.Features[i] = feature } + case KeyAuthType: + var auth string + if auth, err = d.DecodeString(); err != nil { + return err + } + found := false + for _, a := range [...]Auth{ChapSha1Auth, PapSha256Auth} { + if auth == a.String() { + serverProtocolInfo.Auth = a + found = true + } + } + if !found { + return fmt.Errorf("unknown auth type %s", auth) + } default: if err = d.Skip(); err != nil { return err diff --git a/ssl_test.go b/ssl_test.go index 769508284..ca0773b58 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -94,7 +94,7 @@ func createClientServerSslOk(t testing.TB, serverOpts, return l, c, msgs, errs } -func serverTnt(serverOpts, clientOpts SslOpts) (test_helpers.TarantoolInstance, error) { +func serverTnt(serverOpts, clientOpts SslOpts, auth Auth) (test_helpers.TarantoolInstance, error) { listen := tntHost + "?transport=ssl&" key := serverOpts.KeyFile @@ -120,6 +120,7 @@ func serverTnt(serverOpts, clientOpts SslOpts) (test_helpers.TarantoolInstance, listen = listen[:len(listen)-1] return test_helpers.StartTarantool(test_helpers.StartOpts{ + Auth: auth, InitScript: "config.lua", Listen: listen, SslCertsDir: "testdata", @@ -170,7 +171,7 @@ func assertConnectionSslOk(t testing.TB, serverOpts, clientOpts SslOpts) { func assertConnectionTntFail(t testing.TB, serverOpts, clientOpts SslOpts) { t.Helper() - inst, err := serverTnt(serverOpts, clientOpts) + inst, err := serverTnt(serverOpts, clientOpts, AutoAuth) serverTntStop(inst) if err == nil { @@ -181,7 +182,7 @@ func assertConnectionTntFail(t testing.TB, serverOpts, clientOpts SslOpts) { func assertConnectionTntOk(t testing.TB, serverOpts, clientOpts SslOpts) { t.Helper() - inst, err := serverTnt(serverOpts, clientOpts) + inst, err := serverTnt(serverOpts, clientOpts, AutoAuth) serverTntStop(inst) if err != nil { @@ -432,12 +433,14 @@ var tests = []test{ }, } -func TestSslOpts(t *testing.T) { +func isTestTntSsl() bool { testTntSsl, exists := os.LookupEnv("TEST_TNT_SSL") - isTntSsl := false - if exists && (testTntSsl == "1" || strings.ToUpper(testTntSsl) == "TRUE") { - isTntSsl = true - } + return exists && + (testTntSsl == "1" || strings.ToUpper(testTntSsl) == "TRUE") +} + +func TestSslOpts(t *testing.T) { + isTntSsl := isTestTntSsl() for _, test := range tests { if test.ok { @@ -463,3 +466,39 @@ func TestSslOpts(t *testing.T) { } } } + +func TestOpts_PapSha256Auth(t *testing.T) { + isTntSsl := isTestTntSsl() + if !isTntSsl { + t.Skip("TEST_TNT_SSL is not set") + } + + isLess, err := test_helpers.IsTarantoolVersionLess(2, 11, 0) + if err != nil { + t.Fatalf("Could not check Tarantool version.") + } + if isLess { + t.Skip("Skipping test for Tarantoo without pap-sha256 support") + } + + sslOpts := SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + } + inst, err := serverTnt(sslOpts, sslOpts, PapSha256Auth) + defer serverTntStop(inst) + if err != nil { + t.Errorf("An unexpected server error: %s", err) + } + + clientOpts := opts + clientOpts.Transport = "ssl" + clientOpts.Ssl = sslOpts + clientOpts.Auth = PapSha256Auth + conn := test_helpers.ConnectWithValidation(t, tntHost, clientOpts) + conn.Close() + + clientOpts.Auth = AutoAuth + conn = test_helpers.ConnectWithValidation(t, tntHost, clientOpts) + conn.Close() +} diff --git a/tarantool_test.go b/tarantool_test.go index 96bf02254..2c76071b3 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -726,6 +726,38 @@ func BenchmarkSQLSerial(b *testing.B) { } } +func TestOptsAuth_Default(t *testing.T) { + defaultOpts := opts + defaultOpts.Auth = AutoAuth + + conn := test_helpers.ConnectWithValidation(t, server, defaultOpts) + defer conn.Close() +} + +func TestOptsAuth_ChapSha1Auth(t *testing.T) { + chapSha1Opts := opts + chapSha1Opts.Auth = ChapSha1Auth + + conn := test_helpers.ConnectWithValidation(t, server, chapSha1Opts) + defer conn.Close() +} + +func TestOptsAuth_PapSha256AuthForbit(t *testing.T) { + papSha256Opts := opts + papSha256Opts.Auth = PapSha256Auth + + conn, err := Connect(server, papSha256Opts) + if err == nil { + t.Error("An error expected.") + conn.Close() + } + + if err.Error() != "auth: forbidden to use pap-sha256 unless "+ + "SSL is enabled for the connection" { + t.Errorf("An unexpected error: %s", err) + } +} + func TestFutureMultipleGetGetTyped(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() diff --git a/test_helpers/main.go b/test_helpers/main.go index f6e745617..f2a97bef9 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -27,6 +27,9 @@ import ( ) type StartOpts struct { + // Auth is an authentication method for a Tarantool instance. + Auth tarantool.Auth + // InitScript is a Lua script for tarantool to run on start. InitScript string @@ -223,6 +226,7 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { fmt.Sprintf("TEST_TNT_WORK_DIR=%s", startOpts.WorkDir), fmt.Sprintf("TEST_TNT_LISTEN=%s", startOpts.Listen), fmt.Sprintf("TEST_TNT_MEMTX_USE_MVCC_ENGINE=%t", startOpts.MemtxUseMvccEngine), + fmt.Sprintf("TEST_TNT_AUTH_TYPE=%s", startOpts.Auth), ) // Copy SSL certificates. @@ -248,6 +252,7 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { time.Sleep(startOpts.WaitStart) opts := tarantool.Opts{ + Auth: startOpts.Auth, Timeout: 500 * time.Millisecond, User: startOpts.User, Pass: startOpts.Pass, From 7a593cc7ac678b62bc9fc35434cac58750908128 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 27 Dec 2022 13:36:31 +0300 Subject: [PATCH 377/605] ci: improve golangci-lint output The patch adds a second golangci-lint run that prints errors in human-readable format [1]. 1. https://github.com/golangci/golangci-lint-action/issues/119 Closes #231 --- .github/workflows/check.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index d6a72e55b..2297b0b3c 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -37,7 +37,8 @@ jobs: - uses: actions/checkout@v2 - name: golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v3 + continue-on-error: true with: # The suppression of the rule `errcheck` may be removed after adding # errors check in all methods calling EncodeXxx inside. @@ -47,8 +48,17 @@ jobs: # The `//nolint` workaround was not the acceptable way of warnings suppression, # cause those comments get rendered in documentation by godoc. # See https://github.com/tarantool/go-tarantool/pull/160#discussion_r858608221 + # + # The first run is for GitHub Actions error format. args: -E goimports -D errcheck + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # The second run is for human-readable error format with a file name + # and a line number. + args: --out-${NO_FUTURE}format colored-line-number -E goimports -D errcheck + codespell: runs-on: ubuntu-latest if: | From 98eb6615a12719be701f0999bf54b30b8541b1c8 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 27 Dec 2022 11:09:10 +0300 Subject: [PATCH 378/605] test: add Ctx() test for requests Part of #244 --- request_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++ tarantool_test.go | 18 ------------ 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/request_test.go b/request_test.go index a078f6514..70aa29f45 100644 --- a/request_test.go +++ b/request_test.go @@ -2,6 +2,7 @@ package tarantool_test import ( "bytes" + "context" "errors" "testing" "time" @@ -30,6 +31,11 @@ const validTimeout = 500 * time.Millisecond var validStmt *Prepared = &Prepared{StatementID: 1, Conn: &Connection{}} +var validProtocolInfo ProtocolInfo = ProtocolInfo{ + Version: ProtocolVersion(3), + Features: []ProtocolFeature{StreamsFeature}, +} + type ValidSchemeResolver struct { } @@ -184,6 +190,7 @@ func TestRequestsCodes(t *testing.T) { {req: NewBeginRequest(), code: BeginRequestCode}, {req: NewCommitRequest(), code: CommitRequestCode}, {req: NewRollbackRequest(), code: RollbackRequestCode}, + {req: NewIdRequest(validProtocolInfo), code: IdRequestCode}, {req: NewBroadcastRequest(validKey), code: CallRequestCode}, } @@ -216,6 +223,7 @@ func TestRequestsAsync(t *testing.T) { {req: NewBeginRequest(), async: false}, {req: NewCommitRequest(), async: false}, {req: NewRollbackRequest(), async: false}, + {req: NewIdRequest(validProtocolInfo), async: false}, {req: NewBroadcastRequest(validKey), async: false}, } @@ -226,6 +234,73 @@ func TestRequestsAsync(t *testing.T) { } } +func TestRequestsCtx_default(t *testing.T) { + tests := []struct { + req Request + expected context.Context + }{ + {req: NewSelectRequest(validSpace), expected: nil}, + {req: NewUpdateRequest(validSpace), expected: nil}, + {req: NewUpsertRequest(validSpace), expected: nil}, + {req: NewInsertRequest(validSpace), expected: nil}, + {req: NewReplaceRequest(validSpace), expected: nil}, + {req: NewDeleteRequest(validSpace), expected: nil}, + {req: NewCall16Request(validExpr), expected: nil}, + {req: NewCall17Request(validExpr), expected: nil}, + {req: NewEvalRequest(validExpr), expected: nil}, + {req: NewExecuteRequest(validExpr), expected: nil}, + {req: NewPingRequest(), expected: nil}, + {req: NewPrepareRequest(validExpr), expected: nil}, + {req: NewUnprepareRequest(validStmt), expected: nil}, + {req: NewExecutePreparedRequest(validStmt), expected: nil}, + {req: NewBeginRequest(), expected: nil}, + {req: NewCommitRequest(), expected: nil}, + {req: NewRollbackRequest(), expected: nil}, + {req: NewIdRequest(validProtocolInfo), expected: nil}, + {req: NewBroadcastRequest(validKey), expected: nil}, + } + + for _, test := range tests { + if ctx := test.req.Ctx(); ctx != test.expected { + t.Errorf("An invalid ctx %t, expected %t", ctx, test.expected) + } + } +} + +func TestRequestsCtx_setter(t *testing.T) { + ctx := context.Background() + tests := []struct { + req Request + expected context.Context + }{ + {req: NewSelectRequest(validSpace).Context(ctx), expected: ctx}, + {req: NewUpdateRequest(validSpace).Context(ctx), expected: ctx}, + {req: NewUpsertRequest(validSpace).Context(ctx), expected: ctx}, + {req: NewInsertRequest(validSpace).Context(ctx), expected: ctx}, + {req: NewReplaceRequest(validSpace).Context(ctx), expected: ctx}, + {req: NewDeleteRequest(validSpace).Context(ctx), expected: ctx}, + {req: NewCall16Request(validExpr).Context(ctx), expected: ctx}, + {req: NewCall17Request(validExpr).Context(ctx), expected: ctx}, + {req: NewEvalRequest(validExpr).Context(ctx), expected: ctx}, + {req: NewExecuteRequest(validExpr).Context(ctx), expected: ctx}, + {req: NewPingRequest().Context(ctx), expected: ctx}, + {req: NewPrepareRequest(validExpr).Context(ctx), expected: ctx}, + {req: NewUnprepareRequest(validStmt).Context(ctx), expected: ctx}, + {req: NewExecutePreparedRequest(validStmt).Context(ctx), expected: ctx}, + {req: NewBeginRequest().Context(ctx), expected: ctx}, + {req: NewCommitRequest().Context(ctx), expected: ctx}, + {req: NewRollbackRequest().Context(ctx), expected: ctx}, + {req: NewIdRequest(validProtocolInfo).Context(ctx), expected: ctx}, + {req: NewBroadcastRequest(validKey).Context(ctx), expected: ctx}, + } + + for _, test := range tests { + if ctx := test.req.Ctx(); ctx != test.expected { + t.Errorf("An invalid ctx %t, expected %t", ctx, test.expected) + } + } +} + func TestPingRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer diff --git a/tarantool_test.go b/tarantool_test.go index 2c76071b3..b874b4c43 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3005,24 +3005,6 @@ func TestClientIdRequestObjectWithPassedCanceledContext(t *testing.T) { require.Equal(t, err.Error(), "context is done") } -func TestClientIdRequestObjectWithContext(t *testing.T) { - var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) - defer conn.Close() - - ctx, cancel := context.WithCancel(context.Background()) - req := NewIdRequest(ProtocolInfo{ - Version: ProtocolVersion(1), - Features: []ProtocolFeature{StreamsFeature}, - }).Context(ctx) //nolint - fut := conn.Do(req) - cancel() - resp, err := fut.Get() - require.Nilf(t, resp, "Response is empty") - require.NotNilf(t, err, "Error is not empty") - require.Equal(t, err.Error(), "context is done") -} - func TestConnectionProtocolInfoUnsupported(t *testing.T) { test_helpers.SkipIfIdSupported(t) From e8abc171ae19cad9fc41142d4f5b12f79723f9e8 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 27 Dec 2022 12:44:55 +0300 Subject: [PATCH 379/605] code health: simplify context checks We don't need to over-optimize the work with requests with contexts that already done. It doesn't look like a use case. It is better to simplify the code. Part of #244 --- connection.go | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/connection.go b/connection.go index c52d2e0a7..e584d44d5 100644 --- a/connection.go +++ b/connection.go @@ -1045,22 +1045,18 @@ func (conn *Connection) newFuture(ctx context.Context) (fut *Future) { } // This method removes a future from the internal queue if the context -// is "done" before the response is come. Such select logic is inspired -// from this thread: https://groups.google.com/g/golang-dev/c/jX4oQEls3uk +// is "done" before the response is come. func (conn *Connection) contextWatchdog(fut *Future, ctx context.Context) { select { case <-fut.done: + case <-ctx.Done(): + } + + select { + case <-fut.done: + return default: - select { - case <-ctx.Done(): - conn.cancelFuture(fut, fmt.Errorf("context is done")) - default: - select { - case <-fut.done: - case <-ctx.Done(): - conn.cancelFuture(fut, fmt.Errorf("context is done")) - } - } + conn.cancelFuture(fut, fmt.Errorf("context is done")) } } @@ -1076,11 +1072,9 @@ func (conn *Connection) send(req Request, streamId uint64) *Future { return fut default: } - } - conn.putFuture(fut, req, streamId) - if req.Ctx() != nil { go conn.contextWatchdog(fut, req.Ctx()) } + conn.putFuture(fut, req, streamId) return fut } @@ -1293,15 +1287,6 @@ func (conn *Connection) Do(req Request) *Future { return fut } } - if req.Ctx() != nil { - select { - case <-req.Ctx().Done(): - fut := NewFuture() - fut.SetError(fmt.Errorf("context is done")) - return fut - default: - } - } return conn.send(req, ignoreStreamId) } From 41532b68760bb707533322f24b764916d0c5755c Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 27 Dec 2022 12:54:37 +0300 Subject: [PATCH 380/605] bugfix: flaky TestClientRequestObjectsWithContext The patch makes the test more deterministic. It helps to avoid canceling a request with an already received response. Closes #244 --- CHANGELOG.md | 2 ++ tarantool_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca81649e4..7521d3796 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Decimal package uses a test variable DecimalPrecision instead of a package-level variable decimalPrecision (#233) +- Flaky tests TestClientRequestObjectsWithContext and + TestClientIdRequestObjectWithContext (#244) ## [1.9.0] - 2022-11-02 diff --git a/tarantool_test.go b/tarantool_test.go index b874b4c43..a4e85041b 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2429,15 +2429,57 @@ func TestClientRequestObjectsWithPassedCanceledContext(t *testing.T) { } } +// waitCtxRequest waits for the WaitGroup in Body() call and returns +// the context from Ctx() call. The request helps us to make sure that +// the context's cancel() call is called before a response received. +type waitCtxRequest struct { + ctx context.Context + wg sync.WaitGroup +} + +func (req *waitCtxRequest) Code() int32 { + return NewPingRequest().Code() +} + +func (req *waitCtxRequest) Body(res SchemaResolver, enc *encoder) error { + req.wg.Wait() + return NewPingRequest().Body(res, enc) +} + +func (req *waitCtxRequest) Ctx() context.Context { + return req.ctx +} + +func (req *waitCtxRequest) Async() bool { + return NewPingRequest().Async() +} + func TestClientRequestObjectsWithContext(t *testing.T) { var err error conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() ctx, cancel := context.WithCancel(context.Background()) - req := NewPingRequest().Context(ctx) - fut := conn.Do(req) + req := &waitCtxRequest{ctx: ctx} + req.wg.Add(1) + + var futWg sync.WaitGroup + var fut *Future + + futWg.Add(1) + go func() { + defer futWg.Done() + fut = conn.Do(req) + }() + cancel() + req.wg.Done() + + futWg.Wait() + if fut == nil { + t.Fatalf("fut must be not nil") + } + resp, err := fut.Get() if resp != nil { t.Fatalf("response must be nil") From b0956f307e177134a247277ba014f5c06e997e01 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 27 Dec 2022 16:43:43 +0300 Subject: [PATCH 381/605] bugfix: flaky multi/TestDisconnectAll After the fix the test stops tarantool intances to avoid concurrent reconnects by a ConnectionMulti instance. Closes #234 --- CHANGELOG.md | 1 + multi/multi_test.go | 64 ++++++++++++++++++++------------------------- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7521d3796..4fc41089d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. package-level variable decimalPrecision (#233) - Flaky tests TestClientRequestObjectsWithContext and TestClientIdRequestObjectWithContext (#244) +- Flaky test multi/TestDisconnectAll (#234) ## [1.9.0] - 2022-11-02 diff --git a/multi/multi_test.go b/multi/multi_test.go index 3b395864c..ad4fd1962 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -31,6 +31,8 @@ var connOptsMulti = OptsMulti{ ClusterDiscoveryTime: 3 * time.Second, } +var instances []test_helpers.TarantoolInstance + func TestConnError_IncorrectParams(t *testing.T) { multiConn, err := Connect([]string{}, tarantool.Opts{}) if err == nil { @@ -118,36 +120,41 @@ func TestReconnect(t *testing.T) { } func TestDisconnectAll(t *testing.T) { - multiConn, _ := Connect([]string{server1, server2}, connOpts) + sleep := 100 * time.Millisecond + sleepCnt := int((time.Second / sleep) * 2) // Checkout time * 2. + + servers := []string{server1, server2} + multiConn, _ := Connect(servers, connOpts) if multiConn == nil { t.Errorf("conn is nil after Connect") return } - timer := time.NewTimer(300 * time.Millisecond) - <-timer.C - defer multiConn.Close() - conn, _ := multiConn.getConnectionFromPool(server1) - conn.Close() - conn, _ = multiConn.getConnectionFromPool(server2) - conn.Close() + for _, inst := range instances { + test_helpers.StopTarantoolWithCleanup(inst) + } + + for i := 0; i < sleepCnt && multiConn.ConnectedNow(); i++ { + time.Sleep(sleep) + } if multiConn.ConnectedNow() { t.Errorf("incorrect status after desconnect all") } - timer = time.NewTimer(100 * time.Millisecond) - <-timer.C - if !multiConn.ConnectedNow() { - t.Errorf("incorrect multiConn status after reconnecting") + for _, inst := range instances { + err := test_helpers.RestartTarantool(&inst) + if err != nil { + t.Fatalf("failed to restart Tarantool: %s", err) + } } - conn, _ = multiConn.getConnectionFromPool(server1) - if !conn.ConnectedNow() { - t.Errorf("incorrect server1 conn status after reconnecting") + + for i := 0; i < sleepCnt && !multiConn.ConnectedNow(); i++ { + time.Sleep(sleep) } - conn, _ = multiConn.getConnectionFromPool(server2) - if !conn.ConnectedNow() { - t.Errorf("incorrect server2 conn status after reconnecting") + + if !multiConn.ConnectedNow() { + t.Errorf("incorrect multiConn status after reconnecting") } } @@ -589,9 +596,9 @@ func runTestMain(m *testing.M) int { log.Fatalf("Could not check the Tarantool version") } - inst1, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + servers := []string{server1, server2} + instances, err = test_helpers.StartTarantoolInstances(servers, nil, test_helpers.StartOpts{ InitScript: initScript, - Listen: server1, User: connOpts.User, Pass: connOpts.Pass, WaitStart: waitStart, @@ -599,26 +606,13 @@ func runTestMain(m *testing.M) int { RetryTimeout: retryTimeout, MemtxUseMvccEngine: !isStreamUnsupported, }) - defer test_helpers.StopTarantoolWithCleanup(inst1) if err != nil { log.Fatalf("Failed to prepare test tarantool: %s", err) + return -1 } - inst2, err := test_helpers.StartTarantool(test_helpers.StartOpts{ - InitScript: initScript, - Listen: server2, - User: connOpts.User, - Pass: connOpts.Pass, - WaitStart: waitStart, - ConnectRetry: connectRetry, - RetryTimeout: retryTimeout, - }) - defer test_helpers.StopTarantoolWithCleanup(inst2) - - if err != nil { - log.Fatalf("Failed to prepare test tarantool: %s", err) - } + defer test_helpers.StopTarantoolInstances(instances) return m.Run() } From f4f47ae4c29705e4a9465904e59361b4da957d21 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Fri, 16 Dec 2022 12:54:11 +0300 Subject: [PATCH 382/605] code health: reuse Retry in test helpers Part of #214 --- test_helpers/utils.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 4862d90d8..f1002e954 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -47,20 +47,16 @@ func DeleteRecordByKey(t *testing.T, conn tarantool.Connector, // Returns false in case of connection is not in the connected state // after specified retries count, true otherwise. func WaitUntilReconnected(conn *tarantool.Connection, retries uint, timeout time.Duration) bool { - for i := uint(0); ; i++ { + err := Retry(func(arg interface{}) error { + conn := arg.(*tarantool.Connection) connected := conn.ConnectedNow() - if connected { - return true + if !connected { + return fmt.Errorf("not connected") } + return nil + }, conn, int(retries), timeout) - if i == retries { - break - } - - time.Sleep(timeout) - } - - return false + return err == nil } func SkipIfSQLUnsupported(t testing.TB) { From afce61be1810b972f756cfb49c00d120e4dc606e Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Fri, 16 Dec 2022 14:43:16 +0300 Subject: [PATCH 383/605] make: clean cache for test After this patch, "make test" behaves similar to "make test-*". Part of #214 --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 718139967..7ed0aa88e 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,8 @@ golangci-lint: .PHONY: test test: + @echo "Running all packages tests" + go clean -testcache go test -tags "$(TAGS)" ./... -v -p 1 .PHONY: testdata From a16ec6e14a54fea2fd43d68aeafe139fbc6b45ba Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 20 Dec 2022 16:49:58 +0300 Subject: [PATCH 384/605] test: idempotent stop in helpers Before this patch, calling StopTarantool wasn't idempotent because it accepts a struct copy and doesn't actually set Cmd to nil. Setting Cmd.Process to nil is effective since it's a pointer. Reworking helpers to use pointer would be better, but it would break existing API. --- test_helpers/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_helpers/main.go b/test_helpers/main.go index f2a97bef9..3363ed375 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -298,7 +298,7 @@ func StopTarantool(inst TarantoolInstance) { log.Fatalf("Failed to wait for Tarantool process to exit, got %s", err) } - inst.Cmd = nil + inst.Cmd.Process = nil } } From 13b9f8e6d89668fcaa6d399b42c07be634699062 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Fri, 16 Dec 2022 14:53:59 +0300 Subject: [PATCH 385/605] feature: support graceful shutdown If connected to Tarantool 2.10 or newer, after this patch a connection supports server graceful shutdown [1]. In this case, server will wait until all client requests will be finished and client disconnects before going down (server also may go down by timeout). Client reconnect will happen if connection options enable reconnect. Beware that graceful shutdown event initialization is asynchronous. 1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/ Closes #214 --- CHANGELOG.md | 1 + connection.go | 153 +++++++++++-- errors.go | 1 + shutdown_test.go | 551 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 692 insertions(+), 14 deletions(-) create mode 100644 shutdown_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc41089d..2f57bbc22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Event subscription support (#119) - Session settings support (#215) - pap-sha256 authorization method support (Tarantool EE feature) (#243) +- Support graceful shutdown (#214) ### Changed diff --git a/connection.go b/connection.go index e584d44d5..331e1c805 100644 --- a/connection.go +++ b/connection.go @@ -25,7 +25,8 @@ const ignoreStreamId = 0 const ( connDisconnected = 0 connConnected = 1 - connClosed = 2 + connShutdown = 2 + connClosed = 3 ) const ( @@ -33,6 +34,8 @@ const ( connTransportSsl = "ssl" ) +const shutdownEventKey = "box.shutdown" + type ConnEventKind int type ConnLogKind int @@ -45,6 +48,8 @@ const ( ReconnectFailed // Either reconnect attempts exhausted, or explicit Close is called. Closed + // Shutdown signals that shutdown callback is processing. + Shutdown // LogReconnectFailed is logged when reconnect attempt failed. LogReconnectFailed ConnLogKind = iota + 1 @@ -134,10 +139,19 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac // always returns array of array (array of tuples for space related methods). // For Eval* and Call* Tarantool always returns array, but does not forces // array of arrays. +// +// If connected to Tarantool 2.10 or newer, connection supports server graceful +// shutdown. In this case, server will wait until all client requests will be +// finished and client disconnects before going down (server also may go down +// by timeout). Client reconnect will happen if connection options enable +// reconnect. Beware that graceful shutdown event initialization is asynchronous. +// +// More on graceful shutdown: https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/ type Connection struct { addr string c net.Conn mutex sync.Mutex + cond *sync.Cond // Schema contains schema loaded on connection. Schema *Schema // requestId contains the last request ID for requests with nil context. @@ -162,6 +176,11 @@ type Connection struct { serverProtocolInfo ProtocolInfo // watchMap is a map of key -> chan watchState. watchMap sync.Map + + // shutdownWatcher is the "box.shutdown" event watcher. + shutdownWatcher Watcher + // requestCnt is a counter of active requests. + requestCnt int64 } var _ = Connector(&Connection{}) // Check compatibility with connector interface. @@ -387,6 +406,8 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { conn.opts.Logger = defaultLogger{} } + conn.cond = sync.NewCond(&conn.mutex) + if err = conn.createConnection(false); err != nil { ter, ok := err.(Error) if conn.opts.Reconnect <= 0 { @@ -612,10 +633,20 @@ func (conn *Connection) dial() (err error) { conn.lockShards() conn.c = connection atomic.StoreUint32(&conn.state, connConnected) + conn.cond.Broadcast() conn.unlockShards() go conn.writer(w, connection) go conn.reader(r, connection) + // Subscribe shutdown event to process graceful shutdown. + if conn.shutdownWatcher == nil && isFeatureInSlice(WatchersFeature, conn.serverProtocolInfo.Features) { + watcher, werr := conn.newWatcherImpl(shutdownEventKey, shutdownEventCallback) + if werr != nil { + return werr + } + conn.shutdownWatcher = watcher + } + return } @@ -745,10 +776,17 @@ func (conn *Connection) closeConnection(neterr error, forever bool) (err error) if conn.state != connClosed { close(conn.control) atomic.StoreUint32(&conn.state, connClosed) + conn.cond.Broadcast() + // Free the resources. + if conn.shutdownWatcher != nil { + go conn.shutdownWatcher.Unregister() + conn.shutdownWatcher = nil + } conn.notify(Closed) } } else { atomic.StoreUint32(&conn.state, connDisconnected) + conn.cond.Broadcast() conn.notify(Disconnected) } if conn.c != nil { @@ -767,9 +805,7 @@ func (conn *Connection) closeConnection(neterr error, forever bool) (err error) return } -func (conn *Connection) reconnect(neterr error, c net.Conn) { - conn.mutex.Lock() - defer conn.mutex.Unlock() +func (conn *Connection) reconnectImpl(neterr error, c net.Conn) { if conn.opts.Reconnect > 0 { if c == conn.c { conn.closeConnection(neterr, false) @@ -782,6 +818,13 @@ func (conn *Connection) reconnect(neterr error, c net.Conn) { } } +func (conn *Connection) reconnect(neterr error, c net.Conn) { + conn.mutex.Lock() + defer conn.mutex.Unlock() + conn.reconnectImpl(neterr, c) + conn.cond.Broadcast() +} + func (conn *Connection) lockShards() { for i := range conn.shard { conn.shard[i].rmut.Lock() @@ -1009,6 +1052,15 @@ func (conn *Connection) newFuture(ctx context.Context) (fut *Future) { fut.done = nil shard.rmut.Unlock() return + case connShutdown: + fut.err = ClientError{ + ErrConnectionShutdown, + "server shutdown in progress", + } + fut.ready = nil + fut.done = nil + shard.rmut.Unlock() + return } pos := (fut.requestId / conn.opts.Concurrency) & (requestsMap - 1) if ctx != nil { @@ -1060,11 +1112,25 @@ func (conn *Connection) contextWatchdog(fut *Future, ctx context.Context) { } } +func (conn *Connection) incrementRequestCnt() { + atomic.AddInt64(&conn.requestCnt, int64(1)) +} + +func (conn *Connection) decrementRequestCnt() { + if atomic.AddInt64(&conn.requestCnt, int64(-1)) == 0 { + conn.cond.Broadcast() + } +} + func (conn *Connection) send(req Request, streamId uint64) *Future { + conn.incrementRequestCnt() + fut := conn.newFuture(req.Ctx()) if fut.ready == nil { + conn.decrementRequestCnt() return fut } + if req.Ctx() != nil { select { case <-req.Ctx().Done(): @@ -1075,6 +1141,7 @@ func (conn *Connection) send(req Request, streamId uint64) *Future { go conn.contextWatchdog(fut, req.Ctx()) } conn.putFuture(fut, req, streamId) + return fut } @@ -1141,6 +1208,7 @@ func (conn *Connection) markDone(fut *Future) { if conn.rlimit != nil { <-conn.rlimit } + conn.decrementRequestCnt() } func (conn *Connection) peekFuture(reqid uint32) (fut *Future) { @@ -1426,6 +1494,15 @@ func subscribeWatchChannel(conn *Connection, key string) (chan watchState, error return st, nil } +func isFeatureInSlice(expected ProtocolFeature, actualSlice []ProtocolFeature) bool { + for _, actual := range actualSlice { + if expected == actual { + return true + } + } + return false +} + // NewWatcher creates a new Watcher object for the connection. // // You need to require WatchersFeature to use watchers, see examples for the @@ -1464,20 +1541,16 @@ func (conn *Connection) NewWatcher(key string, callback WatchCallback) (Watcher, // asynchronous. We do not expect any response from a Tarantool instance // That's why we can't just check the Tarantool response for an unsupported // request error. - watchersRequired := false - for _, feature := range conn.opts.RequiredProtocolInfo.Features { - if feature == WatchersFeature { - watchersRequired = true - break - } - } - - if !watchersRequired { + if !isFeatureInSlice(WatchersFeature, conn.opts.RequiredProtocolInfo.Features) { err := fmt.Errorf("the feature %s must be required by connection "+ "options to create a watcher", WatchersFeature) return nil, err } + return conn.newWatcherImpl(key, callback) +} + +func (conn *Connection) newWatcherImpl(key string, callback WatchCallback) (Watcher, error) { st, err := subscribeWatchChannel(conn, key) if err != nil { return nil, err @@ -1531,7 +1604,11 @@ func (conn *Connection) NewWatcher(key string, callback WatchCallback) (Watcher, if state.cnt == 0 { // The last one sends IPROTO_UNWATCH. - conn.Do(newUnwatchRequest(key)).Get() + if !conn.ClosedNow() { + // conn.ClosedNow() check is a workaround for calling + // Unregister from connectionClose(). + conn.Do(newUnwatchRequest(key)).Get() + } conn.watchMap.Delete(key) close(state.unready) } @@ -1637,3 +1714,51 @@ func (conn *Connection) ClientProtocolInfo() ProtocolInfo { info.Auth = conn.opts.Auth return info } + +func shutdownEventCallback(event WatchEvent) { + // Receives "true" on server shutdown. + // See https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/ + // step 2. + val, ok := event.Value.(bool) + if ok && val { + go event.Conn.shutdown() + } +} + +func (conn *Connection) shutdown() { + // Forbid state changes. + conn.mutex.Lock() + defer conn.mutex.Unlock() + + if !atomic.CompareAndSwapUint32(&(conn.state), connConnected, connShutdown) { + return + } + conn.cond.Broadcast() + conn.notify(Shutdown) + + c := conn.c + for { + if (atomic.LoadUint32(&conn.state) != connShutdown) || (c != conn.c) { + return + } + if atomic.LoadInt64(&conn.requestCnt) == 0 { + break + } + // Use cond var on conn.mutex since request execution may + // call reconnect(). It is ok if state changes as part of + // reconnect since Tarantool server won't allow to reconnect + // in the middle of shutting down. + conn.cond.Wait() + } + + // Start to reconnect based on common rules, same as in net.box. + // Reconnect also closes the connection: server waits until all + // subscribed connections are terminated. + // See https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/ + // step 3. + conn.reconnectImpl( + ClientError{ + ErrConnectionClosed, + "connection closed after server shutdown", + }, conn.c) +} diff --git a/errors.go b/errors.go index 02e4635bb..906184c24 100644 --- a/errors.go +++ b/errors.go @@ -55,6 +55,7 @@ const ( ErrProtocolError = 0x4000 + iota ErrTimeouted = 0x4000 + iota ErrRateLimited = 0x4000 + iota + ErrConnectionShutdown = 0x4000 + iota ) // Tarantool server error codes. diff --git a/shutdown_test.go b/shutdown_test.go new file mode 100644 index 000000000..d9b1db111 --- /dev/null +++ b/shutdown_test.go @@ -0,0 +1,551 @@ +//go:build linux || (darwin && !cgo) +// +build linux darwin,!cgo + +// Use OS build flags since signals are system-dependent. + +package tarantool_test + +import ( + "fmt" + "sync" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/require" + . "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" +) + +var shtdnServer = "127.0.0.1:3014" +var shtdnClntOpts = Opts{ + User: opts.User, + Pass: opts.Pass, + Timeout: 20 * time.Second, + Reconnect: 200 * time.Millisecond, + MaxReconnects: 10, + RequiredProtocolInfo: ProtocolInfo{Features: []ProtocolFeature{WatchersFeature}}, +} +var shtdnSrvOpts = test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: shtdnServer, + User: shtdnClntOpts.User, + Pass: shtdnClntOpts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, +} + +var evalMsg = "got enough sleep" +var evalBody = ` + local fiber = require('fiber') + local time, msg = ... + fiber.sleep(time) + return msg +` + +func testGracefulShutdown(t *testing.T, conn *Connection, inst *test_helpers.TarantoolInstance) { + var resp *Response + var err error + + // Set a big timeout so it would be easy to differ + // if server went down on timeout or after all connections were terminated. + serverShutdownTimeout := 60 // in seconds + _, err = conn.Call("box.ctl.set_on_shutdown_timeout", []interface{}{serverShutdownTimeout}) + require.Nil(t, err) + + // Send request with sleep. + evalSleep := 1 // in seconds + require.Lessf(t, + time.Duration(evalSleep)*time.Second, + shtdnClntOpts.Timeout, + "test request won't be failed by timeout") + + // Create a helper watcher to ensure that async + // shutdown is set up. + helperCh := make(chan WatchEvent, 10) + helperW, herr := conn.NewWatcher("box.shutdown", func(event WatchEvent) { + helperCh <- event + }) + require.Nil(t, herr) + defer helperW.Unregister() + <-helperCh + + req := NewEvalRequest(evalBody).Args([]interface{}{evalSleep, evalMsg}) + + fut := conn.Do(req) + + // SIGTERM the server. + shutdownStart := time.Now() + require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM)) + + // Check that we can't send new requests after shutdown starts. + // Retry helps to wait a bit until server starts to shutdown + // and send us the shutdown event. + shutdownWaitRetries := 5 + shutdownWaitTimeout := 100 * time.Millisecond + + err = test_helpers.Retry(func(interface{}) error { + _, err = conn.Do(NewPingRequest()).Get() + if err == nil { + return fmt.Errorf("expected error for requests sent on shutdown") + } + + if err.Error() != "server shutdown in progress (0x4005)" { + return err + } + + return nil + }, nil, shutdownWaitRetries, shutdownWaitTimeout) + require.Nil(t, err) + + // Check that requests started before the shutdown finish successfully. + resp, err = fut.Get() + require.Nil(t, err) + require.NotNil(t, resp) + require.Equal(t, resp.Data, []interface{}{evalMsg}) + + // Wait until server go down. + // Server will go down only when it process all requests from our connection + // (or on timeout). + _, err = inst.Cmd.Process.Wait() + require.Nil(t, err) + shutdownFinish := time.Now() + shutdownTime := shutdownFinish.Sub(shutdownStart) + + // Help test helpers to properly clean up. + inst.Cmd.Process = nil + + // Check that it wasn't a timeout. + require.Lessf(t, + shutdownTime, + time.Duration(serverShutdownTimeout/2)*time.Second, + "server went down not by timeout") + + // Connection is unavailable when server is down. + require.Equal(t, false, conn.ConnectedNow()) +} + +func TestGracefulShutdown(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + var inst test_helpers.TarantoolInstance + var conn *Connection + var err error + + inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + require.Nil(t, err) + defer test_helpers.StopTarantoolWithCleanup(inst) + + conn = test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + defer conn.Close() + + testGracefulShutdown(t, conn, &inst) +} + +func TestGracefulShutdownWithReconnect(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + var inst test_helpers.TarantoolInstance + var err error + + inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + require.Nil(t, err) + defer test_helpers.StopTarantoolWithCleanup(inst) + + conn := test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + defer conn.Close() + + testGracefulShutdown(t, conn, &inst) + + err = test_helpers.RestartTarantool(&inst) + require.Nilf(t, err, "Failed to restart tarantool") + + connected := test_helpers.WaitUntilReconnected(conn, shtdnClntOpts.MaxReconnects, shtdnClntOpts.Reconnect) + require.Truef(t, connected, "Reconnect success") + + testGracefulShutdown(t, conn, &inst) +} + +func TestNoGracefulShutdown(t *testing.T) { + // No watchers = no graceful shutdown. + noShtdnClntOpts := shtdnClntOpts.Clone() + noShtdnClntOpts.RequiredProtocolInfo = ProtocolInfo{} + test_helpers.SkipIfWatchersSupported(t) + + var inst test_helpers.TarantoolInstance + var conn *Connection + var err error + + inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + require.Nil(t, err) + defer test_helpers.StopTarantoolWithCleanup(inst) + + conn = test_helpers.ConnectWithValidation(t, shtdnServer, noShtdnClntOpts) + defer conn.Close() + + evalSleep := 10 // in seconds + serverShutdownTimeout := 60 // in seconds + require.Less(t, evalSleep, serverShutdownTimeout) + + // Send request with sleep. + require.Lessf(t, + time.Duration(evalSleep)*time.Second, + shtdnClntOpts.Timeout, + "test request won't be failed by timeout") + + req := NewEvalRequest(evalBody).Args([]interface{}{evalSleep, evalMsg}) + + fut := conn.Do(req) + + // SIGTERM the server. + shutdownStart := time.Now() + require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM)) + + // Check that request was interrupted. + _, err = fut.Get() + require.NotNilf(t, err, "sleep request error") + + // Wait until server go down. + _, err = inst.Cmd.Process.Wait() + require.Nil(t, err) + shutdownFinish := time.Now() + shutdownTime := shutdownFinish.Sub(shutdownStart) + + // Help test helpers to properly clean up. + inst.Cmd.Process = nil + + // Check that server finished without waiting for eval to finish. + require.Lessf(t, + shutdownTime, + time.Duration(evalSleep/2)*time.Second, + "server went down without any additional waiting") +} + +func TestGracefulShutdownRespectsClose(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + var inst test_helpers.TarantoolInstance + var conn *Connection + var err error + + inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + require.Nil(t, err) + defer test_helpers.StopTarantoolWithCleanup(inst) + + conn = test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + defer conn.Close() + + // Create a helper watcher to ensure that async + // shutdown is set up. + helperCh := make(chan WatchEvent, 10) + helperW, herr := conn.NewWatcher("box.shutdown", func(event WatchEvent) { + helperCh <- event + }) + require.Nil(t, herr) + defer helperW.Unregister() + <-helperCh + + // Set a big timeout so it would be easy to differ + // if server went down on timeout or after all connections were terminated. + serverShutdownTimeout := 60 // in seconds + _, err = conn.Call("box.ctl.set_on_shutdown_timeout", []interface{}{serverShutdownTimeout}) + require.Nil(t, err) + + // Send request with sleep. + evalSleep := 10 // in seconds + require.Lessf(t, + time.Duration(evalSleep)*time.Second, + shtdnClntOpts.Timeout, + "test request won't be failed by timeout") + + req := NewEvalRequest(evalBody).Args([]interface{}{evalSleep, evalMsg}) + + fut := conn.Do(req) + + // SIGTERM the server. + shutdownStart := time.Now() + require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM)) + + // Close the connection. + conn.Close() + + // Connection is closed. + require.Equal(t, true, conn.ClosedNow()) + + // Check that request was interrupted. + _, err = fut.Get() + require.NotNilf(t, err, "sleep request error") + + // Wait until server go down. + _, err = inst.Cmd.Process.Wait() + require.Nil(t, err) + shutdownFinish := time.Now() + shutdownTime := shutdownFinish.Sub(shutdownStart) + + // Help test helpers to properly clean up. + inst.Cmd.Process = nil + + // Check that server finished without waiting for eval to finish. + require.Lessf(t, + shutdownTime, + time.Duration(evalSleep/2)*time.Second, + "server went down without any additional waiting") + + // Check that it wasn't a timeout. + require.Lessf(t, + shutdownTime, + time.Duration(serverShutdownTimeout/2)*time.Second, + "server went down not by timeout") + + // Connection is still closed. + require.Equal(t, true, conn.ClosedNow()) +} + +func TestGracefulShutdownNotRacesWithRequestReconnect(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + var inst test_helpers.TarantoolInstance + var conn *Connection + var err error + + inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + require.Nil(t, err) + defer test_helpers.StopTarantoolWithCleanup(inst) + + conn = test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + defer conn.Close() + + // Create a helper watcher to ensure that async + // shutdown is set up. + helperCh := make(chan WatchEvent, 10) + helperW, herr := conn.NewWatcher("box.shutdown", func(event WatchEvent) { + helperCh <- event + }) + require.Nil(t, herr) + defer helperW.Unregister() + <-helperCh + + // Set a small timeout so server will shutdown before requesst finishes. + serverShutdownTimeout := 1 // in seconds + _, err = conn.Call("box.ctl.set_on_shutdown_timeout", []interface{}{serverShutdownTimeout}) + require.Nil(t, err) + + // Send request with sleep. + evalSleep := 5 // in seconds + require.Lessf(t, + serverShutdownTimeout, + evalSleep, + "test request will be failed by timeout") + require.Lessf(t, + time.Duration(serverShutdownTimeout)*time.Second, + shtdnClntOpts.Timeout, + "test request will be failed by timeout") + + req := NewEvalRequest(evalBody).Args([]interface{}{evalSleep, evalMsg}) + + evalStart := time.Now() + fut := conn.Do(req) + + // SIGTERM the server. + require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM)) + + // Wait until server go down. + // Server is expected to go down on timeout. + _, err = inst.Cmd.Process.Wait() + require.Nil(t, err) + + // Help test helpers to properly clean up. + inst.Cmd.Process = nil + + // Check that request failed by server disconnect, not a client timeout. + _, err = fut.Get() + require.NotNil(t, err) + require.NotContains(t, err.Error(), "client timeout for request") + + evalFinish := time.Now() + evalTime := evalFinish.Sub(evalStart) + + // Check that it wasn't a client timeout. + require.Lessf(t, + evalTime, + shtdnClntOpts.Timeout, + "server went down not by timeout") +} + +func TestGracefulShutdownCloseConcurrent(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + var inst test_helpers.TarantoolInstance + var err error + var srvShtdnStart, srvShtdnFinish time.Time + + inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + require.Nil(t, err) + defer test_helpers.StopTarantoolWithCleanup(inst) + + conn := test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + defer conn.Close() + + // Create a helper watcher to ensure that async + // shutdown is set up. + helperCh := make(chan WatchEvent, 10) + helperW, herr := conn.NewWatcher("box.shutdown", func(event WatchEvent) { + helperCh <- event + }) + require.Nil(t, herr) + defer helperW.Unregister() + <-helperCh + + // Set a big timeout so it would be easy to differ + // if server went down on timeout or after all connections were terminated. + serverShutdownTimeout := 60 // in seconds + _, err = conn.Call("box.ctl.set_on_shutdown_timeout", []interface{}{serverShutdownTimeout}) + require.Nil(t, err) + conn.Close() + + const testConcurrency = 50 + + var caseWg, srvToStop, srvStop sync.WaitGroup + caseWg.Add(testConcurrency) + srvToStop.Add(testConcurrency) + srvStop.Add(1) + + // Create many connections. + for i := 0; i < testConcurrency; i++ { + go func(i int) { + defer caseWg.Done() + + // Do not wait till Tarantool register out watcher, + // test everything is ok even on async. + + conn := test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + defer conn.Close() + + // Wait till all connections created. + srvToStop.Done() + srvStop.Wait() + }(i) + } + + var sret error + go func(inst *test_helpers.TarantoolInstance) { + srvToStop.Wait() + srvShtdnStart = time.Now() + cerr := inst.Cmd.Process.Signal(syscall.SIGTERM) + if cerr != nil { + sret = cerr + } + srvStop.Done() + }(&inst) + + srvStop.Wait() + require.Nil(t, sret, "No errors on server SIGTERM") + + _, err = inst.Cmd.Process.Wait() + require.Nil(t, err) + + // Help test helpers to properly clean up. + inst.Cmd.Process = nil + + srvShtdnFinish = time.Now() + srvShtdnTime := srvShtdnFinish.Sub(srvShtdnStart) + + require.Less(t, + srvShtdnTime, + time.Duration(serverShutdownTimeout/2)*time.Second, + "server went down not by timeout") +} + +func TestGracefulShutdownConcurrent(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + var inst test_helpers.TarantoolInstance + var err error + var srvShtdnStart, srvShtdnFinish time.Time + + inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + require.Nil(t, err) + defer test_helpers.StopTarantoolWithCleanup(inst) + + conn := test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + defer conn.Close() + + // Set a big timeout so it would be easy to differ + // if server went down on timeout or after all connections were terminated. + serverShutdownTimeout := 60 // in seconds + _, err = conn.Call("box.ctl.set_on_shutdown_timeout", []interface{}{serverShutdownTimeout}) + require.Nil(t, err) + conn.Close() + + const testConcurrency = 50 + + var caseWg, srvToStop, srvStop sync.WaitGroup + caseWg.Add(testConcurrency) + srvToStop.Add(testConcurrency) + srvStop.Add(1) + + // Create many connections. + var ret error + for i := 0; i < testConcurrency; i++ { + go func(i int) { + defer caseWg.Done() + + conn := test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + defer conn.Close() + + // Create a helper watcher to ensure that async + // shutdown is set up. + helperCh := make(chan WatchEvent, 10) + helperW, _ := conn.NewWatcher("box.shutdown", func(event WatchEvent) { + helperCh <- event + }) + defer helperW.Unregister() + <-helperCh + + evalSleep := 1 // in seconds + req := NewEvalRequest(evalBody).Args([]interface{}{evalSleep, evalMsg}) + fut := conn.Do(req) + + // Wait till all connections had started sleeping. + srvToStop.Done() + srvStop.Wait() + + _, gerr := fut.Get() + if gerr != nil { + ret = gerr + } + }(i) + } + + var sret error + go func(inst *test_helpers.TarantoolInstance) { + srvToStop.Wait() + srvShtdnStart = time.Now() + cerr := inst.Cmd.Process.Signal(syscall.SIGTERM) + if cerr != nil { + sret = cerr + } + srvStop.Done() + }(&inst) + + srvStop.Wait() + require.Nil(t, sret, "No errors on server SIGTERM") + + caseWg.Wait() + require.Nil(t, ret, "No errors on concurrent wait") + + _, err = inst.Cmd.Process.Wait() + require.Nil(t, err) + + // Help test helpers to properly clean up. + inst.Cmd.Process = nil + + srvShtdnFinish = time.Now() + srvShtdnTime := srvShtdnFinish.Sub(srvShtdnStart) + + require.Less(t, + srvShtdnTime, + time.Duration(serverShutdownTimeout/2)*time.Second, + "server went down not by timeout") +} From 0ffd926e219c543fa95f36c7aec25f729749e371 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 28 Dec 2022 13:58:52 +0300 Subject: [PATCH 386/605] Release 1.10.0 Overview The release improves compatibility with new Tarantool versions. Breaking changes There are no breaking changes in the release. New features Support iproto feature discovery (#120). Support errors extended information (#209). Support error type in MessagePack (#209). Support event subscription (#119). Support session settings (#215). Support pap-sha256 authorization method (Tarantool EE feature) (#243). Support graceful shutdown (#214). Bugfixes Decimal package uses a test variable DecimalPrecision instead of a package-level variable decimalPrecision (#233). Flaky test TestClientRequestObjectsWithContext (#244). Flaky test multi/TestDisconnectAll (#234). --- CHANGELOG.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f57bbc22..9bc276b18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,22 +10,29 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +### Changed + +### Fixed + +## [1.10.0] - 2022-12-31 + +The release improves compatibility with new Tarantool versions. + +### Added + - Support iproto feature discovery (#120) - Support errors extended information (#209) -- Error type support in MessagePack (#209) -- Event subscription support (#119) -- Session settings support (#215) -- pap-sha256 authorization method support (Tarantool EE feature) (#243) +- Support error type in MessagePack (#209) +- Support event subscription (#119) +- Support session settings (#215) +- Support pap-sha256 authorization method (Tarantool EE feature) (#243) - Support graceful shutdown (#214) -### Changed - ### Fixed - Decimal package uses a test variable DecimalPrecision instead of a package-level variable decimalPrecision (#233) -- Flaky tests TestClientRequestObjectsWithContext and - TestClientIdRequestObjectWithContext (#244) +- Flaky test TestClientRequestObjectsWithContext (#244) - Flaky test multi/TestDisconnectAll (#234) ## [1.9.0] - 2022-11-02 From e52e9b228450d67b0e143a6ee7a95bd73a949855 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Sun, 25 Dec 2022 14:56:56 +0300 Subject: [PATCH 387/605] readme: support Tarantool 1.10+ Technically, the connector can now work with Tarantool 1.6+. But we decided to simplify our CI and do not declare support for older versions [1]. 1. https://github.com/tarantool/go-tarantool/pull/198 --- README.md | 4 ++-- tarantool_test.go | 56 ++++++++++++++++++++++------------------------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 2bd3f271c..7fcf96d1b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ # Client in Go for Tarantool The package `go-tarantool` contains everything you need to connect to -[Tarantool 1.6+][tarantool-site]. +[Tarantool 1.10+][tarantool-site]. The advantage of integrating Go with Tarantool, which is an application server plus a DBMS, is that Go programmers can handle databases and perform on-the-fly @@ -32,7 +32,7 @@ faster than other packages according to public benchmarks. ## Installation -We assume that you have Tarantool version 1.6+ and a modern Linux or BSD +We assume that you have Tarantool version 1.10+ and a modern Linux or BSD operating system. You need a current version of `go`, version 1.13 or later (use `go version` to diff --git a/tarantool_test.go b/tarantool_test.go index a4e85041b..dc02d2c17 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -977,21 +977,19 @@ func TestClient(t *testing.T) { } // Upsert - if strings.Compare(conn.Greeting.Version, "Tarantool 1.6.7") >= 0 { - resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) - if err != nil { - t.Fatalf("Failed to Upsert (insert): %s", err.Error()) - } - if resp == nil { - t.Errorf("Response is nil after Upsert (insert)") - } - resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) - if err != nil { - t.Fatalf("Failed to Upsert (update): %s", err.Error()) - } - if resp == nil { - t.Errorf("Response is nil after Upsert (update)") - } + resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + if err != nil { + t.Fatalf("Failed to Upsert (insert): %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Upsert (insert)") + } + resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + if err != nil { + t.Fatalf("Failed to Upsert (update): %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Upsert (update)") } // Select @@ -2009,21 +2007,19 @@ func TestClientNamed(t *testing.T) { } // Upsert - if strings.Compare(conn.Greeting.Version, "Tarantool 1.6.7") >= 0 { - resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) - if err != nil { - t.Fatalf("Failed to Upsert (insert): %s", err.Error()) - } - if resp == nil { - t.Errorf("Response is nil after Upsert (insert)") - } - resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) - if err != nil { - t.Fatalf("Failed to Upsert (update): %s", err.Error()) - } - if resp == nil { - t.Errorf("Response is nil after Upsert (update)") - } + resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + if err != nil { + t.Fatalf("Failed to Upsert (insert): %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Upsert (insert)") + } + resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + if err != nil { + t.Fatalf("Failed to Upsert (update): %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Upsert (update)") } // Select From c973b8e092bff4b8d5b30c122b6e54e386fdcf61 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 10 Jan 2023 12:04:44 +0300 Subject: [PATCH 388/605] doc: error feature is supported Part of #246 --- protocol.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol.go b/protocol.go index 06506ee5a..8252f538a 100644 --- a/protocol.go +++ b/protocol.go @@ -41,7 +41,7 @@ const ( // (supported by connector). TransactionsFeature ProtocolFeature = 1 // ErrorExtensionFeature represents support of MP_ERROR objects over MessagePack - // (unsupported by connector). + // (supported by connector). ErrorExtensionFeature ProtocolFeature = 2 // WatchersFeature represents support of watchers // (supported by connector). From d7925800514ec04b4b0818e4eb0da5d5c3a26227 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 12 Jan 2023 17:30:44 +0300 Subject: [PATCH 389/605] api: add pagination support A user could fetch a position of a last tuple using a new method of the SelectRequest type: selectRequest = selectRequest.FetchPos(true) The position will be stored in a new field of the Response type: Response.Pos A user could specify a tuple from which selection must continue or its position with a new method of the SelectRequest type: selectRequest = selectRequest.After([]interface{}{23}) or selectRequest = selectRequest.After(resp.Pos) In action it looks like: req := NewSelectRequest(space).Key(key).Limit(10).FetchPos(true) for condition { resp, err := conn.Do(req).Get() // ... req = req.After(resp.Pos) } 1. https://github.com/tarantool/tarantool/issues/7639 Part of #246 --- CHANGELOG.md | 2 + const.go | 4 + example_test.go | 2 +- export_test.go | 5 +- protocol.go | 5 +- request.go | 141 ++++++++++++++++++++---- request_test.go | 46 ++++++-- response.go | 17 ++- tarantool_test.go | 249 ++++++++++++++++++++++++++++++++++++------ test_helpers/utils.go | 8 ++ 10 files changed, 406 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bc276b18..acfe2a914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +- Support pagination (#246) + ### Changed ### Fixed diff --git a/const.go b/const.go index ead151878..29bcc101b 100644 --- a/const.go +++ b/const.go @@ -30,16 +30,20 @@ const ( KeyLimit = 0x12 KeyOffset = 0x13 KeyIterator = 0x14 + KeyFetchPos = 0x1f KeyKey = 0x20 KeyTuple = 0x21 KeyFunctionName = 0x22 KeyUserName = 0x23 KeyExpression = 0x27 + KeyAfterPos = 0x2e + KeyAfterTuple = 0x2f KeyDefTuple = 0x28 KeyData = 0x30 KeyError24 = 0x31 /* Error in pre-2.4 format. */ KeyMetaData = 0x32 KeyBindCount = 0x34 + KeyPos = 0x35 KeySQLText = 0x40 KeySQLBind = 0x41 KeySQLInfo = 0x42 diff --git a/example_test.go b/example_test.go index 27a962da0..7b0ee9a6a 100644 --- a/example_test.go +++ b/example_test.go @@ -329,7 +329,7 @@ func ExampleProtocolVersion() { fmt.Println("Connector client protocol features:", clientProtocolInfo.Features) // Output: // Connector client protocol version: 4 - // Connector client protocol features: [StreamsFeature TransactionsFeature ErrorExtensionFeature WatchersFeature] + // Connector client protocol features: [StreamsFeature TransactionsFeature ErrorExtensionFeature WatchersFeature PaginationFeature] } func getTestTxnOpts() tarantool.Opts { diff --git a/export_test.go b/export_test.go index 464a85844..71886bb20 100644 --- a/export_test.go +++ b/export_test.go @@ -23,8 +23,9 @@ func RefImplPingBody(enc *encoder) error { // RefImplSelectBody is reference implementation for filling of a select // request's body. -func RefImplSelectBody(enc *encoder, space, index, offset, limit, iterator uint32, key interface{}) error { - return fillSelect(enc, space, index, offset, limit, iterator, key) +func RefImplSelectBody(enc *encoder, space, index, offset, limit, iterator uint32, + key, after interface{}, fetchPos bool) error { + return fillSelect(enc, space, index, offset, limit, iterator, key, after, fetchPos) } // RefImplInsertBody is reference implementation for filling of an insert diff --git a/protocol.go b/protocol.go index 8252f538a..ae8dce306 100644 --- a/protocol.go +++ b/protocol.go @@ -47,7 +47,7 @@ const ( // (supported by connector). WatchersFeature ProtocolFeature = 3 // PaginationFeature represents support of pagination - // (unsupported by connector). + // (supported by connector). PaginationFeature ProtocolFeature = 4 ) @@ -83,11 +83,14 @@ var clientProtocolInfo ProtocolInfo = ProtocolInfo{ // version 2 (Tarantool 2.10.0), in connector since 1.10.0. // Watchers were introduced in protocol version 3 (Tarantool 2.10.0), in // connector since 1.10.0. + // Pagination were introduced in protocol version 4 (Tarantool 2.11.0), in + // connector since 1.11.0. Features: []ProtocolFeature{ StreamsFeature, TransactionsFeature, ErrorExtensionFeature, WatchersFeature, + PaginationFeature, }, } diff --git a/request.go b/request.go index 1b135eaa6..55e36292d 100644 --- a/request.go +++ b/request.go @@ -10,35 +10,103 @@ import ( ) func fillSearch(enc *encoder, spaceNo, indexNo uint32, key interface{}) error { - encodeUint(enc, KeySpaceNo) - encodeUint(enc, uint64(spaceNo)) - encodeUint(enc, KeyIndexNo) - encodeUint(enc, uint64(indexNo)) - encodeUint(enc, KeyKey) + if err := encodeUint(enc, KeySpaceNo); err != nil { + return err + } + if err := encodeUint(enc, uint64(spaceNo)); err != nil { + return err + } + if err := encodeUint(enc, KeyIndexNo); err != nil { + return err + } + if err := encodeUint(enc, uint64(indexNo)); err != nil { + return err + } + if err := encodeUint(enc, KeyKey); err != nil { + return err + } return enc.Encode(key) } -func fillIterator(enc *encoder, offset, limit, iterator uint32) { - encodeUint(enc, KeyIterator) - encodeUint(enc, uint64(iterator)) - encodeUint(enc, KeyOffset) - encodeUint(enc, uint64(offset)) - encodeUint(enc, KeyLimit) - encodeUint(enc, uint64(limit)) +func fillIterator(enc *encoder, offset, limit, iterator uint32) error { + if err := encodeUint(enc, KeyIterator); err != nil { + return err + } + if err := encodeUint(enc, uint64(iterator)); err != nil { + return err + } + if err := encodeUint(enc, KeyOffset); err != nil { + return err + } + if err := encodeUint(enc, uint64(offset)); err != nil { + return err + } + if err := encodeUint(enc, KeyLimit); err != nil { + return err + } + return encodeUint(enc, uint64(limit)) } func fillInsert(enc *encoder, spaceNo uint32, tuple interface{}) error { - enc.EncodeMapLen(2) - encodeUint(enc, KeySpaceNo) - encodeUint(enc, uint64(spaceNo)) - encodeUint(enc, KeyTuple) + if err := enc.EncodeMapLen(2); err != nil { + return err + } + if err := encodeUint(enc, KeySpaceNo); err != nil { + return err + } + if err := encodeUint(enc, uint64(spaceNo)); err != nil { + return err + } + if err := encodeUint(enc, KeyTuple); err != nil { + return err + } return enc.Encode(tuple) } -func fillSelect(enc *encoder, spaceNo, indexNo, offset, limit, iterator uint32, key interface{}) error { - enc.EncodeMapLen(6) - fillIterator(enc, offset, limit, iterator) - return fillSearch(enc, spaceNo, indexNo, key) +func fillSelect(enc *encoder, spaceNo, indexNo, offset, limit, iterator uint32, + key, after interface{}, fetchPos bool) error { + mapLen := 6 + if fetchPos { + mapLen += 1 + } + if after != nil { + mapLen += 1 + } + if err := enc.EncodeMapLen(mapLen); err != nil { + return err + } + if err := fillIterator(enc, offset, limit, iterator); err != nil { + return err + } + if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { + return err + } + if fetchPos { + if err := encodeUint(enc, KeyFetchPos); err != nil { + return err + } + if err := enc.EncodeBool(fetchPos); err != nil { + return err + } + } + if after != nil { + if pos, ok := after.([]byte); ok { + if err := encodeUint(enc, KeyAfterPos); err != nil { + return err + } + if err := enc.EncodeString(string(pos)); err != nil { + return err + } + } else { + if err := encodeUint(enc, KeyAfterTuple); err != nil { + return err + } + if err := enc.Encode(after); err != nil { + return err + } + } + } + return nil } func fillUpdate(enc *encoder, spaceNo, indexNo uint32, key, ops interface{}) error { @@ -660,9 +728,9 @@ func (req *PingRequest) Context(ctx context.Context) *PingRequest { // by a Connection. type SelectRequest struct { spaceIndexRequest - isIteratorSet bool + isIteratorSet, fetchPos bool offset, limit, iterator uint32 - key interface{} + key, after interface{} } // NewSelectRequest returns a new empty SelectRequest. @@ -671,8 +739,10 @@ func NewSelectRequest(space interface{}) *SelectRequest { req.requestCode = SelectRequestCode req.setSpace(space) req.isIteratorSet = false + req.fetchPos = false req.iterator = IterAll req.key = []interface{}{} + req.after = nil req.limit = 0xFFFFFFFF return req } @@ -716,6 +786,30 @@ func (req *SelectRequest) Key(key interface{}) *SelectRequest { return req } +// FetchPos determines whether to fetch positions of the last tuple. A position +// descriptor will be saved in Response.Pos value. +// +// Note: default value is false. +// +// Requires Tarantool >= 2.11. +// Since 1.11.0 +func (req *SelectRequest) FetchPos(fetch bool) *SelectRequest { + req.fetchPos = fetch + return req +} + +// After must contain a tuple from which selection must continue or its +// position (a value from Response.Pos). +// +// Note: default value in nil. +// +// Requires Tarantool >= 2.11. +// Since 1.11.0 +func (req *SelectRequest) After(after interface{}) *SelectRequest { + req.after = after + return req +} + // Body fills an encoder with the select request body. func (req *SelectRequest) Body(res SchemaResolver, enc *encoder) error { spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) @@ -723,7 +817,8 @@ func (req *SelectRequest) Body(res SchemaResolver, enc *encoder) error { return err } - return fillSelect(enc, spaceNo, indexNo, req.offset, req.limit, req.iterator, req.key) + return fillSelect(enc, spaceNo, indexNo, req.offset, req.limit, req.iterator, + req.key, req.after, req.fetchPos) } // Context sets a passed context to the request. diff --git a/request_test.go b/request_test.go index 70aa29f45..32bdd87e1 100644 --- a/request_test.go +++ b/request_test.go @@ -319,7 +319,8 @@ func TestSelectRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer refEnc := NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterAll, []interface{}{}) + err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, + IterAll, []interface{}{}, nil, false) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) return @@ -334,7 +335,8 @@ func TestSelectRequestDefaultIteratorEqIfKey(t *testing.T) { key := []interface{}{uint(18)} refEnc := NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterEq, key) + err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, + IterEq, key, nil, false) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) return @@ -351,7 +353,8 @@ func TestSelectRequestIteratorNotChangedIfKey(t *testing.T) { const iter = IterGe refEnc := NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, iter, key) + err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, + iter, key, nil, false) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) return @@ -368,22 +371,45 @@ func TestSelectRequestSetters(t *testing.T) { const limit = 5 const iter = IterLt key := []interface{}{uint(36)} - var refBuf bytes.Buffer + afterBytes := []byte{0x1, 0x2, 0x3} + afterKey := []interface{}{uint(13)} + var refBufAfterBytes, refBufAfterKey bytes.Buffer - refEnc := NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, validSpace, validIndex, offset, limit, iter, key) + refEncAfterBytes := NewEncoder(&refBufAfterBytes) + err := RefImplSelectBody(refEncAfterBytes, validSpace, validIndex, offset, + limit, iter, key, afterBytes, true) if err != nil { - t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) + t.Errorf("An unexpected RefImplSelectBody() error %s", err) return } - req := NewSelectRequest(validSpace). + refEncAfterKey := NewEncoder(&refBufAfterKey) + err = RefImplSelectBody(refEncAfterKey, validSpace, validIndex, offset, + limit, iter, key, afterKey, true) + if err != nil { + t.Errorf("An unexpected RefImplSelectBody() error %s", err) + return + } + + reqAfterBytes := NewSelectRequest(validSpace). Index(validIndex). Offset(offset). Limit(limit). Iterator(iter). - Key(key) - assertBodyEqual(t, refBuf.Bytes(), req) + Key(key). + After(afterBytes). + FetchPos(true) + reqAfterKey := NewSelectRequest(validSpace). + Index(validIndex). + Offset(offset). + Limit(limit). + Iterator(iter). + Key(key). + After(afterKey). + FetchPos(true) + + assertBodyEqual(t, refBufAfterBytes.Bytes(), reqAfterBytes) + assertBodyEqual(t, refBufAfterKey.Bytes(), reqAfterKey) } func TestInsertRequestDefaultValues(t *testing.T) { diff --git a/response.go b/response.go index dc747c852..e5486b0a4 100644 --- a/response.go +++ b/response.go @@ -7,9 +7,12 @@ import ( type Response struct { RequestId uint32 Code uint32 - Error string // error message - // Data contains deserialized data for untyped requests - Data []interface{} + // Error contains an error message. + Error string + // Data contains deserialized data for untyped requests. + Data []interface{} + // Pos contains a position descriptor of last selected tuple. + Pos []byte MetaData []ColumnMetaData SQLInfo SQLInfo buf smallBuf @@ -228,6 +231,10 @@ func (resp *Response) decodeBody() (err error) { if !found { return fmt.Errorf("unknown auth type %s", auth) } + case KeyPos: + if resp.Pos, err = d.DecodeBytes(); err != nil { + return fmt.Errorf("unable to decode a position: %w", err) + } default: if err = d.Skip(); err != nil { return err @@ -300,6 +307,10 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if err = d.Decode(&resp.MetaData); err != nil { return err } + case KeyPos: + if resp.Pos, err = d.DecodeBytes(); err != nil { + return fmt.Errorf("unable to decode a position: %w", err) + } default: if err = d.Skip(); err != nil { return err diff --git a/tarantool_test.go b/tarantool_test.go index dc02d2c17..3c11aa4ea 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -850,6 +850,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Ping") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } // Insert resp, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) @@ -859,6 +862,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Insert") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 1 { t.Errorf("Response Body len != 1") } @@ -892,6 +898,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Delete") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 1 { t.Errorf("Response Body len != 1") } @@ -915,6 +924,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Delete") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 0 { t.Errorf("Response Data len != 0") } @@ -925,7 +937,10 @@ func TestClient(t *testing.T) { t.Fatalf("Failed to Replace: %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Replace") + t.Fatalf("Response is nil after Replace") + } + if resp.Pos != nil { + t.Errorf("Response should not have a position") } resp, err = conn.Replace(spaceNo, []interface{}{uint(2), "hi", "planet"}) if err != nil { @@ -959,6 +974,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Update") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 1 { t.Errorf("Response Data len != 1") } @@ -982,7 +1000,10 @@ func TestClient(t *testing.T) { t.Fatalf("Failed to Upsert (insert): %s", err.Error()) } if resp == nil { - t.Errorf("Response is nil after Upsert (insert)") + t.Fatalf("Response is nil after Upsert (insert)") + } + if resp.Pos != nil { + t.Errorf("Response should not have a position") } resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) if err != nil { @@ -998,6 +1019,9 @@ func TestClient(t *testing.T) { if err != nil { t.Fatalf("Failed to Replace: %s", err.Error()) } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Code != 0 { t.Errorf("Failed to replace") } @@ -1009,6 +1033,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Select") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 1 { t.Errorf("Response Data len != 1") } @@ -1031,6 +1058,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Select") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 0 { t.Errorf("Response Data len != 0") } @@ -1101,6 +1131,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Call16") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } @@ -1110,6 +1143,9 @@ func TestClient(t *testing.T) { if err != nil { t.Errorf("Failed to use Call16") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if val, ok := resp.Data[0].([]interface{})[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } @@ -1118,6 +1154,9 @@ func TestClient(t *testing.T) { if err != nil { t.Errorf("Failed to use Call") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if val, ok := resp.Data[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } @@ -1130,6 +1169,9 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Eval") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } @@ -1201,6 +1243,9 @@ func TestClientSessionPush(t *testing.T) { t.Errorf("Response is empty after it.Next() == true") break } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after CallAsync") break @@ -2090,6 +2135,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Insert") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Insert") } @@ -2125,6 +2173,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Replace") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Replace") } @@ -2159,6 +2210,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Delete") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Delete") } @@ -2193,6 +2247,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Update") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Update") } @@ -2225,6 +2282,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Update") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Update") } @@ -2255,6 +2315,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Upsert (update)") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Upsert") } @@ -2273,6 +2336,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Upsert (update)") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if resp.Data == nil { t.Fatalf("Response data is nil after Upsert") } @@ -2280,43 +2346,15 @@ func TestClientRequestObjects(t *testing.T) { t.Fatalf("Response Data len != 0") } - // Select. - req = NewSelectRequest(spaceNo). - Index(indexNo). - Limit(20). - Iterator(IterGe). - Key([]interface{}{uint(1010)}) - resp, err = conn.Do(req).Get() - if err != nil { - t.Errorf("Failed to Select: %s", err.Error()) - } - if resp == nil { - t.Errorf("Response is nil after Select") - return - } - if len(resp.Data) != 9 { - t.Fatalf("Response Data len %d != 9", len(resp.Data)) - } - if tpl, ok := resp.Data[0].([]interface{}); !ok { - t.Errorf("Unexpected body of Select") - } else { - if id, err := convertUint64(tpl[0]); err != nil || id != 1010 { - t.Errorf("Unexpected body of Select (0) %v, expected %d", tpl[0], 1010) - } - if h, ok := tpl[1].(string); !ok || h != "bye" { - t.Errorf("Unexpected body of Select (1) %q, expected %q", tpl[1].(string), "bye") - } - if h, ok := tpl[2].(string); !ok || h != "bye" { - t.Errorf("Unexpected body of Select (2) %q, expected %q", tpl[2].(string), "bye") - } - } - // Call16 vs Call17 req = NewCall16Request("simple_concat").Args([]interface{}{"1"}) resp, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to use Call") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if val, ok := resp.Data[0].([]interface{})[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } @@ -2327,6 +2365,9 @@ func TestClientRequestObjects(t *testing.T) { if err != nil { t.Errorf("Failed to use Call17") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if val, ok := resp.Data[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", resp.Data) } @@ -2340,6 +2381,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Eval") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } @@ -2364,6 +2408,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 0 { t.Fatalf("Response Body len != 0") } @@ -2382,6 +2429,9 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } + if resp.Pos != nil { + t.Errorf("Response should not have a position") + } if len(resp.Data) != 0 { t.Fatalf("Response Body len != 0") } @@ -2393,6 +2443,137 @@ func TestClientRequestObjects(t *testing.T) { } } +func testConnectionDoSelectRequestPrepare(t *testing.T, conn Connector) { + t.Helper() + + for i := 1010; i < 1020; i++ { + req := NewReplaceRequest(spaceName).Tuple( + []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) + if _, err := conn.Do(req).Get(); err != nil { + t.Fatalf("Unable to prepare tuples: %s", err) + } + } +} + +func testConnectionDoSelectRequestCheck(t *testing.T, + resp *Response, err error, pos bool, dataLen int, firstKey uint64) { + t.Helper() + + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if !pos && resp.Pos != nil { + t.Errorf("Response should not have a position descriptor") + } + if pos && resp.Pos == nil { + t.Fatalf("A response must have a position descriptor") + } + if len(resp.Data) != dataLen { + t.Fatalf("Response Data len %d != %d", len(resp.Data), dataLen) + } + for i := 0; i < dataLen; i++ { + key := firstKey + uint64(i) + if tpl, ok := resp.Data[i].([]interface{}); !ok { + t.Errorf("Unexpected body of Select") + } else { + if id, err := convertUint64(tpl[0]); err != nil || id != key { + t.Errorf("Unexpected body of Select (0) %v, expected %d", + tpl[0], key) + } + expectedSecond := fmt.Sprintf("val %d", key) + if h, ok := tpl[1].(string); !ok || h != expectedSecond { + t.Errorf("Unexpected body of Select (1) %q, expected %q", + tpl[1].(string), expectedSecond) + } + if h, ok := tpl[2].(string); !ok || h != "bla" { + t.Errorf("Unexpected body of Select (2) %q, expected %q", + tpl[2].(string), "bla") + } + } + } +} + +func TestConnectionDoSelectRequest(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + testConnectionDoSelectRequestPrepare(t, conn) + + req := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(20). + Iterator(IterGe). + Key([]interface{}{uint(1010)}) + resp, err := conn.Do(req).Get() + + testConnectionDoSelectRequestCheck(t, resp, err, false, 10, 1010) +} + +func TestConnectionDoSelectRequest_fetch_pos(t *testing.T) { + test_helpers.SkipIfPaginationUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + testConnectionDoSelectRequestPrepare(t, conn) + + req := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(2). + Iterator(IterGe). + FetchPos(true). + Key([]interface{}{uint(1010)}) + resp, err := conn.Do(req).Get() + + testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1010) +} + +func TestConnectDoSelectRequest_after_tuple(t *testing.T) { + test_helpers.SkipIfPaginationUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + testConnectionDoSelectRequestPrepare(t, conn) + + req := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(2). + Iterator(IterGe). + FetchPos(true). + Key([]interface{}{uint(1010)}). + After([]interface{}{uint(1012)}) + resp, err := conn.Do(req).Get() + + testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1013) +} + +func TestConnectionDoSelectRequest_pagination_pos(t *testing.T) { + test_helpers.SkipIfPaginationUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + testConnectionDoSelectRequestPrepare(t, conn) + + req := NewSelectRequest(spaceNo). + Index(indexNo). + Limit(2). + Iterator(IterGe). + FetchPos(true). + Key([]interface{}{uint(1010)}) + resp, err := conn.Do(req).Get() + + testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1010) + + resp, err = conn.Do(req.After(resp.Pos)).Get() + + testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1012) +} + func TestClientRequestObjectsWithNilContext(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() @@ -2943,6 +3124,7 @@ func TestConnectionProtocolInfoSupported(t *testing.T) { TransactionsFeature, ErrorExtensionFeature, WatchersFeature, + PaginationFeature, }, }) @@ -3059,6 +3241,7 @@ func TestConnectionProtocolInfoUnsupported(t *testing.T) { TransactionsFeature, ErrorExtensionFeature, WatchersFeature, + PaginationFeature, }, }) diff --git a/test_helpers/utils.go b/test_helpers/utils.go index f1002e954..12c65009e 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -182,6 +182,14 @@ func SkipIfErrorMessagePackTypeUnsupported(t *testing.T) { SkipIfFeatureUnsupported(t, "error type in MessagePack", 2, 10, 0) } +// SkipIfPaginationUnsupported skips test run if Tarantool without +// pagination is used. +func SkipIfPaginationUnsupported(t *testing.T) { + t.Helper() + + SkipIfFeatureUnsupported(t, "pagination", 2, 11, 0) +} + // CheckEqualBoxErrors checks equivalence of tarantool.BoxError objects. // // Tarantool errors are not comparable by nature: From c3215e17f2960fdca8cf9d4bc86ecd8f73c048f6 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 11 Jan 2023 13:45:23 +0300 Subject: [PATCH 390/605] ci: testing with a latest Tarantool's commit Closes #246 --- .github/workflows/testing.yml | 32 +++++++++++++++++++++++++++++--- Makefile | 6 +++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 698f23f87..c4516e5bd 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -50,17 +50,43 @@ jobs: - name: Clone the connector uses: actions/checkout@v2 + - name: Setup tt + run: | + curl -L https://tarantool.io/release/2/installer.sh | sudo bash + sudo apt install -y tt + - name: Setup Tarantool ${{ matrix.tarantool }} if: matrix.tarantool != '2.x-latest' uses: tarantool/setup-tarantool@v2 with: tarantool-version: ${{ matrix.tarantool }} - - name: Setup Tarantool 2.x (latest) + - name: Get Tarantool 2.x latest commit if: matrix.tarantool == '2.x-latest' run: | - curl -L https://tarantool.io/pre-release/2/installer.sh | sudo bash - sudo apt install -y tarantool tarantool-dev + commit_hash=$(git ls-remote https://github.com/tarantool/tarantool.git --branch master | head -c 8) + echo "LATEST_COMMIT=${commit_hash}" >> $GITHUB_ENV + shell: bash + + - name: Cache Tarantool 2.x latest + if: matrix.tarantool == '2.x-latest' + id: cache-latest + uses: actions/cache@v3 + with: + path: "/opt/tarantool" + key: cache-latest-${{ env.LATEST_COMMIT }} + + - name: Setup Tarantool 2.x latest + if: matrix.tarantool == '2.x-latest' && steps.cache-latest.outputs.cache-hit != 'true' + run: | + # mkdir could be removed after: + # https://github.com/tarantool/tt/issues/282 + sudo mkdir -p /opt/tarantool + sudo tt install tarantool=master + + - name: Add Tarantool 2.x latest to PATH + if: matrix.tarantool == '2.x-latest' + run: echo "/opt/tarantool/bin" >> $GITHUB_PATH - name: Setup golang for the connector and tests uses: actions/setup-go@v2 diff --git a/Makefile b/Makefile index 7ed0aa88e..be1f6757f 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,10 @@ BENCH_OPTIONS := -bench=. -run=^Benchmark -benchmem -benchtime=${DURATION} -coun GO_TARANTOOL_URL := https://github.com/tarantool/go-tarantool GO_TARANTOOL_DIR := ${PROJECT_DIR}/${BENCH_PATH}/go-tarantool TAGS := +TTCTL := tt +ifeq (,$(shell which tt 2>/dev/null)) + TTCTL := tarantoolctl +endif .PHONY: clean clean: @@ -22,7 +26,7 @@ clean: .PHONY: deps deps: clean - ( cd ./queue/testdata; tarantoolctl rocks install queue 1.2.1 ) + ( cd ./queue/testdata; $(TTCTL) rocks install queue 1.2.1 ) .PHONY: datetime-timezones datetime-timezones: From de7e60bf309c498a64e49d2b86d03725854baba5 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 9 Jan 2023 11:06:24 +0300 Subject: [PATCH 391/605] code health: do not pass a schema to init requests Initial requests should not use a schema because it has not been loaded yet. Part of #218 --- connection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connection.go b/connection.go index 331e1c805..3e88ae4b7 100644 --- a/connection.go +++ b/connection.go @@ -702,7 +702,7 @@ func pack(h *smallWBuf, enc *encoder, reqid uint32, func (conn *Connection) writeRequest(w *bufio.Writer, req Request) error { var packet smallWBuf - err := pack(&packet, newEncoder(&packet), 0, req, ignoreStreamId, conn.Schema) + err := pack(&packet, newEncoder(&packet), 0, req, ignoreStreamId, nil) if err != nil { return fmt.Errorf("pack error: %w", err) From 11b716d13a4f3bdd9373d14e20fbcd7c37a69fbf Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 9 Jan 2023 14:49:37 +0300 Subject: [PATCH 392/605] bugfix: load and usage schema data race We need to block shards to avoid schema usage by concurrent requests. Now it can be a ping request or a watch request so it does not look critical. We don't expect many of this requests and such requests do not use schema at all. Part of #218 --- connection.go | 3 +++ schema.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/connection.go b/connection.go index 3e88ae4b7..9a4b6f3d6 100644 --- a/connection.go +++ b/connection.go @@ -1368,6 +1368,9 @@ func (conn *Connection) OverrideSchema(s *Schema) { if s != nil { conn.mutex.Lock() defer conn.mutex.Unlock() + conn.lockShards() + defer conn.unlockShards() + conn.Schema = s } } diff --git a/schema.go b/schema.go index 35f65eb28..a182e8b10 100644 --- a/schema.go +++ b/schema.go @@ -329,7 +329,10 @@ func (conn *Connection) loadSchema() (err error) { schema.SpacesById[index.SpaceId].Indexes[index.Name] = index } + conn.lockShards() conn.Schema = schema + conn.unlockShards() + return nil } From d7974ea8b95599aa71cd92183804a36fb27dac7d Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 9 Jan 2023 14:54:05 +0300 Subject: [PATCH 393/605] bugfix: read state data race We need to use an atomic method to read the atomic value. Part of #218 --- connection.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/connection.go b/connection.go index 9a4b6f3d6..efdd09e65 100644 --- a/connection.go +++ b/connection.go @@ -1033,7 +1033,7 @@ func (conn *Connection) newFuture(ctx context.Context) (fut *Future) { shardn := fut.requestId & (conn.opts.Concurrency - 1) shard := &conn.shard[shardn] shard.rmut.Lock() - switch conn.state { + switch atomic.LoadUint32(&conn.state) { case connClosed: fut.err = ClientError{ ErrConnectionClosed, @@ -1733,9 +1733,10 @@ func (conn *Connection) shutdown() { conn.mutex.Lock() defer conn.mutex.Unlock() - if !atomic.CompareAndSwapUint32(&(conn.state), connConnected, connShutdown) { + if !atomic.CompareAndSwapUint32(&conn.state, connConnected, connShutdown) { return } + conn.cond.Broadcast() conn.notify(Shutdown) From ec7e2b3a48204dcf7172c94921fb591ad5da95ea Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 9 Jan 2023 18:01:19 +0300 Subject: [PATCH 394/605] bugfix: TestFutureSetStateRaceCondition data race A use-case from our code: a response per AppendPush(). It looks like it's enough to change the test. Part of #218 --- future_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/future_test.go b/future_test.go index bae974158..e7e9ab507 100644 --- a/future_test.go +++ b/future_test.go @@ -237,13 +237,13 @@ func TestFutureGetIteratorError(t *testing.T) { func TestFutureSetStateRaceCondition(t *testing.T) { err := errors.New("any error") resp := &Response{} - respAppend := &Response{} for i := 0; i < 1000; i++ { fut := NewFuture() for j := 0; j < 9; j++ { go func(opt int) { if opt%3 == 0 { + respAppend := &Response{} fut.AppendPush(respAppend) } else if opt%3 == 1 { fut.SetError(err) From 444509cdc5c458a3f50fc962da87348e1471e92e Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 9 Jan 2023 17:58:59 +0300 Subject: [PATCH 395/605] bugfix: ConnectionMulti update addresses data race We need to update an addresses slice under a lock because we use the slice in ConnectionMulti.getCurrentConnection(). Part of #218 --- multi/multi.go | 2 ++ multi/multi_test.go | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/multi/multi.go b/multi/multi.go index ea950cfdf..7b4b65839 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -212,7 +212,9 @@ func (connMulti *ConnectionMulti) checker() { connMulti.deleteConnectionFromPool(v) } } + connMulti.mutex.Lock() connMulti.addrs = addrs + connMulti.mutex.Unlock() } case <-timer.C: for _, addr := range connMulti.addrs { diff --git a/multi/multi_test.go b/multi/multi_test.go index ad4fd1962..ad98912c2 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -200,13 +200,18 @@ func TestRefresh(t *testing.T) { t.Errorf("conn is nil after Connect") return } + + multiConn.mutex.RLock() curAddr := multiConn.addrs[0] + multiConn.mutex.RUnlock() // Wait for refresh timer. // Scenario 1 nodeload, 1 refresh, 1 nodeload. time.Sleep(10 * time.Second) + multiConn.mutex.RLock() newAddr := multiConn.addrs[0] + multiConn.mutex.RUnlock() if curAddr == newAddr { t.Errorf("Expect address refresh") From a3b5b973460646a54012be61aeac968870b61c07 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 9 Jan 2023 18:00:07 +0300 Subject: [PATCH 396/605] bugfix: ConnectionMulti.Close() data race We need to use an atomic method to update the atomic value. Part of #218 --- multi/multi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi/multi.go b/multi/multi.go index 7b4b65839..6aba2a426 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -263,7 +263,7 @@ func (connMulti *ConnectionMulti) Close() (err error) { defer connMulti.mutex.Unlock() close(connMulti.control) - connMulti.state = connClosed + atomic.StoreUint32(&connMulti.state, connClosed) for _, conn := range connMulti.pool { if err == nil { From d8ca1c19dcbd8c9fba07e8c35addc3dfece1e0e8 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 9 Jan 2023 18:00:59 +0300 Subject: [PATCH 397/605] bugfix: poolWatcher.Unregister() data race You need to protect the poolWatcher.unregistered variable to avoid the data race. It does not look too critical because we don't expect that performance of poolWatcher.Unregister() is matter in cuncurrent calls case. It could also lead to crashes. Part of #218 --- connection_pool/watcher.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/connection_pool/watcher.go b/connection_pool/watcher.go index 2876f90bc..d60cfa171 100644 --- a/connection_pool/watcher.go +++ b/connection_pool/watcher.go @@ -33,7 +33,7 @@ func (c *watcherContainer) remove(watcher *poolWatcher) bool { if watcher == c.head { c.head = watcher.next return true - } else { + } else if c.head != nil { cur := c.head for cur.next != nil { if cur.next == watcher { @@ -85,7 +85,11 @@ type poolWatcher struct { // Unregister unregisters the pool watcher. func (w *poolWatcher) Unregister() { - if !w.unregistered && w.container.remove(w) { + w.mutex.Lock() + unregistered := w.unregistered + w.mutex.Unlock() + + if !unregistered && w.container.remove(w) { w.mutex.Lock() w.unregistered = true for _, watcher := range w.watchers { From 6a51ef1aeea56534d71864777411f2d6648dcf2c Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 9 Jan 2023 18:54:01 +0300 Subject: [PATCH 398/605] bugfix: TestConnectionHandlerOpenUpdateClose data race It is better to use atomic counters. Part of #218 --- connection_pool/connection_pool_test.go | 44 ++++++++++++++----------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 970612b5e..b4451074c 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -7,6 +7,7 @@ import ( "reflect" "strings" "sync" + "sync/atomic" "testing" "time" @@ -235,9 +236,8 @@ func TestClose(t *testing.T) { } type testHandler struct { - discovered, deactivated int + discovered, deactivated uint32 errs []error - mutex sync.Mutex } func (h *testHandler) addErr(err error) { @@ -246,10 +246,7 @@ func (h *testHandler) addErr(err error) { func (h *testHandler) Discovered(conn *tarantool.Connection, role connection_pool.Role) error { - h.mutex.Lock() - defer h.mutex.Unlock() - - h.discovered++ + discovered := atomic.AddUint32(&h.discovered, 1) if conn == nil { h.addErr(fmt.Errorf("discovered conn == nil")) @@ -260,14 +257,14 @@ func (h *testHandler) Discovered(conn *tarantool.Connection, // discovered >= 3 - update a connection after a role update addr := conn.Addr() if addr == servers[0] { - if h.discovered < 3 && role != connection_pool.MasterRole { + if discovered < 3 && role != connection_pool.MasterRole { h.addErr(fmt.Errorf("unexpected init role %d for addr %s", role, addr)) } - if h.discovered >= 3 && role != connection_pool.ReplicaRole { + if discovered >= 3 && role != connection_pool.ReplicaRole { h.addErr(fmt.Errorf("unexpected updated role %d for addr %s", role, addr)) } } else if addr == servers[1] { - if h.discovered >= 3 { + if discovered >= 3 { h.addErr(fmt.Errorf("unexpected discovery for addr %s", addr)) } if role != connection_pool.ReplicaRole { @@ -282,10 +279,7 @@ func (h *testHandler) Discovered(conn *tarantool.Connection, func (h *testHandler) Deactivated(conn *tarantool.Connection, role connection_pool.Role) error { - h.mutex.Lock() - defer h.mutex.Unlock() - - h.deactivated++ + deactivated := atomic.AddUint32(&h.deactivated, 1) if conn == nil { h.addErr(fmt.Errorf("removed conn == nil")) @@ -293,7 +287,7 @@ func (h *testHandler) Deactivated(conn *tarantool.Connection, } addr := conn.Addr() - if h.deactivated == 1 && addr == servers[0] { + if deactivated == 1 && addr == servers[0] { // A first close is a role update. if role != connection_pool.MasterRole { h.addErr(fmt.Errorf("unexpected removed role %d for addr %s", role, addr)) @@ -337,19 +331,24 @@ func TestConnectionHandlerOpenUpdateClose(t *testing.T) { for i := 0; i < 100; i++ { // Wait for read_only update, it should report about close connection // with old role. - if h.discovered >= 3 { + if atomic.LoadUint32(&h.discovered) >= 3 { break } time.Sleep(poolOpts.CheckTimeout) } - require.Equalf(t, h.deactivated, 1, "updated not reported as deactivated") - require.Equalf(t, h.discovered, 3, "updated not reported as discovered") + + discovered := atomic.LoadUint32(&h.discovered) + deactivated := atomic.LoadUint32(&h.deactivated) + require.Equalf(t, uint32(3), discovered, + "updated not reported as discovered") + require.Equalf(t, uint32(1), deactivated, + "updated not reported as deactivated") pool.Close() for i := 0; i < 100; i++ { // Wait for close of all connections. - if h.deactivated >= 3 { + if atomic.LoadUint32(&h.deactivated) >= 3 { break } time.Sleep(poolOpts.CheckTimeout) @@ -361,8 +360,13 @@ func TestConnectionHandlerOpenUpdateClose(t *testing.T) { connected, err := pool.ConnectedNow(connection_pool.ANY) require.Nilf(t, err, "failed to get connected state") require.Falsef(t, connected, "connection pool still be connected") - require.Equalf(t, len(poolServers)+1, h.discovered, "unexpected discovered count") - require.Equalf(t, len(poolServers)+1, h.deactivated, "unexpected deactivated count") + + discovered = atomic.LoadUint32(&h.discovered) + deactivated = atomic.LoadUint32(&h.deactivated) + require.Equalf(t, uint32(len(poolServers)+1), discovered, + "unexpected discovered count") + require.Equalf(t, uint32(len(poolServers)+1), deactivated, + "unexpected deactivated count") } type testAddErrorHandler struct { From bda241e944617c18eaf57f7df929e14273011fda Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 9 Jan 2023 19:02:42 +0300 Subject: [PATCH 399/605] bugfix: TestConnectionHandlerUpdateError data race It is better to use atomic counters. Part of #218 --- connection_pool/connection_pool_test.go | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index b4451074c..860786fbe 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -403,18 +403,14 @@ func TestConnectionHandlerOpenError(t *testing.T) { } type testUpdateErrorHandler struct { - discovered, deactivated int - mutex sync.Mutex + discovered, deactivated uint32 } func (h *testUpdateErrorHandler) Discovered(conn *tarantool.Connection, role connection_pool.Role) error { - h.mutex.Lock() - defer h.mutex.Unlock() - - h.discovered++ + atomic.AddUint32(&h.discovered, 1) - if h.deactivated != 0 { + if atomic.LoadUint32(&h.deactivated) != 0 { // Don't add a connection into a pool again after it was deleted. return fmt.Errorf("any error") } @@ -423,10 +419,7 @@ func (h *testUpdateErrorHandler) Discovered(conn *tarantool.Connection, func (h *testUpdateErrorHandler) Deactivated(conn *tarantool.Connection, role connection_pool.Role) error { - h.mutex.Lock() - defer h.mutex.Unlock() - - h.deactivated++ + atomic.AddUint32(&h.deactivated, 1) return nil } @@ -477,7 +470,9 @@ func TestConnectionHandlerUpdateError(t *testing.T) { require.Nilf(t, err, "failed to get ConnectedNow()") require.Falsef(t, connected, "should be deactivated") - require.GreaterOrEqualf(t, h.discovered, h.deactivated, "discovered < deactivated") + discovered := atomic.LoadUint32(&h.discovered) + deactivated := atomic.LoadUint32(&h.deactivated) + require.GreaterOrEqualf(t, discovered, deactivated, "discovered < deactivated") require.Nilf(t, err, "failed to get ConnectedNow()") } From 81a613d236f1ffaf071abbe4396a3b3fced15c72 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 16 Jan 2023 13:30:51 +0300 Subject: [PATCH 400/605] bugfix: queue/Example_connectionPool data race Part of #218 --- queue/example_connection_pool_test.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go index 214e67274..e41cdc639 100644 --- a/queue/example_connection_pool_test.go +++ b/queue/example_connection_pool_test.go @@ -89,8 +89,8 @@ func (h *QueueConnectionHandler) Discovered(conn *tarantool.Connection, } } - atomic.AddInt32(&h.masterCnt, 1) fmt.Printf("Master %s is ready to work!\n", conn.Addr()) + atomic.AddInt32(&h.masterCnt, 1) return nil } @@ -185,8 +185,12 @@ func Example_connectionPool() { // Wait for a new master instance re-identification. <-h.masterUpdated - if h.err != nil { - fmt.Printf("Unable to re-identify in the pool: %s", h.err) + h.mutex.Lock() + err = h.err + h.mutex.Unlock() + + if err != nil { + fmt.Printf("Unable to re-identify in the pool: %s", err) return } From f300bd920138a16dfb881428a32650b34e1362f5 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 9 Jan 2023 18:29:48 +0300 Subject: [PATCH 401/605] ci: add tests with data race detector Tests execution result may differ due to different timings, so it is better to test together, rather than instead. Closes #218 --- .github/workflows/testing.yml | 20 ++++++++++++++++---- CHANGELOG.md | 3 +++ CONTRIBUTING.md | 7 +++++++ Makefile | 6 ++++++ 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index c4516e5bd..05fee7835 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -97,16 +97,24 @@ jobs: run: make deps - name: Run regression tests - run: make test + run: | + make test + make testrace - name: Run regression tests with call_17 - run: make test TAGS="go_tarantool_call_17" + run: | + make test TAGS="go_tarantool_call_17" + make testrace TAGS="go_tarantool_call_17" - name: Run regression tests with msgpack.v5 - run: make test TAGS="go_tarantool_msgpack_v5" + run: | + make test TAGS="go_tarantool_msgpack_v5" + make testrace TAGS="go_tarantool_msgpack_v5" - name: Run regression tests with msgpack.v5 and call_17 - run: make test TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" + run: | + make test TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" + make testrace TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" - name: Run fuzzing tests if: ${{ matrix.fuzzing }} @@ -189,6 +197,7 @@ jobs: run: | source tarantool-enterprise/env.sh make test + make testrace env: TEST_TNT_SSL: ${{matrix.ssl}} @@ -196,6 +205,7 @@ jobs: run: | source tarantool-enterprise/env.sh make test TAGS="go_tarantool_call_17" + make testrace TAGS="go_tarantool_call_17" env: TEST_TNT_SSL: ${{matrix.ssl}} @@ -203,6 +213,7 @@ jobs: run: | source tarantool-enterprise/env.sh make test TAGS="go_tarantool_msgpack_v5" + make testrace TAGS="go_tarantool_msgpack_v5" env: TEST_TNT_SSL: ${{matrix.ssl}} @@ -210,6 +221,7 @@ jobs: run: | source tarantool-enterprise/env.sh make test TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" + make testrace TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" env: TEST_TNT_SSL: ${{matrix.ssl}} diff --git a/CHANGELOG.md b/CHANGELOG.md index acfe2a914..48b38049e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,14 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added - Support pagination (#246) +- A Makefile target to test with race detector (#218) ### Changed ### Fixed +- Several non-critical data race issues (#218) + ## [1.10.0] - 2022-12-31 The release improves compatibility with new Tarantool versions. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 15bccd3a6..ba0f0c7ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,12 +26,18 @@ To run tests for the main package and each subpackage: make test ``` +To run tests for the main package and each subpackage with race detector: +```bash +make testrace +``` + The tests set up all required `tarantool` processes before run and clean up afterwards. If you want to run the tests with specific build tags: ```bash make test TAGS=go_tarantool_ssl_disable,go_tarantool_msgpack_v5 +make testrace TAGS=go_tarantool_ssl_disable,go_tarantool_msgpack_v5 ``` If you have Tarantool Enterprise Edition 2.10 or newer, you can run additional @@ -39,6 +45,7 @@ SSL tests. To do this, you need to set an environment variable 'TEST_TNT_SSL': ```bash TEST_TNT_SSL=true make test +TEST_TNT_SSL=true make testrace ``` If you want to run the tests for a specific package: diff --git a/Makefile b/Makefile index be1f6757f..60016ee3c 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,12 @@ test: testdata: (cd ./testdata; ./generate.sh) +.PHONY: testrace +testrace: + @echo "Running all packages tests with data race detector" + go clean -testcache + go test -race -tags "$(TAGS)" ./... -v -p 1 + .PHONY: test-connection-pool test-connection-pool: @echo "Running tests in connection_pool package" From a22527a8d12b4b1f9d98a99b7da2de5ff892d075 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 16 Jan 2023 13:14:53 +0300 Subject: [PATCH 402/605] ci: fix benchstat build with go 1.13 --- Makefile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 60016ee3c..34de54707 100644 --- a/Makefile +++ b/Makefile @@ -123,7 +123,13 @@ ${BENCH_PATH} bench-deps: rm -rf ${BENCH_PATH} mkdir ${BENCH_PATH} go clean -testcache - cd ${BENCH_PATH} && git clone https://go.googlesource.com/perf && cd perf && go install ./cmd/benchstat + # It is unable to build a latest version of benchstat with go 1.13. So + # we need to switch to an old commit. + cd ${BENCH_PATH} && \ + git clone https://go.googlesource.com/perf && \ + cd perf && \ + git checkout 91a04616dc65ba76dbe9e5cf746b923b1402d303 && \ + go install ./cmd/benchstat rm -rf ${BENCH_PATH}/perf .PHONY: bench From eac63ed6fa276f42159d512c589f2f71269ebd65 Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Wed, 11 Jan 2023 17:06:03 +0300 Subject: [PATCH 403/605] doc: move recommendations to a tarantool's wiki --- CONTRIBUTING.md | 45 +++++---------------------------------------- 1 file changed, 5 insertions(+), 40 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba0f0c7ab..c83cde001 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -139,47 +139,12 @@ Note: the variable `BENCH_PATH` is not purposed to be used with absolute paths. ## Recommendations for how to achieve stable results -Before any judgments, verify whether results are stable on given host and how large the noise. Run `make bench-diff` without changes and look on the report. Several times. +Before any judgments, verify whether results are stable on given host and how +large the noise. Run `make bench-diff` without changes and look on the report. +Several times. -There are suggestions how to achieve best results: - -* Close all background applications, especially web browser. Look at `top` (`htop`, `atop`, ...) and if something bubbles there, close it. -* Disable cron daemon. -* Disable TurboBoost and set fixed frequency. - * If you're using `intel_pstate` frequency driver (it is usually default): - - Disable TurboBoost: - - ```shell - $ echo 0 > /sys/devices/system/cpu/intel_pstate/no_turbo - ``` - - Set fixed frequency: not sure it is possible. - - * If you're using `acpi-cpufreq` driver: - - Ensure you actually don't use intel_pstate: - - ```shell - $ grep -o 'intel_pstate=\w\+' /proc/cmdline - intel_pstate=disable - $ cpupower -c all frequency-info | grep driver: - driver: acpi-cpufreq - <...> - ``` - - Disable TurboBoost: - - ```shell - $ echo 0 > /sys/devices/system/cpu/cpufreq/boost - ``` - - Set fixed frequency: - - ```shell - $ cpupower -c all frequency-set -g userspace - $ cpupower -c all frequency-set -f 1.80GHz # adjust for your CPU - ``` +There are suggestions how to achieve best results, see +https://github.com/tarantool/tarantool/wiki/Benchmarking ## Code review checklist From 3c93506f351d1c541a645d258b6d154689268b9c Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 7 Mar 2023 09:59:32 +0300 Subject: [PATCH 404/605] mod: bump version of go-openssl The patch fixes build on macOS with Apple M1. Related to https://github.com/tarantool/tt/issues/308 Closes #260 --- CHANGELOG.md | 1 + go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48b38049e..ec3e3818f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ The release improves compatibility with new Tarantool versions. package-level variable decimalPrecision (#233) - Flaky test TestClientRequestObjectsWithContext (#244) - Flaky test multi/TestDisconnectAll (#234) +- Build on macOS with Apple M1 (#260) ## [1.9.0] - 2022-11-02 diff --git a/go.mod b/go.mod index 2f3b8c226..eaed3ca30 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.7.1 // indirect - github.com/tarantool/go-openssl v0.0.8-0.20220711094538-d93c1eff4f49 + github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/vmihailenco/msgpack.v2 v2.9.2 diff --git a/go.sum b/go.sum index 38e70ec44..57fa152cc 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tarantool/go-openssl v0.0.8-0.20220711094538-d93c1eff4f49 h1:rZYYi1cI3QXZ3yRFZd2ItYM1XA2BaJqP0buDroMbjNo= -github.com/tarantool/go-openssl v0.0.8-0.20220711094538-d93c1eff4f49/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= +github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 h1:/AN3eUPsTlvF6W+Ng/8ZjnSU6o7L0H4Wb9GMks6RkzU= +github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= From 1469484ae308963673dab8579e85d8aa476e621d Mon Sep 17 00:00:00 2001 From: AnaNek Date: Wed, 11 Jan 2023 12:55:09 +0300 Subject: [PATCH 405/605] api: add CRUD module support This patch provides crud [1] methods as request objects to support CRUD API. The following methods are supported: * `insert` * `insert_object` * `insert_many` * `insert_object_many` * `get` * `update` * `delete` * `replace` * `replace_object` * `replace_many` * `replace_object_many` * `upsert` * `upsert_object` * `upsert_many` * `upsert_object_many` * `select` * `min` * `max` * `truncate` * `len` * `storage_info` * `count` * `stats` * `unflatten_rows` 1. https://github.com/tarantool/crud Closes #108 --- CHANGELOG.md | 1 + Makefile | 10 +- crud/common.go | 95 +++++ crud/conditions.go | 28 ++ crud/count.go | 109 ++++++ crud/delete.go | 66 ++++ crud/error.go | 86 +++++ crud/error_test.go | 27 ++ crud/get.go | 101 +++++ crud/insert.go | 125 ++++++ crud/insert_many.go | 125 ++++++ crud/len.go | 56 +++ crud/max.go | 66 ++++ crud/min.go | 66 ++++ crud/msgpack.go | 29 ++ crud/msgpack_v5.go | 29 ++ crud/operations.go | 33 ++ crud/options.go | 306 +++++++++++++++ crud/replace.go | 125 ++++++ crud/replace_many.go | 125 ++++++ crud/request_test.go | 545 ++++++++++++++++++++++++++ crud/result.go | 285 ++++++++++++++ crud/select.go | 117 ++++++ crud/stats.go | 46 +++ crud/storage_info.go | 130 +++++++ crud/tarantool_test.go | 807 +++++++++++++++++++++++++++++++++++++++ crud/testdata/config.lua | 99 +++++ crud/truncate.go | 56 +++ crud/unflatten_rows.go | 40 ++ crud/update.go | 77 ++++ crud/upsert.go | 147 +++++++ crud/upsert_many.go | 144 +++++++ go.mod | 3 +- go.sum | 9 + request_test.go | 17 +- tarantool_test.go | 74 ++-- test_helpers/main.go | 30 ++ test_helpers/utils.go | 15 + 38 files changed, 4185 insertions(+), 64 deletions(-) create mode 100644 crud/common.go create mode 100644 crud/conditions.go create mode 100644 crud/count.go create mode 100644 crud/delete.go create mode 100644 crud/error.go create mode 100644 crud/error_test.go create mode 100644 crud/get.go create mode 100644 crud/insert.go create mode 100644 crud/insert_many.go create mode 100644 crud/len.go create mode 100644 crud/max.go create mode 100644 crud/min.go create mode 100644 crud/msgpack.go create mode 100644 crud/msgpack_v5.go create mode 100644 crud/operations.go create mode 100644 crud/options.go create mode 100644 crud/replace.go create mode 100644 crud/replace_many.go create mode 100644 crud/request_test.go create mode 100644 crud/result.go create mode 100644 crud/select.go create mode 100644 crud/stats.go create mode 100644 crud/storage_info.go create mode 100644 crud/tarantool_test.go create mode 100644 crud/testdata/config.lua create mode 100644 crud/truncate.go create mode 100644 crud/unflatten_rows.go create mode 100644 crud/update.go create mode 100644 crud/upsert.go create mode 100644 crud/upsert_many.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ec3e3818f..cec9d35f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support pagination (#246) - A Makefile target to test with race detector (#218) +- Support CRUD API (#108) ### Changed diff --git a/Makefile b/Makefile index 34de54707..2f4dd8d93 100644 --- a/Makefile +++ b/Makefile @@ -21,12 +21,13 @@ endif .PHONY: clean clean: - ( cd ./queue; rm -rf .rocks ) + ( rm -rf queue/testdata/.rocks crud/testdata/.rocks ) rm -f $(COVERAGE_FILE) .PHONY: deps deps: clean ( cd ./queue/testdata; $(TTCTL) rocks install queue 1.2.1 ) + ( cd ./crud/testdata; $(TTCTL) rocks install crud 1.0.0 ) .PHONY: datetime-timezones datetime-timezones: @@ -99,6 +100,13 @@ test-settings: go clean -testcache go test -tags "$(TAGS)" ./settings/ -v -p 1 +.PHONY: test-crud +test-crud: + @echo "Running tests in crud package" + cd ./crud/testdata && tarantool -e "require('crud')" + go clean -testcache + go test -tags "$(TAGS)" ./crud/ -v -p 1 + .PHONY: test-main test-main: @echo "Running tests in main package" diff --git a/crud/common.go b/crud/common.go new file mode 100644 index 000000000..877ac2d4a --- /dev/null +++ b/crud/common.go @@ -0,0 +1,95 @@ +// Package crud with support of API of Tarantool's CRUD module. +// +// Supported CRUD methods: +// +// - insert +// +// - insert_object +// +// - insert_many +// +// - insert_object_many +// +// - get +// +// - update +// +// - delete +// +// - replace +// +// - replace_object +// +// - replace_many +// +// - replace_object_many +// +// - upsert +// +// - upsert_object +// +// - upsert_many +// +// - upsert_object_many +// +// - select +// +// - min +// +// - max +// +// - truncate +// +// - len +// +// - storage_info +// +// - count +// +// - stats +// +// - unflatten_rows +// +// Since: 1.11.0. +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// Tuple is a type to describe tuple for CRUD methods. +type Tuple = []interface{} + +type baseRequest struct { + impl *tarantool.CallRequest +} + +func (req *baseRequest) initImpl(methodName string) { + req.impl = tarantool.NewCall17Request(methodName) +} + +// Code returns IPROTO code for CRUD request. +func (req *baseRequest) Code() int32 { + return req.impl.Code() +} + +// Ctx returns a context of CRUD request. +func (req *baseRequest) Ctx() context.Context { + return req.impl.Ctx() +} + +// Async returns is CRUD request expects a response. +func (req *baseRequest) Async() bool { + return req.impl.Async() +} + +type spaceRequest struct { + baseRequest + space string +} + +func (req *spaceRequest) setSpace(space string) { + req.space = space +} diff --git a/crud/conditions.go b/crud/conditions.go new file mode 100644 index 000000000..c11da1c29 --- /dev/null +++ b/crud/conditions.go @@ -0,0 +1,28 @@ +package crud + +// Operator is a type to describe operator of operation. +type Operator string + +const ( + // Eq - comparison operator for "equal". + Eq Operator = "=" + // Lt - comparison operator for "less than". + Lt Operator = "<" + // Le - comparison operator for "less than or equal". + Le Operator = "<=" + // Gt - comparison operator for "greater than". + Gt Operator = ">" + // Ge - comparison operator for "greater than or equal". + Ge Operator = ">=" +) + +// Condition describes CRUD condition as a table +// {operator, field-identifier, value}. +type Condition struct { + // Instruct msgpack to pack this struct as array, so no custom packer + // is needed. + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Operator Operator + Field interface{} // Field name, field number or index name. + Value interface{} +} diff --git a/crud/count.go b/crud/count.go new file mode 100644 index 000000000..2a3992389 --- /dev/null +++ b/crud/count.go @@ -0,0 +1,109 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// CountResult describes result for `crud.count` method. +type CountResult = NumberResult + +// CountOpts describes options for `crud.count` method. +type CountOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Mode is a parameter with `write`/`read` possible values, + // if `write` is specified then operation is performed on master. + Mode OptString + // PreferReplica is a parameter to specify preferred target + // as one of the replicas. + PreferReplica OptBool + // Balance is a parameter to use replica according to vshard + // load balancing policy. + Balance OptBool + // YieldEvery describes number of tuples processed to yield after. + YieldEvery OptUint + // BucketId is a bucket ID. + BucketId OptUint + // ForceMapCall describes the map call is performed without any + // optimizations even if full primary key equal condition is specified. + ForceMapCall OptBool + // Fullscan describes if a critical log entry will be skipped on + // potentially long count. + Fullscan OptBool +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts CountOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 9 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Mode, opts.PreferReplica, opts.Balance, + opts.YieldEvery, opts.BucketId, opts.ForceMapCall, + opts.Fullscan} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + modeOptName, preferReplicaOptName, balanceOptName, + yieldEveryOptName, bucketIdOptName, forceMapCallOptName, + fullscanOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// CountRequest helps you to create request object to call `crud.count` +// for execution by a Connection. +type CountRequest struct { + spaceRequest + conditions []Condition + opts CountOpts +} + +type countArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Conditions []Condition + Opts CountOpts +} + +// NewCountRequest returns a new empty CountRequest. +func NewCountRequest(space string) *CountRequest { + req := new(CountRequest) + req.initImpl("crud.count") + req.setSpace(space) + req.conditions = nil + req.opts = CountOpts{} + return req +} + +// Conditions sets the conditions for the CountRequest request. +// Note: default value is nil. +func (req *CountRequest) Conditions(conditions []Condition) *CountRequest { + req.conditions = conditions + return req +} + +// Opts sets the options for the CountRequest request. +// Note: default value is nil. +func (req *CountRequest) Opts(opts CountOpts) *CountRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *CountRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := countArgs{Space: req.space, Conditions: req.conditions, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *CountRequest) Context(ctx context.Context) *CountRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/delete.go b/crud/delete.go new file mode 100644 index 000000000..6592d1519 --- /dev/null +++ b/crud/delete.go @@ -0,0 +1,66 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// DeleteResult describes result for `crud.delete` method. +type DeleteResult = Result + +// DeleteOpts describes options for `crud.delete` method. +type DeleteOpts = SimpleOperationOpts + +// DeleteRequest helps you to create request object to call `crud.delete` +// for execution by a Connection. +type DeleteRequest struct { + spaceRequest + key Tuple + opts DeleteOpts +} + +type deleteArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Key Tuple + Opts DeleteOpts +} + +// NewDeleteRequest returns a new empty DeleteRequest. +func NewDeleteRequest(space string) *DeleteRequest { + req := new(DeleteRequest) + req.initImpl("crud.delete") + req.setSpace(space) + req.key = Tuple{} + req.opts = DeleteOpts{} + return req +} + +// Key sets the key for the DeleteRequest request. +// Note: default value is nil. +func (req *DeleteRequest) Key(key Tuple) *DeleteRequest { + req.key = key + return req +} + +// Opts sets the options for the DeleteRequest request. +// Note: default value is nil. +func (req *DeleteRequest) Opts(opts DeleteOpts) *DeleteRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *DeleteRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := deleteArgs{Space: req.space, Key: req.key, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *DeleteRequest) Context(ctx context.Context) *DeleteRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/error.go b/crud/error.go new file mode 100644 index 000000000..467c350a4 --- /dev/null +++ b/crud/error.go @@ -0,0 +1,86 @@ +package crud + +import "strings" + +// Error describes CRUD error object. +type Error struct { + // ClassName is an error class that implies its source (for example, "CountError"). + ClassName string + // Err is the text of reason. + Err string + // File is a source code file where the error was caught. + File string + // Line is a number of line in the source code file where the error was caught. + Line uint64 + // Stack is an information about the call stack when an error + // occurs in a string format. + Stack string + // Str is the text of reason with error class. + Str string +} + +// DecodeMsgpack provides custom msgpack decoder. +func (e *Error) DecodeMsgpack(d *decoder) error { + l, err := d.DecodeMapLen() + if err != nil { + return err + } + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + switch key { + case "class_name": + if e.ClassName, err = d.DecodeString(); err != nil { + return err + } + case "err": + if e.Err, err = d.DecodeString(); err != nil { + return err + } + case "file": + if e.File, err = d.DecodeString(); err != nil { + return err + } + case "line": + if e.Line, err = d.DecodeUint64(); err != nil { + return err + } + case "stack": + if e.Stack, err = d.DecodeString(); err != nil { + return err + } + case "str": + if e.Str, err = d.DecodeString(); err != nil { + return err + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + + return nil +} + +// Error converts an Error to a string. +func (err Error) Error() string { + return err.Str +} + +// ErrorMany describes CRUD error object for `_many` methods. +type ErrorMany struct { + Errors []Error +} + +// Error converts an Error to a string. +func (errs ErrorMany) Error() string { + var str []string + for _, err := range errs.Errors { + str = append(str, err.Str) + } + + return strings.Join(str, "\n") +} diff --git a/crud/error_test.go b/crud/error_test.go new file mode 100644 index 000000000..8bd973399 --- /dev/null +++ b/crud/error_test.go @@ -0,0 +1,27 @@ +package crud_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/crud" +) + +func TestErrorMany(t *testing.T) { + errs := crud.ErrorMany{Errors: []crud.Error{ + { + ClassName: "a", + Str: "msg 1", + }, + { + ClassName: "b", + Str: "msg 2", + }, + { + ClassName: "c", + Str: "msg 3", + }, + }} + + require.Equal(t, "msg 1\nmsg 2\nmsg 3", errs.Error()) +} diff --git a/crud/get.go b/crud/get.go new file mode 100644 index 000000000..4234431f6 --- /dev/null +++ b/crud/get.go @@ -0,0 +1,101 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// GetResult describes result for `crud.get` method. +type GetResult = Result + +// GetOpts describes options for `crud.get` method. +type GetOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple + // BucketId is a bucket ID. + BucketId OptUint + // Mode is a parameter with `write`/`read` possible values, + // if `write` is specified then operation is performed on master. + Mode OptString + // PreferReplica is a parameter to specify preferred target + // as one of the replicas. + PreferReplica OptBool + // Balance is a parameter to use replica according to vshard + // load balancing policy. + Balance OptBool +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts GetOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 7 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Fields, opts.BucketId, opts.Mode, + opts.PreferReplica, opts.Balance} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + fieldsOptName, bucketIdOptName, modeOptName, + preferReplicaOptName, balanceOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// GetRequest helps you to create request object to call `crud.get` +// for execution by a Connection. +type GetRequest struct { + spaceRequest + key Tuple + opts GetOpts +} + +type getArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Key Tuple + Opts GetOpts +} + +// NewGetRequest returns a new empty GetRequest. +func NewGetRequest(space string) *GetRequest { + req := new(GetRequest) + req.initImpl("crud.get") + req.setSpace(space) + req.key = Tuple{} + req.opts = GetOpts{} + return req +} + +// Key sets the key for the GetRequest request. +// Note: default value is nil. +func (req *GetRequest) Key(key Tuple) *GetRequest { + req.key = key + return req +} + +// Opts sets the options for the GetRequest request. +// Note: default value is nil. +func (req *GetRequest) Opts(opts GetOpts) *GetRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *GetRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := getArgs{Space: req.space, Key: req.key, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *GetRequest) Context(ctx context.Context) *GetRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/insert.go b/crud/insert.go new file mode 100644 index 000000000..480300249 --- /dev/null +++ b/crud/insert.go @@ -0,0 +1,125 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// InsertResult describes result for `crud.insert` method. +type InsertResult = Result + +// InsertOpts describes options for `crud.insert` method. +type InsertOpts = SimpleOperationOpts + +// InsertRequest helps you to create request object to call `crud.insert` +// for execution by a Connection. +type InsertRequest struct { + spaceRequest + tuple Tuple + opts InsertOpts +} + +type insertArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Tuple Tuple + Opts InsertOpts +} + +// NewInsertRequest returns a new empty InsertRequest. +func NewInsertRequest(space string) *InsertRequest { + req := new(InsertRequest) + req.initImpl("crud.insert") + req.setSpace(space) + req.tuple = Tuple{} + req.opts = InsertOpts{} + return req +} + +// Tuple sets the tuple for the InsertRequest request. +// Note: default value is nil. +func (req *InsertRequest) Tuple(tuple Tuple) *InsertRequest { + req.tuple = tuple + return req +} + +// Opts sets the options for the insert request. +// Note: default value is nil. +func (req *InsertRequest) Opts(opts InsertOpts) *InsertRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *InsertRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := insertArgs{Space: req.space, Tuple: req.tuple, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *InsertRequest) Context(ctx context.Context) *InsertRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// InsertObjectResult describes result for `crud.insert_object` method. +type InsertObjectResult = Result + +// InsertObjectOpts describes options for `crud.insert_object` method. +type InsertObjectOpts = SimpleOperationObjectOpts + +// InsertObjectRequest helps you to create request object to call +// `crud.insert_object` for execution by a Connection. +type InsertObjectRequest struct { + spaceRequest + object Object + opts InsertObjectOpts +} + +type insertObjectArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Object Object + Opts InsertObjectOpts +} + +// NewInsertObjectRequest returns a new empty InsertObjectRequest. +func NewInsertObjectRequest(space string) *InsertObjectRequest { + req := new(InsertObjectRequest) + req.initImpl("crud.insert_object") + req.setSpace(space) + req.object = MapObject{} + req.opts = InsertObjectOpts{} + return req +} + +// Object sets the tuple for the InsertObjectRequest request. +// Note: default value is nil. +func (req *InsertObjectRequest) Object(object Object) *InsertObjectRequest { + req.object = object + return req +} + +// Opts sets the options for the InsertObjectRequest request. +// Note: default value is nil. +func (req *InsertObjectRequest) Opts(opts InsertObjectOpts) *InsertObjectRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *InsertObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := insertObjectArgs{Space: req.space, Object: req.object, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *InsertObjectRequest) Context(ctx context.Context) *InsertObjectRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/insert_many.go b/crud/insert_many.go new file mode 100644 index 000000000..11784f660 --- /dev/null +++ b/crud/insert_many.go @@ -0,0 +1,125 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// InsertManyResult describes result for `crud.insert_many` method. +type InsertManyResult = ResultMany + +// InsertManyOpts describes options for `crud.insert_many` method. +type InsertManyOpts = OperationManyOpts + +// InsertManyRequest helps you to create request object to call +// `crud.insert_many` for execution by a Connection. +type InsertManyRequest struct { + spaceRequest + tuples []Tuple + opts InsertManyOpts +} + +type insertManyArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Tuples []Tuple + Opts InsertManyOpts +} + +// NewInsertManyRequest returns a new empty InsertManyRequest. +func NewInsertManyRequest(space string) *InsertManyRequest { + req := new(InsertManyRequest) + req.initImpl("crud.insert_many") + req.setSpace(space) + req.tuples = []Tuple{} + req.opts = InsertManyOpts{} + return req +} + +// Tuples sets the tuples for the InsertManyRequest request. +// Note: default value is nil. +func (req *InsertManyRequest) Tuples(tuples []Tuple) *InsertManyRequest { + req.tuples = tuples + return req +} + +// Opts sets the options for the InsertManyRequest request. +// Note: default value is nil. +func (req *InsertManyRequest) Opts(opts InsertManyOpts) *InsertManyRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *InsertManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := insertManyArgs{Space: req.space, Tuples: req.tuples, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *InsertManyRequest) Context(ctx context.Context) *InsertManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// InsertObjectManyResult describes result for `crud.insert_object_many` method. +type InsertObjectManyResult = ResultMany + +// InsertObjectManyOpts describes options for `crud.insert_object_many` method. +type InsertObjectManyOpts = OperationObjectManyOpts + +// InsertObjectManyRequest helps you to create request object to call +// `crud.insert_object_many` for execution by a Connection. +type InsertObjectManyRequest struct { + spaceRequest + objects []Object + opts InsertObjectManyOpts +} + +type insertObjectManyArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Objects []Object + Opts InsertObjectManyOpts +} + +// NewInsertObjectManyRequest returns a new empty InsertObjectManyRequest. +func NewInsertObjectManyRequest(space string) *InsertObjectManyRequest { + req := new(InsertObjectManyRequest) + req.initImpl("crud.insert_object_many") + req.setSpace(space) + req.objects = []Object{} + req.opts = InsertObjectManyOpts{} + return req +} + +// Objects sets the objects for the InsertObjectManyRequest request. +// Note: default value is nil. +func (req *InsertObjectManyRequest) Objects(objects []Object) *InsertObjectManyRequest { + req.objects = objects + return req +} + +// Opts sets the options for the InsertObjectManyRequest request. +// Note: default value is nil. +func (req *InsertObjectManyRequest) Opts(opts InsertObjectManyOpts) *InsertObjectManyRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *InsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := insertObjectManyArgs{Space: req.space, Objects: req.objects, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *InsertObjectManyRequest) Context(ctx context.Context) *InsertObjectManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/len.go b/crud/len.go new file mode 100644 index 000000000..6a8c85a2a --- /dev/null +++ b/crud/len.go @@ -0,0 +1,56 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// LenResult describes result for `crud.len` method. +type LenResult = NumberResult + +// LenOpts describes options for `crud.len` method. +type LenOpts = BaseOpts + +// LenRequest helps you to create request object to call `crud.len` +// for execution by a Connection. +type LenRequest struct { + spaceRequest + opts LenOpts +} + +type lenArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Opts LenOpts +} + +// NewLenRequest returns a new empty LenRequest. +func NewLenRequest(space string) *LenRequest { + req := new(LenRequest) + req.initImpl("crud.len") + req.setSpace(space) + req.opts = LenOpts{} + return req +} + +// Opts sets the options for the LenRequest request. +// Note: default value is nil. +func (req *LenRequest) Opts(opts LenOpts) *LenRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *LenRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := lenArgs{Space: req.space, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *LenRequest) Context(ctx context.Context) *LenRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/max.go b/crud/max.go new file mode 100644 index 000000000..842f24d5a --- /dev/null +++ b/crud/max.go @@ -0,0 +1,66 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// MaxResult describes result for `crud.max` method. +type MaxResult = Result + +// MaxOpts describes options for `crud.max` method. +type MaxOpts = BorderOpts + +// MaxRequest helps you to create request object to call `crud.max` +// for execution by a Connection. +type MaxRequest struct { + spaceRequest + index interface{} + opts MaxOpts +} + +type maxArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Index interface{} + Opts MaxOpts +} + +// NewMaxRequest returns a new empty MaxRequest. +func NewMaxRequest(space string) *MaxRequest { + req := new(MaxRequest) + req.initImpl("crud.max") + req.setSpace(space) + req.index = []interface{}{} + req.opts = MaxOpts{} + return req +} + +// Index sets the index name/id for the MaxRequest request. +// Note: default value is nil. +func (req *MaxRequest) Index(index interface{}) *MaxRequest { + req.index = index + return req +} + +// Opts sets the options for the MaxRequest request. +// Note: default value is nil. +func (req *MaxRequest) Opts(opts MaxOpts) *MaxRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *MaxRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := maxArgs{Space: req.space, Index: req.index, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *MaxRequest) Context(ctx context.Context) *MaxRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/min.go b/crud/min.go new file mode 100644 index 000000000..720b6f782 --- /dev/null +++ b/crud/min.go @@ -0,0 +1,66 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// MinResult describes result for `crud.min` method. +type MinResult = Result + +// MinOpts describes options for `crud.min` method. +type MinOpts = BorderOpts + +// MinRequest helps you to create request object to call `crud.min` +// for execution by a Connection. +type MinRequest struct { + spaceRequest + index interface{} + opts MinOpts +} + +type minArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Index interface{} + Opts MinOpts +} + +// NewMinRequest returns a new empty MinRequest. +func NewMinRequest(space string) *MinRequest { + req := new(MinRequest) + req.initImpl("crud.min") + req.setSpace(space) + req.index = []interface{}{} + req.opts = MinOpts{} + return req +} + +// Index sets the index name/id for the MinRequest request. +// Note: default value is nil. +func (req *MinRequest) Index(index interface{}) *MinRequest { + req.index = index + return req +} + +// Opts sets the options for the MinRequest request. +// Note: default value is nil. +func (req *MinRequest) Opts(opts MinOpts) *MinRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *MinRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := minArgs{Space: req.space, Index: req.index, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *MinRequest) Context(ctx context.Context) *MinRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/msgpack.go b/crud/msgpack.go new file mode 100644 index 000000000..fe65bd154 --- /dev/null +++ b/crud/msgpack.go @@ -0,0 +1,29 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + +package crud + +import ( + "io" + + "gopkg.in/vmihailenco/msgpack.v2" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +// Object is an interface to describe object for CRUD methods. +type Object interface { + EncodeMsgpack(enc *encoder) +} + +// MapObject is a type to describe object as a map. +type MapObject map[string]interface{} + +func (o MapObject) EncodeMsgpack(enc *encoder) { + enc.Encode(o) +} + +func NewEncoder(w io.Writer) *encoder { + return msgpack.NewEncoder(w) +} diff --git a/crud/msgpack_v5.go b/crud/msgpack_v5.go new file mode 100644 index 000000000..bfa936a83 --- /dev/null +++ b/crud/msgpack_v5.go @@ -0,0 +1,29 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package crud + +import ( + "io" + + "github.com/vmihailenco/msgpack/v5" +) + +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +// Object is an interface to describe object for CRUD methods. +type Object interface { + EncodeMsgpack(enc *encoder) +} + +// MapObject is a type to describe object as a map. +type MapObject map[string]interface{} + +func (o MapObject) EncodeMsgpack(enc *encoder) { + enc.Encode(o) +} + +func NewEncoder(w io.Writer) *encoder { + return msgpack.NewEncoder(w) +} diff --git a/crud/operations.go b/crud/operations.go new file mode 100644 index 000000000..5d55acc15 --- /dev/null +++ b/crud/operations.go @@ -0,0 +1,33 @@ +package crud + +const ( + // Add - operator for addition. + Add Operator = "+" + // Sub - operator for subtraction. + Sub Operator = "-" + // And - operator for bitwise AND. + And Operator = "&" + // Or - operator for bitwise OR. + Or Operator = "|" + // Xor - operator for bitwise XOR. + Xor Operator = "^" + // Splice - operator for string splice. + Splice Operator = ":" + // Insert - operator for insertion of a new field. + Insert Operator = "!" + // Delete - operator for deletion. + Delete Operator = "#" + // Assign - operator for assignment. + Assign Operator = "=" +) + +// Operation describes CRUD operation as a table +// {operator, field_identifier, value}. +type Operation struct { + // Instruct msgpack to pack this struct as array, so no custom packer + // is needed. + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Operator Operator + Field interface{} // Number or string. + Value interface{} +} diff --git a/crud/options.go b/crud/options.go new file mode 100644 index 000000000..c0a201033 --- /dev/null +++ b/crud/options.go @@ -0,0 +1,306 @@ +package crud + +import ( + "errors" + + "github.com/markphelps/optional" +) + +const ( + timeoutOptName = "timeout" + vshardRouterOptName = "vshard_router" + fieldsOptName = "fields" + bucketIdOptName = "bucket_id" + skipNullabilityCheckOnFlattenOptName = "skip_nullability_check_on_flatten" + stopOnErrorOptName = "stop_on_error" + rollbackOnErrorOptName = "rollback_on_error" + modeOptName = "mode" + preferReplicaOptName = "prefer_replica" + balanceOptName = "balance" + yieldEveryOptName = "yield_every" + forceMapCallOptName = "force_map_call" + fullscanOptName = "fullscan" + firstOptName = "first" + afterOptName = "after" + batchSizeOptName = "batch_size" +) + +type option interface { + getInterface() (interface{}, error) +} + +// OptUint is an optional uint. +type OptUint struct { + optional.Uint +} + +// NewOptUint creates an optional uint from value. +func NewOptUint(value uint) OptUint { + return OptUint{optional.NewUint(value)} +} + +func (opt OptUint) getInterface() (interface{}, error) { + return opt.Get() +} + +// OptInt is an optional int. +type OptInt struct { + optional.Int +} + +// NewOptInt creates an optional int from value. +func NewOptInt(value int) OptInt { + return OptInt{optional.NewInt(value)} +} + +func (opt OptInt) getInterface() (interface{}, error) { + return opt.Get() +} + +// OptString is an optional string. +type OptString struct { + optional.String +} + +// NewOptString creates an optional string from value. +func NewOptString(value string) OptString { + return OptString{optional.NewString(value)} +} + +func (opt OptString) getInterface() (interface{}, error) { + return opt.Get() +} + +// OptBool is an optional bool. +type OptBool struct { + optional.Bool +} + +// NewOptBool creates an optional bool from value. +func NewOptBool(value bool) OptBool { + return OptBool{optional.NewBool(value)} +} + +func (opt OptBool) getInterface() (interface{}, error) { + return opt.Get() +} + +// OptTuple is an optional tuple. +type OptTuple struct { + tuple []interface{} +} + +// NewOptTuple creates an optional tuple from tuple. +func NewOptTuple(tuple []interface{}) OptTuple { + return OptTuple{tuple} +} + +// Get returns the tuple value or an error if not present. +func (o *OptTuple) Get() ([]interface{}, error) { + if o.tuple == nil { + return nil, errors.New("value not present") + } + return o.tuple, nil +} + +func (opt OptTuple) getInterface() (interface{}, error) { + return opt.Get() +} + +// BaseOpts describes base options for CRUD operations. +type BaseOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts BaseOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 2 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// SimpleOperationOpts describes options for simple CRUD operations. +type SimpleOperationOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple + // BucketId is a bucket ID. + BucketId OptUint +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts SimpleOperationOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 4 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Fields, opts.BucketId} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + fieldsOptName, bucketIdOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// SimpleOperationObjectOpts describes options for simple CRUD +// operations with objects. +type SimpleOperationObjectOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple + // BucketId is a bucket ID. + BucketId OptUint + // SkipNullabilityCheckOnFlatten is a parameter to allow + // setting null values to non-nullable fields. + SkipNullabilityCheckOnFlatten OptBool +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts SimpleOperationObjectOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 5 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Fields, opts.BucketId, opts.SkipNullabilityCheckOnFlatten} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + fieldsOptName, bucketIdOptName, skipNullabilityCheckOnFlattenOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// OperationManyOpts describes options for CRUD operations with many tuples. +type OperationManyOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple + // StopOnError is a parameter to stop on a first error and report + // error regarding the failed operation and error about what tuples + // were not performed. + StopOnError OptBool + // RollbackOnError is a parameter because of what any failed operation + // will lead to rollback on a storage, where the operation is failed. + RollbackOnError OptBool +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts OperationManyOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 5 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Fields, opts.StopOnError, opts.RollbackOnError} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + fieldsOptName, stopOnErrorOptName, rollbackOnErrorOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// OperationObjectManyOpts describes options for CRUD operations +// with many objects. +type OperationObjectManyOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple + // StopOnError is a parameter to stop on a first error and report + // error regarding the failed operation and error about what tuples + // were not performed. + StopOnError OptBool + // RollbackOnError is a parameter because of what any failed operation + // will lead to rollback on a storage, where the operation is failed. + RollbackOnError OptBool + // SkipNullabilityCheckOnFlatten is a parameter to allow + // setting null values to non-nullable fields. + SkipNullabilityCheckOnFlatten OptBool +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts OperationObjectManyOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 6 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Fields, opts.StopOnError, opts.RollbackOnError, + opts.SkipNullabilityCheckOnFlatten} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + fieldsOptName, stopOnErrorOptName, rollbackOnErrorOptName, + skipNullabilityCheckOnFlattenOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// BorderOpts describes options for `crud.min` and `crud.max`. +type BorderOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts BorderOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 3 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, opts.Fields} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +func encodeOptions(enc *encoder, options []option, names []string, values []interface{}) error { + mapLen := 0 + + for i, opt := range options { + if value, err := opt.getInterface(); err == nil { + values[i] = value + mapLen += 1 + } + } + + if err := enc.EncodeMapLen(mapLen); err != nil { + return err + } + + if mapLen > 0 { + for i, name := range names { + if values[i] != nil { + enc.EncodeString(name) + enc.Encode(values[i]) + } + } + } + + return nil +} diff --git a/crud/replace.go b/crud/replace.go new file mode 100644 index 000000000..811a08eb8 --- /dev/null +++ b/crud/replace.go @@ -0,0 +1,125 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// ReplaceResult describes result for `crud.replace` method. +type ReplaceResult = Result + +// ReplaceOpts describes options for `crud.replace` method. +type ReplaceOpts = SimpleOperationOpts + +// ReplaceRequest helps you to create request object to call `crud.replace` +// for execution by a Connection. +type ReplaceRequest struct { + spaceRequest + tuple Tuple + opts ReplaceOpts +} + +type replaceArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Tuple Tuple + Opts ReplaceOpts +} + +// NewReplaceRequest returns a new empty ReplaceRequest. +func NewReplaceRequest(space string) *ReplaceRequest { + req := new(ReplaceRequest) + req.initImpl("crud.replace") + req.setSpace(space) + req.tuple = Tuple{} + req.opts = ReplaceOpts{} + return req +} + +// Tuple sets the tuple for the ReplaceRequest request. +// Note: default value is nil. +func (req *ReplaceRequest) Tuple(tuple Tuple) *ReplaceRequest { + req.tuple = tuple + return req +} + +// Opts sets the options for the ReplaceRequest request. +// Note: default value is nil. +func (req *ReplaceRequest) Opts(opts ReplaceOpts) *ReplaceRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *ReplaceRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := replaceArgs{Space: req.space, Tuple: req.tuple, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *ReplaceRequest) Context(ctx context.Context) *ReplaceRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// ReplaceObjectResult describes result for `crud.replace_object` method. +type ReplaceObjectResult = Result + +// ReplaceObjectOpts describes options for `crud.replace_object` method. +type ReplaceObjectOpts = SimpleOperationObjectOpts + +// ReplaceObjectRequest helps you to create request object to call +// `crud.replace_object` for execution by a Connection. +type ReplaceObjectRequest struct { + spaceRequest + object Object + opts ReplaceObjectOpts +} + +type replaceObjectArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Object Object + Opts ReplaceObjectOpts +} + +// NewReplaceObjectRequest returns a new empty ReplaceObjectRequest. +func NewReplaceObjectRequest(space string) *ReplaceObjectRequest { + req := new(ReplaceObjectRequest) + req.initImpl("crud.replace_object") + req.setSpace(space) + req.object = MapObject{} + req.opts = ReplaceObjectOpts{} + return req +} + +// Object sets the tuple for the ReplaceObjectRequest request. +// Note: default value is nil. +func (req *ReplaceObjectRequest) Object(object Object) *ReplaceObjectRequest { + req.object = object + return req +} + +// Opts sets the options for the ReplaceObjectRequest request. +// Note: default value is nil. +func (req *ReplaceObjectRequest) Opts(opts ReplaceObjectOpts) *ReplaceObjectRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *ReplaceObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := replaceObjectArgs{Space: req.space, Object: req.object, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *ReplaceObjectRequest) Context(ctx context.Context) *ReplaceObjectRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/replace_many.go b/crud/replace_many.go new file mode 100644 index 000000000..36350ac4b --- /dev/null +++ b/crud/replace_many.go @@ -0,0 +1,125 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// ReplaceManyResult describes result for `crud.replace_many` method. +type ReplaceManyResult = ResultMany + +// ReplaceManyOpts describes options for `crud.replace_many` method. +type ReplaceManyOpts = OperationManyOpts + +// ReplaceManyRequest helps you to create request object to call +// `crud.replace_many` for execution by a Connection. +type ReplaceManyRequest struct { + spaceRequest + tuples []Tuple + opts ReplaceManyOpts +} + +type replaceManyArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Tuples []Tuple + Opts ReplaceManyOpts +} + +// NewReplaceManyRequest returns a new empty ReplaceManyRequest. +func NewReplaceManyRequest(space string) *ReplaceManyRequest { + req := new(ReplaceManyRequest) + req.initImpl("crud.replace_many") + req.setSpace(space) + req.tuples = []Tuple{} + req.opts = ReplaceManyOpts{} + return req +} + +// Tuples sets the tuples for the ReplaceManyRequest request. +// Note: default value is nil. +func (req *ReplaceManyRequest) Tuples(tuples []Tuple) *ReplaceManyRequest { + req.tuples = tuples + return req +} + +// Opts sets the options for the ReplaceManyRequest request. +// Note: default value is nil. +func (req *ReplaceManyRequest) Opts(opts ReplaceManyOpts) *ReplaceManyRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *ReplaceManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := replaceManyArgs{Space: req.space, Tuples: req.tuples, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *ReplaceManyRequest) Context(ctx context.Context) *ReplaceManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// ReplaceObjectManyResult describes result for `crud.replace_object_many` method. +type ReplaceObjectManyResult = ResultMany + +// ReplaceObjectManyOpts describes options for `crud.replace_object_many` method. +type ReplaceObjectManyOpts = OperationObjectManyOpts + +// ReplaceObjectManyRequest helps you to create request object to call +// `crud.replace_object_many` for execution by a Connection. +type ReplaceObjectManyRequest struct { + spaceRequest + objects []Object + opts ReplaceObjectManyOpts +} + +type replaceObjectManyArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Objects []Object + Opts ReplaceObjectManyOpts +} + +// NewReplaceObjectManyRequest returns a new empty ReplaceObjectManyRequest. +func NewReplaceObjectManyRequest(space string) *ReplaceObjectManyRequest { + req := new(ReplaceObjectManyRequest) + req.initImpl("crud.replace_object_many") + req.setSpace(space) + req.objects = []Object{} + req.opts = ReplaceObjectManyOpts{} + return req +} + +// Objects sets the tuple for the ReplaceObjectManyRequest request. +// Note: default value is nil. +func (req *ReplaceObjectManyRequest) Objects(objects []Object) *ReplaceObjectManyRequest { + req.objects = objects + return req +} + +// Opts sets the options for the ReplaceObjectManyRequest request. +// Note: default value is nil. +func (req *ReplaceObjectManyRequest) Opts(opts ReplaceObjectManyOpts) *ReplaceObjectManyRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *ReplaceObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := replaceObjectManyArgs{Space: req.space, Objects: req.objects, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *ReplaceObjectManyRequest) Context(ctx context.Context) *ReplaceObjectManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/request_test.go b/crud/request_test.go new file mode 100644 index 000000000..c27b8c4ae --- /dev/null +++ b/crud/request_test.go @@ -0,0 +1,545 @@ +package crud_test + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/crud" + "github.com/tarantool/go-tarantool/test_helpers" +) + +const invalidSpaceMsg = "invalid space" +const invalidIndexMsg = "invalid index" + +const invalidSpace = 2 +const invalidIndex = 2 +const validSpace = "test" // Any valid value != default. +const defaultSpace = 0 // And valid too. +const defaultIndex = 0 // And valid too. + +const CrudRequestCode = tarantool.Call17RequestCode + +var reqObject = crud.MapObject{ + "id": uint(24), +} + +var reqObjects = []crud.Object{ + crud.MapObject{ + "id": uint(24), + }, + crud.MapObject{ + "id": uint(25), + }, +} + +var reqObjectsOperationsData = []crud.ObjectOperationsData{ + { + Object: crud.MapObject{ + "id": uint(24), + }, + Operations: []crud.Operation{ + { + Operator: crud.Add, + Field: "id", + Value: uint(1020), + }, + }, + }, + { + Object: crud.MapObject{ + "id": uint(25), + }, + Operations: []crud.Operation{ + { + Operator: crud.Add, + Field: "id", + Value: uint(1020), + }, + }, + }, +} + +var expectedOpts = map[string]interface{}{ + "timeout": timeout, +} + +type ValidSchemeResolver struct { +} + +func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (spaceNo, indexNo uint32, err error) { + if s != nil { + spaceNo = uint32(s.(int)) + } else { + spaceNo = defaultSpace + } + if i != nil { + indexNo = uint32(i.(int)) + } else { + indexNo = defaultIndex + } + if spaceNo == invalidSpace { + return 0, 0, errors.New(invalidSpaceMsg) + } + if indexNo == invalidIndex { + return 0, 0, errors.New(invalidIndexMsg) + } + return spaceNo, indexNo, nil +} + +var resolver ValidSchemeResolver + +func assertBodyEqual(t testing.TB, reference tarantool.Request, req tarantool.Request) { + t.Helper() + + reqBody, err := test_helpers.ExtractRequestBody(req, &resolver, crud.NewEncoder) + if err != nil { + t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) + } + + refBody, err := test_helpers.ExtractRequestBody(reference, &resolver, crud.NewEncoder) + if err != nil { + t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) + } + + if !bytes.Equal(reqBody, refBody) { + t.Errorf("Encoded request %v != reference %v", reqBody, refBody) + } +} + +func TestRequestsCodes(t *testing.T) { + tests := []struct { + req tarantool.Request + code int32 + }{ + {req: crud.NewInsertRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewInsertObjectRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewInsertManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewInsertObjectManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewGetRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewUpdateRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewDeleteRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewReplaceRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewReplaceObjectRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewReplaceManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewReplaceObjectManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewUpsertRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewUpsertObjectRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewUpsertManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewUpsertObjectManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewMinRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewMaxRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewSelectRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewTruncateRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewLenRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewCountRequest(validSpace), code: CrudRequestCode}, + {req: crud.NewStorageInfoRequest(), code: CrudRequestCode}, + {req: crud.NewStatsRequest(), code: CrudRequestCode}, + } + + for _, test := range tests { + if code := test.req.Code(); code != test.code { + t.Errorf("An invalid request code 0x%x, expected 0x%x", code, test.code) + } + } +} + +func TestRequestsAsync(t *testing.T) { + tests := []struct { + req tarantool.Request + async bool + }{ + {req: crud.NewInsertRequest(validSpace), async: false}, + {req: crud.NewInsertObjectRequest(validSpace), async: false}, + {req: crud.NewInsertManyRequest(validSpace), async: false}, + {req: crud.NewInsertObjectManyRequest(validSpace), async: false}, + {req: crud.NewGetRequest(validSpace), async: false}, + {req: crud.NewUpdateRequest(validSpace), async: false}, + {req: crud.NewDeleteRequest(validSpace), async: false}, + {req: crud.NewReplaceRequest(validSpace), async: false}, + {req: crud.NewReplaceObjectRequest(validSpace), async: false}, + {req: crud.NewReplaceManyRequest(validSpace), async: false}, + {req: crud.NewReplaceObjectManyRequest(validSpace), async: false}, + {req: crud.NewUpsertRequest(validSpace), async: false}, + {req: crud.NewUpsertObjectRequest(validSpace), async: false}, + {req: crud.NewUpsertManyRequest(validSpace), async: false}, + {req: crud.NewUpsertObjectManyRequest(validSpace), async: false}, + {req: crud.NewMinRequest(validSpace), async: false}, + {req: crud.NewMaxRequest(validSpace), async: false}, + {req: crud.NewSelectRequest(validSpace), async: false}, + {req: crud.NewTruncateRequest(validSpace), async: false}, + {req: crud.NewLenRequest(validSpace), async: false}, + {req: crud.NewCountRequest(validSpace), async: false}, + {req: crud.NewStorageInfoRequest(), async: false}, + {req: crud.NewStatsRequest(), async: false}, + } + + for _, test := range tests { + if async := test.req.Async(); async != test.async { + t.Errorf("An invalid async %t, expected %t", async, test.async) + } + } +} + +func TestRequestsCtx_default(t *testing.T) { + tests := []struct { + req tarantool.Request + expected context.Context + }{ + {req: crud.NewInsertRequest(validSpace), expected: nil}, + {req: crud.NewInsertObjectRequest(validSpace), expected: nil}, + {req: crud.NewInsertManyRequest(validSpace), expected: nil}, + {req: crud.NewInsertObjectManyRequest(validSpace), expected: nil}, + {req: crud.NewGetRequest(validSpace), expected: nil}, + {req: crud.NewUpdateRequest(validSpace), expected: nil}, + {req: crud.NewDeleteRequest(validSpace), expected: nil}, + {req: crud.NewReplaceRequest(validSpace), expected: nil}, + {req: crud.NewReplaceObjectRequest(validSpace), expected: nil}, + {req: crud.NewReplaceManyRequest(validSpace), expected: nil}, + {req: crud.NewReplaceObjectManyRequest(validSpace), expected: nil}, + {req: crud.NewUpsertRequest(validSpace), expected: nil}, + {req: crud.NewUpsertObjectRequest(validSpace), expected: nil}, + {req: crud.NewUpsertManyRequest(validSpace), expected: nil}, + {req: crud.NewUpsertObjectManyRequest(validSpace), expected: nil}, + {req: crud.NewMinRequest(validSpace), expected: nil}, + {req: crud.NewMaxRequest(validSpace), expected: nil}, + {req: crud.NewSelectRequest(validSpace), expected: nil}, + {req: crud.NewTruncateRequest(validSpace), expected: nil}, + {req: crud.NewLenRequest(validSpace), expected: nil}, + {req: crud.NewCountRequest(validSpace), expected: nil}, + {req: crud.NewStorageInfoRequest(), expected: nil}, + {req: crud.NewStatsRequest(), expected: nil}, + } + + for _, test := range tests { + if ctx := test.req.Ctx(); ctx != test.expected { + t.Errorf("An invalid ctx %t, expected %t", ctx, test.expected) + } + } +} + +func TestRequestsCtx_setter(t *testing.T) { + ctx := context.Background() + tests := []struct { + req tarantool.Request + expected context.Context + }{ + {req: crud.NewInsertRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewInsertObjectRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewInsertManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewInsertObjectManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewGetRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewUpdateRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewDeleteRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewReplaceRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewReplaceObjectRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewReplaceManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewReplaceObjectManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewUpsertRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewUpsertObjectRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewUpsertManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewUpsertObjectManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewMinRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewMaxRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewSelectRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewTruncateRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewLenRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewCountRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.NewStorageInfoRequest().Context(ctx), expected: ctx}, + {req: crud.NewStatsRequest().Context(ctx), expected: ctx}, + } + + for _, test := range tests { + if ctx := test.req.Ctx(); ctx != test.expected { + t.Errorf("An invalid ctx %t, expected %t", ctx, test.expected) + } + } +} + +func TestRequestsDefaultValues(t *testing.T) { + testCases := []struct { + name string + ref tarantool.Request + target tarantool.Request + }{ + { + name: "InsertRequest", + ref: tarantool.NewCall17Request("crud.insert").Args([]interface{}{validSpace, []interface{}{}, + map[string]interface{}{}}), + target: crud.NewInsertRequest(validSpace), + }, + { + name: "InsertObjectRequest", + ref: tarantool.NewCall17Request("crud.insert_object").Args([]interface{}{validSpace, map[string]interface{}{}, + map[string]interface{}{}}), + target: crud.NewInsertObjectRequest(validSpace), + }, + { + name: "InsertManyRequest", + ref: tarantool.NewCall17Request("crud.insert_many").Args([]interface{}{validSpace, []interface{}{}, + map[string]interface{}{}}), + target: crud.NewInsertManyRequest(validSpace), + }, + { + name: "InsertObjectManyRequest", + ref: tarantool.NewCall17Request("crud.insert_object_many").Args([]interface{}{validSpace, []map[string]interface{}{}, + map[string]interface{}{}}), + target: crud.NewInsertObjectManyRequest(validSpace), + }, + { + name: "GetRequest", + ref: tarantool.NewCall17Request("crud.get").Args([]interface{}{validSpace, []interface{}{}, + map[string]interface{}{}}), + target: crud.NewGetRequest(validSpace), + }, + { + name: "UpdateRequest", + ref: tarantool.NewCall17Request("crud.update").Args([]interface{}{validSpace, []interface{}{}, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewUpdateRequest(validSpace), + }, + { + name: "DeleteRequest", + ref: tarantool.NewCall17Request("crud.delete").Args([]interface{}{validSpace, []interface{}{}, + map[string]interface{}{}}), + target: crud.NewDeleteRequest(validSpace), + }, + { + name: "ReplaceRequest", + ref: tarantool.NewCall17Request("crud.replace").Args([]interface{}{validSpace, []interface{}{}, + map[string]interface{}{}}), + target: crud.NewReplaceRequest(validSpace), + }, + { + name: "ReplaceObjectRequest", + ref: tarantool.NewCall17Request("crud.replace_object").Args([]interface{}{validSpace, + map[string]interface{}{}, map[string]interface{}{}}), + target: crud.NewReplaceObjectRequest(validSpace), + }, + { + name: "ReplaceManyRequest", + ref: tarantool.NewCall17Request("crud.replace_many").Args([]interface{}{validSpace, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewReplaceManyRequest(validSpace), + }, + { + name: "ReplaceObjectManyRequest", + ref: tarantool.NewCall17Request("crud.replace_object_many").Args([]interface{}{validSpace, + []map[string]interface{}{}, map[string]interface{}{}}), + target: crud.NewReplaceObjectManyRequest(validSpace), + }, + { + name: "UpsertRequest", + ref: tarantool.NewCall17Request("crud.upsert").Args([]interface{}{validSpace, []interface{}{}, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewUpsertRequest(validSpace), + }, + { + name: "UpsertObjectRequest", + ref: tarantool.NewCall17Request("crud.upsert_object").Args([]interface{}{validSpace, + map[string]interface{}{}, []interface{}{}, map[string]interface{}{}}), + target: crud.NewUpsertObjectRequest(validSpace), + }, + { + name: "UpsertManyRequest", + ref: tarantool.NewCall17Request("crud.upsert_many").Args([]interface{}{validSpace, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewUpsertManyRequest(validSpace), + }, + { + name: "UpsertObjectManyRequest", + ref: tarantool.NewCall17Request("crud.upsert_object_many").Args([]interface{}{validSpace, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewUpsertObjectManyRequest(validSpace), + }, + { + name: "SelectRequest", + ref: tarantool.NewCall17Request("crud.select").Args([]interface{}{validSpace, + nil, map[string]interface{}{}}), + target: crud.NewSelectRequest(validSpace), + }, + { + name: "MinRequest", + ref: tarantool.NewCall17Request("crud.min").Args([]interface{}{validSpace, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewMinRequest(validSpace), + }, + { + name: "MaxRequest", + ref: tarantool.NewCall17Request("crud.max").Args([]interface{}{validSpace, + []interface{}{}, map[string]interface{}{}}), + target: crud.NewMaxRequest(validSpace), + }, + { + name: "TruncateRequest", + ref: tarantool.NewCall17Request("crud.truncate").Args([]interface{}{validSpace, + map[string]interface{}{}}), + target: crud.NewTruncateRequest(validSpace), + }, + { + name: "LenRequest", + ref: tarantool.NewCall17Request("crud.len").Args([]interface{}{validSpace, + map[string]interface{}{}}), + target: crud.NewLenRequest(validSpace), + }, + { + name: "CountRequest", + ref: tarantool.NewCall17Request("crud.count").Args([]interface{}{validSpace, + nil, map[string]interface{}{}}), + target: crud.NewCountRequest(validSpace), + }, + { + name: "StorageInfoRequest", + ref: tarantool.NewCall17Request("crud.storage_info").Args( + []interface{}{map[string]interface{}{}}), + target: crud.NewStorageInfoRequest(), + }, + { + name: "StatsRequest", + ref: tarantool.NewCall17Request("crud.stats").Args( + []interface{}{}), + target: crud.NewStatsRequest(), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assertBodyEqual(t, tc.ref, tc.target) + }) + } +} + +func TestRequestsSetters(t *testing.T) { + testCases := []struct { + name string + ref tarantool.Request + target tarantool.Request + }{ + { + name: "InsertRequest", + ref: tarantool.NewCall17Request("crud.insert").Args([]interface{}{spaceName, tuple, expectedOpts}), + target: crud.NewInsertRequest(spaceName).Tuple(tuple).Opts(simpleOperationOpts), + }, + { + name: "InsertObjectRequest", + ref: tarantool.NewCall17Request("crud.insert_object").Args([]interface{}{spaceName, reqObject, expectedOpts}), + target: crud.NewInsertObjectRequest(spaceName).Object(reqObject).Opts(simpleOperationObjectOpts), + }, + { + name: "InsertManyRequest", + ref: tarantool.NewCall17Request("crud.insert_many").Args([]interface{}{spaceName, tuples, expectedOpts}), + target: crud.NewInsertManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts), + }, + { + name: "InsertObjectManyRequest", + ref: tarantool.NewCall17Request("crud.insert_object_many").Args([]interface{}{spaceName, reqObjects, expectedOpts}), + target: crud.NewInsertObjectManyRequest(spaceName).Objects(reqObjects).Opts(opObjManyOpts), + }, + { + name: "GetRequest", + ref: tarantool.NewCall17Request("crud.get").Args([]interface{}{spaceName, key, expectedOpts}), + target: crud.NewGetRequest(spaceName).Key(key).Opts(getOpts), + }, + { + name: "UpdateRequest", + ref: tarantool.NewCall17Request("crud.update").Args([]interface{}{spaceName, key, operations, expectedOpts}), + target: crud.NewUpdateRequest(spaceName).Key(key).Operations(operations).Opts(simpleOperationOpts), + }, + { + name: "DeleteRequest", + ref: tarantool.NewCall17Request("crud.delete").Args([]interface{}{spaceName, key, expectedOpts}), + target: crud.NewDeleteRequest(spaceName).Key(key).Opts(simpleOperationOpts), + }, + { + name: "ReplaceRequest", + ref: tarantool.NewCall17Request("crud.replace").Args([]interface{}{spaceName, tuple, expectedOpts}), + target: crud.NewReplaceRequest(spaceName).Tuple(tuple).Opts(simpleOperationOpts), + }, + { + name: "ReplaceObjectRequest", + ref: tarantool.NewCall17Request("crud.replace_object").Args([]interface{}{spaceName, reqObject, expectedOpts}), + target: crud.NewReplaceObjectRequest(spaceName).Object(reqObject).Opts(simpleOperationObjectOpts), + }, + { + name: "ReplaceManyRequest", + ref: tarantool.NewCall17Request("crud.replace_many").Args([]interface{}{spaceName, tuples, expectedOpts}), + target: crud.NewReplaceManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts), + }, + { + name: "ReplaceObjectManyRequest", + ref: tarantool.NewCall17Request("crud.replace_object_many").Args([]interface{}{spaceName, reqObjects, expectedOpts}), + target: crud.NewReplaceObjectManyRequest(spaceName).Objects(reqObjects).Opts(opObjManyOpts), + }, + { + name: "UpsertRequest", + ref: tarantool.NewCall17Request("crud.upsert").Args([]interface{}{spaceName, tuple, operations, expectedOpts}), + target: crud.NewUpsertRequest(spaceName).Tuple(tuple).Operations(operations).Opts(simpleOperationOpts), + }, + { + name: "UpsertObjectRequest", + ref: tarantool.NewCall17Request("crud.upsert_object").Args([]interface{}{spaceName, reqObject, + operations, expectedOpts}), + target: crud.NewUpsertObjectRequest(spaceName).Object(reqObject).Operations(operations).Opts(simpleOperationOpts), + }, + { + name: "UpsertManyRequest", + ref: tarantool.NewCall17Request("crud.upsert_many").Args([]interface{}{spaceName, + tuplesOperationsData, expectedOpts}), + target: crud.NewUpsertManyRequest(spaceName).TuplesOperationsData(tuplesOperationsData).Opts(opManyOpts), + }, + { + name: "UpsertObjectManyRequest", + ref: tarantool.NewCall17Request("crud.upsert_object_many").Args([]interface{}{spaceName, + reqObjectsOperationsData, expectedOpts}), + target: crud.NewUpsertObjectManyRequest(spaceName).ObjectsOperationsData(reqObjectsOperationsData).Opts(opManyOpts), + }, + { + name: "SelectRequest", + ref: tarantool.NewCall17Request("crud.select").Args([]interface{}{spaceName, conditions, expectedOpts}), + target: crud.NewSelectRequest(spaceName).Conditions(conditions).Opts(selectOpts), + }, + { + name: "MinRequest", + ref: tarantool.NewCall17Request("crud.min").Args([]interface{}{spaceName, indexName, expectedOpts}), + target: crud.NewMinRequest(spaceName).Index(indexName).Opts(minOpts), + }, + { + name: "MaxRequest", + ref: tarantool.NewCall17Request("crud.max").Args([]interface{}{spaceName, indexName, expectedOpts}), + target: crud.NewMaxRequest(spaceName).Index(indexName).Opts(maxOpts), + }, + { + name: "TruncateRequest", + ref: tarantool.NewCall17Request("crud.truncate").Args([]interface{}{spaceName, expectedOpts}), + target: crud.NewTruncateRequest(spaceName).Opts(baseOpts), + }, + { + name: "LenRequest", + ref: tarantool.NewCall17Request("crud.len").Args([]interface{}{spaceName, expectedOpts}), + target: crud.NewLenRequest(spaceName).Opts(baseOpts), + }, + { + name: "CountRequest", + ref: tarantool.NewCall17Request("crud.count").Args([]interface{}{spaceName, conditions, expectedOpts}), + target: crud.NewCountRequest(spaceName).Conditions(conditions).Opts(countOpts), + }, + { + name: "StorageInfoRequest", + ref: tarantool.NewCall17Request("crud.storage_info").Args([]interface{}{expectedOpts}), + target: crud.NewStorageInfoRequest().Opts(baseOpts), + }, + { + name: "StatsRequest", + ref: tarantool.NewCall17Request("crud.stats").Args([]interface{}{spaceName}), + target: crud.NewStatsRequest().Space(spaceName), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assertBodyEqual(t, tc.ref, tc.target) + }) + } +} diff --git a/crud/result.go b/crud/result.go new file mode 100644 index 000000000..356e2a817 --- /dev/null +++ b/crud/result.go @@ -0,0 +1,285 @@ +package crud + +import ( + "fmt" +) + +// FieldFormat contains field definition: {name='...',type='...'[,is_nullable=...]}. +type FieldFormat struct { + Name string + Type string + IsNullable bool +} + +// DecodeMsgpack provides custom msgpack decoder. +func (format *FieldFormat) DecodeMsgpack(d *decoder) error { + l, err := d.DecodeMapLen() + if err != nil { + return err + } + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + switch key { + case "name": + if format.Name, err = d.DecodeString(); err != nil { + return err + } + case "type": + if format.Type, err = d.DecodeString(); err != nil { + return err + } + case "is_nullable": + if format.IsNullable, err = d.DecodeBool(); err != nil { + return err + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + + return nil +} + +// Result describes CRUD result as an object containing metadata and rows. +type Result struct { + Metadata []FieldFormat + Rows []interface{} +} + +// DecodeMsgpack provides custom msgpack decoder. +func (r *Result) DecodeMsgpack(d *decoder) error { + arrLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + if arrLen < 2 { + return fmt.Errorf("array len doesn't match: %d", arrLen) + } + + l, err := d.DecodeMapLen() + if err != nil { + return err + } + + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + + switch key { + case "metadata": + metadataLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + metadata := make([]FieldFormat, metadataLen) + + for i := 0; i < metadataLen; i++ { + fieldFormat := FieldFormat{} + if err = d.Decode(&fieldFormat); err != nil { + return err + } + + metadata[i] = fieldFormat + } + + r.Metadata = metadata + case "rows": + if err = d.Decode(&r.Rows); err != nil { + return err + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + + var crudErr *Error = nil + + if err := d.Decode(&crudErr); err != nil { + return err + } + + for i := 2; i < arrLen; i++ { + if err := d.Skip(); err != nil { + return err + } + } + + if crudErr != nil { + return crudErr + } + + return nil +} + +// ResultMany describes CRUD result as an object containing metadata and rows. +type ResultMany struct { + Metadata []FieldFormat + Rows []interface{} +} + +// DecodeMsgpack provides custom msgpack decoder. +func (r *ResultMany) DecodeMsgpack(d *decoder) error { + arrLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + if arrLen < 2 { + return fmt.Errorf("array len doesn't match: %d", arrLen) + } + + l, err := d.DecodeMapLen() + if err != nil { + return err + } + + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + + switch key { + case "metadata": + metadataLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + metadata := make([]FieldFormat, metadataLen) + + for i := 0; i < metadataLen; i++ { + fieldFormat := FieldFormat{} + if err = d.Decode(&fieldFormat); err != nil { + return err + } + + metadata[i] = fieldFormat + } + + r.Metadata = metadata + case "rows": + if err = d.Decode(&r.Rows); err != nil { + return err + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + + errLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + var errs []Error + for i := 0; i < errLen; i++ { + var crudErr *Error = nil + + if err := d.Decode(&crudErr); err != nil { + return err + } else if crudErr != nil { + errs = append(errs, *crudErr) + } + } + + for i := 2; i < arrLen; i++ { + if err := d.Skip(); err != nil { + return err + } + } + + if len(errs) > 0 { + return &ErrorMany{Errors: errs} + } + + return nil +} + +// NumberResult describes CRUD result as an object containing number. +type NumberResult struct { + Value uint64 +} + +// DecodeMsgpack provides custom msgpack decoder. +func (r *NumberResult) DecodeMsgpack(d *decoder) error { + arrLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + if arrLen < 2 { + return fmt.Errorf("array len doesn't match: %d", arrLen) + } + + if r.Value, err = d.DecodeUint64(); err != nil { + return err + } + + var crudErr *Error = nil + + if err := d.Decode(&crudErr); err != nil { + return err + } + + for i := 2; i < arrLen; i++ { + if err := d.Skip(); err != nil { + return err + } + } + + if crudErr != nil { + return crudErr + } + + return nil +} + +// BoolResult describes CRUD result as an object containing bool. +type BoolResult struct { + Value bool +} + +// DecodeMsgpack provides custom msgpack decoder. +func (r *BoolResult) DecodeMsgpack(d *decoder) error { + arrLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + if arrLen < 2 { + if r.Value, err = d.DecodeBool(); err != nil { + return err + } + + return nil + } + + if _, err = d.DecodeInterface(); err != nil { + return err + } + + var crudErr *Error = nil + + if err := d.Decode(&crudErr); err != nil { + return err + } + + if crudErr != nil { + return crudErr + } + + return nil +} diff --git a/crud/select.go b/crud/select.go new file mode 100644 index 000000000..5ada1aa99 --- /dev/null +++ b/crud/select.go @@ -0,0 +1,117 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// SelectResult describes result for `crud.select` method. +type SelectResult = Result + +// SelectOpts describes options for `crud.select` method. +type SelectOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptUint + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Fields is field names for getting only a subset of fields. + Fields OptTuple + // BucketId is a bucket ID. + BucketId OptUint + // Mode is a parameter with `write`/`read` possible values, + // if `write` is specified then operation is performed on master. + Mode OptString + // PreferReplica is a parameter to specify preferred target + // as one of the replicas. + PreferReplica OptBool + // Balance is a parameter to use replica according to vshard + // load balancing policy. + Balance OptBool + // First describes the maximum count of the objects to return. + First OptInt + // After is a tuple after which objects should be selected. + After OptTuple + // BatchSize is a number of tuples to process per one request to storage. + BatchSize OptUint + // ForceMapCall describes the map call is performed without any + // optimizations even if full primary key equal condition is specified. + ForceMapCall OptBool + // Fullscan describes if a critical log entry will be skipped on + // potentially long select. + Fullscan OptBool +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts SelectOpts) EncodeMsgpack(enc *encoder) error { + const optsCnt = 12 + + options := [optsCnt]option{opts.Timeout, opts.VshardRouter, + opts.Fields, opts.BucketId, + opts.Mode, opts.PreferReplica, opts.Balance, + opts.First, opts.After, opts.BatchSize, + opts.ForceMapCall, opts.Fullscan} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + fieldsOptName, bucketIdOptName, + modeOptName, preferReplicaOptName, balanceOptName, + firstOptName, afterOptName, batchSizeOptName, + forceMapCallOptName, fullscanOptName} + values := [optsCnt]interface{}{} + + return encodeOptions(enc, options[:], names[:], values[:]) +} + +// SelectRequest helps you to create request object to call `crud.select` +// for execution by a Connection. +type SelectRequest struct { + spaceRequest + conditions []Condition + opts SelectOpts +} + +type selectArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Conditions []Condition + Opts SelectOpts +} + +// NewSelectRequest returns a new empty SelectRequest. +func NewSelectRequest(space string) *SelectRequest { + req := new(SelectRequest) + req.initImpl("crud.select") + req.setSpace(space) + req.conditions = nil + req.opts = SelectOpts{} + return req +} + +// Conditions sets the conditions for the SelectRequest request. +// Note: default value is nil. +func (req *SelectRequest) Conditions(conditions []Condition) *SelectRequest { + req.conditions = conditions + return req +} + +// Opts sets the options for the SelectRequest request. +// Note: default value is nil. +func (req *SelectRequest) Opts(opts SelectOpts) *SelectRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *SelectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := selectArgs{Space: req.space, Conditions: req.conditions, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *SelectRequest) Context(ctx context.Context) *SelectRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/stats.go b/crud/stats.go new file mode 100644 index 000000000..939ed32ce --- /dev/null +++ b/crud/stats.go @@ -0,0 +1,46 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// StatsRequest helps you to create request object to call `crud.stats` +// for execution by a Connection. +type StatsRequest struct { + baseRequest + space OptString +} + +// NewStatsRequest returns a new empty StatsRequest. +func NewStatsRequest() *StatsRequest { + req := new(StatsRequest) + req.initImpl("crud.stats") + return req +} + +// Space sets the space name for the StatsRequest request. +// Note: default value is nil. +func (req *StatsRequest) Space(space string) *StatsRequest { + req.space = NewOptString(space) + return req +} + +// Body fills an encoder with the call request body. +func (req *StatsRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := []interface{}{} + if value, err := req.space.Get(); err == nil { + args = []interface{}{value} + } + req.impl.Args(args) + + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *StatsRequest) Context(ctx context.Context) *StatsRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/storage_info.go b/crud/storage_info.go new file mode 100644 index 000000000..a52ca710c --- /dev/null +++ b/crud/storage_info.go @@ -0,0 +1,130 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// StatusTable describes information for instance. +type StatusTable struct { + Status string + IsMaster bool + Message string +} + +// DecodeMsgpack provides custom msgpack decoder. +func (statusTable *StatusTable) DecodeMsgpack(d *decoder) error { + l, err := d.DecodeMapLen() + if err != nil { + return err + } + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + + switch key { + case "status": + if statusTable.Status, err = d.DecodeString(); err != nil { + return err + } + case "is_master": + if statusTable.IsMaster, err = d.DecodeBool(); err != nil { + return err + } + case "message": + if statusTable.Message, err = d.DecodeString(); err != nil { + return err + } + default: + if err := d.Skip(); err != nil { + return err + } + } + } + + return nil +} + +// StorageInfoResult describes result for `crud.storage_info` method. +type StorageInfoResult struct { + Info map[string]StatusTable +} + +// DecodeMsgpack provides custom msgpack decoder. +func (r *StorageInfoResult) DecodeMsgpack(d *decoder) error { + _, err := d.DecodeArrayLen() + if err != nil { + return err + } + + l, err := d.DecodeMapLen() + if err != nil { + return err + } + + info := make(map[string]StatusTable) + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + + statusTable := StatusTable{} + if err := d.Decode(&statusTable); err != nil { + return nil + } + + info[key] = statusTable + } + + r.Info = info + + return nil +} + +// StorageInfoOpts describes options for `crud.storage_info` method. +type StorageInfoOpts = BaseOpts + +// StorageInfoRequest helps you to create request object to call +// `crud.storage_info` for execution by a Connection. +type StorageInfoRequest struct { + baseRequest + opts StorageInfoOpts +} + +type storageInfoArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Opts StorageInfoOpts +} + +// NewStorageInfoRequest returns a new empty StorageInfoRequest. +func NewStorageInfoRequest() *StorageInfoRequest { + req := new(StorageInfoRequest) + req.initImpl("crud.storage_info") + req.opts = StorageInfoOpts{} + return req +} + +// Opts sets the options for the torageInfoRequest request. +// Note: default value is nil. +func (req *StorageInfoRequest) Opts(opts StorageInfoOpts) *StorageInfoRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *StorageInfoRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := storageInfoArgs{Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *StorageInfoRequest) Context(ctx context.Context) *StorageInfoRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go new file mode 100644 index 000000000..1323b33e9 --- /dev/null +++ b/crud/tarantool_test.go @@ -0,0 +1,807 @@ +package crud_test + +import ( + "bytes" + "fmt" + "log" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/crud" + "github.com/tarantool/go-tarantool/test_helpers" +) + +var server = "127.0.0.1:3013" +var spaceNo = uint32(617) +var spaceName = "test" +var invalidSpaceName = "invalid" +var indexNo = uint32(0) +var indexName = "primary_index" +var opts = tarantool.Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +var startOpts test_helpers.StartOpts = test_helpers.StartOpts{ + InitScript: "testdata/config.lua", + Listen: server, + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, +} + +var timeout = uint(1) + +var operations = []crud.Operation{ + { + Operator: crud.Assign, + Field: "name", + Value: "bye", + }, +} + +var selectOpts = crud.SelectOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var countOpts = crud.CountOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var getOpts = crud.GetOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var minOpts = crud.MinOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var maxOpts = crud.MaxOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var baseOpts = crud.BaseOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var simpleOperationOpts = crud.SimpleOperationOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var simpleOperationObjectOpts = crud.SimpleOperationObjectOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var opManyOpts = crud.OperationManyOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var opObjManyOpts = crud.OperationObjectManyOpts{ + Timeout: crud.NewOptUint(timeout), +} + +var conditions = []crud.Condition{ + { + Operator: crud.Lt, + Field: "id", + Value: uint(1020), + }, +} + +var key = []interface{}{uint(1019)} + +var tuples = generateTuples() +var objects = generateObjects() + +var tuple = []interface{}{uint(1019), nil, "bla"} +var object = crud.MapObject{ + "id": uint(1019), + "name": "bla", +} + +func BenchmarkCrud(b *testing.B) { + var err error + + conn := test_helpers.ConnectWithValidation(b, server, opts) + defer conn.Close() + + _, err = conn.Replace(spaceName, tuple) + if err != nil { + b.Error(err) + } + req := crud.NewLenRequest(spaceName). + Opts(crud.LenOpts{ + Timeout: crud.NewOptUint(3), + VshardRouter: crud.NewOptString("asd"), + }) + + buf := bytes.Buffer{} + buf.Grow(512 * 1024 * 1024) // Avoid allocs in test. + enc := crud.NewEncoder(&buf) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + err := req.Body(nil, enc) + if err != nil { + b.Error(err) + } + } +} + +var testProcessDataCases = []struct { + name string + expectedRespLen int + req tarantool.Request +}{ + { + "Select", + 2, + crud.NewSelectRequest(spaceName). + Conditions(conditions). + Opts(selectOpts), + }, + { + "Get", + 2, + crud.NewGetRequest(spaceName). + Key(key). + Opts(getOpts), + }, + { + "Update", + 2, + crud.NewUpdateRequest(spaceName). + Key(key). + Operations(operations). + Opts(simpleOperationOpts), + }, + { + "Delete", + 2, + crud.NewDeleteRequest(spaceName). + Key(key). + Opts(simpleOperationOpts), + }, + { + "Min", + 2, + crud.NewMinRequest(spaceName).Index(indexName).Opts(minOpts), + }, + { + "Max", + 2, + crud.NewMaxRequest(spaceName).Index(indexName).Opts(maxOpts), + }, + { + "Truncate", + 1, + crud.NewTruncateRequest(spaceName).Opts(baseOpts), + }, + { + "Len", + 1, + crud.NewLenRequest(spaceName).Opts(baseOpts), + }, + { + "Count", + 2, + crud.NewCountRequest(spaceName). + Conditions(conditions). + Opts(countOpts), + }, + { + "Stats", + 1, + crud.NewStatsRequest().Space(spaceName), + }, + { + "StorageInfo", + 1, + crud.NewStorageInfoRequest().Opts(baseOpts), + }, +} + +var testResultWithErrCases = []struct { + name string + resp interface{} + req tarantool.Request +}{ + { + "BaseResult", + &crud.SelectResult{}, + crud.NewSelectRequest(invalidSpaceName).Opts(selectOpts), + }, + { + "ManyResult", + &crud.ReplaceManyResult{}, + crud.NewReplaceManyRequest(invalidSpaceName).Opts(opManyOpts), + }, + { + "NumberResult", + &crud.CountResult{}, + crud.NewCountRequest(invalidSpaceName).Opts(countOpts), + }, + { + "BoolResult", + &crud.TruncateResult{}, + crud.NewTruncateRequest(invalidSpaceName).Opts(baseOpts), + }, +} + +var tuplesOperationsData = generateTuplesOperationsData(tuples, operations) +var objectsOperationData = generateObjectsOperationsData(objects, operations) + +var testGenerateDataCases = []struct { + name string + expectedRespLen int + expectedTuplesCount int + req tarantool.Request +}{ + { + "Insert", + 2, + 1, + crud.NewInsertRequest(spaceName). + Tuple(tuple). + Opts(simpleOperationOpts), + }, + { + "InsertObject", + 2, + 1, + crud.NewInsertObjectRequest(spaceName). + Object(object). + Opts(simpleOperationObjectOpts), + }, + { + "InsertMany", + 2, + 10, + crud.NewInsertManyRequest(spaceName). + Tuples(tuples). + Opts(opManyOpts), + }, + { + "InsertObjectMany", + 2, + 10, + crud.NewInsertObjectManyRequest(spaceName). + Objects(objects). + Opts(opObjManyOpts), + }, + { + "Replace", + 2, + 1, + crud.NewReplaceRequest(spaceName). + Tuple(tuple). + Opts(simpleOperationOpts), + }, + { + "ReplaceObject", + 2, + 1, + crud.NewReplaceObjectRequest(spaceName). + Object(object). + Opts(simpleOperationObjectOpts), + }, + { + "ReplaceMany", + 2, + 10, + crud.NewReplaceManyRequest(spaceName). + Tuples(tuples). + Opts(opManyOpts), + }, + { + "ReplaceObjectMany", + 2, + 10, + crud.NewReplaceObjectManyRequest(spaceName). + Objects(objects). + Opts(opObjManyOpts), + }, + { + "Upsert", + 2, + 1, + crud.NewUpsertRequest(spaceName). + Tuple(tuple). + Operations(operations). + Opts(simpleOperationOpts), + }, + { + "UpsertObject", + 2, + 1, + crud.NewUpsertObjectRequest(spaceName). + Object(object). + Operations(operations). + Opts(simpleOperationOpts), + }, + { + "UpsertMany", + 2, + 10, + crud.NewUpsertManyRequest(spaceName). + TuplesOperationsData(tuplesOperationsData). + Opts(opManyOpts), + }, + { + "UpsertObjectMany", + 2, + 10, + crud.NewUpsertObjectManyRequest(spaceName). + ObjectsOperationsData(objectsOperationData). + Opts(opManyOpts), + }, +} + +func generateTuples() []crud.Tuple { + tpls := []crud.Tuple{} + for i := 1010; i < 1020; i++ { + tpls = append(tpls, crud.Tuple{uint(i), nil, "bla"}) + } + + return tpls +} + +func generateTuplesOperationsData(tpls []crud.Tuple, operations []crud.Operation) []crud.TupleOperationsData { + tuplesOperationsData := []crud.TupleOperationsData{} + for _, tpl := range tpls { + tuplesOperationsData = append(tuplesOperationsData, crud.TupleOperationsData{ + Tuple: tpl, + Operations: operations, + }) + } + + return tuplesOperationsData +} + +func generateObjects() []crud.Object { + objs := []crud.Object{} + for i := 1010; i < 1020; i++ { + objs = append(objs, crud.MapObject{ + "id": uint(i), + "name": "bla", + }) + } + + return objs +} + +func generateObjectsOperationsData(objs []crud.Object, operations []crud.Operation) []crud.ObjectOperationsData { + objectsOperationsData := []crud.ObjectOperationsData{} + for _, obj := range objs { + objectsOperationsData = append(objectsOperationsData, crud.ObjectOperationsData{ + Object: obj, + Operations: operations, + }) + } + + return objectsOperationsData +} + +func getCrudError(req tarantool.Request, crudError interface{}) (interface{}, error) { + var err []interface{} + var ok bool + + code := req.Code() + if crudError != nil { + if code == tarantool.Call17RequestCode { + return crudError, nil + } + + if err, ok = crudError.([]interface{}); !ok { + return nil, fmt.Errorf("Incorrect CRUD error format") + } + + if len(err) < 1 { + return nil, fmt.Errorf("Incorrect CRUD error format") + } + + if err[0] != nil { + return err[0], nil + } + } + + return nil, nil +} + +func testCrudRequestPrepareData(t *testing.T, conn tarantool.Connector) { + t.Helper() + + for i := 1010; i < 1020; i++ { + req := tarantool.NewReplaceRequest(spaceName).Tuple( + []interface{}{uint(i), nil, "bla"}) + if _, err := conn.Do(req).Get(); err != nil { + t.Fatalf("Unable to prepare tuples: %s", err) + } + } +} + +func testSelectGeneratedData(t *testing.T, conn tarantool.Connector, + expectedTuplesCount int) { + req := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(20). + Iterator(tarantool.IterGe). + Key([]interface{}{uint(1010)}) + resp, err := conn.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Select: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + if len(resp.Data) != expectedTuplesCount { + t.Fatalf("Response Data len %d != %d", len(resp.Data), expectedTuplesCount) + } +} + +func testCrudRequestCheck(t *testing.T, req tarantool.Request, + resp *tarantool.Response, err error, expectedLen int) { + t.Helper() + + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err.Error()) + } + + if resp == nil { + t.Fatalf("Response is nil after Do CRUD request") + } + + if len(resp.Data) < expectedLen { + t.Fatalf("Response Body len < %#v, actual len %#v", + expectedLen, len(resp.Data)) + } + + // resp.Data[0] - CRUD res. + // resp.Data[1] - CRUD err. + if expectedLen >= 2 { + if crudErr, err := getCrudError(req, resp.Data[1]); err != nil { + t.Fatalf("Failed to get CRUD error: %#v", err) + } else if crudErr != nil { + t.Fatalf("Failed to perform CRUD request on CRUD side: %#v", crudErr) + } + } +} + +func TestCrudGenerateData(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + for _, testCase := range testGenerateDataCases { + t.Run(testCase.name, func(t *testing.T) { + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } + + resp, err := conn.Do(testCase.req).Get() + testCrudRequestCheck(t, testCase.req, resp, + err, testCase.expectedRespLen) + + testSelectGeneratedData(t, conn, testCase.expectedTuplesCount) + + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } + }) + } +} + +func TestCrudProcessData(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + for _, testCase := range testProcessDataCases { + t.Run(testCase.name, func(t *testing.T) { + testCrudRequestPrepareData(t, conn) + resp, err := conn.Do(testCase.req).Get() + testCrudRequestCheck(t, testCase.req, resp, + err, testCase.expectedRespLen) + + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } + }) + } +} + +func TestUnflattenRows_IncorrectParams(t *testing.T) { + invalidMetadata := []interface{}{ + map[interface{}]interface{}{ + "name": true, + "type": "number", + }, + map[interface{}]interface{}{ + "name": "name", + "type": "string", + }, + } + + tpls := []interface{}{ + tuple, + } + + // Format tuples with invalid format with UnflattenRows. + objs, err := crud.UnflattenRows(tpls, invalidMetadata) + require.Nil(t, objs) + require.NotNil(t, err) + require.Contains(t, err.Error(), "Unexpected space format") +} + +func TestUnflattenRows(t *testing.T) { + var ( + ok bool + err error + expectedId uint64 + actualId uint64 + res map[interface{}]interface{} + metadata []interface{} + tpls []interface{} + ) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + // Do `replace`. + req := crud.NewReplaceRequest(spaceName). + Tuple(tuple). + Opts(simpleOperationOpts) + resp, err := conn.Do(req).Get() + testCrudRequestCheck(t, req, resp, err, 2) + + if res, ok = resp.Data[0].(map[interface{}]interface{}); !ok { + t.Fatalf("Unexpected CRUD result: %#v", resp.Data[0]) + } + + if rawMetadata, ok := res["metadata"]; !ok { + t.Fatalf("Failed to get CRUD metadata") + } else { + if metadata, ok = rawMetadata.([]interface{}); !ok { + t.Fatalf("Unexpected CRUD metadata: %#v", rawMetadata) + } + } + + if rawTuples, ok := res["rows"]; !ok { + t.Fatalf("Failed to get CRUD rows") + } else { + if tpls, ok = rawTuples.([]interface{}); !ok { + t.Fatalf("Unexpected CRUD rows: %#v", rawTuples) + } + } + + // Format `replace` result with UnflattenRows. + objs, err := crud.UnflattenRows(tpls, metadata) + if err != nil { + t.Fatalf("Failed to unflatten rows: %#v", err) + } + if len(objs) < 1 { + t.Fatalf("Unexpected unflatten rows result: %#v", objs) + } + + if _, ok := objs[0]["bucket_id"]; ok { + delete(objs[0], "bucket_id") + } else { + t.Fatalf("Expected `bucket_id` field") + } + + require.Equal(t, len(object), len(objs[0])) + if expectedId, err = test_helpers.ConvertUint64(object["id"]); err != nil { + t.Fatalf("Unexpected `id` type") + } + + if actualId, err = test_helpers.ConvertUint64(objs[0]["id"]); err != nil { + t.Fatalf("Unexpected `id` type") + } + + require.Equal(t, expectedId, actualId) + require.Equal(t, object["name"], objs[0]["name"]) +} + +func TestResultWithErr(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + for _, testCase := range testResultWithErrCases { + t.Run(testCase.name, func(t *testing.T) { + err := conn.Do(testCase.req).GetTyped(testCase.resp) + if err == nil { + t.Fatalf("Expected CRUD fails with error, but error is not received") + } + require.Contains(t, err.Error(), "Space \"invalid\" doesn't exist") + }) + } +} + +func TestBoolResult(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + req := crud.NewTruncateRequest(spaceName).Opts(baseOpts) + resp := crud.TruncateResult{} + + testCrudRequestPrepareData(t, conn) + + err := conn.Do(req).GetTyped(&resp) + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err.Error()) + } + + if resp.Value != true { + t.Fatalf("Unexpected response value: %#v != %#v", resp.Value, true) + } + + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } +} + +func TestNumberResult(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + req := crud.NewCountRequest(spaceName).Opts(countOpts) + resp := crud.CountResult{} + + testCrudRequestPrepareData(t, conn) + + err := conn.Do(req).GetTyped(&resp) + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err.Error()) + } + + if resp.Value != 10 { + t.Fatalf("Unexpected response value: %#v != %#v", resp.Value, 10) + } + + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } +} + +func TestBaseResult(t *testing.T) { + expectedMetadata := []crud.FieldFormat{ + { + Name: "bucket_id", + Type: "unsigned", + IsNullable: true, + }, + { + Name: "id", + Type: "unsigned", + IsNullable: false, + }, + { + Name: "name", + Type: "string", + IsNullable: false, + }, + } + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + req := crud.NewSelectRequest(spaceName).Opts(selectOpts) + resp := crud.SelectResult{} + + testCrudRequestPrepareData(t, conn) + + err := conn.Do(req).GetTyped(&resp) + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err.Error()) + } + + require.ElementsMatch(t, resp.Metadata, expectedMetadata) + + if len(resp.Rows) != 10 { + t.Fatalf("Unexpected rows: %#v", resp.Rows) + } + + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } +} + +func TestManyResult(t *testing.T) { + expectedMetadata := []crud.FieldFormat{ + { + Name: "bucket_id", + Type: "unsigned", + IsNullable: true, + }, + { + Name: "id", + Type: "unsigned", + IsNullable: false, + }, + { + Name: "name", + Type: "string", + IsNullable: false, + }, + } + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + req := crud.NewReplaceManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts) + resp := crud.ReplaceResult{} + + testCrudRequestPrepareData(t, conn) + + err := conn.Do(req).GetTyped(&resp) + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err.Error()) + } + + require.ElementsMatch(t, resp.Metadata, expectedMetadata) + + if len(resp.Rows) != 10 { + t.Fatalf("Unexpected rows: %#v", resp.Rows) + } + + for i := 1010; i < 1020; i++ { + conn.Delete(spaceName, nil, []interface{}{uint(i)}) + } +} + +func TestStorageInfoResult(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + req := crud.NewStorageInfoRequest().Opts(baseOpts) + resp := crud.StorageInfoResult{} + + err := conn.Do(req).GetTyped(&resp) + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err.Error()) + } + + if resp.Info == nil { + t.Fatalf("Failed to Do CRUD storage info request") + } + + for _, info := range resp.Info { + if info.Status != "running" { + t.Fatalf("Unexpected Status: %s != running", info.Status) + } + + if info.IsMaster != true { + t.Fatalf("Unexpected IsMaster: %v != true", info.IsMaster) + } + + if msg := info.Message; msg != "" { + t.Fatalf("Unexpected Message: %s", msg) + } + } +} + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + inst, err := test_helpers.StartTarantool(startOpts) + defer test_helpers.StopTarantoolWithCleanup(inst) + + if err != nil { + log.Fatalf("Failed to prepare test tarantool: %s", err) + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/crud/testdata/config.lua b/crud/testdata/config.lua new file mode 100644 index 000000000..9f8b2d5db --- /dev/null +++ b/crud/testdata/config.lua @@ -0,0 +1,99 @@ +-- configure path so that you can run application +-- from outside the root directory +if package.setsearchroot ~= nil then + package.setsearchroot() +else + -- Workaround for rocks loading in tarantool 1.10 + -- It can be removed in tarantool > 2.2 + -- By default, when you do require('mymodule'), tarantool looks into + -- the current working directory and whatever is specified in + -- package.path and package.cpath. If you run your app while in the + -- root directory of that app, everything goes fine, but if you try to + -- start your app with "tarantool myapp/init.lua", it will fail to load + -- its modules, and modules from myapp/.rocks. + local fio = require('fio') + local app_dir = fio.abspath(fio.dirname(arg[0])) + package.path = app_dir .. '/?.lua;' .. package.path + package.path = app_dir .. '/?/init.lua;' .. package.path + package.path = app_dir .. '/.rocks/share/tarantool/?.lua;' .. package.path + package.path = app_dir .. '/.rocks/share/tarantool/?/init.lua;' .. package.path + package.cpath = app_dir .. '/?.so;' .. package.cpath + package.cpath = app_dir .. '/?.dylib;' .. package.cpath + package.cpath = app_dir .. '/.rocks/lib/tarantool/?.so;' .. package.cpath + package.cpath = app_dir .. '/.rocks/lib/tarantool/?.dylib;' .. package.cpath +end + +local crud = require('crud') +local vshard = require('vshard') + +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +box.cfg{ + work_dir = os.getenv("TEST_TNT_WORK_DIR"), +} + +box.schema.user.grant( + 'guest', + 'read,write,execute', + 'universe' +) + +local s = box.schema.space.create('test', { + id = 617, + if_not_exists = true, + format = { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned', is_nullable = true}, + {name = 'name', type = 'string'}, + } +}) +s:create_index('primary_index', { + parts = { + {field = 1, type = 'unsigned'}, + }, +}) +s:create_index('bucket_id', { + parts = { + {field = 2, type = 'unsigned'}, + }, + unique = false, +}) + +-- Setup vshard. +_G.vshard = vshard +box.once('guest', function() + box.schema.user.grant('guest', 'super') +end) +local uri = 'guest@127.0.0.1:3013' +local cfg = { + bucket_count = 300, + sharding = { + [box.info().cluster.uuid] = { + replicas = { + [box.info().uuid] = { + uri = uri, + name = 'storage', + master = true, + }, + }, + }, + }, +} +vshard.storage.cfg(cfg, box.info().uuid) +vshard.router.cfg(cfg) +vshard.router.bootstrap() + +-- Initialize crud. +crud.init_storage() +crud.init_router() +crud.cfg{stats = true} + +box.schema.user.create('test', { password = 'test' , if_not_exists = true }) +box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) +box.schema.user.grant('test', 'create,read,write,drop,alter', 'space', nil, { if_not_exists = true }) +box.schema.user.grant('test', 'create', 'sequence', nil, { if_not_exists = true }) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} diff --git a/crud/truncate.go b/crud/truncate.go new file mode 100644 index 000000000..e2d6b029d --- /dev/null +++ b/crud/truncate.go @@ -0,0 +1,56 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// TruncateResult describes result for `crud.truncate` method. +type TruncateResult = BoolResult + +// TruncateOpts describes options for `crud.truncate` method. +type TruncateOpts = BaseOpts + +// TruncateRequest helps you to create request object to call `crud.truncate` +// for execution by a Connection. +type TruncateRequest struct { + spaceRequest + opts TruncateOpts +} + +type truncateArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Opts TruncateOpts +} + +// NewTruncateRequest returns a new empty TruncateRequest. +func NewTruncateRequest(space string) *TruncateRequest { + req := new(TruncateRequest) + req.initImpl("crud.truncate") + req.setSpace(space) + req.opts = TruncateOpts{} + return req +} + +// Opts sets the options for the TruncateRequest request. +// Note: default value is nil. +func (req *TruncateRequest) Opts(opts TruncateOpts) *TruncateRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *TruncateRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := truncateArgs{Space: req.space, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *TruncateRequest) Context(ctx context.Context) *TruncateRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/unflatten_rows.go b/crud/unflatten_rows.go new file mode 100644 index 000000000..2efb65999 --- /dev/null +++ b/crud/unflatten_rows.go @@ -0,0 +1,40 @@ +package crud + +import ( + "fmt" +) + +// UnflattenRows can be used to convert received tuples to objects. +func UnflattenRows(tuples []interface{}, format []interface{}) ([]MapObject, error) { + var ( + ok bool + tuple Tuple + fieldName string + fieldInfo map[interface{}]interface{} + ) + + objects := []MapObject{} + + for _, rawTuple := range tuples { + object := make(map[string]interface{}) + if tuple, ok = rawTuple.(Tuple); !ok { + return nil, fmt.Errorf("Unexpected tuple format: %q", rawTuple) + } + + for fieldIdx, field := range tuple { + if fieldInfo, ok = format[fieldIdx].(map[interface{}]interface{}); !ok { + return nil, fmt.Errorf("Unexpected space format: %q", format) + } + + if fieldName, ok = fieldInfo["name"].(string); !ok { + return nil, fmt.Errorf("Unexpected space format: %q", format) + } + + object[fieldName] = field + } + + objects = append(objects, object) + } + + return objects, nil +} diff --git a/crud/update.go b/crud/update.go new file mode 100644 index 000000000..09df6612d --- /dev/null +++ b/crud/update.go @@ -0,0 +1,77 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// UpdateResult describes result for `crud.update` method. +type UpdateResult = Result + +// UpdateOpts describes options for `crud.update` method. +type UpdateOpts = SimpleOperationOpts + +// UpdateRequest helps you to create request object to call `crud.update` +// for execution by a Connection. +type UpdateRequest struct { + spaceRequest + key Tuple + operations []Operation + opts UpdateOpts +} + +type updateArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Key Tuple + Operations []Operation + Opts UpdateOpts +} + +// NewUpdateRequest returns a new empty UpdateRequest. +func NewUpdateRequest(space string) *UpdateRequest { + req := new(UpdateRequest) + req.initImpl("crud.update") + req.setSpace(space) + req.key = Tuple{} + req.operations = []Operation{} + req.opts = UpdateOpts{} + return req +} + +// Key sets the key for the UpdateRequest request. +// Note: default value is nil. +func (req *UpdateRequest) Key(key Tuple) *UpdateRequest { + req.key = key + return req +} + +// Operations sets the operations for UpdateRequest request. +// Note: default value is nil. +func (req *UpdateRequest) Operations(operations []Operation) *UpdateRequest { + req.operations = operations + return req +} + +// Opts sets the options for the UpdateRequest request. +// Note: default value is nil. +func (req *UpdateRequest) Opts(opts UpdateOpts) *UpdateRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *UpdateRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := updateArgs{Space: req.space, Key: req.key, + Operations: req.operations, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *UpdateRequest) Context(ctx context.Context) *UpdateRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/upsert.go b/crud/upsert.go new file mode 100644 index 000000000..07c373b68 --- /dev/null +++ b/crud/upsert.go @@ -0,0 +1,147 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// UpsertResult describes result for `crud.upsert` method. +type UpsertResult = Result + +// UpsertOpts describes options for `crud.upsert` method. +type UpsertOpts = SimpleOperationOpts + +// UpsertRequest helps you to create request object to call `crud.upsert` +// for execution by a Connection. +type UpsertRequest struct { + spaceRequest + tuple Tuple + operations []Operation + opts UpsertOpts +} + +type upsertArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Tuple Tuple + Operations []Operation + Opts UpsertOpts +} + +// NewUpsertRequest returns a new empty UpsertRequest. +func NewUpsertRequest(space string) *UpsertRequest { + req := new(UpsertRequest) + req.initImpl("crud.upsert") + req.setSpace(space) + req.tuple = Tuple{} + req.operations = []Operation{} + req.opts = UpsertOpts{} + return req +} + +// Tuple sets the tuple for the UpsertRequest request. +// Note: default value is nil. +func (req *UpsertRequest) Tuple(tuple Tuple) *UpsertRequest { + req.tuple = tuple + return req +} + +// Operations sets the operations for the UpsertRequest request. +// Note: default value is nil. +func (req *UpsertRequest) Operations(operations []Operation) *UpsertRequest { + req.operations = operations + return req +} + +// Opts sets the options for the UpsertRequest request. +// Note: default value is nil. +func (req *UpsertRequest) Opts(opts UpsertOpts) *UpsertRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *UpsertRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := upsertArgs{Space: req.space, Tuple: req.tuple, + Operations: req.operations, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *UpsertRequest) Context(ctx context.Context) *UpsertRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// UpsertObjectResult describes result for `crud.upsert_object` method. +type UpsertObjectResult = Result + +// UpsertObjectOpts describes options for `crud.upsert_object` method. +type UpsertObjectOpts = SimpleOperationOpts + +// UpsertObjectRequest helps you to create request object to call +// `crud.upsert_object` for execution by a Connection. +type UpsertObjectRequest struct { + spaceRequest + object Object + operations []Operation + opts UpsertObjectOpts +} + +type upsertObjectArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + Object Object + Operations []Operation + Opts UpsertObjectOpts +} + +// NewUpsertObjectRequest returns a new empty UpsertObjectRequest. +func NewUpsertObjectRequest(space string) *UpsertObjectRequest { + req := new(UpsertObjectRequest) + req.initImpl("crud.upsert_object") + req.setSpace(space) + req.object = MapObject{} + req.operations = []Operation{} + req.opts = UpsertObjectOpts{} + return req +} + +// Object sets the tuple for the UpsertObjectRequest request. +// Note: default value is nil. +func (req *UpsertObjectRequest) Object(object Object) *UpsertObjectRequest { + req.object = object + return req +} + +// Operations sets the operations for the UpsertObjectRequest request. +// Note: default value is nil. +func (req *UpsertObjectRequest) Operations(operations []Operation) *UpsertObjectRequest { + req.operations = operations + return req +} + +// Opts sets the options for the UpsertObjectRequest request. +// Note: default value is nil. +func (req *UpsertObjectRequest) Opts(opts UpsertObjectOpts) *UpsertObjectRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *UpsertObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := upsertObjectArgs{Space: req.space, Object: req.object, + Operations: req.operations, Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *UpsertObjectRequest) Context(ctx context.Context) *UpsertObjectRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/crud/upsert_many.go b/crud/upsert_many.go new file mode 100644 index 000000000..b7bdcad81 --- /dev/null +++ b/crud/upsert_many.go @@ -0,0 +1,144 @@ +package crud + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +// UpsertManyResult describes result for `crud.upsert_many` method. +type UpsertManyResult = ResultMany + +// UpsertManyOpts describes options for `crud.upsert_many` method. +type UpsertManyOpts = OperationManyOpts + +// TupleOperationsData contains tuple with operations to be applied to tuple. +type TupleOperationsData struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Tuple Tuple + Operations []Operation +} + +// UpsertManyRequest helps you to create request object to call +// `crud.upsert_many` for execution by a Connection. +type UpsertManyRequest struct { + spaceRequest + tuplesOperationsData []TupleOperationsData + opts UpsertManyOpts +} + +type upsertManyArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + TuplesOperationsData []TupleOperationsData + Opts UpsertManyOpts +} + +// NewUpsertManyRequest returns a new empty UpsertManyRequest. +func NewUpsertManyRequest(space string) *UpsertManyRequest { + req := new(UpsertManyRequest) + req.initImpl("crud.upsert_many") + req.setSpace(space) + req.tuplesOperationsData = []TupleOperationsData{} + req.opts = UpsertManyOpts{} + return req +} + +// TuplesOperationsData sets tuples and operations for +// the UpsertManyRequest request. +// Note: default value is nil. +func (req *UpsertManyRequest) TuplesOperationsData(tuplesOperationData []TupleOperationsData) *UpsertManyRequest { + req.tuplesOperationsData = tuplesOperationData + return req +} + +// Opts sets the options for the UpsertManyRequest request. +// Note: default value is nil. +func (req *UpsertManyRequest) Opts(opts UpsertManyOpts) *UpsertManyRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *UpsertManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := upsertManyArgs{Space: req.space, TuplesOperationsData: req.tuplesOperationsData, + Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *UpsertManyRequest) Context(ctx context.Context) *UpsertManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// UpsertObjectManyResult describes result for `crud.upsert_object_many` method. +type UpsertObjectManyResult = ResultMany + +// UpsertObjectManyOpts describes options for `crud.upsert_object_many` method. +type UpsertObjectManyOpts = OperationManyOpts + +// ObjectOperationsData contains object with operations to be applied to object. +type ObjectOperationsData struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Object Object + Operations []Operation +} + +// UpsertObjectManyRequest helps you to create request object to call +// `crud.upsert_object_many` for execution by a Connection. +type UpsertObjectManyRequest struct { + spaceRequest + objectsOperationsData []ObjectOperationsData + opts UpsertObjectManyOpts +} + +type upsertObjectManyArgs struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Space string + ObjectsOperationsData []ObjectOperationsData + Opts UpsertObjectManyOpts +} + +// NewUpsertObjectManyRequest returns a new empty UpsertObjectManyRequest. +func NewUpsertObjectManyRequest(space string) *UpsertObjectManyRequest { + req := new(UpsertObjectManyRequest) + req.initImpl("crud.upsert_object_many") + req.setSpace(space) + req.objectsOperationsData = []ObjectOperationsData{} + req.opts = UpsertObjectManyOpts{} + return req +} + +// ObjectOperationsData sets objects and operations +// for the UpsertObjectManyRequest request. +// Note: default value is nil. +func (req *UpsertObjectManyRequest) ObjectsOperationsData( + objectsOperationData []ObjectOperationsData) *UpsertObjectManyRequest { + req.objectsOperationsData = objectsOperationData + return req +} + +// Opts sets the options for the UpsertObjectManyRequest request. +// Note: default value is nil. +func (req *UpsertObjectManyRequest) Opts(opts UpsertObjectManyOpts) *UpsertObjectManyRequest { + req.opts = opts + return req +} + +// Body fills an encoder with the call request body. +func (req *UpsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + args := upsertObjectManyArgs{Space: req.space, ObjectsOperationsData: req.objectsOperationsData, + Opts: req.opts} + req.impl = req.impl.Args(args) + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req *UpsertObjectManyRequest) Context(ctx context.Context) *UpsertObjectManyRequest { + req.impl = req.impl.Context(ctx) + + return req +} diff --git a/go.mod b/go.mod index eaed3ca30..ac2f980d2 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,15 @@ go 1.11 require ( github.com/google/go-cmp v0.5.7 // indirect github.com/google/uuid v1.3.0 + github.com/markphelps/optional v0.10.0 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/pkg/errors v0.9.1 // indirect github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.7.1 // indirect github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 + github.com/vmihailenco/msgpack/v5 v5.3.5 google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/vmihailenco/msgpack.v2 v2.9.2 gotest.tools/v3 v3.2.0 // indirect - github.com/vmihailenco/msgpack/v5 v5.3.5 ) diff --git a/go.sum b/go.sum index 57fa152cc..c819e6e50 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -7,9 +9,12 @@ github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/markphelps/optional v0.10.0 h1:vTaMRRTuN7aPY5X8g6K82W23qR4VMqBNyniC5BIJlqo= +github.com/markphelps/optional v0.10.0/go.mod h1:Fvjs1vxcm7/wDqJPFGEiEM1RuxFl9GCyxQlj9M9YMAQ= github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -24,6 +29,7 @@ github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+ github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -42,6 +48,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -64,10 +71,12 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= diff --git a/request_test.go b/request_test.go index 32bdd87e1..8429fef98 100644 --- a/request_test.go +++ b/request_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" . "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" ) const invalidSpaceMsg = "invalid space" @@ -85,17 +86,13 @@ func assertBodyCall(t testing.TB, requests []Request, errorMsg string) { func assertBodyEqual(t testing.TB, reference []byte, req Request) { t.Helper() - var reqBuf bytes.Buffer - reqEnc := NewEncoder(&reqBuf) - - err := req.Body(&resolver, reqEnc) + reqBody, err := test_helpers.ExtractRequestBody(req, &resolver, NewEncoder) if err != nil { - t.Errorf("An unexpected Response.Body() error: %q", err.Error()) - } else { - reqBody := reqBuf.Bytes() - if !bytes.Equal(reqBody, reference) { - t.Errorf("Encoded request %v != reference %v", reqBody, reference) - } + t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) + } + + if !bytes.Equal(reqBody, reference) { + t.Errorf("Encoded request %v != reference %v", reqBody, reference) } } diff --git a/tarantool_test.go b/tarantool_test.go index 3c11aa4ea..f53e6b528 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -67,36 +67,6 @@ func (m *Member) DecodeMsgpack(d *decoder) error { return nil } -// msgpack.v2 and msgpack.v5 return different uint types in responses. The -// function helps to unify a result. -func convertUint64(v interface{}) (result uint64, err error) { - switch v := v.(type) { - case uint: - result = uint64(v) - case uint8: - result = uint64(v) - case uint16: - result = uint64(v) - case uint32: - result = uint64(v) - case uint64: - result = uint64(v) - case int: - result = uint64(v) - case int8: - result = uint64(v) - case int16: - result = uint64(v) - case int32: - result = uint64(v) - case int64: - result = uint64(v) - default: - err = fmt.Errorf("Non-number value %T", v) - } - return -} - var server = "127.0.0.1:3013" var spaceNo = uint32(617) var spaceName = "test" @@ -874,7 +844,7 @@ func TestClient(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Insert (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != 1 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1 { t.Errorf("Unexpected body of Insert (0)") } if h, ok := tpl[1].(string); !ok || h != "hello" { @@ -910,7 +880,7 @@ func TestClient(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Delete (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != 1 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1 { t.Errorf("Unexpected body of Delete (0)") } if h, ok := tpl[1].(string); !ok || h != "hello" { @@ -958,7 +928,7 @@ func TestClient(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Replace (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != 2 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 2 { t.Errorf("Unexpected body of Replace (0)") } if h, ok := tpl[1].(string); !ok || h != "hi" { @@ -986,7 +956,7 @@ func TestClient(t *testing.T) { if len(tpl) != 2 { t.Errorf("Unexpected body of Update (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != 2 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 2 { t.Errorf("Unexpected body of Update (0)") } if h, ok := tpl[1].(string); !ok || h != "bye" { @@ -1042,7 +1012,7 @@ func TestClient(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != 10 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 10 { t.Errorf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "val 10" { @@ -1175,7 +1145,7 @@ func TestClient(t *testing.T) { if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } - if val, err := convertUint64(resp.Data[0]); err != nil || val != 11 { + if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != 11 { t.Errorf("5 + 6 == 11, but got %v", val) } } @@ -1220,7 +1190,7 @@ func TestClientSessionPush(t *testing.T) { t.Errorf("Response is nil after CallAsync") } else if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Call17Async") - } else if val, err := convertUint64(resp.Data[0]); err != nil || val != pushMax { + } else if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != pushMax { t.Errorf("Result is not %d: %v", pushMax, resp.Data) } @@ -1252,12 +1222,12 @@ func TestClientSessionPush(t *testing.T) { } if resp.Code == PushCode { pushCnt += 1 - if val, err := convertUint64(resp.Data[0]); err != nil || val != pushCnt { + if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != pushCnt { t.Errorf("Unexpected push data = %v", resp.Data) } } else { respCnt += 1 - if val, err := convertUint64(resp.Data[0]); err != nil || val != pushMax { + if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != pushMax { t.Errorf("Result is not %d: %v", pushMax, resp.Data) } } @@ -1281,7 +1251,7 @@ func TestClientSessionPush(t *testing.T) { resp, err := fut.Get() if err != nil { t.Errorf("Unable to call fut.Get(): %s", err) - } else if val, err := convertUint64(resp.Data[0]); err != nil || val != pushMax { + } else if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != pushMax { t.Errorf("Result is not %d: %v", pushMax, resp.Data) } @@ -2150,7 +2120,7 @@ func TestClientRequestObjects(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Insert (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != uint64(i) { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != uint64(i) { t.Errorf("Unexpected body of Insert (0)") } if h, ok := tpl[1].(string); !ok || h != fmt.Sprintf("val %d", i) { @@ -2188,7 +2158,7 @@ func TestClientRequestObjects(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Replace (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != uint64(i) { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != uint64(i) { t.Errorf("Unexpected body of Replace (0)") } if h, ok := tpl[1].(string); !ok || h != fmt.Sprintf("val %d", i) { @@ -2225,7 +2195,7 @@ func TestClientRequestObjects(t *testing.T) { if len(tpl) != 3 { t.Errorf("Unexpected body of Delete (tuple len)") } - if id, err := convertUint64(tpl[0]); err != nil || id != uint64(1016) { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != uint64(1016) { t.Errorf("Unexpected body of Delete (0)") } if h, ok := tpl[1].(string); !ok || h != "val 1016" { @@ -2259,7 +2229,7 @@ func TestClientRequestObjects(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Update") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != uint64(1010) { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != uint64(1010) { t.Errorf("Unexpected body of Update (0)") } if h, ok := tpl[1].(string); !ok || h != "val 1010" { @@ -2294,13 +2264,13 @@ func TestClientRequestObjects(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != 1010 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1010 { t.Errorf("Unexpected body of Update (0)") } if h, ok := tpl[1].(string); !ok || h != "bye" { t.Errorf("Unexpected body of Update (1)") } - if h, err := convertUint64(tpl[2]); err != nil || h != 1 { + if h, err := test_helpers.ConvertUint64(tpl[2]); err != nil || h != 1 { t.Errorf("Unexpected body of Update (2)") } } @@ -2387,7 +2357,7 @@ func TestClientRequestObjects(t *testing.T) { if len(resp.Data) < 1 { t.Errorf("Response.Data is empty after Eval") } - if val, err := convertUint64(resp.Data[0]); err != nil || val != 11 { + if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != 11 { t.Errorf("5 + 6 == 11, but got %v", val) } @@ -2479,7 +2449,7 @@ func testConnectionDoSelectRequestCheck(t *testing.T, if tpl, ok := resp.Data[i].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != key { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != key { t.Errorf("Unexpected body of Select (0) %v, expected %d", tpl[0], key) } @@ -2798,7 +2768,7 @@ func TestStream_Commit(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != 1001 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1001 { t.Fatalf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "hello2" { @@ -2833,7 +2803,7 @@ func TestStream_Commit(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != 1001 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1001 { t.Fatalf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "hello2" { @@ -2913,7 +2883,7 @@ func TestStream_Rollback(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { - if id, err := convertUint64(tpl[0]); err != nil || id != 1001 { + if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1001 { t.Fatalf("Unexpected body of Select (0)") } if h, ok := tpl[1].(string); !ok || h != "hello2" { @@ -3018,7 +2988,7 @@ func TestStream_TxnIsolationLevel(t *testing.T) { require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 3, len(tpl), "unexpected body of Select") - key, err := convertUint64(tpl[0]) + key, err := test_helpers.ConvertUint64(tpl[0]) require.Nilf(t, err, "unexpected body of Select (0)") require.Equalf(t, uint64(1001), key, "unexpected body of Select (0)") diff --git a/test_helpers/main.go b/test_helpers/main.go index 3363ed375..4aaa91f50 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -372,3 +372,33 @@ func copyFile(srcFile, dstFile string) error { return nil } + +// msgpack.v2 and msgpack.v5 return different uint types in responses. The +// function helps to unify a result. +func ConvertUint64(v interface{}) (result uint64, err error) { + switch v := v.(type) { + case uint: + result = uint64(v) + case uint8: + result = uint64(v) + case uint16: + result = uint64(v) + case uint32: + result = uint64(v) + case uint64: + result = uint64(v) + case int: + result = uint64(v) + case int8: + result = uint64(v) + case int16: + result = uint64(v) + case int32: + result = uint64(v) + case int64: + result = uint64(v) + default: + err = fmt.Errorf("Non-number value %T", v) + } + return +} diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 12c65009e..34a2e2980 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -1,7 +1,9 @@ package test_helpers import ( + "bytes" "fmt" + "io" "testing" "time" @@ -228,3 +230,16 @@ func CheckEqualBoxErrors(t *testing.T, expected tarantool.BoxError, actual taran } } } + +func ExtractRequestBody(req tarantool.Request, resolver tarantool.SchemaResolver, + newEncFunc func(w io.Writer) *encoder) ([]byte, error) { + var reqBuf bytes.Buffer + reqEnc := newEncFunc(&reqBuf) + + err := req.Body(resolver, reqEnc) + if err != nil { + return nil, fmt.Errorf("An unexpected Response.Body() error: %q", err.Error()) + } + + return reqBuf.Bytes(), nil +} From e257ff30dd4db03d38cee9f7d9ec0ad59d25e999 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 25 Jan 2023 22:37:46 +0300 Subject: [PATCH 406/605] api: add Dialer interface and implementation The patch adds ability to mock network connection to a Tarantool instance. The default implementation can be used as a basic building block for another package or a connector. Part of https://github.com/tarantool/roadmap-internal/issues/197 --- CHANGELOG.md | 2 + connection.go | 297 ++++++----------------------------- dial.go | 392 ++++++++++++++++++++++++++++++++++++++++++++++ dial_test.go | 340 ++++++++++++++++++++++++++++++++++++++++ request.go | 43 +++-- tarantool_test.go | 66 +++++++- 6 files changed, 871 insertions(+), 269 deletions(-) create mode 100644 dial.go create mode 100644 dial_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index cec9d35f1..b49105d21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support pagination (#246) - A Makefile target to test with race detector (#218) - Support CRUD API (#108) +- An ability to replace a base network connection to a Tarantool + instance (#265) ### Changed diff --git a/connection.go b/connection.go index efdd09e65..7da3f26d0 100644 --- a/connection.go +++ b/connection.go @@ -3,8 +3,6 @@ package tarantool import ( - "bufio" - "bytes" "context" "encoding/binary" "errors" @@ -12,9 +10,7 @@ import ( "io" "log" "math" - "net" "runtime" - "strings" "sync" "sync/atomic" "time" @@ -29,11 +25,6 @@ const ( connClosed = 3 ) -const ( - connTransportNone = "" - connTransportSsl = "ssl" -) - const shutdownEventKey = "box.shutdown" type ConnEventKind int @@ -149,7 +140,7 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac // More on graceful shutdown: https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/ type Connection struct { addr string - c net.Conn + c Conn mutex sync.Mutex cond *sync.Cond // Schema contains schema loaded on connection. @@ -237,16 +228,13 @@ type connShard struct { enc *encoder } -// Greeting is a message sent by Tarantool on connect. -type Greeting struct { - Version string - auth string -} - // Opts is a way to configure Connection type Opts struct { // Auth is an authentication method. Auth Auth + // Dialer is a Dialer object used to create a new connection to a + // Tarantool instance. TtDialer is a default one. + Dialer Dialer // Timeout for response to a particular request. The timeout is reset when // push messages are received. If Timeout is zero, any request can be // blocked infinitely. @@ -377,6 +365,9 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { if conn.opts.Concurrency == 0 || conn.opts.Concurrency > maxprocs*128 { conn.opts.Concurrency = maxprocs * 4 } + if conn.opts.Dialer == nil { + conn.opts.Dialer = TtDialer{} + } if c := conn.opts.Concurrency; c&(c-1) != 0 { for i := uint(1); i < 32; i *= 2 { c |= c >> i @@ -504,118 +495,43 @@ func (conn *Connection) cancelFuture(fut *Future, err error) { } func (conn *Connection) dial() (err error) { - var connection net.Conn - network := "tcp" opts := conn.opts - address := conn.addr - timeout := opts.Reconnect / 2 - transport := opts.Transport - if timeout == 0 { - timeout = 500 * time.Millisecond - } else if timeout > 5*time.Second { - timeout = 5 * time.Second - } - // Unix socket connection - addrLen := len(address) - if addrLen > 0 && (address[0] == '.' || address[0] == '/') { - network = "unix" - } else if addrLen >= 7 && address[0:7] == "unix://" { - network = "unix" - address = address[7:] - } else if addrLen >= 5 && address[0:5] == "unix:" { - network = "unix" - address = address[5:] - } else if addrLen >= 6 && address[0:6] == "unix/:" { - network = "unix" - address = address[6:] - } else if addrLen >= 6 && address[0:6] == "tcp://" { - address = address[6:] - } else if addrLen >= 4 && address[0:4] == "tcp:" { - address = address[4:] - } - if transport == connTransportNone { - connection, err = net.DialTimeout(network, address, timeout) - } else if transport == connTransportSsl { - connection, err = sslDialTimeout(network, address, timeout, opts.Ssl) - } else { - err = errors.New("An unsupported transport type: " + transport) - } - if err != nil { - return - } - dc := &DeadlineIO{to: opts.Timeout, c: connection} - r := bufio.NewReaderSize(dc, 128*1024) - w := bufio.NewWriterSize(dc, 128*1024) - greeting := make([]byte, 128) - _, err = io.ReadFull(r, greeting) + dialTimeout := opts.Reconnect / 2 + if dialTimeout == 0 { + dialTimeout = 500 * time.Millisecond + } else if dialTimeout > 5*time.Second { + dialTimeout = 5 * time.Second + } + + var c Conn + c, err = conn.opts.Dialer.Dial(conn.addr, DialOpts{ + DialTimeout: dialTimeout, + IoTimeout: opts.Timeout, + Transport: opts.Transport, + Ssl: opts.Ssl, + RequiredProtocol: opts.RequiredProtocolInfo, + Auth: opts.Auth, + User: opts.User, + Password: opts.Pass, + }) if err != nil { - connection.Close() return } - 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 != "" { - auth := opts.Auth - if opts.Auth == AutoAuth { - if conn.serverProtocolInfo.Auth != AutoAuth { - auth = conn.serverProtocolInfo.Auth - } else { - auth = ChapSha1Auth - } - } - - var req Request - if auth == ChapSha1Auth { - salt := conn.Greeting.auth - req, err = newChapSha1AuthRequest(conn.opts.User, salt, opts.Pass) - if err != nil { - return fmt.Errorf("auth: %w", err) - } - } else if auth == PapSha256Auth { - if opts.Transport != connTransportSsl { - return errors.New("auth: forbidden to use " + auth.String() + - " unless SSL is enabled for the connection") - } - req = newPapSha256AuthRequest(conn.opts.User, opts.Pass) - } else { - connection.Close() - return errors.New("auth: " + auth.String()) - } - if err = conn.writeRequest(w, req); err != nil { - connection.Close() - return fmt.Errorf("auth: %w", err) - } - if _, err = conn.readResponse(r); err != nil { - connection.Close() - return fmt.Errorf("auth: %w", err) - } - } + conn.Greeting.Version = c.Greeting().Version + conn.serverProtocolInfo = c.ProtocolInfo() // Watchers. conn.watchMap.Range(func(key, value interface{}) bool { st := value.(chan watchState) state := <-st if state.unready != nil { + st <- state return true } req := newWatchRequest(key.(string)) - if err = conn.writeRequest(w, req); err != nil { + if err = writeRequest(c, req); err != nil { st <- state return false } @@ -626,17 +542,18 @@ func (conn *Connection) dial() (err error) { }) if err != nil { + c.Close() return fmt.Errorf("unable to register watch: %w", err) } // Only if connected and fully initialized. conn.lockShards() - conn.c = connection + conn.c = c atomic.StoreUint32(&conn.state, connConnected) conn.cond.Broadcast() conn.unlockShards() - go conn.writer(w, connection) - go conn.reader(r, connection) + go conn.writer(c, c) + go conn.reader(c, c) // Subscribe shutdown event to process graceful shutdown. if conn.shutdownWatcher == nil && isFeatureInSlice(WatchersFeature, conn.serverProtocolInfo.Features) { @@ -700,45 +617,6 @@ func pack(h *smallWBuf, enc *encoder, reqid uint32, return } -func (conn *Connection) writeRequest(w *bufio.Writer, req Request) error { - var packet smallWBuf - err := pack(&packet, newEncoder(&packet), 0, req, ignoreStreamId, nil) - - if err != nil { - return fmt.Errorf("pack error: %w", err) - } - if err = write(w, packet.b); err != nil { - return fmt.Errorf("write error: %w", err) - } - if err = w.Flush(); err != nil { - return fmt.Errorf("flush error: %w", err) - } - return err -} - -func (conn *Connection) readResponse(r io.Reader) (Response, error) { - respBytes, err := conn.read(r) - if err != nil { - return Response{}, fmt.Errorf("read error: %w", err) - } - - resp := Response{buf: smallBuf{b: respBytes}} - err = resp.decodeHeader(conn.dec) - if err != nil { - return resp, fmt.Errorf("decode response header error: %w", err) - } - err = resp.decodeBody() - if err != nil { - switch err.(type) { - case Error: - return resp, err - default: - return resp, fmt.Errorf("decode response body error: %w", err) - } - } - return resp, nil -} - func (conn *Connection) createConnection(reconnect bool) (err error) { var reconnects uint for conn.c == nil && conn.state == connDisconnected { @@ -805,7 +683,7 @@ func (conn *Connection) closeConnection(neterr error, forever bool) (err error) return } -func (conn *Connection) reconnectImpl(neterr error, c net.Conn) { +func (conn *Connection) reconnectImpl(neterr error, c Conn) { if conn.opts.Reconnect > 0 { if c == conn.c { conn.closeConnection(neterr, false) @@ -818,7 +696,7 @@ func (conn *Connection) reconnectImpl(neterr error, c net.Conn) { } } -func (conn *Connection) reconnect(neterr error, c net.Conn) { +func (conn *Connection) reconnect(neterr error, c Conn) { conn.mutex.Lock() defer conn.mutex.Unlock() conn.reconnectImpl(neterr, c) @@ -865,7 +743,7 @@ func (conn *Connection) notify(kind ConnEventKind) { } } -func (conn *Connection) writer(w *bufio.Writer, c net.Conn) { +func (conn *Connection) writer(w writeFlusher, c Conn) { var shardn uint32 var packet smallWBuf for atomic.LoadUint32(&conn.state) != connClosed { @@ -897,7 +775,7 @@ func (conn *Connection) writer(w *bufio.Writer, c net.Conn) { if packet.Len() == 0 { continue } - if err := write(w, packet.b); err != nil { + if _, err := w.Write(packet.b); err != nil { conn.reconnect(err, c) return } @@ -945,14 +823,14 @@ func readWatchEvent(reader io.Reader) (connWatchEvent, error) { return event, nil } -func (conn *Connection) reader(r *bufio.Reader, c net.Conn) { +func (conn *Connection) reader(r io.Reader, c Conn) { events := make(chan connWatchEvent, 1024) defer close(events) go conn.eventer(events) for atomic.LoadUint32(&conn.state) != connClosed { - respBytes, err := conn.read(r) + respBytes, err := read(r, conn.lenbuf[:]) if err != nil { conn.reconnect(err, c) return @@ -1299,31 +1177,20 @@ func (conn *Connection) timeouts() { } } -func write(w io.Writer, data []byte) (err error) { - l, err := w.Write(data) - if err != nil { - return - } - if l != len(data) { - panic("Wrong length writed") - } - return -} - -func (conn *Connection) read(r io.Reader) (response []byte, err error) { +func read(r io.Reader, lenbuf []byte) (response []byte, err error) { var length int - if _, err = io.ReadFull(r, conn.lenbuf[:]); err != nil { + if _, err = io.ReadFull(r, lenbuf); err != nil { return } - if conn.lenbuf[0] != 0xce { + if lenbuf[0] != 0xce { err = errors.New("Wrong response header") return } - length = (int(conn.lenbuf[1]) << 24) + - (int(conn.lenbuf[2]) << 16) + - (int(conn.lenbuf[3]) << 8) + - int(conn.lenbuf[4]) + length = (int(lenbuf[1]) << 24) + + (int(lenbuf[2]) << 16) + + (int(lenbuf[3]) << 8) + + int(lenbuf[4]) if length == 0 { err = errors.New("Response should not be 0 length") @@ -1629,78 +1496,6 @@ func (conn *Connection) newWatcherImpl(key string, callback WatchCallback) (Watc }, 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 - - req := NewIdRequest(clientProtocolInfo) - werr := conn.writeRequest(w, req) - if werr != nil { - return fmt.Errorf("identify: %w", werr) - } - - resp, rerr := conn.readResponse(r) - if rerr != nil { - if resp.Code == ErrUnknownRequestType { - // IPROTO_ID requests are not supported by server. - return nil - } - - return fmt.Errorf("identify: %w", 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. diff --git a/dial.go b/dial.go new file mode 100644 index 000000000..abed85e1b --- /dev/null +++ b/dial.go @@ -0,0 +1,392 @@ +package tarantool + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "net" + "strings" + "time" +) + +const ( + dialTransportNone = "" + dialTransportSsl = "ssl" +) + +// Greeting is a message sent by Tarantool on connect. +type Greeting struct { + Version string +} + +// writeFlusher is the interface that groups the basic Write and Flush methods. +type writeFlusher interface { + io.Writer + Flush() error +} + +// Conn is a generic stream-oriented network connection to a Tarantool +// instance. +type Conn interface { + // Read reads data from the connection. + Read(b []byte) (int, error) + // Write writes data to the connection. There may be an internal buffer for + // better performance control from a client side. + Write(b []byte) (int, error) + // Flush writes any buffered data. + Flush() error + // Close closes the connection. + // Any blocked Read or Flush operations will be unblocked and return + // errors. + Close() error + // LocalAddr returns the local network address, if known. + LocalAddr() net.Addr + // RemoteAddr returns the remote network address, if known. + RemoteAddr() net.Addr + // Greeting returns server greeting. + Greeting() Greeting + // ProtocolInfo returns server protocol info. + ProtocolInfo() ProtocolInfo +} + +// DialOpts is a way to configure a Dial method to create a new Conn. +type DialOpts struct { + // DialTimeout is a timeout for an initial network dial. + DialTimeout time.Duration + // IoTimeout is a timeout per a network read/write. + IoTimeout time.Duration + // Transport is a connect transport type. + Transport string + // Ssl configures "ssl" transport. + Ssl SslOpts + // RequiredProtocol contains minimal protocol version and + // list of protocol features that should be supported by + // Tarantool server. By default there are no restrictions. + RequiredProtocol ProtocolInfo + // Auth is an authentication method. + Auth Auth + // Username for logging in to Tarantool. + User string + // User password for logging in to Tarantool. + Password string +} + +// Dialer is the interface that wraps a method to connect to a Tarantool +// instance. The main idea is to provide a ready-to-work connection with +// basic preparation, successful authorization and additional checks. +// +// You can provide your own implementation to Connect() call via Opts.Dialer if +// some functionality is not implemented in the connector. See TtDialer.Dial() +// implementation as example. +type Dialer interface { + // Dial connects to a Tarantool instance to the address with specified + // options. + Dial(address string, opts DialOpts) (Conn, error) +} + +type tntConn struct { + net net.Conn + reader io.Reader + writer writeFlusher + greeting Greeting + protocol ProtocolInfo +} + +// TtDialer is a default implementation of the Dialer interface which is +// used by the connector. +type TtDialer struct { +} + +// Dial connects to a Tarantool instance to the address with specified +// options. +func (t TtDialer) Dial(address string, opts DialOpts) (Conn, error) { + var err error + conn := new(tntConn) + + if conn.net, err = dial(address, opts); err != nil { + return nil, fmt.Errorf("failed to dial: %w", err) + } + + dc := &DeadlineIO{to: opts.IoTimeout, c: conn.net} + conn.reader = bufio.NewReaderSize(dc, 128*1024) + conn.writer = bufio.NewWriterSize(dc, 128*1024) + + var version, salt string + if version, salt, err = readGreeting(conn.reader); err != nil { + conn.net.Close() + return nil, fmt.Errorf("failed to read greeting: %w", err) + } + conn.greeting.Version = version + + if conn.protocol, err = identify(conn.writer, conn.reader); err != nil { + conn.net.Close() + return nil, fmt.Errorf("failed to identify: %w", err) + } + + if err = checkProtocolInfo(opts.RequiredProtocol, conn.protocol); err != nil { + conn.net.Close() + return nil, fmt.Errorf("invalid server protocol: %w", err) + } + + if opts.User != "" { + if opts.Auth == AutoAuth { + if conn.protocol.Auth != AutoAuth { + opts.Auth = conn.protocol.Auth + } else { + opts.Auth = ChapSha1Auth + } + } + + err := authenticate(conn, opts, salt) + if err != nil { + conn.net.Close() + return nil, fmt.Errorf("failed to authenticate: %w", err) + } + } + + return conn, nil +} + +// Read makes tntConn satisfy the Conn interface. +func (c *tntConn) Read(p []byte) (int, error) { + return c.reader.Read(p) +} + +// Write makes tntConn satisfy the Conn interface. +func (c *tntConn) Write(p []byte) (int, error) { + if l, err := c.writer.Write(p); err != nil { + return l, err + } else if l != len(p) { + return l, errors.New("wrong length written") + } else { + return l, nil + } +} + +// Flush makes tntConn satisfy the Conn interface. +func (c *tntConn) Flush() error { + return c.writer.Flush() +} + +// Close makes tntConn satisfy the Conn interface. +func (c *tntConn) Close() error { + return c.net.Close() +} + +// RemoteAddr makes tntConn satisfy the Conn interface. +func (c *tntConn) RemoteAddr() net.Addr { + return c.net.RemoteAddr() +} + +// LocalAddr makes tntConn satisfy the Conn interface. +func (c *tntConn) LocalAddr() net.Addr { + return c.net.LocalAddr() +} + +// Greeting makes tntConn satisfy the Conn interface. +func (c *tntConn) Greeting() Greeting { + return c.greeting +} + +// ProtocolInfo makes tntConn satisfy the Conn interface. +func (c *tntConn) ProtocolInfo() ProtocolInfo { + return c.protocol +} + +// dial connects to a Tarantool instance. +func dial(address string, opts DialOpts) (net.Conn, error) { + network, address := parseAddress(address) + switch opts.Transport { + case dialTransportNone: + return net.DialTimeout(network, address, opts.DialTimeout) + case dialTransportSsl: + return sslDialTimeout(network, address, opts.DialTimeout, opts.Ssl) + default: + return nil, fmt.Errorf("unsupported transport type: %s", opts.Transport) + } +} + +// parseAddress split address into network and address parts. +func parseAddress(address string) (string, string) { + network := "tcp" + addrLen := len(address) + + if addrLen > 0 && (address[0] == '.' || address[0] == '/') { + network = "unix" + } else if addrLen >= 7 && address[0:7] == "unix://" { + network = "unix" + address = address[7:] + } else if addrLen >= 5 && address[0:5] == "unix:" { + network = "unix" + address = address[5:] + } else if addrLen >= 6 && address[0:6] == "unix/:" { + network = "unix" + address = address[6:] + } else if addrLen >= 6 && address[0:6] == "tcp://" { + address = address[6:] + } else if addrLen >= 4 && address[0:4] == "tcp:" { + address = address[4:] + } + + return network, address +} + +// readGreeting reads a greeting message. +func readGreeting(reader io.Reader) (string, string, error) { + var version, salt string + + data := make([]byte, 128) + _, err := io.ReadFull(reader, data) + if err == nil { + version = bytes.NewBuffer(data[:64]).String() + salt = bytes.NewBuffer(data[64:108]).String() + } + + return version, salt, err +} + +// identify sends info about client protocol, receives info +// about server protocol in response and stores it in the connection. +func identify(w writeFlusher, r io.Reader) (ProtocolInfo, error) { + var info ProtocolInfo + + req := NewIdRequest(clientProtocolInfo) + if err := writeRequest(w, req); err != nil { + return info, err + } + + resp, err := readResponse(r) + if err != nil { + if resp.Code == ErrUnknownRequestType { + // IPROTO_ID requests are not supported by server. + return info, nil + } + + return info, err + } + + if len(resp.Data) == 0 { + return info, errors.New("unexpected response: no data") + } + + info, ok := resp.Data[0].(ProtocolInfo) + if !ok { + return info, errors.New("unexpected response: wrong data") + } + + return info, nil +} + +// checkProtocolInfo checks that required protocol version is +// and protocol features are supported. +func checkProtocolInfo(required ProtocolInfo, actual ProtocolInfo) error { + if required.Version > actual.Version { + return fmt.Errorf("protocol version %d is not supported", + required.Version) + } + + // It seems that iterating over a small list is way faster + // than building a map: https://stackoverflow.com/a/52710077/11646599 + var missed []string + for _, requiredFeature := range required.Features { + found := false + for _, actualFeature := range actual.Features { + if requiredFeature == actualFeature { + found = true + } + } + if !found { + missed = append(missed, requiredFeature.String()) + } + } + + switch { + case len(missed) == 1: + return fmt.Errorf("protocol feature %s is not supported", missed[0]) + case len(missed) > 1: + joined := strings.Join(missed, ", ") + return fmt.Errorf("protocol features %s are not supported", joined) + default: + return nil + } +} + +// authenticate authenticate for a connection. +func authenticate(c Conn, opts DialOpts, salt string) error { + auth := opts.Auth + user := opts.User + pass := opts.Password + + var req Request + var err error + + switch opts.Auth { + case ChapSha1Auth: + req, err = newChapSha1AuthRequest(user, pass, salt) + if err != nil { + return err + } + case PapSha256Auth: + if opts.Transport != dialTransportSsl { + return errors.New("forbidden to use " + auth.String() + + " unless SSL is enabled for the connection") + } + req = newPapSha256AuthRequest(user, pass) + default: + return errors.New("unsupported method " + opts.Auth.String()) + } + + if err = writeRequest(c, req); err != nil { + return err + } + if _, err = readResponse(c); err != nil { + return err + } + return nil +} + +// writeRequest writes a request to the writer. +func writeRequest(w writeFlusher, req Request) error { + var packet smallWBuf + err := pack(&packet, newEncoder(&packet), 0, req, ignoreStreamId, nil) + + if err != nil { + return fmt.Errorf("pack error: %w", err) + } + if _, err = w.Write(packet.b); err != nil { + return fmt.Errorf("write error: %w", err) + } + if err = w.Flush(); err != nil { + return fmt.Errorf("flush error: %w", err) + } + return err +} + +// readResponse reads a response from the reader. +func readResponse(r io.Reader) (Response, error) { + var lenbuf [PacketLengthBytes]byte + + respBytes, err := read(r, lenbuf[:]) + if err != nil { + return Response{}, fmt.Errorf("read error: %w", err) + } + + resp := Response{buf: smallBuf{b: respBytes}} + err = resp.decodeHeader(newDecoder(&smallBuf{})) + if err != nil { + return resp, fmt.Errorf("decode response header error: %w", err) + } + + err = resp.decodeBody() + if err != nil { + switch err.(type) { + case Error: + return resp, err + default: + return resp, fmt.Errorf("decode response body error: %w", err) + } + } + return resp, nil +} diff --git a/dial_test.go b/dial_test.go new file mode 100644 index 000000000..182e9c866 --- /dev/null +++ b/dial_test.go @@ -0,0 +1,340 @@ +package tarantool_test + +import ( + "bytes" + "errors" + "net" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/tarantool/go-tarantool" +) + +type mockErrorDialer struct { + err error +} + +func (m mockErrorDialer) Dial(address string, + opts tarantool.DialOpts) (tarantool.Conn, error) { + return nil, m.err +} + +func TestDialer_Dial_error(t *testing.T) { + const errMsg = "any msg" + dialer := mockErrorDialer{ + err: errors.New(errMsg), + } + + conn, err := tarantool.Connect("any", tarantool.Opts{ + Dialer: dialer, + }) + assert.Nil(t, conn) + assert.ErrorContains(t, err, errMsg) +} + +type mockPassedDialer struct { + address string + opts tarantool.DialOpts +} + +func (m *mockPassedDialer) Dial(address string, + opts tarantool.DialOpts) (tarantool.Conn, error) { + m.address = address + m.opts = opts + return nil, errors.New("does not matter") +} + +func TestDialer_Dial_passedOpts(t *testing.T) { + const addr = "127.0.0.1:8080" + opts := tarantool.DialOpts{ + DialTimeout: 500 * time.Millisecond, + IoTimeout: 2, + Transport: "any", + Ssl: tarantool.SslOpts{ + KeyFile: "a", + CertFile: "b", + CaFile: "c", + Ciphers: "d", + }, + RequiredProtocol: tarantool.ProtocolInfo{ + Auth: tarantool.ChapSha1Auth, + Version: 33, + Features: []tarantool.ProtocolFeature{ + tarantool.ErrorExtensionFeature, + }, + }, + Auth: tarantool.ChapSha1Auth, + User: "user", + Password: "password", + } + + dialer := &mockPassedDialer{} + conn, err := tarantool.Connect(addr, tarantool.Opts{ + Dialer: dialer, + Timeout: opts.IoTimeout, + Transport: opts.Transport, + Ssl: opts.Ssl, + Auth: opts.Auth, + User: opts.User, + Pass: opts.Password, + RequiredProtocolInfo: opts.RequiredProtocol, + }) + + assert.Nil(t, conn) + assert.NotNil(t, err) + assert.Equal(t, addr, dialer.address) + assert.Equal(t, opts, dialer.opts) +} + +type mockIoConn struct { + // Sends an event on Read()/Write()/Flush(). + read, written chan struct{} + // Read()/Write() buffers. + readbuf, writebuf bytes.Buffer + // Calls readWg/writeWg.Wait() in Read()/Flush(). + readWg, writeWg sync.WaitGroup + // How many times to wait before a wg.Wait() call. + readWgDelay, writeWgDelay int + // Write()/Read()/Flush()/Close() calls count. + writeCnt, readCnt, flushCnt, closeCnt int + // LocalAddr()/RemoteAddr() calls count. + localCnt, remoteCnt int + // Greeting()/ProtocolInfo() calls count. + greetingCnt, infoCnt int + // Values for LocalAddr()/RemoteAddr(). + local, remote net.Addr + // Value for Greeting(). + greeting tarantool.Greeting + // Value for ProtocolInfo(). + info tarantool.ProtocolInfo +} + +func (m *mockIoConn) Read(b []byte) (int, error) { + m.readCnt++ + if m.readWgDelay == 0 { + m.readWg.Wait() + } + m.readWgDelay-- + + ret, err := m.readbuf.Read(b) + + if m.read != nil { + m.read <- struct{}{} + } + + return ret, err +} + +func (m *mockIoConn) Write(b []byte) (int, error) { + m.writeCnt++ + if m.writeWgDelay == 0 { + m.writeWg.Wait() + } + m.writeWgDelay-- + + ret, err := m.writebuf.Write(b) + + if m.written != nil { + m.written <- struct{}{} + } + + return ret, err +} + +func (m *mockIoConn) Flush() error { + m.flushCnt++ + return nil +} + +func (m *mockIoConn) Close() error { + m.closeCnt++ + return nil +} + +func (m *mockIoConn) LocalAddr() net.Addr { + m.localCnt++ + return m.local +} + +func (m *mockIoConn) RemoteAddr() net.Addr { + m.remoteCnt++ + return m.remote +} + +func (m *mockIoConn) Greeting() tarantool.Greeting { + m.greetingCnt++ + return m.greeting +} + +func (m *mockIoConn) ProtocolInfo() tarantool.ProtocolInfo { + m.infoCnt++ + return m.info +} + +type mockIoDialer struct { + init func(conn *mockIoConn) + conn *mockIoConn +} + +func newMockIoConn() *mockIoConn { + conn := new(mockIoConn) + conn.readWg.Add(1) + conn.writeWg.Add(1) + return conn +} + +func (m *mockIoDialer) Dial(address string, + opts tarantool.DialOpts) (tarantool.Conn, error) { + m.conn = newMockIoConn() + if m.init != nil { + m.init(m.conn) + } + return m.conn, nil +} + +func dialIo(t *testing.T, + init func(conn *mockIoConn)) (*tarantool.Connection, mockIoDialer) { + t.Helper() + + dialer := mockIoDialer{ + init: init, + } + conn, err := tarantool.Connect("any", tarantool.Opts{ + Dialer: &dialer, + Timeout: 1000 * time.Second, // Avoid pings. + SkipSchema: true, + }) + require.Nil(t, err) + require.NotNil(t, conn) + + return conn, dialer +} + +func TestConn_Close(t *testing.T) { + conn, dialer := dialIo(t, nil) + conn.Close() + + assert.Equal(t, 1, dialer.conn.closeCnt) + + dialer.conn.readWg.Done() + dialer.conn.writeWg.Done() +} + +type stubAddr struct { + net.Addr + str string +} + +func (a stubAddr) String() string { + return a.str +} + +func TestConn_LocalAddr(t *testing.T) { + const addr = "any" + conn, dialer := dialIo(t, func(conn *mockIoConn) { + conn.local = stubAddr{str: addr} + }) + defer func() { + dialer.conn.readWg.Done() + dialer.conn.writeWg.Done() + conn.Close() + }() + + assert.Equal(t, addr, conn.LocalAddr()) + assert.Equal(t, 1, dialer.conn.localCnt) +} + +func TestConn_RemoteAddr(t *testing.T) { + const addr = "any" + conn, dialer := dialIo(t, func(conn *mockIoConn) { + conn.remote = stubAddr{str: addr} + }) + defer func() { + dialer.conn.readWg.Done() + dialer.conn.writeWg.Done() + conn.Close() + }() + + assert.Equal(t, addr, conn.RemoteAddr()) + assert.Equal(t, 1, dialer.conn.remoteCnt) +} + +func TestConn_Greeting(t *testing.T) { + greeting := tarantool.Greeting{ + Version: "any", + } + conn, dialer := dialIo(t, func(conn *mockIoConn) { + conn.greeting = greeting + }) + defer func() { + dialer.conn.readWg.Done() + dialer.conn.writeWg.Done() + conn.Close() + }() + + assert.Equal(t, &greeting, conn.Greeting) + assert.Equal(t, 1, dialer.conn.greetingCnt) +} + +func TestConn_ProtocolInfo(t *testing.T) { + info := tarantool.ProtocolInfo{ + Auth: tarantool.ChapSha1Auth, + Version: 33, + Features: []tarantool.ProtocolFeature{ + tarantool.ErrorExtensionFeature, + }, + } + conn, dialer := dialIo(t, func(conn *mockIoConn) { + conn.info = info + }) + defer func() { + dialer.conn.readWg.Done() + dialer.conn.writeWg.Done() + conn.Close() + }() + + assert.Equal(t, info, conn.ServerProtocolInfo()) + assert.Equal(t, 1, dialer.conn.infoCnt) +} + +func TestConn_ReadWrite(t *testing.T) { + conn, dialer := dialIo(t, func(conn *mockIoConn) { + conn.read = make(chan struct{}) + conn.written = make(chan struct{}) + conn.writeWgDelay = 1 + conn.readbuf.Write([]byte{ + 0xce, 0x00, 0x00, 0x00, 0x0a, // Length. + 0x82, // Header map. + 0x00, 0x00, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x02, + 0x80, // Body map. + }) + conn.Close() + }) + defer func() { + dialer.conn.writeWg.Done() + }() + + fut := conn.Do(tarantool.NewPingRequest()) + + <-dialer.conn.written + dialer.conn.readWg.Done() + <-dialer.conn.read + <-dialer.conn.read + + assert.Equal(t, []byte{ + 0xce, 0x00, 0x00, 0x00, 0xa, // Length. + 0x82, // Header map. + 0x00, 0x40, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x02, + 0x80, // Empty map. + }, dialer.conn.writebuf.Bytes()) + + resp, err := fut.Get() + assert.Nil(t, err) + assert.NotNil(t, resp) +} diff --git a/request.go b/request.go index 55e36292d..f6d3cc245 100644 --- a/request.go +++ b/request.go @@ -658,37 +658,54 @@ func (req *spaceIndexRequest) setIndex(index interface{}) { req.index = index } +// authRequest implements IPROTO_AUTH request. type authRequest struct { - baseRequest auth Auth user, pass string } -func newChapSha1AuthRequest(user, salt, password string) (*authRequest, error) { +// newChapSha1AuthRequest create a new authRequest with chap-sha1 +// authentication method. +func newChapSha1AuthRequest(user, password, salt string) (authRequest, error) { + req := authRequest{} scr, err := scramble(salt, password) if err != nil { - return nil, fmt.Errorf("scrambling failure: %w", err) + return req, fmt.Errorf("scrambling failure: %w", err) } - req := new(authRequest) - req.requestCode = AuthRequestCode req.auth = ChapSha1Auth req.user = user req.pass = string(scr) return req, nil } -func newPapSha256AuthRequest(user, password string) *authRequest { - req := new(authRequest) - req.requestCode = AuthRequestCode - req.auth = PapSha256Auth - req.user = user - req.pass = password - return req +// newPapSha256AuthRequest create a new authRequest with pap-sha256 +// authentication method. +func newPapSha256AuthRequest(user, password string) authRequest { + return authRequest{ + auth: PapSha256Auth, + user: user, + pass: password, + } +} + +// Code returns a IPROTO code for the request. +func (req authRequest) Code() int32 { + return AuthRequestCode +} + +// Async returns true if the request does not require a response. +func (req authRequest) Async() bool { + return false +} + +// Ctx returns a context of the request. +func (req authRequest) Ctx() context.Context { + return nil } // Body fills an encoder with the auth request body. -func (req *authRequest) Body(res SchemaResolver, enc *encoder) error { +func (req authRequest) Body(res SchemaResolver, enc *encoder) error { return enc.Encode(map[uint32]interface{}{ KeyUserName: req.user, KeyTuple: []interface{}{req.auth.String(), req.pass}, diff --git a/tarantool_test.go b/tarantool_test.go index f53e6b528..125642dcf 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2,7 +2,9 @@ package tarantool_test import ( "context" + "encoding/binary" "fmt" + "io" "log" "math" "os" @@ -696,6 +698,60 @@ func BenchmarkSQLSerial(b *testing.B) { } } +func TestTtDialer(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + conn, err := TtDialer{}.Dial(server, DialOpts{}) + require.Nil(err) + require.NotNil(conn) + defer conn.Close() + + assert.Contains(conn.LocalAddr().String(), "127.0.0.1") + assert.Equal(server, conn.RemoteAddr().String()) + assert.NotEqual("", conn.Greeting().Version) + + // Write IPROTO_PING. + ping := []byte{ + 0xce, 0x00, 0x00, 0x00, 0xa, // Length. + 0x82, // Header map. + 0x00, 0x40, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x02, + 0x80, // Empty map. + } + ret, err := conn.Write(ping) + require.Equal(len(ping), ret) + require.Nil(err) + require.Nil(conn.Flush()) + + // Read IPROTO_PING response length. + lenbuf := make([]byte, 5) + ret, err = io.ReadFull(conn, lenbuf) + require.Nil(err) + require.Equal(len(lenbuf), ret) + length := int(binary.BigEndian.Uint32(lenbuf[1:])) + require.Greater(length, 0) + + // Read IPROTO_PING response. + buf := make([]byte, length) + ret, err = io.ReadFull(conn, buf) + require.Nil(err) + require.Equal(len(buf), ret) + // Check that it is IPROTO_OK. + assert.Equal([]byte{0x83, 0x00, 0xce, 0x00, 0x00, 0x00, 0x00}, buf[:7]) +} + +func TestTtDialer_worksWithConnection(t *testing.T) { + defaultOpts := opts + defaultOpts.Dialer = TtDialer{} + + conn := test_helpers.ConnectWithValidation(t, server, defaultOpts) + defer conn.Close() + + _, err := conn.Do(NewPingRequest()).Get() + assert.Nil(t, err) +} + func TestOptsAuth_Default(t *testing.T) { defaultOpts := opts defaultOpts.Auth = AutoAuth @@ -722,8 +778,8 @@ func TestOptsAuth_PapSha256AuthForbit(t *testing.T) { conn.Close() } - if err.Error() != "auth: forbidden to use pap-sha256 unless "+ - "SSL is enabled for the connection" { + if err.Error() != "failed to authenticate: forbidden to use pap-sha256"+ + " unless SSL is enabled for the connection" { t.Errorf("An unexpected error: %s", err) } } @@ -3273,7 +3329,7 @@ func TestConnectionProtocolVersionRequirementFail(t *testing.T) { require.Nilf(t, conn, "Connect fail") require.NotNilf(t, err, "Got error on connect") - require.Contains(t, err.Error(), "identify: protocol version 3 is not supported") + require.Contains(t, err.Error(), "invalid server protocol: protocol version 3 is not supported") } func TestConnectionProtocolFeatureRequirementSuccess(t *testing.T) { @@ -3304,7 +3360,7 @@ func TestConnectionProtocolFeatureRequirementFail(t *testing.T) { require.Nilf(t, conn, "Connect fail") require.NotNilf(t, err, "Got error on connect") - require.Contains(t, err.Error(), "identify: protocol feature TransactionsFeature is not supported") + require.Contains(t, err.Error(), "invalid server protocol: protocol feature TransactionsFeature is not supported") } func TestConnectionProtocolFeatureRequirementManyFail(t *testing.T) { @@ -3321,7 +3377,7 @@ func TestConnectionProtocolFeatureRequirementManyFail(t *testing.T) { require.NotNilf(t, err, "Got error on connect") require.Contains(t, err.Error(), - "identify: protocol features TransactionsFeature, Unknown feature (code 15532) are not supported") + "invalid server protocol: protocol features TransactionsFeature, Unknown feature (code 15532) are not supported") } func TestConnectionFeatureOptsImmutable(t *testing.T) { From 686d00648a7cf488376187fec226df547227941e Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 13 Mar 2023 17:40:20 +0300 Subject: [PATCH 407/605] crud: remove NewEncoder/NewDecoder from the API This is only needed for tests. Let it be private API. Part of #271 --- crud/msgpack.go | 6 ------ crud/msgpack_helper_test.go | 10 ++++++++++ crud/msgpack_v5.go | 6 ------ crud/msgpack_v5_helper_test.go | 10 ++++++++++ crud/request_test.go | 4 ++-- crud/tarantool_test.go | 2 +- 6 files changed, 23 insertions(+), 15 deletions(-) create mode 100644 crud/msgpack_helper_test.go create mode 100644 crud/msgpack_v5_helper_test.go diff --git a/crud/msgpack.go b/crud/msgpack.go index fe65bd154..be1c88f7f 100644 --- a/crud/msgpack.go +++ b/crud/msgpack.go @@ -4,8 +4,6 @@ package crud import ( - "io" - "gopkg.in/vmihailenco/msgpack.v2" ) @@ -23,7 +21,3 @@ type MapObject map[string]interface{} func (o MapObject) EncodeMsgpack(enc *encoder) { enc.Encode(o) } - -func NewEncoder(w io.Writer) *encoder { - return msgpack.NewEncoder(w) -} diff --git a/crud/msgpack_helper_test.go b/crud/msgpack_helper_test.go new file mode 100644 index 000000000..7d3998fc6 --- /dev/null +++ b/crud/msgpack_helper_test.go @@ -0,0 +1,10 @@ +//go:build !go_tarantool_msgpack_v5 +// +build !go_tarantool_msgpack_v5 + +package crud_test + +import ( + "gopkg.in/vmihailenco/msgpack.v2" +) + +var newEncoder = msgpack.NewEncoder diff --git a/crud/msgpack_v5.go b/crud/msgpack_v5.go index bfa936a83..3bbbf09fd 100644 --- a/crud/msgpack_v5.go +++ b/crud/msgpack_v5.go @@ -4,8 +4,6 @@ package crud import ( - "io" - "github.com/vmihailenco/msgpack/v5" ) @@ -23,7 +21,3 @@ type MapObject map[string]interface{} func (o MapObject) EncodeMsgpack(enc *encoder) { enc.Encode(o) } - -func NewEncoder(w io.Writer) *encoder { - return msgpack.NewEncoder(w) -} diff --git a/crud/msgpack_v5_helper_test.go b/crud/msgpack_v5_helper_test.go new file mode 100644 index 000000000..f3700bebc --- /dev/null +++ b/crud/msgpack_v5_helper_test.go @@ -0,0 +1,10 @@ +//go:build go_tarantool_msgpack_v5 +// +build go_tarantool_msgpack_v5 + +package crud_test + +import ( + "github.com/vmihailenco/msgpack/v5" +) + +var newEncoder = msgpack.NewEncoder diff --git a/crud/request_test.go b/crud/request_test.go index c27b8c4ae..f7af05223 100644 --- a/crud/request_test.go +++ b/crud/request_test.go @@ -94,12 +94,12 @@ var resolver ValidSchemeResolver func assertBodyEqual(t testing.TB, reference tarantool.Request, req tarantool.Request) { t.Helper() - reqBody, err := test_helpers.ExtractRequestBody(req, &resolver, crud.NewEncoder) + reqBody, err := test_helpers.ExtractRequestBody(req, &resolver, newEncoder) if err != nil { t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) } - refBody, err := test_helpers.ExtractRequestBody(reference, &resolver, crud.NewEncoder) + refBody, err := test_helpers.ExtractRequestBody(reference, &resolver, newEncoder) if err != nil { t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 1323b33e9..33959a026 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -123,7 +123,7 @@ func BenchmarkCrud(b *testing.B) { buf := bytes.Buffer{} buf.Grow(512 * 1024 * 1024) // Avoid allocs in test. - enc := crud.NewEncoder(&buf) + enc := newEncoder(&buf) b.ResetTimer() From d9a60495f39510cab1f5ee0788a741be0cb85f08 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 13 Mar 2023 17:25:02 +0300 Subject: [PATCH 408/605] crud: improve Result* types Now a user can specify his custom type as a type for rows. The patch also removes unnecessary types to make it easier for a user to work with the API. Part of #271 --- crud/delete.go | 3 - crud/error.go | 24 +++++++ crud/example_test.go | 150 +++++++++++++++++++++++++++++++++++++++++ crud/get.go | 3 - crud/insert.go | 6 -- crud/insert_many.go | 6 -- crud/max.go | 3 - crud/min.go | 3 - crud/msgpack.go | 6 ++ crud/msgpack_v5.go | 6 ++ crud/replace.go | 6 -- crud/replace_many.go | 6 -- crud/result.go | 125 ++++++++++------------------------ crud/select.go | 3 - crud/tarantool_test.go | 14 ++-- crud/update.go | 3 - crud/upsert.go | 6 -- crud/upsert_many.go | 6 -- 18 files changed, 229 insertions(+), 150 deletions(-) create mode 100644 crud/example_test.go diff --git a/crud/delete.go b/crud/delete.go index 6592d1519..1fe8a3fe0 100644 --- a/crud/delete.go +++ b/crud/delete.go @@ -6,9 +6,6 @@ import ( "github.com/tarantool/go-tarantool" ) -// DeleteResult describes result for `crud.delete` method. -type DeleteResult = Result - // DeleteOpts describes options for `crud.delete` method. type DeleteOpts = SimpleOperationOpts diff --git a/crud/error.go b/crud/error.go index 467c350a4..12d416cfd 100644 --- a/crud/error.go +++ b/crud/error.go @@ -75,6 +75,30 @@ type ErrorMany struct { Errors []Error } +// DecodeMsgpack provides custom msgpack decoder. +func (e *ErrorMany) DecodeMsgpack(d *decoder) error { + l, err := d.DecodeArrayLen() + if err != nil { + return err + } + + var errs []Error + for i := 0; i < l; i++ { + var crudErr *Error = nil + if err := d.Decode(&crudErr); err != nil { + return err + } else if crudErr != nil { + errs = append(errs, *crudErr) + } + } + + if len(errs) > 0 { + *e = ErrorMany{Errors: errs} + } + + return nil +} + // Error converts an Error to a string. func (errs ErrorMany) Error() string { var str []string diff --git a/crud/example_test.go b/crud/example_test.go new file mode 100644 index 000000000..fade59ae8 --- /dev/null +++ b/crud/example_test.go @@ -0,0 +1,150 @@ +package crud_test + +import ( + "fmt" + "reflect" + "time" + + "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/crud" +) + +const ( + exampleServer = "127.0.0.1:3013" + exampleSpace = "test" +) + +var exampleOpts = tarantool.Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +func exampleConnect() *tarantool.Connection { + conn, err := tarantool.Connect(exampleServer, exampleOpts) + if err != nil { + panic("Connection is not established: " + err.Error()) + } + return conn +} + +// ExampleResult_rowsInterface demonstrates how to use a helper type Result +// to decode a crud response. In this example, rows are decoded as an +// interface{} type. +func ExampleResult_rowsInterface() { + conn := exampleConnect() + req := crud.NewReplaceRequest(exampleSpace). + Tuple([]interface{}{uint(2010), nil, "bla"}) + + ret := crud.Result{} + if err := conn.Do(req).GetTyped(&ret); err != nil { + fmt.Printf("Failed to execute request: %s", err) + return + } + + fmt.Println(ret.Metadata) + fmt.Println(ret.Rows) + // Output: + // [{id unsigned false} {bucket_id unsigned true} {name string false}] + // [[2010 45 bla]] +} + +// ExampleResult_rowsCustomType demonstrates how to use a helper type Result +// to decode a crud response. In this example, rows are decoded as a +// custom type. +func ExampleResult_rowsCustomType() { + conn := exampleConnect() + req := crud.NewReplaceRequest(exampleSpace). + Tuple([]interface{}{uint(2010), nil, "bla"}) + + type Tuple struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Id uint64 + BucketId uint64 + Name string + } + ret := crud.MakeResult(reflect.TypeOf(Tuple{})) + + if err := conn.Do(req).GetTyped(&ret); err != nil { + fmt.Printf("Failed to execute request: %s", err) + return + } + + fmt.Println(ret.Metadata) + rows := ret.Rows.([]Tuple) + fmt.Println(rows) + // Output: + // [{id unsigned false} {bucket_id unsigned true} {name string false}] + // [{{} 2010 45 bla}] +} + +// ExampleResult_many demonstrates that there is no difference in a +// response from *ManyRequest. +func ExampleResult_many() { + conn := exampleConnect() + req := crud.NewReplaceManyRequest(exampleSpace). + Tuples([]crud.Tuple{ + []interface{}{uint(2010), nil, "bla"}, + []interface{}{uint(2011), nil, "bla"}, + }) + + ret := crud.Result{} + if err := conn.Do(req).GetTyped(&ret); err != nil { + fmt.Printf("Failed to execute request: %s", err) + return + } + + fmt.Println(ret.Metadata) + fmt.Println(ret.Rows) + // Output: + // [{id unsigned false} {bucket_id unsigned true} {name string false}] + // [[2010 45 bla] [2011 4 bla]] +} + +// ExampleResult_error demonstrates how to use a helper type Result +// to handle a crud error. +func ExampleResult_error() { + conn := exampleConnect() + req := crud.NewReplaceRequest("not_exist"). + Tuple([]interface{}{uint(2010), nil, "bla"}) + + ret := crud.Result{} + if err := conn.Do(req).GetTyped(&ret); err != nil { + crudErr := err.(crud.Error) + fmt.Printf("Failed to execute request: %s", crudErr) + } else { + fmt.Println(ret.Metadata) + fmt.Println(ret.Rows) + } + // Output: + // Failed to execute request: ReplaceError: Space "not_exist" doesn't exist +} + +// ExampleResult_errorMany demonstrates how to use a helper type Result +// to handle a crud error for a *ManyRequest. +func ExampleResult_errorMany() { + conn := exampleConnect() + initReq := crud.NewReplaceRequest("not_exist"). + Tuple([]interface{}{uint(2010), nil, "bla"}) + if _, err := conn.Do(initReq).Get(); err != nil { + fmt.Printf("Failed to initialize the example: %s\n", err) + } + + req := crud.NewInsertManyRequest(exampleSpace). + Tuples([]crud.Tuple{ + []interface{}{uint(2010), nil, "bla"}, + []interface{}{uint(2010), nil, "bla"}, + }) + ret := crud.Result{} + if err := conn.Do(req).GetTyped(&ret); err != nil { + crudErr := err.(crud.ErrorMany) + // We need to trim the error message to make the example repeatable. + errmsg := crudErr.Error()[:10] + fmt.Printf("Failed to execute request: %s", errmsg) + } else { + fmt.Println(ret.Metadata) + fmt.Println(ret.Rows) + } + // Output: + // Failed to execute request: CallError: +} diff --git a/crud/get.go b/crud/get.go index 4234431f6..0c15bf9a9 100644 --- a/crud/get.go +++ b/crud/get.go @@ -6,9 +6,6 @@ import ( "github.com/tarantool/go-tarantool" ) -// GetResult describes result for `crud.get` method. -type GetResult = Result - // GetOpts describes options for `crud.get` method. type GetOpts struct { // Timeout is a `vshard.call` timeout and vshard diff --git a/crud/insert.go b/crud/insert.go index 480300249..20b441261 100644 --- a/crud/insert.go +++ b/crud/insert.go @@ -6,9 +6,6 @@ import ( "github.com/tarantool/go-tarantool" ) -// InsertResult describes result for `crud.insert` method. -type InsertResult = Result - // InsertOpts describes options for `crud.insert` method. type InsertOpts = SimpleOperationOpts @@ -65,9 +62,6 @@ func (req *InsertRequest) Context(ctx context.Context) *InsertRequest { return req } -// InsertObjectResult describes result for `crud.insert_object` method. -type InsertObjectResult = Result - // InsertObjectOpts describes options for `crud.insert_object` method. type InsertObjectOpts = SimpleOperationObjectOpts diff --git a/crud/insert_many.go b/crud/insert_many.go index 11784f660..98931a3c8 100644 --- a/crud/insert_many.go +++ b/crud/insert_many.go @@ -6,9 +6,6 @@ import ( "github.com/tarantool/go-tarantool" ) -// InsertManyResult describes result for `crud.insert_many` method. -type InsertManyResult = ResultMany - // InsertManyOpts describes options for `crud.insert_many` method. type InsertManyOpts = OperationManyOpts @@ -65,9 +62,6 @@ func (req *InsertManyRequest) Context(ctx context.Context) *InsertManyRequest { return req } -// InsertObjectManyResult describes result for `crud.insert_object_many` method. -type InsertObjectManyResult = ResultMany - // InsertObjectManyOpts describes options for `crud.insert_object_many` method. type InsertObjectManyOpts = OperationObjectManyOpts diff --git a/crud/max.go b/crud/max.go index 842f24d5a..73660a205 100644 --- a/crud/max.go +++ b/crud/max.go @@ -6,9 +6,6 @@ import ( "github.com/tarantool/go-tarantool" ) -// MaxResult describes result for `crud.max` method. -type MaxResult = Result - // MaxOpts describes options for `crud.max` method. type MaxOpts = BorderOpts diff --git a/crud/min.go b/crud/min.go index 720b6f782..b9f4c0f41 100644 --- a/crud/min.go +++ b/crud/min.go @@ -6,9 +6,6 @@ import ( "github.com/tarantool/go-tarantool" ) -// MinResult describes result for `crud.min` method. -type MinResult = Result - // MinOpts describes options for `crud.min` method. type MinOpts = BorderOpts diff --git a/crud/msgpack.go b/crud/msgpack.go index be1c88f7f..b9696b15e 100644 --- a/crud/msgpack.go +++ b/crud/msgpack.go @@ -5,6 +5,7 @@ package crud import ( "gopkg.in/vmihailenco/msgpack.v2" + msgpcode "gopkg.in/vmihailenco/msgpack.v2/codes" ) type encoder = msgpack.Encoder @@ -21,3 +22,8 @@ type MapObject map[string]interface{} func (o MapObject) EncodeMsgpack(enc *encoder) { enc.Encode(o) } + +func msgpackIsArray(code byte) bool { + return code == msgpcode.Array16 || code == msgpcode.Array32 || + msgpcode.IsFixedArray(code) +} diff --git a/crud/msgpack_v5.go b/crud/msgpack_v5.go index 3bbbf09fd..393e359c3 100644 --- a/crud/msgpack_v5.go +++ b/crud/msgpack_v5.go @@ -5,6 +5,7 @@ package crud import ( "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" ) type encoder = msgpack.Encoder @@ -21,3 +22,8 @@ type MapObject map[string]interface{} func (o MapObject) EncodeMsgpack(enc *encoder) { enc.Encode(o) } + +func msgpackIsArray(code byte) bool { + return code == msgpcode.Array16 || code == msgpcode.Array32 || + msgpcode.IsFixedArray(code) +} diff --git a/crud/replace.go b/crud/replace.go index 811a08eb8..d803bfaa6 100644 --- a/crud/replace.go +++ b/crud/replace.go @@ -6,9 +6,6 @@ import ( "github.com/tarantool/go-tarantool" ) -// ReplaceResult describes result for `crud.replace` method. -type ReplaceResult = Result - // ReplaceOpts describes options for `crud.replace` method. type ReplaceOpts = SimpleOperationOpts @@ -65,9 +62,6 @@ func (req *ReplaceRequest) Context(ctx context.Context) *ReplaceRequest { return req } -// ReplaceObjectResult describes result for `crud.replace_object` method. -type ReplaceObjectResult = Result - // ReplaceObjectOpts describes options for `crud.replace_object` method. type ReplaceObjectOpts = SimpleOperationObjectOpts diff --git a/crud/replace_many.go b/crud/replace_many.go index 36350ac4b..503c1e5c3 100644 --- a/crud/replace_many.go +++ b/crud/replace_many.go @@ -6,9 +6,6 @@ import ( "github.com/tarantool/go-tarantool" ) -// ReplaceManyResult describes result for `crud.replace_many` method. -type ReplaceManyResult = ResultMany - // ReplaceManyOpts describes options for `crud.replace_many` method. type ReplaceManyOpts = OperationManyOpts @@ -65,9 +62,6 @@ func (req *ReplaceManyRequest) Context(ctx context.Context) *ReplaceManyRequest return req } -// ReplaceObjectManyResult describes result for `crud.replace_object_many` method. -type ReplaceObjectManyResult = ResultMany - // ReplaceObjectManyOpts describes options for `crud.replace_object_many` method. type ReplaceObjectManyOpts = OperationObjectManyOpts diff --git a/crud/result.go b/crud/result.go index 356e2a817..5ee556a5f 100644 --- a/crud/result.go +++ b/crud/result.go @@ -2,6 +2,7 @@ package crud import ( "fmt" + "reflect" ) // FieldFormat contains field definition: {name='...',type='...'[,is_nullable=...]}. @@ -48,7 +49,15 @@ func (format *FieldFormat) DecodeMsgpack(d *decoder) error { // Result describes CRUD result as an object containing metadata and rows. type Result struct { Metadata []FieldFormat - Rows []interface{} + Rows interface{} + rowType reflect.Type +} + +// MakeResult create a Result object with a custom row type for decoding. +func MakeResult(rowType reflect.Type) Result { + return Result{ + rowType: rowType, + } } // DecodeMsgpack provides custom msgpack decoder. @@ -93,8 +102,19 @@ func (r *Result) DecodeMsgpack(d *decoder) error { r.Metadata = metadata case "rows": - if err = d.Decode(&r.Rows); err != nil { - return err + if r.rowType != nil { + tuples := reflect.New(reflect.SliceOf(r.rowType)) + if err = d.DecodeValue(tuples); err != nil { + fmt.Println(tuples) + return err + } + r.Rows = tuples.Elem().Interface() + } else { + var decoded []interface{} + if err = d.Decode(&decoded); err != nil { + return err + } + r.Rows = decoded } default: if err := d.Skip(); err != nil { @@ -103,96 +123,27 @@ func (r *Result) DecodeMsgpack(d *decoder) error { } } - var crudErr *Error = nil - - if err := d.Decode(&crudErr); err != nil { - return err - } - - for i := 2; i < arrLen; i++ { - if err := d.Skip(); err != nil { - return err - } - } - - if crudErr != nil { - return crudErr - } - - return nil -} - -// ResultMany describes CRUD result as an object containing metadata and rows. -type ResultMany struct { - Metadata []FieldFormat - Rows []interface{} -} - -// DecodeMsgpack provides custom msgpack decoder. -func (r *ResultMany) DecodeMsgpack(d *decoder) error { - arrLen, err := d.DecodeArrayLen() + code, err := d.PeekCode() if err != nil { return err } - if arrLen < 2 { - return fmt.Errorf("array len doesn't match: %d", arrLen) - } - - l, err := d.DecodeMapLen() - if err != nil { - return err - } - - for i := 0; i < l; i++ { - key, err := d.DecodeString() - if err != nil { + var retErr error + if msgpackIsArray(code) { + var crudErr *ErrorMany + if err := d.Decode(&crudErr); err != nil { return err } - - switch key { - case "metadata": - metadataLen, err := d.DecodeArrayLen() - if err != nil { - return err - } - - metadata := make([]FieldFormat, metadataLen) - - for i := 0; i < metadataLen; i++ { - fieldFormat := FieldFormat{} - if err = d.Decode(&fieldFormat); err != nil { - return err - } - - metadata[i] = fieldFormat - } - - r.Metadata = metadata - case "rows": - if err = d.Decode(&r.Rows); err != nil { - return err - } - default: - if err := d.Skip(); err != nil { - return err - } + if crudErr != nil { + retErr = *crudErr } - } - - errLen, err := d.DecodeArrayLen() - if err != nil { - return err - } - - var errs []Error - for i := 0; i < errLen; i++ { - var crudErr *Error = nil - + } else { + var crudErr *Error if err := d.Decode(&crudErr); err != nil { return err - } else if crudErr != nil { - errs = append(errs, *crudErr) + } + if crudErr != nil { + retErr = *crudErr } } @@ -202,11 +153,7 @@ func (r *ResultMany) DecodeMsgpack(d *decoder) error { } } - if len(errs) > 0 { - return &ErrorMany{Errors: errs} - } - - return nil + return retErr } // NumberResult describes CRUD result as an object containing number. diff --git a/crud/select.go b/crud/select.go index 5ada1aa99..3f06ff91d 100644 --- a/crud/select.go +++ b/crud/select.go @@ -6,9 +6,6 @@ import ( "github.com/tarantool/go-tarantool" ) -// SelectResult describes result for `crud.select` method. -type SelectResult = Result - // SelectOpts describes options for `crud.select` method. type SelectOpts struct { // Timeout is a `vshard.call` timeout and vshard diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 33959a026..c889cdc72 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -215,12 +215,12 @@ var testResultWithErrCases = []struct { }{ { "BaseResult", - &crud.SelectResult{}, + &crud.Result{}, crud.NewSelectRequest(invalidSpaceName).Opts(selectOpts), }, { "ManyResult", - &crud.ReplaceManyResult{}, + &crud.Result{}, crud.NewReplaceManyRequest(invalidSpaceName).Opts(opManyOpts), }, { @@ -691,18 +691,18 @@ func TestBaseResult(t *testing.T) { defer conn.Close() req := crud.NewSelectRequest(spaceName).Opts(selectOpts) - resp := crud.SelectResult{} + resp := crud.Result{} testCrudRequestPrepareData(t, conn) err := conn.Do(req).GetTyped(&resp) if err != nil { - t.Fatalf("Failed to Do CRUD request: %s", err.Error()) + t.Fatalf("Failed to Do CRUD request: %s", err) } require.ElementsMatch(t, resp.Metadata, expectedMetadata) - if len(resp.Rows) != 10 { + if len(resp.Rows.([]interface{})) != 10 { t.Fatalf("Unexpected rows: %#v", resp.Rows) } @@ -734,7 +734,7 @@ func TestManyResult(t *testing.T) { defer conn.Close() req := crud.NewReplaceManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts) - resp := crud.ReplaceResult{} + resp := crud.Result{} testCrudRequestPrepareData(t, conn) @@ -745,7 +745,7 @@ func TestManyResult(t *testing.T) { require.ElementsMatch(t, resp.Metadata, expectedMetadata) - if len(resp.Rows) != 10 { + if len(resp.Rows.([]interface{})) != 10 { t.Fatalf("Unexpected rows: %#v", resp.Rows) } diff --git a/crud/update.go b/crud/update.go index 09df6612d..f1d30eb50 100644 --- a/crud/update.go +++ b/crud/update.go @@ -6,9 +6,6 @@ import ( "github.com/tarantool/go-tarantool" ) -// UpdateResult describes result for `crud.update` method. -type UpdateResult = Result - // UpdateOpts describes options for `crud.update` method. type UpdateOpts = SimpleOperationOpts diff --git a/crud/upsert.go b/crud/upsert.go index 07c373b68..e66b14ecd 100644 --- a/crud/upsert.go +++ b/crud/upsert.go @@ -6,9 +6,6 @@ import ( "github.com/tarantool/go-tarantool" ) -// UpsertResult describes result for `crud.upsert` method. -type UpsertResult = Result - // UpsertOpts describes options for `crud.upsert` method. type UpsertOpts = SimpleOperationOpts @@ -76,9 +73,6 @@ func (req *UpsertRequest) Context(ctx context.Context) *UpsertRequest { return req } -// UpsertObjectResult describes result for `crud.upsert_object` method. -type UpsertObjectResult = Result - // UpsertObjectOpts describes options for `crud.upsert_object` method. type UpsertObjectOpts = SimpleOperationOpts diff --git a/crud/upsert_many.go b/crud/upsert_many.go index b7bdcad81..37d533d4a 100644 --- a/crud/upsert_many.go +++ b/crud/upsert_many.go @@ -6,9 +6,6 @@ import ( "github.com/tarantool/go-tarantool" ) -// UpsertManyResult describes result for `crud.upsert_many` method. -type UpsertManyResult = ResultMany - // UpsertManyOpts describes options for `crud.upsert_many` method. type UpsertManyOpts = OperationManyOpts @@ -74,9 +71,6 @@ func (req *UpsertManyRequest) Context(ctx context.Context) *UpsertManyRequest { return req } -// UpsertObjectManyResult describes result for `crud.upsert_object_many` method. -type UpsertObjectManyResult = ResultMany - // UpsertObjectManyOpts describes options for `crud.upsert_object_many` method. type UpsertObjectManyOpts = OperationManyOpts From 7c9952192bf6c9e20b30ad6bd0f0263840f5b2a9 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 14 Mar 2023 12:07:19 +0300 Subject: [PATCH 409/605] crud: allow any type as Tuple This is necessary to use a custom types with custom encoders as tuples. Part of #271 --- crud/common.go | 3 --- crud/delete.go | 2 +- crud/get.go | 2 +- crud/insert.go | 2 +- crud/replace.go | 2 +- crud/tarantool_test.go | 2 +- crud/tuple.go | 5 +++++ crud/unflatten_rows.go | 9 ++------- crud/update.go | 2 +- crud/upsert.go | 2 +- 10 files changed, 14 insertions(+), 17 deletions(-) create mode 100644 crud/tuple.go diff --git a/crud/common.go b/crud/common.go index 877ac2d4a..51271761a 100644 --- a/crud/common.go +++ b/crud/common.go @@ -59,9 +59,6 @@ import ( "github.com/tarantool/go-tarantool" ) -// Tuple is a type to describe tuple for CRUD methods. -type Tuple = []interface{} - type baseRequest struct { impl *tarantool.CallRequest } diff --git a/crud/delete.go b/crud/delete.go index 1fe8a3fe0..b388858aa 100644 --- a/crud/delete.go +++ b/crud/delete.go @@ -29,7 +29,7 @@ func NewDeleteRequest(space string) *DeleteRequest { req := new(DeleteRequest) req.initImpl("crud.delete") req.setSpace(space) - req.key = Tuple{} + req.key = []interface{}{} req.opts = DeleteOpts{} return req } diff --git a/crud/get.go b/crud/get.go index 0c15bf9a9..f2c9ae732 100644 --- a/crud/get.go +++ b/crud/get.go @@ -64,7 +64,7 @@ func NewGetRequest(space string) *GetRequest { req := new(GetRequest) req.initImpl("crud.get") req.setSpace(space) - req.key = Tuple{} + req.key = []interface{}{} req.opts = GetOpts{} return req } diff --git a/crud/insert.go b/crud/insert.go index 20b441261..121bcbd65 100644 --- a/crud/insert.go +++ b/crud/insert.go @@ -29,7 +29,7 @@ func NewInsertRequest(space string) *InsertRequest { req := new(InsertRequest) req.initImpl("crud.insert") req.setSpace(space) - req.tuple = Tuple{} + req.tuple = []interface{}{} req.opts = InsertOpts{} return req } diff --git a/crud/replace.go b/crud/replace.go index d803bfaa6..51da7a614 100644 --- a/crud/replace.go +++ b/crud/replace.go @@ -29,7 +29,7 @@ func NewReplaceRequest(space string) *ReplaceRequest { req := new(ReplaceRequest) req.initImpl("crud.replace") req.setSpace(space) - req.tuple = Tuple{} + req.tuple = []interface{}{} req.opts = ReplaceOpts{} return req } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index c889cdc72..34c7f9184 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -347,7 +347,7 @@ var testGenerateDataCases = []struct { func generateTuples() []crud.Tuple { tpls := []crud.Tuple{} for i := 1010; i < 1020; i++ { - tpls = append(tpls, crud.Tuple{uint(i), nil, "bla"}) + tpls = append(tpls, []interface{}{uint(i), nil, "bla"}) } return tpls diff --git a/crud/tuple.go b/crud/tuple.go new file mode 100644 index 000000000..61291cbb0 --- /dev/null +++ b/crud/tuple.go @@ -0,0 +1,5 @@ +package crud + +// Tuple is a type to describe tuple for CRUD methods. It can be any type that +// msgpask can encode. +type Tuple = interface{} diff --git a/crud/unflatten_rows.go b/crud/unflatten_rows.go index 2efb65999..67ebb2b69 100644 --- a/crud/unflatten_rows.go +++ b/crud/unflatten_rows.go @@ -8,20 +8,15 @@ import ( func UnflattenRows(tuples []interface{}, format []interface{}) ([]MapObject, error) { var ( ok bool - tuple Tuple fieldName string fieldInfo map[interface{}]interface{} ) objects := []MapObject{} - for _, rawTuple := range tuples { + for _, tuple := range tuples { object := make(map[string]interface{}) - if tuple, ok = rawTuple.(Tuple); !ok { - return nil, fmt.Errorf("Unexpected tuple format: %q", rawTuple) - } - - for fieldIdx, field := range tuple { + for fieldIdx, field := range tuple.([]interface{}) { if fieldInfo, ok = format[fieldIdx].(map[interface{}]interface{}); !ok { return nil, fmt.Errorf("Unexpected space format: %q", format) } diff --git a/crud/update.go b/crud/update.go index f1d30eb50..05a0188e2 100644 --- a/crud/update.go +++ b/crud/update.go @@ -31,7 +31,7 @@ func NewUpdateRequest(space string) *UpdateRequest { req := new(UpdateRequest) req.initImpl("crud.update") req.setSpace(space) - req.key = Tuple{} + req.key = []interface{}{} req.operations = []Operation{} req.opts = UpdateOpts{} return req diff --git a/crud/upsert.go b/crud/upsert.go index e66b14ecd..9f4723b88 100644 --- a/crud/upsert.go +++ b/crud/upsert.go @@ -31,7 +31,7 @@ func NewUpsertRequest(space string) *UpsertRequest { req := new(UpsertRequest) req.initImpl("crud.upsert") req.setSpace(space) - req.tuple = Tuple{} + req.tuple = []interface{}{} req.operations = []Operation{} req.opts = UpsertOpts{} return req From 009084f2ea34367aa3bb24bbc5c0d631b5f1440e Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 14 Mar 2023 12:44:42 +0300 Subject: [PATCH 410/605] crud: make less allocations In this patch, we try to make less allocations per a request. Part of #271 --- crud/common.go | 14 ++-- crud/count.go | 20 +++-- crud/delete.go | 4 +- crud/get.go | 16 ++-- crud/insert.go | 8 +- crud/insert_many.go | 8 +- crud/len.go | 4 +- crud/max.go | 4 +- crud/min.go | 4 +- crud/options.go | 164 +++++++++++++++++++++++------------------ crud/replace.go | 8 +- crud/replace_many.go | 8 +- crud/request_test.go | 40 ++++++++++ crud/select.go | 24 ++++-- crud/stats.go | 6 +- crud/storage_info.go | 2 +- crud/tarantool_test.go | 51 +++---------- crud/truncate.go | 4 +- crud/update.go | 4 +- crud/upsert.go | 8 +- crud/upsert_many.go | 8 +- go.mod | 8 +- go.sum | 31 +------- request.go | 7 +- 24 files changed, 236 insertions(+), 219 deletions(-) diff --git a/crud/common.go b/crud/common.go index 51271761a..2c4a3030c 100644 --- a/crud/common.go +++ b/crud/common.go @@ -63,22 +63,22 @@ type baseRequest struct { impl *tarantool.CallRequest } -func (req *baseRequest) initImpl(methodName string) { - req.impl = tarantool.NewCall17Request(methodName) +func newCall(method string) *tarantool.CallRequest { + return tarantool.NewCall17Request(method) } // Code returns IPROTO code for CRUD request. -func (req *baseRequest) Code() int32 { +func (req baseRequest) Code() int32 { return req.impl.Code() } // Ctx returns a context of CRUD request. -func (req *baseRequest) Ctx() context.Context { +func (req baseRequest) Ctx() context.Context { return req.impl.Ctx() } // Async returns is CRUD request expects a response. -func (req *baseRequest) Async() bool { +func (req baseRequest) Async() bool { return req.impl.Async() } @@ -86,7 +86,3 @@ type spaceRequest struct { baseRequest space string } - -func (req *spaceRequest) setSpace(space string) { - req.space = space -} diff --git a/crud/count.go b/crud/count.go index 2a3992389..01768dd53 100644 --- a/crud/count.go +++ b/crud/count.go @@ -42,17 +42,23 @@ type CountOpts struct { func (opts CountOpts) EncodeMsgpack(enc *encoder) error { const optsCnt = 9 - options := [optsCnt]option{opts.Timeout, opts.VshardRouter, - opts.Mode, opts.PreferReplica, opts.Balance, - opts.YieldEvery, opts.BucketId, opts.ForceMapCall, - opts.Fullscan} names := [optsCnt]string{timeoutOptName, vshardRouterOptName, modeOptName, preferReplicaOptName, balanceOptName, yieldEveryOptName, bucketIdOptName, forceMapCallOptName, fullscanOptName} values := [optsCnt]interface{}{} + exists := [optsCnt]bool{} + values[0], exists[0] = opts.Timeout.Get() + values[1], exists[1] = opts.VshardRouter.Get() + values[2], exists[2] = opts.Mode.Get() + values[3], exists[3] = opts.PreferReplica.Get() + values[4], exists[4] = opts.Balance.Get() + values[5], exists[5] = opts.YieldEvery.Get() + values[6], exists[6] = opts.BucketId.Get() + values[7], exists[7] = opts.ForceMapCall.Get() + values[8], exists[8] = opts.Fullscan.Get() - return encodeOptions(enc, options[:], names[:], values[:]) + return encodeOptions(enc, names[:], values[:], exists[:]) } // CountRequest helps you to create request object to call `crud.count` @@ -73,8 +79,8 @@ type countArgs struct { // NewCountRequest returns a new empty CountRequest. func NewCountRequest(space string) *CountRequest { req := new(CountRequest) - req.initImpl("crud.count") - req.setSpace(space) + req.impl = newCall("crud.count") + req.space = space req.conditions = nil req.opts = CountOpts{} return req diff --git a/crud/delete.go b/crud/delete.go index b388858aa..502419316 100644 --- a/crud/delete.go +++ b/crud/delete.go @@ -27,8 +27,8 @@ type deleteArgs struct { // NewDeleteRequest returns a new empty DeleteRequest. func NewDeleteRequest(space string) *DeleteRequest { req := new(DeleteRequest) - req.initImpl("crud.delete") - req.setSpace(space) + req.impl = newCall("crud.delete") + req.space = space req.key = []interface{}{} req.opts = DeleteOpts{} return req diff --git a/crud/get.go b/crud/get.go index f2c9ae732..d7096d52f 100644 --- a/crud/get.go +++ b/crud/get.go @@ -33,15 +33,19 @@ type GetOpts struct { func (opts GetOpts) EncodeMsgpack(enc *encoder) error { const optsCnt = 7 - options := [optsCnt]option{opts.Timeout, opts.VshardRouter, - opts.Fields, opts.BucketId, opts.Mode, - opts.PreferReplica, opts.Balance} names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, bucketIdOptName, modeOptName, preferReplicaOptName, balanceOptName} values := [optsCnt]interface{}{} + exists := [optsCnt]bool{} + values[0], exists[0] = opts.Timeout.Get() + values[1], exists[1] = opts.VshardRouter.Get() + values[1], exists[1] = opts.BucketId.Get() + values[2], exists[2] = opts.Mode.Get() + values[3], exists[3] = opts.PreferReplica.Get() + values[4], exists[4] = opts.Balance.Get() - return encodeOptions(enc, options[:], names[:], values[:]) + return encodeOptions(enc, names[:], values[:], exists[:]) } // GetRequest helps you to create request object to call `crud.get` @@ -62,8 +66,8 @@ type getArgs struct { // NewGetRequest returns a new empty GetRequest. func NewGetRequest(space string) *GetRequest { req := new(GetRequest) - req.initImpl("crud.get") - req.setSpace(space) + req.impl = newCall("crud.get") + req.space = space req.key = []interface{}{} req.opts = GetOpts{} return req diff --git a/crud/insert.go b/crud/insert.go index 121bcbd65..4fcc1ebb9 100644 --- a/crud/insert.go +++ b/crud/insert.go @@ -27,8 +27,8 @@ type insertArgs struct { // NewInsertRequest returns a new empty InsertRequest. func NewInsertRequest(space string) *InsertRequest { req := new(InsertRequest) - req.initImpl("crud.insert") - req.setSpace(space) + req.impl = newCall("crud.insert") + req.space = space req.tuple = []interface{}{} req.opts = InsertOpts{} return req @@ -83,8 +83,8 @@ type insertObjectArgs struct { // NewInsertObjectRequest returns a new empty InsertObjectRequest. func NewInsertObjectRequest(space string) *InsertObjectRequest { req := new(InsertObjectRequest) - req.initImpl("crud.insert_object") - req.setSpace(space) + req.impl = newCall("crud.insert_object") + req.space = space req.object = MapObject{} req.opts = InsertObjectOpts{} return req diff --git a/crud/insert_many.go b/crud/insert_many.go index 98931a3c8..b6695ebba 100644 --- a/crud/insert_many.go +++ b/crud/insert_many.go @@ -27,8 +27,8 @@ type insertManyArgs struct { // NewInsertManyRequest returns a new empty InsertManyRequest. func NewInsertManyRequest(space string) *InsertManyRequest { req := new(InsertManyRequest) - req.initImpl("crud.insert_many") - req.setSpace(space) + req.impl = newCall("crud.insert_many") + req.space = space req.tuples = []Tuple{} req.opts = InsertManyOpts{} return req @@ -83,8 +83,8 @@ type insertObjectManyArgs struct { // NewInsertObjectManyRequest returns a new empty InsertObjectManyRequest. func NewInsertObjectManyRequest(space string) *InsertObjectManyRequest { req := new(InsertObjectManyRequest) - req.initImpl("crud.insert_object_many") - req.setSpace(space) + req.impl = newCall("crud.insert_object_many") + req.space = space req.objects = []Object{} req.opts = InsertObjectManyOpts{} return req diff --git a/crud/len.go b/crud/len.go index 6a8c85a2a..9d409e40d 100644 --- a/crud/len.go +++ b/crud/len.go @@ -28,8 +28,8 @@ type lenArgs struct { // NewLenRequest returns a new empty LenRequest. func NewLenRequest(space string) *LenRequest { req := new(LenRequest) - req.initImpl("crud.len") - req.setSpace(space) + req.impl = newCall("crud.len") + req.space = space req.opts = LenOpts{} return req } diff --git a/crud/max.go b/crud/max.go index 73660a205..5ea44878a 100644 --- a/crud/max.go +++ b/crud/max.go @@ -27,8 +27,8 @@ type maxArgs struct { // NewMaxRequest returns a new empty MaxRequest. func NewMaxRequest(space string) *MaxRequest { req := new(MaxRequest) - req.initImpl("crud.max") - req.setSpace(space) + req.impl = newCall("crud.max") + req.space = space req.index = []interface{}{} req.opts = MaxOpts{} return req diff --git a/crud/min.go b/crud/min.go index b9f4c0f41..88fba9e0c 100644 --- a/crud/min.go +++ b/crud/min.go @@ -27,8 +27,8 @@ type minArgs struct { // NewMinRequest returns a new empty MinRequest. func NewMinRequest(space string) *MinRequest { req := new(MinRequest) - req.initImpl("crud.min") - req.setSpace(space) + req.impl = newCall("crud.min") + req.space = space req.index = []interface{}{} req.opts = MinOpts{} return req diff --git a/crud/options.go b/crud/options.go index c0a201033..9bec34754 100644 --- a/crud/options.go +++ b/crud/options.go @@ -1,11 +1,5 @@ package crud -import ( - "errors" - - "github.com/markphelps/optional" -) - const ( timeoutOptName = "timeout" vshardRouterOptName = "vshard_router" @@ -25,86 +19,95 @@ const ( batchSizeOptName = "batch_size" ) -type option interface { - getInterface() (interface{}, error) -} - // OptUint is an optional uint. type OptUint struct { - optional.Uint + value uint + exist bool } -// NewOptUint creates an optional uint from value. -func NewOptUint(value uint) OptUint { - return OptUint{optional.NewUint(value)} +// MakeOptUint creates an optional uint from value. +func MakeOptUint(value uint) OptUint { + return OptUint{ + value: value, + exist: true, + } } -func (opt OptUint) getInterface() (interface{}, error) { - return opt.Get() +// Get returns the integer value or an error if not present. +func (opt OptUint) Get() (uint, bool) { + return opt.value, opt.exist } // OptInt is an optional int. type OptInt struct { - optional.Int + value int + exist bool } -// NewOptInt creates an optional int from value. -func NewOptInt(value int) OptInt { - return OptInt{optional.NewInt(value)} +// MakeOptInt creates an optional int from value. +func MakeOptInt(value int) OptInt { + return OptInt{ + value: value, + exist: true, + } } -func (opt OptInt) getInterface() (interface{}, error) { - return opt.Get() +// Get returns the integer value or an error if not present. +func (opt OptInt) Get() (int, bool) { + return opt.value, opt.exist } // OptString is an optional string. type OptString struct { - optional.String + value string + exist bool } -// NewOptString creates an optional string from value. -func NewOptString(value string) OptString { - return OptString{optional.NewString(value)} +// MakeOptString creates an optional string from value. +func MakeOptString(value string) OptString { + return OptString{ + value: value, + exist: true, + } } -func (opt OptString) getInterface() (interface{}, error) { - return opt.Get() +// Get returns the string value or an error if not present. +func (opt OptString) Get() (string, bool) { + return opt.value, opt.exist } // OptBool is an optional bool. type OptBool struct { - optional.Bool + value bool + exist bool } -// NewOptBool creates an optional bool from value. -func NewOptBool(value bool) OptBool { - return OptBool{optional.NewBool(value)} +// MakeOptBool creates an optional bool from value. +func MakeOptBool(value bool) OptBool { + return OptBool{ + value: value, + exist: true, + } } -func (opt OptBool) getInterface() (interface{}, error) { - return opt.Get() +// Get returns the boolean value or an error if not present. +func (opt OptBool) Get() (bool, bool) { + return opt.value, opt.exist } // OptTuple is an optional tuple. type OptTuple struct { - tuple []interface{} + tuple interface{} } -// NewOptTuple creates an optional tuple from tuple. -func NewOptTuple(tuple []interface{}) OptTuple { +// MakeOptTuple creates an optional tuple from tuple. +func MakeOptTuple(tuple interface{}) OptTuple { return OptTuple{tuple} } // Get returns the tuple value or an error if not present. -func (o *OptTuple) Get() ([]interface{}, error) { - if o.tuple == nil { - return nil, errors.New("value not present") - } - return o.tuple, nil -} - -func (opt OptTuple) getInterface() (interface{}, error) { - return opt.Get() +func (o *OptTuple) Get() (interface{}, bool) { + return o.tuple, o.tuple != nil } // BaseOpts describes base options for CRUD operations. @@ -121,11 +124,13 @@ type BaseOpts struct { func (opts BaseOpts) EncodeMsgpack(enc *encoder) error { const optsCnt = 2 - options := [optsCnt]option{opts.Timeout, opts.VshardRouter} names := [optsCnt]string{timeoutOptName, vshardRouterOptName} values := [optsCnt]interface{}{} + exists := [optsCnt]bool{} + values[0], exists[0] = opts.Timeout.Get() + values[1], exists[1] = opts.VshardRouter.Get() - return encodeOptions(enc, options[:], names[:], values[:]) + return encodeOptions(enc, names[:], values[:], exists[:]) } // SimpleOperationOpts describes options for simple CRUD operations. @@ -146,13 +151,16 @@ type SimpleOperationOpts struct { func (opts SimpleOperationOpts) EncodeMsgpack(enc *encoder) error { const optsCnt = 4 - options := [optsCnt]option{opts.Timeout, opts.VshardRouter, - opts.Fields, opts.BucketId} names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, bucketIdOptName} values := [optsCnt]interface{}{} + exists := [optsCnt]bool{} + values[0], exists[0] = opts.Timeout.Get() + values[1], exists[1] = opts.VshardRouter.Get() + values[2], exists[2] = opts.Fields.Get() + values[3], exists[3] = opts.BucketId.Get() - return encodeOptions(enc, options[:], names[:], values[:]) + return encodeOptions(enc, names[:], values[:], exists[:]) } // SimpleOperationObjectOpts describes options for simple CRUD @@ -177,13 +185,17 @@ type SimpleOperationObjectOpts struct { func (opts SimpleOperationObjectOpts) EncodeMsgpack(enc *encoder) error { const optsCnt = 5 - options := [optsCnt]option{opts.Timeout, opts.VshardRouter, - opts.Fields, opts.BucketId, opts.SkipNullabilityCheckOnFlatten} names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, bucketIdOptName, skipNullabilityCheckOnFlattenOptName} values := [optsCnt]interface{}{} - - return encodeOptions(enc, options[:], names[:], values[:]) + exists := [optsCnt]bool{} + values[0], exists[0] = opts.Timeout.Get() + values[1], exists[1] = opts.VshardRouter.Get() + values[2], exists[2] = opts.Fields.Get() + values[3], exists[3] = opts.BucketId.Get() + values[4], exists[4] = opts.SkipNullabilityCheckOnFlatten.Get() + + return encodeOptions(enc, names[:], values[:], exists[:]) } // OperationManyOpts describes options for CRUD operations with many tuples. @@ -209,13 +221,17 @@ type OperationManyOpts struct { func (opts OperationManyOpts) EncodeMsgpack(enc *encoder) error { const optsCnt = 5 - options := [optsCnt]option{opts.Timeout, opts.VshardRouter, - opts.Fields, opts.StopOnError, opts.RollbackOnError} names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, stopOnErrorOptName, rollbackOnErrorOptName} values := [optsCnt]interface{}{} - - return encodeOptions(enc, options[:], names[:], values[:]) + exists := [optsCnt]bool{} + values[0], exists[0] = opts.Timeout.Get() + values[1], exists[1] = opts.VshardRouter.Get() + values[2], exists[2] = opts.Fields.Get() + values[3], exists[3] = opts.StopOnError.Get() + values[4], exists[4] = opts.RollbackOnError.Get() + + return encodeOptions(enc, names[:], values[:], exists[:]) } // OperationObjectManyOpts describes options for CRUD operations @@ -245,15 +261,19 @@ type OperationObjectManyOpts struct { func (opts OperationObjectManyOpts) EncodeMsgpack(enc *encoder) error { const optsCnt = 6 - options := [optsCnt]option{opts.Timeout, opts.VshardRouter, - opts.Fields, opts.StopOnError, opts.RollbackOnError, - opts.SkipNullabilityCheckOnFlatten} names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, stopOnErrorOptName, rollbackOnErrorOptName, skipNullabilityCheckOnFlattenOptName} values := [optsCnt]interface{}{} - - return encodeOptions(enc, options[:], names[:], values[:]) + exists := [optsCnt]bool{} + values[0], exists[0] = opts.Timeout.Get() + values[1], exists[1] = opts.VshardRouter.Get() + values[2], exists[2] = opts.Fields.Get() + values[3], exists[3] = opts.StopOnError.Get() + values[4], exists[4] = opts.RollbackOnError.Get() + values[5], exists[5] = opts.SkipNullabilityCheckOnFlatten.Get() + + return encodeOptions(enc, names[:], values[:], exists[:]) } // BorderOpts describes options for `crud.min` and `crud.max`. @@ -272,19 +292,21 @@ type BorderOpts struct { func (opts BorderOpts) EncodeMsgpack(enc *encoder) error { const optsCnt = 3 - options := [optsCnt]option{opts.Timeout, opts.VshardRouter, opts.Fields} names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName} values := [optsCnt]interface{}{} + exists := [optsCnt]bool{} + values[0], exists[0] = opts.Timeout.Get() + values[1], exists[1] = opts.VshardRouter.Get() + values[2], exists[2] = opts.Fields.Get() - return encodeOptions(enc, options[:], names[:], values[:]) + return encodeOptions(enc, names[:], values[:], exists[:]) } -func encodeOptions(enc *encoder, options []option, names []string, values []interface{}) error { +func encodeOptions(enc *encoder, names []string, values []interface{}, exists []bool) error { mapLen := 0 - for i, opt := range options { - if value, err := opt.getInterface(); err == nil { - values[i] = value + for _, exist := range exists { + if exist { mapLen += 1 } } @@ -295,7 +317,7 @@ func encodeOptions(enc *encoder, options []option, names []string, values []inte if mapLen > 0 { for i, name := range names { - if values[i] != nil { + if exists[i] { enc.EncodeString(name) enc.Encode(values[i]) } diff --git a/crud/replace.go b/crud/replace.go index 51da7a614..49be0a18f 100644 --- a/crud/replace.go +++ b/crud/replace.go @@ -27,8 +27,8 @@ type replaceArgs struct { // NewReplaceRequest returns a new empty ReplaceRequest. func NewReplaceRequest(space string) *ReplaceRequest { req := new(ReplaceRequest) - req.initImpl("crud.replace") - req.setSpace(space) + req.impl = newCall("crud.replace") + req.space = space req.tuple = []interface{}{} req.opts = ReplaceOpts{} return req @@ -83,8 +83,8 @@ type replaceObjectArgs struct { // NewReplaceObjectRequest returns a new empty ReplaceObjectRequest. func NewReplaceObjectRequest(space string) *ReplaceObjectRequest { req := new(ReplaceObjectRequest) - req.initImpl("crud.replace_object") - req.setSpace(space) + req.impl = newCall("crud.replace_object") + req.space = space req.object = MapObject{} req.opts = ReplaceObjectOpts{} return req diff --git a/crud/replace_many.go b/crud/replace_many.go index 503c1e5c3..cf95e89b2 100644 --- a/crud/replace_many.go +++ b/crud/replace_many.go @@ -27,8 +27,8 @@ type replaceManyArgs struct { // NewReplaceManyRequest returns a new empty ReplaceManyRequest. func NewReplaceManyRequest(space string) *ReplaceManyRequest { req := new(ReplaceManyRequest) - req.initImpl("crud.replace_many") - req.setSpace(space) + req.impl = newCall("crud.replace_many") + req.space = space req.tuples = []Tuple{} req.opts = ReplaceManyOpts{} return req @@ -83,8 +83,8 @@ type replaceObjectManyArgs struct { // NewReplaceObjectManyRequest returns a new empty ReplaceObjectManyRequest. func NewReplaceObjectManyRequest(space string) *ReplaceObjectManyRequest { req := new(ReplaceObjectManyRequest) - req.initImpl("crud.replace_object_many") - req.setSpace(space) + req.impl = newCall("crud.replace_object_many") + req.space = space req.objects = []Object{} req.opts = ReplaceObjectManyOpts{} return req diff --git a/crud/request_test.go b/crud/request_test.go index f7af05223..fe36a861b 100644 --- a/crud/request_test.go +++ b/crud/request_test.go @@ -109,6 +109,46 @@ func assertBodyEqual(t testing.TB, reference tarantool.Request, req tarantool.Re } } +func BenchmarkLenRequest(b *testing.B) { + buf := bytes.Buffer{} + buf.Grow(512 * 1024 * 1024) // Avoid allocs in test. + enc := newEncoder(&buf) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + buf.Reset() + req := crud.NewLenRequest(spaceName). + Opts(crud.LenOpts{ + Timeout: crud.MakeOptUint(3), + }) + if err := req.Body(nil, enc); err != nil { + b.Error(err) + } + } +} + +func BenchmarkSelectRequest(b *testing.B) { + buf := bytes.Buffer{} + buf.Grow(512 * 1024 * 1024) // Avoid allocs in test. + enc := newEncoder(&buf) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + buf.Reset() + req := crud.NewSelectRequest(spaceName). + Opts(crud.SelectOpts{ + Timeout: crud.MakeOptUint(3), + VshardRouter: crud.MakeOptString("asd"), + Balance: crud.MakeOptBool(true), + }) + if err := req.Body(nil, enc); err != nil { + b.Error(err) + } + } +} + func TestRequestsCodes(t *testing.T) { tests := []struct { req tarantool.Request diff --git a/crud/select.go b/crud/select.go index 3f06ff91d..e9c1f7681 100644 --- a/crud/select.go +++ b/crud/select.go @@ -45,19 +45,27 @@ type SelectOpts struct { func (opts SelectOpts) EncodeMsgpack(enc *encoder) error { const optsCnt = 12 - options := [optsCnt]option{opts.Timeout, opts.VshardRouter, - opts.Fields, opts.BucketId, - opts.Mode, opts.PreferReplica, opts.Balance, - opts.First, opts.After, opts.BatchSize, - opts.ForceMapCall, opts.Fullscan} names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, bucketIdOptName, modeOptName, preferReplicaOptName, balanceOptName, firstOptName, afterOptName, batchSizeOptName, forceMapCallOptName, fullscanOptName} values := [optsCnt]interface{}{} + exists := [optsCnt]bool{} + values[0], exists[0] = opts.Timeout.Get() + values[1], exists[1] = opts.VshardRouter.Get() + values[2], exists[2] = opts.Fields.Get() + values[3], exists[3] = opts.BucketId.Get() + values[4], exists[4] = opts.Mode.Get() + values[5], exists[5] = opts.PreferReplica.Get() + values[6], exists[6] = opts.Balance.Get() + values[7], exists[7] = opts.First.Get() + values[8], exists[8] = opts.After.Get() + values[8], exists[8] = opts.BatchSize.Get() + values[8], exists[8] = opts.ForceMapCall.Get() + values[8], exists[8] = opts.Fullscan.Get() - return encodeOptions(enc, options[:], names[:], values[:]) + return encodeOptions(enc, names[:], values[:], exists[:]) } // SelectRequest helps you to create request object to call `crud.select` @@ -78,8 +86,8 @@ type selectArgs struct { // NewSelectRequest returns a new empty SelectRequest. func NewSelectRequest(space string) *SelectRequest { req := new(SelectRequest) - req.initImpl("crud.select") - req.setSpace(space) + req.impl = newCall("crud.select") + req.space = space req.conditions = nil req.opts = SelectOpts{} return req diff --git a/crud/stats.go b/crud/stats.go index 939ed32ce..72be6a309 100644 --- a/crud/stats.go +++ b/crud/stats.go @@ -16,21 +16,21 @@ type StatsRequest struct { // NewStatsRequest returns a new empty StatsRequest. func NewStatsRequest() *StatsRequest { req := new(StatsRequest) - req.initImpl("crud.stats") + req.impl = newCall("crud.stats") return req } // Space sets the space name for the StatsRequest request. // Note: default value is nil. func (req *StatsRequest) Space(space string) *StatsRequest { - req.space = NewOptString(space) + req.space = MakeOptString(space) return req } // Body fills an encoder with the call request body. func (req *StatsRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { args := []interface{}{} - if value, err := req.space.Get(); err == nil { + if value, ok := req.space.Get(); ok { args = []interface{}{value} } req.impl.Args(args) diff --git a/crud/storage_info.go b/crud/storage_info.go index a52ca710c..2858d3e46 100644 --- a/crud/storage_info.go +++ b/crud/storage_info.go @@ -103,7 +103,7 @@ type storageInfoArgs struct { // NewStorageInfoRequest returns a new empty StorageInfoRequest. func NewStorageInfoRequest() *StorageInfoRequest { req := new(StorageInfoRequest) - req.initImpl("crud.storage_info") + req.impl = newCall("crud.storage_info") req.opts = StorageInfoOpts{} return req } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 34c7f9184..f30b7d5c2 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -1,7 +1,6 @@ package crud_test import ( - "bytes" "fmt" "log" "os" @@ -47,43 +46,43 @@ var operations = []crud.Operation{ } var selectOpts = crud.SelectOpts{ - Timeout: crud.NewOptUint(timeout), + Timeout: crud.MakeOptUint(timeout), } var countOpts = crud.CountOpts{ - Timeout: crud.NewOptUint(timeout), + Timeout: crud.MakeOptUint(timeout), } var getOpts = crud.GetOpts{ - Timeout: crud.NewOptUint(timeout), + Timeout: crud.MakeOptUint(timeout), } var minOpts = crud.MinOpts{ - Timeout: crud.NewOptUint(timeout), + Timeout: crud.MakeOptUint(timeout), } var maxOpts = crud.MaxOpts{ - Timeout: crud.NewOptUint(timeout), + Timeout: crud.MakeOptUint(timeout), } var baseOpts = crud.BaseOpts{ - Timeout: crud.NewOptUint(timeout), + Timeout: crud.MakeOptUint(timeout), } var simpleOperationOpts = crud.SimpleOperationOpts{ - Timeout: crud.NewOptUint(timeout), + Timeout: crud.MakeOptUint(timeout), } var simpleOperationObjectOpts = crud.SimpleOperationObjectOpts{ - Timeout: crud.NewOptUint(timeout), + Timeout: crud.MakeOptUint(timeout), } var opManyOpts = crud.OperationManyOpts{ - Timeout: crud.NewOptUint(timeout), + Timeout: crud.MakeOptUint(timeout), } var opObjManyOpts = crud.OperationObjectManyOpts{ - Timeout: crud.NewOptUint(timeout), + Timeout: crud.MakeOptUint(timeout), } var conditions = []crud.Condition{ @@ -105,36 +104,6 @@ var object = crud.MapObject{ "name": "bla", } -func BenchmarkCrud(b *testing.B) { - var err error - - conn := test_helpers.ConnectWithValidation(b, server, opts) - defer conn.Close() - - _, err = conn.Replace(spaceName, tuple) - if err != nil { - b.Error(err) - } - req := crud.NewLenRequest(spaceName). - Opts(crud.LenOpts{ - Timeout: crud.NewOptUint(3), - VshardRouter: crud.NewOptString("asd"), - }) - - buf := bytes.Buffer{} - buf.Grow(512 * 1024 * 1024) // Avoid allocs in test. - enc := newEncoder(&buf) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - err := req.Body(nil, enc) - if err != nil { - b.Error(err) - } - } -} - var testProcessDataCases = []struct { name string expectedRespLen int diff --git a/crud/truncate.go b/crud/truncate.go index e2d6b029d..6d00540d4 100644 --- a/crud/truncate.go +++ b/crud/truncate.go @@ -28,8 +28,8 @@ type truncateArgs struct { // NewTruncateRequest returns a new empty TruncateRequest. func NewTruncateRequest(space string) *TruncateRequest { req := new(TruncateRequest) - req.initImpl("crud.truncate") - req.setSpace(space) + req.impl = newCall("crud.truncate") + req.space = space req.opts = TruncateOpts{} return req } diff --git a/crud/update.go b/crud/update.go index 05a0188e2..8fa676ffd 100644 --- a/crud/update.go +++ b/crud/update.go @@ -29,8 +29,8 @@ type updateArgs struct { // NewUpdateRequest returns a new empty UpdateRequest. func NewUpdateRequest(space string) *UpdateRequest { req := new(UpdateRequest) - req.initImpl("crud.update") - req.setSpace(space) + req.impl = newCall("crud.update") + req.space = space req.key = []interface{}{} req.operations = []Operation{} req.opts = UpdateOpts{} diff --git a/crud/upsert.go b/crud/upsert.go index 9f4723b88..15c3c9fd2 100644 --- a/crud/upsert.go +++ b/crud/upsert.go @@ -29,8 +29,8 @@ type upsertArgs struct { // NewUpsertRequest returns a new empty UpsertRequest. func NewUpsertRequest(space string) *UpsertRequest { req := new(UpsertRequest) - req.initImpl("crud.upsert") - req.setSpace(space) + req.impl = newCall("crud.upsert") + req.space = space req.tuple = []interface{}{} req.operations = []Operation{} req.opts = UpsertOpts{} @@ -96,8 +96,8 @@ type upsertObjectArgs struct { // NewUpsertObjectRequest returns a new empty UpsertObjectRequest. func NewUpsertObjectRequest(space string) *UpsertObjectRequest { req := new(UpsertObjectRequest) - req.initImpl("crud.upsert_object") - req.setSpace(space) + req.impl = newCall("crud.upsert_object") + req.space = space req.object = MapObject{} req.operations = []Operation{} req.opts = UpsertObjectOpts{} diff --git a/crud/upsert_many.go b/crud/upsert_many.go index 37d533d4a..d2089b09a 100644 --- a/crud/upsert_many.go +++ b/crud/upsert_many.go @@ -34,8 +34,8 @@ type upsertManyArgs struct { // NewUpsertManyRequest returns a new empty UpsertManyRequest. func NewUpsertManyRequest(space string) *UpsertManyRequest { req := new(UpsertManyRequest) - req.initImpl("crud.upsert_many") - req.setSpace(space) + req.impl = newCall("crud.upsert_many") + req.space = space req.tuplesOperationsData = []TupleOperationsData{} req.opts = UpsertManyOpts{} return req @@ -99,8 +99,8 @@ type upsertObjectManyArgs struct { // NewUpsertObjectManyRequest returns a new empty UpsertObjectManyRequest. func NewUpsertObjectManyRequest(space string) *UpsertObjectManyRequest { req := new(UpsertObjectManyRequest) - req.initImpl("crud.upsert_object_many") - req.setSpace(space) + req.impl = newCall("crud.upsert_object_many") + req.space = space req.objectsOperationsData = []ObjectOperationsData{} req.opts = UpsertObjectManyOpts{} return req diff --git a/go.mod b/go.mod index ac2f980d2..ee97cb2a1 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +3,15 @@ module github.com/tarantool/go-tarantool go 1.11 require ( - github.com/google/go-cmp v0.5.7 // indirect github.com/google/uuid v1.3.0 - github.com/markphelps/optional v0.10.0 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/shopspring/decimal v1.3.1 - github.com/stretchr/testify v1.7.1 // indirect + github.com/stretchr/testify v1.7.1 github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 github.com/vmihailenco/msgpack/v5 v5.3.5 + golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/vmihailenco/msgpack.v2 v2.9.2 - gotest.tools/v3 v3.2.0 // indirect ) diff --git a/go.sum b/go.sum index c819e6e50..733f1f96e 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,23 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/markphelps/optional v0.10.0 h1:vTaMRRTuN7aPY5X8g6K82W23qR4VMqBNyniC5BIJlqo= -github.com/markphelps/optional v0.10.0/go.mod h1:Fvjs1vxcm7/wDqJPFGEiEM1RuxFl9GCyxQlj9M9YMAQ= github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -39,45 +27,28 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9 github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= -gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= diff --git a/request.go b/request.go index f6d3cc245..7c79c5863 100644 --- a/request.go +++ b/request.go @@ -1120,7 +1120,6 @@ func NewCallRequest(function string) *CallRequest { req := new(CallRequest) req.requestCode = CallRequestCode req.function = function - req.args = []interface{}{} return req } @@ -1133,7 +1132,11 @@ func (req *CallRequest) Args(args interface{}) *CallRequest { // Body fills an encoder with the call request body. func (req *CallRequest) Body(res SchemaResolver, enc *encoder) error { - return fillCall(enc, req.function, req.args) + args := req.args + if args == nil { + args = []interface{}{} + } + return fillCall(enc, req.function, args) } // Context sets a passed context to the request. From d7a422b425602263eb52e206d98feeb297eb1fa7 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 15 Mar 2023 10:22:59 +0300 Subject: [PATCH 411/605] crud: make requests immutable Closes #271 --- crud/count.go | 14 +- crud/delete.go | 18 +-- crud/example_test.go | 12 +- crud/get.go | 18 +-- crud/insert.go | 36 +++--- crud/insert_many.go | 36 +++--- crud/len.go | 12 +- crud/max.go | 15 +-- crud/min.go | 15 +-- crud/replace.go | 36 +++--- crud/replace_many.go | 36 +++--- crud/request_test.go | 284 ++++++++++++++++++++--------------------- crud/select.go | 14 +- crud/stats.go | 18 +-- crud/storage_info.go | 12 +- crud/tarantool_test.go | 77 ++++++----- crud/truncate.go | 12 +- crud/update.go | 20 +-- crud/upsert.go | 40 +++--- crud/upsert_many.go | 30 ++--- 20 files changed, 394 insertions(+), 361 deletions(-) diff --git a/crud/count.go b/crud/count.go index 01768dd53..68e29f9fb 100644 --- a/crud/count.go +++ b/crud/count.go @@ -76,9 +76,9 @@ type countArgs struct { Opts CountOpts } -// NewCountRequest returns a new empty CountRequest. -func NewCountRequest(space string) *CountRequest { - req := new(CountRequest) +// MakeCountRequest returns a new empty CountRequest. +func MakeCountRequest(space string) CountRequest { + req := CountRequest{} req.impl = newCall("crud.count") req.space = space req.conditions = nil @@ -88,27 +88,27 @@ func NewCountRequest(space string) *CountRequest { // Conditions sets the conditions for the CountRequest request. // Note: default value is nil. -func (req *CountRequest) Conditions(conditions []Condition) *CountRequest { +func (req CountRequest) Conditions(conditions []Condition) CountRequest { req.conditions = conditions return req } // Opts sets the options for the CountRequest request. // Note: default value is nil. -func (req *CountRequest) Opts(opts CountOpts) *CountRequest { +func (req CountRequest) Opts(opts CountOpts) CountRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *CountRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req CountRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { args := countArgs{Space: req.space, Conditions: req.conditions, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *CountRequest) Context(ctx context.Context) *CountRequest { +func (req CountRequest) Context(ctx context.Context) CountRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/delete.go b/crud/delete.go index 502419316..5859d3d6b 100644 --- a/crud/delete.go +++ b/crud/delete.go @@ -24,39 +24,41 @@ type deleteArgs struct { Opts DeleteOpts } -// NewDeleteRequest returns a new empty DeleteRequest. -func NewDeleteRequest(space string) *DeleteRequest { - req := new(DeleteRequest) +// MakeDeleteRequest returns a new empty DeleteRequest. +func MakeDeleteRequest(space string) DeleteRequest { + req := DeleteRequest{} req.impl = newCall("crud.delete") req.space = space - req.key = []interface{}{} req.opts = DeleteOpts{} return req } // Key sets the key for the DeleteRequest request. // Note: default value is nil. -func (req *DeleteRequest) Key(key Tuple) *DeleteRequest { +func (req DeleteRequest) Key(key Tuple) DeleteRequest { req.key = key return req } // Opts sets the options for the DeleteRequest request. // Note: default value is nil. -func (req *DeleteRequest) Opts(opts DeleteOpts) *DeleteRequest { +func (req DeleteRequest) Opts(opts DeleteOpts) DeleteRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *DeleteRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req DeleteRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + if req.key == nil { + req.key = []interface{}{} + } args := deleteArgs{Space: req.space, Key: req.key, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *DeleteRequest) Context(ctx context.Context) *DeleteRequest { +func (req DeleteRequest) Context(ctx context.Context) DeleteRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/example_test.go b/crud/example_test.go index fade59ae8..3f2ebbf88 100644 --- a/crud/example_test.go +++ b/crud/example_test.go @@ -33,7 +33,7 @@ func exampleConnect() *tarantool.Connection { // interface{} type. func ExampleResult_rowsInterface() { conn := exampleConnect() - req := crud.NewReplaceRequest(exampleSpace). + req := crud.MakeReplaceRequest(exampleSpace). Tuple([]interface{}{uint(2010), nil, "bla"}) ret := crud.Result{} @@ -54,7 +54,7 @@ func ExampleResult_rowsInterface() { // custom type. func ExampleResult_rowsCustomType() { conn := exampleConnect() - req := crud.NewReplaceRequest(exampleSpace). + req := crud.MakeReplaceRequest(exampleSpace). Tuple([]interface{}{uint(2010), nil, "bla"}) type Tuple struct { @@ -82,7 +82,7 @@ func ExampleResult_rowsCustomType() { // response from *ManyRequest. func ExampleResult_many() { conn := exampleConnect() - req := crud.NewReplaceManyRequest(exampleSpace). + req := crud.MakeReplaceManyRequest(exampleSpace). Tuples([]crud.Tuple{ []interface{}{uint(2010), nil, "bla"}, []interface{}{uint(2011), nil, "bla"}, @@ -105,7 +105,7 @@ func ExampleResult_many() { // to handle a crud error. func ExampleResult_error() { conn := exampleConnect() - req := crud.NewReplaceRequest("not_exist"). + req := crud.MakeReplaceRequest("not_exist"). Tuple([]interface{}{uint(2010), nil, "bla"}) ret := crud.Result{} @@ -124,13 +124,13 @@ func ExampleResult_error() { // to handle a crud error for a *ManyRequest. func ExampleResult_errorMany() { conn := exampleConnect() - initReq := crud.NewReplaceRequest("not_exist"). + initReq := crud.MakeReplaceRequest("not_exist"). Tuple([]interface{}{uint(2010), nil, "bla"}) if _, err := conn.Do(initReq).Get(); err != nil { fmt.Printf("Failed to initialize the example: %s\n", err) } - req := crud.NewInsertManyRequest(exampleSpace). + req := crud.MakeInsertManyRequest(exampleSpace). Tuples([]crud.Tuple{ []interface{}{uint(2010), nil, "bla"}, []interface{}{uint(2010), nil, "bla"}, diff --git a/crud/get.go b/crud/get.go index d7096d52f..9f65a34fd 100644 --- a/crud/get.go +++ b/crud/get.go @@ -63,39 +63,41 @@ type getArgs struct { Opts GetOpts } -// NewGetRequest returns a new empty GetRequest. -func NewGetRequest(space string) *GetRequest { - req := new(GetRequest) +// MakeGetRequest returns a new empty GetRequest. +func MakeGetRequest(space string) GetRequest { + req := GetRequest{} req.impl = newCall("crud.get") req.space = space - req.key = []interface{}{} req.opts = GetOpts{} return req } // Key sets the key for the GetRequest request. // Note: default value is nil. -func (req *GetRequest) Key(key Tuple) *GetRequest { +func (req GetRequest) Key(key Tuple) GetRequest { req.key = key return req } // Opts sets the options for the GetRequest request. // Note: default value is nil. -func (req *GetRequest) Opts(opts GetOpts) *GetRequest { +func (req GetRequest) Opts(opts GetOpts) GetRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *GetRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req GetRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + if req.key == nil { + req.key = []interface{}{} + } args := getArgs{Space: req.space, Key: req.key, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *GetRequest) Context(ctx context.Context) *GetRequest { +func (req GetRequest) Context(ctx context.Context) GetRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/insert.go b/crud/insert.go index 4fcc1ebb9..b8c34c9bd 100644 --- a/crud/insert.go +++ b/crud/insert.go @@ -24,39 +24,41 @@ type insertArgs struct { Opts InsertOpts } -// NewInsertRequest returns a new empty InsertRequest. -func NewInsertRequest(space string) *InsertRequest { - req := new(InsertRequest) +// MakeInsertRequest returns a new empty InsertRequest. +func MakeInsertRequest(space string) InsertRequest { + req := InsertRequest{} req.impl = newCall("crud.insert") req.space = space - req.tuple = []interface{}{} req.opts = InsertOpts{} return req } // Tuple sets the tuple for the InsertRequest request. // Note: default value is nil. -func (req *InsertRequest) Tuple(tuple Tuple) *InsertRequest { +func (req InsertRequest) Tuple(tuple Tuple) InsertRequest { req.tuple = tuple return req } // Opts sets the options for the insert request. // Note: default value is nil. -func (req *InsertRequest) Opts(opts InsertOpts) *InsertRequest { +func (req InsertRequest) Opts(opts InsertOpts) InsertRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *InsertRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req InsertRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + if req.tuple == nil { + req.tuple = []interface{}{} + } args := insertArgs{Space: req.space, Tuple: req.tuple, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *InsertRequest) Context(ctx context.Context) *InsertRequest { +func (req InsertRequest) Context(ctx context.Context) InsertRequest { req.impl = req.impl.Context(ctx) return req @@ -80,39 +82,41 @@ type insertObjectArgs struct { Opts InsertObjectOpts } -// NewInsertObjectRequest returns a new empty InsertObjectRequest. -func NewInsertObjectRequest(space string) *InsertObjectRequest { - req := new(InsertObjectRequest) +// MakeInsertObjectRequest returns a new empty InsertObjectRequest. +func MakeInsertObjectRequest(space string) InsertObjectRequest { + req := InsertObjectRequest{} req.impl = newCall("crud.insert_object") req.space = space - req.object = MapObject{} req.opts = InsertObjectOpts{} return req } // Object sets the tuple for the InsertObjectRequest request. // Note: default value is nil. -func (req *InsertObjectRequest) Object(object Object) *InsertObjectRequest { +func (req InsertObjectRequest) Object(object Object) InsertObjectRequest { req.object = object return req } // Opts sets the options for the InsertObjectRequest request. // Note: default value is nil. -func (req *InsertObjectRequest) Opts(opts InsertObjectOpts) *InsertObjectRequest { +func (req InsertObjectRequest) Opts(opts InsertObjectOpts) InsertObjectRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *InsertObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req InsertObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + if req.object == nil { + req.object = MapObject{} + } args := insertObjectArgs{Space: req.space, Object: req.object, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *InsertObjectRequest) Context(ctx context.Context) *InsertObjectRequest { +func (req InsertObjectRequest) Context(ctx context.Context) InsertObjectRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/insert_many.go b/crud/insert_many.go index b6695ebba..9d2194642 100644 --- a/crud/insert_many.go +++ b/crud/insert_many.go @@ -24,39 +24,41 @@ type insertManyArgs struct { Opts InsertManyOpts } -// NewInsertManyRequest returns a new empty InsertManyRequest. -func NewInsertManyRequest(space string) *InsertManyRequest { - req := new(InsertManyRequest) +// MakeInsertManyRequest returns a new empty InsertManyRequest. +func MakeInsertManyRequest(space string) InsertManyRequest { + req := InsertManyRequest{} req.impl = newCall("crud.insert_many") req.space = space - req.tuples = []Tuple{} req.opts = InsertManyOpts{} return req } // Tuples sets the tuples for the InsertManyRequest request. // Note: default value is nil. -func (req *InsertManyRequest) Tuples(tuples []Tuple) *InsertManyRequest { +func (req InsertManyRequest) Tuples(tuples []Tuple) InsertManyRequest { req.tuples = tuples return req } // Opts sets the options for the InsertManyRequest request. // Note: default value is nil. -func (req *InsertManyRequest) Opts(opts InsertManyOpts) *InsertManyRequest { +func (req InsertManyRequest) Opts(opts InsertManyOpts) InsertManyRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *InsertManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req InsertManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + if req.tuples == nil { + req.tuples = []Tuple{} + } args := insertManyArgs{Space: req.space, Tuples: req.tuples, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *InsertManyRequest) Context(ctx context.Context) *InsertManyRequest { +func (req InsertManyRequest) Context(ctx context.Context) InsertManyRequest { req.impl = req.impl.Context(ctx) return req @@ -80,39 +82,41 @@ type insertObjectManyArgs struct { Opts InsertObjectManyOpts } -// NewInsertObjectManyRequest returns a new empty InsertObjectManyRequest. -func NewInsertObjectManyRequest(space string) *InsertObjectManyRequest { - req := new(InsertObjectManyRequest) +// MakeInsertObjectManyRequest returns a new empty InsertObjectManyRequest. +func MakeInsertObjectManyRequest(space string) InsertObjectManyRequest { + req := InsertObjectManyRequest{} req.impl = newCall("crud.insert_object_many") req.space = space - req.objects = []Object{} req.opts = InsertObjectManyOpts{} return req } // Objects sets the objects for the InsertObjectManyRequest request. // Note: default value is nil. -func (req *InsertObjectManyRequest) Objects(objects []Object) *InsertObjectManyRequest { +func (req InsertObjectManyRequest) Objects(objects []Object) InsertObjectManyRequest { req.objects = objects return req } // Opts sets the options for the InsertObjectManyRequest request. // Note: default value is nil. -func (req *InsertObjectManyRequest) Opts(opts InsertObjectManyOpts) *InsertObjectManyRequest { +func (req InsertObjectManyRequest) Opts(opts InsertObjectManyOpts) InsertObjectManyRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *InsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req InsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + if req.objects == nil { + req.objects = []Object{} + } args := insertObjectManyArgs{Space: req.space, Objects: req.objects, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *InsertObjectManyRequest) Context(ctx context.Context) *InsertObjectManyRequest { +func (req InsertObjectManyRequest) Context(ctx context.Context) InsertObjectManyRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/len.go b/crud/len.go index 9d409e40d..8ebea253c 100644 --- a/crud/len.go +++ b/crud/len.go @@ -25,9 +25,9 @@ type lenArgs struct { Opts LenOpts } -// NewLenRequest returns a new empty LenRequest. -func NewLenRequest(space string) *LenRequest { - req := new(LenRequest) +// MakeLenRequest returns a new empty LenRequest. +func MakeLenRequest(space string) LenRequest { + req := LenRequest{} req.impl = newCall("crud.len") req.space = space req.opts = LenOpts{} @@ -36,20 +36,20 @@ func NewLenRequest(space string) *LenRequest { // Opts sets the options for the LenRequest request. // Note: default value is nil. -func (req *LenRequest) Opts(opts LenOpts) *LenRequest { +func (req LenRequest) Opts(opts LenOpts) LenRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *LenRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req LenRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { args := lenArgs{Space: req.space, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *LenRequest) Context(ctx context.Context) *LenRequest { +func (req LenRequest) Context(ctx context.Context) LenRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/max.go b/crud/max.go index 5ea44878a..fda960040 100644 --- a/crud/max.go +++ b/crud/max.go @@ -24,39 +24,38 @@ type maxArgs struct { Opts MaxOpts } -// NewMaxRequest returns a new empty MaxRequest. -func NewMaxRequest(space string) *MaxRequest { - req := new(MaxRequest) +// MakeMaxRequest returns a new empty MaxRequest. +func MakeMaxRequest(space string) MaxRequest { + req := MaxRequest{} req.impl = newCall("crud.max") req.space = space - req.index = []interface{}{} req.opts = MaxOpts{} return req } // Index sets the index name/id for the MaxRequest request. // Note: default value is nil. -func (req *MaxRequest) Index(index interface{}) *MaxRequest { +func (req MaxRequest) Index(index interface{}) MaxRequest { req.index = index return req } // Opts sets the options for the MaxRequest request. // Note: default value is nil. -func (req *MaxRequest) Opts(opts MaxOpts) *MaxRequest { +func (req MaxRequest) Opts(opts MaxOpts) MaxRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *MaxRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req MaxRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { args := maxArgs{Space: req.space, Index: req.index, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *MaxRequest) Context(ctx context.Context) *MaxRequest { +func (req MaxRequest) Context(ctx context.Context) MaxRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/min.go b/crud/min.go index 88fba9e0c..53794b21e 100644 --- a/crud/min.go +++ b/crud/min.go @@ -24,39 +24,38 @@ type minArgs struct { Opts MinOpts } -// NewMinRequest returns a new empty MinRequest. -func NewMinRequest(space string) *MinRequest { - req := new(MinRequest) +// MakeMinRequest returns a new empty MinRequest. +func MakeMinRequest(space string) MinRequest { + req := MinRequest{} req.impl = newCall("crud.min") req.space = space - req.index = []interface{}{} req.opts = MinOpts{} return req } // Index sets the index name/id for the MinRequest request. // Note: default value is nil. -func (req *MinRequest) Index(index interface{}) *MinRequest { +func (req MinRequest) Index(index interface{}) MinRequest { req.index = index return req } // Opts sets the options for the MinRequest request. // Note: default value is nil. -func (req *MinRequest) Opts(opts MinOpts) *MinRequest { +func (req MinRequest) Opts(opts MinOpts) MinRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *MinRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req MinRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { args := minArgs{Space: req.space, Index: req.index, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *MinRequest) Context(ctx context.Context) *MinRequest { +func (req MinRequest) Context(ctx context.Context) MinRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/replace.go b/crud/replace.go index 49be0a18f..378a1ae22 100644 --- a/crud/replace.go +++ b/crud/replace.go @@ -24,39 +24,41 @@ type replaceArgs struct { Opts ReplaceOpts } -// NewReplaceRequest returns a new empty ReplaceRequest. -func NewReplaceRequest(space string) *ReplaceRequest { - req := new(ReplaceRequest) +// MakeReplaceRequest returns a new empty ReplaceRequest. +func MakeReplaceRequest(space string) ReplaceRequest { + req := ReplaceRequest{} req.impl = newCall("crud.replace") req.space = space - req.tuple = []interface{}{} req.opts = ReplaceOpts{} return req } // Tuple sets the tuple for the ReplaceRequest request. // Note: default value is nil. -func (req *ReplaceRequest) Tuple(tuple Tuple) *ReplaceRequest { +func (req ReplaceRequest) Tuple(tuple Tuple) ReplaceRequest { req.tuple = tuple return req } // Opts sets the options for the ReplaceRequest request. // Note: default value is nil. -func (req *ReplaceRequest) Opts(opts ReplaceOpts) *ReplaceRequest { +func (req ReplaceRequest) Opts(opts ReplaceOpts) ReplaceRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *ReplaceRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req ReplaceRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + if req.tuple == nil { + req.tuple = []interface{}{} + } args := replaceArgs{Space: req.space, Tuple: req.tuple, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *ReplaceRequest) Context(ctx context.Context) *ReplaceRequest { +func (req ReplaceRequest) Context(ctx context.Context) ReplaceRequest { req.impl = req.impl.Context(ctx) return req @@ -80,39 +82,41 @@ type replaceObjectArgs struct { Opts ReplaceObjectOpts } -// NewReplaceObjectRequest returns a new empty ReplaceObjectRequest. -func NewReplaceObjectRequest(space string) *ReplaceObjectRequest { - req := new(ReplaceObjectRequest) +// MakeReplaceObjectRequest returns a new empty ReplaceObjectRequest. +func MakeReplaceObjectRequest(space string) ReplaceObjectRequest { + req := ReplaceObjectRequest{} req.impl = newCall("crud.replace_object") req.space = space - req.object = MapObject{} req.opts = ReplaceObjectOpts{} return req } // Object sets the tuple for the ReplaceObjectRequest request. // Note: default value is nil. -func (req *ReplaceObjectRequest) Object(object Object) *ReplaceObjectRequest { +func (req ReplaceObjectRequest) Object(object Object) ReplaceObjectRequest { req.object = object return req } // Opts sets the options for the ReplaceObjectRequest request. // Note: default value is nil. -func (req *ReplaceObjectRequest) Opts(opts ReplaceObjectOpts) *ReplaceObjectRequest { +func (req ReplaceObjectRequest) Opts(opts ReplaceObjectOpts) ReplaceObjectRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *ReplaceObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req ReplaceObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + if req.object == nil { + req.object = MapObject{} + } args := replaceObjectArgs{Space: req.space, Object: req.object, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *ReplaceObjectRequest) Context(ctx context.Context) *ReplaceObjectRequest { +func (req ReplaceObjectRequest) Context(ctx context.Context) ReplaceObjectRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/replace_many.go b/crud/replace_many.go index cf95e89b2..511d60575 100644 --- a/crud/replace_many.go +++ b/crud/replace_many.go @@ -24,39 +24,41 @@ type replaceManyArgs struct { Opts ReplaceManyOpts } -// NewReplaceManyRequest returns a new empty ReplaceManyRequest. -func NewReplaceManyRequest(space string) *ReplaceManyRequest { - req := new(ReplaceManyRequest) +// MakeReplaceManyRequest returns a new empty ReplaceManyRequest. +func MakeReplaceManyRequest(space string) ReplaceManyRequest { + req := ReplaceManyRequest{} req.impl = newCall("crud.replace_many") req.space = space - req.tuples = []Tuple{} req.opts = ReplaceManyOpts{} return req } // Tuples sets the tuples for the ReplaceManyRequest request. // Note: default value is nil. -func (req *ReplaceManyRequest) Tuples(tuples []Tuple) *ReplaceManyRequest { +func (req ReplaceManyRequest) Tuples(tuples []Tuple) ReplaceManyRequest { req.tuples = tuples return req } // Opts sets the options for the ReplaceManyRequest request. // Note: default value is nil. -func (req *ReplaceManyRequest) Opts(opts ReplaceManyOpts) *ReplaceManyRequest { +func (req ReplaceManyRequest) Opts(opts ReplaceManyOpts) ReplaceManyRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *ReplaceManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req ReplaceManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + if req.tuples == nil { + req.tuples = []Tuple{} + } args := replaceManyArgs{Space: req.space, Tuples: req.tuples, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *ReplaceManyRequest) Context(ctx context.Context) *ReplaceManyRequest { +func (req ReplaceManyRequest) Context(ctx context.Context) ReplaceManyRequest { req.impl = req.impl.Context(ctx) return req @@ -80,39 +82,41 @@ type replaceObjectManyArgs struct { Opts ReplaceObjectManyOpts } -// NewReplaceObjectManyRequest returns a new empty ReplaceObjectManyRequest. -func NewReplaceObjectManyRequest(space string) *ReplaceObjectManyRequest { - req := new(ReplaceObjectManyRequest) +// MakeReplaceObjectManyRequest returns a new empty ReplaceObjectManyRequest. +func MakeReplaceObjectManyRequest(space string) ReplaceObjectManyRequest { + req := ReplaceObjectManyRequest{} req.impl = newCall("crud.replace_object_many") req.space = space - req.objects = []Object{} req.opts = ReplaceObjectManyOpts{} return req } // Objects sets the tuple for the ReplaceObjectManyRequest request. // Note: default value is nil. -func (req *ReplaceObjectManyRequest) Objects(objects []Object) *ReplaceObjectManyRequest { +func (req ReplaceObjectManyRequest) Objects(objects []Object) ReplaceObjectManyRequest { req.objects = objects return req } // Opts sets the options for the ReplaceObjectManyRequest request. // Note: default value is nil. -func (req *ReplaceObjectManyRequest) Opts(opts ReplaceObjectManyOpts) *ReplaceObjectManyRequest { +func (req ReplaceObjectManyRequest) Opts(opts ReplaceObjectManyOpts) ReplaceObjectManyRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *ReplaceObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req ReplaceObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + if req.objects == nil { + req.objects = []Object{} + } args := replaceObjectManyArgs{Space: req.space, Objects: req.objects, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *ReplaceObjectManyRequest) Context(ctx context.Context) *ReplaceObjectManyRequest { +func (req ReplaceObjectManyRequest) Context(ctx context.Context) ReplaceObjectManyRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/request_test.go b/crud/request_test.go index fe36a861b..3198a39c0 100644 --- a/crud/request_test.go +++ b/crud/request_test.go @@ -118,7 +118,7 @@ func BenchmarkLenRequest(b *testing.B) { for i := 0; i < b.N; i++ { buf.Reset() - req := crud.NewLenRequest(spaceName). + req := crud.MakeLenRequest(spaceName). Opts(crud.LenOpts{ Timeout: crud.MakeOptUint(3), }) @@ -137,7 +137,7 @@ func BenchmarkSelectRequest(b *testing.B) { for i := 0; i < b.N; i++ { buf.Reset() - req := crud.NewSelectRequest(spaceName). + req := crud.MakeSelectRequest(spaceName). Opts(crud.SelectOpts{ Timeout: crud.MakeOptUint(3), VshardRouter: crud.MakeOptString("asd"), @@ -154,29 +154,29 @@ func TestRequestsCodes(t *testing.T) { req tarantool.Request code int32 }{ - {req: crud.NewInsertRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewInsertObjectRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewInsertManyRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewInsertObjectManyRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewGetRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewUpdateRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewDeleteRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewReplaceRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewReplaceObjectRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewReplaceManyRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewReplaceObjectManyRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewUpsertRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewUpsertObjectRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewUpsertManyRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewUpsertObjectManyRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewMinRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewMaxRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewSelectRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewTruncateRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewLenRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewCountRequest(validSpace), code: CrudRequestCode}, - {req: crud.NewStorageInfoRequest(), code: CrudRequestCode}, - {req: crud.NewStatsRequest(), code: CrudRequestCode}, + {req: crud.MakeInsertRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeInsertObjectRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeInsertManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeInsertObjectManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeGetRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeUpdateRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeDeleteRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeReplaceRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeReplaceObjectRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeReplaceManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeReplaceObjectManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeUpsertRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeUpsertObjectRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeUpsertManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeUpsertObjectManyRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeMinRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeMaxRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeSelectRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeTruncateRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeLenRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeCountRequest(validSpace), code: CrudRequestCode}, + {req: crud.MakeStorageInfoRequest(), code: CrudRequestCode}, + {req: crud.MakeStatsRequest(), code: CrudRequestCode}, } for _, test := range tests { @@ -191,29 +191,29 @@ func TestRequestsAsync(t *testing.T) { req tarantool.Request async bool }{ - {req: crud.NewInsertRequest(validSpace), async: false}, - {req: crud.NewInsertObjectRequest(validSpace), async: false}, - {req: crud.NewInsertManyRequest(validSpace), async: false}, - {req: crud.NewInsertObjectManyRequest(validSpace), async: false}, - {req: crud.NewGetRequest(validSpace), async: false}, - {req: crud.NewUpdateRequest(validSpace), async: false}, - {req: crud.NewDeleteRequest(validSpace), async: false}, - {req: crud.NewReplaceRequest(validSpace), async: false}, - {req: crud.NewReplaceObjectRequest(validSpace), async: false}, - {req: crud.NewReplaceManyRequest(validSpace), async: false}, - {req: crud.NewReplaceObjectManyRequest(validSpace), async: false}, - {req: crud.NewUpsertRequest(validSpace), async: false}, - {req: crud.NewUpsertObjectRequest(validSpace), async: false}, - {req: crud.NewUpsertManyRequest(validSpace), async: false}, - {req: crud.NewUpsertObjectManyRequest(validSpace), async: false}, - {req: crud.NewMinRequest(validSpace), async: false}, - {req: crud.NewMaxRequest(validSpace), async: false}, - {req: crud.NewSelectRequest(validSpace), async: false}, - {req: crud.NewTruncateRequest(validSpace), async: false}, - {req: crud.NewLenRequest(validSpace), async: false}, - {req: crud.NewCountRequest(validSpace), async: false}, - {req: crud.NewStorageInfoRequest(), async: false}, - {req: crud.NewStatsRequest(), async: false}, + {req: crud.MakeInsertRequest(validSpace), async: false}, + {req: crud.MakeInsertObjectRequest(validSpace), async: false}, + {req: crud.MakeInsertManyRequest(validSpace), async: false}, + {req: crud.MakeInsertObjectManyRequest(validSpace), async: false}, + {req: crud.MakeGetRequest(validSpace), async: false}, + {req: crud.MakeUpdateRequest(validSpace), async: false}, + {req: crud.MakeDeleteRequest(validSpace), async: false}, + {req: crud.MakeReplaceRequest(validSpace), async: false}, + {req: crud.MakeReplaceObjectRequest(validSpace), async: false}, + {req: crud.MakeReplaceManyRequest(validSpace), async: false}, + {req: crud.MakeReplaceObjectManyRequest(validSpace), async: false}, + {req: crud.MakeUpsertRequest(validSpace), async: false}, + {req: crud.MakeUpsertObjectRequest(validSpace), async: false}, + {req: crud.MakeUpsertManyRequest(validSpace), async: false}, + {req: crud.MakeUpsertObjectManyRequest(validSpace), async: false}, + {req: crud.MakeMinRequest(validSpace), async: false}, + {req: crud.MakeMaxRequest(validSpace), async: false}, + {req: crud.MakeSelectRequest(validSpace), async: false}, + {req: crud.MakeTruncateRequest(validSpace), async: false}, + {req: crud.MakeLenRequest(validSpace), async: false}, + {req: crud.MakeCountRequest(validSpace), async: false}, + {req: crud.MakeStorageInfoRequest(), async: false}, + {req: crud.MakeStatsRequest(), async: false}, } for _, test := range tests { @@ -228,29 +228,29 @@ func TestRequestsCtx_default(t *testing.T) { req tarantool.Request expected context.Context }{ - {req: crud.NewInsertRequest(validSpace), expected: nil}, - {req: crud.NewInsertObjectRequest(validSpace), expected: nil}, - {req: crud.NewInsertManyRequest(validSpace), expected: nil}, - {req: crud.NewInsertObjectManyRequest(validSpace), expected: nil}, - {req: crud.NewGetRequest(validSpace), expected: nil}, - {req: crud.NewUpdateRequest(validSpace), expected: nil}, - {req: crud.NewDeleteRequest(validSpace), expected: nil}, - {req: crud.NewReplaceRequest(validSpace), expected: nil}, - {req: crud.NewReplaceObjectRequest(validSpace), expected: nil}, - {req: crud.NewReplaceManyRequest(validSpace), expected: nil}, - {req: crud.NewReplaceObjectManyRequest(validSpace), expected: nil}, - {req: crud.NewUpsertRequest(validSpace), expected: nil}, - {req: crud.NewUpsertObjectRequest(validSpace), expected: nil}, - {req: crud.NewUpsertManyRequest(validSpace), expected: nil}, - {req: crud.NewUpsertObjectManyRequest(validSpace), expected: nil}, - {req: crud.NewMinRequest(validSpace), expected: nil}, - {req: crud.NewMaxRequest(validSpace), expected: nil}, - {req: crud.NewSelectRequest(validSpace), expected: nil}, - {req: crud.NewTruncateRequest(validSpace), expected: nil}, - {req: crud.NewLenRequest(validSpace), expected: nil}, - {req: crud.NewCountRequest(validSpace), expected: nil}, - {req: crud.NewStorageInfoRequest(), expected: nil}, - {req: crud.NewStatsRequest(), expected: nil}, + {req: crud.MakeInsertRequest(validSpace), expected: nil}, + {req: crud.MakeInsertObjectRequest(validSpace), expected: nil}, + {req: crud.MakeInsertManyRequest(validSpace), expected: nil}, + {req: crud.MakeInsertObjectManyRequest(validSpace), expected: nil}, + {req: crud.MakeGetRequest(validSpace), expected: nil}, + {req: crud.MakeUpdateRequest(validSpace), expected: nil}, + {req: crud.MakeDeleteRequest(validSpace), expected: nil}, + {req: crud.MakeReplaceRequest(validSpace), expected: nil}, + {req: crud.MakeReplaceObjectRequest(validSpace), expected: nil}, + {req: crud.MakeReplaceManyRequest(validSpace), expected: nil}, + {req: crud.MakeReplaceObjectManyRequest(validSpace), expected: nil}, + {req: crud.MakeUpsertRequest(validSpace), expected: nil}, + {req: crud.MakeUpsertObjectRequest(validSpace), expected: nil}, + {req: crud.MakeUpsertManyRequest(validSpace), expected: nil}, + {req: crud.MakeUpsertObjectManyRequest(validSpace), expected: nil}, + {req: crud.MakeMinRequest(validSpace), expected: nil}, + {req: crud.MakeMaxRequest(validSpace), expected: nil}, + {req: crud.MakeSelectRequest(validSpace), expected: nil}, + {req: crud.MakeTruncateRequest(validSpace), expected: nil}, + {req: crud.MakeLenRequest(validSpace), expected: nil}, + {req: crud.MakeCountRequest(validSpace), expected: nil}, + {req: crud.MakeStorageInfoRequest(), expected: nil}, + {req: crud.MakeStatsRequest(), expected: nil}, } for _, test := range tests { @@ -266,29 +266,29 @@ func TestRequestsCtx_setter(t *testing.T) { req tarantool.Request expected context.Context }{ - {req: crud.NewInsertRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewInsertObjectRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewInsertManyRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewInsertObjectManyRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewGetRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewUpdateRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewDeleteRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewReplaceRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewReplaceObjectRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewReplaceManyRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewReplaceObjectManyRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewUpsertRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewUpsertObjectRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewUpsertManyRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewUpsertObjectManyRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewMinRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewMaxRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewSelectRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewTruncateRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewLenRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewCountRequest(validSpace).Context(ctx), expected: ctx}, - {req: crud.NewStorageInfoRequest().Context(ctx), expected: ctx}, - {req: crud.NewStatsRequest().Context(ctx), expected: ctx}, + {req: crud.MakeInsertRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeInsertObjectRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeInsertManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeInsertObjectManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeGetRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeUpdateRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeDeleteRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeReplaceRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeReplaceObjectRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeReplaceManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeReplaceObjectManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeUpsertRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeUpsertObjectRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeUpsertManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeUpsertObjectManyRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeMinRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeMaxRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeSelectRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeTruncateRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeLenRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeCountRequest(validSpace).Context(ctx), expected: ctx}, + {req: crud.MakeStorageInfoRequest().Context(ctx), expected: ctx}, + {req: crud.MakeStatsRequest().Context(ctx), expected: ctx}, } for _, test := range tests { @@ -308,139 +308,139 @@ func TestRequestsDefaultValues(t *testing.T) { name: "InsertRequest", ref: tarantool.NewCall17Request("crud.insert").Args([]interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), - target: crud.NewInsertRequest(validSpace), + target: crud.MakeInsertRequest(validSpace), }, { name: "InsertObjectRequest", ref: tarantool.NewCall17Request("crud.insert_object").Args([]interface{}{validSpace, map[string]interface{}{}, map[string]interface{}{}}), - target: crud.NewInsertObjectRequest(validSpace), + target: crud.MakeInsertObjectRequest(validSpace), }, { name: "InsertManyRequest", ref: tarantool.NewCall17Request("crud.insert_many").Args([]interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), - target: crud.NewInsertManyRequest(validSpace), + target: crud.MakeInsertManyRequest(validSpace), }, { name: "InsertObjectManyRequest", ref: tarantool.NewCall17Request("crud.insert_object_many").Args([]interface{}{validSpace, []map[string]interface{}{}, map[string]interface{}{}}), - target: crud.NewInsertObjectManyRequest(validSpace), + target: crud.MakeInsertObjectManyRequest(validSpace), }, { name: "GetRequest", ref: tarantool.NewCall17Request("crud.get").Args([]interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), - target: crud.NewGetRequest(validSpace), + target: crud.MakeGetRequest(validSpace), }, { name: "UpdateRequest", ref: tarantool.NewCall17Request("crud.update").Args([]interface{}{validSpace, []interface{}{}, []interface{}{}, map[string]interface{}{}}), - target: crud.NewUpdateRequest(validSpace), + target: crud.MakeUpdateRequest(validSpace), }, { name: "DeleteRequest", ref: tarantool.NewCall17Request("crud.delete").Args([]interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), - target: crud.NewDeleteRequest(validSpace), + target: crud.MakeDeleteRequest(validSpace), }, { name: "ReplaceRequest", ref: tarantool.NewCall17Request("crud.replace").Args([]interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), - target: crud.NewReplaceRequest(validSpace), + target: crud.MakeReplaceRequest(validSpace), }, { name: "ReplaceObjectRequest", ref: tarantool.NewCall17Request("crud.replace_object").Args([]interface{}{validSpace, map[string]interface{}{}, map[string]interface{}{}}), - target: crud.NewReplaceObjectRequest(validSpace), + target: crud.MakeReplaceObjectRequest(validSpace), }, { name: "ReplaceManyRequest", ref: tarantool.NewCall17Request("crud.replace_many").Args([]interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), - target: crud.NewReplaceManyRequest(validSpace), + target: crud.MakeReplaceManyRequest(validSpace), }, { name: "ReplaceObjectManyRequest", ref: tarantool.NewCall17Request("crud.replace_object_many").Args([]interface{}{validSpace, []map[string]interface{}{}, map[string]interface{}{}}), - target: crud.NewReplaceObjectManyRequest(validSpace), + target: crud.MakeReplaceObjectManyRequest(validSpace), }, { name: "UpsertRequest", ref: tarantool.NewCall17Request("crud.upsert").Args([]interface{}{validSpace, []interface{}{}, []interface{}{}, map[string]interface{}{}}), - target: crud.NewUpsertRequest(validSpace), + target: crud.MakeUpsertRequest(validSpace), }, { name: "UpsertObjectRequest", ref: tarantool.NewCall17Request("crud.upsert_object").Args([]interface{}{validSpace, map[string]interface{}{}, []interface{}{}, map[string]interface{}{}}), - target: crud.NewUpsertObjectRequest(validSpace), + target: crud.MakeUpsertObjectRequest(validSpace), }, { name: "UpsertManyRequest", ref: tarantool.NewCall17Request("crud.upsert_many").Args([]interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), - target: crud.NewUpsertManyRequest(validSpace), + target: crud.MakeUpsertManyRequest(validSpace), }, { name: "UpsertObjectManyRequest", ref: tarantool.NewCall17Request("crud.upsert_object_many").Args([]interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), - target: crud.NewUpsertObjectManyRequest(validSpace), + target: crud.MakeUpsertObjectManyRequest(validSpace), }, { name: "SelectRequest", ref: tarantool.NewCall17Request("crud.select").Args([]interface{}{validSpace, nil, map[string]interface{}{}}), - target: crud.NewSelectRequest(validSpace), + target: crud.MakeSelectRequest(validSpace), }, { name: "MinRequest", ref: tarantool.NewCall17Request("crud.min").Args([]interface{}{validSpace, - []interface{}{}, map[string]interface{}{}}), - target: crud.NewMinRequest(validSpace), + nil, map[string]interface{}{}}), + target: crud.MakeMinRequest(validSpace), }, { name: "MaxRequest", ref: tarantool.NewCall17Request("crud.max").Args([]interface{}{validSpace, - []interface{}{}, map[string]interface{}{}}), - target: crud.NewMaxRequest(validSpace), + nil, map[string]interface{}{}}), + target: crud.MakeMaxRequest(validSpace), }, { name: "TruncateRequest", ref: tarantool.NewCall17Request("crud.truncate").Args([]interface{}{validSpace, map[string]interface{}{}}), - target: crud.NewTruncateRequest(validSpace), + target: crud.MakeTruncateRequest(validSpace), }, { name: "LenRequest", ref: tarantool.NewCall17Request("crud.len").Args([]interface{}{validSpace, map[string]interface{}{}}), - target: crud.NewLenRequest(validSpace), + target: crud.MakeLenRequest(validSpace), }, { name: "CountRequest", ref: tarantool.NewCall17Request("crud.count").Args([]interface{}{validSpace, nil, map[string]interface{}{}}), - target: crud.NewCountRequest(validSpace), + target: crud.MakeCountRequest(validSpace), }, { name: "StorageInfoRequest", ref: tarantool.NewCall17Request("crud.storage_info").Args( []interface{}{map[string]interface{}{}}), - target: crud.NewStorageInfoRequest(), + target: crud.MakeStorageInfoRequest(), }, { name: "StatsRequest", ref: tarantool.NewCall17Request("crud.stats").Args( []interface{}{}), - target: crud.NewStatsRequest(), + target: crud.MakeStatsRequest(), }, } @@ -460,120 +460,120 @@ func TestRequestsSetters(t *testing.T) { { name: "InsertRequest", ref: tarantool.NewCall17Request("crud.insert").Args([]interface{}{spaceName, tuple, expectedOpts}), - target: crud.NewInsertRequest(spaceName).Tuple(tuple).Opts(simpleOperationOpts), + target: crud.MakeInsertRequest(spaceName).Tuple(tuple).Opts(simpleOperationOpts), }, { name: "InsertObjectRequest", ref: tarantool.NewCall17Request("crud.insert_object").Args([]interface{}{spaceName, reqObject, expectedOpts}), - target: crud.NewInsertObjectRequest(spaceName).Object(reqObject).Opts(simpleOperationObjectOpts), + target: crud.MakeInsertObjectRequest(spaceName).Object(reqObject).Opts(simpleOperationObjectOpts), }, { name: "InsertManyRequest", ref: tarantool.NewCall17Request("crud.insert_many").Args([]interface{}{spaceName, tuples, expectedOpts}), - target: crud.NewInsertManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts), + target: crud.MakeInsertManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts), }, { name: "InsertObjectManyRequest", ref: tarantool.NewCall17Request("crud.insert_object_many").Args([]interface{}{spaceName, reqObjects, expectedOpts}), - target: crud.NewInsertObjectManyRequest(spaceName).Objects(reqObjects).Opts(opObjManyOpts), + target: crud.MakeInsertObjectManyRequest(spaceName).Objects(reqObjects).Opts(opObjManyOpts), }, { name: "GetRequest", ref: tarantool.NewCall17Request("crud.get").Args([]interface{}{spaceName, key, expectedOpts}), - target: crud.NewGetRequest(spaceName).Key(key).Opts(getOpts), + target: crud.MakeGetRequest(spaceName).Key(key).Opts(getOpts), }, { name: "UpdateRequest", ref: tarantool.NewCall17Request("crud.update").Args([]interface{}{spaceName, key, operations, expectedOpts}), - target: crud.NewUpdateRequest(spaceName).Key(key).Operations(operations).Opts(simpleOperationOpts), + target: crud.MakeUpdateRequest(spaceName).Key(key).Operations(operations).Opts(simpleOperationOpts), }, { name: "DeleteRequest", ref: tarantool.NewCall17Request("crud.delete").Args([]interface{}{spaceName, key, expectedOpts}), - target: crud.NewDeleteRequest(spaceName).Key(key).Opts(simpleOperationOpts), + target: crud.MakeDeleteRequest(spaceName).Key(key).Opts(simpleOperationOpts), }, { name: "ReplaceRequest", ref: tarantool.NewCall17Request("crud.replace").Args([]interface{}{spaceName, tuple, expectedOpts}), - target: crud.NewReplaceRequest(spaceName).Tuple(tuple).Opts(simpleOperationOpts), + target: crud.MakeReplaceRequest(spaceName).Tuple(tuple).Opts(simpleOperationOpts), }, { name: "ReplaceObjectRequest", ref: tarantool.NewCall17Request("crud.replace_object").Args([]interface{}{spaceName, reqObject, expectedOpts}), - target: crud.NewReplaceObjectRequest(spaceName).Object(reqObject).Opts(simpleOperationObjectOpts), + target: crud.MakeReplaceObjectRequest(spaceName).Object(reqObject).Opts(simpleOperationObjectOpts), }, { name: "ReplaceManyRequest", ref: tarantool.NewCall17Request("crud.replace_many").Args([]interface{}{spaceName, tuples, expectedOpts}), - target: crud.NewReplaceManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts), + target: crud.MakeReplaceManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts), }, { name: "ReplaceObjectManyRequest", ref: tarantool.NewCall17Request("crud.replace_object_many").Args([]interface{}{spaceName, reqObjects, expectedOpts}), - target: crud.NewReplaceObjectManyRequest(spaceName).Objects(reqObjects).Opts(opObjManyOpts), + target: crud.MakeReplaceObjectManyRequest(spaceName).Objects(reqObjects).Opts(opObjManyOpts), }, { name: "UpsertRequest", ref: tarantool.NewCall17Request("crud.upsert").Args([]interface{}{spaceName, tuple, operations, expectedOpts}), - target: crud.NewUpsertRequest(spaceName).Tuple(tuple).Operations(operations).Opts(simpleOperationOpts), + target: crud.MakeUpsertRequest(spaceName).Tuple(tuple).Operations(operations).Opts(simpleOperationOpts), }, { name: "UpsertObjectRequest", ref: tarantool.NewCall17Request("crud.upsert_object").Args([]interface{}{spaceName, reqObject, operations, expectedOpts}), - target: crud.NewUpsertObjectRequest(spaceName).Object(reqObject).Operations(operations).Opts(simpleOperationOpts), + target: crud.MakeUpsertObjectRequest(spaceName).Object(reqObject).Operations(operations).Opts(simpleOperationOpts), }, { name: "UpsertManyRequest", ref: tarantool.NewCall17Request("crud.upsert_many").Args([]interface{}{spaceName, tuplesOperationsData, expectedOpts}), - target: crud.NewUpsertManyRequest(spaceName).TuplesOperationsData(tuplesOperationsData).Opts(opManyOpts), + target: crud.MakeUpsertManyRequest(spaceName).TuplesOperationsData(tuplesOperationsData).Opts(opManyOpts), }, { name: "UpsertObjectManyRequest", ref: tarantool.NewCall17Request("crud.upsert_object_many").Args([]interface{}{spaceName, reqObjectsOperationsData, expectedOpts}), - target: crud.NewUpsertObjectManyRequest(spaceName).ObjectsOperationsData(reqObjectsOperationsData).Opts(opManyOpts), + target: crud.MakeUpsertObjectManyRequest(spaceName).ObjectsOperationsData(reqObjectsOperationsData).Opts(opManyOpts), }, { name: "SelectRequest", ref: tarantool.NewCall17Request("crud.select").Args([]interface{}{spaceName, conditions, expectedOpts}), - target: crud.NewSelectRequest(spaceName).Conditions(conditions).Opts(selectOpts), + target: crud.MakeSelectRequest(spaceName).Conditions(conditions).Opts(selectOpts), }, { name: "MinRequest", ref: tarantool.NewCall17Request("crud.min").Args([]interface{}{spaceName, indexName, expectedOpts}), - target: crud.NewMinRequest(spaceName).Index(indexName).Opts(minOpts), + target: crud.MakeMinRequest(spaceName).Index(indexName).Opts(minOpts), }, { name: "MaxRequest", ref: tarantool.NewCall17Request("crud.max").Args([]interface{}{spaceName, indexName, expectedOpts}), - target: crud.NewMaxRequest(spaceName).Index(indexName).Opts(maxOpts), + target: crud.MakeMaxRequest(spaceName).Index(indexName).Opts(maxOpts), }, { name: "TruncateRequest", ref: tarantool.NewCall17Request("crud.truncate").Args([]interface{}{spaceName, expectedOpts}), - target: crud.NewTruncateRequest(spaceName).Opts(baseOpts), + target: crud.MakeTruncateRequest(spaceName).Opts(baseOpts), }, { name: "LenRequest", ref: tarantool.NewCall17Request("crud.len").Args([]interface{}{spaceName, expectedOpts}), - target: crud.NewLenRequest(spaceName).Opts(baseOpts), + target: crud.MakeLenRequest(spaceName).Opts(baseOpts), }, { name: "CountRequest", ref: tarantool.NewCall17Request("crud.count").Args([]interface{}{spaceName, conditions, expectedOpts}), - target: crud.NewCountRequest(spaceName).Conditions(conditions).Opts(countOpts), + target: crud.MakeCountRequest(spaceName).Conditions(conditions).Opts(countOpts), }, { name: "StorageInfoRequest", ref: tarantool.NewCall17Request("crud.storage_info").Args([]interface{}{expectedOpts}), - target: crud.NewStorageInfoRequest().Opts(baseOpts), + target: crud.MakeStorageInfoRequest().Opts(baseOpts), }, { name: "StatsRequest", ref: tarantool.NewCall17Request("crud.stats").Args([]interface{}{spaceName}), - target: crud.NewStatsRequest().Space(spaceName), + target: crud.MakeStatsRequest().Space(spaceName), }, } diff --git a/crud/select.go b/crud/select.go index e9c1f7681..97048a365 100644 --- a/crud/select.go +++ b/crud/select.go @@ -83,9 +83,9 @@ type selectArgs struct { Opts SelectOpts } -// NewSelectRequest returns a new empty SelectRequest. -func NewSelectRequest(space string) *SelectRequest { - req := new(SelectRequest) +// MakeSelectRequest returns a new empty SelectRequest. +func MakeSelectRequest(space string) SelectRequest { + req := SelectRequest{} req.impl = newCall("crud.select") req.space = space req.conditions = nil @@ -95,27 +95,27 @@ func NewSelectRequest(space string) *SelectRequest { // Conditions sets the conditions for the SelectRequest request. // Note: default value is nil. -func (req *SelectRequest) Conditions(conditions []Condition) *SelectRequest { +func (req SelectRequest) Conditions(conditions []Condition) SelectRequest { req.conditions = conditions return req } // Opts sets the options for the SelectRequest request. // Note: default value is nil. -func (req *SelectRequest) Opts(opts SelectOpts) *SelectRequest { +func (req SelectRequest) Opts(opts SelectOpts) SelectRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *SelectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req SelectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { args := selectArgs{Space: req.space, Conditions: req.conditions, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *SelectRequest) Context(ctx context.Context) *SelectRequest { +func (req SelectRequest) Context(ctx context.Context) SelectRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/stats.go b/crud/stats.go index 72be6a309..aa4184746 100644 --- a/crud/stats.go +++ b/crud/stats.go @@ -13,33 +13,33 @@ type StatsRequest struct { space OptString } -// NewStatsRequest returns a new empty StatsRequest. -func NewStatsRequest() *StatsRequest { - req := new(StatsRequest) +// MakeStatsRequest returns a new empty StatsRequest. +func MakeStatsRequest() StatsRequest { + req := StatsRequest{} req.impl = newCall("crud.stats") return req } // Space sets the space name for the StatsRequest request. // Note: default value is nil. -func (req *StatsRequest) Space(space string) *StatsRequest { +func (req StatsRequest) Space(space string) StatsRequest { req.space = MakeOptString(space) return req } // Body fills an encoder with the call request body. -func (req *StatsRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { - args := []interface{}{} +func (req StatsRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { if value, ok := req.space.Get(); ok { - args = []interface{}{value} + req.impl = req.impl.Args([]interface{}{value}) + } else { + req.impl = req.impl.Args([]interface{}{}) } - req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *StatsRequest) Context(ctx context.Context) *StatsRequest { +func (req StatsRequest) Context(ctx context.Context) StatsRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/storage_info.go b/crud/storage_info.go index 2858d3e46..625029b51 100644 --- a/crud/storage_info.go +++ b/crud/storage_info.go @@ -100,9 +100,9 @@ type storageInfoArgs struct { Opts StorageInfoOpts } -// NewStorageInfoRequest returns a new empty StorageInfoRequest. -func NewStorageInfoRequest() *StorageInfoRequest { - req := new(StorageInfoRequest) +// MakeStorageInfoRequest returns a new empty StorageInfoRequest. +func MakeStorageInfoRequest() StorageInfoRequest { + req := StorageInfoRequest{} req.impl = newCall("crud.storage_info") req.opts = StorageInfoOpts{} return req @@ -110,20 +110,20 @@ func NewStorageInfoRequest() *StorageInfoRequest { // Opts sets the options for the torageInfoRequest request. // Note: default value is nil. -func (req *StorageInfoRequest) Opts(opts StorageInfoOpts) *StorageInfoRequest { +func (req StorageInfoRequest) Opts(opts StorageInfoOpts) StorageInfoRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *StorageInfoRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req StorageInfoRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { args := storageInfoArgs{Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *StorageInfoRequest) Context(ctx context.Context) *StorageInfoRequest { +func (req StorageInfoRequest) Context(ctx context.Context) StorageInfoRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index f30b7d5c2..71f7d62b1 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -112,21 +112,21 @@ var testProcessDataCases = []struct { { "Select", 2, - crud.NewSelectRequest(spaceName). + crud.MakeSelectRequest(spaceName). Conditions(conditions). Opts(selectOpts), }, { "Get", 2, - crud.NewGetRequest(spaceName). + crud.MakeGetRequest(spaceName). Key(key). Opts(getOpts), }, { "Update", 2, - crud.NewUpdateRequest(spaceName). + crud.MakeUpdateRequest(spaceName). Key(key). Operations(operations). Opts(simpleOperationOpts), @@ -134,46 +134,56 @@ var testProcessDataCases = []struct { { "Delete", 2, - crud.NewDeleteRequest(spaceName). + crud.MakeDeleteRequest(spaceName). Key(key). Opts(simpleOperationOpts), }, { "Min", 2, - crud.NewMinRequest(spaceName).Index(indexName).Opts(minOpts), + crud.MakeMinRequest(spaceName).Opts(minOpts), + }, + { + "Min", + 2, + crud.MakeMinRequest(spaceName).Index(indexName).Opts(minOpts), }, { "Max", 2, - crud.NewMaxRequest(spaceName).Index(indexName).Opts(maxOpts), + crud.MakeMaxRequest(spaceName).Opts(maxOpts), + }, + { + "Max", + 2, + crud.MakeMaxRequest(spaceName).Index(indexName).Opts(maxOpts), }, { "Truncate", 1, - crud.NewTruncateRequest(spaceName).Opts(baseOpts), + crud.MakeTruncateRequest(spaceName).Opts(baseOpts), }, { "Len", 1, - crud.NewLenRequest(spaceName).Opts(baseOpts), + crud.MakeLenRequest(spaceName).Opts(baseOpts), }, { "Count", 2, - crud.NewCountRequest(spaceName). + crud.MakeCountRequest(spaceName). Conditions(conditions). Opts(countOpts), }, { "Stats", 1, - crud.NewStatsRequest().Space(spaceName), + crud.MakeStatsRequest().Space(spaceName), }, { "StorageInfo", 1, - crud.NewStorageInfoRequest().Opts(baseOpts), + crud.MakeStorageInfoRequest().Opts(baseOpts), }, } @@ -185,22 +195,22 @@ var testResultWithErrCases = []struct { { "BaseResult", &crud.Result{}, - crud.NewSelectRequest(invalidSpaceName).Opts(selectOpts), + crud.MakeSelectRequest(invalidSpaceName).Opts(selectOpts), }, { "ManyResult", &crud.Result{}, - crud.NewReplaceManyRequest(invalidSpaceName).Opts(opManyOpts), + crud.MakeReplaceManyRequest(invalidSpaceName).Opts(opManyOpts), }, { "NumberResult", &crud.CountResult{}, - crud.NewCountRequest(invalidSpaceName).Opts(countOpts), + crud.MakeCountRequest(invalidSpaceName).Opts(countOpts), }, { "BoolResult", &crud.TruncateResult{}, - crud.NewTruncateRequest(invalidSpaceName).Opts(baseOpts), + crud.MakeTruncateRequest(invalidSpaceName).Opts(baseOpts), }, } @@ -217,7 +227,7 @@ var testGenerateDataCases = []struct { "Insert", 2, 1, - crud.NewInsertRequest(spaceName). + crud.MakeInsertRequest(spaceName). Tuple(tuple). Opts(simpleOperationOpts), }, @@ -225,7 +235,7 @@ var testGenerateDataCases = []struct { "InsertObject", 2, 1, - crud.NewInsertObjectRequest(spaceName). + crud.MakeInsertObjectRequest(spaceName). Object(object). Opts(simpleOperationObjectOpts), }, @@ -233,7 +243,7 @@ var testGenerateDataCases = []struct { "InsertMany", 2, 10, - crud.NewInsertManyRequest(spaceName). + crud.MakeInsertManyRequest(spaceName). Tuples(tuples). Opts(opManyOpts), }, @@ -241,7 +251,7 @@ var testGenerateDataCases = []struct { "InsertObjectMany", 2, 10, - crud.NewInsertObjectManyRequest(spaceName). + crud.MakeInsertObjectManyRequest(spaceName). Objects(objects). Opts(opObjManyOpts), }, @@ -249,7 +259,7 @@ var testGenerateDataCases = []struct { "Replace", 2, 1, - crud.NewReplaceRequest(spaceName). + crud.MakeReplaceRequest(spaceName). Tuple(tuple). Opts(simpleOperationOpts), }, @@ -257,7 +267,7 @@ var testGenerateDataCases = []struct { "ReplaceObject", 2, 1, - crud.NewReplaceObjectRequest(spaceName). + crud.MakeReplaceObjectRequest(spaceName). Object(object). Opts(simpleOperationObjectOpts), }, @@ -265,7 +275,7 @@ var testGenerateDataCases = []struct { "ReplaceMany", 2, 10, - crud.NewReplaceManyRequest(spaceName). + crud.MakeReplaceManyRequest(spaceName). Tuples(tuples). Opts(opManyOpts), }, @@ -273,7 +283,7 @@ var testGenerateDataCases = []struct { "ReplaceObjectMany", 2, 10, - crud.NewReplaceObjectManyRequest(spaceName). + crud.MakeReplaceObjectManyRequest(spaceName). Objects(objects). Opts(opObjManyOpts), }, @@ -281,7 +291,7 @@ var testGenerateDataCases = []struct { "Upsert", 2, 1, - crud.NewUpsertRequest(spaceName). + crud.MakeUpsertRequest(spaceName). Tuple(tuple). Operations(operations). Opts(simpleOperationOpts), @@ -290,7 +300,7 @@ var testGenerateDataCases = []struct { "UpsertObject", 2, 1, - crud.NewUpsertObjectRequest(spaceName). + crud.MakeUpsertObjectRequest(spaceName). Object(object). Operations(operations). Opts(simpleOperationOpts), @@ -299,7 +309,7 @@ var testGenerateDataCases = []struct { "UpsertMany", 2, 10, - crud.NewUpsertManyRequest(spaceName). + crud.MakeUpsertManyRequest(spaceName). TuplesOperationsData(tuplesOperationsData). Opts(opManyOpts), }, @@ -307,7 +317,7 @@ var testGenerateDataCases = []struct { "UpsertObjectMany", 2, 10, - crud.NewUpsertObjectManyRequest(spaceName). + crud.MakeUpsertObjectManyRequest(spaceName). ObjectsOperationsData(objectsOperationData). Opts(opManyOpts), }, @@ -476,7 +486,6 @@ func TestCrudProcessData(t *testing.T) { resp, err := conn.Do(testCase.req).Get() testCrudRequestCheck(t, testCase.req, resp, err, testCase.expectedRespLen) - for i := 1010; i < 1020; i++ { conn.Delete(spaceName, nil, []interface{}{uint(i)}) } @@ -522,7 +531,7 @@ func TestUnflattenRows(t *testing.T) { defer conn.Close() // Do `replace`. - req := crud.NewReplaceRequest(spaceName). + req := crud.MakeReplaceRequest(spaceName). Tuple(tuple). Opts(simpleOperationOpts) resp, err := conn.Do(req).Get() @@ -595,7 +604,7 @@ func TestBoolResult(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - req := crud.NewTruncateRequest(spaceName).Opts(baseOpts) + req := crud.MakeTruncateRequest(spaceName).Opts(baseOpts) resp := crud.TruncateResult{} testCrudRequestPrepareData(t, conn) @@ -618,7 +627,7 @@ func TestNumberResult(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - req := crud.NewCountRequest(spaceName).Opts(countOpts) + req := crud.MakeCountRequest(spaceName).Opts(countOpts) resp := crud.CountResult{} testCrudRequestPrepareData(t, conn) @@ -659,7 +668,7 @@ func TestBaseResult(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - req := crud.NewSelectRequest(spaceName).Opts(selectOpts) + req := crud.MakeSelectRequest(spaceName).Opts(selectOpts) resp := crud.Result{} testCrudRequestPrepareData(t, conn) @@ -702,7 +711,7 @@ func TestManyResult(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - req := crud.NewReplaceManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts) + req := crud.MakeReplaceManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts) resp := crud.Result{} testCrudRequestPrepareData(t, conn) @@ -727,7 +736,7 @@ func TestStorageInfoResult(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - req := crud.NewStorageInfoRequest().Opts(baseOpts) + req := crud.MakeStorageInfoRequest().Opts(baseOpts) resp := crud.StorageInfoResult{} err := conn.Do(req).GetTyped(&resp) diff --git a/crud/truncate.go b/crud/truncate.go index 6d00540d4..1cd343a4d 100644 --- a/crud/truncate.go +++ b/crud/truncate.go @@ -25,9 +25,9 @@ type truncateArgs struct { Opts TruncateOpts } -// NewTruncateRequest returns a new empty TruncateRequest. -func NewTruncateRequest(space string) *TruncateRequest { - req := new(TruncateRequest) +// MakeTruncateRequest returns a new empty TruncateRequest. +func MakeTruncateRequest(space string) TruncateRequest { + req := TruncateRequest{} req.impl = newCall("crud.truncate") req.space = space req.opts = TruncateOpts{} @@ -36,20 +36,20 @@ func NewTruncateRequest(space string) *TruncateRequest { // Opts sets the options for the TruncateRequest request. // Note: default value is nil. -func (req *TruncateRequest) Opts(opts TruncateOpts) *TruncateRequest { +func (req TruncateRequest) Opts(opts TruncateOpts) TruncateRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *TruncateRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req TruncateRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { args := truncateArgs{Space: req.space, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) } // Context sets a passed context to CRUD request. -func (req *TruncateRequest) Context(ctx context.Context) *TruncateRequest { +func (req TruncateRequest) Context(ctx context.Context) TruncateRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/update.go b/crud/update.go index 8fa676ffd..40951c95a 100644 --- a/crud/update.go +++ b/crud/update.go @@ -26,12 +26,11 @@ type updateArgs struct { Opts UpdateOpts } -// NewUpdateRequest returns a new empty UpdateRequest. -func NewUpdateRequest(space string) *UpdateRequest { - req := new(UpdateRequest) +// MakeUpdateRequest returns a new empty UpdateRequest. +func MakeUpdateRequest(space string) UpdateRequest { + req := UpdateRequest{} req.impl = newCall("crud.update") req.space = space - req.key = []interface{}{} req.operations = []Operation{} req.opts = UpdateOpts{} return req @@ -39,27 +38,30 @@ func NewUpdateRequest(space string) *UpdateRequest { // Key sets the key for the UpdateRequest request. // Note: default value is nil. -func (req *UpdateRequest) Key(key Tuple) *UpdateRequest { +func (req UpdateRequest) Key(key Tuple) UpdateRequest { req.key = key return req } // Operations sets the operations for UpdateRequest request. // Note: default value is nil. -func (req *UpdateRequest) Operations(operations []Operation) *UpdateRequest { +func (req UpdateRequest) Operations(operations []Operation) UpdateRequest { req.operations = operations return req } // Opts sets the options for the UpdateRequest request. // Note: default value is nil. -func (req *UpdateRequest) Opts(opts UpdateOpts) *UpdateRequest { +func (req UpdateRequest) Opts(opts UpdateOpts) UpdateRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *UpdateRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req UpdateRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + if req.key == nil { + req.key = []interface{}{} + } args := updateArgs{Space: req.space, Key: req.key, Operations: req.operations, Opts: req.opts} req.impl = req.impl.Args(args) @@ -67,7 +69,7 @@ func (req *UpdateRequest) Body(res tarantool.SchemaResolver, enc *encoder) error } // Context sets a passed context to CRUD request. -func (req *UpdateRequest) Context(ctx context.Context) *UpdateRequest { +func (req UpdateRequest) Context(ctx context.Context) UpdateRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/upsert.go b/crud/upsert.go index 15c3c9fd2..c116bbca5 100644 --- a/crud/upsert.go +++ b/crud/upsert.go @@ -26,12 +26,11 @@ type upsertArgs struct { Opts UpsertOpts } -// NewUpsertRequest returns a new empty UpsertRequest. -func NewUpsertRequest(space string) *UpsertRequest { - req := new(UpsertRequest) +// MakeUpsertRequest returns a new empty UpsertRequest. +func MakeUpsertRequest(space string) UpsertRequest { + req := UpsertRequest{} req.impl = newCall("crud.upsert") req.space = space - req.tuple = []interface{}{} req.operations = []Operation{} req.opts = UpsertOpts{} return req @@ -39,27 +38,30 @@ func NewUpsertRequest(space string) *UpsertRequest { // Tuple sets the tuple for the UpsertRequest request. // Note: default value is nil. -func (req *UpsertRequest) Tuple(tuple Tuple) *UpsertRequest { +func (req UpsertRequest) Tuple(tuple Tuple) UpsertRequest { req.tuple = tuple return req } // Operations sets the operations for the UpsertRequest request. // Note: default value is nil. -func (req *UpsertRequest) Operations(operations []Operation) *UpsertRequest { +func (req UpsertRequest) Operations(operations []Operation) UpsertRequest { req.operations = operations return req } // Opts sets the options for the UpsertRequest request. // Note: default value is nil. -func (req *UpsertRequest) Opts(opts UpsertOpts) *UpsertRequest { +func (req UpsertRequest) Opts(opts UpsertOpts) UpsertRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *UpsertRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req UpsertRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + if req.tuple == nil { + req.tuple = []interface{}{} + } args := upsertArgs{Space: req.space, Tuple: req.tuple, Operations: req.operations, Opts: req.opts} req.impl = req.impl.Args(args) @@ -67,7 +69,7 @@ func (req *UpsertRequest) Body(res tarantool.SchemaResolver, enc *encoder) error } // Context sets a passed context to CRUD request. -func (req *UpsertRequest) Context(ctx context.Context) *UpsertRequest { +func (req UpsertRequest) Context(ctx context.Context) UpsertRequest { req.impl = req.impl.Context(ctx) return req @@ -93,12 +95,11 @@ type upsertObjectArgs struct { Opts UpsertObjectOpts } -// NewUpsertObjectRequest returns a new empty UpsertObjectRequest. -func NewUpsertObjectRequest(space string) *UpsertObjectRequest { - req := new(UpsertObjectRequest) +// MakeUpsertObjectRequest returns a new empty UpsertObjectRequest. +func MakeUpsertObjectRequest(space string) UpsertObjectRequest { + req := UpsertObjectRequest{} req.impl = newCall("crud.upsert_object") req.space = space - req.object = MapObject{} req.operations = []Operation{} req.opts = UpsertObjectOpts{} return req @@ -106,27 +107,30 @@ func NewUpsertObjectRequest(space string) *UpsertObjectRequest { // Object sets the tuple for the UpsertObjectRequest request. // Note: default value is nil. -func (req *UpsertObjectRequest) Object(object Object) *UpsertObjectRequest { +func (req UpsertObjectRequest) Object(object Object) UpsertObjectRequest { req.object = object return req } // Operations sets the operations for the UpsertObjectRequest request. // Note: default value is nil. -func (req *UpsertObjectRequest) Operations(operations []Operation) *UpsertObjectRequest { +func (req UpsertObjectRequest) Operations(operations []Operation) UpsertObjectRequest { req.operations = operations return req } // Opts sets the options for the UpsertObjectRequest request. // Note: default value is nil. -func (req *UpsertObjectRequest) Opts(opts UpsertObjectOpts) *UpsertObjectRequest { +func (req UpsertObjectRequest) Opts(opts UpsertObjectOpts) UpsertObjectRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *UpsertObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req UpsertObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { + if req.object == nil { + req.object = MapObject{} + } args := upsertObjectArgs{Space: req.space, Object: req.object, Operations: req.operations, Opts: req.opts} req.impl = req.impl.Args(args) @@ -134,7 +138,7 @@ func (req *UpsertObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) } // Context sets a passed context to CRUD request. -func (req *UpsertObjectRequest) Context(ctx context.Context) *UpsertObjectRequest { +func (req UpsertObjectRequest) Context(ctx context.Context) UpsertObjectRequest { req.impl = req.impl.Context(ctx) return req diff --git a/crud/upsert_many.go b/crud/upsert_many.go index d2089b09a..a195f5efa 100644 --- a/crud/upsert_many.go +++ b/crud/upsert_many.go @@ -31,9 +31,9 @@ type upsertManyArgs struct { Opts UpsertManyOpts } -// NewUpsertManyRequest returns a new empty UpsertManyRequest. -func NewUpsertManyRequest(space string) *UpsertManyRequest { - req := new(UpsertManyRequest) +// MakeUpsertManyRequest returns a new empty UpsertManyRequest. +func MakeUpsertManyRequest(space string) UpsertManyRequest { + req := UpsertManyRequest{} req.impl = newCall("crud.upsert_many") req.space = space req.tuplesOperationsData = []TupleOperationsData{} @@ -44,20 +44,20 @@ func NewUpsertManyRequest(space string) *UpsertManyRequest { // TuplesOperationsData sets tuples and operations for // the UpsertManyRequest request. // Note: default value is nil. -func (req *UpsertManyRequest) TuplesOperationsData(tuplesOperationData []TupleOperationsData) *UpsertManyRequest { +func (req UpsertManyRequest) TuplesOperationsData(tuplesOperationData []TupleOperationsData) UpsertManyRequest { req.tuplesOperationsData = tuplesOperationData return req } // Opts sets the options for the UpsertManyRequest request. // Note: default value is nil. -func (req *UpsertManyRequest) Opts(opts UpsertManyOpts) *UpsertManyRequest { +func (req UpsertManyRequest) Opts(opts UpsertManyOpts) UpsertManyRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *UpsertManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req UpsertManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { args := upsertManyArgs{Space: req.space, TuplesOperationsData: req.tuplesOperationsData, Opts: req.opts} req.impl = req.impl.Args(args) @@ -65,7 +65,7 @@ func (req *UpsertManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) e } // Context sets a passed context to CRUD request. -func (req *UpsertManyRequest) Context(ctx context.Context) *UpsertManyRequest { +func (req UpsertManyRequest) Context(ctx context.Context) UpsertManyRequest { req.impl = req.impl.Context(ctx) return req @@ -96,9 +96,9 @@ type upsertObjectManyArgs struct { Opts UpsertObjectManyOpts } -// NewUpsertObjectManyRequest returns a new empty UpsertObjectManyRequest. -func NewUpsertObjectManyRequest(space string) *UpsertObjectManyRequest { - req := new(UpsertObjectManyRequest) +// MakeUpsertObjectManyRequest returns a new empty UpsertObjectManyRequest. +func MakeUpsertObjectManyRequest(space string) UpsertObjectManyRequest { + req := UpsertObjectManyRequest{} req.impl = newCall("crud.upsert_object_many") req.space = space req.objectsOperationsData = []ObjectOperationsData{} @@ -109,21 +109,21 @@ func NewUpsertObjectManyRequest(space string) *UpsertObjectManyRequest { // ObjectOperationsData sets objects and operations // for the UpsertObjectManyRequest request. // Note: default value is nil. -func (req *UpsertObjectManyRequest) ObjectsOperationsData( - objectsOperationData []ObjectOperationsData) *UpsertObjectManyRequest { +func (req UpsertObjectManyRequest) ObjectsOperationsData( + objectsOperationData []ObjectOperationsData) UpsertObjectManyRequest { req.objectsOperationsData = objectsOperationData return req } // Opts sets the options for the UpsertObjectManyRequest request. // Note: default value is nil. -func (req *UpsertObjectManyRequest) Opts(opts UpsertObjectManyOpts) *UpsertObjectManyRequest { +func (req UpsertObjectManyRequest) Opts(opts UpsertObjectManyOpts) UpsertObjectManyRequest { req.opts = opts return req } // Body fills an encoder with the call request body. -func (req *UpsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req UpsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { args := upsertObjectManyArgs{Space: req.space, ObjectsOperationsData: req.objectsOperationsData, Opts: req.opts} req.impl = req.impl.Args(args) @@ -131,7 +131,7 @@ func (req *UpsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *enco } // Context sets a passed context to CRUD request. -func (req *UpsertObjectManyRequest) Context(ctx context.Context) *UpsertObjectManyRequest { +func (req UpsertObjectManyRequest) Context(ctx context.Context) UpsertObjectManyRequest { req.impl = req.impl.Context(ctx) return req From 51a6eb016ede1961b52eddb292c3da08f9ac80e5 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 22 Mar 2023 11:40:54 +0300 Subject: [PATCH 412/605] crud: condition field may be string only Relates to https://github.com/tarantool/crud/pull/350 --- crud/conditions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crud/conditions.go b/crud/conditions.go index c11da1c29..8945adcd1 100644 --- a/crud/conditions.go +++ b/crud/conditions.go @@ -23,6 +23,6 @@ type Condition struct { // is needed. _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused Operator Operator - Field interface{} // Field name, field number or index name. + Field string // Field name or index name. Value interface{} } From 56fb5a300edce5350ca1def835f6fae310294d37 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 15 Mar 2023 14:55:10 +0300 Subject: [PATCH 413/605] pool: handle disconnection with Opts.Reconnect The ConnectionPool didn't close a connection if it couldn't get a role when checking the connection. Closes #272 --- CHANGELOG.md | 2 ++ connection_pool/connection_pool.go | 18 ++++++++-- connection_pool/connection_pool_test.go | 46 +++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b49105d21..24e7c5f65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed - Several non-critical data race issues (#218) +- ConnectionPool does not properly handle disconnection with Opts.Reconnect + set (#272) ## [1.10.0] - 2022-12-31 diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 569669be4..892fea483 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -163,6 +163,10 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co } // ConnectWithOpts creates pool for instances with addresses addrs. +// +// It is useless to set up tarantool.Opts.Reconnect value for a connection. +// The connection pool has its own reconnection logic. See +// OptsPool.CheckTimeout description. func Connect(addrs []string, connOpts tarantool.Opts) (connPool *ConnectionPool, err error) { opts := OptsPool{ CheckTimeout: 1 * time.Second, @@ -977,10 +981,18 @@ func (pool *ConnectionPool) updateConnection(s connState) connState { } s.role = role } - } + pool.poolsMutex.Unlock() + return s + } else { + pool.deleteConnection(s.addr) + pool.poolsMutex.Unlock() - pool.poolsMutex.Unlock() - return s + s.conn.Close() + pool.handlerDeactivated(s.conn, s.role) + s.conn = nil + s.role = UnknownRole + return s + } } func (pool *ConnectionPool) tryConnect(s connState) connState { diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 860786fbe..765e63e72 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -148,6 +148,52 @@ func TestReconnect(t *testing.T) { require.Nil(t, err) } +func TestDisconnect_withReconnect(t *testing.T) { + const serverId = 0 + + opts := connOpts + opts.Reconnect = 10 * time.Second + + connPool, err := connection_pool.Connect([]string{servers[serverId]}, opts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // Test. + test_helpers.StopTarantoolWithCleanup(instances[serverId]) + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{servers[serverId]}, + ExpectedPoolStatus: false, + ExpectedStatuses: map[string]bool{ + servers[serverId]: false, + }, + } + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, + args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) + + // Restart the server after success. + err = test_helpers.RestartTarantool(&instances[serverId]) + require.Nilf(t, err, "failed to restart tarantool") + + args = test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{servers[serverId]}, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + servers[serverId]: true, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, + args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) +} + func TestDisconnectAll(t *testing.T) { server1 := servers[0] server2 := servers[1] From fbe0470d407bfda3a12d5fd755429467c7077319 Mon Sep 17 00:00:00 2001 From: better0fdead Date: Thu, 17 Nov 2022 12:29:06 +0300 Subject: [PATCH 414/605] ci: Add testing on macOS Added macOS testing into ci. Updated actions to versions with node16 runtime. Closes #157 --- .github/workflows/testing.yml | 173 ++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 05fee7835..f065305eb 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -240,3 +240,176 @@ jobs: - name: Check workability of benchmark tests run: make bench-deps bench DURATION=1x COUNT=1 + + testing_mac_os: + # We want to run on external PRs, but not on our own internal + # PRs as they'll be run by the push to the branch. + # + # The main trick is described here: + # https://github.com/Dart-Code/Dart-Code/pull/2375 + if: (github.event_name == 'push') || + (github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.repository) || + (github.event_name == 'workflow_dispatch') + + strategy: + fail-fast: false + matrix: + golang: + - 1.13 + runs-on: + - macos-11 + - macos-12 + tarantool: + - brew + - 1.10.14 + + env: + # Make sense only for non-brew jobs. + # + # Set as absolute paths to avoid any possible confusion + # after changing a current directory. + T_VERSION: ${{ matrix.tarantool }} + T_SRCDIR: ${{ format('{0}/tarantool-{1}', github.workspace, matrix.tarantool) }} + T_TARDIR: ${{ format('{0}/tarantool-{1}-build', github.workspace, matrix.tarantool) }} + SRCDIR: ${{ format('{0}/{1}', github.workspace, github.repository) }} + + runs-on: ${{ matrix.runs-on }} + steps: + - name: Clone the connector + uses: actions/checkout@v3 + with: + path: ${{ env.SRCDIR }} + + - name: Restore cache of tarantool ${{ env.T_VERSION }} + uses: actions/cache@v3 + id: cache + with: + path: ${{ env.T_TARDIR }} + key: ${{ matrix.runs-on }}-${{ matrix.tarantool }} + if: matrix.tarantool != 'brew' + + - name: Install latest tarantool from brew + run: brew install tarantool + if: matrix.tarantool == 'brew' + + - name: Install tarantool build dependencies + run: brew install autoconf automake libtool openssl@1.1 + if: matrix.tarantool != 'brew' && steps.cache.outputs.cache-hit != 'true' + + - name: Clone tarantool ${{ env.T_VERSION }} + uses: actions/checkout@v3 + with: + repository: tarantool/tarantool + ref: ${{ env.T_VERSION }} + path: ${{ env.T_TARDIR }} + submodules: true + # fetch-depth is 1 by default and it is okay for + # building from a tag. However we have master in + # the version list. + fetch-depth: 0 + if: matrix.tarantool != 'brew' && steps.cache.outputs.cache-hit != 'true' + + - name: Build tarantool ${{ env.T_VERSION }} from sources + run: | + cd "${T_TARDIR}" + # Set RelWithDebInfo just to disable -Werror. + # + # There are tarantool releases on which AppleClang + # complains about the problem that was fixed later in + # https://github.com/tarantool/tarantool/commit/7e8688ff8885cc7813d12225e03694eb8886de29 + # + # Set OpenSSL root directory for linking tarantool with OpenSSL of version 1.1 + # This is related to #49. There are too much deprecations which affect the build and tests. + # Must be revisited after fixing https://github.com/tarantool/tarantool/issues/6477 + cmake . -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_DIST=ON -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl@1.1 -DOPENSSL_LIBRARIES=/usr/local/opt/openssl@1.1/lib + # {{{ Workaround Mac OS build failure (gh-6076) + # + # https://github.com/tarantool/tarantool/issues/6076 + # + # In brief: when "src/lib/small" is in include paths, + # `#include ` from inside Mac OS SDK headers + # attempts to include "src/lib/small/VERSION" as a + # header file that leads to a syntax error. + # + # It was fixed in the following commits: + # + # * 1.10.10-24-g7bce4abd1 + # * 2.7.2-44-gbb1d32903 + # * 2.8.1-56-ga6c29c5af + # * 2.9.0-84-gc5ae543f3 + # + # However applying the workaround for all versions looks + # harmless. + # + # Added -f just in case: I guess we'll drop this useless + # obsoleted VERSION file from the git repository sooner + # or later. + rm -f src/lib/small/VERSION + # The same as above, but for the VERSION file generated + # by tarantool's CMake script. + rm VERSION + # }}} Workaround Mac OS build failure (gh-6076) + # Continue the build. + make -j$(sysctl -n hw.logicalcpu) + make install + if: matrix.tarantool != 'brew' && steps.cache.outputs.cache-hit != 'true' + + - name: Install tarantool + run: | + cd "${T_TARDIR}" + make install + if: matrix.tarantool != 'brew' && steps.cache.outputs.cache-hit == 'true' + + - name: Verify tarantool version + run: | + # Workaround https://github.com/tarantool/tarantool/issues/4983 + # Workaround https://github.com/tarantool/tarantool/issues/5040 + tarantool -e "require('fiber').sleep(0) assert(_TARANTOOL:startswith('${T_VERSION}'), _TARANTOOL) os.exit()" + if: matrix.tarantool != 'brew' && matrix.tarantool != 'master' + + - name: Setup golang for the connector and tests + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.golang }} + + - name: Install test dependencies + run: | + brew install luarocks + cd "${SRCDIR}" + make deps + + - name: Run regression tests + run: | + cd "${SRCDIR}" + make test + make testrace + + - name: Run regression tests with call_17 + run: | + cd "${SRCDIR}" + make test TAGS="go_tarantool_call_17" + make testrace TAGS="go_tarantool_call_17" + + - name: Run regression tests with msgpack.v5 + run: | + cd "${SRCDIR}" + make test TAGS="go_tarantool_msgpack_v5" + make testrace TAGS="go_tarantool_msgpack_v5" + + - name: Run regression tests with msgpack.v5 and call_17 + run: | + cd "${SRCDIR}" + make test TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" + make testrace TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" + + - name: Run fuzzing tests + if: ${{ matrix.fuzzing }} + run: | + cd "${SRCDIR}" + make fuzzing TAGS="go_tarantool_decimal_fuzzing" + + - name: Check workability of benchmark tests + run: | + cd "${SRCDIR}" + make bench-deps bench DURATION=1x COUNT=1 From 8ae485fb445b88395fa3340e9f4ada726df7fced Mon Sep 17 00:00:00 2001 From: better0fdead Date: Wed, 15 Mar 2023 13:04:48 +0300 Subject: [PATCH 415/605] ci: add workaround for macOs-12 Added workaround for macOs-12 testrace. Without it testrace sends SIGABRT after start. Part of #157 --- .github/workflows/testing.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index f065305eb..b8fde0412 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -373,6 +373,12 @@ jobs: with: go-version: ${{ matrix.golang }} + # Workaround for Mac OS 12 testrace failure + # https://github.com/golang/go/issues/49138 + - name: disable MallocNanoZone for macos-12 + run: echo "MallocNanoZone=0" >> $GITHUB_ENV + if: matrix.runs-on == 'macos-12' + - name: Install test dependencies run: | brew install luarocks From b6bfb4bb63d7c1e6529a6f66ab69bdf91925566b Mon Sep 17 00:00:00 2001 From: better0fdead Date: Mon, 9 Jan 2023 13:46:29 +0300 Subject: [PATCH 416/605] test: fix connection_pool test Changed checkTimeout time in test. The reason is that by default it updates the connects once every 1 second. And it doesn't have time to update for some reason in the case of a test in the same second. So set the update to half the time. Part of #157 --- connection_pool/connection_pool_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 765e63e72..62e55ea3c 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -2210,7 +2210,11 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") - pool, err := connection_pool.Connect(servers, opts) + poolOpts := connection_pool.OptsPool{ + CheckTimeout: 500 * time.Millisecond, + } + pool, err := connection_pool.ConnectWithOpts(servers, opts, poolOpts) + require.Nilf(t, err, "failed to connect") require.NotNilf(t, pool, "conn is nil after Connect") defer pool.Close() @@ -2238,7 +2242,7 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { } else { testMap[addr] = 1 } - case <-time.After(time.Second): + case <-time.After(poolOpts.CheckTimeout * 2): t.Errorf("Failed to get a watch init event.") break } From cf224ce5a77e0826d79a98694a7bac3820879cf2 Mon Sep 17 00:00:00 2001 From: better0fdead Date: Tue, 7 Feb 2023 16:25:39 +0300 Subject: [PATCH 417/605] test: fix TestUtube_Put test Increased sleep duration. Added more checks. Part of #157 --- queue/queue_test.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/queue/queue_test.go b/queue/queue_test.go index 905fefc32..7e59cefa2 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -791,21 +791,32 @@ func TestUtube_Put(t *testing.T) { close(errChan) }() - time.Sleep(100 * time.Millisecond) + time.Sleep(500 * time.Millisecond) // the queue should be blocked for ~2 seconds start := time.Now() t2, err := q.TakeTimeout(2 * time.Second) if err != nil { + <-errChan t.Fatalf("Failed to take task from utube: %s", err) } + + if t2 == nil { + <-errChan + t.Fatalf("Got nil task") + } + if err := t2.Ack(); err != nil { + <-errChan t.Fatalf("Failed to ack task: %s", err) } end := time.Now() if _, ok := <-errChan; ok { t.Fatalf("One of tasks failed") } - if math.Abs(float64(end.Sub(start)-2*time.Second)) > float64(200*time.Millisecond) { + + timeSpent := math.Abs(float64(end.Sub(start) - 2*time.Second)) + + if timeSpent > float64(700*time.Millisecond) { t.Fatalf("Blocking time is less than expected: actual = %.2fs, expected = 1s", end.Sub(start).Seconds()) } } From 4c6be30a9b9acfce169d322b62eb94e673c319b1 Mon Sep 17 00:00:00 2001 From: better0fdead Date: Thu, 16 Mar 2023 12:36:47 +0300 Subject: [PATCH 418/605] ci: bump action version Changed action version from 2 to 3. Part of #157 --- .github/workflows/testing.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index b8fde0412..6bccb2542 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -48,7 +48,7 @@ jobs: steps: - name: Clone the connector - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup tt run: | @@ -89,7 +89,7 @@ jobs: run: echo "/opt/tarantool/bin" >> $GITHUB_PATH - name: Setup golang for the connector and tests - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: go-version: ${{ matrix.golang }} @@ -167,14 +167,14 @@ jobs: - name: Clone the connector # `ref` as merge request is needed for pull_request_target because this # target runs in the context of the base commit of the pull request. - uses: actions/checkout@v2 + uses: actions/checkout@v3 if: github.event_name == 'pull_request_target' with: ref: refs/pull/${{ github.event.pull_request.number }}/merge - name: Clone the connector if: github.event_name != 'pull_request_target' - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Tarantool ${{ matrix.sdk-version }} run: | @@ -184,7 +184,7 @@ jobs: rm -f ${ARCHIVE_NAME} - name: Setup golang for the connector and tests - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: go-version: 1.13 From 8c35168f38a3c1f879541851db48558ab3606169 Mon Sep 17 00:00:00 2001 From: better0fdead Date: Fri, 24 Mar 2023 12:17:46 +0300 Subject: [PATCH 419/605] test: rewrite TestReconnect Rewrote TestReconnect. Instead of just conn.Close() used stop the tarantool instance to avoid auto-reconnect. Restarted it after. Added checks. Part of #157 --- multi/multi_test.go | 47 +++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/multi/multi_test.go b/multi/multi_test.go index ad98912c2..fc165d1d4 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -92,30 +92,49 @@ func TestConnSuccessfully(t *testing.T) { } func TestReconnect(t *testing.T) { - multiConn, _ := Connect([]string{server1, server2}, connOpts) + sleep := 100 * time.Millisecond + sleepCnt := 50 + servers := []string{server1, server2} + multiConn, _ := Connect(servers, connOpts) if multiConn == nil { t.Errorf("conn is nil after Connect") return } - timer := time.NewTimer(300 * time.Millisecond) - <-timer.C - defer multiConn.Close() + test_helpers.StopTarantoolWithCleanup(instances[0]) - conn, _ := multiConn.getConnectionFromPool(server1) - conn.Close() + for i := 0; i < sleepCnt; i++ { + _, ok := multiConn.getConnectionFromPool(servers[0]) + if !ok { + break + } + time.Sleep(sleep) + } + + _, ok := multiConn.getConnectionFromPool(servers[0]) + if ok { + t.Fatalf("failed to close conn") + } - if multiConn.getCurrentConnection().Addr() == server1 { + if multiConn.getCurrentConnection().Addr() == servers[0] { t.Errorf("conn has incorrect addr: %s after disconnect server1", multiConn.getCurrentConnection().Addr()) } - if !multiConn.ConnectedNow() { - t.Errorf("incorrect multiConn status after reconnecting") + + err := test_helpers.RestartTarantool(&instances[0]) + if err != nil { + t.Fatalf("failed to restart Tarantool: %s", err) } - timer = time.NewTimer(100 * time.Millisecond) - <-timer.C - conn, _ = multiConn.getConnectionFromPool(server1) - if !conn.ConnectedNow() { - t.Errorf("incorrect conn status after reconnecting") + for i := 0; i < sleepCnt; i++ { + _, ok := multiConn.getConnectionFromPool(servers[0]) + if ok { + break + } + time.Sleep(sleep) + } + + _, ok = multiConn.getConnectionFromPool(servers[0]) + if !ok { + t.Fatalf("incorrect conn status after reconnecting") } } From 888d74defea885dcbbef67fc1097a746561572fd Mon Sep 17 00:00:00 2001 From: better0fdead Date: Fri, 24 Mar 2023 13:41:54 +0300 Subject: [PATCH 420/605] test: fix TestTtlQueue Changed sleep duration time in test. The reason is that ttl time is set to 5 seconds in test. And it doesn't have time for some reason in the case of a test in the same time. So set the sleep duration to 10 seconds. Part of #157 --- queue/queue_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queue/queue_test.go b/queue/queue_test.go index 7e59cefa2..0a3d4e919 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -687,7 +687,7 @@ func TestTtlQueue(t *testing.T) { } } - time.Sleep(5 * time.Second) + time.Sleep(10 * time.Second) //Take task, err = q.TakeTimeout(2 * time.Second) From 7d426095621469e7d5c65db789f05e4e3d33d412 Mon Sep 17 00:00:00 2001 From: Kiswono Prayogo Date: Tue, 25 Apr 2023 21:41:29 +0700 Subject: [PATCH 421/605] api: add missing iterator constant Closes #285 --- const.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/const.go b/const.go index 29bcc101b..4b6458645 100644 --- a/const.go +++ b/const.go @@ -68,16 +68,18 @@ const ( // https://github.com/fl00r/go-tarantool-1.6/issues/2 - IterEq = uint32(0) // key == x ASC order - IterReq = uint32(1) // key == x DESC order - IterAll = uint32(2) // all tuples - IterLt = uint32(3) // key < x - IterLe = uint32(4) // key <= x - IterGe = uint32(5) // key >= x - IterGt = uint32(6) // key > x - IterBitsAllSet = uint32(7) // all bits from x are set in key - IterBitsAnySet = uint32(8) // at least one x's bit is set - IterBitsAllNotSet = uint32(9) // all bits are not set + IterEq = uint32(0) // key == x ASC order + IterReq = uint32(1) // key == x DESC order + IterAll = uint32(2) // all tuples + IterLt = uint32(3) // key < x + IterLe = uint32(4) // key <= x + IterGe = uint32(5) // key >= x + IterGt = uint32(6) // key > x + IterBitsAllSet = uint32(7) // all bits from x are set in key + IterBitsAnySet = uint32(8) // at least one x's bit is set + IterBitsAllNotSet = uint32(9) // all bits are not set + IterOverlaps = uint32(10) // key overlaps x + IterNeighbor = uint32(11) // tuples in distance ascending order from specified point RLimitDrop = 1 RLimitWait = 2 From 4871044a28e343172119af58b12230f40b06108f Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 25 Apr 2023 17:21:28 +0300 Subject: [PATCH 422/605] ci: fix Tarantool master installation The semantics of the `tt install` command have been updated [1]. A default installation path have been updated too. 1. https://github.com/tarantool/tt/commit/5bec7c1c0f1ce697ea457f8aa06ec6d151ea5e94 --- .github/workflows/testing.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 6bccb2542..7d90fa892 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -73,20 +73,18 @@ jobs: id: cache-latest uses: actions/cache@v3 with: - path: "/opt/tarantool" + path: "${GITHUB_WORKSPACE}/bin" key: cache-latest-${{ env.LATEST_COMMIT }} - name: Setup Tarantool 2.x latest if: matrix.tarantool == '2.x-latest' && steps.cache-latest.outputs.cache-hit != 'true' run: | - # mkdir could be removed after: - # https://github.com/tarantool/tt/issues/282 - sudo mkdir -p /opt/tarantool - sudo tt install tarantool=master + tt init + sudo tt install tarantool master - name: Add Tarantool 2.x latest to PATH if: matrix.tarantool == '2.x-latest' - run: echo "/opt/tarantool/bin" >> $GITHUB_PATH + run: echo "${GITHUB_WORKSPACE}/bin" >> $GITHUB_PATH - name: Setup golang for the connector and tests uses: actions/setup-go@v3 From 846af750a6eed40b1f418ca97b44998036743e1d Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 10 May 2023 15:15:19 +0300 Subject: [PATCH 423/605] test: fix data race in NewWatcher test Goroutines could set a value to `ret` shared variable without protection. The shared variable has been replaced with a channel. Part of #284 --- tarantool_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tarantool_test.go b/tarantool_test.go index 125642dcf..3c2181f2e 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3835,7 +3835,7 @@ func TestConnection_NewWatcher_concurrent(t *testing.T) { var wg sync.WaitGroup wg.Add(testConcurrency) - var ret error + errors := make(chan error, testConcurrency) for i := 0; i < testConcurrency; i++ { go func(i int) { defer wg.Done() @@ -3846,21 +3846,22 @@ func TestConnection_NewWatcher_concurrent(t *testing.T) { close(events) }) if err != nil { - ret = err + errors <- err } else { select { case <-events: case <-time.After(time.Second): - ret = fmt.Errorf("Unable to get an event %d", i) + errors <- fmt.Errorf("Unable to get an event %d", i) } watcher.Unregister() } }(i) } wg.Wait() + close(errors) - if ret != nil { - t.Fatalf("An error found: %s", ret) + for err := range errors { + t.Errorf("An error found: %s", err) } } From ed5028c5282829182bd3ba1b019a558a318a7d09 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 10 May 2023 17:28:49 +0300 Subject: [PATCH 424/605] internal: finish an async request after it queued The change helps to make the order of execution and sending of async requests the same. As example: conn.Do(AsyncRequest1).Get() conn.Do(AsyncRequest2).Get() It is now guaranteed that AsyncRequest2 will be sent to the network after AsyncRequest1. Before the patch the order was undefined. Part of #284 --- connection.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/connection.go b/connection.go index 7da3f26d0..47505dfa7 100644 --- a/connection.go +++ b/connection.go @@ -1066,6 +1066,10 @@ func (conn *Connection) putFuture(fut *Future, req Request, streamId uint64) { } shard.bufmut.Unlock() + if firstWritten { + conn.dirtyShard <- shardn + } + if req.Async() { if fut = conn.fetchFuture(reqid); fut != nil { resp := &Response{ @@ -1076,10 +1080,6 @@ func (conn *Connection) putFuture(fut *Future, req Request, streamId uint64) { conn.markDone(fut) } } - - if firstWritten { - conn.dirtyShard <- shardn - } } func (conn *Connection) markDone(fut *Future) { From 4b8254f5d4603c2821c4e6e8a5a84832864c979b Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 10 May 2023 16:49:12 +0300 Subject: [PATCH 425/605] bugfix: watcher events loss Watchers may behave incorrectly if a request timeout is too small. A re-IPROTO_WATCH request may be not send to a server. It could lead to loss of events stream. It also could lead to a lost IPROTO_UNREGISTER request, but a user won't see the problem. Closes #284 --- CHANGELOG.md | 1 + connection.go | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24e7c5f65..80fc40f5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Several non-critical data race issues (#218) - ConnectionPool does not properly handle disconnection with Opts.Reconnect set (#272) +- Watcher events loss with a small per-request timeout (#284) ## [1.10.0] - 2022-12-31 diff --git a/connection.go b/connection.go index 47505dfa7..65d21365c 100644 --- a/connection.go +++ b/connection.go @@ -1456,10 +1456,13 @@ func (conn *Connection) newWatcherImpl(key string, callback WatchCallback) (Watc st <- state if sendAck { - conn.Do(newWatchRequest(key)).Get() // We expect a reconnect and re-subscribe if it fails to // send the watch request. So it looks ok do not check a - // result. + // result. But we need to make sure that the re-watch + // request will not be finished by a small per-request + // timeout. + req := newWatchRequest(key).Context(context.Background()) + conn.Do(req).Get() } } @@ -1477,7 +1480,12 @@ func (conn *Connection) newWatcherImpl(key string, callback WatchCallback) (Watc if !conn.ClosedNow() { // conn.ClosedNow() check is a workaround for calling // Unregister from connectionClose(). - conn.Do(newUnwatchRequest(key)).Get() + // + // We need to make sure that the unwatch request will + // not be finished by a small per-request timeout to + // avoid lost of the request. + req := newUnwatchRequest(key).Context(context.Background()) + conn.Do(req).Get() } conn.watchMap.Delete(key) close(state.unready) From d2952f4f4db216f16dbb6c003823844ccda2f48c Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 11 May 2023 15:11:46 +0300 Subject: [PATCH 426/605] bugfix: Connect() panics on schema update Part of #278 --- CHANGELOG.md | 1 + config.lua | 17 +++++++++++++++++ schema.go | 9 +++++++-- tarantool_test.go | 23 +++++++++++++++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80fc40f5a..efa4bcc89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - ConnectionPool does not properly handle disconnection with Opts.Reconnect set (#272) - Watcher events loss with a small per-request timeout (#284) +- Connect() panics on concurrent schema update (#278) ## [1.10.0] - 2022-12-31 diff --git a/config.lua b/config.lua index eadfb3825..aafb98ceb 100644 --- a/config.lua +++ b/config.lua @@ -181,6 +181,23 @@ local function push_func(cnt) end rawset(_G, 'push_func', push_func) +local function create_spaces() + for i=1,10 do + local s = box.schema.space.create('test' .. tostring(i), { + id = 700 + i, + if_not_exists = true, + }) + local idx = s:create_index('test' .. tostring(i) .. 'primary', { + type = 'tree', + parts = {1, 'uint'}, + if_not_exists = true + }) + idx:drop() + s:drop() + end +end +rawset(_G, 'create_spaces', create_spaces) + local function tarantool_version_at_least(wanted_major, wanted_minor, wanted_patch) -- https://github.com/tarantool/crud/blob/733528be02c1ffa3dacc12c034ee58c9903127fc/test/helper.lua#L316-L337 local major_minor_patch = _TARANTOOL:split('-', 1)[1] diff --git a/schema.go b/schema.go index a182e8b10..87c788fe0 100644 --- a/schema.go +++ b/schema.go @@ -325,8 +325,13 @@ func (conn *Connection) loadSchema() (err error) { return err } for _, index := range indexes { - schema.SpacesById[index.SpaceId].IndexesById[index.Id] = index - schema.SpacesById[index.SpaceId].Indexes[index.Name] = index + spaceId := index.SpaceId + if _, ok := schema.SpacesById[spaceId]; ok { + schema.SpacesById[spaceId].IndexesById[index.Id] = index + schema.SpacesById[spaceId].Indexes[index.Name] = index + } else { + return errors.New("concurrent schema update") + } } conn.lockShards() diff --git a/tarantool_test.go b/tarantool_test.go index 3c2181f2e..8f28392d9 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3894,6 +3894,29 @@ func TestWatcher_Unregister_concurrent(t *testing.T) { wg.Wait() } +func TestConnect_schema_update(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + for i := 0; i < 100; i++ { + fut := conn.Do(NewCallRequest("create_spaces")) + + if conn, err := Connect(server, opts); err != nil { + if err.Error() != "concurrent schema update" { + t.Errorf("unexpected error: %s", err) + } + } else if conn == nil { + t.Errorf("conn is nil") + } else { + conn.Close() + } + + if _, err := fut.Get(); err != nil { + t.Errorf("Failed to call create_spaces: %s", err) + } + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body From 21123d256c37ba685c69e3e3fa433544dd119eea Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 12 May 2023 10:36:36 +0300 Subject: [PATCH 427/605] queue: bump queue module to 1.3.0 Part of #278 --- CHANGELOG.md | 2 ++ Makefile | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efa4bcc89..5f1861883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Changed +- queue module version bumped to 1.3.0 (#278) + ### Fixed - Several non-critical data race issues (#218) diff --git a/Makefile b/Makefile index 2f4dd8d93..21363da4a 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ clean: .PHONY: deps deps: clean - ( cd ./queue/testdata; $(TTCTL) rocks install queue 1.2.1 ) + ( cd ./queue/testdata; $(TTCTL) rocks install queue 1.3.0 ) ( cd ./crud/testdata; $(TTCTL) rocks install crud 1.0.0 ) .PHONY: datetime-timezones From 579dea889b8cd1659f87bd64b41df154b2712519 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 12 May 2023 12:48:10 +0300 Subject: [PATCH 428/605] bugfix: set wrong Ttr by Queue.Cfg() Ttr should be in seconds. Part of #278 --- CHANGELOG.md | 1 + queue/queue.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f1861883..d5fe188b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. set (#272) - Watcher events loss with a small per-request timeout (#284) - Connect() panics on concurrent schema update (#278) +- Wrong Ttr setup by Queue.Cfg() (#278) ## [1.10.0] - 2022-12-31 diff --git a/queue/queue.go b/queue/queue.go index df13f09bb..8d161b033 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -143,7 +143,7 @@ func (opts CfgOpts) toMap() map[string]interface{} { ret := make(map[string]interface{}) ret["in_replicaset"] = opts.InReplicaset if opts.Ttr != 0 { - ret["ttr"] = opts.Ttr + ret["ttr"] = opts.Ttr.Seconds() } return ret } From 5a511ba360d213868f6dfec505646051140b8d7c Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 12 May 2023 15:32:34 +0300 Subject: [PATCH 429/605] bugfix: call queue.cfg() before box.cfg() We don't have stable results if queue.cfg({in_replicaset = true}) is called after box.cfg(). Probably it is expected [1]. 1. https://github.com/tarantool/queue/issues/206 Part of #278 --- queue/example_connection_pool_test.go | 7 +++++++ queue/testdata/pool.lua | 3 +++ 2 files changed, 10 insertions(+) diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go index e41cdc639..5425d6c39 100644 --- a/queue/example_connection_pool_test.go +++ b/queue/example_connection_pool_test.go @@ -61,6 +61,13 @@ func (h *QueueConnectionHandler) Discovered(conn *tarantool.Connection, // Set up a queue module configuration for an instance. q := queue.New(conn, h.name) + + // Set up a queue module configuration for an instance. Ideally, this + // should be done before box.cfg({}) or you need to wait some time + // before start a work. + // + // See: + // https://github.com/tarantool/queue/issues/206 opts := queue.CfgOpts{InReplicaset: true, Ttr: 60 * time.Second} if h.err = q.Cfg(opts); h.err != nil { diff --git a/queue/testdata/pool.lua b/queue/testdata/pool.lua index 1bcc11654..9ca98bbf1 100644 --- a/queue/testdata/pool.lua +++ b/queue/testdata/pool.lua @@ -25,6 +25,9 @@ end local queue = require('queue') rawset(_G, 'queue', queue) +-- queue.cfg({in_replicaset = true}) should be called before box.cfg({}) +-- https://github.com/tarantool/queue/issues/206 +queue.cfg({in_replicaset = true, ttr = 60}) local listen = os.getenv("TEST_TNT_LISTEN") box.cfg{ From 38c675a6ef1a58589531b02dc0925f1d0c6e0afd Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 12 May 2023 10:08:24 +0300 Subject: [PATCH 430/605] test: fix flaky queue/Example_connectionPool We need to wait for an additional events to make the test stable: 1. A ready state for a queue. 2. A success queue configuration on all instances. 3. An available RW instance. 4. A success role switch. Closes #278 --- CHANGELOG.md | 1 + queue/example_connection_pool_test.go | 99 ++++++++++++++++----------- 2 files changed, 61 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5fe188b5..3d59dd69f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Watcher events loss with a small per-request timeout (#284) - Connect() panics on concurrent schema update (#278) - Wrong Ttr setup by Queue.Cfg() (#278) +- Flaky queue/Example_connectionPool (#278) ## [1.10.0] - 2022-12-31 diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go index 5425d6c39..f5eb54155 100644 --- a/queue/example_connection_pool_test.go +++ b/queue/example_connection_pool_test.go @@ -18,12 +18,12 @@ type QueueConnectionHandler struct { name string cfg queue.Cfg - uuid uuid.UUID - registered bool - err error - mutex sync.Mutex - masterUpdated chan struct{} - masterCnt int32 + uuid uuid.UUID + registered bool + err error + mutex sync.Mutex + updated chan struct{} + masterCnt int32 } // QueueConnectionHandler implements the ConnectionHandler interface. @@ -32,9 +32,9 @@ var _ connection_pool.ConnectionHandler = &QueueConnectionHandler{} // NewQueueConnectionHandler creates a QueueConnectionHandler object. func NewQueueConnectionHandler(name string, cfg queue.Cfg) *QueueConnectionHandler { return &QueueConnectionHandler{ - name: name, - cfg: cfg, - masterUpdated: make(chan struct{}, 10), + name: name, + cfg: cfg, + updated: make(chan struct{}, 10), } } @@ -53,15 +53,24 @@ func (h *QueueConnectionHandler) Discovered(conn *tarantool.Connection, } master := role == connection_pool.MasterRole - if master { - defer func() { - h.masterUpdated <- struct{}{} - }() - } - // Set up a queue module configuration for an instance. q := queue.New(conn, h.name) + // Check is queue ready to work. + if state, err := q.State(); err != nil { + h.updated <- struct{}{} + h.err = err + return err + } else if master && state != queue.RunningState { + return fmt.Errorf("queue state is not RUNNING: %d", state) + } else if !master && state != queue.InitState && state != queue.WaitingState { + return fmt.Errorf("queue state is not INIT and not WAITING: %d", state) + } + + defer func() { + h.updated <- struct{}{} + }() + // Set up a queue module configuration for an instance. Ideally, this // should be done before box.cfg({}) or you need to wait some time // before start a work. @@ -79,10 +88,6 @@ func (h *QueueConnectionHandler) Discovered(conn *tarantool.Connection, return nil } - if h.err = q.Create(h.cfg); h.err != nil { - return h.err - } - if !h.registered { // We register a shared session at the first time. if h.uuid, h.err = q.Identify(nil); h.err != nil { @@ -96,6 +101,10 @@ func (h *QueueConnectionHandler) Discovered(conn *tarantool.Connection, } } + if h.err = q.Create(h.cfg); h.err != nil { + return h.err + } + fmt.Printf("Master %s is ready to work!\n", conn.Addr()) atomic.AddInt32(&h.masterCnt, 1) @@ -113,7 +122,7 @@ func (h *QueueConnectionHandler) Deactivated(conn *tarantool.Connection, // Closes closes a QueueConnectionHandler object. func (h *QueueConnectionHandler) Close() { - close(h.masterUpdated) + close(h.updated) } // Example demonstrates how to use the queue package with the connection_pool @@ -162,8 +171,10 @@ func Example_connectionPool() { } defer connPool.Close() - // Wait for a master instance identification in the queue. - <-h.masterUpdated + // Wait for a queue initialization and master instance identification in + // the queue. + <-h.updated + <-h.updated if h.err != nil { fmt.Printf("Unable to identify in the pool: %s", h.err) return @@ -184,14 +195,17 @@ func Example_connectionPool() { // Switch a master instance in the pool. roles := []bool{true, false} - err = test_helpers.SetClusterRO(servers, connOpts, roles) - if err != nil { - fmt.Printf("Unable to set cluster roles: %s", err) - return + for { + err := test_helpers.SetClusterRO(servers, connOpts, roles) + if err == nil { + break + } } - // Wait for a new master instance re-identification. - <-h.masterUpdated + // Wait for a replica instance connection and a new master instance + // re-identification. + <-h.updated + <-h.updated h.mutex.Lock() err = h.err h.mutex.Unlock() @@ -211,17 +225,24 @@ func Example_connectionPool() { time.Sleep(poolOpts.CheckTimeout) } - // Take a data from the new master instance. - task, err := q.Take() - if err != nil { - fmt.Println("Unable to got task:", err) - } else if task == nil { - fmt.Println("task == nil") - } else if task.Data() == nil { - fmt.Println("task.Data() == nil") - } else { - task.Ack() - fmt.Println("Got data:", task.Data()) + for { + // Take a data from the new master instance. + task, err := q.Take() + + if err == connection_pool.ErrNoRwInstance { + // It may be not registered yet by the pool. + continue + } else if err != nil { + fmt.Println("Unable to got task:", err) + } else if task == nil { + fmt.Println("task == nil") + } else if task.Data() == nil { + fmt.Println("task.Data() == nil") + } else { + task.Ack() + fmt.Println("Got data:", task.Data()) + } + break } // Output: From 9bdd26b8e67337b71085d321fde460b9d55a3930 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Sun, 14 May 2023 01:08:02 +0300 Subject: [PATCH 431/605] crud: bump crud module to 1.1.1 Part of #288 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 21363da4a..cc689dae7 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ clean: .PHONY: deps deps: clean ( cd ./queue/testdata; $(TTCTL) rocks install queue 1.3.0 ) - ( cd ./crud/testdata; $(TTCTL) rocks install crud 1.0.0 ) + ( cd ./crud/testdata; $(TTCTL) rocks install crud 1.1.1 ) .PHONY: datetime-timezones datetime-timezones: From 38f635bed6d9f9757436c708406a0dcc8e912a39 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Sun, 14 May 2023 01:02:21 +0300 Subject: [PATCH 432/605] test: fix flaky crud tests An instance was listening on a testing port until the configuration was complete. At the end of the configuration, the port was reopened. As a result, we saw connection loss in tests. Closes #288 --- crud/tarantool_test.go | 45 ++++++++++++++++++++++++++++++++-------- crud/testdata/config.lua | 16 ++++++++++---- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 71f7d62b1..4d7e7cd0e 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -104,6 +104,33 @@ var object = crud.MapObject{ "name": "bla", } +func connect(t testing.TB) *tarantool.Connection { + for i := 0; i < 10; i++ { + conn, err := tarantool.Connect(server, opts) + if err != nil { + t.Fatalf("Failed to connect: %s", err) + } + + ret := struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Result bool + }{} + err = conn.Do(tarantool.NewCall17Request("is_ready")).GetTyped(&ret) + if err != nil { + t.Fatalf("Failed to check is_ready: %s", err) + } + + if ret.Result { + return conn + } + + time.Sleep(time.Second) + } + + t.Fatalf("Failed to wait for a ready state connect.") + return nil +} + var testProcessDataCases = []struct { name string expectedRespLen int @@ -454,7 +481,7 @@ func testCrudRequestCheck(t *testing.T, req tarantool.Request, } func TestCrudGenerateData(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := connect(t) defer conn.Close() for _, testCase := range testGenerateDataCases { @@ -477,7 +504,7 @@ func TestCrudGenerateData(t *testing.T) { } func TestCrudProcessData(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := connect(t) defer conn.Close() for _, testCase := range testProcessDataCases { @@ -527,7 +554,7 @@ func TestUnflattenRows(t *testing.T) { tpls []interface{} ) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := connect(t) defer conn.Close() // Do `replace`. @@ -586,7 +613,7 @@ func TestUnflattenRows(t *testing.T) { } func TestResultWithErr(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := connect(t) defer conn.Close() for _, testCase := range testResultWithErrCases { @@ -601,7 +628,7 @@ func TestResultWithErr(t *testing.T) { } func TestBoolResult(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := connect(t) defer conn.Close() req := crud.MakeTruncateRequest(spaceName).Opts(baseOpts) @@ -624,7 +651,7 @@ func TestBoolResult(t *testing.T) { } func TestNumberResult(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := connect(t) defer conn.Close() req := crud.MakeCountRequest(spaceName).Opts(countOpts) @@ -665,7 +692,7 @@ func TestBaseResult(t *testing.T) { }, } - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := connect(t) defer conn.Close() req := crud.MakeSelectRequest(spaceName).Opts(selectOpts) @@ -708,7 +735,7 @@ func TestManyResult(t *testing.T) { }, } - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := connect(t) defer conn.Close() req := crud.MakeReplaceManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts) @@ -733,7 +760,7 @@ func TestManyResult(t *testing.T) { } func TestStorageInfoResult(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := connect(t) defer conn.Close() req := crud.MakeStorageInfoRequest().Opts(baseOpts) diff --git a/crud/testdata/config.lua b/crud/testdata/config.lua index 9f8b2d5db..4f4db077f 100644 --- a/crud/testdata/config.lua +++ b/crud/testdata/config.lua @@ -59,6 +59,16 @@ s:create_index('bucket_id', { unique = false, }) +local function is_ready_false() + return false +end + +local function is_ready_true() + return true +end + +rawset(_G, 'is_ready', is_ready_false) + -- Setup vshard. _G.vshard = vshard box.once('guest', function() @@ -93,7 +103,5 @@ box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true box.schema.user.grant('test', 'create,read,write,drop,alter', 'space', nil, { if_not_exists = true }) box.schema.user.grant('test', 'create', 'sequence', nil, { if_not_exists = true }) --- Set listen only when every other thing is configured. -box.cfg{ - listen = os.getenv("TEST_TNT_LISTEN"), -} +-- Set is_ready = is_ready_true only when every other thing is configured. +rawset(_G, 'is_ready', is_ready_true) From 0593018bc21f72d7e82482ad06b54e5524eedefc Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 15 May 2023 13:59:41 +0300 Subject: [PATCH 433/605] test: wait for queue cluster We need to wait for a successful role cluster configuration because an instance bootstrap may haven't finished yet. Part of #291 --- queue/queue_test.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/queue/queue_test.go b/queue/queue_test.go index 0a3d4e919..247c1751a 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -938,13 +938,22 @@ func runTestMain(m *testing.M) int { defer test_helpers.StopTarantoolInstances(instances) - roles := []bool{false, true} - connOpts := Opts{ - Timeout: 500 * time.Millisecond, - User: "test", - Pass: "test", + for i := 0; i < 10; i++ { + // We need to skip bootstrap errors and to make sure that cluster is + // configured. + roles := []bool{false, true} + connOpts := Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", + } + + err = test_helpers.SetClusterRO(serversPool, connOpts, roles) + if err == nil { + break + } + time.Sleep(time.Second) } - err = test_helpers.SetClusterRO(serversPool, connOpts, roles) if err != nil { log.Fatalf("Failed to set roles in tarantool pool: %s", err) From 60111b3150a4dc946b96c623a765869d6550b311 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 11 May 2023 13:05:04 +0300 Subject: [PATCH 434/605] test: fix TestGracefulShutdownCloseConcurrent hang We should release the lock even if connect fails. Part of #282 --- shutdown_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/shutdown_test.go b/shutdown_test.go index d9b1db111..105a4dff6 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -418,9 +418,12 @@ func TestGracefulShutdownCloseConcurrent(t *testing.T) { // Do not wait till Tarantool register out watcher, // test everything is ok even on async. - - conn := test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) - defer conn.Close() + conn, err := Connect(shtdnServer, shtdnClntOpts) + if err != nil { + t.Errorf("Failed to connect: %s", err) + } else { + defer conn.Close() + } // Wait till all connections created. srvToStop.Done() From 521c0c34804e93cd2648c3b5c27bc7194d3870d0 Mon Sep 17 00:00:00 2001 From: better0fdead Date: Mon, 10 Apr 2023 13:20:14 +0300 Subject: [PATCH 435/605] test: increase timeout for all tests Increased timeout for all test's server options. Have done it to solve the problem with macOs flaky tests. Closes #277 Closes #282 Closes #291 --- CHANGELOG.md | 1 + connection_pool/connection_pool_test.go | 4 ++-- crud/example_test.go | 2 +- crud/tarantool_test.go | 4 ++-- datetime/datetime_test.go | 4 ++-- decimal/decimal_test.go | 2 +- decimal/example_test.go | 2 +- example_test.go | 22 +++++++++------------- multi/example_test.go | 4 ++-- multi/multi_test.go | 4 ++-- queue/example_connection_pool_test.go | 4 ++-- queue/example_msgpack_test.go | 8 ++++---- queue/queue_test.go | 4 ++-- settings/tarantool_test.go | 4 ++-- shutdown_test.go | 4 ++-- ssl_test.go | 2 +- tarantool_test.go | 6 +++--- uuid/uuid_test.go | 4 ++-- 18 files changed, 41 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d59dd69f..139f02bb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Connect() panics on concurrent schema update (#278) - Wrong Ttr setup by Queue.Cfg() (#278) - Flaky queue/Example_connectionPool (#278) +- Flaky queue/Example_simpleQueueCustomMsgPack (#277) ## [1.10.0] - 2022-12-31 diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 62e55ea3c..e6738c2a1 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -33,7 +33,7 @@ var servers = []string{ } var connOpts = tarantool.Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 5 * time.Second, User: "test", Pass: "test", } @@ -2426,7 +2426,7 @@ func TestWatcher_Unregister_concurrent(t *testing.T) { func runTestMain(m *testing.M) int { initScript := "config.lua" waitStart := 100 * time.Millisecond - connectRetry := 3 + connectRetry := 10 retryTimeout := 500 * time.Millisecond // Tarantool supports streams and interactive transactions since version 2.10.0 diff --git a/crud/example_test.go b/crud/example_test.go index 3f2ebbf88..2b80212ca 100644 --- a/crud/example_test.go +++ b/crud/example_test.go @@ -15,7 +15,7 @@ const ( ) var exampleOpts = tarantool.Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 5 * time.Second, User: "test", Pass: "test", } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 4d7e7cd0e..0787c99e5 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -20,7 +20,7 @@ var invalidSpaceName = "invalid" var indexNo = uint32(0) var indexName = "primary_index" var opts = tarantool.Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 5 * time.Second, User: "test", Pass: "test", } @@ -31,7 +31,7 @@ var startOpts test_helpers.StartOpts = test_helpers.StartOpts{ User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, - ConnectRetry: 3, + ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, } diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index f4cc8b2a1..f2d1a37c7 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -37,7 +37,7 @@ var isDatetimeSupported = false var server = "127.0.0.1:3013" var opts = Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 5 * time.Second, User: "test", Pass: "test", } @@ -1144,7 +1144,7 @@ func runTestMain(m *testing.M) int { User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, - ConnectRetry: 3, + ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, }) defer test_helpers.StopTarantoolWithCleanup(instance) diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index d81e6c488..b4e95ebe1 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -616,7 +616,7 @@ func runTestMain(m *testing.M) int { User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, - ConnectRetry: 3, + ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, }) defer test_helpers.StopTarantoolWithCleanup(instance) diff --git a/decimal/example_test.go b/decimal/example_test.go index 1d335a4c3..346419125 100644 --- a/decimal/example_test.go +++ b/decimal/example_test.go @@ -22,7 +22,7 @@ import ( func Example() { server := "127.0.0.1:3013" opts := tarantool.Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 5 * time.Second, Reconnect: 1 * time.Second, MaxReconnects: 3, User: "test", diff --git a/example_test.go b/example_test.go index 7b0ee9a6a..7d16893e1 100644 --- a/example_test.go +++ b/example_test.go @@ -799,7 +799,7 @@ func ExampleConnection_Eval() { func ExampleConnect() { conn, err := tarantool.Connect("127.0.0.1:3013", tarantool.Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 5 * time.Second, User: "test", Pass: "test", Concurrency: 32, @@ -895,11 +895,9 @@ func ExampleConnection_Execute() { } server := "127.0.0.1:3013" opts := tarantool.Opts{ - Timeout: 500 * time.Millisecond, - Reconnect: 1 * time.Second, - MaxReconnects: 3, - User: "test", - Pass: "test", + Timeout: 5 * time.Second, + User: "test", + Pass: "test", } client, err := tarantool.Connect(server, opts) if err != nil { @@ -1015,11 +1013,9 @@ func ExampleConnection_NewPrepared() { server := "127.0.0.1:3013" opts := tarantool.Opts{ - Timeout: 500 * time.Millisecond, - Reconnect: 1 * time.Second, - MaxReconnects: 3, - User: "test", - Pass: "test", + Timeout: 5 * time.Second, + User: "test", + Pass: "test", } conn, err := tarantool.Connect(server, opts) if err != nil { @@ -1057,8 +1053,8 @@ func ExampleConnection_NewWatcher() { server := "127.0.0.1:3013" opts := tarantool.Opts{ - Timeout: 500 * time.Millisecond, - Reconnect: 1 * time.Second, + Timeout: 5 * time.Second, + Reconnect: 5 * time.Second, MaxReconnects: 3, User: "test", Pass: "test", diff --git a/multi/example_test.go b/multi/example_test.go index e43461141..1ef369e4c 100644 --- a/multi/example_test.go +++ b/multi/example_test.go @@ -9,7 +9,7 @@ import ( func ExampleConnect() { multiConn, err := Connect([]string{"127.0.0.1:3031", "127.0.0.1:3032"}, tarantool.Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 5 * time.Second, User: "test", Pass: "test", }) @@ -21,7 +21,7 @@ func ExampleConnect() { func ExampleConnectWithOpts() { multiConn, err := ConnectWithOpts([]string{"127.0.0.1:3301", "127.0.0.1:3302"}, tarantool.Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 5 * time.Second, User: "test", Pass: "test", }, OptsMulti{ diff --git a/multi/multi_test.go b/multi/multi_test.go index fc165d1d4..88db98321 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -20,7 +20,7 @@ var spaceNo = uint32(617) var spaceName = "test" var indexNo = uint32(0) var connOpts = tarantool.Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 5 * time.Second, User: "test", Pass: "test", } @@ -611,7 +611,7 @@ func TestConnectionMulti_NewWatcher(t *testing.T) { func runTestMain(m *testing.M) int { initScript := "config.lua" waitStart := 100 * time.Millisecond - connectRetry := 3 + connectRetry := 10 retryTimeout := 500 * time.Millisecond // Tarantool supports streams and interactive transactions since version 2.10.0 diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go index f5eb54155..7e80110fd 100644 --- a/queue/example_connection_pool_test.go +++ b/queue/example_connection_pool_test.go @@ -156,12 +156,12 @@ func Example_connectionPool() { "127.0.0.1:3015", } connOpts := tarantool.Opts{ - Timeout: 1 * time.Second, + Timeout: 5 * time.Second, User: "test", Pass: "test", } poolOpts := connection_pool.OptsPool{ - CheckTimeout: 1 * time.Second, + CheckTimeout: 5 * time.Second, ConnectionHandler: h, } connPool, err := connection_pool.ConnectWithOpts(servers, connOpts, poolOpts) diff --git a/queue/example_msgpack_test.go b/queue/example_msgpack_test.go index 2ed7f5542..5b179f5c0 100644 --- a/queue/example_msgpack_test.go +++ b/queue/example_msgpack_test.go @@ -48,7 +48,7 @@ func (c *dummyData) EncodeMsgpack(e *encoder) error { func Example_simpleQueueCustomMsgPack() { opts := tarantool.Opts{ Reconnect: time.Second, - Timeout: 2500 * time.Millisecond, + Timeout: 5 * time.Second, MaxReconnects: 5, User: "test", Pass: "test", @@ -65,9 +65,9 @@ func Example_simpleQueueCustomMsgPack() { IfNotExists: true, Kind: queue.FIFO, Opts: queue.Opts{ - Ttl: 10 * time.Second, - Ttr: 5 * time.Second, - Delay: 3 * time.Second, + Ttl: 20 * time.Second, + Ttr: 10 * time.Second, + Delay: 6 * time.Second, Pri: 1, }, } diff --git a/queue/queue_test.go b/queue/queue_test.go index 247c1751a..ffbf8feb5 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -22,7 +22,7 @@ var serversPool = []string{ var instances []test_helpers.TarantoolInstance var opts = Opts{ - Timeout: 2500 * time.Millisecond, + Timeout: 5 * time.Second, User: "test", Pass: "test", //Concurrency: 32, @@ -913,7 +913,7 @@ func runTestMain(m *testing.M) int { User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, - ConnectRetry: 3, + ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, }) diff --git a/settings/tarantool_test.go b/settings/tarantool_test.go index d32767116..4bfa5cf37 100644 --- a/settings/tarantool_test.go +++ b/settings/tarantool_test.go @@ -19,7 +19,7 @@ var isSettingsSupported = false var server = "127.0.0.1:3013" var opts = tarantool.Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 5 * time.Second, User: "test", Pass: "test", } @@ -637,7 +637,7 @@ func runTestMain(m *testing.M) int { User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, - ConnectRetry: 3, + ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, }) diff --git a/shutdown_test.go b/shutdown_test.go index 105a4dff6..fe268b41e 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -22,7 +22,7 @@ var shtdnClntOpts = Opts{ User: opts.User, Pass: opts.Pass, Timeout: 20 * time.Second, - Reconnect: 200 * time.Millisecond, + Reconnect: 500 * time.Millisecond, MaxReconnects: 10, RequiredProtocolInfo: ProtocolInfo{Features: []ProtocolFeature{WatchersFeature}}, } @@ -32,7 +32,7 @@ var shtdnSrvOpts = test_helpers.StartOpts{ User: shtdnClntOpts.User, Pass: shtdnClntOpts.Pass, WaitStart: 100 * time.Millisecond, - ConnectRetry: 3, + ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, } diff --git a/ssl_test.go b/ssl_test.go index ca0773b58..0a62d03c3 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -130,7 +130,7 @@ func serverTnt(serverOpts, clientOpts SslOpts, auth Auth) (test_helpers.Tarantoo User: "test", Pass: "test", WaitStart: 100 * time.Millisecond, - ConnectRetry: 3, + ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, }) } diff --git a/tarantool_test.go b/tarantool_test.go index 8f28392d9..a351850ae 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -28,7 +28,7 @@ var startOpts test_helpers.StartOpts = test_helpers.StartOpts{ User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, - ConnectRetry: 3, + ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, } @@ -75,7 +75,7 @@ var spaceName = "test" var indexNo = uint32(0) var indexName = "primary" var opts = Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 5 * time.Second, User: "test", Pass: "test", //Concurrency: 32, @@ -3580,7 +3580,7 @@ func TestConnection_NewWatcher_reconnect(t *testing.T) { User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, - ConnectRetry: 3, + ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, }) defer test_helpers.StopTarantoolWithCleanup(inst) diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index e04ab5ab9..f5a596e76 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -20,7 +20,7 @@ var isUUIDSupported = false var server = "127.0.0.1:3013" var opts = Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 5 * time.Second, User: "test", Pass: "test", } @@ -158,7 +158,7 @@ func runTestMain(m *testing.M) int { User: opts.User, Pass: opts.Pass, WaitStart: 100 * time.Millisecond, - ConnectRetry: 3, + ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, }) defer test_helpers.StopTarantoolWithCleanup(inst) From cdfcddffafc5beafe300a51b18edbb978ed2a2ec Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 24 Mar 2023 16:49:58 +0300 Subject: [PATCH 436/605] Release 1.11.0 Overview The release adds pagination support and wrappers for the crud module. Breaking changes There are no breaking changes in the release. New features Support pagination (#246). A Makefile target to test with race detector (#218). Support CRUD API (#108). An ability to replace a base network connection to a Tarantool instance (#265). Missed iterator constant (#285). Bugfixes Several non-critical data race issues (#218). Build on Apple M1 with OpenSSL (#260). ConnectionPool does not properly handle disconnection with Opts.Reconnect set (#272). Watcher events loss with a small per-request timeout (#284). Connect() panics on concurrent schema update (#278). Wrong Ttr setup by Queue.Cfg() (#278). Flaky queue/Example_connectionPool (#278). Flaky queue/Example_simpleQueueCustomMsgPack (#277). Other queue module version bumped to 1.3.0 (#278). --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 139f02bb9..b23c6bc69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,23 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +### Changed + +### Fixed + +## [1.11.0] - 2023-05-18 + +The release adds pagination support and wrappers for the +[crud](https://github.com/tarantool/crud) module. + +### Added + - Support pagination (#246) - A Makefile target to test with race detector (#218) - Support CRUD API (#108) - An ability to replace a base network connection to a Tarantool instance (#265) +- Missed iterator constant (#285) ### Changed @@ -23,6 +35,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed - Several non-critical data race issues (#218) +- Build on Apple M1 with OpenSSL (#260) - ConnectionPool does not properly handle disconnection with Opts.Reconnect set (#272) - Watcher events loss with a small per-request timeout (#284) From ab957c466e8a14d4f7fe935d3b4a5db17736bd37 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 18 May 2023 16:21:42 +0300 Subject: [PATCH 437/605] test: fix compatibility with Tarantool master We need use `box.info.replication.uuid` instead of `box.info.cluster.uuid` to support Tarantool 3.0. Closes #293 --- CHANGELOG.md | 2 ++ crud/testdata/config.lua | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b23c6bc69..ca54b0000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +- crud tests with Tarantool 3.0 (#293) + ## [1.11.0] - 2023-05-18 The release adds pagination support and wrappers for the diff --git a/crud/testdata/config.lua b/crud/testdata/config.lua index 4f4db077f..81f486b38 100644 --- a/crud/testdata/config.lua +++ b/crud/testdata/config.lua @@ -75,12 +75,22 @@ box.once('guest', function() box.schema.user.grant('guest', 'super') end) local uri = 'guest@127.0.0.1:3013' +local box_info = box.info() + +local replicaset_uuid +if box_info.replicaset then + -- Since Tarantool 3.0. + replicaset_uuid = box_info.replicaset.uuid +else + replicaset_uuid = box_info.cluster.uuid +end + local cfg = { bucket_count = 300, sharding = { - [box.info().cluster.uuid] = { + [replicaset_uuid] = { replicas = { - [box.info().uuid] = { + [box_info.uuid] = { uri = uri, name = 'storage', master = true, @@ -89,7 +99,7 @@ local cfg = { }, }, } -vshard.storage.cfg(cfg, box.info().uuid) +vshard.storage.cfg(cfg, box_info.uuid) vshard.router.cfg(cfg) vshard.router.bootstrap() From d6ede3178c887b54914aa6899593e866d7bfae27 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 24 May 2023 22:09:50 +0300 Subject: [PATCH 438/605] test: fix compatibility with Tarantool master New behavior: SELECT scan queries are only allowed if the SEQSCAN keyword is used correctly[1]. 1. https://www.tarantool.io/en/doc/latest/reference/reference_lua/compat/sql_seq_scan_default/ Related to https://github.com/tarantool/tarantool/pull/8602 --- CHANGELOG.md | 1 + settings/tarantool_test.go | 11 +++++++++-- tarantool_test.go | 12 ++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca54b0000..58b390aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed - crud tests with Tarantool 3.0 (#293) +- SQL tests with Tarantool 3.0 (#295) ## [1.11.0] - 2023-05-18 diff --git a/settings/tarantool_test.go b/settings/tarantool_test.go index 4bfa5cf37..054d5052a 100644 --- a/settings/tarantool_test.go +++ b/settings/tarantool_test.go @@ -481,7 +481,14 @@ func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", false}}, resp.Data) // Select multiple records. - resp, err = conn.Execute("SELECT * FROM data;", []interface{}{}) + query := "SELECT * FROM seqscan data;" + if isSeqScanOld, err := test_helpers.IsTarantoolVersionLess(3, 0, 0); err != nil { + t.Fatal("Could not check the Tarantool version") + } else if isSeqScanOld { + query = "SELECT * FROM data;" + } + + resp, err = conn.Execute(query, []interface{}{}) require.Nil(t, err) require.NotNil(t, resp) require.EqualValues(t, []interface{}{"1"}, resp.Data[0]) @@ -500,7 +507,7 @@ func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", true}}, resp.Data) // Select multiple records. - resp, err = conn.Execute("SELECT * FROM data;", []interface{}{}) + resp, err = conn.Execute(query, []interface{}{}) require.Nil(t, err) require.NotNil(t, resp) require.EqualValues(t, []interface{}{"2"}, resp.Data[0]) diff --git a/tarantool_test.go b/tarantool_test.go index a351850ae..39e9c3511 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -1330,7 +1330,8 @@ const ( selectPosQuery = "SELECT id, name FROM SQL_SPACE WHERE id=? AND name=?;" updateQuery = "UPDATE SQL_SPACE SET name=? WHERE id=?;" enableFullMetaDataQuery = "SET SESSION \"sql_full_metadata\" = true;" - selectSpanDifQuery = "SELECT id||id, name, id FROM SQL_SPACE WHERE name=?;" + selectSpanDifQueryNew = "SELECT id||id, name, id FROM seqscan SQL_SPACE WHERE name=?;" + selectSpanDifQueryOld = "SELECT id||id, name, id FROM SQL_SPACE WHERE name=?;" alterTableQuery = "ALTER TABLE SQL_SPACE RENAME TO SQL_SPACE2;" insertIncrQuery = "INSERT INTO SQL_SPACE2 VALUES (?, ?);" deleteQuery = "DELETE FROM SQL_SPACE2 WHERE name=?;" @@ -1353,6 +1354,13 @@ func TestSQL(t *testing.T) { Resp Response } + selectSpanDifQuery := selectSpanDifQueryNew + if isSeqScanOld, err := test_helpers.IsTarantoolVersionLess(3, 0, 0); err != nil { + t.Fatal("Could not check the Tarantool version") + } else if isSeqScanOld { + selectSpanDifQuery = selectSpanDifQueryOld + } + testCases := []testCase{ { createTableQuery, @@ -1498,7 +1506,7 @@ func TestSQL(t *testing.T) { for i, test := range testCases { resp, err := conn.Execute(test.Query, test.Args) - assert.NoError(t, err, "Failed to Execute, Query number: %d", i) + assert.NoError(t, err, "Failed to Execute, query: %s", test.Query) assert.NotNil(t, resp, "Response is nil after Execute\nQuery number: %d", i) for j := range resp.Data { assert.Equal(t, resp.Data[j], test.Resp.Data[j], "Response data is wrong") From c7c577ec5ba9ebb14141306bf681c5079f725ff1 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 24 May 2023 17:34:27 +0300 Subject: [PATCH 439/605] godoc: add a note about Shutdown state We forgot to update a comment about the state. Part of #257 --- connection.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/connection.go b/connection.go index 65d21365c..4a675e6db 100644 --- a/connection.go +++ b/connection.go @@ -37,10 +37,10 @@ const ( Disconnected // ReconnectFailed signals that attempt to reconnect has failed. ReconnectFailed - // Either reconnect attempts exhausted, or explicit Close is called. - Closed // Shutdown signals that shutdown callback is processing. Shutdown + // Either reconnect attempts exhausted, or explicit Close is called. + Closed // LogReconnectFailed is logged when reconnect attempt failed. LogReconnectFailed ConnLogKind = iota + 1 @@ -109,6 +109,11 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac // - In "Disconnected" state it rejects queries with ClientError{Code: // ErrConnectionNotReady} // +// - In "Shutdown" state it rejects queries with ClientError{Code: +// ErrConnectionShutdown}. The state indicates that a graceful shutdown +// in progress. The connection waits for all active requests to +// complete. +// // - In "Closed" state it rejects queries with ClientError{Code: // ErrConnectionClosed}. Connection could become "Closed" when // Connection.Close() method called, or when Tarantool disconnected and From dccf688f013d30ef3d3bd0eb2d7fbfc2d7329e08 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 25 May 2023 10:53:38 +0300 Subject: [PATCH 440/605] api: add Connection.CloseGraceful() CloseGraceful closes Connection gracefully. Unlike Connection.Close() it waits for all requests to complete. Part of #257 --- CHANGELOG.md | 3 +++ connection.go | 44 +++++++++++++++++++++++++++++++++----------- example_test.go | 36 ++++++++++++++++++++++++++++++++++++ shutdown_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58b390aed..228a27c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +- Connection.CloseGraceful() unlike Connection.Close() waits for all + requests to complete (#257) + ### Changed ### Fixed diff --git a/connection.go b/connection.go index 4a675e6db..055941e78 100644 --- a/connection.go +++ b/connection.go @@ -462,6 +462,13 @@ func (conn *Connection) Close() error { return conn.closeConnection(err, true) } +// CloseGraceful closes Connection gracefully. It waits for all requests to +// complete. +// After this method called, there is no way to reopen this Connection. +func (conn *Connection) CloseGraceful() error { + return conn.shutdown(true) +} + // Addr returns a configured address of Tarantool socket. func (conn *Connection) Addr() string { return conn.addr @@ -1532,17 +1539,27 @@ func shutdownEventCallback(event WatchEvent) { // step 2. val, ok := event.Value.(bool) if ok && val { - go event.Conn.shutdown() + go event.Conn.shutdown(false) } } -func (conn *Connection) shutdown() { +func (conn *Connection) shutdown(forever bool) error { // Forbid state changes. conn.mutex.Lock() defer conn.mutex.Unlock() if !atomic.CompareAndSwapUint32(&conn.state, connConnected, connShutdown) { - return + if forever { + err := ClientError{ErrConnectionClosed, "connection closed by client"} + return conn.closeConnection(err, true) + } + return nil + } + + if forever { + // We don't want to reconnect any more. + conn.opts.Reconnect = 0 + conn.opts.MaxReconnects = 0 } conn.cond.Broadcast() @@ -1551,7 +1568,7 @@ func (conn *Connection) shutdown() { c := conn.c for { if (atomic.LoadUint32(&conn.state) != connShutdown) || (c != conn.c) { - return + return nil } if atomic.LoadInt64(&conn.requestCnt) == 0 { break @@ -1563,14 +1580,19 @@ func (conn *Connection) shutdown() { conn.cond.Wait() } - // Start to reconnect based on common rules, same as in net.box. - // Reconnect also closes the connection: server waits until all - // subscribed connections are terminated. - // See https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/ - // step 3. - conn.reconnectImpl( - ClientError{ + if forever { + err := ClientError{ErrConnectionClosed, "connection closed by client"} + return conn.closeConnection(err, true) + } else { + // Start to reconnect based on common rules, same as in net.box. + // Reconnect also closes the connection: server waits until all + // subscribed connections are terminated. + // See https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/ + // step 3. + conn.reconnectImpl(ClientError{ ErrConnectionClosed, "connection closed after server shutdown", }, conn.c) + return nil + } } diff --git a/example_test.go b/example_test.go index 7d16893e1..e608d0f42 100644 --- a/example_test.go +++ b/example_test.go @@ -1115,3 +1115,39 @@ func ExamplePingRequest_Context() { // Ping Resp // Ping Error context is done } + +// ExampleConnection_CloseGraceful_force demonstrates how to force close +// a connection with graceful close in progress after a while. +func ExampleConnection_CloseGraceful_force() { + conn := example_connect(opts) + + eval := `local fiber = require('fiber') + local time = ... + fiber.sleep(time) +` + req := tarantool.NewEvalRequest(eval).Args([]interface{}{10}) + fut := conn.Do(req) + + done := make(chan struct{}) + go func() { + conn.CloseGraceful() + fmt.Println("Connection.CloseGraceful() done!") + close(done) + }() + + select { + case <-done: + case <-time.After(time.Second): + fmt.Println("Force Connection.Close()!") + conn.Close() + } + <-done + + fmt.Println("Result:") + fmt.Println(fut.Get()) + // Output: + // Force Connection.Close()! + // Connection.CloseGraceful() done! + // Result: + // connection closed by client (0x4001) +} diff --git a/shutdown_test.go b/shutdown_test.go index fe268b41e..1b06284c0 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" . "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/test_helpers" @@ -143,6 +144,48 @@ func TestGracefulShutdown(t *testing.T) { testGracefulShutdown(t, conn, &inst) } +func TestCloseGraceful(t *testing.T) { + opts := Opts{ + User: shtdnClntOpts.User, + Pass: shtdnClntOpts.Pass, + Timeout: shtdnClntOpts.Timeout, + } + + inst, err := test_helpers.StartTarantool(shtdnSrvOpts) + require.Nil(t, err) + defer test_helpers.StopTarantoolWithCleanup(inst) + + conn := test_helpers.ConnectWithValidation(t, shtdnServer, opts) + defer conn.Close() + + // Send request with sleep. + evalSleep := 3 // In seconds. + require.Lessf(t, + time.Duration(evalSleep)*time.Second, + shtdnClntOpts.Timeout, + "test request won't be failed by timeout") + + req := NewEvalRequest(evalBody).Args([]interface{}{evalSleep, evalMsg}) + fut := conn.Do(req) + + go func() { + // CloseGraceful closes the connection gracefully. + conn.CloseGraceful() + // Connection is closed. + assert.Equal(t, true, conn.ClosedNow()) + }() + + // Check that a request rejected if graceful shutdown in progress. + time.Sleep((time.Duration(evalSleep) * time.Second) / 2) + _, err = conn.Do(NewPingRequest()).Get() + assert.ErrorContains(t, err, "server shutdown in progress") + + // Check that a previous request was successful. + resp, err := fut.Get() + assert.Nilf(t, err, "sleep request no error") + assert.NotNilf(t, resp, "sleep response exists") +} + func TestGracefulShutdownWithReconnect(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) From 897ba41207ab74f07c8853fe134aedd4550e15e8 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 26 May 2023 17:43:19 +0300 Subject: [PATCH 441/605] api: add ConnectionPool.CloseGraceful() CloseGraceful closes ConnectionPool gracefully. Unlike ConnectionPool.Close() it waits for all requests to complete. Closes #257 --- CHANGELOG.md | 2 + connection_pool/connection_pool.go | 259 +++++++++++++++--------- connection_pool/connection_pool_test.go | 63 +++++- connection_pool/example_test.go | 40 ++++ connection_pool/state.go | 1 + 5 files changed, 267 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 228a27c58..81c4f9341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Connection.CloseGraceful() unlike Connection.Close() waits for all requests to complete (#257) +- ConnectionPool.CloseGraceful() unlike ConnectionPool.Close() waits for all + requests to complete (#257) ### Changed diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 892fea483..f67ac7092 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -87,7 +87,7 @@ Main features: - Automatic master discovery by mode parameter. */ type ConnectionPool struct { - addrs []string + addrs map[string]*endpointState connOpts tarantool.Opts opts OptsPool @@ -102,11 +102,16 @@ type ConnectionPool struct { var _ Pooler = (*ConnectionPool)(nil) -type connState struct { +type endpointState struct { addr string notify chan tarantool.ConnEvent conn *tarantool.Connection role Role + // This is used to switch a connection states. + shutdown chan struct{} + close chan struct{} + closed chan struct{} + closeErr error } // ConnectWithOpts creates pool for instances with addresses addrs @@ -125,7 +130,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co anyPool := NewEmptyRoundRobin(size) connPool = &ConnectionPool{ - addrs: make([]string, 0, len(addrs)), + addrs: make(map[string]*endpointState), connOpts: connOpts.Clone(), opts: opts, state: unknownState, @@ -135,28 +140,20 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co anyPool: anyPool, } - m := make(map[string]bool) for _, addr := range addrs { - if _, ok := m[addr]; !ok { - m[addr] = true - connPool.addrs = append(connPool.addrs, addr) - } + connPool.addrs[addr] = nil } - states, somebodyAlive := connPool.fillPools() + somebodyAlive := connPool.fillPools() if !somebodyAlive { connPool.state.set(closedState) - connPool.closeImpl() - for _, s := range states { - close(s.notify) - } return nil, ErrNoConnection } connPool.state.set(connectedState) - for _, s := range states { - go connPool.checker(s) + for _, s := range connPool.addrs { + go connPool.controller(s) } return connPool, nil @@ -208,44 +205,55 @@ func (connPool *ConnectionPool) ConfiguredTimeout(mode Mode) (time.Duration, err return conn.ConfiguredTimeout(), nil } -func (connPool *ConnectionPool) closeImpl() []error { - errs := make([]error, 0, len(connPool.addrs)) - - for _, addr := range connPool.addrs { - if conn := connPool.anyPool.DeleteConnByAddr(addr); conn != nil { - if err := conn.Close(); err != nil { - errs = append(errs, err) - } - - role := UnknownRole - if conn := connPool.rwPool.DeleteConnByAddr(addr); conn != nil { - role = MasterRole - } else if conn := connPool.roPool.DeleteConnByAddr(addr); conn != nil { - role = ReplicaRole - } - connPool.handlerDeactivated(conn, role) +func (pool *ConnectionPool) waitClose() []error { + errs := make([]error, 0, len(pool.addrs)) + for _, s := range pool.addrs { + <-s.closed + if s.closeErr != nil { + errs = append(errs, s.closeErr) } } - - close(connPool.done) return errs } -// Close closes connections in pool. -func (connPool *ConnectionPool) Close() []error { - if connPool.state.cas(connectedState, closedState) { - connPool.poolsMutex.Lock() - defer connPool.poolsMutex.Unlock() +// Close closes connections in the ConnectionPool. +func (pool *ConnectionPool) Close() []error { + if pool.state.cas(connectedState, closedState) || + pool.state.cas(shutdownState, closedState) { + pool.poolsMutex.Lock() + for _, s := range pool.addrs { + close(s.close) + } + pool.poolsMutex.Unlock() + } - return connPool.closeImpl() + return pool.waitClose() +} + +// CloseGraceful closes connections in the ConnectionPool gracefully. It waits +// for all requests to complete. +func (pool *ConnectionPool) CloseGraceful() []error { + if pool.state.cas(connectedState, shutdownState) { + pool.poolsMutex.Lock() + for _, s := range pool.addrs { + close(s.shutdown) + } + pool.poolsMutex.Unlock() } - return nil + + return pool.waitClose() } // GetAddrs gets addresses of connections in pool. func (connPool *ConnectionPool) GetAddrs() []string { cpy := make([]string, len(connPool.addrs)) - copy(cpy, connPool.addrs) + + i := 0 + for addr := range connPool.addrs { + cpy[i] = addr + i++ + } + return cpy } @@ -260,7 +268,7 @@ func (connPool *ConnectionPool) GetPoolInfo() map[string]*ConnectionInfo { return info } - for _, addr := range connPool.addrs { + for addr := range connPool.addrs { conn, role := connPool.getConnectionFromPool(addr) if conn != nil { info[addr] = &ConnectionInfo{ConnectedNow: conn.ConnectedNow(), ConnRole: role} @@ -886,22 +894,25 @@ func (connPool *ConnectionPool) handlerDeactivated(conn *tarantool.Connection, } } -func (connPool *ConnectionPool) fillPools() ([]connState, bool) { - states := make([]connState, len(connPool.addrs)) +func (connPool *ConnectionPool) fillPools() bool { somebodyAlive := false - // It is called before checker() goroutines and before closeImpl() may be - // called so we don't expect concurrency issues here. - for i, addr := range connPool.addrs { - states[i] = connState{ - addr: addr, - notify: make(chan tarantool.ConnEvent, 10), - conn: nil, - role: UnknownRole, + // It is called before controller() goroutines so we don't expect + // concurrency issues here. + for addr := range connPool.addrs { + state := &endpointState{ + addr: addr, + notify: make(chan tarantool.ConnEvent, 10), + conn: nil, + role: UnknownRole, + shutdown: make(chan struct{}), + close: make(chan struct{}), + closed: make(chan struct{}), } - connOpts := connPool.connOpts - connOpts.Notify = states[i].notify + connPool.addrs[addr] = state + connOpts := connPool.connOpts + connOpts.Notify = state.notify conn, err := tarantool.Connect(addr, connOpts) if err != nil { log.Printf("tarantool: connect to %s failed: %s\n", addr, err.Error()) @@ -920,8 +931,8 @@ func (connPool *ConnectionPool) fillPools() ([]connState, bool) { } if conn.ConnectedNow() { - states[i].conn = conn - states[i].role = role + state.conn = conn + state.role = role somebodyAlive = true } else { connPool.deleteConnection(addr) @@ -934,15 +945,15 @@ func (connPool *ConnectionPool) fillPools() ([]connState, bool) { } } - return states, somebodyAlive + return somebodyAlive } -func (pool *ConnectionPool) updateConnection(s connState) connState { +func (pool *ConnectionPool) updateConnection(s *endpointState) { pool.poolsMutex.Lock() if pool.state.get() != connectedState { pool.poolsMutex.Unlock() - return s + return } if role, err := pool.getConnectionRole(s.conn); err == nil { @@ -956,7 +967,7 @@ func (pool *ConnectionPool) updateConnection(s connState) connState { s.conn.Close() s.conn = nil s.role = UnknownRole - return s + return } pool.poolsMutex.Lock() @@ -967,7 +978,7 @@ func (pool *ConnectionPool) updateConnection(s connState) connState { pool.handlerDeactivated(s.conn, role) s.conn = nil s.role = UnknownRole - return s + return } if pool.addConnection(s.addr, s.conn, role) != nil { @@ -977,12 +988,12 @@ func (pool *ConnectionPool) updateConnection(s connState) connState { pool.handlerDeactivated(s.conn, role) s.conn = nil s.role = UnknownRole - return s + return } s.role = role } pool.poolsMutex.Unlock() - return s + return } else { pool.deleteConnection(s.addr) pool.poolsMutex.Unlock() @@ -991,16 +1002,16 @@ func (pool *ConnectionPool) updateConnection(s connState) connState { pool.handlerDeactivated(s.conn, s.role) s.conn = nil s.role = UnknownRole - return s + return } } -func (pool *ConnectionPool) tryConnect(s connState) connState { +func (pool *ConnectionPool) tryConnect(s *endpointState) { pool.poolsMutex.Lock() if pool.state.get() != connectedState { pool.poolsMutex.Unlock() - return s + return } s.conn = nil @@ -1016,13 +1027,13 @@ func (pool *ConnectionPool) tryConnect(s connState) connState { if err != nil { conn.Close() log.Printf("tarantool: storing connection to %s failed: %s\n", s.addr, err) - return s + return } opened := pool.handlerDiscovered(conn, role) if !opened { conn.Close() - return s + return } pool.poolsMutex.Lock() @@ -1030,29 +1041,28 @@ func (pool *ConnectionPool) tryConnect(s connState) connState { pool.poolsMutex.Unlock() conn.Close() pool.handlerDeactivated(conn, role) - return s + return } if pool.addConnection(s.addr, conn, role) != nil { pool.poolsMutex.Unlock() conn.Close() pool.handlerDeactivated(conn, role) - return s + return } s.conn = conn s.role = role } pool.poolsMutex.Unlock() - return s } -func (pool *ConnectionPool) reconnect(s connState) connState { +func (pool *ConnectionPool) reconnect(s *endpointState) { pool.poolsMutex.Lock() if pool.state.get() != connectedState { pool.poolsMutex.Unlock() - return s + return } pool.deleteConnection(s.addr) @@ -1062,41 +1072,100 @@ func (pool *ConnectionPool) reconnect(s connState) connState { s.conn = nil s.role = UnknownRole - return pool.tryConnect(s) + pool.tryConnect(s) } -func (pool *ConnectionPool) checker(s connState) { +func (pool *ConnectionPool) controller(s *endpointState) { timer := time.NewTicker(pool.opts.CheckTimeout) defer timer.Stop() + shutdown := false for { + if shutdown { + // Graceful shutdown in progress. We need to wait for a finish or + // to force close. + select { + case <-s.closed: + case <-s.close: + } + } + select { - case <-pool.done: - close(s.notify) + case <-s.closed: return - case <-s.notify: - if s.conn != nil && s.conn.ClosedNow() { + default: + } + + select { + // s.close has priority to avoid concurrency with s.shutdown. + case <-s.close: + if s.conn != nil { pool.poolsMutex.Lock() - if pool.state.get() == connectedState { - pool.deleteConnection(s.addr) - pool.poolsMutex.Unlock() + pool.deleteConnection(s.addr) + pool.poolsMutex.Unlock() + + if !shutdown { + s.closeErr = s.conn.Close() pool.handlerDeactivated(s.conn, s.role) - s.conn = nil - s.role = UnknownRole + close(s.closed) } else { - pool.poolsMutex.Unlock() + // Force close the connection. + s.conn.Close() + // And wait for a finish. + <-s.closed } - } - case <-timer.C: - // Reopen connection - // Relocate connection between subpools - // if ro/rw was updated - if s.conn == nil { - s = pool.tryConnect(s) - } else if !s.conn.ClosedNow() { - s = pool.updateConnection(s) } else { - s = pool.reconnect(s) + close(s.closed) + } + default: + select { + case <-s.shutdown: + shutdown = true + if s.conn != nil { + pool.poolsMutex.Lock() + pool.deleteConnection(s.addr) + pool.poolsMutex.Unlock() + + // We need to catch s.close in the current goroutine, so + // we need to start an another one for the shutdown. + go func() { + s.closeErr = s.conn.CloseGraceful() + close(s.closed) + }() + } else { + close(s.closed) + } + default: + select { + case <-s.close: + // Will be processed at an upper level. + case <-s.shutdown: + // Will be processed at an upper level. + case <-s.notify: + if s.conn != nil && s.conn.ClosedNow() { + pool.poolsMutex.Lock() + if pool.state.get() == connectedState { + pool.deleteConnection(s.addr) + pool.poolsMutex.Unlock() + pool.handlerDeactivated(s.conn, s.role) + s.conn = nil + s.role = UnknownRole + } else { + pool.poolsMutex.Unlock() + } + } + case <-timer.C: + // Reopen connection. + // Relocate connection between subpools + // if ro/rw was updated. + if s.conn == nil { + pool.tryConnect(s) + } else if !s.conn.ClosedNow() { + pool.updateConnection(s) + } else { + pool.reconnect(s) + } + } } } } diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index e6738c2a1..763df3e64 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -281,6 +281,64 @@ func TestClose(t *testing.T) { require.Nil(t, err) } +func TestCloseGraceful(t *testing.T) { + server1 := servers[0] + server2 := servers[1] + + connPool, err := connection_pool.Connect([]string{server1, server2}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server1, server2}, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + server1: true, + server2: true, + }, + } + + err = test_helpers.CheckPoolStatuses(args) + require.Nil(t, err) + + eval := `local fiber = require('fiber') + local time = ... + fiber.sleep(time) +` + evalSleep := 3 // In seconds. + req := tarantool.NewEvalRequest(eval).Args([]interface{}{evalSleep}) + fut := connPool.Do(req, connection_pool.ANY) + go func() { + connPool.CloseGraceful() + }() + + // Check that a request rejected if graceful shutdown in progress. + time.Sleep((time.Duration(evalSleep) * time.Second) / 2) + _, err = connPool.Do(tarantool.NewPingRequest(), connection_pool.ANY).Get() + require.ErrorContains(t, err, "can't find healthy instance in pool") + + // Check that a previous request was successful. + resp, err := fut.Get() + require.Nilf(t, err, "sleep request no error") + require.NotNilf(t, resp, "sleep response exists") + + args = test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: []string{server1, server2}, + ExpectedPoolStatus: false, + ExpectedStatuses: map[string]bool{ + server1: false, + server2: false, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) +} + type testHandler struct { discovered, deactivated uint32 errs []error @@ -572,9 +630,8 @@ func TestGetPoolInfo(t *testing.T) { srvs[0] = "x" connPool.GetAddrs()[1] = "y" - for i, addr := range connPool.GetAddrs() { - require.Equal(t, expected[i], addr) - } + + require.ElementsMatch(t, expected, connPool.GetAddrs()) } func TestCall17(t *testing.T) { diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go index cf59455ca..4ed422375 100644 --- a/connection_pool/example_test.go +++ b/connection_pool/example_test.go @@ -926,3 +926,43 @@ func ExampleConnectorAdapter() { // Ping Data [] // Ping Error } + +// ExampleConnectionPool_CloseGraceful_force demonstrates how to force close +// a connection pool with graceful close in progress after a while. +func ExampleConnectionPool_CloseGraceful_force() { + pool, err := examplePool(testRoles, connOpts) + if err != nil { + fmt.Println(err) + return + } + + eval := `local fiber = require('fiber') + local time = ... + fiber.sleep(time) +` + req := tarantool.NewEvalRequest(eval).Args([]interface{}{10}) + fut := pool.Do(req, connection_pool.ANY) + + done := make(chan struct{}) + go func() { + pool.CloseGraceful() + fmt.Println("ConnectionPool.CloseGraceful() done!") + close(done) + }() + + select { + case <-done: + case <-time.After(3 * time.Second): + fmt.Println("Force ConnectionPool.Close()!") + pool.Close() + } + <-done + + fmt.Println("Result:") + fmt.Println(fut.Get()) + // Output: + // Force ConnectionPool.Close()! + // ConnectionPool.CloseGraceful() done! + // Result: + // connection closed by client (0x4001) +} diff --git a/connection_pool/state.go b/connection_pool/state.go index a9d20392e..20cd070af 100644 --- a/connection_pool/state.go +++ b/connection_pool/state.go @@ -10,6 +10,7 @@ type state uint32 const ( unknownState state = iota connectedState + shutdownState closedState ) From c810cb3934cedb6149224e0af93adef737036983 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 29 May 2023 15:39:55 +0300 Subject: [PATCH 442/605] api: ConnectionPool.Add()/ConnectionPool.Remove() * ConnectionPool.Add() allows to add a new endpoint into a pool. * ConnectionPool.Remove() allows to remove an endpoint from a pool. Closes #290 --- CHANGELOG.md | 2 + connection_pool/connection_pool.go | 300 +++++++++++++++--------- connection_pool/connection_pool_test.go | 300 ++++++++++++++++++++++++ 3 files changed, 490 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c4f9341..b2655e52e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. requests to complete (#257) - ConnectionPool.CloseGraceful() unlike ConnectionPool.Close() waits for all requests to complete (#257) +- ConnectionPool.Add()/ConnectionPool.Remove() to add/remove endpoints + from a pool (#290) ### Changed diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index f67ac7092..f1b3951b2 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -30,6 +30,7 @@ var ( ErrNoRwInstance = errors.New("can't find rw instance in pool") ErrNoRoInstance = errors.New("can't find ro instance in pool") ErrNoHealthyInstance = errors.New("can't find healthy instance in pool") + ErrClosed = errors.New("pool is closed") ) // ConnectionHandler provides callbacks for components interested in handling @@ -87,7 +88,9 @@ Main features: - Automatic master discovery by mode parameter. */ type ConnectionPool struct { - addrs map[string]*endpointState + addrs map[string]*endpoint + addrsMutex sync.RWMutex + connOpts tarantool.Opts opts OptsPool @@ -102,7 +105,7 @@ type ConnectionPool struct { var _ Pooler = (*ConnectionPool)(nil) -type endpointState struct { +type endpoint struct { addr string notify chan tarantool.ConnEvent conn *tarantool.Connection @@ -114,6 +117,18 @@ type endpointState struct { closeErr error } +func newEndpoint(addr string) *endpoint { + return &endpoint{ + addr: addr, + notify: make(chan tarantool.ConnEvent, 100), + conn: nil, + role: UnknownRole, + shutdown: make(chan struct{}), + close: make(chan struct{}), + closed: make(chan struct{}), + } +} + // ConnectWithOpts creates pool for instances with addresses addrs // with options opts. func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (connPool *ConnectionPool, err error) { @@ -130,7 +145,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co anyPool := NewEmptyRoundRobin(size) connPool = &ConnectionPool{ - addrs: make(map[string]*endpointState), + addrs: make(map[string]*endpoint), connOpts: connOpts.Clone(), opts: opts, state: unknownState, @@ -205,12 +220,75 @@ func (connPool *ConnectionPool) ConfiguredTimeout(mode Mode) (time.Duration, err return conn.ConfiguredTimeout(), nil } +// Add adds a new endpoint with the address into the pool. This function +// adds the endpoint only after successful connection. +func (pool *ConnectionPool) Add(addr string) error { + e := newEndpoint(addr) + + pool.addrsMutex.Lock() + // Ensure that Close()/CloseGraceful() not in progress/done. + if pool.state.get() != connectedState { + pool.addrsMutex.Unlock() + return ErrClosed + } + if _, ok := pool.addrs[addr]; ok { + pool.addrsMutex.Unlock() + return errors.New("endpoint exist") + } + pool.addrs[addr] = e + pool.addrsMutex.Unlock() + + if err := pool.tryConnect(e); err != nil { + pool.addrsMutex.Lock() + delete(pool.addrs, addr) + pool.addrsMutex.Unlock() + close(e.closed) + return err + } + + go pool.controller(e) + return nil +} + +// Remove removes an endpoint with the address from the pool. The call +// closes an active connection gracefully. +func (pool *ConnectionPool) Remove(addr string) error { + pool.addrsMutex.Lock() + endpoint, ok := pool.addrs[addr] + if !ok { + pool.addrsMutex.Unlock() + return errors.New("endpoint not exist") + } + + select { + case <-endpoint.close: + // Close() in progress/done. + case <-endpoint.shutdown: + // CloseGraceful()/Remove() in progress/done. + default: + close(endpoint.shutdown) + } + + delete(pool.addrs, addr) + pool.addrsMutex.Unlock() + + <-endpoint.closed + return nil +} + func (pool *ConnectionPool) waitClose() []error { - errs := make([]error, 0, len(pool.addrs)) - for _, s := range pool.addrs { - <-s.closed - if s.closeErr != nil { - errs = append(errs, s.closeErr) + pool.addrsMutex.RLock() + endpoints := make([]*endpoint, 0, len(pool.addrs)) + for _, e := range pool.addrs { + endpoints = append(endpoints, e) + } + pool.addrsMutex.RUnlock() + + errs := make([]error, 0, len(endpoints)) + for _, e := range endpoints { + <-e.closed + if e.closeErr != nil { + errs = append(errs, e.closeErr) } } return errs @@ -220,11 +298,11 @@ func (pool *ConnectionPool) waitClose() []error { func (pool *ConnectionPool) Close() []error { if pool.state.cas(connectedState, closedState) || pool.state.cas(shutdownState, closedState) { - pool.poolsMutex.Lock() + pool.addrsMutex.RLock() for _, s := range pool.addrs { close(s.close) } - pool.poolsMutex.Unlock() + pool.addrsMutex.RUnlock() } return pool.waitClose() @@ -234,22 +312,25 @@ func (pool *ConnectionPool) Close() []error { // for all requests to complete. func (pool *ConnectionPool) CloseGraceful() []error { if pool.state.cas(connectedState, shutdownState) { - pool.poolsMutex.Lock() + pool.addrsMutex.RLock() for _, s := range pool.addrs { close(s.shutdown) } - pool.poolsMutex.Unlock() + pool.addrsMutex.RUnlock() } return pool.waitClose() } // GetAddrs gets addresses of connections in pool. -func (connPool *ConnectionPool) GetAddrs() []string { - cpy := make([]string, len(connPool.addrs)) +func (pool *ConnectionPool) GetAddrs() []string { + pool.addrsMutex.RLock() + defer pool.addrsMutex.RUnlock() + + cpy := make([]string, len(pool.addrs)) i := 0 - for addr := range connPool.addrs { + for addr := range pool.addrs { cpy[i] = addr i++ } @@ -258,18 +339,20 @@ func (connPool *ConnectionPool) GetAddrs() []string { } // GetPoolInfo gets information of connections (connected status, ro/rw role). -func (connPool *ConnectionPool) GetPoolInfo() map[string]*ConnectionInfo { +func (pool *ConnectionPool) GetPoolInfo() map[string]*ConnectionInfo { info := make(map[string]*ConnectionInfo) - connPool.poolsMutex.RLock() - defer connPool.poolsMutex.RUnlock() + pool.addrsMutex.RLock() + defer pool.addrsMutex.RUnlock() + pool.poolsMutex.RLock() + defer pool.poolsMutex.RUnlock() - if connPool.state.get() != connectedState { + if pool.state.get() != connectedState { return info } - for addr := range connPool.addrs { - conn, role := connPool.getConnectionFromPool(addr) + for addr := range pool.addrs { + conn, role := pool.getConnectionFromPool(addr) if conn != nil { info[addr] = &ConnectionInfo{ConnectedNow: conn.ConnectedNow(), ConnRole: role} } @@ -900,19 +983,11 @@ func (connPool *ConnectionPool) fillPools() bool { // It is called before controller() goroutines so we don't expect // concurrency issues here. for addr := range connPool.addrs { - state := &endpointState{ - addr: addr, - notify: make(chan tarantool.ConnEvent, 10), - conn: nil, - role: UnknownRole, - shutdown: make(chan struct{}), - close: make(chan struct{}), - closed: make(chan struct{}), - } - connPool.addrs[addr] = state + end := newEndpoint(addr) + connPool.addrs[addr] = end connOpts := connPool.connOpts - connOpts.Notify = state.notify + connOpts.Notify = end.notify conn, err := tarantool.Connect(addr, connOpts) if err != nil { log.Printf("tarantool: connect to %s failed: %s\n", addr, err.Error()) @@ -931,8 +1006,8 @@ func (connPool *ConnectionPool) fillPools() bool { } if conn.ConnectedNow() { - state.conn = conn - state.role = role + end.conn = conn + end.role = role somebodyAlive = true } else { connPool.deleteConnection(addr) @@ -948,7 +1023,7 @@ func (connPool *ConnectionPool) fillPools() bool { return somebodyAlive } -func (pool *ConnectionPool) updateConnection(s *endpointState) { +func (pool *ConnectionPool) updateConnection(e *endpoint) { pool.poolsMutex.Lock() if pool.state.get() != connectedState { @@ -956,17 +1031,17 @@ func (pool *ConnectionPool) updateConnection(s *endpointState) { return } - if role, err := pool.getConnectionRole(s.conn); err == nil { - if s.role != role { - pool.deleteConnection(s.addr) + if role, err := pool.getConnectionRole(e.conn); err == nil { + if e.role != role { + pool.deleteConnection(e.addr) pool.poolsMutex.Unlock() - pool.handlerDeactivated(s.conn, s.role) - opened := pool.handlerDiscovered(s.conn, role) + pool.handlerDeactivated(e.conn, e.role) + opened := pool.handlerDiscovered(e.conn, role) if !opened { - s.conn.Close() - s.conn = nil - s.role = UnknownRole + e.conn.Close() + e.conn = nil + e.role = UnknownRole return } @@ -974,66 +1049,66 @@ func (pool *ConnectionPool) updateConnection(s *endpointState) { if pool.state.get() != connectedState { pool.poolsMutex.Unlock() - s.conn.Close() - pool.handlerDeactivated(s.conn, role) - s.conn = nil - s.role = UnknownRole + e.conn.Close() + pool.handlerDeactivated(e.conn, role) + e.conn = nil + e.role = UnknownRole return } - if pool.addConnection(s.addr, s.conn, role) != nil { + if pool.addConnection(e.addr, e.conn, role) != nil { pool.poolsMutex.Unlock() - s.conn.Close() - pool.handlerDeactivated(s.conn, role) - s.conn = nil - s.role = UnknownRole + e.conn.Close() + pool.handlerDeactivated(e.conn, role) + e.conn = nil + e.role = UnknownRole return } - s.role = role + e.role = role } pool.poolsMutex.Unlock() return } else { - pool.deleteConnection(s.addr) + pool.deleteConnection(e.addr) pool.poolsMutex.Unlock() - s.conn.Close() - pool.handlerDeactivated(s.conn, s.role) - s.conn = nil - s.role = UnknownRole + e.conn.Close() + pool.handlerDeactivated(e.conn, e.role) + e.conn = nil + e.role = UnknownRole return } } -func (pool *ConnectionPool) tryConnect(s *endpointState) { +func (pool *ConnectionPool) tryConnect(e *endpoint) error { pool.poolsMutex.Lock() if pool.state.get() != connectedState { pool.poolsMutex.Unlock() - return + return ErrClosed } - s.conn = nil - s.role = UnknownRole + e.conn = nil + e.role = UnknownRole connOpts := pool.connOpts - connOpts.Notify = s.notify - conn, _ := tarantool.Connect(s.addr, connOpts) - if conn != nil { + connOpts.Notify = e.notify + conn, err := tarantool.Connect(e.addr, connOpts) + if err == nil { role, err := pool.getConnectionRole(conn) pool.poolsMutex.Unlock() if err != nil { conn.Close() - log.Printf("tarantool: storing connection to %s failed: %s\n", s.addr, err) - return + log.Printf("tarantool: storing connection to %s failed: %s\n", e.addr, err) + return err } opened := pool.handlerDiscovered(conn, role) if !opened { conn.Close() - return + return errors.New("storing connection canceled") } pool.poolsMutex.Lock() @@ -1041,23 +1116,24 @@ func (pool *ConnectionPool) tryConnect(s *endpointState) { pool.poolsMutex.Unlock() conn.Close() pool.handlerDeactivated(conn, role) - return + return ErrClosed } - if pool.addConnection(s.addr, conn, role) != nil { + if err = pool.addConnection(e.addr, conn, role); err != nil { pool.poolsMutex.Unlock() conn.Close() pool.handlerDeactivated(conn, role) - return + return err } - s.conn = conn - s.role = role + e.conn = conn + e.role = role } pool.poolsMutex.Unlock() + return err } -func (pool *ConnectionPool) reconnect(s *endpointState) { +func (pool *ConnectionPool) reconnect(e *endpoint) { pool.poolsMutex.Lock() if pool.state.get() != connectedState { @@ -1065,17 +1141,17 @@ func (pool *ConnectionPool) reconnect(s *endpointState) { return } - pool.deleteConnection(s.addr) + pool.deleteConnection(e.addr) pool.poolsMutex.Unlock() - pool.handlerDeactivated(s.conn, s.role) - s.conn = nil - s.role = UnknownRole + pool.handlerDeactivated(e.conn, e.role) + e.conn = nil + e.role = UnknownRole - pool.tryConnect(s) + pool.tryConnect(e) } -func (pool *ConnectionPool) controller(s *endpointState) { +func (pool *ConnectionPool) controller(e *endpoint) { timer := time.NewTicker(pool.opts.CheckTimeout) defer timer.Stop() @@ -1085,71 +1161,71 @@ func (pool *ConnectionPool) controller(s *endpointState) { // Graceful shutdown in progress. We need to wait for a finish or // to force close. select { - case <-s.closed: - case <-s.close: + case <-e.closed: + case <-e.close: } } select { - case <-s.closed: + case <-e.closed: return default: } select { - // s.close has priority to avoid concurrency with s.shutdown. - case <-s.close: - if s.conn != nil { + // e.close has priority to avoid concurrency with e.shutdown. + case <-e.close: + if e.conn != nil { pool.poolsMutex.Lock() - pool.deleteConnection(s.addr) + pool.deleteConnection(e.addr) pool.poolsMutex.Unlock() if !shutdown { - s.closeErr = s.conn.Close() - pool.handlerDeactivated(s.conn, s.role) - close(s.closed) + e.closeErr = e.conn.Close() + pool.handlerDeactivated(e.conn, e.role) + close(e.closed) } else { // Force close the connection. - s.conn.Close() + e.conn.Close() // And wait for a finish. - <-s.closed + <-e.closed } } else { - close(s.closed) + close(e.closed) } default: select { - case <-s.shutdown: + case <-e.shutdown: shutdown = true - if s.conn != nil { + if e.conn != nil { pool.poolsMutex.Lock() - pool.deleteConnection(s.addr) + pool.deleteConnection(e.addr) pool.poolsMutex.Unlock() // We need to catch s.close in the current goroutine, so // we need to start an another one for the shutdown. go func() { - s.closeErr = s.conn.CloseGraceful() - close(s.closed) + e.closeErr = e.conn.CloseGraceful() + close(e.closed) }() } else { - close(s.closed) + close(e.closed) } default: select { - case <-s.close: + case <-e.close: // Will be processed at an upper level. - case <-s.shutdown: + case <-e.shutdown: // Will be processed at an upper level. - case <-s.notify: - if s.conn != nil && s.conn.ClosedNow() { + case <-e.notify: + if e.conn != nil && e.conn.ClosedNow() { pool.poolsMutex.Lock() if pool.state.get() == connectedState { - pool.deleteConnection(s.addr) + pool.deleteConnection(e.addr) pool.poolsMutex.Unlock() - pool.handlerDeactivated(s.conn, s.role) - s.conn = nil - s.role = UnknownRole + pool.handlerDeactivated(e.conn, e.role) + e.conn = nil + e.role = UnknownRole } else { pool.poolsMutex.Unlock() } @@ -1158,12 +1234,12 @@ func (pool *ConnectionPool) controller(s *endpointState) { // Reopen connection. // Relocate connection between subpools // if ro/rw was updated. - if s.conn == nil { - pool.tryConnect(s) - } else if !s.conn.ClosedNow() { - pool.updateConnection(s) + if e.conn == nil { + pool.tryConnect(e) + } else if !e.conn.ClosedNow() { + pool.updateConnection(e) } else { - pool.reconnect(s) + pool.reconnect(e) } } } diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 763df3e64..57441ee37 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tarantool/go-tarantool" "github.com/tarantool/go-tarantool/connection_pool" @@ -242,6 +243,305 @@ func TestDisconnectAll(t *testing.T) { require.Nil(t, err) } +func TestAdd(t *testing.T) { + connPool, err := connection_pool.Connect([]string{servers[0]}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + for _, server := range servers[1:] { + err = connPool.Add(server) + require.Nil(t, err) + } + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: servers, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + servers[0]: true, + servers[1]: true, + servers[2]: true, + servers[3]: true, + servers[4]: true, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) +} + +func TestAdd_exist(t *testing.T) { + connPool, err := connection_pool.Connect([]string{servers[0]}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + err = connPool.Add(servers[0]) + require.ErrorContains(t, err, "endpoint exist") + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: servers, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + servers[0]: true, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) +} + +func TestAdd_unreachable(t *testing.T) { + connPool, err := connection_pool.Connect([]string{servers[0]}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + err = connPool.Add("127.0.0.2:6667") + // The OS-dependent error so we just check for existence. + require.NotNil(t, err) + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: servers, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + servers[0]: true, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) +} + +func TestAdd_afterClose(t *testing.T) { + connPool, err := connection_pool.Connect([]string{servers[0]}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + connPool.Close() + err = connPool.Add(servers[0]) + assert.Equal(t, err, connection_pool.ErrClosed) +} + +func TestAdd_Close_concurrent(t *testing.T) { + connPool, err := connection_pool.Connect([]string{servers[0]}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + err = connPool.Add(servers[0]) + if err != nil { + assert.Equal(t, err, connection_pool.ErrClosed) + } + }() + + connPool.Close() + + wg.Wait() +} + +func TestAdd_CloseGraceful_concurrent(t *testing.T) { + connPool, err := connection_pool.Connect([]string{servers[0]}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + err = connPool.Add(servers[0]) + if err != nil { + assert.Equal(t, err, connection_pool.ErrClosed) + } + }() + + connPool.CloseGraceful() + + wg.Wait() +} + +func TestRemove(t *testing.T) { + connPool, err := connection_pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + for _, server := range servers[1:] { + err = connPool.Remove(server) + require.Nil(t, err) + } + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: servers, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + servers[0]: true, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) +} + +func TestRemove_double(t *testing.T) { + connPool, err := connection_pool.Connect([]string{servers[0], servers[1]}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + err = connPool.Remove(servers[1]) + require.Nil(t, err) + err = connPool.Remove(servers[1]) + require.ErrorContains(t, err, "endpoint not exist") + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: servers, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + servers[0]: true, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) +} + +func TestRemove_unknown(t *testing.T) { + connPool, err := connection_pool.Connect([]string{servers[0], servers[1]}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + err = connPool.Remove("not_exist:6667") + require.ErrorContains(t, err, "endpoint not exist") + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: servers, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + servers[0]: true, + servers[1]: true, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) +} + +func TestRemove_concurrent(t *testing.T) { + connPool, err := connection_pool.Connect([]string{servers[0], servers[1]}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + const concurrency = 10 + var ( + wg sync.WaitGroup + ok uint32 + errs uint32 + ) + + wg.Add(concurrency) + for i := 0; i < concurrency; i++ { + go func() { + defer wg.Done() + err := connPool.Remove(servers[1]) + if err == nil { + atomic.AddUint32(&ok, 1) + } else { + assert.ErrorContains(t, err, "endpoint not exist") + atomic.AddUint32(&errs, 1) + } + }() + } + + wg.Wait() + assert.Equal(t, uint32(1), ok) + assert.Equal(t, uint32(concurrency-1), errs) + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: connection_pool.ANY, + Servers: servers, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + servers[0]: true, + }, + } + + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) + require.Nil(t, err) +} + +func TestRemove_Close_concurrent(t *testing.T) { + connPool, err := connection_pool.Connect([]string{servers[0], servers[1]}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + err = connPool.Remove(servers[1]) + assert.Nil(t, err) + }() + + connPool.Close() + + wg.Wait() +} + +func TestRemove_CloseGraceful_concurrent(t *testing.T) { + connPool, err := connection_pool.Connect([]string{servers[0], servers[1]}, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + err = connPool.Remove(servers[1]) + assert.Nil(t, err) + }() + + connPool.CloseGraceful() + + wg.Wait() +} + func TestClose(t *testing.T) { server1 := servers[0] server2 := servers[1] From 32963600aff0e8d716722045cb458416078511cb Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 6 Jun 2023 12:09:22 +0300 Subject: [PATCH 443/605] api: add connection_pool.ErrExists ConnectionPool.Add() returns the error if an endpoint exists in a connection pool. --- connection_pool/connection_pool.go | 3 ++- connection_pool/connection_pool_test.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index f1b3951b2..5113e9ea5 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -30,6 +30,7 @@ var ( ErrNoRwInstance = errors.New("can't find rw instance in pool") ErrNoRoInstance = errors.New("can't find ro instance in pool") ErrNoHealthyInstance = errors.New("can't find healthy instance in pool") + ErrExists = errors.New("endpoint exists") ErrClosed = errors.New("pool is closed") ) @@ -233,7 +234,7 @@ func (pool *ConnectionPool) Add(addr string) error { } if _, ok := pool.addrs[addr]; ok { pool.addrsMutex.Unlock() - return errors.New("endpoint exist") + return ErrExists } pool.addrs[addr] = e pool.addrsMutex.Unlock() diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 57441ee37..1c5b14a46 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -282,7 +282,7 @@ func TestAdd_exist(t *testing.T) { defer connPool.Close() err = connPool.Add(servers[0]) - require.ErrorContains(t, err, "endpoint exist") + require.Equal(t, connection_pool.ErrExists, err) args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, From 814e695c2f97b9e037ccdd7c9d4f09ad916077ec Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 6 Jun 2023 12:09:38 +0300 Subject: [PATCH 444/605] bugfix: flaky TestAdd_Close_concurrent We need to add non-added endpoint to avoid `endpoint exist` error. --- connection_pool/connection_pool_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index 1c5b14a46..dde6815d9 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -345,9 +345,9 @@ func TestAdd_Close_concurrent(t *testing.T) { go func() { defer wg.Done() - err = connPool.Add(servers[0]) + err = connPool.Add(servers[1]) if err != nil { - assert.Equal(t, err, connection_pool.ErrClosed) + assert.Equal(t, connection_pool.ErrClosed, err) } }() @@ -366,9 +366,9 @@ func TestAdd_CloseGraceful_concurrent(t *testing.T) { go func() { defer wg.Done() - err = connPool.Add(servers[0]) + err = connPool.Add(servers[1]) if err != nil { - assert.Equal(t, err, connection_pool.ErrClosed) + assert.Equal(t, connection_pool.ErrClosed, err) } }() From 403f2c3ddbb3f7f98c133bad6daf6d3f9de76fd7 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 29 May 2023 16:34:55 +0300 Subject: [PATCH 445/605] Release 1.12.0 Overview The release introduces the ability to gracefully close Connection and ConnectionPool and also provides methods for adding or removing an endpoint from a ConnectionPool. Breaking changes There are no breaking changes in the release. New features Connection.CloseGraceful() unlike Connection.Close() waits for all requests to complete (#257). ConnectionPool.CloseGraceful() unlike ConnectionPool.Close() waits for all requests to complete (#257). ConnectionPool.Add()/ConnectionPool.Remove() to add/remove endpoints from a pool (#290). Other Updates crud tests with Tarantool 3.0 (#293). Updates SQL tests with Tarantool 3.0 (#295). --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2655e52e..0332f376f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,18 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +### Changed + +### Fixed + +## [1.12.0] - 2023-06-07 + +The release introduces the ability to gracefully close Connection +and ConnectionPool and also provides methods for adding or removing an endpoint +from a ConnectionPool. + +### Added + - Connection.CloseGraceful() unlike Connection.Close() waits for all requests to complete (#257) - ConnectionPool.CloseGraceful() unlike ConnectionPool.Close() waits for all From 7ef86f00f5f13c08fd851469558eb7d61aaaffde Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Sun, 25 Dec 2022 13:23:43 +0300 Subject: [PATCH 446/605] api: start v2 --- README.md | 6 +++--- auth_test.go | 2 +- box_error_test.go | 4 ++-- call_16_test.go | 4 ++-- call_17_test.go | 4 ++-- connection_pool/call_16_test.go | 4 ++-- connection_pool/call_17_test.go | 4 ++-- connection_pool/connection_pool.go | 2 +- connection_pool/connection_pool_test.go | 6 +++--- connection_pool/connector.go | 2 +- connection_pool/connector_test.go | 4 ++-- connection_pool/example_test.go | 6 +++--- connection_pool/pooler.go | 2 +- connection_pool/round_robin.go | 2 +- connection_pool/round_robin_test.go | 4 ++-- connection_pool/watcher.go | 2 +- connection_test.go | 2 +- crud/common.go | 2 +- crud/count.go | 2 +- crud/delete.go | 2 +- crud/error_test.go | 2 +- crud/example_test.go | 4 ++-- crud/get.go | 2 +- crud/insert.go | 2 +- crud/insert_many.go | 2 +- crud/len.go | 2 +- crud/max.go | 2 +- crud/min.go | 2 +- crud/replace.go | 2 +- crud/replace_many.go | 2 +- crud/request_test.go | 6 +++--- crud/select.go | 2 +- crud/stats.go | 2 +- crud/storage_info.go | 2 +- crud/tarantool_test.go | 6 +++--- crud/truncate.go | 2 +- crud/update.go | 2 +- crud/upsert.go | 2 +- crud/upsert_many.go | 2 +- datetime/datetime_test.go | 6 +++--- datetime/example_test.go | 4 ++-- datetime/interval_test.go | 4 ++-- datetime/msgpack_helper_test.go | 2 +- datetime/msgpack_v5_helper_test.go | 2 +- decimal/decimal_test.go | 6 +++--- decimal/example_test.go | 4 ++-- decimal/fuzzing_test.go | 2 +- decimal/msgpack_helper_test.go | 2 +- decimal/msgpack_v5_helper_test.go | 2 +- dial_test.go | 2 +- example_custom_unpacking_test.go | 2 +- example_test.go | 4 ++-- future_test.go | 2 +- go.mod | 2 +- msgpack_helper_test.go | 2 +- msgpack_v5_helper_test.go | 2 +- multi/call_16_test.go | 2 +- multi/call_17_test.go | 2 +- multi/example_test.go | 2 +- multi/multi.go | 2 +- multi/multi_test.go | 4 ++-- protocol_test.go | 2 +- queue/example_connection_pool_test.go | 8 ++++---- queue/example_msgpack_test.go | 4 ++-- queue/example_test.go | 4 ++-- queue/queue.go | 2 +- queue/queue_test.go | 6 +++--- request_test.go | 4 ++-- settings/example_test.go | 6 +++--- settings/msgpack_helper_test.go | 2 +- settings/msgpack_v5_helper_test.go | 2 +- settings/request.go | 2 +- settings/request_test.go | 4 ++-- settings/tarantool_test.go | 6 +++--- shutdown_test.go | 4 ++-- ssl_test.go | 4 ++-- tarantool_test.go | 4 ++-- test_helpers/main.go | 4 ++-- test_helpers/pool_helper.go | 4 ++-- test_helpers/request_mock.go | 2 +- test_helpers/utils.go | 2 +- uuid/example_test.go | 4 ++-- uuid/uuid_test.go | 6 +++--- 83 files changed, 131 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index 7fcf96d1b..becc371ca 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ The package `go-tarantool` is located in [tarantool/go-tarantool][go-tarantool] repository. To download and install, say: ``` -$ go get github.com/tarantool/go-tarantool +$ go get github.com/tarantool/go-tarantool/v2 ``` This should put the source and binary files in subdirectories of @@ -112,7 +112,7 @@ package tarantool import ( "fmt" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) func main() { @@ -129,7 +129,7 @@ func main() { } ``` -**Observation 1:** The line "`github.com/tarantool/go-tarantool`" in the +**Observation 1:** The line "`github.com/tarantool/go-tarantool/v2`" in the `import(...)` section brings in all Tarantool-related functions and structures. **Observation 2:** The line starting with "`Opts :=`" sets up the options for diff --git a/auth_test.go b/auth_test.go index 6964f2552..712226d63 100644 --- a/auth_test.go +++ b/auth_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - . "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/v2" ) func TestAuth_String(t *testing.T) { diff --git a/box_error_test.go b/box_error_test.go index 276ca2cf8..4c38d359c 100644 --- a/box_error_test.go +++ b/box_error_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/stretchr/testify/require" - . "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/test_helpers" + . "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) var samples = map[string]BoxError{ diff --git a/call_16_test.go b/call_16_test.go index f23f0f783..8407f109d 100644 --- a/call_16_test.go +++ b/call_16_test.go @@ -6,8 +6,8 @@ package tarantool_test import ( "testing" - . "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/test_helpers" + . "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) func TestConnection_Call(t *testing.T) { diff --git a/call_17_test.go b/call_17_test.go index 824ed850b..6111aa52c 100644 --- a/call_17_test.go +++ b/call_17_test.go @@ -6,8 +6,8 @@ package tarantool_test import ( "testing" - . "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/test_helpers" + . "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) func TestConnection_Call(t *testing.T) { diff --git a/connection_pool/call_16_test.go b/connection_pool/call_16_test.go index bf2ab2eb7..806c67ba0 100644 --- a/connection_pool/call_16_test.go +++ b/connection_pool/call_16_test.go @@ -7,8 +7,8 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/connection_pool" - "github.com/tarantool/go-tarantool/test_helpers" + "github.com/tarantool/go-tarantool/v2/connection_pool" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) func TestCall(t *testing.T) { diff --git a/connection_pool/call_17_test.go b/connection_pool/call_17_test.go index 94e5bb888..16f1a5e6b 100644 --- a/connection_pool/call_17_test.go +++ b/connection_pool/call_17_test.go @@ -7,8 +7,8 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/connection_pool" - "github.com/tarantool/go-tarantool/test_helpers" + "github.com/tarantool/go-tarantool/v2/connection_pool" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) func TestCall(t *testing.T) { diff --git a/connection_pool/connection_pool.go b/connection_pool/connection_pool.go index 5113e9ea5..d1b7de9f5 100644 --- a/connection_pool/connection_pool.go +++ b/connection_pool/connection_pool.go @@ -17,7 +17,7 @@ import ( "sync" "time" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) var ( diff --git a/connection_pool/connection_pool_test.go b/connection_pool/connection_pool_test.go index dde6815d9..e6cb31db7 100644 --- a/connection_pool/connection_pool_test.go +++ b/connection_pool/connection_pool_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/connection_pool" - "github.com/tarantool/go-tarantool/test_helpers" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/connection_pool" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) var spaceNo = uint32(520) diff --git a/connection_pool/connector.go b/connection_pool/connector.go index c108aba0b..3688b8309 100644 --- a/connection_pool/connector.go +++ b/connection_pool/connector.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // ConnectorAdapter allows to use Pooler as Connector. diff --git a/connection_pool/connector_test.go b/connection_pool/connector_test.go index f53a05b22..717fdaec4 100644 --- a/connection_pool/connector_test.go +++ b/connection_pool/connector_test.go @@ -7,8 +7,8 @@ import ( "time" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool" - . "github.com/tarantool/go-tarantool/connection_pool" + "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v2/connection_pool" ) var testMode Mode = RW diff --git a/connection_pool/example_test.go b/connection_pool/example_test.go index 4ed422375..84b0a5594 100644 --- a/connection_pool/example_test.go +++ b/connection_pool/example_test.go @@ -4,9 +4,9 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/connection_pool" - "github.com/tarantool/go-tarantool/test_helpers" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/connection_pool" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) type Tuple struct { diff --git a/connection_pool/pooler.go b/connection_pool/pooler.go index 856f5d5be..31790620c 100644 --- a/connection_pool/pooler.go +++ b/connection_pool/pooler.go @@ -3,7 +3,7 @@ package connection_pool import ( "time" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // Pooler is the interface that must be implemented by a connection pool. diff --git a/connection_pool/round_robin.go b/connection_pool/round_robin.go index a7fb73e18..0a0988890 100644 --- a/connection_pool/round_robin.go +++ b/connection_pool/round_robin.go @@ -3,7 +3,7 @@ package connection_pool import ( "sync" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) type RoundRobinStrategy struct { diff --git a/connection_pool/round_robin_test.go b/connection_pool/round_robin_test.go index 03038eada..254217e62 100644 --- a/connection_pool/round_robin_test.go +++ b/connection_pool/round_robin_test.go @@ -3,8 +3,8 @@ package connection_pool_test import ( "testing" - "github.com/tarantool/go-tarantool" - . "github.com/tarantool/go-tarantool/connection_pool" + "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v2/connection_pool" ) const ( diff --git a/connection_pool/watcher.go b/connection_pool/watcher.go index d60cfa171..f4acc0bdd 100644 --- a/connection_pool/watcher.go +++ b/connection_pool/watcher.go @@ -3,7 +3,7 @@ package connection_pool import ( "sync" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // watcherContainer is a very simple implementation of a thread-safe container diff --git a/connection_test.go b/connection_test.go index 05f29b093..3e7be1966 100644 --- a/connection_test.go +++ b/connection_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - . "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/v2" ) func TestOptsClonePreservesRequiredProtocolFeatures(t *testing.T) { diff --git a/crud/common.go b/crud/common.go index 2c4a3030c..bb882fb8b 100644 --- a/crud/common.go +++ b/crud/common.go @@ -56,7 +56,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) type baseRequest struct { diff --git a/crud/count.go b/crud/count.go index 68e29f9fb..aede8f800 100644 --- a/crud/count.go +++ b/crud/count.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // CountResult describes result for `crud.count` method. diff --git a/crud/delete.go b/crud/delete.go index 5859d3d6b..cd6e59382 100644 --- a/crud/delete.go +++ b/crud/delete.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // DeleteOpts describes options for `crud.delete` method. diff --git a/crud/error_test.go b/crud/error_test.go index 8bd973399..a3db035bf 100644 --- a/crud/error_test.go +++ b/crud/error_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/crud" + "github.com/tarantool/go-tarantool/v2/crud" ) func TestErrorMany(t *testing.T) { diff --git a/crud/example_test.go b/crud/example_test.go index 2b80212ca..3cd5e7bad 100644 --- a/crud/example_test.go +++ b/crud/example_test.go @@ -5,8 +5,8 @@ import ( "reflect" "time" - "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/crud" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/crud" ) const ( diff --git a/crud/get.go b/crud/get.go index 9f65a34fd..f63bfda9f 100644 --- a/crud/get.go +++ b/crud/get.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // GetOpts describes options for `crud.get` method. diff --git a/crud/insert.go b/crud/insert.go index b8c34c9bd..2f09613a7 100644 --- a/crud/insert.go +++ b/crud/insert.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // InsertOpts describes options for `crud.insert` method. diff --git a/crud/insert_many.go b/crud/insert_many.go index 9d2194642..a3c3aead3 100644 --- a/crud/insert_many.go +++ b/crud/insert_many.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // InsertManyOpts describes options for `crud.insert_many` method. diff --git a/crud/len.go b/crud/len.go index 8ebea253c..dc8a4cb10 100644 --- a/crud/len.go +++ b/crud/len.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // LenResult describes result for `crud.len` method. diff --git a/crud/max.go b/crud/max.go index fda960040..7464b0480 100644 --- a/crud/max.go +++ b/crud/max.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // MaxOpts describes options for `crud.max` method. diff --git a/crud/min.go b/crud/min.go index 53794b21e..f186d303e 100644 --- a/crud/min.go +++ b/crud/min.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // MinOpts describes options for `crud.min` method. diff --git a/crud/replace.go b/crud/replace.go index 378a1ae22..87a60930a 100644 --- a/crud/replace.go +++ b/crud/replace.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // ReplaceOpts describes options for `crud.replace` method. diff --git a/crud/replace_many.go b/crud/replace_many.go index 511d60575..7216a67c2 100644 --- a/crud/replace_many.go +++ b/crud/replace_many.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // ReplaceManyOpts describes options for `crud.replace_many` method. diff --git a/crud/request_test.go b/crud/request_test.go index 3198a39c0..3e4f171a7 100644 --- a/crud/request_test.go +++ b/crud/request_test.go @@ -6,9 +6,9 @@ import ( "errors" "testing" - "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/crud" - "github.com/tarantool/go-tarantool/test_helpers" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/crud" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) const invalidSpaceMsg = "invalid space" diff --git a/crud/select.go b/crud/select.go index 97048a365..cce17ccf4 100644 --- a/crud/select.go +++ b/crud/select.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // SelectOpts describes options for `crud.select` method. diff --git a/crud/stats.go b/crud/stats.go index aa4184746..d29c0eb70 100644 --- a/crud/stats.go +++ b/crud/stats.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // StatsRequest helps you to create request object to call `crud.stats` diff --git a/crud/storage_info.go b/crud/storage_info.go index 625029b51..146e0a22e 100644 --- a/crud/storage_info.go +++ b/crud/storage_info.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // StatusTable describes information for instance. diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 0787c99e5..addddf27a 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -8,9 +8,9 @@ import ( "time" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/crud" - "github.com/tarantool/go-tarantool/test_helpers" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/crud" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) var server = "127.0.0.1:3013" diff --git a/crud/truncate.go b/crud/truncate.go index 1cd343a4d..5ad1d785e 100644 --- a/crud/truncate.go +++ b/crud/truncate.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // TruncateResult describes result for `crud.truncate` method. diff --git a/crud/update.go b/crud/update.go index 40951c95a..a2b9a5572 100644 --- a/crud/update.go +++ b/crud/update.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // UpdateOpts describes options for `crud.update` method. diff --git a/crud/upsert.go b/crud/upsert.go index c116bbca5..8fc9efa69 100644 --- a/crud/upsert.go +++ b/crud/upsert.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // UpsertOpts describes options for `crud.upsert` method. diff --git a/crud/upsert_many.go b/crud/upsert_many.go index a195f5efa..884a574ed 100644 --- a/crud/upsert_many.go +++ b/crud/upsert_many.go @@ -3,7 +3,7 @@ package crud import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // UpsertManyOpts describes options for `crud.upsert_many` method. diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index f2d1a37c7..7c24fa922 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -9,9 +9,9 @@ import ( "testing" "time" - . "github.com/tarantool/go-tarantool" - . "github.com/tarantool/go-tarantool/datetime" - "github.com/tarantool/go-tarantool/test_helpers" + . "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v2/datetime" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) var noTimezoneLoc = time.FixedZone(NoTimezone, 0) diff --git a/datetime/example_test.go b/datetime/example_test.go index 50725e6a3..1faa5b24d 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -12,8 +12,8 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool" - . "github.com/tarantool/go-tarantool/datetime" + "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v2/datetime" ) // Example demonstrates how to use tuples with datetime. To enable support of diff --git a/datetime/interval_test.go b/datetime/interval_test.go index 2d1ad41f9..aa43f2bab 100644 --- a/datetime/interval_test.go +++ b/datetime/interval_test.go @@ -5,8 +5,8 @@ import ( "reflect" "testing" - . "github.com/tarantool/go-tarantool/datetime" - "github.com/tarantool/go-tarantool/test_helpers" + . "github.com/tarantool/go-tarantool/v2/datetime" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) func TestIntervalAdd(t *testing.T) { diff --git a/datetime/msgpack_helper_test.go b/datetime/msgpack_helper_test.go index 7af2ee6ad..12be0bce4 100644 --- a/datetime/msgpack_helper_test.go +++ b/datetime/msgpack_helper_test.go @@ -4,7 +4,7 @@ package datetime_test import ( - . "github.com/tarantool/go-tarantool/datetime" + . "github.com/tarantool/go-tarantool/v2/datetime" "gopkg.in/vmihailenco/msgpack.v2" ) diff --git a/datetime/msgpack_v5_helper_test.go b/datetime/msgpack_v5_helper_test.go index d750ef006..3ee42f0bd 100644 --- a/datetime/msgpack_v5_helper_test.go +++ b/datetime/msgpack_v5_helper_test.go @@ -4,7 +4,7 @@ package datetime_test import ( - . "github.com/tarantool/go-tarantool/datetime" + . "github.com/tarantool/go-tarantool/v2/datetime" "github.com/vmihailenco/msgpack/v5" ) diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index b4e95ebe1..c1b8b60f7 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -10,9 +10,9 @@ import ( "time" "github.com/shopspring/decimal" - . "github.com/tarantool/go-tarantool" - . "github.com/tarantool/go-tarantool/decimal" - "github.com/tarantool/go-tarantool/test_helpers" + . "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v2/decimal" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) var isDecimalSupported = false diff --git a/decimal/example_test.go b/decimal/example_test.go index 346419125..cfa43c166 100644 --- a/decimal/example_test.go +++ b/decimal/example_test.go @@ -12,8 +12,8 @@ import ( "log" "time" - "github.com/tarantool/go-tarantool" - . "github.com/tarantool/go-tarantool/decimal" + "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v2/decimal" ) // To enable support of decimal in msgpack with diff --git a/decimal/fuzzing_test.go b/decimal/fuzzing_test.go index c69a68719..c7df2e080 100644 --- a/decimal/fuzzing_test.go +++ b/decimal/fuzzing_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/shopspring/decimal" - . "github.com/tarantool/go-tarantool/decimal" + . "github.com/tarantool/go-tarantool/v2/decimal" ) func strToDecimal(t *testing.T, buf string) decimal.Decimal { diff --git a/decimal/msgpack_helper_test.go b/decimal/msgpack_helper_test.go index 3824f70b8..b58ea9731 100644 --- a/decimal/msgpack_helper_test.go +++ b/decimal/msgpack_helper_test.go @@ -4,7 +4,7 @@ package decimal_test import ( - . "github.com/tarantool/go-tarantool/decimal" + . "github.com/tarantool/go-tarantool/v2/decimal" "gopkg.in/vmihailenco/msgpack.v2" ) diff --git a/decimal/msgpack_v5_helper_test.go b/decimal/msgpack_v5_helper_test.go index 6bb78168a..e253bb0bc 100644 --- a/decimal/msgpack_v5_helper_test.go +++ b/decimal/msgpack_v5_helper_test.go @@ -4,7 +4,7 @@ package decimal_test import ( - . "github.com/tarantool/go-tarantool/decimal" + . "github.com/tarantool/go-tarantool/v2/decimal" "github.com/vmihailenco/msgpack/v5" ) diff --git a/dial_test.go b/dial_test.go index 182e9c866..ff8a50aab 100644 --- a/dial_test.go +++ b/dial_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) type mockErrorDialer struct { diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index 26d19f3af..6fe8eff83 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -5,7 +5,7 @@ import ( "log" "time" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) type Tuple2 struct { diff --git a/example_test.go b/example_test.go index e608d0f42..60106001e 100644 --- a/example_test.go +++ b/example_test.go @@ -5,8 +5,8 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/test_helpers" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) type Tuple struct { diff --git a/future_test.go b/future_test.go index e7e9ab507..274bee7d5 100644 --- a/future_test.go +++ b/future_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - . "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/v2" ) func assertResponseIteratorValue(t testing.TB, it ResponseIterator, diff --git a/go.mod b/go.mod index ee97cb2a1..e93a083e5 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/tarantool/go-tarantool +module github.com/tarantool/go-tarantool/v2 go 1.11 diff --git a/msgpack_helper_test.go b/msgpack_helper_test.go index 896c105d3..5b618bdd1 100644 --- a/msgpack_helper_test.go +++ b/msgpack_helper_test.go @@ -4,7 +4,7 @@ package tarantool_test import ( - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" "gopkg.in/vmihailenco/msgpack.v2" ) diff --git a/msgpack_v5_helper_test.go b/msgpack_v5_helper_test.go index 88154c26f..ba1d72908 100644 --- a/msgpack_v5_helper_test.go +++ b/msgpack_v5_helper_test.go @@ -4,7 +4,7 @@ package tarantool_test import ( - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" "github.com/vmihailenco/msgpack/v5" ) diff --git a/multi/call_16_test.go b/multi/call_16_test.go index 35bcd20fb..6c67dab3f 100644 --- a/multi/call_16_test.go +++ b/multi/call_16_test.go @@ -6,7 +6,7 @@ package multi import ( "testing" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) func TestCall(t *testing.T) { diff --git a/multi/call_17_test.go b/multi/call_17_test.go index 378961fda..c59d7d97f 100644 --- a/multi/call_17_test.go +++ b/multi/call_17_test.go @@ -6,7 +6,7 @@ package multi import ( "testing" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) func TestCall(t *testing.T) { diff --git a/multi/example_test.go b/multi/example_test.go index 1ef369e4c..d22e3d1c8 100644 --- a/multi/example_test.go +++ b/multi/example_test.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) func ExampleConnect() { diff --git a/multi/multi.go b/multi/multi.go index 6aba2a426..e3693630a 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -18,7 +18,7 @@ import ( "sync/atomic" "time" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) const ( diff --git a/multi/multi_test.go b/multi/multi_test.go index 88db98321..dbb783860 100644 --- a/multi/multi_test.go +++ b/multi/multi_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/test_helpers" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) var server1 = "127.0.0.1:3013" diff --git a/protocol_test.go b/protocol_test.go index c747d9bff..6aa94463b 100644 --- a/protocol_test.go +++ b/protocol_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - . "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/v2" ) func TestProtocolInfoClonePreservesFeatures(t *testing.T) { diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go index 7e80110fd..11cd3e9b3 100644 --- a/queue/example_connection_pool_test.go +++ b/queue/example_connection_pool_test.go @@ -7,10 +7,10 @@ import ( "time" "github.com/google/uuid" - "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/connection_pool" - "github.com/tarantool/go-tarantool/queue" - "github.com/tarantool/go-tarantool/test_helpers" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/connection_pool" + "github.com/tarantool/go-tarantool/v2/queue" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) // QueueConnectionHandler handles new connections in a ConnectionPool. diff --git a/queue/example_msgpack_test.go b/queue/example_msgpack_test.go index 5b179f5c0..71e5cf814 100644 --- a/queue/example_msgpack_test.go +++ b/queue/example_msgpack_test.go @@ -13,8 +13,8 @@ import ( "log" "time" - "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/queue" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/queue" ) type dummyData struct { diff --git a/queue/example_test.go b/queue/example_test.go index d546b43d7..711ee31d4 100644 --- a/queue/example_test.go +++ b/queue/example_test.go @@ -12,8 +12,8 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/queue" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/queue" ) // Example demonstrates an operations like Put and Take with queue. diff --git a/queue/queue.go b/queue/queue.go index 8d161b033..5dfc147f3 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -13,7 +13,7 @@ import ( "time" "github.com/google/uuid" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // Queue is a handle to Tarantool queue's tube. diff --git a/queue/queue_test.go b/queue/queue_test.go index ffbf8feb5..11f4a65a9 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -8,9 +8,9 @@ import ( "testing" "time" - . "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/queue" - "github.com/tarantool/go-tarantool/test_helpers" + . "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/queue" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) var server = "127.0.0.1:3013" diff --git a/request_test.go b/request_test.go index 8429fef98..3458f397b 100644 --- a/request_test.go +++ b/request_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/assert" - . "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/test_helpers" + . "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) const invalidSpaceMsg = "invalid space" diff --git a/settings/example_test.go b/settings/example_test.go index a2391328f..c9bdc3c49 100644 --- a/settings/example_test.go +++ b/settings/example_test.go @@ -3,9 +3,9 @@ package settings_test import ( "fmt" - "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/settings" - "github.com/tarantool/go-tarantool/test_helpers" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/settings" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) func example_connect(opts tarantool.Opts) *tarantool.Connection { diff --git a/settings/msgpack_helper_test.go b/settings/msgpack_helper_test.go index 0c002213a..efc3285e4 100644 --- a/settings/msgpack_helper_test.go +++ b/settings/msgpack_helper_test.go @@ -6,7 +6,7 @@ package settings_test import ( "io" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" "gopkg.in/vmihailenco/msgpack.v2" ) diff --git a/settings/msgpack_v5_helper_test.go b/settings/msgpack_v5_helper_test.go index 96df6bae1..e58651f61 100644 --- a/settings/msgpack_v5_helper_test.go +++ b/settings/msgpack_v5_helper_test.go @@ -6,7 +6,7 @@ package settings_test import ( "io" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" "github.com/vmihailenco/msgpack/v5" ) diff --git a/settings/request.go b/settings/request.go index 84723bb2d..fa4e1eadd 100644 --- a/settings/request.go +++ b/settings/request.go @@ -60,7 +60,7 @@ package settings import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // SetRequest helps to set session settings. diff --git a/settings/request_test.go b/settings/request_test.go index bc26ff058..662a7002e 100644 --- a/settings/request_test.go +++ b/settings/request_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool" - . "github.com/tarantool/go-tarantool/settings" + "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v2/settings" ) type ValidSchemeResolver struct { diff --git a/settings/tarantool_test.go b/settings/tarantool_test.go index 054d5052a..dc6a79825 100644 --- a/settings/tarantool_test.go +++ b/settings/tarantool_test.go @@ -7,9 +7,9 @@ import ( "time" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool" - . "github.com/tarantool/go-tarantool/settings" - "github.com/tarantool/go-tarantool/test_helpers" + "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v2/settings" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) // There is no way to skip tests in testing.M, diff --git a/shutdown_test.go b/shutdown_test.go index 1b06284c0..7a6843811 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - . "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/test_helpers" + . "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) var shtdnServer = "127.0.0.1:3014" diff --git a/ssl_test.go b/ssl_test.go index 0a62d03c3..58d03fa73 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -15,8 +15,8 @@ import ( "time" "github.com/tarantool/go-openssl" - . "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/test_helpers" + . "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) const sslHost = "127.0.0.1" diff --git a/tarantool_test.go b/tarantool_test.go index 39e9c3511..9c315b358 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -18,8 +18,8 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/assert" - . "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/test_helpers" + . "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) var startOpts test_helpers.StartOpts = test_helpers.StartOpts{ diff --git a/test_helpers/main.go b/test_helpers/main.go index 4aaa91f50..c6c03b00d 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -3,7 +3,7 @@ // Package introduces go helpers for starting a tarantool process and // validating Tarantool version. Helpers are based on os/exec calls. // Retries to connect test tarantool instance handled explicitly, -// see tarantool/go-tarantool#136. +// see tarantool/go-tarantool/#136. // // Tarantool's instance Lua scripts use environment variables to configure // box.cfg. Listen port is set in the end of script so it is possible to @@ -23,7 +23,7 @@ import ( "strconv" "time" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) type StartOpts struct { diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index 8c3a7e6ff..4833d4cc6 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -5,8 +5,8 @@ import ( "reflect" "time" - "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/connection_pool" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/connection_pool" ) type ListenOnInstanceArgs struct { diff --git a/test_helpers/request_mock.go b/test_helpers/request_mock.go index 19c18545e..d668c8f52 100644 --- a/test_helpers/request_mock.go +++ b/test_helpers/request_mock.go @@ -3,7 +3,7 @@ package test_helpers import ( "context" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) type StrangerRequest struct { diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 34a2e2980..cf17971f5 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -8,7 +8,7 @@ import ( "time" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/v2" ) // ConnectWithValidation tries to connect to a Tarantool instance. diff --git a/uuid/example_test.go b/uuid/example_test.go index ef0993e59..6ad8ebad9 100644 --- a/uuid/example_test.go +++ b/uuid/example_test.go @@ -13,8 +13,8 @@ import ( "log" "github.com/google/uuid" - "github.com/tarantool/go-tarantool" - _ "github.com/tarantool/go-tarantool/uuid" + "github.com/tarantool/go-tarantool/v2" + _ "github.com/tarantool/go-tarantool/v2/uuid" ) // Example demonstrates how to use tuples with UUID. To enable UUID support diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index f5a596e76..a000f99cc 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -8,9 +8,9 @@ import ( "time" "github.com/google/uuid" - . "github.com/tarantool/go-tarantool" - "github.com/tarantool/go-tarantool/test_helpers" - _ "github.com/tarantool/go-tarantool/uuid" + . "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" + _ "github.com/tarantool/go-tarantool/v2/uuid" ) // There is no way to skip tests in testing.M, From 8917f7fcbf87e5f1bef9fab32fd2bebee36d975e Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 22 Dec 2022 09:08:35 +0300 Subject: [PATCH 447/605] readme: add v2 migration article --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index becc371ca..9fb5611b9 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ faster than other packages according to public benchmarks. * [Documentation](#documentation) * [API reference](#api-reference) * [Walking\-through example](#walking-through-example) - * [msgpack.v5 migration](#msgpackv5-migration) + * [Migration to v2](#migration-to-v2) + * [msgpack.v5](#msgpackv5) * [Contributing](#contributing) * [Alternative connectors](#alternative-connectors) @@ -153,7 +154,11 @@ There are two parameters: * a space number (it could just as easily have been a space name), and * a tuple. -### msgpack.v5 migration +### Migration to v2 + +The article describes migration from go-tarantool to go-tarantool/v2. + +#### msgpack.v5 Most function names and argument types in `msgpack.v5` and `msgpack.v2` have not changed (in our code, we noticed changes in `EncodeInt`, `EncodeUint` From acfefc7ad544375f73faf9cd47222ad98b160452 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 22 Dec 2022 09:09:05 +0300 Subject: [PATCH 448/605] api: remove multi package The package has been deprecated and we don't want to support it anymore. `pool` could be used instead of the package. Closes #240 --- CHANGELOG.md | 4 + Makefile | 6 - README.md | 5 + multi/call_16_test.go | 33 -- multi/call_17_test.go | 33 -- multi/config.lua | 49 --- multi/config_load_nodes.lua | 7 - multi/example_test.go | 39 --- multi/multi.go | 534 ----------------------------- multi/multi_test.go | 647 ------------------------------------ 10 files changed, 9 insertions(+), 1348 deletions(-) delete mode 100644 multi/call_16_test.go delete mode 100644 multi/call_17_test.go delete mode 100644 multi/config.lua delete mode 100644 multi/config_load_nodes.lua delete mode 100644 multi/example_test.go delete mode 100644 multi/multi.go delete mode 100644 multi/multi_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0332f376f..a5a8743c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Changed +### Removed + +- multi subpackage (#240) + ### Fixed ## [1.12.0] - 2023-06-07 diff --git a/Makefile b/Makefile index cc689dae7..b1d808cda 100644 --- a/Makefile +++ b/Makefile @@ -75,12 +75,6 @@ test-decimal: go clean -testcache go test -tags "$(TAGS)" ./decimal/ -v -p 1 -.PHONY: test-multi -test-multi: - @echo "Running tests in multiconnection package" - go clean -testcache - go test -tags "$(TAGS)" ./multi/ -v -p 1 - .PHONY: test-queue test-queue: @echo "Running tests in queue package" diff --git a/README.md b/README.md index 9fb5611b9..8b7b2fbb4 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ faster than other packages according to public benchmarks. * [API reference](#api-reference) * [Walking\-through example](#walking-through-example) * [Migration to v2](#migration-to-v2) + * [multi package](#multi-package) * [msgpack.v5](#msgpackv5) * [Contributing](#contributing) * [Alternative connectors](#alternative-connectors) @@ -158,6 +159,10 @@ There are two parameters: The article describes migration from go-tarantool to go-tarantool/v2. +#### multi package + +The subpackage has been deleted. You could use `connection_pool` instead. + #### msgpack.v5 Most function names and argument types in `msgpack.v5` and `msgpack.v2` diff --git a/multi/call_16_test.go b/multi/call_16_test.go deleted file mode 100644 index 6c67dab3f..000000000 --- a/multi/call_16_test.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build !go_tarantool_call_17 -// +build !go_tarantool_call_17 - -package multi - -import ( - "testing" - - "github.com/tarantool/go-tarantool/v2" -) - -func TestCall(t *testing.T) { - var resp *tarantool.Response - var err error - - multiConn, err := Connect([]string{server1, server2}, connOpts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if multiConn == nil { - t.Fatalf("conn is nil after Connect") - } - defer multiConn.Close() - - // Call16 - resp, err = multiConn.Call("simple_concat", []interface{}{"t"}) - if err != nil { - t.Fatalf("Failed to use Call: %s", err.Error()) - } - if resp.Data[0].([]interface{})[0].(string) != "tt" { - t.Fatalf("result is not {{1}} : %v", resp.Data) - } -} diff --git a/multi/call_17_test.go b/multi/call_17_test.go deleted file mode 100644 index c59d7d97f..000000000 --- a/multi/call_17_test.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build go_tarantool_call_17 -// +build go_tarantool_call_17 - -package multi - -import ( - "testing" - - "github.com/tarantool/go-tarantool/v2" -) - -func TestCall(t *testing.T) { - var resp *tarantool.Response - var err error - - multiConn, err := Connect([]string{server1, server2}, connOpts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if multiConn == nil { - t.Fatalf("conn is nil after Connect") - } - defer multiConn.Close() - - // Call17 - resp, err = multiConn.Call("simple_concat", []interface{}{"t"}) - if err != nil { - t.Fatalf("Failed to use Call: %s", err.Error()) - } - if resp.Data[0].(string) != "tt" { - t.Fatalf("result is not {{1}} : %v", resp.Data) - } -} diff --git a/multi/config.lua b/multi/config.lua deleted file mode 100644 index 7364c0bd6..000000000 --- a/multi/config.lua +++ /dev/null @@ -1,49 +0,0 @@ -local nodes_load = require("config_load_nodes") - --- Do not set listen for now so connector won't be --- able to send requests until everything is configured. -box.cfg{ - work_dir = os.getenv("TEST_TNT_WORK_DIR"), - memtx_use_mvcc_engine = os.getenv("TEST_TNT_MEMTX_USE_MVCC_ENGINE") == 'true' or nil, -} - --- Function to call for getting address list, part of tarantool/multi API. -local get_cluster_nodes = nodes_load.get_cluster_nodes -rawset(_G, 'get_cluster_nodes', get_cluster_nodes) - -box.once("init", function() - local s = box.schema.space.create('test', { - id = 617, - if_not_exists = true, - }) - s:create_index('primary', {type = 'tree', parts = {1, 'string'}, if_not_exists = true}) - - box.schema.user.create('test', { password = 'test' }) - box.schema.user.grant('test', 'read,write,execute', 'universe') - - local sp = box.schema.space.create('SQL_TEST', { - id = 621, - if_not_exists = true, - format = { - {name = "NAME0", type = "unsigned"}, - {name = "NAME1", type = "string"}, - {name = "NAME2", type = "string"}, - } - }) - sp:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) - sp:insert{1, "test", "test"} - -- grants for sql tests - box.schema.user.grant('test', 'create,read,write,drop,alter', 'space') - box.schema.user.grant('test', 'create', 'sequence') -end) - -local function simple_concat(a) - return a .. a -end - -rawset(_G, 'simple_concat', simple_concat) - --- Set listen only when every other thing is configured. -box.cfg{ - listen = os.getenv("TEST_TNT_LISTEN"), -} diff --git a/multi/config_load_nodes.lua b/multi/config_load_nodes.lua deleted file mode 100644 index 4df87fc28..000000000 --- a/multi/config_load_nodes.lua +++ /dev/null @@ -1,7 +0,0 @@ -local function get_cluster_nodes() - return { 'localhost:3013', 'localhost:3014' } -end - -return { - get_cluster_nodes = get_cluster_nodes -} \ No newline at end of file diff --git a/multi/example_test.go b/multi/example_test.go deleted file mode 100644 index d22e3d1c8..000000000 --- a/multi/example_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package multi - -import ( - "fmt" - "time" - - "github.com/tarantool/go-tarantool/v2" -) - -func ExampleConnect() { - multiConn, err := Connect([]string{"127.0.0.1:3031", "127.0.0.1:3032"}, tarantool.Opts{ - Timeout: 5 * time.Second, - User: "test", - Pass: "test", - }) - if err != nil { - fmt.Printf("error in connect is %v", err) - } - fmt.Println(multiConn) -} - -func ExampleConnectWithOpts() { - multiConn, err := ConnectWithOpts([]string{"127.0.0.1:3301", "127.0.0.1:3302"}, tarantool.Opts{ - Timeout: 5 * time.Second, - User: "test", - Pass: "test", - }, OptsMulti{ - // Check for connection timeout every 1 second. - CheckTimeout: 1 * time.Second, - // Lua function name for getting address list. - NodesGetFunctionName: "get_cluster_nodes", - // Ask server for updated address list every 3 seconds. - ClusterDiscoveryTime: 3 * time.Second, - }) - if err != nil { - fmt.Printf("error in connect is %v", err) - } - fmt.Println(multiConn) -} diff --git a/multi/multi.go b/multi/multi.go deleted file mode 100644 index e3693630a..000000000 --- a/multi/multi.go +++ /dev/null @@ -1,534 +0,0 @@ -// Package with methods to work with a Tarantool cluster. -// -// Main features: -// -// - Check the active connection with a configurable time interval and switch -// to the next connection in the pool if there is a connection failure. -// -// - Get the address list from the server and reconfigure it for use in -// MultiConnection. -// -// Since: 1.5 -package multi - -import ( - "errors" - "fmt" - "sync" - "sync/atomic" - "time" - - "github.com/tarantool/go-tarantool/v2" -) - -const ( - connConnected = iota - connClosed -) - -var ( - ErrEmptyAddrs = errors.New("addrs should not be empty") - ErrWrongCheckTimeout = errors.New("wrong check timeout, must be greater than 0") - ErrNoConnection = errors.New("no active connections") -) - -func indexOf(sstring string, data []string) int { - for i, v := range data { - if sstring == v { - return i - } - } - return -1 -} - -// ConnectionMulti is a handle with connections to a number of Tarantool instances. -// -// It is created and configured with Connect function, and could not be -// reconfigured later. -type ConnectionMulti struct { - addrs []string - connOpts tarantool.Opts - opts OptsMulti - - mutex sync.RWMutex - notify chan tarantool.ConnEvent - state uint32 - control chan struct{} - pool map[string]*tarantool.Connection - fallback *tarantool.Connection -} - -var _ = tarantool.Connector(&ConnectionMulti{}) // Check compatibility with connector interface. - -// OptsMulti is a way to configure Connection with multiconnect-specific options. -type OptsMulti struct { - // CheckTimeout is a time interval to check for connection timeout and try to - // switch connection. - CheckTimeout time.Duration - // Lua function name of the server called to retrieve the address list. - NodesGetFunctionName string - // Time interval to ask the server for an updated address list (works - // if NodesGetFunctionName is set). - ClusterDiscoveryTime time.Duration -} - -// Connect creates and configures new ConnectionMulti with multiconnection options. -func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsMulti) (connMulti *ConnectionMulti, err error) { - if len(addrs) == 0 { - return nil, ErrEmptyAddrs - } - if opts.CheckTimeout <= 0 { - return nil, ErrWrongCheckTimeout - } - if opts.ClusterDiscoveryTime <= 0 { - opts.ClusterDiscoveryTime = 60 * time.Second - } - - notify := make(chan tarantool.ConnEvent, 10*len(addrs)) // x10 to accept disconnected and closed event (with a margin). - connOpts.Notify = notify - connMulti = &ConnectionMulti{ - addrs: addrs, - connOpts: connOpts.Clone(), - opts: opts, - notify: notify, - control: make(chan struct{}), - pool: make(map[string]*tarantool.Connection), - } - somebodyAlive, _ := connMulti.warmUp() - if !somebodyAlive { - connMulti.Close() - return nil, ErrNoConnection - } - go connMulti.checker() - - return connMulti, nil -} - -// Connect creates and configures new ConnectionMulti. -func Connect(addrs []string, connOpts tarantool.Opts) (connMulti *ConnectionMulti, err error) { - opts := OptsMulti{ - CheckTimeout: 1 * time.Second, - } - return ConnectWithOpts(addrs, connOpts, opts) -} - -func (connMulti *ConnectionMulti) warmUp() (somebodyAlive bool, errs []error) { - errs = make([]error, len(connMulti.addrs)) - - for i, addr := range connMulti.addrs { - conn, err := tarantool.Connect(addr, connMulti.connOpts) - errs[i] = err - if conn != nil && err == nil { - if connMulti.fallback == nil { - connMulti.fallback = conn - } - connMulti.pool[addr] = conn - if conn.ConnectedNow() { - somebodyAlive = true - } - } - } - return -} - -func (connMulti *ConnectionMulti) getState() uint32 { - return atomic.LoadUint32(&connMulti.state) -} - -func (connMulti *ConnectionMulti) getConnectionFromPool(addr string) (*tarantool.Connection, bool) { - connMulti.mutex.RLock() - defer connMulti.mutex.RUnlock() - conn, ok := connMulti.pool[addr] - return conn, ok -} - -func (connMulti *ConnectionMulti) setConnectionToPool(addr string, conn *tarantool.Connection) { - connMulti.mutex.Lock() - defer connMulti.mutex.Unlock() - connMulti.pool[addr] = conn -} - -func (connMulti *ConnectionMulti) deleteConnectionFromPool(addr string) { - connMulti.mutex.Lock() - defer connMulti.mutex.Unlock() - delete(connMulti.pool, addr) -} - -func (connMulti *ConnectionMulti) checker() { - - refreshTimer := time.NewTicker(connMulti.opts.ClusterDiscoveryTime) - timer := time.NewTicker(connMulti.opts.CheckTimeout) - defer refreshTimer.Stop() - defer timer.Stop() - - for connMulti.getState() != connClosed { - - select { - case <-connMulti.control: - return - case e := <-connMulti.notify: - if connMulti.getState() == connClosed { - return - } - if e.Conn.ClosedNow() { - addr := e.Conn.Addr() - if _, ok := connMulti.getConnectionFromPool(addr); !ok { - continue - } - conn, _ := tarantool.Connect(addr, connMulti.connOpts) - if conn != nil { - connMulti.setConnectionToPool(addr, conn) - } else { - connMulti.deleteConnectionFromPool(addr) - } - } - case <-refreshTimer.C: - if connMulti.getState() == connClosed || connMulti.opts.NodesGetFunctionName == "" { - continue - } - var resp [][]string - err := connMulti.Call17Typed(connMulti.opts.NodesGetFunctionName, []interface{}{}, &resp) - if err != nil { - continue - } - if len(resp) > 0 && len(resp[0]) > 0 { - addrs := resp[0] - // Fill pool with new connections. - for _, v := range addrs { - if indexOf(v, connMulti.addrs) < 0 { - conn, _ := tarantool.Connect(v, connMulti.connOpts) - if conn != nil { - connMulti.setConnectionToPool(v, conn) - } - } - } - // Clear pool from obsolete connections. - for _, v := range connMulti.addrs { - if indexOf(v, addrs) < 0 { - con, ok := connMulti.getConnectionFromPool(v) - if con != nil && ok { - con.Close() - } - connMulti.deleteConnectionFromPool(v) - } - } - connMulti.mutex.Lock() - connMulti.addrs = addrs - connMulti.mutex.Unlock() - } - case <-timer.C: - for _, addr := range connMulti.addrs { - if connMulti.getState() == connClosed { - return - } - if conn, ok := connMulti.getConnectionFromPool(addr); ok { - if !conn.ClosedNow() { - continue - } - } - conn, _ := tarantool.Connect(addr, connMulti.connOpts) - if conn != nil { - connMulti.setConnectionToPool(addr, conn) - } - } - } - } -} - -func (connMulti *ConnectionMulti) getCurrentConnection() *tarantool.Connection { - connMulti.mutex.RLock() - defer connMulti.mutex.RUnlock() - - for _, addr := range connMulti.addrs { - conn := connMulti.pool[addr] - if conn != nil { - if conn.ConnectedNow() { - return conn - } - connMulti.fallback = conn - } - } - return connMulti.fallback -} - -// ConnectedNow reports if connection is established at the moment. -func (connMulti *ConnectionMulti) ConnectedNow() bool { - return connMulti.getState() == connConnected && connMulti.getCurrentConnection().ConnectedNow() -} - -// Close closes Connection. -// After this method called, there is no way to reopen this Connection. -func (connMulti *ConnectionMulti) Close() (err error) { - connMulti.mutex.Lock() - defer connMulti.mutex.Unlock() - - close(connMulti.control) - atomic.StoreUint32(&connMulti.state, connClosed) - - for _, conn := range connMulti.pool { - if err == nil { - err = conn.Close() - } else { - conn.Close() - } - } - if connMulti.fallback != nil { - connMulti.fallback.Close() - } - - return -} - -// Ping sends empty request to Tarantool to check connection. -func (connMulti *ConnectionMulti) Ping() (resp *tarantool.Response, err error) { - return connMulti.getCurrentConnection().Ping() -} - -// ConfiguredTimeout returns a timeout from connection config. -func (connMulti *ConnectionMulti) ConfiguredTimeout() time.Duration { - return connMulti.getCurrentConnection().ConfiguredTimeout() -} - -// Select performs select to box space. -func (connMulti *ConnectionMulti) Select(space, index interface{}, offset, limit, iterator uint32, key interface{}) (resp *tarantool.Response, err error) { - return connMulti.getCurrentConnection().Select(space, index, offset, limit, iterator, key) -} - -// Insert performs insertion to box space. -// Tarantool will reject Insert when tuple with same primary key exists. -func (connMulti *ConnectionMulti) Insert(space interface{}, tuple interface{}) (resp *tarantool.Response, err error) { - return connMulti.getCurrentConnection().Insert(space, tuple) -} - -// Replace performs "insert or replace" action to box space. -// If tuple with same primary key exists, it will be replaced. -func (connMulti *ConnectionMulti) Replace(space interface{}, tuple interface{}) (resp *tarantool.Response, err error) { - return connMulti.getCurrentConnection().Replace(space, tuple) -} - -// Delete performs deletion of a tuple by key. -// Result will contain array with deleted tuple. -func (connMulti *ConnectionMulti) Delete(space, index interface{}, key interface{}) (resp *tarantool.Response, err error) { - return connMulti.getCurrentConnection().Delete(space, index, key) -} - -// Update performs update of a tuple by key. -// Result will contain array with updated tuple. -func (connMulti *ConnectionMulti) Update(space, index interface{}, key, ops interface{}) (resp *tarantool.Response, err error) { - return connMulti.getCurrentConnection().Update(space, index, key, ops) -} - -// Upsert performs "update or insert" action of a tuple by key. -// Result will not contain any tuple. -func (connMulti *ConnectionMulti) Upsert(space interface{}, tuple, ops interface{}) (resp *tarantool.Response, err error) { - return connMulti.getCurrentConnection().Upsert(space, tuple, ops) -} - -// Call calls registered Tarantool function. -// It uses request code for Tarantool >= 1.7 if go-tarantool -// was build with go_tarantool_call_17 tag. -// Otherwise, uses request code for Tarantool 1.6. -func (connMulti *ConnectionMulti) Call(functionName string, args interface{}) (resp *tarantool.Response, err error) { - return connMulti.getCurrentConnection().Call(functionName, args) -} - -// Call16 calls registered Tarantool function. -// It uses request code for Tarantool 1.6, so result is converted to array of -// arrays. -// Deprecated since Tarantool 1.7.2. -func (connMulti *ConnectionMulti) Call16(functionName string, args interface{}) (resp *tarantool.Response, err error) { - return connMulti.getCurrentConnection().Call16(functionName, args) -} - -// Call17 calls registered Tarantool function. -// It uses request code for Tarantool >= 1.7, so result is not converted -// (though, keep in mind, result is always array). -func (connMulti *ConnectionMulti) Call17(functionName string, args interface{}) (resp *tarantool.Response, err error) { - return connMulti.getCurrentConnection().Call17(functionName, args) -} - -// Eval passes Lua expression for evaluation. -func (connMulti *ConnectionMulti) Eval(expr string, args interface{}) (resp *tarantool.Response, err error) { - return connMulti.getCurrentConnection().Eval(expr, args) -} - -// Execute passes sql expression to Tarantool for execution. -// -// Since 1.6.0 -func (connMulti *ConnectionMulti) Execute(expr string, args interface{}) (resp *tarantool.Response, err error) { - return connMulti.getCurrentConnection().Execute(expr, args) -} - -// GetTyped performs select (with limit = 1 and offset = 0) to box space and -// fills typed result. -func (connMulti *ConnectionMulti) GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) { - return connMulti.getCurrentConnection().GetTyped(space, index, key, result) -} - -// SelectTyped performs select to box space and fills typed result. -func (connMulti *ConnectionMulti) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key interface{}, result interface{}) (err error) { - return connMulti.getCurrentConnection().SelectTyped(space, index, offset, limit, iterator, key, result) -} - -// InsertTyped performs insertion to box space. -// Tarantool will reject Insert when tuple with same primary key exists. -func (connMulti *ConnectionMulti) InsertTyped(space interface{}, tuple interface{}, result interface{}) (err error) { - return connMulti.getCurrentConnection().InsertTyped(space, tuple, result) -} - -// ReplaceTyped performs "insert or replace" action to box space. -// If tuple with same primary key exists, it will be replaced. -func (connMulti *ConnectionMulti) ReplaceTyped(space interface{}, tuple interface{}, result interface{}) (err error) { - return connMulti.getCurrentConnection().ReplaceTyped(space, tuple, result) -} - -// DeleteTyped performs deletion of a tuple by key and fills result with -// deleted tuple. -func (connMulti *ConnectionMulti) DeleteTyped(space, index interface{}, key interface{}, result interface{}) (err error) { - return connMulti.getCurrentConnection().DeleteTyped(space, index, key, result) -} - -// UpdateTyped performs update of a tuple by key and fills result with updated -// tuple. -func (connMulti *ConnectionMulti) UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) (err error) { - return connMulti.getCurrentConnection().UpdateTyped(space, index, key, ops, result) -} - -// CallTyped calls registered function. -// It uses request code for Tarantool >= 1.7 if go-tarantool -// was build with go_tarantool_call_17 tag. -// Otherwise, uses request code for Tarantool 1.6. -func (connMulti *ConnectionMulti) CallTyped(functionName string, args interface{}, result interface{}) (err error) { - return connMulti.getCurrentConnection().CallTyped(functionName, args, result) -} - -// Call16Typed calls registered function. -// It uses request code for Tarantool 1.6, so result is converted to array of -// arrays. -// Deprecated since Tarantool 1.7.2. -func (connMulti *ConnectionMulti) Call16Typed(functionName string, args interface{}, result interface{}) (err error) { - return connMulti.getCurrentConnection().Call16Typed(functionName, args, result) -} - -// Call17Typed calls registered function. -// It uses request code for Tarantool >= 1.7, so result is not converted (though, -// keep in mind, result is always array) -func (connMulti *ConnectionMulti) Call17Typed(functionName string, args interface{}, result interface{}) (err error) { - return connMulti.getCurrentConnection().Call17Typed(functionName, args, result) -} - -// EvalTyped passes Lua expression for evaluation. -func (connMulti *ConnectionMulti) EvalTyped(expr string, args interface{}, result interface{}) (err error) { - return connMulti.getCurrentConnection().EvalTyped(expr, args, result) -} - -// ExecuteTyped passes sql expression to Tarantool for execution. -func (connMulti *ConnectionMulti) ExecuteTyped(expr string, args interface{}, result interface{}) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) { - return connMulti.getCurrentConnection().ExecuteTyped(expr, args, result) -} - -// SelectAsync sends select request to Tarantool and returns Future. -func (connMulti *ConnectionMulti) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *tarantool.Future { - return connMulti.getCurrentConnection().SelectAsync(space, index, offset, limit, iterator, key) -} - -// InsertAsync sends insert action to Tarantool and returns Future. -// Tarantool will reject Insert when tuple with same primary key exists. -func (connMulti *ConnectionMulti) InsertAsync(space interface{}, tuple interface{}) *tarantool.Future { - return connMulti.getCurrentConnection().InsertAsync(space, tuple) -} - -// ReplaceAsync sends "insert or replace" action to Tarantool and returns Future. -// If tuple with same primary key exists, it will be replaced. -func (connMulti *ConnectionMulti) ReplaceAsync(space interface{}, tuple interface{}) *tarantool.Future { - return connMulti.getCurrentConnection().ReplaceAsync(space, tuple) -} - -// DeleteAsync sends deletion action to Tarantool and returns Future. -// Future's result will contain array with deleted tuple. -func (connMulti *ConnectionMulti) DeleteAsync(space, index interface{}, key interface{}) *tarantool.Future { - return connMulti.getCurrentConnection().DeleteAsync(space, index, key) -} - -// Update sends deletion of a tuple by key and returns Future. -// Future's result will contain array with updated tuple. -func (connMulti *ConnectionMulti) UpdateAsync(space, index interface{}, key, ops interface{}) *tarantool.Future { - return connMulti.getCurrentConnection().UpdateAsync(space, index, key, ops) -} - -// UpsertAsync sends "update or insert" action to Tarantool and returns Future. -// Future's sesult will not contain any tuple. -func (connMulti *ConnectionMulti) UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *tarantool.Future { - return connMulti.getCurrentConnection().UpsertAsync(space, tuple, ops) -} - -// CallAsync sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool >= 1.7 if go-tarantool -// was build with go_tarantool_call_17 tag. -// Otherwise, uses request code for Tarantool 1.6. -func (connMulti *ConnectionMulti) CallAsync(functionName string, args interface{}) *tarantool.Future { - return connMulti.getCurrentConnection().CallAsync(functionName, args) -} - -// Call16Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool 1.6, so future's result is always array -// of arrays. -// Deprecated since Tarantool 1.7.2. -func (connMulti *ConnectionMulti) Call16Async(functionName string, args interface{}) *tarantool.Future { - return connMulti.getCurrentConnection().Call16Async(functionName, args) -} - -// Call17Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool >= 1.7, so future's result will not be converted -// (though, keep in mind, result is always array). -func (connMulti *ConnectionMulti) Call17Async(functionName string, args interface{}) *tarantool.Future { - return connMulti.getCurrentConnection().Call17Async(functionName, args) -} - -// EvalAsync passes Lua expression for evaluation. -func (connMulti *ConnectionMulti) EvalAsync(expr string, args interface{}) *tarantool.Future { - return connMulti.getCurrentConnection().EvalAsync(expr, args) -} - -// ExecuteAsync passes sql expression to Tarantool for execution. -func (connMulti *ConnectionMulti) ExecuteAsync(expr string, args interface{}) *tarantool.Future { - return connMulti.getCurrentConnection().ExecuteAsync(expr, args) -} - -// NewPrepared passes a sql statement to Tarantool for preparation synchronously. -func (connMulti *ConnectionMulti) NewPrepared(expr string) (*tarantool.Prepared, error) { - return connMulti.getCurrentConnection().NewPrepared(expr) -} - -// NewStream creates new Stream object for connection. -// -// Since v. 2.10.0, Tarantool supports streams and interactive transactions over them. -// To use interactive transactions, memtx_use_mvcc_engine box option should be set to true. -// Since 1.7.0 -func (connMulti *ConnectionMulti) NewStream() (*tarantool.Stream, error) { - return connMulti.getCurrentConnection().NewStream() -} - -// NewWatcher does not supported by the ConnectionMulti. The ConnectionMulti is -// deprecated: use ConnectionPool instead. -// -// Since 1.10.0 -func (connMulti *ConnectionMulti) NewWatcher(key string, - callback tarantool.WatchCallback) (tarantool.Watcher, error) { - return nil, errors.New("ConnectionMulti is deprecated " + - "use ConnectionPool") -} - -// Do sends the request and returns a future. -func (connMulti *ConnectionMulti) Do(req tarantool.Request) *tarantool.Future { - if connectedReq, ok := req.(tarantool.ConnectedRequest); ok { - _, belongs := connMulti.getConnectionFromPool(connectedReq.Conn().Addr()) - if !belongs { - fut := tarantool.NewFuture() - fut.SetError(fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool")) - return fut - } - return connectedReq.Conn().Do(req) - } - return connMulti.getCurrentConnection().Do(req) -} diff --git a/multi/multi_test.go b/multi/multi_test.go deleted file mode 100644 index dbb783860..000000000 --- a/multi/multi_test.go +++ /dev/null @@ -1,647 +0,0 @@ -package multi - -import ( - "fmt" - "log" - "os" - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" -) - -var server1 = "127.0.0.1:3013" -var server2 = "127.0.0.1:3014" -var spaceNo = uint32(617) -var spaceName = "test" -var indexNo = uint32(0) -var connOpts = tarantool.Opts{ - Timeout: 5 * time.Second, - User: "test", - Pass: "test", -} - -var connOptsMulti = OptsMulti{ - CheckTimeout: 1 * time.Second, - NodesGetFunctionName: "get_cluster_nodes", - ClusterDiscoveryTime: 3 * time.Second, -} - -var instances []test_helpers.TarantoolInstance - -func TestConnError_IncorrectParams(t *testing.T) { - multiConn, err := Connect([]string{}, tarantool.Opts{}) - if err == nil { - t.Fatalf("err is nil with incorrect params") - } - if multiConn != nil { - t.Fatalf("conn is not nill with incorrect params") - } - if err.Error() != "addrs should not be empty" { - t.Errorf("incorrect error: %s", err.Error()) - } - - multiConn, err = ConnectWithOpts([]string{server1}, tarantool.Opts{}, OptsMulti{}) - if err == nil { - t.Fatal("err is nil with incorrect params") - } - if multiConn != nil { - t.Fatal("conn is not nill with incorrect params") - } - if err.Error() != "wrong check timeout, must be greater than 0" { - t.Errorf("incorrect error: %s", err.Error()) - } -} - -func TestConnError_Connection(t *testing.T) { - multiConn, err := Connect([]string{"err1", "err2"}, connOpts) - if err == nil { - t.Errorf("err is nil with incorrect params") - return - } - if multiConn != nil { - t.Errorf("conn is not nil with incorrect params") - return - } -} - -func TestConnSuccessfully(t *testing.T) { - multiConn, err := Connect([]string{"err", server1}, connOpts) - if err != nil { - t.Errorf("Failed to connect: %s", err.Error()) - return - } - if multiConn == nil { - t.Errorf("conn is nil after Connect") - return - } - defer multiConn.Close() - - if !multiConn.ConnectedNow() { - t.Errorf("conn has incorrect status") - return - } - if multiConn.getCurrentConnection().Addr() != server1 { - t.Errorf("conn has incorrect addr") - return - } -} - -func TestReconnect(t *testing.T) { - sleep := 100 * time.Millisecond - sleepCnt := 50 - servers := []string{server1, server2} - multiConn, _ := Connect(servers, connOpts) - if multiConn == nil { - t.Errorf("conn is nil after Connect") - return - } - test_helpers.StopTarantoolWithCleanup(instances[0]) - - for i := 0; i < sleepCnt; i++ { - _, ok := multiConn.getConnectionFromPool(servers[0]) - if !ok { - break - } - time.Sleep(sleep) - } - - _, ok := multiConn.getConnectionFromPool(servers[0]) - if ok { - t.Fatalf("failed to close conn") - } - - if multiConn.getCurrentConnection().Addr() == servers[0] { - t.Errorf("conn has incorrect addr: %s after disconnect server1", multiConn.getCurrentConnection().Addr()) - } - - err := test_helpers.RestartTarantool(&instances[0]) - if err != nil { - t.Fatalf("failed to restart Tarantool: %s", err) - } - - for i := 0; i < sleepCnt; i++ { - _, ok := multiConn.getConnectionFromPool(servers[0]) - if ok { - break - } - time.Sleep(sleep) - } - - _, ok = multiConn.getConnectionFromPool(servers[0]) - if !ok { - t.Fatalf("incorrect conn status after reconnecting") - } -} - -func TestDisconnectAll(t *testing.T) { - sleep := 100 * time.Millisecond - sleepCnt := int((time.Second / sleep) * 2) // Checkout time * 2. - - servers := []string{server1, server2} - multiConn, _ := Connect(servers, connOpts) - if multiConn == nil { - t.Errorf("conn is nil after Connect") - return - } - - for _, inst := range instances { - test_helpers.StopTarantoolWithCleanup(inst) - } - - for i := 0; i < sleepCnt && multiConn.ConnectedNow(); i++ { - time.Sleep(sleep) - } - - if multiConn.ConnectedNow() { - t.Errorf("incorrect status after desconnect all") - } - - for _, inst := range instances { - err := test_helpers.RestartTarantool(&inst) - if err != nil { - t.Fatalf("failed to restart Tarantool: %s", err) - } - } - - for i := 0; i < sleepCnt && !multiConn.ConnectedNow(); i++ { - time.Sleep(sleep) - } - - if !multiConn.ConnectedNow() { - t.Errorf("incorrect multiConn status after reconnecting") - } -} - -func TestClose(t *testing.T) { - multiConn, _ := Connect([]string{server1, server2}, connOpts) - if multiConn == nil { - t.Errorf("conn is nil after Connect") - return - } - timer := time.NewTimer(300 * time.Millisecond) - <-timer.C - - conn, _ := multiConn.getConnectionFromPool(server1) - if !conn.ConnectedNow() { - t.Errorf("incorrect conn server1 status") - } - conn, _ = multiConn.getConnectionFromPool(server2) - if !conn.ConnectedNow() { - t.Errorf("incorrect conn server2 status") - } - - multiConn.Close() - timer = time.NewTimer(100 * time.Millisecond) - <-timer.C - - if multiConn.ConnectedNow() { - t.Errorf("incorrect multiConn status after close") - } - conn, _ = multiConn.getConnectionFromPool(server1) - if conn.ConnectedNow() { - t.Errorf("incorrect server1 conn status after close") - } - conn, _ = multiConn.getConnectionFromPool(server2) - if conn.ConnectedNow() { - t.Errorf("incorrect server2 conn status after close") - } -} - -func TestRefresh(t *testing.T) { - - multiConn, _ := ConnectWithOpts([]string{server1, server2}, connOpts, connOptsMulti) - if multiConn == nil { - t.Errorf("conn is nil after Connect") - return - } - - multiConn.mutex.RLock() - curAddr := multiConn.addrs[0] - multiConn.mutex.RUnlock() - - // Wait for refresh timer. - // Scenario 1 nodeload, 1 refresh, 1 nodeload. - time.Sleep(10 * time.Second) - - multiConn.mutex.RLock() - newAddr := multiConn.addrs[0] - multiConn.mutex.RUnlock() - - if curAddr == newAddr { - t.Errorf("Expect address refresh") - } - - if !multiConn.ConnectedNow() { - t.Errorf("Expect connection to exist") - } - - _, err := multiConn.Call17(multiConn.opts.NodesGetFunctionName, []interface{}{}) - if err != nil { - t.Error("Expect to get data after reconnect") - } -} - -func TestCall17(t *testing.T) { - var resp *tarantool.Response - var err error - - multiConn, err := Connect([]string{server1, server2}, connOpts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if multiConn == nil { - t.Fatalf("conn is nil after Connect") - } - defer multiConn.Close() - - // Call17 - resp, err = multiConn.Call17("simple_concat", []interface{}{"s"}) - if err != nil { - t.Fatalf("Failed to use Call: %s", err.Error()) - } - if resp.Data[0].(string) != "ss" { - t.Fatalf("result is not {{1}} : %v", resp.Data) - } -} - -func TestNewPrepared(t *testing.T) { - test_helpers.SkipIfSQLUnsupported(t) - - multiConn, err := Connect([]string{server1, server2}, connOpts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if multiConn == nil { - t.Fatalf("conn is nil after Connect") - } - defer multiConn.Close() - - stmt, err := multiConn.NewPrepared("SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:id AND NAME1=:name;") - require.Nilf(t, err, "fail to prepare statement: %v", err) - - executeReq := tarantool.NewExecutePreparedRequest(stmt) - unprepareReq := tarantool.NewUnprepareRequest(stmt) - - resp, err := multiConn.Do(executeReq.Args([]interface{}{1, "test"})).Get() - if err != nil { - t.Fatalf("failed to execute prepared: %v", err) - } - if resp == nil { - t.Fatalf("nil response") - } - if resp.Code != tarantool.OkCode { - t.Fatalf("failed to execute prepared: code %d", resp.Code) - } - if reflect.DeepEqual(resp.Data[0], []interface{}{1, "test"}) { - t.Error("Select with named arguments failed") - } - if resp.MetaData[0].FieldType != "unsigned" || - resp.MetaData[0].FieldName != "NAME0" || - resp.MetaData[1].FieldType != "string" || - resp.MetaData[1].FieldName != "NAME1" { - t.Error("Wrong metadata") - } - - // the second argument for unprepare request is unused - it already belongs to some connection - resp, err = multiConn.Do(unprepareReq).Get() - if err != nil { - t.Errorf("failed to unprepare prepared statement: %v", err) - } - if resp.Code != tarantool.OkCode { - t.Errorf("failed to unprepare prepared statement: code %d", resp.Code) - } - - _, err = multiConn.Do(unprepareReq).Get() - if err == nil { - t.Errorf("the statement must be already unprepared") - } - require.Contains(t, err.Error(), "Prepared statement with id") - - _, err = multiConn.Do(executeReq).Get() - if err == nil { - t.Errorf("the statement must be already unprepared") - } - require.Contains(t, err.Error(), "Prepared statement with id") -} - -func TestDoWithStrangerConn(t *testing.T) { - expectedErr := fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool") - - multiConn, err := Connect([]string{server1, server2}, connOpts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if multiConn == nil { - t.Fatalf("conn is nil after Connect") - } - defer multiConn.Close() - - req := test_helpers.NewStrangerRequest() - - _, err = multiConn.Do(req).Get() - if err == nil { - t.Fatalf("nil error caught") - } - if err.Error() != expectedErr.Error() { - t.Fatalf("Unexpected error caught") - } -} - -func TestStream_Commit(t *testing.T) { - var req tarantool.Request - var resp *tarantool.Response - var err error - - test_helpers.SkipIfStreamsUnsupported(t) - - multiConn, err := Connect([]string{server1, server2}, connOpts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if multiConn == nil { - t.Fatalf("conn is nil after Connect") - } - defer multiConn.Close() - - stream, _ := multiConn.NewStream() - - // Begin transaction - req = tarantool.NewBeginRequest() - resp, err = stream.Do(req).Get() - if err != nil { - t.Fatalf("Failed to Begin: %s", err.Error()) - } - if resp.Code != tarantool.OkCode { - t.Fatalf("Failed to Begin: wrong code returned %d", resp.Code) - } - - // Insert in stream - req = tarantool.NewInsertRequest(spaceName). - Tuple([]interface{}{"1001", "hello2", "world2"}) - resp, err = stream.Do(req).Get() - if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) - } - if resp.Code != tarantool.OkCode { - t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) - } - defer test_helpers.DeleteRecordByKey(t, multiConn, spaceNo, indexNo, []interface{}{"1001"}) - - // Select not related to the transaction - // while transaction is not committed - // result of select is empty - selectReq := tarantool.NewSelectRequest(spaceNo). - Index(indexNo). - Limit(1). - Iterator(tarantool.IterEq). - Key([]interface{}{"1001"}) - resp, err = multiConn.Do(selectReq).Get() - if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) - } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != 0 { - t.Fatalf("Response Data len != 0") - } - - // Select in stream - resp, err = stream.Do(selectReq).Get() - if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) - } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != 1 { - t.Fatalf("Response Data len != 1") - } - if tpl, ok := resp.Data[0].([]interface{}); !ok { - t.Fatalf("Unexpected body of Select") - } else { - if id, ok := tpl[0].(string); !ok || id != "1001" { - t.Fatalf("Unexpected body of Select (0)") - } - if h, ok := tpl[1].(string); !ok || h != "hello2" { - t.Fatalf("Unexpected body of Select (1)") - } - if h, ok := tpl[2].(string); !ok || h != "world2" { - t.Fatalf("Unexpected body of Select (2)") - } - } - - // Commit transaction - req = tarantool.NewCommitRequest() - resp, err = stream.Do(req).Get() - if err != nil { - t.Fatalf("Failed to Commit: %s", err.Error()) - } - if resp.Code != tarantool.OkCode { - t.Fatalf("Failed to Commit: wrong code returned %d", resp.Code) - } - - // Select outside of transaction - resp, err = multiConn.Do(selectReq).Get() - if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) - } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != 1 { - t.Fatalf("Response Data len != 1") - } - if tpl, ok := resp.Data[0].([]interface{}); !ok { - t.Fatalf("Unexpected body of Select") - } else { - if id, ok := tpl[0].(string); !ok || id != "1001" { - t.Fatalf("Unexpected body of Select (0)") - } - if h, ok := tpl[1].(string); !ok || h != "hello2" { - t.Fatalf("Unexpected body of Select (1)") - } - if h, ok := tpl[2].(string); !ok || h != "world2" { - t.Fatalf("Unexpected body of Select (2)") - } - } -} - -func TestStream_Rollback(t *testing.T) { - var req tarantool.Request - var resp *tarantool.Response - var err error - - test_helpers.SkipIfStreamsUnsupported(t) - - multiConn, err := Connect([]string{server1, server2}, connOpts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if multiConn == nil { - t.Fatalf("conn is nil after Connect") - } - defer multiConn.Close() - - stream, _ := multiConn.NewStream() - - // Begin transaction - req = tarantool.NewBeginRequest() - resp, err = stream.Do(req).Get() - if err != nil { - t.Fatalf("Failed to Begin: %s", err.Error()) - } - if resp.Code != tarantool.OkCode { - t.Fatalf("Failed to Begin: wrong code returned %d", resp.Code) - } - - // Insert in stream - req = tarantool.NewInsertRequest(spaceName). - Tuple([]interface{}{"1001", "hello2", "world2"}) - resp, err = stream.Do(req).Get() - if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) - } - if resp.Code != tarantool.OkCode { - t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) - } - defer test_helpers.DeleteRecordByKey(t, multiConn, spaceNo, indexNo, []interface{}{"1001"}) - - // Select not related to the transaction - // while transaction is not committed - // result of select is empty - selectReq := tarantool.NewSelectRequest(spaceNo). - Index(indexNo). - Limit(1). - Iterator(tarantool.IterEq). - Key([]interface{}{"1001"}) - resp, err = multiConn.Do(selectReq).Get() - if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) - } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != 0 { - t.Fatalf("Response Data len != 0") - } - - // Select in stream - resp, err = stream.Do(selectReq).Get() - if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) - } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != 1 { - t.Fatalf("Response Data len != 1") - } - if tpl, ok := resp.Data[0].([]interface{}); !ok { - t.Fatalf("Unexpected body of Select") - } else { - if id, ok := tpl[0].(string); !ok || id != "1001" { - t.Fatalf("Unexpected body of Select (0)") - } - if h, ok := tpl[1].(string); !ok || h != "hello2" { - t.Fatalf("Unexpected body of Select (1)") - } - if h, ok := tpl[2].(string); !ok || h != "world2" { - t.Fatalf("Unexpected body of Select (2)") - } - } - - // Rollback transaction - req = tarantool.NewRollbackRequest() - resp, err = stream.Do(req).Get() - if err != nil { - t.Fatalf("Failed to Rollback: %s", err.Error()) - } - if resp.Code != tarantool.OkCode { - t.Fatalf("Failed to Rollback: wrong code returned %d", resp.Code) - } - - // Select outside of transaction - resp, err = multiConn.Do(selectReq).Get() - if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) - } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != 0 { - t.Fatalf("Response Data len != 0") - } -} - -func TestConnectionMulti_NewWatcher(t *testing.T) { - test_helpers.SkipIfStreamsUnsupported(t) - - multiConn, err := Connect([]string{server1, server2}, connOpts) - if err != nil { - t.Fatalf("Failed to connect: %s", err.Error()) - } - if multiConn == nil { - t.Fatalf("conn is nil after Connect") - } - defer multiConn.Close() - - watcher, err := multiConn.NewWatcher("foo", func(event tarantool.WatchEvent) {}) - if watcher != nil { - t.Errorf("Unexpected watcher") - } - if err == nil { - t.Fatalf("Unexpected success") - } - if err.Error() != "ConnectionMulti is deprecated use ConnectionPool" { - t.Fatalf("Unexpected error: %s", err) - } -} - -// runTestMain is a body of TestMain function -// (see https://pkg.go.dev/testing#hdr-Main). -// Using defer + os.Exit is not works so TestMain body -// is a separate function, see -// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls -func runTestMain(m *testing.M) int { - initScript := "config.lua" - waitStart := 100 * time.Millisecond - connectRetry := 10 - retryTimeout := 500 * time.Millisecond - - // Tarantool supports streams and interactive transactions since version 2.10.0 - isStreamUnsupported, err := test_helpers.IsTarantoolVersionLess(2, 10, 0) - if err != nil { - log.Fatalf("Could not check the Tarantool version") - } - - servers := []string{server1, server2} - instances, err = test_helpers.StartTarantoolInstances(servers, nil, test_helpers.StartOpts{ - InitScript: initScript, - User: connOpts.User, - Pass: connOpts.Pass, - WaitStart: waitStart, - ConnectRetry: connectRetry, - RetryTimeout: retryTimeout, - MemtxUseMvccEngine: !isStreamUnsupported, - }) - - if err != nil { - log.Fatalf("Failed to prepare test tarantool: %s", err) - return -1 - } - - defer test_helpers.StopTarantoolInstances(instances) - - return m.Run() -} - -func TestMain(m *testing.M) { - code := runTestMain(m) - os.Exit(code) -} From c66accd45a58a06f26e238a4db93b20f904f7a45 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 22 Dec 2022 09:16:10 +0300 Subject: [PATCH 449/605] api: rename connection_pool to pool By convention, packages are given lower case, single-word names; there should be no need for underscores or mixedCaps [1]. 1. https://go.dev/doc/effective_go#package-names Closes #239 --- CHANGELOG.md | 2 + Makefile | 8 +- README.md | 10 +- {connection_pool => pool}/call_16_test.go | 14 +- {connection_pool => pool}/call_17_test.go | 14 +- {connection_pool => pool}/config.lua | 0 {connection_pool => pool}/connection_pool.go | 14 +- .../connection_pool_test.go | 390 +++++++++--------- {connection_pool => pool}/connector.go | 2 +- {connection_pool => pool}/connector_test.go | 4 +- {connection_pool => pool}/const.go | 2 +- {connection_pool => pool}/example_test.go | 186 ++++----- .../msgpack_helper_test.go | 2 +- .../msgpack_v5_helper_test.go | 2 +- {connection_pool => pool}/pooler.go | 2 +- {connection_pool => pool}/round_robin.go | 2 +- {connection_pool => pool}/round_robin_test.go | 4 +- {connection_pool => pool}/state.go | 2 +- {connection_pool => pool}/watcher.go | 2 +- queue/example_connection_pool_test.go | 22 +- test_helpers/pool_helper.go | 10 +- 21 files changed, 352 insertions(+), 342 deletions(-) rename {connection_pool => pool}/call_16_test.go (82%) rename {connection_pool => pool}/call_17_test.go (81%) rename {connection_pool => pool}/config.lua (100%) rename {connection_pool => pool}/connection_pool.go (99%) rename {connection_pool => pool}/connection_pool_test.go (88%) rename {connection_pool => pool}/connector.go (99%) rename {connection_pool => pool}/connector_test.go (99%) rename {connection_pool => pool}/const.go (97%) rename {connection_pool => pool}/example_test.go (83%) rename {connection_pool => pool}/msgpack_helper_test.go (83%) rename {connection_pool => pool}/msgpack_v5_helper_test.go (83%) rename {connection_pool => pool}/pooler.go (99%) rename {connection_pool => pool}/round_robin.go (98%) rename {connection_pool => pool}/round_robin_test.go (95%) rename {connection_pool => pool}/state.go (94%) rename {connection_pool => pool}/watcher.go (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5a8743c5..5e21632c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Changed +- connection_pool renamed to pool (#239) + ### Removed - multi subpackage (#240) diff --git a/Makefile b/Makefile index b1d808cda..f48b789cc 100644 --- a/Makefile +++ b/Makefile @@ -57,11 +57,11 @@ testrace: go clean -testcache go test -race -tags "$(TAGS)" ./... -v -p 1 -.PHONY: test-connection-pool -test-connection-pool: - @echo "Running tests in connection_pool package" +.PHONY: test-pool +test-pool: + @echo "Running tests in pool package" go clean -testcache - go test -tags "$(TAGS)" ./connection_pool/ -v -p 1 + go test -tags "$(TAGS)" ./pool/ -v -p 1 .PHONY: test-datetime test-datetime: diff --git a/README.md b/README.md index 8b7b2fbb4..8151f71e2 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ faster than other packages according to public benchmarks. * [Walking\-through example](#walking-through-example) * [Migration to v2](#migration-to-v2) * [multi package](#multi-package) + * [pool package](#pool-package) * [msgpack.v5](#msgpackv5) * [Contributing](#contributing) * [Alternative connectors](#alternative-connectors) @@ -161,7 +162,14 @@ The article describes migration from go-tarantool to go-tarantool/v2. #### multi package -The subpackage has been deleted. You could use `connection_pool` instead. +The subpackage has been deleted. You could use `pool` instead. + +#### pool package + +The logic has not changed, but there are a few renames: + +* The `connection_pool` subpackage has been renamed to `pool`. +* The type `PoolOpts` has been renamed to `Opts`. #### msgpack.v5 diff --git a/connection_pool/call_16_test.go b/pool/call_16_test.go similarity index 82% rename from connection_pool/call_16_test.go rename to pool/call_16_test.go index 806c67ba0..04b8898bc 100644 --- a/connection_pool/call_16_test.go +++ b/pool/call_16_test.go @@ -1,13 +1,13 @@ //go:build !go_tarantool_call_17 // +build !go_tarantool_call_17 -package connection_pool_test +package pool_test import ( "testing" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/connection_pool" + "github.com/tarantool/go-tarantool/v2/pool" "github.com/tarantool/go-tarantool/v2/test_helpers" ) @@ -17,14 +17,14 @@ func TestCall(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() // PreferRO - resp, err := connPool.Call("box.info", []interface{}{}, connection_pool.PreferRO) + resp, err := connPool.Call("box.info", []interface{}{}, pool.PreferRO) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") @@ -35,7 +35,7 @@ func TestCall(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `PreferRO`") // PreferRW - resp, err = connPool.Call("box.info", []interface{}{}, connection_pool.PreferRW) + resp, err = connPool.Call("box.info", []interface{}{}, pool.PreferRW) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") @@ -46,7 +46,7 @@ func TestCall(t *testing.T) { require.Falsef(t, ro, "expected `false` with mode `PreferRW`") // RO - resp, err = connPool.Call("box.info", []interface{}{}, connection_pool.RO) + resp, err = connPool.Call("box.info", []interface{}{}, pool.RO) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") @@ -57,7 +57,7 @@ func TestCall(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `RO`") // RW - resp, err = connPool.Call("box.info", []interface{}{}, connection_pool.RW) + resp, err = connPool.Call("box.info", []interface{}{}, pool.RW) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") diff --git a/connection_pool/call_17_test.go b/pool/call_17_test.go similarity index 81% rename from connection_pool/call_17_test.go rename to pool/call_17_test.go index 16f1a5e6b..6ca8381ad 100644 --- a/connection_pool/call_17_test.go +++ b/pool/call_17_test.go @@ -1,13 +1,13 @@ //go:build go_tarantool_call_17 // +build go_tarantool_call_17 -package connection_pool_test +package pool_test import ( "testing" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/connection_pool" + "github.com/tarantool/go-tarantool/v2/pool" "github.com/tarantool/go-tarantool/v2/test_helpers" ) @@ -17,14 +17,14 @@ func TestCall(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() // PreferRO - resp, err := connPool.Call("box.info", []interface{}{}, connection_pool.PreferRO) + resp, err := connPool.Call("box.info", []interface{}{}, pool.PreferRO) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") @@ -35,7 +35,7 @@ func TestCall(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `PreferRO`") // PreferRW - resp, err = connPool.Call("box.info", []interface{}{}, connection_pool.PreferRW) + resp, err = connPool.Call("box.info", []interface{}{}, pool.PreferRW) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") @@ -46,7 +46,7 @@ func TestCall(t *testing.T) { require.Falsef(t, ro, "expected `false` with mode `PreferRW`") // RO - resp, err = connPool.Call("box.info", []interface{}{}, connection_pool.RO) + resp, err = connPool.Call("box.info", []interface{}{}, pool.RO) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") @@ -57,7 +57,7 @@ func TestCall(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `RO`") // RW - resp, err = connPool.Call("box.info", []interface{}{}, connection_pool.RW) + resp, err = connPool.Call("box.info", []interface{}{}, pool.RW) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") diff --git a/connection_pool/config.lua b/pool/config.lua similarity index 100% rename from connection_pool/config.lua rename to pool/config.lua diff --git a/connection_pool/connection_pool.go b/pool/connection_pool.go similarity index 99% rename from connection_pool/connection_pool.go rename to pool/connection_pool.go index d1b7de9f5..6d0865d57 100644 --- a/connection_pool/connection_pool.go +++ b/pool/connection_pool.go @@ -8,7 +8,7 @@ // - Automatic master discovery by mode parameter. // // Since: 1.6.0 -package connection_pool +package pool import ( "errors" @@ -59,8 +59,8 @@ type ConnectionHandler interface { Deactivated(conn *tarantool.Connection, role Role) error } -// OptsPool provides additional options (configurable via ConnectWithOpts). -type OptsPool struct { +// Opts provides additional options (configurable via ConnectWithOpts). +type Opts struct { // Timeout for timer to reopen connections that have been closed by some // events and to relocate connection between subpools if ro/rw role has // been updated. @@ -93,7 +93,7 @@ type ConnectionPool struct { addrsMutex sync.RWMutex connOpts tarantool.Opts - opts OptsPool + opts Opts state state done chan struct{} @@ -132,7 +132,7 @@ func newEndpoint(addr string) *endpoint { // ConnectWithOpts creates pool for instances with addresses addrs // with options opts. -func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (connPool *ConnectionPool, err error) { +func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts Opts) (connPool *ConnectionPool, err error) { if len(addrs) == 0 { return nil, ErrEmptyAddrs } @@ -179,9 +179,9 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts OptsPool) (co // // It is useless to set up tarantool.Opts.Reconnect value for a connection. // The connection pool has its own reconnection logic. See -// OptsPool.CheckTimeout description. +// Opts.CheckTimeout description. func Connect(addrs []string, connOpts tarantool.Opts) (connPool *ConnectionPool, err error) { - opts := OptsPool{ + opts := Opts{ CheckTimeout: 1 * time.Second, } return ConnectWithOpts(addrs, connOpts, opts) diff --git a/connection_pool/connection_pool_test.go b/pool/connection_pool_test.go similarity index 88% rename from connection_pool/connection_pool_test.go rename to pool/connection_pool_test.go index e6cb31db7..f96afd2f3 100644 --- a/connection_pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -1,4 +1,4 @@ -package connection_pool_test +package pool_test import ( "fmt" @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/connection_pool" + "github.com/tarantool/go-tarantool/v2/pool" "github.com/tarantool/go-tarantool/v2/test_helpers" ) @@ -45,17 +45,17 @@ var defaultTimeoutRetry = 500 * time.Millisecond var instances []test_helpers.TarantoolInstance func TestConnError_IncorrectParams(t *testing.T) { - connPool, err := connection_pool.Connect([]string{}, tarantool.Opts{}) + connPool, err := pool.Connect([]string{}, tarantool.Opts{}) require.Nilf(t, connPool, "conn is not nil with incorrect param") require.NotNilf(t, err, "err is nil with incorrect params") require.Equal(t, "addrs (first argument) should not be empty", err.Error()) - connPool, err = connection_pool.Connect([]string{"err1", "err2"}, connOpts) + connPool, err = pool.Connect([]string{"err1", "err2"}, connOpts) require.Nilf(t, connPool, "conn is not nil with incorrect param") require.NotNilf(t, err, "err is nil with incorrect params") require.Equal(t, "no active connections", err.Error()) - connPool, err = connection_pool.ConnectWithOpts(servers, tarantool.Opts{}, connection_pool.OptsPool{}) + connPool, err = pool.ConnectWithOpts(servers, tarantool.Opts{}, pool.Opts{}) require.Nilf(t, connPool, "conn is not nil with incorrect param") require.NotNilf(t, err, "err is nil with incorrect params") require.Equal(t, "wrong check timeout, must be greater than 0", err.Error()) @@ -63,7 +63,7 @@ func TestConnError_IncorrectParams(t *testing.T) { func TestConnSuccessfully(t *testing.T) { server := servers[0] - connPool, err := connection_pool.Connect([]string{"err", server}, connOpts) + connPool, err := pool.Connect([]string{"err", server}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -71,7 +71,7 @@ func TestConnSuccessfully(t *testing.T) { args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: []string{server}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -85,7 +85,7 @@ func TestConnSuccessfully(t *testing.T) { func TestConnSuccessfullyDuplicates(t *testing.T) { server := servers[0] - connPool, err := connection_pool.Connect([]string{server, server, server, server}, connOpts) + connPool, err := pool.Connect([]string{server, server, server, server}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -93,7 +93,7 @@ func TestConnSuccessfullyDuplicates(t *testing.T) { args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: []string{server}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -111,7 +111,7 @@ func TestConnSuccessfullyDuplicates(t *testing.T) { func TestReconnect(t *testing.T) { server := servers[0] - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -121,7 +121,7 @@ func TestReconnect(t *testing.T) { args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: []string{server}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -137,7 +137,7 @@ func TestReconnect(t *testing.T) { args = test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: []string{server}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -155,7 +155,7 @@ func TestDisconnect_withReconnect(t *testing.T) { opts := connOpts opts.Reconnect = 10 * time.Second - connPool, err := connection_pool.Connect([]string{servers[serverId]}, opts) + connPool, err := pool.Connect([]string{servers[serverId]}, opts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -165,7 +165,7 @@ func TestDisconnect_withReconnect(t *testing.T) { test_helpers.StopTarantoolWithCleanup(instances[serverId]) args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: []string{servers[serverId]}, ExpectedPoolStatus: false, ExpectedStatuses: map[string]bool{ @@ -182,7 +182,7 @@ func TestDisconnect_withReconnect(t *testing.T) { args = test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: []string{servers[serverId]}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -199,7 +199,7 @@ func TestDisconnectAll(t *testing.T) { server1 := servers[0] server2 := servers[1] - connPool, err := connection_pool.Connect([]string{server1, server2}, connOpts) + connPool, err := pool.Connect([]string{server1, server2}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -210,7 +210,7 @@ func TestDisconnectAll(t *testing.T) { args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: []string{server1, server2}, ExpectedPoolStatus: false, ExpectedStatuses: map[string]bool{ @@ -230,7 +230,7 @@ func TestDisconnectAll(t *testing.T) { args = test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: []string{server1, server2}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -244,7 +244,7 @@ func TestDisconnectAll(t *testing.T) { } func TestAdd(t *testing.T) { - connPool, err := connection_pool.Connect([]string{servers[0]}, connOpts) + connPool, err := pool.Connect([]string{servers[0]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -257,7 +257,7 @@ func TestAdd(t *testing.T) { args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: servers, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -275,18 +275,18 @@ func TestAdd(t *testing.T) { } func TestAdd_exist(t *testing.T) { - connPool, err := connection_pool.Connect([]string{servers[0]}, connOpts) + connPool, err := pool.Connect([]string{servers[0]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() err = connPool.Add(servers[0]) - require.Equal(t, connection_pool.ErrExists, err) + require.Equal(t, pool.ErrExists, err) args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: servers, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -300,7 +300,7 @@ func TestAdd_exist(t *testing.T) { } func TestAdd_unreachable(t *testing.T) { - connPool, err := connection_pool.Connect([]string{servers[0]}, connOpts) + connPool, err := pool.Connect([]string{servers[0]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -312,7 +312,7 @@ func TestAdd_unreachable(t *testing.T) { args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: servers, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -326,17 +326,17 @@ func TestAdd_unreachable(t *testing.T) { } func TestAdd_afterClose(t *testing.T) { - connPool, err := connection_pool.Connect([]string{servers[0]}, connOpts) + connPool, err := pool.Connect([]string{servers[0]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") connPool.Close() err = connPool.Add(servers[0]) - assert.Equal(t, err, connection_pool.ErrClosed) + assert.Equal(t, err, pool.ErrClosed) } func TestAdd_Close_concurrent(t *testing.T) { - connPool, err := connection_pool.Connect([]string{servers[0]}, connOpts) + connPool, err := pool.Connect([]string{servers[0]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -347,7 +347,7 @@ func TestAdd_Close_concurrent(t *testing.T) { err = connPool.Add(servers[1]) if err != nil { - assert.Equal(t, connection_pool.ErrClosed, err) + assert.Equal(t, pool.ErrClosed, err) } }() @@ -357,7 +357,7 @@ func TestAdd_Close_concurrent(t *testing.T) { } func TestAdd_CloseGraceful_concurrent(t *testing.T) { - connPool, err := connection_pool.Connect([]string{servers[0]}, connOpts) + connPool, err := pool.Connect([]string{servers[0]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -368,7 +368,7 @@ func TestAdd_CloseGraceful_concurrent(t *testing.T) { err = connPool.Add(servers[1]) if err != nil { - assert.Equal(t, connection_pool.ErrClosed, err) + assert.Equal(t, pool.ErrClosed, err) } }() @@ -378,7 +378,7 @@ func TestAdd_CloseGraceful_concurrent(t *testing.T) { } func TestRemove(t *testing.T) { - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -391,7 +391,7 @@ func TestRemove(t *testing.T) { args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: servers, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -405,7 +405,7 @@ func TestRemove(t *testing.T) { } func TestRemove_double(t *testing.T) { - connPool, err := connection_pool.Connect([]string{servers[0], servers[1]}, connOpts) + connPool, err := pool.Connect([]string{servers[0], servers[1]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -418,7 +418,7 @@ func TestRemove_double(t *testing.T) { args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: servers, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -432,7 +432,7 @@ func TestRemove_double(t *testing.T) { } func TestRemove_unknown(t *testing.T) { - connPool, err := connection_pool.Connect([]string{servers[0], servers[1]}, connOpts) + connPool, err := pool.Connect([]string{servers[0], servers[1]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -443,7 +443,7 @@ func TestRemove_unknown(t *testing.T) { args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: servers, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -458,7 +458,7 @@ func TestRemove_unknown(t *testing.T) { } func TestRemove_concurrent(t *testing.T) { - connPool, err := connection_pool.Connect([]string{servers[0], servers[1]}, connOpts) + connPool, err := pool.Connect([]string{servers[0], servers[1]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -491,7 +491,7 @@ func TestRemove_concurrent(t *testing.T) { args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: servers, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -505,7 +505,7 @@ func TestRemove_concurrent(t *testing.T) { } func TestRemove_Close_concurrent(t *testing.T) { - connPool, err := connection_pool.Connect([]string{servers[0], servers[1]}, connOpts) + connPool, err := pool.Connect([]string{servers[0], servers[1]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -524,7 +524,7 @@ func TestRemove_Close_concurrent(t *testing.T) { } func TestRemove_CloseGraceful_concurrent(t *testing.T) { - connPool, err := connection_pool.Connect([]string{servers[0], servers[1]}, connOpts) + connPool, err := pool.Connect([]string{servers[0], servers[1]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -546,13 +546,13 @@ func TestClose(t *testing.T) { server1 := servers[0] server2 := servers[1] - connPool, err := connection_pool.Connect([]string{server1, server2}, connOpts) + connPool, err := pool.Connect([]string{server1, server2}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: []string{server1, server2}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -568,7 +568,7 @@ func TestClose(t *testing.T) { args = test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: []string{server1, server2}, ExpectedPoolStatus: false, ExpectedStatuses: map[string]bool{ @@ -585,13 +585,13 @@ func TestCloseGraceful(t *testing.T) { server1 := servers[0] server2 := servers[1] - connPool, err := connection_pool.Connect([]string{server1, server2}, connOpts) + connPool, err := pool.Connect([]string{server1, server2}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: []string{server1, server2}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ @@ -609,14 +609,14 @@ func TestCloseGraceful(t *testing.T) { ` evalSleep := 3 // In seconds. req := tarantool.NewEvalRequest(eval).Args([]interface{}{evalSleep}) - fut := connPool.Do(req, connection_pool.ANY) + fut := connPool.Do(req, pool.ANY) go func() { connPool.CloseGraceful() }() // Check that a request rejected if graceful shutdown in progress. time.Sleep((time.Duration(evalSleep) * time.Second) / 2) - _, err = connPool.Do(tarantool.NewPingRequest(), connection_pool.ANY).Get() + _, err = connPool.Do(tarantool.NewPingRequest(), pool.ANY).Get() require.ErrorContains(t, err, "can't find healthy instance in pool") // Check that a previous request was successful. @@ -626,7 +626,7 @@ func TestCloseGraceful(t *testing.T) { args = test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: []string{server1, server2}, ExpectedPoolStatus: false, ExpectedStatuses: map[string]bool{ @@ -649,7 +649,7 @@ func (h *testHandler) addErr(err error) { } func (h *testHandler) Discovered(conn *tarantool.Connection, - role connection_pool.Role) error { + role pool.Role) error { discovered := atomic.AddUint32(&h.discovered, 1) if conn == nil { @@ -661,17 +661,17 @@ func (h *testHandler) Discovered(conn *tarantool.Connection, // discovered >= 3 - update a connection after a role update addr := conn.Addr() if addr == servers[0] { - if discovered < 3 && role != connection_pool.MasterRole { + if discovered < 3 && role != pool.MasterRole { h.addErr(fmt.Errorf("unexpected init role %d for addr %s", role, addr)) } - if discovered >= 3 && role != connection_pool.ReplicaRole { + if discovered >= 3 && role != pool.ReplicaRole { h.addErr(fmt.Errorf("unexpected updated role %d for addr %s", role, addr)) } } else if addr == servers[1] { if discovered >= 3 { h.addErr(fmt.Errorf("unexpected discovery for addr %s", addr)) } - if role != connection_pool.ReplicaRole { + if role != pool.ReplicaRole { h.addErr(fmt.Errorf("unexpected role %d for addr %s", role, addr)) } } else { @@ -682,7 +682,7 @@ func (h *testHandler) Discovered(conn *tarantool.Connection, } func (h *testHandler) Deactivated(conn *tarantool.Connection, - role connection_pool.Role) error { + role pool.Role) error { deactivated := atomic.AddUint32(&h.deactivated, 1) if conn == nil { @@ -693,7 +693,7 @@ func (h *testHandler) Deactivated(conn *tarantool.Connection, addr := conn.Addr() if deactivated == 1 && addr == servers[0] { // A first close is a role update. - if role != connection_pool.MasterRole { + if role != pool.MasterRole { h.addErr(fmt.Errorf("unexpected removed role %d for addr %s", role, addr)) } return nil @@ -701,7 +701,7 @@ func (h *testHandler) Deactivated(conn *tarantool.Connection, if addr == servers[0] || addr == servers[1] { // Close. - if role != connection_pool.ReplicaRole { + if role != pool.ReplicaRole { h.addErr(fmt.Errorf("unexpected removed role %d for addr %s", role, addr)) } } else { @@ -719,17 +719,17 @@ func TestConnectionHandlerOpenUpdateClose(t *testing.T) { require.Nilf(t, err, "fail to set roles for cluster") h := &testHandler{} - poolOpts := connection_pool.OptsPool{ + poolOpts := pool.Opts{ CheckTimeout: 100 * time.Microsecond, ConnectionHandler: h, } - pool, err := connection_pool.ConnectWithOpts(poolServers, connOpts, poolOpts) + connPool, err := pool.ConnectWithOpts(poolServers, connOpts, poolOpts) require.Nilf(t, err, "failed to connect") - require.NotNilf(t, pool, "conn is nil after Connect") + require.NotNilf(t, connPool, "conn is nil after Connect") - _, err = pool.Call17("box.cfg", []interface{}{map[string]bool{ + _, err = connPool.Call17("box.cfg", []interface{}{map[string]bool{ "read_only": true, - }}, connection_pool.RW) + }}, pool.RW) require.Nilf(t, err, "failed to make ro") for i := 0; i < 100; i++ { @@ -748,7 +748,7 @@ func TestConnectionHandlerOpenUpdateClose(t *testing.T) { require.Equalf(t, uint32(1), deactivated, "updated not reported as deactivated") - pool.Close() + connPool.Close() for i := 0; i < 100; i++ { // Wait for close of all connections. @@ -761,7 +761,7 @@ func TestConnectionHandlerOpenUpdateClose(t *testing.T) { for _, err := range h.errs { t.Errorf("Unexpected error: %s", err) } - connected, err := pool.ConnectedNow(connection_pool.ANY) + connected, err := connPool.ConnectedNow(pool.ANY) require.Nilf(t, err, "failed to get connected state") require.Falsef(t, connected, "connection pool still be connected") @@ -778,13 +778,13 @@ type testAddErrorHandler struct { } func (h *testAddErrorHandler) Discovered(conn *tarantool.Connection, - role connection_pool.Role) error { + role pool.Role) error { h.discovered++ return fmt.Errorf("any error") } func (h *testAddErrorHandler) Deactivated(conn *tarantool.Connection, - role connection_pool.Role) error { + role pool.Role) error { h.deactivated++ return nil } @@ -793,11 +793,11 @@ func TestConnectionHandlerOpenError(t *testing.T) { poolServers := []string{servers[0], servers[1]} h := &testAddErrorHandler{} - poolOpts := connection_pool.OptsPool{ + poolOpts := pool.Opts{ CheckTimeout: 100 * time.Microsecond, ConnectionHandler: h, } - connPool, err := connection_pool.ConnectWithOpts(poolServers, connOpts, poolOpts) + connPool, err := pool.ConnectWithOpts(poolServers, connOpts, poolOpts) if err == nil { defer connPool.Close() } @@ -811,7 +811,7 @@ type testUpdateErrorHandler struct { } func (h *testUpdateErrorHandler) Discovered(conn *tarantool.Connection, - role connection_pool.Role) error { + role pool.Role) error { atomic.AddUint32(&h.discovered, 1) if atomic.LoadUint32(&h.deactivated) != 0 { @@ -822,7 +822,7 @@ func (h *testUpdateErrorHandler) Discovered(conn *tarantool.Connection, } func (h *testUpdateErrorHandler) Deactivated(conn *tarantool.Connection, - role connection_pool.Role) error { + role pool.Role) error { atomic.AddUint32(&h.deactivated, 1) return nil } @@ -835,42 +835,42 @@ func TestConnectionHandlerUpdateError(t *testing.T) { require.Nilf(t, err, "fail to set roles for cluster") h := &testUpdateErrorHandler{} - poolOpts := connection_pool.OptsPool{ + poolOpts := pool.Opts{ CheckTimeout: 100 * time.Microsecond, ConnectionHandler: h, } - connPool, err := connection_pool.ConnectWithOpts(poolServers, connOpts, poolOpts) + connPool, err := pool.ConnectWithOpts(poolServers, connOpts, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() - connected, err := connPool.ConnectedNow(connection_pool.ANY) + connected, err := connPool.ConnectedNow(pool.ANY) require.Nilf(t, err, "failed to get ConnectedNow()") require.Truef(t, connected, "should be connected") for i := 0; i < len(poolServers); i++ { _, err = connPool.Call17("box.cfg", []interface{}{map[string]bool{ "read_only": true, - }}, connection_pool.RW) + }}, pool.RW) require.Nilf(t, err, "failed to make ro") } for i := 0; i < 100; i++ { // Wait for updates done. - connected, err = connPool.ConnectedNow(connection_pool.ANY) + connected, err = connPool.ConnectedNow(pool.ANY) if !connected || err != nil { break } time.Sleep(poolOpts.CheckTimeout) } - connected, err = connPool.ConnectedNow(connection_pool.ANY) + connected, err = connPool.ConnectedNow(pool.ANY) require.Nilf(t, err, "failed to get ConnectedNow()") require.Falsef(t, connected, "should not be any active connection") connPool.Close() - connected, err = connPool.ConnectedNow(connection_pool.ANY) + connected, err = connPool.ConnectedNow(pool.ANY) require.Nilf(t, err, "failed to get ConnectedNow()") require.Falsef(t, connected, "should be deactivated") @@ -884,7 +884,7 @@ func TestRequestOnClosed(t *testing.T) { server1 := servers[0] server2 := servers[1] - connPool, err := connection_pool.Connect([]string{server1, server2}, connOpts) + connPool, err := pool.Connect([]string{server1, server2}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -895,7 +895,7 @@ func TestRequestOnClosed(t *testing.T) { args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, Servers: []string{server1, server2}, ExpectedPoolStatus: false, ExpectedStatuses: map[string]bool{ @@ -906,7 +906,7 @@ func TestRequestOnClosed(t *testing.T) { err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) - _, err = connPool.Ping(connection_pool.ANY) + _, err = connPool.Ping(pool.ANY) require.NotNilf(t, err, "err is nil after Ping") err = test_helpers.RestartTarantool(&instances[0]) @@ -922,7 +922,7 @@ func TestGetPoolInfo(t *testing.T) { srvs := []string{server1, server2} expected := []string{server1, server2} - connPool, err := connection_pool.Connect(srvs, connOpts) + connPool, err := pool.Connect(srvs, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -940,14 +940,14 @@ func TestCall17(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() // PreferRO - resp, err := connPool.Call17("box.info", []interface{}{}, connection_pool.PreferRO) + resp, err := connPool.Call17("box.info", []interface{}{}, pool.PreferRO) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") @@ -958,7 +958,7 @@ func TestCall17(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `PreferRO`") // PreferRW - resp, err = connPool.Call17("box.info", []interface{}{}, connection_pool.PreferRW) + resp, err = connPool.Call17("box.info", []interface{}{}, pool.PreferRW) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") @@ -969,7 +969,7 @@ func TestCall17(t *testing.T) { require.Falsef(t, ro, "expected `false` with mode `PreferRW`") // RO - resp, err = connPool.Call17("box.info", []interface{}{}, connection_pool.RO) + resp, err = connPool.Call17("box.info", []interface{}{}, pool.RO) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") @@ -980,7 +980,7 @@ func TestCall17(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `RO`") // RW - resp, err = connPool.Call17("box.info", []interface{}{}, connection_pool.RW) + resp, err = connPool.Call17("box.info", []interface{}{}, pool.RW) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") @@ -997,14 +997,14 @@ func TestEval(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() // PreferRO - resp, err := connPool.Eval("return box.info().ro", []interface{}{}, connection_pool.PreferRO) + resp, err := connPool.Eval("return box.info().ro", []interface{}{}, pool.PreferRO) require.Nilf(t, err, "failed to Eval") require.NotNilf(t, resp, "response is nil after Eval") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Eval") @@ -1014,7 +1014,7 @@ func TestEval(t *testing.T) { require.Truef(t, val, "expected `true` with mode `PreferRO`") // PreferRW - resp, err = connPool.Eval("return box.info().ro", []interface{}{}, connection_pool.PreferRW) + resp, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.PreferRW) require.Nilf(t, err, "failed to Eval") require.NotNilf(t, resp, "response is nil after Eval") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Eval") @@ -1024,7 +1024,7 @@ func TestEval(t *testing.T) { require.Falsef(t, val, "expected `false` with mode `PreferRW`") // RO - resp, err = connPool.Eval("return box.info().ro", []interface{}{}, connection_pool.RO) + resp, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.RO) require.Nilf(t, err, "failed to Eval") require.NotNilf(t, resp, "response is nil after Eval") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Eval") @@ -1034,7 +1034,7 @@ func TestEval(t *testing.T) { require.Truef(t, val, "expected `true` with mode `RO`") // RW - resp, err = connPool.Eval("return box.info().ro", []interface{}{}, connection_pool.RW) + resp, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.RW) require.Nilf(t, err, "failed to Eval") require.NotNilf(t, resp, "response is nil after Eval") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Eval") @@ -1075,7 +1075,7 @@ func TestExecute(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1083,7 +1083,7 @@ func TestExecute(t *testing.T) { request := "SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0 == 1;" // Execute - resp, err := connPool.Execute(request, []interface{}{}, connection_pool.ANY) + resp, err := connPool.Execute(request, []interface{}{}, pool.ANY) require.Nilf(t, err, "failed to Execute") require.NotNilf(t, resp, "response is nil after Execute") require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Execute") @@ -1091,13 +1091,13 @@ func TestExecute(t *testing.T) { // ExecuteTyped mem := []Member{} - info, _, err := connPool.ExecuteTyped(request, []interface{}{}, &mem, connection_pool.ANY) + info, _, err := connPool.ExecuteTyped(request, []interface{}{}, &mem, pool.ANY) require.Nilf(t, err, "failed to ExecuteTyped") require.Equalf(t, info.AffectedCount, uint64(0), "unexpected info.AffectedCount") require.Equalf(t, len(mem), 1, "wrong count of results") // ExecuteAsync - fut := connPool.ExecuteAsync(request, []interface{}{}, connection_pool.ANY) + fut := connPool.ExecuteAsync(request, []interface{}{}, pool.ANY) resp, err = fut.Get() require.Nilf(t, err, "failed to ExecuteAsync") require.NotNilf(t, resp, "response is nil after ExecuteAsync") @@ -1132,7 +1132,7 @@ func TestRoundRobinStrategy(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1143,7 +1143,7 @@ func TestRoundRobinStrategy(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: allPorts, ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, } err = test_helpers.ProcessListenOnInstance(args) @@ -1154,7 +1154,7 @@ func TestRoundRobinStrategy(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: masterPorts, ConnPool: connPool, - Mode: connection_pool.RW, + Mode: pool.RW, } err = test_helpers.ProcessListenOnInstance(args) @@ -1165,7 +1165,7 @@ func TestRoundRobinStrategy(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: replicaPorts, ConnPool: connPool, - Mode: connection_pool.RO, + Mode: pool.RO, } err = test_helpers.ProcessListenOnInstance(args) @@ -1176,7 +1176,7 @@ func TestRoundRobinStrategy(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: masterPorts, ConnPool: connPool, - Mode: connection_pool.PreferRW, + Mode: pool.PreferRW, } err = test_helpers.ProcessListenOnInstance(args) @@ -1187,7 +1187,7 @@ func TestRoundRobinStrategy(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: replicaPorts, ConnPool: connPool, - Mode: connection_pool.PreferRO, + Mode: pool.PreferRO, } err = test_helpers.ProcessListenOnInstance(args) @@ -1209,14 +1209,14 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() // RO - _, err = connPool.Eval("return box.cfg.listen", []interface{}{}, connection_pool.RO) + _, err = connPool.Eval("return box.cfg.listen", []interface{}{}, pool.RO) require.NotNilf(t, err, "expected to fail after Eval, but error is nil") require.Equal(t, "can't find ro instance in pool", err.Error()) @@ -1225,7 +1225,7 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: allPorts, ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, } err = test_helpers.ProcessListenOnInstance(args) @@ -1236,7 +1236,7 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: allPorts, ConnPool: connPool, - Mode: connection_pool.RW, + Mode: pool.RW, } err = test_helpers.ProcessListenOnInstance(args) @@ -1247,7 +1247,7 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: allPorts, ConnPool: connPool, - Mode: connection_pool.PreferRW, + Mode: pool.PreferRW, } err = test_helpers.ProcessListenOnInstance(args) @@ -1258,7 +1258,7 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: allPorts, ConnPool: connPool, - Mode: connection_pool.PreferRO, + Mode: pool.PreferRO, } err = test_helpers.ProcessListenOnInstance(args) @@ -1280,14 +1280,14 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() // RW - _, err = connPool.Eval("return box.cfg.listen", []interface{}{}, connection_pool.RW) + _, err = connPool.Eval("return box.cfg.listen", []interface{}{}, pool.RW) require.NotNilf(t, err, "expected to fail after Eval, but error is nil") require.Equal(t, "can't find rw instance in pool", err.Error()) @@ -1296,7 +1296,7 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: allPorts, ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, } err = test_helpers.ProcessListenOnInstance(args) @@ -1307,7 +1307,7 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: allPorts, ConnPool: connPool, - Mode: connection_pool.RO, + Mode: pool.RO, } err = test_helpers.ProcessListenOnInstance(args) @@ -1318,7 +1318,7 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: allPorts, ConnPool: connPool, - Mode: connection_pool.PreferRW, + Mode: pool.PreferRW, } err = test_helpers.ProcessListenOnInstance(args) @@ -1329,7 +1329,7 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: allPorts, ConnPool: connPool, - Mode: connection_pool.PreferRO, + Mode: pool.PreferRO, } err = test_helpers.ProcessListenOnInstance(args) @@ -1363,7 +1363,7 @@ func TestUpdateInstancesRoles(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1374,7 +1374,7 @@ func TestUpdateInstancesRoles(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: allPorts, ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, } err = test_helpers.ProcessListenOnInstance(args) @@ -1385,7 +1385,7 @@ func TestUpdateInstancesRoles(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: masterPorts, ConnPool: connPool, - Mode: connection_pool.RW, + Mode: pool.RW, } err = test_helpers.ProcessListenOnInstance(args) @@ -1396,7 +1396,7 @@ func TestUpdateInstancesRoles(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: replicaPorts, ConnPool: connPool, - Mode: connection_pool.RO, + Mode: pool.RO, } err = test_helpers.ProcessListenOnInstance(args) @@ -1407,7 +1407,7 @@ func TestUpdateInstancesRoles(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: masterPorts, ConnPool: connPool, - Mode: connection_pool.PreferRW, + Mode: pool.PreferRW, } err = test_helpers.ProcessListenOnInstance(args) @@ -1418,7 +1418,7 @@ func TestUpdateInstancesRoles(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: replicaPorts, ConnPool: connPool, - Mode: connection_pool.PreferRO, + Mode: pool.PreferRO, } err = test_helpers.ProcessListenOnInstance(args) @@ -1445,7 +1445,7 @@ func TestUpdateInstancesRoles(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: allPorts, ConnPool: connPool, - Mode: connection_pool.ANY, + Mode: pool.ANY, } err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) @@ -1456,7 +1456,7 @@ func TestUpdateInstancesRoles(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: masterPorts, ConnPool: connPool, - Mode: connection_pool.RW, + Mode: pool.RW, } err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) @@ -1467,7 +1467,7 @@ func TestUpdateInstancesRoles(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: replicaPorts, ConnPool: connPool, - Mode: connection_pool.RO, + Mode: pool.RO, } err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) @@ -1478,7 +1478,7 @@ func TestUpdateInstancesRoles(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: masterPorts, ConnPool: connPool, - Mode: connection_pool.PreferRW, + Mode: pool.PreferRW, } err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) @@ -1489,7 +1489,7 @@ func TestUpdateInstancesRoles(t *testing.T) { ServersNumber: serversNumber, ExpectedPorts: replicaPorts, ConnPool: connPool, - Mode: connection_pool.PreferRO, + Mode: pool.PreferRO, } err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) @@ -1502,7 +1502,7 @@ func TestInsert(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1590,7 +1590,7 @@ func TestDelete(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1648,7 +1648,7 @@ func TestUpsert(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1684,7 +1684,7 @@ func TestUpsert(t *testing.T) { // PreferRW resp, err = connPool.Upsert( spaceName, []interface{}{"upsert_key", "upsert_value"}, - []interface{}{[]interface{}{"=", 1, "new_value"}}, connection_pool.PreferRW) + []interface{}{[]interface{}{"=", 1, "new_value"}}, pool.PreferRW) require.Nilf(t, err, "failed to Upsert") require.NotNilf(t, resp, "response is nil after Upsert") @@ -1713,7 +1713,7 @@ func TestUpdate(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1766,7 +1766,7 @@ func TestUpdate(t *testing.T) { // PreferRW resp, err = connPool.Update( spaceName, indexNo, []interface{}{"update_key"}, - []interface{}{[]interface{}{"=", 1, "another_value"}}, connection_pool.PreferRW) + []interface{}{[]interface{}{"=", 1, "another_value"}}, pool.PreferRW) require.Nilf(t, err, "failed to Update") require.NotNilf(t, resp, "response is nil after Update") @@ -1795,7 +1795,7 @@ func TestReplace(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1846,7 +1846,7 @@ func TestReplace(t *testing.T) { require.Equalf(t, "new_value", value, "unexpected body of Select (1)") // PreferRW - resp, err = connPool.Replace(spaceNo, []interface{}{"new_key", "new_value"}, connection_pool.PreferRW) + resp, err = connPool.Replace(spaceNo, []interface{}{"new_key", "new_value"}, pool.PreferRW) require.Nilf(t, err, "failed to Replace") require.NotNilf(t, resp, "response is nil after Replace") @@ -1874,7 +1874,7 @@ func TestSelect(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1920,7 +1920,7 @@ func TestSelect(t *testing.T) { require.Equalf(t, "any_select_value", value, "unexpected body of Select (1)") // PreferRO - resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, connection_pool.PreferRO) + resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, pool.PreferRO) require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") @@ -1934,7 +1934,7 @@ func TestSelect(t *testing.T) { require.Equalf(t, "ro_select_key", key, "unexpected body of Select (0)") // PreferRW - resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, connection_pool.PreferRW) + resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, pool.PreferRW) require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") @@ -1952,7 +1952,7 @@ func TestSelect(t *testing.T) { require.Equalf(t, "rw_select_value", value, "unexpected body of Select (1)") // RO - resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, connection_pool.RO) + resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, pool.RO) require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") @@ -1970,7 +1970,7 @@ func TestSelect(t *testing.T) { require.Equalf(t, "ro_select_value", value, "unexpected body of Select (1)") // RW - resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, connection_pool.RW) + resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, pool.RW) require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") @@ -1994,34 +1994,34 @@ func TestPing(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() // ANY - resp, err := connPool.Ping(connection_pool.ANY) + resp, err := connPool.Ping(pool.ANY) require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") // RW - resp, err = connPool.Ping(connection_pool.RW) + resp, err = connPool.Ping(pool.RW) require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") // RO - resp, err = connPool.Ping(connection_pool.RO) + resp, err = connPool.Ping(pool.RO) require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") // PreferRW - resp, err = connPool.Ping(connection_pool.PreferRW) + resp, err = connPool.Ping(pool.PreferRW) require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") // PreferRO - resp, err = connPool.Ping(connection_pool.PreferRO) + resp, err = connPool.Ping(pool.PreferRO) require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") } @@ -2032,7 +2032,7 @@ func TestDo(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2040,27 +2040,27 @@ func TestDo(t *testing.T) { req := tarantool.NewPingRequest() // ANY - resp, err := connPool.Do(req, connection_pool.ANY).Get() + resp, err := connPool.Do(req, pool.ANY).Get() require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") // RW - resp, err = connPool.Do(req, connection_pool.RW).Get() + resp, err = connPool.Do(req, pool.RW).Get() require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") // RO - resp, err = connPool.Do(req, connection_pool.RO).Get() + resp, err = connPool.Do(req, pool.RO).Get() require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") // PreferRW - resp, err = connPool.Do(req, connection_pool.PreferRW).Get() + resp, err = connPool.Do(req, pool.PreferRW).Get() require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") // PreferRO - resp, err = connPool.Do(req, connection_pool.PreferRO).Get() + resp, err = connPool.Do(req, pool.PreferRO).Get() require.Nilf(t, err, "failed to Ping") require.NotNilf(t, resp, "response is nil after Ping") } @@ -2073,23 +2073,23 @@ func TestNewPrepared(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() - stmt, err := connPool.NewPrepared("SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:id AND NAME1=:name;", connection_pool.RO) + stmt, err := connPool.NewPrepared("SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:id AND NAME1=:name;", pool.RO) require.Nilf(t, err, "fail to prepare statement: %v", err) - if connPool.GetPoolInfo()[stmt.Conn.Addr()].ConnRole != connection_pool.ReplicaRole { + if connPool.GetPoolInfo()[stmt.Conn.Addr()].ConnRole != pool.ReplicaRole { t.Errorf("wrong role for the statement's connection") } executeReq := tarantool.NewExecutePreparedRequest(stmt) unprepareReq := tarantool.NewUnprepareRequest(stmt) - resp, err := connPool.Do(executeReq.Args([]interface{}{1, "test"}), connection_pool.ANY).Get() + resp, err := connPool.Do(executeReq.Args([]interface{}{1, "test"}), pool.ANY).Get() if err != nil { t.Fatalf("failed to execute prepared: %v", err) } @@ -2110,7 +2110,7 @@ func TestNewPrepared(t *testing.T) { } // the second argument for unprepare request is unused - it already belongs to some connection - resp, err = connPool.Do(unprepareReq, connection_pool.ANY).Get() + resp, err = connPool.Do(unprepareReq, pool.ANY).Get() if err != nil { t.Errorf("failed to unprepare prepared statement: %v", err) } @@ -2118,13 +2118,13 @@ func TestNewPrepared(t *testing.T) { t.Errorf("failed to unprepare prepared statement: code %d", resp.Code) } - _, err = connPool.Do(unprepareReq, connection_pool.ANY).Get() + _, err = connPool.Do(unprepareReq, pool.ANY).Get() if err == nil { t.Errorf("the statement must be already unprepared") } require.Contains(t, err.Error(), "Prepared statement with id") - _, err = connPool.Do(executeReq, connection_pool.ANY).Get() + _, err = connPool.Do(executeReq, pool.ANY).Get() if err == nil { t.Errorf("the statement must be already unprepared") } @@ -2139,7 +2139,7 @@ func TestDoWithStrangerConn(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2147,7 +2147,7 @@ func TestDoWithStrangerConn(t *testing.T) { req := test_helpers.NewStrangerRequest() - _, err = connPool.Do(req, connection_pool.ANY).Get() + _, err = connPool.Do(req, pool.ANY).Get() if err == nil { t.Fatalf("nil error caught") } @@ -2168,12 +2168,12 @@ func TestStream_Commit(t *testing.T) { err = test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() - stream, err := connPool.NewStream(connection_pool.PreferRW) + stream, err := connPool.NewStream(pool.PreferRW) require.Nilf(t, err, "failed to create stream") require.NotNilf(t, connPool, "stream is nil after NewStream") @@ -2267,12 +2267,12 @@ func TestStream_Rollback(t *testing.T) { err = test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() - stream, err := connPool.NewStream(connection_pool.PreferRW) + stream, err := connPool.NewStream(pool.PreferRW) require.Nilf(t, err, "failed to create stream") require.NotNilf(t, connPool, "stream is nil after NewStream") @@ -2366,12 +2366,12 @@ func TestStream_TxnIsolationLevel(t *testing.T) { err = test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() - stream, err := connPool.NewStream(connection_pool.PreferRW) + stream, err := connPool.NewStream(pool.PreferRW) require.Nilf(t, err, "failed to create stream") require.NotNilf(t, connPool, "stream is nil after NewStream") @@ -2460,13 +2460,13 @@ func TestConnectionPool_NewWatcher_noWatchersFeature(t *testing.T) { err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") - pool, err := connection_pool.Connect(servers, opts) + connPool, err := pool.Connect(servers, opts) require.Nilf(t, err, "failed to connect") - require.NotNilf(t, pool, "conn is nil after Connect") - defer pool.Close() + require.NotNilf(t, connPool, "conn is nil after Connect") + defer connPool.Close() - watcher, err := pool.NewWatcher(key, - func(event tarantool.WatchEvent) {}, connection_pool.ANY) + watcher, err := connPool.NewWatcher(key, + func(event tarantool.WatchEvent) {}, pool.ANY) require.Nilf(t, watcher, "watcher must not be created") require.NotNilf(t, err, "an error is expected") expected := "the feature WatchersFeature must be required by connection " + @@ -2488,25 +2488,25 @@ func TestConnectionPool_NewWatcher_modes(t *testing.T) { err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") - pool, err := connection_pool.Connect(servers, opts) + connPool, err := pool.Connect(servers, opts) require.Nilf(t, err, "failed to connect") - require.NotNilf(t, pool, "conn is nil after Connect") - defer pool.Close() + require.NotNilf(t, connPool, "conn is nil after Connect") + defer connPool.Close() - modes := []connection_pool.Mode{ - connection_pool.ANY, - connection_pool.RW, - connection_pool.RO, - connection_pool.PreferRW, - connection_pool.PreferRO, + modes := []pool.Mode{ + pool.ANY, + pool.RW, + pool.RO, + pool.PreferRW, + pool.PreferRO, } for _, mode := range modes { t.Run(fmt.Sprintf("%d", mode), func(t *testing.T) { expectedServers := []string{} for i, server := range servers { - if roles[i] && mode == connection_pool.RW { + if roles[i] && mode == pool.RW { continue - } else if !roles[i] && mode == connection_pool.RO { + } else if !roles[i] && mode == pool.RO { continue } expectedServers = append(expectedServers, server) @@ -2515,7 +2515,7 @@ func TestConnectionPool_NewWatcher_modes(t *testing.T) { events := make(chan tarantool.WatchEvent, 1024) defer close(events) - watcher, err := pool.NewWatcher(key, func(event tarantool.WatchEvent) { + watcher, err := connPool.NewWatcher(key, func(event tarantool.WatchEvent) { require.Equal(t, key, event.Key) require.Equal(t, nil, event.Value) events <- event @@ -2556,7 +2556,7 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) const key = "TestConnectionPool_NewWatcher_update" - const mode = connection_pool.RW + const mode = pool.RW const initCnt = 2 roles := []bool{true, false, false, true, true} @@ -2567,10 +2567,10 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") - poolOpts := connection_pool.OptsPool{ + poolOpts := pool.Opts{ CheckTimeout: 500 * time.Millisecond, } - pool, err := connection_pool.ConnectWithOpts(servers, opts, poolOpts) + pool, err := pool.ConnectWithOpts(servers, opts, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, pool, "conn is nil after Connect") @@ -2643,7 +2643,7 @@ func TestWatcher_Unregister(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) const key = "TestWatcher_Unregister" - const mode = connection_pool.RW + const mode = pool.RW const expectedCnt = 2 roles := []bool{true, false, false, true, true} @@ -2654,7 +2654,7 @@ func TestWatcher_Unregister(t *testing.T) { err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") - pool, err := connection_pool.Connect(servers, opts) + pool, err := pool.Connect(servers, opts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, pool, "conn is nil after Connect") defer pool.Close() @@ -2713,21 +2713,21 @@ func TestConnectionPool_NewWatcher_concurrent(t *testing.T) { err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") - pool, err := connection_pool.Connect(servers, opts) + connPool, err := pool.Connect(servers, opts) require.Nilf(t, err, "failed to connect") - require.NotNilf(t, pool, "conn is nil after Connect") - defer pool.Close() + require.NotNilf(t, connPool, "conn is nil after Connect") + defer connPool.Close() var wg sync.WaitGroup wg.Add(testConcurrency) - mode := connection_pool.ANY + mode := pool.ANY callback := func(event tarantool.WatchEvent) {} for i := 0; i < testConcurrency; i++ { go func(i int) { defer wg.Done() - watcher, err := pool.NewWatcher(key, callback, mode) + watcher, err := connPool.NewWatcher(key, callback, mode) if err != nil { t.Errorf("Failed to create a watcher: %s", err) } else { @@ -2753,13 +2753,13 @@ func TestWatcher_Unregister_concurrent(t *testing.T) { err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") - pool, err := connection_pool.Connect(servers, opts) + connPool, err := pool.Connect(servers, opts) require.Nilf(t, err, "failed to connect") - require.NotNilf(t, pool, "conn is nil after Connect") - defer pool.Close() + require.NotNilf(t, connPool, "conn is nil after Connect") + defer connPool.Close() - mode := connection_pool.ANY - watcher, err := pool.NewWatcher(key, func(event tarantool.WatchEvent) { + mode := pool.ANY + watcher, err := connPool.NewWatcher(key, func(event tarantool.WatchEvent) { }, mode) require.Nilf(t, err, "failed to create a watcher") diff --git a/connection_pool/connector.go b/pool/connector.go similarity index 99% rename from connection_pool/connector.go rename to pool/connector.go index 3688b8309..128d80575 100644 --- a/connection_pool/connector.go +++ b/pool/connector.go @@ -1,4 +1,4 @@ -package connection_pool +package pool import ( "errors" diff --git a/connection_pool/connector_test.go b/pool/connector_test.go similarity index 99% rename from connection_pool/connector_test.go rename to pool/connector_test.go index 717fdaec4..d4d197607 100644 --- a/connection_pool/connector_test.go +++ b/pool/connector_test.go @@ -1,4 +1,4 @@ -package connection_pool_test +package pool_test import ( "errors" @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/connection_pool" + . "github.com/tarantool/go-tarantool/v2/pool" ) var testMode Mode = RW diff --git a/connection_pool/const.go b/pool/const.go similarity index 97% rename from connection_pool/const.go rename to pool/const.go index 26b028f5a..7ec239f7d 100644 --- a/connection_pool/const.go +++ b/pool/const.go @@ -1,4 +1,4 @@ -package connection_pool +package pool /* Default mode for each request table: diff --git a/connection_pool/example_test.go b/pool/example_test.go similarity index 83% rename from connection_pool/example_test.go rename to pool/example_test.go index 84b0a5594..f917668a6 100644 --- a/connection_pool/example_test.go +++ b/pool/example_test.go @@ -1,11 +1,11 @@ -package connection_pool_test +package pool_test import ( "fmt" "time" "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/connection_pool" + "github.com/tarantool/go-tarantool/v2/pool" "github.com/tarantool/go-tarantool/v2/test_helpers" ) @@ -19,12 +19,12 @@ type Tuple struct { var testRoles = []bool{true, true, false, true, true} -func examplePool(roles []bool, connOpts tarantool.Opts) (*connection_pool.ConnectionPool, error) { +func examplePool(roles []bool, connOpts tarantool.Opts) (*pool.ConnectionPool, error) { err := test_helpers.SetClusterRO(servers, connOpts, roles) if err != nil { return nil, fmt.Errorf("ConnectionPool is not established") } - connPool, err := connection_pool.Connect(servers, connOpts) + connPool, err := pool.Connect(servers, connOpts) if err != nil || connPool == nil { return nil, fmt.Errorf("ConnectionPool is not established") } @@ -33,11 +33,11 @@ func examplePool(roles []bool, connOpts tarantool.Opts) (*connection_pool.Connec } func ExampleConnectionPool_Select() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() // Connect to servers[2] to check if tuple // was inserted on RW instance @@ -60,17 +60,17 @@ func ExampleConnectionPool_Select() { return } - resp, err := pool.Select( + resp, err := connPool.Select( spaceNo, indexNo, 0, 100, tarantool.IterEq, - []interface{}{"key1"}, connection_pool.PreferRW) + []interface{}{"key1"}, pool.PreferRW) if err != nil { fmt.Printf("error in select is %v", err) return } fmt.Printf("response is %#v\n", resp.Data) - resp, err = pool.Select( + resp, err = connPool.Select( spaceNo, indexNo, 0, 100, tarantool.IterEq, - []interface{}{"key2"}, connection_pool.PreferRW) + []interface{}{"key2"}, pool.PreferRW) if err != nil { fmt.Printf("error in select is %v", err) return @@ -94,11 +94,11 @@ func ExampleConnectionPool_Select() { } func ExampleConnectionPool_SelectTyped() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() // Connect to servers[2] to check if tuple // was inserted on RW instance @@ -122,17 +122,17 @@ func ExampleConnectionPool_SelectTyped() { } var res []Tuple - err = pool.SelectTyped( + err = connPool.SelectTyped( spaceNo, indexNo, 0, 100, tarantool.IterEq, - []interface{}{"key1"}, &res, connection_pool.PreferRW) + []interface{}{"key1"}, &res, pool.PreferRW) if err != nil { fmt.Printf("error in select is %v", err) return } fmt.Printf("response is %v\n", res) - err = pool.SelectTyped( + err = connPool.SelectTyped( spaceName, indexName, 0, 100, tarantool.IterEq, - []interface{}{"key2"}, &res, connection_pool.PreferRW) + []interface{}{"key2"}, &res, pool.PreferRW) if err != nil { fmt.Printf("error in select is %v", err) return @@ -156,11 +156,11 @@ func ExampleConnectionPool_SelectTyped() { } func ExampleConnectionPool_SelectAsync() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() // Connect to servers[2] to check if tuple // was inserted on RW instance @@ -190,15 +190,15 @@ func ExampleConnectionPool_SelectAsync() { } var futs [3]*tarantool.Future - futs[0] = pool.SelectAsync( + futs[0] = connPool.SelectAsync( spaceName, indexName, 0, 2, tarantool.IterEq, - []interface{}{"key1"}, connection_pool.PreferRW) - futs[1] = pool.SelectAsync( + []interface{}{"key1"}, pool.PreferRW) + futs[1] = connPool.SelectAsync( spaceName, indexName, 0, 1, tarantool.IterEq, - []interface{}{"key2"}, connection_pool.RW) - futs[2] = pool.SelectAsync( + []interface{}{"key2"}, pool.RW) + futs[2] = connPool.SelectAsync( spaceName, indexName, 0, 1, tarantool.IterEq, - []interface{}{"key3"}, connection_pool.RW) + []interface{}{"key3"}, pool.RW) var t []Tuple err = futs[0].GetTyped(&t) fmt.Println("Future", 0, "Error", err) @@ -239,16 +239,16 @@ func ExampleConnectionPool_SelectAsync() { func ExampleConnectionPool_SelectAsync_err() { roles := []bool{true, true, true, true, true} - pool, err := examplePool(roles, connOpts) + connPool, err := examplePool(roles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() var futs [3]*tarantool.Future - futs[0] = pool.SelectAsync( + futs[0] = connPool.SelectAsync( spaceName, indexName, 0, 2, tarantool.IterEq, - []interface{}{"key1"}, connection_pool.RW) + []interface{}{"key1"}, pool.RW) err = futs[0].Err() fmt.Println("Future", 0, "Error", err) @@ -258,14 +258,14 @@ func ExampleConnectionPool_SelectAsync_err() { } func ExampleConnectionPool_Ping() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() // Ping a Tarantool instance to check connection. - resp, err := pool.Ping(connection_pool.ANY) + resp, err := connPool.Ping(pool.ANY) fmt.Println("Ping Code", resp.Code) fmt.Println("Ping Data", resp.Data) fmt.Println("Ping Error", err) @@ -276,20 +276,20 @@ func ExampleConnectionPool_Ping() { } func ExampleConnectionPool_Insert() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() // Insert a new tuple {"key1", "value1"}. - resp, err := pool.Insert(spaceNo, []interface{}{"key1", "value1"}) + resp, err := connPool.Insert(spaceNo, []interface{}{"key1", "value1"}) fmt.Println("Insert key1") fmt.Println("Error", err) fmt.Println("Code", resp.Code) fmt.Println("Data", resp.Data) // Insert a new tuple {"key2", "value2"}. - resp, err = pool.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}, connection_pool.PreferRW) + resp, err = connPool.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}, pool.PreferRW) fmt.Println("Insert key2") fmt.Println("Error", err) fmt.Println("Code", resp.Code) @@ -325,11 +325,11 @@ func ExampleConnectionPool_Insert() { } func ExampleConnectionPool_Delete() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() // Connect to servers[2] to check if tuple // was inserted on RW instance @@ -353,14 +353,14 @@ func ExampleConnectionPool_Delete() { } // Delete tuple with primary key {"key1"}. - resp, err := pool.Delete(spaceNo, indexNo, []interface{}{"key1"}) + resp, err := connPool.Delete(spaceNo, indexNo, []interface{}{"key1"}) fmt.Println("Delete key1") fmt.Println("Error", err) fmt.Println("Code", resp.Code) fmt.Println("Data", resp.Data) // Delete tuple with primary key { "key2" }. - resp, err = pool.Delete(spaceName, indexName, []interface{}{"key2"}, connection_pool.PreferRW) + resp, err = connPool.Delete(spaceName, indexName, []interface{}{"key2"}, pool.PreferRW) fmt.Println("Delete key2") fmt.Println("Error", err) fmt.Println("Code", resp.Code) @@ -377,11 +377,11 @@ func ExampleConnectionPool_Delete() { } func ExampleConnectionPool_Replace() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() // Connect to servers[2] to check if tuple // was inserted on RW instance @@ -401,22 +401,22 @@ func ExampleConnectionPool_Replace() { // Replace a tuple with primary key ""key1. // Note, Tuple is defined within tests, and has EncdodeMsgpack and // DecodeMsgpack methods. - resp, err := pool.Replace(spaceNo, []interface{}{"key1", "new_value"}) + resp, err := connPool.Replace(spaceNo, []interface{}{"key1", "new_value"}) fmt.Println("Replace key1") fmt.Println("Error", err) fmt.Println("Code", resp.Code) fmt.Println("Data", resp.Data) - resp, err = pool.Replace(spaceName, []interface{}{"key1", "another_value"}) + resp, err = connPool.Replace(spaceName, []interface{}{"key1", "another_value"}) fmt.Println("Replace key1") fmt.Println("Error", err) fmt.Println("Code", resp.Code) fmt.Println("Data", resp.Data) - resp, err = pool.Replace(spaceName, &Tuple{Key: "key1", Value: "value2"}) + resp, err = connPool.Replace(spaceName, &Tuple{Key: "key1", Value: "value2"}) fmt.Println("Replace key1") fmt.Println("Error", err) fmt.Println("Code", resp.Code) fmt.Println("Data", resp.Data) - resp, err = pool.Replace(spaceName, &Tuple{Key: "key1", Value: "new_value2"}, connection_pool.PreferRW) + resp, err = connPool.Replace(spaceName, &Tuple{Key: "key1", Value: "new_value2"}, pool.PreferRW) fmt.Println("Replace key1") fmt.Println("Error", err) fmt.Println("Code", resp.Code) @@ -448,11 +448,11 @@ func ExampleConnectionPool_Replace() { } func ExampleConnectionPool_Update() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() // Connect to servers[2] to check if tuple // was inserted on RW instance @@ -470,9 +470,9 @@ func ExampleConnectionPool_Update() { } // Update tuple with primary key { "key1" }. - resp, err := pool.Update( + resp, err := connPool.Update( spaceName, indexName, []interface{}{"key1"}, - []interface{}{[]interface{}{"=", 1, "new_value"}}, connection_pool.PreferRW) + []interface{}{[]interface{}{"=", 1, "new_value"}}, pool.PreferRW) fmt.Println("Update key1") fmt.Println("Error", err) fmt.Println("Code", resp.Code) @@ -492,14 +492,14 @@ func ExampleConnectionPool_Update() { } func ExampleConnectionPool_Call() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() // Call a function 'simple_incr' with arguments. - resp, err := pool.Call17("simple_incr", []interface{}{1}, connection_pool.PreferRW) + resp, err := connPool.Call17("simple_incr", []interface{}{1}, pool.PreferRW) fmt.Println("Call simple_incr()") fmt.Println("Error", err) fmt.Println("Code", resp.Code) @@ -512,14 +512,14 @@ func ExampleConnectionPool_Call() { } func ExampleConnectionPool_Eval() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() // Run raw Lua code. - resp, err := pool.Eval("return 1 + 2", []interface{}{}, connection_pool.PreferRW) + resp, err := connPool.Eval("return 1 + 2", []interface{}{}, pool.PreferRW) fmt.Println("Eval 'return 1 + 2'") fmt.Println("Error", err) fmt.Println("Code", resp.Code) @@ -532,15 +532,15 @@ func ExampleConnectionPool_Eval() { } func ExampleConnectionPool_Do() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() // Ping a Tarantool instance to check connection. req := tarantool.NewPingRequest() - resp, err := pool.Do(req, connection_pool.ANY).Get() + resp, err := connPool.Do(req, pool.ANY).Get() fmt.Println("Ping Code", resp.Code) fmt.Println("Ping Data", resp.Data) fmt.Println("Ping Error", err) @@ -551,13 +551,13 @@ func ExampleConnectionPool_Do() { } func ExampleConnectionPool_NewPrepared() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() - stmt, err := pool.NewPrepared("SELECT 1", connection_pool.ANY) + stmt, err := connPool.NewPrepared("SELECT 1", pool.ANY) if err != nil { fmt.Println(err) } @@ -565,11 +565,11 @@ func ExampleConnectionPool_NewPrepared() { executeReq := tarantool.NewExecutePreparedRequest(stmt) unprepareReq := tarantool.NewUnprepareRequest(stmt) - _, err = pool.Do(executeReq, connection_pool.ANY).Get() + _, err = connPool.Do(executeReq, pool.ANY).Get() if err != nil { fmt.Printf("Failed to execute prepared stmt") } - _, err = pool.Do(unprepareReq, connection_pool.ANY).Get() + _, err = connPool.Do(unprepareReq, pool.ANY).Get() if err != nil { fmt.Printf("Failed to prepare") } @@ -584,26 +584,26 @@ func ExampleConnectionPool_NewWatcher() { tarantool.WatchersFeature, } - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() callback := func(event tarantool.WatchEvent) { fmt.Printf("event connection: %s\n", event.Conn.Addr()) fmt.Printf("event key: %s\n", event.Key) fmt.Printf("event value: %v\n", event.Value) } - mode := connection_pool.ANY - watcher, err := pool.NewWatcher(key, callback, mode) + mode := pool.ANY + watcher, err := connPool.NewWatcher(key, callback, mode) if err != nil { fmt.Printf("Unexpected error: %s\n", err) return } defer watcher.Unregister() - pool.Do(tarantool.NewBroadcastRequest(key).Value(value), mode).Get() + connPool.Do(tarantool.NewBroadcastRequest(key).Value(value), mode).Get() time.Sleep(time.Second) } @@ -613,14 +613,14 @@ func ExampleConnectionPool_NewWatcher_noWatchersFeature() { opts := connOpts.Clone() opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{} - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() callback := func(event tarantool.WatchEvent) {} - watcher, err := pool.NewWatcher(key, callback, connection_pool.ANY) + watcher, err := connPool.NewWatcher(key, callback, pool.ANY) fmt.Println(watcher) fmt.Println(err) // Output: @@ -655,15 +655,15 @@ func ExampleCommitRequest() { } txnOpts := getTestTxnOpts() - pool, err := examplePool(testRoles, txnOpts) + connPool, err := examplePool(testRoles, txnOpts) if err != nil { fmt.Println(err) return } - defer pool.Close() + defer connPool.Close() // example pool has only one rw instance - stream, err := pool.NewStream(connection_pool.RW) + stream, err := connPool.NewStream(pool.RW) if err != nil { fmt.Println(err) return @@ -696,7 +696,7 @@ func ExampleCommitRequest() { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"example_commit_key"}) - resp, err = pool.Do(selectReq, connection_pool.RW).Get() + resp, err = connPool.Do(selectReq, pool.RW).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return @@ -722,7 +722,7 @@ func ExampleCommitRequest() { // Select outside of transaction // example pool has only one rw instance - resp, err = pool.Do(selectReq, connection_pool.RW).Get() + resp, err = connPool.Do(selectReq, pool.RW).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return @@ -743,14 +743,14 @@ func ExampleRollbackRequest() { txnOpts := getTestTxnOpts() // example pool has only one rw instance - pool, err := examplePool(testRoles, txnOpts) + connPool, err := examplePool(testRoles, txnOpts) if err != nil { fmt.Println(err) return } - defer pool.Close() + defer connPool.Close() - stream, err := pool.NewStream(connection_pool.RW) + stream, err := connPool.NewStream(pool.RW) if err != nil { fmt.Println(err) return @@ -783,7 +783,7 @@ func ExampleRollbackRequest() { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"example_rollback_key"}) - resp, err = pool.Do(selectReq, connection_pool.RW).Get() + resp, err = connPool.Do(selectReq, pool.RW).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return @@ -809,7 +809,7 @@ func ExampleRollbackRequest() { // Select outside of transaction // example pool has only one rw instance - resp, err = pool.Do(selectReq, connection_pool.RW).Get() + resp, err = connPool.Do(selectReq, pool.RW).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return @@ -830,14 +830,14 @@ func ExampleBeginRequest_TxnIsolation() { txnOpts := getTestTxnOpts() // example pool has only one rw instance - pool, err := examplePool(testRoles, txnOpts) + connPool, err := examplePool(testRoles, txnOpts) if err != nil { fmt.Println(err) return } - defer pool.Close() + defer connPool.Close() - stream, err := pool.NewStream(connection_pool.RW) + stream, err := connPool.NewStream(pool.RW) if err != nil { fmt.Println(err) return @@ -872,7 +872,7 @@ func ExampleBeginRequest_TxnIsolation() { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"isolation_level_key"}) - resp, err = pool.Do(selectReq, connection_pool.RW).Get() + resp, err = connPool.Do(selectReq, pool.RW).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return @@ -898,7 +898,7 @@ func ExampleBeginRequest_TxnIsolation() { // Select outside of transaction // example pool has only one rw instance - resp, err = pool.Do(selectReq, connection_pool.RW).Get() + resp, err = connPool.Do(selectReq, pool.RW).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return @@ -907,13 +907,13 @@ func ExampleBeginRequest_TxnIsolation() { } func ExampleConnectorAdapter() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } - defer pool.Close() + defer connPool.Close() - adapter := connection_pool.NewConnectorAdapter(pool, connection_pool.RW) + adapter := pool.NewConnectorAdapter(connPool, pool.RW) var connector tarantool.Connector = adapter // Ping an RW instance to check connection. @@ -930,7 +930,7 @@ func ExampleConnectorAdapter() { // ExampleConnectionPool_CloseGraceful_force demonstrates how to force close // a connection pool with graceful close in progress after a while. func ExampleConnectionPool_CloseGraceful_force() { - pool, err := examplePool(testRoles, connOpts) + connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) return @@ -941,11 +941,11 @@ func ExampleConnectionPool_CloseGraceful_force() { fiber.sleep(time) ` req := tarantool.NewEvalRequest(eval).Args([]interface{}{10}) - fut := pool.Do(req, connection_pool.ANY) + fut := connPool.Do(req, pool.ANY) done := make(chan struct{}) go func() { - pool.CloseGraceful() + connPool.CloseGraceful() fmt.Println("ConnectionPool.CloseGraceful() done!") close(done) }() @@ -954,7 +954,7 @@ func ExampleConnectionPool_CloseGraceful_force() { case <-done: case <-time.After(3 * time.Second): fmt.Println("Force ConnectionPool.Close()!") - pool.Close() + connPool.Close() } <-done diff --git a/connection_pool/msgpack_helper_test.go b/pool/msgpack_helper_test.go similarity index 83% rename from connection_pool/msgpack_helper_test.go rename to pool/msgpack_helper_test.go index d60c7d84d..f54df2038 100644 --- a/connection_pool/msgpack_helper_test.go +++ b/pool/msgpack_helper_test.go @@ -1,7 +1,7 @@ //go:build !go_tarantool_msgpack_v5 // +build !go_tarantool_msgpack_v5 -package connection_pool_test +package pool_test import ( "gopkg.in/vmihailenco/msgpack.v2" diff --git a/connection_pool/msgpack_v5_helper_test.go b/pool/msgpack_v5_helper_test.go similarity index 83% rename from connection_pool/msgpack_v5_helper_test.go rename to pool/msgpack_v5_helper_test.go index 7c449bec5..a507ffa64 100644 --- a/connection_pool/msgpack_v5_helper_test.go +++ b/pool/msgpack_v5_helper_test.go @@ -1,7 +1,7 @@ //go:build go_tarantool_msgpack_v5 // +build go_tarantool_msgpack_v5 -package connection_pool_test +package pool_test import ( "github.com/vmihailenco/msgpack/v5" diff --git a/connection_pool/pooler.go b/pool/pooler.go similarity index 99% rename from connection_pool/pooler.go rename to pool/pooler.go index 31790620c..626c5af93 100644 --- a/connection_pool/pooler.go +++ b/pool/pooler.go @@ -1,4 +1,4 @@ -package connection_pool +package pool import ( "time" diff --git a/connection_pool/round_robin.go b/pool/round_robin.go similarity index 98% rename from connection_pool/round_robin.go rename to pool/round_robin.go index 0a0988890..6078b136e 100644 --- a/connection_pool/round_robin.go +++ b/pool/round_robin.go @@ -1,4 +1,4 @@ -package connection_pool +package pool import ( "sync" diff --git a/connection_pool/round_robin_test.go b/pool/round_robin_test.go similarity index 95% rename from connection_pool/round_robin_test.go rename to pool/round_robin_test.go index 254217e62..abd7cdfb0 100644 --- a/connection_pool/round_robin_test.go +++ b/pool/round_robin_test.go @@ -1,10 +1,10 @@ -package connection_pool_test +package pool_test import ( "testing" "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/connection_pool" + . "github.com/tarantool/go-tarantool/v2/pool" ) const ( diff --git a/connection_pool/state.go b/pool/state.go similarity index 94% rename from connection_pool/state.go rename to pool/state.go index 20cd070af..2af093e60 100644 --- a/connection_pool/state.go +++ b/pool/state.go @@ -1,4 +1,4 @@ -package connection_pool +package pool import ( "sync/atomic" diff --git a/connection_pool/watcher.go b/pool/watcher.go similarity index 99% rename from connection_pool/watcher.go rename to pool/watcher.go index f4acc0bdd..2d2b72e17 100644 --- a/connection_pool/watcher.go +++ b/pool/watcher.go @@ -1,4 +1,4 @@ -package connection_pool +package pool import ( "sync" diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go index 11cd3e9b3..51fb967a5 100644 --- a/queue/example_connection_pool_test.go +++ b/queue/example_connection_pool_test.go @@ -8,7 +8,7 @@ import ( "github.com/google/uuid" "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/connection_pool" + "github.com/tarantool/go-tarantool/v2/pool" "github.com/tarantool/go-tarantool/v2/queue" "github.com/tarantool/go-tarantool/v2/test_helpers" ) @@ -27,7 +27,7 @@ type QueueConnectionHandler struct { } // QueueConnectionHandler implements the ConnectionHandler interface. -var _ connection_pool.ConnectionHandler = &QueueConnectionHandler{} +var _ pool.ConnectionHandler = &QueueConnectionHandler{} // NewQueueConnectionHandler creates a QueueConnectionHandler object. func NewQueueConnectionHandler(name string, cfg queue.Cfg) *QueueConnectionHandler { @@ -44,7 +44,7 @@ func NewQueueConnectionHandler(name string, cfg queue.Cfg) *QueueConnectionHandl // NOTE: the Queue supports only a master-replica cluster configuration. It // does not support a master-master configuration. func (h *QueueConnectionHandler) Discovered(conn *tarantool.Connection, - role connection_pool.Role) error { + role pool.Role) error { h.mutex.Lock() defer h.mutex.Unlock() @@ -52,7 +52,7 @@ func (h *QueueConnectionHandler) Discovered(conn *tarantool.Connection, return h.err } - master := role == connection_pool.MasterRole + master := role == pool.MasterRole q := queue.New(conn, h.name) @@ -113,8 +113,8 @@ func (h *QueueConnectionHandler) Discovered(conn *tarantool.Connection, // Deactivated doesn't do anything useful for the example. func (h *QueueConnectionHandler) Deactivated(conn *tarantool.Connection, - role connection_pool.Role) error { - if role == connection_pool.MasterRole { + role pool.Role) error { + if role == pool.MasterRole { atomic.AddInt32(&h.masterCnt, -1) } return nil @@ -125,7 +125,7 @@ func (h *QueueConnectionHandler) Close() { close(h.updated) } -// Example demonstrates how to use the queue package with the connection_pool +// Example demonstrates how to use the queue package with the pool // package. First of all, you need to create a ConnectionHandler implementation // for the a ConnectionPool object to process new connections from // RW-instances. @@ -160,11 +160,11 @@ func Example_connectionPool() { User: "test", Pass: "test", } - poolOpts := connection_pool.OptsPool{ + poolOpts := pool.Opts{ CheckTimeout: 5 * time.Second, ConnectionHandler: h, } - connPool, err := connection_pool.ConnectWithOpts(servers, connOpts, poolOpts) + connPool, err := pool.ConnectWithOpts(servers, connOpts, poolOpts) if err != nil { fmt.Printf("Unable to connect to the pool: %s", err) return @@ -182,7 +182,7 @@ func Example_connectionPool() { // Create a Queue object from the ConnectionPool object via // a ConnectorAdapter. - rw := connection_pool.NewConnectorAdapter(connPool, connection_pool.RW) + rw := pool.NewConnectorAdapter(connPool, pool.RW) q := queue.New(rw, "test_queue") fmt.Println("A Queue object is ready to work.") @@ -229,7 +229,7 @@ func Example_connectionPool() { // Take a data from the new master instance. task, err := q.Take() - if err == connection_pool.ErrNoRwInstance { + if err == pool.ErrNoRwInstance { // It may be not registered yet by the pool. continue } else if err != nil { diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index 4833d4cc6..a559ef98d 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -6,20 +6,20 @@ import ( "time" "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/connection_pool" + "github.com/tarantool/go-tarantool/v2/pool" ) type ListenOnInstanceArgs struct { - ConnPool *connection_pool.ConnectionPool - Mode connection_pool.Mode + ConnPool *pool.ConnectionPool + Mode pool.Mode ServersNumber int ExpectedPorts map[string]bool } type CheckStatusesArgs struct { - ConnPool *connection_pool.ConnectionPool + ConnPool *pool.ConnectionPool Servers []string - Mode connection_pool.Mode + Mode pool.Mode ExpectedPoolStatus bool ExpectedStatuses map[string]bool } From 445c3aef5526c58ff5ed15dc3a40594ad9202bb1 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Sun, 25 Dec 2022 12:13:41 +0300 Subject: [PATCH 450/605] api: use msgpack/v5 instead of msgpack.v2 We found several critical bugs in the library msgpack.v2. It was decided to update the library to msgpack/v5. Closes #211 Closes #236 --- .github/workflows/testing.yml | 38 --------- CHANGELOG.md | 2 + README.md | 10 +-- box_error.go | 30 ++++--- box_error_test.go | 19 ++--- client_tools.go | 32 ++++---- connection.go | 14 ++-- crud/count.go | 6 +- crud/delete.go | 4 +- crud/error.go | 10 ++- crud/get.go | 6 +- crud/insert.go | 6 +- crud/insert_many.go | 6 +- crud/len.go | 4 +- crud/max.go | 4 +- crud/min.go | 4 +- crud/msgpack.go | 29 ------- crud/msgpack_helper_test.go | 10 --- crud/msgpack_v5.go | 29 ------- crud/msgpack_v5_helper_test.go | 10 --- crud/object.go | 17 ++++ crud/options.go | 19 +++-- crud/replace.go | 6 +- crud/replace_many.go | 6 +- crud/request_test.go | 25 ++++-- crud/result.go | 16 +++- crud/select.go | 6 +- crud/stats.go | 4 +- crud/storage_info.go | 8 +- crud/truncate.go | 4 +- crud/update.go | 4 +- crud/upsert.go | 6 +- crud/upsert_many.go | 6 +- datetime/datetime.go | 6 ++ datetime/datetime_test.go | 32 ++++---- datetime/interval.go | 35 ++++++-- datetime/msgpack.go | 28 ------- datetime/msgpack_helper_test.go | 26 ------ datetime/msgpack_v5.go | 42 ---------- datetime/msgpack_v5_helper_test.go | 28 ------- decimal/decimal.go | 5 ++ decimal/decimal_test.go | 60 +++++++------- decimal/msgpack.go | 12 --- decimal/msgpack_helper_test.go | 26 ------ decimal/msgpack_v5.go | 12 --- decimal/msgpack_v5_helper_test.go | 28 ------- dial.go | 6 +- example_custom_unpacking_test.go | 8 +- export_test.go | 41 +++++----- go.mod | 3 - go.sum | 19 ----- msgpack.go | 54 ------------- msgpack_helper_test.go | 29 ------- msgpack_v5.go | 58 ------------- msgpack_v5_helper_test.go | 32 -------- pool/connection_pool_test.go | 4 +- pool/msgpack_helper_test.go | 10 --- pool/msgpack_v5_helper_test.go | 10 --- prepared.go | 32 ++++---- protocol.go | 12 +-- queue/example_msgpack_test.go | 6 +- queue/msgpack.go | 10 --- queue/msgpack_helper_test.go | 11 --- queue/msgpack_v5.go | 10 --- queue/msgpack_v5_helper_test.go | 11 --- queue/queue.go | 8 +- queue/queue_test.go | 6 +- queue/task.go | 4 +- request.go | 126 +++++++++++++++-------------- request_test.go | 74 +++++++++-------- response.go | 22 +++-- schema.go | 31 ++++++- settings/msgpack.go | 10 --- settings/msgpack_helper_test.go | 22 ----- settings/msgpack_v5.go | 10 --- settings/msgpack_v5_helper_test.go | 25 ------ settings/request.go | 6 +- settings/request_test.go | 4 +- settings/tarantool_test.go | 2 +- stream.go | 26 +++--- tarantool_test.go | 11 +-- test_helpers/main.go | 2 +- test_helpers/msgpack.go | 10 --- test_helpers/msgpack_v5.go | 10 --- test_helpers/request_mock.go | 4 +- test_helpers/utils.go | 15 ---- uuid/msgpack.go | 19 ----- uuid/msgpack_helper_test.go | 10 --- uuid/msgpack_v5.go | 27 ------- uuid/msgpack_v5_helper_test.go | 10 --- uuid/uuid.go | 20 ++++- uuid/uuid_test.go | 4 +- watch.go | 18 +++-- 93 files changed, 555 insertions(+), 1087 deletions(-) delete mode 100644 crud/msgpack.go delete mode 100644 crud/msgpack_helper_test.go delete mode 100644 crud/msgpack_v5.go delete mode 100644 crud/msgpack_v5_helper_test.go create mode 100644 crud/object.go delete mode 100644 datetime/msgpack.go delete mode 100644 datetime/msgpack_helper_test.go delete mode 100644 datetime/msgpack_v5.go delete mode 100644 datetime/msgpack_v5_helper_test.go delete mode 100644 decimal/msgpack.go delete mode 100644 decimal/msgpack_helper_test.go delete mode 100644 decimal/msgpack_v5.go delete mode 100644 decimal/msgpack_v5_helper_test.go delete mode 100644 msgpack.go delete mode 100644 msgpack_helper_test.go delete mode 100644 msgpack_v5.go delete mode 100644 msgpack_v5_helper_test.go delete mode 100644 pool/msgpack_helper_test.go delete mode 100644 pool/msgpack_v5_helper_test.go delete mode 100644 queue/msgpack.go delete mode 100644 queue/msgpack_helper_test.go delete mode 100644 queue/msgpack_v5.go delete mode 100644 queue/msgpack_v5_helper_test.go delete mode 100644 settings/msgpack.go delete mode 100644 settings/msgpack_helper_test.go delete mode 100644 settings/msgpack_v5.go delete mode 100644 settings/msgpack_v5_helper_test.go delete mode 100644 test_helpers/msgpack.go delete mode 100644 test_helpers/msgpack_v5.go delete mode 100644 uuid/msgpack.go delete mode 100644 uuid/msgpack_helper_test.go delete mode 100644 uuid/msgpack_v5.go delete mode 100644 uuid/msgpack_v5_helper_test.go diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 7d90fa892..68f283b95 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -104,16 +104,6 @@ jobs: make test TAGS="go_tarantool_call_17" make testrace TAGS="go_tarantool_call_17" - - name: Run regression tests with msgpack.v5 - run: | - make test TAGS="go_tarantool_msgpack_v5" - make testrace TAGS="go_tarantool_msgpack_v5" - - - name: Run regression tests with msgpack.v5 and call_17 - run: | - make test TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" - make testrace TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" - - name: Run fuzzing tests if: ${{ matrix.fuzzing }} run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" @@ -207,22 +197,6 @@ jobs: env: TEST_TNT_SSL: ${{matrix.ssl}} - - name: Run regression tests with msgpack.v5 - run: | - source tarantool-enterprise/env.sh - make test TAGS="go_tarantool_msgpack_v5" - make testrace TAGS="go_tarantool_msgpack_v5" - env: - TEST_TNT_SSL: ${{matrix.ssl}} - - - name: Run regression tests with msgpack.v5 and call_17 - run: | - source tarantool-enterprise/env.sh - make test TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" - make testrace TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" - env: - TEST_TNT_SSL: ${{matrix.ssl}} - - name: Run fuzzing tests if: ${{ matrix.fuzzing }} run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" @@ -395,18 +369,6 @@ jobs: make test TAGS="go_tarantool_call_17" make testrace TAGS="go_tarantool_call_17" - - name: Run regression tests with msgpack.v5 - run: | - cd "${SRCDIR}" - make test TAGS="go_tarantool_msgpack_v5" - make testrace TAGS="go_tarantool_msgpack_v5" - - - name: Run regression tests with msgpack.v5 and call_17 - run: | - cd "${SRCDIR}" - make test TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" - make testrace TAGS="go_tarantool_msgpack_v5,go_tarantool_call_17" - - name: Run fuzzing tests if: ${{ matrix.fuzzing }} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e21632c1..7f9d1b99a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,12 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Changed - connection_pool renamed to pool (#239) +- Use msgpack/v5 instead of msgpack.v2 (#236) ### Removed - multi subpackage (#240) +- msgpack.v2 support (#236) ### Fixed diff --git a/README.md b/README.md index 8151f71e2..ea941d964 100644 --- a/README.md +++ b/README.md @@ -72,15 +72,7 @@ This allows us to introduce new features without losing backward compatibility. go_tarantool_call_17 ``` **Note:** In future releases, `Call17` may be used as default `Call` behavior. -3. To replace usage of `msgpack.v2` with `msgpack.v5`, you can use the build - tag: - ``` - go_tarantool_msgpack_v5 - ``` - **Note:** In future releases, `msgpack.v5` may be used by default. We recommend - to read [msgpack.v5 migration notes](#msgpackv5-migration) and try to - use msgpack.v5 before the changes. -4. To run fuzz tests with decimals, you can use the build tag: +3. To run fuzz tests with decimals, you can use the build tag: ``` go_tarantool_decimal_fuzzing ``` diff --git a/box_error.go b/box_error.go index ab8981b0b..3d76a942c 100644 --- a/box_error.go +++ b/box_error.go @@ -3,6 +3,8 @@ package tarantool import ( "bytes" "fmt" + + "github.com/vmihailenco/msgpack/v5" ) const errorExtID = 3 @@ -69,7 +71,7 @@ func (e *BoxError) Depth() int { return depth } -func decodeBoxError(d *decoder) (*BoxError, error) { +func decodeBoxError(d *msgpack.Decoder) (*BoxError, error) { var l, larr, l1, l2 int var errorStack []BoxError var err error @@ -169,7 +171,7 @@ func decodeBoxError(d *decoder) (*BoxError, error) { return &errorStack[0], nil } -func encodeBoxError(enc *encoder, boxError *BoxError) error { +func encodeBoxError(enc *msgpack.Encoder, boxError *BoxError) error { if boxError == nil { return fmt.Errorf("msgpack: unexpected nil BoxError on encode") } @@ -177,7 +179,7 @@ func encodeBoxError(enc *encoder, boxError *BoxError) error { if err := enc.EncodeMapLen(1); err != nil { return err } - if err := encodeUint(enc, keyErrorStack); err != nil { + if err := enc.EncodeUint(keyErrorStack); err != nil { return err } @@ -199,42 +201,42 @@ func encodeBoxError(enc *encoder, boxError *BoxError) error { } } - if err := encodeUint(enc, keyErrorType); err != nil { + if err := enc.EncodeUint(keyErrorType); err != nil { return err } if err := enc.EncodeString(boxError.Type); err != nil { return err } - if err := encodeUint(enc, keyErrorFile); err != nil { + if err := enc.EncodeUint(keyErrorFile); err != nil { return err } if err := enc.EncodeString(boxError.File); err != nil { return err } - if err := encodeUint(enc, keyErrorLine); err != nil { + if err := enc.EncodeUint(keyErrorLine); err != nil { return err } if err := enc.EncodeUint64(boxError.Line); err != nil { return err } - if err := encodeUint(enc, keyErrorMessage); err != nil { + if err := enc.EncodeUint(keyErrorMessage); err != nil { return err } if err := enc.EncodeString(boxError.Msg); err != nil { return err } - if err := encodeUint(enc, keyErrorErrno); err != nil { + if err := enc.EncodeUint(keyErrorErrno); err != nil { return err } if err := enc.EncodeUint64(boxError.Errno); err != nil { return err } - if err := encodeUint(enc, keyErrorErrcode); err != nil { + if err := enc.EncodeUint(keyErrorErrcode); err != nil { return err } if err := enc.EncodeUint64(boxError.Code); err != nil { @@ -242,7 +244,7 @@ func encodeBoxError(enc *encoder, boxError *BoxError) error { } if fieldsLen > 0 { - if err := encodeUint(enc, keyErrorFields); err != nil { + if err := enc.EncodeUint(keyErrorFields); err != nil { return err } @@ -276,7 +278,7 @@ func (e *BoxError) UnmarshalMsgpack(b []byte) error { } buf := bytes.NewBuffer(b) - dec := newDecoder(buf) + dec := msgpack.NewDecoder(buf) if val, err := decodeBoxError(dec); err != nil { return err @@ -290,10 +292,14 @@ func (e *BoxError) UnmarshalMsgpack(b []byte) error { func (e *BoxError) MarshalMsgpack() ([]byte, error) { var buf bytes.Buffer - enc := newEncoder(&buf) + enc := msgpack.NewEncoder(&buf) if err := encodeBoxError(enc, e); err != nil { return nil, err } return buf.Bytes(), nil } + +func init() { + msgpack.RegisterExt(errorExtID, (*BoxError)(nil)) +} diff --git a/box_error_test.go b/box_error_test.go index 4c38d359c..d8ff5b11f 100644 --- a/box_error_test.go +++ b/box_error_test.go @@ -6,6 +6,8 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack/v5" + . "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/test_helpers" ) @@ -217,7 +219,7 @@ type TupleBoxError struct { val BoxError } -func (t *TupleBoxError) EncodeMsgpack(e *encoder) error { +func (t *TupleBoxError) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeArrayLen(2); err != nil { return err } @@ -229,7 +231,7 @@ func (t *TupleBoxError) EncodeMsgpack(e *encoder) error { return e.Encode(&t.val) } -func (t *TupleBoxError) DecodeMsgpack(d *decoder) error { +func (t *TupleBoxError) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { @@ -278,11 +280,11 @@ var tupleCases = map[string]struct { func TestErrorTypeMPEncodeDecode(t *testing.T) { for name, testcase := range tupleCases { t.Run(name, func(t *testing.T) { - buf, err := marshal(&testcase.tuple) + buf, err := msgpack.Marshal(&testcase.tuple) require.Nil(t, err) var res TupleBoxError - err = unmarshal(buf, &res) + err = msgpack.Unmarshal(buf, &res) require.Nil(t, err) require.Equal(t, testcase.tuple, res) @@ -302,9 +304,9 @@ func TestErrorTypeEval(t *testing.T) { require.Nil(t, err) require.NotNil(t, resp.Data) require.Equal(t, len(resp.Data), 1) - actual, ok := toBoxError(resp.Data[0]) + actual, ok := resp.Data[0].(*BoxError) require.Truef(t, ok, "Response data has valid type") - require.Equal(t, testcase.tuple.val, actual) + require.Equal(t, testcase.tuple.val, *actual) }) } } @@ -440,14 +442,13 @@ func TestErrorTypeSelect(t *testing.T) { tpl, ok := resp.Data[0].([]interface{}) require.Truef(t, ok, "Tuple has valid type") require.Equal(t, testcase.tuple.pk, tpl[0]) - var actual BoxError - actual, ok = toBoxError(tpl[1]) + actual, ok := tpl[1].(*BoxError) require.Truef(t, ok, "BoxError tuple field has valid type") // In fact, CheckEqualBoxErrors does not check than File and Line // of connector BoxError are equal to the Tarantool ones // since they may differ between different Tarantool versions // and editions. - test_helpers.CheckEqualBoxErrors(t, testcase.tuple.val, actual) + test_helpers.CheckEqualBoxErrors(t, testcase.tuple.val, *actual) }) } } diff --git a/client_tools.go b/client_tools.go index 9c439ec21..159f3d0ce 100644 --- a/client_tools.go +++ b/client_tools.go @@ -1,14 +1,18 @@ package tarantool +import ( + "github.com/vmihailenco/msgpack/v5" +) + // IntKey is utility type for passing integer key to Select*, Update*, // Delete* and GetTyped. It serializes to array with single integer element. type IntKey struct { I int } -func (k IntKey) EncodeMsgpack(enc *encoder) error { +func (k IntKey) EncodeMsgpack(enc *msgpack.Encoder) error { enc.EncodeArrayLen(1) - encodeInt(enc, int64(k.I)) + enc.EncodeInt(int64(k.I)) return nil } @@ -19,9 +23,9 @@ type UintKey struct { I uint } -func (k UintKey) EncodeMsgpack(enc *encoder) error { +func (k UintKey) EncodeMsgpack(enc *msgpack.Encoder) error { enc.EncodeArrayLen(1) - encodeUint(enc, uint64(k.I)) + enc.EncodeUint(uint64(k.I)) return nil } @@ -31,7 +35,7 @@ type StringKey struct { S string } -func (k StringKey) EncodeMsgpack(enc *encoder) error { +func (k StringKey) EncodeMsgpack(enc *msgpack.Encoder) error { enc.EncodeArrayLen(1) enc.EncodeString(k.S) return nil @@ -43,10 +47,10 @@ type IntIntKey struct { I1, I2 int } -func (k IntIntKey) EncodeMsgpack(enc *encoder) error { +func (k IntIntKey) EncodeMsgpack(enc *msgpack.Encoder) error { enc.EncodeArrayLen(2) - encodeInt(enc, int64(k.I1)) - encodeInt(enc, int64(k.I2)) + enc.EncodeInt(int64(k.I1)) + enc.EncodeInt(int64(k.I2)) return nil } @@ -57,10 +61,10 @@ type Op struct { Arg interface{} } -func (o Op) EncodeMsgpack(enc *encoder) error { +func (o Op) EncodeMsgpack(enc *msgpack.Encoder) error { enc.EncodeArrayLen(3) enc.EncodeString(o.Op) - encodeInt(enc, int64(o.Field)) + enc.EncodeInt(int64(o.Field)) return enc.Encode(o.Arg) } @@ -145,12 +149,12 @@ type OpSplice struct { Replace string } -func (o OpSplice) EncodeMsgpack(enc *encoder) error { +func (o OpSplice) EncodeMsgpack(enc *msgpack.Encoder) error { enc.EncodeArrayLen(5) enc.EncodeString(o.Op) - encodeInt(enc, int64(o.Field)) - encodeInt(enc, int64(o.Pos)) - encodeInt(enc, int64(o.Len)) + enc.EncodeInt(int64(o.Field)) + enc.EncodeInt(int64(o.Pos)) + enc.EncodeInt(int64(o.Len)) enc.EncodeString(o.Replace) return nil } diff --git a/connection.go b/connection.go index 055941e78..a79657072 100644 --- a/connection.go +++ b/connection.go @@ -14,6 +14,8 @@ import ( "sync" "sync/atomic" "time" + + "github.com/vmihailenco/msgpack/v5" ) const requestsMap = 128 @@ -164,7 +166,7 @@ type Connection struct { rlimit chan struct{} opts Opts state uint32 - dec *decoder + dec *msgpack.Decoder lenbuf [PacketLengthBytes]byte lastStreamId uint64 @@ -230,7 +232,7 @@ type connShard struct { requestsWithCtx [requestsMap]futureList bufmut sync.Mutex buf smallWBuf - enc *encoder + enc *msgpack.Encoder } // Opts is a way to configure Connection @@ -364,7 +366,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { Greeting: &Greeting{}, control: make(chan struct{}), opts: opts.Clone(), - dec: newDecoder(&smallBuf{}), + dec: msgpack.NewDecoder(&smallBuf{}), } maxprocs := uint32(runtime.GOMAXPROCS(-1)) if conn.opts.Concurrency == 0 || conn.opts.Concurrency > maxprocs*128 { @@ -579,7 +581,7 @@ func (conn *Connection) dial() (err error) { return } -func pack(h *smallWBuf, enc *encoder, reqid uint32, +func pack(h *smallWBuf, enc *msgpack.Encoder, reqid uint32, req Request, streamId uint64, res SchemaResolver) (err error) { const uint32Code = 0xce const uint64Code = 0xcf @@ -798,7 +800,7 @@ func (conn *Connection) writer(w writeFlusher, c Conn) { func readWatchEvent(reader io.Reader) (connWatchEvent, error) { keyExist := false event := connWatchEvent{} - d := newDecoder(reader) + d := msgpack.NewDecoder(reader) l, err := d.DecodeMapLen() if err != nil { @@ -1048,7 +1050,7 @@ func (conn *Connection) putFuture(fut *Future, req Request, streamId uint64) { firstWritten := shard.buf.Len() == 0 if shard.buf.Cap() == 0 { shard.buf.b = make([]byte, 0, 128) - shard.enc = newEncoder(&shard.buf) + shard.enc = msgpack.NewEncoder(&shard.buf) } blen := shard.buf.Len() reqid := fut.requestId diff --git a/crud/count.go b/crud/count.go index aede8f800..dc89f916e 100644 --- a/crud/count.go +++ b/crud/count.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -39,7 +41,7 @@ type CountOpts struct { } // EncodeMsgpack provides custom msgpack encoder. -func (opts CountOpts) EncodeMsgpack(enc *encoder) error { +func (opts CountOpts) EncodeMsgpack(enc *msgpack.Encoder) error { const optsCnt = 9 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, @@ -101,7 +103,7 @@ func (req CountRequest) Opts(opts CountOpts) CountRequest { } // Body fills an encoder with the call request body. -func (req CountRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req CountRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { args := countArgs{Space: req.space, Conditions: req.conditions, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) diff --git a/crud/delete.go b/crud/delete.go index cd6e59382..f37da7ac5 100644 --- a/crud/delete.go +++ b/crud/delete.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -48,7 +50,7 @@ func (req DeleteRequest) Opts(opts DeleteOpts) DeleteRequest { } // Body fills an encoder with the call request body. -func (req DeleteRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req DeleteRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if req.key == nil { req.key = []interface{}{} } diff --git a/crud/error.go b/crud/error.go index 12d416cfd..d7c36a333 100644 --- a/crud/error.go +++ b/crud/error.go @@ -1,6 +1,10 @@ package crud -import "strings" +import ( + "strings" + + "github.com/vmihailenco/msgpack/v5" +) // Error describes CRUD error object. type Error struct { @@ -20,7 +24,7 @@ type Error struct { } // DecodeMsgpack provides custom msgpack decoder. -func (e *Error) DecodeMsgpack(d *decoder) error { +func (e *Error) DecodeMsgpack(d *msgpack.Decoder) error { l, err := d.DecodeMapLen() if err != nil { return err @@ -76,7 +80,7 @@ type ErrorMany struct { } // DecodeMsgpack provides custom msgpack decoder. -func (e *ErrorMany) DecodeMsgpack(d *decoder) error { +func (e *ErrorMany) DecodeMsgpack(d *msgpack.Decoder) error { l, err := d.DecodeArrayLen() if err != nil { return err diff --git a/crud/get.go b/crud/get.go index f63bfda9f..e1855f35c 100644 --- a/crud/get.go +++ b/crud/get.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -30,7 +32,7 @@ type GetOpts struct { } // EncodeMsgpack provides custom msgpack encoder. -func (opts GetOpts) EncodeMsgpack(enc *encoder) error { +func (opts GetOpts) EncodeMsgpack(enc *msgpack.Encoder) error { const optsCnt = 7 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, @@ -87,7 +89,7 @@ func (req GetRequest) Opts(opts GetOpts) GetRequest { } // Body fills an encoder with the call request body. -func (req GetRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req GetRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if req.key == nil { req.key = []interface{}{} } diff --git a/crud/insert.go b/crud/insert.go index 2f09613a7..c696d201f 100644 --- a/crud/insert.go +++ b/crud/insert.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -48,7 +50,7 @@ func (req InsertRequest) Opts(opts InsertOpts) InsertRequest { } // Body fills an encoder with the call request body. -func (req InsertRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req InsertRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if req.tuple == nil { req.tuple = []interface{}{} } @@ -106,7 +108,7 @@ func (req InsertObjectRequest) Opts(opts InsertObjectOpts) InsertObjectRequest { } // Body fills an encoder with the call request body. -func (req InsertObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req InsertObjectRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if req.object == nil { req.object = MapObject{} } diff --git a/crud/insert_many.go b/crud/insert_many.go index a3c3aead3..602e210d5 100644 --- a/crud/insert_many.go +++ b/crud/insert_many.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -48,7 +50,7 @@ func (req InsertManyRequest) Opts(opts InsertManyOpts) InsertManyRequest { } // Body fills an encoder with the call request body. -func (req InsertManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req InsertManyRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if req.tuples == nil { req.tuples = []Tuple{} } @@ -106,7 +108,7 @@ func (req InsertObjectManyRequest) Opts(opts InsertObjectManyOpts) InsertObjectM } // Body fills an encoder with the call request body. -func (req InsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req InsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if req.objects == nil { req.objects = []Object{} } diff --git a/crud/len.go b/crud/len.go index dc8a4cb10..5fef700d7 100644 --- a/crud/len.go +++ b/crud/len.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -42,7 +44,7 @@ func (req LenRequest) Opts(opts LenOpts) LenRequest { } // Body fills an encoder with the call request body. -func (req LenRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req LenRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { args := lenArgs{Space: req.space, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) diff --git a/crud/max.go b/crud/max.go index 7464b0480..727a17ac5 100644 --- a/crud/max.go +++ b/crud/max.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -48,7 +50,7 @@ func (req MaxRequest) Opts(opts MaxOpts) MaxRequest { } // Body fills an encoder with the call request body. -func (req MaxRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req MaxRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { args := maxArgs{Space: req.space, Index: req.index, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) diff --git a/crud/min.go b/crud/min.go index f186d303e..ab3bcfe07 100644 --- a/crud/min.go +++ b/crud/min.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -48,7 +50,7 @@ func (req MinRequest) Opts(opts MinOpts) MinRequest { } // Body fills an encoder with the call request body. -func (req MinRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req MinRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { args := minArgs{Space: req.space, Index: req.index, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) diff --git a/crud/msgpack.go b/crud/msgpack.go deleted file mode 100644 index b9696b15e..000000000 --- a/crud/msgpack.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package crud - -import ( - "gopkg.in/vmihailenco/msgpack.v2" - msgpcode "gopkg.in/vmihailenco/msgpack.v2/codes" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -// Object is an interface to describe object for CRUD methods. -type Object interface { - EncodeMsgpack(enc *encoder) -} - -// MapObject is a type to describe object as a map. -type MapObject map[string]interface{} - -func (o MapObject) EncodeMsgpack(enc *encoder) { - enc.Encode(o) -} - -func msgpackIsArray(code byte) bool { - return code == msgpcode.Array16 || code == msgpcode.Array32 || - msgpcode.IsFixedArray(code) -} diff --git a/crud/msgpack_helper_test.go b/crud/msgpack_helper_test.go deleted file mode 100644 index 7d3998fc6..000000000 --- a/crud/msgpack_helper_test.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package crud_test - -import ( - "gopkg.in/vmihailenco/msgpack.v2" -) - -var newEncoder = msgpack.NewEncoder diff --git a/crud/msgpack_v5.go b/crud/msgpack_v5.go deleted file mode 100644 index 393e359c3..000000000 --- a/crud/msgpack_v5.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package crud - -import ( - "github.com/vmihailenco/msgpack/v5" - "github.com/vmihailenco/msgpack/v5/msgpcode" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -// Object is an interface to describe object for CRUD methods. -type Object interface { - EncodeMsgpack(enc *encoder) -} - -// MapObject is a type to describe object as a map. -type MapObject map[string]interface{} - -func (o MapObject) EncodeMsgpack(enc *encoder) { - enc.Encode(o) -} - -func msgpackIsArray(code byte) bool { - return code == msgpcode.Array16 || code == msgpcode.Array32 || - msgpcode.IsFixedArray(code) -} diff --git a/crud/msgpack_v5_helper_test.go b/crud/msgpack_v5_helper_test.go deleted file mode 100644 index f3700bebc..000000000 --- a/crud/msgpack_v5_helper_test.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package crud_test - -import ( - "github.com/vmihailenco/msgpack/v5" -) - -var newEncoder = msgpack.NewEncoder diff --git a/crud/object.go b/crud/object.go new file mode 100644 index 000000000..3f266a7ee --- /dev/null +++ b/crud/object.go @@ -0,0 +1,17 @@ +package crud + +import ( + "github.com/vmihailenco/msgpack/v5" +) + +// Object is an interface to describe object for CRUD methods. +type Object interface { + EncodeMsgpack(enc *msgpack.Encoder) +} + +// MapObject is a type to describe object as a map. +type MapObject map[string]interface{} + +func (o MapObject) EncodeMsgpack(enc *msgpack.Encoder) { + enc.Encode(o) +} diff --git a/crud/options.go b/crud/options.go index 9bec34754..c073b7222 100644 --- a/crud/options.go +++ b/crud/options.go @@ -1,5 +1,9 @@ package crud +import ( + "github.com/vmihailenco/msgpack/v5" +) + const ( timeoutOptName = "timeout" vshardRouterOptName = "vshard_router" @@ -121,7 +125,7 @@ type BaseOpts struct { } // EncodeMsgpack provides custom msgpack encoder. -func (opts BaseOpts) EncodeMsgpack(enc *encoder) error { +func (opts BaseOpts) EncodeMsgpack(enc *msgpack.Encoder) error { const optsCnt = 2 names := [optsCnt]string{timeoutOptName, vshardRouterOptName} @@ -148,7 +152,7 @@ type SimpleOperationOpts struct { } // EncodeMsgpack provides custom msgpack encoder. -func (opts SimpleOperationOpts) EncodeMsgpack(enc *encoder) error { +func (opts SimpleOperationOpts) EncodeMsgpack(enc *msgpack.Encoder) error { const optsCnt = 4 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, @@ -182,7 +186,7 @@ type SimpleOperationObjectOpts struct { } // EncodeMsgpack provides custom msgpack encoder. -func (opts SimpleOperationObjectOpts) EncodeMsgpack(enc *encoder) error { +func (opts SimpleOperationObjectOpts) EncodeMsgpack(enc *msgpack.Encoder) error { const optsCnt = 5 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, @@ -218,7 +222,7 @@ type OperationManyOpts struct { } // EncodeMsgpack provides custom msgpack encoder. -func (opts OperationManyOpts) EncodeMsgpack(enc *encoder) error { +func (opts OperationManyOpts) EncodeMsgpack(enc *msgpack.Encoder) error { const optsCnt = 5 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, @@ -258,7 +262,7 @@ type OperationObjectManyOpts struct { } // EncodeMsgpack provides custom msgpack encoder. -func (opts OperationObjectManyOpts) EncodeMsgpack(enc *encoder) error { +func (opts OperationObjectManyOpts) EncodeMsgpack(enc *msgpack.Encoder) error { const optsCnt = 6 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, @@ -289,7 +293,7 @@ type BorderOpts struct { } // EncodeMsgpack provides custom msgpack encoder. -func (opts BorderOpts) EncodeMsgpack(enc *encoder) error { +func (opts BorderOpts) EncodeMsgpack(enc *msgpack.Encoder) error { const optsCnt = 3 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName} @@ -302,7 +306,8 @@ func (opts BorderOpts) EncodeMsgpack(enc *encoder) error { return encodeOptions(enc, names[:], values[:], exists[:]) } -func encodeOptions(enc *encoder, names []string, values []interface{}, exists []bool) error { +func encodeOptions(enc *msgpack.Encoder, + names []string, values []interface{}, exists []bool) error { mapLen := 0 for _, exist := range exists { diff --git a/crud/replace.go b/crud/replace.go index 87a60930a..8231c9aa5 100644 --- a/crud/replace.go +++ b/crud/replace.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -48,7 +50,7 @@ func (req ReplaceRequest) Opts(opts ReplaceOpts) ReplaceRequest { } // Body fills an encoder with the call request body. -func (req ReplaceRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req ReplaceRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if req.tuple == nil { req.tuple = []interface{}{} } @@ -106,7 +108,7 @@ func (req ReplaceObjectRequest) Opts(opts ReplaceObjectOpts) ReplaceObjectReques } // Body fills an encoder with the call request body. -func (req ReplaceObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req ReplaceObjectRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if req.object == nil { req.object = MapObject{} } diff --git a/crud/replace_many.go b/crud/replace_many.go index 7216a67c2..77c947718 100644 --- a/crud/replace_many.go +++ b/crud/replace_many.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -48,7 +50,7 @@ func (req ReplaceManyRequest) Opts(opts ReplaceManyOpts) ReplaceManyRequest { } // Body fills an encoder with the call request body. -func (req ReplaceManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req ReplaceManyRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if req.tuples == nil { req.tuples = []Tuple{} } @@ -106,7 +108,7 @@ func (req ReplaceObjectManyRequest) Opts(opts ReplaceObjectManyOpts) ReplaceObje } // Body fills an encoder with the call request body. -func (req ReplaceObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req ReplaceObjectManyRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if req.objects == nil { req.objects = []Object{} } diff --git a/crud/request_test.go b/crud/request_test.go index 3e4f171a7..5a81e0d1e 100644 --- a/crud/request_test.go +++ b/crud/request_test.go @@ -4,11 +4,13 @@ import ( "bytes" "context" "errors" + "fmt" "testing" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/crud" - "github.com/tarantool/go-tarantool/v2/test_helpers" ) const invalidSpaceMsg = "invalid space" @@ -91,15 +93,28 @@ func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (spaceNo, indexN var resolver ValidSchemeResolver +func extractRequestBody(req tarantool.Request, + resolver tarantool.SchemaResolver) ([]byte, error) { + var reqBuf bytes.Buffer + reqEnc := msgpack.NewEncoder(&reqBuf) + + err := req.Body(resolver, reqEnc) + if err != nil { + return nil, fmt.Errorf("An unexpected Response.Body() error: %q", err.Error()) + } + + return reqBuf.Bytes(), nil +} + func assertBodyEqual(t testing.TB, reference tarantool.Request, req tarantool.Request) { t.Helper() - reqBody, err := test_helpers.ExtractRequestBody(req, &resolver, newEncoder) + reqBody, err := extractRequestBody(req, &resolver) if err != nil { t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) } - refBody, err := test_helpers.ExtractRequestBody(reference, &resolver, newEncoder) + refBody, err := extractRequestBody(reference, &resolver) if err != nil { t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) } @@ -112,7 +127,7 @@ func assertBodyEqual(t testing.TB, reference tarantool.Request, req tarantool.Re func BenchmarkLenRequest(b *testing.B) { buf := bytes.Buffer{} buf.Grow(512 * 1024 * 1024) // Avoid allocs in test. - enc := newEncoder(&buf) + enc := msgpack.NewEncoder(&buf) b.ResetTimer() @@ -131,7 +146,7 @@ func BenchmarkLenRequest(b *testing.B) { func BenchmarkSelectRequest(b *testing.B) { buf := bytes.Buffer{} buf.Grow(512 * 1024 * 1024) // Avoid allocs in test. - enc := newEncoder(&buf) + enc := msgpack.NewEncoder(&buf) b.ResetTimer() diff --git a/crud/result.go b/crud/result.go index 5ee556a5f..100ec2a95 100644 --- a/crud/result.go +++ b/crud/result.go @@ -3,6 +3,9 @@ package crud import ( "fmt" "reflect" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" ) // FieldFormat contains field definition: {name='...',type='...'[,is_nullable=...]}. @@ -13,7 +16,7 @@ type FieldFormat struct { } // DecodeMsgpack provides custom msgpack decoder. -func (format *FieldFormat) DecodeMsgpack(d *decoder) error { +func (format *FieldFormat) DecodeMsgpack(d *msgpack.Decoder) error { l, err := d.DecodeMapLen() if err != nil { return err @@ -60,8 +63,13 @@ func MakeResult(rowType reflect.Type) Result { } } +func msgpackIsArray(code byte) bool { + return code == msgpcode.Array16 || code == msgpcode.Array32 || + msgpcode.IsFixedArray(code) +} + // DecodeMsgpack provides custom msgpack decoder. -func (r *Result) DecodeMsgpack(d *decoder) error { +func (r *Result) DecodeMsgpack(d *msgpack.Decoder) error { arrLen, err := d.DecodeArrayLen() if err != nil { return err @@ -162,7 +170,7 @@ type NumberResult struct { } // DecodeMsgpack provides custom msgpack decoder. -func (r *NumberResult) DecodeMsgpack(d *decoder) error { +func (r *NumberResult) DecodeMsgpack(d *msgpack.Decoder) error { arrLen, err := d.DecodeArrayLen() if err != nil { return err @@ -201,7 +209,7 @@ type BoolResult struct { } // DecodeMsgpack provides custom msgpack decoder. -func (r *BoolResult) DecodeMsgpack(d *decoder) error { +func (r *BoolResult) DecodeMsgpack(d *msgpack.Decoder) error { arrLen, err := d.DecodeArrayLen() if err != nil { return err diff --git a/crud/select.go b/crud/select.go index cce17ccf4..bdd0e9d42 100644 --- a/crud/select.go +++ b/crud/select.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -42,7 +44,7 @@ type SelectOpts struct { } // EncodeMsgpack provides custom msgpack encoder. -func (opts SelectOpts) EncodeMsgpack(enc *encoder) error { +func (opts SelectOpts) EncodeMsgpack(enc *msgpack.Encoder) error { const optsCnt = 12 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, @@ -108,7 +110,7 @@ func (req SelectRequest) Opts(opts SelectOpts) SelectRequest { } // Body fills an encoder with the call request body. -func (req SelectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req SelectRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { args := selectArgs{Space: req.space, Conditions: req.conditions, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) diff --git a/crud/stats.go b/crud/stats.go index d29c0eb70..47737f33a 100644 --- a/crud/stats.go +++ b/crud/stats.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -28,7 +30,7 @@ func (req StatsRequest) Space(space string) StatsRequest { } // Body fills an encoder with the call request body. -func (req StatsRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req StatsRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if value, ok := req.space.Get(); ok { req.impl = req.impl.Args([]interface{}{value}) } else { diff --git a/crud/storage_info.go b/crud/storage_info.go index 146e0a22e..e2d67aadb 100644 --- a/crud/storage_info.go +++ b/crud/storage_info.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -14,7 +16,7 @@ type StatusTable struct { } // DecodeMsgpack provides custom msgpack decoder. -func (statusTable *StatusTable) DecodeMsgpack(d *decoder) error { +func (statusTable *StatusTable) DecodeMsgpack(d *msgpack.Decoder) error { l, err := d.DecodeMapLen() if err != nil { return err @@ -54,7 +56,7 @@ type StorageInfoResult struct { } // DecodeMsgpack provides custom msgpack decoder. -func (r *StorageInfoResult) DecodeMsgpack(d *decoder) error { +func (r *StorageInfoResult) DecodeMsgpack(d *msgpack.Decoder) error { _, err := d.DecodeArrayLen() if err != nil { return err @@ -116,7 +118,7 @@ func (req StorageInfoRequest) Opts(opts StorageInfoOpts) StorageInfoRequest { } // Body fills an encoder with the call request body. -func (req StorageInfoRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req StorageInfoRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { args := storageInfoArgs{Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) diff --git a/crud/truncate.go b/crud/truncate.go index 5ad1d785e..9f80063d1 100644 --- a/crud/truncate.go +++ b/crud/truncate.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -42,7 +44,7 @@ func (req TruncateRequest) Opts(opts TruncateOpts) TruncateRequest { } // Body fills an encoder with the call request body. -func (req TruncateRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req TruncateRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { args := truncateArgs{Space: req.space, Opts: req.opts} req.impl = req.impl.Args(args) return req.impl.Body(res, enc) diff --git a/crud/update.go b/crud/update.go index a2b9a5572..41ebd2c09 100644 --- a/crud/update.go +++ b/crud/update.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -58,7 +60,7 @@ func (req UpdateRequest) Opts(opts UpdateOpts) UpdateRequest { } // Body fills an encoder with the call request body. -func (req UpdateRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req UpdateRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if req.key == nil { req.key = []interface{}{} } diff --git a/crud/upsert.go b/crud/upsert.go index 8fc9efa69..e44523d45 100644 --- a/crud/upsert.go +++ b/crud/upsert.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -58,7 +60,7 @@ func (req UpsertRequest) Opts(opts UpsertOpts) UpsertRequest { } // Body fills an encoder with the call request body. -func (req UpsertRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req UpsertRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if req.tuple == nil { req.tuple = []interface{}{} } @@ -127,7 +129,7 @@ func (req UpsertObjectRequest) Opts(opts UpsertObjectOpts) UpsertObjectRequest { } // Body fills an encoder with the call request body. -func (req UpsertObjectRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req UpsertObjectRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if req.object == nil { req.object = MapObject{} } diff --git a/crud/upsert_many.go b/crud/upsert_many.go index 884a574ed..c7a54ba05 100644 --- a/crud/upsert_many.go +++ b/crud/upsert_many.go @@ -3,6 +3,8 @@ package crud import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -57,7 +59,7 @@ func (req UpsertManyRequest) Opts(opts UpsertManyOpts) UpsertManyRequest { } // Body fills an encoder with the call request body. -func (req UpsertManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req UpsertManyRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { args := upsertManyArgs{Space: req.space, TuplesOperationsData: req.tuplesOperationsData, Opts: req.opts} req.impl = req.impl.Args(args) @@ -123,7 +125,7 @@ func (req UpsertObjectManyRequest) Opts(opts UpsertObjectManyOpts) UpsertObjectM } // Body fills an encoder with the call request body. -func (req UpsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req UpsertObjectManyRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { args := upsertObjectManyArgs{Space: req.space, ObjectsOperationsData: req.objectsOperationsData, Opts: req.opts} req.impl = req.impl.Args(args) diff --git a/datetime/datetime.go b/datetime/datetime.go index 105d2cca5..09e86ce46 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -13,6 +13,8 @@ import ( "encoding/binary" "fmt" "time" + + "github.com/vmihailenco/msgpack/v5" ) // Datetime MessagePack serialization schema is an MP_EXT extension, which @@ -319,3 +321,7 @@ func (tm *Datetime) UnmarshalMsgpack(b []byte) error { } return err } + +func init() { + msgpack.RegisterExt(datetime_extId, (*Datetime)(nil)) +} diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 7c24fa922..a47126049 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "github.com/vmihailenco/msgpack/v5" + . "github.com/tarantool/go-tarantool/v2" . "github.com/tarantool/go-tarantool/v2/datetime" "github.com/tarantool/go-tarantool/v2/test_helpers" @@ -431,7 +433,7 @@ func assertDatetimeIsEqual(t *testing.T, tuples []interface{}, tm time.Time) { if len(tpl) != 2 { t.Fatalf("Unexpected return value body (tuple len = %d)", len(tpl)) } - if val, ok := toDatetime(tpl[dtIndex]); !ok || !val.ToTime().Equal(tm) { + if val, ok := tpl[dtIndex].(*Datetime); !ok || !val.ToTime().Equal(tm) { t.Fatalf("Unexpected tuple %d field %v, expected %v", dtIndex, val, @@ -546,7 +548,7 @@ func TestCustomTimezone(t *testing.T) { assertDatetimeIsEqual(t, resp.Data, tm) tpl := resp.Data[0].([]interface{}) - if respDt, ok := toDatetime(tpl[0]); ok { + if respDt, ok := tpl[0].(*Datetime); ok { zone := respDt.ToTime().Location().String() _, offset := respDt.ToTime().Zone() if zone != customZone { @@ -776,7 +778,7 @@ type Tuple1 struct { Datetime Datetime } -func (t *Tuple1) EncodeMsgpack(e *encoder) error { +func (t *Tuple1) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeArrayLen(2); err != nil { return err } @@ -786,7 +788,7 @@ func (t *Tuple1) EncodeMsgpack(e *encoder) error { return nil } -func (t *Tuple1) DecodeMsgpack(d *decoder) error { +func (t *Tuple1) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { @@ -802,7 +804,7 @@ func (t *Tuple1) DecodeMsgpack(d *decoder) error { return nil } -func (ev *Event) EncodeMsgpack(e *encoder) error { +func (ev *Event) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeArrayLen(2); err != nil { return err } @@ -815,7 +817,7 @@ func (ev *Event) EncodeMsgpack(e *encoder) error { return nil } -func (ev *Event) DecodeMsgpack(d *decoder) error { +func (ev *Event) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { @@ -831,14 +833,16 @@ func (ev *Event) DecodeMsgpack(d *decoder) error { if err != nil { return err } - var ok bool - if ev.Datetime, ok = toDatetime(res); !ok { + + if dt, ok := res.(*Datetime); !ok { return fmt.Errorf("Datetime doesn't match") + } else { + ev.Datetime = *dt } return nil } -func (c *Tuple2) EncodeMsgpack(e *encoder) error { +func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeArrayLen(3); err != nil { return err } @@ -852,7 +856,7 @@ func (c *Tuple2) EncodeMsgpack(e *encoder) error { return nil } -func (c *Tuple2) DecodeMsgpack(d *decoder) error { +func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { @@ -938,7 +942,7 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { } for i, tv := range []time.Time{tm1, tm2} { - dt, ok := toDatetime(events[i].([]interface{})[1]) + dt, ok := events[i].([]interface{})[1].(*Datetime) if !ok || !dt.ToTime().Equal(tv) { t.Fatalf("%v != %v", dt.ToTime(), tv) } @@ -1011,7 +1015,7 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if val, ok := toDatetime(tpl[0]); !ok || !val.ToTime().Equal(tm) { + if val, ok := tpl[0].(*Datetime); !ok || !val.ToTime().Equal(tm) { t.Fatalf("Unexpected body of Select") } } @@ -1043,7 +1047,7 @@ func TestMPEncode(t *testing.T) { if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) } - buf, err := marshal(dt) + buf, err := msgpack.Marshal(dt) if err != nil { t.Fatalf("Marshalling failed: %s", err.Error()) } @@ -1076,7 +1080,7 @@ func TestMPDecode(t *testing.T) { } buf, _ := hex.DecodeString(testcase.mpBuf) var v Datetime - err = unmarshal(buf, &v) + err = msgpack.Unmarshal(buf, &v) if err != nil { t.Fatalf("Unmarshalling failed: %s", err.Error()) } diff --git a/datetime/interval.go b/datetime/interval.go index eee6b2d97..ef378e405 100644 --- a/datetime/interval.go +++ b/datetime/interval.go @@ -1,8 +1,11 @@ package datetime import ( + "bytes" "fmt" "reflect" + + "github.com/vmihailenco/msgpack/v5" ) const interval_extId = 6 @@ -63,22 +66,22 @@ func (ival Interval) Sub(sub Interval) Interval { return ival } -func encodeIntervalValue(e *encoder, typ uint64, value int64) (err error) { +func encodeIntervalValue(e *msgpack.Encoder, typ uint64, value int64) (err error) { if value == 0 { return } - err = encodeUint(e, typ) + err = e.EncodeUint(typ) if err == nil { if value > 0 { - err = encodeUint(e, uint64(value)) + err = e.EncodeUint(uint64(value)) } else if value < 0 { - err = encodeInt(e, int64(value)) + err = e.EncodeInt(int64(value)) } } return } -func encodeInterval(e *encoder, v reflect.Value) (err error) { +func encodeInterval(e *msgpack.Encoder, v reflect.Value) (err error) { val := v.Interface().(Interval) var fieldNum uint64 @@ -89,7 +92,7 @@ func encodeInterval(e *encoder, v reflect.Value) (err error) { fieldNum++ } } - if err = encodeUint(e, fieldNum); err != nil { + if err = e.EncodeUint(fieldNum); err != nil { return } @@ -123,7 +126,7 @@ func encodeInterval(e *encoder, v reflect.Value) (err error) { return nil } -func decodeInterval(d *decoder, v reflect.Value) (err error) { +func decodeInterval(d *msgpack.Decoder, v reflect.Value) (err error) { var fieldNum uint if fieldNum, err = d.DecodeUint(); err != nil { return @@ -177,3 +180,21 @@ func decodeInterval(d *decoder, v reflect.Value) (err error) { v.Set(reflect.ValueOf(val)) return nil } + +func init() { + msgpack.RegisterExtEncoder(interval_extId, Interval{}, + func(e *msgpack.Encoder, v reflect.Value) (ret []byte, err error) { + var b bytes.Buffer + + enc := msgpack.NewEncoder(&b) + if err = encodeInterval(enc, v); err == nil { + ret = b.Bytes() + } + + return + }) + msgpack.RegisterExtDecoder(interval_extId, Interval{}, + func(d *msgpack.Decoder, v reflect.Value, extLen int) error { + return decodeInterval(d, v) + }) +} diff --git a/datetime/msgpack.go b/datetime/msgpack.go deleted file mode 100644 index b5bc0d7c5..000000000 --- a/datetime/msgpack.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package datetime - -import ( - "reflect" - - "gopkg.in/vmihailenco/msgpack.v2" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -func encodeUint(e *encoder, v uint64) error { - return e.EncodeUint(uint(v)) -} - -func encodeInt(e *encoder, v int64) error { - return e.EncodeInt(int(v)) -} - -func init() { - msgpack.RegisterExt(datetime_extId, &Datetime{}) - - msgpack.Register(reflect.TypeOf((*Interval)(nil)).Elem(), encodeInterval, decodeInterval) - msgpack.RegisterExt(interval_extId, (*Interval)(nil)) -} diff --git a/datetime/msgpack_helper_test.go b/datetime/msgpack_helper_test.go deleted file mode 100644 index 12be0bce4..000000000 --- a/datetime/msgpack_helper_test.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package datetime_test - -import ( - . "github.com/tarantool/go-tarantool/v2/datetime" - - "gopkg.in/vmihailenco/msgpack.v2" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -func toDatetime(i interface{}) (dt Datetime, ok bool) { - dt, ok = i.(Datetime) - return -} - -func marshal(v interface{}) ([]byte, error) { - return msgpack.Marshal(v) -} - -func unmarshal(data []byte, v interface{}) error { - return msgpack.Unmarshal(data, v) -} diff --git a/datetime/msgpack_v5.go b/datetime/msgpack_v5.go deleted file mode 100644 index 69285576e..000000000 --- a/datetime/msgpack_v5.go +++ /dev/null @@ -1,42 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package datetime - -import ( - "bytes" - "reflect" - - "github.com/vmihailenco/msgpack/v5" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -func encodeUint(e *encoder, v uint64) error { - return e.EncodeUint(v) -} - -func encodeInt(e *encoder, v int64) error { - return e.EncodeInt(v) -} - -func init() { - msgpack.RegisterExt(datetime_extId, (*Datetime)(nil)) - - msgpack.RegisterExtEncoder(interval_extId, Interval{}, - func(e *msgpack.Encoder, v reflect.Value) (ret []byte, err error) { - var b bytes.Buffer - - enc := msgpack.NewEncoder(&b) - if err = encodeInterval(enc, v); err == nil { - ret = b.Bytes() - } - - return - }) - msgpack.RegisterExtDecoder(interval_extId, Interval{}, - func(d *msgpack.Decoder, v reflect.Value, extLen int) error { - return decodeInterval(d, v) - }) -} diff --git a/datetime/msgpack_v5_helper_test.go b/datetime/msgpack_v5_helper_test.go deleted file mode 100644 index 3ee42f0bd..000000000 --- a/datetime/msgpack_v5_helper_test.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package datetime_test - -import ( - . "github.com/tarantool/go-tarantool/v2/datetime" - "github.com/vmihailenco/msgpack/v5" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -func toDatetime(i interface{}) (dt Datetime, ok bool) { - var ptr *Datetime - if ptr, ok = i.(*Datetime); ok { - dt = *ptr - } - return -} - -func marshal(v interface{}) ([]byte, error) { - return msgpack.Marshal(v) -} - -func unmarshal(data []byte, v interface{}) error { - return msgpack.Unmarshal(data, v) -} diff --git a/decimal/decimal.go b/decimal/decimal.go index cda08c12e..6cca53404 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -19,6 +19,7 @@ import ( "fmt" "github.com/shopspring/decimal" + "github.com/vmihailenco/msgpack/v5" ) // Decimal numbers have 38 digits of precision, that is, the total @@ -98,3 +99,7 @@ func (decNum *Decimal) UnmarshalMsgpack(b []byte) error { return nil } + +func init() { + msgpack.RegisterExt(decimalExtID, (*Decimal)(nil)) +} diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index c1b8b60f7..372a06fce 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -10,6 +10,8 @@ import ( "time" "github.com/shopspring/decimal" + "github.com/vmihailenco/msgpack/v5" + . "github.com/tarantool/go-tarantool/v2" . "github.com/tarantool/go-tarantool/v2/decimal" "github.com/tarantool/go-tarantool/v2/test_helpers" @@ -39,14 +41,14 @@ type TupleDecimal struct { number Decimal } -func (t *TupleDecimal) EncodeMsgpack(e *encoder) error { +func (t *TupleDecimal) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeArrayLen(1); err != nil { return err } return e.EncodeValue(reflect.ValueOf(&t.number)) } -func (t *TupleDecimal) DecodeMsgpack(d *decoder) error { +func (t *TupleDecimal) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { @@ -60,9 +62,11 @@ func (t *TupleDecimal) DecodeMsgpack(d *decoder) error { if err != nil { return err } - var ok bool - if t.number, ok = toDecimal(res); !ok { + + if dec, ok := res.(*Decimal); !ok { return fmt.Errorf("decimal doesn't match") + } else { + t.number = *dec } return nil } @@ -145,11 +149,11 @@ func TestMPEncodeDecode(t *testing.T) { } var buf []byte tuple := TupleDecimal{number: *decNum} - if buf, err = marshal(&tuple); err != nil { - t.Fatalf("Failed to encode decimal number '%s' to a MessagePack buffer: %s", testcase.numString, err) + if buf, err = msgpack.Marshal(&tuple); err != nil { + t.Fatalf("Failed to msgpack.Encoder decimal number '%s' to a MessagePack buffer: %s", testcase.numString, err) } var v TupleDecimal - if err = unmarshal(buf, &v); err != nil { + if err = msgpack.Unmarshal(buf, &v); err != nil { t.Fatalf("Failed to decode MessagePack buffer '%x' to a decimal number: %s", buf, err) } if !decNum.Equal(v.number.Decimal) { @@ -248,12 +252,12 @@ func TestEncodeMaxNumber(t *testing.T) { referenceErrMsg := "msgpack: decimal number is bigger than maximum supported number (10^38 - 1)" decNum := decimal.New(1, DecimalPrecision) // // 10^DecimalPrecision tuple := TupleDecimal{number: *NewDecimal(decNum)} - _, err := marshal(&tuple) + _, err := msgpack.Marshal(&tuple) if err == nil { - t.Fatalf("It is possible to encode a number unsupported by Tarantool") + t.Fatalf("It is possible to msgpack.Encoder a number unsupported by Tarantool") } if err.Error() != referenceErrMsg { - t.Fatalf("Incorrect error message on attempt to encode number unsupported by Tarantool") + t.Fatalf("Incorrect error message on attempt to msgpack.Encoder number unsupported by Tarantool") } } @@ -262,14 +266,14 @@ func TestEncodeMinNumber(t *testing.T) { two := decimal.NewFromInt(2) decNum := decimal.New(1, DecimalPrecision).Neg().Sub(two) // -10^DecimalPrecision - 2 tuple := TupleDecimal{number: *NewDecimal(decNum)} - _, err := marshal(&tuple) + _, err := msgpack.Marshal(&tuple) if err == nil { - t.Fatalf("It is possible to encode a number unsupported by Tarantool") + t.Fatalf("It is possible to msgpack.Encoder a number unsupported by Tarantool") } if err.Error() != referenceErrMsg { fmt.Println("Actual message: ", err.Error()) fmt.Println("Expected message: ", referenceErrMsg) - t.Fatalf("Incorrect error message on attempt to encode number unsupported by Tarantool") + t.Fatalf("Incorrect error message on attempt to msgpack.Encoder number unsupported by Tarantool") } } @@ -281,10 +285,10 @@ func benchmarkMPEncodeDecode(b *testing.B, src decimal.Decimal, dst interface{}) var err error for i := 0; i < b.N; i++ { tuple := TupleDecimal{number: *NewDecimal(src)} - if buf, err = marshal(&tuple); err != nil { + if buf, err = msgpack.Marshal(&tuple); err != nil { b.Fatal(err) } - if err = unmarshal(buf, &v); err != nil { + if err = msgpack.Unmarshal(buf, &v); err != nil { b.Fatal(err) } } @@ -311,7 +315,7 @@ func BenchmarkMPEncodeDecimal(b *testing.B) { } b.ResetTimer() for i := 0; i < b.N; i++ { - marshal(decNum) + msgpack.Marshal(decNum) } }) } @@ -325,13 +329,13 @@ func BenchmarkMPDecodeDecimal(b *testing.B) { b.Fatal(err) } var buf []byte - if buf, err = marshal(decNum); err != nil { + if buf, err = msgpack.Marshal(decNum); err != nil { b.Fatal(err) } b.ResetTimer() var v TupleDecimal for i := 0; i < b.N; i++ { - unmarshal(buf, &v) + msgpack.Unmarshal(buf, &v) } }) @@ -349,7 +353,7 @@ func tupleValueIsDecimal(t *testing.T, tuples []interface{}, number decimal.Deci if len(tpl) != 1 { t.Fatalf("Unexpected return value body (tuple len)") } - if val, ok := toDecimal(tpl[0]); !ok || !val.Equal(number) { + if val, ok := tpl[0].(*Decimal); !ok || !val.Equal(number) { t.Fatalf("Unexpected return value body (tuple 0 field)") } } @@ -370,13 +374,13 @@ func TestEncodeStringToBCD(t *testing.T) { t.Run(testcase.numString, func(t *testing.T) { buf, err := EncodeStringToBCD(testcase.numString) if err != nil { - t.Fatalf("Failed to encode decimal '%s' to BCD: %s", testcase.numString, err) + t.Fatalf("Failed to msgpack.Encoder decimal '%s' to BCD: %s", testcase.numString, err) } b, _ := hex.DecodeString(testcase.mpBuf) bcdBuf := trimMPHeader(b, testcase.fixExt) if reflect.DeepEqual(buf, bcdBuf) != true { - t.Fatalf("Failed to encode decimal '%s' to BCD: expected '%x', actual '%x'", testcase.numString, bcdBuf, buf) + t.Fatalf("Failed to msgpack.Encoder decimal '%s' to BCD: expected '%x', actual '%x'", testcase.numString, bcdBuf, buf) } }) } @@ -396,11 +400,11 @@ func TestDecodeStringFromBCD(t *testing.T) { decActual, err := decimal.NewFromString(s) if err != nil { - t.Fatalf("Failed to encode string ('%s') to decimal", s) + t.Fatalf("Failed to msgpack.Encoder string ('%s') to decimal", s) } decExpected, err := decimal.NewFromString(testcase.numString) if err != nil { - t.Fatalf("Failed to encode string ('%s') to decimal", testcase.numString) + t.Fatalf("Failed to msgpack.Encoder string ('%s') to decimal", testcase.numString) } if !decExpected.Equal(decActual) { t.Fatalf("Decoded decimal from BCD ('%x') is incorrect: expected '%s', actual '%s'", bcdBuf, testcase.numString, s) @@ -418,13 +422,13 @@ func TestMPEncode(t *testing.T) { if err != nil { t.Fatalf("NewDecimalFromString() failed: %s", err.Error()) } - buf, err := marshal(dec) + buf, err := msgpack.Marshal(dec) if err != nil { t.Fatalf("Marshalling failed: %s", err.Error()) } refBuf, _ := hex.DecodeString(testcase.mpBuf) if reflect.DeepEqual(buf, refBuf) != true { - t.Fatalf("Failed to encode decimal '%s', actual %x, expected %x", + t.Fatalf("Failed to msgpack.Encoder decimal '%s', actual %x, expected %x", testcase.numString, buf, refBuf) @@ -443,11 +447,11 @@ func TestMPDecode(t *testing.T) { t.Fatalf("hex.DecodeString() failed: %s", err) } var v interface{} - err = unmarshal(mpBuf, &v) + err = msgpack.Unmarshal(mpBuf, &v) if err != nil { - t.Fatalf("Unmarshalling failed: %s", err.Error()) + t.Fatalf("Unmsgpack.Marshalling failed: %s", err.Error()) } - decActual, ok := toDecimal(v) + decActual, ok := v.(*Decimal) if !ok { t.Fatalf("Unable to convert to Decimal") } diff --git a/decimal/msgpack.go b/decimal/msgpack.go deleted file mode 100644 index 5a455ae59..000000000 --- a/decimal/msgpack.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package decimal - -import ( - "gopkg.in/vmihailenco/msgpack.v2" -) - -func init() { - msgpack.RegisterExt(decimalExtID, &Decimal{}) -} diff --git a/decimal/msgpack_helper_test.go b/decimal/msgpack_helper_test.go deleted file mode 100644 index b58ea9731..000000000 --- a/decimal/msgpack_helper_test.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package decimal_test - -import ( - . "github.com/tarantool/go-tarantool/v2/decimal" - - "gopkg.in/vmihailenco/msgpack.v2" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -func toDecimal(i interface{}) (dec Decimal, ok bool) { - dec, ok = i.(Decimal) - return -} - -func marshal(v interface{}) ([]byte, error) { - return msgpack.Marshal(v) -} - -func unmarshal(data []byte, v interface{}) error { - return msgpack.Unmarshal(data, v) -} diff --git a/decimal/msgpack_v5.go b/decimal/msgpack_v5.go deleted file mode 100644 index 59fa713d0..000000000 --- a/decimal/msgpack_v5.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package decimal - -import ( - "github.com/vmihailenco/msgpack/v5" -) - -func init() { - msgpack.RegisterExt(decimalExtID, (*Decimal)(nil)) -} diff --git a/decimal/msgpack_v5_helper_test.go b/decimal/msgpack_v5_helper_test.go deleted file mode 100644 index e253bb0bc..000000000 --- a/decimal/msgpack_v5_helper_test.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package decimal_test - -import ( - . "github.com/tarantool/go-tarantool/v2/decimal" - "github.com/vmihailenco/msgpack/v5" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -func toDecimal(i interface{}) (dec Decimal, ok bool) { - var ptr *Decimal - if ptr, ok = i.(*Decimal); ok { - dec = *ptr - } - return -} - -func marshal(v interface{}) ([]byte, error) { - return msgpack.Marshal(v) -} - -func unmarshal(data []byte, v interface{}) error { - return msgpack.Unmarshal(data, v) -} diff --git a/dial.go b/dial.go index abed85e1b..3baf25986 100644 --- a/dial.go +++ b/dial.go @@ -9,6 +9,8 @@ import ( "net" "strings" "time" + + "github.com/vmihailenco/msgpack/v5" ) const ( @@ -350,7 +352,7 @@ func authenticate(c Conn, opts DialOpts, salt string) error { // writeRequest writes a request to the writer. func writeRequest(w writeFlusher, req Request) error { var packet smallWBuf - err := pack(&packet, newEncoder(&packet), 0, req, ignoreStreamId, nil) + err := pack(&packet, msgpack.NewEncoder(&packet), 0, req, ignoreStreamId, nil) if err != nil { return fmt.Errorf("pack error: %w", err) @@ -374,7 +376,7 @@ func readResponse(r io.Reader) (Response, error) { } resp := Response{buf: smallBuf{b: respBytes}} - err = resp.decodeHeader(newDecoder(&smallBuf{})) + err = resp.decodeHeader(msgpack.NewDecoder(&smallBuf{})) if err != nil { return resp, fmt.Errorf("decode response header error: %w", err) } diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index 6fe8eff83..5bac7ae62 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -5,6 +5,8 @@ import ( "log" "time" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -23,11 +25,11 @@ type Tuple3 struct { Members []Member } -func (c *Tuple2) EncodeMsgpack(e *encoder) error { +func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeArrayLen(3); err != nil { return err } - if err := encodeUint(e, uint64(c.Cid)); err != nil { + if err := e.EncodeUint(uint64(c.Cid)); err != nil { return err } if err := e.EncodeString(c.Orig); err != nil { @@ -37,7 +39,7 @@ func (c *Tuple2) EncodeMsgpack(e *encoder) error { return nil } -func (c *Tuple2) DecodeMsgpack(d *decoder) error { +func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { diff --git a/export_test.go b/export_test.go index 71886bb20..879d345ac 100644 --- a/export_test.go +++ b/export_test.go @@ -1,9 +1,10 @@ package tarantool import ( - "io" "net" "time" + + "github.com/vmihailenco/msgpack/v5" ) func SslDialTimeout(network, address string, timeout time.Duration, @@ -17,107 +18,103 @@ func SslCreateContext(opts SslOpts) (ctx interface{}, err error) { // RefImplPingBody is reference implementation for filling of a ping // request's body. -func RefImplPingBody(enc *encoder) error { +func RefImplPingBody(enc *msgpack.Encoder) error { return fillPing(enc) } // RefImplSelectBody is reference implementation for filling of a select // request's body. -func RefImplSelectBody(enc *encoder, space, index, offset, limit, iterator uint32, +func RefImplSelectBody(enc *msgpack.Encoder, space, index, offset, limit, iterator uint32, key, after interface{}, fetchPos bool) error { return fillSelect(enc, space, index, offset, limit, iterator, key, after, fetchPos) } // RefImplInsertBody is reference implementation for filling of an insert // request's body. -func RefImplInsertBody(enc *encoder, space uint32, tuple interface{}) error { +func RefImplInsertBody(enc *msgpack.Encoder, space uint32, tuple interface{}) error { return fillInsert(enc, space, tuple) } // RefImplReplaceBody is reference implementation for filling of a replace // request's body. -func RefImplReplaceBody(enc *encoder, space uint32, tuple interface{}) error { +func RefImplReplaceBody(enc *msgpack.Encoder, space uint32, tuple interface{}) error { return fillInsert(enc, space, tuple) } // RefImplDeleteBody is reference implementation for filling of a delete // request's body. -func RefImplDeleteBody(enc *encoder, space, index uint32, key interface{}) error { +func RefImplDeleteBody(enc *msgpack.Encoder, space, index uint32, key interface{}) error { return fillDelete(enc, space, index, key) } // RefImplUpdateBody is reference implementation for filling of an update // request's body. -func RefImplUpdateBody(enc *encoder, space, index uint32, key, ops interface{}) error { +func RefImplUpdateBody(enc *msgpack.Encoder, space, index uint32, key, ops interface{}) error { return fillUpdate(enc, space, index, key, ops) } // RefImplUpsertBody is reference implementation for filling of an upsert // request's body. -func RefImplUpsertBody(enc *encoder, space uint32, tuple, ops interface{}) error { +func RefImplUpsertBody(enc *msgpack.Encoder, space uint32, tuple, ops interface{}) error { return fillUpsert(enc, space, tuple, ops) } // RefImplCallBody is reference implementation for filling of a call or call17 // request's body. -func RefImplCallBody(enc *encoder, function string, args interface{}) error { +func RefImplCallBody(enc *msgpack.Encoder, function string, args interface{}) error { return fillCall(enc, function, args) } // RefImplEvalBody is reference implementation for filling of an eval // request's body. -func RefImplEvalBody(enc *encoder, expr string, args interface{}) error { +func RefImplEvalBody(enc *msgpack.Encoder, expr string, args interface{}) error { return fillEval(enc, expr, args) } // RefImplExecuteBody is reference implementation for filling of an execute // request's body. -func RefImplExecuteBody(enc *encoder, expr string, args interface{}) error { +func RefImplExecuteBody(enc *msgpack.Encoder, expr string, args interface{}) error { return fillExecute(enc, expr, args) } // RefImplPrepareBody is reference implementation for filling of an prepare // request's body. -func RefImplPrepareBody(enc *encoder, expr string) error { +func RefImplPrepareBody(enc *msgpack.Encoder, expr string) error { return fillPrepare(enc, expr) } // RefImplUnprepareBody is reference implementation for filling of an execute prepared // request's body. -func RefImplExecutePreparedBody(enc *encoder, stmt Prepared, args interface{}) error { +func RefImplExecutePreparedBody(enc *msgpack.Encoder, stmt Prepared, args interface{}) error { return fillExecutePrepared(enc, stmt, args) } // RefImplUnprepareBody is reference implementation for filling of an unprepare // request's body. -func RefImplUnprepareBody(enc *encoder, stmt Prepared) error { +func RefImplUnprepareBody(enc *msgpack.Encoder, stmt Prepared) error { return fillUnprepare(enc, stmt) } // RefImplBeginBody is reference implementation for filling of an begin // request's body. -func RefImplBeginBody(enc *encoder, txnIsolation TxnIsolationLevel, timeout time.Duration) error { +func RefImplBeginBody(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, timeout time.Duration) error { return fillBegin(enc, txnIsolation, timeout) } // RefImplCommitBody is reference implementation for filling of an commit // request's body. -func RefImplCommitBody(enc *encoder) error { +func RefImplCommitBody(enc *msgpack.Encoder) error { return fillCommit(enc) } // RefImplRollbackBody is reference implementation for filling of an rollback // request's body. -func RefImplRollbackBody(enc *encoder) error { +func RefImplRollbackBody(enc *msgpack.Encoder) error { return fillRollback(enc) } // RefImplIdBody is reference implementation for filling of an id // request's body. -func RefImplIdBody(enc *encoder, protocolInfo ProtocolInfo) error { +func RefImplIdBody(enc *msgpack.Encoder, protocolInfo ProtocolInfo) error { return fillId(enc, protocolInfo) } - -func NewEncoder(w io.Writer) *encoder { - return newEncoder(w) -} diff --git a/go.mod b/go.mod index e93a083e5..9e179e327 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,6 @@ require ( github.com/stretchr/testify v1.7.1 github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 github.com/vmihailenco/msgpack/v5 v5.3.5 - golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect - google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect - gopkg.in/vmihailenco/msgpack.v2 v2.9.2 ) diff --git a/go.sum b/go.sum index 733f1f96e..95fc38125 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -27,28 +25,11 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9 github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= -gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/msgpack.go b/msgpack.go deleted file mode 100644 index 9977e9399..000000000 --- a/msgpack.go +++ /dev/null @@ -1,54 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package tarantool - -import ( - "io" - - "gopkg.in/vmihailenco/msgpack.v2" - msgpcode "gopkg.in/vmihailenco/msgpack.v2/codes" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -func newEncoder(w io.Writer) *encoder { - return msgpack.NewEncoder(w) -} - -func newDecoder(r io.Reader) *decoder { - return msgpack.NewDecoder(r) -} - -func encodeUint(e *encoder, v uint64) error { - return e.EncodeUint(uint(v)) -} - -func encodeInt(e *encoder, v int64) error { - return e.EncodeInt(int(v)) -} - -func msgpackIsUint(code byte) bool { - return code == msgpcode.Uint8 || code == msgpcode.Uint16 || - code == msgpcode.Uint32 || code == msgpcode.Uint64 || - msgpcode.IsFixedNum(code) -} - -func msgpackIsMap(code byte) bool { - return code == msgpcode.Map16 || code == msgpcode.Map32 || msgpcode.IsFixedMap(code) -} - -func msgpackIsArray(code byte) bool { - return code == msgpcode.Array16 || code == msgpcode.Array32 || - msgpcode.IsFixedArray(code) -} - -func msgpackIsString(code byte) bool { - return msgpcode.IsFixedString(code) || code == msgpcode.Str8 || - code == msgpcode.Str16 || code == msgpcode.Str32 -} - -func init() { - msgpack.RegisterExt(errorExtID, &BoxError{}) -} diff --git a/msgpack_helper_test.go b/msgpack_helper_test.go deleted file mode 100644 index 5b618bdd1..000000000 --- a/msgpack_helper_test.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package tarantool_test - -import ( - "github.com/tarantool/go-tarantool/v2" - "gopkg.in/vmihailenco/msgpack.v2" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -func encodeUint(e *encoder, v uint64) error { - return e.EncodeUint(uint(v)) -} - -func toBoxError(i interface{}) (v tarantool.BoxError, ok bool) { - v, ok = i.(tarantool.BoxError) - return -} - -func marshal(v interface{}) ([]byte, error) { - return msgpack.Marshal(v) -} - -func unmarshal(data []byte, v interface{}) error { - return msgpack.Unmarshal(data, v) -} diff --git a/msgpack_v5.go b/msgpack_v5.go deleted file mode 100644 index e8cd9aa29..000000000 --- a/msgpack_v5.go +++ /dev/null @@ -1,58 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package tarantool - -import ( - "io" - - "github.com/vmihailenco/msgpack/v5" - "github.com/vmihailenco/msgpack/v5/msgpcode" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -func newEncoder(w io.Writer) *encoder { - return msgpack.NewEncoder(w) -} - -func newDecoder(r io.Reader) *decoder { - dec := msgpack.NewDecoder(r) - dec.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { - return dec.DecodeUntypedMap() - }) - return dec -} - -func encodeUint(e *encoder, v uint64) error { - return e.EncodeUint(v) -} - -func encodeInt(e *encoder, v int64) error { - return e.EncodeInt(v) -} - -func msgpackIsUint(code byte) bool { - return code == msgpcode.Uint8 || code == msgpcode.Uint16 || - code == msgpcode.Uint32 || code == msgpcode.Uint64 || - msgpcode.IsFixedNum(code) -} - -func msgpackIsMap(code byte) bool { - return code == msgpcode.Map16 || code == msgpcode.Map32 || msgpcode.IsFixedMap(code) -} - -func msgpackIsArray(code byte) bool { - return code == msgpcode.Array16 || code == msgpcode.Array32 || - msgpcode.IsFixedArray(code) -} - -func msgpackIsString(code byte) bool { - return msgpcode.IsFixedString(code) || code == msgpcode.Str8 || - code == msgpcode.Str16 || code == msgpcode.Str32 -} - -func init() { - msgpack.RegisterExt(errorExtID, (*BoxError)(nil)) -} diff --git a/msgpack_v5_helper_test.go b/msgpack_v5_helper_test.go deleted file mode 100644 index ba1d72908..000000000 --- a/msgpack_v5_helper_test.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package tarantool_test - -import ( - "github.com/tarantool/go-tarantool/v2" - "github.com/vmihailenco/msgpack/v5" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -func encodeUint(e *encoder, v uint64) error { - return e.EncodeUint(v) -} - -func toBoxError(i interface{}) (v tarantool.BoxError, ok bool) { - var ptr *tarantool.BoxError - if ptr, ok = i.(*tarantool.BoxError); ok { - v = *ptr - } - return -} - -func marshal(v interface{}) ([]byte, error) { - return msgpack.Marshal(v) -} - -func unmarshal(data []byte, v interface{}) error { - return msgpack.Unmarshal(data, v) -} diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index f96afd2f3..645644081 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -13,6 +13,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/pool" "github.com/tarantool/go-tarantool/v2/test_helpers" @@ -1049,7 +1051,7 @@ type Member struct { val string } -func (m *Member) DecodeMsgpack(d *decoder) error { +func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { diff --git a/pool/msgpack_helper_test.go b/pool/msgpack_helper_test.go deleted file mode 100644 index f54df2038..000000000 --- a/pool/msgpack_helper_test.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package pool_test - -import ( - "gopkg.in/vmihailenco/msgpack.v2" -) - -type decoder = msgpack.Decoder diff --git a/pool/msgpack_v5_helper_test.go b/pool/msgpack_v5_helper_test.go deleted file mode 100644 index a507ffa64..000000000 --- a/pool/msgpack_v5_helper_test.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package pool_test - -import ( - "github.com/vmihailenco/msgpack/v5" -) - -type decoder = msgpack.Decoder diff --git a/prepared.go b/prepared.go index 9019a81f3..4e3f494ee 100644 --- a/prepared.go +++ b/prepared.go @@ -3,6 +3,8 @@ package tarantool import ( "context" "fmt" + + "github.com/vmihailenco/msgpack/v5" ) // PreparedID is a type for Prepared Statement ID @@ -18,23 +20,23 @@ type Prepared struct { Conn *Connection } -func fillPrepare(enc *encoder, expr string) error { +func fillPrepare(enc *msgpack.Encoder, expr string) error { enc.EncodeMapLen(1) - encodeUint(enc, KeySQLText) + enc.EncodeUint(KeySQLText) return enc.EncodeString(expr) } -func fillUnprepare(enc *encoder, stmt Prepared) error { +func fillUnprepare(enc *msgpack.Encoder, stmt Prepared) error { enc.EncodeMapLen(1) - encodeUint(enc, KeyStmtID) - return encodeUint(enc, uint64(stmt.StatementID)) + enc.EncodeUint(KeyStmtID) + return enc.EncodeUint(uint64(stmt.StatementID)) } -func fillExecutePrepared(enc *encoder, stmt Prepared, args interface{}) error { +func fillExecutePrepared(enc *msgpack.Encoder, stmt Prepared, args interface{}) error { enc.EncodeMapLen(2) - encodeUint(enc, KeyStmtID) - encodeUint(enc, uint64(stmt.StatementID)) - encodeUint(enc, KeySQLBind) + enc.EncodeUint(KeyStmtID) + enc.EncodeUint(uint64(stmt.StatementID)) + enc.EncodeUint(KeySQLBind) return encodeSQLBind(enc, args) } @@ -72,8 +74,8 @@ func NewPrepareRequest(expr string) *PrepareRequest { return req } -// Body fills an encoder with the execute request body. -func (req *PrepareRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the execute request body. +func (req *PrepareRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillPrepare(enc, req.expr) } @@ -108,8 +110,8 @@ func (req *UnprepareRequest) Conn() *Connection { return req.stmt.Conn } -// Body fills an encoder with the execute request body. -func (req *UnprepareRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the execute request body. +func (req *UnprepareRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillUnprepare(enc, *req.stmt) } @@ -153,8 +155,8 @@ func (req *ExecutePreparedRequest) Args(args interface{}) *ExecutePreparedReques return req } -// Body fills an encoder with the execute request body. -func (req *ExecutePreparedRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the execute request body. +func (req *ExecutePreparedRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillExecutePrepared(enc, *req.stmt, req.args) } diff --git a/protocol.go b/protocol.go index ae8dce306..6ddfd8d48 100644 --- a/protocol.go +++ b/protocol.go @@ -3,6 +3,8 @@ package tarantool import ( "context" "fmt" + + "github.com/vmihailenco/msgpack/v5" ) // ProtocolVersion type stores Tarantool protocol version. @@ -101,15 +103,15 @@ type IdRequest struct { protocolInfo ProtocolInfo } -func fillId(enc *encoder, protocolInfo ProtocolInfo) error { +func fillId(enc *msgpack.Encoder, protocolInfo ProtocolInfo) error { enc.EncodeMapLen(2) - encodeUint(enc, KeyVersion) + enc.EncodeUint(KeyVersion) if err := enc.Encode(protocolInfo.Version); err != nil { return err } - encodeUint(enc, KeyFeatures) + enc.EncodeUint(KeyFeatures) t := len(protocolInfo.Features) if err := enc.EncodeArrayLen(t); err != nil { @@ -133,8 +135,8 @@ func NewIdRequest(protocolInfo ProtocolInfo) *IdRequest { return req } -// Body fills an encoder with the id request body. -func (req *IdRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the id request body. +func (req *IdRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillId(enc, req.protocolInfo) } diff --git a/queue/example_msgpack_test.go b/queue/example_msgpack_test.go index 71e5cf814..ff37c1879 100644 --- a/queue/example_msgpack_test.go +++ b/queue/example_msgpack_test.go @@ -13,6 +13,8 @@ import ( "log" "time" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/queue" ) @@ -21,7 +23,7 @@ type dummyData struct { Dummy bool } -func (c *dummyData) DecodeMsgpack(d *decoder) error { +func (c *dummyData) DecodeMsgpack(d *msgpack.Decoder) error { var err error if c.Dummy, err = d.DecodeBool(); err != nil { return err @@ -29,7 +31,7 @@ func (c *dummyData) DecodeMsgpack(d *decoder) error { return nil } -func (c *dummyData) EncodeMsgpack(e *encoder) error { +func (c *dummyData) EncodeMsgpack(e *msgpack.Encoder) error { return e.EncodeBool(c.Dummy) } diff --git a/queue/msgpack.go b/queue/msgpack.go deleted file mode 100644 index d9e0b58db..000000000 --- a/queue/msgpack.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package queue - -import ( - "gopkg.in/vmihailenco/msgpack.v2" -) - -type decoder = msgpack.Decoder diff --git a/queue/msgpack_helper_test.go b/queue/msgpack_helper_test.go deleted file mode 100644 index 49b61240c..000000000 --- a/queue/msgpack_helper_test.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package queue_test - -import ( - "gopkg.in/vmihailenco/msgpack.v2" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder diff --git a/queue/msgpack_v5.go b/queue/msgpack_v5.go deleted file mode 100644 index b5037caaf..000000000 --- a/queue/msgpack_v5.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package queue - -import ( - "github.com/vmihailenco/msgpack/v5" -) - -type decoder = msgpack.Decoder diff --git a/queue/msgpack_v5_helper_test.go b/queue/msgpack_v5_helper_test.go deleted file mode 100644 index ea2991f34..000000000 --- a/queue/msgpack_v5_helper_test.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package queue_test - -import ( - "github.com/vmihailenco/msgpack/v5" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder diff --git a/queue/queue.go b/queue/queue.go index 5dfc147f3..2407bad03 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -13,6 +13,8 @@ import ( "time" "github.com/google/uuid" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -371,7 +373,7 @@ type kickResult struct { id uint64 } -func (r *kickResult) DecodeMsgpack(d *decoder) (err error) { +func (r *kickResult) DecodeMsgpack(d *msgpack.Decoder) (err error) { var l int if l, err = d.DecodeArrayLen(); err != nil { return err @@ -453,7 +455,7 @@ type queueData struct { result interface{} } -func (qd *queueData) DecodeMsgpack(d *decoder) error { +func (qd *queueData) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { @@ -472,7 +474,7 @@ func (qd *queueData) DecodeMsgpack(d *decoder) error { } if qd.task.Data() == nil { - // It may happen if the decoder has a code.Nil value inside. As a + // It may happen if the msgpack.Decoder has a code.Nil value inside. As a // result, the task will not be decoded. qd.task = nil } diff --git a/queue/queue_test.go b/queue/queue_test.go index 11f4a65a9..313032f49 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/vmihailenco/msgpack/v5" + . "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/queue" "github.com/tarantool/go-tarantool/v2/test_helpers" @@ -300,7 +302,7 @@ type customData struct { customField string } -func (c *customData) DecodeMsgpack(d *decoder) error { +func (c *customData) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { @@ -315,7 +317,7 @@ func (c *customData) DecodeMsgpack(d *decoder) error { return nil } -func (c *customData) EncodeMsgpack(e *encoder) error { +func (c *customData) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeArrayLen(1); err != nil { return err } diff --git a/queue/task.go b/queue/task.go index c1b0aad98..db970884e 100644 --- a/queue/task.go +++ b/queue/task.go @@ -3,6 +3,8 @@ package queue import ( "fmt" "time" + + "github.com/vmihailenco/msgpack/v5" ) // Task represents a task from Tarantool queue's tube. @@ -13,7 +15,7 @@ type Task struct { q *queue } -func (t *Task) DecodeMsgpack(d *decoder) error { +func (t *Task) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { diff --git a/request.go b/request.go index 7c79c5863..5388c0e1a 100644 --- a/request.go +++ b/request.go @@ -7,63 +7,65 @@ import ( "reflect" "strings" "sync" + + "github.com/vmihailenco/msgpack/v5" ) -func fillSearch(enc *encoder, spaceNo, indexNo uint32, key interface{}) error { - if err := encodeUint(enc, KeySpaceNo); err != nil { +func fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { + if err := enc.EncodeUint(KeySpaceNo); err != nil { return err } - if err := encodeUint(enc, uint64(spaceNo)); err != nil { + if err := enc.EncodeUint(uint64(spaceNo)); err != nil { return err } - if err := encodeUint(enc, KeyIndexNo); err != nil { + if err := enc.EncodeUint(KeyIndexNo); err != nil { return err } - if err := encodeUint(enc, uint64(indexNo)); err != nil { + if err := enc.EncodeUint(uint64(indexNo)); err != nil { return err } - if err := encodeUint(enc, KeyKey); err != nil { + if err := enc.EncodeUint(KeyKey); err != nil { return err } return enc.Encode(key) } -func fillIterator(enc *encoder, offset, limit, iterator uint32) error { - if err := encodeUint(enc, KeyIterator); err != nil { +func fillIterator(enc *msgpack.Encoder, offset, limit, iterator uint32) error { + if err := enc.EncodeUint(KeyIterator); err != nil { return err } - if err := encodeUint(enc, uint64(iterator)); err != nil { + if err := enc.EncodeUint(uint64(iterator)); err != nil { return err } - if err := encodeUint(enc, KeyOffset); err != nil { + if err := enc.EncodeUint(KeyOffset); err != nil { return err } - if err := encodeUint(enc, uint64(offset)); err != nil { + if err := enc.EncodeUint(uint64(offset)); err != nil { return err } - if err := encodeUint(enc, KeyLimit); err != nil { + if err := enc.EncodeUint(KeyLimit); err != nil { return err } - return encodeUint(enc, uint64(limit)) + return enc.EncodeUint(uint64(limit)) } -func fillInsert(enc *encoder, spaceNo uint32, tuple interface{}) error { +func fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { if err := enc.EncodeMapLen(2); err != nil { return err } - if err := encodeUint(enc, KeySpaceNo); err != nil { + if err := enc.EncodeUint(KeySpaceNo); err != nil { return err } - if err := encodeUint(enc, uint64(spaceNo)); err != nil { + if err := enc.EncodeUint(uint64(spaceNo)); err != nil { return err } - if err := encodeUint(enc, KeyTuple); err != nil { + if err := enc.EncodeUint(KeyTuple); err != nil { return err } return enc.Encode(tuple) } -func fillSelect(enc *encoder, spaceNo, indexNo, offset, limit, iterator uint32, +func fillSelect(enc *msgpack.Encoder, spaceNo, indexNo, offset, limit, iterator uint32, key, after interface{}, fetchPos bool) error { mapLen := 6 if fetchPos { @@ -82,7 +84,7 @@ func fillSelect(enc *encoder, spaceNo, indexNo, offset, limit, iterator uint32, return err } if fetchPos { - if err := encodeUint(enc, KeyFetchPos); err != nil { + if err := enc.EncodeUint(KeyFetchPos); err != nil { return err } if err := enc.EncodeBool(fetchPos); err != nil { @@ -91,14 +93,14 @@ func fillSelect(enc *encoder, spaceNo, indexNo, offset, limit, iterator uint32, } if after != nil { if pos, ok := after.([]byte); ok { - if err := encodeUint(enc, KeyAfterPos); err != nil { + if err := enc.EncodeUint(KeyAfterPos); err != nil { return err } if err := enc.EncodeString(string(pos)); err != nil { return err } } else { - if err := encodeUint(enc, KeyAfterTuple); err != nil { + if err := enc.EncodeUint(KeyAfterTuple); err != nil { return err } if err := enc.Encode(after); err != nil { @@ -109,57 +111,57 @@ func fillSelect(enc *encoder, spaceNo, indexNo, offset, limit, iterator uint32, return nil } -func fillUpdate(enc *encoder, spaceNo, indexNo uint32, key, ops interface{}) error { +func fillUpdate(enc *msgpack.Encoder, spaceNo, indexNo uint32, key, ops interface{}) error { enc.EncodeMapLen(4) if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { return err } - encodeUint(enc, KeyTuple) + enc.EncodeUint(KeyTuple) return enc.Encode(ops) } -func fillUpsert(enc *encoder, spaceNo uint32, tuple, ops interface{}) error { +func fillUpsert(enc *msgpack.Encoder, spaceNo uint32, tuple, ops interface{}) error { enc.EncodeMapLen(3) - encodeUint(enc, KeySpaceNo) - encodeUint(enc, uint64(spaceNo)) - encodeUint(enc, KeyTuple) + enc.EncodeUint(KeySpaceNo) + enc.EncodeUint(uint64(spaceNo)) + enc.EncodeUint(KeyTuple) if err := enc.Encode(tuple); err != nil { return err } - encodeUint(enc, KeyDefTuple) + enc.EncodeUint(KeyDefTuple) return enc.Encode(ops) } -func fillDelete(enc *encoder, spaceNo, indexNo uint32, key interface{}) error { +func fillDelete(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { enc.EncodeMapLen(3) return fillSearch(enc, spaceNo, indexNo, key) } -func fillCall(enc *encoder, functionName string, args interface{}) error { +func fillCall(enc *msgpack.Encoder, functionName string, args interface{}) error { enc.EncodeMapLen(2) - encodeUint(enc, KeyFunctionName) + enc.EncodeUint(KeyFunctionName) enc.EncodeString(functionName) - encodeUint(enc, KeyTuple) + enc.EncodeUint(KeyTuple) return enc.Encode(args) } -func fillEval(enc *encoder, expr string, args interface{}) error { +func fillEval(enc *msgpack.Encoder, expr string, args interface{}) error { enc.EncodeMapLen(2) - encodeUint(enc, KeyExpression) + enc.EncodeUint(KeyExpression) enc.EncodeString(expr) - encodeUint(enc, KeyTuple) + enc.EncodeUint(KeyTuple) return enc.Encode(args) } -func fillExecute(enc *encoder, expr string, args interface{}) error { +func fillExecute(enc *msgpack.Encoder, expr string, args interface{}) error { enc.EncodeMapLen(2) - encodeUint(enc, KeySQLText) + enc.EncodeUint(KeySQLText) enc.EncodeString(expr) - encodeUint(enc, KeySQLBind) + enc.EncodeUint(KeySQLBind) return encodeSQLBind(enc, args) } -func fillPing(enc *encoder) error { +func fillPing(enc *msgpack.Encoder) error { return enc.EncodeMapLen(0) } @@ -264,7 +266,7 @@ type single struct { found bool } -func (s *single) DecodeMsgpack(d *decoder) error { +func (s *single) DecodeMsgpack(d *msgpack.Decoder) error { var err error var len int if len, err = d.DecodeArrayLen(); err != nil { @@ -471,7 +473,7 @@ type KeyValueBind struct { // to avoid extra allocations in heap by calling strings.ToLower() var lowerCaseNames sync.Map -func encodeSQLBind(enc *encoder, from interface{}) error { +func encodeSQLBind(enc *msgpack.Encoder, from interface{}) error { // internal function for encoding single map in msgpack encodeKeyInterface := func(key string, val interface{}) error { if err := enc.EncodeMapLen(1); err != nil { @@ -603,8 +605,8 @@ func encodeSQLBind(enc *encoder, from interface{}) error { type Request interface { // Code returns a IPROTO code for the request. Code() int32 - // Body fills an encoder with a request body. - Body(resolver SchemaResolver, enc *encoder) error + // Body fills an msgpack.Encoder with a request body. + Body(resolver SchemaResolver, enc *msgpack.Encoder) error // Ctx returns a context of the request. Ctx() context.Context // Async returns true if the request does not expect response. @@ -705,7 +707,7 @@ func (req authRequest) Ctx() context.Context { } // Body fills an encoder with the auth request body. -func (req authRequest) Body(res SchemaResolver, enc *encoder) error { +func (req authRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return enc.Encode(map[uint32]interface{}{ KeyUserName: req.user, KeyTuple: []interface{}{req.auth.String(), req.pass}, @@ -725,8 +727,8 @@ func NewPingRequest() *PingRequest { return req } -// Body fills an encoder with the ping request body. -func (req *PingRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the ping request body. +func (req *PingRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillPing(enc) } @@ -828,7 +830,7 @@ func (req *SelectRequest) After(after interface{}) *SelectRequest { } // Body fills an encoder with the select request body. -func (req *SelectRequest) Body(res SchemaResolver, enc *encoder) error { +func (req *SelectRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) if err != nil { return err @@ -872,8 +874,8 @@ func (req *InsertRequest) Tuple(tuple interface{}) *InsertRequest { return req } -// Body fills an encoder with the insert request body. -func (req *InsertRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the insert request body. +func (req *InsertRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) if err != nil { return err @@ -916,8 +918,8 @@ func (req *ReplaceRequest) Tuple(tuple interface{}) *ReplaceRequest { return req } -// Body fills an encoder with the replace request body. -func (req *ReplaceRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the replace request body. +func (req *ReplaceRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) if err != nil { return err @@ -967,8 +969,8 @@ func (req *DeleteRequest) Key(key interface{}) *DeleteRequest { return req } -// Body fills an encoder with the delete request body. -func (req *DeleteRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the delete request body. +func (req *DeleteRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) if err != nil { return err @@ -1029,8 +1031,8 @@ func (req *UpdateRequest) Operations(ops *Operations) *UpdateRequest { return req } -// Body fills an encoder with the update request body. -func (req *UpdateRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the update request body. +func (req *UpdateRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) if err != nil { return err @@ -1084,8 +1086,8 @@ func (req *UpsertRequest) Operations(ops *Operations) *UpsertRequest { return req } -// Body fills an encoder with the upsert request body. -func (req *UpsertRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the upsert request body. +func (req *UpsertRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) if err != nil { return err @@ -1131,7 +1133,7 @@ func (req *CallRequest) Args(args interface{}) *CallRequest { } // Body fills an encoder with the call request body. -func (req *CallRequest) Body(res SchemaResolver, enc *encoder) error { +func (req *CallRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { args := req.args if args == nil { args = []interface{}{} @@ -1191,8 +1193,8 @@ func (req *EvalRequest) Args(args interface{}) *EvalRequest { return req } -// Body fills an encoder with the eval request body. -func (req *EvalRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the eval request body. +func (req *EvalRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillEval(enc, req.expr, req.args) } @@ -1231,8 +1233,8 @@ func (req *ExecuteRequest) Args(args interface{}) *ExecuteRequest { return req } -// Body fills an encoder with the execute request body. -func (req *ExecuteRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the execute request body. +func (req *ExecuteRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillExecute(enc, req.expr, req.args) } diff --git a/request_test.go b/request_test.go index 3458f397b..d3f15c179 100644 --- a/request_test.go +++ b/request_test.go @@ -8,9 +8,9 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" ) const invalidSpaceMsg = "invalid space" @@ -68,7 +68,7 @@ func assertBodyCall(t testing.TB, requests []Request, errorMsg string) { const errBegin = "An unexpected Request.Body() " for _, req := range requests { var reqBuf bytes.Buffer - enc := NewEncoder(&reqBuf) + enc := msgpack.NewEncoder(&reqBuf) err := req.Body(&resolver, enc) if err != nil && errorMsg != "" && err.Error() != errorMsg { @@ -86,11 +86,15 @@ func assertBodyCall(t testing.TB, requests []Request, errorMsg string) { func assertBodyEqual(t testing.TB, reference []byte, req Request) { t.Helper() - reqBody, err := test_helpers.ExtractRequestBody(req, &resolver, NewEncoder) + var reqBuf bytes.Buffer + reqEnc := msgpack.NewEncoder(&reqBuf) + + err := req.Body(&resolver, reqEnc) if err != nil { t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) } + reqBody := reqBuf.Bytes() if !bytes.Equal(reqBody, reference) { t.Errorf("Encoded request %v != reference %v", reqBody, reference) } @@ -301,7 +305,7 @@ func TestRequestsCtx_setter(t *testing.T) { func TestPingRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplPingBody(refEnc) if err != nil { t.Errorf("An unexpected RefImplPingBody() error: %q", err.Error()) @@ -315,7 +319,7 @@ func TestPingRequestDefaultValues(t *testing.T) { func TestSelectRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterAll, []interface{}{}, nil, false) if err != nil { @@ -331,7 +335,7 @@ func TestSelectRequestDefaultIteratorEqIfKey(t *testing.T) { var refBuf bytes.Buffer key := []interface{}{uint(18)} - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterEq, key, nil, false) if err != nil { @@ -349,7 +353,7 @@ func TestSelectRequestIteratorNotChangedIfKey(t *testing.T) { key := []interface{}{uint(678)} const iter = IterGe - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, iter, key, nil, false) if err != nil { @@ -372,7 +376,7 @@ func TestSelectRequestSetters(t *testing.T) { afterKey := []interface{}{uint(13)} var refBufAfterBytes, refBufAfterKey bytes.Buffer - refEncAfterBytes := NewEncoder(&refBufAfterBytes) + refEncAfterBytes := msgpack.NewEncoder(&refBufAfterBytes) err := RefImplSelectBody(refEncAfterBytes, validSpace, validIndex, offset, limit, iter, key, afterBytes, true) if err != nil { @@ -380,7 +384,7 @@ func TestSelectRequestSetters(t *testing.T) { return } - refEncAfterKey := NewEncoder(&refBufAfterKey) + refEncAfterKey := msgpack.NewEncoder(&refBufAfterKey) err = RefImplSelectBody(refEncAfterKey, validSpace, validIndex, offset, limit, iter, key, afterKey, true) if err != nil { @@ -412,7 +416,7 @@ func TestSelectRequestSetters(t *testing.T) { func TestInsertRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplInsertBody(refEnc, validSpace, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplInsertBody() error: %q", err.Error()) @@ -427,7 +431,7 @@ func TestInsertRequestSetters(t *testing.T) { tuple := []interface{}{uint(24)} var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplInsertBody(refEnc, validSpace, tuple) if err != nil { t.Errorf("An unexpected RefImplInsertBody() error: %q", err.Error()) @@ -442,7 +446,7 @@ func TestInsertRequestSetters(t *testing.T) { func TestReplaceRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplReplaceBody(refEnc, validSpace, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplReplaceBody() error: %q", err.Error()) @@ -457,7 +461,7 @@ func TestReplaceRequestSetters(t *testing.T) { tuple := []interface{}{uint(99)} var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplReplaceBody(refEnc, validSpace, tuple) if err != nil { t.Errorf("An unexpected RefImplReplaceBody() error: %q", err.Error()) @@ -472,7 +476,7 @@ func TestReplaceRequestSetters(t *testing.T) { func TestDeleteRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplDeleteBody(refEnc, validSpace, defaultIndex, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplDeleteBody() error: %q", err.Error()) @@ -487,7 +491,7 @@ func TestDeleteRequestSetters(t *testing.T) { key := []interface{}{uint(923)} var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplDeleteBody(refEnc, validSpace, validIndex, key) if err != nil { t.Errorf("An unexpected RefImplDeleteBody() error: %q", err.Error()) @@ -503,7 +507,7 @@ func TestDeleteRequestSetters(t *testing.T) { func TestUpdateRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplUpdateBody(refEnc, validSpace, defaultIndex, []interface{}{}, []Op{}) if err != nil { t.Errorf("An unexpected RefImplUpdateBody() error: %q", err.Error()) @@ -519,7 +523,7 @@ func TestUpdateRequestSetters(t *testing.T) { refOps, reqOps := getTestOps() var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplUpdateBody(refEnc, validSpace, validIndex, key, refOps) if err != nil { t.Errorf("An unexpected RefImplUpdateBody() error: %q", err.Error()) @@ -536,7 +540,7 @@ func TestUpdateRequestSetters(t *testing.T) { func TestUpsertRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplUpsertBody(refEnc, validSpace, []interface{}{}, []Op{}) if err != nil { t.Errorf("An unexpected RefImplUpsertBody() error: %q", err.Error()) @@ -552,7 +556,7 @@ func TestUpsertRequestSetters(t *testing.T) { refOps, reqOps := getTestOps() var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplUpsertBody(refEnc, validSpace, tuple, refOps) if err != nil { t.Errorf("An unexpected RefImplUpsertBody() error: %q", err.Error()) @@ -568,7 +572,7 @@ func TestUpsertRequestSetters(t *testing.T) { func TestCallRequestsDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplCallBody(refEnc, validExpr, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) @@ -587,7 +591,7 @@ func TestCallRequestsSetters(t *testing.T) { args := []interface{}{uint(34)} var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplCallBody(refEnc, validExpr, args) if err != nil { t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) @@ -608,7 +612,7 @@ func TestCallRequestsSetters(t *testing.T) { func TestEvalRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplEvalBody(refEnc, validExpr, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplEvalBody() error: %q", err.Error()) @@ -623,7 +627,7 @@ func TestEvalRequestSetters(t *testing.T) { args := []interface{}{uint(34), int(12)} var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplEvalBody(refEnc, validExpr, args) if err != nil { t.Errorf("An unexpected RefImplEvalBody() error: %q", err.Error()) @@ -638,7 +642,7 @@ func TestEvalRequestSetters(t *testing.T) { func TestExecuteRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplExecuteBody(refEnc, validExpr, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplExecuteBody() error: %q", err.Error()) @@ -653,7 +657,7 @@ func TestExecuteRequestSetters(t *testing.T) { args := []interface{}{uint(11)} var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplExecuteBody(refEnc, validExpr, args) if err != nil { t.Errorf("An unexpected RefImplExecuteBody() error: %q", err.Error()) @@ -668,7 +672,7 @@ func TestExecuteRequestSetters(t *testing.T) { func TestPrepareRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplPrepareBody(refEnc, validExpr) if err != nil { t.Errorf("An unexpected RefImplPrepareBody() error: %q", err.Error()) @@ -682,7 +686,7 @@ func TestPrepareRequestDefaultValues(t *testing.T) { func TestUnprepareRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplUnprepareBody(refEnc, *validStmt) if err != nil { t.Errorf("An unexpected RefImplUnprepareBody() error: %q", err.Error()) @@ -698,7 +702,7 @@ func TestExecutePreparedRequestSetters(t *testing.T) { args := []interface{}{uint(11)} var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplExecutePreparedBody(refEnc, *validStmt, args) if err != nil { t.Errorf("An unexpected RefImplExecutePreparedBody() error: %q", err.Error()) @@ -714,7 +718,7 @@ func TestExecutePreparedRequestSetters(t *testing.T) { func TestExecutePreparedRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplExecutePreparedBody(refEnc, *validStmt, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplExecutePreparedBody() error: %q", err.Error()) @@ -729,7 +733,7 @@ func TestExecutePreparedRequestDefaultValues(t *testing.T) { func TestBeginRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplBeginBody(refEnc, defaultIsolationLevel, defaultTimeout) if err != nil { t.Errorf("An unexpected RefImplBeginBody() error: %q", err.Error()) @@ -743,7 +747,7 @@ func TestBeginRequestDefaultValues(t *testing.T) { func TestBeginRequestSetters(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplBeginBody(refEnc, ReadConfirmedLevel, validTimeout) if err != nil { t.Errorf("An unexpected RefImplBeginBody() error: %q", err.Error()) @@ -757,7 +761,7 @@ func TestBeginRequestSetters(t *testing.T) { func TestCommitRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplCommitBody(refEnc) if err != nil { t.Errorf("An unexpected RefImplCommitBody() error: %q", err.Error()) @@ -771,7 +775,7 @@ func TestCommitRequestDefaultValues(t *testing.T) { func TestRollbackRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) err := RefImplRollbackBody(refEnc) if err != nil { t.Errorf("An unexpected RefImplRollbackBody() error: %q", err.Error()) @@ -785,7 +789,7 @@ func TestRollbackRequestDefaultValues(t *testing.T) { func TestBroadcastRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) expectedArgs := []interface{}{validKey} err := RefImplCallBody(refEnc, "box.broadcast", expectedArgs) if err != nil { @@ -801,7 +805,7 @@ func TestBroadcastRequestSetters(t *testing.T) { value := []interface{}{uint(34), int(12)} var refBuf bytes.Buffer - refEnc := NewEncoder(&refBuf) + refEnc := msgpack.NewEncoder(&refBuf) expectedArgs := []interface{}{validKey, value} err := RefImplCallBody(refEnc, "box.broadcast", expectedArgs) if err != nil { diff --git a/response.go b/response.go index e5486b0a4..b07e8690f 100644 --- a/response.go +++ b/response.go @@ -2,6 +2,8 @@ package tarantool import ( "fmt" + + "github.com/vmihailenco/msgpack/v5" ) type Response struct { @@ -32,7 +34,7 @@ type SQLInfo struct { InfoAutoincrementIds []uint64 } -func (meta *ColumnMetaData) DecodeMsgpack(d *decoder) error { +func (meta *ColumnMetaData) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeMapLen(); err != nil { @@ -70,7 +72,7 @@ func (meta *ColumnMetaData) DecodeMsgpack(d *decoder) error { return nil } -func (info *SQLInfo) DecodeMsgpack(d *decoder) error { +func (info *SQLInfo) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeMapLen(); err != nil { @@ -100,7 +102,7 @@ func (info *SQLInfo) DecodeMsgpack(d *decoder) error { return nil } -func (resp *Response) smallInt(d *decoder) (i int, err error) { +func (resp *Response) smallInt(d *msgpack.Decoder) (i int, err error) { b, err := resp.buf.ReadByte() if err != nil { return @@ -112,7 +114,7 @@ func (resp *Response) smallInt(d *decoder) (i int, err error) { return d.DecodeInt() } -func (resp *Response) decodeHeader(d *decoder) (err error) { +func (resp *Response) decodeHeader(d *msgpack.Decoder) (err error) { var l int d.Reset(&resp.buf) if l, err = d.DecodeMapLen(); err != nil { @@ -156,7 +158,10 @@ func (resp *Response) decodeBody() (err error) { var feature ProtocolFeature var errorExtendedInfo *BoxError = nil - d := newDecoder(&resp.buf) + d := msgpack.NewDecoder(&resp.buf) + d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { + return dec.DecodeUntypedMap() + }) if l, err = d.DecodeMapLen(); err != nil { return err @@ -277,7 +282,12 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { var errorExtendedInfo *BoxError = nil var l int - d := newDecoder(&resp.buf) + + d := msgpack.NewDecoder(&resp.buf) + d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { + return dec.DecodeUntypedMap() + }) + if l, err = d.DecodeMapLen(); err != nil { return err } diff --git a/schema.go b/schema.go index 87c788fe0..9e9f0ac17 100644 --- a/schema.go +++ b/schema.go @@ -3,6 +3,9 @@ package tarantool import ( "errors" "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" ) // nolint: varcheck,deadcode @@ -16,6 +19,26 @@ const ( vspaceSpFormatFieldNum = 7 ) +func msgpackIsUint(code byte) bool { + return code == msgpcode.Uint8 || code == msgpcode.Uint16 || + code == msgpcode.Uint32 || code == msgpcode.Uint64 || + msgpcode.IsFixedNum(code) +} + +func msgpackIsMap(code byte) bool { + return code == msgpcode.Map16 || code == msgpcode.Map32 || msgpcode.IsFixedMap(code) +} + +func msgpackIsArray(code byte) bool { + return code == msgpcode.Array16 || code == msgpcode.Array32 || + msgpcode.IsFixedArray(code) +} + +func msgpackIsString(code byte) bool { + return msgpcode.IsFixedString(code) || code == msgpcode.Str8 || + code == msgpcode.Str16 || code == msgpcode.Str32 +} + // SchemaResolver is an interface for resolving schema details. type SchemaResolver interface { // ResolveSpaceIndex returns resolved space and index numbers or an @@ -49,7 +72,7 @@ type Space struct { IndexesById map[uint32]*Index } -func (space *Space) DecodeMsgpack(d *decoder) error { +func (space *Space) DecodeMsgpack(d *msgpack.Decoder) error { arrayLen, err := d.DecodeArrayLen() if err != nil { return err @@ -134,7 +157,7 @@ type Field struct { Type string } -func (field *Field) DecodeMsgpack(d *decoder) error { +func (field *Field) DecodeMsgpack(d *msgpack.Decoder) error { l, err := d.DecodeMapLen() if err != nil { return err @@ -172,7 +195,7 @@ type Index struct { Fields []*IndexField } -func (index *Index) DecodeMsgpack(d *decoder) error { +func (index *Index) DecodeMsgpack(d *msgpack.Decoder) error { _, err := d.DecodeArrayLen() if err != nil { return err @@ -248,7 +271,7 @@ type IndexField struct { Type string } -func (indexField *IndexField) DecodeMsgpack(d *decoder) error { +func (indexField *IndexField) DecodeMsgpack(d *msgpack.Decoder) error { code, err := d.PeekCode() if err != nil { return err diff --git a/settings/msgpack.go b/settings/msgpack.go deleted file mode 100644 index 295620aba..000000000 --- a/settings/msgpack.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package settings - -import ( - "gopkg.in/vmihailenco/msgpack.v2" -) - -type encoder = msgpack.Encoder diff --git a/settings/msgpack_helper_test.go b/settings/msgpack_helper_test.go deleted file mode 100644 index efc3285e4..000000000 --- a/settings/msgpack_helper_test.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package settings_test - -import ( - "io" - - "github.com/tarantool/go-tarantool/v2" - "gopkg.in/vmihailenco/msgpack.v2" -) - -type encoder = msgpack.Encoder - -func NewEncoder(w io.Writer) *encoder { - return msgpack.NewEncoder(w) -} - -func toBoxError(i interface{}) (v tarantool.BoxError, ok bool) { - v, ok = i.(tarantool.BoxError) - return -} diff --git a/settings/msgpack_v5.go b/settings/msgpack_v5.go deleted file mode 100644 index 288418ec6..000000000 --- a/settings/msgpack_v5.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package settings - -import ( - "github.com/vmihailenco/msgpack/v5" -) - -type encoder = msgpack.Encoder diff --git a/settings/msgpack_v5_helper_test.go b/settings/msgpack_v5_helper_test.go deleted file mode 100644 index e58651f61..000000000 --- a/settings/msgpack_v5_helper_test.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package settings_test - -import ( - "io" - - "github.com/tarantool/go-tarantool/v2" - "github.com/vmihailenco/msgpack/v5" -) - -type encoder = msgpack.Encoder - -func NewEncoder(w io.Writer) *encoder { - return msgpack.NewEncoder(w) -} - -func toBoxError(i interface{}) (v tarantool.BoxError, ok bool) { - var ptr *tarantool.BoxError - if ptr, ok = i.(*tarantool.BoxError); ok { - v = *ptr - } - return -} diff --git a/settings/request.go b/settings/request.go index fa4e1eadd..df6dcd0ac 100644 --- a/settings/request.go +++ b/settings/request.go @@ -60,6 +60,8 @@ package settings import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -89,7 +91,7 @@ func (req *SetRequest) Code() int32 { } // Body fills an encoder with set session settings request body. -func (req *SetRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req *SetRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { return req.impl.Body(res, enc) } @@ -129,7 +131,7 @@ func (req *GetRequest) Code() int32 { } // Body fills an encoder with get session settings request body. -func (req *GetRequest) Body(res tarantool.SchemaResolver, enc *encoder) error { +func (req *GetRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { return req.impl.Body(res, enc) } diff --git a/settings/request_test.go b/settings/request_test.go index 662a7002e..7623a6cac 100644 --- a/settings/request_test.go +++ b/settings/request_test.go @@ -6,6 +6,8 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" . "github.com/tarantool/go-tarantool/v2/settings" ) @@ -68,7 +70,7 @@ func TestRequestsAPI(t *testing.T) { require.Equal(t, test.code, test.req.Code()) var reqBuf bytes.Buffer - enc := NewEncoder(&reqBuf) + enc := msgpack.NewEncoder(&reqBuf) require.Nilf(t, test.req.Body(&resolver, enc), "No errors on fill") } } diff --git a/settings/tarantool_test.go b/settings/tarantool_test.go index dc6a79825..aeec28cc0 100644 --- a/settings/tarantool_test.go +++ b/settings/tarantool_test.go @@ -89,7 +89,7 @@ func TestErrorMarshalingEnabledSetting(t *testing.T) { resp, err = conn.Eval("return box.error.new(box.error.UNKNOWN)", []interface{}{}) require.Nil(t, err) require.NotNil(t, resp) - _, ok := toBoxError(resp.Data[0]) + _, ok := resp.Data[0].(*tarantool.BoxError) require.True(t, ok) } diff --git a/stream.go b/stream.go index 3a03ec68f..5e1220dff 100644 --- a/stream.go +++ b/stream.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "time" + + "github.com/vmihailenco/msgpack/v5" ) type TxnIsolationLevel uint @@ -27,7 +29,7 @@ type Stream struct { Conn *Connection } -func fillBegin(enc *encoder, txnIsolation TxnIsolationLevel, timeout time.Duration) error { +func fillBegin(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, timeout time.Duration) error { hasTimeout := timeout > 0 hasIsolationLevel := txnIsolation != DefaultIsolationLevel mapLen := 0 @@ -44,7 +46,7 @@ func fillBegin(enc *encoder, txnIsolation TxnIsolationLevel, timeout time.Durati } if hasTimeout { - err = encodeUint(enc, KeyTimeout) + err = enc.EncodeUint(KeyTimeout) if err != nil { return err } @@ -56,12 +58,12 @@ func fillBegin(enc *encoder, txnIsolation TxnIsolationLevel, timeout time.Durati } if hasIsolationLevel { - err = encodeUint(enc, KeyTxnIsolation) + err = enc.EncodeUint(KeyTxnIsolation) if err != nil { return err } - err = encodeUint(enc, uint64(txnIsolation)) + err = enc.EncodeUint(uint64(txnIsolation)) if err != nil { return err } @@ -70,11 +72,11 @@ func fillBegin(enc *encoder, txnIsolation TxnIsolationLevel, timeout time.Durati return err } -func fillCommit(enc *encoder) error { +func fillCommit(enc *msgpack.Encoder) error { return enc.EncodeMapLen(0) } -func fillRollback(enc *encoder) error { +func fillRollback(enc *msgpack.Encoder) error { return enc.EncodeMapLen(0) } @@ -108,8 +110,8 @@ func (req *BeginRequest) Timeout(timeout time.Duration) *BeginRequest { return req } -// Body fills an encoder with the begin request body. -func (req *BeginRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the begin request body. +func (req *BeginRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillBegin(enc, req.txnIsolation, req.timeout) } @@ -138,8 +140,8 @@ func NewCommitRequest() *CommitRequest { return req } -// Body fills an encoder with the commit request body. -func (req *CommitRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the commit request body. +func (req *CommitRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillCommit(enc) } @@ -168,8 +170,8 @@ func NewRollbackRequest() *RollbackRequest { return req } -// Body fills an encoder with the rollback request body. -func (req *RollbackRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the rollback request body. +func (req *RollbackRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return fillRollback(enc) } diff --git a/tarantool_test.go b/tarantool_test.go index 9c315b358..c663fa190 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -15,9 +15,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack/v5" - "github.com/stretchr/testify/assert" . "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/test_helpers" ) @@ -38,20 +39,20 @@ type Member struct { Val uint } -func (m *Member) EncodeMsgpack(e *encoder) error { +func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeArrayLen(2); err != nil { return err } if err := e.EncodeString(m.Name); err != nil { return err } - if err := encodeUint(e, uint64(m.Val)); err != nil { + if err := e.EncodeUint(uint64(m.Val)); err != nil { return err } return nil } -func (m *Member) DecodeMsgpack(d *decoder) error { +func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { @@ -2652,7 +2653,7 @@ func (req *waitCtxRequest) Code() int32 { return NewPingRequest().Code() } -func (req *waitCtxRequest) Body(res SchemaResolver, enc *encoder) error { +func (req *waitCtxRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { req.wg.Wait() return NewPingRequest().Body(res, enc) } diff --git a/test_helpers/main.go b/test_helpers/main.go index c6c03b00d..c234d4c02 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -373,7 +373,7 @@ func copyFile(srcFile, dstFile string) error { return nil } -// msgpack.v2 and msgpack.v5 return different uint types in responses. The +// msgpack.v5 decodes different uint types depending on value. The // function helps to unify a result. func ConvertUint64(v interface{}) (result uint64, err error) { switch v := v.(type) { diff --git a/test_helpers/msgpack.go b/test_helpers/msgpack.go deleted file mode 100644 index 1ea712b38..000000000 --- a/test_helpers/msgpack.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package test_helpers - -import ( - "gopkg.in/vmihailenco/msgpack.v2" -) - -type encoder = msgpack.Encoder diff --git a/test_helpers/msgpack_v5.go b/test_helpers/msgpack_v5.go deleted file mode 100644 index 37f85ef31..000000000 --- a/test_helpers/msgpack_v5.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package test_helpers - -import ( - "github.com/vmihailenco/msgpack/v5" -) - -type encoder = msgpack.Encoder diff --git a/test_helpers/request_mock.go b/test_helpers/request_mock.go index d668c8f52..0eaa11739 100644 --- a/test_helpers/request_mock.go +++ b/test_helpers/request_mock.go @@ -3,6 +3,8 @@ package test_helpers import ( "context" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-tarantool/v2" ) @@ -21,7 +23,7 @@ func (sr *StrangerRequest) Async() bool { return false } -func (sr *StrangerRequest) Body(resolver tarantool.SchemaResolver, enc *encoder) error { +func (sr *StrangerRequest) Body(resolver tarantool.SchemaResolver, enc *msgpack.Encoder) error { return nil } diff --git a/test_helpers/utils.go b/test_helpers/utils.go index cf17971f5..3771a5f9e 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -1,9 +1,7 @@ package test_helpers import ( - "bytes" "fmt" - "io" "testing" "time" @@ -230,16 +228,3 @@ func CheckEqualBoxErrors(t *testing.T, expected tarantool.BoxError, actual taran } } } - -func ExtractRequestBody(req tarantool.Request, resolver tarantool.SchemaResolver, - newEncFunc func(w io.Writer) *encoder) ([]byte, error) { - var reqBuf bytes.Buffer - reqEnc := newEncFunc(&reqBuf) - - err := req.Body(resolver, reqEnc) - if err != nil { - return nil, fmt.Errorf("An unexpected Response.Body() error: %q", err.Error()) - } - - return reqBuf.Bytes(), nil -} diff --git a/uuid/msgpack.go b/uuid/msgpack.go deleted file mode 100644 index 62504e5f9..000000000 --- a/uuid/msgpack.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package uuid - -import ( - "reflect" - - "github.com/google/uuid" - "gopkg.in/vmihailenco/msgpack.v2" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -func init() { - msgpack.Register(reflect.TypeOf((*uuid.UUID)(nil)).Elem(), encodeUUID, decodeUUID) - msgpack.RegisterExt(UUID_extId, (*uuid.UUID)(nil)) -} diff --git a/uuid/msgpack_helper_test.go b/uuid/msgpack_helper_test.go deleted file mode 100644 index d5a1cb70e..000000000 --- a/uuid/msgpack_helper_test.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !go_tarantool_msgpack_v5 -// +build !go_tarantool_msgpack_v5 - -package uuid_test - -import ( - "gopkg.in/vmihailenco/msgpack.v2" -) - -type decoder = msgpack.Decoder diff --git a/uuid/msgpack_v5.go b/uuid/msgpack_v5.go deleted file mode 100644 index 951c437dd..000000000 --- a/uuid/msgpack_v5.go +++ /dev/null @@ -1,27 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package uuid - -import ( - "reflect" - - "github.com/google/uuid" - "github.com/vmihailenco/msgpack/v5" -) - -type encoder = msgpack.Encoder -type decoder = msgpack.Decoder - -func init() { - msgpack.Register(reflect.TypeOf((*uuid.UUID)(nil)).Elem(), encodeUUID, decodeUUID) - msgpack.RegisterExtEncoder(UUID_extId, uuid.UUID{}, - func(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { - uuid := v.Interface().(uuid.UUID) - return uuid.MarshalBinary() - }) - msgpack.RegisterExtDecoder(UUID_extId, uuid.UUID{}, - func(d *msgpack.Decoder, v reflect.Value, extLen int) error { - return decodeUUID(d, v) - }) -} diff --git a/uuid/msgpack_v5_helper_test.go b/uuid/msgpack_v5_helper_test.go deleted file mode 100644 index c2356ef1a..000000000 --- a/uuid/msgpack_v5_helper_test.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build go_tarantool_msgpack_v5 -// +build go_tarantool_msgpack_v5 - -package uuid_test - -import ( - "github.com/vmihailenco/msgpack/v5" -) - -type decoder = msgpack.Decoder diff --git a/uuid/uuid.go b/uuid/uuid.go index a72785c0e..262335646 100644 --- a/uuid/uuid.go +++ b/uuid/uuid.go @@ -18,12 +18,13 @@ import ( "reflect" "github.com/google/uuid" + "github.com/vmihailenco/msgpack/v5" ) // UUID external type. const UUID_extId = 2 -func encodeUUID(e *encoder, v reflect.Value) error { +func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { id := v.Interface().(uuid.UUID) bytes, err := id.MarshalBinary() @@ -33,13 +34,13 @@ func encodeUUID(e *encoder, v reflect.Value) error { _, err = e.Writer().Write(bytes) if err != nil { - return fmt.Errorf("msgpack: can't write bytes to encoder writer: %w", err) + return fmt.Errorf("msgpack: can't write bytes to msgpack.Encoder writer: %w", err) } return nil } -func decodeUUID(d *decoder, v reflect.Value) error { +func decodeUUID(d *msgpack.Decoder, v reflect.Value) error { var bytesCount int = 16 bytes := make([]byte, bytesCount) @@ -59,3 +60,16 @@ func decodeUUID(d *decoder, v reflect.Value) error { v.Set(reflect.ValueOf(id)) return nil } + +func init() { + msgpack.Register(reflect.TypeOf((*uuid.UUID)(nil)).Elem(), encodeUUID, decodeUUID) + msgpack.RegisterExtEncoder(UUID_extId, uuid.UUID{}, + func(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { + uuid := v.Interface().(uuid.UUID) + return uuid.MarshalBinary() + }) + msgpack.RegisterExtDecoder(UUID_extId, uuid.UUID{}, + func(d *msgpack.Decoder, v reflect.Value, extLen int) error { + return decodeUUID(d, v) + }) +} diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index a000f99cc..f09caf03d 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -8,6 +8,8 @@ import ( "time" "github.com/google/uuid" + "github.com/vmihailenco/msgpack/v5" + . "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/test_helpers" _ "github.com/tarantool/go-tarantool/v2/uuid" @@ -32,7 +34,7 @@ type TupleUUID struct { id uuid.UUID } -func (t *TupleUUID) DecodeMsgpack(d *decoder) error { +func (t *TupleUUID) DecodeMsgpack(d *msgpack.Decoder) error { var err error var l int if l, err = d.DecodeArrayLen(); err != nil { diff --git a/watch.go b/watch.go index 61631657c..bf045de4b 100644 --- a/watch.go +++ b/watch.go @@ -2,6 +2,8 @@ package tarantool import ( "context" + + "github.com/vmihailenco/msgpack/v5" ) // BroadcastRequest helps to send broadcast messages. See: @@ -37,8 +39,8 @@ func (req *BroadcastRequest) Code() int32 { return req.call.Code() } -// Body fills an encoder with the broadcast request body. -func (req *BroadcastRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the broadcast request body. +func (req *BroadcastRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return req.call.Body(res, enc) } @@ -70,12 +72,12 @@ func newWatchRequest(key string) *watchRequest { return req } -// Body fills an encoder with the watch request body. -func (req *watchRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the watch request body. +func (req *watchRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { if err := enc.EncodeMapLen(1); err != nil { return err } - if err := encodeUint(enc, KeyEvent); err != nil { + if err := enc.EncodeUint(KeyEvent); err != nil { return err } return enc.EncodeString(req.key) @@ -104,12 +106,12 @@ func newUnwatchRequest(key string) *unwatchRequest { return req } -// Body fills an encoder with the unwatch request body. -func (req *unwatchRequest) Body(res SchemaResolver, enc *encoder) error { +// Body fills an msgpack.Encoder with the unwatch request body. +func (req *unwatchRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { if err := enc.EncodeMapLen(1); err != nil { return err } - if err := encodeUint(enc, KeyEvent); err != nil { + if err := enc.EncodeUint(KeyEvent); err != nil { return err } return enc.EncodeString(req.key) From 54471b36c4488b3bdbb9adb5743b8f139f8195ce Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 22 Mar 2023 15:24:08 +0300 Subject: [PATCH 451/605] api: make Call = Call17 Closes #235 --- .github/workflows/testing.yml | 19 ------ CHANGELOG.md | 1 + README.md | 16 ++--- call_16_test.go | 54 ---------------- call_17_test.go | 54 ---------------- const.go | 1 + const_call_16.go | 8 --- const_call_17.go | 8 --- pool/call_16_test.go | 69 -------------------- pool/call_17_test.go | 69 -------------------- pool/connection_pool.go | 29 +++------ pool/connection_pool_test.go | 114 ++++++++++++++++++++++++++++++++++ pool/connector.go | 27 +++----- request.go | 32 ++++------ request_test.go | 7 ++- tarantool_test.go | 33 ++++++++++ 16 files changed, 194 insertions(+), 347 deletions(-) delete mode 100644 call_16_test.go delete mode 100644 call_17_test.go delete mode 100644 const_call_16.go delete mode 100644 const_call_17.go delete mode 100644 pool/call_16_test.go delete mode 100644 pool/call_17_test.go diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 68f283b95..6745870a2 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -99,11 +99,6 @@ jobs: make test make testrace - - name: Run regression tests with call_17 - run: | - make test TAGS="go_tarantool_call_17" - make testrace TAGS="go_tarantool_call_17" - - name: Run fuzzing tests if: ${{ matrix.fuzzing }} run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" @@ -189,14 +184,6 @@ jobs: env: TEST_TNT_SSL: ${{matrix.ssl}} - - name: Run regression tests with call_17 - run: | - source tarantool-enterprise/env.sh - make test TAGS="go_tarantool_call_17" - make testrace TAGS="go_tarantool_call_17" - env: - TEST_TNT_SSL: ${{matrix.ssl}} - - name: Run fuzzing tests if: ${{ matrix.fuzzing }} run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" @@ -363,12 +350,6 @@ jobs: make test make testrace - - name: Run regression tests with call_17 - run: | - cd "${SRCDIR}" - make test TAGS="go_tarantool_call_17" - make testrace TAGS="go_tarantool_call_17" - - name: Run fuzzing tests if: ${{ matrix.fuzzing }} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f9d1b99a..955e67809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - connection_pool renamed to pool (#239) - Use msgpack/v5 instead of msgpack.v2 (#236) +- Call/NewCallRequest = Call17/NewCall17Request (#235) ### Removed diff --git a/README.md b/README.md index ea941d964..d51a11cdd 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ faster than other packages according to public benchmarks. * [multi package](#multi-package) * [pool package](#pool-package) * [msgpack.v5](#msgpackv5) + * [Call = Call17](#call--call17) * [Contributing](#contributing) * [Alternative connectors](#alternative-connectors) @@ -66,13 +67,7 @@ This allows us to introduce new features without losing backward compatibility. ``` go_tarantool_ssl_disable ``` -2. To change the default `Call` behavior from `Call16` to `Call17`, you can use - the build tag: - ``` - go_tarantool_call_17 - ``` - **Note:** In future releases, `Call17` may be used as default `Call` behavior. -3. To run fuzz tests with decimals, you can use the build tag: +2. To run fuzz tests with decimals, you can use the build tag: ``` go_tarantool_decimal_fuzzing ``` @@ -188,6 +183,13 @@ There are also changes in the logic that can lead to errors in the old code, to achieve full compliance of behavior between `msgpack.v5` and `msgpack.v2`. So we don't go this way. We use standard settings if it possible. +#### Call = Call17 + +Call requests uses `IPROTO_CALL` instead of `IPROTO_CALL_16`. + +So now `Call` = `Call17` and `NewCallRequest` = `NewCall17Request`. A result +of the requests is an array instead of array of arrays. + ## Contributing See [the contributing guide](CONTRIBUTING.md) for detailed instructions on how diff --git a/call_16_test.go b/call_16_test.go deleted file mode 100644 index 8407f109d..000000000 --- a/call_16_test.go +++ /dev/null @@ -1,54 +0,0 @@ -//go:build !go_tarantool_call_17 -// +build !go_tarantool_call_17 - -package tarantool_test - -import ( - "testing" - - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" -) - -func TestConnection_Call(t *testing.T) { - var resp *Response - var err error - - conn := test_helpers.ConnectWithValidation(t, server, opts) - defer conn.Close() - - // Call16 - resp, err = conn.Call("simple_concat", []interface{}{"1"}) - if err != nil { - t.Errorf("Failed to use Call") - } - if val, ok := resp.Data[0].([]interface{})[0].(string); !ok || val != "11" { - t.Errorf("result is not {{1}} : %v", resp.Data) - } -} - -func TestCallRequest(t *testing.T) { - var resp *Response - var err error - - conn := test_helpers.ConnectWithValidation(t, server, opts) - defer conn.Close() - - req := NewCallRequest("simple_concat").Args([]interface{}{"1"}) - resp, err = conn.Do(req).Get() - if err != nil { - t.Errorf("Failed to use Call") - } - if val, ok := resp.Data[0].([]interface{})[0].(string); !ok || val != "11" { - t.Errorf("result is not {{1}} : %v", resp.Data) - } -} - -func TestCallRequestCode(t *testing.T) { - req := NewCallRequest("simple_concat") - code := req.Code() - expected := Call16RequestCode - if code != int32(expected) { - t.Errorf("CallRequest actual code %v != %v", code, expected) - } -} diff --git a/call_17_test.go b/call_17_test.go deleted file mode 100644 index 6111aa52c..000000000 --- a/call_17_test.go +++ /dev/null @@ -1,54 +0,0 @@ -//go:build go_tarantool_call_17 -// +build go_tarantool_call_17 - -package tarantool_test - -import ( - "testing" - - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" -) - -func TestConnection_Call(t *testing.T) { - var resp *Response - var err error - - conn := test_helpers.ConnectWithValidation(t, server, opts) - defer conn.Close() - - // Call17 - resp, err = conn.Call17("simple_concat", []interface{}{"1"}) - if err != nil { - t.Errorf("Failed to use Call") - } - if val, ok := resp.Data[0].(string); !ok || val != "11" { - t.Errorf("result is not {{1}} : %v", resp.Data) - } -} - -func TestCallRequest(t *testing.T) { - var resp *Response - var err error - - conn := test_helpers.ConnectWithValidation(t, server, opts) - defer conn.Close() - - req := NewCallRequest("simple_concat").Args([]interface{}{"1"}) - resp, err = conn.Do(req).Get() - if err != nil { - t.Errorf("Failed to use Call") - } - if val, ok := resp.Data[0].(string); !ok || val != "11" { - t.Errorf("result is not {{1}} : %v", resp.Data) - } -} - -func TestCallRequestCode(t *testing.T) { - req := NewCallRequest("simple_concat") - code := req.Code() - expected := Call17RequestCode - if code != int32(expected) { - t.Errorf("CallRequest actual code %v != %v", code, expected) - } -} diff --git a/const.go b/const.go index 4b6458645..4580e4cb0 100644 --- a/const.go +++ b/const.go @@ -21,6 +21,7 @@ const ( IdRequestCode = 73 WatchRequestCode = 74 UnwatchRequestCode = 75 + CallRequestCode = Call17RequestCode KeyCode = 0x00 KeySync = 0x01 diff --git a/const_call_16.go b/const_call_16.go deleted file mode 100644 index 7d80cc631..000000000 --- a/const_call_16.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !go_tarantool_call_17 -// +build !go_tarantool_call_17 - -package tarantool - -const ( - CallRequestCode = Call16RequestCode -) diff --git a/const_call_17.go b/const_call_17.go deleted file mode 100644 index d50d8f1c1..000000000 --- a/const_call_17.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build go_tarantool_call_17 -// +build go_tarantool_call_17 - -package tarantool - -const ( - CallRequestCode = Call17RequestCode -) diff --git a/pool/call_16_test.go b/pool/call_16_test.go deleted file mode 100644 index 04b8898bc..000000000 --- a/pool/call_16_test.go +++ /dev/null @@ -1,69 +0,0 @@ -//go:build !go_tarantool_call_17 -// +build !go_tarantool_call_17 - -package pool_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/pool" - "github.com/tarantool/go-tarantool/v2/test_helpers" -) - -func TestCall(t *testing.T) { - roles := []bool{false, true, false, false, true} - - err := test_helpers.SetClusterRO(servers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - - connPool, err := pool.Connect(servers, connOpts) - require.Nilf(t, err, "failed to connect") - require.NotNilf(t, connPool, "conn is nil after Connect") - - defer connPool.Close() - - // PreferRO - resp, err := connPool.Call("box.info", []interface{}{}, pool.PreferRO) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") - - val := resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] - ro, ok := val.(bool) - require.Truef(t, ok, "expected `true` with mode `PreferRO`") - require.Truef(t, ro, "expected `true` with mode `PreferRO`") - - // PreferRW - resp, err = connPool.Call("box.info", []interface{}{}, pool.PreferRW) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") - - val = resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] - ro, ok = val.(bool) - require.Truef(t, ok, "expected `false` with mode `PreferRW`") - require.Falsef(t, ro, "expected `false` with mode `PreferRW`") - - // RO - resp, err = connPool.Call("box.info", []interface{}{}, pool.RO) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") - - val = resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] - ro, ok = val.(bool) - require.Truef(t, ok, "expected `true` with mode `RO`") - require.Truef(t, ro, "expected `true` with mode `RO`") - - // RW - resp, err = connPool.Call("box.info", []interface{}{}, pool.RW) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") - - val = resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] - ro, ok = val.(bool) - require.Truef(t, ok, "expected `false` with mode `RW`") - require.Falsef(t, ro, "expected `false` with mode `RW`") -} diff --git a/pool/call_17_test.go b/pool/call_17_test.go deleted file mode 100644 index 6ca8381ad..000000000 --- a/pool/call_17_test.go +++ /dev/null @@ -1,69 +0,0 @@ -//go:build go_tarantool_call_17 -// +build go_tarantool_call_17 - -package pool_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/pool" - "github.com/tarantool/go-tarantool/v2/test_helpers" -) - -func TestCall(t *testing.T) { - roles := []bool{false, true, false, false, true} - - err := test_helpers.SetClusterRO(servers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - - connPool, err := pool.Connect(servers, connOpts) - require.Nilf(t, err, "failed to connect") - require.NotNilf(t, connPool, "conn is nil after Connect") - - defer connPool.Close() - - // PreferRO - resp, err := connPool.Call("box.info", []interface{}{}, pool.PreferRO) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") - - val := resp.Data[0].(map[interface{}]interface{})["ro"] - ro, ok := val.(bool) - require.Truef(t, ok, "expected `true` with mode `PreferRO`") - require.Truef(t, ro, "expected `true` with mode `PreferRO`") - - // PreferRW - resp, err = connPool.Call("box.info", []interface{}{}, pool.PreferRW) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") - - val = resp.Data[0].(map[interface{}]interface{})["ro"] - ro, ok = val.(bool) - require.Truef(t, ok, "expected `false` with mode `PreferRW`") - require.Falsef(t, ro, "expected `false` with mode `PreferRW`") - - // RO - resp, err = connPool.Call("box.info", []interface{}{}, pool.RO) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") - - val = resp.Data[0].(map[interface{}]interface{})["ro"] - ro, ok = val.(bool) - require.Truef(t, ok, "expected `true` with mode `RO`") - require.Truef(t, ro, "expected `true` with mode `RO`") - - // RW - resp, err = connPool.Call("box.info", []interface{}{}, pool.RW) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") - - val = resp.Data[0].(map[interface{}]interface{})["ro"] - ro, ok = val.(bool) - require.Truef(t, ok, "expected `false` with mode `RW`") - require.Falsef(t, ro, "expected `false` with mode `RW`") -} diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 6d0865d57..fa4d6f732 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -437,10 +437,8 @@ func (connPool *ConnectionPool) Upsert(space interface{}, tuple, ops interface{} return conn.Upsert(space, tuple, ops) } -// Call16 calls registered Tarantool function. -// It uses request code for Tarantool >= 1.7 if go-tarantool -// was build with go_tarantool_call_17 tag. -// Otherwise, uses request code for Tarantool 1.6. +// Call calls registered Tarantool function. +// It uses request code for Tarantool >= 1.7, result is an array. func (connPool *ConnectionPool) Call(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -451,7 +449,7 @@ func (connPool *ConnectionPool) Call(functionName string, args interface{}, user } // Call16 calls registered Tarantool function. -// It uses request code for Tarantool 1.6, so result is converted to array of arrays. +// It uses request code for Tarantool 1.6, result is an array of arrays. // Deprecated since Tarantool 1.7.2. func (connPool *ConnectionPool) Call16(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getNextConnection(userMode) @@ -463,8 +461,7 @@ func (connPool *ConnectionPool) Call16(functionName string, args interface{}, us } // Call17 calls registered Tarantool function. -// It uses request code for Tarantool >= 1.7, so result is not converted -// (though, keep in mind, result is always array). +// It uses request code for Tarantool >= 1.7, result is an array. func (connPool *ConnectionPool) Call17(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -558,9 +555,7 @@ func (connPool *ConnectionPool) UpdateTyped(space, index interface{}, key, ops i } // CallTyped calls registered function. -// It uses request code for Tarantool >= 1.7 if go-tarantool -// was build with go_tarantool_call_17 tag. -// Otherwise, uses request code for Tarantool 1.6. +// It uses request code for Tarantool >= 1.7, result is an array. func (connPool *ConnectionPool) CallTyped(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -571,7 +566,7 @@ func (connPool *ConnectionPool) CallTyped(functionName string, args interface{}, } // Call16Typed calls registered function. -// It uses request code for Tarantool 1.6, so result is converted to array of arrays. +// It uses request code for Tarantool 1.6, result is an array of arrays. // Deprecated since Tarantool 1.7.2. func (connPool *ConnectionPool) Call16Typed(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { conn, err := connPool.getNextConnection(userMode) @@ -583,8 +578,7 @@ func (connPool *ConnectionPool) Call16Typed(functionName string, args interface{ } // Call17Typed calls registered function. -// It uses request code for Tarantool >= 1.7, so result is not converted -// (though, keep in mind, result is always array). +// It uses request code for Tarantool >= 1.7, result is an array. func (connPool *ConnectionPool) Call17Typed(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -680,9 +674,7 @@ func (connPool *ConnectionPool) UpsertAsync(space interface{}, tuple interface{} } // CallAsync sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool >= 1.7 if go-tarantool -// was build with go_tarantool_call_17 tag. -// Otherwise, uses request code for Tarantool 1.6. +// It uses request code for Tarantool >= 1.7, future's result is an array. func (connPool *ConnectionPool) CallAsync(functionName string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -693,7 +685,7 @@ func (connPool *ConnectionPool) CallAsync(functionName string, args interface{}, } // Call16Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool 1.6, so future's result is always array of arrays. +// It uses request code for Tarantool 1.6, so future's result is an array of arrays. // Deprecated since Tarantool 1.7.2. func (connPool *ConnectionPool) Call16Async(functionName string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) @@ -705,8 +697,7 @@ func (connPool *ConnectionPool) Call16Async(functionName string, args interface{ } // Call17Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool >= 1.7, so future's result will not be converted -// (though, keep in mind, result is always array). +// It uses request code for Tarantool >= 1.7, future's result is an array. func (connPool *ConnectionPool) Call17Async(functionName string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index 645644081..eda8560e8 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -936,6 +936,120 @@ func TestGetPoolInfo(t *testing.T) { require.ElementsMatch(t, expected, connPool.GetAddrs()) } +func TestCall(t *testing.T) { + roles := []bool{false, true, false, false, true} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // PreferRO + resp, err := connPool.Call("box.info", []interface{}{}, pool.PreferRO) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val := resp.Data[0].(map[interface{}]interface{})["ro"] + ro, ok := val.(bool) + require.Truef(t, ok, "expected `true` with mode `PreferRO`") + require.Truef(t, ro, "expected `true` with mode `PreferRO`") + + // PreferRW + resp, err = connPool.Call("box.info", []interface{}{}, pool.PreferRW) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `false` with mode `PreferRW`") + require.Falsef(t, ro, "expected `false` with mode `PreferRW`") + + // RO + resp, err = connPool.Call("box.info", []interface{}{}, pool.RO) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `true` with mode `RO`") + require.Truef(t, ro, "expected `true` with mode `RO`") + + // RW + resp, err = connPool.Call("box.info", []interface{}{}, pool.RW) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `false` with mode `RW`") + require.Falsef(t, ro, "expected `false` with mode `RW`") +} + +func TestCall16(t *testing.T) { + roles := []bool{false, true, false, false, true} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + // PreferRO + resp, err := connPool.Call16("box.info", []interface{}{}, pool.PreferRO) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val := resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] + ro, ok := val.(bool) + require.Truef(t, ok, "expected `true` with mode `PreferRO`") + require.Truef(t, ro, "expected `true` with mode `PreferRO`") + + // PreferRW + resp, err = connPool.Call16("box.info", []interface{}{}, pool.PreferRW) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `false` with mode `PreferRW`") + require.Falsef(t, ro, "expected `false` with mode `PreferRW`") + + // RO + resp, err = connPool.Call16("box.info", []interface{}{}, pool.RO) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `true` with mode `RO`") + require.Truef(t, ro, "expected `true` with mode `RO`") + + // RW + resp, err = connPool.Call16("box.info", []interface{}{}, pool.RW) + require.Nilf(t, err, "failed to Call") + require.NotNilf(t, resp, "response is nil after Call") + require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + + val = resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] + ro, ok = val.(bool) + require.Truef(t, ok, "expected `false` with mode `RW`") + require.Falsef(t, ro, "expected `false` with mode `RW`") +} + func TestCall17(t *testing.T) { roles := []bool{false, true, false, false, true} diff --git a/pool/connector.go b/pool/connector.go index 128d80575..22760801b 100644 --- a/pool/connector.go +++ b/pool/connector.go @@ -99,16 +99,14 @@ func (c *ConnectorAdapter) Upsert(space interface{}, } // Call calls registered Tarantool function. -// It uses request code for Tarantool >= 1.7 if go-tarantool -// was build with go_tarantool_call_17 tag. -// Otherwise, uses request code for Tarantool 1.6. +// It uses request code for Tarantool >= 1.7, result is an array. func (c *ConnectorAdapter) Call(functionName string, args interface{}) (*tarantool.Response, error) { return c.pool.Call(functionName, args, c.mode) } // Call16 calls registered Tarantool function. -// It uses request code for Tarantool 1.6, so result is converted to array of arrays +// It uses request code for Tarantool 1.6, result is an array of arrays. // Deprecated since Tarantool 1.7.2. func (c *ConnectorAdapter) Call16(functionName string, args interface{}) (*tarantool.Response, error) { @@ -116,8 +114,7 @@ func (c *ConnectorAdapter) Call16(functionName string, } // Call17 calls registered Tarantool function. -// It uses request code for Tarantool >= 1.7, so result is not converted -// (though, keep in mind, result is always array) +// It uses request code for Tarantool >= 1.7, result is an array. func (c *ConnectorAdapter) Call17(functionName string, args interface{}) (*tarantool.Response, error) { return c.pool.Call17(functionName, args, c.mode) @@ -174,16 +171,14 @@ func (c *ConnectorAdapter) UpdateTyped(space, index interface{}, } // CallTyped calls registered function. -// It uses request code for Tarantool >= 1.7 if go-tarantool -// was build with go_tarantool_call_17 tag. -// Otherwise, uses request code for Tarantool 1.6. +// It uses request code for Tarantool >= 1.7, result is an array. func (c *ConnectorAdapter) CallTyped(functionName string, args interface{}, result interface{}) error { return c.pool.CallTyped(functionName, args, result, c.mode) } // Call16Typed calls registered function. -// It uses request code for Tarantool 1.6, so result is converted to array of arrays +// It uses request code for Tarantool 1.6, result is an array of arrays. // Deprecated since Tarantool 1.7.2. func (c *ConnectorAdapter) Call16Typed(functionName string, args interface{}, result interface{}) error { @@ -191,8 +186,7 @@ func (c *ConnectorAdapter) Call16Typed(functionName string, } // Call17Typed calls registered function. -// It uses request code for Tarantool >= 1.7, so result is not converted -// (though, keep in mind, result is always array) +// It uses request code for Tarantool >= 1.7, result is an array. func (c *ConnectorAdapter) Call17Typed(functionName string, args interface{}, result interface{}) error { return c.pool.Call17Typed(functionName, args, result, c.mode) @@ -247,16 +241,14 @@ func (c *ConnectorAdapter) UpsertAsync(space interface{}, tuple interface{}, } // CallAsync sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool >= 1.7 if go-tarantool -// was build with go_tarantool_call_17 tag. -// Otherwise, uses request code for Tarantool 1.6. +// It uses request code for Tarantool >= 1.7, future's result is an array. func (c *ConnectorAdapter) CallAsync(functionName string, args interface{}) *tarantool.Future { return c.pool.CallAsync(functionName, args, c.mode) } // Call16Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool 1.6, so future's result is always array of arrays. +// It uses request code for Tarantool 1.6, so future's result is an array of arrays. // Deprecated since Tarantool 1.7.2. func (c *ConnectorAdapter) Call16Async(functionName string, args interface{}) *tarantool.Future { @@ -264,8 +256,7 @@ func (c *ConnectorAdapter) Call16Async(functionName string, } // Call17Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool >= 1.7, so future's result will not be converted -// (though, keep in mind, result is always array) +// It uses request code for Tarantool >= 1.7, future's result is an array. func (c *ConnectorAdapter) Call17Async(functionName string, args interface{}) *tarantool.Future { return c.pool.Call17Async(functionName, args, c.mode) diff --git a/request.go b/request.go index 5388c0e1a..3f539b2dd 100644 --- a/request.go +++ b/request.go @@ -218,9 +218,7 @@ func (conn *Connection) Upsert(space interface{}, tuple, ops interface{}) (resp } // Call calls registered Tarantool function. -// It uses request code for Tarantool >= 1.7 if go-tarantool -// was build with go_tarantool_call_17 tag. -// Otherwise, uses request code for Tarantool 1.6. +// It uses request code for Tarantool >= 1.7, result is an array. // // It is equal to conn.CallAsync(functionName, args).Get(). func (conn *Connection) Call(functionName string, args interface{}) (resp *Response, err error) { @@ -228,7 +226,7 @@ func (conn *Connection) Call(functionName string, args interface{}) (resp *Respo } // Call16 calls registered Tarantool function. -// It uses request code for Tarantool 1.6, so result is converted to array of arrays +// It uses request code for Tarantool 1.6, result is an array of arrays. // Deprecated since Tarantool 1.7.2. // // It is equal to conn.Call16Async(functionName, args).Get(). @@ -237,8 +235,7 @@ func (conn *Connection) Call16(functionName string, args interface{}) (resp *Res } // Call17 calls registered Tarantool function. -// It uses request code for Tarantool >= 1.7, so result is not converted -// (though, keep in mind, result is always array) +// It uses request code for Tarantool >= 1.7, result is an array. // // It is equal to conn.Call17Async(functionName, args).Get(). func (conn *Connection) Call17(functionName string, args interface{}) (resp *Response, err error) { @@ -329,9 +326,7 @@ func (conn *Connection) UpdateTyped(space, index interface{}, key, ops interface } // CallTyped calls registered function. -// It uses request code for Tarantool >= 1.7 if go-tarantool -// was build with go_tarantool_call_17 tag. -// Otherwise, uses request code for Tarantool 1.6. +// It uses request code for Tarantool >= 1.7, result is an array. // // It is equal to conn.Call16Async(functionName, args).GetTyped(&result). func (conn *Connection) CallTyped(functionName string, args interface{}, result interface{}) (err error) { @@ -339,7 +334,7 @@ func (conn *Connection) CallTyped(functionName string, args interface{}, result } // Call16Typed calls registered function. -// It uses request code for Tarantool 1.6, so result is converted to array of arrays +// It uses request code for Tarantool 1.6, result is an array of arrays. // Deprecated since Tarantool 1.7.2. // // It is equal to conn.Call16Async(functionName, args).GetTyped(&result). @@ -348,8 +343,7 @@ func (conn *Connection) Call16Typed(functionName string, args interface{}, resul } // Call17Typed calls registered function. -// It uses request code for Tarantool >= 1.7, so result is not converted -// (though, keep in mind, result is always array) +// It uses request code for Tarantool >= 1.7, result is an array. // // It is equal to conn.Call17Async(functionName, args).GetTyped(&result). func (conn *Connection) Call17Typed(functionName string, args interface{}, result interface{}) (err error) { @@ -422,16 +416,14 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops in } // CallAsync sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool >= 1.7 if go-tarantool -// was build with go_tarantool_call_17 tag. -// Otherwise, uses request code for Tarantool 1.6. +// It uses request code for Tarantool >= 1.7, so future's result is an array. func (conn *Connection) CallAsync(functionName string, args interface{}) *Future { req := NewCallRequest(functionName).Args(args) return conn.Do(req) } // Call16Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool 1.6, so future's result is always array of arrays. +// It uses request code for Tarantool 1.6, so future's result is an array of arrays. // Deprecated since Tarantool 1.7.2. func (conn *Connection) Call16Async(functionName string, args interface{}) *Future { req := NewCall16Request(functionName).Args(args) @@ -439,8 +431,7 @@ func (conn *Connection) Call16Async(functionName string, args interface{}) *Futu } // Call17Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool >= 1.7, so future's result will not be converted -// (though, keep in mind, result is always array) +// It uses request code for Tarantool >= 1.7, so future's result is an array. func (conn *Connection) Call17Async(functionName string, args interface{}) *Future { req := NewCall17Request(functionName).Args(args) return conn.Do(req) @@ -1115,9 +1106,8 @@ type CallRequest struct { args interface{} } -// NewCallRequest return a new empty CallRequest. It uses request code for -// Tarantool >= 1.7 if go-tarantool was build with go_tarantool_call_17 tag. -// Otherwise, uses request code for Tarantool 1.6. +// NewCallRequest returns a new empty CallRequest. It uses request code for +// Tarantool >= 1.7. func NewCallRequest(function string) *CallRequest { req := new(CallRequest) req.requestCode = CallRequestCode diff --git a/request_test.go b/request_test.go index d3f15c179..b9a3a656d 100644 --- a/request_test.go +++ b/request_test.go @@ -180,6 +180,8 @@ func TestRequestsCodes(t *testing.T) { {req: NewInsertRequest(validSpace), code: InsertRequestCode}, {req: NewReplaceRequest(validSpace), code: ReplaceRequestCode}, {req: NewDeleteRequest(validSpace), code: DeleteRequestCode}, + {req: NewCallRequest(validExpr), code: CallRequestCode}, + {req: NewCallRequest(validExpr), code: Call17RequestCode}, {req: NewCall16Request(validExpr), code: Call16RequestCode}, {req: NewCall17Request(validExpr), code: Call17RequestCode}, {req: NewEvalRequest(validExpr), code: EvalRequestCode}, @@ -213,6 +215,7 @@ func TestRequestsAsync(t *testing.T) { {req: NewInsertRequest(validSpace), async: false}, {req: NewReplaceRequest(validSpace), async: false}, {req: NewDeleteRequest(validSpace), async: false}, + {req: NewCallRequest(validExpr), async: false}, {req: NewCall16Request(validExpr), async: false}, {req: NewCall17Request(validExpr), async: false}, {req: NewEvalRequest(validExpr), async: false}, @@ -246,6 +249,7 @@ func TestRequestsCtx_default(t *testing.T) { {req: NewInsertRequest(validSpace), expected: nil}, {req: NewReplaceRequest(validSpace), expected: nil}, {req: NewDeleteRequest(validSpace), expected: nil}, + {req: NewCallRequest(validExpr), expected: nil}, {req: NewCall16Request(validExpr), expected: nil}, {req: NewCall17Request(validExpr), expected: nil}, {req: NewEvalRequest(validExpr), expected: nil}, @@ -280,6 +284,7 @@ func TestRequestsCtx_setter(t *testing.T) { {req: NewInsertRequest(validSpace).Context(ctx), expected: ctx}, {req: NewReplaceRequest(validSpace).Context(ctx), expected: ctx}, {req: NewDeleteRequest(validSpace).Context(ctx), expected: ctx}, + {req: NewCallRequest(validExpr).Context(ctx), expected: ctx}, {req: NewCall16Request(validExpr).Context(ctx), expected: ctx}, {req: NewCall17Request(validExpr).Context(ctx), expected: ctx}, {req: NewEvalRequest(validExpr).Context(ctx), expected: ctx}, @@ -598,7 +603,7 @@ func TestCallRequestsSetters(t *testing.T) { return } - req := NewCall16Request(validExpr). + req := NewCallRequest(validExpr). Args(args) req16 := NewCall16Request(validExpr). Args(args) diff --git a/tarantool_test.go b/tarantool_test.go index c663fa190..61870d27d 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2609,6 +2609,39 @@ func TestConnectionDoSelectRequest_pagination_pos(t *testing.T) { testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1012) } +func TestConnection_Call(t *testing.T) { + var resp *Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + resp, err = conn.Call("simple_concat", []interface{}{"1"}) + if err != nil { + t.Errorf("Failed to use Call") + } + if val, ok := resp.Data[0].(string); !ok || val != "11" { + t.Errorf("result is not {{1}} : %v", resp.Data) + } +} + +func TestCallRequest(t *testing.T) { + var resp *Response + var err error + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + req := NewCallRequest("simple_concat").Args([]interface{}{"1"}) + resp, err = conn.Do(req).Get() + if err != nil { + t.Errorf("Failed to use Call") + } + if val, ok := resp.Data[0].(string); !ok || val != "11" { + t.Errorf("result is not {{1}} : %v", resp.Data) + } +} + func TestClientRequestObjectsWithNilContext(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() From 30649e9e37e5ee3e162816787bf5082e68771eba Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 24 Mar 2023 13:12:57 +0300 Subject: [PATCH 452/605] api: make RoundRobinStrategy private Part of #158 --- CHANGELOG.md | 1 + pool/connection_pool.go | 12 ++++++------ pool/round_robin.go | 20 ++++++++++---------- pool/round_robin_test.go | 11 +++++------ 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 955e67809..4369e8e5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - multi subpackage (#240) - msgpack.v2 support (#236) +- pool/RoundRobinStrategy (#158) ### Fixed diff --git a/pool/connection_pool.go b/pool/connection_pool.go index fa4d6f732..e6d31575d 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -97,9 +97,9 @@ type ConnectionPool struct { state state done chan struct{} - roPool *RoundRobinStrategy - rwPool *RoundRobinStrategy - anyPool *RoundRobinStrategy + roPool *roundRobinStrategy + rwPool *roundRobinStrategy + anyPool *roundRobinStrategy poolsMutex sync.RWMutex watcherContainer watcherContainer } @@ -141,9 +141,9 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts Opts) (connPo } size := len(addrs) - rwPool := NewEmptyRoundRobin(size) - roPool := NewEmptyRoundRobin(size) - anyPool := NewEmptyRoundRobin(size) + rwPool := newRoundRobinStrategy(size) + roPool := newRoundRobinStrategy(size) + anyPool := newRoundRobinStrategy(size) connPool = &ConnectionPool{ addrs: make(map[string]*endpoint), diff --git a/pool/round_robin.go b/pool/round_robin.go index 6078b136e..1c50b97d9 100644 --- a/pool/round_robin.go +++ b/pool/round_robin.go @@ -6,7 +6,7 @@ import ( "github.com/tarantool/go-tarantool/v2" ) -type RoundRobinStrategy struct { +type roundRobinStrategy struct { conns []*tarantool.Connection indexByAddr map[string]uint mutex sync.RWMutex @@ -14,8 +14,8 @@ type RoundRobinStrategy struct { current uint } -func NewEmptyRoundRobin(size int) *RoundRobinStrategy { - return &RoundRobinStrategy{ +func newRoundRobinStrategy(size int) *roundRobinStrategy { + return &roundRobinStrategy{ conns: make([]*tarantool.Connection, 0, size), indexByAddr: make(map[string]uint), size: 0, @@ -23,7 +23,7 @@ func NewEmptyRoundRobin(size int) *RoundRobinStrategy { } } -func (r *RoundRobinStrategy) GetConnByAddr(addr string) *tarantool.Connection { +func (r *roundRobinStrategy) GetConnByAddr(addr string) *tarantool.Connection { r.mutex.RLock() defer r.mutex.RUnlock() @@ -35,7 +35,7 @@ func (r *RoundRobinStrategy) GetConnByAddr(addr string) *tarantool.Connection { return r.conns[index] } -func (r *RoundRobinStrategy) DeleteConnByAddr(addr string) *tarantool.Connection { +func (r *roundRobinStrategy) DeleteConnByAddr(addr string) *tarantool.Connection { r.mutex.Lock() defer r.mutex.Unlock() @@ -63,14 +63,14 @@ func (r *RoundRobinStrategy) DeleteConnByAddr(addr string) *tarantool.Connection return conn } -func (r *RoundRobinStrategy) IsEmpty() bool { +func (r *roundRobinStrategy) IsEmpty() bool { r.mutex.RLock() defer r.mutex.RUnlock() return r.size == 0 } -func (r *RoundRobinStrategy) GetNextConnection() *tarantool.Connection { +func (r *roundRobinStrategy) GetNextConnection() *tarantool.Connection { r.mutex.RLock() defer r.mutex.RUnlock() @@ -80,7 +80,7 @@ func (r *RoundRobinStrategy) GetNextConnection() *tarantool.Connection { return r.conns[r.nextIndex()] } -func (r *RoundRobinStrategy) GetConnections() []*tarantool.Connection { +func (r *roundRobinStrategy) GetConnections() []*tarantool.Connection { r.mutex.RLock() defer r.mutex.RUnlock() @@ -90,7 +90,7 @@ func (r *RoundRobinStrategy) GetConnections() []*tarantool.Connection { return ret } -func (r *RoundRobinStrategy) AddConn(addr string, conn *tarantool.Connection) { +func (r *roundRobinStrategy) AddConn(addr string, conn *tarantool.Connection) { r.mutex.Lock() defer r.mutex.Unlock() @@ -103,7 +103,7 @@ func (r *RoundRobinStrategy) AddConn(addr string, conn *tarantool.Connection) { } } -func (r *RoundRobinStrategy) nextIndex() uint { +func (r *roundRobinStrategy) nextIndex() uint { ret := r.current % r.size r.current++ return ret diff --git a/pool/round_robin_test.go b/pool/round_robin_test.go index abd7cdfb0..4aacb1a26 100644 --- a/pool/round_robin_test.go +++ b/pool/round_robin_test.go @@ -1,10 +1,9 @@ -package pool_test +package pool import ( "testing" "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/pool" ) const ( @@ -13,7 +12,7 @@ const ( ) func TestRoundRobinAddDelete(t *testing.T) { - rr := NewEmptyRoundRobin(10) + rr := newRoundRobinStrategy(10) addrs := []string{validAddr1, validAddr2} conns := []*tarantool.Connection{&tarantool.Connection{}, &tarantool.Connection{}} @@ -33,7 +32,7 @@ func TestRoundRobinAddDelete(t *testing.T) { } func TestRoundRobinAddDuplicateDelete(t *testing.T) { - rr := NewEmptyRoundRobin(10) + rr := newRoundRobinStrategy(10) conn1 := &tarantool.Connection{} conn2 := &tarantool.Connection{} @@ -53,7 +52,7 @@ func TestRoundRobinAddDuplicateDelete(t *testing.T) { } func TestRoundRobinGetNextConnection(t *testing.T) { - rr := NewEmptyRoundRobin(10) + rr := newRoundRobinStrategy(10) addrs := []string{validAddr1, validAddr2} conns := []*tarantool.Connection{&tarantool.Connection{}, &tarantool.Connection{}} @@ -71,7 +70,7 @@ func TestRoundRobinGetNextConnection(t *testing.T) { } func TestRoundRobinStrategy_GetConnections(t *testing.T) { - rr := NewEmptyRoundRobin(10) + rr := newRoundRobinStrategy(10) addrs := []string{validAddr1, validAddr2} conns := []*tarantool.Connection{&tarantool.Connection{}, &tarantool.Connection{}} From e4ac45276fd42252b3d78cdfed56c156fd6b3a63 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 24 Mar 2023 13:17:01 +0300 Subject: [PATCH 453/605] api: make DeadlineIO private Part of #158 --- CHANGELOG.md | 1 + deadline_io.go | 6 +++--- dial.go | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4369e8e5f..c0d9f32ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - multi subpackage (#240) - msgpack.v2 support (#236) - pool/RoundRobinStrategy (#158) +- DeadlineIO (#158) ### Fixed diff --git a/deadline_io.go b/deadline_io.go index 3bda73ac8..da547f5e2 100644 --- a/deadline_io.go +++ b/deadline_io.go @@ -5,12 +5,12 @@ import ( "time" ) -type DeadlineIO struct { +type deadlineIO struct { to time.Duration c net.Conn } -func (d *DeadlineIO) Write(b []byte) (n int, err error) { +func (d *deadlineIO) Write(b []byte) (n int, err error) { if d.to > 0 { d.c.SetWriteDeadline(time.Now().Add(d.to)) } @@ -18,7 +18,7 @@ func (d *DeadlineIO) Write(b []byte) (n int, err error) { return } -func (d *DeadlineIO) Read(b []byte) (n int, err error) { +func (d *deadlineIO) Read(b []byte) (n int, err error) { if d.to > 0 { d.c.SetReadDeadline(time.Now().Add(d.to)) } diff --git a/dial.go b/dial.go index 3baf25986..4c9e9dd99 100644 --- a/dial.go +++ b/dial.go @@ -111,7 +111,7 @@ func (t TtDialer) Dial(address string, opts DialOpts) (Conn, error) { return nil, fmt.Errorf("failed to dial: %w", err) } - dc := &DeadlineIO{to: opts.IoTimeout, c: conn.net} + dc := &deadlineIO{to: opts.IoTimeout, c: conn.net} conn.reader = bufio.NewReaderSize(dc, 128*1024) conn.writer = bufio.NewWriterSize(dc, 128*1024) From 9d9e2a0efdef2fad80c6afb50cd53d05bd8d64e8 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 24 Mar 2023 13:19:12 +0300 Subject: [PATCH 454/605] api: make UUID_extId private Part of #158 --- CHANGELOG.md | 1 + uuid/uuid.go | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0d9f32ae..381c4010d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - msgpack.v2 support (#236) - pool/RoundRobinStrategy (#158) - DeadlineIO (#158) +- UUID_extId (#158) ### Fixed diff --git a/uuid/uuid.go b/uuid/uuid.go index 262335646..eadd42ae1 100644 --- a/uuid/uuid.go +++ b/uuid/uuid.go @@ -22,7 +22,7 @@ import ( ) // UUID external type. -const UUID_extId = 2 +const uuid_extID = 2 func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { id := v.Interface().(uuid.UUID) @@ -63,12 +63,12 @@ func decodeUUID(d *msgpack.Decoder, v reflect.Value) error { func init() { msgpack.Register(reflect.TypeOf((*uuid.UUID)(nil)).Elem(), encodeUUID, decodeUUID) - msgpack.RegisterExtEncoder(UUID_extId, uuid.UUID{}, + msgpack.RegisterExtEncoder(uuid_extID, uuid.UUID{}, func(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { uuid := v.Interface().(uuid.UUID) return uuid.MarshalBinary() }) - msgpack.RegisterExtDecoder(UUID_extId, uuid.UUID{}, + msgpack.RegisterExtDecoder(uuid_extID, uuid.UUID{}, func(d *msgpack.Decoder, v reflect.Value, extLen int) error { return decodeUUID(d, v) }) From cf992ebbe65b8782546eea602b39133f4f39e84a Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 24 Mar 2023 16:41:18 +0300 Subject: [PATCH 455/605] doc: add comments for Field and IndexField Part of #158 --- schema.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/schema.go b/schema.go index 9e9f0ac17..0617924eb 100644 --- a/schema.go +++ b/schema.go @@ -151,6 +151,7 @@ func (space *Space) DecodeMsgpack(d *msgpack.Decoder) error { return nil } +// Field is a schema field. type Field struct { Id uint32 Name string @@ -266,6 +267,7 @@ func (index *Index) DecodeMsgpack(d *msgpack.Decoder) error { return nil } +// IndexFields is an index field. type IndexField struct { Id uint32 Type string From 5ce7209856a5db24b4335dcf2139d1bc054bd928 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 24 Mar 2023 13:46:52 +0300 Subject: [PATCH 456/605] api: remove IPROTO constants IPROTO constants have been moved to a separate package go-iproto [1]. 1. https://github.com/tarantool/go-iproto Part of #158 --- CHANGELOG.md | 4 ++ README.md | 10 +++ connection.go | 21 +++--- const.go | 103 ++++++----------------------- crud/common.go | 8 ++- crud/request_test.go | 57 ++++++++-------- crud/tarantool_test.go | 6 +- dial.go | 5 +- errors.go | 123 ++--------------------------------- go.mod | 1 + go.sum | 2 + prepared.go | 15 +++-- protocol.go | 7 +- request.go | 93 +++++++++++++------------- request_test.go | 53 +++++++-------- response.go | 73 +++++++++++---------- settings/request.go | 13 ++-- settings/request_test.go | 47 ++++++------- stream.go | 11 ++-- tarantool_test.go | 12 ++-- test_helpers/request_mock.go | 5 +- watch.go | 13 ++-- 22 files changed, 271 insertions(+), 411 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 381c4010d..3a7c31945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +- Type() method to the Request interface (#158) + ### Changed - connection_pool renamed to pool (#239) @@ -23,6 +25,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - pool/RoundRobinStrategy (#158) - DeadlineIO (#158) - UUID_extId (#158) +- IPROTO constants (#158) +- Code() method from the Request interface (#158) ### Fixed diff --git a/README.md b/README.md index d51a11cdd..8d67db1b2 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ faster than other packages according to public benchmarks. * [pool package](#pool-package) * [msgpack.v5](#msgpackv5) * [Call = Call17](#call--call17) + * [IPROTO constants](#iproto-constants) + * [Request interface](#request-interface) * [Contributing](#contributing) * [Alternative connectors](#alternative-connectors) @@ -190,6 +192,14 @@ Call requests uses `IPROTO_CALL` instead of `IPROTO_CALL_16`. So now `Call` = `Call17` and `NewCallRequest` = `NewCall17Request`. A result of the requests is an array instead of array of arrays. +#### IPROTO constants + +IPROTO constants have been moved to a separate package [go-iproto](https://github.com/tarantool/go-iproto). + +#### Request interface + +* The method `Code() uint32` replaced by the `Type() iproto.Type`. + ## Contributing See [the contributing guide](CONTRIBUTING.md) for detailed instructions on how diff --git a/connection.go b/connection.go index a79657072..46110d766 100644 --- a/connection.go +++ b/connection.go @@ -15,6 +15,7 @@ import ( "sync/atomic" "time" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" ) @@ -167,7 +168,7 @@ type Connection struct { opts Opts state uint32 dec *msgpack.Decoder - lenbuf [PacketLengthBytes]byte + lenbuf [packetLengthBytes]byte lastStreamId uint64 @@ -410,8 +411,8 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { ter, ok := err.(Error) if conn.opts.Reconnect <= 0 { return nil, err - } else if ok && (ter.Code == ErrNoSuchUser || - ter.Code == ErrPasswordMismatch) { + } else if ok && (ter.Code == iproto.ER_NO_SUCH_USER || + ter.Code == iproto.ER_CREDS_MISMATCH) { // Reported auth errors immediately. return nil, err } else { @@ -595,7 +596,7 @@ func pack(h *smallWBuf, enc *msgpack.Encoder, reqid uint32, hMapLen := byte(0x82) // 2 element map. if streamId != ignoreStreamId { hMapLen = byte(0x83) // 3 element map. - streamBytes[0] = KeyStreamId + streamBytes[0] = byte(iproto.IPROTO_STREAM_ID) if streamId > math.MaxUint32 { streamBytesLen = streamBytesLenUint64 streamBytes[1] = uint64Code @@ -610,8 +611,8 @@ func pack(h *smallWBuf, enc *msgpack.Encoder, reqid uint32, hBytes := append([]byte{ uint32Code, 0, 0, 0, 0, // Length. hMapLen, - KeyCode, byte(req.Code()), // Request code. - KeySync, uint32Code, + byte(iproto.IPROTO_REQUEST_TYPE), byte(req.Type()), // Request type. + byte(iproto.IPROTO_SYNC), uint32Code, byte(reqid >> 24), byte(reqid >> 16), byte(reqid >> 8), byte(reqid), }, streamBytes[:streamBytesLen]...) @@ -813,13 +814,13 @@ func readWatchEvent(reader io.Reader) (connWatchEvent, error) { return event, err } - switch cd { - case KeyEvent: + switch iproto.Key(cd) { + case iproto.IPROTO_EVENT_KEY: if event.key, err = d.DecodeString(); err != nil { return event, err } keyExist = true - case KeyEventData: + case iproto.IPROTO_EVENT_DATA: if event.value, err = d.DecodeInterface(); err != nil { return event, err } @@ -857,7 +858,7 @@ func (conn *Connection) reader(r io.Reader, c Conn) { } var fut *Future = nil - if resp.Code == EventCode { + if iproto.Type(resp.Code) == iproto.IPROTO_EVENT { if event, err := readWatchEvent(&resp.buf); err == nil { events <- event } else { diff --git a/const.go b/const.go index 4580e4cb0..4d9e94a9d 100644 --- a/const.go +++ b/const.go @@ -1,95 +1,30 @@ package tarantool -const ( - SelectRequestCode = 1 - InsertRequestCode = 2 - ReplaceRequestCode = 3 - UpdateRequestCode = 4 - DeleteRequestCode = 5 - Call16RequestCode = 6 /* call in 1.6 format */ - AuthRequestCode = 7 - EvalRequestCode = 8 - UpsertRequestCode = 9 - Call17RequestCode = 10 /* call in >= 1.7 format */ - ExecuteRequestCode = 11 - PrepareRequestCode = 13 - BeginRequestCode = 14 - CommitRequestCode = 15 - RollbackRequestCode = 16 - PingRequestCode = 64 - SubscribeRequestCode = 66 - IdRequestCode = 73 - WatchRequestCode = 74 - UnwatchRequestCode = 75 - CallRequestCode = Call17RequestCode - - KeyCode = 0x00 - KeySync = 0x01 - KeyStreamId = 0x0a - KeySpaceNo = 0x10 - KeyIndexNo = 0x11 - KeyLimit = 0x12 - KeyOffset = 0x13 - KeyIterator = 0x14 - KeyFetchPos = 0x1f - KeyKey = 0x20 - KeyTuple = 0x21 - KeyFunctionName = 0x22 - KeyUserName = 0x23 - KeyExpression = 0x27 - KeyAfterPos = 0x2e - KeyAfterTuple = 0x2f - KeyDefTuple = 0x28 - KeyData = 0x30 - KeyError24 = 0x31 /* Error in pre-2.4 format. */ - KeyMetaData = 0x32 - KeyBindCount = 0x34 - KeyPos = 0x35 - KeySQLText = 0x40 - KeySQLBind = 0x41 - KeySQLInfo = 0x42 - KeyStmtID = 0x43 - KeyError = 0x52 /* Extended error in >= 2.4 format. */ - KeyVersion = 0x54 - KeyFeatures = 0x55 - KeyTimeout = 0x56 - KeyEvent = 0x57 - KeyEventData = 0x58 - KeyTxnIsolation = 0x59 - KeyAuthType = 0x5b - - KeyFieldName = 0x00 - KeyFieldType = 0x01 - KeyFieldColl = 0x02 - KeyFieldIsNullable = 0x03 - KeyIsAutoincrement = 0x04 - KeyFieldSpan = 0x05 - KeySQLInfoRowCount = 0x00 - KeySQLInfoAutoincrementIds = 0x01 +import ( + "github.com/tarantool/go-iproto" +) - // https://github.com/fl00r/go-tarantool-1.6/issues/2 +const ( + packetLengthBytes = 5 +) - IterEq = uint32(0) // key == x ASC order - IterReq = uint32(1) // key == x DESC order - IterAll = uint32(2) // all tuples - IterLt = uint32(3) // key < x - IterLe = uint32(4) // key <= x - IterGe = uint32(5) // key >= x - IterGt = uint32(6) // key > x - IterBitsAllSet = uint32(7) // all bits from x are set in key - IterBitsAnySet = uint32(8) // at least one x's bit is set - IterBitsAllNotSet = uint32(9) // all bits are not set +const ( + IterEq = uint32(0) // key == x ASC order + IterReq = uint32(1) // key == x DESC order + IterAll = uint32(2) // all tuples + IterLt = uint32(3) // key < x + IterLe = uint32(4) // key <= x + IterGe = uint32(5) // key >= x + IterGt = uint32(6) // key > x + IterBitsAllSet = uint32(7) // all bits from x are set in key + IterBitsAnySet = uint32(8) // at least one x's bit is set + IterBitsAllNotSet = uint32(9) // all bits are not set IterOverlaps = uint32(10) // key overlaps x IterNeighbor = uint32(11) // tuples in distance ascending order from specified point RLimitDrop = 1 RLimitWait = 2 - OkCode = uint32(0) - EventCode = uint32(0x4c) - PushCode = uint32(0x80) - ErrorCodeBit = 0x8000 - PacketLengthBytes = 5 - ErSpaceExistsCode = 0xa - IteratorCode = 0x14 + OkCode = uint32(iproto.IPROTO_OK) + PushCode = uint32(iproto.IPROTO_CHUNK) ) diff --git a/crud/common.go b/crud/common.go index bb882fb8b..7a6f9e03b 100644 --- a/crud/common.go +++ b/crud/common.go @@ -56,6 +56,8 @@ package crud import ( "context" + "github.com/tarantool/go-iproto" + "github.com/tarantool/go-tarantool/v2" ) @@ -67,9 +69,9 @@ func newCall(method string) *tarantool.CallRequest { return tarantool.NewCall17Request(method) } -// Code returns IPROTO code for CRUD request. -func (req baseRequest) Code() int32 { - return req.impl.Code() +// Type returns IPROTO type for CRUD request. +func (req baseRequest) Type() iproto.Type { + return req.impl.Type() } // Ctx returns a context of CRUD request. diff --git a/crud/request_test.go b/crud/request_test.go index 5a81e0d1e..d36d8a264 100644 --- a/crud/request_test.go +++ b/crud/request_test.go @@ -7,6 +7,7 @@ import ( "fmt" "testing" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" "github.com/tarantool/go-tarantool/v2" @@ -22,7 +23,7 @@ const validSpace = "test" // Any valid value != default. const defaultSpace = 0 // And valid too. const defaultIndex = 0 // And valid too. -const CrudRequestCode = tarantool.Call17RequestCode +const CrudRequestType = iproto.IPROTO_CALL var reqObject = crud.MapObject{ "id": uint(24), @@ -166,37 +167,37 @@ func BenchmarkSelectRequest(b *testing.B) { func TestRequestsCodes(t *testing.T) { tests := []struct { - req tarantool.Request - code int32 + req tarantool.Request + rtype iproto.Type }{ - {req: crud.MakeInsertRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeInsertObjectRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeInsertManyRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeInsertObjectManyRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeGetRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeUpdateRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeDeleteRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeReplaceRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeReplaceObjectRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeReplaceManyRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeReplaceObjectManyRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeUpsertRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeUpsertObjectRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeUpsertManyRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeUpsertObjectManyRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeMinRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeMaxRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeSelectRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeTruncateRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeLenRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeCountRequest(validSpace), code: CrudRequestCode}, - {req: crud.MakeStorageInfoRequest(), code: CrudRequestCode}, - {req: crud.MakeStatsRequest(), code: CrudRequestCode}, + {req: crud.MakeInsertRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeInsertObjectRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeInsertManyRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeInsertObjectManyRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeGetRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeUpdateRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeDeleteRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeReplaceRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeReplaceObjectRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeReplaceManyRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeReplaceObjectManyRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeUpsertRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeUpsertObjectRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeUpsertManyRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeUpsertObjectManyRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeMinRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeMaxRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeSelectRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeTruncateRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeLenRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeCountRequest(validSpace), rtype: CrudRequestType}, + {req: crud.MakeStorageInfoRequest(), rtype: CrudRequestType}, + {req: crud.MakeStatsRequest(), rtype: CrudRequestType}, } for _, test := range tests { - if code := test.req.Code(); code != test.code { - t.Errorf("An invalid request code 0x%x, expected 0x%x", code, test.code) + if rtype := test.req.Type(); rtype != test.rtype { + t.Errorf("An invalid request type 0x%x, expected 0x%x", rtype, test.rtype) } } } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index addddf27a..837fa2787 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -8,6 +8,8 @@ import ( "time" "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" + "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/crud" "github.com/tarantool/go-tarantool/v2/test_helpers" @@ -399,9 +401,9 @@ func getCrudError(req tarantool.Request, crudError interface{}) (interface{}, er var err []interface{} var ok bool - code := req.Code() + rtype := req.Type() if crudError != nil { - if code == tarantool.Call17RequestCode { + if rtype == iproto.IPROTO_CALL { return crudError, nil } diff --git a/dial.go b/dial.go index 4c9e9dd99..3ba493ac7 100644 --- a/dial.go +++ b/dial.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" ) @@ -261,7 +262,7 @@ func identify(w writeFlusher, r io.Reader) (ProtocolInfo, error) { resp, err := readResponse(r) if err != nil { - if resp.Code == ErrUnknownRequestType { + if iproto.Error(resp.Code) == iproto.ER_UNKNOWN_REQUEST_TYPE { // IPROTO_ID requests are not supported by server. return info, nil } @@ -368,7 +369,7 @@ func writeRequest(w writeFlusher, req Request) error { // readResponse reads a response from the reader. func readResponse(r io.Reader) (Response, error) { - var lenbuf [PacketLengthBytes]byte + var lenbuf [packetLengthBytes]byte respBytes, err := read(r, lenbuf[:]) if err != nil { diff --git a/errors.go b/errors.go index 906184c24..ab40e4f63 100644 --- a/errors.go +++ b/errors.go @@ -1,10 +1,14 @@ package tarantool -import "fmt" +import ( + "fmt" + + "github.com/tarantool/go-iproto" +) // Error is wrapper around error returned by Tarantool. type Error struct { - Code uint32 + Code iproto.Error Msg string ExtendedInfo *BoxError } @@ -57,118 +61,3 @@ const ( ErrRateLimited = 0x4000 + iota ErrConnectionShutdown = 0x4000 + iota ) - -// Tarantool server error codes. -const ( - ErrUnknown = 0 // Unknown error - ErrIllegalParams = 1 // Illegal parameters, %s - ErrMemoryIssue = 2 // Failed to allocate %u bytes in %s for %s - ErrTupleFound = 3 // Duplicate key exists in unique index '%s' in space '%s' - ErrTupleNotFound = 4 // Tuple doesn't exist in index '%s' in space '%s' - ErrUnsupported = 5 // %s does not support %s - ErrNonmaster = 6 // Can't modify data on a replication slave. My master is: %s - ErrReadonly = 7 // Can't modify data because this server is in read-only mode. - ErrInjection = 8 // Error injection '%s' - ErrCreateSpace = 9 // Failed to create space '%s': %s - ErrSpaceExists = 10 // Space '%s' already exists - ErrDropSpace = 11 // Can't drop space '%s': %s - ErrAlterSpace = 12 // Can't modify space '%s': %s - ErrIndexType = 13 // Unsupported index type supplied for index '%s' in space '%s' - ErrModifyIndex = 14 // Can't create or modify index '%s' in space '%s': %s - ErrLastDrop = 15 // Can't drop the primary key in a system space, space '%s' - ErrTupleFormatLimit = 16 // Tuple format limit reached: %u - ErrDropPrimaryKey = 17 // Can't drop primary key in space '%s' while secondary keys exist - ErrKeyPartType = 18 // Supplied key type of part %u does not match index part type: expected %s - ErrExactMatch = 19 // Invalid key part count in an exact match (expected %u, got %u) - ErrInvalidMsgpack = 20 // Invalid MsgPack - %s - ErrProcRet = 21 // msgpack.encode: can not encode Lua type '%s' - ErrTupleNotArray = 22 // Tuple/Key must be MsgPack array - ErrFieldType = 23 // Tuple field %u type does not match one required by operation: expected %s - ErrFieldTypeMismatch = 24 // Ambiguous field type in index '%s', key part %u. Requested type is %s but the field has previously been defined as %s - ErrSplice = 25 // SPLICE error on field %u: %s - ErrArgType = 26 // Argument type in operation '%c' on field %u does not match field type: expected a %s - ErrTupleIsTooLong = 27 // Tuple is too long %u - ErrUnknownUpdateOp = 28 // Unknown UPDATE operation - ErrUpdateField = 29 // Field %u UPDATE error: %s - ErrFiberStack = 30 // Can not create a new fiber: recursion limit reached - ErrKeyPartCount = 31 // Invalid key part count (expected [0..%u], got %u) - ErrProcLua = 32 // %s - ErrNoSuchProc = 33 // Procedure '%.*s' is not defined - ErrNoSuchTrigger = 34 // Trigger is not found - ErrNoSuchIndex = 35 // No index #%u is defined in space '%s' - ErrNoSuchSpace = 36 // Space '%s' does not exist - ErrNoSuchField = 37 // Field %d was not found in the tuple - ErrSpaceFieldCount = 38 // Tuple field count %u does not match space '%s' field count %u - ErrIndexFieldCount = 39 // Tuple field count %u is less than required by a defined index (expected %u) - ErrWalIo = 40 // Failed to write to disk - ErrMoreThanOneTuple = 41 // More than one tuple found by get() - ErrAccessDenied = 42 // %s access denied for user '%s' - ErrCreateUser = 43 // Failed to create user '%s': %s - ErrDropUser = 44 // Failed to drop user '%s': %s - ErrNoSuchUser = 45 // User '%s' is not found - ErrUserExists = 46 // User '%s' already exists - ErrPasswordMismatch = 47 // Incorrect password supplied for user '%s' - ErrUnknownRequestType = 48 // Unknown request type %u - ErrUnknownSchemaObject = 49 // Unknown object type '%s' - ErrCreateFunction = 50 // Failed to create function '%s': %s - ErrNoSuchFunction = 51 // Function '%s' does not exist - ErrFunctionExists = 52 // Function '%s' already exists - ErrFunctionAccessDenied = 53 // %s access denied for user '%s' to function '%s' - ErrFunctionMax = 54 // A limit on the total number of functions has been reached: %u - ErrSpaceAccessDenied = 55 // %s access denied for user '%s' to space '%s' - ErrUserMax = 56 // A limit on the total number of users has been reached: %u - ErrNoSuchEngine = 57 // Space engine '%s' does not exist - ErrReloadCfg = 58 // Can't set option '%s' dynamically - ErrCfg = 59 // Incorrect value for option '%s': %s - ErrSophia = 60 // %s - ErrLocalServerIsNotActive = 61 // Local server is not active - ErrUnknownServer = 62 // Server %s is not registered with the cluster - ErrClusterIdMismatch = 63 // Cluster id of the replica %s doesn't match cluster id of the master %s - ErrInvalidUUID = 64 // Invalid UUID: %s - ErrClusterIdIsRo = 65 // Can't reset cluster id: it is already assigned - ErrReserved66 = 66 // Reserved66 - ErrServerIdIsReserved = 67 // Can't initialize server id with a reserved value %u - ErrInvalidOrder = 68 // Invalid LSN order for server %u: previous LSN = %llu, new lsn = %llu - ErrMissingRequestField = 69 // Missing mandatory field '%s' in request - ErrIdentifier = 70 // Invalid identifier '%s' (expected letters, digits or an underscore) - ErrDropFunction = 71 // Can't drop function %u: %s - ErrIteratorType = 72 // Unknown iterator type '%s' - ErrReplicaMax = 73 // Replica count limit reached: %u - ErrInvalidXlog = 74 // Failed to read xlog: %lld - ErrInvalidXlogName = 75 // Invalid xlog name: expected %lld got %lld - ErrInvalidXlogOrder = 76 // Invalid xlog order: %lld and %lld - ErrNoConnection = 77 // Connection is not established - ErrTimeout = 78 // Timeout exceeded - ErrActiveTransaction = 79 // Operation is not permitted when there is an active transaction - ErrNoActiveTransaction = 80 // Operation is not permitted when there is no active transaction - ErrCrossEngineTransaction = 81 // A multi-statement transaction can not use multiple storage engines - ErrNoSuchRole = 82 // Role '%s' is not found - ErrRoleExists = 83 // Role '%s' already exists - ErrCreateRole = 84 // Failed to create role '%s': %s - ErrIndexExists = 85 // Index '%s' already exists - ErrTupleRefOverflow = 86 // Tuple reference counter overflow - ErrRoleLoop = 87 // Granting role '%s' to role '%s' would create a loop - ErrGrant = 88 // Incorrect grant arguments: %s - ErrPrivGranted = 89 // User '%s' already has %s access on %s '%s' - ErrRoleGranted = 90 // User '%s' already has role '%s' - ErrPrivNotGranted = 91 // User '%s' does not have %s access on %s '%s' - ErrRoleNotGranted = 92 // User '%s' does not have role '%s' - ErrMissingSnapshot = 93 // Can't find snapshot - ErrCantUpdatePrimaryKey = 94 // Attempt to modify a tuple field which is part of index '%s' in space '%s' - ErrUpdateIntegerOverflow = 95 // Integer overflow when performing '%c' operation on field %u - ErrGuestUserPassword = 96 // Setting password for guest user has no effect - ErrTransactionConflict = 97 // Transaction has been aborted by conflict - ErrUnsupportedRolePriv = 98 // Unsupported role privilege '%s' - ErrLoadFunction = 99 // Failed to dynamically load function '%s': %s - ErrFunctionLanguage = 100 // Unsupported language '%s' specified for function '%s' - ErrRtreeRect = 101 // RTree: %s must be an array with %u (point) or %u (rectangle/box) numeric coordinates - ErrProcC = 102 // ??? - ErrUnknownRtreeIndexDistanceType = 103 //Unknown RTREE index distance type %s - ErrProtocol = 104 // %s - ErrUpsertUniqueSecondaryKey = 105 // Space %s has a unique secondary index and does not support UPSERT - ErrWrongIndexRecord = 106 // Wrong record in _index space: got {%s}, expected {%s} - ErrWrongIndexParts = 107 // Wrong index parts (field %u): %s; expected field1 id (number), field1 type (string), ... - ErrWrongIndexOptions = 108 // Wrong index options (field %u): %s - ErrWrongSchemaVaersion = 109 // Wrong schema version, current: %d, in request: %u - ErrSlabAllocMax = 110 // Failed to allocate %u bytes for tuple in the slab allocator: tuple is too large. Check 'slab_alloc_maximal' configuration option. -) diff --git a/go.mod b/go.mod index 9e179e327..44bc63483 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.7.1 + github.com/tarantool/go-iproto v0.1.0 github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 github.com/vmihailenco/msgpack/v5 v5.3.5 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect diff --git a/go.sum b/go.sum index 95fc38125..d38645c63 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tarantool/go-iproto v0.1.0 h1:zHN9AA8LDawT+JBD0/Nxgr/bIsWkkpDzpcMuaNPSIAQ= +github.com/tarantool/go-iproto v0.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 h1:/AN3eUPsTlvF6W+Ng/8ZjnSU6o7L0H4Wb9GMks6RkzU= github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= diff --git a/prepared.go b/prepared.go index 4e3f494ee..8f2d95519 100644 --- a/prepared.go +++ b/prepared.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" ) @@ -22,21 +23,21 @@ type Prepared struct { func fillPrepare(enc *msgpack.Encoder, expr string) error { enc.EncodeMapLen(1) - enc.EncodeUint(KeySQLText) + enc.EncodeUint(uint64(iproto.IPROTO_SQL_TEXT)) return enc.EncodeString(expr) } func fillUnprepare(enc *msgpack.Encoder, stmt Prepared) error { enc.EncodeMapLen(1) - enc.EncodeUint(KeyStmtID) + enc.EncodeUint(uint64(iproto.IPROTO_STMT_ID)) return enc.EncodeUint(uint64(stmt.StatementID)) } func fillExecutePrepared(enc *msgpack.Encoder, stmt Prepared, args interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint(KeyStmtID) + enc.EncodeUint(uint64(iproto.IPROTO_STMT_ID)) enc.EncodeUint(uint64(stmt.StatementID)) - enc.EncodeUint(KeySQLBind) + enc.EncodeUint(uint64(iproto.IPROTO_SQL_BIND)) return encodeSQLBind(enc, args) } @@ -69,7 +70,7 @@ type PrepareRequest struct { // NewPrepareRequest returns a new empty PrepareRequest. func NewPrepareRequest(expr string) *PrepareRequest { req := new(PrepareRequest) - req.requestCode = PrepareRequestCode + req.rtype = iproto.IPROTO_PREPARE req.expr = expr return req } @@ -100,7 +101,7 @@ type UnprepareRequest struct { // NewUnprepareRequest returns a new empty UnprepareRequest. func NewUnprepareRequest(stmt *Prepared) *UnprepareRequest { req := new(UnprepareRequest) - req.requestCode = PrepareRequestCode + req.rtype = iproto.IPROTO_PREPARE req.stmt = stmt return req } @@ -137,7 +138,7 @@ type ExecutePreparedRequest struct { // NewExecutePreparedRequest returns a new empty preparedExecuteRequest. func NewExecutePreparedRequest(stmt *Prepared) *ExecutePreparedRequest { req := new(ExecutePreparedRequest) - req.requestCode = ExecuteRequestCode + req.rtype = iproto.IPROTO_EXECUTE req.stmt = stmt req.args = []interface{}{} return req diff --git a/protocol.go b/protocol.go index 6ddfd8d48..63424283b 100644 --- a/protocol.go +++ b/protocol.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" ) @@ -106,12 +107,12 @@ type IdRequest struct { func fillId(enc *msgpack.Encoder, protocolInfo ProtocolInfo) error { enc.EncodeMapLen(2) - enc.EncodeUint(KeyVersion) + enc.EncodeUint(uint64(iproto.IPROTO_VERSION)) if err := enc.Encode(protocolInfo.Version); err != nil { return err } - enc.EncodeUint(KeyFeatures) + enc.EncodeUint(uint64(iproto.IPROTO_FEATURES)) t := len(protocolInfo.Features) if err := enc.EncodeArrayLen(t); err != nil { @@ -130,7 +131,7 @@ func fillId(enc *msgpack.Encoder, protocolInfo ProtocolInfo) error { // NewIdRequest returns a new IdRequest. func NewIdRequest(protocolInfo ProtocolInfo) *IdRequest { req := new(IdRequest) - req.requestCode = IdRequestCode + req.rtype = iproto.IPROTO_ID req.protocolInfo = protocolInfo.Clone() return req } diff --git a/request.go b/request.go index 3f539b2dd..a86be1c6d 100644 --- a/request.go +++ b/request.go @@ -8,42 +8,43 @@ import ( "strings" "sync" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" ) func fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { - if err := enc.EncodeUint(KeySpaceNo); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_SPACE_ID)); err != nil { return err } if err := enc.EncodeUint(uint64(spaceNo)); err != nil { return err } - if err := enc.EncodeUint(KeyIndexNo); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_INDEX_ID)); err != nil { return err } if err := enc.EncodeUint(uint64(indexNo)); err != nil { return err } - if err := enc.EncodeUint(KeyKey); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_KEY)); err != nil { return err } return enc.Encode(key) } func fillIterator(enc *msgpack.Encoder, offset, limit, iterator uint32) error { - if err := enc.EncodeUint(KeyIterator); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_ITERATOR)); err != nil { return err } if err := enc.EncodeUint(uint64(iterator)); err != nil { return err } - if err := enc.EncodeUint(KeyOffset); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_OFFSET)); err != nil { return err } if err := enc.EncodeUint(uint64(offset)); err != nil { return err } - if err := enc.EncodeUint(KeyLimit); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_LIMIT)); err != nil { return err } return enc.EncodeUint(uint64(limit)) @@ -53,13 +54,13 @@ func fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { if err := enc.EncodeMapLen(2); err != nil { return err } - if err := enc.EncodeUint(KeySpaceNo); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_SPACE_ID)); err != nil { return err } if err := enc.EncodeUint(uint64(spaceNo)); err != nil { return err } - if err := enc.EncodeUint(KeyTuple); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)); err != nil { return err } return enc.Encode(tuple) @@ -84,7 +85,7 @@ func fillSelect(enc *msgpack.Encoder, spaceNo, indexNo, offset, limit, iterator return err } if fetchPos { - if err := enc.EncodeUint(KeyFetchPos); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_FETCH_POSITION)); err != nil { return err } if err := enc.EncodeBool(fetchPos); err != nil { @@ -93,14 +94,14 @@ func fillSelect(enc *msgpack.Encoder, spaceNo, indexNo, offset, limit, iterator } if after != nil { if pos, ok := after.([]byte); ok { - if err := enc.EncodeUint(KeyAfterPos); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_AFTER_POSITION)); err != nil { return err } if err := enc.EncodeString(string(pos)); err != nil { return err } } else { - if err := enc.EncodeUint(KeyAfterTuple); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_AFTER_TUPLE)); err != nil { return err } if err := enc.Encode(after); err != nil { @@ -116,19 +117,19 @@ func fillUpdate(enc *msgpack.Encoder, spaceNo, indexNo uint32, key, ops interfac if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { return err } - enc.EncodeUint(KeyTuple) + enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)) return enc.Encode(ops) } func fillUpsert(enc *msgpack.Encoder, spaceNo uint32, tuple, ops interface{}) error { enc.EncodeMapLen(3) - enc.EncodeUint(KeySpaceNo) + enc.EncodeUint(uint64(iproto.IPROTO_SPACE_ID)) enc.EncodeUint(uint64(spaceNo)) - enc.EncodeUint(KeyTuple) + enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)) if err := enc.Encode(tuple); err != nil { return err } - enc.EncodeUint(KeyDefTuple) + enc.EncodeUint(uint64(iproto.IPROTO_OPS)) return enc.Encode(ops) } @@ -139,25 +140,25 @@ func fillDelete(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) func fillCall(enc *msgpack.Encoder, functionName string, args interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint(KeyFunctionName) + enc.EncodeUint(uint64(iproto.IPROTO_FUNCTION_NAME)) enc.EncodeString(functionName) - enc.EncodeUint(KeyTuple) + enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)) return enc.Encode(args) } func fillEval(enc *msgpack.Encoder, expr string, args interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint(KeyExpression) + enc.EncodeUint(uint64(iproto.IPROTO_EXPR)) enc.EncodeString(expr) - enc.EncodeUint(KeyTuple) + enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)) return enc.Encode(args) } func fillExecute(enc *msgpack.Encoder, expr string, args interface{}) error { enc.EncodeMapLen(2) - enc.EncodeUint(KeySQLText) + enc.EncodeUint(uint64(iproto.IPROTO_SQL_TEXT)) enc.EncodeString(expr) - enc.EncodeUint(KeySQLBind) + enc.EncodeUint(uint64(iproto.IPROTO_SQL_BIND)) return encodeSQLBind(enc, args) } @@ -594,8 +595,8 @@ func encodeSQLBind(enc *msgpack.Encoder, from interface{}) error { // Request is an interface that provides the necessary data to create a request // that will be sent to a tarantool instance. type Request interface { - // Code returns a IPROTO code for the request. - Code() int32 + // Type returns a IPROTO type of the request. + Type() iproto.Type // Body fills an msgpack.Encoder with a request body. Body(resolver SchemaResolver, enc *msgpack.Encoder) error // Ctx returns a context of the request. @@ -613,14 +614,14 @@ type ConnectedRequest interface { } type baseRequest struct { - requestCode int32 - async bool - ctx context.Context + rtype iproto.Type + async bool + ctx context.Context } -// Code returns a IPROTO code for the request. -func (req *baseRequest) Code() int32 { - return req.requestCode +// Type returns a IPROTO type for the request. +func (req *baseRequest) Type() iproto.Type { + return req.rtype } // Async returns true if the request does not require a response. @@ -682,9 +683,9 @@ func newPapSha256AuthRequest(user, password string) authRequest { } } -// Code returns a IPROTO code for the request. -func (req authRequest) Code() int32 { - return AuthRequestCode +// Type returns a IPROTO type for the request. +func (req authRequest) Type() iproto.Type { + return iproto.IPROTO_AUTH } // Async returns true if the request does not require a response. @@ -700,8 +701,8 @@ func (req authRequest) Ctx() context.Context { // Body fills an encoder with the auth request body. func (req authRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return enc.Encode(map[uint32]interface{}{ - KeyUserName: req.user, - KeyTuple: []interface{}{req.auth.String(), req.pass}, + uint32(iproto.IPROTO_USER_NAME): req.user, + uint32(iproto.IPROTO_TUPLE): []interface{}{req.auth.String(), req.pass}, }) } @@ -714,7 +715,7 @@ type PingRequest struct { // NewPingRequest returns a new PingRequest. func NewPingRequest() *PingRequest { req := new(PingRequest) - req.requestCode = PingRequestCode + req.rtype = iproto.IPROTO_PING return req } @@ -746,7 +747,7 @@ type SelectRequest struct { // NewSelectRequest returns a new empty SelectRequest. func NewSelectRequest(space interface{}) *SelectRequest { req := new(SelectRequest) - req.requestCode = SelectRequestCode + req.rtype = iproto.IPROTO_SELECT req.setSpace(space) req.isIteratorSet = false req.fetchPos = false @@ -852,7 +853,7 @@ type InsertRequest struct { // NewInsertRequest returns a new empty InsertRequest. func NewInsertRequest(space interface{}) *InsertRequest { req := new(InsertRequest) - req.requestCode = InsertRequestCode + req.rtype = iproto.IPROTO_INSERT req.setSpace(space) req.tuple = []interface{}{} return req @@ -896,7 +897,7 @@ type ReplaceRequest struct { // NewReplaceRequest returns a new empty ReplaceRequest. func NewReplaceRequest(space interface{}) *ReplaceRequest { req := new(ReplaceRequest) - req.requestCode = ReplaceRequestCode + req.rtype = iproto.IPROTO_REPLACE req.setSpace(space) req.tuple = []interface{}{} return req @@ -940,7 +941,7 @@ type DeleteRequest struct { // NewDeleteRequest returns a new empty DeleteRequest. func NewDeleteRequest(space interface{}) *DeleteRequest { req := new(DeleteRequest) - req.requestCode = DeleteRequestCode + req.rtype = iproto.IPROTO_DELETE req.setSpace(space) req.key = []interface{}{} return req @@ -992,7 +993,7 @@ type UpdateRequest struct { // NewUpdateRequest returns a new empty UpdateRequest. func NewUpdateRequest(space interface{}) *UpdateRequest { req := new(UpdateRequest) - req.requestCode = UpdateRequestCode + req.rtype = iproto.IPROTO_UPDATE req.setSpace(space) req.key = []interface{}{} req.ops = []interface{}{} @@ -1054,7 +1055,7 @@ type UpsertRequest struct { // NewUpsertRequest returns a new empty UpsertRequest. func NewUpsertRequest(space interface{}) *UpsertRequest { req := new(UpsertRequest) - req.requestCode = UpsertRequestCode + req.rtype = iproto.IPROTO_UPSERT req.setSpace(space) req.tuple = []interface{}{} req.ops = []interface{}{} @@ -1110,7 +1111,7 @@ type CallRequest struct { // Tarantool >= 1.7. func NewCallRequest(function string) *CallRequest { req := new(CallRequest) - req.requestCode = CallRequestCode + req.rtype = iproto.IPROTO_CALL req.function = function return req } @@ -1147,7 +1148,7 @@ func (req *CallRequest) Context(ctx context.Context) *CallRequest { // Deprecated since Tarantool 1.7.2. func NewCall16Request(function string) *CallRequest { req := NewCallRequest(function) - req.requestCode = Call16RequestCode + req.rtype = iproto.IPROTO_CALL_16 return req } @@ -1155,7 +1156,7 @@ func NewCall16Request(function string) *CallRequest { // Tarantool >= 1.7. func NewCall17Request(function string) *CallRequest { req := NewCallRequest(function) - req.requestCode = Call17RequestCode + req.rtype = iproto.IPROTO_CALL return req } @@ -1170,7 +1171,7 @@ type EvalRequest struct { // NewEvalRequest returns a new empty EvalRequest. func NewEvalRequest(expr string) *EvalRequest { req := new(EvalRequest) - req.requestCode = EvalRequestCode + req.rtype = iproto.IPROTO_EVAL req.expr = expr req.args = []interface{}{} return req @@ -1210,7 +1211,7 @@ type ExecuteRequest struct { // NewExecuteRequest returns a new empty ExecuteRequest. func NewExecuteRequest(expr string) *ExecuteRequest { req := new(ExecuteRequest) - req.requestCode = ExecuteRequestCode + req.rtype = iproto.IPROTO_EXECUTE req.expr = expr req.args = []interface{}{} return req diff --git a/request_test.go b/request_test.go index b9a3a656d..a7e70ec65 100644 --- a/request_test.go +++ b/request_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" . "github.com/tarantool/go-tarantool/v2" @@ -169,37 +170,37 @@ func TestRequestsInvalidIndex(t *testing.T) { assertBodyCall(t, requests, invalidIndexMsg) } -func TestRequestsCodes(t *testing.T) { +func TestRequestsTypes(t *testing.T) { tests := []struct { - req Request - code int32 + req Request + rtype iproto.Type }{ - {req: NewSelectRequest(validSpace), code: SelectRequestCode}, - {req: NewUpdateRequest(validSpace), code: UpdateRequestCode}, - {req: NewUpsertRequest(validSpace), code: UpsertRequestCode}, - {req: NewInsertRequest(validSpace), code: InsertRequestCode}, - {req: NewReplaceRequest(validSpace), code: ReplaceRequestCode}, - {req: NewDeleteRequest(validSpace), code: DeleteRequestCode}, - {req: NewCallRequest(validExpr), code: CallRequestCode}, - {req: NewCallRequest(validExpr), code: Call17RequestCode}, - {req: NewCall16Request(validExpr), code: Call16RequestCode}, - {req: NewCall17Request(validExpr), code: Call17RequestCode}, - {req: NewEvalRequest(validExpr), code: EvalRequestCode}, - {req: NewExecuteRequest(validExpr), code: ExecuteRequestCode}, - {req: NewPingRequest(), code: PingRequestCode}, - {req: NewPrepareRequest(validExpr), code: PrepareRequestCode}, - {req: NewUnprepareRequest(validStmt), code: PrepareRequestCode}, - {req: NewExecutePreparedRequest(validStmt), code: ExecuteRequestCode}, - {req: NewBeginRequest(), code: BeginRequestCode}, - {req: NewCommitRequest(), code: CommitRequestCode}, - {req: NewRollbackRequest(), code: RollbackRequestCode}, - {req: NewIdRequest(validProtocolInfo), code: IdRequestCode}, - {req: NewBroadcastRequest(validKey), code: CallRequestCode}, + {req: NewSelectRequest(validSpace), rtype: iproto.IPROTO_SELECT}, + {req: NewUpdateRequest(validSpace), rtype: iproto.IPROTO_UPDATE}, + {req: NewUpsertRequest(validSpace), rtype: iproto.IPROTO_UPSERT}, + {req: NewInsertRequest(validSpace), rtype: iproto.IPROTO_INSERT}, + {req: NewReplaceRequest(validSpace), rtype: iproto.IPROTO_REPLACE}, + {req: NewDeleteRequest(validSpace), rtype: iproto.IPROTO_DELETE}, + {req: NewCallRequest(validExpr), rtype: iproto.IPROTO_CALL}, + {req: NewCall16Request(validExpr), rtype: iproto.IPROTO_CALL_16}, + {req: NewCall17Request(validExpr), rtype: iproto.IPROTO_CALL}, + {req: NewEvalRequest(validExpr), rtype: iproto.IPROTO_EVAL}, + {req: NewExecuteRequest(validExpr), rtype: iproto.IPROTO_EXECUTE}, + {req: NewPingRequest(), rtype: iproto.IPROTO_PING}, + {req: NewPrepareRequest(validExpr), rtype: iproto.IPROTO_PREPARE}, + {req: NewUnprepareRequest(validStmt), rtype: iproto.IPROTO_PREPARE}, + {req: NewExecutePreparedRequest(validStmt), rtype: iproto.IPROTO_EXECUTE}, + {req: NewBeginRequest(), rtype: iproto.IPROTO_BEGIN}, + {req: NewCommitRequest(), rtype: iproto.IPROTO_COMMIT}, + {req: NewRollbackRequest(), rtype: iproto.IPROTO_ROLLBACK}, + {req: NewIdRequest(validProtocolInfo), rtype: iproto.IPROTO_ID}, + {req: NewBroadcastRequest(validKey), rtype: iproto.IPROTO_CALL}, } for _, test := range tests { - if code := test.req.Code(); code != test.code { - t.Errorf("An invalid request code 0x%x, expected 0x%x", code, test.code) + if rtype := test.req.Type(); rtype != test.rtype { + t.Errorf("An invalid request type 0x%x, expected 0x%x", + rtype, test.rtype) } } } diff --git a/response.go b/response.go index b07e8690f..897b6ce73 100644 --- a/response.go +++ b/response.go @@ -3,6 +3,7 @@ package tarantool import ( "fmt" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" ) @@ -52,18 +53,18 @@ func (meta *ColumnMetaData) DecodeMsgpack(d *msgpack.Decoder) error { if mv, err = d.DecodeInterface(); err != nil { return fmt.Errorf("failed to decode meta data") } - switch mk { - case KeyFieldName: + switch iproto.MetadataKey(mk) { + case iproto.IPROTO_FIELD_NAME: meta.FieldName = mv.(string) - case KeyFieldType: + case iproto.IPROTO_FIELD_TYPE: meta.FieldType = mv.(string) - case KeyFieldColl: + case iproto.IPROTO_FIELD_COLL: meta.FieldCollation = mv.(string) - case KeyFieldIsNullable: + case iproto.IPROTO_FIELD_IS_NULLABLE: meta.FieldIsNullable = mv.(bool) - case KeyIsAutoincrement: + case iproto.IPROTO_FIELD_IS_AUTOINCREMENT: meta.FieldIsAutoincrement = mv.(bool) - case KeyFieldSpan: + case iproto.IPROTO_FIELD_SPAN: meta.FieldSpan = mv.(string) default: return fmt.Errorf("failed to decode meta data") @@ -86,12 +87,12 @@ func (info *SQLInfo) DecodeMsgpack(d *msgpack.Decoder) error { if mk, err = d.DecodeUint64(); err != nil { return fmt.Errorf("failed to decode meta data") } - switch mk { - case KeySQLInfoRowCount: + switch iproto.SqlInfoKey(mk) { + case iproto.SQL_INFO_ROW_COUNT: if info.AffectedCount, err = d.DecodeUint64(); err != nil { return fmt.Errorf("failed to decode meta data") } - case KeySQLInfoAutoincrementIds: + case iproto.SQL_INFO_AUTOINCREMENT_IDS: if err = d.Decode(&info.InfoAutoincrementIds); err != nil { return fmt.Errorf("failed to decode meta data") } @@ -125,14 +126,14 @@ func (resp *Response) decodeHeader(d *msgpack.Decoder) (err error) { if cd, err = resp.smallInt(d); err != nil { return } - switch cd { - case KeySync: + switch iproto.Key(cd) { + case iproto.IPROTO_SYNC: var rid uint64 if rid, err = d.DecodeUint64(); err != nil { return } resp.RequestId = uint32(rid) - case KeyCode: + case iproto.IPROTO_REQUEST_TYPE: var rcode uint64 if rcode, err = d.DecodeUint64(); err != nil { return @@ -171,8 +172,8 @@ func (resp *Response) decodeBody() (err error) { if cd, err = resp.smallInt(d); err != nil { return err } - switch cd { - case KeyData: + switch iproto.Key(cd) { + case iproto.IPROTO_DATA: var res interface{} var ok bool if res, err = d.DecodeInterface(); err != nil { @@ -181,35 +182,35 @@ func (resp *Response) decodeBody() (err error) { if resp.Data, ok = res.([]interface{}); !ok { return fmt.Errorf("result is not array: %v", res) } - case KeyError: + case iproto.IPROTO_ERROR: if errorExtendedInfo, err = decodeBoxError(d); err != nil { return err } - case KeyError24: + case iproto.IPROTO_ERROR_24: if resp.Error, err = d.DecodeString(); err != nil { return err } - case KeySQLInfo: + case iproto.IPROTO_SQL_INFO: if err = d.Decode(&resp.SQLInfo); err != nil { return err } - case KeyMetaData: + case iproto.IPROTO_METADATA: if err = d.Decode(&resp.MetaData); err != nil { return err } - case KeyStmtID: + case iproto.IPROTO_STMT_ID: if stmtID, err = d.DecodeUint64(); err != nil { return err } - case KeyBindCount: + case iproto.IPROTO_BIND_COUNT: if bindCount, err = d.DecodeUint64(); err != nil { return err } - case KeyVersion: + case iproto.IPROTO_VERSION: if err = d.Decode(&serverProtocolInfo.Version); err != nil { return err } - case KeyFeatures: + case iproto.IPROTO_FEATURES: if larr, err = d.DecodeArrayLen(); err != nil { return err } @@ -221,7 +222,7 @@ func (resp *Response) decodeBody() (err error) { } serverProtocolInfo.Features[i] = feature } - case KeyAuthType: + case iproto.IPROTO_AUTH_TYPE: var auth string if auth, err = d.DecodeString(); err != nil { return err @@ -236,7 +237,7 @@ func (resp *Response) decodeBody() (err error) { if !found { return fmt.Errorf("unknown auth type %s", auth) } - case KeyPos: + case iproto.IPROTO_POSITION: if resp.Pos, err = d.DecodeBytes(); err != nil { return fmt.Errorf("unable to decode a position: %w", err) } @@ -267,8 +268,8 @@ func (resp *Response) decodeBody() (err error) { } if resp.Code != OkCode && resp.Code != PushCode { - resp.Code &^= ErrorCodeBit - err = Error{resp.Code, resp.Error, errorExtendedInfo} + resp.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) + err = Error{iproto.Error(resp.Code), resp.Error, errorExtendedInfo} } } return @@ -296,28 +297,28 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if cd, err = resp.smallInt(d); err != nil { return err } - switch cd { - case KeyData: + switch iproto.Key(cd) { + case iproto.IPROTO_DATA: if err = d.Decode(res); err != nil { return err } - case KeyError: + case iproto.IPROTO_ERROR: if errorExtendedInfo, err = decodeBoxError(d); err != nil { return err } - case KeyError24: + case iproto.IPROTO_ERROR_24: if resp.Error, err = d.DecodeString(); err != nil { return err } - case KeySQLInfo: + case iproto.IPROTO_SQL_INFO: if err = d.Decode(&resp.SQLInfo); err != nil { return err } - case KeyMetaData: + case iproto.IPROTO_METADATA: if err = d.Decode(&resp.MetaData); err != nil { return err } - case KeyPos: + case iproto.IPROTO_POSITION: if resp.Pos, err = d.DecodeBytes(); err != nil { return fmt.Errorf("unable to decode a position: %w", err) } @@ -328,8 +329,8 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { } } if resp.Code != OkCode && resp.Code != PushCode { - resp.Code &^= ErrorCodeBit - err = Error{resp.Code, resp.Error, errorExtendedInfo} + resp.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) + err = Error{iproto.Error(resp.Code), resp.Error, errorExtendedInfo} } } return diff --git a/settings/request.go b/settings/request.go index df6dcd0ac..ae54aebf3 100644 --- a/settings/request.go +++ b/settings/request.go @@ -60,6 +60,7 @@ package settings import ( "context" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" "github.com/tarantool/go-tarantool/v2" @@ -85,9 +86,9 @@ func (req *SetRequest) Context(ctx context.Context) *SetRequest { return req } -// Code returns IPROTO code for set session settings request. -func (req *SetRequest) Code() int32 { - return req.impl.Code() +// Type returns IPROTO type for set session settings request. +func (req *SetRequest) Type() iproto.Type { + return req.impl.Type() } // Body fills an encoder with set session settings request body. @@ -125,9 +126,9 @@ func (req *GetRequest) Context(ctx context.Context) *GetRequest { return req } -// Code returns IPROTO code for get session settings request. -func (req *GetRequest) Code() int32 { - return req.impl.Code() +// Type returns IPROTO type for get session settings request. +func (req *GetRequest) Type() iproto.Type { + return req.impl.Type() } // Body fills an encoder with get session settings request body. diff --git a/settings/request_test.go b/settings/request_test.go index 7623a6cac..477795d2d 100644 --- a/settings/request_test.go +++ b/settings/request_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" "github.com/tarantool/go-tarantool/v2" @@ -40,34 +41,34 @@ func TestRequestsAPI(t *testing.T) { tests := []struct { req tarantool.Request async bool - code int32 + rtype iproto.Type }{ - {req: NewErrorMarshalingEnabledSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, - {req: NewErrorMarshalingEnabledGetRequest(), async: false, code: tarantool.SelectRequestCode}, - {req: NewSQLDefaultEngineSetRequest("memtx"), async: false, code: tarantool.UpdateRequestCode}, - {req: NewSQLDefaultEngineGetRequest(), async: false, code: tarantool.SelectRequestCode}, - {req: NewSQLDeferForeignKeysSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, - {req: NewSQLDeferForeignKeysGetRequest(), async: false, code: tarantool.SelectRequestCode}, - {req: NewSQLFullColumnNamesSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, - {req: NewSQLFullColumnNamesGetRequest(), async: false, code: tarantool.SelectRequestCode}, - {req: NewSQLFullMetadataSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, - {req: NewSQLFullMetadataGetRequest(), async: false, code: tarantool.SelectRequestCode}, - {req: NewSQLParserDebugSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, - {req: NewSQLParserDebugGetRequest(), async: false, code: tarantool.SelectRequestCode}, - {req: NewSQLRecursiveTriggersSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, - {req: NewSQLRecursiveTriggersGetRequest(), async: false, code: tarantool.SelectRequestCode}, - {req: NewSQLReverseUnorderedSelectsSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, - {req: NewSQLReverseUnorderedSelectsGetRequest(), async: false, code: tarantool.SelectRequestCode}, - {req: NewSQLSelectDebugSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, - {req: NewSQLSelectDebugGetRequest(), async: false, code: tarantool.SelectRequestCode}, - {req: NewSQLVDBEDebugSetRequest(false), async: false, code: tarantool.UpdateRequestCode}, - {req: NewSQLVDBEDebugGetRequest(), async: false, code: tarantool.SelectRequestCode}, - {req: NewSessionSettingsGetRequest(), async: false, code: tarantool.SelectRequestCode}, + {req: NewErrorMarshalingEnabledSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, + {req: NewErrorMarshalingEnabledGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, + {req: NewSQLDefaultEngineSetRequest("memtx"), async: false, rtype: iproto.IPROTO_UPDATE}, + {req: NewSQLDefaultEngineGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, + {req: NewSQLDeferForeignKeysSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, + {req: NewSQLDeferForeignKeysGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, + {req: NewSQLFullColumnNamesSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, + {req: NewSQLFullColumnNamesGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, + {req: NewSQLFullMetadataSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, + {req: NewSQLFullMetadataGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, + {req: NewSQLParserDebugSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, + {req: NewSQLParserDebugGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, + {req: NewSQLRecursiveTriggersSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, + {req: NewSQLRecursiveTriggersGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, + {req: NewSQLReverseUnorderedSelectsSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, + {req: NewSQLReverseUnorderedSelectsGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, + {req: NewSQLSelectDebugSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, + {req: NewSQLSelectDebugGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, + {req: NewSQLVDBEDebugSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, + {req: NewSQLVDBEDebugGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, + {req: NewSessionSettingsGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, } for _, test := range tests { require.Equal(t, test.async, test.req.Async()) - require.Equal(t, test.code, test.req.Code()) + require.Equal(t, test.rtype, test.req.Type()) var reqBuf bytes.Buffer enc := msgpack.NewEncoder(&reqBuf) diff --git a/stream.go b/stream.go index 5e1220dff..939840d19 100644 --- a/stream.go +++ b/stream.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" ) @@ -46,7 +47,7 @@ func fillBegin(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, timeout tim } if hasTimeout { - err = enc.EncodeUint(KeyTimeout) + err = enc.EncodeUint(uint64(iproto.IPROTO_TIMEOUT)) if err != nil { return err } @@ -58,7 +59,7 @@ func fillBegin(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, timeout tim } if hasIsolationLevel { - err = enc.EncodeUint(KeyTxnIsolation) + err = enc.EncodeUint(uint64(iproto.IPROTO_TXN_ISOLATION)) if err != nil { return err } @@ -92,7 +93,7 @@ type BeginRequest struct { // NewBeginRequest returns a new BeginRequest. func NewBeginRequest() *BeginRequest { req := new(BeginRequest) - req.requestCode = BeginRequestCode + req.rtype = iproto.IPROTO_BEGIN req.txnIsolation = DefaultIsolationLevel return req } @@ -136,7 +137,7 @@ type CommitRequest struct { // NewCommitRequest returns a new CommitRequest. func NewCommitRequest() *CommitRequest { req := new(CommitRequest) - req.requestCode = CommitRequestCode + req.rtype = iproto.IPROTO_COMMIT return req } @@ -166,7 +167,7 @@ type RollbackRequest struct { // NewRollbackRequest returns a new RollbackRequest. func NewRollbackRequest() *RollbackRequest { req := new(RollbackRequest) - req.requestCode = RollbackRequestCode + req.rtype = iproto.IPROTO_ROLLBACK return req } diff --git a/tarantool_test.go b/tarantool_test.go index 61870d27d..0d56b4ada 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" . "github.com/tarantool/go-tarantool/v2" @@ -910,8 +911,8 @@ func TestClient(t *testing.T) { } //resp, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) resp, err = conn.Insert(spaceNo, &Tuple{Id: 1, Msg: "hello", Name: "world"}) - if tntErr, ok := err.(Error); !ok || tntErr.Code != ErrTupleFound { - t.Errorf("Expected ErrTupleFound but got: %v", err) + if tntErr, ok := err.(Error); !ok || tntErr.Code != iproto.ER_TUPLE_FOUND { + t.Errorf("Expected %s but got: %v", iproto.ER_TUPLE_FOUND, err) } if len(resp.Data) != 0 { t.Errorf("Response Body len != 0") @@ -1685,7 +1686,8 @@ func TestStressSQL(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Code != ErSpaceExistsCode { + + if iproto.Error(resp.Code) != iproto.ER_SPACE_EXISTS { t.Fatalf("Unexpected response code: %d", resp.Code) } if resp.SQLInfo.AffectedCount != 0 { @@ -2682,8 +2684,8 @@ type waitCtxRequest struct { wg sync.WaitGroup } -func (req *waitCtxRequest) Code() int32 { - return NewPingRequest().Code() +func (req *waitCtxRequest) Type() iproto.Type { + return NewPingRequest().Type() } func (req *waitCtxRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { diff --git a/test_helpers/request_mock.go b/test_helpers/request_mock.go index 0eaa11739..80ca1b541 100644 --- a/test_helpers/request_mock.go +++ b/test_helpers/request_mock.go @@ -3,6 +3,7 @@ package test_helpers import ( "context" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" "github.com/tarantool/go-tarantool/v2" @@ -15,8 +16,8 @@ func NewStrangerRequest() *StrangerRequest { return &StrangerRequest{} } -func (sr *StrangerRequest) Code() int32 { - return 0 +func (sr *StrangerRequest) Type() iproto.Type { + return iproto.Type(0) } func (sr *StrangerRequest) Async() bool { diff --git a/watch.go b/watch.go index bf045de4b..0508899f0 100644 --- a/watch.go +++ b/watch.go @@ -3,6 +3,7 @@ package tarantool import ( "context" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" ) @@ -35,8 +36,8 @@ func (req *BroadcastRequest) Context(ctx context.Context) *BroadcastRequest { } // Code returns IPROTO code for the broadcast request. -func (req *BroadcastRequest) Code() int32 { - return req.call.Code() +func (req *BroadcastRequest) Type() iproto.Type { + return req.call.Type() } // Body fills an msgpack.Encoder with the broadcast request body. @@ -66,7 +67,7 @@ type watchRequest struct { // newWatchRequest returns a new watchRequest. func newWatchRequest(key string) *watchRequest { req := new(watchRequest) - req.requestCode = WatchRequestCode + req.rtype = iproto.IPROTO_WATCH req.async = true req.key = key return req @@ -77,7 +78,7 @@ func (req *watchRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { if err := enc.EncodeMapLen(1); err != nil { return err } - if err := enc.EncodeUint(KeyEvent); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_EVENT_KEY)); err != nil { return err } return enc.EncodeString(req.key) @@ -100,7 +101,7 @@ type unwatchRequest struct { // newUnwatchRequest returns a new unwatchRequest. func newUnwatchRequest(key string) *unwatchRequest { req := new(unwatchRequest) - req.requestCode = UnwatchRequestCode + req.rtype = iproto.IPROTO_UNWATCH req.async = true req.key = key return req @@ -111,7 +112,7 @@ func (req *unwatchRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error if err := enc.EncodeMapLen(1); err != nil { return err } - if err := enc.EncodeUint(KeyEvent); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_EVENT_KEY)); err != nil { return err } return enc.EncodeString(req.key) From acedf568ab64f3d869f1377e2666db1091b2ccd9 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 20 Apr 2023 17:08:26 +0300 Subject: [PATCH 457/605] api: add separate types for constants Closes #158 --- CHANGELOG.md | 1 + connection.go | 22 +++++++++++++++------- connector.go | 6 +++--- const.go | 16 ---------------- export_test.go | 2 +- iterator.go | 35 +++++++++++++++++++++++++++++++++++ pool/connection_pool.go | 12 +++++++++--- pool/connector.go | 6 +++--- pool/connector_test.go | 23 ++++++++++++----------- pool/pooler.go | 6 +++--- request.go | 15 ++++++++------- 11 files changed, 90 insertions(+), 54 deletions(-) create mode 100644 iterator.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a7c31945..759dc43c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added - Type() method to the Request interface (#158) +- Enumeration types for RLimitAction/iterators (#158) ### Changed diff --git a/connection.go b/connection.go index 46110d766..ea7f236fe 100644 --- a/connection.go +++ b/connection.go @@ -236,6 +236,19 @@ type connShard struct { enc *msgpack.Encoder } +// RLimitActions is an enumeration type for an action to do when a rate limit +// is reached. +type RLimitAction int + +const ( + // RLimitDrop immediately aborts the request. + RLimitDrop RLimitAction = iota + // RLimitWait waits during timeout period for some request to be answered. + // If no request answered during timeout period, this request is aborted. + // If no timeout period is set, it will wait forever. + RLimitWait +) + // Opts is a way to configure Connection type Opts struct { // Auth is an authentication method. @@ -274,14 +287,9 @@ type Opts struct { // It is disabled by default. // See RLimitAction for possible actions when RateLimit.reached. RateLimit uint - // RLimitAction tells what to do when RateLimit reached: - // RLimitDrop - immediately abort request, - // RLimitWait - wait during timeout period for some request to be answered. - // If no request answered during timeout period, this request - // is aborted. - // If no timeout period is set, it will wait forever. + // RLimitAction tells what to do when RateLimit is reached. // It is required if RateLimit is specified. - RLimitAction uint + RLimitAction RLimitAction // Concurrency is amount of separate mutexes for request // queues and buffers inside of connection. // It is rounded up to nearest power of 2. diff --git a/connector.go b/connector.go index d93c69ec8..3aa2f9542 100644 --- a/connector.go +++ b/connector.go @@ -8,7 +8,7 @@ type Connector interface { Ping() (resp *Response, err error) ConfiguredTimeout() time.Duration - Select(space, index interface{}, offset, limit, iterator uint32, key interface{}) (resp *Response, err error) + Select(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}) (resp *Response, err error) Insert(space interface{}, tuple interface{}) (resp *Response, err error) Replace(space interface{}, tuple interface{}) (resp *Response, err error) Delete(space, index interface{}, key interface{}) (resp *Response, err error) @@ -21,7 +21,7 @@ type Connector interface { Execute(expr string, args interface{}) (resp *Response, err error) GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) - SelectTyped(space, index interface{}, offset, limit, iterator uint32, key interface{}, result interface{}) (err error) + SelectTyped(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}, result interface{}) (err error) InsertTyped(space interface{}, tuple interface{}, result interface{}) (err error) ReplaceTyped(space interface{}, tuple interface{}, result interface{}) (err error) DeleteTyped(space, index interface{}, key interface{}, result interface{}) (err error) @@ -32,7 +32,7 @@ type Connector interface { EvalTyped(expr string, args interface{}, result interface{}) (err error) ExecuteTyped(expr string, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error) - SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Future + SelectAsync(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}) *Future InsertAsync(space interface{}, tuple interface{}) *Future ReplaceAsync(space interface{}, tuple interface{}) *Future DeleteAsync(space, index interface{}, key interface{}) *Future diff --git a/const.go b/const.go index 4d9e94a9d..62650b5be 100644 --- a/const.go +++ b/const.go @@ -9,22 +9,6 @@ const ( ) const ( - IterEq = uint32(0) // key == x ASC order - IterReq = uint32(1) // key == x DESC order - IterAll = uint32(2) // all tuples - IterLt = uint32(3) // key < x - IterLe = uint32(4) // key <= x - IterGe = uint32(5) // key >= x - IterGt = uint32(6) // key > x - IterBitsAllSet = uint32(7) // all bits from x are set in key - IterBitsAnySet = uint32(8) // at least one x's bit is set - IterBitsAllNotSet = uint32(9) // all bits are not set - IterOverlaps = uint32(10) // key overlaps x - IterNeighbor = uint32(11) // tuples in distance ascending order from specified point - - RLimitDrop = 1 - RLimitWait = 2 - OkCode = uint32(iproto.IPROTO_OK) PushCode = uint32(iproto.IPROTO_CHUNK) ) diff --git a/export_test.go b/export_test.go index 879d345ac..688d09059 100644 --- a/export_test.go +++ b/export_test.go @@ -24,7 +24,7 @@ func RefImplPingBody(enc *msgpack.Encoder) error { // RefImplSelectBody is reference implementation for filling of a select // request's body. -func RefImplSelectBody(enc *msgpack.Encoder, space, index, offset, limit, iterator uint32, +func RefImplSelectBody(enc *msgpack.Encoder, space, index, offset, limit uint32, iterator Iter, key, after interface{}, fetchPos bool) error { return fillSelect(enc, space, index, offset, limit, iterator, key, after, fetchPos) } diff --git a/iterator.go b/iterator.go new file mode 100644 index 000000000..128168d7b --- /dev/null +++ b/iterator.go @@ -0,0 +1,35 @@ +package tarantool + +import ( + "github.com/tarantool/go-iproto" +) + +// Iter is an enumeration type of a select iterator. +type Iter uint32 + +const ( + // Key == x ASC order. + IterEq Iter = Iter(iproto.ITER_EQ) + // Key == x DESC order. + IterReq Iter = Iter(iproto.ITER_REQ) + // All tuples. + IterAll Iter = Iter(iproto.ITER_ALL) + // Key < x. + IterLt Iter = Iter(iproto.ITER_LT) + // Key <= x. + IterLe Iter = Iter(iproto.ITER_LE) + // Key >= x. + IterGe Iter = Iter(iproto.ITER_GE) + // Key > x. + IterGt Iter = Iter(iproto.ITER_GT) + // All bits from x are set in key. + IterBitsAllSet Iter = Iter(iproto.ITER_BITS_ALL_SET) + // All bits are not set. + IterBitsAnySet Iter = Iter(iproto.ITER_BITS_ANY_SET) + // All bits are not set. + IterBitsAllNotSet Iter = Iter(iproto.ITER_BITS_ALL_NOT_SET) + // Key overlaps x. + IterOverlaps Iter = Iter(iproto.ITER_OVERLAPS) + // Tuples in distance ascending order from specified point. + IterNeighbor Iter = Iter(iproto.ITER_NEIGHBOR) +) diff --git a/pool/connection_pool.go b/pool/connection_pool.go index e6d31575d..f2d85f445 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -373,7 +373,9 @@ func (connPool *ConnectionPool) Ping(userMode Mode) (*tarantool.Response, error) } // Select performs select to box space. -func (connPool *ConnectionPool) Select(space, index interface{}, offset, limit, iterator uint32, key interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { +func (connPool *ConnectionPool) Select(space, index interface{}, + offset, limit uint32, + iterator tarantool.Iter, key interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getConnByMode(ANY, userMode) if err != nil { return nil, err @@ -503,7 +505,9 @@ func (connPool *ConnectionPool) GetTyped(space, index interface{}, key interface } // SelectTyped performs select to box space and fills typed result. -func (connPool *ConnectionPool) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key interface{}, result interface{}, userMode ...Mode) (err error) { +func (connPool *ConnectionPool) SelectTyped(space, index interface{}, + offset, limit uint32, + iterator tarantool.Iter, key interface{}, result interface{}, userMode ...Mode) (err error) { conn, err := connPool.getConnByMode(ANY, userMode) if err != nil { return err @@ -609,7 +613,9 @@ func (connPool *ConnectionPool) ExecuteTyped(expr string, args interface{}, resu } // SelectAsync sends select request to Tarantool and returns Future. -func (connPool *ConnectionPool) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}, userMode ...Mode) *tarantool.Future { +func (connPool *ConnectionPool) SelectAsync(space, index interface{}, + offset, limit uint32, + iterator tarantool.Iter, key interface{}, userMode ...Mode) *tarantool.Future { conn, err := connPool.getConnByMode(ANY, userMode) if err != nil { return newErrorFuture(err) diff --git a/pool/connector.go b/pool/connector.go index 22760801b..852f47cee 100644 --- a/pool/connector.go +++ b/pool/connector.go @@ -63,7 +63,7 @@ func (c *ConnectorAdapter) ConfiguredTimeout() time.Duration { // Select performs select to box space. func (c *ConnectorAdapter) Select(space, index interface{}, - offset, limit, iterator uint32, + offset, limit uint32, iterator tarantool.Iter, key interface{}) (*tarantool.Response, error) { return c.pool.Select(space, index, offset, limit, iterator, key, c.mode) } @@ -141,7 +141,7 @@ func (c *ConnectorAdapter) GetTyped(space, index interface{}, // SelectTyped performs select to box space and fills typed result. func (c *ConnectorAdapter) SelectTyped(space, index interface{}, - offset, limit, iterator uint32, + offset, limit uint32, iterator tarantool.Iter, key interface{}, result interface{}) error { return c.pool.SelectTyped(space, index, offset, limit, iterator, key, result, c.mode) } @@ -206,7 +206,7 @@ func (c *ConnectorAdapter) ExecuteTyped(expr string, args interface{}, // SelectAsync sends select request to Tarantool and returns Future. func (c *ConnectorAdapter) SelectAsync(space, index interface{}, - offset, limit, iterator uint32, key interface{}) *tarantool.Future { + offset, limit uint32, iterator tarantool.Iter, key interface{}) *tarantool.Future { return c.pool.SelectAsync(space, index, offset, limit, iterator, key, c.mode) } diff --git a/pool/connector_test.go b/pool/connector_test.go index d4d197607..8a954fa6e 100644 --- a/pool/connector_test.go +++ b/pool/connector_test.go @@ -125,13 +125,14 @@ func TestConnectorConfiguredTimeoutWithError(t *testing.T) { type baseRequestMock struct { Pooler - called int - functionName string - offset, limit, iterator uint32 - space, index interface{} - args, tuple, key, ops interface{} - result interface{} - mode Mode + called int + functionName string + offset, limit uint32 + iterator tarantool.Iter + space, index interface{} + args, tuple, key, ops interface{} + result interface{} + mode Mode } var reqResp *tarantool.Response = &tarantool.Response{} @@ -141,7 +142,7 @@ var reqFuture *tarantool.Future = &tarantool.Future{} var reqFunctionName string = "any_name" var reqOffset uint32 = 1 var reqLimit uint32 = 2 -var reqIterator uint32 = 3 +var reqIterator tarantool.Iter = tarantool.IterLt var reqSpace interface{} = []interface{}{1} var reqIndex interface{} = []interface{}{2} var reqArgs interface{} = []interface{}{3} @@ -188,7 +189,7 @@ type selectMock struct { } func (m *selectMock) Select(space, index interface{}, - offset, limit, iterator uint32, key interface{}, + offset, limit uint32, iterator tarantool.Iter, key interface{}, mode ...Mode) (*tarantool.Response, error) { m.called++ m.space = space @@ -224,7 +225,7 @@ type selectTypedMock struct { } func (m *selectTypedMock) SelectTyped(space, index interface{}, - offset, limit, iterator uint32, key interface{}, + offset, limit uint32, iterator tarantool.Iter, key interface{}, result interface{}, mode ...Mode) error { m.called++ m.space = space @@ -262,7 +263,7 @@ type selectAsyncMock struct { } func (m *selectAsyncMock) SelectAsync(space, index interface{}, - offset, limit, iterator uint32, key interface{}, + offset, limit uint32, iterator tarantool.Iter, key interface{}, mode ...Mode) *tarantool.Future { m.called++ m.space = space diff --git a/pool/pooler.go b/pool/pooler.go index 626c5af93..a7cae8f95 100644 --- a/pool/pooler.go +++ b/pool/pooler.go @@ -13,7 +13,7 @@ type Pooler interface { Ping(mode Mode) (*tarantool.Response, error) ConfiguredTimeout(mode Mode) (time.Duration, error) - Select(space, index interface{}, offset, limit, iterator uint32, + Select(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}, mode ...Mode) (*tarantool.Response, error) Insert(space interface{}, tuple interface{}, mode ...Mode) (*tarantool.Response, error) @@ -38,7 +38,7 @@ type Pooler interface { GetTyped(space, index interface{}, key interface{}, result interface{}, mode ...Mode) error - SelectTyped(space, index interface{}, offset, limit, iterator uint32, + SelectTyped(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}, result interface{}, mode ...Mode) error InsertTyped(space interface{}, tuple interface{}, result interface{}, mode ...Mode) error @@ -59,7 +59,7 @@ type Pooler interface { ExecuteTyped(expr string, args interface{}, result interface{}, mode Mode) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) - SelectAsync(space, index interface{}, offset, limit, iterator uint32, + SelectAsync(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}, mode ...Mode) *tarantool.Future InsertAsync(space interface{}, tuple interface{}, mode ...Mode) *tarantool.Future diff --git a/request.go b/request.go index a86be1c6d..adb4a7b31 100644 --- a/request.go +++ b/request.go @@ -31,7 +31,7 @@ func fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) return enc.Encode(key) } -func fillIterator(enc *msgpack.Encoder, offset, limit, iterator uint32) error { +func fillIterator(enc *msgpack.Encoder, offset, limit uint32, iterator Iter) error { if err := enc.EncodeUint(uint64(iproto.IPROTO_ITERATOR)); err != nil { return err } @@ -66,7 +66,7 @@ func fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { return enc.Encode(tuple) } -func fillSelect(enc *msgpack.Encoder, spaceNo, indexNo, offset, limit, iterator uint32, +func fillSelect(enc *msgpack.Encoder, spaceNo, indexNo, offset, limit uint32, iterator Iter, key, after interface{}, fetchPos bool) error { mapLen := 6 if fetchPos { @@ -174,7 +174,7 @@ func (conn *Connection) Ping() (resp *Response, err error) { // Select performs select to box space. // // It is equal to conn.SelectAsync(...).Get(). -func (conn *Connection) Select(space, index interface{}, offset, limit, iterator uint32, key interface{}) (resp *Response, err error) { +func (conn *Connection) Select(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}) (resp *Response, err error) { return conn.SelectAsync(space, index, offset, limit, iterator, key).Get() } @@ -292,7 +292,7 @@ func (conn *Connection) GetTyped(space, index interface{}, key interface{}, resu // SelectTyped performs select to box space and fills typed result. // // It is equal to conn.SelectAsync(space, index, offset, limit, iterator, key).GetTyped(&result) -func (conn *Connection) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key interface{}, result interface{}) (err error) { +func (conn *Connection) SelectTyped(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}, result interface{}) (err error) { return conn.SelectAsync(space, index, offset, limit, iterator, key).GetTyped(result) } @@ -369,7 +369,7 @@ func (conn *Connection) ExecuteTyped(expr string, args interface{}, result inter } // SelectAsync sends select request to Tarantool and returns Future. -func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Future { +func (conn *Connection) SelectAsync(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}) *Future { req := NewSelectRequest(space). Index(index). Offset(offset). @@ -740,7 +740,8 @@ func (req *PingRequest) Context(ctx context.Context) *PingRequest { type SelectRequest struct { spaceIndexRequest isIteratorSet, fetchPos bool - offset, limit, iterator uint32 + offset, limit uint32 + iterator Iter key, after interface{} } @@ -781,7 +782,7 @@ func (req *SelectRequest) Limit(limit uint32) *SelectRequest { // Iterator set the iterator for the select request. // Note: default value is IterAll if key is not set or IterEq otherwise. -func (req *SelectRequest) Iterator(iterator uint32) *SelectRequest { +func (req *SelectRequest) Iterator(iterator Iter) *SelectRequest { req.iterator = iterator req.isIteratorSet = true return req From 40fe0d3a6926559328bf4e1cd93d4c90a5b316a4 Mon Sep 17 00:00:00 2001 From: Borislav Demidov Date: Thu, 8 Jun 2023 20:32:08 +0300 Subject: [PATCH 458/605] schema: add IsNullable flag to Field --- schema.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/schema.go b/schema.go index 0617924eb..af7dd21f5 100644 --- a/schema.go +++ b/schema.go @@ -153,9 +153,10 @@ func (space *Space) DecodeMsgpack(d *msgpack.Decoder) error { // Field is a schema field. type Field struct { - Id uint32 - Name string - Type string + Id uint32 + Name string + Type string + IsNullable bool } func (field *Field) DecodeMsgpack(d *msgpack.Decoder) error { @@ -177,6 +178,10 @@ func (field *Field) DecodeMsgpack(d *msgpack.Decoder) error { if field.Type, err = d.DecodeString(); err != nil { return err } + case "is_nullable": + if field.IsNullable, err = d.DecodeBool(); err != nil { + return err + } default: if err := d.Skip(); err != nil { return err From 6bb17ea36702c667ddf8a440bc783f8703631543 Mon Sep 17 00:00:00 2001 From: Borislav Demidov Date: Thu, 8 Jun 2023 21:41:31 +0300 Subject: [PATCH 459/605] test: add test for IsNullable Field flag --- config.lua | 3 ++- tarantool_test.go | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/config.lua b/config.lua index aafb98ceb..2553b94c8 100644 --- a/config.lua +++ b/config.lua @@ -16,7 +16,7 @@ box.once("init", function() id = 616, temporary = true, if_not_exists = true, - field_count = 7, + field_count = 8, format = { {name = "name0", type = "unsigned"}, {name = "name1", type = "unsigned"}, @@ -24,6 +24,7 @@ box.once("init", function() {name = "name3", type = "unsigned"}, {name = "name4", type = "unsigned"}, {name = "name5", type = "string"}, + {name = "nullable", is_nullable = true}, }, }) st:create_index('primary', { diff --git a/tarantool_test.go b/tarantool_test.go index 0d56b4ada..d58a6775f 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -1908,7 +1908,7 @@ func TestSchema(t *testing.T) { if space.Engine != "memtx" { t.Errorf("space 616 engine should be memtx") } - if space.FieldsCount != 7 { + if space.FieldsCount != 8 { t.Errorf("space 616 has incorrect fields count") } @@ -1918,10 +1918,10 @@ func TestSchema(t *testing.T) { if space.Fields == nil { t.Errorf("space.Fields is nill") } - if len(space.FieldsById) != 6 { + if len(space.FieldsById) != 7 { t.Errorf("space.FieldsById len is incorrect") } - if len(space.Fields) != 6 { + if len(space.Fields) != 7 { t.Errorf("space.Fields len is incorrect") } @@ -2045,6 +2045,39 @@ func TestSchema(t *testing.T) { } } +func TestSchema_IsNullable(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + schema := conn.Schema + if schema.Spaces == nil { + t.Errorf("schema.Spaces is nil") + } + + var space *Space + var ok bool + if space, ok = schema.SpacesById[616]; !ok { + t.Errorf("space with id = 616 was not found in schema.SpacesById") + } + + var field, field_nullable *Field + for i := 0; i <= 5; i++ { + name := fmt.Sprintf("name%d", i) + if field, ok = space.Fields[name]; !ok { + t.Errorf("field name = %s was not found", name) + } + if field.IsNullable { + t.Errorf("field %s has incorrect IsNullable", name) + } + } + if field_nullable, ok = space.Fields["nullable"]; !ok { + t.Errorf("field name = nullable was not found") + } + if !field_nullable.IsNullable { + t.Errorf("field nullable has incorrect IsNullable") + } +} + func TestClientNamed(t *testing.T) { var resp *Response var err error From 00a9e43c235a34f8dfc613b230ca2ec0b60d9701 Mon Sep 17 00:00:00 2001 From: Borislav Demidov Date: Thu, 8 Jun 2023 21:47:48 +0300 Subject: [PATCH 460/605] changelog: add message on IsNullable Field flag --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 759dc43c7..185ec2915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Type() method to the Request interface (#158) - Enumeration types for RLimitAction/iterators (#158) +- IsNullable flag for Field (#302) ### Changed From 6cddcd71029d90372a7889a7959124a389a75222 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 7 Jun 2023 11:02:33 +0300 Subject: [PATCH 461/605] test: fix flaky decimal.TestSelect We forgot to increase the timeout [1] when we fixed tests for macos. 1. https://github.com/tarantool/go-tarantool/commit/521c0c34804e93cd2648c3b5c27bc7194d3870d0 --- CHANGELOG.md | 2 ++ decimal/decimal_test.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 185ec2915..5b7366b46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +- Flaky decimal/TestSelect (#300) + ## [1.12.0] - 2023-06-07 The release introduces the ability to gracefully close Connection diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 372a06fce..97b929ca2 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -21,7 +21,7 @@ var isDecimalSupported = false var server = "127.0.0.1:3013" var opts = Opts{ - Timeout: 500 * time.Millisecond, + Timeout: 5 * time.Second, User: "test", Pass: "test", } From 8ae660c28264519147b776d4548c3fdcf4ec3050 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 14 Jun 2023 22:19:31 +0300 Subject: [PATCH 462/605] ci: don't run EE tests on forks by default This patch adds the condition to run this workflow only for main repo. It helps to avoid failed CI on forks. --- .github/workflows/testing.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 6745870a2..61e185789 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -115,17 +115,19 @@ jobs: run-tests-ee: # The same as for run-tests-ce, but it does not run on pull requests from - # forks by default. Tests will run only when the pull request is labeled - # with `full-ci`. To avoid security problems, the label must be reset - # manually for every run. + # forks and on forks by default. Tests from forks will run only when the + # pull request is labeled with `full-ci`. To avoid security problems, the + # label must be reset manually for every run. # # We need to use `pull_request_target` because it has access to base # repository secrets unlike `pull_request`. - if: (github.event_name == 'push') || - (github.event_name == 'pull_request_target' && - github.event.pull_request.head.repo.full_name != github.repository && - github.event.label.name == 'full-ci') || - (github.event_name == 'workflow_dispatch') + if: | + github.repository == 'tarantool/go-tarantool' && + (github.event_name == 'push' || + (github.event_name == 'pull_request_target' && + github.event.pull_request.head.repo.full_name != github.repository && + github.event.label.name == 'full-ci')) || + github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest From 0d266dd69173d911b3b3de360e54afa9a8661d32 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 14 Jun 2023 16:12:48 +0300 Subject: [PATCH 463/605] doc: update CONTRIBUTING.md The patch updates CONTRIBUTING.md according to latest changes: * multi package has been removed. * go_tarantool_msgpack_v5 build tag has been removed. --- CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c83cde001..fdfcc67a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ Clone the repository and install dependencies. ```sh -$ git clone git@github.com:/tarantool/go-tarantool +$ git clone https://github.com/tarantool/go-tarantool $ cd go-tarantool $ go get . ``` @@ -36,8 +36,8 @@ afterwards. If you want to run the tests with specific build tags: ```bash -make test TAGS=go_tarantool_ssl_disable,go_tarantool_msgpack_v5 -make testrace TAGS=go_tarantool_ssl_disable,go_tarantool_msgpack_v5 +make test TAGS=go_tarantool_ssl_disable +make testrace TAGS=go_tarantool_ssl_disable ``` If you have Tarantool Enterprise Edition 2.10 or newer, you can run additional @@ -52,9 +52,9 @@ If you want to run the tests for a specific package: ```bash make test- ``` -For example, for running tests in `multi`, `uuid` and `main` packages, call +For example, for running tests in `pool`, `uuid` and `main` packages, call ```bash -make test-multi test-uuid test-main +make test-pool test-uuid test-main ``` To run [fuzz tests](https://go.dev/doc/tutorial/fuzz) for the main package and each subpackage: From 337ca730e8c2dc3135bf0c49764253b4e2ba1421 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 15 Jun 2023 16:24:44 +0300 Subject: [PATCH 464/605] api: mark all request methods as deprecated We mark all request methods as deprecated because the logic based on request methods is very difficult to extend. These methods will be removed in a next major version. The client code should prefer a request object + Do(). We have updated all examples for this purpose. Closes #241 --- CHANGELOG.md | 9 + README.md | 13 +- connector.go | 80 +- crud/tarantool_test.go | 28 +- datetime/datetime_test.go | 71 +- datetime/example_test.go | 17 +- datetime/interval_test.go | 5 +- decimal/decimal_test.go | 30 +- decimal/example_test.go | 4 +- example_custom_unpacking_test.go | 16 +- example_test.go | 1161 ++++++++++++++---------------- pool/connection_pool.go | 104 ++- pool/connection_pool_test.go | 60 +- pool/connector.go | 112 ++- pool/example_test.go | 522 +------------- pool/pooler.go | 82 ++- queue/queue.go | 38 +- request.go | 102 +++ settings/example_test.go | 11 +- settings/tarantool_test.go | 81 ++- test_helpers/main.go | 2 +- test_helpers/pool_helper.go | 10 +- uuid/example_test.go | 4 +- uuid/uuid_test.go | 19 +- 24 files changed, 1333 insertions(+), 1248 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7366b46..6fcd90d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,15 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Use msgpack/v5 instead of msgpack.v2 (#236) - Call/NewCallRequest = Call17/NewCall17Request (#235) +### Deprecated + +- All Connection., Connection.Typed and + Connection.Async methods. Instead you should use requests objects + + Connection.Do() (#241) +- All ConnectionPool., ConnectionPool.Typed and + ConnectionPool.Async methods. Instead you should use requests + objects + ConnectionPool.Do() (#241) + ### Removed - multi subpackage (#240) diff --git a/README.md b/README.md index 8d67db1b2..b3bff4d3d 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,9 @@ func main() { if err != nil { fmt.Println("Connection refused:", err) } - resp, err := conn.Insert(999, []interface{}{99999, "BB"}) + resp, err := conn.Do(tarantool.NewInsertRequest(999). + Tuple([]interface{}{99999, "BB"}), + ).Get() if err != nil { fmt.Println("Error", err) fmt.Println("Code", resp.Code) @@ -138,12 +140,9 @@ starting a session. There are two parameters: **Observation 4:** The `err` structure will be `nil` if there is no error, otherwise it will have a description which can be retrieved with `err.Error()`. -**Observation 5:** The `Insert` request, like almost all requests, is preceded by -"`conn.`" which is the name of the object that was returned by `Connect()`. -There are two parameters: - -* a space number (it could just as easily have been a space name), and -* a tuple. +**Observation 5:** The `Insert` request, like almost all requests, is preceded +by the method `Do` of object `conn` which is the object that was returned +by `Connect()`. ### Migration to v2 diff --git a/connector.go b/connector.go index 3aa2f9542..016dbbfb3 100644 --- a/connector.go +++ b/connector.go @@ -5,48 +5,114 @@ import "time" type Connector interface { ConnectedNow() bool Close() error - Ping() (resp *Response, err error) ConfiguredTimeout() time.Duration + NewPrepared(expr string) (*Prepared, error) + NewStream() (*Stream, error) + NewWatcher(key string, callback WatchCallback) (Watcher, error) + Do(req Request) (fut *Future) + // Deprecated: the method will be removed in the next major version, + // use a PingRequest object + Do() instead. + Ping() (resp *Response, err error) + // Deprecated: the method will be removed in the next major version, + // use a SelectRequest object + Do() instead. Select(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}) (resp *Response, err error) + // Deprecated: the method will be removed in the next major version, + // use an InsertRequest object + Do() instead. Insert(space interface{}, tuple interface{}) (resp *Response, err error) + // Deprecated: the method will be removed in the next major version, + // use a ReplicaRequest object + Do() instead. Replace(space interface{}, tuple interface{}) (resp *Response, err error) + // Deprecated: the method will be removed in the next major version, + // use a DeleteRequest object + Do() instead. Delete(space, index interface{}, key interface{}) (resp *Response, err error) + // Deprecated: the method will be removed in the next major version, + // use a UpdateRequest object + Do() instead. Update(space, index interface{}, key, ops interface{}) (resp *Response, err error) + // Deprecated: the method will be removed in the next major version, + // use a UpsertRequest object + Do() instead. Upsert(space interface{}, tuple, ops interface{}) (resp *Response, err error) + // Deprecated: the method will be removed in the next major version, + // use a CallRequest object + Do() instead. Call(functionName string, args interface{}) (resp *Response, err error) + // Deprecated: the method will be removed in the next major version, + // use a Call16Request object + Do() instead. Call16(functionName string, args interface{}) (resp *Response, err error) + // Deprecated: the method will be removed in the next major version, + // use a Call17Request object + Do() instead. Call17(functionName string, args interface{}) (resp *Response, err error) + // Deprecated: the method will be removed in the next major version, + // use an EvalRequest object + Do() instead. Eval(expr string, args interface{}) (resp *Response, err error) + // Deprecated: the method will be removed in the next major version, + // use an ExecuteRequest object + Do() instead. Execute(expr string, args interface{}) (resp *Response, err error) + // Deprecated: the method will be removed in the next major version, + // use a SelectRequest object + Do() instead. GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) + // Deprecated: the method will be removed in the next major version, + // use a SelectRequest object + Do() instead. SelectTyped(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}, result interface{}) (err error) + // Deprecated: the method will be removed in the next major version, + // use an InsertRequest object + Do() instead. InsertTyped(space interface{}, tuple interface{}, result interface{}) (err error) + // Deprecated: the method will be removed in the next major version, + // use a ReplaceRequest object + Do() instead. ReplaceTyped(space interface{}, tuple interface{}, result interface{}) (err error) + // Deprecated: the method will be removed in the next major version, + // use a DeleteRequest object + Do() instead. DeleteTyped(space, index interface{}, key interface{}, result interface{}) (err error) + // Deprecated: the method will be removed in the next major version, + // use a UpdateRequest object + Do() instead. UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) (err error) + // Deprecated: the method will be removed in the next major version, + // use a CallRequest object + Do() instead. CallTyped(functionName string, args interface{}, result interface{}) (err error) + // Deprecated: the method will be removed in the next major version, + // use a Call16Request object + Do() instead. Call16Typed(functionName string, args interface{}, result interface{}) (err error) + // Deprecated: the method will be removed in the next major version, + // use a Call17Request object + Do() instead. Call17Typed(functionName string, args interface{}, result interface{}) (err error) + // Deprecated: the method will be removed in the next major version, + // use an EvalRequest object + Do() instead. EvalTyped(expr string, args interface{}, result interface{}) (err error) + // Deprecated: the method will be removed in the next major version, + // use an ExecuteRequest object + Do() instead. ExecuteTyped(expr string, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error) + // Deprecated: the method will be removed in the next major version, + // use a SelectRequest object + Do() instead. SelectAsync(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}) *Future + // Deprecated: the method will be removed in the next major version, + // use an InsertRequest object + Do() instead. InsertAsync(space interface{}, tuple interface{}) *Future + // Deprecated: the method will be removed in the next major version, + // use a ReplaceRequest object + Do() instead. ReplaceAsync(space interface{}, tuple interface{}) *Future + // Deprecated: the method will be removed in the next major version, + // use a DeleteRequest object + Do() instead. DeleteAsync(space, index interface{}, key interface{}) *Future + // Deprecated: the method will be removed in the next major version, + // use a UpdateRequest object + Do() instead. UpdateAsync(space, index interface{}, key, ops interface{}) *Future + // Deprecated: the method will be removed in the next major version, + // use a UpsertRequest object + Do() instead. UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *Future + // Deprecated: the method will be removed in the next major version, + // use a CallRequest object + Do() instead. CallAsync(functionName string, args interface{}) *Future + // Deprecated: the method will be removed in the next major version, + // use a Call16Request object + Do() instead. Call16Async(functionName string, args interface{}) *Future + // Deprecated: the method will be removed in the next major version, + // use a Call17Request object + Do() instead. Call17Async(functionName string, args interface{}) *Future + // Deprecated: the method will be removed in the next major version, + // use an EvalRequest object + Do() instead. EvalAsync(expr string, args interface{}) *Future + // Deprecated: the method will be removed in the next major version, + // use an ExecuteRequest object + Do() instead. ExecuteAsync(expr string, args interface{}) *Future - - NewPrepared(expr string) (*Prepared, error) - NewStream() (*Stream, error) - NewWatcher(key string, callback WatchCallback) (Watcher, error) - - Do(req Request) (fut *Future) } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 837fa2787..c9870abc6 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -489,7 +489,9 @@ func TestCrudGenerateData(t *testing.T) { for _, testCase := range testGenerateDataCases { t.Run(testCase.name, func(t *testing.T) { for i := 1010; i < 1020; i++ { - conn.Delete(spaceName, nil, []interface{}{uint(i)}) + req := tarantool.NewDeleteRequest(spaceName). + Key([]interface{}{uint(i)}) + conn.Do(req).Get() } resp, err := conn.Do(testCase.req).Get() @@ -499,7 +501,9 @@ func TestCrudGenerateData(t *testing.T) { testSelectGeneratedData(t, conn, testCase.expectedTuplesCount) for i := 1010; i < 1020; i++ { - conn.Delete(spaceName, nil, []interface{}{uint(i)}) + req := tarantool.NewDeleteRequest(spaceName). + Key([]interface{}{uint(i)}) + conn.Do(req).Get() } }) } @@ -516,7 +520,9 @@ func TestCrudProcessData(t *testing.T) { testCrudRequestCheck(t, testCase.req, resp, err, testCase.expectedRespLen) for i := 1010; i < 1020; i++ { - conn.Delete(spaceName, nil, []interface{}{uint(i)}) + req := tarantool.NewDeleteRequest(spaceName). + Key([]interface{}{uint(i)}) + conn.Do(req).Get() } }) } @@ -648,7 +654,9 @@ func TestBoolResult(t *testing.T) { } for i := 1010; i < 1020; i++ { - conn.Delete(spaceName, nil, []interface{}{uint(i)}) + req := tarantool.NewDeleteRequest(spaceName). + Key([]interface{}{uint(i)}) + conn.Do(req).Get() } } @@ -671,7 +679,9 @@ func TestNumberResult(t *testing.T) { } for i := 1010; i < 1020; i++ { - conn.Delete(spaceName, nil, []interface{}{uint(i)}) + req := tarantool.NewDeleteRequest(spaceName). + Key([]interface{}{uint(i)}) + conn.Do(req).Get() } } @@ -714,7 +724,9 @@ func TestBaseResult(t *testing.T) { } for i := 1010; i < 1020; i++ { - conn.Delete(spaceName, nil, []interface{}{uint(i)}) + req := tarantool.NewDeleteRequest(spaceName). + Key([]interface{}{uint(i)}) + conn.Do(req).Get() } } @@ -757,7 +769,9 @@ func TestManyResult(t *testing.T) { } for i := 1010; i < 1020; i++ { - conn.Delete(spaceName, nil, []interface{}{uint(i)}) + req := tarantool.NewDeleteRequest(spaceName). + Key([]interface{}{uint(i)}) + conn.Do(req).Get() } } diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index a47126049..e2f707f6f 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -406,8 +406,9 @@ func TestDatetimeTarantoolInterval(t *testing.T) { for _, dtj := range datetimes { t.Run(fmt.Sprintf("%s_to_%s", dti.ToTime(), dtj.ToTime()), func(t *testing.T) { - resp, err := conn.Call17("call_datetime_interval", - []interface{}{dti, dtj}) + req := NewCallRequest("call_datetime_interval"). + Args([]interface{}{dti, dtj}) + resp, err := conn.Do(req).Get() if err != nil { t.Fatalf("Unable to call call_datetime_interval: %s", err) } @@ -541,7 +542,8 @@ func TestCustomTimezone(t *testing.T) { t.Fatalf("Unable to create datetime: %s", err.Error()) } - resp, err := conn.Replace(spaceTuple1, []interface{}{dt, "payload"}) + req := NewReplaceRequest(spaceTuple1).Tuple([]interface{}{dt, "payload"}) + resp, err := conn.Do(req).Get() if err != nil { t.Fatalf("Datetime replace failed %s", err.Error()) } @@ -558,7 +560,8 @@ func TestCustomTimezone(t *testing.T) { t.Fatalf("Expected offset %d instead of %d", customOffset, offset) } - _, err = conn.Delete(spaceTuple1, 0, []interface{}{dt}) + req := NewDeleteRequest(spaceTuple1).Key([]interface{}{dt}) + _, err = conn.Do(req).Get() if err != nil { t.Fatalf("Datetime delete failed: %s", err.Error()) } @@ -577,7 +580,8 @@ func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) { } // Insert tuple with datetime. - _, err = conn.Insert(spaceTuple1, []interface{}{dt, "payload"}) + ins := NewInsertRequest(spaceTuple1).Tuple([]interface{}{dt, "payload"}) + _, err = conn.Do(ins).Get() if err != nil { t.Fatalf("Datetime insert failed: %s", err.Error()) } @@ -585,7 +589,13 @@ func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) { // Select tuple with datetime. var offset uint32 = 0 var limit uint32 = 1 - resp, err := conn.Select(spaceTuple1, index, offset, limit, IterEq, []interface{}{dt}) + sel := NewSelectRequest(spaceTuple1). + Index(index). + Offset(offset). + Limit(limit). + Iterator(IterEq). + Key([]interface{}{dt}) + resp, err := conn.Do(sel).Get() if err != nil { t.Fatalf("Datetime select failed: %s", err.Error()) } @@ -595,7 +605,8 @@ func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) { assertDatetimeIsEqual(t, resp.Data, tm) // Delete tuple with datetime. - resp, err = conn.Delete(spaceTuple1, index, []interface{}{dt}) + del := NewDeleteRequest(spaceTuple1).Index(index).Key([]interface{}{dt}) + resp, err = conn.Do(del).Get() if err != nil { t.Fatalf("Datetime delete failed: %s", err.Error()) } @@ -738,7 +749,8 @@ func TestDatetimeReplace(t *testing.T) { if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) } - resp, err := conn.Replace(spaceTuple1, []interface{}{dt, "payload"}) + rep := NewReplaceRequest(spaceTuple1).Tuple([]interface{}{dt, "payload"}) + resp, err := conn.Do(rep).Get() if err != nil { t.Fatalf("Datetime replace failed: %s", err) } @@ -747,7 +759,12 @@ func TestDatetimeReplace(t *testing.T) { } assertDatetimeIsEqual(t, resp.Data, tm) - resp, err = conn.Select(spaceTuple1, index, 0, 1, IterEq, []interface{}{dt}) + sel := NewSelectRequest(spaceTuple1). + Index(index). + Limit(1). + Iterator(IterEq). + Key([]interface{}{dt}) + resp, err = conn.Do(sel).Get() if err != nil { t.Fatalf("Datetime select failed: %s", err) } @@ -757,7 +774,8 @@ func TestDatetimeReplace(t *testing.T) { assertDatetimeIsEqual(t, resp.Data, tm) // Delete tuple with datetime. - _, err = conn.Delete(spaceTuple1, index, []interface{}{dt}) + del := NewDeleteRequest(spaceTuple1).Index(index).Key([]interface{}{dt}) + _, err = conn.Do(del).Get() if err != nil { t.Fatalf("Datetime delete failed: %s", err.Error()) } @@ -907,7 +925,8 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { {*dt2, "Moscow"}, }, } - resp, err := conn.Replace(spaceTuple2, &tuple) + rep := NewReplaceRequest(spaceTuple2).Tuple(&tuple) + resp, err := conn.Do(rep).Get() if err != nil || resp.Code != 0 { t.Fatalf("Failed to replace: %s", err.Error()) } @@ -921,7 +940,8 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { } // Delete the tuple. - _, err = conn.Delete(spaceTuple2, index, []interface{}{cid}) + del := NewDeleteRequest(spaceTuple2).Index(index).Key([]interface{}{cid}) + _, err = conn.Do(del).Get() if err != nil { t.Fatalf("Datetime delete failed: %s", err.Error()) } @@ -955,21 +975,22 @@ func TestCustomDecodeFunction(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - // Call function 'call_datetime_testdata' returning a table of custom tuples. - var tuples []Tuple2 - err := conn.Call16Typed("call_datetime_testdata", []interface{}{1}, &tuples) + // Call function 'call_datetime_testdata' returning a custom tuples. + var tuple [][]Tuple2 + call := NewCallRequest("call_datetime_testdata").Args([]interface{}{1}) + err := conn.Do(call).GetTyped(&tuple) if err != nil { t.Fatalf("Failed to CallTyped: %s", err.Error()) } - if cid := tuples[0].Cid; cid != 5 { + if cid := tuple[0][0].Cid; cid != 5 { t.Fatalf("Wrong Cid (%d), should be 5", cid) } - if orig := tuples[0].Orig; orig != "Go!" { + if orig := tuple[0][0].Orig; orig != "Go!" { t.Fatalf("Wrong Orig (%s), should be 'Hello, there!'", orig) } - events := tuples[0].Events + events := tuple[0][0].Events if len(events) != 3 { t.Fatalf("Wrong a number of Events (%d), should be 3", len(events)) } @@ -1003,12 +1024,19 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) { if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) } - _, err = conn.Insert(spaceTuple1, []interface{}{dt}) + + ins := NewInsertRequest(spaceTuple1).Tuple([]interface{}{dt}) + _, err = conn.Do(ins).Get() if err != nil { t.Fatalf("Datetime insert failed: %s", err.Error()) } - resp, errSel := conn.Select(spaceTuple1, index, 0, 1, IterEq, []interface{}{dt}) + sel := NewSelectRequest(spaceTuple1). + Index(index). + Limit(1). + Iterator(IterEq). + Key([]interface{}{dt}) + resp, errSel := conn.Do(sel).Get() if errSel != nil { t.Errorf("Failed to Select: %s", errSel.Error()) } @@ -1021,7 +1049,8 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) { } // Teardown: delete a value. - _, err = conn.Delete(spaceTuple1, index, []interface{}{dt}) + del := NewDeleteRequest(spaceTuple1).Index(index).Key([]interface{}{dt}) + _, err = conn.Do(del).Get() if err != nil { t.Fatalf("Datetime delete failed: %s", err.Error()) } diff --git a/datetime/example_test.go b/datetime/example_test.go index 1faa5b24d..5fb1e9ba8 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -45,7 +45,9 @@ func Example() { index := "primary" // Replace a tuple with datetime. - resp, err := conn.Replace(space, []interface{}{dt}) + resp, err := conn.Do(tarantool.NewReplaceRequest(space). + Tuple([]interface{}{dt}), + ).Get() if err != nil { fmt.Printf("Error in replace is %v", err) return @@ -58,7 +60,13 @@ func Example() { // Select a tuple with datetime. var offset uint32 = 0 var limit uint32 = 1 - resp, err = conn.Select(space, index, offset, limit, tarantool.IterEq, []interface{}{dt}) + resp, err = conn.Do(tarantool.NewSelectRequest(space). + Index(index). + Offset(offset). + Limit(limit). + Iterator(tarantool.IterEq). + Key([]interface{}{dt}), + ).Get() if err != nil { fmt.Printf("Error in select is %v", err) return @@ -69,7 +77,10 @@ func Example() { fmt.Printf("Data: %v\n", respDt.ToTime()) // Delete a tuple with datetime. - resp, err = conn.Delete(space, index, []interface{}{dt}) + resp, err = conn.Do(tarantool.NewDeleteRequest(space). + Index(index). + Key([]interface{}{dt}), + ).Get() if err != nil { fmt.Printf("Error in delete is %v", err) return diff --git a/datetime/interval_test.go b/datetime/interval_test.go index aa43f2bab..ae42e92c7 100644 --- a/datetime/interval_test.go +++ b/datetime/interval_test.go @@ -5,6 +5,7 @@ import ( "reflect" "testing" + "github.com/tarantool/go-tarantool/v2" . "github.com/tarantool/go-tarantool/v2/datetime" "github.com/tarantool/go-tarantool/v2/test_helpers" ) @@ -118,7 +119,9 @@ func TestIntervalTarantoolEncoding(t *testing.T) { } for _, tc := range cases { t.Run(fmt.Sprintf("%v", tc), func(t *testing.T) { - resp, err := conn.Call17("call_interval_testdata", []interface{}{tc}) + req := tarantool.NewCallRequest("call_interval_testdata"). + Args([]interface{}{tc}) + resp, err := conn.Do(req).Get() if err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 97b929ca2..e3eb867c2 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -502,7 +502,8 @@ func TestSelect(t *testing.T) { t.Fatalf("Failed to prepare test decimal: %s", err) } - resp, err := conn.Insert(space, []interface{}{NewDecimal(number)}) + ins := NewInsertRequest(space).Tuple([]interface{}{NewDecimal(number)}) + resp, err := conn.Do(ins).Get() if err != nil { t.Fatalf("Decimal insert failed: %s", err) } @@ -513,7 +514,13 @@ func TestSelect(t *testing.T) { var offset uint32 = 0 var limit uint32 = 1 - resp, err = conn.Select(space, index, offset, limit, IterEq, []interface{}{NewDecimal(number)}) + sel := NewSelectRequest(space). + Index(index). + Offset(offset). + Limit(limit). + Iterator(IterEq). + Key([]interface{}{NewDecimal(number)}) + resp, err = conn.Do(sel).Get() if err != nil { t.Fatalf("Decimal select failed: %s", err.Error()) } @@ -522,7 +529,8 @@ func TestSelect(t *testing.T) { } tupleValueIsDecimal(t, resp.Data, number) - resp, err = conn.Delete(space, index, []interface{}{NewDecimal(number)}) + del := NewDeleteRequest(space).Index(index).Key([]interface{}{NewDecimal(number)}) + resp, err = conn.Do(del).Get() if err != nil { t.Fatalf("Decimal delete failed: %s", err) } @@ -535,7 +543,8 @@ func assertInsert(t *testing.T, conn *Connection, numString string) { t.Fatalf("Failed to prepare test decimal: %s", err) } - resp, err := conn.Insert(space, []interface{}{NewDecimal(number)}) + ins := NewInsertRequest(space).Tuple([]interface{}{NewDecimal(number)}) + resp, err := conn.Do(ins).Get() if err != nil { t.Fatalf("Decimal insert failed: %s", err) } @@ -544,7 +553,8 @@ func assertInsert(t *testing.T, conn *Connection, numString string) { } tupleValueIsDecimal(t, resp.Data, number) - resp, err = conn.Delete(space, index, []interface{}{NewDecimal(number)}) + del := NewDeleteRequest(space).Index(index).Key([]interface{}{NewDecimal(number)}) + resp, err = conn.Do(del).Get() if err != nil { t.Fatalf("Decimal delete failed: %s", err) } @@ -576,7 +586,8 @@ func TestReplace(t *testing.T) { t.Fatalf("Failed to prepare test decimal: %s", err) } - respRep, errRep := conn.Replace(space, []interface{}{NewDecimal(number)}) + rep := NewReplaceRequest(space).Tuple([]interface{}{NewDecimal(number)}) + respRep, errRep := conn.Do(rep).Get() if errRep != nil { t.Fatalf("Decimal replace failed: %s", errRep) } @@ -585,7 +596,12 @@ func TestReplace(t *testing.T) { } tupleValueIsDecimal(t, respRep.Data, number) - respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{NewDecimal(number)}) + sel := NewSelectRequest(space). + Index(index). + Limit(1). + Iterator(IterEq). + Key([]interface{}{NewDecimal(number)}) + respSel, errSel := conn.Do(sel).Get() if errSel != nil { t.Fatalf("Decimal select failed: %s", errSel) } diff --git a/decimal/example_test.go b/decimal/example_test.go index cfa43c166..c57cc0603 100644 --- a/decimal/example_test.go +++ b/decimal/example_test.go @@ -40,7 +40,9 @@ func Example() { log.Fatalf("Failed to prepare test decimal: %s", err) } - resp, err := client.Replace(spaceNo, []interface{}{number}) + resp, err := client.Do(tarantool.NewReplaceRequest(spaceNo). + Tuple([]interface{}{number}), + ).Get() if err != nil { log.Fatalf("Decimal replace failed: %s", err) } diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index 5bac7ae62..1189e16a3 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -93,7 +93,9 @@ func Example_customUnpacking() { indexNo := uint32(0) tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} - resp, err := conn.Replace(spaceNo, &tuple) // NOTE: insert a structure itself. + // Insert a structure itself. + initReq := tarantool.NewReplaceRequest(spaceNo).Tuple(&tuple) + resp, err := conn.Do(initReq).Get() if err != nil { log.Fatalf("Failed to insert: %s", err.Error()) return @@ -102,7 +104,12 @@ func Example_customUnpacking() { fmt.Println("Code", resp.Code) var tuples1 []Tuple2 - err = conn.SelectTyped(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{777}, &tuples1) + selectReq := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{777}) + err = conn.Do(selectReq).GetTyped(&tuples1) if err != nil { log.Fatalf("Failed to SelectTyped: %s", err.Error()) return @@ -111,7 +118,7 @@ func Example_customUnpacking() { // Same result in a "magic" way. var tuples2 []Tuple3 - err = conn.SelectTyped(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{777}, &tuples2) + err = conn.Do(selectReq).GetTyped(&tuples2) if err != nil { log.Fatalf("Failed to SelectTyped: %s", err.Error()) return @@ -120,7 +127,8 @@ func Example_customUnpacking() { // Call a function "func_name" returning a table of custom tuples. var tuples3 [][]Tuple3 - err = conn.Call17Typed("func_name", []interface{}{}, &tuples3) + callReq := tarantool.NewCallRequest("func_name") + err = conn.Do(callReq).GetTyped(&tuples3) if err != nil { log.Fatalf("Failed to CallTyped: %s", err.Error()) return diff --git a/example_test.go b/example_test.go index 60106001e..70d499820 100644 --- a/example_test.go +++ b/example_test.go @@ -18,7 +18,7 @@ type Tuple struct { Name string } -func example_connect(opts tarantool.Opts) *tarantool.Connection { +func exampleConnect(opts tarantool.Opts) *tarantool.Connection { conn, err := tarantool.Connect(server, opts) if err != nil { panic("Connection is not established: " + err.Error()) @@ -44,215 +44,341 @@ func ExampleSslOpts() { } } -func ExampleConnection_Select() { - conn := example_connect(opts) - defer conn.Close() - - conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) - conn.Replace(spaceNo, []interface{}{uint(1112), "hallo", "werld"}) - - resp, err := conn.Select(617, 0, 0, 100, tarantool.IterEq, []interface{}{uint(1111)}) - - if err != nil { - fmt.Printf("error in select is %v", err) - return - } - fmt.Printf("response is %#v\n", resp.Data) - resp, err = conn.Select("test", "primary", 0, 100, tarantool.IterEq, tarantool.IntKey{1111}) - if err != nil { - fmt.Printf("error in select is %v", err) - return - } - fmt.Printf("response is %#v\n", resp.Data) - - // Output: - // response is []interface {}{[]interface {}{0x457, "hello", "world"}} - // response is []interface {}{[]interface {}{0x457, "hello", "world"}} -} - -func ExampleConnection_SelectTyped() { - conn := example_connect(opts) - defer conn.Close() - var res []Tuple - - err := conn.SelectTyped(617, 0, 0, 100, tarantool.IterEq, tarantool.IntKey{1111}, &res) - - if err != nil { - fmt.Printf("error in select is %v", err) - return - } - fmt.Printf("response is %v\n", res) - err = conn.SelectTyped("test", "primary", 0, 100, tarantool.IterEq, tarantool.IntKey{1111}, &res) - if err != nil { - fmt.Printf("error in select is %v", err) - return - } - fmt.Printf("response is %v\n", res) - // Output: - // response is [{{} 1111 hello world}] - // response is [{{} 1111 hello world}] -} - -func ExampleConnection_SelectAsync() { - conn := example_connect(opts) - defer conn.Close() - spaceNo := uint32(617) - - conn.Insert(spaceNo, []interface{}{uint(16), "test", "one"}) - conn.Insert(spaceNo, []interface{}{uint(17), "test", "one"}) - conn.Insert(spaceNo, []interface{}{uint(18), "test", "one"}) - - var futs [3]*tarantool.Future - futs[0] = conn.SelectAsync("test", "primary", 0, 2, tarantool.IterLe, tarantool.UintKey{16}) - futs[1] = conn.SelectAsync("test", "primary", 0, 1, tarantool.IterEq, tarantool.UintKey{17}) - futs[2] = conn.SelectAsync("test", "primary", 0, 1, tarantool.IterEq, tarantool.UintKey{18}) - var t []Tuple - err := futs[0].GetTyped(&t) - fmt.Println("Future", 0, "Error", err) - fmt.Println("Future", 0, "Data", t) - - resp, err := futs[1].Get() - fmt.Println("Future", 1, "Error", err) - fmt.Println("Future", 1, "Data", resp.Data) - - resp, err = futs[2].Get() - fmt.Println("Future", 2, "Error", err) - fmt.Println("Future", 2, "Data", resp.Data) - // Output: - // Future 0 Error - // Future 0 Data [{{} 16 val 16 bla} {{} 15 val 15 bla}] - // Future 1 Error - // Future 1 Data [[17 val 17 bla]] - // Future 2 Error - // Future 2 Data [[18 val 18 bla]] -} - -func ExampleConnection_GetTyped() { - conn := example_connect(opts) - defer conn.Close() - - const space = "test" - const index = "primary" - conn.Replace(space, []interface{}{uint(1111), "hello", "world"}) - - var t Tuple - err := conn.GetTyped(space, index, []interface{}{1111}, &t) - fmt.Println("Error", err) - fmt.Println("Data", t) - // Output: - // Error - // Data {{} 1111 hello world} -} - func ExampleIntKey() { - conn := example_connect(opts) + conn := exampleConnect(opts) defer conn.Close() const space = "test" const index = "primary" - conn.Replace(space, []interface{}{int(1111), "hello", "world"}) + tuple := []interface{}{int(1111), "hello", "world"} + conn.Do(tarantool.NewReplaceRequest(space).Tuple(tuple)).Get() - var t Tuple - err := conn.GetTyped(space, index, tarantool.IntKey{1111}, &t) + var t []Tuple + err := conn.Do(tarantool.NewSelectRequest(space). + Index(index). + Iterator(tarantool.IterEq). + Key(tarantool.IntKey{1111}), + ).GetTyped(&t) fmt.Println("Error", err) fmt.Println("Data", t) // Output: // Error - // Data {{} 1111 hello world} + // Data [{{} 1111 hello world}] } func ExampleUintKey() { - conn := example_connect(opts) + conn := exampleConnect(opts) defer conn.Close() const space = "test" const index = "primary" - conn.Replace(space, []interface{}{uint(1111), "hello", "world"}) + tuple := []interface{}{uint(1111), "hello", "world"} + conn.Do(tarantool.NewReplaceRequest(space).Tuple(tuple)).Get() - var t Tuple - err := conn.GetTyped(space, index, tarantool.UintKey{1111}, &t) + var t []Tuple + err := conn.Do(tarantool.NewSelectRequest(space). + Index(index). + Iterator(tarantool.IterEq). + Key(tarantool.UintKey{1111}), + ).GetTyped(&t) fmt.Println("Error", err) fmt.Println("Data", t) // Output: // Error - // Data {{} 1111 hello world} + // Data [{{} 1111 hello world}] } func ExampleStringKey() { - conn := example_connect(opts) + conn := exampleConnect(opts) defer conn.Close() const space = "teststring" const index = "primary" - conn.Replace(space, []interface{}{"any", []byte{0x01, 0x02}}) + tuple := []interface{}{"any", []byte{0x01, 0x02}} + conn.Do(tarantool.NewReplaceRequest(space).Tuple(tuple)).Get() - t := struct { + t := []struct { Key string Value []byte }{} - err := conn.GetTyped(space, index, tarantool.StringKey{"any"}, &t) + err := conn.Do(tarantool.NewSelectRequest(space). + Index(index). + Iterator(tarantool.IterEq). + Key(tarantool.StringKey{"any"}), + ).GetTyped(&t) fmt.Println("Error", err) fmt.Println("Data", t) // Output: // Error - // Data {any [1 2]} + // Data [{any [1 2]}] } func ExampleIntIntKey() { - conn := example_connect(opts) + conn := exampleConnect(opts) defer conn.Close() const space = "testintint" const index = "primary" - conn.Replace(space, []interface{}{1, 2, "foo"}) + tuple := []interface{}{1, 2, "foo"} + conn.Do(tarantool.NewReplaceRequest(space).Tuple(tuple)).Get() - t := struct { + t := []struct { Key1 int Key2 int Value string }{} - err := conn.GetTyped(space, index, tarantool.IntIntKey{1, 2}, &t) + err := conn.Do(tarantool.NewSelectRequest(space). + Index(index). + Iterator(tarantool.IterEq). + Key(tarantool.IntIntKey{1, 2}), + ).GetTyped(&t) fmt.Println("Error", err) fmt.Println("Data", t) // Output: // Error - // Data {1 2 foo} + // Data [{1 2 foo}] +} + +func ExamplePingRequest() { + conn := exampleConnect(opts) + defer conn.Close() + + // Ping a Tarantool instance to check connection. + resp, err := conn.Do(tarantool.NewPingRequest()).Get() + fmt.Println("Ping Code", resp.Code) + fmt.Println("Ping Data", resp.Data) + fmt.Println("Ping Error", err) + // Output: + // Ping Code 0 + // Ping Data [] + // Ping Error +} + +// To pass contexts to request objects, use the Context() method. +// Pay attention that when using context with request objects, +// the timeout option for Connection will not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func ExamplePingRequest_Context() { + conn := exampleConnect(opts) + defer conn.Close() + + timeout := time.Nanosecond + + // This way you may set the a common timeout for requests with a context. + rootCtx, cancelRoot := context.WithTimeout(context.Background(), timeout) + defer cancelRoot() + + // This context will be canceled with the root after commonTimeout. + ctx, cancel := context.WithCancel(rootCtx) + defer cancel() + + req := tarantool.NewPingRequest().Context(ctx) + + // Ping a Tarantool instance to check connection. + resp, err := conn.Do(req).Get() + fmt.Println("Ping Resp", resp) + fmt.Println("Ping Error", err) + // Output: + // Ping Resp + // Ping Error context is done } func ExampleSelectRequest() { - conn := example_connect(opts) + conn := exampleConnect(opts) defer conn.Close() - req := tarantool.NewSelectRequest(617). + for i := 1111; i <= 1112; i++ { + conn.Do(tarantool.NewReplaceRequest(spaceNo). + Tuple([]interface{}{uint(i), "hello", "world"}), + ).Get() + } + + key := []interface{}{uint(1111)} + resp, err := conn.Do(tarantool.NewSelectRequest(617). Limit(100). - Key(tarantool.IntKey{1111}) - resp, err := conn.Do(req).Get() + Iterator(tarantool.IterEq). + Key(key), + ).Get() + if err != nil { - fmt.Printf("error in do select request is %v", err) + fmt.Printf("error in select is %v", err) return } fmt.Printf("response is %#v\n", resp.Data) - req = tarantool.NewSelectRequest("test"). + var res []Tuple + err = conn.Do(tarantool.NewSelectRequest("test"). Index("primary"). Limit(100). - Key(tarantool.IntKey{1111}) - fut := conn.Do(req) - resp, err = fut.Get() + Iterator(tarantool.IterEq). + Key(key), + ).GetTyped(&res) if err != nil { - fmt.Printf("error in do async select request is %v", err) + fmt.Printf("error in select is %v", err) return } - fmt.Printf("response is %#v\n", resp.Data) + fmt.Printf("response is %v\n", res) + // Output: // response is []interface {}{[]interface {}{0x457, "hello", "world"}} - // response is []interface {}{[]interface {}{0x457, "hello", "world"}} + // response is [{{} 1111 hello world}] +} + +func ExampleInsertRequest() { + conn := exampleConnect(opts) + defer conn.Close() + + // Insert a new tuple { 31, 1 }. + resp, err := conn.Do(tarantool.NewInsertRequest(spaceNo). + Tuple([]interface{}{uint(31), "test", "one"}), + ).Get() + fmt.Println("Insert 31") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Insert a new tuple { 32, 1 }. + resp, err = conn.Do(tarantool.NewInsertRequest("test"). + Tuple(&Tuple{Id: 32, Msg: "test", Name: "one"}), + ).Get() + fmt.Println("Insert 32") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + + // Delete tuple with primary key { 31 }. + conn.Do(tarantool.NewDeleteRequest("test"). + Index("primary"). + Key([]interface{}{uint(31)}), + ).Get() + // Delete tuple with primary key { 32 }. + conn.Do(tarantool.NewDeleteRequest("test"). + Index(indexNo). + Key([]interface{}{uint(31)}), + ).Get() + // Output: + // Insert 31 + // Error + // Code 0 + // Data [[31 test one]] + // Insert 32 + // Error + // Code 0 + // Data [[32 test one]] +} + +func ExampleDeleteRequest() { + conn := exampleConnect(opts) + defer conn.Close() + + // Insert a new tuple { 35, 1 }. + conn.Do(tarantool.NewInsertRequest(spaceNo). + Tuple([]interface{}{uint(35), "test", "one"}), + ).Get() + // Insert a new tuple { 36, 1 }. + conn.Do(tarantool.NewInsertRequest("test"). + Tuple(&Tuple{Id: 36, Msg: "test", Name: "one"}), + ).Get() + + // Delete tuple with primary key { 35 }. + resp, err := conn.Do(tarantool.NewDeleteRequest(spaceNo). + Index(indexNo). + Key([]interface{}{uint(35)}), + ).Get() + fmt.Println("Delete 35") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + + // Delete tuple with primary key { 36 }. + resp, err = conn.Do(tarantool.NewDeleteRequest("test"). + Index("primary"). + Key([]interface{}{uint(36)}), + ).Get() + fmt.Println("Delete 36") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Output: + // Delete 35 + // Error + // Code 0 + // Data [[35 test one]] + // Delete 36 + // Error + // Code 0 + // Data [[36 test one]] +} + +func ExampleReplaceRequest() { + conn := exampleConnect(opts) + defer conn.Close() + + // Insert a new tuple { 13, 1 }. + conn.Do(tarantool.NewInsertRequest(spaceNo). + Tuple([]interface{}{uint(13), "test", "one"}), + ).Get() + + // Replace a tuple with primary key 13. + // Note, Tuple is defined within tests, and has EncdodeMsgpack and + // DecodeMsgpack methods. + resp, err := conn.Do(tarantool.NewReplaceRequest(spaceNo). + Tuple([]interface{}{uint(13), 1}), + ).Get() + fmt.Println("Replace 13") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + resp, err = conn.Do(tarantool.NewReplaceRequest("test"). + Tuple([]interface{}{uint(13), 1}), + ).Get() + fmt.Println("Replace 13") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + resp, err = conn.Do(tarantool.NewReplaceRequest("test"). + Tuple(&Tuple{Id: 13, Msg: "test", Name: "eleven"}), + ).Get() + fmt.Println("Replace 13") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + resp, err = conn.Do(tarantool.NewReplaceRequest("test"). + Tuple(&Tuple{Id: 13, Msg: "test", Name: "twelve"}), + ).Get() + fmt.Println("Replace 13") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Output: + // Replace 13 + // Error + // Code 0 + // Data [[13 1]] + // Replace 13 + // Error + // Code 0 + // Data [[13 1]] + // Replace 13 + // Error + // Code 0 + // Data [[13 test eleven]] + // Replace 13 + // Error + // Code 0 + // Data [[13 test twelve]] } func ExampleUpdateRequest() { - conn := example_connect(opts) + conn := exampleConnect(opts) defer conn.Close() + for i := 1111; i <= 1112; i++ { + conn.Do(tarantool.NewReplaceRequest(spaceNo). + Tuple([]interface{}{uint(i), "hello", "world"}), + ).Get() + } + req := tarantool.NewUpdateRequest(617). Key(tarantool.IntKey{1111}). Operations(tarantool.NewOperations().Assign(1, "bye")) @@ -280,7 +406,7 @@ func ExampleUpdateRequest() { } func ExampleUpsertRequest() { - conn := example_connect(opts) + conn := exampleConnect(opts) defer conn.Close() var req tarantool.Request @@ -305,23 +431,189 @@ func ExampleUpsertRequest() { } fmt.Printf("response is %#v\n", resp.Data) - req = tarantool.NewSelectRequest(617). - Limit(100). - Key(tarantool.IntKey{1113}) + req = tarantool.NewSelectRequest(617). + Limit(100). + Key(tarantool.IntKey{1113}) + resp, err = conn.Do(req).Get() + if err != nil { + fmt.Printf("error in do select request is %v", err) + return + } + fmt.Printf("response is %#v\n", resp.Data) + // Output: + // response is []interface {}{} + // response is []interface {}{} + // response is []interface {}{[]interface {}{0x459, "first", "updated"}} +} + +func ExampleCallRequest() { + conn := exampleConnect(opts) + defer conn.Close() + + // Call a function 'simple_concat' with arguments. + resp, err := conn.Do(tarantool.NewCallRequest("simple_concat"). + Args([]interface{}{"1"}), + ).Get() + fmt.Println("Call simple_concat()") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Output: + // Call simple_concat() + // Error + // Code 0 + // Data [11] +} + +func ExampleEvalRequest() { + conn := exampleConnect(opts) + defer conn.Close() + + // Run raw Lua code. + resp, err := conn.Do(tarantool.NewEvalRequest("return 1 + 2")).Get() + fmt.Println("Eval 'return 1 + 2'") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + // Output: + // Eval 'return 1 + 2' + // Error + // Code 0 + // Data [3] +} + +// To use SQL to query a tarantool instance, use ExecuteRequest. +// +// Pay attention that with different types of queries (DDL, DQL, DML etc.) +// some fields of the response structure (MetaData and InfoAutoincrementIds +// in SQLInfo) may be nil. +func ExampleExecuteRequest() { + // Tarantool supports SQL since version 2.0.0 + isLess, _ := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if isLess { + return + } + + conn := exampleConnect(opts) + defer conn.Close() + + req := tarantool.NewExecuteRequest( + "CREATE TABLE SQL_TEST (id INTEGER PRIMARY KEY, name STRING)") + resp, err := conn.Do(req).Get() + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // There are 4 options to pass named parameters to an SQL query: + // 1) The simple map; + sqlBind1 := map[string]interface{}{ + "id": 1, + "name": "test", + } + + // 2) Any type of structure; + sqlBind2 := struct { + Id int + Name string + }{1, "test"} + + // 3) It is possible to use []tarantool.KeyValueBind; + sqlBind3 := []interface{}{ + tarantool.KeyValueBind{Key: "id", Value: 1}, + tarantool.KeyValueBind{Key: "name", Value: "test"}, + } + + // 4) []interface{} slice with tarantool.KeyValueBind items inside; + sqlBind4 := []tarantool.KeyValueBind{ + {"id", 1}, + {"name", "test"}, + } + + // 1) + req = tarantool.NewExecuteRequest( + "CREATE TABLE SQL_TEST (id INTEGER PRIMARY KEY, name STRING)") + req = req.Args(sqlBind1) + resp, err = conn.Do(req).Get() + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // 2) + req = req.Args(sqlBind2) + resp, err = conn.Do(req).Get() + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // 3) + req = req.Args(sqlBind3) + resp, err = conn.Do(req).Get() + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // 4) + req = req.Args(sqlBind4) + resp, err = conn.Do(req).Get() + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // The way to pass positional arguments to an SQL query. + req = tarantool.NewExecuteRequest( + "SELECT id FROM SQL_TEST WHERE id=? AND name=?"). + Args([]interface{}{2, "test"}) + resp, err = conn.Do(req).Get() + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // The way to pass SQL expression with using custom packing/unpacking for + // a type. + var res []Tuple + req = tarantool.NewExecuteRequest( + "SELECT id, name, name FROM SQL_TEST WHERE id=?"). + Args([]interface{}{2}) + err = conn.Do(req).GetTyped(&res) + fmt.Println("ExecuteTyped") + fmt.Println("Error", err) + fmt.Println("Data", res) + + // For using different types of parameters (positioned/named), collect all + // items in []interface{}. + // All "named" items must be passed with tarantool.KeyValueBind{}. + req = tarantool.NewExecuteRequest( + "SELECT id FROM SQL_TEST WHERE id=? AND name=?"). + Args([]interface{}{tarantool.KeyValueBind{"id", 1}, "test"}) resp, err = conn.Do(req).Get() - if err != nil { - fmt.Printf("error in do select request is %v", err) - return - } - fmt.Printf("response is %#v\n", resp.Data) - // Output: - // response is []interface {}{} - // response is []interface {}{} - // response is []interface {}{[]interface {}{0x459, "first", "updated"}} + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) } func ExampleProtocolVersion() { - conn := example_connect(opts) + conn := exampleConnect(opts) defer conn.Close() clientProtocolInfo := conn.ClientProtocolInfo() @@ -359,7 +651,7 @@ func ExampleCommitRequest() { } txnOpts := getTestTxnOpts() - conn := example_connect(txnOpts) + conn := exampleConnect(txnOpts) defer conn.Close() stream, _ := conn.NewStream() @@ -436,7 +728,7 @@ func ExampleRollbackRequest() { } txnOpts := getTestTxnOpts() - conn := example_connect(txnOpts) + conn := exampleConnect(txnOpts) defer conn.Close() stream, _ := conn.NewStream() @@ -485,316 +777,133 @@ func ExampleRollbackRequest() { // Rollback transaction req = tarantool.NewRollbackRequest() - resp, err = stream.Do(req).Get() - if err != nil { - fmt.Printf("Failed to Rollback: %s", err.Error()) - return - } - fmt.Printf("Rollback transaction: response is %#v\n", resp.Code) - - // Select outside of transaction - resp, err = conn.Do(selectReq).Get() - if err != nil { - fmt.Printf("Failed to Select: %s", err.Error()) - return - } - fmt.Printf("Select after Rollback: response is %#v\n", resp.Data) -} - -func ExampleBeginRequest_TxnIsolation() { - var req tarantool.Request - var resp *tarantool.Response - var err error - - // Tarantool supports streams and interactive transactions since version 2.10.0 - isLess, _ := test_helpers.IsTarantoolVersionLess(2, 10, 0) - if err != nil || isLess { - return - } - - txnOpts := getTestTxnOpts() - conn := example_connect(txnOpts) - defer conn.Close() - - stream, _ := conn.NewStream() - - // Begin transaction - req = tarantool.NewBeginRequest(). - TxnIsolation(tarantool.ReadConfirmedLevel). - Timeout(500 * time.Millisecond) - resp, err = stream.Do(req).Get() - if err != nil { - fmt.Printf("Failed to Begin: %s", err.Error()) - return - } - fmt.Printf("Begin transaction: response is %#v\n", resp.Code) - - // Insert in stream - req = tarantool.NewInsertRequest(spaceName). - Tuple([]interface{}{uint(2001), "rollback_hello", "rollback_world"}) - resp, err = stream.Do(req).Get() - if err != nil { - fmt.Printf("Failed to Insert: %s", err.Error()) - return - } - fmt.Printf("Insert in stream: response is %#v\n", resp.Code) - - // Select not related to the transaction - // while transaction is not committed - // result of select is empty - selectReq := tarantool.NewSelectRequest(spaceNo). - Index(indexNo). - Limit(1). - Iterator(tarantool.IterEq). - Key([]interface{}{uint(2001)}) - resp, err = conn.Do(selectReq).Get() - if err != nil { - fmt.Printf("Failed to Select: %s", err.Error()) - return - } - fmt.Printf("Select out of stream: response is %#v\n", resp.Data) - - // Select in stream - resp, err = stream.Do(selectReq).Get() - if err != nil { - fmt.Printf("Failed to Select: %s", err.Error()) - return - } - fmt.Printf("Select in stream: response is %#v\n", resp.Data) - - // Rollback transaction - req = tarantool.NewRollbackRequest() - resp, err = stream.Do(req).Get() - if err != nil { - fmt.Printf("Failed to Rollback: %s", err.Error()) - return - } - fmt.Printf("Rollback transaction: response is %#v\n", resp.Code) - - // Select outside of transaction - resp, err = conn.Do(selectReq).Get() - if err != nil { - fmt.Printf("Failed to Select: %s", err.Error()) - return - } - fmt.Printf("Select after Rollback: response is %#v\n", resp.Data) -} - -func ExampleFuture_GetIterator() { - conn := example_connect(opts) - defer conn.Close() - - const timeout = 3 * time.Second - // Or any other Connection.*Async() call. - fut := conn.Call17Async("push_func", []interface{}{4}) - - var it tarantool.ResponseIterator - for it = fut.GetIterator().WithTimeout(timeout); it.Next(); { - resp := it.Value() - if resp.Code == tarantool.PushCode { - // It is a push message. - fmt.Printf("push message: %v\n", resp.Data[0]) - } else if resp.Code == tarantool.OkCode { - // It is a regular response. - fmt.Printf("response: %v", resp.Data[0]) - } else { - fmt.Printf("an unexpected response code %d", resp.Code) - } - } - if err := it.Err(); err != nil { - fmt.Printf("error in call of push_func is %v", err) - return - } - // Output: - // push message: 1 - // push message: 2 - // push message: 3 - // push message: 4 - // response: 4 -} - -func ExampleConnection_Ping() { - conn := example_connect(opts) - defer conn.Close() - - // Ping a Tarantool instance to check connection. - resp, err := conn.Ping() - fmt.Println("Ping Code", resp.Code) - fmt.Println("Ping Data", resp.Data) - fmt.Println("Ping Error", err) - // Output: - // Ping Code 0 - // Ping Data [] - // Ping Error -} - -func ExampleConnection_Insert() { - conn := example_connect(opts) - defer conn.Close() - - // Insert a new tuple { 31, 1 }. - resp, err := conn.Insert(spaceNo, []interface{}{uint(31), "test", "one"}) - fmt.Println("Insert 31") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - // Insert a new tuple { 32, 1 }. - resp, err = conn.Insert("test", &Tuple{Id: 32, Msg: "test", Name: "one"}) - fmt.Println("Insert 32") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - - // Delete tuple with primary key { 31 }. - conn.Delete("test", "primary", []interface{}{uint(31)}) - // Delete tuple with primary key { 32 }. - conn.Delete(spaceNo, indexNo, []interface{}{uint(32)}) - // Output: - // Insert 31 - // Error - // Code 0 - // Data [[31 test one]] - // Insert 32 - // Error - // Code 0 - // Data [[32 test one]] - -} - -func ExampleConnection_Delete() { - conn := example_connect(opts) - defer conn.Close() - - // Insert a new tuple { 35, 1 }. - conn.Insert(spaceNo, []interface{}{uint(35), "test", "one"}) - // Insert a new tuple { 36, 1 }. - conn.Insert("test", &Tuple{Id: 36, Msg: "test", Name: "one"}) - - // Delete tuple with primary key { 35 }. - resp, err := conn.Delete(spaceNo, indexNo, []interface{}{uint(35)}) - fmt.Println("Delete 35") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - - // Delete tuple with primary key { 36 }. - resp, err = conn.Delete("test", "primary", []interface{}{uint(36)}) - fmt.Println("Delete 36") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - // Output: - // Delete 35 - // Error - // Code 0 - // Data [[35 test one]] - // Delete 36 - // Error - // Code 0 - // Data [[36 test one]] -} - -func ExampleConnection_Replace() { - conn := example_connect(opts) - defer conn.Close() - - // Insert a new tuple { 13, 1 }. - conn.Insert(spaceNo, []interface{}{uint(13), "test", "one"}) - - // Replace a tuple with primary key 13. - // Note, Tuple is defined within tests, and has EncdodeMsgpack and - // DecodeMsgpack methods. - resp, err := conn.Replace(spaceNo, []interface{}{uint(13), 1}) - fmt.Println("Replace 13") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - resp, err = conn.Replace("test", []interface{}{uint(13), 1}) - fmt.Println("Replace 13") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - resp, err = conn.Replace("test", &Tuple{Id: 13, Msg: "test", Name: "eleven"}) - fmt.Println("Replace 13") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - resp, err = conn.Replace("test", &Tuple{Id: 13, Msg: "test", Name: "twelve"}) - fmt.Println("Replace 13") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - // Output: - // Replace 13 - // Error - // Code 0 - // Data [[13 1]] - // Replace 13 - // Error - // Code 0 - // Data [[13 1]] - // Replace 13 - // Error - // Code 0 - // Data [[13 test eleven]] - // Replace 13 - // Error - // Code 0 - // Data [[13 test twelve]] + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Rollback: %s", err.Error()) + return + } + fmt.Printf("Rollback transaction: response is %#v\n", resp.Code) + + // Select outside of transaction + resp, err = conn.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select after Rollback: response is %#v\n", resp.Data) } -func ExampleConnection_Update() { - conn := example_connect(opts) +func ExampleBeginRequest_TxnIsolation() { + var req tarantool.Request + var resp *tarantool.Response + var err error + + // Tarantool supports streams and interactive transactions since version 2.10.0 + isLess, _ := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil || isLess { + return + } + + txnOpts := getTestTxnOpts() + conn := exampleConnect(txnOpts) defer conn.Close() - // Insert a new tuple { 14, 1 }. - conn.Insert(spaceNo, []interface{}{uint(14), "test", "one"}) + stream, _ := conn.NewStream() - // Update tuple with primary key { 14 }. - resp, err := conn.Update(spaceName, indexName, []interface{}{uint(14)}, []interface{}{[]interface{}{"=", 1, "bye"}}) - fmt.Println("Update 14") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - // Output: - // Update 14 - // Error - // Code 0 - // Data [[14 bye bla]] -} + // Begin transaction + req = tarantool.NewBeginRequest(). + TxnIsolation(tarantool.ReadConfirmedLevel). + Timeout(500 * time.Millisecond) + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Begin: %s", err.Error()) + return + } + fmt.Printf("Begin transaction: response is %#v\n", resp.Code) -func ExampleConnection_Call() { - conn := example_connect(opts) - defer conn.Close() + // Insert in stream + req = tarantool.NewInsertRequest(spaceName). + Tuple([]interface{}{uint(2001), "rollback_hello", "rollback_world"}) + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Insert: %s", err.Error()) + return + } + fmt.Printf("Insert in stream: response is %#v\n", resp.Code) - // Call a function 'simple_concat' with arguments. - resp, err := conn.Call17("simple_concat", []interface{}{"1"}) - fmt.Println("Call simple_concat()") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - // Output: - // Call simple_concat() - // Error - // Code 0 - // Data [11] + // Select not related to the transaction + // while transaction is not committed + // result of select is empty + selectReq := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{uint(2001)}) + resp, err = conn.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select out of stream: response is %#v\n", resp.Data) + + // Select in stream + resp, err = stream.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select in stream: response is %#v\n", resp.Data) + + // Rollback transaction + req = tarantool.NewRollbackRequest() + resp, err = stream.Do(req).Get() + if err != nil { + fmt.Printf("Failed to Rollback: %s", err.Error()) + return + } + fmt.Printf("Rollback transaction: response is %#v\n", resp.Code) + + // Select outside of transaction + resp, err = conn.Do(selectReq).Get() + if err != nil { + fmt.Printf("Failed to Select: %s", err.Error()) + return + } + fmt.Printf("Select after Rollback: response is %#v\n", resp.Data) } -func ExampleConnection_Eval() { - conn := example_connect(opts) +func ExampleFuture_GetIterator() { + conn := exampleConnect(opts) defer conn.Close() - // Run raw Lua code. - resp, err := conn.Eval("return 1 + 2", []interface{}{}) - fmt.Println("Eval 'return 1 + 2'") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) + const timeout = 3 * time.Second + fut := conn.Do(tarantool.NewCallRequest("push_func"). + Args([]interface{}{4}), + ) + + var it tarantool.ResponseIterator + for it = fut.GetIterator().WithTimeout(timeout); it.Next(); { + resp := it.Value() + if resp.Code == tarantool.PushCode { + // It is a push message. + fmt.Printf("push message: %v\n", resp.Data[0]) + } else if resp.Code == tarantool.OkCode { + // It is a regular response. + fmt.Printf("response: %v", resp.Data[0]) + } else { + fmt.Printf("an unexpected response code %d", resp.Code) + } + } + if err := it.Err(); err != nil { + fmt.Printf("error in call of push_func is %v", err) + return + } // Output: - // Eval 'return 1 + 2' - // Error - // Code 0 - // Data [3] + // push message: 1 + // push message: 2 + // push message: 3 + // push message: 4 + // response: 4 } func ExampleConnect() { @@ -818,7 +927,7 @@ func ExampleConnect() { // Example demonstrates how to retrieve information with space schema. func ExampleSchema() { - conn := example_connect(opts) + conn := exampleConnect(opts) defer conn.Close() schema := conn.Schema @@ -840,7 +949,7 @@ func ExampleSchema() { // Example demonstrates how to retrieve information with space schema. func ExampleSpace() { - conn := example_connect(opts) + conn := exampleConnect(opts) defer conn.Close() // Save Schema to a local variable to avoid races @@ -883,126 +992,6 @@ func ExampleSpace() { // SpaceField 2 name3 unsigned } -// To use SQL to query a tarantool instance, call Execute. -// -// Pay attention that with different types of queries (DDL, DQL, DML etc.) -// some fields of the response structure (MetaData and InfoAutoincrementIds in SQLInfo) may be nil. -func ExampleConnection_Execute() { - // Tarantool supports SQL since version 2.0.0 - isLess, _ := test_helpers.IsTarantoolVersionLess(2, 0, 0) - if isLess { - return - } - server := "127.0.0.1:3013" - opts := tarantool.Opts{ - Timeout: 5 * time.Second, - User: "test", - Pass: "test", - } - client, err := tarantool.Connect(server, opts) - if err != nil { - fmt.Printf("Failed to connect: %s", err.Error()) - } - - resp, err := client.Execute("CREATE TABLE SQL_TEST (id INTEGER PRIMARY KEY, name STRING)", []interface{}{}) - fmt.Println("Execute") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) - - // there are 4 options to pass named parameters to an SQL query - // the simple map: - sqlBind1 := map[string]interface{}{ - "id": 1, - "name": "test", - } - - // any type of structure - sqlBind2 := struct { - Id int - Name string - }{1, "test"} - - // it is possible to use []tarantool.KeyValueBind - sqlBind3 := []interface{}{ - tarantool.KeyValueBind{Key: "id", Value: 1}, - tarantool.KeyValueBind{Key: "name", Value: "test"}, - } - - // or []interface{} slice with tarantool.KeyValueBind items inside - sqlBind4 := []tarantool.KeyValueBind{ - {"id", 1}, - {"name", "test"}, - } - - // the next usage - resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=:name", sqlBind1) - fmt.Println("Execute") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) - - // the same as - resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=:name", sqlBind2) - fmt.Println("Execute") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) - - // the same as - resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=:name", sqlBind3) - fmt.Println("Execute") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) - - // the same as - resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=:name", sqlBind4) - fmt.Println("Execute") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) - - // the way to pass positional arguments to an SQL query - resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=? AND name=?", []interface{}{2, "test"}) - fmt.Println("Execute") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) - - // the way to pass SQL expression with using custom packing/unpacking for a type - var res []Tuple - sqlInfo, metaData, err := client.ExecuteTyped("SELECT id, name, name FROM SQL_TEST WHERE id=?", []interface{}{2}, &res) - fmt.Println("ExecuteTyped") - fmt.Println("Error", err) - fmt.Println("Data", res) - fmt.Println("MetaData", metaData) - fmt.Println("SQL Info", sqlInfo) - - // for using different types of parameters (positioned/named), collect all items in []interface{} - // all "named" items must be passed with tarantool.KeyValueBind{} - resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=?", - []interface{}{tarantool.KeyValueBind{"id", 1}, "test"}) - fmt.Println("Execute") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) -} - // To use prepared statements to query a tarantool instance, call NewPrepared. func ExampleConnection_NewPrepared() { // Tarantool supports SQL since version 2.0.0 @@ -1086,40 +1075,10 @@ func ExampleConnection_NewWatcher() { time.Sleep(time.Second) } -// To pass contexts to request objects, use the Context() method. -// Pay attention that when using context with request objects, -// the timeout option for Connection will not affect the lifetime -// of the request. For those purposes use context.WithTimeout() as -// the root context. -func ExamplePingRequest_Context() { - conn := example_connect(opts) - defer conn.Close() - - timeout := time.Nanosecond - - // this way you may set the common timeout for requests with context - rootCtx, cancelRoot := context.WithTimeout(context.Background(), timeout) - defer cancelRoot() - - // this context will be canceled with the root after commonTimeout - ctx, cancel := context.WithCancel(rootCtx) - defer cancel() - - req := tarantool.NewPingRequest().Context(ctx) - - // Ping a Tarantool instance to check connection. - resp, err := conn.Do(req).Get() - fmt.Println("Ping Resp", resp) - fmt.Println("Ping Error", err) - // Output: - // Ping Resp - // Ping Error context is done -} - // ExampleConnection_CloseGraceful_force demonstrates how to force close // a connection with graceful close in progress after a while. func ExampleConnection_CloseGraceful_force() { - conn := example_connect(opts) + conn := exampleConnect(opts) eval := `local fiber = require('fiber') local time = ... diff --git a/pool/connection_pool.go b/pool/connection_pool.go index f2d85f445..581b226fd 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -363,6 +363,9 @@ func (pool *ConnectionPool) GetPoolInfo() map[string]*ConnectionInfo { } // Ping sends empty request to Tarantool to check connection. +// +// Deprecated: the method will be removed in the next major version, +// use a PingRequest object + Do() instead. func (connPool *ConnectionPool) Ping(userMode Mode) (*tarantool.Response, error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -373,6 +376,9 @@ func (connPool *ConnectionPool) Ping(userMode Mode) (*tarantool.Response, error) } // Select performs select to box space. +// +// Deprecated: the method will be removed in the next major version, +// use a SelectRequest object + Do() instead. func (connPool *ConnectionPool) Select(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { @@ -386,6 +392,9 @@ func (connPool *ConnectionPool) Select(space, index interface{}, // Insert performs insertion to box space. // Tarantool will reject Insert when tuple with same primary key exists. +// +// Deprecated: the method will be removed in the next major version, +// use an InsertRequest object + Do() instead. func (connPool *ConnectionPool) Insert(space interface{}, tuple interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -397,6 +406,9 @@ func (connPool *ConnectionPool) Insert(space interface{}, tuple interface{}, use // Replace performs "insert or replace" action to box space. // If tuple with same primary key exists, it will be replaced. +// +// Deprecated: the method will be removed in the next major version, +// use a ReplaceRequest object + Do() instead. func (connPool *ConnectionPool) Replace(space interface{}, tuple interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -408,6 +420,9 @@ func (connPool *ConnectionPool) Replace(space interface{}, tuple interface{}, us // Delete performs deletion of a tuple by key. // Result will contain array with deleted tuple. +// +// Deprecated: the method will be removed in the next major version, +// use a DeleteRequest object + Do() instead. func (connPool *ConnectionPool) Delete(space, index interface{}, key interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -419,6 +434,9 @@ func (connPool *ConnectionPool) Delete(space, index interface{}, key interface{} // Update performs update of a tuple by key. // Result will contain array with updated tuple. +// +// Deprecated: the method will be removed in the next major version, +// use a UpdateRequest object + Do() instead. func (connPool *ConnectionPool) Update(space, index interface{}, key, ops interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -430,6 +448,9 @@ func (connPool *ConnectionPool) Update(space, index interface{}, key, ops interf // Upsert performs "update or insert" action of a tuple by key. // Result will not contain any tuple. +// +// Deprecated: the method will be removed in the next major version, +// use a UpsertRequest object + Do() instead. func (connPool *ConnectionPool) Upsert(space interface{}, tuple, ops interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -441,6 +462,9 @@ func (connPool *ConnectionPool) Upsert(space interface{}, tuple, ops interface{} // Call calls registered Tarantool function. // It uses request code for Tarantool >= 1.7, result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a CallRequest object + Do() instead. func (connPool *ConnectionPool) Call(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -453,6 +477,9 @@ func (connPool *ConnectionPool) Call(functionName string, args interface{}, user // Call16 calls registered Tarantool function. // It uses request code for Tarantool 1.6, result is an array of arrays. // Deprecated since Tarantool 1.7.2. +// +// Deprecated: the method will be removed in the next major version, +// use a Call16Request object + Do() instead. func (connPool *ConnectionPool) Call16(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -464,6 +491,9 @@ func (connPool *ConnectionPool) Call16(functionName string, args interface{}, us // Call17 calls registered Tarantool function. // It uses request code for Tarantool >= 1.7, result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a Call17Request object + Do() instead. func (connPool *ConnectionPool) Call17(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -474,6 +504,9 @@ func (connPool *ConnectionPool) Call17(functionName string, args interface{}, us } // Eval passes lua expression for evaluation. +// +// Deprecated: the method will be removed in the next major version, +// use an EvalRequest object + Do() instead. func (connPool *ConnectionPool) Eval(expr string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -484,6 +517,9 @@ func (connPool *ConnectionPool) Eval(expr string, args interface{}, userMode Mod } // Execute passes sql expression to Tarantool for execution. +// +// Deprecated: the method will be removed in the next major version, +// use an ExecuteRequest object + Do() instead. func (connPool *ConnectionPool) Execute(expr string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -495,6 +531,9 @@ func (connPool *ConnectionPool) Execute(expr string, args interface{}, userMode // GetTyped performs select (with limit = 1 and offset = 0) // to box space and fills typed result. +// +// Deprecated: the method will be removed in the next major version, +// use a SelectRequest object + Do() instead. func (connPool *ConnectionPool) GetTyped(space, index interface{}, key interface{}, result interface{}, userMode ...Mode) (err error) { conn, err := connPool.getConnByMode(ANY, userMode) if err != nil { @@ -505,6 +544,9 @@ func (connPool *ConnectionPool) GetTyped(space, index interface{}, key interface } // SelectTyped performs select to box space and fills typed result. +// +// Deprecated: the method will be removed in the next major version, +// use a SelectRequest object + Do() instead. func (connPool *ConnectionPool) SelectTyped(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}, result interface{}, userMode ...Mode) (err error) { @@ -518,6 +560,9 @@ func (connPool *ConnectionPool) SelectTyped(space, index interface{}, // InsertTyped performs insertion to box space. // Tarantool will reject Insert when tuple with same primary key exists. +// +// Deprecated: the method will be removed in the next major version, +// use an InsertRequest object + Do() instead. func (connPool *ConnectionPool) InsertTyped(space interface{}, tuple interface{}, result interface{}, userMode ...Mode) (err error) { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -529,6 +574,9 @@ func (connPool *ConnectionPool) InsertTyped(space interface{}, tuple interface{} // ReplaceTyped performs "insert or replace" action to box space. // If tuple with same primary key exists, it will be replaced. +// +// Deprecated: the method will be removed in the next major version, +// use a ReplaceRequest object + Do() instead. func (connPool *ConnectionPool) ReplaceTyped(space interface{}, tuple interface{}, result interface{}, userMode ...Mode) (err error) { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -539,6 +587,9 @@ func (connPool *ConnectionPool) ReplaceTyped(space interface{}, tuple interface{ } // DeleteTyped performs deletion of a tuple by key and fills result with deleted tuple. +// +// Deprecated: the method will be removed in the next major version, +// use a DeleteRequest object + Do() instead. func (connPool *ConnectionPool) DeleteTyped(space, index interface{}, key interface{}, result interface{}, userMode ...Mode) (err error) { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -549,6 +600,9 @@ func (connPool *ConnectionPool) DeleteTyped(space, index interface{}, key interf } // UpdateTyped performs update of a tuple by key and fills result with updated tuple. +// +// Deprecated: the method will be removed in the next major version, +// use a UpdateRequest object + Do() instead. func (connPool *ConnectionPool) UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}, userMode ...Mode) (err error) { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -560,6 +614,9 @@ func (connPool *ConnectionPool) UpdateTyped(space, index interface{}, key, ops i // CallTyped calls registered function. // It uses request code for Tarantool >= 1.7, result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a CallRequest object + Do() instead. func (connPool *ConnectionPool) CallTyped(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -572,6 +629,9 @@ func (connPool *ConnectionPool) CallTyped(functionName string, args interface{}, // Call16Typed calls registered function. // It uses request code for Tarantool 1.6, result is an array of arrays. // Deprecated since Tarantool 1.7.2. +// +// Deprecated: the method will be removed in the next major version, +// use a Call16Request object + Do() instead. func (connPool *ConnectionPool) Call16Typed(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -583,6 +643,9 @@ func (connPool *ConnectionPool) Call16Typed(functionName string, args interface{ // Call17Typed calls registered function. // It uses request code for Tarantool >= 1.7, result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a Call17Request object + Do() instead. func (connPool *ConnectionPool) Call17Typed(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -593,6 +656,9 @@ func (connPool *ConnectionPool) Call17Typed(functionName string, args interface{ } // EvalTyped passes lua expression for evaluation. +// +// Deprecated: the method will be removed in the next major version, +// use an EvalRequest object + Do() instead. func (connPool *ConnectionPool) EvalTyped(expr string, args interface{}, result interface{}, userMode Mode) (err error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -603,6 +669,9 @@ func (connPool *ConnectionPool) EvalTyped(expr string, args interface{}, result } // ExecuteTyped passes sql expression to Tarantool for execution. +// +// Deprecated: the method will be removed in the next major version, +// use an ExecuteRequest object + Do() instead. func (connPool *ConnectionPool) ExecuteTyped(expr string, args interface{}, result interface{}, userMode Mode) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -613,6 +682,9 @@ func (connPool *ConnectionPool) ExecuteTyped(expr string, args interface{}, resu } // SelectAsync sends select request to Tarantool and returns Future. +// +// Deprecated: the method will be removed in the next major version, +// use a SelectRequest object + Do() instead. func (connPool *ConnectionPool) SelectAsync(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}, userMode ...Mode) *tarantool.Future { @@ -626,6 +698,9 @@ func (connPool *ConnectionPool) SelectAsync(space, index interface{}, // InsertAsync sends insert action to Tarantool and returns Future. // Tarantool will reject Insert when tuple with same primary key exists. +// +// Deprecated: the method will be removed in the next major version, +// use an InsertRequest object + Do() instead. func (connPool *ConnectionPool) InsertAsync(space interface{}, tuple interface{}, userMode ...Mode) *tarantool.Future { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -637,6 +712,9 @@ func (connPool *ConnectionPool) InsertAsync(space interface{}, tuple interface{} // ReplaceAsync sends "insert or replace" action to Tarantool and returns Future. // If tuple with same primary key exists, it will be replaced. +// +// Deprecated: the method will be removed in the next major version, +// use a ReplaceRequest object + Do() instead. func (connPool *ConnectionPool) ReplaceAsync(space interface{}, tuple interface{}, userMode ...Mode) *tarantool.Future { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -648,6 +726,9 @@ func (connPool *ConnectionPool) ReplaceAsync(space interface{}, tuple interface{ // DeleteAsync sends deletion action to Tarantool and returns Future. // Future's result will contain array with deleted tuple. +// +// Deprecated: the method will be removed in the next major version, +// use a DeleteRequest object + Do() instead. func (connPool *ConnectionPool) DeleteAsync(space, index interface{}, key interface{}, userMode ...Mode) *tarantool.Future { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -659,6 +740,9 @@ func (connPool *ConnectionPool) DeleteAsync(space, index interface{}, key interf // UpdateAsync sends deletion of a tuple by key and returns Future. // Future's result will contain array with updated tuple. +// +// Deprecated: the method will be removed in the next major version, +// use a UpdateRequest object + Do() instead. func (connPool *ConnectionPool) UpdateAsync(space, index interface{}, key, ops interface{}, userMode ...Mode) *tarantool.Future { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -670,6 +754,9 @@ func (connPool *ConnectionPool) UpdateAsync(space, index interface{}, key, ops i // UpsertAsync sends "update or insert" action to Tarantool and returns Future. // Future's sesult will not contain any tuple. +// +// Deprecated: the method will be removed in the next major version, +// use a UpsertRequest object + Do() instead. func (connPool *ConnectionPool) UpsertAsync(space interface{}, tuple interface{}, ops interface{}, userMode ...Mode) *tarantool.Future { conn, err := connPool.getConnByMode(RW, userMode) if err != nil { @@ -681,6 +768,9 @@ func (connPool *ConnectionPool) UpsertAsync(space interface{}, tuple interface{} // CallAsync sends a call to registered Tarantool function and returns Future. // It uses request code for Tarantool >= 1.7, future's result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a CallRequest object + Do() instead. func (connPool *ConnectionPool) CallAsync(functionName string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -693,6 +783,9 @@ func (connPool *ConnectionPool) CallAsync(functionName string, args interface{}, // Call16Async sends a call to registered Tarantool function and returns Future. // It uses request code for Tarantool 1.6, so future's result is an array of arrays. // Deprecated since Tarantool 1.7.2. +// +// Deprecated: the method will be removed in the next major version, +// use a Call16Request object + Do() instead. func (connPool *ConnectionPool) Call16Async(functionName string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -704,6 +797,9 @@ func (connPool *ConnectionPool) Call16Async(functionName string, args interface{ // Call17Async sends a call to registered Tarantool function and returns Future. // It uses request code for Tarantool >= 1.7, future's result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a Call17Request object + Do() instead. func (connPool *ConnectionPool) Call17Async(functionName string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -714,6 +810,9 @@ func (connPool *ConnectionPool) Call17Async(functionName string, args interface{ } // EvalAsync sends a lua expression for evaluation and returns Future. +// +// Deprecated: the method will be removed in the next major version, +// use an EvalRequest object + Do() instead. func (connPool *ConnectionPool) EvalAsync(expr string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -725,6 +824,9 @@ func (connPool *ConnectionPool) EvalAsync(expr string, args interface{}, userMod // ExecuteAsync sends sql expression to Tarantool for execution and returns // Future. +// +// Deprecated: the method will be removed in the next major version, +// use an ExecuteRequest object + Do() instead. func (connPool *ConnectionPool) ExecuteAsync(expr string, args interface{}, userMode Mode) *tarantool.Future { conn, err := connPool.getNextConnection(userMode) if err != nil { @@ -842,7 +944,7 @@ func (connPool *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarant // func (connPool *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (Role, error) { - resp, err := conn.Call17("box.info", []interface{}{}) + resp, err := conn.Do(tarantool.NewCallRequest("box.info")).Get() if err != nil { return UnknownRole, err } diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index eda8560e8..912721cd4 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -23,7 +23,6 @@ import ( var spaceNo = uint32(520) var spaceName = "testPool" var indexNo = uint32(0) -var indexName = "pk" var ports = []string{"3013", "3014", "3015", "3016", "3017"} var host = "127.0.0.1" @@ -1647,7 +1646,12 @@ func TestInsert(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) defer conn.Close() - resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"rw_insert_key"}) + sel := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{"rw_insert_key"}) + resp, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") @@ -1682,7 +1686,12 @@ func TestInsert(t *testing.T) { require.Truef(t, ok, "unexpected body of Insert (1)") require.Equalf(t, "preferRW_insert_value", value, "unexpected body of Insert (1)") - resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"preferRW_insert_key"}) + sel = tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{"preferRW_insert_key"}) + resp, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") @@ -1717,7 +1726,8 @@ func TestDelete(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) defer conn.Close() - resp, err := conn.Insert(spaceNo, []interface{}{"delete_key", "delete_value"}) + ins := tarantool.NewInsertRequest(spaceNo).Tuple([]interface{}{"delete_key", "delete_value"}) + resp, err := conn.Do(ins).Get() require.Nilf(t, err, "failed to Insert") require.NotNilf(t, resp, "response is nil after Insert") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") @@ -1752,7 +1762,12 @@ func TestDelete(t *testing.T) { require.Truef(t, ok, "unexpected body of Delete (1)") require.Equalf(t, "delete_value", value, "unexpected body of Delete (1)") - resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"delete_key"}) + sel := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{"delete_key"}) + resp, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") require.Equalf(t, 0, len(resp.Data), "response Body len != 0 after Select") @@ -1780,7 +1795,12 @@ func TestUpsert(t *testing.T) { require.Nilf(t, err, "failed to Upsert") require.NotNilf(t, resp, "response is nil after Upsert") - resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"upsert_key"}) + sel := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{"upsert_key"}) + resp, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") @@ -1805,7 +1825,7 @@ func TestUpsert(t *testing.T) { require.Nilf(t, err, "failed to Upsert") require.NotNilf(t, resp, "response is nil after Upsert") - resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"upsert_key"}) + resp, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") @@ -1840,7 +1860,9 @@ func TestUpdate(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) defer conn.Close() - resp, err := conn.Insert(spaceNo, []interface{}{"update_key", "update_value"}) + ins := tarantool.NewInsertRequest(spaceNo). + Tuple([]interface{}{"update_key", "update_value"}) + resp, err := conn.Do(ins).Get() require.Nilf(t, err, "failed to Insert") require.NotNilf(t, resp, "response is nil after Insert") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") @@ -1862,7 +1884,12 @@ func TestUpdate(t *testing.T) { require.Nilf(t, err, "failed to Update") require.NotNilf(t, resp, "response is nil after Update") - resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"update_key"}) + sel := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{"update_key"}) + resp, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") @@ -1887,7 +1914,7 @@ func TestUpdate(t *testing.T) { require.Nilf(t, err, "failed to Update") require.NotNilf(t, resp, "response is nil after Update") - resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"update_key"}) + resp, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") @@ -1922,7 +1949,9 @@ func TestReplace(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) defer conn.Close() - resp, err := conn.Insert(spaceNo, []interface{}{"replace_key", "replace_value"}) + ins := tarantool.NewInsertRequest(spaceNo). + Tuple([]interface{}{"replace_key", "replace_value"}) + resp, err := conn.Do(ins).Get() require.Nilf(t, err, "failed to Insert") require.NotNilf(t, resp, "response is nil after Insert") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") @@ -1944,7 +1973,12 @@ func TestReplace(t *testing.T) { require.Nilf(t, err, "failed to Replace") require.NotNilf(t, resp, "response is nil after Replace") - resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"new_key"}) + sel := tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Limit(1). + Iterator(tarantool.IterEq). + Key([]interface{}{"new_key"}) + resp, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") @@ -1966,7 +2000,7 @@ func TestReplace(t *testing.T) { require.Nilf(t, err, "failed to Replace") require.NotNilf(t, resp, "response is nil after Replace") - resp, err = conn.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, []interface{}{"new_key"}) + resp, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") diff --git a/pool/connector.go b/pool/connector.go index 852f47cee..acb9a9187 100644 --- a/pool/connector.go +++ b/pool/connector.go @@ -47,11 +47,6 @@ func (c *ConnectorAdapter) Close() error { return err } -// Ping sends empty request to Tarantool to check connection. -func (c *ConnectorAdapter) Ping() (*tarantool.Response, error) { - return c.pool.Ping(c.mode) -} - // ConfiguredTimeout returns a timeout from connections config. func (c *ConnectorAdapter) ConfiguredTimeout() time.Duration { ret, err := c.pool.ConfiguredTimeout(c.mode) @@ -61,7 +56,18 @@ func (c *ConnectorAdapter) ConfiguredTimeout() time.Duration { return ret } +// Ping sends empty request to Tarantool to check connection. +// +// Deprecated: the method will be removed in the next major version, +// use a PingRequest object + Do() instead. +func (c *ConnectorAdapter) Ping() (*tarantool.Response, error) { + return c.pool.Ping(c.mode) +} + // Select performs select to box space. +// +// Deprecated: the method will be removed in the next major version, +// use a SelectRequest object + Do() instead. func (c *ConnectorAdapter) Select(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}) (*tarantool.Response, error) { @@ -69,30 +75,45 @@ func (c *ConnectorAdapter) Select(space, index interface{}, } // Insert performs insertion to box space. +// +// Deprecated: the method will be removed in the next major version, +// use an InsertRequest object + Do() instead. func (c *ConnectorAdapter) Insert(space interface{}, tuple interface{}) (*tarantool.Response, error) { return c.pool.Insert(space, tuple, c.mode) } // Replace performs "insert or replace" action to box space. +// +// Deprecated: the method will be removed in the next major version, +// use a ReplaceRequest object + Do() instead. func (c *ConnectorAdapter) Replace(space interface{}, tuple interface{}) (*tarantool.Response, error) { return c.pool.Replace(space, tuple, c.mode) } // Delete performs deletion of a tuple by key. +// +// Deprecated: the method will be removed in the next major version, +// use a DeleteRequest object + Do() instead. func (c *ConnectorAdapter) Delete(space, index interface{}, key interface{}) (*tarantool.Response, error) { return c.pool.Delete(space, index, key, c.mode) } // Update performs update of a tuple by key. +// +// Deprecated: the method will be removed in the next major version, +// use a UpdateRequest object + Do() instead. func (c *ConnectorAdapter) Update(space, index interface{}, key, ops interface{}) (*tarantool.Response, error) { return c.pool.Update(space, index, key, ops, c.mode) } // Upsert performs "update or insert" action of a tuple by key. +// +// Deprecated: the method will be removed in the next major version, +// use a UpsertRequest object + Do() instead. func (c *ConnectorAdapter) Upsert(space interface{}, tuple, ops interface{}) (*tarantool.Response, error) { return c.pool.Upsert(space, tuple, ops, c.mode) @@ -100,6 +121,9 @@ func (c *ConnectorAdapter) Upsert(space interface{}, // Call calls registered Tarantool function. // It uses request code for Tarantool >= 1.7, result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a CallRequest object + Do() instead. func (c *ConnectorAdapter) Call(functionName string, args interface{}) (*tarantool.Response, error) { return c.pool.Call(functionName, args, c.mode) @@ -108,6 +132,9 @@ func (c *ConnectorAdapter) Call(functionName string, // Call16 calls registered Tarantool function. // It uses request code for Tarantool 1.6, result is an array of arrays. // Deprecated since Tarantool 1.7.2. +// +// Deprecated: the method will be removed in the next major version, +// use a Call16Request object + Do() instead. func (c *ConnectorAdapter) Call16(functionName string, args interface{}) (*tarantool.Response, error) { return c.pool.Call16(functionName, args, c.mode) @@ -115,18 +142,27 @@ func (c *ConnectorAdapter) Call16(functionName string, // Call17 calls registered Tarantool function. // It uses request code for Tarantool >= 1.7, result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a Call17Request object + Do() instead. func (c *ConnectorAdapter) Call17(functionName string, args interface{}) (*tarantool.Response, error) { return c.pool.Call17(functionName, args, c.mode) } // Eval passes Lua expression for evaluation. +// +// Deprecated: the method will be removed in the next major version, +// use an EvalRequest object + Do() instead. func (c *ConnectorAdapter) Eval(expr string, args interface{}) (*tarantool.Response, error) { return c.pool.Eval(expr, args, c.mode) } // Execute passes sql expression to Tarantool for execution. +// +// Deprecated: the method will be removed in the next major version, +// use an ExecuteRequest object + Do() instead. func (c *ConnectorAdapter) Execute(expr string, args interface{}) (*tarantool.Response, error) { return c.pool.Execute(expr, args, c.mode) @@ -134,12 +170,18 @@ func (c *ConnectorAdapter) Execute(expr string, // GetTyped performs select (with limit = 1 and offset = 0) // to box space and fills typed result. +// +// Deprecated: the method will be removed in the next major version, +// use a SelectRequest object + Do() instead. func (c *ConnectorAdapter) GetTyped(space, index interface{}, key interface{}, result interface{}) error { return c.pool.GetTyped(space, index, key, result, c.mode) } // SelectTyped performs select to box space and fills typed result. +// +// Deprecated: the method will be removed in the next major version, +// use a SelectRequest object + Do() instead. func (c *ConnectorAdapter) SelectTyped(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}, result interface{}) error { @@ -147,24 +189,36 @@ func (c *ConnectorAdapter) SelectTyped(space, index interface{}, } // InsertTyped performs insertion to box space. +// +// Deprecated: the method will be removed in the next major version, +// use an InsertRequest object + Do() instead. func (c *ConnectorAdapter) InsertTyped(space interface{}, tuple interface{}, result interface{}) error { return c.pool.InsertTyped(space, tuple, result, c.mode) } // ReplaceTyped performs "insert or replace" action to box space. +// +// Deprecated: the method will be removed in the next major version, +// use a ReplaceRequest object + Do() instead. func (c *ConnectorAdapter) ReplaceTyped(space interface{}, tuple interface{}, result interface{}) error { return c.pool.ReplaceTyped(space, tuple, result, c.mode) } // DeleteTyped performs deletion of a tuple by key and fills result with deleted tuple. +// +// Deprecated: the method will be removed in the next major version, +// use a DeleteRequest object + Do() instead. func (c *ConnectorAdapter) DeleteTyped(space, index interface{}, key interface{}, result interface{}) error { return c.pool.DeleteTyped(space, index, key, result, c.mode) } // UpdateTyped performs update of a tuple by key and fills result with updated tuple. +// +// Deprecated: the method will be removed in the next major version, +// use a UpdateRequest object + Do() instead. func (c *ConnectorAdapter) UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) error { return c.pool.UpdateTyped(space, index, key, ops, result, c.mode) @@ -172,6 +226,9 @@ func (c *ConnectorAdapter) UpdateTyped(space, index interface{}, // CallTyped calls registered function. // It uses request code for Tarantool >= 1.7, result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a CallRequest object + Do() instead. func (c *ConnectorAdapter) CallTyped(functionName string, args interface{}, result interface{}) error { return c.pool.CallTyped(functionName, args, result, c.mode) @@ -180,6 +237,9 @@ func (c *ConnectorAdapter) CallTyped(functionName string, // Call16Typed calls registered function. // It uses request code for Tarantool 1.6, result is an array of arrays. // Deprecated since Tarantool 1.7.2. +// +// Deprecated: the method will be removed in the next major version, +// use a Call16Request object + Do() instead. func (c *ConnectorAdapter) Call16Typed(functionName string, args interface{}, result interface{}) error { return c.pool.Call16Typed(functionName, args, result, c.mode) @@ -187,54 +247,81 @@ func (c *ConnectorAdapter) Call16Typed(functionName string, // Call17Typed calls registered function. // It uses request code for Tarantool >= 1.7, result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a Call17Request object + Do() instead. func (c *ConnectorAdapter) Call17Typed(functionName string, args interface{}, result interface{}) error { return c.pool.Call17Typed(functionName, args, result, c.mode) } // EvalTyped passes Lua expression for evaluation. +// +// Deprecated: the method will be removed in the next major version, +// use an EvalRequest object + Do() instead. func (c *ConnectorAdapter) EvalTyped(expr string, args interface{}, result interface{}) error { return c.pool.EvalTyped(expr, args, result, c.mode) } // ExecuteTyped passes sql expression to Tarantool for execution. +// +// Deprecated: the method will be removed in the next major version, +// use an ExecuteRequest object + Do() instead. func (c *ConnectorAdapter) ExecuteTyped(expr string, args interface{}, result interface{}) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) { return c.pool.ExecuteTyped(expr, args, result, c.mode) } // SelectAsync sends select request to Tarantool and returns Future. +// +// Deprecated: the method will be removed in the next major version, +// use a SelectRequest object + Do() instead. func (c *ConnectorAdapter) SelectAsync(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}) *tarantool.Future { return c.pool.SelectAsync(space, index, offset, limit, iterator, key, c.mode) } // InsertAsync sends insert action to Tarantool and returns Future. +// +// Deprecated: the method will be removed in the next major version, +// use an InsertRequest object + Do() instead. func (c *ConnectorAdapter) InsertAsync(space interface{}, tuple interface{}) *tarantool.Future { return c.pool.InsertAsync(space, tuple, c.mode) } // ReplaceAsync sends "insert or replace" action to Tarantool and returns Future. +// +// Deprecated: the method will be removed in the next major version, +// use a ReplaceRequest object + Do() instead. func (c *ConnectorAdapter) ReplaceAsync(space interface{}, tuple interface{}) *tarantool.Future { return c.pool.ReplaceAsync(space, tuple, c.mode) } // DeleteAsync sends deletion action to Tarantool and returns Future. +// +// Deprecated: the method will be removed in the next major version, +// use a DeleteRequest object + Do() instead. func (c *ConnectorAdapter) DeleteAsync(space, index interface{}, key interface{}) *tarantool.Future { return c.pool.DeleteAsync(space, index, key, c.mode) } // Update sends deletion of a tuple by key and returns Future. +// +// Deprecated: the method will be removed in the next major version, +// use a UpdateRequest object + Do() instead. func (c *ConnectorAdapter) UpdateAsync(space, index interface{}, key, ops interface{}) *tarantool.Future { return c.pool.UpdateAsync(space, index, key, ops, c.mode) } // UpsertAsync sends "update or insert" action to Tarantool and returns Future. +// +// Deprecated: the method will be removed in the next major version, +// use a UpsertRequest object + Do() instead. func (c *ConnectorAdapter) UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *tarantool.Future { return c.pool.UpsertAsync(space, tuple, ops, c.mode) @@ -242,6 +329,9 @@ func (c *ConnectorAdapter) UpsertAsync(space interface{}, tuple interface{}, // CallAsync sends a call to registered Tarantool function and returns Future. // It uses request code for Tarantool >= 1.7, future's result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a CallRequest object + Do() instead. func (c *ConnectorAdapter) CallAsync(functionName string, args interface{}) *tarantool.Future { return c.pool.CallAsync(functionName, args, c.mode) @@ -250,6 +340,9 @@ func (c *ConnectorAdapter) CallAsync(functionName string, // Call16Async sends a call to registered Tarantool function and returns Future. // It uses request code for Tarantool 1.6, so future's result is an array of arrays. // Deprecated since Tarantool 1.7.2. +// +// Deprecated: the method will be removed in the next major version, +// use a Call16Request object + Do() instead. func (c *ConnectorAdapter) Call16Async(functionName string, args interface{}) *tarantool.Future { return c.pool.Call16Async(functionName, args, c.mode) @@ -257,18 +350,27 @@ func (c *ConnectorAdapter) Call16Async(functionName string, // Call17Async sends a call to registered Tarantool function and returns Future. // It uses request code for Tarantool >= 1.7, future's result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a Call17Request object + Do() instead. func (c *ConnectorAdapter) Call17Async(functionName string, args interface{}) *tarantool.Future { return c.pool.Call17Async(functionName, args, c.mode) } // EvalAsync sends a Lua expression for evaluation and returns Future. +// +// Deprecated: the method will be removed in the next major version, +// use an EvalRequest object + Do() instead. func (c *ConnectorAdapter) EvalAsync(expr string, args interface{}) *tarantool.Future { return c.pool.EvalAsync(expr, args, c.mode) } // ExecuteAsync sends a sql expression for execution and returns Future. +// +// Deprecated: the method will be removed in the next major version, +// use an EvalRequest object + Do() instead. func (c *ConnectorAdapter) ExecuteAsync(expr string, args interface{}) *tarantool.Future { return c.pool.ExecuteAsync(expr, args, c.mode) diff --git a/pool/example_test.go b/pool/example_test.go index f917668a6..84a41ff7b 100644 --- a/pool/example_test.go +++ b/pool/example_test.go @@ -32,521 +32,31 @@ func examplePool(roles []bool, connOpts tarantool.Opts) (*pool.ConnectionPool, e return connPool, nil } -func ExampleConnectionPool_Select() { - connPool, err := examplePool(testRoles, connOpts) - if err != nil { - fmt.Println(err) - } - defer connPool.Close() - - // Connect to servers[2] to check if tuple - // was inserted on RW instance - conn, err := tarantool.Connect(servers[2], connOpts) - if err != nil || conn == nil { - fmt.Printf("failed to connect to %s", servers[2]) - return - } - - // Insert a new tuple {"key1", "value1"}. - _, err = conn.Insert(spaceNo, []interface{}{"key1", "value1"}) - if err != nil { - fmt.Printf("Failed to insert: %s", err.Error()) - return - } - // Insert a new tuple {"key2", "value2"}. - _, err = conn.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}) - if err != nil { - fmt.Printf("Failed to insert: %s", err.Error()) - return - } - - resp, err := connPool.Select( - spaceNo, indexNo, 0, 100, tarantool.IterEq, - []interface{}{"key1"}, pool.PreferRW) - if err != nil { - fmt.Printf("error in select is %v", err) - return - } - fmt.Printf("response is %#v\n", resp.Data) - resp, err = connPool.Select( - spaceNo, indexNo, 0, 100, tarantool.IterEq, - []interface{}{"key2"}, pool.PreferRW) - if err != nil { - fmt.Printf("error in select is %v", err) - return - } - fmt.Printf("response is %#v\n", resp.Data) - - // Delete tuple with primary key "key1". - _, err = conn.Delete(spaceName, indexName, []interface{}{"key1"}) - if err != nil { - fmt.Printf("Failed to delete: %s", err.Error()) - } - // Delete tuple with primary key "key2". - _, err = conn.Delete(spaceNo, indexNo, []interface{}{"key2"}) - if err != nil { - fmt.Printf("Failed to delete: %s", err.Error()) - } - - // Output: - // response is []interface {}{[]interface {}{"key1", "value1"}} - // response is []interface {}{[]interface {}{"key2", "value2"}} -} - -func ExampleConnectionPool_SelectTyped() { - connPool, err := examplePool(testRoles, connOpts) - if err != nil { - fmt.Println(err) - } - defer connPool.Close() - - // Connect to servers[2] to check if tuple - // was inserted on RW instance - conn, err := tarantool.Connect(servers[2], connOpts) - if err != nil || conn == nil { - fmt.Printf("failed to connect to %s", servers[2]) - return - } - - // Insert a new tuple {"key1", "value1"}. - _, err = conn.Insert(spaceNo, []interface{}{"key1", "value1"}) - if err != nil { - fmt.Printf("Failed to insert: %s", err.Error()) - return - } - // Insert a new tuple {"key2", "value2"}. - _, err = conn.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}) - if err != nil { - fmt.Printf("Failed to insert: %s", err.Error()) - return - } - - var res []Tuple - err = connPool.SelectTyped( - spaceNo, indexNo, 0, 100, tarantool.IterEq, - []interface{}{"key1"}, &res, pool.PreferRW) - if err != nil { - fmt.Printf("error in select is %v", err) - return - } - fmt.Printf("response is %v\n", res) - err = connPool.SelectTyped( - spaceName, indexName, 0, 100, tarantool.IterEq, - []interface{}{"key2"}, &res, pool.PreferRW) - if err != nil { - fmt.Printf("error in select is %v", err) - return - } - fmt.Printf("response is %v\n", res) - - // Delete tuple with primary key "key1". - _, err = conn.Delete(spaceName, indexName, []interface{}{"key1"}) - if err != nil { - fmt.Printf("Failed to delete: %s", err.Error()) - } - // Delete tuple with primary key "key2". - _, err = conn.Delete(spaceNo, indexNo, []interface{}{"key2"}) - if err != nil { - fmt.Printf("Failed to delete: %s", err.Error()) - } - - // Output: - // response is [{{} key1 value1}] - // response is [{{} key2 value2}] -} - -func ExampleConnectionPool_SelectAsync() { +func ExampleConnectionPool_Do() { connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) } defer connPool.Close() - // Connect to servers[2] to check if tuple - // was inserted on RW instance - conn, err := tarantool.Connect(servers[2], connOpts) - if err != nil || conn == nil { - fmt.Printf("failed to connect to %s", servers[2]) - return - } - - // Insert a new tuple {"key1", "value1"}. - _, err = conn.Insert(spaceNo, []interface{}{"key1", "value1"}) - if err != nil { - fmt.Printf("Failed to insert: %s", err.Error()) - return - } - // Insert a new tuple {"key2", "value2"}. - _, err = conn.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}) - if err != nil { - fmt.Printf("Failed to insert: %s", err.Error()) - return - } - // Insert a new tuple {"key3", "value3"}. - _, err = conn.Insert(spaceNo, []interface{}{"key3", "value3"}) - if err != nil { - fmt.Printf("Failed to insert: %s", err.Error()) - return - } - - var futs [3]*tarantool.Future - futs[0] = connPool.SelectAsync( - spaceName, indexName, 0, 2, tarantool.IterEq, - []interface{}{"key1"}, pool.PreferRW) - futs[1] = connPool.SelectAsync( - spaceName, indexName, 0, 1, tarantool.IterEq, - []interface{}{"key2"}, pool.RW) - futs[2] = connPool.SelectAsync( - spaceName, indexName, 0, 1, tarantool.IterEq, - []interface{}{"key3"}, pool.RW) - var t []Tuple - err = futs[0].GetTyped(&t) - fmt.Println("Future", 0, "Error", err) - fmt.Println("Future", 0, "Data", t) - - resp, err := futs[1].Get() - fmt.Println("Future", 1, "Error", err) - fmt.Println("Future", 1, "Data", resp.Data) - - resp, err = futs[2].Get() - fmt.Println("Future", 2, "Error", err) - fmt.Println("Future", 2, "Data", resp.Data) - - // Delete tuple with primary key "key1". - _, err = conn.Delete(spaceName, indexName, []interface{}{"key1"}) - if err != nil { - fmt.Printf("Failed to delete: %s", err.Error()) - } - // Delete tuple with primary key "key2". - _, err = conn.Delete(spaceNo, indexNo, []interface{}{"key2"}) - if err != nil { - fmt.Printf("Failed to delete: %s", err.Error()) - } - // Delete tuple with primary key "key3". - _, err = conn.Delete(spaceNo, indexNo, []interface{}{"key3"}) - if err != nil { - fmt.Printf("Failed to delete: %s", err.Error()) - } - - // Output: - // Future 0 Error - // Future 0 Data [{{} key1 value1}] - // Future 1 Error - // Future 1 Data [[key2 value2]] - // Future 2 Error - // Future 2 Data [[key3 value3]] -} - -func ExampleConnectionPool_SelectAsync_err() { - roles := []bool{true, true, true, true, true} - connPool, err := examplePool(roles, connOpts) - if err != nil { - fmt.Println(err) + modes := []pool.Mode{ + pool.ANY, + pool.RW, + pool.RO, + pool.PreferRW, + pool.PreferRO, } - defer connPool.Close() - - var futs [3]*tarantool.Future - futs[0] = connPool.SelectAsync( - spaceName, indexName, 0, 2, tarantool.IterEq, - []interface{}{"key1"}, pool.RW) - - err = futs[0].Err() - fmt.Println("Future", 0, "Error", err) - - // Output: - // Future 0 Error can't find rw instance in pool -} - -func ExampleConnectionPool_Ping() { - connPool, err := examplePool(testRoles, connOpts) - if err != nil { - fmt.Println(err) + for _, m := range modes { + // It could be any request object. + req := tarantool.NewPingRequest() + _, err := connPool.Do(req, m).Get() + fmt.Println("Ping Error", err) } - defer connPool.Close() - - // Ping a Tarantool instance to check connection. - resp, err := connPool.Ping(pool.ANY) - fmt.Println("Ping Code", resp.Code) - fmt.Println("Ping Data", resp.Data) - fmt.Println("Ping Error", err) // Output: - // Ping Code 0 - // Ping Data [] // Ping Error -} - -func ExampleConnectionPool_Insert() { - connPool, err := examplePool(testRoles, connOpts) - if err != nil { - fmt.Println(err) - } - defer connPool.Close() - - // Insert a new tuple {"key1", "value1"}. - resp, err := connPool.Insert(spaceNo, []interface{}{"key1", "value1"}) - fmt.Println("Insert key1") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - // Insert a new tuple {"key2", "value2"}. - resp, err = connPool.Insert(spaceName, &Tuple{Key: "key2", Value: "value2"}, pool.PreferRW) - fmt.Println("Insert key2") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - - // Connect to servers[2] to check if tuple - // was inserted on RW instance - conn, err := tarantool.Connect(servers[2], connOpts) - if err != nil || conn == nil { - fmt.Printf("failed to connect to %s", servers[2]) - return - } - - // Delete tuple with primary key "key1". - _, err = conn.Delete(spaceName, indexName, []interface{}{"key1"}) - if err != nil { - fmt.Printf("Failed to delete: %s", err.Error()) - } - // Delete tuple with primary key "key2". - _, err = conn.Delete(spaceNo, indexNo, []interface{}{"key2"}) - if err != nil { - fmt.Printf("Failed to delete: %s", err.Error()) - } - // Output: - // Insert key1 - // Error - // Code 0 - // Data [[key1 value1]] - // Insert key2 - // Error - // Code 0 - // Data [[key2 value2]] -} - -func ExampleConnectionPool_Delete() { - connPool, err := examplePool(testRoles, connOpts) - if err != nil { - fmt.Println(err) - } - defer connPool.Close() - - // Connect to servers[2] to check if tuple - // was inserted on RW instance - conn, err := tarantool.Connect(servers[2], connOpts) - if err != nil || conn == nil { - fmt.Printf("failed to connect to %s", servers[2]) - return - } - - // Insert a new tuple {"key1", "value1"}. - _, err = conn.Insert(spaceNo, []interface{}{"key1", "value1"}) - if err != nil { - fmt.Printf("Failed to insert: %s", err.Error()) - return - } - // Insert a new tuple {"key2", "value2"}. - _, err = conn.Insert(spaceNo, []interface{}{"key2", "value2"}) - if err != nil { - fmt.Printf("Failed to insert: %s", err.Error()) - return - } - - // Delete tuple with primary key {"key1"}. - resp, err := connPool.Delete(spaceNo, indexNo, []interface{}{"key1"}) - fmt.Println("Delete key1") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - - // Delete tuple with primary key { "key2" }. - resp, err = connPool.Delete(spaceName, indexName, []interface{}{"key2"}, pool.PreferRW) - fmt.Println("Delete key2") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - // Output: - // Delete key1 - // Error - // Code 0 - // Data [[key1 value1]] - // Delete key2 - // Error - // Code 0 - // Data [[key2 value2]] -} - -func ExampleConnectionPool_Replace() { - connPool, err := examplePool(testRoles, connOpts) - if err != nil { - fmt.Println(err) - } - defer connPool.Close() - - // Connect to servers[2] to check if tuple - // was inserted on RW instance - conn, err := tarantool.Connect(servers[2], connOpts) - if err != nil || conn == nil { - fmt.Printf("failed to connect to %s", servers[2]) - return - } - - // Insert a new tuple {"key1", "value1"}. - _, err = conn.Insert(spaceNo, []interface{}{"key1", "value1"}) - if err != nil { - fmt.Printf("Failed to insert: %s", err.Error()) - return - } - - // Replace a tuple with primary key ""key1. - // Note, Tuple is defined within tests, and has EncdodeMsgpack and - // DecodeMsgpack methods. - resp, err := connPool.Replace(spaceNo, []interface{}{"key1", "new_value"}) - fmt.Println("Replace key1") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - resp, err = connPool.Replace(spaceName, []interface{}{"key1", "another_value"}) - fmt.Println("Replace key1") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - resp, err = connPool.Replace(spaceName, &Tuple{Key: "key1", Value: "value2"}) - fmt.Println("Replace key1") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - resp, err = connPool.Replace(spaceName, &Tuple{Key: "key1", Value: "new_value2"}, pool.PreferRW) - fmt.Println("Replace key1") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - - // Delete tuple with primary key "key1". - _, err = conn.Delete(spaceName, indexName, []interface{}{"key1"}) - if err != nil { - fmt.Printf("Failed to delete: %s", err.Error()) - } - - // Output: - // Replace key1 - // Error - // Code 0 - // Data [[key1 new_value]] - // Replace key1 - // Error - // Code 0 - // Data [[key1 another_value]] - // Replace key1 - // Error - // Code 0 - // Data [[key1 value2]] - // Replace key1 - // Error - // Code 0 - // Data [[key1 new_value2]] -} - -func ExampleConnectionPool_Update() { - connPool, err := examplePool(testRoles, connOpts) - if err != nil { - fmt.Println(err) - } - defer connPool.Close() - - // Connect to servers[2] to check if tuple - // was inserted on RW instance - conn, err := tarantool.Connect(servers[2], connOpts) - if err != nil || conn == nil { - fmt.Printf("failed to connect to %s", servers[2]) - return - } - - // Insert a new tuple {"key1", "value1"}. - _, err = conn.Insert(spaceNo, []interface{}{"key1", "value1"}) - if err != nil { - fmt.Printf("Failed to insert: %s", err.Error()) - return - } - - // Update tuple with primary key { "key1" }. - resp, err := connPool.Update( - spaceName, indexName, []interface{}{"key1"}, - []interface{}{[]interface{}{"=", 1, "new_value"}}, pool.PreferRW) - fmt.Println("Update key1") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - - // Delete tuple with primary key "key1". - _, err = conn.Delete(spaceName, indexName, []interface{}{"key1"}) - if err != nil { - fmt.Printf("Failed to delete: %s", err.Error()) - } - - // Output: - // Update key1 - // Error - // Code 0 - // Data [[key1 new_value]] -} - -func ExampleConnectionPool_Call() { - connPool, err := examplePool(testRoles, connOpts) - if err != nil { - fmt.Println(err) - } - defer connPool.Close() - - // Call a function 'simple_incr' with arguments. - resp, err := connPool.Call17("simple_incr", []interface{}{1}, pool.PreferRW) - fmt.Println("Call simple_incr()") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - // Output: - // Call simple_incr() - // Error - // Code 0 - // Data [2] -} - -func ExampleConnectionPool_Eval() { - connPool, err := examplePool(testRoles, connOpts) - if err != nil { - fmt.Println(err) - } - defer connPool.Close() - - // Run raw Lua code. - resp, err := connPool.Eval("return 1 + 2", []interface{}{}, pool.PreferRW) - fmt.Println("Eval 'return 1 + 2'") - fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - // Output: - // Eval 'return 1 + 2' - // Error - // Code 0 - // Data [3] -} - -func ExampleConnectionPool_Do() { - connPool, err := examplePool(testRoles, connOpts) - if err != nil { - fmt.Println(err) - } - defer connPool.Close() - - // Ping a Tarantool instance to check connection. - req := tarantool.NewPingRequest() - resp, err := connPool.Do(req, pool.ANY).Get() - fmt.Println("Ping Code", resp.Code) - fmt.Println("Ping Data", resp.Data) - fmt.Println("Ping Error", err) - // Output: - // Ping Code 0 - // Ping Data [] + // Ping Error + // Ping Error + // Ping Error // Ping Error } @@ -917,7 +427,7 @@ func ExampleConnectorAdapter() { var connector tarantool.Connector = adapter // Ping an RW instance to check connection. - resp, err := connector.Ping() + resp, err := connector.Do(tarantool.NewPingRequest()).Get() fmt.Println("Ping Code", resp.Code) fmt.Println("Ping Data", resp.Data) fmt.Println("Ping Error", err) diff --git a/pool/pooler.go b/pool/pooler.go index a7cae8f95..a588d67f4 100644 --- a/pool/pooler.go +++ b/pool/pooler.go @@ -10,82 +10,148 @@ import ( type Pooler interface { ConnectedNow(mode Mode) (bool, error) Close() []error - Ping(mode Mode) (*tarantool.Response, error) ConfiguredTimeout(mode Mode) (time.Duration, error) + NewPrepared(expr string, mode Mode) (*tarantool.Prepared, error) + NewStream(mode Mode) (*tarantool.Stream, error) + NewWatcher(key string, callback tarantool.WatchCallback, + mode Mode) (tarantool.Watcher, error) + Do(req tarantool.Request, mode Mode) (fut *tarantool.Future) + // Deprecated: the method will be removed in the next major version, + // use a PingRequest object + Do() instead. + Ping(mode Mode) (*tarantool.Response, error) + // Deprecated: the method will be removed in the next major version, + // use a SelectRequest object + Do() instead. Select(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}, mode ...Mode) (*tarantool.Response, error) + // Deprecated: the method will be removed in the next major version, + // use an InsertRequest object + Do() instead. Insert(space interface{}, tuple interface{}, mode ...Mode) (*tarantool.Response, error) + // Deprecated: the method will be removed in the next major version, + // use a ReplaceRequest object + Do() instead. Replace(space interface{}, tuple interface{}, mode ...Mode) (*tarantool.Response, error) + // Deprecated: the method will be removed in the next major version, + // use a DeleteRequest object + Do() instead. Delete(space, index interface{}, key interface{}, mode ...Mode) (*tarantool.Response, error) + // Deprecated: the method will be removed in the next major version, + // use a UpdateRequest object + Do() instead. Update(space, index interface{}, key, ops interface{}, mode ...Mode) (*tarantool.Response, error) + // Deprecated: the method will be removed in the next major version, + // use a UpsertRequest object + Do() instead. Upsert(space interface{}, tuple, ops interface{}, mode ...Mode) (*tarantool.Response, error) + // Deprecated: the method will be removed in the next major version, + // use a CallRequest object + Do() instead. Call(functionName string, args interface{}, mode Mode) (*tarantool.Response, error) + // Deprecated: the method will be removed in the next major version, + // use a Call16Request object + Do() instead. Call16(functionName string, args interface{}, mode Mode) (*tarantool.Response, error) + // Deprecated: the method will be removed in the next major version, + // use a Call17Request object + Do() instead. Call17(functionName string, args interface{}, mode Mode) (*tarantool.Response, error) + // Deprecated: the method will be removed in the next major version, + // use an EvalRequest object + Do() instead. Eval(expr string, args interface{}, mode Mode) (*tarantool.Response, error) + // Deprecated: the method will be removed in the next major version, + // use an ExecuteRequest object + Do() instead. Execute(expr string, args interface{}, mode Mode) (*tarantool.Response, error) + // Deprecated: the method will be removed in the next major version, + // use a SelectRequest object + Do() instead. GetTyped(space, index interface{}, key interface{}, result interface{}, mode ...Mode) error + // Deprecated: the method will be removed in the next major version, + // use a SelectRequest object + Do() instead. SelectTyped(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}, result interface{}, mode ...Mode) error + // Deprecated: the method will be removed in the next major version, + // use an InsertRequest object + Do() instead. InsertTyped(space interface{}, tuple interface{}, result interface{}, mode ...Mode) error + // Deprecated: the method will be removed in the next major version, + // use a ReplaceRequest object + Do() instead. ReplaceTyped(space interface{}, tuple interface{}, result interface{}, mode ...Mode) error + // Deprecated: the method will be removed in the next major version, + // use a DeleteRequest object + Do() instead. DeleteTyped(space, index interface{}, key interface{}, result interface{}, mode ...Mode) error + // Deprecated: the method will be removed in the next major version, + // use a UpdateRequest object + Do() instead. UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}, mode ...Mode) error + // Deprecated: the method will be removed in the next major version, + // use a CallRequest object + Do() instead. CallTyped(functionName string, args interface{}, result interface{}, mode Mode) error + // Deprecated: the method will be removed in the next major version, + // use a Call16Request object + Do() instead. Call16Typed(functionName string, args interface{}, result interface{}, mode Mode) error + // Deprecated: the method will be removed in the next major version, + // use a Call17Request object + Do() instead. Call17Typed(functionName string, args interface{}, result interface{}, mode Mode) error + // Deprecated: the method will be removed in the next major version, + // use an EvalRequest object + Do() instead. EvalTyped(expr string, args interface{}, result interface{}, mode Mode) error + // Deprecated: the method will be removed in the next major version, + // use an ExecuteRequest object + Do() instead. ExecuteTyped(expr string, args interface{}, result interface{}, mode Mode) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) + // Deprecated: the method will be removed in the next major version, + // use a SelectRequest object + Do() instead. SelectAsync(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}, mode ...Mode) *tarantool.Future + // Deprecated: the method will be removed in the next major version, + // use an InsertRequest object + Do() instead. InsertAsync(space interface{}, tuple interface{}, mode ...Mode) *tarantool.Future + // Deprecated: the method will be removed in the next major version, + // use a ReplaceRequest object + Do() instead. ReplaceAsync(space interface{}, tuple interface{}, mode ...Mode) *tarantool.Future + // Deprecated: the method will be removed in the next major version, + // use a DeleteRequest object + Do() instead. DeleteAsync(space, index interface{}, key interface{}, mode ...Mode) *tarantool.Future + // Deprecated: the method will be removed in the next major version, + // use a UpdateRequest object + Do() instead. UpdateAsync(space, index interface{}, key, ops interface{}, mode ...Mode) *tarantool.Future + // Deprecated: the method will be removed in the next major version, + // use a UpsertRequest object + Do() instead. UpsertAsync(space interface{}, tuple interface{}, ops interface{}, mode ...Mode) *tarantool.Future + // Deprecated: the method will be removed in the next major version, + // use a CallRequest object + Do() instead. CallAsync(functionName string, args interface{}, mode Mode) *tarantool.Future + // Deprecated: the method will be removed in the next major version, + // use a Call16Request object + Do() instead. Call16Async(functionName string, args interface{}, mode Mode) *tarantool.Future + // Deprecated: the method will be removed in the next major version, + // use a Call17Request object + Do() instead. Call17Async(functionName string, args interface{}, mode Mode) *tarantool.Future + // Deprecated: the method will be removed in the next major version, + // use an EvalRequest object + Do() instead. EvalAsync(expr string, args interface{}, mode Mode) *tarantool.Future + // Deprecated: the method will be removed in the next major version, + // use an ExecuteRequest object + Do() instead. ExecuteAsync(expr string, args interface{}, mode Mode) *tarantool.Future - - NewPrepared(expr string, mode Mode) (*tarantool.Prepared, error) - NewStream(mode Mode) (*tarantool.Stream, error) - NewWatcher(key string, callback tarantool.WatchCallback, - mode Mode) (tarantool.Watcher, error) - - Do(req tarantool.Request, mode Mode) (fut *tarantool.Future) } diff --git a/queue/queue.go b/queue/queue.go index 2407bad03..615e56819 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -197,20 +197,25 @@ func New(conn tarantool.Connector, name string) Queue { // Create creates a new queue with config. func (q *queue) Create(cfg Cfg) error { cmd := "local name, type, cfg = ... ; queue.create_tube(name, type, cfg)" - _, err := q.conn.Eval(cmd, []interface{}{q.name, cfg.getType(), cfg.toMap()}) + _, err := q.conn.Do(tarantool.NewEvalRequest(cmd). + Args([]interface{}{q.name, cfg.getType(), cfg.toMap()}), + ).Get() return err } // Set queue settings. func (q *queue) Cfg(opts CfgOpts) error { - _, err := q.conn.Call17(q.cmds.cfg, []interface{}{opts.toMap()}) + req := tarantool.NewCallRequest(q.cmds.cfg).Args([]interface{}{opts.toMap()}) + _, err := q.conn.Do(req).Get() return err } // Exists checks existence of a tube. func (q *queue) Exists() (bool, error) { cmd := "local name = ... ; return queue.tube[name] ~= nil" - resp, err := q.conn.Eval(cmd, []string{q.name}) + resp, err := q.conn.Do(tarantool.NewEvalRequest(cmd). + Args([]string{q.name}), + ).Get() if err != nil { return false, err } @@ -238,7 +243,8 @@ func (q *queue) Identify(u *uuid.UUID) (uuid.UUID, error) { } } - if resp, err := q.conn.Call17(q.cmds.identify, args); err == nil { + req := tarantool.NewCallRequest(q.cmds.identify).Args(args) + if resp, err := q.conn.Do(req).Get(); err == nil { if us, ok := resp.Data[0].(string); ok { return uuid.FromBytes([]byte(us)) } else { @@ -264,7 +270,8 @@ func (q *queue) put(params ...interface{}) (*Task, error) { result: params[0], q: q, } - if err := q.conn.Call17Typed(q.cmds.put, params, &qd); err != nil { + req := tarantool.NewCallRequest(q.cmds.put).Args(params) + if err := q.conn.Do(req).GetTyped(&qd); err != nil { return nil, err } return qd.task, nil @@ -315,7 +322,8 @@ func (q *queue) take(params interface{}, result ...interface{}) (*Task, error) { if len(result) > 0 { qd.result = result[0] } - if err := q.conn.Call17Typed(q.cmds.take, []interface{}{params}, &qd); err != nil { + req := tarantool.NewCallRequest(q.cmds.take).Args([]interface{}{params}) + if err := q.conn.Do(req).GetTyped(&qd); err != nil { return nil, err } return qd.task, nil @@ -323,20 +331,21 @@ func (q *queue) take(params interface{}, result ...interface{}) (*Task, error) { // Drop queue. func (q *queue) Drop() error { - _, err := q.conn.Call17(q.cmds.drop, []interface{}{}) + _, err := q.conn.Do(tarantool.NewCallRequest(q.cmds.drop)).Get() return err } // ReleaseAll forcibly returns all taken tasks to a ready state. func (q *queue) ReleaseAll() error { - _, err := q.conn.Call17(q.cmds.releaseAll, []interface{}{}) + _, err := q.conn.Do(tarantool.NewCallRequest(q.cmds.releaseAll)).Get() return err } // Look at a task without changing its state. func (q *queue) Peek(taskId uint64) (*Task, error) { qd := queueData{q: q} - if err := q.conn.Call17Typed(q.cmds.peek, []interface{}{taskId}, &qd); err != nil { + req := tarantool.NewCallRequest(q.cmds.peek).Args([]interface{}{taskId}) + if err := q.conn.Do(req).GetTyped(&qd); err != nil { return nil, err } return qd.task, nil @@ -363,7 +372,8 @@ func (q *queue) _release(taskId uint64, cfg Opts) (string, error) { } func (q *queue) produce(cmd string, params ...interface{}) (string, error) { qd := queueData{q: q} - if err := q.conn.Call17Typed(cmd, params, &qd); err != nil || qd.task == nil { + req := tarantool.NewCallRequest(cmd).Args(params) + if err := q.conn.Do(req).GetTyped(&qd); err != nil || qd.task == nil { return "", err } return qd.task.status, nil @@ -388,7 +398,8 @@ func (r *kickResult) DecodeMsgpack(d *msgpack.Decoder) (err error) { // Reverse the effect of a bury request on one or more tasks. func (q *queue) Kick(count uint64) (uint64, error) { var r kickResult - err := q.conn.Call17Typed(q.cmds.kick, []interface{}{count}, &r) + req := tarantool.NewCallRequest(q.cmds.kick).Args([]interface{}{count}) + err := q.conn.Do(req).GetTyped(&r) return r.id, err } @@ -400,7 +411,7 @@ func (q *queue) Delete(taskId uint64) error { // State returns a current queue state. func (q *queue) State() (State, error) { - resp, err := q.conn.Call17(q.cmds.state, []interface{}{}) + resp, err := q.conn.Do(tarantool.NewCallRequest(q.cmds.state)).Get() if err != nil { return UnknownState, err } @@ -417,7 +428,8 @@ func (q *queue) State() (State, error) { // Return the number of tasks in a queue broken down by task_state, and the // number of requests broken down by the type of request. func (q *queue) Statistic() (interface{}, error) { - resp, err := q.conn.Call17(q.cmds.statistics, []interface{}{q.name}) + req := tarantool.NewCallRequest(q.cmds.statistics).Args([]interface{}{q.name}) + resp, err := q.conn.Do(req).Get() if err != nil { return nil, err } diff --git a/request.go b/request.go index adb4a7b31..48c13e869 100644 --- a/request.go +++ b/request.go @@ -167,6 +167,9 @@ func fillPing(enc *msgpack.Encoder) error { } // Ping sends empty request to Tarantool to check connection. +// +// Deprecated: the method will be removed in the next major version, +// use a PingRequest object + Do() instead. func (conn *Connection) Ping() (resp *Response, err error) { return conn.Do(NewPingRequest()).Get() } @@ -174,6 +177,9 @@ func (conn *Connection) Ping() (resp *Response, err error) { // Select performs select to box space. // // It is equal to conn.SelectAsync(...).Get(). +// +// Deprecated: the method will be removed in the next major version, +// use a SelectRequest object + Do() instead. func (conn *Connection) Select(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}) (resp *Response, err error) { return conn.SelectAsync(space, index, offset, limit, iterator, key).Get() } @@ -182,6 +188,9 @@ func (conn *Connection) Select(space, index interface{}, offset, limit uint32, i // Tarantool will reject Insert when tuple with same primary key exists. // // It is equal to conn.InsertAsync(space, tuple).Get(). +// +// Deprecated: the method will be removed in the next major version, +// use an InsertRequest object + Do() instead. func (conn *Connection) Insert(space interface{}, tuple interface{}) (resp *Response, err error) { return conn.InsertAsync(space, tuple).Get() } @@ -190,6 +199,9 @@ func (conn *Connection) Insert(space interface{}, tuple interface{}) (resp *Resp // If tuple with same primary key exists, it will be replaced. // // It is equal to conn.ReplaceAsync(space, tuple).Get(). +// +// Deprecated: the method will be removed in the next major version, +// use a ReplaceRequest object + Do() instead. func (conn *Connection) Replace(space interface{}, tuple interface{}) (resp *Response, err error) { return conn.ReplaceAsync(space, tuple).Get() } @@ -198,6 +210,9 @@ func (conn *Connection) Replace(space interface{}, tuple interface{}) (resp *Res // Result will contain array with deleted tuple. // // It is equal to conn.DeleteAsync(space, tuple).Get(). +// +// Deprecated: the method will be removed in the next major version, +// use a DeleteRequest object + Do() instead. func (conn *Connection) Delete(space, index interface{}, key interface{}) (resp *Response, err error) { return conn.DeleteAsync(space, index, key).Get() } @@ -206,6 +221,9 @@ func (conn *Connection) Delete(space, index interface{}, key interface{}) (resp // Result will contain array with updated tuple. // // It is equal to conn.UpdateAsync(space, tuple).Get(). +// +// Deprecated: the method will be removed in the next major version, +// use a UpdateRequest object + Do() instead. func (conn *Connection) Update(space, index interface{}, key, ops interface{}) (resp *Response, err error) { return conn.UpdateAsync(space, index, key, ops).Get() } @@ -214,6 +232,9 @@ func (conn *Connection) Update(space, index interface{}, key, ops interface{}) ( // Result will not contain any tuple. // // It is equal to conn.UpsertAsync(space, tuple, ops).Get(). +// +// Deprecated: the method will be removed in the next major version, +// use a UpsertRequest object + Do() instead. func (conn *Connection) Upsert(space interface{}, tuple, ops interface{}) (resp *Response, err error) { return conn.UpsertAsync(space, tuple, ops).Get() } @@ -222,6 +243,9 @@ func (conn *Connection) Upsert(space interface{}, tuple, ops interface{}) (resp // It uses request code for Tarantool >= 1.7, result is an array. // // It is equal to conn.CallAsync(functionName, args).Get(). +// +// Deprecated: the method will be removed in the next major version, +// use a CallRequest object + Do() instead. func (conn *Connection) Call(functionName string, args interface{}) (resp *Response, err error) { return conn.CallAsync(functionName, args).Get() } @@ -231,6 +255,9 @@ func (conn *Connection) Call(functionName string, args interface{}) (resp *Respo // Deprecated since Tarantool 1.7.2. // // It is equal to conn.Call16Async(functionName, args).Get(). +// +// Deprecated: the method will be removed in the next major version, +// use a Call16Request object + Do() instead. func (conn *Connection) Call16(functionName string, args interface{}) (resp *Response, err error) { return conn.Call16Async(functionName, args).Get() } @@ -239,6 +266,9 @@ func (conn *Connection) Call16(functionName string, args interface{}) (resp *Res // It uses request code for Tarantool >= 1.7, result is an array. // // It is equal to conn.Call17Async(functionName, args).Get(). +// +// Deprecated: the method will be removed in the next major version, +// use a Call17Request object + Do() instead. func (conn *Connection) Call17(functionName string, args interface{}) (resp *Response, err error) { return conn.Call17Async(functionName, args).Get() } @@ -246,6 +276,9 @@ func (conn *Connection) Call17(functionName string, args interface{}) (resp *Res // Eval passes Lua expression for evaluation. // // It is equal to conn.EvalAsync(space, tuple).Get(). +// +// Deprecated: the method will be removed in the next major version, +// use an EvalRequest object + Do() instead. func (conn *Connection) Eval(expr string, args interface{}) (resp *Response, err error) { return conn.EvalAsync(expr, args).Get() } @@ -254,6 +287,9 @@ func (conn *Connection) Eval(expr string, args interface{}) (resp *Response, err // // It is equal to conn.ExecuteAsync(expr, args).Get(). // Since 1.6.0 +// +// Deprecated: the method will be removed in the next major version, +// use an ExecuteRequest object + Do() instead. func (conn *Connection) Execute(expr string, args interface{}) (resp *Response, err error) { return conn.ExecuteAsync(expr, args).Get() } @@ -283,6 +319,9 @@ func (s *single) DecodeMsgpack(d *msgpack.Decoder) error { // to box space and fills typed result. // // It is equal to conn.SelectAsync(space, index, 0, 1, IterEq, key).GetTyped(&result) +// +// Deprecated: the method will be removed in the next major version, +// use a SelectRequest object + Do() instead. func (conn *Connection) GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) { s := single{res: result} err = conn.SelectAsync(space, index, 0, 1, IterEq, key).GetTyped(&s) @@ -292,6 +331,9 @@ func (conn *Connection) GetTyped(space, index interface{}, key interface{}, resu // SelectTyped performs select to box space and fills typed result. // // It is equal to conn.SelectAsync(space, index, offset, limit, iterator, key).GetTyped(&result) +// +// Deprecated: the method will be removed in the next major version, +// use a SelectRequest object + Do() instead. func (conn *Connection) SelectTyped(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}, result interface{}) (err error) { return conn.SelectAsync(space, index, offset, limit, iterator, key).GetTyped(result) } @@ -300,6 +342,9 @@ func (conn *Connection) SelectTyped(space, index interface{}, offset, limit uint // Tarantool will reject Insert when tuple with same primary key exists. // // It is equal to conn.InsertAsync(space, tuple).GetTyped(&result). +// +// Deprecated: the method will be removed in the next major version, +// use an InsertRequest object + Do() instead. func (conn *Connection) InsertTyped(space interface{}, tuple interface{}, result interface{}) (err error) { return conn.InsertAsync(space, tuple).GetTyped(result) } @@ -308,6 +353,9 @@ func (conn *Connection) InsertTyped(space interface{}, tuple interface{}, result // If tuple with same primary key exists, it will be replaced. // // It is equal to conn.ReplaceAsync(space, tuple).GetTyped(&result). +// +// Deprecated: the method will be removed in the next major version, +// use a ReplaceRequest object + Do() instead. func (conn *Connection) ReplaceTyped(space interface{}, tuple interface{}, result interface{}) (err error) { return conn.ReplaceAsync(space, tuple).GetTyped(result) } @@ -315,6 +363,9 @@ func (conn *Connection) ReplaceTyped(space interface{}, tuple interface{}, resul // DeleteTyped performs deletion of a tuple by key and fills result with deleted tuple. // // It is equal to conn.DeleteAsync(space, tuple).GetTyped(&result). +// +// Deprecated: the method will be removed in the next major version, +// use a DeleteRequest object + Do() instead. func (conn *Connection) DeleteTyped(space, index interface{}, key interface{}, result interface{}) (err error) { return conn.DeleteAsync(space, index, key).GetTyped(result) } @@ -322,6 +373,9 @@ func (conn *Connection) DeleteTyped(space, index interface{}, key interface{}, r // UpdateTyped performs update of a tuple by key and fills result with updated tuple. // // It is equal to conn.UpdateAsync(space, tuple, ops).GetTyped(&result). +// +// Deprecated: the method will be removed in the next major version, +// use a UpdateRequest object + Do() instead. func (conn *Connection) UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) (err error) { return conn.UpdateAsync(space, index, key, ops).GetTyped(result) } @@ -330,6 +384,9 @@ func (conn *Connection) UpdateTyped(space, index interface{}, key, ops interface // It uses request code for Tarantool >= 1.7, result is an array. // // It is equal to conn.Call16Async(functionName, args).GetTyped(&result). +// +// Deprecated: the method will be removed in the next major version, +// use a CallRequest object + Do() instead. func (conn *Connection) CallTyped(functionName string, args interface{}, result interface{}) (err error) { return conn.CallAsync(functionName, args).GetTyped(result) } @@ -339,6 +396,9 @@ func (conn *Connection) CallTyped(functionName string, args interface{}, result // Deprecated since Tarantool 1.7.2. // // It is equal to conn.Call16Async(functionName, args).GetTyped(&result). +// +// Deprecated: the method will be removed in the next major version, +// use a Call16Request object + Do() instead. func (conn *Connection) Call16Typed(functionName string, args interface{}, result interface{}) (err error) { return conn.Call16Async(functionName, args).GetTyped(result) } @@ -347,6 +407,9 @@ func (conn *Connection) Call16Typed(functionName string, args interface{}, resul // It uses request code for Tarantool >= 1.7, result is an array. // // It is equal to conn.Call17Async(functionName, args).GetTyped(&result). +// +// Deprecated: the method will be removed in the next major version, +// use a Call17Request object + Do() instead. func (conn *Connection) Call17Typed(functionName string, args interface{}, result interface{}) (err error) { return conn.Call17Async(functionName, args).GetTyped(result) } @@ -354,6 +417,9 @@ func (conn *Connection) Call17Typed(functionName string, args interface{}, resul // EvalTyped passes Lua expression for evaluation. // // It is equal to conn.EvalAsync(space, tuple).GetTyped(&result). +// +// Deprecated: the method will be removed in the next major version, +// use an EvalRequest object + Do() instead. func (conn *Connection) EvalTyped(expr string, args interface{}, result interface{}) (err error) { return conn.EvalAsync(expr, args).GetTyped(result) } @@ -362,6 +428,9 @@ func (conn *Connection) EvalTyped(expr string, args interface{}, result interfac // // In addition to error returns sql info and columns meta data // Since 1.6.0 +// +// Deprecated: the method will be removed in the next major version, +// use an ExecuteRequest object + Do() instead. func (conn *Connection) ExecuteTyped(expr string, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error) { fut := conn.ExecuteAsync(expr, args) err := fut.GetTyped(&result) @@ -369,6 +438,9 @@ func (conn *Connection) ExecuteTyped(expr string, args interface{}, result inter } // SelectAsync sends select request to Tarantool and returns Future. +// +// Deprecated: the method will be removed in the next major version, +// use a SelectRequest object + Do() instead. func (conn *Connection) SelectAsync(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}) *Future { req := NewSelectRequest(space). Index(index). @@ -381,6 +453,9 @@ func (conn *Connection) SelectAsync(space, index interface{}, offset, limit uint // InsertAsync sends insert action to Tarantool and returns Future. // Tarantool will reject Insert when tuple with same primary key exists. +// +// Deprecated: the method will be removed in the next major version, +// use an InsertRequest object + Do() instead. func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Future { req := NewInsertRequest(space).Tuple(tuple) return conn.Do(req) @@ -388,6 +463,9 @@ func (conn *Connection) InsertAsync(space interface{}, tuple interface{}) *Futur // ReplaceAsync sends "insert or replace" action to Tarantool and returns Future. // If tuple with same primary key exists, it will be replaced. +// +// Deprecated: the method will be removed in the next major version, +// use a ReplaceRequest object + Do() instead. func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Future { req := NewReplaceRequest(space).Tuple(tuple) return conn.Do(req) @@ -395,6 +473,9 @@ func (conn *Connection) ReplaceAsync(space interface{}, tuple interface{}) *Futu // DeleteAsync sends deletion action to Tarantool and returns Future. // Future's result will contain array with deleted tuple. +// +// Deprecated: the method will be removed in the next major version, +// use a DeleteRequest object + Do() instead. func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) *Future { req := NewDeleteRequest(space).Index(index).Key(key) return conn.Do(req) @@ -402,6 +483,9 @@ func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) * // Update sends deletion of a tuple by key and returns Future. // Future's result will contain array with updated tuple. +// +// Deprecated: the method will be removed in the next major version, +// use a UpdateRequest object + Do() instead. func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface{}) *Future { req := NewUpdateRequest(space).Index(index).Key(key) req.ops = ops @@ -410,6 +494,9 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface // UpsertAsync sends "update or insert" action to Tarantool and returns Future. // Future's sesult will not contain any tuple. +// +// Deprecated: the method will be removed in the next major version, +// use a UpsertRequest object + Do() instead. func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *Future { req := NewUpsertRequest(space).Tuple(tuple) req.ops = ops @@ -418,6 +505,9 @@ func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops in // CallAsync sends a call to registered Tarantool function and returns Future. // It uses request code for Tarantool >= 1.7, so future's result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a CallRequest object + Do() instead. func (conn *Connection) CallAsync(functionName string, args interface{}) *Future { req := NewCallRequest(functionName).Args(args) return conn.Do(req) @@ -426,6 +516,9 @@ func (conn *Connection) CallAsync(functionName string, args interface{}) *Future // Call16Async sends a call to registered Tarantool function and returns Future. // It uses request code for Tarantool 1.6, so future's result is an array of arrays. // Deprecated since Tarantool 1.7.2. +// +// Deprecated: the method will be removed in the next major version, +// use a Call16Request object + Do() instead. func (conn *Connection) Call16Async(functionName string, args interface{}) *Future { req := NewCall16Request(functionName).Args(args) return conn.Do(req) @@ -433,12 +526,18 @@ func (conn *Connection) Call16Async(functionName string, args interface{}) *Futu // Call17Async sends a call to registered Tarantool function and returns Future. // It uses request code for Tarantool >= 1.7, so future's result is an array. +// +// Deprecated: the method will be removed in the next major version, +// use a Call17Request object + Do() instead. func (conn *Connection) Call17Async(functionName string, args interface{}) *Future { req := NewCall17Request(functionName).Args(args) return conn.Do(req) } // EvalAsync sends a Lua expression for evaluation and returns Future. +// +// Deprecated: the method will be removed in the next major version, +// use an EvalRequest object + Do() instead. func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { req := NewEvalRequest(expr).Args(args) return conn.Do(req) @@ -446,6 +545,9 @@ func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { // ExecuteAsync sends a sql expression for execution and returns Future. // Since 1.6.0 +// +// Deprecated: the method will be removed in the next major version, +// use an ExecuteRequest object + Do() instead. func (conn *Connection) ExecuteAsync(expr string, args interface{}) *Future { req := NewExecuteRequest(expr).Args(args) return conn.Do(req) diff --git a/settings/example_test.go b/settings/example_test.go index c9bdc3c49..b1d0e5d4f 100644 --- a/settings/example_test.go +++ b/settings/example_test.go @@ -31,14 +31,16 @@ func Example_sqlFullColumnNames() { } // Create a space. - _, err = conn.Execute("CREATE TABLE example(id INT PRIMARY KEY, x INT);", []interface{}{}) + req := tarantool.NewExecuteRequest("CREATE TABLE example(id INT PRIMARY KEY, x INT);") + _, err = conn.Do(req).Get() if err != nil { fmt.Printf("error in create table: %v\n", err) return } // Insert some tuple into space. - _, err = conn.Execute("INSERT INTO example VALUES (1, 1);", []interface{}{}) + req = tarantool.NewExecuteRequest("INSERT INTO example VALUES (1, 1);") + _, err = conn.Do(req).Get() if err != nil { fmt.Printf("error on insert: %v\n", err) return @@ -52,7 +54,8 @@ func Example_sqlFullColumnNames() { } // Get some data with SQL query. - resp, err = conn.Execute("SELECT x FROM example WHERE id = 1;", []interface{}{}) + req = tarantool.NewExecuteRequest("SELECT x FROM example WHERE id = 1;") + _, err = conn.Do(req).Get() if err != nil { fmt.Printf("error on select: %v\n", err) return @@ -68,7 +71,7 @@ func Example_sqlFullColumnNames() { } // Get some data with SQL query. - resp, err = conn.Execute("SELECT x FROM example WHERE id = 1;", []interface{}{}) + _, err = conn.Do(req).Get() if err != nil { fmt.Printf("error on select: %v\n", err) return diff --git a/settings/tarantool_test.go b/settings/tarantool_test.go index aeec28cc0..128e84328 100644 --- a/settings/tarantool_test.go +++ b/settings/tarantool_test.go @@ -68,7 +68,8 @@ func TestErrorMarshalingEnabledSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", false}}, resp.Data) // Get a box.Error value. - resp, err = conn.Eval("return box.error.new(box.error.UNKNOWN)", []interface{}{}) + eval := tarantool.NewEvalRequest("return box.error.new(box.error.UNKNOWN)") + resp, err = conn.Do(eval).Get() require.Nil(t, err) require.NotNil(t, resp) require.IsType(t, "string", resp.Data[0]) @@ -86,7 +87,7 @@ func TestErrorMarshalingEnabledSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", true}}, resp.Data) // Get a box.Error value. - resp, err = conn.Eval("return box.error.new(box.error.UNKNOWN)", []interface{}{}) + resp, err = conn.Do(eval).Get() require.Nil(t, err) require.NotNil(t, resp) _, ok := resp.Data[0].(*tarantool.BoxError) @@ -116,13 +117,15 @@ func TestSQLDefaultEngineSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "vinyl"}}, resp.Data) // Create a space with "CREATE TABLE". - resp, err = conn.Execute("CREATE TABLE t1_vinyl(a INT PRIMARY KEY, b INT, c INT);", []interface{}{}) + exec := tarantool.NewExecuteRequest("CREATE TABLE t1_vinyl(a INT PRIMARY KEY, b INT, c INT);") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) // Check new space engine. - resp, err = conn.Eval("return box.space['T1_VINYL'].engine", []interface{}{}) + eval := tarantool.NewEvalRequest("return box.space['T1_VINYL'].engine") + resp, err = conn.Do(eval).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, "vinyl", resp.Data[0]) @@ -140,13 +143,15 @@ func TestSQLDefaultEngineSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, resp.Data) // Create a space with "CREATE TABLE". - resp, err = conn.Execute("CREATE TABLE t2_memtx(a INT PRIMARY KEY, b INT, c INT);", []interface{}{}) + exec = tarantool.NewExecuteRequest("CREATE TABLE t2_memtx(a INT PRIMARY KEY, b INT, c INT);") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) // Check new space engine. - resp, err = conn.Eval("return box.space['T2_MEMTX'].engine", []interface{}{}) + eval = tarantool.NewEvalRequest("return box.space['T2_MEMTX'].engine") + resp, err = conn.Do(eval).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, "memtx", resp.Data[0]) @@ -163,13 +168,15 @@ func TestSQLDeferForeignKeysSetting(t *testing.T) { defer conn.Close() // Create a parent space. - resp, err = conn.Execute("CREATE TABLE parent(id INT PRIMARY KEY, y INT UNIQUE);", []interface{}{}) + exec := tarantool.NewExecuteRequest("CREATE TABLE parent(id INT PRIMARY KEY, y INT UNIQUE);") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) // Create a space with reference to the parent space. - resp, err = conn.Execute("CREATE TABLE child(id INT PRIMARY KEY, x INT REFERENCES parent(y));", []interface{}{}) + exec = tarantool.NewExecuteRequest("CREATE TABLE child(id INT PRIMARY KEY, x INT REFERENCES parent(y));") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) @@ -200,7 +207,7 @@ func TestSQLDeferForeignKeysSetting(t *testing.T) { // Evaluate a scenario when foreign key not exists // on INSERT, but exists on commit. - _, err = conn.Eval(deferEval, []interface{}{}) + _, err = conn.Do(tarantool.NewEvalRequest(deferEval)).Get() require.NotNil(t, err) require.ErrorContains(t, err, "Failed to execute SQL statement: FOREIGN KEY constraint failed") @@ -217,7 +224,7 @@ func TestSQLDeferForeignKeysSetting(t *testing.T) { // Evaluate a scenario when foreign key not exists // on INSERT, but exists on commit. - resp, err = conn.Eval(deferEval, []interface{}{}) + resp, err = conn.Do(tarantool.NewEvalRequest(deferEval)).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, true, resp.Data[0]) @@ -233,13 +240,15 @@ func TestSQLFullColumnNamesSetting(t *testing.T) { defer conn.Close() // Create a space. - resp, err = conn.Execute("CREATE TABLE fkname(id INT PRIMARY KEY, x INT);", []interface{}{}) + exec := tarantool.NewExecuteRequest("CREATE TABLE fkname(id INT PRIMARY KEY, x INT);") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) // Fill it with some data. - resp, err = conn.Execute("INSERT INTO fkname VALUES (1, 1);", []interface{}{}) + exec = tarantool.NewExecuteRequest("INSERT INTO fkname VALUES (1, 1);") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) @@ -257,7 +266,8 @@ func TestSQLFullColumnNamesSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", false}}, resp.Data) // Get a data with short column names in metadata. - resp, err = conn.Execute("SELECT x FROM fkname WHERE id = 1;", []interface{}{}) + exec = tarantool.NewExecuteRequest("SELECT x FROM fkname WHERE id = 1;") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, "X", resp.MetaData[0].FieldName) @@ -275,7 +285,8 @@ func TestSQLFullColumnNamesSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, resp.Data) // Get a data with full column names in metadata. - resp, err = conn.Execute("SELECT x FROM fkname WHERE id = 1;", []interface{}{}) + exec = tarantool.NewExecuteRequest("SELECT x FROM fkname WHERE id = 1;") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, "FKNAME.X", resp.MetaData[0].FieldName) @@ -291,13 +302,15 @@ func TestSQLFullMetadataSetting(t *testing.T) { defer conn.Close() // Create a space. - resp, err = conn.Execute("CREATE TABLE fmt(id INT PRIMARY KEY, x INT);", []interface{}{}) + exec := tarantool.NewExecuteRequest("CREATE TABLE fmt(id INT PRIMARY KEY, x INT);") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) // Fill it with some data. - resp, err = conn.Execute("INSERT INTO fmt VALUES (1, 1);", []interface{}{}) + exec = tarantool.NewExecuteRequest("INSERT INTO fmt VALUES (1, 1);") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) @@ -314,7 +327,8 @@ func TestSQLFullMetadataSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", false}}, resp.Data) // Get a data without additional fields in metadata. - resp, err = conn.Execute("SELECT x FROM fmt WHERE id = 1;", []interface{}{}) + exec = tarantool.NewExecuteRequest("SELECT x FROM fmt WHERE id = 1;") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, "", resp.MetaData[0].FieldSpan) @@ -332,7 +346,8 @@ func TestSQLFullMetadataSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", true}}, resp.Data) // Get a data with additional fields in metadata. - resp, err = conn.Execute("SELECT x FROM fmt WHERE id = 1;", []interface{}{}) + exec = tarantool.NewExecuteRequest("SELECT x FROM fmt WHERE id = 1;") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, "x", resp.MetaData[0].FieldSpan) @@ -386,22 +401,25 @@ func TestSQLRecursiveTriggersSetting(t *testing.T) { defer conn.Close() // Create a space. - resp, err = conn.Execute("CREATE TABLE rec(id INTEGER PRIMARY KEY, a INT, b INT);", []interface{}{}) + exec := tarantool.NewExecuteRequest("CREATE TABLE rec(id INTEGER PRIMARY KEY, a INT, b INT);") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) // Fill it with some data. - resp, err = conn.Execute("INSERT INTO rec VALUES(1, 1, 2);", []interface{}{}) + exec = tarantool.NewExecuteRequest("INSERT INTO rec VALUES(1, 1, 2);") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) // Create a recursive trigger (with infinite depth). - resp, err = conn.Execute(` + exec = tarantool.NewExecuteRequest(` CREATE TRIGGER tr12 AFTER UPDATE ON rec FOR EACH ROW BEGIN UPDATE rec SET a=new.a+1, b=new.b+1; - END;`, []interface{}{}) + END;`) + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) @@ -419,7 +437,8 @@ func TestSQLRecursiveTriggersSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", true}}, resp.Data) // Trigger the recursion. - _, err = conn.Execute("UPDATE rec SET a=a+1, b=b+1;", []interface{}{}) + exec = tarantool.NewExecuteRequest("UPDATE rec SET a=a+1, b=b+1;") + _, err = conn.Do(exec).Get() require.NotNil(t, err) require.ErrorContains(t, err, "Failed to execute SQL statement: too many levels of trigger recursion") @@ -436,7 +455,8 @@ func TestSQLRecursiveTriggersSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", false}}, resp.Data) // Trigger the recursion. - resp, err = conn.Execute("UPDATE rec SET a=a+1, b=b+1;", []interface{}{}) + exec = tarantool.NewExecuteRequest("UPDATE rec SET a=a+1, b=b+1;") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) @@ -452,18 +472,21 @@ func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { defer conn.Close() // Create a space. - resp, err = conn.Execute("CREATE TABLE data(id STRING PRIMARY KEY);", []interface{}{}) + exec := tarantool.NewExecuteRequest("CREATE TABLE data(id STRING PRIMARY KEY);") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) // Fill it with some data. - resp, err = conn.Execute("INSERT INTO data VALUES('1');", []interface{}{}) + exec = tarantool.NewExecuteRequest("INSERT INTO data VALUES('1');") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) - resp, err = conn.Execute("INSERT INTO data VALUES('2');", []interface{}{}) + exec = tarantool.NewExecuteRequest("INSERT INTO data VALUES('2');") + resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) @@ -488,7 +511,7 @@ func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { query = "SELECT * FROM data;" } - resp, err = conn.Execute(query, []interface{}{}) + resp, err = conn.Do(tarantool.NewExecuteRequest(query)).Get() require.Nil(t, err) require.NotNil(t, resp) require.EqualValues(t, []interface{}{"1"}, resp.Data[0]) @@ -507,7 +530,7 @@ func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", true}}, resp.Data) // Select multiple records. - resp, err = conn.Execute(query, []interface{}{}) + resp, err = conn.Do(tarantool.NewExecuteRequest(query)).Get() require.Nil(t, err) require.NotNil(t, resp) require.EqualValues(t, []interface{}{"2"}, resp.Data[0]) diff --git a/test_helpers/main.go b/test_helpers/main.go index c234d4c02..c777f2f26 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -106,7 +106,7 @@ func isReady(server string, opts *tarantool.Opts) error { } defer conn.Close() - resp, err = conn.Ping() + resp, err = conn.Do(tarantool.NewPingRequest()).Get() if err != nil { return err } diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index a559ef98d..7dcd70cbc 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -82,7 +82,8 @@ func ProcessListenOnInstance(args interface{}) error { } for i := 0; i < listenArgs.ServersNumber; i++ { - resp, err := listenArgs.ConnPool.Eval("return box.cfg.listen", []interface{}{}, listenArgs.Mode) + req := tarantool.NewEvalRequest("return box.cfg.listen") + resp, err := listenArgs.ConnPool.Do(req, listenArgs.Mode).Get() if err != nil { return fmt.Errorf("fail to Eval: %s", err.Error()) } @@ -138,7 +139,7 @@ func InsertOnInstance(server string, connOpts tarantool.Opts, space interface{}, } defer conn.Close() - resp, err := conn.Insert(space, tuple) + resp, err := conn.Do(tarantool.NewInsertRequest(space).Tuple(tuple)).Get() if err != nil { return fmt.Errorf("Failed to Insert: %s", err.Error()) } @@ -195,8 +196,9 @@ func SetInstanceRO(server string, connOpts tarantool.Opts, isReplica bool) error defer conn.Close() - _, err = conn.Call17("box.cfg", []interface{}{map[string]bool{"read_only": isReplica}}) - if err != nil { + req := tarantool.NewCallRequest("box.cfg"). + Args([]interface{}{map[string]bool{"read_only": isReplica}}) + if _, err := conn.Do(req).Get(); err != nil { return err } diff --git a/uuid/example_test.go b/uuid/example_test.go index 6ad8ebad9..632f620be 100644 --- a/uuid/example_test.go +++ b/uuid/example_test.go @@ -37,7 +37,9 @@ func Example() { log.Fatalf("Failed to prepare uuid: %s", uuidErr) } - resp, err := client.Replace(spaceNo, []interface{}{id}) + resp, err := client.Do(tarantool.NewReplaceRequest(spaceNo). + Tuple([]interface{}{id}), + ).Get() fmt.Println("UUID tuple replace") fmt.Println("Error", err) diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index f09caf03d..84abd42d2 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -81,7 +81,12 @@ func TestSelect(t *testing.T) { t.Fatalf("Failed to prepare test uuid: %s", uuidErr) } - resp, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{id}) + sel := NewSelectRequest(space). + Index(index). + Limit(1). + Iterator(IterEq). + Key([]interface{}{id}) + resp, errSel := conn.Do(sel).Get() if errSel != nil { t.Fatalf("UUID select failed: %s", errSel.Error()) } @@ -91,7 +96,7 @@ func TestSelect(t *testing.T) { tupleValueIsId(t, resp.Data, id) var tuples []TupleUUID - errTyp := conn.SelectTyped(space, index, 0, 1, IterEq, []interface{}{id}, &tuples) + errTyp := conn.Do(sel).GetTyped(&tuples) if errTyp != nil { t.Fatalf("Failed to SelectTyped: %s", errTyp.Error()) } @@ -116,7 +121,8 @@ func TestReplace(t *testing.T) { t.Errorf("Failed to prepare test uuid: %s", uuidErr) } - respRep, errRep := conn.Replace(space, []interface{}{id}) + rep := NewReplaceRequest(space).Tuple([]interface{}{id}) + respRep, errRep := conn.Do(rep).Get() if errRep != nil { t.Errorf("UUID replace failed: %s", errRep) } @@ -125,7 +131,12 @@ func TestReplace(t *testing.T) { } tupleValueIsId(t, respRep.Data, id) - respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{id}) + sel := NewSelectRequest(space). + Index(index). + Limit(1). + Iterator(IterEq). + Key([]interface{}{id}) + respSel, errSel := conn.Do(sel).Get() if errSel != nil { t.Errorf("UUID select failed: %s", errSel) } From 4f9b16162f08f3c3db8e7e38e5786654d34bfbed Mon Sep 17 00:00:00 2001 From: Islam Elkanov Date: Mon, 26 Jun 2023 18:22:32 +0300 Subject: [PATCH 465/605] ci: bump GitHub action versions in reusable_testing.yml - actions/checkout: v2 -> v3 - actions/download-artifact: v2 -> v3 --- .github/workflows/reusable_testing.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index 987d1d734..7a23e6f8a 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -14,12 +14,12 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Clone the go-tarantool connector - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: ${{ github.repository_owner }}/go-tarantool - name: Download the tarantool build artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: ${{ inputs.artifact_name }} From c2498be2dd8794aeef335e880f6324f2c5ceacc0 Mon Sep 17 00:00:00 2001 From: Vladimir Davydov Date: Tue, 27 Jun 2023 16:40:52 +0300 Subject: [PATCH 466/605] queue: encode UUID argument of identify() as string instead of binary The identify() function expects the UUID argument to be a plain string while the go connector encodes it in MsgPack as a binary blob (MP_BIN). This works fine for now because Tarantool stores MP_BIN data in a string when decoded to Lua but this behavior is going to change soon: we're planning to introduce the new Lua type for binary data and update the MsgPack decoder to store MP_BIN data in a varbianry object instead of a plain string. Let's prepare for that by converting the UUID data to a string before encoding. Needed for tarantool/tarantool#1629 --- CHANGELOG.md | 3 +++ queue/queue.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fcd90d53..23c0bb63f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - connection_pool renamed to pool (#239) - Use msgpack/v5 instead of msgpack.v2 (#236) - Call/NewCallRequest = Call17/NewCall17Request (#235) +- Change encoding of the queue.Identify() UUID argument from binary blob to + plain string. Needed for upgrade to Tarantool 3.0, where a binary blob is + decoded to a varbinary object (#313). ### Deprecated diff --git a/queue/queue.go b/queue/queue.go index 615e56819..5c0506f3e 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -239,7 +239,7 @@ func (q *queue) Identify(u *uuid.UUID) (uuid.UUID, error) { if bytes, err := u.MarshalBinary(); err != nil { return uuid.UUID{}, err } else { - args = []interface{}{bytes} + args = []interface{}{string(bytes)} } } From 44db92bf1ce43c5c247e755e8c972492b575134a Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 27 Jun 2023 12:37:40 +0300 Subject: [PATCH 467/605] ci: enable more linters - forbidigo - gocritic - lll - reassign - stylecheck - unconvert --- .github/workflows/check.yaml | 13 +- .golangci.yaml | 30 ++ CHANGELOG.md | 1 + Makefile | 2 +- box_error_test.go | 18 +- connection.go | 31 +- connector.go | 52 ++-- crud/error.go | 8 +- crud/request_test.go | 221 ++++++++------ crud/result.go | 1 - crud/tarantool_test.go | 11 +- crud/unflatten_rows.go | 4 +- crud/upsert_many.go | 3 +- datetime/datetime.go | 35 +-- datetime/datetime_test.go | 3 +- datetime/interval.go | 2 +- decimal/bcd.go | 20 +- decimal/decimal.go | 25 +- decimal/decimal_test.go | 72 +++-- example_test.go | 10 +- export_test.go | 3 +- future.go | 3 +- pool/connection_pool.go | 551 ++++++++++++++++++---------------- pool/connection_pool_test.go | 52 ++-- pool/connector_test.go | 88 +++--- queue/example_msgpack_test.go | 20 +- queue/queue_test.go | 160 +++++----- request.go | 68 +++-- request_test.go | 3 +- response.go | 6 +- schema.go | 33 +- settings/request.go | 3 +- settings/request_test.go | 12 +- settings/tarantool_test.go | 18 +- shutdown_test.go | 3 +- smallbuf.go | 2 +- ssl_test.go | 8 +- stream.go | 9 +- tarantool_test.go | 100 +++--- test_helpers/main.go | 8 +- test_helpers/pool_helper.go | 28 +- uuid/uuid.go | 11 +- uuid/uuid_test.go | 3 +- 43 files changed, 991 insertions(+), 763 deletions(-) create mode 100644 .golangci.yaml diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 2297b0b3c..88b6afa43 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -40,24 +40,15 @@ jobs: uses: golangci/golangci-lint-action@v3 continue-on-error: true with: - # The suppression of the rule `errcheck` may be removed after adding - # errors check in all methods calling EncodeXxx inside. - # For now those methods are not even able to return any error - # cause of internal implementation of writer interface (see smallbuf.go). - # - # The `//nolint` workaround was not the acceptable way of warnings suppression, - # cause those comments get rendered in documentation by godoc. - # See https://github.com/tarantool/go-tarantool/pull/160#discussion_r858608221 - # # The first run is for GitHub Actions error format. - args: -E goimports -D errcheck + args: --config=.golangci.yaml - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: # The second run is for human-readable error format with a file name # and a line number. - args: --out-${NO_FUTURE}format colored-line-number -E goimports -D errcheck + args: --out-${NO_FUTURE}format colored-line-number --config=.golangci.yaml codespell: runs-on: ubuntu-latest diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 000000000..32f87cb0d --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,30 @@ +run: + timeout: 3m + +linters: + disable: + - errcheck + enable: + - forbidigo + - gocritic + - goimports + - lll + - reassign + - stylecheck + - unconvert + +linters-settings: + gocritic: + disabled-checks: + - ifElseChain + lll: + line-length: 100 + tab-width: 4 + stylecheck: + checks: ["all", "-ST1003"] + +issues: + exclude-rules: + - linters: + - lll + source: "\t?// *(see )?https://" diff --git a/CHANGELOG.md b/CHANGELOG.md index 23c0bb63f..c449d98ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Type() method to the Request interface (#158) - Enumeration types for RLimitAction/iterators (#158) - IsNullable flag for Field (#302) +- More linters on CI (#310) ### Changed diff --git a/Makefile b/Makefile index f48b789cc..4f676a530 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ format: .PHONY: golangci-lint golangci-lint: - golangci-lint run -E goimports -D errcheck + golangci-lint run --config=.golangci.yaml .PHONY: test test: diff --git a/box_error_test.go b/box_error_test.go index d8ff5b11f..0d7d3f47b 100644 --- a/box_error_test.go +++ b/box_error_test.go @@ -129,12 +129,14 @@ var mpDecodeSamples = map[string]struct { "InnerMapInvalidErrorType": { []byte{0x81, 0x00, 0x91, 0x81, 0x00, 0xc1}, false, - regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`), + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1` + + ` decoding (?:string\/bytes|bytes) length`), }, "InnerMapInvalidErrorFile": { []byte{0x81, 0x00, 0x91, 0x81, 0x01, 0xc1}, false, - regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`), + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1` + + ` decoding (?:string\/bytes|bytes) length`), }, "InnerMapInvalidErrorLine": { []byte{0x81, 0x00, 0x91, 0x81, 0x02, 0xc1}, @@ -144,7 +146,8 @@ var mpDecodeSamples = map[string]struct { "InnerMapInvalidErrorMessage": { []byte{0x81, 0x00, 0x91, 0x81, 0x03, 0xc1}, false, - regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`), + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding` + + ` (?:string\/bytes|bytes) length`), }, "InnerMapInvalidErrorErrno": { []byte{0x81, 0x00, 0x91, 0x81, 0x04, 0xc1}, @@ -164,7 +167,8 @@ var mpDecodeSamples = map[string]struct { "InnerMapInvalidErrorFieldsKey": { []byte{0x81, 0x00, 0x91, 0x81, 0x06, 0x81, 0xc1}, false, - regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`), + regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1` + + ` decoding (?:string\/bytes|bytes) length`), }, "InnerMapInvalidErrorFieldsValue": { []byte{0x81, 0x00, 0x91, 0x81, 0x06, 0x81, 0xa3, 0x6b, 0x65, 0x79, 0xc1}, @@ -435,7 +439,8 @@ func TestErrorTypeSelect(t *testing.T) { var resp *Response var offset uint32 = 0 var limit uint32 = 1 - resp, err = conn.Select(space, index, offset, limit, IterEq, []interface{}{testcase.tuple.pk}) + resp, err = conn.Select(space, index, offset, limit, IterEq, + []interface{}{testcase.tuple.pk}) require.Nil(t, err) require.NotNil(t, resp.Data) require.Equalf(t, len(resp.Data), 1, "Exactly one tuple had been found") @@ -479,7 +484,8 @@ func TestErrorTypeSelectTyped(t *testing.T) { var offset uint32 = 0 var limit uint32 = 1 var resp []TupleBoxError - err = conn.SelectTyped(space, index, offset, limit, IterEq, []interface{}{testcase.tuple.pk}, &resp) + err = conn.SelectTyped(space, index, offset, limit, IterEq, + []interface{}{testcase.tuple.pk}, &resp) require.Nil(t, err) require.NotNil(t, resp) require.Equalf(t, len(resp), 1, "Exactly one tuple had been found") diff --git a/connection.go b/connection.go index ea7f236fe..367cf9640 100644 --- a/connection.go +++ b/connection.go @@ -33,6 +33,11 @@ const shutdownEventKey = "box.shutdown" type ConnEventKind int type ConnLogKind int +var ( + errUnknownRequest = errors.New("the passed connected request doesn't belong " + + "to the current connection or connection pool") +) + const ( // Connected signals that connection is established or reestablished. Connected ConnEventKind = iota + 1 @@ -84,13 +89,16 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac case LogReconnectFailed: reconnects := v[0].(uint) err := v[1].(error) - log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s", reconnects, conn.opts.MaxReconnects, conn.addr, err) + log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s", + reconnects, conn.opts.MaxReconnects, conn.addr, err) case LogLastReconnectFailed: err := v[0].(error) - log.Printf("tarantool: last reconnect to %s failed: %s, giving it up", conn.addr, err) + log.Printf("tarantool: last reconnect to %s failed: %s, giving it up", + conn.addr, err) case LogUnexpectedResultId: resp := v[0].(*Response) - log.Printf("tarantool: connection %s got unexpected resultId (%d) in response", conn.addr, resp.RequestId) + log.Printf("tarantool: connection %s got unexpected resultId (%d) in response", + conn.addr, resp.RequestId) case LogWatchEventReadFailed: err := v[0].(error) log.Printf("tarantool: unable to parse watch event: %s", err) @@ -145,7 +153,8 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac // by timeout). Client reconnect will happen if connection options enable // reconnect. Beware that graceful shutdown event initialization is asynchronous. // -// More on graceful shutdown: https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/ +// More on graceful shutdown: +// https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/ type Connection struct { addr string c Conn @@ -579,7 +588,8 @@ func (conn *Connection) dial() (err error) { go conn.reader(c, c) // Subscribe shutdown event to process graceful shutdown. - if conn.shutdownWatcher == nil && isFeatureInSlice(WatchersFeature, conn.serverProtocolInfo.Features) { + if conn.shutdownWatcher == nil && + isFeatureInSlice(WatchersFeature, conn.serverProtocolInfo.Features) { watcher, werr := conn.newWatcherImpl(shutdownEventKey, shutdownEventCallback) if werr != nil { return werr @@ -696,7 +706,10 @@ func (conn *Connection) closeConnection(neterr error, forever bool) (err error) } for i := range conn.shard { conn.shard[i].buf.Reset() - requestsLists := []*[requestsMap]futureList{&conn.shard[i].requests, &conn.shard[i].requestsWithCtx} + requestsLists := []*[requestsMap]futureList{ + &conn.shard[i].requests, + &conn.shard[i].requestsWithCtx, + } for _, requests := range requestsLists { for pos := range requests { requests[pos].clear(neterr, conn) @@ -1207,7 +1220,7 @@ func read(r io.Reader, lenbuf []byte) (response []byte, err error) { return } if lenbuf[0] != 0xce { - err = errors.New("Wrong response header") + err = errors.New("wrong response header") return } length = (int(lenbuf[1]) << 24) + @@ -1216,7 +1229,7 @@ func read(r io.Reader, lenbuf []byte) (response []byte, err error) { int(lenbuf[4]) if length == 0 { - err = errors.New("Response should not be 0 length") + err = errors.New("response should not be 0 length") return } response = make([]byte, length) @@ -1241,7 +1254,7 @@ func (conn *Connection) Do(req Request) *Future { if connectedReq, ok := req.(ConnectedRequest); ok { if connectedReq.Conn() != conn { fut := NewFuture() - fut.SetError(fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool")) + fut.SetError(errUnknownRequest) return fut } } diff --git a/connector.go b/connector.go index 016dbbfb3..5d4b5be39 100644 --- a/connector.go +++ b/connector.go @@ -13,78 +13,82 @@ type Connector interface { // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. - Ping() (resp *Response, err error) + Ping() (*Response, error) // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. - Select(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}) (resp *Response, err error) + Select(space, index interface{}, offset, limit uint32, iterator Iter, + key interface{}) (*Response, error) // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. - Insert(space interface{}, tuple interface{}) (resp *Response, err error) + Insert(space interface{}, tuple interface{}) (*Response, error) // Deprecated: the method will be removed in the next major version, // use a ReplicaRequest object + Do() instead. - Replace(space interface{}, tuple interface{}) (resp *Response, err error) + Replace(space interface{}, tuple interface{}) (*Response, error) // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. - Delete(space, index interface{}, key interface{}) (resp *Response, err error) + Delete(space, index interface{}, key interface{}) (*Response, error) // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. - Update(space, index interface{}, key, ops interface{}) (resp *Response, err error) + Update(space, index interface{}, key, ops interface{}) (*Response, error) // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. - Upsert(space interface{}, tuple, ops interface{}) (resp *Response, err error) + Upsert(space interface{}, tuple, ops interface{}) (*Response, error) // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. - Call(functionName string, args interface{}) (resp *Response, err error) + Call(functionName string, args interface{}) (*Response, error) // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. - Call16(functionName string, args interface{}) (resp *Response, err error) + Call16(functionName string, args interface{}) (*Response, error) // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. - Call17(functionName string, args interface{}) (resp *Response, err error) + Call17(functionName string, args interface{}) (*Response, error) // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. - Eval(expr string, args interface{}) (resp *Response, err error) + Eval(expr string, args interface{}) (*Response, error) // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. - Execute(expr string, args interface{}) (resp *Response, err error) + Execute(expr string, args interface{}) (*Response, error) // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. - GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) + GetTyped(space, index interface{}, key interface{}, result interface{}) error // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. - SelectTyped(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}, result interface{}) (err error) + SelectTyped(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}, + result interface{}) error // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. - InsertTyped(space interface{}, tuple interface{}, result interface{}) (err error) + InsertTyped(space interface{}, tuple interface{}, result interface{}) error // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. - ReplaceTyped(space interface{}, tuple interface{}, result interface{}) (err error) + ReplaceTyped(space interface{}, tuple interface{}, result interface{}) error // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. - DeleteTyped(space, index interface{}, key interface{}, result interface{}) (err error) + DeleteTyped(space, index interface{}, key interface{}, result interface{}) error // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. - UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) (err error) + UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) error // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. - CallTyped(functionName string, args interface{}, result interface{}) (err error) + CallTyped(functionName string, args interface{}, result interface{}) error // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. - Call16Typed(functionName string, args interface{}, result interface{}) (err error) + Call16Typed(functionName string, args interface{}, result interface{}) error // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. - Call17Typed(functionName string, args interface{}, result interface{}) (err error) + Call17Typed(functionName string, args interface{}, result interface{}) error // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. - EvalTyped(expr string, args interface{}, result interface{}) (err error) + EvalTyped(expr string, args interface{}, result interface{}) error // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. - ExecuteTyped(expr string, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error) + ExecuteTyped(expr string, args interface{}, + result interface{}) (SQLInfo, []ColumnMetaData, error) // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. - SelectAsync(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}) *Future + SelectAsync(space, index interface{}, offset, limit uint32, iterator Iter, + key interface{}) *Future // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. InsertAsync(space interface{}, tuple interface{}) *Future diff --git a/crud/error.go b/crud/error.go index d7c36a333..b0b267fbe 100644 --- a/crud/error.go +++ b/crud/error.go @@ -70,8 +70,8 @@ func (e *Error) DecodeMsgpack(d *msgpack.Decoder) error { } // Error converts an Error to a string. -func (err Error) Error() string { - return err.Str +func (e Error) Error() string { + return e.Str } // ErrorMany describes CRUD error object for `_many` methods. @@ -104,9 +104,9 @@ func (e *ErrorMany) DecodeMsgpack(d *msgpack.Decoder) error { } // Error converts an Error to a string. -func (errs ErrorMany) Error() string { +func (e ErrorMany) Error() string { var str []string - for _, err := range errs.Errors { + for _, err := range e.Errors { str = append(str, err.Str) } diff --git a/crud/request_test.go b/crud/request_test.go index d36d8a264..4c49a9214 100644 --- a/crud/request_test.go +++ b/crud/request_test.go @@ -72,7 +72,8 @@ var expectedOpts = map[string]interface{}{ type ValidSchemeResolver struct { } -func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (spaceNo, indexNo uint32, err error) { +func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (uint32, uint32, error) { + var spaceNo, indexNo uint32 if s != nil { spaceNo = uint32(s.(int)) } else { @@ -322,50 +323,51 @@ func TestRequestsDefaultValues(t *testing.T) { }{ { name: "InsertRequest", - ref: tarantool.NewCall17Request("crud.insert").Args([]interface{}{validSpace, []interface{}{}, - map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.insert").Args( + []interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), target: crud.MakeInsertRequest(validSpace), }, { name: "InsertObjectRequest", - ref: tarantool.NewCall17Request("crud.insert_object").Args([]interface{}{validSpace, map[string]interface{}{}, - map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.insert_object").Args( + []interface{}{validSpace, map[string]interface{}{}, map[string]interface{}{}}), target: crud.MakeInsertObjectRequest(validSpace), }, { name: "InsertManyRequest", - ref: tarantool.NewCall17Request("crud.insert_many").Args([]interface{}{validSpace, []interface{}{}, - map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.insert_many").Args( + []interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), target: crud.MakeInsertManyRequest(validSpace), }, { name: "InsertObjectManyRequest", - ref: tarantool.NewCall17Request("crud.insert_object_many").Args([]interface{}{validSpace, []map[string]interface{}{}, - map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.insert_object_many").Args( + []interface{}{validSpace, []map[string]interface{}{}, map[string]interface{}{}}), target: crud.MakeInsertObjectManyRequest(validSpace), }, { name: "GetRequest", - ref: tarantool.NewCall17Request("crud.get").Args([]interface{}{validSpace, []interface{}{}, - map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.get").Args( + []interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), target: crud.MakeGetRequest(validSpace), }, { name: "UpdateRequest", - ref: tarantool.NewCall17Request("crud.update").Args([]interface{}{validSpace, []interface{}{}, - []interface{}{}, map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.update").Args( + []interface{}{validSpace, []interface{}{}, + []interface{}{}, map[string]interface{}{}}), target: crud.MakeUpdateRequest(validSpace), }, { name: "DeleteRequest", - ref: tarantool.NewCall17Request("crud.delete").Args([]interface{}{validSpace, []interface{}{}, - map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.delete").Args( + []interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), target: crud.MakeDeleteRequest(validSpace), }, { name: "ReplaceRequest", - ref: tarantool.NewCall17Request("crud.replace").Args([]interface{}{validSpace, []interface{}{}, - map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.replace").Args( + []interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), target: crud.MakeReplaceRequest(validSpace), }, { @@ -382,68 +384,70 @@ func TestRequestsDefaultValues(t *testing.T) { }, { name: "ReplaceObjectManyRequest", - ref: tarantool.NewCall17Request("crud.replace_object_many").Args([]interface{}{validSpace, - []map[string]interface{}{}, map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.replace_object_many").Args( + []interface{}{validSpace, []map[string]interface{}{}, map[string]interface{}{}}), target: crud.MakeReplaceObjectManyRequest(validSpace), }, { name: "UpsertRequest", - ref: tarantool.NewCall17Request("crud.upsert").Args([]interface{}{validSpace, []interface{}{}, - []interface{}{}, map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.upsert").Args( + []interface{}{validSpace, []interface{}{}, []interface{}{}, + map[string]interface{}{}}), target: crud.MakeUpsertRequest(validSpace), }, { name: "UpsertObjectRequest", - ref: tarantool.NewCall17Request("crud.upsert_object").Args([]interface{}{validSpace, - map[string]interface{}{}, []interface{}{}, map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.upsert_object").Args( + []interface{}{validSpace, map[string]interface{}{}, []interface{}{}, + map[string]interface{}{}}), target: crud.MakeUpsertObjectRequest(validSpace), }, { name: "UpsertManyRequest", - ref: tarantool.NewCall17Request("crud.upsert_many").Args([]interface{}{validSpace, - []interface{}{}, map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.upsert_many").Args( + []interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), target: crud.MakeUpsertManyRequest(validSpace), }, { name: "UpsertObjectManyRequest", - ref: tarantool.NewCall17Request("crud.upsert_object_many").Args([]interface{}{validSpace, - []interface{}{}, map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.upsert_object_many").Args( + []interface{}{validSpace, []interface{}{}, map[string]interface{}{}}), target: crud.MakeUpsertObjectManyRequest(validSpace), }, { name: "SelectRequest", - ref: tarantool.NewCall17Request("crud.select").Args([]interface{}{validSpace, - nil, map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.select").Args( + []interface{}{validSpace, nil, map[string]interface{}{}}), target: crud.MakeSelectRequest(validSpace), }, { name: "MinRequest", - ref: tarantool.NewCall17Request("crud.min").Args([]interface{}{validSpace, - nil, map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.min").Args( + []interface{}{validSpace, nil, map[string]interface{}{}}), target: crud.MakeMinRequest(validSpace), }, { name: "MaxRequest", - ref: tarantool.NewCall17Request("crud.max").Args([]interface{}{validSpace, - nil, map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.max").Args( + []interface{}{validSpace, nil, map[string]interface{}{}}), target: crud.MakeMaxRequest(validSpace), }, { name: "TruncateRequest", - ref: tarantool.NewCall17Request("crud.truncate").Args([]interface{}{validSpace, - map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.truncate").Args( + []interface{}{validSpace, map[string]interface{}{}}), target: crud.MakeTruncateRequest(validSpace), }, { name: "LenRequest", - ref: tarantool.NewCall17Request("crud.len").Args([]interface{}{validSpace, - map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.len").Args( + []interface{}{validSpace, map[string]interface{}{}}), target: crud.MakeLenRequest(validSpace), }, { name: "CountRequest", - ref: tarantool.NewCall17Request("crud.count").Args([]interface{}{validSpace, - nil, map[string]interface{}{}}), + ref: tarantool.NewCall17Request("crud.count").Args( + []interface{}{validSpace, nil, map[string]interface{}{}}), target: crud.MakeCountRequest(validSpace), }, { @@ -474,121 +478,150 @@ func TestRequestsSetters(t *testing.T) { target tarantool.Request }{ { - name: "InsertRequest", - ref: tarantool.NewCall17Request("crud.insert").Args([]interface{}{spaceName, tuple, expectedOpts}), + name: "InsertRequest", + ref: tarantool.NewCall17Request("crud.insert").Args( + []interface{}{spaceName, tuple, expectedOpts}), target: crud.MakeInsertRequest(spaceName).Tuple(tuple).Opts(simpleOperationOpts), }, { - name: "InsertObjectRequest", - ref: tarantool.NewCall17Request("crud.insert_object").Args([]interface{}{spaceName, reqObject, expectedOpts}), - target: crud.MakeInsertObjectRequest(spaceName).Object(reqObject).Opts(simpleOperationObjectOpts), + name: "InsertObjectRequest", + ref: tarantool.NewCall17Request("crud.insert_object").Args( + []interface{}{spaceName, reqObject, expectedOpts}), + target: crud.MakeInsertObjectRequest(spaceName).Object(reqObject). + Opts(simpleOperationObjectOpts), }, { - name: "InsertManyRequest", - ref: tarantool.NewCall17Request("crud.insert_many").Args([]interface{}{spaceName, tuples, expectedOpts}), + name: "InsertManyRequest", + ref: tarantool.NewCall17Request("crud.insert_many").Args( + []interface{}{spaceName, tuples, expectedOpts}), target: crud.MakeInsertManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts), }, { - name: "InsertObjectManyRequest", - ref: tarantool.NewCall17Request("crud.insert_object_many").Args([]interface{}{spaceName, reqObjects, expectedOpts}), - target: crud.MakeInsertObjectManyRequest(spaceName).Objects(reqObjects).Opts(opObjManyOpts), + name: "InsertObjectManyRequest", + ref: tarantool.NewCall17Request("crud.insert_object_many").Args( + []interface{}{spaceName, reqObjects, expectedOpts}), + target: crud.MakeInsertObjectManyRequest(spaceName).Objects(reqObjects). + Opts(opObjManyOpts), }, { - name: "GetRequest", - ref: tarantool.NewCall17Request("crud.get").Args([]interface{}{spaceName, key, expectedOpts}), + name: "GetRequest", + ref: tarantool.NewCall17Request("crud.get").Args( + []interface{}{spaceName, key, expectedOpts}), target: crud.MakeGetRequest(spaceName).Key(key).Opts(getOpts), }, { - name: "UpdateRequest", - ref: tarantool.NewCall17Request("crud.update").Args([]interface{}{spaceName, key, operations, expectedOpts}), - target: crud.MakeUpdateRequest(spaceName).Key(key).Operations(operations).Opts(simpleOperationOpts), + name: "UpdateRequest", + ref: tarantool.NewCall17Request("crud.update").Args( + []interface{}{spaceName, key, operations, expectedOpts}), + target: crud.MakeUpdateRequest(spaceName).Key(key).Operations(operations). + Opts(simpleOperationOpts), }, { - name: "DeleteRequest", - ref: tarantool.NewCall17Request("crud.delete").Args([]interface{}{spaceName, key, expectedOpts}), + name: "DeleteRequest", + ref: tarantool.NewCall17Request("crud.delete").Args( + []interface{}{spaceName, key, expectedOpts}), target: crud.MakeDeleteRequest(spaceName).Key(key).Opts(simpleOperationOpts), }, { - name: "ReplaceRequest", - ref: tarantool.NewCall17Request("crud.replace").Args([]interface{}{spaceName, tuple, expectedOpts}), + name: "ReplaceRequest", + ref: tarantool.NewCall17Request("crud.replace").Args( + []interface{}{spaceName, tuple, expectedOpts}), target: crud.MakeReplaceRequest(spaceName).Tuple(tuple).Opts(simpleOperationOpts), }, { - name: "ReplaceObjectRequest", - ref: tarantool.NewCall17Request("crud.replace_object").Args([]interface{}{spaceName, reqObject, expectedOpts}), - target: crud.MakeReplaceObjectRequest(spaceName).Object(reqObject).Opts(simpleOperationObjectOpts), + name: "ReplaceObjectRequest", + ref: tarantool.NewCall17Request("crud.replace_object").Args( + []interface{}{spaceName, reqObject, expectedOpts}), + target: crud.MakeReplaceObjectRequest(spaceName).Object(reqObject). + Opts(simpleOperationObjectOpts), }, { - name: "ReplaceManyRequest", - ref: tarantool.NewCall17Request("crud.replace_many").Args([]interface{}{spaceName, tuples, expectedOpts}), + name: "ReplaceManyRequest", + ref: tarantool.NewCall17Request("crud.replace_many").Args( + []interface{}{spaceName, tuples, expectedOpts}), target: crud.MakeReplaceManyRequest(spaceName).Tuples(tuples).Opts(opManyOpts), }, { - name: "ReplaceObjectManyRequest", - ref: tarantool.NewCall17Request("crud.replace_object_many").Args([]interface{}{spaceName, reqObjects, expectedOpts}), - target: crud.MakeReplaceObjectManyRequest(spaceName).Objects(reqObjects).Opts(opObjManyOpts), + name: "ReplaceObjectManyRequest", + ref: tarantool.NewCall17Request("crud.replace_object_many").Args( + []interface{}{spaceName, reqObjects, expectedOpts}), + target: crud.MakeReplaceObjectManyRequest(spaceName).Objects(reqObjects). + Opts(opObjManyOpts), }, { - name: "UpsertRequest", - ref: tarantool.NewCall17Request("crud.upsert").Args([]interface{}{spaceName, tuple, operations, expectedOpts}), - target: crud.MakeUpsertRequest(spaceName).Tuple(tuple).Operations(operations).Opts(simpleOperationOpts), + name: "UpsertRequest", + ref: tarantool.NewCall17Request("crud.upsert").Args( + []interface{}{spaceName, tuple, operations, expectedOpts}), + target: crud.MakeUpsertRequest(spaceName).Tuple(tuple).Operations(operations). + Opts(simpleOperationOpts), }, { name: "UpsertObjectRequest", - ref: tarantool.NewCall17Request("crud.upsert_object").Args([]interface{}{spaceName, reqObject, - operations, expectedOpts}), - target: crud.MakeUpsertObjectRequest(spaceName).Object(reqObject).Operations(operations).Opts(simpleOperationOpts), + ref: tarantool.NewCall17Request("crud.upsert_object").Args( + []interface{}{spaceName, reqObject, operations, expectedOpts}), + target: crud.MakeUpsertObjectRequest(spaceName).Object(reqObject). + Operations(operations).Opts(simpleOperationOpts), }, { name: "UpsertManyRequest", - ref: tarantool.NewCall17Request("crud.upsert_many").Args([]interface{}{spaceName, - tuplesOperationsData, expectedOpts}), - target: crud.MakeUpsertManyRequest(spaceName).TuplesOperationsData(tuplesOperationsData).Opts(opManyOpts), + ref: tarantool.NewCall17Request("crud.upsert_many").Args( + []interface{}{spaceName, tuplesOperationsData, expectedOpts}), + target: crud.MakeUpsertManyRequest(spaceName). + TuplesOperationsData(tuplesOperationsData).Opts(opManyOpts), }, { name: "UpsertObjectManyRequest", - ref: tarantool.NewCall17Request("crud.upsert_object_many").Args([]interface{}{spaceName, - reqObjectsOperationsData, expectedOpts}), - target: crud.MakeUpsertObjectManyRequest(spaceName).ObjectsOperationsData(reqObjectsOperationsData).Opts(opManyOpts), + ref: tarantool.NewCall17Request("crud.upsert_object_many").Args( + []interface{}{spaceName, reqObjectsOperationsData, expectedOpts}), + target: crud.MakeUpsertObjectManyRequest(spaceName). + ObjectsOperationsData(reqObjectsOperationsData).Opts(opManyOpts), }, { - name: "SelectRequest", - ref: tarantool.NewCall17Request("crud.select").Args([]interface{}{spaceName, conditions, expectedOpts}), + name: "SelectRequest", + ref: tarantool.NewCall17Request("crud.select").Args( + []interface{}{spaceName, conditions, expectedOpts}), target: crud.MakeSelectRequest(spaceName).Conditions(conditions).Opts(selectOpts), }, { - name: "MinRequest", - ref: tarantool.NewCall17Request("crud.min").Args([]interface{}{spaceName, indexName, expectedOpts}), + name: "MinRequest", + ref: tarantool.NewCall17Request("crud.min").Args( + []interface{}{spaceName, indexName, expectedOpts}), target: crud.MakeMinRequest(spaceName).Index(indexName).Opts(minOpts), }, { - name: "MaxRequest", - ref: tarantool.NewCall17Request("crud.max").Args([]interface{}{spaceName, indexName, expectedOpts}), + name: "MaxRequest", + ref: tarantool.NewCall17Request("crud.max").Args( + []interface{}{spaceName, indexName, expectedOpts}), target: crud.MakeMaxRequest(spaceName).Index(indexName).Opts(maxOpts), }, { - name: "TruncateRequest", - ref: tarantool.NewCall17Request("crud.truncate").Args([]interface{}{spaceName, expectedOpts}), + name: "TruncateRequest", + ref: tarantool.NewCall17Request("crud.truncate").Args( + []interface{}{spaceName, expectedOpts}), target: crud.MakeTruncateRequest(spaceName).Opts(baseOpts), }, { - name: "LenRequest", - ref: tarantool.NewCall17Request("crud.len").Args([]interface{}{spaceName, expectedOpts}), + name: "LenRequest", + ref: tarantool.NewCall17Request("crud.len").Args( + []interface{}{spaceName, expectedOpts}), target: crud.MakeLenRequest(spaceName).Opts(baseOpts), }, { - name: "CountRequest", - ref: tarantool.NewCall17Request("crud.count").Args([]interface{}{spaceName, conditions, expectedOpts}), + name: "CountRequest", + ref: tarantool.NewCall17Request("crud.count").Args( + []interface{}{spaceName, conditions, expectedOpts}), target: crud.MakeCountRequest(spaceName).Conditions(conditions).Opts(countOpts), }, { - name: "StorageInfoRequest", - ref: tarantool.NewCall17Request("crud.storage_info").Args([]interface{}{expectedOpts}), + name: "StorageInfoRequest", + ref: tarantool.NewCall17Request("crud.storage_info").Args( + []interface{}{expectedOpts}), target: crud.MakeStorageInfoRequest().Opts(baseOpts), }, { - name: "StatsRequest", - ref: tarantool.NewCall17Request("crud.stats").Args([]interface{}{spaceName}), + name: "StatsRequest", + ref: tarantool.NewCall17Request("crud.stats").Args( + []interface{}{spaceName}), target: crud.MakeStatsRequest().Space(spaceName), }, } diff --git a/crud/result.go b/crud/result.go index 100ec2a95..b594e5de7 100644 --- a/crud/result.go +++ b/crud/result.go @@ -113,7 +113,6 @@ func (r *Result) DecodeMsgpack(d *msgpack.Decoder) error { if r.rowType != nil { tuples := reflect.New(reflect.SliceOf(r.rowType)) if err = d.DecodeValue(tuples); err != nil { - fmt.Println(tuples) return err } r.Rows = tuples.Elem().Interface() diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index c9870abc6..5cf29f66a 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -361,7 +361,8 @@ func generateTuples() []crud.Tuple { return tpls } -func generateTuplesOperationsData(tpls []crud.Tuple, operations []crud.Operation) []crud.TupleOperationsData { +func generateTuplesOperationsData(tpls []crud.Tuple, + operations []crud.Operation) []crud.TupleOperationsData { tuplesOperationsData := []crud.TupleOperationsData{} for _, tpl := range tpls { tuplesOperationsData = append(tuplesOperationsData, crud.TupleOperationsData{ @@ -385,7 +386,8 @@ func generateObjects() []crud.Object { return objs } -func generateObjectsOperationsData(objs []crud.Object, operations []crud.Operation) []crud.ObjectOperationsData { +func generateObjectsOperationsData(objs []crud.Object, + operations []crud.Operation) []crud.ObjectOperationsData { objectsOperationsData := []crud.ObjectOperationsData{} for _, obj := range objs { objectsOperationsData = append(objectsOperationsData, crud.ObjectOperationsData{ @@ -548,7 +550,7 @@ func TestUnflattenRows_IncorrectParams(t *testing.T) { objs, err := crud.UnflattenRows(tpls, invalidMetadata) require.Nil(t, objs) require.NotNil(t, err) - require.Contains(t, err.Error(), "Unexpected space format") + require.Contains(t, err.Error(), "unexpected space format") } func TestUnflattenRows(t *testing.T) { @@ -816,7 +818,8 @@ func runTestMain(m *testing.M) int { defer test_helpers.StopTarantoolWithCleanup(inst) if err != nil { - log.Fatalf("Failed to prepare test tarantool: %s", err) + log.Printf("Failed to prepare test tarantool: %s", err) + return 1 } return m.Run() diff --git a/crud/unflatten_rows.go b/crud/unflatten_rows.go index 67ebb2b69..f360f8ad5 100644 --- a/crud/unflatten_rows.go +++ b/crud/unflatten_rows.go @@ -18,11 +18,11 @@ func UnflattenRows(tuples []interface{}, format []interface{}) ([]MapObject, err object := make(map[string]interface{}) for fieldIdx, field := range tuple.([]interface{}) { if fieldInfo, ok = format[fieldIdx].(map[interface{}]interface{}); !ok { - return nil, fmt.Errorf("Unexpected space format: %q", format) + return nil, fmt.Errorf("unexpected space format: %q", format) } if fieldName, ok = fieldInfo["name"].(string); !ok { - return nil, fmt.Errorf("Unexpected space format: %q", format) + return nil, fmt.Errorf("unexpected space format: %q", format) } object[fieldName] = field diff --git a/crud/upsert_many.go b/crud/upsert_many.go index c7a54ba05..b0ccedf09 100644 --- a/crud/upsert_many.go +++ b/crud/upsert_many.go @@ -46,7 +46,8 @@ func MakeUpsertManyRequest(space string) UpsertManyRequest { // TuplesOperationsData sets tuples and operations for // the UpsertManyRequest request. // Note: default value is nil. -func (req UpsertManyRequest) TuplesOperationsData(tuplesOperationData []TupleOperationsData) UpsertManyRequest { +func (req UpsertManyRequest) TuplesOperationsData( + tuplesOperationData []TupleOperationsData) UpsertManyRequest { req.tuplesOperationsData = tuplesOperationData return req } diff --git a/datetime/datetime.go b/datetime/datetime.go index 09e86ce46..1f3bff775 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -181,8 +181,8 @@ func addMonth(ival *Interval, delta int64, adjust Adjust) { } } -func (dtime *Datetime) add(ival Interval, positive bool) (*Datetime, error) { - newVal := intervalFromDatetime(dtime) +func (d *Datetime) add(ival Interval, positive bool) (*Datetime, error) { + newVal := intervalFromDatetime(d) var direction int64 if positive { @@ -201,28 +201,28 @@ func (dtime *Datetime) add(ival Interval, positive bool) (*Datetime, error) { tm := time.Date(int(newVal.Year), time.Month(newVal.Month), int(newVal.Day), int(newVal.Hour), int(newVal.Min), - int(newVal.Sec), int(newVal.Nsec), dtime.time.Location()) + int(newVal.Sec), int(newVal.Nsec), d.time.Location()) return NewDatetime(tm) } // Add creates a new Datetime as addition of the Datetime and Interval. It may // return an error if a new Datetime is out of supported range. -func (dtime *Datetime) Add(ival Interval) (*Datetime, error) { - return dtime.add(ival, true) +func (d *Datetime) Add(ival Interval) (*Datetime, error) { + return d.add(ival, true) } // Sub creates a new Datetime as subtraction of the Datetime and Interval. It // may return an error if a new Datetime is out of supported range. -func (dtime *Datetime) Sub(ival Interval) (*Datetime, error) { - return dtime.add(ival, false) +func (d *Datetime) Sub(ival Interval) (*Datetime, error) { + return d.add(ival, false) } // Interval returns an Interval value to a next Datetime value. -func (dtime *Datetime) Interval(next *Datetime) Interval { - curIval := intervalFromDatetime(dtime) +func (d *Datetime) Interval(next *Datetime) Interval { + curIval := intervalFromDatetime(d) nextIval := intervalFromDatetime(next) - _, curOffset := dtime.time.Zone() + _, curOffset := d.time.Zone() _, nextOffset := next.time.Zone() curIval.Min -= int64(curOffset-nextOffset) / 60 return nextIval.Sub(curIval) @@ -236,12 +236,12 @@ func (dtime *Datetime) Interval(next *Datetime) Interval { // If a Datetime created via unmarshaling Tarantool's datetime then we try to // create a location with time.LoadLocation() first. In case of failure, we use // a location created with time.FixedZone(). -func (dtime *Datetime) ToTime() time.Time { - return dtime.time +func (d *Datetime) ToTime() time.Time { + return d.time } -func (dtime *Datetime) MarshalMsgpack() ([]byte, error) { - tm := dtime.ToTime() +func (d *Datetime) MarshalMsgpack() ([]byte, error) { + tm := d.ToTime() var dt datetime dt.seconds = tm.Unix() @@ -272,10 +272,11 @@ func (dtime *Datetime) MarshalMsgpack() ([]byte, error) { return buf, nil } -func (tm *Datetime) UnmarshalMsgpack(b []byte) error { +func (d *Datetime) UnmarshalMsgpack(b []byte) error { l := len(b) if l != maxSize && l != secondsSize { - return fmt.Errorf("invalid data length: got %d, wanted %d or %d", len(b), secondsSize, maxSize) + return fmt.Errorf("invalid data length: got %d, wanted %d or %d", + len(b), secondsSize, maxSize) } var dt datetime @@ -317,7 +318,7 @@ func (tm *Datetime) UnmarshalMsgpack(b []byte) error { dtp, err := NewDatetime(tt) if dtp != nil { - *tm = *dtp + *d = *dtp } return err } diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index e2f707f6f..705d2c56b 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -1183,7 +1183,8 @@ func runTestMain(m *testing.M) int { defer test_helpers.StopTarantoolWithCleanup(instance) if err != nil { - log.Fatalf("Failed to prepare test Tarantool: %s", err) + log.Printf("Failed to prepare test Tarantool: %s", err) + return 1 } return m.Run() diff --git a/datetime/interval.go b/datetime/interval.go index ef378e405..bcd052383 100644 --- a/datetime/interval.go +++ b/datetime/interval.go @@ -75,7 +75,7 @@ func encodeIntervalValue(e *msgpack.Encoder, typ uint64, value int64) (err error if value > 0 { err = e.EncodeUint(uint64(value)) } else if value < 0 { - err = e.EncodeInt(int64(value)) + err = e.EncodeInt(value) } } return diff --git a/decimal/bcd.go b/decimal/bcd.go index 2b5a4b258..d6222b861 100644 --- a/decimal/bcd.go +++ b/decimal/bcd.go @@ -39,9 +39,11 @@ package decimal // // See also: // -// * MessagePack extensions https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/ +// * MessagePack extensions: +// https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/ // -// * An implementation in C language https://github.com/tarantool/decNumber/blob/master/decPacked.c +// * An implementation in C language: +// https://github.com/tarantool/decNumber/blob/master/decPacked.c import ( "fmt" @@ -106,7 +108,7 @@ func getNumberLength(buf string) int { // is set to the scale of the number (this is the exponent, negated). func encodeStringToBCD(buf string) ([]byte, error) { if len(buf) == 0 { - return nil, fmt.Errorf("Length of number is zero") + return nil, fmt.Errorf("length of number is zero") } signByte := bytePlus // By default number is positive. if buf[0] == '-' { @@ -134,19 +136,19 @@ func encodeStringToBCD(buf string) ([]byte, error) { // Calculate a number of digits after the decimal point. if ch == '.' { if scale != 0 { - return nil, fmt.Errorf("Number contains more than one point") + return nil, fmt.Errorf("number contains more than one point") } scale = len(buf) - i - 1 continue } if ch < '0' || ch > '9' { - return nil, fmt.Errorf("Failed to convert symbol '%c' to a digit", ch) + return nil, fmt.Errorf("failed to convert symbol '%c' to a digit", ch) } - digit := byte(ch - '0') + digit := ch - '0' if highNibble { // Add a digit to a high nibble. - digit = digit << 4 + digit <<= 4 byteBuf = append(byteBuf, digit) highNibble = false } else { @@ -155,7 +157,7 @@ func encodeStringToBCD(buf string) ([]byte, error) { } // Add a digit to a low nibble. lowByteIdx := len(byteBuf) - 1 - byteBuf[lowByteIdx] = byteBuf[lowByteIdx] | digit + byteBuf[lowByteIdx] |= digit highNibble = true } } @@ -169,7 +171,7 @@ func encodeStringToBCD(buf string) ([]byte, error) { } else { // Put a sign to a low nibble. lowByteIdx := len(byteBuf) - 1 - byteBuf[lowByteIdx] = byteBuf[lowByteIdx] | signByte + byteBuf[lowByteIdx] |= signByte } byteBuf = append([]byte{byte(scale)}, byteBuf...) diff --git a/decimal/decimal.go b/decimal/decimal.go index 6cca53404..dd7b30433 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -6,13 +6,17 @@ // // See also: // -// * Tarantool MessagePack extensions https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type +// - Tarantool MessagePack extensions: +// https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type // -// * Tarantool data model https://www.tarantool.io/en/doc/latest/book/box/data_model/ +// - Tarantool data model: +// https://www.tarantool.io/en/doc/latest/book/box/data_model/ // -// * Tarantool issue for support decimal type https://github.com/tarantool/tarantool/issues/692 +// - Tarantool issue for support decimal type: +// https://github.com/tarantool/tarantool/issues/692 // -// * Tarantool module decimal https://www.tarantool.io/en/doc/latest/reference/reference_lua/decimal/ +// - Tarantool module decimal: +// https://www.tarantool.io/en/doc/latest/reference/reference_lua/decimal/ package decimal import ( @@ -29,7 +33,8 @@ import ( // // See also: // -// * Tarantool module decimal https://www.tarantool.io/en/doc/latest/reference/reference_lua/decimal/ +// - Tarantool module decimal: +// https://www.tarantool.io/en/doc/latest/reference/reference_lua/decimal/ const ( // Decimal external type. @@ -62,10 +67,16 @@ func (decNum *Decimal) MarshalMsgpack() ([]byte, error) { maxSupportedDecimal := decimal.New(1, decimalPrecision).Sub(one) // 10^decimalPrecision - 1 minSupportedDecimal := maxSupportedDecimal.Neg().Sub(one) // -10^decimalPrecision - 1 if decNum.GreaterThan(maxSupportedDecimal) { - return nil, fmt.Errorf("msgpack: decimal number is bigger than maximum supported number (10^%d - 1)", decimalPrecision) + return nil, + fmt.Errorf( + "msgpack: decimal number is bigger than maximum supported number (10^%d - 1)", + decimalPrecision) } if decNum.LessThan(minSupportedDecimal) { - return nil, fmt.Errorf("msgpack: decimal number is lesser than minimum supported number (-10^%d - 1)", decimalPrecision) + return nil, + fmt.Errorf( + "msgpack: decimal number is lesser than minimum supported number (-10^%d - 1)", + decimalPrecision) } strBuf := decNum.String() diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index e3eb867c2..92616c5f2 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -82,7 +82,8 @@ var benchmarkSamples = []struct { {"0.00000000000000000000000000000000000009", "d501269c", true}, {"-18.34", "d6010201834d", true}, {"-108.123456789", "d701090108123456789d", true}, - {"-11111111111111111111111111111111111111", "c7150100011111111111111111111111111111111111111d", false}, + {"-11111111111111111111111111111111111111", "c7150100011111111111111111111111111111111111111d", + false}, } var correctnessSamples = []struct { @@ -103,8 +104,10 @@ var correctnessSamples = []struct { {"-0", "d501000c", true}, {"0.01", "d501021c", true}, {"0.001", "d501031c", true}, - {"99999999999999999999999999999999999999", "c7150100099999999999999999999999999999999999999c", false}, - {"-99999999999999999999999999999999999999", "c7150100099999999999999999999999999999999999999d", false}, + {"99999999999999999999999999999999999999", "c7150100099999999999999999999999999999999999999c", + false}, + {"-99999999999999999999999999999999999999", "c7150100099999999999999999999999999999999999999d", + false}, {"-12.34", "d6010201234d", true}, {"12.34", "d6010201234c", true}, {"1.4", "c7030101014c", false}, @@ -112,8 +115,10 @@ var correctnessSamples = []struct { {"-2.718281828459045", "c70a010f02718281828459045d", false}, {"3.141592653589793", "c70a010f03141592653589793c", false}, {"-3.141592653589793", "c70a010f03141592653589793d", false}, - {"1234567891234567890.0987654321987654321", "c7150113012345678912345678900987654321987654321c", false}, - {"-1234567891234567890.0987654321987654321", "c7150113012345678912345678900987654321987654321d", false}, + {"1234567891234567890.0987654321987654321", "c7150113012345678912345678900987654321987654321c", + false}, + {"-1234567891234567890.0987654321987654321", + "c7150113012345678912345678900987654321987654321d", false}, } // There is a difference between encoding result from a raw string and from @@ -150,15 +155,16 @@ func TestMPEncodeDecode(t *testing.T) { var buf []byte tuple := TupleDecimal{number: *decNum} if buf, err = msgpack.Marshal(&tuple); err != nil { - t.Fatalf("Failed to msgpack.Encoder decimal number '%s' to a MessagePack buffer: %s", testcase.numString, err) + t.Fatalf( + "Failed to msgpack.Encoder decimal number '%s' to a MessagePack buffer: %s", + testcase.numString, err) } var v TupleDecimal if err = msgpack.Unmarshal(buf, &v); err != nil { - t.Fatalf("Failed to decode MessagePack buffer '%x' to a decimal number: %s", buf, err) + t.Fatalf("Failed to decode MessagePack buffer '%x' to a decimal number: %s", + buf, err) } if !decNum.Equal(v.number.Decimal) { - fmt.Println(decNum) - fmt.Println(v.number) t.Fatal("Decimal numbers are not equal") } }) @@ -208,7 +214,7 @@ func TestGetNumberLength(t *testing.T) { } func TestEncodeStringToBCDIncorrectNumber(t *testing.T) { - referenceErrMsg := "Number contains more than one point" + referenceErrMsg := "number contains more than one point" var numString = "0.1.0" buf, err := EncodeStringToBCD(numString) if err == nil { @@ -221,7 +227,7 @@ func TestEncodeStringToBCDIncorrectNumber(t *testing.T) { t.Fatalf("wrong error message on encoding of a string double points") } - referenceErrMsg = "Length of number is zero" + referenceErrMsg = "length of number is zero" numString = "" buf, err = EncodeStringToBCD(numString) if err == nil { @@ -234,7 +240,7 @@ func TestEncodeStringToBCDIncorrectNumber(t *testing.T) { t.Fatalf("wrong error message on encoding of an empty string") } - referenceErrMsg = "Failed to convert symbol 'a' to a digit" + referenceErrMsg = "failed to convert symbol 'a' to a digit" numString = "0.1a" buf, err = EncodeStringToBCD(numString) if err == nil { @@ -249,7 +255,8 @@ func TestEncodeStringToBCDIncorrectNumber(t *testing.T) { } func TestEncodeMaxNumber(t *testing.T) { - referenceErrMsg := "msgpack: decimal number is bigger than maximum supported number (10^38 - 1)" + referenceErrMsg := "msgpack: decimal number is bigger than maximum " + + "supported number (10^38 - 1)" decNum := decimal.New(1, DecimalPrecision) // // 10^DecimalPrecision tuple := TupleDecimal{number: *NewDecimal(decNum)} _, err := msgpack.Marshal(&tuple) @@ -257,12 +264,13 @@ func TestEncodeMaxNumber(t *testing.T) { t.Fatalf("It is possible to msgpack.Encoder a number unsupported by Tarantool") } if err.Error() != referenceErrMsg { - t.Fatalf("Incorrect error message on attempt to msgpack.Encoder number unsupported by Tarantool") + t.Fatalf("Incorrect error message on attempt to msgpack.Encoder number unsupported") } } func TestEncodeMinNumber(t *testing.T) { - referenceErrMsg := "msgpack: decimal number is lesser than minimum supported number (-10^38 - 1)" + referenceErrMsg := "msgpack: decimal number is lesser than minimum " + + "supported number (-10^38 - 1)" two := decimal.NewFromInt(2) decNum := decimal.New(1, DecimalPrecision).Neg().Sub(two) // -10^DecimalPrecision - 2 tuple := TupleDecimal{number: *NewDecimal(decNum)} @@ -271,9 +279,7 @@ func TestEncodeMinNumber(t *testing.T) { t.Fatalf("It is possible to msgpack.Encoder a number unsupported by Tarantool") } if err.Error() != referenceErrMsg { - fmt.Println("Actual message: ", err.Error()) - fmt.Println("Expected message: ", referenceErrMsg) - t.Fatalf("Incorrect error message on attempt to msgpack.Encoder number unsupported by Tarantool") + t.Fatalf("Incorrect error message on attempt to msgpack.Encoder number unsupported") } } @@ -368,26 +374,30 @@ func trimMPHeader(mpBuf []byte, fixExt bool) []byte { } func TestEncodeStringToBCD(t *testing.T) { - samples := append(correctnessSamples, rawSamples...) + samples := correctnessSamples + samples = append(samples, rawSamples...) samples = append(samples, benchmarkSamples...) for _, testcase := range samples { t.Run(testcase.numString, func(t *testing.T) { buf, err := EncodeStringToBCD(testcase.numString) if err != nil { - t.Fatalf("Failed to msgpack.Encoder decimal '%s' to BCD: %s", testcase.numString, err) - + t.Fatalf("Failed to msgpack.Encoder decimal '%s' to BCD: %s", + testcase.numString, err) } b, _ := hex.DecodeString(testcase.mpBuf) bcdBuf := trimMPHeader(b, testcase.fixExt) if reflect.DeepEqual(buf, bcdBuf) != true { - t.Fatalf("Failed to msgpack.Encoder decimal '%s' to BCD: expected '%x', actual '%x'", testcase.numString, bcdBuf, buf) + t.Fatalf( + "Failed to msgpack.Encoder decimal '%s' to BCD: expected '%x', actual '%x'", + testcase.numString, bcdBuf, buf) } }) } } func TestDecodeStringFromBCD(t *testing.T) { - samples := append(correctnessSamples, rawSamples...) + samples := correctnessSamples + samples = append(samples, rawSamples...) samples = append(samples, benchmarkSamples...) for _, testcase := range samples { t.Run(testcase.numString, func(t *testing.T) { @@ -407,14 +417,17 @@ func TestDecodeStringFromBCD(t *testing.T) { t.Fatalf("Failed to msgpack.Encoder string ('%s') to decimal", testcase.numString) } if !decExpected.Equal(decActual) { - t.Fatalf("Decoded decimal from BCD ('%x') is incorrect: expected '%s', actual '%s'", bcdBuf, testcase.numString, s) + t.Fatalf( + "Decoded decimal from BCD ('%x') is incorrect: expected '%s', actual '%s'", + bcdBuf, testcase.numString, s) } }) } } func TestMPEncode(t *testing.T) { - samples := append(correctnessSamples, decimalSamples...) + samples := correctnessSamples + samples = append(samples, decimalSamples...) samples = append(samples, benchmarkSamples...) for _, testcase := range samples { t.Run(testcase.numString, func(t *testing.T) { @@ -438,7 +451,8 @@ func TestMPEncode(t *testing.T) { } func TestMPDecode(t *testing.T) { - samples := append(correctnessSamples, decimalSamples...) + samples := correctnessSamples + samples = append(samples, decimalSamples...) samples = append(samples, benchmarkSamples...) for _, testcase := range samples { t.Run(testcase.numString, func(t *testing.T) { @@ -567,7 +581,8 @@ func TestInsert(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - samples := append(correctnessSamples, benchmarkSamples...) + samples := correctnessSamples + samples = append(samples, benchmarkSamples...) for _, testcase := range samples { t.Run(testcase.numString, func(t *testing.T) { assertInsert(t, conn, testcase.numString) @@ -642,7 +657,8 @@ func runTestMain(m *testing.M) int { defer test_helpers.StopTarantoolWithCleanup(instance) if err != nil { - log.Fatalf("Failed to prepare test Tarantool: %s", err) + log.Printf("Failed to prepare test Tarantool: %s", err) + return 1 } return m.Run() diff --git a/example_test.go b/example_test.go index 70d499820..e2a645937 100644 --- a/example_test.go +++ b/example_test.go @@ -618,10 +618,16 @@ func ExampleProtocolVersion() { clientProtocolInfo := conn.ClientProtocolInfo() fmt.Println("Connector client protocol version:", clientProtocolInfo.Version) - fmt.Println("Connector client protocol features:", clientProtocolInfo.Features) + for _, f := range clientProtocolInfo.Features { + fmt.Println("Connector client protocol feature:", f) + } // Output: // Connector client protocol version: 4 - // Connector client protocol features: [StreamsFeature TransactionsFeature ErrorExtensionFeature WatchersFeature PaginationFeature] + // Connector client protocol feature: StreamsFeature + // Connector client protocol feature: TransactionsFeature + // Connector client protocol feature: ErrorExtensionFeature + // Connector client protocol feature: WatchersFeature + // Connector client protocol feature: PaginationFeature } func getTestTxnOpts() tarantool.Opts { diff --git a/export_test.go b/export_test.go index 688d09059..10d194840 100644 --- a/export_test.go +++ b/export_test.go @@ -97,7 +97,8 @@ func RefImplUnprepareBody(enc *msgpack.Encoder, stmt Prepared) error { // RefImplBeginBody is reference implementation for filling of an begin // request's body. -func RefImplBeginBody(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, timeout time.Duration) error { +func RefImplBeginBody(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, + timeout time.Duration) error { return fillBegin(enc, txnIsolation, timeout) } diff --git a/future.go b/future.go index a92a4c091..09229788c 100644 --- a/future.go +++ b/future.go @@ -206,7 +206,8 @@ func (fut *Future) GetTyped(result interface{}) error { // // # See also // -// * box.session.push() https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_session/push/ +// - box.session.push(): +// https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_session/push/ func (fut *Future) GetIterator() (it TimeoutResponseIterator) { futit := &asyncResponseIterator{ fut: fut, diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 581b226fd..26e2199e9 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -12,7 +12,6 @@ package pool import ( "errors" - "fmt" "log" "sync" "time" @@ -32,6 +31,8 @@ var ( ErrNoHealthyInstance = errors.New("can't find healthy instance in pool") ErrExists = errors.New("endpoint exists") ErrClosed = errors.New("pool is closed") + ErrUnknownRequest = errors.New("the passed connected request doesn't belong to " + + "the current connection pool") ) // ConnectionHandler provides callbacks for components interested in handling @@ -132,7 +133,7 @@ func newEndpoint(addr string) *endpoint { // ConnectWithOpts creates pool for instances with addresses addrs // with options opts. -func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts Opts) (connPool *ConnectionPool, err error) { +func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts Opts) (*ConnectionPool, error) { if len(addrs) == 0 { return nil, ErrEmptyAddrs } @@ -145,7 +146,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts Opts) (connPo roPool := newRoundRobinStrategy(size) anyPool := newRoundRobinStrategy(size) - connPool = &ConnectionPool{ + connPool := &ConnectionPool{ addrs: make(map[string]*endpoint), connOpts: connOpts.Clone(), opts: opts, @@ -180,7 +181,7 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts Opts) (connPo // It is useless to set up tarantool.Opts.Reconnect value for a connection. // The connection pool has its own reconnection logic. See // Opts.CheckTimeout description. -func Connect(addrs []string, connOpts tarantool.Opts) (connPool *ConnectionPool, err error) { +func Connect(addrs []string, connOpts tarantool.Opts) (*ConnectionPool, error) { opts := Opts{ CheckTimeout: 1 * time.Second, } @@ -188,32 +189,32 @@ func Connect(addrs []string, connOpts tarantool.Opts) (connPool *ConnectionPool, } // ConnectedNow gets connected status of pool. -func (connPool *ConnectionPool) ConnectedNow(mode Mode) (bool, error) { - connPool.poolsMutex.RLock() - defer connPool.poolsMutex.RUnlock() +func (p *ConnectionPool) ConnectedNow(mode Mode) (bool, error) { + p.poolsMutex.RLock() + defer p.poolsMutex.RUnlock() - if connPool.state.get() != connectedState { + if p.state.get() != connectedState { return false, nil } switch mode { case ANY: - return !connPool.anyPool.IsEmpty(), nil + return !p.anyPool.IsEmpty(), nil case RW: - return !connPool.rwPool.IsEmpty(), nil + return !p.rwPool.IsEmpty(), nil case RO: - return !connPool.roPool.IsEmpty(), nil + return !p.roPool.IsEmpty(), nil case PreferRW: fallthrough case PreferRO: - return !connPool.rwPool.IsEmpty() || !connPool.roPool.IsEmpty(), nil + return !p.rwPool.IsEmpty() || !p.roPool.IsEmpty(), nil default: return false, ErrNoHealthyInstance } } // ConfiguredTimeout gets timeout of current connection. -func (connPool *ConnectionPool) ConfiguredTimeout(mode Mode) (time.Duration, error) { - conn, err := connPool.getNextConnection(mode) +func (p *ConnectionPool) ConfiguredTimeout(mode Mode) (time.Duration, error) { + conn, err := p.getNextConnection(mode) if err != nil { return 0, err } @@ -223,41 +224,41 @@ func (connPool *ConnectionPool) ConfiguredTimeout(mode Mode) (time.Duration, err // Add adds a new endpoint with the address into the pool. This function // adds the endpoint only after successful connection. -func (pool *ConnectionPool) Add(addr string) error { +func (p *ConnectionPool) Add(addr string) error { e := newEndpoint(addr) - pool.addrsMutex.Lock() + p.addrsMutex.Lock() // Ensure that Close()/CloseGraceful() not in progress/done. - if pool.state.get() != connectedState { - pool.addrsMutex.Unlock() + if p.state.get() != connectedState { + p.addrsMutex.Unlock() return ErrClosed } - if _, ok := pool.addrs[addr]; ok { - pool.addrsMutex.Unlock() + if _, ok := p.addrs[addr]; ok { + p.addrsMutex.Unlock() return ErrExists } - pool.addrs[addr] = e - pool.addrsMutex.Unlock() + p.addrs[addr] = e + p.addrsMutex.Unlock() - if err := pool.tryConnect(e); err != nil { - pool.addrsMutex.Lock() - delete(pool.addrs, addr) - pool.addrsMutex.Unlock() + if err := p.tryConnect(e); err != nil { + p.addrsMutex.Lock() + delete(p.addrs, addr) + p.addrsMutex.Unlock() close(e.closed) return err } - go pool.controller(e) + go p.controller(e) return nil } // Remove removes an endpoint with the address from the pool. The call // closes an active connection gracefully. -func (pool *ConnectionPool) Remove(addr string) error { - pool.addrsMutex.Lock() - endpoint, ok := pool.addrs[addr] +func (p *ConnectionPool) Remove(addr string) error { + p.addrsMutex.Lock() + endpoint, ok := p.addrs[addr] if !ok { - pool.addrsMutex.Unlock() + p.addrsMutex.Unlock() return errors.New("endpoint not exist") } @@ -270,20 +271,20 @@ func (pool *ConnectionPool) Remove(addr string) error { close(endpoint.shutdown) } - delete(pool.addrs, addr) - pool.addrsMutex.Unlock() + delete(p.addrs, addr) + p.addrsMutex.Unlock() <-endpoint.closed return nil } -func (pool *ConnectionPool) waitClose() []error { - pool.addrsMutex.RLock() - endpoints := make([]*endpoint, 0, len(pool.addrs)) - for _, e := range pool.addrs { +func (p *ConnectionPool) waitClose() []error { + p.addrsMutex.RLock() + endpoints := make([]*endpoint, 0, len(p.addrs)) + for _, e := range p.addrs { endpoints = append(endpoints, e) } - pool.addrsMutex.RUnlock() + p.addrsMutex.RUnlock() errs := make([]error, 0, len(endpoints)) for _, e := range endpoints { @@ -296,42 +297,42 @@ func (pool *ConnectionPool) waitClose() []error { } // Close closes connections in the ConnectionPool. -func (pool *ConnectionPool) Close() []error { - if pool.state.cas(connectedState, closedState) || - pool.state.cas(shutdownState, closedState) { - pool.addrsMutex.RLock() - for _, s := range pool.addrs { +func (p *ConnectionPool) Close() []error { + if p.state.cas(connectedState, closedState) || + p.state.cas(shutdownState, closedState) { + p.addrsMutex.RLock() + for _, s := range p.addrs { close(s.close) } - pool.addrsMutex.RUnlock() + p.addrsMutex.RUnlock() } - return pool.waitClose() + return p.waitClose() } // CloseGraceful closes connections in the ConnectionPool gracefully. It waits // for all requests to complete. -func (pool *ConnectionPool) CloseGraceful() []error { - if pool.state.cas(connectedState, shutdownState) { - pool.addrsMutex.RLock() - for _, s := range pool.addrs { +func (p *ConnectionPool) CloseGraceful() []error { + if p.state.cas(connectedState, shutdownState) { + p.addrsMutex.RLock() + for _, s := range p.addrs { close(s.shutdown) } - pool.addrsMutex.RUnlock() + p.addrsMutex.RUnlock() } - return pool.waitClose() + return p.waitClose() } // GetAddrs gets addresses of connections in pool. -func (pool *ConnectionPool) GetAddrs() []string { - pool.addrsMutex.RLock() - defer pool.addrsMutex.RUnlock() +func (p *ConnectionPool) GetAddrs() []string { + p.addrsMutex.RLock() + defer p.addrsMutex.RUnlock() - cpy := make([]string, len(pool.addrs)) + cpy := make([]string, len(p.addrs)) i := 0 - for addr := range pool.addrs { + for addr := range p.addrs { cpy[i] = addr i++ } @@ -340,20 +341,20 @@ func (pool *ConnectionPool) GetAddrs() []string { } // GetPoolInfo gets information of connections (connected status, ro/rw role). -func (pool *ConnectionPool) GetPoolInfo() map[string]*ConnectionInfo { +func (p *ConnectionPool) GetPoolInfo() map[string]*ConnectionInfo { info := make(map[string]*ConnectionInfo) - pool.addrsMutex.RLock() - defer pool.addrsMutex.RUnlock() - pool.poolsMutex.RLock() - defer pool.poolsMutex.RUnlock() + p.addrsMutex.RLock() + defer p.addrsMutex.RUnlock() + p.poolsMutex.RLock() + defer p.poolsMutex.RUnlock() - if pool.state.get() != connectedState { + if p.state.get() != connectedState { return info } - for addr := range pool.addrs { - conn, role := pool.getConnectionFromPool(addr) + for addr := range p.addrs { + conn, role := p.getConnectionFromPool(addr) if conn != nil { info[addr] = &ConnectionInfo{ConnectedNow: conn.ConnectedNow(), ConnRole: role} } @@ -366,8 +367,8 @@ func (pool *ConnectionPool) GetPoolInfo() map[string]*ConnectionInfo { // // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. -func (connPool *ConnectionPool) Ping(userMode Mode) (*tarantool.Response, error) { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) Ping(userMode Mode) (*tarantool.Response, error) { + conn, err := p.getNextConnection(userMode) if err != nil { return nil, err } @@ -379,10 +380,10 @@ func (connPool *ConnectionPool) Ping(userMode Mode) (*tarantool.Response, error) // // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. -func (connPool *ConnectionPool) Select(space, index interface{}, +func (p *ConnectionPool) Select(space, index interface{}, offset, limit uint32, - iterator tarantool.Iter, key interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { - conn, err := connPool.getConnByMode(ANY, userMode) + iterator tarantool.Iter, key interface{}, userMode ...Mode) (*tarantool.Response, error) { + conn, err := p.getConnByMode(ANY, userMode) if err != nil { return nil, err } @@ -395,8 +396,9 @@ func (connPool *ConnectionPool) Select(space, index interface{}, // // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. -func (connPool *ConnectionPool) Insert(space interface{}, tuple interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) Insert(space interface{}, tuple interface{}, + userMode ...Mode) (*tarantool.Response, error) { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err } @@ -409,8 +411,9 @@ func (connPool *ConnectionPool) Insert(space interface{}, tuple interface{}, use // // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. -func (connPool *ConnectionPool) Replace(space interface{}, tuple interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) Replace(space interface{}, tuple interface{}, + userMode ...Mode) (*tarantool.Response, error) { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err } @@ -423,8 +426,9 @@ func (connPool *ConnectionPool) Replace(space interface{}, tuple interface{}, us // // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. -func (connPool *ConnectionPool) Delete(space, index interface{}, key interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) Delete(space, index interface{}, key interface{}, + userMode ...Mode) (*tarantool.Response, error) { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err } @@ -437,8 +441,9 @@ func (connPool *ConnectionPool) Delete(space, index interface{}, key interface{} // // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. -func (connPool *ConnectionPool) Update(space, index interface{}, key, ops interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) Update(space, index interface{}, key, ops interface{}, + userMode ...Mode) (*tarantool.Response, error) { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err } @@ -451,8 +456,9 @@ func (connPool *ConnectionPool) Update(space, index interface{}, key, ops interf // // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. -func (connPool *ConnectionPool) Upsert(space interface{}, tuple, ops interface{}, userMode ...Mode) (resp *tarantool.Response, err error) { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) Upsert(space interface{}, tuple, ops interface{}, + userMode ...Mode) (*tarantool.Response, error) { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err } @@ -465,8 +471,9 @@ func (connPool *ConnectionPool) Upsert(space interface{}, tuple, ops interface{} // // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. -func (connPool *ConnectionPool) Call(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) Call(functionName string, args interface{}, + userMode Mode) (*tarantool.Response, error) { + conn, err := p.getNextConnection(userMode) if err != nil { return nil, err } @@ -480,8 +487,9 @@ func (connPool *ConnectionPool) Call(functionName string, args interface{}, user // // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. -func (connPool *ConnectionPool) Call16(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) Call16(functionName string, args interface{}, + userMode Mode) (*tarantool.Response, error) { + conn, err := p.getNextConnection(userMode) if err != nil { return nil, err } @@ -494,8 +502,9 @@ func (connPool *ConnectionPool) Call16(functionName string, args interface{}, us // // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. -func (connPool *ConnectionPool) Call17(functionName string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) Call17(functionName string, args interface{}, + userMode Mode) (*tarantool.Response, error) { + conn, err := p.getNextConnection(userMode) if err != nil { return nil, err } @@ -507,8 +516,9 @@ func (connPool *ConnectionPool) Call17(functionName string, args interface{}, us // // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. -func (connPool *ConnectionPool) Eval(expr string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) Eval(expr string, args interface{}, + userMode Mode) (*tarantool.Response, error) { + conn, err := p.getNextConnection(userMode) if err != nil { return nil, err } @@ -520,8 +530,9 @@ func (connPool *ConnectionPool) Eval(expr string, args interface{}, userMode Mod // // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. -func (connPool *ConnectionPool) Execute(expr string, args interface{}, userMode Mode) (resp *tarantool.Response, err error) { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) Execute(expr string, args interface{}, + userMode Mode) (*tarantool.Response, error) { + conn, err := p.getNextConnection(userMode) if err != nil { return nil, err } @@ -534,8 +545,9 @@ func (connPool *ConnectionPool) Execute(expr string, args interface{}, userMode // // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. -func (connPool *ConnectionPool) GetTyped(space, index interface{}, key interface{}, result interface{}, userMode ...Mode) (err error) { - conn, err := connPool.getConnByMode(ANY, userMode) +func (p *ConnectionPool) GetTyped(space, index interface{}, key interface{}, result interface{}, + userMode ...Mode) error { + conn, err := p.getConnByMode(ANY, userMode) if err != nil { return err } @@ -547,10 +559,10 @@ func (connPool *ConnectionPool) GetTyped(space, index interface{}, key interface // // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. -func (connPool *ConnectionPool) SelectTyped(space, index interface{}, +func (p *ConnectionPool) SelectTyped(space, index interface{}, offset, limit uint32, - iterator tarantool.Iter, key interface{}, result interface{}, userMode ...Mode) (err error) { - conn, err := connPool.getConnByMode(ANY, userMode) + iterator tarantool.Iter, key interface{}, result interface{}, userMode ...Mode) error { + conn, err := p.getConnByMode(ANY, userMode) if err != nil { return err } @@ -563,8 +575,9 @@ func (connPool *ConnectionPool) SelectTyped(space, index interface{}, // // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. -func (connPool *ConnectionPool) InsertTyped(space interface{}, tuple interface{}, result interface{}, userMode ...Mode) (err error) { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) InsertTyped(space interface{}, tuple interface{}, result interface{}, + userMode ...Mode) error { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return err } @@ -577,8 +590,9 @@ func (connPool *ConnectionPool) InsertTyped(space interface{}, tuple interface{} // // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. -func (connPool *ConnectionPool) ReplaceTyped(space interface{}, tuple interface{}, result interface{}, userMode ...Mode) (err error) { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) ReplaceTyped(space interface{}, tuple interface{}, result interface{}, + userMode ...Mode) error { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return err } @@ -590,8 +604,9 @@ func (connPool *ConnectionPool) ReplaceTyped(space interface{}, tuple interface{ // // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. -func (connPool *ConnectionPool) DeleteTyped(space, index interface{}, key interface{}, result interface{}, userMode ...Mode) (err error) { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) DeleteTyped(space, index interface{}, key interface{}, result interface{}, + userMode ...Mode) error { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return err } @@ -603,8 +618,9 @@ func (connPool *ConnectionPool) DeleteTyped(space, index interface{}, key interf // // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. -func (connPool *ConnectionPool) UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}, userMode ...Mode) (err error) { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) UpdateTyped(space, index interface{}, key, ops interface{}, + result interface{}, userMode ...Mode) error { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return err } @@ -617,8 +633,9 @@ func (connPool *ConnectionPool) UpdateTyped(space, index interface{}, key, ops i // // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. -func (connPool *ConnectionPool) CallTyped(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) CallTyped(functionName string, args interface{}, result interface{}, + userMode Mode) error { + conn, err := p.getNextConnection(userMode) if err != nil { return err } @@ -632,8 +649,9 @@ func (connPool *ConnectionPool) CallTyped(functionName string, args interface{}, // // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. -func (connPool *ConnectionPool) Call16Typed(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) Call16Typed(functionName string, args interface{}, result interface{}, + userMode Mode) error { + conn, err := p.getNextConnection(userMode) if err != nil { return err } @@ -646,8 +664,9 @@ func (connPool *ConnectionPool) Call16Typed(functionName string, args interface{ // // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. -func (connPool *ConnectionPool) Call17Typed(functionName string, args interface{}, result interface{}, userMode Mode) (err error) { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) Call17Typed(functionName string, args interface{}, result interface{}, + userMode Mode) error { + conn, err := p.getNextConnection(userMode) if err != nil { return err } @@ -659,8 +678,9 @@ func (connPool *ConnectionPool) Call17Typed(functionName string, args interface{ // // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. -func (connPool *ConnectionPool) EvalTyped(expr string, args interface{}, result interface{}, userMode Mode) (err error) { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) EvalTyped(expr string, args interface{}, result interface{}, + userMode Mode) error { + conn, err := p.getNextConnection(userMode) if err != nil { return err } @@ -672,8 +692,9 @@ func (connPool *ConnectionPool) EvalTyped(expr string, args interface{}, result // // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. -func (connPool *ConnectionPool) ExecuteTyped(expr string, args interface{}, result interface{}, userMode Mode) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) ExecuteTyped(expr string, args interface{}, result interface{}, + userMode Mode) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) { + conn, err := p.getNextConnection(userMode) if err != nil { return tarantool.SQLInfo{}, nil, err } @@ -685,10 +706,10 @@ func (connPool *ConnectionPool) ExecuteTyped(expr string, args interface{}, resu // // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. -func (connPool *ConnectionPool) SelectAsync(space, index interface{}, +func (p *ConnectionPool) SelectAsync(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}, userMode ...Mode) *tarantool.Future { - conn, err := connPool.getConnByMode(ANY, userMode) + conn, err := p.getConnByMode(ANY, userMode) if err != nil { return newErrorFuture(err) } @@ -701,8 +722,9 @@ func (connPool *ConnectionPool) SelectAsync(space, index interface{}, // // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. -func (connPool *ConnectionPool) InsertAsync(space interface{}, tuple interface{}, userMode ...Mode) *tarantool.Future { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) InsertAsync(space interface{}, tuple interface{}, + userMode ...Mode) *tarantool.Future { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return newErrorFuture(err) } @@ -715,8 +737,9 @@ func (connPool *ConnectionPool) InsertAsync(space interface{}, tuple interface{} // // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. -func (connPool *ConnectionPool) ReplaceAsync(space interface{}, tuple interface{}, userMode ...Mode) *tarantool.Future { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) ReplaceAsync(space interface{}, tuple interface{}, + userMode ...Mode) *tarantool.Future { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return newErrorFuture(err) } @@ -729,8 +752,9 @@ func (connPool *ConnectionPool) ReplaceAsync(space interface{}, tuple interface{ // // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. -func (connPool *ConnectionPool) DeleteAsync(space, index interface{}, key interface{}, userMode ...Mode) *tarantool.Future { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) DeleteAsync(space, index interface{}, key interface{}, + userMode ...Mode) *tarantool.Future { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return newErrorFuture(err) } @@ -743,8 +767,9 @@ func (connPool *ConnectionPool) DeleteAsync(space, index interface{}, key interf // // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. -func (connPool *ConnectionPool) UpdateAsync(space, index interface{}, key, ops interface{}, userMode ...Mode) *tarantool.Future { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) UpdateAsync(space, index interface{}, key, ops interface{}, + userMode ...Mode) *tarantool.Future { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return newErrorFuture(err) } @@ -757,8 +782,9 @@ func (connPool *ConnectionPool) UpdateAsync(space, index interface{}, key, ops i // // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. -func (connPool *ConnectionPool) UpsertAsync(space interface{}, tuple interface{}, ops interface{}, userMode ...Mode) *tarantool.Future { - conn, err := connPool.getConnByMode(RW, userMode) +func (p *ConnectionPool) UpsertAsync(space interface{}, tuple interface{}, ops interface{}, + userMode ...Mode) *tarantool.Future { + conn, err := p.getConnByMode(RW, userMode) if err != nil { return newErrorFuture(err) } @@ -771,8 +797,9 @@ func (connPool *ConnectionPool) UpsertAsync(space interface{}, tuple interface{} // // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. -func (connPool *ConnectionPool) CallAsync(functionName string, args interface{}, userMode Mode) *tarantool.Future { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) CallAsync(functionName string, args interface{}, + userMode Mode) *tarantool.Future { + conn, err := p.getNextConnection(userMode) if err != nil { return newErrorFuture(err) } @@ -786,8 +813,9 @@ func (connPool *ConnectionPool) CallAsync(functionName string, args interface{}, // // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. -func (connPool *ConnectionPool) Call16Async(functionName string, args interface{}, userMode Mode) *tarantool.Future { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) Call16Async(functionName string, args interface{}, + userMode Mode) *tarantool.Future { + conn, err := p.getNextConnection(userMode) if err != nil { return newErrorFuture(err) } @@ -800,8 +828,9 @@ func (connPool *ConnectionPool) Call16Async(functionName string, args interface{ // // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. -func (connPool *ConnectionPool) Call17Async(functionName string, args interface{}, userMode Mode) *tarantool.Future { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) Call17Async(functionName string, args interface{}, + userMode Mode) *tarantool.Future { + conn, err := p.getNextConnection(userMode) if err != nil { return newErrorFuture(err) } @@ -813,8 +842,9 @@ func (connPool *ConnectionPool) Call17Async(functionName string, args interface{ // // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. -func (connPool *ConnectionPool) EvalAsync(expr string, args interface{}, userMode Mode) *tarantool.Future { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) EvalAsync(expr string, args interface{}, + userMode Mode) *tarantool.Future { + conn, err := p.getNextConnection(userMode) if err != nil { return newErrorFuture(err) } @@ -827,8 +857,9 @@ func (connPool *ConnectionPool) EvalAsync(expr string, args interface{}, userMod // // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. -func (connPool *ConnectionPool) ExecuteAsync(expr string, args interface{}, userMode Mode) *tarantool.Future { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) ExecuteAsync(expr string, args interface{}, + userMode Mode) *tarantool.Future { + conn, err := p.getNextConnection(userMode) if err != nil { return newErrorFuture(err) } @@ -837,13 +868,13 @@ func (connPool *ConnectionPool) ExecuteAsync(expr string, args interface{}, user } // NewStream creates new Stream object for connection selected -// by userMode from connPool. +// by userMode from pool. // // Since v. 2.10.0, Tarantool supports streams and interactive transactions over them. // To use interactive transactions, memtx_use_mvcc_engine box option should be set to true. // Since 1.7.0 -func (connPool *ConnectionPool) NewStream(userMode Mode) (*tarantool.Stream, error) { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) NewStream(userMode Mode) (*tarantool.Stream, error) { + conn, err := p.getNextConnection(userMode) if err != nil { return nil, err } @@ -851,8 +882,8 @@ func (connPool *ConnectionPool) NewStream(userMode Mode) (*tarantool.Stream, err } // NewPrepared passes a sql statement to Tarantool for preparation synchronously. -func (connPool *ConnectionPool) NewPrepared(expr string, userMode Mode) (*tarantool.Prepared, error) { - conn, err := connPool.getNextConnection(userMode) +func (p *ConnectionPool) NewPrepared(expr string, userMode Mode) (*tarantool.Prepared, error) { + conn, err := p.getNextConnection(userMode) if err != nil { return nil, err } @@ -878,10 +909,10 @@ func (connPool *ConnectionPool) NewPrepared(expr string, userMode Mode) (*tarant // https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_events/#box-watchers // // Since 1.10.0 -func (pool *ConnectionPool) NewWatcher(key string, +func (p *ConnectionPool) NewWatcher(key string, callback tarantool.WatchCallback, mode Mode) (tarantool.Watcher, error) { watchersRequired := false - for _, feature := range pool.connOpts.RequiredProtocolInfo.Features { + for _, feature := range p.connOpts.RequiredProtocolInfo.Features { if tarantool.WatchersFeature == feature { watchersRequired = true break @@ -893,7 +924,7 @@ func (pool *ConnectionPool) NewWatcher(key string, } watcher := &poolWatcher{ - container: &pool.watcherContainer, + container: &p.watcherContainer, mode: mode, key: key, callback: callback, @@ -903,11 +934,11 @@ func (pool *ConnectionPool) NewWatcher(key string, watcher.container.add(watcher) - rr := pool.anyPool + rr := p.anyPool if mode == RW { - rr = pool.rwPool + rr = p.rwPool } else if mode == RO { - rr = pool.roPool + rr = p.roPool } conns := rr.GetConnections() @@ -923,15 +954,15 @@ func (pool *ConnectionPool) NewWatcher(key string, // Do sends the request and returns a future. // For requests that belong to the only one connection (e.g. Unprepare or ExecutePrepared) // the argument of type Mode is unused. -func (connPool *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarantool.Future { +func (p *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarantool.Future { if connectedReq, ok := req.(tarantool.ConnectedRequest); ok { - conn, _ := connPool.getConnectionFromPool(connectedReq.Conn().Addr()) + conn, _ := p.getConnectionFromPool(connectedReq.Conn().Addr()) if conn == nil { - return newErrorFuture(fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool")) + return newErrorFuture(ErrUnknownRequest) } return connectedReq.Conn().Do(req) } - conn, err := connPool.getNextConnection(userMode) + conn, err := p.getNextConnection(userMode) if err != nil { return newErrorFuture(err) } @@ -943,7 +974,7 @@ func (connPool *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarant // private // -func (connPool *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (Role, error) { +func (p *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (Role, error) { resp, err := conn.Do(tarantool.NewCallRequest("box.info")).Get() if err != nil { return UnknownRole, err @@ -978,48 +1009,49 @@ func (connPool *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (R return UnknownRole, nil } -func (connPool *ConnectionPool) getConnectionFromPool(addr string) (*tarantool.Connection, Role) { - if conn := connPool.rwPool.GetConnByAddr(addr); conn != nil { +func (p *ConnectionPool) getConnectionFromPool(addr string) (*tarantool.Connection, Role) { + if conn := p.rwPool.GetConnByAddr(addr); conn != nil { return conn, MasterRole } - if conn := connPool.roPool.GetConnByAddr(addr); conn != nil { + if conn := p.roPool.GetConnByAddr(addr); conn != nil { return conn, ReplicaRole } - return connPool.anyPool.GetConnByAddr(addr), UnknownRole + return p.anyPool.GetConnByAddr(addr), UnknownRole } -func (pool *ConnectionPool) deleteConnection(addr string) { - if conn := pool.anyPool.DeleteConnByAddr(addr); conn != nil { - if conn := pool.rwPool.DeleteConnByAddr(addr); conn == nil { - pool.roPool.DeleteConnByAddr(addr) +func (p *ConnectionPool) deleteConnection(addr string) { + if conn := p.anyPool.DeleteConnByAddr(addr); conn != nil { + if conn := p.rwPool.DeleteConnByAddr(addr); conn == nil { + p.roPool.DeleteConnByAddr(addr) } // The internal connection deinitialization. - pool.watcherContainer.mutex.RLock() - defer pool.watcherContainer.mutex.RUnlock() + p.watcherContainer.mutex.RLock() + defer p.watcherContainer.mutex.RUnlock() - pool.watcherContainer.foreach(func(watcher *poolWatcher) error { + p.watcherContainer.foreach(func(watcher *poolWatcher) error { watcher.unwatch(conn) return nil }) } } -func (pool *ConnectionPool) addConnection(addr string, +func (p *ConnectionPool) addConnection(addr string, conn *tarantool.Connection, role Role) error { // The internal connection initialization. - pool.watcherContainer.mutex.RLock() - defer pool.watcherContainer.mutex.RUnlock() + p.watcherContainer.mutex.RLock() + defer p.watcherContainer.mutex.RUnlock() watched := []*poolWatcher{} - err := pool.watcherContainer.foreach(func(watcher *poolWatcher) error { + err := p.watcherContainer.foreach(func(watcher *poolWatcher) error { watch := false - if watcher.mode == RW { + switch watcher.mode { + case RW: watch = role == MasterRole - } else if watcher.mode == RO { + case RO: watch = role == ReplicaRole - } else { + default: watch = true } if watch { @@ -1038,22 +1070,22 @@ func (pool *ConnectionPool) addConnection(addr string, return err } - pool.anyPool.AddConn(addr, conn) + p.anyPool.AddConn(addr, conn) switch role { case MasterRole: - pool.rwPool.AddConn(addr, conn) + p.rwPool.AddConn(addr, conn) case ReplicaRole: - pool.roPool.AddConn(addr, conn) + p.roPool.AddConn(addr, conn) } return nil } -func (connPool *ConnectionPool) handlerDiscovered(conn *tarantool.Connection, +func (p *ConnectionPool) handlerDiscovered(conn *tarantool.Connection, role Role) bool { var err error - if connPool.opts.ConnectionHandler != nil { - err = connPool.opts.ConnectionHandler.Discovered(conn, role) + if p.opts.ConnectionHandler != nil { + err = p.opts.ConnectionHandler.Discovered(conn, role) } if err != nil { @@ -1064,11 +1096,11 @@ func (connPool *ConnectionPool) handlerDiscovered(conn *tarantool.Connection, return true } -func (connPool *ConnectionPool) handlerDeactivated(conn *tarantool.Connection, +func (p *ConnectionPool) handlerDeactivated(conn *tarantool.Connection, role Role) { var err error - if connPool.opts.ConnectionHandler != nil { - err = connPool.opts.ConnectionHandler.Deactivated(conn, role) + if p.opts.ConnectionHandler != nil { + err = p.opts.ConnectionHandler.Deactivated(conn, role) } if err != nil { @@ -1077,32 +1109,32 @@ func (connPool *ConnectionPool) handlerDeactivated(conn *tarantool.Connection, } } -func (connPool *ConnectionPool) fillPools() bool { +func (p *ConnectionPool) fillPools() bool { somebodyAlive := false // It is called before controller() goroutines so we don't expect // concurrency issues here. - for addr := range connPool.addrs { + for addr := range p.addrs { end := newEndpoint(addr) - connPool.addrs[addr] = end + p.addrs[addr] = end - connOpts := connPool.connOpts + connOpts := p.connOpts connOpts.Notify = end.notify conn, err := tarantool.Connect(addr, connOpts) if err != nil { log.Printf("tarantool: connect to %s failed: %s\n", addr, err.Error()) } else if conn != nil { - role, err := connPool.getConnectionRole(conn) + role, err := p.getConnectionRole(conn) if err != nil { conn.Close() log.Printf("tarantool: storing connection to %s failed: %s\n", addr, err) continue } - if connPool.handlerDiscovered(conn, role) { - if connPool.addConnection(addr, conn, role) != nil { + if p.handlerDiscovered(conn, role) { + if p.addConnection(addr, conn, role) != nil { conn.Close() - connPool.handlerDeactivated(conn, role) + p.handlerDeactivated(conn, role) } if conn.ConnectedNow() { @@ -1110,9 +1142,9 @@ func (connPool *ConnectionPool) fillPools() bool { end.role = role somebodyAlive = true } else { - connPool.deleteConnection(addr) + p.deleteConnection(addr) conn.Close() - connPool.handlerDeactivated(conn, role) + p.handlerDeactivated(conn, role) } } else { conn.Close() @@ -1123,21 +1155,21 @@ func (connPool *ConnectionPool) fillPools() bool { return somebodyAlive } -func (pool *ConnectionPool) updateConnection(e *endpoint) { - pool.poolsMutex.Lock() +func (p *ConnectionPool) updateConnection(e *endpoint) { + p.poolsMutex.Lock() - if pool.state.get() != connectedState { - pool.poolsMutex.Unlock() + if p.state.get() != connectedState { + p.poolsMutex.Unlock() return } - if role, err := pool.getConnectionRole(e.conn); err == nil { + if role, err := p.getConnectionRole(e.conn); err == nil { if e.role != role { - pool.deleteConnection(e.addr) - pool.poolsMutex.Unlock() + p.deleteConnection(e.addr) + p.poolsMutex.Unlock() - pool.handlerDeactivated(e.conn, e.role) - opened := pool.handlerDiscovered(e.conn, role) + p.handlerDeactivated(e.conn, e.role) + opened := p.handlerDiscovered(e.conn, role) if !opened { e.conn.Close() e.conn = nil @@ -1145,59 +1177,59 @@ func (pool *ConnectionPool) updateConnection(e *endpoint) { return } - pool.poolsMutex.Lock() - if pool.state.get() != connectedState { - pool.poolsMutex.Unlock() + p.poolsMutex.Lock() + if p.state.get() != connectedState { + p.poolsMutex.Unlock() e.conn.Close() - pool.handlerDeactivated(e.conn, role) + p.handlerDeactivated(e.conn, role) e.conn = nil e.role = UnknownRole return } - if pool.addConnection(e.addr, e.conn, role) != nil { - pool.poolsMutex.Unlock() + if p.addConnection(e.addr, e.conn, role) != nil { + p.poolsMutex.Unlock() e.conn.Close() - pool.handlerDeactivated(e.conn, role) + p.handlerDeactivated(e.conn, role) e.conn = nil e.role = UnknownRole return } e.role = role } - pool.poolsMutex.Unlock() + p.poolsMutex.Unlock() return } else { - pool.deleteConnection(e.addr) - pool.poolsMutex.Unlock() + p.deleteConnection(e.addr) + p.poolsMutex.Unlock() e.conn.Close() - pool.handlerDeactivated(e.conn, e.role) + p.handlerDeactivated(e.conn, e.role) e.conn = nil e.role = UnknownRole return } } -func (pool *ConnectionPool) tryConnect(e *endpoint) error { - pool.poolsMutex.Lock() +func (p *ConnectionPool) tryConnect(e *endpoint) error { + p.poolsMutex.Lock() - if pool.state.get() != connectedState { - pool.poolsMutex.Unlock() + if p.state.get() != connectedState { + p.poolsMutex.Unlock() return ErrClosed } e.conn = nil e.role = UnknownRole - connOpts := pool.connOpts + connOpts := p.connOpts connOpts.Notify = e.notify conn, err := tarantool.Connect(e.addr, connOpts) if err == nil { - role, err := pool.getConnectionRole(conn) - pool.poolsMutex.Unlock() + role, err := p.getConnectionRole(conn) + p.poolsMutex.Unlock() if err != nil { conn.Close() @@ -1205,54 +1237,54 @@ func (pool *ConnectionPool) tryConnect(e *endpoint) error { return err } - opened := pool.handlerDiscovered(conn, role) + opened := p.handlerDiscovered(conn, role) if !opened { conn.Close() return errors.New("storing connection canceled") } - pool.poolsMutex.Lock() - if pool.state.get() != connectedState { - pool.poolsMutex.Unlock() + p.poolsMutex.Lock() + if p.state.get() != connectedState { + p.poolsMutex.Unlock() conn.Close() - pool.handlerDeactivated(conn, role) + p.handlerDeactivated(conn, role) return ErrClosed } - if err = pool.addConnection(e.addr, conn, role); err != nil { - pool.poolsMutex.Unlock() + if err = p.addConnection(e.addr, conn, role); err != nil { + p.poolsMutex.Unlock() conn.Close() - pool.handlerDeactivated(conn, role) + p.handlerDeactivated(conn, role) return err } e.conn = conn e.role = role } - pool.poolsMutex.Unlock() + p.poolsMutex.Unlock() return err } -func (pool *ConnectionPool) reconnect(e *endpoint) { - pool.poolsMutex.Lock() +func (p *ConnectionPool) reconnect(e *endpoint) { + p.poolsMutex.Lock() - if pool.state.get() != connectedState { - pool.poolsMutex.Unlock() + if p.state.get() != connectedState { + p.poolsMutex.Unlock() return } - pool.deleteConnection(e.addr) - pool.poolsMutex.Unlock() + p.deleteConnection(e.addr) + p.poolsMutex.Unlock() - pool.handlerDeactivated(e.conn, e.role) + p.handlerDeactivated(e.conn, e.role) e.conn = nil e.role = UnknownRole - pool.tryConnect(e) + p.tryConnect(e) } -func (pool *ConnectionPool) controller(e *endpoint) { - timer := time.NewTicker(pool.opts.CheckTimeout) +func (p *ConnectionPool) controller(e *endpoint) { + timer := time.NewTicker(p.opts.CheckTimeout) defer timer.Stop() shutdown := false @@ -1276,13 +1308,13 @@ func (pool *ConnectionPool) controller(e *endpoint) { // e.close has priority to avoid concurrency with e.shutdown. case <-e.close: if e.conn != nil { - pool.poolsMutex.Lock() - pool.deleteConnection(e.addr) - pool.poolsMutex.Unlock() + p.poolsMutex.Lock() + p.deleteConnection(e.addr) + p.poolsMutex.Unlock() if !shutdown { e.closeErr = e.conn.Close() - pool.handlerDeactivated(e.conn, e.role) + p.handlerDeactivated(e.conn, e.role) close(e.closed) } else { // Force close the connection. @@ -1298,9 +1330,9 @@ func (pool *ConnectionPool) controller(e *endpoint) { case <-e.shutdown: shutdown = true if e.conn != nil { - pool.poolsMutex.Lock() - pool.deleteConnection(e.addr) - pool.poolsMutex.Unlock() + p.poolsMutex.Lock() + p.deleteConnection(e.addr) + p.poolsMutex.Unlock() // We need to catch s.close in the current goroutine, so // we need to start an another one for the shutdown. @@ -1319,15 +1351,15 @@ func (pool *ConnectionPool) controller(e *endpoint) { // Will be processed at an upper level. case <-e.notify: if e.conn != nil && e.conn.ClosedNow() { - pool.poolsMutex.Lock() - if pool.state.get() == connectedState { - pool.deleteConnection(e.addr) - pool.poolsMutex.Unlock() - pool.handlerDeactivated(e.conn, e.role) + p.poolsMutex.Lock() + if p.state.get() == connectedState { + p.deleteConnection(e.addr) + p.poolsMutex.Unlock() + p.handlerDeactivated(e.conn, e.role) e.conn = nil e.role = UnknownRole } else { - pool.poolsMutex.Unlock() + p.poolsMutex.Unlock() } } case <-timer.C: @@ -1335,11 +1367,11 @@ func (pool *ConnectionPool) controller(e *endpoint) { // Relocate connection between subpools // if ro/rw was updated. if e.conn == nil { - pool.tryConnect(e) + p.tryConnect(e) } else if !e.conn.ClosedNow() { - pool.updateConnection(e) + p.updateConnection(e) } else { - pool.reconnect(e) + p.reconnect(e) } } } @@ -1347,42 +1379,43 @@ func (pool *ConnectionPool) controller(e *endpoint) { } } -func (connPool *ConnectionPool) getNextConnection(mode Mode) (*tarantool.Connection, error) { +func (p *ConnectionPool) getNextConnection(mode Mode) (*tarantool.Connection, error) { switch mode { case ANY: - if next := connPool.anyPool.GetNextConnection(); next != nil { + if next := p.anyPool.GetNextConnection(); next != nil { return next, nil } case RW: - if next := connPool.rwPool.GetNextConnection(); next != nil { + if next := p.rwPool.GetNextConnection(); next != nil { return next, nil } return nil, ErrNoRwInstance case RO: - if next := connPool.roPool.GetNextConnection(); next != nil { + if next := p.roPool.GetNextConnection(); next != nil { return next, nil } return nil, ErrNoRoInstance case PreferRW: - if next := connPool.rwPool.GetNextConnection(); next != nil { + if next := p.rwPool.GetNextConnection(); next != nil { return next, nil } - if next := connPool.roPool.GetNextConnection(); next != nil { + if next := p.roPool.GetNextConnection(); next != nil { return next, nil } case PreferRO: - if next := connPool.roPool.GetNextConnection(); next != nil { + if next := p.roPool.GetNextConnection(); next != nil { return next, nil } - if next := connPool.rwPool.GetNextConnection(); next != nil { + if next := p.rwPool.GetNextConnection(); next != nil { return next, nil } } return nil, ErrNoHealthyInstance } -func (connPool *ConnectionPool) getConnByMode(defaultMode Mode, userMode []Mode) (*tarantool.Connection, error) { +func (p *ConnectionPool) getConnByMode(defaultMode Mode, + userMode []Mode) (*tarantool.Connection, error) { if len(userMode) > 1 { return nil, ErrTooManyArgs } @@ -1392,7 +1425,7 @@ func (connPool *ConnectionPool) getConnByMode(defaultMode Mode, userMode []Mode) mode = userMode[0] } - return connPool.getNextConnection(mode) + return p.getNextConnection(mode) } func newErrorFuture(err error) *tarantool.Future { diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index 912721cd4..c34e71673 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -130,7 +130,8 @@ func TestReconnect(t *testing.T) { }, } - err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) err = test_helpers.RestartTarantool(&instances[0]) @@ -146,7 +147,8 @@ func TestReconnect(t *testing.T) { }, } - err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) } @@ -220,7 +222,8 @@ func TestDisconnectAll(t *testing.T) { }, } - err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) err = test_helpers.RestartTarantool(&instances[0]) @@ -240,7 +243,8 @@ func TestDisconnectAll(t *testing.T) { }, } - err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) } @@ -578,7 +582,8 @@ func TestClose(t *testing.T) { }, } - err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) } @@ -636,7 +641,8 @@ func TestCloseGraceful(t *testing.T) { }, } - err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) } @@ -904,7 +910,8 @@ func TestRequestOnClosed(t *testing.T) { server2: false, }, } - err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry) + err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, + defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) _, err = connPool.Ping(pool.ANY) @@ -1563,7 +1570,8 @@ func TestUpdateInstancesRoles(t *testing.T) { Mode: pool.ANY, } - err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) + err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, + defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) // RW @@ -1574,7 +1582,8 @@ func TestUpdateInstancesRoles(t *testing.T) { Mode: pool.RW, } - err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) + err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, + defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) // RO @@ -1585,7 +1594,8 @@ func TestUpdateInstancesRoles(t *testing.T) { Mode: pool.RO, } - err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) + err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, + defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) // PreferRW @@ -1596,7 +1606,8 @@ func TestUpdateInstancesRoles(t *testing.T) { Mode: pool.PreferRW, } - err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) + err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, + defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) // PreferRO @@ -1607,7 +1618,8 @@ func TestUpdateInstancesRoles(t *testing.T) { Mode: pool.PreferRO, } - err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, defaultCountRetry, defaultTimeoutRetry) + err = test_helpers.Retry(test_helpers.ProcessListenOnInstance, args, + defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) } @@ -1669,7 +1681,8 @@ func TestInsert(t *testing.T) { require.Equalf(t, "rw_insert_value", value, "unexpected body of Select (1)") // PreferRW - resp, err = connPool.Insert(spaceName, []interface{}{"preferRW_insert_key", "preferRW_insert_value"}) + resp, err = connPool.Insert(spaceName, + []interface{}{"preferRW_insert_key", "preferRW_insert_value"}) require.Nilf(t, err, "failed to Insert") require.NotNilf(t, resp, "response is nil after Insert") require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") @@ -1791,7 +1804,9 @@ func TestUpsert(t *testing.T) { defer conn.Close() // Mode is `RW` by default, we have only one RW instance (servers[2]) - resp, err := connPool.Upsert(spaceName, []interface{}{"upsert_key", "upsert_value"}, []interface{}{[]interface{}{"=", 1, "new_value"}}) + resp, err := connPool.Upsert(spaceName, + []interface{}{"upsert_key", "upsert_value"}, + []interface{}{[]interface{}{"=", 1, "new_value"}}) require.Nilf(t, err, "failed to Upsert") require.NotNilf(t, resp, "response is nil after Upsert") @@ -1880,7 +1895,8 @@ func TestUpdate(t *testing.T) { require.Equalf(t, "update_value", value, "unexpected body of Insert (1)") // Mode is `RW` by default, we have only one RW instance (servers[2]) - resp, err = connPool.Update(spaceName, indexNo, []interface{}{"update_key"}, []interface{}{[]interface{}{"=", 1, "new_value"}}) + resp, err = connPool.Update(spaceName, indexNo, + []interface{}{"update_key"}, []interface{}{[]interface{}{"=", 1, "new_value"}}) require.Nilf(t, err, "failed to Update") require.NotNilf(t, resp, "response is nil after Update") @@ -2229,7 +2245,8 @@ func TestNewPrepared(t *testing.T) { defer connPool.Close() - stmt, err := connPool.NewPrepared("SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:id AND NAME1=:name;", pool.RO) + stmt, err := connPool.NewPrepared( + "SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:id AND NAME1=:name;", pool.RO) require.Nilf(t, err, "fail to prepare statement: %v", err) if connPool.GetPoolInfo()[stmt.Conn.Addr()].ConnRole != pool.ReplicaRole { @@ -2282,7 +2299,8 @@ func TestNewPrepared(t *testing.T) { } func TestDoWithStrangerConn(t *testing.T) { - expectedErr := fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool") + expectedErr := fmt.Errorf("the passed connected request doesn't belong to " + + "the current connection pool") roles := []bool{true, true, false, true, false} diff --git a/pool/connector_test.go b/pool/connector_test.go index 8a954fa6e..b667b0d34 100644 --- a/pool/connector_test.go +++ b/pool/connector_test.go @@ -136,7 +136,7 @@ type baseRequestMock struct { } var reqResp *tarantool.Response = &tarantool.Response{} -var reqErr error = errors.New("response error") +var errReq error = errors.New("response error") var reqFuture *tarantool.Future = &tarantool.Future{} var reqFunctionName string = "any_name" @@ -166,7 +166,7 @@ func (m *getTypedMock) GetTyped(space, index, key interface{}, m.key = key m.result = result m.mode = mode[0] - return reqErr + return errReq } func TestConnectorGetTyped(t *testing.T) { @@ -175,7 +175,7 @@ func TestConnectorGetTyped(t *testing.T) { err := c.GetTyped(reqSpace, reqIndex, reqKey, reqResult) - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") require.Equalf(t, reqIndex, m.index, "unexpected index was passed") @@ -199,7 +199,7 @@ func (m *selectMock) Select(space, index interface{}, m.iterator = iterator m.key = key m.mode = mode[0] - return reqResp, reqErr + return reqResp, errReq } func TestConnectorSelect(t *testing.T) { @@ -209,7 +209,7 @@ func TestConnectorSelect(t *testing.T) { resp, err := c.Select(reqSpace, reqIndex, reqOffset, reqLimit, reqIterator, reqKey) require.Equalf(t, reqResp, resp, "unexpected response") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") require.Equalf(t, reqIndex, m.index, "unexpected index was passed") @@ -236,7 +236,7 @@ func (m *selectTypedMock) SelectTyped(space, index interface{}, m.key = key m.result = result m.mode = mode[0] - return reqErr + return errReq } func TestConnectorSelectTyped(t *testing.T) { @@ -246,7 +246,7 @@ func TestConnectorSelectTyped(t *testing.T) { err := c.SelectTyped(reqSpace, reqIndex, reqOffset, reqLimit, reqIterator, reqKey, reqResult) - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") require.Equalf(t, reqIndex, m.index, "unexpected index was passed") @@ -304,7 +304,7 @@ func (m *insertMock) Insert(space, tuple interface{}, m.space = space m.tuple = tuple m.mode = mode[0] - return reqResp, reqErr + return reqResp, errReq } func TestConnectorInsert(t *testing.T) { @@ -314,7 +314,7 @@ func TestConnectorInsert(t *testing.T) { resp, err := c.Insert(reqSpace, reqTuple) require.Equalf(t, reqResp, resp, "unexpected response") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") @@ -332,7 +332,7 @@ func (m *insertTypedMock) InsertTyped(space, tuple interface{}, m.tuple = tuple m.result = result m.mode = mode[0] - return reqErr + return errReq } func TestConnectorInsertTyped(t *testing.T) { @@ -341,7 +341,7 @@ func TestConnectorInsertTyped(t *testing.T) { err := c.InsertTyped(reqSpace, reqTuple, reqResult) - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") @@ -385,7 +385,7 @@ func (m *replaceMock) Replace(space, tuple interface{}, m.space = space m.tuple = tuple m.mode = mode[0] - return reqResp, reqErr + return reqResp, errReq } func TestConnectorReplace(t *testing.T) { @@ -395,7 +395,7 @@ func TestConnectorReplace(t *testing.T) { resp, err := c.Replace(reqSpace, reqTuple) require.Equalf(t, reqResp, resp, "unexpected response") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") @@ -413,7 +413,7 @@ func (m *replaceTypedMock) ReplaceTyped(space, tuple interface{}, m.tuple = tuple m.result = result m.mode = mode[0] - return reqErr + return errReq } func TestConnectorReplaceTyped(t *testing.T) { @@ -422,7 +422,7 @@ func TestConnectorReplaceTyped(t *testing.T) { err := c.ReplaceTyped(reqSpace, reqTuple, reqResult) - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") @@ -467,7 +467,7 @@ func (m *deleteMock) Delete(space, index, key interface{}, m.index = index m.key = key m.mode = mode[0] - return reqResp, reqErr + return reqResp, errReq } func TestConnectorDelete(t *testing.T) { @@ -477,7 +477,7 @@ func TestConnectorDelete(t *testing.T) { resp, err := c.Delete(reqSpace, reqIndex, reqKey) require.Equalf(t, reqResp, resp, "unexpected response") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") require.Equalf(t, reqIndex, m.index, "unexpected index was passed") @@ -497,7 +497,7 @@ func (m *deleteTypedMock) DeleteTyped(space, index, key interface{}, m.key = key m.result = result m.mode = mode[0] - return reqErr + return errReq } func TestConnectorDeleteTyped(t *testing.T) { @@ -506,7 +506,7 @@ func TestConnectorDeleteTyped(t *testing.T) { err := c.DeleteTyped(reqSpace, reqIndex, reqKey, reqResult) - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") require.Equalf(t, reqIndex, m.index, "unexpected index was passed") @@ -555,7 +555,7 @@ func (m *updateMock) Update(space, index, key, ops interface{}, m.key = key m.ops = ops m.mode = mode[0] - return reqResp, reqErr + return reqResp, errReq } func TestConnectorUpdate(t *testing.T) { @@ -565,7 +565,7 @@ func TestConnectorUpdate(t *testing.T) { resp, err := c.Update(reqSpace, reqIndex, reqKey, reqOps) require.Equalf(t, reqResp, resp, "unexpected response") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") require.Equalf(t, reqIndex, m.index, "unexpected index was passed") @@ -587,7 +587,7 @@ func (m *updateTypedMock) UpdateTyped(space, index, key, ops interface{}, m.ops = ops m.result = result m.mode = mode[0] - return reqErr + return errReq } func TestConnectorUpdateTyped(t *testing.T) { @@ -596,7 +596,7 @@ func TestConnectorUpdateTyped(t *testing.T) { err := c.UpdateTyped(reqSpace, reqIndex, reqKey, reqOps, reqResult) - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") require.Equalf(t, reqIndex, m.index, "unexpected index was passed") @@ -647,7 +647,7 @@ func (m *upsertMock) Upsert(space, tuple, ops interface{}, m.tuple = tuple m.ops = ops m.mode = mode[0] - return reqResp, reqErr + return reqResp, errReq } func TestConnectorUpsert(t *testing.T) { @@ -657,7 +657,7 @@ func TestConnectorUpsert(t *testing.T) { resp, err := c.Upsert(reqSpace, reqTuple, reqOps) require.Equalf(t, reqResp, resp, "unexpected response") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") @@ -703,7 +703,7 @@ func (m *baseCallMock) call(functionName string, args interface{}, m.functionName = functionName m.args = args m.mode = mode - return reqResp, reqErr + return reqResp, errReq } func (m *baseCallMock) callTyped(functionName string, args interface{}, @@ -713,7 +713,7 @@ func (m *baseCallMock) callTyped(functionName string, args interface{}, m.args = args m.result = result m.mode = mode - return reqErr + return errReq } func (m *baseCallMock) callAsync(functionName string, args interface{}, @@ -741,7 +741,7 @@ func TestConnectorCall(t *testing.T) { resp, err := c.Call(reqFunctionName, reqArgs) require.Equalf(t, reqResp, resp, "unexpected response") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, "unexpected functionName was passed") @@ -764,7 +764,7 @@ func TestConnectorCallTyped(t *testing.T) { err := c.CallTyped(reqFunctionName, reqArgs, reqResult) - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, "unexpected functionName was passed") @@ -812,7 +812,7 @@ func TestConnectorCall16(t *testing.T) { resp, err := c.Call16(reqFunctionName, reqArgs) require.Equalf(t, reqResp, resp, "unexpected response") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, "unexpected functionName was passed") @@ -835,7 +835,7 @@ func TestConnectorCall16Typed(t *testing.T) { err := c.Call16Typed(reqFunctionName, reqArgs, reqResult) - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, "unexpected functionName was passed") @@ -883,7 +883,7 @@ func TestConnectorCall17(t *testing.T) { resp, err := c.Call17(reqFunctionName, reqArgs) require.Equalf(t, reqResp, resp, "unexpected response") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, "unexpected functionName was passed") @@ -906,7 +906,7 @@ func TestConnectorCall17Typed(t *testing.T) { err := c.Call17Typed(reqFunctionName, reqArgs, reqResult) - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, "unexpected functionName was passed") @@ -954,7 +954,7 @@ func TestConnectorEval(t *testing.T) { resp, err := c.Eval(reqFunctionName, reqArgs) require.Equalf(t, reqResp, resp, "unexpected response") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, "unexpected expr was passed") @@ -977,7 +977,7 @@ func TestConnectorEvalTyped(t *testing.T) { err := c.EvalTyped(reqFunctionName, reqArgs, reqResult) - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, "unexpected expr was passed") @@ -1025,7 +1025,7 @@ func TestConnectorExecute(t *testing.T) { resp, err := c.Execute(reqFunctionName, reqArgs) require.Equalf(t, reqResp, resp, "unexpected response") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, "unexpected expr was passed") @@ -1040,7 +1040,7 @@ type executeTypedMock struct { func (m *executeTypedMock) ExecuteTyped(functionName string, args, result interface{}, mode Mode) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) { m.callTyped(functionName, args, result, mode) - return reqSqlInfo, reqMeta, reqErr + return reqSqlInfo, reqMeta, errReq } func TestConnectorExecuteTyped(t *testing.T) { @@ -1051,7 +1051,7 @@ func TestConnectorExecuteTyped(t *testing.T) { require.Equalf(t, reqSqlInfo, info, "unexpected info") require.Equalf(t, reqMeta, meta, "unexpected meta") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, "unexpected expr was passed") @@ -1097,7 +1097,7 @@ func (m *newPreparedMock) NewPrepared(expr string, m.called++ m.expr = expr m.mode = mode - return reqPrepared, reqErr + return reqPrepared, errReq } func TestConnectorNewPrepared(t *testing.T) { @@ -1107,7 +1107,7 @@ func TestConnectorNewPrepared(t *testing.T) { p, err := c.NewPrepared(reqFunctionName) require.Equalf(t, reqPrepared, p, "unexpected prepared") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.expr, "unexpected expr was passed") @@ -1125,7 +1125,7 @@ type newStreamMock struct { func (m *newStreamMock) NewStream(mode Mode) (*tarantool.Stream, error) { m.called++ m.mode = mode - return reqStream, reqErr + return reqStream, errReq } func TestConnectorNewStream(t *testing.T) { @@ -1135,7 +1135,7 @@ func TestConnectorNewStream(t *testing.T) { s, err := c.NewStream() require.Equalf(t, reqStream, s, "unexpected stream") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, testMode, m.mode, "unexpected proxy mode") } @@ -1162,7 +1162,7 @@ func (m *newWatcherMock) NewWatcher(key string, m.key = key m.callback = callback m.mode = mode - return reqWatcher, reqErr + return reqWatcher, errReq } func TestConnectorNewWatcher(t *testing.T) { @@ -1172,7 +1172,7 @@ func TestConnectorNewWatcher(t *testing.T) { w, err := c.NewWatcher(reqWatchKey, func(event tarantool.WatchEvent) {}) require.Equalf(t, reqWatcher, w, "unexpected watcher") - require.Equalf(t, reqErr, err, "unexpected error") + require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqWatchKey, m.key, "unexpected key") require.NotNilf(t, m.callback, "callback must be set") diff --git a/queue/example_msgpack_test.go b/queue/example_msgpack_test.go index ff37c1879..6fd101e09 100644 --- a/queue/example_msgpack_test.go +++ b/queue/example_msgpack_test.go @@ -76,21 +76,23 @@ func Example_simpleQueueCustomMsgPack() { que := queue.New(conn, "test_queue_msgpack") if err = que.Create(cfg); err != nil { - log.Fatalf("queue create: %s", err) + fmt.Printf("queue create: %s", err) return } // Put data. task, err := que.Put("test_data") if err != nil { - log.Fatalf("put task: %s", err) + fmt.Printf("put task: %s", err) + return } fmt.Println("Task id is", task.Id()) // Take data. task, err = que.Take() // Blocking operation. if err != nil { - log.Fatalf("take task: %s", err) + fmt.Printf("take task: %s", err) + return } fmt.Println("Data is", task.Data()) task.Ack() @@ -100,7 +102,8 @@ func Example_simpleQueueCustomMsgPack() { // Put data. task, err = que.Put(&putData) if err != nil { - log.Fatalf("put typed task: %s", err) + fmt.Printf("put typed task: %s", err) + return } fmt.Println("Task id is ", task.Id()) @@ -108,7 +111,8 @@ func Example_simpleQueueCustomMsgPack() { // Take data. task, err = que.TakeTyped(&takeData) // Blocking operation. if err != nil { - log.Fatalf("take take typed: %s", err) + fmt.Printf("take take typed: %s", err) + return } fmt.Println("Data is ", takeData) // Same data. @@ -116,13 +120,15 @@ func Example_simpleQueueCustomMsgPack() { task, err = que.Put([]int{1, 2, 3}) if err != nil { - log.Fatalf("Put failed: %s", err) + fmt.Printf("Put failed: %s", err) + return } task.Bury() task, err = que.TakeTimeout(2 * time.Second) if err != nil { - log.Fatalf("Take with timeout failed: %s", err) + fmt.Printf("Take with timeout failed: %s", err) + return } if task == nil { fmt.Println("Task is nil") diff --git a/queue/queue_test.go b/queue/queue_test.go index 313032f49..575ce78a0 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -129,7 +129,7 @@ func TestQueue_ReIdentify(t *testing.T) { if newuuid.String() != uuid.String() { t.Fatalf("Unequal UUIDs after re-identify: %s, expected %s", newuuid, uuid) } - //Put + // Put. putData := "put_data" task, err := q.Put(putData) if err != nil { @@ -137,13 +137,11 @@ func TestQueue_ReIdentify(t *testing.T) { t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { t.Fatalf("Task is nil after put") - } else { - if task.Data() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) - } + } else if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) } - //Take + // Take. task, err = q.TakeTimeout(2 * time.Second) if err != nil { t.Fatalf("Failed take from queue: %s", err) @@ -157,7 +155,7 @@ func TestQueue_ReIdentify(t *testing.T) { conn = test_helpers.ConnectWithValidation(t, server, opts) q = queue.New(conn, name) - //Identify in another connection + // Identify in another connection. newuuid, err = q.Identify(&uuid) if err != nil { t.Fatalf("Failed to identify: %s", err) @@ -166,7 +164,7 @@ func TestQueue_ReIdentify(t *testing.T) { t.Fatalf("Unequal UUIDs after re-identify: %s, expected %s", newuuid, uuid) } - //Peek in another connection + // Peek in another connection. task, err = q.Peek(task.Id()) if err != nil { t.Fatalf("Failed take from queue: %s", err) @@ -174,7 +172,7 @@ func TestQueue_ReIdentify(t *testing.T) { t.Fatalf("Task is nil after take") } - //Ack in another connection + // Ack in another connection. err = task.Ack() if err != nil { t.Errorf("Failed ack %s", err) @@ -238,17 +236,15 @@ func TestFifoQueue_Put(t *testing.T) { q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) defer dropQueue(t, q) - //Put + // Put. putData := "put_data" task, err := q.Put(putData) if err != nil { t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { t.Fatalf("Task is nil after put") - } else { - if task.Data() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) - } + } else if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) } } @@ -260,20 +256,18 @@ func TestFifoQueue_Take(t *testing.T) { q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) defer dropQueue(t, q) - //Put + // Put. putData := "put_data" task, err := q.Put(putData) if err != nil { t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { t.Fatalf("Task is nil after put") - } else { - if task.Data() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) - } + } else if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) } - //Take + // Take. task, err = q.TakeTimeout(2 * time.Second) if err != nil { t.Errorf("Failed take from queue: %s", err) @@ -335,7 +329,7 @@ func TestFifoQueue_TakeTyped(t *testing.T) { q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) defer dropQueue(t, q) - //Put + // Put. putData := &customData{customField: "put_data"} task, err := q.Put(putData) if err != nil { @@ -345,14 +339,16 @@ func TestFifoQueue_TakeTyped(t *testing.T) { } else { typedData, ok := task.Data().(*customData) if !ok { - t.Errorf("Task data after put has different type. %#v != %#v", task.Data(), putData) + t.Errorf("Task data after put has different type. %#v != %#v", + task.Data(), putData) } if *typedData != *putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) + t.Errorf("Task data after put not equal with example. %s != %s", + task.Data(), putData) } } - //Take + // Take. takeData := &customData{} task, err = q.TakeTypedTimeout(2*time.Second, takeData) if err != nil { @@ -362,23 +358,28 @@ func TestFifoQueue_TakeTyped(t *testing.T) { } else { typedData, ok := task.Data().(*customData) if !ok { - t.Errorf("Task data after put has different type. %#v != %#v", task.Data(), putData) + t.Errorf("Task data after put has different type. %#v != %#v", + task.Data(), putData) } if *typedData != *putData { - t.Errorf("Task data after take not equal with example. %#v != %#v", task.Data(), putData) + t.Errorf("Task data after take not equal with example. %#v != %#v", + task.Data(), putData) } if *takeData != *putData { - t.Errorf("Task data after take not equal with example. %#v != %#v", task.Data(), putData) + t.Errorf("Task data after take not equal with example. %#v != %#v", + task.Data(), putData) } if !task.IsTaken() { - t.Errorf("Task status after take is not taken. Status = %s", task.Status()) + t.Errorf("Task status after take is not taken. Status = %s", + task.Status()) } err = task.Ack() if err != nil { t.Errorf("Failed ack %s", err) } else if !task.IsDone() { - t.Errorf("Task status after take is not done. Status = %s", task.Status()) + t.Errorf("Task status after take is not done. Status = %s", + task.Status()) } } } @@ -391,33 +392,27 @@ func TestFifoQueue_Peek(t *testing.T) { q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) defer dropQueue(t, q) - //Put + // Put. putData := "put_data" task, err := q.Put(putData) if err != nil { t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { t.Fatalf("Task is nil after put") - } else { - if task.Data() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) - } + } else if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) } - //Peek + // Peek. task, err = q.Peek(task.Id()) if err != nil { t.Errorf("Failed peek from queue: %s", err) } else if task == nil { t.Errorf("Task is nil after peek") - } else { - if task.Data() != putData { - t.Errorf("Task data after peek not equal with example. %s != %s", task.Data(), putData) - } - - if !task.IsReady() { - t.Errorf("Task status after peek is not ready. Status = %s", task.Status()) - } + } else if task.Data() != putData { + t.Errorf("Task data after peek not equal with example. %s != %s", task.Data(), putData) + } else if !task.IsReady() { + t.Errorf("Task status after peek is not ready. Status = %s", task.Status()) } } @@ -429,20 +424,18 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) defer dropQueue(t, q) - //Put + // Put. putData := "put_data" task, err := q.Put(putData) if err != nil { t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { t.Fatalf("Task is nil after put") - } else { - if task.Data() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) - } + } else if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) } - //Bury + // Bury. err = task.Bury() if err != nil { t.Fatalf("Failed bury task %s", err) @@ -450,7 +443,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { t.Errorf("Task status after bury is not buried. Status = %s", task.Status()) } - //Kick + // Kick. count, err := q.Kick(1) if err != nil { t.Fatalf("Failed kick task %s", err) @@ -458,7 +451,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { t.Fatalf("Kick result != 1") } - //Take + // Take. task, err = q.TakeTimeout(2 * time.Second) if err != nil { t.Errorf("Failed take from queue: %s", err) @@ -492,7 +485,7 @@ func TestFifoQueue_Delete(t *testing.T) { q := createQueue(t, conn, name, queue.Cfg{Temporary: true, Kind: queue.FIFO}) defer dropQueue(t, q) - //Put + // Put. var putData = "put_data" var tasks = [2]*queue.Task{} @@ -502,14 +495,14 @@ func TestFifoQueue_Delete(t *testing.T) { t.Fatalf("Failed put to queue: %s", err) } else if err == nil && tasks[i] == nil { t.Fatalf("Task is nil after put") - } else { - if tasks[i].Data() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", tasks[i].Data(), putData) - } + } else if tasks[i].Data() != putData { + t.Errorf( + "Task data after put not equal with example. %s != %s", + tasks[i].Data(), putData) } } - //Delete by task method + // Delete by task method. err = tasks[0].Delete() if err != nil { t.Fatalf("Failed bury task %s", err) @@ -517,7 +510,7 @@ func TestFifoQueue_Delete(t *testing.T) { t.Errorf("Task status after delete is not done. Status = %s", tasks[0].Status()) } - //Delete by task ID + // Delete by task ID. err = q.Delete(tasks[1].Id()) if err != nil { t.Fatalf("Failed bury task %s", err) @@ -525,7 +518,7 @@ func TestFifoQueue_Delete(t *testing.T) { t.Errorf("Task status after delete is not done. Status = %s", tasks[0].Status()) } - //Take + // Take. for i := 0; i < 2; i++ { tasks[i], err = q.TakeTimeout(2 * time.Second) if err != nil { @@ -550,13 +543,11 @@ func TestFifoQueue_Release(t *testing.T) { t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { t.Fatalf("Task is nil after put") - } else { - if task.Data() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) - } + } else if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) } - //Take + // Take. task, err = q.Take() if err != nil { t.Fatalf("Failed take from queue: %s", err) @@ -564,7 +555,7 @@ func TestFifoQueue_Release(t *testing.T) { t.Fatal("Task is nil after take") } - //Release + // Release. err = task.Release() if err != nil { t.Fatalf("Failed release task %s", err) @@ -574,7 +565,7 @@ func TestFifoQueue_Release(t *testing.T) { t.Fatalf("Task status is not ready, but %s", task.Status()) } - //Take + // Take. task, err = q.Take() if err != nil { t.Fatalf("Failed take from queue: %s", err) @@ -612,13 +603,11 @@ func TestQueue_ReleaseAll(t *testing.T) { t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { t.Fatalf("Task is nil after put") - } else { - if task.Data() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) - } + } else if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) } - //Take + // Take. task, err = q.Take() if err != nil { t.Fatalf("Failed take from queue: %s", err) @@ -626,7 +615,7 @@ func TestQueue_ReleaseAll(t *testing.T) { t.Fatal("Task is nil after take") } - //ReleaseAll + // ReleaseAll. err = q.ReleaseAll() if err != nil { t.Fatalf("Failed release task %s", err) @@ -640,7 +629,7 @@ func TestQueue_ReleaseAll(t *testing.T) { t.Fatalf("Task status is not ready, but %s", task.Status()) } - //Take + // Take. task, err = q.Take() if err != nil { t.Fatalf("Failed take from queue: %s", err) @@ -683,15 +672,13 @@ func TestTtlQueue(t *testing.T) { t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { t.Fatalf("Task is nil after put") - } else { - if task.Data() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) - } + } else if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) } time.Sleep(10 * time.Second) - //Take + // Take. task, err = q.TakeTimeout(2 * time.Second) if err != nil { t.Errorf("Failed take from queue: %s", err) @@ -719,15 +706,13 @@ func TestTtlQueue_Put(t *testing.T) { t.Fatalf("Failed put to queue: %s", err) } else if err == nil && task == nil { t.Fatalf("Task is nil after put") - } else { - if task.Data() != putData { - t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) - } + } else if task.Data() != putData { + t.Errorf("Task data after put not equal with example. %s != %s", task.Data(), putData) } time.Sleep(5 * time.Second) - //Take + // Take. task, err = q.TakeTimeout(2 * time.Second) if err != nil { t.Errorf("Failed take from queue: %s", err) @@ -819,7 +804,8 @@ func TestUtube_Put(t *testing.T) { timeSpent := math.Abs(float64(end.Sub(start) - 2*time.Second)) if timeSpent > float64(700*time.Millisecond) { - t.Fatalf("Blocking time is less than expected: actual = %.2fs, expected = 1s", end.Sub(start).Seconds()) + t.Fatalf("Blocking time is less than expected: actual = %.2fs, expected = 1s", + end.Sub(start).Seconds()) } } @@ -935,7 +921,8 @@ func runTestMain(m *testing.M) int { instances, err = test_helpers.StartTarantoolInstances(serversPool, nil, poolOpts) if err != nil { - log.Fatalf("Failed to prepare test tarantool pool: %s", err) + log.Printf("Failed to prepare test tarantool pool: %s", err) + return 1 } defer test_helpers.StopTarantoolInstances(instances) @@ -958,7 +945,8 @@ func runTestMain(m *testing.M) int { } if err != nil { - log.Fatalf("Failed to set roles in tarantool pool: %s", err) + log.Printf("Failed to set roles in tarantool pool: %s", err) + return 1 } return m.Run() } diff --git a/request.go b/request.go index 48c13e869..917c1fb6a 100644 --- a/request.go +++ b/request.go @@ -170,7 +170,7 @@ func fillPing(enc *msgpack.Encoder) error { // // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. -func (conn *Connection) Ping() (resp *Response, err error) { +func (conn *Connection) Ping() (*Response, error) { return conn.Do(NewPingRequest()).Get() } @@ -180,7 +180,8 @@ func (conn *Connection) Ping() (resp *Response, err error) { // // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. -func (conn *Connection) Select(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}) (resp *Response, err error) { +func (conn *Connection) Select(space, index interface{}, offset, limit uint32, iterator Iter, + key interface{}) (*Response, error) { return conn.SelectAsync(space, index, offset, limit, iterator, key).Get() } @@ -191,7 +192,7 @@ func (conn *Connection) Select(space, index interface{}, offset, limit uint32, i // // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. -func (conn *Connection) Insert(space interface{}, tuple interface{}) (resp *Response, err error) { +func (conn *Connection) Insert(space interface{}, tuple interface{}) (*Response, error) { return conn.InsertAsync(space, tuple).Get() } @@ -202,7 +203,7 @@ func (conn *Connection) Insert(space interface{}, tuple interface{}) (resp *Resp // // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. -func (conn *Connection) Replace(space interface{}, tuple interface{}) (resp *Response, err error) { +func (conn *Connection) Replace(space interface{}, tuple interface{}) (*Response, error) { return conn.ReplaceAsync(space, tuple).Get() } @@ -213,7 +214,7 @@ func (conn *Connection) Replace(space interface{}, tuple interface{}) (resp *Res // // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. -func (conn *Connection) Delete(space, index interface{}, key interface{}) (resp *Response, err error) { +func (conn *Connection) Delete(space, index interface{}, key interface{}) (*Response, error) { return conn.DeleteAsync(space, index, key).Get() } @@ -224,7 +225,7 @@ func (conn *Connection) Delete(space, index interface{}, key interface{}) (resp // // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. -func (conn *Connection) Update(space, index interface{}, key, ops interface{}) (resp *Response, err error) { +func (conn *Connection) Update(space, index interface{}, key, ops interface{}) (*Response, error) { return conn.UpdateAsync(space, index, key, ops).Get() } @@ -235,7 +236,7 @@ func (conn *Connection) Update(space, index interface{}, key, ops interface{}) ( // // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. -func (conn *Connection) Upsert(space interface{}, tuple, ops interface{}) (resp *Response, err error) { +func (conn *Connection) Upsert(space interface{}, tuple, ops interface{}) (*Response, error) { return conn.UpsertAsync(space, tuple, ops).Get() } @@ -246,7 +247,7 @@ func (conn *Connection) Upsert(space interface{}, tuple, ops interface{}) (resp // // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. -func (conn *Connection) Call(functionName string, args interface{}) (resp *Response, err error) { +func (conn *Connection) Call(functionName string, args interface{}) (*Response, error) { return conn.CallAsync(functionName, args).Get() } @@ -258,7 +259,7 @@ func (conn *Connection) Call(functionName string, args interface{}) (resp *Respo // // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. -func (conn *Connection) Call16(functionName string, args interface{}) (resp *Response, err error) { +func (conn *Connection) Call16(functionName string, args interface{}) (*Response, error) { return conn.Call16Async(functionName, args).Get() } @@ -269,7 +270,7 @@ func (conn *Connection) Call16(functionName string, args interface{}) (resp *Res // // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. -func (conn *Connection) Call17(functionName string, args interface{}) (resp *Response, err error) { +func (conn *Connection) Call17(functionName string, args interface{}) (*Response, error) { return conn.Call17Async(functionName, args).Get() } @@ -279,7 +280,7 @@ func (conn *Connection) Call17(functionName string, args interface{}) (resp *Res // // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. -func (conn *Connection) Eval(expr string, args interface{}) (resp *Response, err error) { +func (conn *Connection) Eval(expr string, args interface{}) (*Response, error) { return conn.EvalAsync(expr, args).Get() } @@ -290,7 +291,7 @@ func (conn *Connection) Eval(expr string, args interface{}) (resp *Response, err // // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. -func (conn *Connection) Execute(expr string, args interface{}) (resp *Response, err error) { +func (conn *Connection) Execute(expr string, args interface{}) (*Response, error) { return conn.ExecuteAsync(expr, args).Get() } @@ -310,7 +311,7 @@ func (s *single) DecodeMsgpack(d *msgpack.Decoder) error { return nil } if len != 1 { - return errors.New("Tarantool returns unexpected value for Select(limit=1)") + return errors.New("tarantool returns unexpected value for Select(limit=1)") } return d.Decode(s.res) } @@ -322,10 +323,10 @@ func (s *single) DecodeMsgpack(d *msgpack.Decoder) error { // // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. -func (conn *Connection) GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) { +func (conn *Connection) GetTyped(space, index interface{}, key interface{}, + result interface{}) error { s := single{res: result} - err = conn.SelectAsync(space, index, 0, 1, IterEq, key).GetTyped(&s) - return + return conn.SelectAsync(space, index, 0, 1, IterEq, key).GetTyped(&s) } // SelectTyped performs select to box space and fills typed result. @@ -334,7 +335,8 @@ func (conn *Connection) GetTyped(space, index interface{}, key interface{}, resu // // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. -func (conn *Connection) SelectTyped(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}, result interface{}) (err error) { +func (conn *Connection) SelectTyped(space, index interface{}, offset, limit uint32, iterator Iter, + key interface{}, result interface{}) error { return conn.SelectAsync(space, index, offset, limit, iterator, key).GetTyped(result) } @@ -345,7 +347,8 @@ func (conn *Connection) SelectTyped(space, index interface{}, offset, limit uint // // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. -func (conn *Connection) InsertTyped(space interface{}, tuple interface{}, result interface{}) (err error) { +func (conn *Connection) InsertTyped(space interface{}, tuple interface{}, + result interface{}) error { return conn.InsertAsync(space, tuple).GetTyped(result) } @@ -356,7 +359,8 @@ func (conn *Connection) InsertTyped(space interface{}, tuple interface{}, result // // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. -func (conn *Connection) ReplaceTyped(space interface{}, tuple interface{}, result interface{}) (err error) { +func (conn *Connection) ReplaceTyped(space interface{}, tuple interface{}, + result interface{}) error { return conn.ReplaceAsync(space, tuple).GetTyped(result) } @@ -366,7 +370,8 @@ func (conn *Connection) ReplaceTyped(space interface{}, tuple interface{}, resul // // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. -func (conn *Connection) DeleteTyped(space, index interface{}, key interface{}, result interface{}) (err error) { +func (conn *Connection) DeleteTyped(space, index interface{}, key interface{}, + result interface{}) error { return conn.DeleteAsync(space, index, key).GetTyped(result) } @@ -376,7 +381,8 @@ func (conn *Connection) DeleteTyped(space, index interface{}, key interface{}, r // // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. -func (conn *Connection) UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) (err error) { +func (conn *Connection) UpdateTyped(space, index interface{}, key, ops interface{}, + result interface{}) error { return conn.UpdateAsync(space, index, key, ops).GetTyped(result) } @@ -387,7 +393,8 @@ func (conn *Connection) UpdateTyped(space, index interface{}, key, ops interface // // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. -func (conn *Connection) CallTyped(functionName string, args interface{}, result interface{}) (err error) { +func (conn *Connection) CallTyped(functionName string, args interface{}, + result interface{}) error { return conn.CallAsync(functionName, args).GetTyped(result) } @@ -399,7 +406,8 @@ func (conn *Connection) CallTyped(functionName string, args interface{}, result // // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. -func (conn *Connection) Call16Typed(functionName string, args interface{}, result interface{}) (err error) { +func (conn *Connection) Call16Typed(functionName string, args interface{}, + result interface{}) error { return conn.Call16Async(functionName, args).GetTyped(result) } @@ -410,7 +418,8 @@ func (conn *Connection) Call16Typed(functionName string, args interface{}, resul // // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. -func (conn *Connection) Call17Typed(functionName string, args interface{}, result interface{}) (err error) { +func (conn *Connection) Call17Typed(functionName string, args interface{}, + result interface{}) error { return conn.Call17Async(functionName, args).GetTyped(result) } @@ -420,7 +429,7 @@ func (conn *Connection) Call17Typed(functionName string, args interface{}, resul // // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. -func (conn *Connection) EvalTyped(expr string, args interface{}, result interface{}) (err error) { +func (conn *Connection) EvalTyped(expr string, args interface{}, result interface{}) error { return conn.EvalAsync(expr, args).GetTyped(result) } @@ -431,7 +440,8 @@ func (conn *Connection) EvalTyped(expr string, args interface{}, result interfac // // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. -func (conn *Connection) ExecuteTyped(expr string, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error) { +func (conn *Connection) ExecuteTyped(expr string, args interface{}, + result interface{}) (SQLInfo, []ColumnMetaData, error) { fut := conn.ExecuteAsync(expr, args) err := fut.GetTyped(&result) return fut.resp.SQLInfo, fut.resp.MetaData, err @@ -441,7 +451,8 @@ func (conn *Connection) ExecuteTyped(expr string, args interface{}, result inter // // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. -func (conn *Connection) SelectAsync(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}) *Future { +func (conn *Connection) SelectAsync(space, index interface{}, offset, limit uint32, iterator Iter, + key interface{}) *Future { req := NewSelectRequest(space). Index(index). Offset(offset). @@ -497,7 +508,8 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface // // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. -func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *Future { +func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, + ops interface{}) *Future { req := NewUpsertRequest(space).Tuple(tuple) req.ops = ops return conn.Do(req) diff --git a/request_test.go b/request_test.go index a7e70ec65..f44b12d52 100644 --- a/request_test.go +++ b/request_test.go @@ -41,7 +41,8 @@ var validProtocolInfo ProtocolInfo = ProtocolInfo{ type ValidSchemeResolver struct { } -func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (spaceNo, indexNo uint32, err error) { +func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (uint32, uint32, error) { + var spaceNo, indexNo uint32 if s != nil { spaceNo = uint32(s.(int)) } else { diff --git a/response.go b/response.go index 897b6ce73..7ee314e04 100644 --- a/response.go +++ b/response.go @@ -257,12 +257,12 @@ func (resp *Response) decodeBody() (err error) { } // Tarantool may send only version >= 1 - if (serverProtocolInfo.Version != ProtocolVersion(0)) || (serverProtocolInfo.Features != nil) { + if serverProtocolInfo.Version != ProtocolVersion(0) || serverProtocolInfo.Features != nil { if serverProtocolInfo.Version == ProtocolVersion(0) { - return fmt.Errorf("No protocol version provided in Id response") + return fmt.Errorf("no protocol version provided in Id response") } if serverProtocolInfo.Features == nil { - return fmt.Errorf("No features provided in Id response") + return fmt.Errorf("no features provided in Id response") } resp.Data = []interface{}{serverProtocolInfo} } diff --git a/schema.go b/schema.go index af7dd21f5..714f51c5b 100644 --- a/schema.go +++ b/schema.go @@ -374,20 +374,21 @@ func (conn *Connection) loadSchema() (err error) { // ResolveSpaceIndex tries to resolve space and index numbers. // Note: s can be a number, string, or an object of Space type. // Note: i can be a number, string, or an object of Index type. -func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (spaceNo, indexNo uint32, err error) { - var space *Space - var index *Index - var ok bool +func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (uint32, uint32, error) { + var ( + spaceNo, indexNo uint32 + space *Space + index *Index + ok bool + ) switch s := s.(type) { case string: if schema == nil { - err = fmt.Errorf("Schema is not loaded") - return + return spaceNo, indexNo, fmt.Errorf("Schema is not loaded") } if space, ok = schema.Spaces[s]; !ok { - err = fmt.Errorf("there is no space with name %s", s) - return + return spaceNo, indexNo, fmt.Errorf("there is no space with name %s", s) } spaceNo = space.Id case uint: @@ -395,7 +396,7 @@ func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (spaceNo, case uint64: spaceNo = uint32(s) case uint32: - spaceNo = uint32(s) + spaceNo = s case uint16: spaceNo = uint32(s) case uint8: @@ -422,18 +423,16 @@ func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (spaceNo, switch i := i.(type) { case string: if schema == nil { - err = fmt.Errorf("Schema is not loaded") - return + return spaceNo, indexNo, fmt.Errorf("schema is not loaded") } if space == nil { if space, ok = schema.SpacesById[spaceNo]; !ok { - err = fmt.Errorf("there is no space with id %d", spaceNo) - return + return spaceNo, indexNo, fmt.Errorf("there is no space with id %d", spaceNo) } } if index, ok = space.Indexes[i]; !ok { - err = fmt.Errorf("space %s has not index with name %s", space.Name, i) - return + err := fmt.Errorf("space %s has not index with name %s", space.Name, i) + return spaceNo, indexNo, err } indexNo = index.Id case uint: @@ -441,7 +440,7 @@ func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (spaceNo, case uint64: indexNo = uint32(i) case uint32: - indexNo = uint32(i) + indexNo = i case uint16: indexNo = uint32(i) case uint8: @@ -465,5 +464,5 @@ func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (spaceNo, } } - return + return spaceNo, indexNo, nil } diff --git a/settings/request.go b/settings/request.go index ae54aebf3..02252fe47 100644 --- a/settings/request.go +++ b/settings/request.go @@ -54,7 +54,8 @@ // // See also: // -// * Session settings https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/_session_settings/ +// - Session settings: +// https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/_session_settings/ package settings import ( diff --git a/settings/request_test.go b/settings/request_test.go index 477795d2d..2438f81cc 100644 --- a/settings/request_test.go +++ b/settings/request_test.go @@ -16,7 +16,8 @@ import ( type ValidSchemeResolver struct { } -func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (spaceNo, indexNo uint32, err error) { +func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (uint32, uint32, error) { + var spaceNo, indexNo uint32 if s == nil { if s == "_session_settings" { spaceNo = 380 @@ -43,7 +44,8 @@ func TestRequestsAPI(t *testing.T) { async bool rtype iproto.Type }{ - {req: NewErrorMarshalingEnabledSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, + {req: NewErrorMarshalingEnabledSetRequest(false), async: false, + rtype: iproto.IPROTO_UPDATE}, {req: NewErrorMarshalingEnabledGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, {req: NewSQLDefaultEngineSetRequest("memtx"), async: false, rtype: iproto.IPROTO_UPDATE}, {req: NewSQLDefaultEngineGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, @@ -57,8 +59,10 @@ func TestRequestsAPI(t *testing.T) { {req: NewSQLParserDebugGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, {req: NewSQLRecursiveTriggersSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, {req: NewSQLRecursiveTriggersGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, - {req: NewSQLReverseUnorderedSelectsSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, - {req: NewSQLReverseUnorderedSelectsGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, + {req: NewSQLReverseUnorderedSelectsSetRequest(false), async: false, + rtype: iproto.IPROTO_UPDATE}, + {req: NewSQLReverseUnorderedSelectsGetRequest(), async: false, + rtype: iproto.IPROTO_SELECT}, {req: NewSQLSelectDebugSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, {req: NewSQLSelectDebugGetRequest(), async: false, rtype: iproto.IPROTO_SELECT}, {req: NewSQLVDBEDebugSetRequest(false), async: false, rtype: iproto.IPROTO_UPDATE}, diff --git a/settings/tarantool_test.go b/settings/tarantool_test.go index 128e84328..645d48c73 100644 --- a/settings/tarantool_test.go +++ b/settings/tarantool_test.go @@ -175,7 +175,8 @@ func TestSQLDeferForeignKeysSetting(t *testing.T) { require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) // Create a space with reference to the parent space. - exec = tarantool.NewExecuteRequest("CREATE TABLE child(id INT PRIMARY KEY, x INT REFERENCES parent(y));") + exec = tarantool.NewExecuteRequest( + "CREATE TABLE child(id INT PRIMARY KEY, x INT REFERENCES parent(y));") resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) @@ -440,7 +441,8 @@ func TestSQLRecursiveTriggersSetting(t *testing.T) { exec = tarantool.NewExecuteRequest("UPDATE rec SET a=a+1, b=b+1;") _, err = conn.Do(exec).Get() require.NotNil(t, err) - require.ErrorContains(t, err, "Failed to execute SQL statement: too many levels of trigger recursion") + require.ErrorContains(t, err, + "Failed to execute SQL statement: too many levels of trigger recursion") // Disable SQL recursive triggers. resp, err = conn.Do(NewSQLRecursiveTriggersSetRequest(false)).Get() @@ -495,13 +497,15 @@ func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { resp, err = conn.Do(NewSQLReverseUnorderedSelectsSetRequest(false)).Get() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", false}}, + resp.Data) // Fetch current setting value. resp, err = conn.Do(NewSQLReverseUnorderedSelectsGetRequest()).Get() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", false}}, + resp.Data) // Select multiple records. query := "SELECT * FROM seqscan data;" @@ -521,13 +525,15 @@ func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { resp, err = conn.Do(NewSQLReverseUnorderedSelectsSetRequest(true)).Get() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", true}}, + resp.Data) // Fetch current setting value. resp, err = conn.Do(NewSQLReverseUnorderedSelectsGetRequest()).Get() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", true}}, + resp.Data) // Select multiple records. resp, err = conn.Do(tarantool.NewExecuteRequest(query)).Get() diff --git a/shutdown_test.go b/shutdown_test.go index 7a6843811..bb4cfa099 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -204,7 +204,8 @@ func TestGracefulShutdownWithReconnect(t *testing.T) { err = test_helpers.RestartTarantool(&inst) require.Nilf(t, err, "Failed to restart tarantool") - connected := test_helpers.WaitUntilReconnected(conn, shtdnClntOpts.MaxReconnects, shtdnClntOpts.Reconnect) + connected := test_helpers.WaitUntilReconnected(conn, shtdnClntOpts.MaxReconnects, + shtdnClntOpts.Reconnect) require.Truef(t, connected, "Reconnect success") testGracefulShutdown(t, conn, &inst) diff --git a/smallbuf.go b/smallbuf.go index a5b926835..a6590b409 100644 --- a/smallbuf.go +++ b/smallbuf.go @@ -34,7 +34,7 @@ func (s *smallBuf) ReadByte() (b byte, err error) { func (s *smallBuf) UnreadByte() error { if s.p == 0 { - return errors.New("Could not unread") + return errors.New("could not unread") } s.p-- return nil diff --git a/ssl_test.go b/ssl_test.go index 58d03fa73..12cab7304 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -66,7 +66,7 @@ func clientSsl(network, address string, opts SslOpts) (net.Conn, error) { } func createClientServerSsl(t testing.TB, serverOpts, - clientOpts SslOpts) (net.Listener, net.Conn, error, <-chan string, <-chan error) { + clientOpts SslOpts) (net.Listener, net.Conn, <-chan string, <-chan error, error) { t.Helper() l, err := serverSsl("tcp", sslHost+":0", serverOpts) @@ -79,14 +79,14 @@ func createClientServerSsl(t testing.TB, serverOpts, port := l.Addr().(*net.TCPAddr).Port c, err := clientSsl("tcp", sslHost+":"+strconv.Itoa(port), clientOpts) - return l, c, err, msgs, errs + return l, c, msgs, errs, err } func createClientServerSslOk(t testing.TB, serverOpts, clientOpts SslOpts) (net.Listener, net.Conn, <-chan string, <-chan error) { t.Helper() - l, c, err, msgs, errs := createClientServerSsl(t, serverOpts, clientOpts) + l, c, msgs, errs, err := createClientServerSsl(t, serverOpts, clientOpts) if err != nil { t.Fatalf("Unable to create client, error %q", err.Error()) } @@ -142,7 +142,7 @@ func serverTntStop(inst test_helpers.TarantoolInstance) { func assertConnectionSslFail(t testing.TB, serverOpts, clientOpts SslOpts) { t.Helper() - l, c, err, _, _ := createClientServerSsl(t, serverOpts, clientOpts) + l, c, _, _, err := createClientServerSsl(t, serverOpts, clientOpts) l.Close() if err == nil { c.Close() diff --git a/stream.go b/stream.go index 939840d19..5144ea6f1 100644 --- a/stream.go +++ b/stream.go @@ -2,7 +2,7 @@ package tarantool import ( "context" - "fmt" + "errors" "time" "github.com/tarantool/go-iproto" @@ -25,6 +25,11 @@ const ( BestEffortLevel TxnIsolationLevel = 3 ) +var ( + errUnknownStreamRequest = errors.New("the passed connected request doesn't belong " + + "to the current connection or connection pool") +) + type Stream struct { Id uint64 Conn *Connection @@ -195,7 +200,7 @@ func (s *Stream) Do(req Request) *Future { if connectedReq, ok := req.(ConnectedRequest); ok { if connectedReq.Conn() != s.Conn { fut := NewFuture() - fut.SetError(fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool")) + fut.SetError(errUnknownStreamRequest) return fut } } diff --git a/tarantool_test.go b/tarantool_test.go index d58a6775f..6339164f1 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -196,7 +196,8 @@ func BenchmarkClientSerialSQL(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", []interface{}{uint(1111)}) + _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", + []interface{}{uint(1111)}) if err != nil { b.Errorf("Select failed: %s", err.Error()) break @@ -614,7 +615,8 @@ func BenchmarkClientLargeSelectParallel(b *testing.B) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _, err := conn.Select(rSpaceNo, rIndexNo, offset, limit, IterEq, []interface{}{"test_name"}) + _, err := conn.Select(rSpaceNo, rIndexNo, offset, limit, IterEq, + []interface{}{"test_name"}) if err != nil { b.Fatal(err) } @@ -636,7 +638,8 @@ func BenchmarkClientParallelSQL(b *testing.B) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", []interface{}{uint(1111)}) + _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", + []interface{}{uint(1111)}) if err != nil { b.Errorf("Select failed: %s", err.Error()) break @@ -692,7 +695,8 @@ func BenchmarkSQLSerial(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", []interface{}{uint(1111)}) + _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", + []interface{}{uint(1111)}) if err != nil { b.Errorf("Select failed: %s", err.Error()) break @@ -909,7 +913,6 @@ func TestClient(t *testing.T) { t.Errorf("Unexpected body of Insert (1)") } } - //resp, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) resp, err = conn.Insert(spaceNo, &Tuple{Id: 1, Msg: "hello", Name: "world"}) if tntErr, ok := err.(Error); !ok || tntErr.Code != iproto.ER_TUPLE_FOUND { t.Errorf("Expected %s but got: %v", iproto.ER_TUPLE_FOUND, err) @@ -995,7 +998,8 @@ func TestClient(t *testing.T) { } // Update - resp, err = conn.Update(spaceNo, indexNo, []interface{}{uint(2)}, []interface{}{[]interface{}{"=", 1, "bye"}, []interface{}{"#", 2, 1}}) + resp, err = conn.Update(spaceNo, indexNo, []interface{}{uint(2)}, + []interface{}{[]interface{}{"=", 1, "bye"}, []interface{}{"#", 2, 1}}) if err != nil { t.Fatalf("Failed to Update: %s", err.Error()) } @@ -1023,7 +1027,8 @@ func TestClient(t *testing.T) { } // Upsert - resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, + []interface{}{[]interface{}{"+", 1, 1}}) if err != nil { t.Fatalf("Failed to Upsert (insert): %s", err.Error()) } @@ -1033,7 +1038,8 @@ func TestClient(t *testing.T) { if resp.Pos != nil { t.Errorf("Response should not have a position") } - resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, + []interface{}{[]interface{}{"+", 1, 1}}) if err != nil { t.Fatalf("Failed to Upsert (update): %s", err.Error()) } @@ -1101,10 +1107,8 @@ func TestClient(t *testing.T) { } if len(tpl) != 1 { t.Errorf("Result len of SelectTyped != 1") - } else { - if tpl[0].Id != 10 { - t.Errorf("Bad value loaded from SelectTyped") - } + } else if tpl[0].Id != 10 { + t.Errorf("Bad value loaded from SelectTyped") } // Get Typed @@ -1125,10 +1129,8 @@ func TestClient(t *testing.T) { } if len(tpl) != 1 { t.Errorf("Result len of SelectTyped != 1") - } else { - if tpl[0].Id != 10 { - t.Errorf("Bad value loaded from SelectTyped") - } + } else if tpl[0].Id != 10 { + t.Errorf("Bad value loaded from SelectTyped") } // Get Typed Empty @@ -1280,12 +1282,14 @@ func TestClientSessionPush(t *testing.T) { } if resp.Code == PushCode { pushCnt += 1 - if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != pushCnt { + val, err := test_helpers.ConvertUint64(resp.Data[0]) + if err != nil || val != pushCnt { t.Errorf("Unexpected push data = %v", resp.Data) } } else { respCnt += 1 - if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != pushMax { + val, err := test_helpers.ConvertUint64(resp.Data[0]) + if err != nil || val != pushMax { t.Errorf("Result is not %d: %v", pushMax, resp.Data) } } @@ -1309,7 +1313,9 @@ func TestClientSessionPush(t *testing.T) { resp, err := fut.Get() if err != nil { t.Errorf("Unable to call fut.Get(): %s", err) - } else if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != pushMax { + } + val, err := test_helpers.ConvertUint64(resp.Data[0]) + if err != nil || val != pushMax { t.Errorf("Result is not %d: %v", pushMax, resp.Data) } @@ -1326,7 +1332,8 @@ func TestClientSessionPush(t *testing.T) { } const ( - createTableQuery = "CREATE TABLE SQL_SPACE (id STRING PRIMARY KEY, name STRING COLLATE \"unicode\" DEFAULT NULL);" + createTableQuery = "CREATE TABLE SQL_SPACE (id STRING PRIMARY KEY, name " + + "STRING COLLATE \"unicode\" DEFAULT NULL);" insertQuery = "INSERT INTO SQL_SPACE VALUES (?, ?);" selectNamedQuery = "SELECT id, name FROM SQL_SPACE WHERE id=:id AND name=:name;" selectPosQuery = "SELECT id, name FROM SQL_SPACE WHERE id=? AND name=?;" @@ -1429,7 +1436,8 @@ func TestSQL(t *testing.T) { selectSpanDifQuery, []interface{}{"test_test"}, Response{ - SQLInfo: SQLInfo{AffectedCount: 0}, Data: []interface{}{[]interface{}{"11", "test_test", "1"}}, + SQLInfo: SQLInfo{AffectedCount: 0}, + Data: []interface{}{[]interface{}{"11", "test_test", "1"}}, MetaData: []ColumnMetaData{ { FieldType: "string", @@ -1513,13 +1521,17 @@ func TestSQL(t *testing.T) { for j := range resp.Data { assert.Equal(t, resp.Data[j], test.Resp.Data[j], "Response data is wrong") } - assert.Equal(t, resp.SQLInfo.AffectedCount, test.Resp.SQLInfo.AffectedCount, "Affected count is wrong") + assert.Equal(t, resp.SQLInfo.AffectedCount, test.Resp.SQLInfo.AffectedCount, + "Affected count is wrong") errorMsg := "Response Metadata is wrong" for j := range resp.MetaData { - assert.Equal(t, resp.MetaData[j].FieldIsAutoincrement, test.Resp.MetaData[j].FieldIsAutoincrement, errorMsg) - assert.Equal(t, resp.MetaData[j].FieldIsNullable, test.Resp.MetaData[j].FieldIsNullable, errorMsg) - assert.Equal(t, resp.MetaData[j].FieldCollation, test.Resp.MetaData[j].FieldCollation, errorMsg) + assert.Equal(t, resp.MetaData[j].FieldIsAutoincrement, + test.Resp.MetaData[j].FieldIsAutoincrement, errorMsg) + assert.Equal(t, resp.MetaData[j].FieldIsNullable, + test.Resp.MetaData[j].FieldIsNullable, errorMsg) + assert.Equal(t, resp.MetaData[j].FieldCollation, + test.Resp.MetaData[j].FieldCollation, errorMsg) assert.Equal(t, resp.MetaData[j].FieldName, test.Resp.MetaData[j].FieldName, errorMsg) assert.Equal(t, resp.MetaData[j].FieldSpan, test.Resp.MetaData[j].FieldSpan, errorMsg) assert.Equal(t, resp.MetaData[j].FieldType, test.Resp.MetaData[j].FieldType, errorMsg) @@ -1591,7 +1603,7 @@ func TestSQLBindings(t *testing.T) { sqlBind4, } - //positioned sql bind + // positioned sql bind sqlBind5 := []interface{}{ 1, "test", } @@ -1831,7 +1843,8 @@ func TestNewPrepared(t *testing.T) { } func TestConnection_DoWithStrangerConn(t *testing.T) { - expectedErr := fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool") + expectedErr := fmt.Errorf("the passed connected request doesn't belong to the current" + + " connection or connection pool") conn1 := &Connection{} req := test_helpers.NewStrangerRequest() @@ -2014,7 +2027,8 @@ func TestSchema(t *testing.T) { if ifield1.Id != 1 || ifield2.Id != 2 { t.Errorf("index field has incorrect Id") } - if (ifield1.Type != "num" && ifield1.Type != "unsigned") || (ifield2.Type != "STR" && ifield2.Type != "string") { + if (ifield1.Type != "num" && ifield1.Type != "unsigned") || + (ifield2.Type != "STR" && ifield2.Type != "string") { t.Errorf("index field has incorrect Type '%s'", ifield2.Type) } @@ -2113,7 +2127,10 @@ func TestClientNamed(t *testing.T) { } // Update - resp, err = conn.Update(spaceName, indexName, []interface{}{uint(1002)}, []interface{}{[]interface{}{"=", 1, "bye"}, []interface{}{"#", 2, 1}}) + resp, err = conn.Update(spaceName, indexName, + []interface{}{ + uint(1002)}, + []interface{}{[]interface{}{"=", 1, "bye"}, []interface{}{"#", 2, 1}}) if err != nil { t.Fatalf("Failed to Update: %s", err.Error()) } @@ -2122,14 +2139,16 @@ func TestClientNamed(t *testing.T) { } // Upsert - resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + resp, err = conn.Upsert(spaceName, + []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) if err != nil { t.Fatalf("Failed to Upsert (insert): %s", err.Error()) } if resp == nil { t.Errorf("Response is nil after Upsert (insert)") } - resp, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + resp, err = conn.Upsert(spaceName, + []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) if err != nil { t.Fatalf("Failed to Upsert (update): %s", err.Error()) } @@ -2139,7 +2158,8 @@ func TestClientNamed(t *testing.T) { // Select for i := 1010; i < 1020; i++ { - resp, err = conn.Replace(spaceName, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) + resp, err = conn.Replace(spaceName, + []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) if err != nil { t.Fatalf("Failed to Replace: %s", err.Error()) } @@ -2795,7 +2815,9 @@ func TestComplexStructs(t *testing.T) { return } - if tuple.Cid != tuples[0].Cid || len(tuple.Members) != len(tuples[0].Members) || tuple.Members[1].Name != tuples[0].Members[1].Name { + if tuple.Cid != tuples[0].Cid || + len(tuple.Members) != len(tuples[0].Members) || + tuple.Members[1].Name != tuples[0].Members[1].Name { t.Errorf("Failed to selectTyped: incorrect data") return } @@ -3437,7 +3459,8 @@ func TestConnectionProtocolFeatureRequirementFail(t *testing.T) { require.Nilf(t, conn, "Connect fail") require.NotNilf(t, err, "Got error on connect") - require.Contains(t, err.Error(), "invalid server protocol: protocol feature TransactionsFeature is not supported") + require.Contains(t, err.Error(), + "invalid server protocol: protocol feature TransactionsFeature is not supported") } func TestConnectionProtocolFeatureRequirementManyFail(t *testing.T) { @@ -3454,7 +3477,8 @@ func TestConnectionProtocolFeatureRequirementManyFail(t *testing.T) { require.NotNilf(t, err, "Got error on connect") require.Contains(t, err.Error(), - "invalid server protocol: protocol features TransactionsFeature, Unknown feature (code 15532) are not supported") + "invalid server protocol: protocol features TransactionsFeature, "+ + "Unknown feature (code 15532) are not supported") } func TestConnectionFeatureOptsImmutable(t *testing.T) { @@ -3466,7 +3490,8 @@ func TestConnectionFeatureOptsImmutable(t *testing.T) { defer test_helpers.StopTarantoolWithCleanup(inst) if err != nil { - log.Fatalf("Failed to prepare test tarantool: %s", err) + log.Printf("Failed to prepare test tarantool: %s", err) + return } retries := uint(10) @@ -4012,7 +4037,8 @@ func runTestMain(m *testing.M) int { defer test_helpers.StopTarantoolWithCleanup(inst) if err != nil { - log.Fatalf("Failed to prepare test tarantool: %s", err) + log.Printf("Failed to prepare test tarantool: %s", err) + return 1 } return m.Run() diff --git a/test_helpers/main.go b/test_helpers/main.go index c777f2f26..894ebb653 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -102,7 +102,7 @@ func isReady(server string, opts *tarantool.Opts) error { return err } if conn == nil { - return errors.New("Conn is nil after connect") + return errors.New("connection is nil after connect") } defer conn.Close() @@ -111,7 +111,7 @@ func isReady(server string, opts *tarantool.Opts) error { return err } if resp == nil { - return errors.New("Response is nil after ping") + return errors.New("response is nil after ping") } return nil @@ -386,7 +386,7 @@ func ConvertUint64(v interface{}) (result uint64, err error) { case uint32: result = uint64(v) case uint64: - result = uint64(v) + result = v case int: result = uint64(v) case int8: @@ -398,7 +398,7 @@ func ConvertUint64(v interface{}) (result uint64, err error) { case int64: result = uint64(v) default: - err = fmt.Errorf("Non-number value %T", v) + err = fmt.Errorf("non-number value %T", v) } return } diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index 7dcd70cbc..c44df2f6a 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -26,12 +26,12 @@ type CheckStatusesArgs struct { func compareTuples(expectedTpl []interface{}, actualTpl []interface{}) error { if len(actualTpl) != len(expectedTpl) { - return fmt.Errorf("Unexpected body of Insert (tuple len)") + return fmt.Errorf("unexpected body of Insert (tuple len)") } for i, field := range actualTpl { if field != expectedTpl[i] { - return fmt.Errorf("Unexpected field, expected: %v actual: %v", expectedTpl[i], field) + return fmt.Errorf("unexpected field, expected: %v actual: %v", expectedTpl[i], field) } } @@ -104,7 +104,8 @@ func ProcessListenOnInstance(args interface{}) error { equal := reflect.DeepEqual(actualPorts, listenArgs.ExpectedPorts) if !equal { - return fmt.Errorf("expected ports: %v, actual ports: %v", actualPorts, listenArgs.ExpectedPorts) + return fmt.Errorf("expected ports: %v, actual ports: %v", + actualPorts, listenArgs.ExpectedPorts) } return nil @@ -129,10 +130,11 @@ func Retry(f func(interface{}) error, args interface{}, count int, timeout time. return err } -func InsertOnInstance(server string, connOpts tarantool.Opts, space interface{}, tuple interface{}) error { +func InsertOnInstance(server string, connOpts tarantool.Opts, space interface{}, + tuple interface{}) error { conn, err := tarantool.Connect(server, connOpts) if err != nil { - return fmt.Errorf("Fail to connect to %s: %s", server, err.Error()) + return fmt.Errorf("fail to connect to %s: %s", server, err.Error()) } if conn == nil { return fmt.Errorf("conn is nil after Connect") @@ -141,20 +143,20 @@ func InsertOnInstance(server string, connOpts tarantool.Opts, space interface{}, resp, err := conn.Do(tarantool.NewInsertRequest(space).Tuple(tuple)).Get() if err != nil { - return fmt.Errorf("Failed to Insert: %s", err.Error()) + return fmt.Errorf("failed to Insert: %s", err.Error()) } if resp == nil { - return fmt.Errorf("Response is nil after Insert") + return fmt.Errorf("response is nil after Insert") } if len(resp.Data) != 1 { - return fmt.Errorf("Response Body len != 1") + return fmt.Errorf("response Body len != 1") } if tpl, ok := resp.Data[0].([]interface{}); !ok { - return fmt.Errorf("Unexpected body of Insert") + return fmt.Errorf("unexpected body of Insert") } else { expectedTpl, ok := tuple.([]interface{}) if !ok { - return fmt.Errorf("Failed to cast") + return fmt.Errorf("failed to cast") } err = compareTuples(expectedTpl, tpl) @@ -166,7 +168,8 @@ func InsertOnInstance(server string, connOpts tarantool.Opts, space interface{}, return nil } -func InsertOnInstances(servers []string, connOpts tarantool.Opts, space interface{}, tuple interface{}) error { +func InsertOnInstances(servers []string, connOpts tarantool.Opts, space interface{}, + tuple interface{}) error { serversNumber := len(servers) roles := make([]bool, serversNumber) for i := 0; i < serversNumber; i++ { @@ -220,7 +223,8 @@ func SetClusterRO(servers []string, connOpts tarantool.Opts, roles []bool) error return nil } -func StartTarantoolInstances(servers []string, workDirs []string, opts StartOpts) ([]TarantoolInstance, error) { +func StartTarantoolInstances(servers []string, workDirs []string, + opts StartOpts) ([]TarantoolInstance, error) { isUserWorkDirs := (workDirs != nil) if isUserWorkDirs && (len(servers) != len(workDirs)) { return nil, fmt.Errorf("number of servers should be equal to number of workDirs") diff --git a/uuid/uuid.go b/uuid/uuid.go index eadd42ae1..cc2be736f 100644 --- a/uuid/uuid.go +++ b/uuid/uuid.go @@ -6,11 +6,14 @@ // // # See also // -// * Tarantool commit with UUID support https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5 +// - Tarantool commit with UUID support: +// https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5 // -// * Tarantool data model https://www.tarantool.io/en/doc/latest/book/box/data_model/ +// - Tarantool data model: +// https://www.tarantool.io/en/doc/latest/book/box/data_model/ // -// * Module UUID https://www.tarantool.io/en/doc/latest/reference/reference_lua/uuid/ +// - Module UUID: +// https://www.tarantool.io/en/doc/latest/reference/reference_lua/uuid/ package uuid import ( @@ -41,7 +44,7 @@ func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { } func decodeUUID(d *msgpack.Decoder, v reflect.Value) error { - var bytesCount int = 16 + var bytesCount = 16 bytes := make([]byte, bytesCount) n, err := d.Buffered().Read(bytes) diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index 84abd42d2..94baaa6ea 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -177,7 +177,8 @@ func runTestMain(m *testing.M) int { defer test_helpers.StopTarantoolWithCleanup(inst) if err != nil { - log.Fatalf("Failed to prepare test tarantool: %s", err) + log.Printf("Failed to prepare test tarantool: %s", err) + return 1 } return m.Run() From dbfaab5078b5313bf99eaef661dcdf74a4a5f547 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 27 Jun 2023 16:22:52 +0300 Subject: [PATCH 468/605] pool: fix race condition at GetNextConnection() The `r.current` value can be changed by concurrent threads because the change happens under read-lock. We could use the atomic counter for a current connection number to avoid the race condition. Closes #309 --- CHANGELOG.md | 1 + pool/connection_pool_test.go | 29 +++++++++++++++++++++++++++++ pool/round_robin.go | 14 +++++++------- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c449d98ea..6f025e122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed - Flaky decimal/TestSelect (#300) +- Race condition at roundRobinStrategy.GetNextConnection() (#309) ## [1.12.0] - 2023-06-07 diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index c34e71673..dd0210d62 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -2231,6 +2231,35 @@ func TestDo(t *testing.T) { require.NotNilf(t, resp, "response is nil after Ping") } +func TestDo_concurrent(t *testing.T) { + roles := []bool{true, true, false, true, false} + + err := test_helpers.SetClusterRO(servers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + connPool, err := pool.Connect(servers, connOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + req := tarantool.NewPingRequest() + const concurrency = 100 + var wg sync.WaitGroup + wg.Add(concurrency) + + for i := 0; i < concurrency; i++ { + go func() { + defer wg.Done() + + _, err := connPool.Do(req, pool.ANY).Get() + assert.Nil(t, err) + }() + } + + wg.Wait() +} + func TestNewPrepared(t *testing.T) { test_helpers.SkipIfSQLUnsupported(t) diff --git a/pool/round_robin.go b/pool/round_robin.go index 1c50b97d9..c3400d371 100644 --- a/pool/round_robin.go +++ b/pool/round_robin.go @@ -2,6 +2,7 @@ package pool import ( "sync" + "sync/atomic" "github.com/tarantool/go-tarantool/v2" ) @@ -10,8 +11,8 @@ type roundRobinStrategy struct { conns []*tarantool.Connection indexByAddr map[string]uint mutex sync.RWMutex - size uint - current uint + size uint64 + current uint64 } func newRoundRobinStrategy(size int) *roundRobinStrategy { @@ -98,13 +99,12 @@ func (r *roundRobinStrategy) AddConn(addr string, conn *tarantool.Connection) { r.conns[idx] = conn } else { r.conns = append(r.conns, conn) - r.indexByAddr[addr] = r.size + r.indexByAddr[addr] = uint(r.size) r.size += 1 } } -func (r *roundRobinStrategy) nextIndex() uint { - ret := r.current % r.size - r.current++ - return ret +func (r *roundRobinStrategy) nextIndex() uint64 { + next := atomic.AddUint64(&r.current, 1) + return (next - 1) % r.size } From f9a51e05113b5041af9ad94b5fe7f7de6a96442e Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 27 Jun 2023 12:49:11 +0300 Subject: [PATCH 469/605] api: meaningful read/write networt error We need to add meaningful error descriptions for a read/write socket errors. Part of #129 --- CHANGELOG.md | 1 + connection.go | 20 ++++++++++++++++++++ errors.go | 3 ++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f025e122..c443a4ce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Enumeration types for RLimitAction/iterators (#158) - IsNullable flag for Field (#302) - More linters on CI (#310) +- Meaningful description for read/write socket errors (#129) ### Changed diff --git a/connection.go b/connection.go index 367cf9640..37de22e82 100644 --- a/connection.go +++ b/connection.go @@ -789,6 +789,10 @@ func (conn *Connection) writer(w writeFlusher, c Conn) { runtime.Gosched() if len(conn.dirtyShard) == 0 { if err := w.Flush(); err != nil { + err = ClientError{ + ErrIoError, + fmt.Sprintf("failed to flush data to the connection: %s", err), + } conn.reconnect(err, c) return } @@ -812,6 +816,10 @@ func (conn *Connection) writer(w writeFlusher, c Conn) { continue } if _, err := w.Write(packet.b); err != nil { + err = ClientError{ + ErrIoError, + fmt.Sprintf("failed to write data to the connection: %s", err), + } conn.reconnect(err, c) return } @@ -868,12 +876,20 @@ func (conn *Connection) reader(r io.Reader, c Conn) { for atomic.LoadUint32(&conn.state) != connClosed { respBytes, err := read(r, conn.lenbuf[:]) if err != nil { + err = ClientError{ + ErrIoError, + fmt.Sprintf("failed to read data from the connection: %s", err), + } conn.reconnect(err, c) return } resp := &Response{buf: smallBuf{b: respBytes}} err = resp.decodeHeader(conn.dec) if err != nil { + err = ClientError{ + ErrProtocolError, + fmt.Sprintf("failed to decode IPROTO header: %s", err), + } conn.reconnect(err, c) return } @@ -883,6 +899,10 @@ func (conn *Connection) reader(r io.Reader, c Conn) { if event, err := readWatchEvent(&resp.buf); err == nil { events <- event } else { + err = ClientError{ + ErrProtocolError, + fmt.Sprintf("failed to decode IPROTO_EVENT: %s", err), + } conn.opts.Logger.Report(LogWatchEventReadFailed, conn, err) } continue diff --git a/errors.go b/errors.go index ab40e4f63..60f71a2b1 100644 --- a/errors.go +++ b/errors.go @@ -45,7 +45,7 @@ func (clierr ClientError) Error() string { // - request is aborted due to rate limit func (clierr ClientError) Temporary() bool { switch clierr.Code { - case ErrConnectionNotReady, ErrTimeouted, ErrRateLimited: + case ErrConnectionNotReady, ErrTimeouted, ErrRateLimited, ErrIoError: return true default: return false @@ -60,4 +60,5 @@ const ( ErrTimeouted = 0x4000 + iota ErrRateLimited = 0x4000 + iota ErrConnectionShutdown = 0x4000 + iota + ErrIoError = 0x4000 + iota ) From 7cc61fd2bca21482068fda913aa19d7d5b57b021 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 27 Jun 2023 13:27:50 +0300 Subject: [PATCH 470/605] doc: add Connection.Do result processing examples - ExampleConnection_Do demonstrates how to process a result. - ExampleConnection_Do_failure demonstrates how to process a request failure. Closes #128 --- example_test.go | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/example_test.go b/example_test.go index e2a645937..d871d578a 100644 --- a/example_test.go +++ b/example_test.go @@ -998,6 +998,75 @@ func ExampleSpace() { // SpaceField 2 name3 unsigned } +// ExampleConnection_Do demonstrates how to send a request and process +// a response. +func ExampleConnection_Do() { + conn := exampleConnect(opts) + defer conn.Close() + + // It could be any request. + req := tarantool.NewReplaceRequest("test"). + Tuple([]interface{}{int(1111), "foo", "bar"}) + + // We got a future, the request actually not performed yet. + future := conn.Do(req) + + // When the future receives the response, the result of the Future is set + // and becomes available. We could wait for that moment with Future.Get() + // or Future.GetTyped() methods. + resp, err := future.Get() + if err != nil { + fmt.Printf("Failed to execute the request: %s\n", err) + } else { + fmt.Println(resp.Data) + } + + // Output: + // [[1111 foo bar]] +} + +// ExampleConnection_Do_failure demonstrates how to send a request and process +// failure. +func ExampleConnection_Do_failure() { + conn := exampleConnect(opts) + defer conn.Close() + + // It could be any request. + req := tarantool.NewCallRequest("not_exist") + + // We got a future, the request actually not performed yet. + future := conn.Do(req) + + // When the future receives the response, the result of the Future is set + // and becomes available. We could wait for that moment with Future.Get() + // or Future.GetTyped() methods. + resp, err := future.Get() + if err != nil { + // We don't print the error here to keep the example reproducible. + // fmt.Printf("Failed to execute the request: %s\n", err) + if resp == nil { + // Something happens in a client process (timeout, IO error etc). + fmt.Printf("Resp == nil, ClientErr = %s\n", err.(tarantool.ClientError)) + } else { + // Response exist. So it could be a Tarantool error or a decode + // error. We need to check the error code. + fmt.Printf("Error code from the response: %d\n", resp.Code) + if resp.Code == tarantool.OkCode { + fmt.Printf("Decode error: %s\n", err) + } else { + code := err.(tarantool.Error).Code + fmt.Printf("Error code from the error: %d\n", code) + fmt.Printf("Error short from the error: %s\n", code) + } + } + } + + // Output: + // Error code from the response: 33 + // Error code from the error: 33 + // Error short from the error: ER_NO_SUCH_PROC +} + // To use prepared statements to query a tarantool instance, call NewPrepared. func ExampleConnection_NewPrepared() { // Tarantool supports SQL since version 2.0.0 From 3aeb8c28512adbfd54443cbc7aa7975d1f936bea Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 28 Jun 2023 11:53:27 +0300 Subject: [PATCH 471/605] decimal: incorrect MP_DECIMAL decoding with scale < 0 The `scale` value in `MP_DECIMAL` may be negative [1]. We need to handle the case. 1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type --- CHANGELOG.md | 1 + decimal/bcd.go | 44 ++++++++++++++---------------------- decimal/decimal.go | 8 +++++-- decimal/decimal_test.go | 49 ++++++++++++++++++++++++++++++++++++++++- decimal/export_test.go | 2 +- decimal/fuzzing_test.go | 11 +++++---- 6 files changed, 80 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c443a4ce8..fd6d93fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Flaky decimal/TestSelect (#300) - Race condition at roundRobinStrategy.GetNextConnection() (#309) +- Incorrect decoding of an MP_DECIMAL when the `scale` value is negative (#314) ## [1.12.0] - 2023-06-07 diff --git a/decimal/bcd.go b/decimal/bcd.go index d6222b861..61a437276 100644 --- a/decimal/bcd.go +++ b/decimal/bcd.go @@ -46,8 +46,11 @@ package decimal // https://github.com/tarantool/decNumber/blob/master/decPacked.c import ( + "bytes" "fmt" "strings" + + "github.com/vmihailenco/msgpack/v5" ) const ( @@ -185,13 +188,17 @@ func encodeStringToBCD(buf string) ([]byte, error) { // ended by a 4-bit sign nibble in the least significant four bits of the final // byte. The scale is used (negated) as the exponent of the decimal number. // Note that zeroes may have a sign and/or a scale. -func decodeStringFromBCD(bcdBuf []byte) (string, error) { - // Index of a byte with scale. - const scaleIdx = 0 - scale := int(bcdBuf[scaleIdx]) +func decodeStringFromBCD(bcdBuf []byte) (string, int, error) { + // Read scale. + buf := bytes.NewBuffer(bcdBuf) + dec := msgpack.NewDecoder(buf) + scale, err := dec.DecodeInt() + if err != nil { + return "", 0, fmt.Errorf("unable to decode the decimal scale: %w", err) + } - // Get a BCD buffer without scale. - bcdBuf = bcdBuf[scaleIdx+1:] + // Get the data without the scale. + bcdBuf = buf.Bytes() bufLen := len(bcdBuf) // Every nibble contains a digit, and the last low nibble contains a @@ -206,10 +213,6 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) { // Reserve bytes for dot and sign. numLen := ndigits + 2 - // Reserve bytes for zeroes. - if scale >= ndigits { - numLen += scale - ndigits - } var bld strings.Builder bld.Grow(numLen) @@ -221,26 +224,10 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) { bld.WriteByte('-') } - // Add missing zeroes to the left side when scale is bigger than a - // number of digits and a single missed zero to the right side when - // equal. - if scale > ndigits { - bld.WriteByte('0') - bld.WriteByte('.') - for diff := scale - ndigits; diff > 0; diff-- { - bld.WriteByte('0') - } - } else if scale == ndigits { - bld.WriteByte('0') - } - const MaxDigit = 0x09 // Builds a buffer with symbols of decimal number (digits, dot and sign). processNibble := func(nibble byte) { if nibble <= MaxDigit { - if ndigits == scale { - bld.WriteByte('.') - } bld.WriteByte(nibble + '0') ndigits-- } @@ -256,5 +243,8 @@ func decodeStringFromBCD(bcdBuf []byte) (string, error) { processNibble(lowNibble) } - return bld.String(), nil + if bld.Len() == 0 || isNegative[sign] && bld.Len() == 1 { + bld.WriteByte('0') + } + return bld.String(), -1 * scale, nil } diff --git a/decimal/decimal.go b/decimal/decimal.go index dd7b30433..d9eae23ed 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -98,16 +98,20 @@ func (decNum *Decimal) UnmarshalMsgpack(b []byte) error { // +--------+-------------------+------------+===============+ // | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal | // +--------+-------------------+------------+===============+ - digits, err := decodeStringFromBCD(b) + digits, exp, err := decodeStringFromBCD(b) if err != nil { return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", b, err) } + dec, err := decimal.NewFromString(digits) - *decNum = *NewDecimal(dec) if err != nil { return fmt.Errorf("msgpack: can't encode string (%s) to a decimal number: %w", digits, err) } + if exp != 0 { + dec = dec.Shift(int32(exp)) + } + *decNum = *NewDecimal(dec) return nil } diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 92616c5f2..ae98dc386 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -121,6 +121,18 @@ var correctnessSamples = []struct { "c7150113012345678912345678900987654321987654321d", false}, } +var correctnessDecodeSamples = []struct { + numString string + mpBuf string + fixExt bool +}{ + {"1e2", "d501fe1c", true}, + {"1e33", "c70301d0df1c", false}, + {"1.1e31", "c70301e2011c", false}, + {"13e-2", "c7030102013c", false}, + {"-1e3", "d501fd1d", true}, +} + // There is a difference between encoding result from a raw string and from // decimal.Decimal. It's expected because decimal.Decimal simplifies decimals: // 0.00010000 -> 0.0001 @@ -397,18 +409,22 @@ func TestEncodeStringToBCD(t *testing.T) { func TestDecodeStringFromBCD(t *testing.T) { samples := correctnessSamples + samples = append(samples, correctnessDecodeSamples...) samples = append(samples, rawSamples...) samples = append(samples, benchmarkSamples...) for _, testcase := range samples { t.Run(testcase.numString, func(t *testing.T) { b, _ := hex.DecodeString(testcase.mpBuf) bcdBuf := trimMPHeader(b, testcase.fixExt) - s, err := DecodeStringFromBCD(bcdBuf) + s, exp, err := DecodeStringFromBCD(bcdBuf) if err != nil { t.Fatalf("Failed to decode BCD '%x' to decimal: %s", bcdBuf, err) } decActual, err := decimal.NewFromString(s) + if exp != 0 { + decActual = decActual.Shift(int32(exp)) + } if err != nil { t.Fatalf("Failed to msgpack.Encoder string ('%s') to decimal", s) } @@ -551,6 +567,37 @@ func TestSelect(t *testing.T) { tupleValueIsDecimal(t, resp.Data, number) } +func TestUnmarshal_from_decimal_new(t *testing.T) { + skipIfDecimalUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + samples := correctnessSamples + samples = append(samples, correctnessDecodeSamples...) + samples = append(samples, benchmarkSamples...) + for _, testcase := range samples { + str := testcase.numString + t.Run(str, func(t *testing.T) { + number, err := decimal.NewFromString(str) + if err != nil { + t.Fatalf("Failed to prepare test decimal: %s", err) + } + + call := NewEvalRequest("return require('decimal').new(...)"). + Args([]interface{}{str}) + resp, err := conn.Do(call).Get() + if err != nil { + t.Fatalf("Decimal create failed: %s", err) + } + if resp == nil { + t.Fatalf("Response is nil after Call") + } + tupleValueIsDecimal(t, []interface{}{resp.Data}, number) + }) + } +} + func assertInsert(t *testing.T, conn *Connection, numString string) { number, err := decimal.NewFromString(numString) if err != nil { diff --git a/decimal/export_test.go b/decimal/export_test.go index c43a812c6..2c8fda1c7 100644 --- a/decimal/export_test.go +++ b/decimal/export_test.go @@ -4,7 +4,7 @@ func EncodeStringToBCD(buf string) ([]byte, error) { return encodeStringToBCD(buf) } -func DecodeStringFromBCD(bcdBuf []byte) (string, error) { +func DecodeStringFromBCD(bcdBuf []byte) (string, int, error) { return decodeStringFromBCD(bcdBuf) } diff --git a/decimal/fuzzing_test.go b/decimal/fuzzing_test.go index c7df2e080..5ec2a2b8f 100644 --- a/decimal/fuzzing_test.go +++ b/decimal/fuzzing_test.go @@ -10,11 +10,14 @@ import ( . "github.com/tarantool/go-tarantool/v2/decimal" ) -func strToDecimal(t *testing.T, buf string) decimal.Decimal { +func strToDecimal(t *testing.T, buf string, exp int) decimal.Decimal { decNum, err := decimal.NewFromString(buf) if err != nil { t.Fatal(err) } + if exp != 0 { + decNum = decNum.Shift(int32(exp)) + } return decNum } @@ -33,13 +36,13 @@ func FuzzEncodeDecodeBCD(f *testing.F) { if err != nil { t.Skip("Only correct requests are interesting: %w", err) } - var dec string - dec, err = DecodeStringFromBCD(bcdBuf) + + dec, exp, err := DecodeStringFromBCD(bcdBuf) if err != nil { t.Fatalf("Failed to decode encoded value ('%s')", orig) } - if !strToDecimal(t, dec).Equal(strToDecimal(t, orig)) { + if !strToDecimal(t, dec, exp).Equal(strToDecimal(t, orig, 0)) { t.Fatal("Decimal numbers are not equal") } }) From b38f61956c0bfbe7a137a16ca48fcc8b5faf8c40 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 20 Jun 2023 11:22:57 +0300 Subject: [PATCH 472/605] api: the Decimal type is immutable The patch forces the use of objects of type Decimal instead of pointers. Part of #238 --- CHANGELOG.md | 1 + README.md | 6 +++++ decimal/decimal.go | 58 ++++++++++++++++++++++++++--------------- decimal/decimal_test.go | 54 ++++++++++++++++++++------------------ decimal/example_test.go | 2 +- 5 files changed, 73 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd6d93fbf..f902a8ba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Change encoding of the queue.Identify() UUID argument from binary blob to plain string. Needed for upgrade to Tarantool 3.0, where a binary blob is decoded to a varbinary object (#313). +- Use objects of the Decimal type instead of pointers (#238) ### Deprecated diff --git a/README.md b/README.md index b3bff4d3d..df71d2ad9 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ faster than other packages according to public benchmarks. * [API reference](#api-reference) * [Walking\-through example](#walking-through-example) * [Migration to v2](#migration-to-v2) + * [decimal package](#decimal-package) * [multi package](#multi-package) * [pool package](#pool-package) * [msgpack.v5](#msgpackv5) @@ -148,6 +149,11 @@ by `Connect()`. The article describes migration from go-tarantool to go-tarantool/v2. +#### decimal package + +Now you need to use objects of the Decimal type instead of pointers to it. A +new constructor `MakeDecimal` returns an object. `NewDecimal` has been removed. + #### multi package The subpackage has been deleted. You could use `pool` instead. diff --git a/decimal/decimal.go b/decimal/decimal.go index d9eae23ed..3a1abb76e 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -21,6 +21,7 @@ package decimal import ( "fmt" + "reflect" "github.com/shopspring/decimal" "github.com/vmihailenco/msgpack/v5" @@ -42,44 +43,50 @@ const ( decimalPrecision = 38 ) +var ( + one decimal.Decimal = decimal.NewFromInt(1) + // -10^decimalPrecision - 1 + minSupportedDecimal decimal.Decimal = maxSupportedDecimal.Neg().Sub(one) + // 10^decimalPrecision - 1 + maxSupportedDecimal decimal.Decimal = decimal.New(1, decimalPrecision).Sub(one) +) + type Decimal struct { decimal.Decimal } -// NewDecimal creates a new Decimal from a decimal.Decimal. -func NewDecimal(decimal decimal.Decimal) *Decimal { - return &Decimal{Decimal: decimal} +// MakeDecimal creates a new Decimal from a decimal.Decimal. +func MakeDecimal(decimal decimal.Decimal) Decimal { + return Decimal{Decimal: decimal} } -// NewDecimalFromString creates a new Decimal from a string. -func NewDecimalFromString(src string) (result *Decimal, err error) { +// MakeDecimalFromString creates a new Decimal from a string. +func MakeDecimalFromString(src string) (Decimal, error) { + result := Decimal{} dec, err := decimal.NewFromString(src) if err != nil { - return + return result, err } - result = NewDecimal(dec) - return + result = MakeDecimal(dec) + return result, nil } -// MarshalMsgpack serializes the Decimal into a MessagePack representation. -func (decNum *Decimal) MarshalMsgpack() ([]byte, error) { - one := decimal.NewFromInt(1) - maxSupportedDecimal := decimal.New(1, decimalPrecision).Sub(one) // 10^decimalPrecision - 1 - minSupportedDecimal := maxSupportedDecimal.Neg().Sub(one) // -10^decimalPrecision - 1 - if decNum.GreaterThan(maxSupportedDecimal) { +func decimalEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { + dec := v.Interface().(Decimal) + if dec.GreaterThan(maxSupportedDecimal) { return nil, fmt.Errorf( "msgpack: decimal number is bigger than maximum supported number (10^%d - 1)", decimalPrecision) } - if decNum.LessThan(minSupportedDecimal) { + if dec.LessThan(minSupportedDecimal) { return nil, fmt.Errorf( "msgpack: decimal number is lesser than minimum supported number (-10^%d - 1)", decimalPrecision) } - strBuf := decNum.String() + strBuf := dec.String() bcdBuf, err := encodeStringToBCD(strBuf) if err != nil { return nil, fmt.Errorf("msgpack: can't encode string (%s) to a BCD buffer: %w", strBuf, err) @@ -87,9 +94,16 @@ func (decNum *Decimal) MarshalMsgpack() ([]byte, error) { return bcdBuf, nil } -// UnmarshalMsgpack deserializes a Decimal value from a MessagePack -// representation. -func (decNum *Decimal) UnmarshalMsgpack(b []byte) error { +func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { + b := make([]byte, extLen) + n, err := d.Buffered().Read(b) + if err != nil { + return err + } + if n < extLen { + return fmt.Errorf("msgpack: unexpected end of stream after %d decimal bytes", n) + } + // Decimal values can be encoded to fixext MessagePack, where buffer // has a fixed length encoded by first byte, and ext MessagePack, where // buffer length is not fixed and encoded by a number in a separate @@ -111,10 +125,12 @@ func (decNum *Decimal) UnmarshalMsgpack(b []byte) error { if exp != 0 { dec = dec.Shift(int32(exp)) } - *decNum = *NewDecimal(dec) + ptr := v.Addr().Interface().(*Decimal) + *ptr = MakeDecimal(dec) return nil } func init() { - msgpack.RegisterExt(decimalExtID, (*Decimal)(nil)) + msgpack.RegisterExtDecoder(decimalExtID, Decimal{}, decimalDecoder) + msgpack.RegisterExtEncoder(decimalExtID, Decimal{}, decimalEncoder) } diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index ae98dc386..57c70d1d7 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -63,10 +63,10 @@ func (t *TupleDecimal) DecodeMsgpack(d *msgpack.Decoder) error { return err } - if dec, ok := res.(*Decimal); !ok { + if dec, ok := res.(Decimal); !ok { return fmt.Errorf("decimal doesn't match") } else { - t.number = *dec + t.number = dec } return nil } @@ -160,12 +160,12 @@ var decimalSamples = []struct { func TestMPEncodeDecode(t *testing.T) { for _, testcase := range benchmarkSamples { t.Run(testcase.numString, func(t *testing.T) { - decNum, err := NewDecimalFromString(testcase.numString) + decNum, err := MakeDecimalFromString(testcase.numString) if err != nil { t.Fatal(err) } var buf []byte - tuple := TupleDecimal{number: *decNum} + tuple := TupleDecimal{number: decNum} if buf, err = msgpack.Marshal(&tuple); err != nil { t.Fatalf( "Failed to msgpack.Encoder decimal number '%s' to a MessagePack buffer: %s", @@ -270,7 +270,7 @@ func TestEncodeMaxNumber(t *testing.T) { referenceErrMsg := "msgpack: decimal number is bigger than maximum " + "supported number (10^38 - 1)" decNum := decimal.New(1, DecimalPrecision) // // 10^DecimalPrecision - tuple := TupleDecimal{number: *NewDecimal(decNum)} + tuple := TupleDecimal{number: MakeDecimal(decNum)} _, err := msgpack.Marshal(&tuple) if err == nil { t.Fatalf("It is possible to msgpack.Encoder a number unsupported by Tarantool") @@ -285,7 +285,7 @@ func TestEncodeMinNumber(t *testing.T) { "supported number (-10^38 - 1)" two := decimal.NewFromInt(2) decNum := decimal.New(1, DecimalPrecision).Neg().Sub(two) // -10^DecimalPrecision - 2 - tuple := TupleDecimal{number: *NewDecimal(decNum)} + tuple := TupleDecimal{number: MakeDecimal(decNum)} _, err := msgpack.Marshal(&tuple) if err == nil { t.Fatalf("It is possible to msgpack.Encoder a number unsupported by Tarantool") @@ -302,7 +302,7 @@ func benchmarkMPEncodeDecode(b *testing.B, src decimal.Decimal, dst interface{}) var buf []byte var err error for i := 0; i < b.N; i++ { - tuple := TupleDecimal{number: *NewDecimal(src)} + tuple := TupleDecimal{number: MakeDecimal(src)} if buf, err = msgpack.Marshal(&tuple); err != nil { b.Fatal(err) } @@ -327,13 +327,15 @@ func BenchmarkMPEncodeDecodeDecimal(b *testing.B) { func BenchmarkMPEncodeDecimal(b *testing.B) { for _, testcase := range benchmarkSamples { b.Run(testcase.numString, func(b *testing.B) { - decNum, err := NewDecimalFromString(testcase.numString) + decNum, err := MakeDecimalFromString(testcase.numString) if err != nil { b.Fatal(err) } b.ResetTimer() for i := 0; i < b.N; i++ { - msgpack.Marshal(decNum) + if _, err := msgpack.Marshal(decNum); err != nil { + b.Fatal(err) + } } }) } @@ -342,20 +344,20 @@ func BenchmarkMPEncodeDecimal(b *testing.B) { func BenchmarkMPDecodeDecimal(b *testing.B) { for _, testcase := range benchmarkSamples { b.Run(testcase.numString, func(b *testing.B) { - decNum, err := NewDecimalFromString(testcase.numString) + decNum, err := MakeDecimalFromString(testcase.numString) if err != nil { b.Fatal(err) } - var buf []byte - if buf, err = msgpack.Marshal(decNum); err != nil { + buf, err := msgpack.Marshal(decNum) + if err != nil { b.Fatal(err) } b.ResetTimer() - var v TupleDecimal for i := 0; i < b.N; i++ { - msgpack.Unmarshal(buf, &v) + if err := msgpack.Unmarshal(buf, &decNum); err != nil { + b.Fatal(err) + } } - }) } } @@ -371,7 +373,7 @@ func tupleValueIsDecimal(t *testing.T, tuples []interface{}, number decimal.Deci if len(tpl) != 1 { t.Fatalf("Unexpected return value body (tuple len)") } - if val, ok := tpl[0].(*Decimal); !ok || !val.Equal(number) { + if val, ok := tpl[0].(Decimal); !ok || !val.Equal(number) { t.Fatalf("Unexpected return value body (tuple 0 field)") } } @@ -447,9 +449,9 @@ func TestMPEncode(t *testing.T) { samples = append(samples, benchmarkSamples...) for _, testcase := range samples { t.Run(testcase.numString, func(t *testing.T) { - dec, err := NewDecimalFromString(testcase.numString) + dec, err := MakeDecimalFromString(testcase.numString) if err != nil { - t.Fatalf("NewDecimalFromString() failed: %s", err.Error()) + t.Fatalf("MakeDecimalFromString() failed: %s", err.Error()) } buf, err := msgpack.Marshal(dec) if err != nil { @@ -481,7 +483,7 @@ func TestMPDecode(t *testing.T) { if err != nil { t.Fatalf("Unmsgpack.Marshalling failed: %s", err.Error()) } - decActual, ok := v.(*Decimal) + decActual, ok := v.(Decimal) if !ok { t.Fatalf("Unable to convert to Decimal") } @@ -532,7 +534,7 @@ func TestSelect(t *testing.T) { t.Fatalf("Failed to prepare test decimal: %s", err) } - ins := NewInsertRequest(space).Tuple([]interface{}{NewDecimal(number)}) + ins := NewInsertRequest(space).Tuple([]interface{}{MakeDecimal(number)}) resp, err := conn.Do(ins).Get() if err != nil { t.Fatalf("Decimal insert failed: %s", err) @@ -549,7 +551,7 @@ func TestSelect(t *testing.T) { Offset(offset). Limit(limit). Iterator(IterEq). - Key([]interface{}{NewDecimal(number)}) + Key([]interface{}{MakeDecimal(number)}) resp, err = conn.Do(sel).Get() if err != nil { t.Fatalf("Decimal select failed: %s", err.Error()) @@ -559,7 +561,7 @@ func TestSelect(t *testing.T) { } tupleValueIsDecimal(t, resp.Data, number) - del := NewDeleteRequest(space).Index(index).Key([]interface{}{NewDecimal(number)}) + del := NewDeleteRequest(space).Index(index).Key([]interface{}{MakeDecimal(number)}) resp, err = conn.Do(del).Get() if err != nil { t.Fatalf("Decimal delete failed: %s", err) @@ -604,7 +606,7 @@ func assertInsert(t *testing.T, conn *Connection, numString string) { t.Fatalf("Failed to prepare test decimal: %s", err) } - ins := NewInsertRequest(space).Tuple([]interface{}{NewDecimal(number)}) + ins := NewInsertRequest(space).Tuple([]interface{}{MakeDecimal(number)}) resp, err := conn.Do(ins).Get() if err != nil { t.Fatalf("Decimal insert failed: %s", err) @@ -614,7 +616,7 @@ func assertInsert(t *testing.T, conn *Connection, numString string) { } tupleValueIsDecimal(t, resp.Data, number) - del := NewDeleteRequest(space).Index(index).Key([]interface{}{NewDecimal(number)}) + del := NewDeleteRequest(space).Index(index).Key([]interface{}{MakeDecimal(number)}) resp, err = conn.Do(del).Get() if err != nil { t.Fatalf("Decimal delete failed: %s", err) @@ -648,7 +650,7 @@ func TestReplace(t *testing.T) { t.Fatalf("Failed to prepare test decimal: %s", err) } - rep := NewReplaceRequest(space).Tuple([]interface{}{NewDecimal(number)}) + rep := NewReplaceRequest(space).Tuple([]interface{}{MakeDecimal(number)}) respRep, errRep := conn.Do(rep).Get() if errRep != nil { t.Fatalf("Decimal replace failed: %s", errRep) @@ -662,7 +664,7 @@ func TestReplace(t *testing.T) { Index(index). Limit(1). Iterator(IterEq). - Key([]interface{}{NewDecimal(number)}) + Key([]interface{}{MakeDecimal(number)}) respSel, errSel := conn.Do(sel).Get() if errSel != nil { t.Fatalf("Decimal select failed: %s", errSel) diff --git a/decimal/example_test.go b/decimal/example_test.go index c57cc0603..a355767f1 100644 --- a/decimal/example_test.go +++ b/decimal/example_test.go @@ -35,7 +35,7 @@ func Example() { spaceNo := uint32(524) - number, err := NewDecimalFromString("-22.804") + number, err := MakeDecimalFromString("-22.804") if err != nil { log.Fatalf("Failed to prepare test decimal: %s", err) } From b30e2b69c9fca74953178afe9a80c38530d85aff Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 22 Jun 2023 14:20:00 +0300 Subject: [PATCH 473/605] api: the Datetime type is immutable The patch forces the use of objects of type Datetime instead of pointers. Part of #238 --- CHANGELOG.md | 1 + README.md | 7 +++++ datetime/datetime.go | 66 ++++++++++++++++++++++----------------- datetime/datetime_test.go | 66 ++++++++++++++++++--------------------- datetime/example_test.go | 22 ++++++------- 5 files changed, 87 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f902a8ba0..bf229b143 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. plain string. Needed for upgrade to Tarantool 3.0, where a binary blob is decoded to a varbinary object (#313). - Use objects of the Decimal type instead of pointers (#238) +- Use objects of the Datetime type instead of pointers (#238) ### Deprecated diff --git a/README.md b/README.md index df71d2ad9..aa4c6deac 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ faster than other packages according to public benchmarks. * [API reference](#api-reference) * [Walking\-through example](#walking-through-example) * [Migration to v2](#migration-to-v2) + * [datetime package](#datetime-package) * [decimal package](#decimal-package) * [multi package](#multi-package) * [pool package](#pool-package) @@ -149,6 +150,12 @@ by `Connect()`. The article describes migration from go-tarantool to go-tarantool/v2. +#### datetime package + +Now you need to use objects of the Datetime type instead of pointers to it. A +new constructor `MakeDatetime` returns an object. `NewDatetime` has been +removed. + #### decimal package Now you need to use objects of the Decimal type instead of pointers to it. A diff --git a/datetime/datetime.go b/datetime/datetime.go index 1f3bff775..2ac309eee 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -12,6 +12,7 @@ package datetime import ( "encoding/binary" "fmt" + "reflect" "time" "github.com/vmihailenco/msgpack/v5" @@ -35,7 +36,7 @@ import ( // Datetime external type. Supported since Tarantool 2.10. See more details in // issue https://github.com/tarantool/tarantool/issues/5946. -const datetime_extId = 4 +const datetimeExtID = 4 // datetime structure keeps a number of seconds and nanoseconds since Unix Epoch. // Time is normalized by UTC, so time-zone offset is informative only. @@ -93,7 +94,7 @@ const ( offsetMax = 14 * 60 * 60 ) -// NewDatetime returns a pointer to a new datetime.Datetime that contains a +// MakeDatetime returns a datetime.Datetime object that contains a // specified time.Time. It may return an error if the Time value is out of // supported range: [-5879610-06-22T00:00Z .. 5879611-07-11T00:00Z] or // an invalid timezone or offset value is out of supported range: @@ -101,33 +102,33 @@ const ( // // NOTE: Tarantool's datetime.tz value is picked from t.Location().String(). // "Local" location is unsupported, see ExampleNewDatetime_localUnsupported. -func NewDatetime(t time.Time) (*Datetime, error) { +func MakeDatetime(t time.Time) (Datetime, error) { + dt := Datetime{} seconds := t.Unix() if seconds < minSeconds || seconds > maxSeconds { - return nil, fmt.Errorf("time %s is out of supported range", t) + return dt, fmt.Errorf("time %s is out of supported range", t) } zone := t.Location().String() _, offset := t.Zone() if zone != NoTimezone { if _, ok := timezoneToIndex[zone]; !ok { - return nil, fmt.Errorf("unknown timezone %s with offset %d", + return dt, fmt.Errorf("unknown timezone %s with offset %d", zone, offset) } } if offset < offsetMin || offset > offsetMax { - return nil, fmt.Errorf("offset must be between %d and %d hours", + return dt, fmt.Errorf("offset must be between %d and %d hours", offsetMin, offsetMax) } - dt := new(Datetime) dt.time = t return dt, nil } -func intervalFromDatetime(dtime *Datetime) (ival Interval) { +func intervalFromDatetime(dtime Datetime) (ival Interval) { ival.Year = int64(dtime.time.Year()) ival.Month = int64(dtime.time.Month()) ival.Day = int64(dtime.time.Day()) @@ -158,7 +159,7 @@ func daysInMonth(year int64, month int64) int64 { // C implementation: // https://github.com/tarantool/c-dt/blob/cec6acebb54d9e73ea0b99c63898732abd7683a6/dt_arithmetic.c#L74-L98 -func addMonth(ival *Interval, delta int64, adjust Adjust) { +func addMonth(ival Interval, delta int64, adjust Adjust) Interval { oldYear := ival.Year oldMonth := ival.Month @@ -172,16 +173,17 @@ func addMonth(ival *Interval, delta int64, adjust Adjust) { } } if adjust == ExcessAdjust || ival.Day < 28 { - return + return ival } dim := daysInMonth(ival.Year, ival.Month) if ival.Day > dim || (adjust == LastAdjust && ival.Day == daysInMonth(oldYear, oldMonth)) { ival.Day = dim } + return ival } -func (d *Datetime) add(ival Interval, positive bool) (*Datetime, error) { +func (d Datetime) add(ival Interval, positive bool) (Datetime, error) { newVal := intervalFromDatetime(d) var direction int64 @@ -191,7 +193,7 @@ func (d *Datetime) add(ival Interval, positive bool) (*Datetime, error) { direction = -1 } - addMonth(&newVal, direction*ival.Year*12+direction*ival.Month, ival.Adjust) + newVal = addMonth(newVal, direction*ival.Year*12+direction*ival.Month, ival.Adjust) newVal.Day += direction * 7 * ival.Week newVal.Day += direction * ival.Day newVal.Hour += direction * ival.Hour @@ -203,23 +205,23 @@ func (d *Datetime) add(ival Interval, positive bool) (*Datetime, error) { int(newVal.Day), int(newVal.Hour), int(newVal.Min), int(newVal.Sec), int(newVal.Nsec), d.time.Location()) - return NewDatetime(tm) + return MakeDatetime(tm) } // Add creates a new Datetime as addition of the Datetime and Interval. It may // return an error if a new Datetime is out of supported range. -func (d *Datetime) Add(ival Interval) (*Datetime, error) { +func (d Datetime) Add(ival Interval) (Datetime, error) { return d.add(ival, true) } // Sub creates a new Datetime as subtraction of the Datetime and Interval. It // may return an error if a new Datetime is out of supported range. -func (d *Datetime) Sub(ival Interval) (*Datetime, error) { +func (d Datetime) Sub(ival Interval) (Datetime, error) { return d.add(ival, false) } // Interval returns an Interval value to a next Datetime value. -func (d *Datetime) Interval(next *Datetime) Interval { +func (d Datetime) Interval(next Datetime) Interval { curIval := intervalFromDatetime(d) nextIval := intervalFromDatetime(next) _, curOffset := d.time.Zone() @@ -240,8 +242,9 @@ func (d *Datetime) ToTime() time.Time { return d.time } -func (d *Datetime) MarshalMsgpack() ([]byte, error) { - tm := d.ToTime() +func datetimeEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { + dtime := v.Interface().(Datetime) + tm := dtime.ToTime() var dt datetime dt.seconds = tm.Unix() @@ -272,18 +275,26 @@ func (d *Datetime) MarshalMsgpack() ([]byte, error) { return buf, nil } -func (d *Datetime) UnmarshalMsgpack(b []byte) error { - l := len(b) - if l != maxSize && l != secondsSize { +func datetimeDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { + if extLen != maxSize && extLen != secondsSize { return fmt.Errorf("invalid data length: got %d, wanted %d or %d", - len(b), secondsSize, maxSize) + extLen, secondsSize, maxSize) + } + + b := make([]byte, extLen) + n, err := d.Buffered().Read(b) + if err != nil { + return err + } + if n < extLen { + return fmt.Errorf("msgpack: unexpected end of stream after %d datetime bytes", n) } var dt datetime sec := binary.LittleEndian.Uint64(b) dt.seconds = int64(sec) dt.nsec = 0 - if l == maxSize { + if extLen == maxSize { dt.nsec = int32(binary.LittleEndian.Uint32(b[secondsSize:])) dt.tzOffset = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize:])) dt.tzIndex = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize+tzOffsetSize:])) @@ -316,13 +327,12 @@ func (d *Datetime) UnmarshalMsgpack(b []byte) error { } tt = tt.In(loc) - dtp, err := NewDatetime(tt) - if dtp != nil { - *d = *dtp - } + ptr := v.Addr().Interface().(*Datetime) + *ptr, err = MakeDatetime(tt) return err } func init() { - msgpack.RegisterExt(datetime_extId, (*Datetime)(nil)) + msgpack.RegisterExtDecoder(datetimeExtID, Datetime{}, datetimeDecoder) + msgpack.RegisterExtEncoder(datetimeExtID, Datetime{}, datetimeEncoder) } diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 705d2c56b..858c7b84a 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -58,7 +58,7 @@ func skipIfDatetimeUnsupported(t *testing.T) { func TestDatetimeAdd(t *testing.T) { tm := time.Unix(0, 0).UTC() - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } @@ -229,7 +229,7 @@ func TestDatetimeAddAdjust(t *testing.T) { if err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } @@ -243,9 +243,6 @@ func TestDatetimeAddAdjust(t *testing.T) { if err != nil { t.Fatalf("Unable to add: %s", err.Error()) } - if newdt == nil { - t.Fatalf("Unexpected nil value") - } res := newdt.ToTime().Format(time.RFC3339) if res != tc.want { t.Fatalf("Unexpected result %s, expected %s", res, tc.want) @@ -256,7 +253,7 @@ func TestDatetimeAddAdjust(t *testing.T) { func TestDatetimeAddSubSymmetric(t *testing.T) { tm := time.Unix(0, 0).UTC() - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } @@ -304,7 +301,7 @@ func TestDatetimeAddSubSymmetric(t *testing.T) { // We have a separate test for accurate Datetime boundaries. func TestDatetimeAddOutOfRange(t *testing.T) { tm := time.Unix(0, 0).UTC() - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } @@ -317,9 +314,6 @@ func TestDatetimeAddOutOfRange(t *testing.T) { if err.Error() != expected { t.Fatalf("Unexpected error: %s", err.Error()) } - if newdt != nil { - t.Fatalf("Unexpected result: %v", newdt) - } } func TestDatetimeInterval(t *testing.T) { @@ -335,11 +329,11 @@ func TestDatetimeInterval(t *testing.T) { t.Fatalf("Error in time.Parse(): %s", err) } - dtFirst, err := NewDatetime(tmFirst) + dtFirst, err := MakeDatetime(tmFirst) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tmFirst, err) } - dtSecond, err := NewDatetime(tmSecond) + dtSecond, err := MakeDatetime(tmSecond) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tmSecond, err) } @@ -389,15 +383,15 @@ func TestDatetimeTarantoolInterval(t *testing.T) { "2015-12-21T17:50:53Z", "1980-03-28T13:18:39.000099Z", } - datetimes := []*Datetime{} + datetimes := []Datetime{} for _, date := range dates { tm, err := time.Parse(time.RFC3339, date) if err != nil { t.Fatalf("Error in time.Parse(%s): %s", date, err) } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { - t.Fatalf("Error in NewDatetime(%s): %s", tm, err) + t.Fatalf("Error in MakeDatetime(%s): %s", tm, err) } datetimes = append(datetimes, dt) } @@ -434,7 +428,7 @@ func assertDatetimeIsEqual(t *testing.T, tuples []interface{}, tm time.Time) { if len(tpl) != 2 { t.Fatalf("Unexpected return value body (tuple len = %d)", len(tpl)) } - if val, ok := tpl[dtIndex].(*Datetime); !ok || !val.ToTime().Equal(tm) { + if val, ok := tpl[dtIndex].(Datetime); !ok || !val.ToTime().Equal(tm) { t.Fatalf("Unexpected tuple %d field %v, expected %v", dtIndex, val, @@ -466,7 +460,7 @@ func TestInvalidTimezone(t *testing.T) { t.Fatalf("Time parse failed: %s", err) } tm = tm.In(invalidLoc) - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err == nil { t.Fatalf("Unexpected success: %v", dt) } @@ -502,7 +496,7 @@ func TestInvalidOffset(t *testing.T) { t.Fatalf("Time parse failed: %s", err) } tm = tm.In(loc) - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if testcase.ok && err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } @@ -537,7 +531,7 @@ func TestCustomTimezone(t *testing.T) { t.Fatalf("Time parse failed: %s", err) } tm = tm.In(customLoc) - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unable to create datetime: %s", err.Error()) } @@ -550,7 +544,7 @@ func TestCustomTimezone(t *testing.T) { assertDatetimeIsEqual(t, resp.Data, tm) tpl := resp.Data[0].([]interface{}) - if respDt, ok := tpl[0].(*Datetime); ok { + if respDt, ok := tpl[0].(Datetime); ok { zone := respDt.ToTime().Location().String() _, offset := respDt.ToTime().Zone() if zone != customZone { @@ -574,7 +568,7 @@ func TestCustomTimezone(t *testing.T) { func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) { t.Helper() - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) } @@ -726,7 +720,7 @@ func TestDatetimeOutOfRange(t *testing.T) { for _, tm := range greaterBoundaryTimes { t.Run(tm.String(), func(t *testing.T) { - _, err := NewDatetime(tm) + _, err := MakeDatetime(tm) if err == nil { t.Errorf("Time %s should be unsupported!", tm) } @@ -745,7 +739,7 @@ func TestDatetimeReplace(t *testing.T) { t.Fatalf("Time parse failed: %s", err) } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) } @@ -852,10 +846,10 @@ func (ev *Event) DecodeMsgpack(d *msgpack.Decoder) error { return err } - if dt, ok := res.(*Datetime); !ok { + if dt, ok := res.(Datetime); !ok { return fmt.Errorf("Datetime doesn't match") } else { - ev.Datetime = *dt + ev.Datetime = dt } return nil } @@ -907,11 +901,11 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { tm1, _ := time.Parse(time.RFC3339, "2010-05-24T17:51:56.000000009Z") tm2, _ := time.Parse(time.RFC3339, "2022-05-24T17:51:56.000000009Z") - dt1, err := NewDatetime(tm1) + dt1, err := MakeDatetime(tm1) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm1, err) } - dt2, err := NewDatetime(tm2) + dt2, err := MakeDatetime(tm2) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm2, err) } @@ -921,8 +915,8 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { tuple := Tuple2{Cid: cid, Orig: orig, Events: []Event{ - {*dt1, "Minsk"}, - {*dt2, "Moscow"}, + {dt1, "Minsk"}, + {dt2, "Moscow"}, }, } rep := NewReplaceRequest(spaceTuple2).Tuple(&tuple) @@ -962,7 +956,7 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { } for i, tv := range []time.Time{tm1, tm2} { - dt, ok := events[i].([]interface{})[1].(*Datetime) + dt, ok := events[i].([]interface{})[1].(Datetime) if !ok || !dt.ToTime().Equal(tv) { t.Fatalf("%v != %v", dt.ToTime(), tv) } @@ -1020,7 +1014,7 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) { defer conn.Close() tm := time.Unix(500, 1000).In(time.FixedZone(NoTimezone, 0)) - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) } @@ -1043,7 +1037,7 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) { if tpl, ok := resp.Data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { - if val, ok := tpl[0].(*Datetime); !ok || !val.ToTime().Equal(tm) { + if val, ok := tpl[0].(Datetime); !ok || !val.ToTime().Equal(tm) { t.Fatalf("Unexpected body of Select") } } @@ -1072,7 +1066,7 @@ func TestMPEncode(t *testing.T) { if err != nil { t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) } @@ -1126,7 +1120,7 @@ func TestMPDecode(t *testing.T) { func TestUnmarshalMsgpackInvalidLength(t *testing.T) { var v Datetime - err := v.UnmarshalMsgpack([]byte{0x04}) + err := msgpack.Unmarshal([]byte{0xd4, 0x04, 0x04}, &v) if err == nil { t.Fatalf("Unexpected success %v", v) } @@ -1142,8 +1136,8 @@ func TestUnmarshalMsgpackInvalidZone(t *testing.T) { // {time.RFC3339 + " MST", // "2006-01-02T15:04:00+03:00 MSK", // "d804b016b9430000000000000000b400ee00"} - buf, _ := hex.DecodeString("b016b9430000000000000000b400ee01") - err := v.UnmarshalMsgpack(buf) + buf, _ := hex.DecodeString("d804b016b9430000000000000000b400ee01") + err := msgpack.Unmarshal(buf, &v) if err == nil { t.Fatalf("Unexpected success %v", v) } diff --git a/datetime/example_test.go b/datetime/example_test.go index 5fb1e9ba8..346551629 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -35,7 +35,7 @@ func Example() { fmt.Printf("Error in time.Parse() is %v", err) return } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { fmt.Printf("Unable to create Datetime from %s: %s", tm, err) return @@ -91,13 +91,13 @@ func Example() { fmt.Printf("Data: %v\n", respDt.ToTime()) } -// ExampleNewDatetime_localUnsupported demonstrates that "Local" location is +// ExampleMakeDatetime_localUnsupported demonstrates that "Local" location is // unsupported. -func ExampleNewDatetime_localUnsupported() { +func ExampleMakeDatetime_localUnsupported() { tm := time.Now().Local() loc := tm.Location() fmt.Println("Location:", loc) - if _, err := NewDatetime(tm); err != nil { + if _, err := MakeDatetime(tm); err != nil { fmt.Printf("Could not create a Datetime with %s location.\n", loc) } else { fmt.Printf("A Datetime with %s location created.\n", loc) @@ -109,7 +109,7 @@ func ExampleNewDatetime_localUnsupported() { // Example demonstrates how to create a datetime for Tarantool without UTC // timezone in datetime. -func ExampleNewDatetime_noTimezone() { +func ExampleMakeDatetime_noTimezone() { var datetime = "2013-10-28T17:51:56.000000009Z" tm, err := time.Parse(time.RFC3339, datetime) if err != nil { @@ -119,7 +119,7 @@ func ExampleNewDatetime_noTimezone() { tm = tm.In(time.FixedZone(NoTimezone, 0)) - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { fmt.Printf("Unable to create Datetime from %s: %s", tm, err) return @@ -145,12 +145,12 @@ func ExampleDatetime_Interval() { return } - dtFirst, err := NewDatetime(tmFirst) + dtFirst, err := MakeDatetime(tmFirst) if err != nil { fmt.Printf("Unable to create Datetime from %s: %s", tmFirst, err) return } - dtSecond, err := NewDatetime(tmSecond) + dtSecond, err := MakeDatetime(tmSecond) if err != nil { fmt.Printf("Unable to create Datetime from %s: %s", tmSecond, err) return @@ -170,7 +170,7 @@ func ExampleDatetime_Add() { fmt.Printf("Error in time.Parse() is %s", err) return } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { fmt.Printf("Unable to create Datetime from %s: %s", tm, err) return @@ -201,7 +201,7 @@ func ExampleDatetime_Add_dst() { return } tm := time.Date(2008, 1, 1, 1, 1, 1, 1, loc) - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { fmt.Printf("Unable to create Datetime: %s", err) return @@ -237,7 +237,7 @@ func ExampleDatetime_Sub() { fmt.Printf("Error in time.Parse() is %s", err) return } - dt, err := NewDatetime(tm) + dt, err := MakeDatetime(tm) if err != nil { fmt.Printf("Unable to create Datetime from %s: %s", tm, err) return From f56fb906e20a5960ad2bc14706362d3b5682de84 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 31 Jul 2023 19:59:03 +0300 Subject: [PATCH 474/605] crud: fix options for SelectRequest The patch fixes a typo that made it impossible to setup SelectOpts.After, SelectOpts.BatchSize and SelectOpts.ForceMapCall. Part of #320 --- CHANGELOG.md | 2 ++ crud/example_test.go | 38 ++++++++++++++++++++++++++++++++++++++ crud/select.go | 6 +++--- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf229b143..62d3d3545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Flaky decimal/TestSelect (#300) - Race condition at roundRobinStrategy.GetNextConnection() (#309) - Incorrect decoding of an MP_DECIMAL when the `scale` value is negative (#314) +- Incorrect options (`after`, `batch_size` and `force_map_call`) setup for + crud.SelectRequest (#320) ## [1.12.0] - 2023-06-07 diff --git a/crud/example_test.go b/crud/example_test.go index 3cd5e7bad..155bb175d 100644 --- a/crud/example_test.go +++ b/crud/example_test.go @@ -148,3 +148,41 @@ func ExampleResult_errorMany() { // Output: // Failed to execute request: CallError: } + +func ExampleSelectRequest_pagination() { + conn := exampleConnect() + + const ( + fromTuple = 5 + allTuples = 10 + ) + var tuple interface{} + for i := 0; i < allTuples; i++ { + req := crud.MakeReplaceRequest(exampleSpace). + Tuple([]interface{}{uint(3000 + i), nil, "bla"}) + ret := crud.Result{} + if err := conn.Do(req).GetTyped(&ret); err != nil { + fmt.Printf("Failed to initialize the example: %s\n", err) + return + } + if i == fromTuple { + tuple = ret.Rows.([]interface{})[0] + } + } + + req := crud.MakeSelectRequest(exampleSpace). + Opts(crud.SelectOpts{ + First: crud.MakeOptInt(2), + After: crud.MakeOptTuple(tuple), + }) + ret := crud.Result{} + if err := conn.Do(req).GetTyped(&ret); err != nil { + fmt.Printf("Failed to execute request: %s", err) + return + } + fmt.Println(ret.Metadata) + fmt.Println(ret.Rows) + // Output: + // [{id unsigned false} {bucket_id unsigned true} {name string false}] + // [[3006 32 bla] [3007 33 bla]] +} diff --git a/crud/select.go b/crud/select.go index bdd0e9d42..073c9492b 100644 --- a/crud/select.go +++ b/crud/select.go @@ -63,9 +63,9 @@ func (opts SelectOpts) EncodeMsgpack(enc *msgpack.Encoder) error { values[6], exists[6] = opts.Balance.Get() values[7], exists[7] = opts.First.Get() values[8], exists[8] = opts.After.Get() - values[8], exists[8] = opts.BatchSize.Get() - values[8], exists[8] = opts.ForceMapCall.Get() - values[8], exists[8] = opts.Fullscan.Get() + values[9], exists[9] = opts.BatchSize.Get() + values[10], exists[10] = opts.ForceMapCall.Get() + values[11], exists[11] = opts.Fullscan.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } From 764d8d55f68a07e8d02413b58e623de17888ca36 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Wed, 26 Jul 2023 15:58:20 +0300 Subject: [PATCH 475/605] ci: bump latest EE SDK Run with release 2.11.0 SDK instead of a dev one. 1. https://github.com/tarantool/tt/commit/12bf40453614a89a4c898c2ad8e2da739e6de0af --- .github/workflows/testing.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 61e185789..268a6a3ea 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -143,8 +143,8 @@ jobs: - sdk-version: 'bundle-2.10.0-1-gfa775b383-r486-linux-x86_64' coveralls: false ssl: true - - sdk-path: 'dev/linux/x86_64/master/' - sdk-version: 'sdk-gc64-2.11.0-entrypoint-113-g803baaffe-r529.linux.x86_64' + - sdk-path: 'release/linux/x86_64/2.11/' + sdk-version: 'sdk-gc64-2.11.0-0-r577.linux.x86_64' coveralls: true ssl: true From 8ede0618b75f52c5116f2bf2480ba7fd8cd31467 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 1 Aug 2023 15:36:47 +0300 Subject: [PATCH 476/605] test: remove core tarantool server SSL cases "empty" and "key_crt_client" test cases do not provide SSL files to a server started with SSL transport. In these cases server fails to start, and tests ensures that server fails. It doesn't related to go-tarantool connector testing in any way -- it's the test of a tarantool binary. Since testing core tarantool is not the part of go-tarantool project, this patch removes these cases. The main motivation of this patch is the next commit in the patchset, which separates check for server start and client success of fail. --- ssl_test.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/ssl_test.go b/ssl_test.go index 12cab7304..0b751a1cb 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -214,21 +214,6 @@ CaFile - optional, Ciphers - optional */ var tests = []test{ - { - "empty", - false, - SslOpts{}, - SslOpts{}, - }, - { - "key_crt_client", - false, - SslOpts{}, - SslOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - }, - }, { "key_crt_server", true, From 1bd61d1e2bf45078626837a0c8173cbf66b1f17c Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 1 Aug 2023 15:34:38 +0300 Subject: [PATCH 477/605] test: separate SSL server and client check Current SSL tests are as follows. We start a Tarantool server with default helpers. "ok" tests are successful if everything had started, "fail" tests are successful if ping check had failed (aka we failed to connect). This is a dangerous approach, since "server had failed to start" here is indistinguishable from "client cannot connect". Moreover, because of it each tnt_fail test runs for 5 seconds (10 retry attempts * 500 ms retry wait), which is frustrating. After this patch, there is a separate check for a server start and for a client success or fail. --- ssl_test.go | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/ssl_test.go b/ssl_test.go index 0b751a1cb..3243788c8 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -94,7 +94,7 @@ func createClientServerSslOk(t testing.TB, serverOpts, return l, c, msgs, errs } -func serverTnt(serverOpts, clientOpts SslOpts, auth Auth) (test_helpers.TarantoolInstance, error) { +func serverTnt(serverOpts SslOpts, auth Auth) (test_helpers.TarantoolInstance, error) { listen := tntHost + "?transport=ssl&" key := serverOpts.KeyFile @@ -126,7 +126,7 @@ func serverTnt(serverOpts, clientOpts SslOpts, auth Auth) (test_helpers.Tarantoo SslCertsDir: "testdata", ClientServer: tntHost, ClientTransport: "ssl", - ClientSsl: clientOpts, + ClientSsl: serverOpts, User: "test", Pass: "test", WaitStart: 100 * time.Millisecond, @@ -139,6 +139,23 @@ func serverTntStop(inst test_helpers.TarantoolInstance) { test_helpers.StopTarantoolWithCleanup(inst) } +func checkTntConn(clientOpts SslOpts) error { + conn, err := Connect(tntHost, Opts{ + Auth: AutoAuth, + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", + SkipSchema: true, + Transport: "ssl", + Ssl: clientOpts, + }) + if err != nil { + return err + } + conn.Close() + return nil +} + func assertConnectionSslFail(t testing.TB, serverOpts, clientOpts SslOpts) { t.Helper() @@ -171,9 +188,13 @@ func assertConnectionSslOk(t testing.TB, serverOpts, clientOpts SslOpts) { func assertConnectionTntFail(t testing.TB, serverOpts, clientOpts SslOpts) { t.Helper() - inst, err := serverTnt(serverOpts, clientOpts, AutoAuth) - serverTntStop(inst) + inst, err := serverTnt(serverOpts, AutoAuth) + defer serverTntStop(inst) + if err != nil { + t.Fatalf("An unexpected server error %q", err.Error()) + } + err = checkTntConn(clientOpts) if err == nil { t.Errorf("An unexpected connection to the server") } @@ -182,11 +203,15 @@ func assertConnectionTntFail(t testing.TB, serverOpts, clientOpts SslOpts) { func assertConnectionTntOk(t testing.TB, serverOpts, clientOpts SslOpts) { t.Helper() - inst, err := serverTnt(serverOpts, clientOpts, AutoAuth) - serverTntStop(inst) + inst, err := serverTnt(serverOpts, AutoAuth) + defer serverTntStop(inst) + if err != nil { + t.Fatalf("An unexpected server error %q", err.Error()) + } + err = checkTntConn(clientOpts) if err != nil { - t.Errorf("An unexpected server error %q", err.Error()) + t.Errorf("An unexpected connection error %q", err.Error()) } } @@ -470,10 +495,11 @@ func TestOpts_PapSha256Auth(t *testing.T) { KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", } - inst, err := serverTnt(sslOpts, sslOpts, PapSha256Auth) + + inst, err := serverTnt(sslOpts, PapSha256Auth) defer serverTntStop(inst) if err != nil { - t.Errorf("An unexpected server error: %s", err) + t.Fatalf("An unexpected server error %q", err.Error()) } clientOpts := opts From b17735b99c7ea8374019bd1ba60170be9b0290f1 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 1 Aug 2023 16:00:15 +0300 Subject: [PATCH 478/605] api: support SSL private key file decryption Support `ssl_password` and `ssl_password_file` options in SslOpts. Tarantool EE supports SSL passwords and password files since 2.11.0 [1]. Since it is possible to use corresponding non-encrypted key, cert and CA on server, tests works fine even for Tarantool EE 2.10.0. Same as in Tarantool, we try `SslOpts.Password`, then each line in `SslOpts.PasswordFile`. If all of the above fail, we re-raise errors. If the key is encrypted and password is not provided, `openssl.LoadPrivateKeyFromPEM(keyBytes)` asks to enter PEM pass phrase interactively. On the other hand, `openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password)` works fine for non-encrypted key with any password, including empty string. If the key is encrypted, we fast fail with password error instead of requesting the pass phrase interactively. The patch also bumps go-openssl since latest patch fixes flaky tests [2]. The patch is based on a similar patch for tarantool-python [3]. 1. https://github.com/tarantool/tarantool-ee/issues/22 2. https://github.com/tarantool/go-openssl/pull/9 3. https://github.com/tarantool/tarantool-python/pull/274 --- CHANGELOG.md | 1 + connection.go | 8 ++ go.mod | 2 +- go.sum | 4 +- ssl.go | 48 +++++++++-- ssl_test.go | 168 +++++++++++++++++++++++++++++++++++++ testdata/generate.sh | 13 +++ testdata/invalidpasswords | 1 + testdata/localhost.enc.key | 30 +++++++ testdata/passwords | 2 + 10 files changed, 267 insertions(+), 10 deletions(-) create mode 100644 testdata/invalidpasswords create mode 100644 testdata/localhost.enc.key create mode 100644 testdata/passwords diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d3d3545..8ad9faa38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - IsNullable flag for Field (#302) - More linters on CI (#310) - Meaningful description for read/write socket errors (#129) +- Support password and password file to decrypt private SSL key file (#319) ### Changed diff --git a/connection.go b/connection.go index 37de22e82..9bb42626a 100644 --- a/connection.go +++ b/connection.go @@ -345,6 +345,14 @@ type SslOpts struct { // // * https://www.openssl.org/docs/man1.1.1/man1/ciphers.html Ciphers string + // Password is a password for decrypting the private SSL key file. + // The priority is as follows: try to decrypt with Password, then + // try PasswordFile. + Password string + // PasswordFile is a path to the list of passwords for decrypting + // the private SSL key file. The connection tries every line from the + // file as a password. + PasswordFile string } // Clone returns a copy of the Opts object. diff --git a/go.mod b/go.mod index 44bc63483..bd848308c 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.7.1 github.com/tarantool/go-iproto v0.1.0 - github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 + github.com/tarantool/go-openssl v0.0.8-0.20230801114713-b452431f934a github.com/vmihailenco/msgpack/v5 v5.3.5 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect diff --git a/go.sum b/go.sum index d38645c63..1810c2b3a 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tarantool/go-iproto v0.1.0 h1:zHN9AA8LDawT+JBD0/Nxgr/bIsWkkpDzpcMuaNPSIAQ= github.com/tarantool/go-iproto v0.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= -github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 h1:/AN3eUPsTlvF6W+Ng/8ZjnSU6o7L0H4Wb9GMks6RkzU= -github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= +github.com/tarantool/go-openssl v0.0.8-0.20230801114713-b452431f934a h1:eeElglRXJ3xWKkHmDbeXrQWlZyQ4t3Ca1YlZsrfdXFU= +github.com/tarantool/go-openssl v0.0.8-0.20230801114713-b452431f934a/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= diff --git a/ssl.go b/ssl.go index d9373ace2..a23238849 100644 --- a/ssl.go +++ b/ssl.go @@ -4,9 +4,12 @@ package tarantool import ( + "bufio" "errors" "io/ioutil" "net" + "os" + "strings" "time" "github.com/tarantool/go-openssl" @@ -43,7 +46,7 @@ func sslCreateContext(opts SslOpts) (ctx interface{}, err error) { } if opts.KeyFile != "" { - if err = sslLoadKey(sslCtx, opts.KeyFile); err != nil { + if err = sslLoadKey(sslCtx, opts.KeyFile, opts.Password, opts.PasswordFile); err != nil { return } } @@ -95,16 +98,47 @@ func sslLoadCert(ctx *openssl.Ctx, certFile string) (err error) { return } -func sslLoadKey(ctx *openssl.Ctx, keyFile string) (err error) { +func sslLoadKey(ctx *openssl.Ctx, keyFile string, password string, + passwordFile string) error { var keyBytes []byte + var err, firstDecryptErr error + if keyBytes, err = ioutil.ReadFile(keyFile); err != nil { - return + return err } - var key openssl.PrivateKey - if key, err = openssl.LoadPrivateKeyFromPEM(keyBytes); err != nil { - return + // If the key is encrypted and password is not provided, + // openssl.LoadPrivateKeyFromPEM(keyBytes) asks to enter PEM pass phrase + // interactively. On the other hand, + // openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) works fine + // for non-encrypted key with any password, including empty string. If + // the key is encrypted, we fast fail with password error instead of + // requesting the pass phrase interactively. + passwords := []string{password} + if passwordFile != "" { + file, err := os.Open(passwordFile) + if err == nil { + defer file.Close() + + scanner := bufio.NewScanner(file) + // Tarantool itself tries each password file line. + for scanner.Scan() { + password = strings.TrimSpace(scanner.Text()) + passwords = append(passwords, password) + } + } else { + firstDecryptErr = err + } + } + + for _, password := range passwords { + key, err := openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) + if err == nil { + return ctx.UsePrivateKey(key) + } else if firstDecryptErr == nil { + firstDecryptErr = err + } } - return ctx.UsePrivateKey(key) + return firstDecryptErr } diff --git a/ssl_test.go b/ssl_test.go index 3243788c8..30078703c 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -117,6 +117,16 @@ func serverTnt(serverOpts SslOpts, auth Auth) (test_helpers.TarantoolInstance, e listen += fmt.Sprintf("ssl_ciphers=%s&", ciphers) } + password := serverOpts.Password + if password != "" { + listen += fmt.Sprintf("ssl_password=%s&", password) + } + + passwordFile := serverOpts.PasswordFile + if passwordFile != "" { + listen += fmt.Sprintf("ssl_password_file=%s&", passwordFile) + } + listen = listen[:len(listen)-1] return test_helpers.StartTarantool(test_helpers.StartOpts{ @@ -441,6 +451,164 @@ var tests = []test{ Ciphers: "TLS_AES_128_GCM_SHA256", }, }, + { + "pass_key_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "mysslpassword", + }, + }, + { + "passfile_key_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + PasswordFile: "testdata/passwords", + }, + }, + { + "pass_and_passfile_key_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "mysslpassword", + PasswordFile: "testdata/passwords", + }, + }, + { + "inv_pass_and_passfile_key_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "invalidpassword", + PasswordFile: "testdata/passwords", + }, + }, + { + "pass_and_inv_passfile_key_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "mysslpassword", + PasswordFile: "testdata/invalidpasswords", + }, + }, + { + "pass_and_not_existing_passfile_key_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "mysslpassword", + PasswordFile: "testdata/notafile", + }, + }, + { + "inv_pass_and_inv_passfile_key_encrypt_client", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "invalidpassword", + PasswordFile: "testdata/invalidpasswords", + }, + }, + { + "not_existing_passfile_key_encrypt_client", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + PasswordFile: "testdata/notafile", + }, + }, + { + "no_pass_key_encrypt_client", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + }, + }, + { + "pass_key_non_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + Password: "invalidpassword", + }, + }, + { + "passfile_key_non_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + PasswordFile: "testdata/invalidpasswords", + }, + }, } func isTestTntSsl() bool { diff --git a/testdata/generate.sh b/testdata/generate.sh index f29f41c90..4b8cf3630 100755 --- a/testdata/generate.sh +++ b/testdata/generate.sh @@ -23,3 +23,16 @@ openssl x509 -outform pem -in ca.pem -out ca.crt openssl req -new -nodes -newkey rsa:2048 -keyout localhost.key -out localhost.csr -subj "/C=US/ST=YourState/L=YourCity/O=Example-Certificates/CN=localhost" openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains.ext -out localhost.crt +password=mysslpassword + +# Tarantool tries every line from the password file. +cat < passwords +unusedpassword +$password +EOF + +cat < invalidpasswords +unusedpassword1 +EOF + +openssl rsa -aes256 -passout "pass:${password}" -in localhost.key -out localhost.enc.key diff --git a/testdata/invalidpasswords b/testdata/invalidpasswords new file mode 100644 index 000000000..b09d795aa --- /dev/null +++ b/testdata/invalidpasswords @@ -0,0 +1 @@ +unusedpassword1 diff --git a/testdata/localhost.enc.key b/testdata/localhost.enc.key new file mode 100644 index 000000000..b881820a3 --- /dev/null +++ b/testdata/localhost.enc.key @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIm+0WC9xe38cCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBNOE4KD+yauMfsnOiNAaaZBIIE +0DtXaHGpacJ8MjjL6zciYhgJOD9SJHE4vwPxpNDWuS9mf6wk/cdBNFMqnYwJmlYw +J/eQ+Z8MsZUqjnhDQz9YXXd8JftexAAa1bHnmfv2N/czJCx57dAHVdmJzgibfp18 +GCpqR23tklEO2Nj2HCbR59rh7IsnW9mD6jh+mVtkOix5HMCUSxwc3bEUutIQE80P +JHG2BsEfAeeHZa+QgG3Y15c6uSXD6wY73ldPPOgZ3NFOqcw/RDqYf1zsohx7auxi +Y6zHA7LdYtQjbNJ5slIfxPhAh75Fws0g4QvWbAwqqdEOVmlamYYjAOdVBBxTvcRs +/63ZN55VTQ8rYhShNA3BVFOLHaRD4mnlKE5Xh7gJXltCED7EHdpHdT9K3uM9U7nW +b2JSylt2RzY+LDsio2U0xsQp9jHzRRw81p8P1jmo5alP8jPACMsE8nnNNSDF4p43 +fG7hNNBq/dhq80iOnaArY05TIBMsD079tB0VKrYyyfaL0RbsAdgtCEmF9bCpnsTM +y9ExcJGQQJx9WNAHkSyjdzJd0jR6Zc0MrgRuj26nJ3Ahq58zaQKdfFO9RfGWd38n +MH3jshEtAuF+jXFbMcM4rVdIBPSuhYgHzYIC6yteziy7+6hittpWeNGLKpC5oZ8R +oEwH3MVsjCbd6Pp3vdcR412vLMgy1ZUOraDoY08FXC82RBJViVX6LLltIJu96kiX +WWUcRZAwzlJsTvh1EGmDcNNKCgmvWQaojqTNgTjxjJ3SzD2/TV6uQrSLgZ6ulyNl +7vKWt/YMTvIgoJA9JeH8Aik/XNd4bRXL+VXfUHpLTgn+WKiq2irVYd9R/yITDunP +a/kzqxitjU4OGdf/LOtYxfxfoGvFw5ym4KikoHKVg4ILcIQ+W4roOQQlu4/yezAK +fwYCrMVJWq4ESuQh3rn7eFR+eyBV6YcNBLm4iUcQTMhnXMMYxQ3TnDNga5eYhmV1 +ByYx+nFQDrbDolXo5JfXs3x6kXhoT/7wMHgsXtmRSd5PSBbaeJTrbMGA0Op6YgWr +EpvX3Yt863s4h+JgDpg9ouH+OJGgn7LGGye+TjjuDds8CStFdcFDDOayBS3EH4Cr +jgJwzvTdTZl+1YLYJXB67M4zmVPRRs5H88+fZYYA9bhZACL/rQBj2wDq/sIxvrIM +SCjOhSJ4z5Sm3XaBKnRG2GBBt67MeHB0+T3HR3VHKR+zStbCnsbOLythsE/CIA8L +fBNXMvnWa5bLgaCaEcK6Q3LOamJiKaigbmhI+3U3NUdb9cT1GhE0rtx6/IO9eapz +IUDOrtX9U+1o6iW2dahezxwLo9ftRwQ7qwG4qOk/Co/1c2WuuQ+d4YPpj/JOO5mf +LanA35mQjQrr2MZII91psznx05ffb5xMp2pqNbC6DVuZq8ZlhvVHGk+wM9RK3kYP +/ITwpbUvLmmN892kvZgLAXadSupBV8R/L5ZjDUO9U2all9p4eGfWZBk/yiivOLmh +VQxKCqAmThTO1hRa56+AjgzRJO6cY85ra+4Mm3FhhdR4gYvap2QTq0o2Vn0WlCHh +1SIeaDKfw9v4aGBbhqyQU2mPlXO5JiLktO+lZ5styVq9Qm+b0ROZxHzL1lRUNbRA +VfQO4fRnINKPgyzgH3tNxJTzw4pLkrkBD/g+zxDZVqkx +-----END ENCRYPTED PRIVATE KEY----- diff --git a/testdata/passwords b/testdata/passwords new file mode 100644 index 000000000..58530047f --- /dev/null +++ b/testdata/passwords @@ -0,0 +1,2 @@ +unusedpassword +mysslpassword From d8df65dcd0f29a6a5d07472115cbcf2753b12609 Mon Sep 17 00:00:00 2001 From: Albert Skalt Date: Mon, 21 Aug 2023 17:19:36 +0300 Subject: [PATCH 479/605] crud: support `operation_data` field in errors This patch adds `operation_data` decoding for the `crud.Error`. The `operation_data` type is determined as `rowType` in `crud.Result`. Also, according to [1], an error can contain one of the following: - an error - an array of errors - nil So the error decoding logic has been modified to consider each case, in order to avoid comparing an error to nil. 1. https://github.com/tarantool/crud/tree/master#api Closes #330 --- CHANGELOG.md | 1 + crud/error.go | 36 ++++++++++++++++--- crud/example_test.go | 86 ++++++++++++++++++++++++++++++++++++++++++++ crud/result.go | 16 ++++----- crud/result_test.go | 33 +++++++++++++++++ 5 files changed, 160 insertions(+), 12 deletions(-) create mode 100644 crud/result_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ad9faa38..211ea25a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - More linters on CI (#310) - Meaningful description for read/write socket errors (#129) - Support password and password file to decrypt private SSL key file (#319) +- Support `operation_data` in `crud.Error` (#330) ### Changed diff --git a/crud/error.go b/crud/error.go index b0b267fbe..9233de5c3 100644 --- a/crud/error.go +++ b/crud/error.go @@ -1,6 +1,7 @@ package crud import ( + "reflect" "strings" "github.com/vmihailenco/msgpack/v5" @@ -21,6 +22,15 @@ type Error struct { Stack string // Str is the text of reason with error class. Str string + // OperationData is the object/tuple with which an error occurred. + OperationData interface{} + // operationDataType contains the type of OperationData. + operationDataType reflect.Type +} + +// newError creates an Error object with a custom operation data type to decoding. +func newError(operationDataType reflect.Type) *Error { + return &Error{operationDataType: operationDataType} } // DecodeMsgpack provides custom msgpack decoder. @@ -59,6 +69,18 @@ func (e *Error) DecodeMsgpack(d *msgpack.Decoder) error { if e.Str, err = d.DecodeString(); err != nil { return err } + case "operation_data": + if e.operationDataType != nil { + tuple := reflect.New(e.operationDataType) + if err = d.DecodeValue(tuple); err != nil { + return err + } + e.OperationData = tuple.Elem().Interface() + } else { + if err = d.Decode(&e.OperationData); err != nil { + return err + } + } default: if err := d.Skip(); err != nil { return err @@ -77,6 +99,13 @@ func (e Error) Error() string { // ErrorMany describes CRUD error object for `_many` methods. type ErrorMany struct { Errors []Error + // operationDataType contains the type of OperationData for each Error. + operationDataType reflect.Type +} + +// newErrorMany creates an ErrorMany object with a custom operation data type to decoding. +func newErrorMany(operationDataType reflect.Type) *ErrorMany { + return &ErrorMany{operationDataType: operationDataType} } // DecodeMsgpack provides custom msgpack decoder. @@ -88,16 +117,15 @@ func (e *ErrorMany) DecodeMsgpack(d *msgpack.Decoder) error { var errs []Error for i := 0; i < l; i++ { - var crudErr *Error = nil + crudErr := newError(e.operationDataType) if err := d.Decode(&crudErr); err != nil { return err - } else if crudErr != nil { - errs = append(errs, *crudErr) } + errs = append(errs, *crudErr) } if len(errs) > 0 { - *e = ErrorMany{Errors: errs} + e.Errors = errs } return nil diff --git a/crud/example_test.go b/crud/example_test.go index 155bb175d..363d0570d 100644 --- a/crud/example_test.go +++ b/crud/example_test.go @@ -78,6 +78,92 @@ func ExampleResult_rowsCustomType() { // [{{} 2010 45 bla}] } +// ExampleResult_operationData demonstrates how to obtain information +// about erroneous objects from crud.Error using `OperationData` field. +func ExampleResult_operationData() { + conn := exampleConnect() + req := crud.MakeInsertObjectManyRequest(exampleSpace).Objects([]crud.Object{ + crud.MapObject{ + "id": 2, + "bucket_id": 3, + "name": "Makar", + }, + crud.MapObject{ + "id": 2, + "bucket_id": 3, + "name": "Vasya", + }, + crud.MapObject{ + "id": 3, + "bucket_id": 5, + }, + }) + + ret := crud.Result{} + if err := conn.Do(req).GetTyped(&ret); err != nil { + crudErrs := err.(crud.ErrorMany) + fmt.Println("Erroneous data:") + for _, crudErr := range crudErrs.Errors { + fmt.Println(crudErr.OperationData) + } + } else { + fmt.Println(ret.Metadata) + fmt.Println(ret.Rows) + } + + // Output: + // Erroneous data: + // [2 3 Vasya] + // map[bucket_id:5 id:3] +} + +// ExampleResult_operationDataCustomType demonstrates the ability +// to cast `OperationData` field, extracted from a CRUD error during decoding +// using crud.Result, to a custom type. +// The type of `OperationData` is determined as the crud.Result row type. +func ExampleResult_operationDataCustomType() { + conn := exampleConnect() + req := crud.MakeInsertObjectManyRequest(exampleSpace).Objects([]crud.Object{ + crud.MapObject{ + "id": 1, + "bucket_id": 3, + "name": "Makar", + }, + crud.MapObject{ + "id": 1, + "bucket_id": 3, + "name": "Vasya", + }, + crud.MapObject{ + "id": 3, + "bucket_id": 5, + }, + }) + + type Tuple struct { + Id uint64 `msgpack:"id,omitempty"` + BucketId uint64 `msgpack:"bucket_id,omitempty"` + Name string `msgpack:"name,omitempty"` + } + + ret := crud.MakeResult(reflect.TypeOf(Tuple{})) + if err := conn.Do(req).GetTyped(&ret); err != nil { + crudErrs := err.(crud.ErrorMany) + fmt.Println("Erroneous data:") + for _, crudErr := range crudErrs.Errors { + operationData := crudErr.OperationData.(Tuple) + fmt.Println(operationData) + } + } else { + fmt.Println(ret.Metadata) + fmt.Println(ret.Rows) + } + // Output: + // Erroneous data: + // {1 3 Vasya} + // {3 5 } +} + // ExampleResult_many demonstrates that there is no difference in a // response from *ManyRequest. func ExampleResult_many() { diff --git a/crud/result.go b/crud/result.go index b594e5de7..e65b3e55e 100644 --- a/crud/result.go +++ b/crud/result.go @@ -137,20 +137,20 @@ func (r *Result) DecodeMsgpack(d *msgpack.Decoder) error { var retErr error if msgpackIsArray(code) { - var crudErr *ErrorMany + crudErr := newErrorMany(r.rowType) if err := d.Decode(&crudErr); err != nil { return err } - if crudErr != nil { - retErr = *crudErr - } - } else { - var crudErr *Error + retErr = *crudErr + } else if code != msgpcode.Nil { + crudErr := newError(r.rowType) if err := d.Decode(&crudErr); err != nil { return err } - if crudErr != nil { - retErr = *crudErr + retErr = *crudErr + } else { + if err := d.DecodeNil(); err != nil { + return err } } diff --git a/crud/result_test.go b/crud/result_test.go new file mode 100644 index 000000000..c67649f96 --- /dev/null +++ b/crud/result_test.go @@ -0,0 +1,33 @@ +package crud_test + +import ( + "bytes" + "reflect" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/v2/crud" + "github.com/vmihailenco/msgpack/v5" +) + +func TestResult_DecodeMsgpack(t *testing.T) { + sampleCrudResponse := []interface{}{ + map[string]interface{}{ + "rows": []interface{}{"1", "2", "3"}, + }, + nil, + } + responses := []interface{}{sampleCrudResponse, sampleCrudResponse} + + b := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(b) + err := enc.Encode(responses) + require.NoError(t, err) + + var results []crud.Result + decoder := msgpack.NewDecoder(b) + err = decoder.DecodeValue(reflect.ValueOf(&results)) + require.NoError(t, err) + require.Equal(t, results[0].Rows, []interface{}{"1", "2", "3"}) + require.Equal(t, results[1].Rows, []interface{}{"1", "2", "3"}) +} From 4193c693c1f0ff3707025544a2f02ded2bc12e8c Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Thu, 5 Oct 2023 16:16:54 +0300 Subject: [PATCH 480/605] doc: make crud options description more specific Since `upsert_object` and `upsert_object_many` not support `skip_nullability_check_on_flatten` option [1], `UpsertObjectOpts` is `SimpleOperationOpts` and not `SimpleOperationObjectOpts`, same for many operation. But it is possible to get confused while studying options descriptions. 1. https://github.com/tarantool/crud/commit/7504da391c20ac1d2dfbbb7de6d3c32d73d75f60 --- crud/options.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crud/options.go b/crud/options.go index c073b7222..f9cde7be8 100644 --- a/crud/options.go +++ b/crud/options.go @@ -138,6 +138,7 @@ func (opts BaseOpts) EncodeMsgpack(enc *msgpack.Encoder) error { } // SimpleOperationOpts describes options for simple CRUD operations. +// It also covers `upsert_object` options. type SimpleOperationOpts struct { // Timeout is a `vshard.call` timeout and vshard // master discovery timeout (in seconds). @@ -168,7 +169,7 @@ func (opts SimpleOperationOpts) EncodeMsgpack(enc *msgpack.Encoder) error { } // SimpleOperationObjectOpts describes options for simple CRUD -// operations with objects. +// operations with objects. It doesn't cover `upsert_object` options. type SimpleOperationObjectOpts struct { // Timeout is a `vshard.call` timeout and vshard // master discovery timeout (in seconds). @@ -203,6 +204,7 @@ func (opts SimpleOperationObjectOpts) EncodeMsgpack(enc *msgpack.Encoder) error } // OperationManyOpts describes options for CRUD operations with many tuples. +// It also covers `upsert_object_many` options. type OperationManyOpts struct { // Timeout is a `vshard.call` timeout and vshard // master discovery timeout (in seconds). @@ -239,7 +241,7 @@ func (opts OperationManyOpts) EncodeMsgpack(enc *msgpack.Encoder) error { } // OperationObjectManyOpts describes options for CRUD operations -// with many objects. +// with many objects. It doesn't cover `upsert_object_many` options. type OperationObjectManyOpts struct { // Timeout is a `vshard.call` timeout and vshard // master discovery timeout (in seconds). From 16c1b1cde290a17fa452ff3b090065ef3456aa58 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Thu, 5 Oct 2023 18:26:27 +0300 Subject: [PATCH 481/605] crud: fix Get options Before this patch, `vshard_router`, `fields`, `bucket_id`, `mode`, `prefer_replica`, `balance` were either ignored or had a wrong name. This patch fixes the issue. --- CHANGELOG.md | 2 ++ crud/get.go | 9 +++++---- crud/tarantool_test.go | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 211ea25a2..020a37c70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Incorrect decoding of an MP_DECIMAL when the `scale` value is negative (#314) - Incorrect options (`after`, `batch_size` and `force_map_call`) setup for crud.SelectRequest (#320) +- Incorrect options (`vshard_router`, `fields`, `bucket_id`, `mode`, + `prefer_replica`, `balance`) setup for crud.GetRequest (#335) ## [1.12.0] - 2023-06-07 diff --git a/crud/get.go b/crud/get.go index e1855f35c..a957219dc 100644 --- a/crud/get.go +++ b/crud/get.go @@ -42,10 +42,11 @@ func (opts GetOpts) EncodeMsgpack(enc *msgpack.Encoder) error { exists := [optsCnt]bool{} values[0], exists[0] = opts.Timeout.Get() values[1], exists[1] = opts.VshardRouter.Get() - values[1], exists[1] = opts.BucketId.Get() - values[2], exists[2] = opts.Mode.Get() - values[3], exists[3] = opts.PreferReplica.Get() - values[4], exists[4] = opts.Balance.Get() + values[2], exists[2] = opts.Fields.Get() + values[3], exists[3] = opts.BucketId.Get() + values[4], exists[4] = opts.Mode.Get() + values[5], exists[5] = opts.PreferReplica.Get() + values[6], exists[6] = opts.Balance.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 5cf29f66a..aa32aae95 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -808,6 +808,27 @@ func TestStorageInfoResult(t *testing.T) { } } +func TestGetAdditionalOpts(t *testing.T) { + conn := connect(t) + defer conn.Close() + + req := crud.MakeGetRequest(spaceName).Key(key).Opts(crud.GetOpts{ + Timeout: crud.MakeOptUint(1), + Fields: crud.MakeOptTuple([]interface{}{"name"}), + Mode: crud.MakeOptString("read"), + PreferReplica: crud.MakeOptBool(true), + Balance: crud.MakeOptBool(true), + }) + resp := crud.Result{} + + testCrudRequestPrepareData(t, conn) + + err := conn.Do(req).GetTyped(&resp) + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err) + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body From a8f320a17be5563fbd525ec1f1cdc90bec07012a Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Fri, 6 Oct 2023 11:42:05 +0300 Subject: [PATCH 482/605] crud: support fetch_latest_metadata option `fetch_latest_metadata` option was introduced in crud 1.2.0 [1]. 1. https://github.com/tarantool/crud/commit/2675925d17240d7065a718be2e4ad685a4a21913 --- CHANGELOG.md | 1 + Makefile | 2 +- crud/get.go | 10 ++- crud/options.go | 54 +++++++++--- crud/select.go | 10 ++- crud/tarantool_test.go | 183 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 245 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 020a37c70..1fc71aac7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Meaningful description for read/write socket errors (#129) - Support password and password file to decrypt private SSL key file (#319) - Support `operation_data` in `crud.Error` (#330) +- Support `fetch_latest_metadata` option for crud requests with metadata (#335) ### Changed diff --git a/Makefile b/Makefile index 4f676a530..c435764a8 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ clean: .PHONY: deps deps: clean ( cd ./queue/testdata; $(TTCTL) rocks install queue 1.3.0 ) - ( cd ./crud/testdata; $(TTCTL) rocks install crud 1.1.1 ) + ( cd ./crud/testdata; $(TTCTL) rocks install crud 1.3.0 ) .PHONY: datetime-timezones datetime-timezones: diff --git a/crud/get.go b/crud/get.go index a957219dc..fcfef5426 100644 --- a/crud/get.go +++ b/crud/get.go @@ -29,15 +29,20 @@ type GetOpts struct { // Balance is a parameter to use replica according to vshard // load balancing policy. Balance OptBool + // FetchLatestMetadata guarantees the up-to-date metadata (space format) + // in first return value, otherwise it may not take into account + // the latest migration of the data format. Performance overhead is up to 15%. + // Disabled by default. + FetchLatestMetadata OptBool } // EncodeMsgpack provides custom msgpack encoder. func (opts GetOpts) EncodeMsgpack(enc *msgpack.Encoder) error { - const optsCnt = 7 + const optsCnt = 8 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, bucketIdOptName, modeOptName, - preferReplicaOptName, balanceOptName} + preferReplicaOptName, balanceOptName, fetchLatestMetadataOptName} values := [optsCnt]interface{}{} exists := [optsCnt]bool{} values[0], exists[0] = opts.Timeout.Get() @@ -47,6 +52,7 @@ func (opts GetOpts) EncodeMsgpack(enc *msgpack.Encoder) error { values[4], exists[4] = opts.Mode.Get() values[5], exists[5] = opts.PreferReplica.Get() values[6], exists[6] = opts.Balance.Get() + values[7], exists[7] = opts.FetchLatestMetadata.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } diff --git a/crud/options.go b/crud/options.go index f9cde7be8..8a6997380 100644 --- a/crud/options.go +++ b/crud/options.go @@ -21,6 +21,7 @@ const ( firstOptName = "first" afterOptName = "after" batchSizeOptName = "batch_size" + fetchLatestMetadataOptName = "fetch_latest_metadata" ) // OptUint is an optional uint. @@ -150,20 +151,26 @@ type SimpleOperationOpts struct { Fields OptTuple // BucketId is a bucket ID. BucketId OptUint + // FetchLatestMetadata guarantees the up-to-date metadata (space format) + // in first return value, otherwise it may not take into account + // the latest migration of the data format. Performance overhead is up to 15%. + // Disabled by default. + FetchLatestMetadata OptBool } // EncodeMsgpack provides custom msgpack encoder. func (opts SimpleOperationOpts) EncodeMsgpack(enc *msgpack.Encoder) error { - const optsCnt = 4 + const optsCnt = 5 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, - fieldsOptName, bucketIdOptName} + fieldsOptName, bucketIdOptName, fetchLatestMetadataOptName} values := [optsCnt]interface{}{} exists := [optsCnt]bool{} values[0], exists[0] = opts.Timeout.Get() values[1], exists[1] = opts.VshardRouter.Get() values[2], exists[2] = opts.Fields.Get() values[3], exists[3] = opts.BucketId.Get() + values[4], exists[4] = opts.FetchLatestMetadata.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } @@ -184,14 +191,20 @@ type SimpleOperationObjectOpts struct { // SkipNullabilityCheckOnFlatten is a parameter to allow // setting null values to non-nullable fields. SkipNullabilityCheckOnFlatten OptBool + // FetchLatestMetadata guarantees the up-to-date metadata (space format) + // in first return value, otherwise it may not take into account + // the latest migration of the data format. Performance overhead is up to 15%. + // Disabled by default. + FetchLatestMetadata OptBool } // EncodeMsgpack provides custom msgpack encoder. func (opts SimpleOperationObjectOpts) EncodeMsgpack(enc *msgpack.Encoder) error { - const optsCnt = 5 + const optsCnt = 6 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, - fieldsOptName, bucketIdOptName, skipNullabilityCheckOnFlattenOptName} + fieldsOptName, bucketIdOptName, skipNullabilityCheckOnFlattenOptName, + fetchLatestMetadataOptName} values := [optsCnt]interface{}{} exists := [optsCnt]bool{} values[0], exists[0] = opts.Timeout.Get() @@ -199,6 +212,7 @@ func (opts SimpleOperationObjectOpts) EncodeMsgpack(enc *msgpack.Encoder) error values[2], exists[2] = opts.Fields.Get() values[3], exists[3] = opts.BucketId.Get() values[4], exists[4] = opts.SkipNullabilityCheckOnFlatten.Get() + values[5], exists[5] = opts.FetchLatestMetadata.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } @@ -221,14 +235,20 @@ type OperationManyOpts struct { // RollbackOnError is a parameter because of what any failed operation // will lead to rollback on a storage, where the operation is failed. RollbackOnError OptBool + // FetchLatestMetadata guarantees the up-to-date metadata (space format) + // in first return value, otherwise it may not take into account + // the latest migration of the data format. Performance overhead is up to 15%. + // Disabled by default. + FetchLatestMetadata OptBool } // EncodeMsgpack provides custom msgpack encoder. func (opts OperationManyOpts) EncodeMsgpack(enc *msgpack.Encoder) error { - const optsCnt = 5 + const optsCnt = 6 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, - fieldsOptName, stopOnErrorOptName, rollbackOnErrorOptName} + fieldsOptName, stopOnErrorOptName, rollbackOnErrorOptName, + fetchLatestMetadataOptName} values := [optsCnt]interface{}{} exists := [optsCnt]bool{} values[0], exists[0] = opts.Timeout.Get() @@ -236,6 +256,7 @@ func (opts OperationManyOpts) EncodeMsgpack(enc *msgpack.Encoder) error { values[2], exists[2] = opts.Fields.Get() values[3], exists[3] = opts.StopOnError.Get() values[4], exists[4] = opts.RollbackOnError.Get() + values[5], exists[5] = opts.FetchLatestMetadata.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } @@ -261,15 +282,20 @@ type OperationObjectManyOpts struct { // SkipNullabilityCheckOnFlatten is a parameter to allow // setting null values to non-nullable fields. SkipNullabilityCheckOnFlatten OptBool + // FetchLatestMetadata guarantees the up-to-date metadata (space format) + // in first return value, otherwise it may not take into account + // the latest migration of the data format. Performance overhead is up to 15%. + // Disabled by default. + FetchLatestMetadata OptBool } // EncodeMsgpack provides custom msgpack encoder. func (opts OperationObjectManyOpts) EncodeMsgpack(enc *msgpack.Encoder) error { - const optsCnt = 6 + const optsCnt = 7 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, stopOnErrorOptName, rollbackOnErrorOptName, - skipNullabilityCheckOnFlattenOptName} + skipNullabilityCheckOnFlattenOptName, fetchLatestMetadataOptName} values := [optsCnt]interface{}{} exists := [optsCnt]bool{} values[0], exists[0] = opts.Timeout.Get() @@ -278,6 +304,7 @@ func (opts OperationObjectManyOpts) EncodeMsgpack(enc *msgpack.Encoder) error { values[3], exists[3] = opts.StopOnError.Get() values[4], exists[4] = opts.RollbackOnError.Get() values[5], exists[5] = opts.SkipNullabilityCheckOnFlatten.Get() + values[6], exists[6] = opts.FetchLatestMetadata.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } @@ -292,18 +319,25 @@ type BorderOpts struct { VshardRouter OptString // Fields is field names for getting only a subset of fields. Fields OptTuple + // FetchLatestMetadata guarantees the up-to-date metadata (space format) + // in first return value, otherwise it may not take into account + // the latest migration of the data format. Performance overhead is up to 15%. + // Disabled by default. + FetchLatestMetadata OptBool } // EncodeMsgpack provides custom msgpack encoder. func (opts BorderOpts) EncodeMsgpack(enc *msgpack.Encoder) error { - const optsCnt = 3 + const optsCnt = 4 - names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName} + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, + fetchLatestMetadataOptName} values := [optsCnt]interface{}{} exists := [optsCnt]bool{} values[0], exists[0] = opts.Timeout.Get() values[1], exists[1] = opts.VshardRouter.Get() values[2], exists[2] = opts.Fields.Get() + values[3], exists[3] = opts.FetchLatestMetadata.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } diff --git a/crud/select.go b/crud/select.go index 073c9492b..0d0b5708a 100644 --- a/crud/select.go +++ b/crud/select.go @@ -41,17 +41,22 @@ type SelectOpts struct { // Fullscan describes if a critical log entry will be skipped on // potentially long select. Fullscan OptBool + // FetchLatestMetadata guarantees the up-to-date metadata (space format) + // in first return value, otherwise it may not take into account + // the latest migration of the data format. Performance overhead is up to 15%. + // Disabled by default. + FetchLatestMetadata OptBool } // EncodeMsgpack provides custom msgpack encoder. func (opts SelectOpts) EncodeMsgpack(enc *msgpack.Encoder) error { - const optsCnt = 12 + const optsCnt = 13 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, bucketIdOptName, modeOptName, preferReplicaOptName, balanceOptName, firstOptName, afterOptName, batchSizeOptName, - forceMapCallOptName, fullscanOptName} + forceMapCallOptName, fullscanOptName, fetchLatestMetadataOptName} values := [optsCnt]interface{}{} exists := [optsCnt]bool{} values[0], exists[0] = opts.Timeout.Get() @@ -66,6 +71,7 @@ func (opts SelectOpts) EncodeMsgpack(enc *msgpack.Encoder) error { values[9], exists[9] = opts.BatchSize.Get() values[10], exists[10] = opts.ForceMapCall.Get() values[11], exists[11] = opts.Fullscan.Get() + values[12], exists[12] = opts.FetchLatestMetadata.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index aa32aae95..457cb2aa0 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -829,6 +829,189 @@ func TestGetAdditionalOpts(t *testing.T) { } } +var testMetadataCases = []struct { + name string + req tarantool.Request +}{ + { + "Insert", + crud.MakeInsertRequest(spaceName). + Tuple(tuple). + Opts(crud.InsertOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "InsertObject", + crud.MakeInsertObjectRequest(spaceName). + Object(object). + Opts(crud.InsertObjectOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "InsertMany", + crud.MakeInsertManyRequest(spaceName). + Tuples(tuples). + Opts(crud.InsertManyOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "InsertObjectMany", + crud.MakeInsertObjectManyRequest(spaceName). + Objects(objects). + Opts(crud.InsertObjectManyOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "Replace", + crud.MakeReplaceRequest(spaceName). + Tuple(tuple). + Opts(crud.ReplaceOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "ReplaceObject", + crud.MakeReplaceObjectRequest(spaceName). + Object(object). + Opts(crud.ReplaceObjectOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "ReplaceMany", + crud.MakeReplaceManyRequest(spaceName). + Tuples(tuples). + Opts(crud.ReplaceManyOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "ReplaceObjectMany", + crud.MakeReplaceObjectManyRequest(spaceName). + Objects(objects). + Opts(crud.ReplaceObjectManyOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "Upsert", + crud.MakeUpsertRequest(spaceName). + Tuple(tuple). + Operations(operations). + Opts(crud.UpsertOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "UpsertObject", + crud.MakeUpsertObjectRequest(spaceName). + Object(object). + Operations(operations). + Opts(crud.UpsertObjectOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "UpsertMany", + crud.MakeUpsertManyRequest(spaceName). + TuplesOperationsData(tuplesOperationsData). + Opts(crud.UpsertManyOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "UpsertObjectMany", + crud.MakeUpsertObjectManyRequest(spaceName). + ObjectsOperationsData(objectsOperationData). + Opts(crud.UpsertObjectManyOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "Select", + crud.MakeSelectRequest(spaceName). + Conditions(conditions). + Opts(crud.SelectOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "Get", + crud.MakeGetRequest(spaceName). + Key(key). + Opts(crud.GetOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "Update", + crud.MakeUpdateRequest(spaceName). + Key(key). + Operations(operations). + Opts(crud.UpdateOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "Delete", + crud.MakeDeleteRequest(spaceName). + Key(key). + Opts(crud.DeleteOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "Min", + crud.MakeMinRequest(spaceName). + Opts(crud.MinOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, + { + "Max", + crud.MakeMaxRequest(spaceName). + Opts(crud.MaxOpts{ + FetchLatestMetadata: crud.MakeOptBool(true), + }), + }, +} + +func TestFetchLatestMetadataOption(t *testing.T) { + conn := connect(t) + defer conn.Close() + + for _, testCase := range testMetadataCases { + t.Run(testCase.name, func(t *testing.T) { + for i := 1010; i < 1020; i++ { + req := tarantool.NewDeleteRequest(spaceName). + Key([]interface{}{uint(i)}) + conn.Do(req).Get() + } + + resp := crud.Result{} + + err := conn.Do(testCase.req).GetTyped(&resp) + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err) + } + + if len(resp.Metadata) == 0 { + t.Fatalf("Failed to get relevant metadata") + } + + for i := 1010; i < 1020; i++ { + req := tarantool.NewDeleteRequest(spaceName). + Key([]interface{}{uint(i)}) + conn.Do(req).Get() + } + }) + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body From c3ba5b59b7d1d7fdedba1e914925abe7d6b1c77d Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Fri, 6 Oct 2023 13:03:03 +0300 Subject: [PATCH 483/605] crud: more careful response process In go-tarantool, crud API result decoding assumes that there are always two response values, except for truncate. Such approach is based on actual experience: success result is always `return res, nil`, not `return res`. But this is not a feature guaranteed by crud module API, just a current implementation quirk [1] (since in Lua function result process there is no any difference). See also similar patch in tarantool/python [2]. 1. https://github.com/tarantool/crud/blob/53457477974fed42351cbd87f566d11e9f7e39bb/crud/common/schema.lua#L88 2. https://github.com/tarantool/tarantool-python/commit/a4b734aa46facc89fd967de1e74f9768341e56fc --- crud/result.go | 96 ++++++++++++++++++++++++------------------ crud/tarantool_test.go | 26 ++++++------ 2 files changed, 67 insertions(+), 55 deletions(-) diff --git a/crud/result.go b/crud/result.go index e65b3e55e..cc875a5de 100644 --- a/crud/result.go +++ b/crud/result.go @@ -75,8 +75,8 @@ func (r *Result) DecodeMsgpack(d *msgpack.Decoder) error { return err } - if arrLen < 2 { - return fmt.Errorf("array len doesn't match: %d", arrLen) + if arrLen == 0 { + return fmt.Errorf("unexpected empty response array") } l, err := d.DecodeMapLen() @@ -130,27 +130,32 @@ func (r *Result) DecodeMsgpack(d *msgpack.Decoder) error { } } - code, err := d.PeekCode() - if err != nil { - return err - } - - var retErr error - if msgpackIsArray(code) { - crudErr := newErrorMany(r.rowType) - if err := d.Decode(&crudErr); err != nil { - return err - } - retErr = *crudErr - } else if code != msgpcode.Nil { - crudErr := newError(r.rowType) - if err := d.Decode(&crudErr); err != nil { + if arrLen > 1 { + code, err := d.PeekCode() + if err != nil { return err } - retErr = *crudErr - } else { - if err := d.DecodeNil(); err != nil { - return err + + if msgpackIsArray(code) { + crudErr := newErrorMany(r.rowType) + if err := d.Decode(&crudErr); err != nil { + return err + } + if crudErr != nil { + return *crudErr + } + } else if code != msgpcode.Nil { + crudErr := newError(r.rowType) + if err := d.Decode(&crudErr); err != nil { + return err + } + if crudErr != nil { + return *crudErr + } + } else { + if err := d.DecodeNil(); err != nil { + return err + } } } @@ -160,7 +165,7 @@ func (r *Result) DecodeMsgpack(d *msgpack.Decoder) error { } } - return retErr + return nil } // NumberResult describes CRUD result as an object containing number. @@ -175,18 +180,24 @@ func (r *NumberResult) DecodeMsgpack(d *msgpack.Decoder) error { return err } - if arrLen < 2 { - return fmt.Errorf("array len doesn't match: %d", arrLen) + if arrLen == 0 { + return fmt.Errorf("unexpected empty response array") } if r.Value, err = d.DecodeUint64(); err != nil { return err } - var crudErr *Error = nil + if arrLen > 1 { + var crudErr *Error = nil - if err := d.Decode(&crudErr); err != nil { - return err + if err := d.Decode(&crudErr); err != nil { + return err + } + + if crudErr != nil { + return crudErr + } } for i := 2; i < arrLen; i++ { @@ -195,10 +206,6 @@ func (r *NumberResult) DecodeMsgpack(d *msgpack.Decoder) error { } } - if crudErr != nil { - return crudErr - } - return nil } @@ -213,26 +220,31 @@ func (r *BoolResult) DecodeMsgpack(d *msgpack.Decoder) error { if err != nil { return err } - if arrLen < 2 { - if r.Value, err = d.DecodeBool(); err != nil { - return err - } - return nil + if arrLen == 0 { + return fmt.Errorf("unexpected empty response array") } - if _, err = d.DecodeInterface(); err != nil { + if r.Value, err = d.DecodeBool(); err != nil { return err } - var crudErr *Error = nil + if arrLen > 1 { + var crudErr *Error = nil - if err := d.Decode(&crudErr); err != nil { - return err + if err := d.Decode(&crudErr); err != nil { + return err + } + + if crudErr != nil { + return crudErr + } } - if crudErr != nil { - return crudErr + for i := 2; i < arrLen; i++ { + if err := d.Skip(); err != nil { + return err + } } return nil diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 457cb2aa0..88572b56f 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -254,7 +254,7 @@ var testGenerateDataCases = []struct { }{ { "Insert", - 2, + 1, 1, crud.MakeInsertRequest(spaceName). Tuple(tuple). @@ -262,7 +262,7 @@ var testGenerateDataCases = []struct { }, { "InsertObject", - 2, + 1, 1, crud.MakeInsertObjectRequest(spaceName). Object(object). @@ -270,7 +270,7 @@ var testGenerateDataCases = []struct { }, { "InsertMany", - 2, + 1, 10, crud.MakeInsertManyRequest(spaceName). Tuples(tuples). @@ -278,7 +278,7 @@ var testGenerateDataCases = []struct { }, { "InsertObjectMany", - 2, + 1, 10, crud.MakeInsertObjectManyRequest(spaceName). Objects(objects). @@ -286,7 +286,7 @@ var testGenerateDataCases = []struct { }, { "Replace", - 2, + 1, 1, crud.MakeReplaceRequest(spaceName). Tuple(tuple). @@ -294,7 +294,7 @@ var testGenerateDataCases = []struct { }, { "ReplaceObject", - 2, + 1, 1, crud.MakeReplaceObjectRequest(spaceName). Object(object). @@ -302,7 +302,7 @@ var testGenerateDataCases = []struct { }, { "ReplaceMany", - 2, + 1, 10, crud.MakeReplaceManyRequest(spaceName). Tuples(tuples). @@ -310,7 +310,7 @@ var testGenerateDataCases = []struct { }, { "ReplaceObjectMany", - 2, + 1, 10, crud.MakeReplaceObjectManyRequest(spaceName). Objects(objects). @@ -318,7 +318,7 @@ var testGenerateDataCases = []struct { }, { "Upsert", - 2, + 1, 1, crud.MakeUpsertRequest(spaceName). Tuple(tuple). @@ -327,7 +327,7 @@ var testGenerateDataCases = []struct { }, { "UpsertObject", - 2, + 1, 1, crud.MakeUpsertObjectRequest(spaceName). Object(object). @@ -336,7 +336,7 @@ var testGenerateDataCases = []struct { }, { "UpsertMany", - 2, + 1, 10, crud.MakeUpsertManyRequest(spaceName). TuplesOperationsData(tuplesOperationsData). @@ -344,7 +344,7 @@ var testGenerateDataCases = []struct { }, { "UpsertObjectMany", - 2, + 1, 10, crud.MakeUpsertObjectManyRequest(spaceName). ObjectsOperationsData(objectsOperationData). @@ -475,7 +475,7 @@ func testCrudRequestCheck(t *testing.T, req tarantool.Request, // resp.Data[0] - CRUD res. // resp.Data[1] - CRUD err. - if expectedLen >= 2 { + if expectedLen >= 2 && resp.Data[1] != nil { if crudErr, err := getCrudError(req, resp.Data[1]); err != nil { t.Fatalf("Failed to get CRUD error: %#v", err) } else if crudErr != nil { From a30080037ba3c2c98003771546c5bd092e174699 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Fri, 6 Oct 2023 13:49:00 +0300 Subject: [PATCH 484/605] crud: support noreturn option `noreturn` option was introduced in crud 1.2.0 [1]. 1. https://github.com/tarantool/crud/pull/356/commits/af0ce90536aafd277608897eaf01c3ba42adeaec --- CHANGELOG.md | 1 + crud/example_test.go | 27 ++++++ crud/options.go | 35 ++++++-- crud/tarantool_test.go | 195 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fc71aac7..2dc22ecd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support password and password file to decrypt private SSL key file (#319) - Support `operation_data` in `crud.Error` (#330) - Support `fetch_latest_metadata` option for crud requests with metadata (#335) +- Support `noreturn` option for data change crud requests (#335) ### Changed diff --git a/crud/example_test.go b/crud/example_test.go index 363d0570d..96185756c 100644 --- a/crud/example_test.go +++ b/crud/example_test.go @@ -187,6 +187,33 @@ func ExampleResult_many() { // [[2010 45 bla] [2011 4 bla]] } +// ExampleResult_noreturn demonstrates noreturn request: a data change +// request where you don't need to retrieve the result, just want to know +// whether it was successful or not. +func ExampleResult_noreturn() { + conn := exampleConnect() + req := crud.MakeReplaceManyRequest(exampleSpace). + Tuples([]crud.Tuple{ + []interface{}{uint(2010), nil, "bla"}, + []interface{}{uint(2011), nil, "bla"}, + }). + Opts(crud.ReplaceManyOpts{ + Noreturn: crud.MakeOptBool(true), + }) + + ret := crud.Result{} + if err := conn.Do(req).GetTyped(&ret); err != nil { + fmt.Printf("Failed to execute request: %s", err) + return + } + + fmt.Println(ret.Metadata) + fmt.Println(ret.Rows) + // Output: + // [] + // +} + // ExampleResult_error demonstrates how to use a helper type Result // to handle a crud error. func ExampleResult_error() { diff --git a/crud/options.go b/crud/options.go index 8a6997380..214417d1f 100644 --- a/crud/options.go +++ b/crud/options.go @@ -22,6 +22,7 @@ const ( afterOptName = "after" batchSizeOptName = "batch_size" fetchLatestMetadataOptName = "fetch_latest_metadata" + noreturnOptName = "noreturn" ) // OptUint is an optional uint. @@ -156,14 +157,18 @@ type SimpleOperationOpts struct { // the latest migration of the data format. Performance overhead is up to 15%. // Disabled by default. FetchLatestMetadata OptBool + // Noreturn suppresses successfully processed data (first return value is `nil`). + // Disabled by default. + Noreturn OptBool } // EncodeMsgpack provides custom msgpack encoder. func (opts SimpleOperationOpts) EncodeMsgpack(enc *msgpack.Encoder) error { - const optsCnt = 5 + const optsCnt = 6 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, - fieldsOptName, bucketIdOptName, fetchLatestMetadataOptName} + fieldsOptName, bucketIdOptName, fetchLatestMetadataOptName, + noreturnOptName} values := [optsCnt]interface{}{} exists := [optsCnt]bool{} values[0], exists[0] = opts.Timeout.Get() @@ -171,6 +176,7 @@ func (opts SimpleOperationOpts) EncodeMsgpack(enc *msgpack.Encoder) error { values[2], exists[2] = opts.Fields.Get() values[3], exists[3] = opts.BucketId.Get() values[4], exists[4] = opts.FetchLatestMetadata.Get() + values[5], exists[5] = opts.Noreturn.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } @@ -196,15 +202,18 @@ type SimpleOperationObjectOpts struct { // the latest migration of the data format. Performance overhead is up to 15%. // Disabled by default. FetchLatestMetadata OptBool + // Noreturn suppresses successfully processed data (first return value is `nil`). + // Disabled by default. + Noreturn OptBool } // EncodeMsgpack provides custom msgpack encoder. func (opts SimpleOperationObjectOpts) EncodeMsgpack(enc *msgpack.Encoder) error { - const optsCnt = 6 + const optsCnt = 7 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, bucketIdOptName, skipNullabilityCheckOnFlattenOptName, - fetchLatestMetadataOptName} + fetchLatestMetadataOptName, noreturnOptName} values := [optsCnt]interface{}{} exists := [optsCnt]bool{} values[0], exists[0] = opts.Timeout.Get() @@ -213,6 +222,7 @@ func (opts SimpleOperationObjectOpts) EncodeMsgpack(enc *msgpack.Encoder) error values[3], exists[3] = opts.BucketId.Get() values[4], exists[4] = opts.SkipNullabilityCheckOnFlatten.Get() values[5], exists[5] = opts.FetchLatestMetadata.Get() + values[6], exists[6] = opts.Noreturn.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } @@ -240,15 +250,18 @@ type OperationManyOpts struct { // the latest migration of the data format. Performance overhead is up to 15%. // Disabled by default. FetchLatestMetadata OptBool + // Noreturn suppresses successfully processed data (first return value is `nil`). + // Disabled by default. + Noreturn OptBool } // EncodeMsgpack provides custom msgpack encoder. func (opts OperationManyOpts) EncodeMsgpack(enc *msgpack.Encoder) error { - const optsCnt = 6 + const optsCnt = 7 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, stopOnErrorOptName, rollbackOnErrorOptName, - fetchLatestMetadataOptName} + fetchLatestMetadataOptName, noreturnOptName} values := [optsCnt]interface{}{} exists := [optsCnt]bool{} values[0], exists[0] = opts.Timeout.Get() @@ -257,6 +270,7 @@ func (opts OperationManyOpts) EncodeMsgpack(enc *msgpack.Encoder) error { values[3], exists[3] = opts.StopOnError.Get() values[4], exists[4] = opts.RollbackOnError.Get() values[5], exists[5] = opts.FetchLatestMetadata.Get() + values[6], exists[6] = opts.Noreturn.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } @@ -287,15 +301,19 @@ type OperationObjectManyOpts struct { // the latest migration of the data format. Performance overhead is up to 15%. // Disabled by default. FetchLatestMetadata OptBool + // Noreturn suppresses successfully processed data (first return value is `nil`). + // Disabled by default. + Noreturn OptBool } // EncodeMsgpack provides custom msgpack encoder. func (opts OperationObjectManyOpts) EncodeMsgpack(enc *msgpack.Encoder) error { - const optsCnt = 7 + const optsCnt = 8 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, stopOnErrorOptName, rollbackOnErrorOptName, - skipNullabilityCheckOnFlattenOptName, fetchLatestMetadataOptName} + skipNullabilityCheckOnFlattenOptName, fetchLatestMetadataOptName, + noreturnOptName} values := [optsCnt]interface{}{} exists := [optsCnt]bool{} values[0], exists[0] = opts.Timeout.Get() @@ -305,6 +323,7 @@ func (opts OperationObjectManyOpts) EncodeMsgpack(enc *msgpack.Encoder) error { values[4], exists[4] = opts.RollbackOnError.Get() values[5], exists[5] = opts.SkipNullabilityCheckOnFlatten.Get() values[6], exists[6] = opts.FetchLatestMetadata.Get() + values[7], exists[7] = opts.Noreturn.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 88572b56f..b361579ae 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -1012,6 +1012,201 @@ func TestFetchLatestMetadataOption(t *testing.T) { } } +var testNoreturnCases = []struct { + name string + req tarantool.Request +}{ + { + "Insert", + crud.MakeInsertRequest(spaceName). + Tuple(tuple). + Opts(crud.InsertOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, + { + "InsertObject", + crud.MakeInsertObjectRequest(spaceName). + Object(object). + Opts(crud.InsertObjectOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, + { + "InsertMany", + crud.MakeInsertManyRequest(spaceName). + Tuples(tuples). + Opts(crud.InsertManyOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, + { + "InsertObjectMany", + crud.MakeInsertObjectManyRequest(spaceName). + Objects(objects). + Opts(crud.InsertObjectManyOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, + { + "Replace", + crud.MakeReplaceRequest(spaceName). + Tuple(tuple). + Opts(crud.ReplaceOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, + { + "ReplaceObject", + crud.MakeReplaceObjectRequest(spaceName). + Object(object). + Opts(crud.ReplaceObjectOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, + { + "ReplaceMany", + crud.MakeReplaceManyRequest(spaceName). + Tuples(tuples). + Opts(crud.ReplaceManyOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, + { + "ReplaceObjectMany", + crud.MakeReplaceObjectManyRequest(spaceName). + Objects(objects). + Opts(crud.ReplaceObjectManyOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, + { + "Upsert", + crud.MakeUpsertRequest(spaceName). + Tuple(tuple). + Operations(operations). + Opts(crud.UpsertOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, + { + "UpsertObject", + crud.MakeUpsertObjectRequest(spaceName). + Object(object). + Operations(operations). + Opts(crud.UpsertObjectOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, + { + "UpsertMany", + crud.MakeUpsertManyRequest(spaceName). + TuplesOperationsData(tuplesOperationsData). + Opts(crud.UpsertManyOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, + { + "UpsertObjectMany", + crud.MakeUpsertObjectManyRequest(spaceName). + ObjectsOperationsData(objectsOperationData). + Opts(crud.UpsertObjectManyOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, + { + "Update", + crud.MakeUpdateRequest(spaceName). + Key(key). + Operations(operations). + Opts(crud.UpdateOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, + { + "Delete", + crud.MakeDeleteRequest(spaceName). + Key(key). + Opts(crud.DeleteOpts{ + Noreturn: crud.MakeOptBool(true), + }), + }, +} + +func TestNoreturnOption(t *testing.T) { + conn := connect(t) + defer conn.Close() + + for _, testCase := range testNoreturnCases { + t.Run(testCase.name, func(t *testing.T) { + for i := 1010; i < 1020; i++ { + req := tarantool.NewDeleteRequest(spaceName). + Key([]interface{}{uint(i)}) + conn.Do(req).Get() + } + + resp, err := conn.Do(testCase.req).Get() + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err) + } + + if len(resp.Data) == 0 { + t.Fatalf("Expected explicit nil") + } + + if resp.Data[0] != nil { + t.Fatalf("Expected nil result, got %v", resp.Data[0]) + } + + if len(resp.Data) >= 2 && resp.Data[1] != nil { + t.Fatalf("Expected no returned errors, got %v", resp.Data[1]) + } + + for i := 1010; i < 1020; i++ { + req := tarantool.NewDeleteRequest(spaceName). + Key([]interface{}{uint(i)}) + conn.Do(req).Get() + } + }) + } +} + +func TestNoreturnOptionTyped(t *testing.T) { + conn := connect(t) + defer conn.Close() + + for _, testCase := range testNoreturnCases { + t.Run(testCase.name, func(t *testing.T) { + for i := 1010; i < 1020; i++ { + req := tarantool.NewDeleteRequest(spaceName). + Key([]interface{}{uint(i)}) + conn.Do(req).Get() + } + + resp := crud.Result{} + + err := conn.Do(testCase.req).GetTyped(&resp) + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err) + } + + if resp.Rows != nil { + t.Fatalf("Expected nil rows, got %v", resp.Rows) + } + + if len(resp.Metadata) != 0 { + t.Fatalf("Expected no metadata") + } + + for i := 1010; i < 1020; i++ { + req := tarantool.NewDeleteRequest(spaceName). + Key([]interface{}{uint(i)}) + conn.Do(req).Get() + } + }) + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body From 852ec7e207a47be873cf1c31801650cd78c752f4 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Fri, 6 Oct 2023 18:20:32 +0300 Subject: [PATCH 485/605] doc: clarify crud error process decoding It is a bit confusing for non-experienced vmihailenco/msgpack users than `nil` values are actually parsed by `DecodeMapLen`, `DecodeUint64` and `DecodeBool` methods to some default value. So I decided to leave a note here. --- crud/result.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crud/result.go b/crud/result.go index cc875a5de..7ae00e68f 100644 --- a/crud/result.go +++ b/crud/result.go @@ -79,6 +79,9 @@ func (r *Result) DecodeMsgpack(d *msgpack.Decoder) error { return fmt.Errorf("unexpected empty response array") } + // DecodeMapLen processes `nil` as zero length map, + // so in `return nil, err` case we don't miss error info. + // https://github.com/vmihailenco/msgpack/blob/3f7bd806fea698e7a9fe80979aa3512dea0a7368/decode_map.go#L79-L81 l, err := d.DecodeMapLen() if err != nil { return err @@ -184,6 +187,9 @@ func (r *NumberResult) DecodeMsgpack(d *msgpack.Decoder) error { return fmt.Errorf("unexpected empty response array") } + // DecodeUint64 processes `nil` as `0`, + // so in `return nil, err` case we don't miss error info. + // https://github.com/vmihailenco/msgpack/blob/3f7bd806fea698e7a9fe80979aa3512dea0a7368/decode_number.go#L91-L93 if r.Value, err = d.DecodeUint64(); err != nil { return err } @@ -225,6 +231,9 @@ func (r *BoolResult) DecodeMsgpack(d *msgpack.Decoder) error { return fmt.Errorf("unexpected empty response array") } + // DecodeBool processes `nil` as `false`, + // so in `return nil, err` case we don't miss error info. + // https://github.com/vmihailenco/msgpack/blob/3f7bd806fea698e7a9fe80979aa3512dea0a7368/decode.go#L367-L369 if r.Value, err = d.DecodeBool(); err != nil { return err } From 807591494659d63ed55687d249b1978d4890cbb0 Mon Sep 17 00:00:00 2001 From: DerekBum Date: Tue, 26 Sep 2023 15:51:19 +0300 Subject: [PATCH 486/605] api: add context to connection create `connection.Connect` and `pool.Connect` no longer return non-working connection objects. Those functions now accept context as their first arguments, which user may cancel in process. `connection.Connect` will block until either the working connection created (and returned), `opts.MaxReconnects` creation attempts were made (returns error) or the context is canceled by user (returns error too). Closes #136 --- CHANGELOG.md | 7 + README.md | 32 ++- connection.go | 125 +++++----- crud/example_test.go | 5 +- crud/tarantool_test.go | 4 +- datetime/example_test.go | 5 +- decimal/example_test.go | 13 +- dial.go | 16 +- dial_test.go | 60 +++-- example_custom_unpacking_test.go | 13 +- example_test.go | 55 ++++- export_test.go | 5 +- go.mod | 2 +- go.sum | 4 +- pool/connection_pool.go | 129 +++++++---- pool/connection_pool_test.go | 315 +++++++++++++++++++++----- pool/example_test.go | 4 +- queue/example_connection_pool_test.go | 5 +- queue/example_msgpack_test.go | 5 +- queue/example_test.go | 5 +- settings/example_test.go | 6 +- shutdown_test.go | 5 +- ssl.go | 10 +- ssl_disable.go | 4 +- ssl_test.go | 66 ++++-- tarantool_test.go | 58 ++++- test_helpers/main.go | 9 +- test_helpers/pool_helper.go | 24 +- test_helpers/utils.go | 4 +- uuid/example_test.go | 6 +- 30 files changed, 738 insertions(+), 263 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc22ecd2..a26ba7265 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,13 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. decoded to a varbinary object (#313). - Use objects of the Decimal type instead of pointers (#238) - Use objects of the Datetime type instead of pointers (#238) +- `connection.Connect` no longer return non-working + connection objects (#136). This function now does not attempt to reconnect + and tries to establish a connection only once. Function might be canceled + via context. Context accepted as first argument. + `pool.Connect` and `pool.Add` now accept context as first argument, which + user may cancel in process. If `pool.Connect` is canceled in progress, an + error will be returned. All created connections will be closed. ### Deprecated diff --git a/README.md b/README.md index aa4c6deac..378c344b3 100644 --- a/README.md +++ b/README.md @@ -105,13 +105,19 @@ about what it does. package tarantool import ( + "context" "fmt" + "time" + "github.com/tarantool/go-tarantool/v2" ) func main() { opts := tarantool.Opts{User: "guest"} - conn, err := tarantool.Connect("127.0.0.1:3301", opts) + ctx, cancel := context.WithTimeout(context.Background(), + 500 * time.Millisecond) + defer cancel() + conn, err := tarantool.Connect(ctx, "127.0.0.1:3301", opts) if err != nil { fmt.Println("Connection refused:", err) } @@ -134,11 +140,17 @@ username. The structure may also contain other settings, see more in [documentation][godoc-opts-url] for the "`Opts`" structure. **Observation 3:** The line containing "`tarantool.Connect`" is essential for -starting a session. There are two parameters: +starting a session. There are three parameters: -* a string with `host:port` format, and +* a context, +* a string with `host:port` format, * the option structure that was set up earlier. +There will be only one attempt to connect. If multiple attempts needed, +"`tarantool.Connect`" could be placed inside the loop with some timeout +between each try. Example could be found in the [example_test](./example_test.go), +name - `ExampleConnect_reconnects`. + **Observation 4:** The `err` structure will be `nil` if there is no error, otherwise it will have a description which can be retrieved with `err.Error()`. @@ -167,10 +179,13 @@ The subpackage has been deleted. You could use `pool` instead. #### pool package -The logic has not changed, but there are a few renames: - * The `connection_pool` subpackage has been renamed to `pool`. * The type `PoolOpts` has been renamed to `Opts`. +* `pool.Connect` now accepts context as first argument, which user may cancel + in process. If it is canceled in progress, an error will be returned. + All created connections will be closed. +* `pool.Add` now accepts context as first argument, which user may cancel in + process. #### msgpack.v5 @@ -212,6 +227,13 @@ IPROTO constants have been moved to a separate package [go-iproto](https://githu * The method `Code() uint32` replaced by the `Type() iproto.Type`. +#### Connect function + +`connection.Connect` no longer return non-working connection objects. This function +now does not attempt to reconnect and tries to establish a connection only once. +Function might be canceled via context. Context accepted as first argument, +and user may cancel it in process. + ## Contributing See [the contributing guide](CONTRIBUTING.md) for detailed instructions on how diff --git a/connection.go b/connection.go index 9bb42626a..f304a12ba 100644 --- a/connection.go +++ b/connection.go @@ -375,16 +375,7 @@ func (opts Opts) Clone() Opts { // - Unix socket, first '/' or '.' indicates Unix socket // (unix:///abs/path/tnt.sock, unix:path/tnt.sock, /abs/path/tnt.sock, // ./rel/path/tnt.sock, unix/:path/tnt.sock) -// -// Notes: -// -// - If opts.Reconnect is zero (default), then connection either already connected -// or error is returned. -// -// - If opts.Reconnect is non-zero, then error will be returned only if authorization -// fails. But if Tarantool is not reachable, then it will make an attempt to reconnect later -// and will not finish to make attempts on authorization failures. -func Connect(addr string, opts Opts) (conn *Connection, err error) { +func Connect(ctx context.Context, addr string, opts Opts) (conn *Connection, err error) { conn = &Connection{ addr: addr, requestId: 0, @@ -432,25 +423,8 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) { conn.cond = sync.NewCond(&conn.mutex) - if err = conn.createConnection(false); err != nil { - ter, ok := err.(Error) - if conn.opts.Reconnect <= 0 { - return nil, err - } else if ok && (ter.Code == iproto.ER_NO_SUCH_USER || - ter.Code == iproto.ER_CREDS_MISMATCH) { - // Reported auth errors immediately. - return nil, err - } else { - // Without SkipSchema it is useless. - go func(conn *Connection) { - conn.mutex.Lock() - defer conn.mutex.Unlock() - if err := conn.createConnection(true); err != nil { - conn.closeConnection(err, true) - } - }(conn) - err = nil - } + if err = conn.createConnection(ctx); err != nil { + return nil, err } go conn.pinger() @@ -534,18 +508,11 @@ func (conn *Connection) cancelFuture(fut *Future, err error) { } } -func (conn *Connection) dial() (err error) { +func (conn *Connection) dial(ctx context.Context) error { opts := conn.opts - dialTimeout := opts.Reconnect / 2 - if dialTimeout == 0 { - dialTimeout = 500 * time.Millisecond - } else if dialTimeout > 5*time.Second { - dialTimeout = 5 * time.Second - } var c Conn - c, err = conn.opts.Dialer.Dial(conn.addr, DialOpts{ - DialTimeout: dialTimeout, + c, err := conn.opts.Dialer.Dial(ctx, conn.addr, DialOpts{ IoTimeout: opts.Timeout, Transport: opts.Transport, Ssl: opts.Ssl, @@ -555,7 +522,7 @@ func (conn *Connection) dial() (err error) { Password: opts.Pass, }) if err != nil { - return + return err } conn.Greeting.Version = c.Greeting().Version @@ -605,7 +572,7 @@ func (conn *Connection) dial() (err error) { conn.shutdownWatcher = watcher } - return + return nil } func pack(h *smallWBuf, enc *msgpack.Encoder, reqid uint32, @@ -658,34 +625,18 @@ func pack(h *smallWBuf, enc *msgpack.Encoder, reqid uint32, return } -func (conn *Connection) createConnection(reconnect bool) (err error) { - var reconnects uint - for conn.c == nil && conn.state == connDisconnected { - now := time.Now() - err = conn.dial() - if err == nil || !reconnect { - if err == nil { - conn.notify(Connected) - } - return - } - if conn.opts.MaxReconnects > 0 && reconnects > conn.opts.MaxReconnects { - conn.opts.Logger.Report(LogLastReconnectFailed, conn, err) - err = ClientError{ErrConnectionClosed, "last reconnect failed"} - // mark connection as closed to avoid reopening by another goroutine - return +func (conn *Connection) createConnection(ctx context.Context) error { + var err error + if conn.c == nil && conn.state == connDisconnected { + if err = conn.dial(ctx); err == nil { + conn.notify(Connected) + return nil } - conn.opts.Logger.Report(LogReconnectFailed, conn, reconnects, err) - conn.notify(ReconnectFailed) - reconnects++ - conn.mutex.Unlock() - time.Sleep(time.Until(now.Add(conn.opts.Reconnect))) - conn.mutex.Lock() } if conn.state == connClosed { err = ClientError{ErrConnectionClosed, "using closed connection"} } - return + return err } func (conn *Connection) closeConnection(neterr error, forever bool) (err error) { @@ -727,11 +678,57 @@ func (conn *Connection) closeConnection(neterr error, forever bool) (err error) return } +func (conn *Connection) getDialTimeout() time.Duration { + dialTimeout := conn.opts.Reconnect / 2 + if dialTimeout == 0 { + dialTimeout = 500 * time.Millisecond + } else if dialTimeout > 5*time.Second { + dialTimeout = 5 * time.Second + } + return dialTimeout +} + +func (conn *Connection) runReconnects() error { + dialTimeout := conn.getDialTimeout() + var reconnects uint + var err error + + for conn.opts.MaxReconnects == 0 || reconnects <= conn.opts.MaxReconnects { + now := time.Now() + + ctx, cancel := context.WithTimeout(context.Background(), dialTimeout) + err = conn.createConnection(ctx) + cancel() + + if err != nil { + if clientErr, ok := err.(ClientError); ok && + clientErr.Code == ErrConnectionClosed { + return err + } + } else { + return nil + } + + conn.opts.Logger.Report(LogReconnectFailed, conn, reconnects, err) + conn.notify(ReconnectFailed) + reconnects++ + conn.mutex.Unlock() + + time.Sleep(time.Until(now.Add(conn.opts.Reconnect))) + + conn.mutex.Lock() + } + + conn.opts.Logger.Report(LogLastReconnectFailed, conn, err) + // mark connection as closed to avoid reopening by another goroutine + return ClientError{ErrConnectionClosed, "last reconnect failed"} +} + func (conn *Connection) reconnectImpl(neterr error, c Conn) { if conn.opts.Reconnect > 0 { if c == conn.c { conn.closeConnection(neterr, false) - if err := conn.createConnection(true); err != nil { + if err := conn.runReconnects(); err != nil { conn.closeConnection(err, true) } } diff --git a/crud/example_test.go b/crud/example_test.go index 96185756c..cb15aca4e 100644 --- a/crud/example_test.go +++ b/crud/example_test.go @@ -1,6 +1,7 @@ package crud_test import ( + "context" "fmt" "reflect" "time" @@ -21,7 +22,9 @@ var exampleOpts = tarantool.Opts{ } func exampleConnect() *tarantool.Connection { - conn, err := tarantool.Connect(exampleServer, exampleOpts) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + conn, err := tarantool.Connect(ctx, exampleServer, exampleOpts) if err != nil { panic("Connection is not established: " + err.Error()) } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index b361579ae..61425f144 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -108,7 +108,9 @@ var object = crud.MapObject{ func connect(t testing.TB) *tarantool.Connection { for i := 0; i < 10; i++ { - conn, err := tarantool.Connect(server, opts) + ctx, cancel := test_helpers.GetConnectContext() + conn, err := tarantool.Connect(ctx, server, opts) + cancel() if err != nil { t.Fatalf("Failed to connect: %s", err) } diff --git a/datetime/example_test.go b/datetime/example_test.go index 346551629..954f43548 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -9,6 +9,7 @@ package datetime_test import ( + "context" "fmt" "time" @@ -23,7 +24,9 @@ func Example() { User: "test", Pass: "test", } - conn, err := tarantool.Connect("127.0.0.1:3013", opts) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + conn, err := tarantool.Connect(ctx, "127.0.0.1:3013", opts) if err != nil { fmt.Printf("Error in connect is %v", err) return diff --git a/decimal/example_test.go b/decimal/example_test.go index a355767f1..f1984283c 100644 --- a/decimal/example_test.go +++ b/decimal/example_test.go @@ -9,6 +9,7 @@ package decimal_test import ( + "context" "log" "time" @@ -22,13 +23,13 @@ import ( func Example() { server := "127.0.0.1:3013" opts := tarantool.Opts{ - Timeout: 5 * time.Second, - Reconnect: 1 * time.Second, - MaxReconnects: 3, - User: "test", - Pass: "test", + Timeout: 5 * time.Second, + User: "test", + Pass: "test", } - client, err := tarantool.Connect(server, opts) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + client, err := tarantool.Connect(ctx, server, opts) + cancel() if err != nil { log.Fatalf("Failed to connect: %s", err.Error()) } diff --git a/dial.go b/dial.go index 3ba493ac7..5b17c0534 100644 --- a/dial.go +++ b/dial.go @@ -3,6 +3,7 @@ package tarantool import ( "bufio" "bytes" + "context" "errors" "fmt" "io" @@ -56,8 +57,6 @@ type Conn interface { // DialOpts is a way to configure a Dial method to create a new Conn. type DialOpts struct { - // DialTimeout is a timeout for an initial network dial. - DialTimeout time.Duration // IoTimeout is a timeout per a network read/write. IoTimeout time.Duration // Transport is a connect transport type. @@ -86,7 +85,7 @@ type DialOpts struct { type Dialer interface { // Dial connects to a Tarantool instance to the address with specified // options. - Dial(address string, opts DialOpts) (Conn, error) + Dial(ctx context.Context, address string, opts DialOpts) (Conn, error) } type tntConn struct { @@ -104,11 +103,11 @@ type TtDialer struct { // Dial connects to a Tarantool instance to the address with specified // options. -func (t TtDialer) Dial(address string, opts DialOpts) (Conn, error) { +func (t TtDialer) Dial(ctx context.Context, address string, opts DialOpts) (Conn, error) { var err error conn := new(tntConn) - if conn.net, err = dial(address, opts); err != nil { + if conn.net, err = dial(ctx, address, opts); err != nil { return nil, fmt.Errorf("failed to dial: %w", err) } @@ -199,13 +198,14 @@ func (c *tntConn) ProtocolInfo() ProtocolInfo { } // dial connects to a Tarantool instance. -func dial(address string, opts DialOpts) (net.Conn, error) { +func dial(ctx context.Context, address string, opts DialOpts) (net.Conn, error) { network, address := parseAddress(address) switch opts.Transport { case dialTransportNone: - return net.DialTimeout(network, address, opts.DialTimeout) + dialer := net.Dialer{} + return dialer.DialContext(ctx, network, address) case dialTransportSsl: - return sslDialTimeout(network, address, opts.DialTimeout, opts.Ssl) + return sslDialContext(ctx, network, address, opts.Ssl) default: return nil, fmt.Errorf("unsupported transport type: %s", opts.Transport) } diff --git a/dial_test.go b/dial_test.go index ff8a50aab..acd6737c5 100644 --- a/dial_test.go +++ b/dial_test.go @@ -2,8 +2,11 @@ package tarantool_test import ( "bytes" + "context" "errors" + "fmt" "net" + "strings" "sync" "testing" "time" @@ -12,13 +15,14 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) type mockErrorDialer struct { err error } -func (m mockErrorDialer) Dial(address string, +func (m mockErrorDialer) Dial(ctx context.Context, address string, opts tarantool.DialOpts) (tarantool.Conn, error) { return nil, m.err } @@ -29,7 +33,9 @@ func TestDialer_Dial_error(t *testing.T) { err: errors.New(errMsg), } - conn, err := tarantool.Connect("any", tarantool.Opts{ + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := tarantool.Connect(ctx, "any", tarantool.Opts{ Dialer: dialer, }) assert.Nil(t, conn) @@ -37,23 +43,26 @@ func TestDialer_Dial_error(t *testing.T) { } type mockPassedDialer struct { + ctx context.Context address string opts tarantool.DialOpts } -func (m *mockPassedDialer) Dial(address string, +func (m *mockPassedDialer) Dial(ctx context.Context, address string, opts tarantool.DialOpts) (tarantool.Conn, error) { m.address = address m.opts = opts + if ctx != m.ctx { + return nil, errors.New("wrong context") + } return nil, errors.New("does not matter") } func TestDialer_Dial_passedOpts(t *testing.T) { const addr = "127.0.0.1:8080" opts := tarantool.DialOpts{ - DialTimeout: 500 * time.Millisecond, - IoTimeout: 2, - Transport: "any", + IoTimeout: 2, + Transport: "any", Ssl: tarantool.SslOpts{ KeyFile: "a", CertFile: "b", @@ -73,7 +82,12 @@ func TestDialer_Dial_passedOpts(t *testing.T) { } dialer := &mockPassedDialer{} - conn, err := tarantool.Connect(addr, tarantool.Opts{ + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + + dialer.ctx = ctx + + conn, err := tarantool.Connect(ctx, addr, tarantool.Opts{ Dialer: dialer, Timeout: opts.IoTimeout, Transport: opts.Transport, @@ -86,6 +100,7 @@ func TestDialer_Dial_passedOpts(t *testing.T) { assert.Nil(t, conn) assert.NotNil(t, err) + assert.NotEqual(t, err.Error(), "wrong context") assert.Equal(t, addr, dialer.address) assert.Equal(t, opts, dialer.opts) } @@ -187,7 +202,7 @@ func newMockIoConn() *mockIoConn { return conn } -func (m *mockIoDialer) Dial(address string, +func (m *mockIoDialer) Dial(ctx context.Context, address string, opts tarantool.DialOpts) (tarantool.Conn, error) { m.conn = newMockIoConn() if m.init != nil { @@ -203,11 +218,14 @@ func dialIo(t *testing.T, dialer := mockIoDialer{ init: init, } - conn, err := tarantool.Connect("any", tarantool.Opts{ - Dialer: &dialer, - Timeout: 1000 * time.Second, // Avoid pings. - SkipSchema: true, - }) + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := tarantool.Connect(ctx, "any", + tarantool.Opts{ + Dialer: &dialer, + Timeout: 1000 * time.Second, // Avoid pings. + SkipSchema: true, + }) require.Nil(t, err) require.NotNil(t, conn) @@ -338,3 +356,19 @@ func TestConn_ReadWrite(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, resp) } + +func TestConn_ContextCancel(t *testing.T) { + const addr = "127.0.0.1:8080" + + dialer := tarantool.TtDialer{} + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + conn, err := dialer.Dial(ctx, addr, tarantool.DialOpts{}) + + assert.Nil(t, conn) + assert.NotNil(t, err) + assert.Truef(t, strings.Contains(err.Error(), "operation was canceled"), + fmt.Sprintf("unexpected error, expected to contain %s, got %v", + "operation was canceled", err)) +} diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index 1189e16a3..8fb243db0 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -1,6 +1,7 @@ package tarantool_test import ( + "context" "fmt" "log" "time" @@ -78,13 +79,13 @@ func Example_customUnpacking() { // Establish a connection. server := "127.0.0.1:3013" opts := tarantool.Opts{ - Timeout: 500 * time.Millisecond, - Reconnect: 1 * time.Second, - MaxReconnects: 3, - User: "test", - Pass: "test", + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", } - conn, err := tarantool.Connect(server, opts) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + conn, err := tarantool.Connect(ctx, server, opts) + cancel() if err != nil { log.Fatalf("Failed to connect: %s", err.Error()) } diff --git a/example_test.go b/example_test.go index d871d578a..77b2cff24 100644 --- a/example_test.go +++ b/example_test.go @@ -19,7 +19,9 @@ type Tuple struct { } func exampleConnect(opts tarantool.Opts) *tarantool.Connection { - conn, err := tarantool.Connect(server, opts) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + conn, err := tarantool.Connect(ctx, server, opts) if err != nil { panic("Connection is not established: " + err.Error()) } @@ -38,7 +40,9 @@ func ExampleSslOpts() { CaFile: "testdata/ca.crt", }, } - _, err := tarantool.Connect("127.0.0.1:3013", opts) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + _, err := tarantool.Connect(ctx, "127.0.0.1:3013", opts) if err != nil { panic("Connection is not established: " + err.Error()) } @@ -913,7 +917,10 @@ func ExampleFuture_GetIterator() { } func ExampleConnect() { - conn, err := tarantool.Connect("127.0.0.1:3013", tarantool.Opts{ + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + conn, err := tarantool.Connect(ctx, "127.0.0.1:3013", tarantool.Opts{ Timeout: 5 * time.Second, User: "test", Pass: "test", @@ -931,6 +938,40 @@ func ExampleConnect() { // Connection is ready } +func ExampleConnect_reconnects() { + opts := tarantool.Opts{ + Timeout: 5 * time.Second, + User: "test", + Pass: "test", + Concurrency: 32, + Reconnect: time.Second, + MaxReconnects: 10, + } + + var conn *tarantool.Connection + var err error + + for i := uint(0); i < opts.MaxReconnects; i++ { + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + conn, err = tarantool.Connect(ctx, "127.0.0.1:3013", opts) + cancel() + if err == nil { + break + } + time.Sleep(opts.Reconnect) + } + if err != nil { + fmt.Println("No connection available") + return + } + defer conn.Close() + if conn != nil { + fmt.Println("Connection is ready") + } + // Output: + // Connection is ready +} + // Example demonstrates how to retrieve information with space schema. func ExampleSchema() { conn := exampleConnect(opts) @@ -1081,7 +1122,9 @@ func ExampleConnection_NewPrepared() { User: "test", Pass: "test", } - conn, err := tarantool.Connect(server, opts) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + conn, err := tarantool.Connect(ctx, server, opts) if err != nil { fmt.Printf("Failed to connect: %s", err.Error()) } @@ -1127,7 +1170,9 @@ func ExampleConnection_NewWatcher() { Features: []tarantool.ProtocolFeature{tarantool.WatchersFeature}, }, } - conn, err := tarantool.Connect(server, opts) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + conn, err := tarantool.Connect(ctx, server, opts) if err != nil { fmt.Printf("Failed to connect: %s\n", err) return diff --git a/export_test.go b/export_test.go index 10d194840..fc5d90c34 100644 --- a/export_test.go +++ b/export_test.go @@ -1,15 +1,16 @@ package tarantool import ( + "context" "net" "time" "github.com/vmihailenco/msgpack/v5" ) -func SslDialTimeout(network, address string, timeout time.Duration, +func SslDialContext(ctx context.Context, network, address string, opts SslOpts) (connection net.Conn, err error) { - return sslDialTimeout(network, address, timeout, opts) + return sslDialContext(ctx, network, address, opts) } func SslCreateContext(opts SslOpts) (ctx interface{}, err error) { diff --git a/go.mod b/go.mod index bd848308c..22cb7aee3 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.7.1 github.com/tarantool/go-iproto v0.1.0 - github.com/tarantool/go-openssl v0.0.8-0.20230801114713-b452431f934a + github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca github.com/vmihailenco/msgpack/v5 v5.3.5 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect diff --git a/go.sum b/go.sum index 1810c2b3a..44d00984f 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tarantool/go-iproto v0.1.0 h1:zHN9AA8LDawT+JBD0/Nxgr/bIsWkkpDzpcMuaNPSIAQ= github.com/tarantool/go-iproto v0.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= -github.com/tarantool/go-openssl v0.0.8-0.20230801114713-b452431f934a h1:eeElglRXJ3xWKkHmDbeXrQWlZyQ4t3Ca1YlZsrfdXFU= -github.com/tarantool/go-openssl v0.0.8-0.20230801114713-b452431f934a/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= +github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca h1:oOrBh73tDDyooIXajfr+0pfnM+89404ClAhJpTTHI7E= +github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 26e2199e9..e101b3208 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -11,6 +11,7 @@ package pool import ( + "context" "errors" "log" "sync" @@ -33,6 +34,7 @@ var ( ErrClosed = errors.New("pool is closed") ErrUnknownRequest = errors.New("the passed connected request doesn't belong to " + "the current connection pool") + ErrContextCanceled = errors.New("operation was canceled") ) // ConnectionHandler provides callbacks for components interested in handling @@ -116,6 +118,7 @@ type endpoint struct { shutdown chan struct{} close chan struct{} closed chan struct{} + cancel context.CancelFunc closeErr error } @@ -128,12 +131,14 @@ func newEndpoint(addr string) *endpoint { shutdown: make(chan struct{}), close: make(chan struct{}), closed: make(chan struct{}), + cancel: nil, } } // ConnectWithOpts creates pool for instances with addresses addrs // with options opts. -func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts Opts) (*ConnectionPool, error) { +func ConnectWithOpts(ctx context.Context, addrs []string, + connOpts tarantool.Opts, opts Opts) (*ConnectionPool, error) { if len(addrs) == 0 { return nil, ErrEmptyAddrs } @@ -161,16 +166,21 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts Opts) (*Conne connPool.addrs[addr] = nil } - somebodyAlive := connPool.fillPools() + somebodyAlive, ctxCanceled := connPool.fillPools(ctx) if !somebodyAlive { connPool.state.set(closedState) + if ctxCanceled { + return nil, ErrContextCanceled + } return nil, ErrNoConnection } connPool.state.set(connectedState) for _, s := range connPool.addrs { - go connPool.controller(s) + endpointCtx, cancel := context.WithCancel(context.Background()) + s.cancel = cancel + go connPool.controller(endpointCtx, s) } return connPool, nil @@ -181,11 +191,12 @@ func ConnectWithOpts(addrs []string, connOpts tarantool.Opts, opts Opts) (*Conne // It is useless to set up tarantool.Opts.Reconnect value for a connection. // The connection pool has its own reconnection logic. See // Opts.CheckTimeout description. -func Connect(addrs []string, connOpts tarantool.Opts) (*ConnectionPool, error) { +func Connect(ctx context.Context, addrs []string, + connOpts tarantool.Opts) (*ConnectionPool, error) { opts := Opts{ CheckTimeout: 1 * time.Second, } - return ConnectWithOpts(addrs, connOpts, opts) + return ConnectWithOpts(ctx, addrs, connOpts, opts) } // ConnectedNow gets connected status of pool. @@ -224,7 +235,7 @@ func (p *ConnectionPool) ConfiguredTimeout(mode Mode) (time.Duration, error) { // Add adds a new endpoint with the address into the pool. This function // adds the endpoint only after successful connection. -func (p *ConnectionPool) Add(addr string) error { +func (p *ConnectionPool) Add(ctx context.Context, addr string) error { e := newEndpoint(addr) p.addrsMutex.Lock() @@ -237,18 +248,23 @@ func (p *ConnectionPool) Add(addr string) error { p.addrsMutex.Unlock() return ErrExists } + + endpointCtx, cancel := context.WithCancel(context.Background()) + e.cancel = cancel + p.addrs[addr] = e p.addrsMutex.Unlock() - if err := p.tryConnect(e); err != nil { + if err := p.tryConnect(ctx, e); err != nil { p.addrsMutex.Lock() delete(p.addrs, addr) p.addrsMutex.Unlock() + e.cancel() close(e.closed) return err } - go p.controller(e) + go p.controller(endpointCtx, e) return nil } @@ -268,6 +284,7 @@ func (p *ConnectionPool) Remove(addr string) error { case <-endpoint.shutdown: // CloseGraceful()/Remove() in progress/done. default: + endpoint.cancel() close(endpoint.shutdown) } @@ -302,6 +319,7 @@ func (p *ConnectionPool) Close() []error { p.state.cas(shutdownState, closedState) { p.addrsMutex.RLock() for _, s := range p.addrs { + s.cancel() close(s.close) } p.addrsMutex.RUnlock() @@ -316,6 +334,7 @@ func (p *ConnectionPool) CloseGraceful() []error { if p.state.cas(connectedState, shutdownState) { p.addrsMutex.RLock() for _, s := range p.addrs { + s.cancel() close(s.shutdown) } p.addrsMutex.RUnlock() @@ -1109,8 +1128,48 @@ func (p *ConnectionPool) handlerDeactivated(conn *tarantool.Connection, } } -func (p *ConnectionPool) fillPools() bool { +func (p *ConnectionPool) deactivateConnection(addr string, + conn *tarantool.Connection, role Role) { + p.deleteConnection(addr) + conn.Close() + p.handlerDeactivated(conn, role) +} + +func (p *ConnectionPool) deactivateConnections() { + for address, endpoint := range p.addrs { + if endpoint != nil && endpoint.conn != nil { + p.deactivateConnection(address, endpoint.conn, endpoint.role) + } + } +} + +func (p *ConnectionPool) processConnection(conn *tarantool.Connection, + addr string, end *endpoint) bool { + role, err := p.getConnectionRole(conn) + if err != nil { + conn.Close() + log.Printf("tarantool: storing connection to %s failed: %s\n", addr, err) + return false + } + + if !p.handlerDiscovered(conn, role) { + conn.Close() + return false + } + if p.addConnection(addr, conn, role) != nil { + conn.Close() + p.handlerDeactivated(conn, role) + return false + } + + end.conn = conn + end.role = role + return true +} + +func (p *ConnectionPool) fillPools(ctx context.Context) (bool, bool) { somebodyAlive := false + ctxCanceled := false // It is called before controller() goroutines so we don't expect // concurrency issues here. @@ -1120,39 +1179,27 @@ func (p *ConnectionPool) fillPools() bool { connOpts := p.connOpts connOpts.Notify = end.notify - conn, err := tarantool.Connect(addr, connOpts) + conn, err := tarantool.Connect(ctx, addr, connOpts) if err != nil { log.Printf("tarantool: connect to %s failed: %s\n", addr, err.Error()) - } else if conn != nil { - role, err := p.getConnectionRole(conn) - if err != nil { - conn.Close() - log.Printf("tarantool: storing connection to %s failed: %s\n", addr, err) - continue - } + select { + case <-ctx.Done(): + ctxCanceled = true - if p.handlerDiscovered(conn, role) { - if p.addConnection(addr, conn, role) != nil { - conn.Close() - p.handlerDeactivated(conn, role) - } + p.addrs[addr] = nil + log.Printf("tarantool: operation was canceled") - if conn.ConnectedNow() { - end.conn = conn - end.role = role - somebodyAlive = true - } else { - p.deleteConnection(addr) - conn.Close() - p.handlerDeactivated(conn, role) - } - } else { - conn.Close() + p.deactivateConnections() + + return false, ctxCanceled + default: } + } else if p.processConnection(conn, addr, end) { + somebodyAlive = true } } - return somebodyAlive + return somebodyAlive, ctxCanceled } func (p *ConnectionPool) updateConnection(e *endpoint) { @@ -1213,7 +1260,7 @@ func (p *ConnectionPool) updateConnection(e *endpoint) { } } -func (p *ConnectionPool) tryConnect(e *endpoint) error { +func (p *ConnectionPool) tryConnect(ctx context.Context, e *endpoint) error { p.poolsMutex.Lock() if p.state.get() != connectedState { @@ -1226,7 +1273,7 @@ func (p *ConnectionPool) tryConnect(e *endpoint) error { connOpts := p.connOpts connOpts.Notify = e.notify - conn, err := tarantool.Connect(e.addr, connOpts) + conn, err := tarantool.Connect(ctx, e.addr, connOpts) if err == nil { role, err := p.getConnectionRole(conn) p.poolsMutex.Unlock() @@ -1265,7 +1312,7 @@ func (p *ConnectionPool) tryConnect(e *endpoint) error { return err } -func (p *ConnectionPool) reconnect(e *endpoint) { +func (p *ConnectionPool) reconnect(ctx context.Context, e *endpoint) { p.poolsMutex.Lock() if p.state.get() != connectedState { @@ -1280,10 +1327,10 @@ func (p *ConnectionPool) reconnect(e *endpoint) { e.conn = nil e.role = UnknownRole - p.tryConnect(e) + p.tryConnect(ctx, e) } -func (p *ConnectionPool) controller(e *endpoint) { +func (p *ConnectionPool) controller(ctx context.Context, e *endpoint) { timer := time.NewTicker(p.opts.CheckTimeout) defer timer.Stop() @@ -1367,11 +1414,11 @@ func (p *ConnectionPool) controller(e *endpoint) { // Relocate connection between subpools // if ro/rw was updated. if e.conn == nil { - p.tryConnect(e) + p.tryConnect(ctx, e) } else if !e.conn.ClosedNow() { p.updateConnection(e) } else { - p.reconnect(e) + p.reconnect(ctx, e) } } } diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index dd0210d62..b944d0c6c 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -1,6 +1,7 @@ package pool_test import ( + "context" "fmt" "log" "os" @@ -46,17 +47,23 @@ var defaultTimeoutRetry = 500 * time.Millisecond var instances []test_helpers.TarantoolInstance func TestConnError_IncorrectParams(t *testing.T) { - connPool, err := pool.Connect([]string{}, tarantool.Opts{}) + ctx, cancel := test_helpers.GetPoolConnectContext() + connPool, err := pool.Connect(ctx, []string{}, tarantool.Opts{}) + cancel() require.Nilf(t, connPool, "conn is not nil with incorrect param") require.NotNilf(t, err, "err is nil with incorrect params") require.Equal(t, "addrs (first argument) should not be empty", err.Error()) - connPool, err = pool.Connect([]string{"err1", "err2"}, connOpts) + ctx, cancel = test_helpers.GetPoolConnectContext() + connPool, err = pool.Connect(ctx, []string{"err1", "err2"}, connOpts) + cancel() require.Nilf(t, connPool, "conn is not nil with incorrect param") require.NotNilf(t, err, "err is nil with incorrect params") require.Equal(t, "no active connections", err.Error()) - connPool, err = pool.ConnectWithOpts(servers, tarantool.Opts{}, pool.Opts{}) + ctx, cancel = test_helpers.GetPoolConnectContext() + connPool, err = pool.ConnectWithOpts(ctx, servers, tarantool.Opts{}, pool.Opts{}) + cancel() require.Nilf(t, connPool, "conn is not nil with incorrect param") require.NotNilf(t, err, "err is nil with incorrect params") require.Equal(t, "wrong check timeout, must be greater than 0", err.Error()) @@ -64,7 +71,9 @@ func TestConnError_IncorrectParams(t *testing.T) { func TestConnSuccessfully(t *testing.T) { server := servers[0] - connPool, err := pool.Connect([]string{"err", server}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []string{"err", server}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -84,9 +93,77 @@ func TestConnSuccessfully(t *testing.T) { require.Nil(t, err) } +func TestConnErrorAfterCtxCancel(t *testing.T) { + var connLongReconnectOpts = tarantool.Opts{ + Timeout: 5 * time.Second, + User: "test", + Pass: "test", + Reconnect: time.Second, + MaxReconnects: 100, + } + + ctx, cancel := context.WithCancel(context.Background()) + + var connPool *pool.ConnectionPool + var err error + + cancel() + connPool, err = pool.Connect(ctx, servers, connLongReconnectOpts) + + if connPool != nil || err == nil { + t.Fatalf("ConnectionPool was created after cancel") + } + if !strings.Contains(err.Error(), "operation was canceled") { + t.Fatalf("Unexpected error, expected to contain %s, got %v", + "operation was canceled", err) + } +} + +type mockClosingDialer struct { + cnt int + ctx context.Context + ctxCancel context.CancelFunc +} + +func (m *mockClosingDialer) Dial(ctx context.Context, address string, + opts tarantool.DialOpts) (tarantool.Conn, error) { + + dialer := tarantool.TtDialer{} + conn, err := dialer.Dial(m.ctx, address, tarantool.DialOpts{ + User: "test", + Password: "test", + }) + + if m.cnt == 0 { + m.ctxCancel() + } + m.cnt++ + + return conn, err +} + +func TestContextCancelInProgress(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + dialer := &mockClosingDialer{0, ctx, cancel} + + connPool, err := pool.Connect(ctx, servers, tarantool.Opts{ + Dialer: dialer, + }) + require.NotNilf(t, err, "expected err after ctx cancel") + assert.Truef(t, strings.Contains(err.Error(), "operation was canceled"), + fmt.Sprintf("unexpected error, expected to contain %s, got %v", + "operation was canceled", err)) + require.Nilf(t, connPool, "conn is not nil after ctx cancel") +} + func TestConnSuccessfullyDuplicates(t *testing.T) { server := servers[0] - connPool, err := pool.Connect([]string{server, server, server, server}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []string{server, server, server, server}, + connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -112,7 +189,9 @@ func TestConnSuccessfullyDuplicates(t *testing.T) { func TestReconnect(t *testing.T) { server := servers[0] - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -158,7 +237,9 @@ func TestDisconnect_withReconnect(t *testing.T) { opts := connOpts opts.Reconnect = 10 * time.Second - connPool, err := pool.Connect([]string{servers[serverId]}, opts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []string{servers[serverId]}, opts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -202,7 +283,9 @@ func TestDisconnectAll(t *testing.T) { server1 := servers[0] server2 := servers[1] - connPool, err := pool.Connect([]string{server1, server2}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []string{server1, server2}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -249,14 +332,18 @@ func TestDisconnectAll(t *testing.T) { } func TestAdd(t *testing.T) { - connPool, err := pool.Connect([]string{servers[0]}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []string{servers[0]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() for _, server := range servers[1:] { - err = connPool.Add(server) + ctx, cancel := test_helpers.GetConnectContext() + err = connPool.Add(ctx, server) + cancel() require.Nil(t, err) } @@ -280,13 +367,17 @@ func TestAdd(t *testing.T) { } func TestAdd_exist(t *testing.T) { - connPool, err := pool.Connect([]string{servers[0]}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + connPool, err := pool.Connect(ctx, []string{servers[0]}, connOpts) + cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() - err = connPool.Add(servers[0]) + ctx, cancel = test_helpers.GetConnectContext() + err = connPool.Add(ctx, servers[0]) + cancel() require.Equal(t, pool.ErrExists, err) args := test_helpers.CheckStatusesArgs{ @@ -305,13 +396,17 @@ func TestAdd_exist(t *testing.T) { } func TestAdd_unreachable(t *testing.T) { - connPool, err := pool.Connect([]string{servers[0]}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + connPool, err := pool.Connect(ctx, []string{servers[0]}, connOpts) + cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() - err = connPool.Add("127.0.0.2:6667") + ctx, cancel = test_helpers.GetConnectContext() + err = connPool.Add(ctx, "127.0.0.2:6667") + cancel() // The OS-dependent error so we just check for existence. require.NotNil(t, err) @@ -331,17 +426,23 @@ func TestAdd_unreachable(t *testing.T) { } func TestAdd_afterClose(t *testing.T) { - connPool, err := pool.Connect([]string{servers[0]}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + connPool, err := pool.Connect(ctx, []string{servers[0]}, connOpts) + cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") connPool.Close() - err = connPool.Add(servers[0]) + ctx, cancel = test_helpers.GetConnectContext() + err = connPool.Add(ctx, servers[0]) + cancel() assert.Equal(t, err, pool.ErrClosed) } func TestAdd_Close_concurrent(t *testing.T) { - connPool, err := pool.Connect([]string{servers[0]}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + connPool, err := pool.Connect(ctx, []string{servers[0]}, connOpts) + cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -350,7 +451,9 @@ func TestAdd_Close_concurrent(t *testing.T) { go func() { defer wg.Done() - err = connPool.Add(servers[1]) + ctx, cancel := test_helpers.GetConnectContext() + err = connPool.Add(ctx, servers[1]) + cancel() if err != nil { assert.Equal(t, pool.ErrClosed, err) } @@ -362,7 +465,9 @@ func TestAdd_Close_concurrent(t *testing.T) { } func TestAdd_CloseGraceful_concurrent(t *testing.T) { - connPool, err := pool.Connect([]string{servers[0]}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + connPool, err := pool.Connect(ctx, []string{servers[0]}, connOpts) + cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -371,7 +476,9 @@ func TestAdd_CloseGraceful_concurrent(t *testing.T) { go func() { defer wg.Done() - err = connPool.Add(servers[1]) + ctx, cancel := test_helpers.GetConnectContext() + err = connPool.Add(ctx, servers[1]) + cancel() if err != nil { assert.Equal(t, pool.ErrClosed, err) } @@ -383,7 +490,9 @@ func TestAdd_CloseGraceful_concurrent(t *testing.T) { } func TestRemove(t *testing.T) { - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -410,7 +519,9 @@ func TestRemove(t *testing.T) { } func TestRemove_double(t *testing.T) { - connPool, err := pool.Connect([]string{servers[0], servers[1]}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []string{servers[0], servers[1]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -437,7 +548,9 @@ func TestRemove_double(t *testing.T) { } func TestRemove_unknown(t *testing.T) { - connPool, err := pool.Connect([]string{servers[0], servers[1]}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []string{servers[0], servers[1]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -463,7 +576,9 @@ func TestRemove_unknown(t *testing.T) { } func TestRemove_concurrent(t *testing.T) { - connPool, err := pool.Connect([]string{servers[0], servers[1]}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []string{servers[0], servers[1]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -510,7 +625,9 @@ func TestRemove_concurrent(t *testing.T) { } func TestRemove_Close_concurrent(t *testing.T) { - connPool, err := pool.Connect([]string{servers[0], servers[1]}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []string{servers[0], servers[1]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -529,7 +646,9 @@ func TestRemove_Close_concurrent(t *testing.T) { } func TestRemove_CloseGraceful_concurrent(t *testing.T) { - connPool, err := pool.Connect([]string{servers[0], servers[1]}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []string{servers[0], servers[1]}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -551,7 +670,9 @@ func TestClose(t *testing.T) { server1 := servers[0] server2 := servers[1] - connPool, err := pool.Connect([]string{server1, server2}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []string{server1, server2}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -591,7 +712,9 @@ func TestCloseGraceful(t *testing.T) { server1 := servers[0] server2 := servers[1] - connPool, err := pool.Connect([]string{server1, server2}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []string{server1, server2}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -730,7 +853,9 @@ func TestConnectionHandlerOpenUpdateClose(t *testing.T) { CheckTimeout: 100 * time.Microsecond, ConnectionHandler: h, } - connPool, err := pool.ConnectWithOpts(poolServers, connOpts, poolOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.ConnectWithOpts(ctx, poolServers, connOpts, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -804,7 +929,9 @@ func TestConnectionHandlerOpenError(t *testing.T) { CheckTimeout: 100 * time.Microsecond, ConnectionHandler: h, } - connPool, err := pool.ConnectWithOpts(poolServers, connOpts, poolOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.ConnectWithOpts(ctx, poolServers, connOpts, poolOpts) if err == nil { defer connPool.Close() } @@ -846,7 +973,9 @@ func TestConnectionHandlerUpdateError(t *testing.T) { CheckTimeout: 100 * time.Microsecond, ConnectionHandler: h, } - connPool, err := pool.ConnectWithOpts(poolServers, connOpts, poolOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.ConnectWithOpts(ctx, poolServers, connOpts, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -891,7 +1020,9 @@ func TestRequestOnClosed(t *testing.T) { server1 := servers[0] server2 := servers[1] - connPool, err := pool.Connect([]string{server1, server2}, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []string{server1, server2}, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -930,7 +1061,9 @@ func TestGetPoolInfo(t *testing.T) { srvs := []string{server1, server2} expected := []string{server1, server2} - connPool, err := pool.Connect(srvs, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, srvs, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -948,7 +1081,9 @@ func TestCall(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1005,7 +1140,9 @@ func TestCall16(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1062,7 +1199,9 @@ func TestCall17(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1119,7 +1258,9 @@ func TestEval(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1197,7 +1338,9 @@ func TestExecute(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1254,7 +1397,9 @@ func TestRoundRobinStrategy(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1331,7 +1476,9 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1402,7 +1549,9 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1485,7 +1634,9 @@ func TestUpdateInstancesRoles(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1629,7 +1780,9 @@ func TestInsert(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1728,7 +1881,9 @@ func TestDelete(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1792,7 +1947,9 @@ func TestUpsert(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1864,7 +2021,9 @@ func TestUpdate(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1954,7 +2113,9 @@ func TestReplace(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2040,7 +2201,9 @@ func TestSelect(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2160,7 +2323,9 @@ func TestPing(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2198,7 +2363,9 @@ func TestDo(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2237,7 +2404,9 @@ func TestDo_concurrent(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2268,7 +2437,9 @@ func TestNewPrepared(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2336,7 +2507,9 @@ func TestDoWithStrangerConn(t *testing.T) { err := test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2365,7 +2538,9 @@ func TestStream_Commit(t *testing.T) { err = test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2464,7 +2639,9 @@ func TestStream_Rollback(t *testing.T) { err = test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2563,7 +2740,9 @@ func TestStream_TxnIsolationLevel(t *testing.T) { err = test_helpers.SetClusterRO(servers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2657,7 +2836,9 @@ func TestConnectionPool_NewWatcher_noWatchersFeature(t *testing.T) { err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, opts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, opts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2685,7 +2866,9 @@ func TestConnectionPool_NewWatcher_modes(t *testing.T) { err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, opts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, opts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2767,7 +2950,9 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { poolOpts := pool.Opts{ CheckTimeout: 500 * time.Millisecond, } - pool, err := pool.ConnectWithOpts(servers, opts, poolOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + pool, err := pool.ConnectWithOpts(ctx, servers, opts, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, pool, "conn is nil after Connect") @@ -2851,7 +3036,9 @@ func TestWatcher_Unregister(t *testing.T) { err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") - pool, err := pool.Connect(servers, opts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + pool, err := pool.Connect(ctx, servers, opts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, pool, "conn is nil after Connect") defer pool.Close() @@ -2910,7 +3097,9 @@ func TestConnectionPool_NewWatcher_concurrent(t *testing.T) { err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, opts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, opts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2950,7 +3139,9 @@ func TestWatcher_Unregister_concurrent(t *testing.T) { err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") - connPool, err := pool.Connect(servers, opts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, opts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() diff --git a/pool/example_test.go b/pool/example_test.go index 84a41ff7b..dae28de90 100644 --- a/pool/example_test.go +++ b/pool/example_test.go @@ -24,7 +24,9 @@ func examplePool(roles []bool, connOpts tarantool.Opts) (*pool.ConnectionPool, e if err != nil { return nil, fmt.Errorf("ConnectionPool is not established") } - connPool, err := pool.Connect(servers, connOpts) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, servers, connOpts) if err != nil || connPool == nil { return nil, fmt.Errorf("ConnectionPool is not established") } diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go index 51fb967a5..a3bfaf0a4 100644 --- a/queue/example_connection_pool_test.go +++ b/queue/example_connection_pool_test.go @@ -1,6 +1,7 @@ package queue_test import ( + "context" "fmt" "sync" "sync/atomic" @@ -164,7 +165,9 @@ func Example_connectionPool() { CheckTimeout: 5 * time.Second, ConnectionHandler: h, } - connPool, err := pool.ConnectWithOpts(servers, connOpts, poolOpts) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + connPool, err := pool.ConnectWithOpts(ctx, servers, connOpts, poolOpts) if err != nil { fmt.Printf("Unable to connect to the pool: %s", err) return diff --git a/queue/example_msgpack_test.go b/queue/example_msgpack_test.go index 6fd101e09..6d3637417 100644 --- a/queue/example_msgpack_test.go +++ b/queue/example_msgpack_test.go @@ -9,6 +9,7 @@ package queue_test import ( + "context" "fmt" "log" "time" @@ -55,7 +56,9 @@ func Example_simpleQueueCustomMsgPack() { User: "test", Pass: "test", } - conn, err := tarantool.Connect("127.0.0.1:3013", opts) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + conn, err := tarantool.Connect(ctx, "127.0.0.1:3013", opts) + cancel() if err != nil { log.Fatalf("connection: %s", err) return diff --git a/queue/example_test.go b/queue/example_test.go index 711ee31d4..e81acca40 100644 --- a/queue/example_test.go +++ b/queue/example_test.go @@ -9,6 +9,7 @@ package queue_test import ( + "context" "fmt" "time" @@ -31,7 +32,9 @@ func Example_simpleQueue() { Pass: "test", } - conn, err := tarantool.Connect("127.0.0.1:3013", opts) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + conn, err := tarantool.Connect(ctx, "127.0.0.1:3013", opts) if err != nil { fmt.Printf("error in prepare is %v", err) return diff --git a/settings/example_test.go b/settings/example_test.go index b1d0e5d4f..29be33bfe 100644 --- a/settings/example_test.go +++ b/settings/example_test.go @@ -1,7 +1,9 @@ package settings_test import ( + "context" "fmt" + "time" "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/settings" @@ -9,7 +11,9 @@ import ( ) func example_connect(opts tarantool.Opts) *tarantool.Connection { - conn, err := tarantool.Connect(server, opts) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + conn, err := tarantool.Connect(ctx, server, opts) if err != nil { panic("Connection is not established: " + err.Error()) } diff --git a/shutdown_test.go b/shutdown_test.go index bb4cfa099..412d27ea4 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -460,9 +460,12 @@ func TestGracefulShutdownCloseConcurrent(t *testing.T) { go func(i int) { defer caseWg.Done() + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + // Do not wait till Tarantool register out watcher, // test everything is ok even on async. - conn, err := Connect(shtdnServer, shtdnClntOpts) + conn, err := Connect(ctx, shtdnServer, shtdnClntOpts) if err != nil { t.Errorf("Failed to connect: %s", err) } else { diff --git a/ssl.go b/ssl.go index a23238849..8ca430559 100644 --- a/ssl.go +++ b/ssl.go @@ -5,24 +5,24 @@ package tarantool import ( "bufio" + "context" "errors" "io/ioutil" "net" "os" "strings" - "time" "github.com/tarantool/go-openssl" ) -func sslDialTimeout(network, address string, timeout time.Duration, +func sslDialContext(ctx context.Context, network, address string, opts SslOpts) (connection net.Conn, err error) { - var ctx interface{} - if ctx, err = sslCreateContext(opts); err != nil { + var sslCtx interface{} + if sslCtx, err = sslCreateContext(opts); err != nil { return } - return openssl.DialTimeout(network, address, timeout, ctx.(*openssl.Ctx), 0) + return openssl.DialContext(ctx, network, address, sslCtx.(*openssl.Ctx), 0) } // interface{} is a hack. It helps to avoid dependency of go-openssl in build diff --git a/ssl_disable.go b/ssl_disable.go index 8d0ab406b..6a2aa2163 100644 --- a/ssl_disable.go +++ b/ssl_disable.go @@ -4,12 +4,12 @@ package tarantool import ( + "context" "errors" "net" - "time" ) -func sslDialTimeout(network, address string, timeout time.Duration, +func sslDialContext(ctx context.Context, network, address string, opts SslOpts) (connection net.Conn, err error) { return nil, errors.New("SSL support is disabled.") } diff --git a/ssl_test.go b/ssl_test.go index 30078703c..65be85504 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -4,6 +4,7 @@ package tarantool_test import ( + "context" "errors" "fmt" "io/ioutil" @@ -60,12 +61,12 @@ func serverSslRecv(msgs <-chan string, errs <-chan error) (string, error) { return <-msgs, <-errs } -func clientSsl(network, address string, opts SslOpts) (net.Conn, error) { - timeout := 5 * time.Second - return SslDialTimeout(network, address, timeout, opts) +func clientSsl(ctx context.Context, network, address string, + opts SslOpts) (net.Conn, error) { + return SslDialContext(ctx, network, address, opts) } -func createClientServerSsl(t testing.TB, serverOpts, +func createClientServerSsl(ctx context.Context, t testing.TB, serverOpts, clientOpts SslOpts) (net.Listener, net.Conn, <-chan string, <-chan error, error) { t.Helper() @@ -77,16 +78,16 @@ func createClientServerSsl(t testing.TB, serverOpts, msgs, errs := serverSslAccept(l) port := l.Addr().(*net.TCPAddr).Port - c, err := clientSsl("tcp", sslHost+":"+strconv.Itoa(port), clientOpts) + c, err := clientSsl(ctx, "tcp", sslHost+":"+strconv.Itoa(port), clientOpts) return l, c, msgs, errs, err } -func createClientServerSslOk(t testing.TB, serverOpts, +func createClientServerSslOk(ctx context.Context, t testing.TB, serverOpts, clientOpts SslOpts) (net.Listener, net.Conn, <-chan string, <-chan error) { t.Helper() - l, c, msgs, errs, err := createClientServerSsl(t, serverOpts, clientOpts) + l, c, msgs, errs, err := createClientServerSsl(ctx, t, serverOpts, clientOpts) if err != nil { t.Fatalf("Unable to create client, error %q", err.Error()) } @@ -150,7 +151,9 @@ func serverTntStop(inst test_helpers.TarantoolInstance) { } func checkTntConn(clientOpts SslOpts) error { - conn, err := Connect(tntHost, Opts{ + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := Connect(ctx, tntHost, Opts{ Auth: AutoAuth, Timeout: 500 * time.Millisecond, User: "test", @@ -166,10 +169,11 @@ func checkTntConn(clientOpts SslOpts) error { return nil } -func assertConnectionSslFail(t testing.TB, serverOpts, clientOpts SslOpts) { +func assertConnectionSslFail(ctx context.Context, t testing.TB, serverOpts, + clientOpts SslOpts) { t.Helper() - l, c, _, _, err := createClientServerSsl(t, serverOpts, clientOpts) + l, c, _, _, err := createClientServerSsl(ctx, t, serverOpts, clientOpts) l.Close() if err == nil { c.Close() @@ -177,10 +181,11 @@ func assertConnectionSslFail(t testing.TB, serverOpts, clientOpts SslOpts) { } } -func assertConnectionSslOk(t testing.TB, serverOpts, clientOpts SslOpts) { +func assertConnectionSslOk(ctx context.Context, t testing.TB, serverOpts, + clientOpts SslOpts) { t.Helper() - l, c, msgs, errs := createClientServerSslOk(t, serverOpts, clientOpts) + l, c, msgs, errs := createClientServerSslOk(ctx, t, serverOpts, clientOpts) const message = "any test string" c.Write([]byte(message)) c.Close() @@ -621,15 +626,19 @@ func TestSslOpts(t *testing.T) { isTntSsl := isTestTntSsl() for _, test := range tests { + var ctx context.Context + var cancel context.CancelFunc + ctx, cancel = test_helpers.GetConnectContext() if test.ok { t.Run("ok_ssl_"+test.name, func(t *testing.T) { - assertConnectionSslOk(t, test.serverOpts, test.clientOpts) + assertConnectionSslOk(ctx, t, test.serverOpts, test.clientOpts) }) } else { t.Run("fail_ssl_"+test.name, func(t *testing.T) { - assertConnectionSslFail(t, test.serverOpts, test.clientOpts) + assertConnectionSslFail(ctx, t, test.serverOpts, test.clientOpts) }) } + cancel() if !isTntSsl { continue } @@ -645,6 +654,35 @@ func TestSslOpts(t *testing.T) { } } +func TestSslDialContextCancel(t *testing.T) { + serverOpts := SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + } + clientOpts := SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + l, c, _, _, err := createClientServerSsl(ctx, t, serverOpts, clientOpts) + l.Close() + + if err == nil { + c.Close() + t.Fatalf("Expected error, dial was not canceled") + } + if !strings.Contains(err.Error(), "operation was canceled") { + t.Fatalf("Unexpected error, expected to contain %s, got %v", + "operation was canceled", err) + } +} + func TestOpts_PapSha256Auth(t *testing.T) { isTntSsl := isTestTntSsl() if !isTntSsl { diff --git a/tarantool_test.go b/tarantool_test.go index 6339164f1..bde3060c7 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -708,7 +708,9 @@ func TestTtDialer(t *testing.T) { assert := assert.New(t) require := require.New(t) - conn, err := TtDialer{}.Dial(server, DialOpts{}) + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := TtDialer{}.Dial(ctx, server, DialOpts{}) require.Nil(err) require.NotNil(conn) defer conn.Close() @@ -778,7 +780,9 @@ func TestOptsAuth_PapSha256AuthForbit(t *testing.T) { papSha256Opts := opts papSha256Opts.Auth = PapSha256Auth - conn, err := Connect(server, papSha256Opts) + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := Connect(ctx, server, papSha256Opts) if err == nil { t.Error("An error expected.") conn.Close() @@ -3408,7 +3412,9 @@ func TestConnectionProtocolVersionRequirementSuccess(t *testing.T) { Version: ProtocolVersion(3), } - conn, err := Connect(server, connOpts) + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := Connect(ctx, server, connOpts) require.Nilf(t, err, "No errors on connect") require.NotNilf(t, conn, "Connect success") @@ -3424,7 +3430,9 @@ func TestConnectionProtocolVersionRequirementFail(t *testing.T) { Version: ProtocolVersion(3), } - conn, err := Connect(server, connOpts) + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := Connect(ctx, server, connOpts) require.Nilf(t, conn, "Connect fail") require.NotNilf(t, err, "Got error on connect") @@ -3439,7 +3447,9 @@ func TestConnectionProtocolFeatureRequirementSuccess(t *testing.T) { Features: []ProtocolFeature{TransactionsFeature}, } - conn, err := Connect(server, connOpts) + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := Connect(ctx, server, connOpts) require.NotNilf(t, conn, "Connect success") require.Nilf(t, err, "No errors on connect") @@ -3455,7 +3465,9 @@ func TestConnectionProtocolFeatureRequirementFail(t *testing.T) { Features: []ProtocolFeature{TransactionsFeature}, } - conn, err := Connect(server, connOpts) + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := Connect(ctx, server, connOpts) require.Nilf(t, conn, "Connect fail") require.NotNilf(t, err, "Got error on connect") @@ -3471,7 +3483,9 @@ func TestConnectionProtocolFeatureRequirementManyFail(t *testing.T) { Features: []ProtocolFeature{TransactionsFeature, ProtocolFeature(15532)}, } - conn, err := Connect(server, connOpts) + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := Connect(ctx, server, connOpts) require.Nilf(t, conn, "Connect fail") require.NotNilf(t, err, "Got error on connect") @@ -4003,7 +4017,9 @@ func TestConnect_schema_update(t *testing.T) { for i := 0; i < 100; i++ { fut := conn.Do(NewCallRequest("create_spaces")) - if conn, err := Connect(server, opts); err != nil { + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + if conn, err := Connect(ctx, server, opts); err != nil { if err.Error() != "concurrent schema update" { t.Errorf("unexpected error: %s", err) } @@ -4019,6 +4035,32 @@ func TestConnect_schema_update(t *testing.T) { } } +func TestConnect_context_cancel(t *testing.T) { + var connLongReconnectOpts = Opts{ + Timeout: 5 * time.Second, + User: "test", + Pass: "test", + Reconnect: time.Second, + MaxReconnects: 100, + } + + ctx, cancel := context.WithCancel(context.Background()) + + var conn *Connection + var err error + + cancel() + conn, err = Connect(ctx, server, connLongReconnectOpts) + + if conn != nil || err == nil { + t.Fatalf("Connection was created after cancel") + } + if !strings.Contains(err.Error(), "operation was canceled") { + t.Fatalf("Unexpected error, expected to contain %s, got %v", + "operation was canceled", err) + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/test_helpers/main.go b/test_helpers/main.go index 894ebb653..cc806a7d2 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -11,6 +11,7 @@ package test_helpers import ( + "context" "errors" "fmt" "io" @@ -97,7 +98,9 @@ func isReady(server string, opts *tarantool.Opts) error { var conn *tarantool.Connection var resp *tarantool.Response - conn, err = tarantool.Connect(server, *opts) + ctx, cancel := GetConnectContext() + defer cancel() + conn, err = tarantool.Connect(ctx, server, *opts) if err != nil { return err } @@ -402,3 +405,7 @@ func ConvertUint64(v interface{}) (result uint64, err error) { } return } + +func GetConnectContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 500*time.Millisecond) +} diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index c44df2f6a..b2340ccb8 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -1,6 +1,7 @@ package test_helpers import ( + "context" "fmt" "reflect" "time" @@ -130,9 +131,9 @@ func Retry(f func(interface{}) error, args interface{}, count int, timeout time. return err } -func InsertOnInstance(server string, connOpts tarantool.Opts, space interface{}, - tuple interface{}) error { - conn, err := tarantool.Connect(server, connOpts) +func InsertOnInstance(ctx context.Context, server string, connOpts tarantool.Opts, + space interface{}, tuple interface{}) error { + conn, err := tarantool.Connect(ctx, server, connOpts) if err != nil { return fmt.Errorf("fail to connect to %s: %s", server, err.Error()) } @@ -182,7 +183,9 @@ func InsertOnInstances(servers []string, connOpts tarantool.Opts, space interfac } for _, server := range servers { - err := InsertOnInstance(server, connOpts, space, tuple) + ctx, cancel := GetConnectContext() + err := InsertOnInstance(ctx, server, connOpts, space, tuple) + cancel() if err != nil { return err } @@ -191,8 +194,9 @@ func InsertOnInstances(servers []string, connOpts tarantool.Opts, space interfac return nil } -func SetInstanceRO(server string, connOpts tarantool.Opts, isReplica bool) error { - conn, err := tarantool.Connect(server, connOpts) +func SetInstanceRO(ctx context.Context, server string, connOpts tarantool.Opts, + isReplica bool) error { + conn, err := tarantool.Connect(ctx, server, connOpts) if err != nil { return err } @@ -214,7 +218,9 @@ func SetClusterRO(servers []string, connOpts tarantool.Opts, roles []bool) error } for i, server := range servers { - err := SetInstanceRO(server, connOpts, roles[i]) + ctx, cancel := GetConnectContext() + err := SetInstanceRO(ctx, server, connOpts, roles[i]) + cancel() if err != nil { return err } @@ -257,3 +263,7 @@ func StopTarantoolInstances(instances []TarantoolInstance) { StopTarantoolWithCleanup(instance) } } + +func GetPoolConnectContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 500*time.Millisecond) +} diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 3771a5f9e..898ae84e3 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -17,7 +17,9 @@ func ConnectWithValidation(t testing.TB, opts tarantool.Opts) *tarantool.Connection { t.Helper() - conn, err := tarantool.Connect(server, opts) + ctx, cancel := GetConnectContext() + defer cancel() + conn, err := tarantool.Connect(ctx, server, opts) if err != nil { t.Fatalf("Failed to connect: %s", err.Error()) } diff --git a/uuid/example_test.go b/uuid/example_test.go index 632f620be..08bd64aae 100644 --- a/uuid/example_test.go +++ b/uuid/example_test.go @@ -9,8 +9,10 @@ package uuid_test import ( + "context" "fmt" "log" + "time" "github.com/google/uuid" "github.com/tarantool/go-tarantool/v2" @@ -25,7 +27,9 @@ func Example() { User: "test", Pass: "test", } - client, err := tarantool.Connect("127.0.0.1:3013", opts) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + client, err := tarantool.Connect(ctx, "127.0.0.1:3013", opts) + cancel() if err != nil { log.Fatalf("Failed to connect: %s", err.Error()) } From 8e1a911254a353ee28a26da89f4c59b52240feea Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 10 Oct 2023 16:21:13 +0300 Subject: [PATCH 487/605] crud: support schema Support `crud.schema` request [1] and response parsing. 1. https://github.com/tarantool/crud/pull/380 --- CHANGELOG.md | 1 + Makefile | 2 +- crud/example_test.go | 29 ++++++ crud/schema.go | 214 +++++++++++++++++++++++++++++++++++++++++ crud/tarantool_test.go | 124 ++++++++++++++++++++++++ 5 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 crud/schema.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a26ba7265..88fcf1ff5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support `operation_data` in `crud.Error` (#330) - Support `fetch_latest_metadata` option for crud requests with metadata (#335) - Support `noreturn` option for data change crud requests (#335) +- Support `crud.schema` request (#336) ### Changed diff --git a/Makefile b/Makefile index c435764a8..378dced39 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ clean: .PHONY: deps deps: clean ( cd ./queue/testdata; $(TTCTL) rocks install queue 1.3.0 ) - ( cd ./crud/testdata; $(TTCTL) rocks install crud 1.3.0 ) + ( cd ./crud/testdata; $(TTCTL) rocks install crud 1.4.0 ) .PHONY: datetime-timezones datetime-timezones: diff --git a/crud/example_test.go b/crud/example_test.go index cb15aca4e..763ab5deb 100644 --- a/crud/example_test.go +++ b/crud/example_test.go @@ -302,3 +302,32 @@ func ExampleSelectRequest_pagination() { // [{id unsigned false} {bucket_id unsigned true} {name string false}] // [[3006 32 bla] [3007 33 bla]] } + +func ExampleSchema() { + conn := exampleConnect() + + req := crud.MakeSchemaRequest() + var result crud.SchemaResult + + if err := conn.Do(req).GetTyped(&result); err != nil { + fmt.Printf("Failed to execute request: %s", err) + return + } + + // Schema may differ between different Tarantool versions. + // https://github.com/tarantool/tarantool/issues/4091 + // https://github.com/tarantool/tarantool/commit/17c9c034933d726925910ce5bf8b20e8e388f6e3 + for spaceName, spaceSchema := range result.Value { + fmt.Printf("Space format for '%s' is as follows:\n", spaceName) + + for _, field := range spaceSchema.Format { + fmt.Printf(" - field '%s' with type '%s'\n", field.Name, field.Type) + } + } + + // Output: + // Space format for 'test' is as follows: + // - field 'id' with type 'unsigned' + // - field 'bucket_id' with type 'unsigned' + // - field 'name' with type 'string' +} diff --git a/crud/schema.go b/crud/schema.go new file mode 100644 index 000000000..3335a3347 --- /dev/null +++ b/crud/schema.go @@ -0,0 +1,214 @@ +package crud + +import ( + "context" + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-tarantool/v2" +) + +func msgpackIsMap(code byte) bool { + return code == msgpcode.Map16 || code == msgpcode.Map32 || msgpcode.IsFixedMap(code) +} + +// SchemaRequest helps you to create request object to call `crud.schema` +// for execution by a Connection. +type SchemaRequest struct { + baseRequest + space OptString +} + +// MakeSchemaRequest returns a new empty StatsRequest. +func MakeSchemaRequest() SchemaRequest { + req := SchemaRequest{} + req.impl = newCall("crud.schema") + return req +} + +// Space sets the space name for the StatsRequest request. +// Note: default value is nil. +func (req SchemaRequest) Space(space string) SchemaRequest { + req.space = MakeOptString(space) + return req +} + +// Body fills an encoder with the call request body. +func (req SchemaRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { + if value, ok := req.space.Get(); ok { + req.impl = req.impl.Args([]interface{}{value}) + } else { + req.impl = req.impl.Args([]interface{}{}) + } + + return req.impl.Body(res, enc) +} + +// Context sets a passed context to CRUD request. +func (req SchemaRequest) Context(ctx context.Context) SchemaRequest { + req.impl = req.impl.Context(ctx) + + return req +} + +// Schema contains CRUD cluster schema definition. +type Schema map[string]SpaceSchema + +// DecodeMsgpack provides custom msgpack decoder. +func (schema *Schema) DecodeMsgpack(d *msgpack.Decoder) error { + var l int + + code, err := d.PeekCode() + if err != nil { + return err + } + + if msgpackIsArray(code) { + // Process empty schema case. + l, err = d.DecodeArrayLen() + if err != nil { + return err + } + if l != 0 { + return fmt.Errorf("expected map or empty array, got non-empty array") + } + *schema = make(map[string]SpaceSchema, l) + } else if msgpackIsMap(code) { + l, err := d.DecodeMapLen() + if err != nil { + return err + } + *schema = make(map[string]SpaceSchema, l) + + for i := 0; i < l; i++ { + key, err := d.DecodeString() + if err != nil { + return err + } + + var spaceSchema SpaceSchema + if err := d.Decode(&spaceSchema); err != nil { + return err + } + + (*schema)[key] = spaceSchema + } + } else { + return fmt.Errorf("unexpected code=%d decoding map or empty array", code) + } + + return nil +} + +// SpaceSchema contains a single CRUD space schema definition. +type SpaceSchema struct { + Format []FieldFormat `msgpack:"format"` + Indexes map[uint32]Index `msgpack:"indexes"` +} + +// Index contains a CRUD space index definition. +type Index struct { + Id uint32 `msgpack:"id"` + Name string `msgpack:"name"` + Type string `msgpack:"type"` + Unique bool `msgpack:"unique"` + Parts []IndexPart `msgpack:"parts"` +} + +// IndexField contains a CRUD space index part definition. +type IndexPart struct { + Fieldno uint32 `msgpack:"fieldno"` + Type string `msgpack:"type"` + ExcludeNull bool `msgpack:"exclude_null"` + IsNullable bool `msgpack:"is_nullable"` +} + +// SchemaResult contains a schema request result for all spaces. +type SchemaResult struct { + Value Schema +} + +// DecodeMsgpack provides custom msgpack decoder. +func (result *SchemaResult) DecodeMsgpack(d *msgpack.Decoder) error { + arrLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + if arrLen == 0 { + return fmt.Errorf("unexpected empty response array") + } + + // DecodeMapLen inside Schema decode processes `nil` as zero length map, + // so in `return nil, err` case we don't miss error info. + // https://github.com/vmihailenco/msgpack/blob/3f7bd806fea698e7a9fe80979aa3512dea0a7368/decode_map.go#L79-L81 + if err = d.Decode(&result.Value); err != nil { + return err + } + + if arrLen > 1 { + var crudErr *Error = nil + + if err := d.Decode(&crudErr); err != nil { + return err + } + + if crudErr != nil { + return crudErr + } + } + + for i := 2; i < arrLen; i++ { + if err := d.Skip(); err != nil { + return err + } + } + + return nil +} + +// SchemaResult contains a schema request result for a single space. +type SpaceSchemaResult struct { + Value SpaceSchema +} + +// DecodeMsgpack provides custom msgpack decoder. +func (result *SpaceSchemaResult) DecodeMsgpack(d *msgpack.Decoder) error { + arrLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + if arrLen == 0 { + return fmt.Errorf("unexpected empty response array") + } + + // DecodeMapLen inside SpaceSchema decode processes `nil` as zero length map, + // so in `return nil, err` case we don't miss error info. + // https://github.com/vmihailenco/msgpack/blob/3f7bd806fea698e7a9fe80979aa3512dea0a7368/decode_map.go#L79-L81 + if err = d.Decode(&result.Value); err != nil { + return err + } + + if arrLen > 1 { + var crudErr *Error = nil + + if err := d.Decode(&crudErr); err != nil { + return err + } + + if crudErr != nil { + return crudErr + } + } + + for i := 2; i < arrLen; i++ { + if err := d.Skip(); err != nil { + return err + } + } + + return nil +} diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 61425f144..25dacb91a 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -1209,6 +1209,130 @@ func TestNoreturnOptionTyped(t *testing.T) { } } +func getTestSchema(t *testing.T) crud.Schema { + schema := crud.Schema{ + "test": crud.SpaceSchema{ + Format: []crud.FieldFormat{ + crud.FieldFormat{ + Name: "id", + Type: "unsigned", + IsNullable: false, + }, + { + Name: "bucket_id", + Type: "unsigned", + IsNullable: true, + }, + { + Name: "name", + Type: "string", + IsNullable: false, + }, + }, + Indexes: map[uint32]crud.Index{ + 0: { + Id: 0, + Name: "primary_index", + Type: "TREE", + Unique: true, + Parts: []crud.IndexPart{ + { + Fieldno: 1, + Type: "unsigned", + ExcludeNull: false, + IsNullable: false, + }, + }, + }, + }, + }, + } + + // https://github.com/tarantool/tarantool/issues/4091 + uniqueIssue, err := test_helpers.IsTarantoolVersionLess(2, 2, 1) + require.Equal(t, err, nil, "expected version check to succeed") + + if uniqueIssue { + for sk, sv := range schema { + for ik, iv := range sv.Indexes { + iv.Unique = false + sv.Indexes[ik] = iv + } + schema[sk] = sv + } + } + + // https://github.com/tarantool/tarantool/commit/17c9c034933d726925910ce5bf8b20e8e388f6e3 + excludeNullUnsupported, err := test_helpers.IsTarantoolVersionLess(2, 8, 1) + require.Equal(t, err, nil, "expected version check to succeed") + + if excludeNullUnsupported { + for sk, sv := range schema { + for ik, iv := range sv.Indexes { + for pk, pv := range iv.Parts { + // Struct default value. + pv.ExcludeNull = false + iv.Parts[pk] = pv + } + sv.Indexes[ik] = iv + } + schema[sk] = sv + } + } + + return schema +} + +func TestSchemaTyped(t *testing.T) { + conn := connect(t) + defer conn.Close() + + req := crud.MakeSchemaRequest() + var result crud.SchemaResult + + err := conn.Do(req).GetTyped(&result) + require.Equal(t, err, nil, "Expected CRUD request to succeed") + require.Equal(t, result.Value, getTestSchema(t), "map with \"test\" schema expected") +} + +func TestSpaceSchemaTyped(t *testing.T) { + conn := connect(t) + defer conn.Close() + + req := crud.MakeSchemaRequest().Space("test") + var result crud.SpaceSchemaResult + + err := conn.Do(req).GetTyped(&result) + require.Equal(t, err, nil, "Expected CRUD request to succeed") + require.Equal(t, result.Value, getTestSchema(t)["test"], "map with \"test\" schema expected") +} + +func TestSpaceSchemaTypedError(t *testing.T) { + conn := connect(t) + defer conn.Close() + + req := crud.MakeSchemaRequest().Space("not_exist") + var result crud.SpaceSchemaResult + + err := conn.Do(req).GetTyped(&result) + require.NotEqual(t, err, nil, "Expected CRUD request to fail") + require.Regexp(t, "Space \"not_exist\" doesn't exist", err.Error()) +} + +func TestUnitEmptySchema(t *testing.T) { + // We need to create another cluster with no spaces + // to test `{}` schema, so let's at least add a unit test. + conn := connect(t) + defer conn.Close() + + req := tarantool.NewEvalRequest("return {}") + var result crud.SchemaResult + + err := conn.Do(req).GetTyped(&result) + require.Equal(t, err, nil, "Expected CRUD request to succeed") + require.Equal(t, result.Value, crud.Schema{}, "empty schema expected") +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body From bd6aab9db09260abe3bfe929b23e81a0171abf2b Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Thu, 12 Oct 2023 15:08:26 +0300 Subject: [PATCH 488/605] test: fix running with new crud After [1], tuple arguments are validated before space validation, so error contents assertion fails. 1. https://github.com/tarantool/crud/commit/6b20c8c5ce16a3b59ce4b998659b9baceaeb16e4 --- CHANGELOG.md | 1 + crud/tarantool_test.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88fcf1ff5..ea6c49f40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. crud.SelectRequest (#320) - Incorrect options (`vshard_router`, `fields`, `bucket_id`, `mode`, `prefer_replica`, `balance`) setup for crud.GetRequest (#335) +- Tests with crud 1.4.0 (#336) ## [1.12.0] - 2023-06-07 diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 25dacb91a..e3b8615b8 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -231,7 +231,7 @@ var testResultWithErrCases = []struct { { "ManyResult", &crud.Result{}, - crud.MakeReplaceManyRequest(invalidSpaceName).Opts(opManyOpts), + crud.MakeReplaceManyRequest(invalidSpaceName).Tuples(tuples).Opts(opManyOpts), }, { "NumberResult", From b6e8abad3c8fe9e75040138eff05ef160c0866b2 Mon Sep 17 00:00:00 2001 From: Georgiy Lebedev Date: Tue, 24 Oct 2023 12:57:31 +0300 Subject: [PATCH 489/605] crud: bump rocks minor version with compatibility fixes Needed for tarantool/tarantool#8147 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 378dced39..3af9699ef 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ clean: .PHONY: deps deps: clean ( cd ./queue/testdata; $(TTCTL) rocks install queue 1.3.0 ) - ( cd ./crud/testdata; $(TTCTL) rocks install crud 1.4.0 ) + ( cd ./crud/testdata; $(TTCTL) rocks install crud 1.4.1 ) .PHONY: datetime-timezones datetime-timezones: From a42226292f55ff7899497c43dc473899e69e13d8 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 25 Oct 2023 16:55:51 +0300 Subject: [PATCH 490/605] test: fix SQL and case sensitive cases The feature will be introduced in Tarantool 3.0 [1]. 1. https://github.com/tarantool/tarantool/pull/9249 --- CHANGELOG.md | 1 + settings/tarantool_test.go | 12 ++++++------ tarantool_test.go | 24 ++++++++++++------------ 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea6c49f40..42bcc5421 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Incorrect options (`vshard_router`, `fields`, `bucket_id`, `mode`, `prefer_replica`, `balance`) setup for crud.GetRequest (#335) - Tests with crud 1.4.0 (#336) +- Tests with case sensitive SQL (#341) ## [1.12.0] - 2023-06-07 diff --git a/settings/tarantool_test.go b/settings/tarantool_test.go index 645d48c73..2f7e01442 100644 --- a/settings/tarantool_test.go +++ b/settings/tarantool_test.go @@ -117,7 +117,7 @@ func TestSQLDefaultEngineSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "vinyl"}}, resp.Data) // Create a space with "CREATE TABLE". - exec := tarantool.NewExecuteRequest("CREATE TABLE t1_vinyl(a INT PRIMARY KEY, b INT, c INT);") + exec := tarantool.NewExecuteRequest("CREATE TABLE T1_VINYL(a INT PRIMARY KEY, b INT, c INT);") resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) @@ -143,7 +143,7 @@ func TestSQLDefaultEngineSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, resp.Data) // Create a space with "CREATE TABLE". - exec = tarantool.NewExecuteRequest("CREATE TABLE t2_memtx(a INT PRIMARY KEY, b INT, c INT);") + exec = tarantool.NewExecuteRequest("CREATE TABLE T2_MEMTX(a INT PRIMARY KEY, b INT, c INT);") resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) @@ -241,14 +241,14 @@ func TestSQLFullColumnNamesSetting(t *testing.T) { defer conn.Close() // Create a space. - exec := tarantool.NewExecuteRequest("CREATE TABLE fkname(id INT PRIMARY KEY, x INT);") + exec := tarantool.NewExecuteRequest("CREATE TABLE FKNAME(ID INT PRIMARY KEY, X INT);") resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) // Fill it with some data. - exec = tarantool.NewExecuteRequest("INSERT INTO fkname VALUES (1, 1);") + exec = tarantool.NewExecuteRequest("INSERT INTO FKNAME VALUES (1, 1);") resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) @@ -267,7 +267,7 @@ func TestSQLFullColumnNamesSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", false}}, resp.Data) // Get a data with short column names in metadata. - exec = tarantool.NewExecuteRequest("SELECT x FROM fkname WHERE id = 1;") + exec = tarantool.NewExecuteRequest("SELECT X FROM FKNAME WHERE ID = 1;") resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) @@ -286,7 +286,7 @@ func TestSQLFullColumnNamesSetting(t *testing.T) { require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, resp.Data) // Get a data with full column names in metadata. - exec = tarantool.NewExecuteRequest("SELECT x FROM fkname WHERE id = 1;") + exec = tarantool.NewExecuteRequest("SELECT X FROM FKNAME WHERE ID = 1;") resp, err = conn.Do(exec).Get() require.Nil(t, err) require.NotNil(t, resp) diff --git a/tarantool_test.go b/tarantool_test.go index bde3060c7..d5237dbb1 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -1336,18 +1336,18 @@ func TestClientSessionPush(t *testing.T) { } const ( - createTableQuery = "CREATE TABLE SQL_SPACE (id STRING PRIMARY KEY, name " + + createTableQuery = "CREATE TABLE SQL_SPACE (ID STRING PRIMARY KEY, NAME " + "STRING COLLATE \"unicode\" DEFAULT NULL);" insertQuery = "INSERT INTO SQL_SPACE VALUES (?, ?);" - selectNamedQuery = "SELECT id, name FROM SQL_SPACE WHERE id=:id AND name=:name;" - selectPosQuery = "SELECT id, name FROM SQL_SPACE WHERE id=? AND name=?;" - updateQuery = "UPDATE SQL_SPACE SET name=? WHERE id=?;" + selectNamedQuery = "SELECT ID, NAME FROM SQL_SPACE WHERE ID=:ID AND NAME=:NAME;" + selectPosQuery = "SELECT ID, NAME FROM SQL_SPACE WHERE ID=? AND NAME=?;" + updateQuery = "UPDATE SQL_SPACE SET NAME=? WHERE ID=?;" enableFullMetaDataQuery = "SET SESSION \"sql_full_metadata\" = true;" - selectSpanDifQueryNew = "SELECT id||id, name, id FROM seqscan SQL_SPACE WHERE name=?;" - selectSpanDifQueryOld = "SELECT id||id, name, id FROM SQL_SPACE WHERE name=?;" + selectSpanDifQueryNew = "SELECT ID||ID, NAME, ID FROM seqscan SQL_SPACE WHERE NAME=?;" + selectSpanDifQueryOld = "SELECT ID||ID, NAME, ID FROM SQL_SPACE WHERE NAME=?;" alterTableQuery = "ALTER TABLE SQL_SPACE RENAME TO SQL_SPACE2;" insertIncrQuery = "INSERT INTO SQL_SPACE2 VALUES (?, ?);" - deleteQuery = "DELETE FROM SQL_SPACE2 WHERE name=?;" + deleteQuery = "DELETE FROM SQL_SPACE2 WHERE NAME=?;" dropQuery = "DROP TABLE SQL_SPACE2;" dropQuery2 = "DROP TABLE SQL_SPACE;" disableFullMetaDataQuery = "SET SESSION \"sql_full_metadata\" = false;" @@ -1396,8 +1396,8 @@ func TestSQL(t *testing.T) { { selectNamedQuery, map[string]interface{}{ - "id": "1", - "name": "test", + "ID": "1", + "NAME": "test", }, Response{ SQLInfo: SQLInfo{AffectedCount: 0}, @@ -1448,14 +1448,14 @@ func TestSQL(t *testing.T) { FieldName: "COLUMN_1", FieldIsNullable: false, FieldIsAutoincrement: false, - FieldSpan: "id||id", + FieldSpan: "ID||ID", }, { FieldType: "string", FieldName: "NAME", FieldIsNullable: true, FieldIsAutoincrement: false, - FieldSpan: "name", + FieldSpan: "NAME", FieldCollation: "unicode", }, { @@ -1463,7 +1463,7 @@ func TestSQL(t *testing.T) { FieldName: "ID", FieldIsNullable: false, FieldIsAutoincrement: false, - FieldSpan: "id", + FieldSpan: "ID", FieldCollation: "", }, }}, From 3f7860eee53d241ee8c1f93482b399fad2eb0526 Mon Sep 17 00:00:00 2001 From: DerekBum Date: Tue, 24 Oct 2023 19:59:23 +0300 Subject: [PATCH 491/605] api: support `IPROTO_WATCH_ONCE` request type Add support of `IPROTO_WATCH_ONCE` request type. It works only for Tarantool version >= 3.0.0-alpha1. Part of #337 --- CHANGELOG.md | 2 ++ example_test.go | 26 ++++++++++++++++++++- export_test.go | 6 +++++ go.mod | 2 +- go.sum | 4 ++-- protocol.go | 11 +++++++-- protocol_test.go | 1 + request.go | 36 +++++++++++++++++++++++++++++ request_test.go | 18 +++++++++++++++ tarantool_test.go | 53 +++++++++++++++++++++++++++++++++++++++++-- test_helpers/utils.go | 8 +++++++ 11 files changed, 159 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42bcc5421..6a4886780 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support `fetch_latest_metadata` option for crud requests with metadata (#335) - Support `noreturn` option for data change crud requests (#335) - Support `crud.schema` request (#336) +- Support `IPROTO_WATCH_ONCE` request type for Tarantool + version >= 3.0.0-alpha1 (#337) ### Changed diff --git a/example_test.go b/example_test.go index 77b2cff24..5557e7a4c 100644 --- a/example_test.go +++ b/example_test.go @@ -626,12 +626,13 @@ func ExampleProtocolVersion() { fmt.Println("Connector client protocol feature:", f) } // Output: - // Connector client protocol version: 4 + // Connector client protocol version: 6 // Connector client protocol feature: StreamsFeature // Connector client protocol feature: TransactionsFeature // Connector client protocol feature: ErrorExtensionFeature // Connector client protocol feature: WatchersFeature // Connector client protocol feature: PaginationFeature + // Connector client protocol feature: WatchOnceFeature } func getTestTxnOpts() tarantool.Opts { @@ -1230,3 +1231,26 @@ func ExampleConnection_CloseGraceful_force() { // Result: // connection closed by client (0x4001) } + +func ExampleWatchOnceRequest() { + const key = "foo" + const value = "bar" + + // WatchOnce request present in Tarantool since version 3.0 + isLess, err := test_helpers.IsTarantoolVersionLess(3, 0, 0) + if err != nil || isLess { + return + } + + conn := exampleConnect(opts) + defer conn.Close() + + conn.Do(tarantool.NewBroadcastRequest(key).Value(value)).Get() + + resp, err := conn.Do(tarantool.NewWatchOnceRequest(key)).Get() + if err != nil { + fmt.Printf("Failed to execute the request: %s\n", err) + } else { + fmt.Println(resp.Data) + } +} diff --git a/export_test.go b/export_test.go index fc5d90c34..38211a4fc 100644 --- a/export_test.go +++ b/export_test.go @@ -120,3 +120,9 @@ func RefImplRollbackBody(enc *msgpack.Encoder) error { func RefImplIdBody(enc *msgpack.Encoder, protocolInfo ProtocolInfo) error { return fillId(enc, protocolInfo) } + +// RefImplWatchOnceBody is reference implementation for filling of an watchOnce +// request's body. +func RefImplWatchOnceBody(enc *msgpack.Encoder, key string) error { + return fillWatchOnce(enc, key) +} diff --git a/go.mod b/go.mod index 22cb7aee3..440597972 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.7.1 - github.com/tarantool/go-iproto v0.1.0 + github.com/tarantool/go-iproto v0.1.1-0.20231025103136-cb7894473931 github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca github.com/vmihailenco/msgpack/v5 v5.3.5 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect diff --git a/go.sum b/go.sum index 44d00984f..038e8af34 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tarantool/go-iproto v0.1.0 h1:zHN9AA8LDawT+JBD0/Nxgr/bIsWkkpDzpcMuaNPSIAQ= -github.com/tarantool/go-iproto v0.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= +github.com/tarantool/go-iproto v0.1.1-0.20231025103136-cb7894473931 h1:YrsRc1sDZ6HOZccvM2eJ3Nu2TMBq7NMZMsaT5KCu5qU= +github.com/tarantool/go-iproto v0.1.1-0.20231025103136-cb7894473931/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca h1:oOrBh73tDDyooIXajfr+0pfnM+89404ClAhJpTTHI7E= github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= diff --git a/protocol.go b/protocol.go index 63424283b..b947c6cbb 100644 --- a/protocol.go +++ b/protocol.go @@ -12,7 +12,7 @@ import ( type ProtocolVersion uint64 // ProtocolVersion type stores a Tarantool protocol feature. -type ProtocolFeature uint64 +type ProtocolFeature iproto.Feature // ProtocolInfo type aggregates Tarantool protocol version and features info. type ProtocolInfo struct { @@ -52,6 +52,8 @@ const ( // PaginationFeature represents support of pagination // (supported by connector). PaginationFeature ProtocolFeature = 4 + // WatchOnceFeature represents support of WatchOnce request types. + WatchOnceFeature ProtocolFeature = 6 ) // String returns the name of a Tarantool feature. @@ -68,6 +70,8 @@ func (ftr ProtocolFeature) String() string { return "WatchersFeature" case PaginationFeature: return "PaginationFeature" + case WatchOnceFeature: + return "WatchOnceFeature" default: return fmt.Sprintf("Unknown feature (code %d)", ftr) } @@ -79,7 +83,7 @@ var clientProtocolInfo ProtocolInfo = ProtocolInfo{ // introduced in master 948e5cd (possible 2.10.5 or 2.11.0). // Support of protocol version on connector side was introduced in // 1.10.0. - Version: ProtocolVersion(4), + Version: ProtocolVersion(6), // Streams and transactions were introduced in protocol version 1 // (Tarantool 2.10.0), in connector since 1.7.0. // Error extension type was introduced in protocol @@ -88,12 +92,15 @@ var clientProtocolInfo ProtocolInfo = ProtocolInfo{ // connector since 1.10.0. // Pagination were introduced in protocol version 4 (Tarantool 2.11.0), in // connector since 1.11.0. + // WatchOnce request type was introduces in protocol version 6 + // (Tarantool 3.0.0), in connector since 2.0.0. Features: []ProtocolFeature{ StreamsFeature, TransactionsFeature, ErrorExtensionFeature, WatchersFeature, PaginationFeature, + WatchOnceFeature, }, } diff --git a/protocol_test.go b/protocol_test.go index 6aa94463b..2ec4b0bc3 100644 --- a/protocol_test.go +++ b/protocol_test.go @@ -32,6 +32,7 @@ func TestFeatureStringRepresentation(t *testing.T) { require.Equal(t, ErrorExtensionFeature.String(), "ErrorExtensionFeature") require.Equal(t, WatchersFeature.String(), "WatchersFeature") require.Equal(t, PaginationFeature.String(), "PaginationFeature") + require.Equal(t, WatchOnceFeature.String(), "WatchOnceFeature") require.Equal(t, ProtocolFeature(15532).String(), "Unknown feature (code 15532)") } diff --git a/request.go b/request.go index 917c1fb6a..3332486f4 100644 --- a/request.go +++ b/request.go @@ -166,6 +166,16 @@ func fillPing(enc *msgpack.Encoder) error { return enc.EncodeMapLen(0) } +func fillWatchOnce(enc *msgpack.Encoder, key string) error { + if err := enc.EncodeMapLen(1); err != nil { + return err + } + if err := enc.EncodeUint(uint64(iproto.IPROTO_EVENT_KEY)); err != nil { + return err + } + return enc.EncodeString(key) +} + // Ping sends empty request to Tarantool to check connection. // // Deprecated: the method will be removed in the next major version, @@ -1354,3 +1364,29 @@ func (req *ExecuteRequest) Context(ctx context.Context) *ExecuteRequest { req.ctx = ctx return req } + +// WatchOnceRequest synchronously fetches the value currently associated with a +// specified notification key without subscribing to changes. +type WatchOnceRequest struct { + baseRequest + key string +} + +// NewWatchOnceRequest returns a new watchOnceRequest. +func NewWatchOnceRequest(key string) *WatchOnceRequest { + req := new(WatchOnceRequest) + req.rtype = iproto.IPROTO_WATCH_ONCE + req.key = key + return req +} + +// Body fills an msgpack.Encoder with the watchOnce request body. +func (req *WatchOnceRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { + return fillWatchOnce(enc, req.key) +} + +// Context sets a passed context to the request. +func (req *WatchOnceRequest) Context(ctx context.Context) *WatchOnceRequest { + req.ctx = ctx + return req +} diff --git a/request_test.go b/request_test.go index f44b12d52..da825b707 100644 --- a/request_test.go +++ b/request_test.go @@ -196,6 +196,7 @@ func TestRequestsTypes(t *testing.T) { {req: NewRollbackRequest(), rtype: iproto.IPROTO_ROLLBACK}, {req: NewIdRequest(validProtocolInfo), rtype: iproto.IPROTO_ID}, {req: NewBroadcastRequest(validKey), rtype: iproto.IPROTO_CALL}, + {req: NewWatchOnceRequest(validKey), rtype: iproto.IPROTO_WATCH_ONCE}, } for _, test := range tests { @@ -231,6 +232,7 @@ func TestRequestsAsync(t *testing.T) { {req: NewRollbackRequest(), async: false}, {req: NewIdRequest(validProtocolInfo), async: false}, {req: NewBroadcastRequest(validKey), async: false}, + {req: NewWatchOnceRequest(validKey), async: false}, } for _, test := range tests { @@ -265,6 +267,7 @@ func TestRequestsCtx_default(t *testing.T) { {req: NewRollbackRequest(), expected: nil}, {req: NewIdRequest(validProtocolInfo), expected: nil}, {req: NewBroadcastRequest(validKey), expected: nil}, + {req: NewWatchOnceRequest(validKey), expected: nil}, } for _, test := range tests { @@ -300,6 +303,7 @@ func TestRequestsCtx_setter(t *testing.T) { {req: NewRollbackRequest().Context(ctx), expected: ctx}, {req: NewIdRequest(validProtocolInfo).Context(ctx), expected: ctx}, {req: NewBroadcastRequest(validKey).Context(ctx), expected: ctx}, + {req: NewWatchOnceRequest(validKey).Context(ctx), expected: ctx}, } for _, test := range tests { @@ -823,3 +827,17 @@ func TestBroadcastRequestSetters(t *testing.T) { req := NewBroadcastRequest(validKey).Value(value) assertBodyEqual(t, refBuf.Bytes(), req) } + +func TestWatchOnceRequestDefaultValues(t *testing.T) { + var refBuf bytes.Buffer + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplWatchOnceBody(refEnc, validKey) + if err != nil { + t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) + return + } + + req := NewWatchOnceRequest(validKey) + assertBodyEqual(t, refBuf.Bytes(), req) +} diff --git a/tarantool_test.go b/tarantool_test.go index d5237dbb1..fc5788f23 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2606,6 +2606,53 @@ func TestConnectionDoSelectRequest(t *testing.T) { testConnectionDoSelectRequestCheck(t, resp, err, false, 10, 1010) } +func TestConnectionDoWatchOnceRequest(t *testing.T) { + test_helpers.SkipIfWatchOnceUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + _, err := conn.Do(NewBroadcastRequest("hello").Value("world")).Get() + if err != nil { + t.Fatalf("Failed to create a broadcast : %s", err.Error()) + } + + resp, err := conn.Do(NewWatchOnceRequest("hello")).Get() + if err != nil { + t.Fatalf("Failed to WatchOnce: %s", err.Error()) + } + if resp.Code != OkCode { + t.Errorf("Failed to WatchOnce: wrong code returned %d", resp.Code) + } + if len(resp.Data) < 1 || resp.Data[0] != "world" { + t.Errorf("Failed to WatchOnce: wrong value returned %v", resp.Data) + } +} + +func TestConnectionDoWatchOnceOnEmptyKey(t *testing.T) { + watchOnceNotSupported, err := test_helpers.IsTarantoolVersionLess(3, 0, 0) + if err != nil { + log.Fatalf("Could not check the Tarantool version") + } + if watchOnceNotSupported { + return + } + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + resp, err := conn.Do(NewWatchOnceRequest("notexists!")).Get() + if err != nil { + t.Fatalf("Failed to WatchOnce: %s", err.Error()) + } + if resp.Code != OkCode { + t.Errorf("Failed to WatchOnce: wrong code returned %d", resp.Code) + } + if len(resp.Data) > 0 { + t.Errorf("Failed to WatchOnce: wrong value returned %v", resp.Data) + } +} + func TestConnectionDoSelectRequest_fetch_pos(t *testing.T) { test_helpers.SkipIfPaginationUnsupported(t) @@ -3247,13 +3294,14 @@ func TestConnectionProtocolInfoSupported(t *testing.T) { require.Equal(t, clientProtocolInfo, ProtocolInfo{ - Version: ProtocolVersion(4), + Version: ProtocolVersion(6), Features: []ProtocolFeature{ StreamsFeature, TransactionsFeature, ErrorExtensionFeature, WatchersFeature, PaginationFeature, + WatchOnceFeature, }, }) @@ -3364,13 +3412,14 @@ func TestConnectionProtocolInfoUnsupported(t *testing.T) { require.Equal(t, clientProtocolInfo, ProtocolInfo{ - Version: ProtocolVersion(4), + Version: ProtocolVersion(6), Features: []ProtocolFeature{ StreamsFeature, TransactionsFeature, ErrorExtensionFeature, WatchersFeature, PaginationFeature, + WatchOnceFeature, }, }) diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 898ae84e3..d2a941775 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -192,6 +192,14 @@ func SkipIfPaginationUnsupported(t *testing.T) { SkipIfFeatureUnsupported(t, "pagination", 2, 11, 0) } +// SkipIfWatchOnceUnsupported skips test run if Tarantool without WatchOnce +// request type is used. +func SkipIfWatchOnceUnsupported(t *testing.T) { + t.Helper() + + SkipIfFeatureUnsupported(t, "watch once", 3, 0, 0) +} + // CheckEqualBoxErrors checks equivalence of tarantool.BoxError objects. // // Tarantool errors are not comparable by nature: From a664c6bbb6a1f21aecfccf199858e19a21857700 Mon Sep 17 00:00:00 2001 From: DerekBum Date: Wed, 25 Oct 2023 21:15:58 +0300 Subject: [PATCH 492/605] api: use `iproto` types and constants for the `protocol` Replaced the local `ProtocolFeature` type with the `iproto.Feature`. Replaced local `Feature` constants with their `iproto.IPROTO_FEATURE_` analogues. Closes #337 --- CHANGELOG.md | 3 + README.md | 5 ++ connection.go | 14 ++-- connection_test.go | 7 +- dial_test.go | 9 ++- example_test.go | 22 +++--- pool/connection_pool.go | 12 +-- pool/connection_pool_test.go | 27 +++---- pool/example_test.go | 28 +++++-- protocol.go | 62 +++------------- protocol_test.go | 18 +---- request_test.go | 2 +- response.go | 4 +- shutdown_test.go | 3 +- tarantool_test.go | 139 ++++++++++++++++++----------------- 15 files changed, 167 insertions(+), 188 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a4886780..63ed00b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. `pool.Connect` and `pool.Add` now accept context as first argument, which user may cancel in process. If `pool.Connect` is canceled in progress, an error will be returned. All created connections will be closed. +- `iproto.Feature` type now used instead of `ProtocolFeature` (#337) +- `iproto.IPROTO_FEATURE_` constants now used instead of local `Feature` + constants for `protocol` (#337) ### Deprecated diff --git a/README.md b/README.md index 378c344b3..2c7de68c4 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,11 @@ now does not attempt to reconnect and tries to establish a connection only once. Function might be canceled via context. Context accepted as first argument, and user may cancel it in process. +#### Protocol changes + +* `iproto.Feature` type used instead of `ProtocolFeature`. +* `iproto.IPROTO_FEATURE_` constants used instead of local ones. + ## Contributing See [the contributing guide](CONTRIBUTING.md) for detailed instructions on how diff --git a/connection.go b/connection.go index f304a12ba..018df6caa 100644 --- a/connection.go +++ b/connection.go @@ -564,7 +564,8 @@ func (conn *Connection) dial(ctx context.Context) error { // Subscribe shutdown event to process graceful shutdown. if conn.shutdownWatcher == nil && - isFeatureInSlice(WatchersFeature, conn.serverProtocolInfo.Features) { + isFeatureInSlice(iproto.IPROTO_FEATURE_WATCHERS, + conn.serverProtocolInfo.Features) { watcher, werr := conn.newWatcherImpl(shutdownEventKey, shutdownEventCallback) if werr != nil { return werr @@ -1425,7 +1426,7 @@ func subscribeWatchChannel(conn *Connection, key string) (chan watchState, error return st, nil } -func isFeatureInSlice(expected ProtocolFeature, actualSlice []ProtocolFeature) bool { +func isFeatureInSlice(expected iproto.Feature, actualSlice []iproto.Feature) bool { for _, actual := range actualSlice { if expected == actual { return true @@ -1436,8 +1437,8 @@ func isFeatureInSlice(expected ProtocolFeature, actualSlice []ProtocolFeature) b // NewWatcher creates a new Watcher object for the connection. // -// You need to require WatchersFeature to use watchers, see examples for the -// function. +// You need to require IPROTO_FEATURE_WATCHERS to use watchers, see examples +// for the function. // // After watcher creation, the watcher callback is invoked for the first time. // In this case, the callback is triggered whether or not the key has already @@ -1472,9 +1473,10 @@ func (conn *Connection) NewWatcher(key string, callback WatchCallback) (Watcher, // asynchronous. We do not expect any response from a Tarantool instance // That's why we can't just check the Tarantool response for an unsupported // request error. - if !isFeatureInSlice(WatchersFeature, conn.opts.RequiredProtocolInfo.Features) { + if !isFeatureInSlice(iproto.IPROTO_FEATURE_WATCHERS, + conn.opts.RequiredProtocolInfo.Features) { err := fmt.Errorf("the feature %s must be required by connection "+ - "options to create a watcher", WatchersFeature) + "options to create a watcher", iproto.IPROTO_FEATURE_WATCHERS) return nil, err } diff --git a/connection_test.go b/connection_test.go index 3e7be1966..20d06b183 100644 --- a/connection_test.go +++ b/connection_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" . "github.com/tarantool/go-tarantool/v2" ) @@ -12,20 +13,20 @@ func TestOptsClonePreservesRequiredProtocolFeatures(t *testing.T) { original := Opts{ RequiredProtocolInfo: ProtocolInfo{ Version: ProtocolVersion(100), - Features: []ProtocolFeature{ProtocolFeature(99), ProtocolFeature(100)}, + Features: []iproto.Feature{iproto.Feature(99), iproto.Feature(100)}, }, } origCopy := original.Clone() - original.RequiredProtocolInfo.Features[1] = ProtocolFeature(98) + original.RequiredProtocolInfo.Features[1] = iproto.Feature(98) require.Equal(t, origCopy, Opts{ RequiredProtocolInfo: ProtocolInfo{ Version: ProtocolVersion(100), - Features: []ProtocolFeature{ProtocolFeature(99), ProtocolFeature(100)}, + Features: []iproto.Feature{iproto.Feature(99), iproto.Feature(100)}, }, }) } diff --git a/dial_test.go b/dial_test.go index acd6737c5..209fc50cd 100644 --- a/dial_test.go +++ b/dial_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/test_helpers" @@ -72,8 +73,8 @@ func TestDialer_Dial_passedOpts(t *testing.T) { RequiredProtocol: tarantool.ProtocolInfo{ Auth: tarantool.ChapSha1Auth, Version: 33, - Features: []tarantool.ProtocolFeature{ - tarantool.ErrorExtensionFeature, + Features: []iproto.Feature{ + iproto.IPROTO_FEATURE_ERROR_EXTENSION, }, }, Auth: tarantool.ChapSha1Auth, @@ -302,8 +303,8 @@ func TestConn_ProtocolInfo(t *testing.T) { info := tarantool.ProtocolInfo{ Auth: tarantool.ChapSha1Auth, Version: 33, - Features: []tarantool.ProtocolFeature{ - tarantool.ErrorExtensionFeature, + Features: []iproto.Feature{ + iproto.IPROTO_FEATURE_ERROR_EXTENSION, }, } conn, dialer := dialIo(t, func(conn *mockIoConn) { diff --git a/example_test.go b/example_test.go index 5557e7a4c..f85f532d0 100644 --- a/example_test.go +++ b/example_test.go @@ -5,6 +5,8 @@ import ( "fmt" "time" + "github.com/tarantool/go-iproto" + "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/test_helpers" ) @@ -627,12 +629,12 @@ func ExampleProtocolVersion() { } // Output: // Connector client protocol version: 6 - // Connector client protocol feature: StreamsFeature - // Connector client protocol feature: TransactionsFeature - // Connector client protocol feature: ErrorExtensionFeature - // Connector client protocol feature: WatchersFeature - // Connector client protocol feature: PaginationFeature - // Connector client protocol feature: WatchOnceFeature + // Connector client protocol feature: IPROTO_FEATURE_STREAMS + // Connector client protocol feature: IPROTO_FEATURE_TRANSACTIONS + // Connector client protocol feature: IPROTO_FEATURE_ERROR_EXTENSION + // Connector client protocol feature: IPROTO_FEATURE_WATCHERS + // Connector client protocol feature: IPROTO_FEATURE_PAGINATION + // Connector client protocol feature: IPROTO_FEATURE_WATCH_ONCE } func getTestTxnOpts() tarantool.Opts { @@ -641,9 +643,9 @@ func getTestTxnOpts() tarantool.Opts { // Assert that server supports expected protocol features txnOpts.RequiredProtocolInfo = tarantool.ProtocolInfo{ Version: tarantool.ProtocolVersion(1), - Features: []tarantool.ProtocolFeature{ - tarantool.StreamsFeature, - tarantool.TransactionsFeature, + Features: []iproto.Feature{ + iproto.IPROTO_FEATURE_STREAMS, + iproto.IPROTO_FEATURE_TRANSACTIONS, }, } @@ -1168,7 +1170,7 @@ func ExampleConnection_NewWatcher() { Pass: "test", // You need to require the feature to create a watcher. RequiredProtocolInfo: tarantool.ProtocolInfo{ - Features: []tarantool.ProtocolFeature{tarantool.WatchersFeature}, + Features: []iproto.Feature{iproto.IPROTO_FEATURE_WATCHERS}, }, } ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) diff --git a/pool/connection_pool.go b/pool/connection_pool.go index e101b3208..aa84d8c24 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -17,6 +17,8 @@ import ( "sync" "time" + "github.com/tarantool/go-iproto" + "github.com/tarantool/go-tarantool/v2" ) @@ -911,8 +913,8 @@ func (p *ConnectionPool) NewPrepared(expr string, userMode Mode) (*tarantool.Pre // NewWatcher creates a new Watcher object for the connection pool. // -// You need to require WatchersFeature to use watchers, see examples for the -// function. +// You need to require IPROTO_FEATURE_WATCHERS to use watchers, see examples +// for the function. // // The behavior is same as if Connection.NewWatcher() called for each // connection with a suitable role. @@ -932,14 +934,14 @@ func (p *ConnectionPool) NewWatcher(key string, callback tarantool.WatchCallback, mode Mode) (tarantool.Watcher, error) { watchersRequired := false for _, feature := range p.connOpts.RequiredProtocolInfo.Features { - if tarantool.WatchersFeature == feature { + if iproto.IPROTO_FEATURE_WATCHERS == feature { watchersRequired = true break } } if !watchersRequired { - return nil, errors.New("the feature WatchersFeature must be " + - "required by connection options to create a watcher") + return nil, errors.New("the feature IPROTO_FEATURE_WATCHERS must " + + "be required by connection options to create a watcher") } watcher := &poolWatcher{ diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index b944d0c6c..27310be23 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" "github.com/tarantool/go-tarantool/v2" @@ -2832,7 +2833,7 @@ func TestConnectionPool_NewWatcher_noWatchersFeature(t *testing.T) { roles := []bool{true, false, false, true, true} opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{} + opts.RequiredProtocolInfo.Features = []iproto.Feature{} err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") @@ -2847,8 +2848,8 @@ func TestConnectionPool_NewWatcher_noWatchersFeature(t *testing.T) { func(event tarantool.WatchEvent) {}, pool.ANY) require.Nilf(t, watcher, "watcher must not be created") require.NotNilf(t, err, "an error is expected") - expected := "the feature WatchersFeature must be required by connection " + - "options to create a watcher" + expected := "the feature IPROTO_FEATURE_WATCHERS must be required by " + + "connection options to create a watcher" require.Equal(t, expected, err.Error()) } @@ -2860,8 +2861,8 @@ func TestConnectionPool_NewWatcher_modes(t *testing.T) { roles := []bool{true, false, false, true, true} opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{ - tarantool.WatchersFeature, + opts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") @@ -2941,8 +2942,8 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { roles := []bool{true, false, false, true, true} opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{ - tarantool.WatchersFeature, + opts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") @@ -3030,8 +3031,8 @@ func TestWatcher_Unregister(t *testing.T) { roles := []bool{true, false, false, true, true} opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{ - tarantool.WatchersFeature, + opts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") @@ -3091,8 +3092,8 @@ func TestConnectionPool_NewWatcher_concurrent(t *testing.T) { roles := []bool{true, false, false, true, true} opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{ - tarantool.WatchersFeature, + opts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") @@ -3133,8 +3134,8 @@ func TestWatcher_Unregister_concurrent(t *testing.T) { roles := []bool{true, false, false, true, true} opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{ - tarantool.WatchersFeature, + opts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } err := test_helpers.SetClusterRO(servers, opts, roles) require.Nilf(t, err, "fail to set roles for cluster") diff --git a/pool/example_test.go b/pool/example_test.go index dae28de90..a38ae2831 100644 --- a/pool/example_test.go +++ b/pool/example_test.go @@ -2,8 +2,11 @@ package pool_test import ( "fmt" + "strings" "time" + "github.com/tarantool/go-iproto" + "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/pool" "github.com/tarantool/go-tarantool/v2/test_helpers" @@ -92,8 +95,8 @@ func ExampleConnectionPool_NewWatcher() { const value = "bar" opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{ - tarantool.WatchersFeature, + opts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } connPool, err := examplePool(testRoles, connOpts) @@ -123,7 +126,7 @@ func ExampleConnectionPool_NewWatcher_noWatchersFeature() { const key = "foo" opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []tarantool.ProtocolFeature{} + opts.RequiredProtocolInfo.Features = []iproto.Feature{} connPool, err := examplePool(testRoles, connOpts) if err != nil { @@ -134,10 +137,19 @@ func ExampleConnectionPool_NewWatcher_noWatchersFeature() { callback := func(event tarantool.WatchEvent) {} watcher, err := connPool.NewWatcher(key, callback, pool.ANY) fmt.Println(watcher) - fmt.Println(err) + if err != nil { + // Need to split the error message into two lines to pass + // golangci-lint. + str := err.Error() + fmt.Println(strings.Trim(str[:56], " ")) + fmt.Println(str[56:]) + } else { + fmt.Println(err) + } // Output: // - // the feature WatchersFeature must be required by connection options to create a watcher + // the feature IPROTO_FEATURE_WATCHERS must be required by + // connection options to create a watcher } func getTestTxnOpts() tarantool.Opts { @@ -146,9 +158,9 @@ func getTestTxnOpts() tarantool.Opts { // Assert that server supports expected protocol features txnOpts.RequiredProtocolInfo = tarantool.ProtocolInfo{ Version: tarantool.ProtocolVersion(1), - Features: []tarantool.ProtocolFeature{ - tarantool.StreamsFeature, - tarantool.TransactionsFeature, + Features: []iproto.Feature{ + iproto.IPROTO_FEATURE_STREAMS, + iproto.IPROTO_FEATURE_TRANSACTIONS, }, } diff --git a/protocol.go b/protocol.go index b947c6cbb..7de8b197e 100644 --- a/protocol.go +++ b/protocol.go @@ -2,7 +2,6 @@ package tarantool import ( "context" - "fmt" "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" @@ -11,9 +10,6 @@ import ( // ProtocolVersion type stores Tarantool protocol version. type ProtocolVersion uint64 -// ProtocolVersion type stores a Tarantool protocol feature. -type ProtocolFeature iproto.Feature - // ProtocolInfo type aggregates Tarantool protocol version and features info. type ProtocolInfo struct { // Auth is an authentication method. @@ -21,7 +17,7 @@ type ProtocolInfo struct { // Version is the supported protocol version. Version ProtocolVersion // Features are supported protocol features. - Features []ProtocolFeature + Features []iproto.Feature } // Clone returns an exact copy of the ProtocolInfo object. @@ -30,53 +26,13 @@ func (info ProtocolInfo) Clone() ProtocolInfo { infoCopy := info if info.Features != nil { - infoCopy.Features = make([]ProtocolFeature, len(info.Features)) + infoCopy.Features = make([]iproto.Feature, len(info.Features)) copy(infoCopy.Features, info.Features) } return infoCopy } -const ( - // StreamsFeature represents streams support (supported by connector). - StreamsFeature ProtocolFeature = 0 - // TransactionsFeature represents interactive transactions support. - // (supported by connector). - TransactionsFeature ProtocolFeature = 1 - // ErrorExtensionFeature represents support of MP_ERROR objects over MessagePack - // (supported by connector). - ErrorExtensionFeature ProtocolFeature = 2 - // WatchersFeature represents support of watchers - // (supported by connector). - WatchersFeature ProtocolFeature = 3 - // PaginationFeature represents support of pagination - // (supported by connector). - PaginationFeature ProtocolFeature = 4 - // WatchOnceFeature represents support of WatchOnce request types. - WatchOnceFeature ProtocolFeature = 6 -) - -// String returns the name of a Tarantool feature. -// If value X is not a known feature, returns "Unknown feature (code X)" string. -func (ftr ProtocolFeature) String() string { - switch ftr { - case StreamsFeature: - return "StreamsFeature" - case TransactionsFeature: - return "TransactionsFeature" - case ErrorExtensionFeature: - return "ErrorExtensionFeature" - case WatchersFeature: - return "WatchersFeature" - case PaginationFeature: - return "PaginationFeature" - case WatchOnceFeature: - return "WatchOnceFeature" - default: - return fmt.Sprintf("Unknown feature (code %d)", ftr) - } -} - var clientProtocolInfo ProtocolInfo = ProtocolInfo{ // Protocol version supported by connector. Version 3 // was introduced in Tarantool 2.10.0, version 4 was @@ -94,13 +50,13 @@ var clientProtocolInfo ProtocolInfo = ProtocolInfo{ // connector since 1.11.0. // WatchOnce request type was introduces in protocol version 6 // (Tarantool 3.0.0), in connector since 2.0.0. - Features: []ProtocolFeature{ - StreamsFeature, - TransactionsFeature, - ErrorExtensionFeature, - WatchersFeature, - PaginationFeature, - WatchOnceFeature, + Features: []iproto.Feature{ + iproto.IPROTO_FEATURE_STREAMS, + iproto.IPROTO_FEATURE_TRANSACTIONS, + iproto.IPROTO_FEATURE_ERROR_EXTENSION, + iproto.IPROTO_FEATURE_WATCHERS, + iproto.IPROTO_FEATURE_PAGINATION, + iproto.IPROTO_FEATURE_WATCH_ONCE, }, } diff --git a/protocol_test.go b/protocol_test.go index 2ec4b0bc3..81ed2d3b5 100644 --- a/protocol_test.go +++ b/protocol_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" . "github.com/tarantool/go-tarantool/v2" ) @@ -11,28 +12,17 @@ import ( func TestProtocolInfoClonePreservesFeatures(t *testing.T) { original := ProtocolInfo{ Version: ProtocolVersion(100), - Features: []ProtocolFeature{ProtocolFeature(99), ProtocolFeature(100)}, + Features: []iproto.Feature{iproto.Feature(99), iproto.Feature(100)}, } origCopy := original.Clone() - original.Features[1] = ProtocolFeature(98) + original.Features[1] = iproto.Feature(98) require.Equal(t, origCopy, ProtocolInfo{ Version: ProtocolVersion(100), - Features: []ProtocolFeature{ProtocolFeature(99), ProtocolFeature(100)}, + Features: []iproto.Feature{iproto.Feature(99), iproto.Feature(100)}, }) } - -func TestFeatureStringRepresentation(t *testing.T) { - require.Equal(t, StreamsFeature.String(), "StreamsFeature") - require.Equal(t, TransactionsFeature.String(), "TransactionsFeature") - require.Equal(t, ErrorExtensionFeature.String(), "ErrorExtensionFeature") - require.Equal(t, WatchersFeature.String(), "WatchersFeature") - require.Equal(t, PaginationFeature.String(), "PaginationFeature") - require.Equal(t, WatchOnceFeature.String(), "WatchOnceFeature") - - require.Equal(t, ProtocolFeature(15532).String(), "Unknown feature (code 15532)") -} diff --git a/request_test.go b/request_test.go index da825b707..6d08533d3 100644 --- a/request_test.go +++ b/request_test.go @@ -35,7 +35,7 @@ var validStmt *Prepared = &Prepared{StatementID: 1, Conn: &Connection{}} var validProtocolInfo ProtocolInfo = ProtocolInfo{ Version: ProtocolVersion(3), - Features: []ProtocolFeature{StreamsFeature}, + Features: []iproto.Feature{iproto.IPROTO_FEATURE_STREAMS}, } type ValidSchemeResolver struct { diff --git a/response.go b/response.go index 7ee314e04..0d6d062b8 100644 --- a/response.go +++ b/response.go @@ -156,7 +156,7 @@ func (resp *Response) decodeBody() (err error) { var l, larr int var stmtID, bindCount uint64 var serverProtocolInfo ProtocolInfo - var feature ProtocolFeature + var feature iproto.Feature var errorExtendedInfo *BoxError = nil d := msgpack.NewDecoder(&resp.buf) @@ -215,7 +215,7 @@ func (resp *Response) decodeBody() (err error) { return err } - serverProtocolInfo.Features = make([]ProtocolFeature, larr) + serverProtocolInfo.Features = make([]iproto.Feature, larr) for i := 0; i < larr; i++ { if err = d.Decode(&feature); err != nil { return err diff --git a/shutdown_test.go b/shutdown_test.go index 412d27ea4..cf4894344 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" . "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/test_helpers" ) @@ -25,7 +26,7 @@ var shtdnClntOpts = Opts{ Timeout: 20 * time.Second, Reconnect: 500 * time.Millisecond, MaxReconnects: 10, - RequiredProtocolInfo: ProtocolInfo{Features: []ProtocolFeature{WatchersFeature}}, + RequiredProtocolInfo: ProtocolInfo{Features: []iproto.Feature{iproto.IPROTO_FEATURE_WATCHERS}}, } var shtdnSrvOpts = test_helpers.StartOpts{ InitScript: "config.lua", diff --git a/tarantool_test.go b/tarantool_test.go index fc5788f23..0ad365286 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3273,20 +3273,21 @@ func TestConnectionProtocolInfoSupported(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - // First Tarantool protocol version (1, StreamsFeature and TransactionsFeature) - // was introduced between 2.10.0-beta1 and 2.10.0-beta2. - // Versions 2 (ErrorExtensionFeature) and 3 (WatchersFeature) were also - // introduced between 2.10.0-beta1 and 2.10.0-beta2. Version 4 - // (PaginationFeature) was introduced in master 948e5cd (possible 2.10.5 or - // 2.11.0). So each release Tarantool >= 2.10 (same as each Tarantool with - // id support) has protocol version >= 3 and first four features. + // First Tarantool protocol version (1, IPROTO_FEATURE_STREAMS and + // IPROTO_FEATURE_TRANSACTIONS) was introduced between 2.10.0-beta1 and + // 2.10.0-beta2. Versions 2 (IPROTO_FEATURE_ERROR_EXTENSION) and + // 3 (IPROTO_FEATURE_WATCHERS) were also introduced between 2.10.0-beta1 and + // 2.10.0-beta2. Version 4 (IPROTO_FEATURE_PAGINATION) was introduced in + // master 948e5cd (possible 2.10.5 or 2.11.0). So each release + // Tarantool >= 2.10 (same as each Tarantool with id support) has protocol + // version >= 3 and first four features. tarantool210ProtocolInfo := ProtocolInfo{ Version: ProtocolVersion(3), - Features: []ProtocolFeature{ - StreamsFeature, - TransactionsFeature, - ErrorExtensionFeature, - WatchersFeature, + Features: []iproto.Feature{ + iproto.IPROTO_FEATURE_STREAMS, + iproto.IPROTO_FEATURE_TRANSACTIONS, + iproto.IPROTO_FEATURE_ERROR_EXTENSION, + iproto.IPROTO_FEATURE_WATCHERS, }, } @@ -3295,13 +3296,13 @@ func TestConnectionProtocolInfoSupported(t *testing.T) { clientProtocolInfo, ProtocolInfo{ Version: ProtocolVersion(6), - Features: []ProtocolFeature{ - StreamsFeature, - TransactionsFeature, - ErrorExtensionFeature, - WatchersFeature, - PaginationFeature, - WatchOnceFeature, + Features: []iproto.Feature{ + iproto.IPROTO_FEATURE_STREAMS, + iproto.IPROTO_FEATURE_TRANSACTIONS, + iproto.IPROTO_FEATURE_ERROR_EXTENSION, + iproto.IPROTO_FEATURE_WATCHERS, + iproto.IPROTO_FEATURE_PAGINATION, + iproto.IPROTO_FEATURE_WATCH_ONCE, }, }) @@ -3322,17 +3323,17 @@ func TestClientIdRequestObject(t *testing.T) { tarantool210ProtocolInfo := ProtocolInfo{ Version: ProtocolVersion(3), - Features: []ProtocolFeature{ - StreamsFeature, - TransactionsFeature, - ErrorExtensionFeature, - WatchersFeature, + Features: []iproto.Feature{ + iproto.IPROTO_FEATURE_STREAMS, + iproto.IPROTO_FEATURE_TRANSACTIONS, + iproto.IPROTO_FEATURE_ERROR_EXTENSION, + iproto.IPROTO_FEATURE_WATCHERS, }, } req := NewIdRequest(ProtocolInfo{ Version: ProtocolVersion(1), - Features: []ProtocolFeature{StreamsFeature}, + Features: []iproto.Feature{iproto.IPROTO_FEATURE_STREAMS}, }) resp, err := conn.Do(req).Get() require.Nilf(t, err, "No errors on Id request execution") @@ -3358,17 +3359,17 @@ func TestClientIdRequestObjectWithNilContext(t *testing.T) { tarantool210ProtocolInfo := ProtocolInfo{ Version: ProtocolVersion(3), - Features: []ProtocolFeature{ - StreamsFeature, - TransactionsFeature, - ErrorExtensionFeature, - WatchersFeature, + Features: []iproto.Feature{ + iproto.IPROTO_FEATURE_STREAMS, + iproto.IPROTO_FEATURE_TRANSACTIONS, + iproto.IPROTO_FEATURE_ERROR_EXTENSION, + iproto.IPROTO_FEATURE_WATCHERS, }, } req := NewIdRequest(ProtocolInfo{ Version: ProtocolVersion(1), - Features: []ProtocolFeature{StreamsFeature}, + Features: []iproto.Feature{iproto.IPROTO_FEATURE_STREAMS}, }).Context(nil) //nolint resp, err := conn.Do(req).Get() require.Nilf(t, err, "No errors on Id request execution") @@ -3393,7 +3394,7 @@ func TestClientIdRequestObjectWithPassedCanceledContext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) req := NewIdRequest(ProtocolInfo{ Version: ProtocolVersion(1), - Features: []ProtocolFeature{StreamsFeature}, + Features: []iproto.Feature{iproto.IPROTO_FEATURE_STREAMS}, }).Context(ctx) //nolint cancel() resp, err := conn.Do(req).Get() @@ -3413,13 +3414,13 @@ func TestConnectionProtocolInfoUnsupported(t *testing.T) { clientProtocolInfo, ProtocolInfo{ Version: ProtocolVersion(6), - Features: []ProtocolFeature{ - StreamsFeature, - TransactionsFeature, - ErrorExtensionFeature, - WatchersFeature, - PaginationFeature, - WatchOnceFeature, + Features: []iproto.Feature{ + iproto.IPROTO_FEATURE_STREAMS, + iproto.IPROTO_FEATURE_TRANSACTIONS, + iproto.IPROTO_FEATURE_ERROR_EXTENSION, + iproto.IPROTO_FEATURE_WATCHERS, + iproto.IPROTO_FEATURE_PAGINATION, + iproto.IPROTO_FEATURE_WATCH_ONCE, }, }) @@ -3433,7 +3434,7 @@ func TestConnectionClientFeaturesUmmutable(t *testing.T) { info := conn.ClientProtocolInfo() infoOrig := info.Clone() - info.Features[0] = ProtocolFeature(15532) + info.Features[0] = iproto.Feature(15532) require.Equal(t, conn.ClientProtocolInfo(), infoOrig) require.NotEqual(t, conn.ClientProtocolInfo(), info) @@ -3447,7 +3448,7 @@ func TestConnectionServerFeaturesUmmutable(t *testing.T) { info := conn.ServerProtocolInfo() infoOrig := info.Clone() - info.Features[0] = ProtocolFeature(15532) + info.Features[0] = iproto.Feature(15532) require.Equal(t, conn.ServerProtocolInfo(), infoOrig) require.NotEqual(t, conn.ServerProtocolInfo(), info) @@ -3493,7 +3494,7 @@ func TestConnectionProtocolFeatureRequirementSuccess(t *testing.T) { connOpts := opts.Clone() connOpts.RequiredProtocolInfo = ProtocolInfo{ - Features: []ProtocolFeature{TransactionsFeature}, + Features: []iproto.Feature{iproto.IPROTO_FEATURE_TRANSACTIONS}, } ctx, cancel := test_helpers.GetConnectContext() @@ -3511,7 +3512,7 @@ func TestConnectionProtocolFeatureRequirementFail(t *testing.T) { connOpts := opts.Clone() connOpts.RequiredProtocolInfo = ProtocolInfo{ - Features: []ProtocolFeature{TransactionsFeature}, + Features: []iproto.Feature{iproto.IPROTO_FEATURE_TRANSACTIONS}, } ctx, cancel := test_helpers.GetConnectContext() @@ -3521,7 +3522,8 @@ func TestConnectionProtocolFeatureRequirementFail(t *testing.T) { require.Nilf(t, conn, "Connect fail") require.NotNilf(t, err, "Got error on connect") require.Contains(t, err.Error(), - "invalid server protocol: protocol feature TransactionsFeature is not supported") + "invalid server protocol: protocol feature "+ + "IPROTO_FEATURE_TRANSACTIONS is not supported") } func TestConnectionProtocolFeatureRequirementManyFail(t *testing.T) { @@ -3529,7 +3531,8 @@ func TestConnectionProtocolFeatureRequirementManyFail(t *testing.T) { connOpts := opts.Clone() connOpts.RequiredProtocolInfo = ProtocolInfo{ - Features: []ProtocolFeature{TransactionsFeature, ProtocolFeature(15532)}, + Features: []iproto.Feature{iproto.IPROTO_FEATURE_TRANSACTIONS, + iproto.Feature(15532)}, } ctx, cancel := test_helpers.GetConnectContext() @@ -3540,8 +3543,8 @@ func TestConnectionProtocolFeatureRequirementManyFail(t *testing.T) { require.NotNilf(t, err, "Got error on connect") require.Contains(t, err.Error(), - "invalid server protocol: protocol features TransactionsFeature, "+ - "Unknown feature (code 15532) are not supported") + "invalid server protocol: protocol features IPROTO_FEATURE_TRANSACTIONS, "+ + "Feature(15532) are not supported") } func TestConnectionFeatureOptsImmutable(t *testing.T) { @@ -3564,7 +3567,7 @@ func TestConnectionFeatureOptsImmutable(t *testing.T) { connOpts.Reconnect = timeout connOpts.MaxReconnects = retries connOpts.RequiredProtocolInfo = ProtocolInfo{ - Features: []ProtocolFeature{TransactionsFeature}, + Features: []iproto.Feature{iproto.IPROTO_FEATURE_TRANSACTIONS}, } // Connect with valid opts @@ -3572,7 +3575,7 @@ func TestConnectionFeatureOptsImmutable(t *testing.T) { defer conn.Close() // Change opts outside - connOpts.RequiredProtocolInfo.Features[0] = ProtocolFeature(15532) + connOpts.RequiredProtocolInfo.Features[0] = iproto.Feature(15532) // Trigger reconnect with opts re-check test_helpers.StopTarantool(inst) @@ -3685,8 +3688,8 @@ func TestConnection_NewWatcher(t *testing.T) { const key = "TestConnection_NewWatcher" connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ - WatchersFeature, + connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } conn := test_helpers.ConnectWithValidation(t, server, connOpts) defer conn.Close() @@ -3721,15 +3724,15 @@ func TestConnection_NewWatcher(t *testing.T) { func TestConnection_NewWatcher_noWatchersFeature(t *testing.T) { const key = "TestConnection_NewWatcher_noWatchersFeature" connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{} + connOpts.RequiredProtocolInfo.Features = []iproto.Feature{} conn := test_helpers.ConnectWithValidation(t, server, connOpts) defer conn.Close() watcher, err := conn.NewWatcher(key, func(event WatchEvent) {}) require.Nilf(t, watcher, "watcher must not be created") require.NotNilf(t, err, "an error is expected") - expected := "the feature WatchersFeature must be required by connection " + - "options to create a watcher" + expected := "the feature IPROTO_FEATURE_WATCHERS must be required by " + + "connection options to create a watcher" require.Equal(t, expected, err.Error()) } @@ -3756,8 +3759,8 @@ func TestConnection_NewWatcher_reconnect(t *testing.T) { reconnectOpts := opts reconnectOpts.Reconnect = 100 * time.Millisecond reconnectOpts.MaxReconnects = 10 - reconnectOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ - WatchersFeature, + reconnectOpts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } conn := test_helpers.ConnectWithValidation(t, server, reconnectOpts) defer conn.Close() @@ -3794,8 +3797,8 @@ func TestBroadcastRequest(t *testing.T) { const value = "bar" connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ - WatchersFeature, + connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } conn := test_helpers.ConnectWithValidation(t, server, connOpts) defer conn.Close() @@ -3844,8 +3847,8 @@ func TestBroadcastRequest_multi(t *testing.T) { const key = "TestBroadcastRequest_multi" connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ - WatchersFeature, + connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } conn := test_helpers.ConnectWithValidation(t, server, connOpts) defer conn.Close() @@ -3892,8 +3895,8 @@ func TestConnection_NewWatcher_multiOnKey(t *testing.T) { const value = "bar" connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ - WatchersFeature, + connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } conn := test_helpers.ConnectWithValidation(t, server, connOpts) defer conn.Close() @@ -3955,8 +3958,8 @@ func TestWatcher_Unregister(t *testing.T) { const value = "bar" connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ - WatchersFeature, + connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } conn := test_helpers.ConnectWithValidation(t, server, connOpts) defer conn.Close() @@ -3991,8 +3994,8 @@ func TestConnection_NewWatcher_concurrent(t *testing.T) { const testConcurrency = 1000 const key = "TestConnection_NewWatcher_concurrent" connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ - WatchersFeature, + connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } conn := test_helpers.ConnectWithValidation(t, server, connOpts) defer conn.Close() @@ -4036,8 +4039,8 @@ func TestWatcher_Unregister_concurrent(t *testing.T) { const testConcurrency = 1000 const key = "TestWatcher_Unregister_concurrent" connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []ProtocolFeature{ - WatchersFeature, + connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ + iproto.IPROTO_FEATURE_WATCHERS, } conn := test_helpers.ConnectWithValidation(t, server, connOpts) defer conn.Close() From 76d4125b619a85fc140ca3fd7be8a2b8f602db96 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 31 Oct 2023 16:54:56 +0300 Subject: [PATCH 493/605] crud: change timeout option type `Timeout` is an option supported for all crud operations using VShard API. In Lua, timeout has `number` type, so float values are allowed. Float timeouts can be useful to set the timeout less than a second. After this patch, `Timeout` will be an optional float64 instead of an optional int. This is a breaking change. CRUD allows to use negative timeouts. This approach does not make any sense for an operation: for example, initial schema fetch will fail. Since the module itself does not check for a negative value argument, we do not forbid it here too. Closes #342 --- CHANGELOG.md | 2 ++ README.md | 6 ++++++ crud/count.go | 2 +- crud/get.go | 2 +- crud/options.go | 31 +++++++++++++++++++++++++------ crud/request_test.go | 4 ++-- crud/select.go | 2 +- crud/tarantool_test.go | 24 ++++++++++++------------ 8 files changed, 50 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ed00b39..fbec3f1b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - `iproto.Feature` type now used instead of `ProtocolFeature` (#337) - `iproto.IPROTO_FEATURE_` constants now used instead of local `Feature` constants for `protocol` (#337) +- Change `crud` operations `Timeout` option type to `crud.OptFloat64` + instead of `crud.OptUint` (#342) ### Deprecated diff --git a/README.md b/README.md index 2c7de68c4..eae6f92ab 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ faster than other packages according to public benchmarks. * [decimal package](#decimal-package) * [multi package](#multi-package) * [pool package](#pool-package) + * [crud package](#crud-package) * [msgpack.v5](#msgpackv5) * [Call = Call17](#call--call17) * [IPROTO constants](#iproto-constants) @@ -187,6 +188,11 @@ The subpackage has been deleted. You could use `pool` instead. * `pool.Add` now accepts context as first argument, which user may cancel in process. +#### crud package + +* `crud` operations `Timeout` option has `crud.OptFloat64` type + instead of `crud.OptUint`. + #### msgpack.v5 Most function names and argument types in `msgpack.v5` and `msgpack.v2` diff --git a/crud/count.go b/crud/count.go index dc89f916e..e4c163249 100644 --- a/crud/count.go +++ b/crud/count.go @@ -15,7 +15,7 @@ type CountResult = NumberResult type CountOpts struct { // Timeout is a `vshard.call` timeout and vshard // master discovery timeout (in seconds). - Timeout OptUint + Timeout OptFloat64 // VshardRouter is cartridge vshard group name or // vshard router instance. VshardRouter OptString diff --git a/crud/get.go b/crud/get.go index fcfef5426..6ab91440e 100644 --- a/crud/get.go +++ b/crud/get.go @@ -12,7 +12,7 @@ import ( type GetOpts struct { // Timeout is a `vshard.call` timeout and vshard // master discovery timeout (in seconds). - Timeout OptUint + Timeout OptFloat64 // VshardRouter is cartridge vshard group name or // vshard router instance. VshardRouter OptString diff --git a/crud/options.go b/crud/options.go index 214417d1f..eec3fc0db 100644 --- a/crud/options.go +++ b/crud/options.go @@ -63,6 +63,25 @@ func (opt OptInt) Get() (int, bool) { return opt.value, opt.exist } +// OptFloat64 is an optional float64. +type OptFloat64 struct { + value float64 + exist bool +} + +// MakeOptFloat64 creates an optional float64 from value. +func MakeOptFloat64(value float64) OptFloat64 { + return OptFloat64{ + value: value, + exist: true, + } +} + +// Get returns the float64 value or an error if not present. +func (opt OptFloat64) Get() (float64, bool) { + return opt.value, opt.exist +} + // OptString is an optional string. type OptString struct { value string @@ -120,7 +139,7 @@ func (o *OptTuple) Get() (interface{}, bool) { type BaseOpts struct { // Timeout is a `vshard.call` timeout and vshard // master discovery timeout (in seconds). - Timeout OptUint + Timeout OptFloat64 // VshardRouter is cartridge vshard group name or // vshard router instance. VshardRouter OptString @@ -144,7 +163,7 @@ func (opts BaseOpts) EncodeMsgpack(enc *msgpack.Encoder) error { type SimpleOperationOpts struct { // Timeout is a `vshard.call` timeout and vshard // master discovery timeout (in seconds). - Timeout OptUint + Timeout OptFloat64 // VshardRouter is cartridge vshard group name or // vshard router instance. VshardRouter OptString @@ -186,7 +205,7 @@ func (opts SimpleOperationOpts) EncodeMsgpack(enc *msgpack.Encoder) error { type SimpleOperationObjectOpts struct { // Timeout is a `vshard.call` timeout and vshard // master discovery timeout (in seconds). - Timeout OptUint + Timeout OptFloat64 // VshardRouter is cartridge vshard group name or // vshard router instance. VshardRouter OptString @@ -232,7 +251,7 @@ func (opts SimpleOperationObjectOpts) EncodeMsgpack(enc *msgpack.Encoder) error type OperationManyOpts struct { // Timeout is a `vshard.call` timeout and vshard // master discovery timeout (in seconds). - Timeout OptUint + Timeout OptFloat64 // VshardRouter is cartridge vshard group name or // vshard router instance. VshardRouter OptString @@ -280,7 +299,7 @@ func (opts OperationManyOpts) EncodeMsgpack(enc *msgpack.Encoder) error { type OperationObjectManyOpts struct { // Timeout is a `vshard.call` timeout and vshard // master discovery timeout (in seconds). - Timeout OptUint + Timeout OptFloat64 // VshardRouter is cartridge vshard group name or // vshard router instance. VshardRouter OptString @@ -332,7 +351,7 @@ func (opts OperationObjectManyOpts) EncodeMsgpack(enc *msgpack.Encoder) error { type BorderOpts struct { // Timeout is a `vshard.call` timeout and vshard // master discovery timeout (in seconds). - Timeout OptUint + Timeout OptFloat64 // VshardRouter is cartridge vshard group name or // vshard router instance. VshardRouter OptString diff --git a/crud/request_test.go b/crud/request_test.go index 4c49a9214..8a64ab427 100644 --- a/crud/request_test.go +++ b/crud/request_test.go @@ -137,7 +137,7 @@ func BenchmarkLenRequest(b *testing.B) { buf.Reset() req := crud.MakeLenRequest(spaceName). Opts(crud.LenOpts{ - Timeout: crud.MakeOptUint(3), + Timeout: crud.MakeOptFloat64(3.5), }) if err := req.Body(nil, enc); err != nil { b.Error(err) @@ -156,7 +156,7 @@ func BenchmarkSelectRequest(b *testing.B) { buf.Reset() req := crud.MakeSelectRequest(spaceName). Opts(crud.SelectOpts{ - Timeout: crud.MakeOptUint(3), + Timeout: crud.MakeOptFloat64(3.5), VshardRouter: crud.MakeOptString("asd"), Balance: crud.MakeOptBool(true), }) diff --git a/crud/select.go b/crud/select.go index 0d0b5708a..198f74bae 100644 --- a/crud/select.go +++ b/crud/select.go @@ -12,7 +12,7 @@ import ( type SelectOpts struct { // Timeout is a `vshard.call` timeout and vshard // master discovery timeout (in seconds). - Timeout OptUint + Timeout OptFloat64 // VshardRouter is cartridge vshard group name or // vshard router instance. VshardRouter OptString diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index e3b8615b8..511980bd9 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -37,7 +37,7 @@ var startOpts test_helpers.StartOpts = test_helpers.StartOpts{ RetryTimeout: 500 * time.Millisecond, } -var timeout = uint(1) +var timeout = float64(1.1) var operations = []crud.Operation{ { @@ -48,43 +48,43 @@ var operations = []crud.Operation{ } var selectOpts = crud.SelectOpts{ - Timeout: crud.MakeOptUint(timeout), + Timeout: crud.MakeOptFloat64(timeout), } var countOpts = crud.CountOpts{ - Timeout: crud.MakeOptUint(timeout), + Timeout: crud.MakeOptFloat64(timeout), } var getOpts = crud.GetOpts{ - Timeout: crud.MakeOptUint(timeout), + Timeout: crud.MakeOptFloat64(timeout), } var minOpts = crud.MinOpts{ - Timeout: crud.MakeOptUint(timeout), + Timeout: crud.MakeOptFloat64(timeout), } var maxOpts = crud.MaxOpts{ - Timeout: crud.MakeOptUint(timeout), + Timeout: crud.MakeOptFloat64(timeout), } var baseOpts = crud.BaseOpts{ - Timeout: crud.MakeOptUint(timeout), + Timeout: crud.MakeOptFloat64(timeout), } var simpleOperationOpts = crud.SimpleOperationOpts{ - Timeout: crud.MakeOptUint(timeout), + Timeout: crud.MakeOptFloat64(timeout), } var simpleOperationObjectOpts = crud.SimpleOperationObjectOpts{ - Timeout: crud.MakeOptUint(timeout), + Timeout: crud.MakeOptFloat64(timeout), } var opManyOpts = crud.OperationManyOpts{ - Timeout: crud.MakeOptUint(timeout), + Timeout: crud.MakeOptFloat64(timeout), } var opObjManyOpts = crud.OperationObjectManyOpts{ - Timeout: crud.MakeOptUint(timeout), + Timeout: crud.MakeOptFloat64(timeout), } var conditions = []crud.Condition{ @@ -815,7 +815,7 @@ func TestGetAdditionalOpts(t *testing.T) { defer conn.Close() req := crud.MakeGetRequest(spaceName).Key(key).Opts(crud.GetOpts{ - Timeout: crud.MakeOptUint(1), + Timeout: crud.MakeOptFloat64(1.1), Fields: crud.MakeOptTuple([]interface{}{"name"}), Mode: crud.MakeOptString("read"), PreferReplica: crud.MakeOptBool(true), From ce8e039506f940b2bd49e2691695dc52f85c3db4 Mon Sep 17 00:00:00 2001 From: Igor Zolotarev Date: Thu, 2 Nov 2023 22:49:35 +0300 Subject: [PATCH 494/605] doc: rename NewDatetime to MakeDatetime in comments --- datetime/datetime.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datetime/datetime.go b/datetime/datetime.go index 2ac309eee..f5a2a8278 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -101,7 +101,7 @@ const ( // [-12 * 60 * 60, 14 * 60 * 60]. // // NOTE: Tarantool's datetime.tz value is picked from t.Location().String(). -// "Local" location is unsupported, see ExampleNewDatetime_localUnsupported. +// "Local" location is unsupported, see ExampleMakeDatetime_localUnsupported. func MakeDatetime(t time.Time) (Datetime, error) { dt := Datetime{} seconds := t.Unix() @@ -253,7 +253,7 @@ func datetimeEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { zone := tm.Location().String() _, offset := tm.Zone() if zone != NoTimezone { - // The zone value already checked in NewDatetime() or + // The zone value already checked in MakeDatetime() or // UnmarshalMsgpack() calls. dt.tzIndex = int16(timezoneToIndex[zone]) } From 971e4ec707c06c0cd913e740b0916c23c435771b Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 2 Nov 2023 20:24:34 +0300 Subject: [PATCH 495/605] ci: rename 2.x-latest to master The `master` branch on Tarantool is about 3.x now. --- .github/workflows/testing.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 268a6a3ea..a44234d72 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -34,14 +34,14 @@ jobs: - '1.10' - '2.8' - '2.10' - - '2.x-latest' + - 'master' coveralls: [false] fuzzing: [false] include: - - tarantool: '2.x-latest' + - tarantool: 'master' coveralls: true golang: 1.13 - - tarantool: '2.x-latest' + - tarantool: 'master' fuzzing: true golang: 1.18 coveralls: false @@ -56,34 +56,34 @@ jobs: sudo apt install -y tt - name: Setup Tarantool ${{ matrix.tarantool }} - if: matrix.tarantool != '2.x-latest' + if: matrix.tarantool != 'master' uses: tarantool/setup-tarantool@v2 with: tarantool-version: ${{ matrix.tarantool }} - - name: Get Tarantool 2.x latest commit - if: matrix.tarantool == '2.x-latest' + - name: Get Tarantool master commit + if: matrix.tarantool == 'master' run: | commit_hash=$(git ls-remote https://github.com/tarantool/tarantool.git --branch master | head -c 8) echo "LATEST_COMMIT=${commit_hash}" >> $GITHUB_ENV shell: bash - - name: Cache Tarantool 2.x latest - if: matrix.tarantool == '2.x-latest' + - name: Cache Tarantool master + if: matrix.tarantool == 'master' id: cache-latest uses: actions/cache@v3 with: path: "${GITHUB_WORKSPACE}/bin" key: cache-latest-${{ env.LATEST_COMMIT }} - - name: Setup Tarantool 2.x latest - if: matrix.tarantool == '2.x-latest' && steps.cache-latest.outputs.cache-hit != 'true' + - name: Setup Tarantool master + if: matrix.tarantool == 'master' && steps.cache-latest.outputs.cache-hit != 'true' run: | tt init sudo tt install tarantool master - - name: Add Tarantool 2.x latest to PATH - if: matrix.tarantool == '2.x-latest' + - name: Add Tarantool master to PATH + if: matrix.tarantool == 'master' run: echo "${GITHUB_WORKSPACE}/bin" >> $GITHUB_PATH - name: Setup golang for the connector and tests From c564e6d75d10b1bfef276ebacc2eb7cf4acfdb24 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 2 Nov 2023 20:25:20 +0300 Subject: [PATCH 496/605] ci: fix master build cache Environment variables are not allowed[1] in the step context configuration. 1. https://github.com/orgs/community/discussions/35739 --- .github/workflows/testing.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index a44234d72..66d0b7063 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -55,6 +55,9 @@ jobs: curl -L https://tarantool.io/release/2/installer.sh | sudo bash sudo apt install -y tt + - name: Setup tt environment + run: tt init + - name: Setup Tarantool ${{ matrix.tarantool }} if: matrix.tarantool != 'master' uses: tarantool/setup-tarantool@v2 @@ -73,13 +76,14 @@ jobs: id: cache-latest uses: actions/cache@v3 with: - path: "${GITHUB_WORKSPACE}/bin" + path: | + ${{ github.workspace }}/bin + ${{ github.workspace }}/include key: cache-latest-${{ env.LATEST_COMMIT }} - name: Setup Tarantool master if: matrix.tarantool == 'master' && steps.cache-latest.outputs.cache-hit != 'true' run: | - tt init sudo tt install tarantool master - name: Add Tarantool master to PATH From 772b21f4ecb88f3f242efa430c9dc43b853db3f0 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 7 Nov 2023 17:19:15 +0300 Subject: [PATCH 497/605] crud: support yield_every in select `yield_every` was introduced to crud.count and crud.select in crud 1.1.1 [1]. The option is already supported for count requests, but select request misses it. This patch adds the option. 1. https://github.com/tarantool/crud/releases/tag/1.1.1 --- CHANGELOG.md | 1 + crud/count.go | 1 + crud/options.go | 1 + crud/select.go | 9 +++++++-- crud/tarantool_test.go | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbec3f1b7..b4cdf0012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support `crud.schema` request (#336) - Support `IPROTO_WATCH_ONCE` request type for Tarantool version >= 3.0.0-alpha1 (#337) +- Support `yield_every` option for crud select requests (#350) ### Changed diff --git a/crud/count.go b/crud/count.go index e4c163249..9deb1e44c 100644 --- a/crud/count.go +++ b/crud/count.go @@ -29,6 +29,7 @@ type CountOpts struct { // load balancing policy. Balance OptBool // YieldEvery describes number of tuples processed to yield after. + // Should be positive. YieldEvery OptUint // BucketId is a bucket ID. BucketId OptUint diff --git a/crud/options.go b/crud/options.go index eec3fc0db..fa5dca2c9 100644 --- a/crud/options.go +++ b/crud/options.go @@ -23,6 +23,7 @@ const ( batchSizeOptName = "batch_size" fetchLatestMetadataOptName = "fetch_latest_metadata" noreturnOptName = "noreturn" + yieldEvery = "yield_every" ) // OptUint is an optional uint. diff --git a/crud/select.go b/crud/select.go index 198f74bae..24dbd0cb0 100644 --- a/crud/select.go +++ b/crud/select.go @@ -46,17 +46,21 @@ type SelectOpts struct { // the latest migration of the data format. Performance overhead is up to 15%. // Disabled by default. FetchLatestMetadata OptBool + // YieldEvery describes number of tuples processed to yield after. + // Should be positive. + YieldEvery OptUint } // EncodeMsgpack provides custom msgpack encoder. func (opts SelectOpts) EncodeMsgpack(enc *msgpack.Encoder) error { - const optsCnt = 13 + const optsCnt = 14 names := [optsCnt]string{timeoutOptName, vshardRouterOptName, fieldsOptName, bucketIdOptName, modeOptName, preferReplicaOptName, balanceOptName, firstOptName, afterOptName, batchSizeOptName, - forceMapCallOptName, fullscanOptName, fetchLatestMetadataOptName} + forceMapCallOptName, fullscanOptName, fetchLatestMetadataOptName, + yieldEveryOptName} values := [optsCnt]interface{}{} exists := [optsCnt]bool{} values[0], exists[0] = opts.Timeout.Get() @@ -72,6 +76,7 @@ func (opts SelectOpts) EncodeMsgpack(enc *msgpack.Encoder) error { values[10], exists[10] = opts.ForceMapCall.Get() values[11], exists[11] = opts.Fullscan.Get() values[12], exists[12] = opts.FetchLatestMetadata.Get() + values[13], exists[13] = opts.YieldEvery.Get() return encodeOptions(enc, names[:], values[:], exists[:]) } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 511980bd9..399b0d48f 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -1333,6 +1333,40 @@ func TestUnitEmptySchema(t *testing.T) { require.Equal(t, result.Value, crud.Schema{}, "empty schema expected") } +var testStorageYieldCases = []struct { + name string + req tarantool.Request +}{ + { + "Count", + crud.MakeCountRequest(spaceName). + Opts(crud.CountOpts{ + YieldEvery: crud.MakeOptUint(500), + }), + }, + { + "Select", + crud.MakeSelectRequest(spaceName). + Opts(crud.SelectOpts{ + YieldEvery: crud.MakeOptUint(500), + }), + }, +} + +func TestYieldEveryOption(t *testing.T) { + conn := connect(t) + defer conn.Close() + + for _, testCase := range testStorageYieldCases { + t.Run(testCase.name, func(t *testing.T) { + _, err := conn.Do(testCase.req).Get() + if err != nil { + t.Fatalf("Failed to Do CRUD request: %s", err) + } + }) + } +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body From 192998c5fa321edd9ee28546b2c3e635ae9b4102 Mon Sep 17 00:00:00 2001 From: DerekBum Date: Wed, 1 Nov 2023 20:26:03 +0300 Subject: [PATCH 498/605] api: support `IPROTO_FEATURE_SPACE_AND_INDEX_NAMES` Support `IPROTO_FEATURE_SPACE_AND_INDEX_NAMES` for Tarantool version >= 3.0.0-alpha1. It allows to use space and index names in requests instead of their IDs. `ResolveSpaceIndex` function for `SchemaResolver` interface split into two: `ResolveSpace` and `ResolveIndex`. `NamesUseSupported` function added into the interface to get information if usage of space and index names is supported. `Schema` structure no longer implements `SchemaResolver` interface. Part of #338 --- CHANGELOG.md | 3 + README.md | 8 ++ connection.go | 12 +- crud/request_test.go | 43 +----- example_test.go | 88 +++++++++++++ export_test.go | 67 ++++++++-- protocol.go | 1 + request.go | 149 ++++++++++++++++----- request_test.go | 276 ++++++++++++++++++++++++++++++++++----- schema.go | 189 ++++++++++++++++++--------- settings/request_test.go | 26 ++-- tarantool_test.go | 45 +------ 12 files changed, 668 insertions(+), 239 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4cdf0012..8500568ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support `IPROTO_WATCH_ONCE` request type for Tarantool version >= 3.0.0-alpha1 (#337) - Support `yield_every` option for crud select requests (#350) +- Support `IPROTO_FEATURE_SPACE_AND_INDEX_NAMES` for Tarantool + version >= 3.0.0-alpha1 (#338). It allows to use space and index names + in requests instead of their IDs. ### Changed diff --git a/README.md b/README.md index eae6f92ab..f8483dc34 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,14 @@ and user may cancel it in process. * `iproto.Feature` type used instead of `ProtocolFeature`. * `iproto.IPROTO_FEATURE_` constants used instead of local ones. +#### Schema changes + +* `ResolveSpaceIndex` function for `SchemaResolver` interface split into two: +`ResolveSpace` and `ResolveIndex`. `NamesUseSupported` function added into the +interface to get information if the usage of space and index names in requests +is supported. +* `Schema` structure no longer implements `SchemaResolver` interface. + ## Contributing See [the contributing guide](CONTRIBUTING.md) for detailed instructions on how diff --git a/connection.go b/connection.go index 018df6caa..217c153dd 100644 --- a/connection.go +++ b/connection.go @@ -162,6 +162,8 @@ type Connection struct { cond *sync.Cond // Schema contains schema loaded on connection. Schema *Schema + // schemaResolver contains a SchemaResolver implementation. + schemaResolver SchemaResolver // requestId contains the last request ID for requests with nil context. requestId uint32 // contextRequestId contains the last request ID for requests with context. @@ -528,6 +530,14 @@ func (conn *Connection) dial(ctx context.Context) error { conn.Greeting.Version = c.Greeting().Version conn.serverProtocolInfo = c.ProtocolInfo() + spaceAndIndexNamesSupported := + isFeatureInSlice(iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, + conn.serverProtocolInfo.Features) + + conn.schemaResolver = &noSchemaResolver{ + SpaceAndIndexNamesSupported: spaceAndIndexNamesSupported, + } + // Watchers. conn.watchMap.Range(func(key, value interface{}) bool { st := value.(chan watchState) @@ -1102,7 +1112,7 @@ func (conn *Connection) putFuture(fut *Future, req Request, streamId uint64) { } blen := shard.buf.Len() reqid := fut.requestId - if err := pack(&shard.buf, shard.enc, reqid, req, streamId, conn.Schema); err != nil { + if err := pack(&shard.buf, shard.enc, reqid, req, streamId, conn.schemaResolver); err != nil { shard.buf.Trunc(blen) shard.bufmut.Unlock() if f := conn.fetchFuture(reqid); f == fut { diff --git a/crud/request_test.go b/crud/request_test.go index 8a64ab427..0b728162e 100644 --- a/crud/request_test.go +++ b/crud/request_test.go @@ -3,7 +3,6 @@ package crud_test import ( "bytes" "context" - "errors" "fmt" "testing" @@ -14,14 +13,7 @@ import ( "github.com/tarantool/go-tarantool/v2/crud" ) -const invalidSpaceMsg = "invalid space" -const invalidIndexMsg = "invalid index" - -const invalidSpace = 2 -const invalidIndex = 2 const validSpace = "test" // Any valid value != default. -const defaultSpace = 0 // And valid too. -const defaultIndex = 0 // And valid too. const CrudRequestType = iproto.IPROTO_CALL @@ -69,38 +61,11 @@ var expectedOpts = map[string]interface{}{ "timeout": timeout, } -type ValidSchemeResolver struct { -} - -func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (uint32, uint32, error) { - var spaceNo, indexNo uint32 - if s != nil { - spaceNo = uint32(s.(int)) - } else { - spaceNo = defaultSpace - } - if i != nil { - indexNo = uint32(i.(int)) - } else { - indexNo = defaultIndex - } - if spaceNo == invalidSpace { - return 0, 0, errors.New(invalidSpaceMsg) - } - if indexNo == invalidIndex { - return 0, 0, errors.New(invalidIndexMsg) - } - return spaceNo, indexNo, nil -} - -var resolver ValidSchemeResolver - -func extractRequestBody(req tarantool.Request, - resolver tarantool.SchemaResolver) ([]byte, error) { +func extractRequestBody(req tarantool.Request) ([]byte, error) { var reqBuf bytes.Buffer reqEnc := msgpack.NewEncoder(&reqBuf) - err := req.Body(resolver, reqEnc) + err := req.Body(nil, reqEnc) if err != nil { return nil, fmt.Errorf("An unexpected Response.Body() error: %q", err.Error()) } @@ -111,12 +76,12 @@ func extractRequestBody(req tarantool.Request, func assertBodyEqual(t testing.TB, reference tarantool.Request, req tarantool.Request) { t.Helper() - reqBody, err := extractRequestBody(req, &resolver) + reqBody, err := extractRequestBody(req) if err != nil { t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) } - refBody, err := extractRequestBody(reference, &resolver) + refBody, err := extractRequestBody(reference) if err != nil { t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) } diff --git a/example_test.go b/example_test.go index f85f532d0..9b66ea29b 100644 --- a/example_test.go +++ b/example_test.go @@ -231,6 +231,21 @@ func ExampleSelectRequest() { // response is [{{} 1111 hello world}] } +func ExampleSelectRequest_spaceAndIndexNames() { + conn := exampleConnect(opts) + defer conn.Close() + + req := tarantool.NewSelectRequest(spaceName) + req.Index(indexName) + resp, err := conn.Do(req).Get() + + if err != nil { + fmt.Printf("Failed to execute the request: %s\n", err) + } else { + fmt.Println(resp.Data) + } +} + func ExampleInsertRequest() { conn := exampleConnect(opts) defer conn.Close() @@ -273,6 +288,20 @@ func ExampleInsertRequest() { // Data [[32 test one]] } +func ExampleInsertRequest_spaceAndIndexNames() { + conn := exampleConnect(opts) + defer conn.Close() + + req := tarantool.NewInsertRequest(spaceName) + resp, err := conn.Do(req).Get() + + if err != nil { + fmt.Printf("Failed to execute the request: %s\n", err) + } else { + fmt.Println(resp.Data) + } +} + func ExampleDeleteRequest() { conn := exampleConnect(opts) defer conn.Close() @@ -316,6 +345,21 @@ func ExampleDeleteRequest() { // Data [[36 test one]] } +func ExampleDeleteRequest_spaceAndIndexNames() { + conn := exampleConnect(opts) + defer conn.Close() + + req := tarantool.NewDeleteRequest(spaceName) + req.Index(indexName) + resp, err := conn.Do(req).Get() + + if err != nil { + fmt.Printf("Failed to execute the request: %s\n", err) + } else { + fmt.Println(resp.Data) + } +} + func ExampleReplaceRequest() { conn := exampleConnect(opts) defer conn.Close() @@ -375,6 +419,20 @@ func ExampleReplaceRequest() { // Data [[13 test twelve]] } +func ExampleReplaceRequest_spaceAndIndexNames() { + conn := exampleConnect(opts) + defer conn.Close() + + req := tarantool.NewReplaceRequest(spaceName) + resp, err := conn.Do(req).Get() + + if err != nil { + fmt.Printf("Failed to execute the request: %s\n", err) + } else { + fmt.Println(resp.Data) + } +} + func ExampleUpdateRequest() { conn := exampleConnect(opts) defer conn.Close() @@ -411,6 +469,21 @@ func ExampleUpdateRequest() { // response is []interface {}{[]interface {}{0x457, "hello", "world"}} } +func ExampleUpdateRequest_spaceAndIndexNames() { + conn := exampleConnect(opts) + defer conn.Close() + + req := tarantool.NewUpdateRequest(spaceName) + req.Index(indexName) + resp, err := conn.Do(req).Get() + + if err != nil { + fmt.Printf("Failed to execute the request: %s\n", err) + } else { + fmt.Println(resp.Data) + } +} + func ExampleUpsertRequest() { conn := exampleConnect(opts) defer conn.Close() @@ -452,6 +525,20 @@ func ExampleUpsertRequest() { // response is []interface {}{[]interface {}{0x459, "first", "updated"}} } +func ExampleUpsertRequest_spaceAndIndexNames() { + conn := exampleConnect(opts) + defer conn.Close() + + req := tarantool.NewUpsertRequest(spaceName) + resp, err := conn.Do(req).Get() + + if err != nil { + fmt.Printf("Failed to execute the request: %s\n", err) + } else { + fmt.Println(resp.Data) + } +} + func ExampleCallRequest() { conn := exampleConnect(opts) defer conn.Close() @@ -634,6 +721,7 @@ func ExampleProtocolVersion() { // Connector client protocol feature: IPROTO_FEATURE_ERROR_EXTENSION // Connector client protocol feature: IPROTO_FEATURE_WATCHERS // Connector client protocol feature: IPROTO_FEATURE_PAGINATION + // Connector client protocol feature: IPROTO_FEATURE_SPACE_AND_INDEX_NAMES // Connector client protocol feature: IPROTO_FEATURE_WATCH_ONCE } diff --git a/export_test.go b/export_test.go index 38211a4fc..a52ef5b26 100644 --- a/export_test.go +++ b/export_test.go @@ -25,39 +25,80 @@ func RefImplPingBody(enc *msgpack.Encoder) error { // RefImplSelectBody is reference implementation for filling of a select // request's body. -func RefImplSelectBody(enc *msgpack.Encoder, space, index, offset, limit uint32, iterator Iter, - key, after interface{}, fetchPos bool) error { - return fillSelect(enc, space, index, offset, limit, iterator, key, after, fetchPos) +func RefImplSelectBody(enc *msgpack.Encoder, res SchemaResolver, space, index interface{}, + offset, limit uint32, iterator Iter, key, after interface{}, fetchPos bool) error { + spaceEnc, err := newSpaceEncoder(res, space) + if err != nil { + return err + } + indexEnc, err := newIndexEncoder(res, index, spaceEnc.Id) + if err != nil { + return err + } + return fillSelect(enc, spaceEnc, indexEnc, offset, limit, iterator, key, after, fetchPos) } // RefImplInsertBody is reference implementation for filling of an insert // request's body. -func RefImplInsertBody(enc *msgpack.Encoder, space uint32, tuple interface{}) error { - return fillInsert(enc, space, tuple) +func RefImplInsertBody(enc *msgpack.Encoder, res SchemaResolver, space, + tuple interface{}) error { + spaceEnc, err := newSpaceEncoder(res, space) + if err != nil { + return err + } + return fillInsert(enc, spaceEnc, tuple) } // RefImplReplaceBody is reference implementation for filling of a replace // request's body. -func RefImplReplaceBody(enc *msgpack.Encoder, space uint32, tuple interface{}) error { - return fillInsert(enc, space, tuple) +func RefImplReplaceBody(enc *msgpack.Encoder, res SchemaResolver, space, + tuple interface{}) error { + spaceEnc, err := newSpaceEncoder(res, space) + if err != nil { + return err + } + return fillInsert(enc, spaceEnc, tuple) } // RefImplDeleteBody is reference implementation for filling of a delete // request's body. -func RefImplDeleteBody(enc *msgpack.Encoder, space, index uint32, key interface{}) error { - return fillDelete(enc, space, index, key) +func RefImplDeleteBody(enc *msgpack.Encoder, res SchemaResolver, space, index, + key interface{}) error { + spaceEnc, err := newSpaceEncoder(res, space) + if err != nil { + return err + } + indexEnc, err := newIndexEncoder(res, index, spaceEnc.Id) + if err != nil { + return err + } + return fillDelete(enc, spaceEnc, indexEnc, key) } // RefImplUpdateBody is reference implementation for filling of an update // request's body. -func RefImplUpdateBody(enc *msgpack.Encoder, space, index uint32, key, ops interface{}) error { - return fillUpdate(enc, space, index, key, ops) +func RefImplUpdateBody(enc *msgpack.Encoder, res SchemaResolver, space, index, + key, ops interface{}) error { + spaceEnc, err := newSpaceEncoder(res, space) + if err != nil { + return err + } + indexEnc, err := newIndexEncoder(res, index, spaceEnc.Id) + if err != nil { + return err + } + return fillUpdate(enc, spaceEnc, indexEnc, key, ops) } // RefImplUpsertBody is reference implementation for filling of an upsert // request's body. -func RefImplUpsertBody(enc *msgpack.Encoder, space uint32, tuple, ops interface{}) error { - return fillUpsert(enc, space, tuple, ops) +func RefImplUpsertBody(enc *msgpack.Encoder, res SchemaResolver, space, + tuple, ops interface{}) error { + spaceEnc, err := newSpaceEncoder(res, space) + if err != nil { + return err + } + return fillUpsert(enc, spaceEnc, tuple, ops) } // RefImplCallBody is reference implementation for filling of a call or call17 diff --git a/protocol.go b/protocol.go index 7de8b197e..9296943ce 100644 --- a/protocol.go +++ b/protocol.go @@ -56,6 +56,7 @@ var clientProtocolInfo ProtocolInfo = ProtocolInfo{ iproto.IPROTO_FEATURE_ERROR_EXTENSION, iproto.IPROTO_FEATURE_WATCHERS, iproto.IPROTO_FEATURE_PAGINATION, + iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, iproto.IPROTO_FEATURE_WATCH_ONCE, }, } diff --git a/request.go b/request.go index 3332486f4..c2e7cf96d 100644 --- a/request.go +++ b/request.go @@ -12,19 +12,90 @@ import ( "github.com/vmihailenco/msgpack/v5" ) -func fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { - if err := enc.EncodeUint(uint64(iproto.IPROTO_SPACE_ID)); err != nil { +type spaceEncoder struct { + Id uint32 + Name string + IsId bool +} + +func newSpaceEncoder(res SchemaResolver, spaceInfo interface{}) (spaceEncoder, error) { + if res.NamesUseSupported() { + if spaceName, ok := spaceInfo.(string); ok { + return spaceEncoder{ + Id: 0, + Name: spaceName, + IsId: false, + }, nil + } + } + + spaceId, err := res.ResolveSpace(spaceInfo) + return spaceEncoder{ + Id: spaceId, + IsId: true, + }, err +} + +func (e spaceEncoder) Encode(enc *msgpack.Encoder) error { + if e.IsId { + if err := enc.EncodeUint(uint64(iproto.IPROTO_SPACE_ID)); err != nil { + return err + } + return enc.EncodeUint(uint64(e.Id)) + } + if err := enc.EncodeUint(uint64(iproto.IPROTO_SPACE_NAME)); err != nil { return err } - if err := enc.EncodeUint(uint64(spaceNo)); err != nil { + return enc.EncodeString(e.Name) +} + +type indexEncoder struct { + Id uint32 + Name string + IsId bool +} + +func newIndexEncoder(res SchemaResolver, indexInfo interface{}, + spaceNo uint32) (indexEncoder, error) { + if res.NamesUseSupported() { + if indexName, ok := indexInfo.(string); ok { + return indexEncoder{ + Name: indexName, + IsId: false, + }, nil + } + } + + indexId, err := res.ResolveIndex(indexInfo, spaceNo) + return indexEncoder{ + Id: indexId, + IsId: true, + }, err +} + +func (e indexEncoder) Encode(enc *msgpack.Encoder) error { + if e.IsId { + if err := enc.EncodeUint(uint64(iproto.IPROTO_INDEX_ID)); err != nil { + return err + } + return enc.EncodeUint(uint64(e.Id)) + } + if err := enc.EncodeUint(uint64(iproto.IPROTO_INDEX_NAME)); err != nil { return err } - if err := enc.EncodeUint(uint64(iproto.IPROTO_INDEX_ID)); err != nil { + return enc.EncodeString(e.Name) +} + +func fillSearch(enc *msgpack.Encoder, spaceEnc spaceEncoder, indexEnc indexEncoder, + key interface{}) error { + if err := spaceEnc.Encode(enc); err != nil { return err } - if err := enc.EncodeUint(uint64(indexNo)); err != nil { + + if err := indexEnc.Encode(enc); err != nil { return err } + if err := enc.EncodeUint(uint64(iproto.IPROTO_KEY)); err != nil { return err } @@ -50,24 +121,22 @@ func fillIterator(enc *msgpack.Encoder, offset, limit uint32, iterator Iter) err return enc.EncodeUint(uint64(limit)) } -func fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { +func fillInsert(enc *msgpack.Encoder, spaceEnc spaceEncoder, tuple interface{}) error { if err := enc.EncodeMapLen(2); err != nil { return err } - if err := enc.EncodeUint(uint64(iproto.IPROTO_SPACE_ID)); err != nil { - return err - } - if err := enc.EncodeUint(uint64(spaceNo)); err != nil { + if err := spaceEnc.Encode(enc); err != nil { return err } + if err := enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)); err != nil { return err } return enc.Encode(tuple) } -func fillSelect(enc *msgpack.Encoder, spaceNo, indexNo, offset, limit uint32, iterator Iter, - key, after interface{}, fetchPos bool) error { +func fillSelect(enc *msgpack.Encoder, spaceEnc spaceEncoder, indexEnc indexEncoder, + offset, limit uint32, iterator Iter, key, after interface{}, fetchPos bool) error { mapLen := 6 if fetchPos { mapLen += 1 @@ -81,7 +150,7 @@ func fillSelect(enc *msgpack.Encoder, spaceNo, indexNo, offset, limit uint32, it if err := fillIterator(enc, offset, limit, iterator); err != nil { return err } - if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { + if err := fillSearch(enc, spaceEnc, indexEnc, key); err != nil { return err } if fetchPos { @@ -112,19 +181,22 @@ func fillSelect(enc *msgpack.Encoder, spaceNo, indexNo, offset, limit uint32, it return nil } -func fillUpdate(enc *msgpack.Encoder, spaceNo, indexNo uint32, key, ops interface{}) error { +func fillUpdate(enc *msgpack.Encoder, spaceEnc spaceEncoder, indexEnc indexEncoder, + key, ops interface{}) error { enc.EncodeMapLen(4) - if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { + if err := fillSearch(enc, spaceEnc, indexEnc, key); err != nil { return err } enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)) return enc.Encode(ops) } -func fillUpsert(enc *msgpack.Encoder, spaceNo uint32, tuple, ops interface{}) error { +func fillUpsert(enc *msgpack.Encoder, spaceEnc spaceEncoder, tuple, ops interface{}) error { enc.EncodeMapLen(3) - enc.EncodeUint(uint64(iproto.IPROTO_SPACE_ID)) - enc.EncodeUint(uint64(spaceNo)) + if err := spaceEnc.Encode(enc); err != nil { + return err + } + enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)) if err := enc.Encode(tuple); err != nil { return err @@ -133,9 +205,10 @@ func fillUpsert(enc *msgpack.Encoder, spaceNo uint32, tuple, ops interface{}) er return enc.Encode(ops) } -func fillDelete(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { +func fillDelete(enc *msgpack.Encoder, spaceEnc spaceEncoder, indexEnc indexEncoder, + key interface{}) error { enc.EncodeMapLen(3) - return fillSearch(enc, spaceNo, indexNo, key) + return fillSearch(enc, spaceEnc, indexEnc, key) } func fillCall(enc *msgpack.Encoder, functionName string, args interface{}) error { @@ -948,12 +1021,16 @@ func (req *SelectRequest) After(after interface{}) *SelectRequest { // Body fills an encoder with the select request body. func (req *SelectRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) + spaceEnc, err := newSpaceEncoder(res, req.space) + if err != nil { + return err + } + indexEnc, err := newIndexEncoder(res, req.index, spaceEnc.Id) if err != nil { return err } - return fillSelect(enc, spaceNo, indexNo, req.offset, req.limit, req.iterator, + return fillSelect(enc, spaceEnc, indexEnc, req.offset, req.limit, req.iterator, req.key, req.after, req.fetchPos) } @@ -993,12 +1070,12 @@ func (req *InsertRequest) Tuple(tuple interface{}) *InsertRequest { // Body fills an msgpack.Encoder with the insert request body. func (req *InsertRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) + spaceEnc, err := newSpaceEncoder(res, req.space) if err != nil { return err } - return fillInsert(enc, spaceNo, req.tuple) + return fillInsert(enc, spaceEnc, req.tuple) } // Context sets a passed context to the request. @@ -1037,12 +1114,12 @@ func (req *ReplaceRequest) Tuple(tuple interface{}) *ReplaceRequest { // Body fills an msgpack.Encoder with the replace request body. func (req *ReplaceRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) + spaceEnc, err := newSpaceEncoder(res, req.space) if err != nil { return err } - return fillInsert(enc, spaceNo, req.tuple) + return fillInsert(enc, spaceEnc, req.tuple) } // Context sets a passed context to the request. @@ -1088,12 +1165,16 @@ func (req *DeleteRequest) Key(key interface{}) *DeleteRequest { // Body fills an msgpack.Encoder with the delete request body. func (req *DeleteRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) + spaceEnc, err := newSpaceEncoder(res, req.space) + if err != nil { + return err + } + indexEnc, err := newIndexEncoder(res, req.index, spaceEnc.Id) if err != nil { return err } - return fillDelete(enc, spaceNo, indexNo, req.key) + return fillDelete(enc, spaceEnc, indexEnc, req.key) } // Context sets a passed context to the request. @@ -1150,12 +1231,16 @@ func (req *UpdateRequest) Operations(ops *Operations) *UpdateRequest { // Body fills an msgpack.Encoder with the update request body. func (req *UpdateRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - spaceNo, indexNo, err := res.ResolveSpaceIndex(req.space, req.index) + spaceEnc, err := newSpaceEncoder(res, req.space) + if err != nil { + return err + } + indexEnc, err := newIndexEncoder(res, req.index, spaceEnc.Id) if err != nil { return err } - return fillUpdate(enc, spaceNo, indexNo, req.key, req.ops) + return fillUpdate(enc, spaceEnc, indexEnc, req.key, req.ops) } // Context sets a passed context to the request. @@ -1205,12 +1290,12 @@ func (req *UpsertRequest) Operations(ops *Operations) *UpsertRequest { // Body fills an msgpack.Encoder with the upsert request body. func (req *UpsertRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - spaceNo, _, err := res.ResolveSpaceIndex(req.space, nil) + spaceEnc, err := newSpaceEncoder(res, req.space) if err != nil { return err } - return fillUpsert(enc, spaceNo, req.tuple, req.ops) + return fillUpsert(enc, spaceEnc, req.tuple, req.ops) } // Context sets a passed context to the request. diff --git a/request_test.go b/request_test.go index 6d08533d3..f5968b738 100644 --- a/request_test.go +++ b/request_test.go @@ -17,14 +17,14 @@ import ( const invalidSpaceMsg = "invalid space" const invalidIndexMsg = "invalid index" -const invalidSpace = 2 -const invalidIndex = 2 -const validSpace = 1 // Any valid value != default. -const validIndex = 3 // Any valid value != default. +const invalidSpace uint32 = 2 +const invalidIndex uint32 = 2 +const validSpace uint32 = 1 // Any valid value != default. +const validIndex uint32 = 3 // Any valid value != default. const validExpr = "any string" // We don't check the value here. const validKey = "foo" // Any string. -const defaultSpace = 0 // And valid too. -const defaultIndex = 0 // And valid too. +const defaultSpace uint32 = 0 // And valid too. +const defaultIndex uint32 = 0 // And valid too. const defaultIsolationLevel = DefaultIsolationLevel const defaultTimeout = 0 @@ -39,27 +39,43 @@ var validProtocolInfo ProtocolInfo = ProtocolInfo{ } type ValidSchemeResolver struct { + nameUseSupported bool + spaceResolverCalls int + indexResolverCalls int } -func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (uint32, uint32, error) { - var spaceNo, indexNo uint32 - if s != nil { - spaceNo = uint32(s.(int)) +func (r *ValidSchemeResolver) ResolveSpace(s interface{}) (uint32, error) { + r.spaceResolverCalls++ + + var spaceNo uint32 + if no, ok := s.(uint32); ok { + spaceNo = no } else { spaceNo = defaultSpace } - if i != nil { - indexNo = uint32(i.(int)) + if spaceNo == invalidSpace { + return 0, errors.New(invalidSpaceMsg) + } + return spaceNo, nil +} + +func (r *ValidSchemeResolver) ResolveIndex(i interface{}, spaceNo uint32) (uint32, error) { + r.indexResolverCalls++ + + var indexNo uint32 + if no, ok := i.(uint32); ok { + indexNo = no } else { indexNo = defaultIndex } - if spaceNo == invalidSpace { - return 0, 0, errors.New(invalidSpaceMsg) - } if indexNo == invalidIndex { - return 0, 0, errors.New(invalidIndexMsg) + return 0, errors.New(invalidIndexMsg) } - return spaceNo, indexNo, nil + return indexNo, nil +} + +func (r *ValidSchemeResolver) NamesUseSupported() bool { + return r.nameUseSupported } var resolver ValidSchemeResolver @@ -313,6 +329,58 @@ func TestRequestsCtx_setter(t *testing.T) { } } +func TestResolverCalledWithoutNameSupport(t *testing.T) { + resolver.nameUseSupported = false + resolver.spaceResolverCalls = 0 + resolver.indexResolverCalls = 0 + + req := NewSelectRequest("valid") + req.Index("valid") + + var reqBuf bytes.Buffer + reqEnc := msgpack.NewEncoder(&reqBuf) + + err := req.Body(&resolver, reqEnc) + if err != nil { + t.Errorf("An unexpected Response.Body() error: %q", err.Error()) + } + + if resolver.spaceResolverCalls != 1 { + t.Errorf("ResolveSpace was called %d times instead of 1.", + resolver.spaceResolverCalls) + } + if resolver.indexResolverCalls != 1 { + t.Errorf("ResolveIndex was called %d times instead of 1.", + resolver.indexResolverCalls) + } +} + +func TestResolverNotCalledWithNameSupport(t *testing.T) { + resolver.nameUseSupported = true + resolver.spaceResolverCalls = 0 + resolver.indexResolverCalls = 0 + + req := NewSelectRequest("valid") + req.Index("valid") + + var reqBuf bytes.Buffer + reqEnc := msgpack.NewEncoder(&reqBuf) + + err := req.Body(&resolver, reqEnc) + if err != nil { + t.Errorf("An unexpected Response.Body() error: %q", err.Error()) + } + + if resolver.spaceResolverCalls != 0 { + t.Errorf("ResolveSpace was called %d times instead of 0.", + resolver.spaceResolverCalls) + } + if resolver.indexResolverCalls != 0 { + t.Errorf("ResolveIndex was called %d times instead of 0.", + resolver.indexResolverCalls) + } +} + func TestPingRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer @@ -331,7 +399,7 @@ func TestSelectRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, + err := RefImplSelectBody(refEnc, &resolver, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterAll, []interface{}{}, nil, false) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) @@ -342,12 +410,45 @@ func TestSelectRequestDefaultValues(t *testing.T) { assertBodyEqual(t, refBuf.Bytes(), req) } +func TestSelectRequestSpaceByName(t *testing.T) { + var refBuf bytes.Buffer + + resolver.nameUseSupported = true + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplSelectBody(refEnc, &resolver, "valid", defaultIndex, 0, 0xFFFFFFFF, + IterAll, []interface{}{}, nil, false) + if err != nil { + t.Fatalf("An unexpected RefImplSelectBody() error %q", err.Error()) + } + + req := NewSelectRequest("valid") + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestSelectRequestIndexByName(t *testing.T) { + var refBuf bytes.Buffer + + resolver.nameUseSupported = true + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplSelectBody(refEnc, &resolver, defaultSpace, "valid", 0, 0xFFFFFFFF, + IterAll, []interface{}{}, nil, false) + if err != nil { + t.Fatalf("An unexpected RefImplSelectBody() error %q", err.Error()) + } + + req := NewSelectRequest(defaultSpace) + req.Index("valid") + assertBodyEqual(t, refBuf.Bytes(), req) +} + func TestSelectRequestDefaultIteratorEqIfKey(t *testing.T) { var refBuf bytes.Buffer key := []interface{}{uint(18)} refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, + err := RefImplSelectBody(refEnc, &resolver, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterEq, key, nil, false) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) @@ -365,7 +466,7 @@ func TestSelectRequestIteratorNotChangedIfKey(t *testing.T) { const iter = IterGe refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, validSpace, defaultIndex, 0, 0xFFFFFFFF, + err := RefImplSelectBody(refEnc, &resolver, validSpace, defaultIndex, 0, 0xFFFFFFFF, iter, key, nil, false) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) @@ -388,7 +489,7 @@ func TestSelectRequestSetters(t *testing.T) { var refBufAfterBytes, refBufAfterKey bytes.Buffer refEncAfterBytes := msgpack.NewEncoder(&refBufAfterBytes) - err := RefImplSelectBody(refEncAfterBytes, validSpace, validIndex, offset, + err := RefImplSelectBody(refEncAfterBytes, &resolver, validSpace, validIndex, offset, limit, iter, key, afterBytes, true) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %s", err) @@ -396,7 +497,7 @@ func TestSelectRequestSetters(t *testing.T) { } refEncAfterKey := msgpack.NewEncoder(&refBufAfterKey) - err = RefImplSelectBody(refEncAfterKey, validSpace, validIndex, offset, + err = RefImplSelectBody(refEncAfterKey, &resolver, validSpace, validIndex, offset, limit, iter, key, afterKey, true) if err != nil { t.Errorf("An unexpected RefImplSelectBody() error %s", err) @@ -428,7 +529,7 @@ func TestInsertRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplInsertBody(refEnc, validSpace, []interface{}{}) + err := RefImplInsertBody(refEnc, &resolver, validSpace, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplInsertBody() error: %q", err.Error()) return @@ -438,12 +539,27 @@ func TestInsertRequestDefaultValues(t *testing.T) { assertBodyEqual(t, refBuf.Bytes(), req) } +func TestInsertRequestSpaceByName(t *testing.T) { + var refBuf bytes.Buffer + + resolver.nameUseSupported = true + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplInsertBody(refEnc, &resolver, "valid", []interface{}{}) + if err != nil { + t.Fatalf("An unexpected RefImplInsertBody() error: %q", err.Error()) + } + + req := NewInsertRequest("valid") + assertBodyEqual(t, refBuf.Bytes(), req) +} + func TestInsertRequestSetters(t *testing.T) { tuple := []interface{}{uint(24)} var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplInsertBody(refEnc, validSpace, tuple) + err := RefImplInsertBody(refEnc, &resolver, validSpace, tuple) if err != nil { t.Errorf("An unexpected RefImplInsertBody() error: %q", err.Error()) return @@ -458,7 +574,7 @@ func TestReplaceRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplReplaceBody(refEnc, validSpace, []interface{}{}) + err := RefImplReplaceBody(refEnc, &resolver, validSpace, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplReplaceBody() error: %q", err.Error()) return @@ -468,12 +584,28 @@ func TestReplaceRequestDefaultValues(t *testing.T) { assertBodyEqual(t, refBuf.Bytes(), req) } +func TestReplaceRequestSpaceByName(t *testing.T) { + var refBuf bytes.Buffer + + resolver.nameUseSupported = true + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplReplaceBody(refEnc, &resolver, "valid", []interface{}{}) + if err != nil { + t.Errorf("An unexpected RefImplReplaceBody() error: %q", err.Error()) + return + } + + req := NewReplaceRequest("valid") + assertBodyEqual(t, refBuf.Bytes(), req) +} + func TestReplaceRequestSetters(t *testing.T) { tuple := []interface{}{uint(99)} var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplReplaceBody(refEnc, validSpace, tuple) + err := RefImplReplaceBody(refEnc, &resolver, validSpace, tuple) if err != nil { t.Errorf("An unexpected RefImplReplaceBody() error: %q", err.Error()) return @@ -488,7 +620,7 @@ func TestDeleteRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplDeleteBody(refEnc, validSpace, defaultIndex, []interface{}{}) + err := RefImplDeleteBody(refEnc, &resolver, validSpace, defaultIndex, []interface{}{}) if err != nil { t.Errorf("An unexpected RefImplDeleteBody() error: %q", err.Error()) return @@ -498,12 +630,43 @@ func TestDeleteRequestDefaultValues(t *testing.T) { assertBodyEqual(t, refBuf.Bytes(), req) } +func TestDeleteRequestSpaceByName(t *testing.T) { + var refBuf bytes.Buffer + + resolver.nameUseSupported = true + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplDeleteBody(refEnc, &resolver, "valid", defaultIndex, []interface{}{}) + if err != nil { + t.Fatalf("An unexpected RefImplDeleteBody() error: %q", err.Error()) + } + + req := NewDeleteRequest("valid") + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestDeleteRequestIndexByName(t *testing.T) { + var refBuf bytes.Buffer + + resolver.nameUseSupported = true + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplDeleteBody(refEnc, &resolver, defaultSpace, "valid", []interface{}{}) + if err != nil { + t.Fatalf("An unexpected RefImplDeleteBody() error: %q", err.Error()) + } + + req := NewDeleteRequest(defaultSpace) + req.Index("valid") + assertBodyEqual(t, refBuf.Bytes(), req) +} + func TestDeleteRequestSetters(t *testing.T) { key := []interface{}{uint(923)} var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplDeleteBody(refEnc, validSpace, validIndex, key) + err := RefImplDeleteBody(refEnc, &resolver, validSpace, validIndex, key) if err != nil { t.Errorf("An unexpected RefImplDeleteBody() error: %q", err.Error()) return @@ -519,7 +682,8 @@ func TestUpdateRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpdateBody(refEnc, validSpace, defaultIndex, []interface{}{}, []Op{}) + err := RefImplUpdateBody(refEnc, &resolver, validSpace, defaultIndex, + []interface{}{}, []Op{}) if err != nil { t.Errorf("An unexpected RefImplUpdateBody() error: %q", err.Error()) return @@ -529,13 +693,46 @@ func TestUpdateRequestDefaultValues(t *testing.T) { assertBodyEqual(t, refBuf.Bytes(), req) } +func TestUpdateRequestSpaceByName(t *testing.T) { + var refBuf bytes.Buffer + + resolver.nameUseSupported = true + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplUpdateBody(refEnc, &resolver, "valid", defaultIndex, + []interface{}{}, []Op{}) + if err != nil { + t.Fatalf("An unexpected RefImplUpdateBody() error: %q", err.Error()) + } + + req := NewUpdateRequest("valid") + assertBodyEqual(t, refBuf.Bytes(), req) +} + +func TestUpdateRequestIndexByName(t *testing.T) { + var refBuf bytes.Buffer + + resolver.nameUseSupported = true + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplUpdateBody(refEnc, &resolver, defaultSpace, "valid", + []interface{}{}, []Op{}) + if err != nil { + t.Fatalf("An unexpected RefImplUpdateBody() error: %q", err.Error()) + } + + req := NewUpdateRequest(defaultSpace) + req.Index("valid") + assertBodyEqual(t, refBuf.Bytes(), req) +} + func TestUpdateRequestSetters(t *testing.T) { key := []interface{}{uint(44)} refOps, reqOps := getTestOps() var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpdateBody(refEnc, validSpace, validIndex, key, refOps) + err := RefImplUpdateBody(refEnc, &resolver, validSpace, validIndex, key, refOps) if err != nil { t.Errorf("An unexpected RefImplUpdateBody() error: %q", err.Error()) return @@ -552,7 +749,7 @@ func TestUpsertRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpsertBody(refEnc, validSpace, []interface{}{}, []Op{}) + err := RefImplUpsertBody(refEnc, &resolver, validSpace, []interface{}{}, []Op{}) if err != nil { t.Errorf("An unexpected RefImplUpsertBody() error: %q", err.Error()) return @@ -562,13 +759,28 @@ func TestUpsertRequestDefaultValues(t *testing.T) { assertBodyEqual(t, refBuf.Bytes(), req) } +func TestUpsertRequestSpaceByName(t *testing.T) { + var refBuf bytes.Buffer + + resolver.nameUseSupported = true + + refEnc := msgpack.NewEncoder(&refBuf) + err := RefImplUpsertBody(refEnc, &resolver, "valid", []interface{}{}, []Op{}) + if err != nil { + t.Fatalf("An unexpected RefImplUpsertBody() error: %q", err.Error()) + } + + req := NewUpsertRequest("valid") + assertBodyEqual(t, refBuf.Bytes(), req) +} + func TestUpsertRequestSetters(t *testing.T) { tuple := []interface{}{uint(64)} refOps, reqOps := getTestOps() var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpsertBody(refEnc, validSpace, tuple, refOps) + err := RefImplUpsertBody(refEnc, &resolver, validSpace, tuple, refOps) if err != nil { t.Errorf("An unexpected RefImplUpsertBody() error: %q", err.Error()) return diff --git a/schema.go b/schema.go index 714f51c5b..f0be7a162 100644 --- a/schema.go +++ b/schema.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" "github.com/vmihailenco/msgpack/v5/msgpcode" ) @@ -41,9 +42,16 @@ func msgpackIsString(code byte) bool { // SchemaResolver is an interface for resolving schema details. type SchemaResolver interface { - // ResolveSpaceIndex returns resolved space and index numbers or an - // error if it cannot be resolved. - ResolveSpaceIndex(s interface{}, i interface{}) (spaceNo, indexNo uint32, err error) + // ResolveSpace returns resolved space number or an error + // if it cannot be resolved. + ResolveSpace(s interface{}) (spaceNo uint32, err error) + // ResolveIndex returns resolved index number or an error + // if it cannot be resolved. + ResolveIndex(i interface{}, spaceNo uint32) (indexNo uint32, err error) + // NamesUseSupported shows if usage of space and index names, instead of + // IDs, is supported. It must return true if + // iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES is supported. + NamesUseSupported() bool } // Schema contains information about spaces and indexes. @@ -364,33 +372,27 @@ func (conn *Connection) loadSchema() (err error) { } } + spaceAndIndexNamesSupported := + isFeatureInSlice(iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, + conn.serverProtocolInfo.Features) + conn.lockShards() conn.Schema = schema + conn.schemaResolver = &loadedSchemaResolver{ + Schema: schema, + SpaceAndIndexNamesSupported: spaceAndIndexNamesSupported, + } conn.unlockShards() return nil } -// ResolveSpaceIndex tries to resolve space and index numbers. -// Note: s can be a number, string, or an object of Space type. -// Note: i can be a number, string, or an object of Index type. -func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (uint32, uint32, error) { - var ( - spaceNo, indexNo uint32 - space *Space - index *Index - ok bool - ) +// resolveSpaceNumber tries to resolve a space number. +// Note: at this point, s can be a number, or an object of Space type. +func resolveSpaceNumber(s interface{}) (uint32, error) { + var spaceNo uint32 switch s := s.(type) { - case string: - if schema == nil { - return spaceNo, indexNo, fmt.Errorf("Schema is not loaded") - } - if space, ok = schema.Spaces[s]; !ok { - return spaceNo, indexNo, fmt.Errorf("there is no space with name %s", s) - } - spaceNo = space.Id case uint: spaceNo = uint32(s) case uint64: @@ -419,50 +421,109 @@ func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (uint32, u panic("unexpected type of space param") } - if i != nil { - switch i := i.(type) { - case string: - if schema == nil { - return spaceNo, indexNo, fmt.Errorf("schema is not loaded") - } - if space == nil { - if space, ok = schema.SpacesById[spaceNo]; !ok { - return spaceNo, indexNo, fmt.Errorf("there is no space with id %d", spaceNo) - } - } - if index, ok = space.Indexes[i]; !ok { - err := fmt.Errorf("space %s has not index with name %s", space.Name, i) - return spaceNo, indexNo, err - } - indexNo = index.Id - case uint: - indexNo = uint32(i) - case uint64: - indexNo = uint32(i) - case uint32: - indexNo = i - case uint16: - indexNo = uint32(i) - case uint8: - indexNo = uint32(i) - case int: - indexNo = uint32(i) - case int64: - indexNo = uint32(i) - case int32: - indexNo = uint32(i) - case int16: - indexNo = uint32(i) - case int8: - indexNo = uint32(i) - case Index: - indexNo = i.Id - case *Index: - indexNo = i.Id - default: - panic("unexpected type of index param") + return spaceNo, nil +} + +// resolveIndexNumber tries to resolve an index number. +// Note: at this point, i can be a number, or an object of Index type. +func resolveIndexNumber(i interface{}) (uint32, error) { + var indexNo uint32 + + switch i := i.(type) { + case uint: + indexNo = uint32(i) + case uint64: + indexNo = uint32(i) + case uint32: + indexNo = i + case uint16: + indexNo = uint32(i) + case uint8: + indexNo = uint32(i) + case int: + indexNo = uint32(i) + case int64: + indexNo = uint32(i) + case int32: + indexNo = uint32(i) + case int16: + indexNo = uint32(i) + case int8: + indexNo = uint32(i) + case Index: + indexNo = i.Id + case *Index: + indexNo = i.Id + default: + panic("unexpected type of index param") + } + + return indexNo, nil +} + +type loadedSchemaResolver struct { + Schema *Schema + // SpaceAndIndexNamesSupported shows if a current Tarantool version supports + // iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES. + SpaceAndIndexNamesSupported bool +} + +func (r *loadedSchemaResolver) ResolveSpace(s interface{}) (uint32, error) { + if str, ok := s.(string); ok { + space, ok := r.Schema.Spaces[str] + if !ok { + return 0, fmt.Errorf("there is no space with name %s", s) + } + return space.Id, nil + } + return resolveSpaceNumber(s) +} + +func (r *loadedSchemaResolver) ResolveIndex(i interface{}, spaceNo uint32) (uint32, error) { + if i == nil { + return 0, nil + } + if str, ok := i.(string); ok { + space, ok := r.Schema.SpacesById[spaceNo] + if !ok { + return 0, fmt.Errorf("there is no space with id %d", spaceNo) + } + index, ok := space.Indexes[str] + if !ok { + err := fmt.Errorf("space %s has not index with name %s", space.Name, i) + return 0, err } + return index.Id, nil } + return resolveIndexNumber(i) +} + +func (r *loadedSchemaResolver) NamesUseSupported() bool { + return r.SpaceAndIndexNamesSupported +} + +type noSchemaResolver struct { + // SpaceAndIndexNamesSupported shows if a current Tarantool version supports + // iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES. + SpaceAndIndexNamesSupported bool +} + +func (*noSchemaResolver) ResolveSpace(s interface{}) (uint32, error) { + if _, ok := s.(string); ok { + return 0, fmt.Errorf("unable to use an index name " + + "because schema is not loaded") + } + return resolveSpaceNumber(s) +} + +func (*noSchemaResolver) ResolveIndex(i interface{}, spaceNo uint32) (uint32, error) { + if _, ok := i.(string); ok { + return 0, fmt.Errorf("unable to use an index name " + + "because schema is not loaded") + } + return resolveIndexNumber(i) +} - return spaceNo, indexNo, nil +func (r *noSchemaResolver) NamesUseSupported() bool { + return r.SpaceAndIndexNamesSupported } diff --git a/settings/request_test.go b/settings/request_test.go index 2438f81cc..b4c537a29 100644 --- a/settings/request_test.go +++ b/settings/request_test.go @@ -16,24 +16,16 @@ import ( type ValidSchemeResolver struct { } -func (*ValidSchemeResolver) ResolveSpaceIndex(s, i interface{}) (uint32, uint32, error) { - var spaceNo, indexNo uint32 - if s == nil { - if s == "_session_settings" { - spaceNo = 380 - } else { - spaceNo = uint32(s.(int)) - } - } else { - spaceNo = 0 - } - if i != nil { - indexNo = uint32(i.(int)) - } else { - indexNo = 0 - } +func (*ValidSchemeResolver) ResolveSpace(s interface{}) (uint32, error) { + return 0, nil +} + +func (*ValidSchemeResolver) ResolveIndex(i interface{}, spaceNo uint32) (uint32, error) { + return 0, nil +} - return spaceNo, indexNo, nil +func (r *ValidSchemeResolver) NamesUseSupported() bool { + return false } var resolver ValidSchemeResolver diff --git a/tarantool_test.go b/tarantool_test.go index 0ad365286..8be963474 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -585,15 +585,10 @@ func BenchmarkClientReplaceParallel(b *testing.B) { conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() - rSpaceNo, _, err := conn.Schema.ResolveSpaceIndex("test_perf", "secondary") - if err != nil { - b.Fatalf("Space is not resolved: %s", err.Error()) - } - b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _, err := conn.Replace(rSpaceNo, []interface{}{uint(1), "hello", []interface{}{}}) + _, err := conn.Replace("test_perf", []interface{}{uint(1), "hello", []interface{}{}}) if err != nil { b.Error(err) } @@ -605,17 +600,11 @@ func BenchmarkClientLargeSelectParallel(b *testing.B) { conn := test_helpers.ConnectWithValidation(b, server, opts) defer conn.Close() - schema := conn.Schema - rSpaceNo, rIndexNo, err := schema.ResolveSpaceIndex("test_perf", "secondary") - if err != nil { - b.Fatalf("symbolic space and index params not resolved") - } - offset, limit := uint32(0), uint32(1000) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _, err := conn.Select(rSpaceNo, rIndexNo, offset, limit, IterEq, + _, err := conn.Select("test_perf", "secondary", offset, limit, IterEq, []interface{}{"test_name"}) if err != nil { b.Fatal(err) @@ -1889,8 +1878,6 @@ func TestNewPreparedFromResponse(t *testing.T) { } func TestSchema(t *testing.T) { - var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() @@ -2035,32 +2022,6 @@ func TestSchema(t *testing.T) { (ifield2.Type != "STR" && ifield2.Type != "string") { t.Errorf("index field has incorrect Type '%s'", ifield2.Type) } - - var rSpaceNo, rIndexNo uint32 - rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex(616, 3) - if err != nil || rSpaceNo != 616 || rIndexNo != 3 { - t.Errorf("numeric space and index params not resolved as-is") - } - rSpaceNo, _, err = schema.ResolveSpaceIndex(616, nil) - if err != nil || rSpaceNo != 616 { - t.Errorf("numeric space param not resolved as-is") - } - rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex("schematest", "secondary") - if err != nil || rSpaceNo != 616 || rIndexNo != 3 { - t.Errorf("symbolic space and index params not resolved") - } - rSpaceNo, _, err = schema.ResolveSpaceIndex("schematest", nil) - if err != nil || rSpaceNo != 616 { - t.Errorf("symbolic space param not resolved") - } - _, _, err = schema.ResolveSpaceIndex("schematest22", "secondary") - if err == nil { - t.Errorf("ResolveSpaceIndex didn't returned error with not existing space name") - } - _, _, err = schema.ResolveSpaceIndex("schematest", "secondary22") - if err == nil { - t.Errorf("ResolveSpaceIndex didn't returned error with not existing index name") - } } func TestSchema_IsNullable(t *testing.T) { @@ -3302,6 +3263,7 @@ func TestConnectionProtocolInfoSupported(t *testing.T) { iproto.IPROTO_FEATURE_ERROR_EXTENSION, iproto.IPROTO_FEATURE_WATCHERS, iproto.IPROTO_FEATURE_PAGINATION, + iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, iproto.IPROTO_FEATURE_WATCH_ONCE, }, }) @@ -3420,6 +3382,7 @@ func TestConnectionProtocolInfoUnsupported(t *testing.T) { iproto.IPROTO_FEATURE_ERROR_EXTENSION, iproto.IPROTO_FEATURE_WATCHERS, iproto.IPROTO_FEATURE_PAGINATION, + iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, iproto.IPROTO_FEATURE_WATCH_ONCE, }, }) From c64f80a13508537d0ba0cf94df142bf4435bd83d Mon Sep 17 00:00:00 2001 From: DerekBum Date: Thu, 9 Nov 2023 15:11:11 +0300 Subject: [PATCH 499/605] test: more correct use of the `Fatalf` in `request_test.go` Replaced `t.Errorf` + `return` by `t.Fatalf`. This made all tests in the file follow the same code style. Part of #338 --- request_test.go | 102 ++++++++++++++++-------------------------------- 1 file changed, 34 insertions(+), 68 deletions(-) diff --git a/request_test.go b/request_test.go index f5968b738..7c2e5e514 100644 --- a/request_test.go +++ b/request_test.go @@ -387,8 +387,7 @@ func TestPingRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplPingBody(refEnc) if err != nil { - t.Errorf("An unexpected RefImplPingBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplPingBody() error: %q", err.Error()) } req := NewPingRequest() @@ -402,8 +401,7 @@ func TestSelectRequestDefaultValues(t *testing.T) { err := RefImplSelectBody(refEnc, &resolver, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterAll, []interface{}{}, nil, false) if err != nil { - t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) - return + t.Fatalf("An unexpected RefImplSelectBody() error %q", err.Error()) } req := NewSelectRequest(validSpace) @@ -451,8 +449,7 @@ func TestSelectRequestDefaultIteratorEqIfKey(t *testing.T) { err := RefImplSelectBody(refEnc, &resolver, validSpace, defaultIndex, 0, 0xFFFFFFFF, IterEq, key, nil, false) if err != nil { - t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) - return + t.Fatalf("An unexpected RefImplSelectBody() error %q", err.Error()) } req := NewSelectRequest(validSpace). @@ -469,8 +466,7 @@ func TestSelectRequestIteratorNotChangedIfKey(t *testing.T) { err := RefImplSelectBody(refEnc, &resolver, validSpace, defaultIndex, 0, 0xFFFFFFFF, iter, key, nil, false) if err != nil { - t.Errorf("An unexpected RefImplSelectBody() error %q", err.Error()) - return + t.Fatalf("An unexpected RefImplSelectBody() error %q", err.Error()) } req := NewSelectRequest(validSpace). @@ -492,16 +488,14 @@ func TestSelectRequestSetters(t *testing.T) { err := RefImplSelectBody(refEncAfterBytes, &resolver, validSpace, validIndex, offset, limit, iter, key, afterBytes, true) if err != nil { - t.Errorf("An unexpected RefImplSelectBody() error %s", err) - return + t.Fatalf("An unexpected RefImplSelectBody() error %s", err) } refEncAfterKey := msgpack.NewEncoder(&refBufAfterKey) err = RefImplSelectBody(refEncAfterKey, &resolver, validSpace, validIndex, offset, limit, iter, key, afterKey, true) if err != nil { - t.Errorf("An unexpected RefImplSelectBody() error %s", err) - return + t.Fatalf("An unexpected RefImplSelectBody() error %s", err) } reqAfterBytes := NewSelectRequest(validSpace). @@ -531,8 +525,7 @@ func TestInsertRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplInsertBody(refEnc, &resolver, validSpace, []interface{}{}) if err != nil { - t.Errorf("An unexpected RefImplInsertBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplInsertBody() error: %q", err.Error()) } req := NewInsertRequest(validSpace) @@ -561,8 +554,7 @@ func TestInsertRequestSetters(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplInsertBody(refEnc, &resolver, validSpace, tuple) if err != nil { - t.Errorf("An unexpected RefImplInsertBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplInsertBody() error: %q", err.Error()) } req := NewInsertRequest(validSpace). @@ -576,8 +568,7 @@ func TestReplaceRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplReplaceBody(refEnc, &resolver, validSpace, []interface{}{}) if err != nil { - t.Errorf("An unexpected RefImplReplaceBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplReplaceBody() error: %q", err.Error()) } req := NewReplaceRequest(validSpace) @@ -592,8 +583,7 @@ func TestReplaceRequestSpaceByName(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplReplaceBody(refEnc, &resolver, "valid", []interface{}{}) if err != nil { - t.Errorf("An unexpected RefImplReplaceBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplReplaceBody() error: %q", err.Error()) } req := NewReplaceRequest("valid") @@ -607,8 +597,7 @@ func TestReplaceRequestSetters(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplReplaceBody(refEnc, &resolver, validSpace, tuple) if err != nil { - t.Errorf("An unexpected RefImplReplaceBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplReplaceBody() error: %q", err.Error()) } req := NewReplaceRequest(validSpace). @@ -622,8 +611,7 @@ func TestDeleteRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplDeleteBody(refEnc, &resolver, validSpace, defaultIndex, []interface{}{}) if err != nil { - t.Errorf("An unexpected RefImplDeleteBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplDeleteBody() error: %q", err.Error()) } req := NewDeleteRequest(validSpace) @@ -668,8 +656,7 @@ func TestDeleteRequestSetters(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplDeleteBody(refEnc, &resolver, validSpace, validIndex, key) if err != nil { - t.Errorf("An unexpected RefImplDeleteBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplDeleteBody() error: %q", err.Error()) } req := NewDeleteRequest(validSpace). @@ -685,8 +672,7 @@ func TestUpdateRequestDefaultValues(t *testing.T) { err := RefImplUpdateBody(refEnc, &resolver, validSpace, defaultIndex, []interface{}{}, []Op{}) if err != nil { - t.Errorf("An unexpected RefImplUpdateBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplUpdateBody() error: %q", err.Error()) } req := NewUpdateRequest(validSpace) @@ -734,8 +720,7 @@ func TestUpdateRequestSetters(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplUpdateBody(refEnc, &resolver, validSpace, validIndex, key, refOps) if err != nil { - t.Errorf("An unexpected RefImplUpdateBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplUpdateBody() error: %q", err.Error()) } req := NewUpdateRequest(validSpace). @@ -751,8 +736,7 @@ func TestUpsertRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplUpsertBody(refEnc, &resolver, validSpace, []interface{}{}, []Op{}) if err != nil { - t.Errorf("An unexpected RefImplUpsertBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplUpsertBody() error: %q", err.Error()) } req := NewUpsertRequest(validSpace) @@ -782,8 +766,7 @@ func TestUpsertRequestSetters(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplUpsertBody(refEnc, &resolver, validSpace, tuple, refOps) if err != nil { - t.Errorf("An unexpected RefImplUpsertBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplUpsertBody() error: %q", err.Error()) } req := NewUpsertRequest(validSpace). @@ -798,8 +781,7 @@ func TestCallRequestsDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplCallBody(refEnc, validExpr, []interface{}{}) if err != nil { - t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplCallBody() error: %q", err.Error()) } req := NewCallRequest(validExpr) @@ -817,8 +799,7 @@ func TestCallRequestsSetters(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplCallBody(refEnc, validExpr, args) if err != nil { - t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplCallBody() error: %q", err.Error()) } req := NewCallRequest(validExpr). @@ -838,8 +819,7 @@ func TestEvalRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplEvalBody(refEnc, validExpr, []interface{}{}) if err != nil { - t.Errorf("An unexpected RefImplEvalBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplEvalBody() error: %q", err.Error()) } req := NewEvalRequest(validExpr) @@ -853,8 +833,7 @@ func TestEvalRequestSetters(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplEvalBody(refEnc, validExpr, args) if err != nil { - t.Errorf("An unexpected RefImplEvalBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplEvalBody() error: %q", err.Error()) } req := NewEvalRequest(validExpr). @@ -868,8 +847,7 @@ func TestExecuteRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplExecuteBody(refEnc, validExpr, []interface{}{}) if err != nil { - t.Errorf("An unexpected RefImplExecuteBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplExecuteBody() error: %q", err.Error()) } req := NewExecuteRequest(validExpr) @@ -883,8 +861,7 @@ func TestExecuteRequestSetters(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplExecuteBody(refEnc, validExpr, args) if err != nil { - t.Errorf("An unexpected RefImplExecuteBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplExecuteBody() error: %q", err.Error()) } req := NewExecuteRequest(validExpr). @@ -898,8 +875,7 @@ func TestPrepareRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplPrepareBody(refEnc, validExpr) if err != nil { - t.Errorf("An unexpected RefImplPrepareBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplPrepareBody() error: %q", err.Error()) } req := NewPrepareRequest(validExpr) @@ -912,8 +888,7 @@ func TestUnprepareRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplUnprepareBody(refEnc, *validStmt) if err != nil { - t.Errorf("An unexpected RefImplUnprepareBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplUnprepareBody() error: %q", err.Error()) } req := NewUnprepareRequest(validStmt) @@ -928,8 +903,7 @@ func TestExecutePreparedRequestSetters(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplExecutePreparedBody(refEnc, *validStmt, args) if err != nil { - t.Errorf("An unexpected RefImplExecutePreparedBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplExecutePreparedBody() error: %q", err.Error()) } req := NewExecutePreparedRequest(validStmt). @@ -944,8 +918,7 @@ func TestExecutePreparedRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplExecutePreparedBody(refEnc, *validStmt, []interface{}{}) if err != nil { - t.Errorf("An unexpected RefImplExecutePreparedBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplExecutePreparedBody() error: %q", err.Error()) } req := NewExecutePreparedRequest(validStmt) @@ -959,8 +932,7 @@ func TestBeginRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplBeginBody(refEnc, defaultIsolationLevel, defaultTimeout) if err != nil { - t.Errorf("An unexpected RefImplBeginBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplBeginBody() error: %q", err.Error()) } req := NewBeginRequest() @@ -973,8 +945,7 @@ func TestBeginRequestSetters(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplBeginBody(refEnc, ReadConfirmedLevel, validTimeout) if err != nil { - t.Errorf("An unexpected RefImplBeginBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplBeginBody() error: %q", err.Error()) } req := NewBeginRequest().TxnIsolation(ReadConfirmedLevel).Timeout(validTimeout) @@ -987,8 +958,7 @@ func TestCommitRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplCommitBody(refEnc) if err != nil { - t.Errorf("An unexpected RefImplCommitBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplCommitBody() error: %q", err.Error()) } req := NewCommitRequest() @@ -1001,8 +971,7 @@ func TestRollbackRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplRollbackBody(refEnc) if err != nil { - t.Errorf("An unexpected RefImplRollbackBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplRollbackBody() error: %q", err.Error()) } req := NewRollbackRequest() @@ -1016,8 +985,7 @@ func TestBroadcastRequestDefaultValues(t *testing.T) { expectedArgs := []interface{}{validKey} err := RefImplCallBody(refEnc, "box.broadcast", expectedArgs) if err != nil { - t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplCallBody() error: %q", err.Error()) } req := NewBroadcastRequest(validKey) @@ -1032,8 +1000,7 @@ func TestBroadcastRequestSetters(t *testing.T) { expectedArgs := []interface{}{validKey, value} err := RefImplCallBody(refEnc, "box.broadcast", expectedArgs) if err != nil { - t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplCallBody() error: %q", err.Error()) } req := NewBroadcastRequest(validKey).Value(value) @@ -1046,8 +1013,7 @@ func TestWatchOnceRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplWatchOnceBody(refEnc, validKey) if err != nil { - t.Errorf("An unexpected RefImplCallBody() error: %q", err.Error()) - return + t.Fatalf("An unexpected RefImplCallBody() error: %q", err.Error()) } req := NewWatchOnceRequest(validKey) From 39ec344a4f57d378f96738513950e660b7133fb4 Mon Sep 17 00:00:00 2001 From: DerekBum Date: Thu, 9 Nov 2023 15:57:53 +0300 Subject: [PATCH 500/605] ci: update Tarantool EE 1.10, 2.10 and 2.11 versions Update Tarantool EE version 1.10.11 to 1.10.15, 2.10.0 to 2.10.8 and 2.11.0 to 2.11.1. This was done because of the one flacking test: https://github.com/tarantool/go-tarantool/actions/runs/6805504621/job/18505152412 Closes #338 --- .github/workflows/testing.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 66d0b7063..c21344e27 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -138,17 +138,20 @@ jobs: strategy: fail-fast: false matrix: + sdk-path: + - 'release/linux/x86_64/1.10/' sdk-version: - - 'bundle-1.10.11-0-gf0b0e7ecf-r470' + - 'sdk-1.10.15-0-r598' coveralls: [false] fuzzing: [false] ssl: [false] include: - - sdk-version: 'bundle-2.10.0-1-gfa775b383-r486-linux-x86_64' + - sdk-path: 'release/linux/x86_64/2.10/' + sdk-version: 'sdk-gc64-2.10.8-0-r598.linux.x86_64' coveralls: false ssl: true - sdk-path: 'release/linux/x86_64/2.11/' - sdk-version: 'sdk-gc64-2.11.0-0-r577.linux.x86_64' + sdk-version: 'sdk-gc64-2.11.1-0-r598.linux.x86_64' coveralls: true ssl: true From eecc9b0743114ccbc6ac6c1b2dc1a535b06c4835 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Wed, 8 Nov 2023 16:18:32 +0300 Subject: [PATCH 501/605] crud: remove redundant name Option name is already provided by `yieldEveryOptName`. Follows #350 --- crud/options.go | 1 - 1 file changed, 1 deletion(-) diff --git a/crud/options.go b/crud/options.go index fa5dca2c9..eec3fc0db 100644 --- a/crud/options.go +++ b/crud/options.go @@ -23,7 +23,6 @@ const ( batchSizeOptName = "batch_size" fetchLatestMetadataOptName = "fetch_latest_metadata" noreturnOptName = "noreturn" - yieldEvery = "yield_every" ) // OptUint is an optional uint. From 9892410daaaee0c86c2b431cc3d55338eb147f17 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Wed, 8 Nov 2023 16:20:31 +0300 Subject: [PATCH 502/605] doc: fix crud.SchemaRequest methods --- crud/schema.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crud/schema.go b/crud/schema.go index 3335a3347..7f055e90a 100644 --- a/crud/schema.go +++ b/crud/schema.go @@ -21,14 +21,14 @@ type SchemaRequest struct { space OptString } -// MakeSchemaRequest returns a new empty StatsRequest. +// MakeSchemaRequest returns a new empty SchemaRequest. func MakeSchemaRequest() SchemaRequest { req := SchemaRequest{} req.impl = newCall("crud.schema") return req } -// Space sets the space name for the StatsRequest request. +// Space sets the space name for the SchemaRequest request. // Note: default value is nil. func (req SchemaRequest) Space(space string) SchemaRequest { req.space = MakeOptString(space) From 92b6ff5f19c64ce279dcaa8e60441962c1cb6b20 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Wed, 8 Nov 2023 16:26:21 +0300 Subject: [PATCH 503/605] crud: support schema opts Follows #336 --- CHANGELOG.md | 2 +- crud/options.go | 1 + crud/request_test.go | 24 ++++++++++++++++++++++++ crud/schema.go | 39 +++++++++++++++++++++++++++++++++++++-- crud/tarantool_test.go | 10 ++++++++++ 5 files changed, 73 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8500568ef..5fa34ce19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support `operation_data` in `crud.Error` (#330) - Support `fetch_latest_metadata` option for crud requests with metadata (#335) - Support `noreturn` option for data change crud requests (#335) -- Support `crud.schema` request (#336) +- Support `crud.schema` request (#336, #351) - Support `IPROTO_WATCH_ONCE` request type for Tarantool version >= 3.0.0-alpha1 (#337) - Support `yield_every` option for crud select requests (#350) diff --git a/crud/options.go b/crud/options.go index eec3fc0db..311df522c 100644 --- a/crud/options.go +++ b/crud/options.go @@ -23,6 +23,7 @@ const ( batchSizeOptName = "batch_size" fetchLatestMetadataOptName = "fetch_latest_metadata" noreturnOptName = "noreturn" + cachedOptName = "cached" ) // OptUint is an optional uint. diff --git a/crud/request_test.go b/crud/request_test.go index 0b728162e..d370dbdea 100644 --- a/crud/request_test.go +++ b/crud/request_test.go @@ -159,6 +159,7 @@ func TestRequestsCodes(t *testing.T) { {req: crud.MakeCountRequest(validSpace), rtype: CrudRequestType}, {req: crud.MakeStorageInfoRequest(), rtype: CrudRequestType}, {req: crud.MakeStatsRequest(), rtype: CrudRequestType}, + {req: crud.MakeSchemaRequest(), rtype: CrudRequestType}, } for _, test := range tests { @@ -196,6 +197,7 @@ func TestRequestsAsync(t *testing.T) { {req: crud.MakeCountRequest(validSpace), async: false}, {req: crud.MakeStorageInfoRequest(), async: false}, {req: crud.MakeStatsRequest(), async: false}, + {req: crud.MakeSchemaRequest(), async: false}, } for _, test := range tests { @@ -233,6 +235,7 @@ func TestRequestsCtx_default(t *testing.T) { {req: crud.MakeCountRequest(validSpace), expected: nil}, {req: crud.MakeStorageInfoRequest(), expected: nil}, {req: crud.MakeStatsRequest(), expected: nil}, + {req: crud.MakeSchemaRequest(), expected: nil}, } for _, test := range tests { @@ -271,6 +274,7 @@ func TestRequestsCtx_setter(t *testing.T) { {req: crud.MakeCountRequest(validSpace).Context(ctx), expected: ctx}, {req: crud.MakeStorageInfoRequest().Context(ctx), expected: ctx}, {req: crud.MakeStatsRequest().Context(ctx), expected: ctx}, + {req: crud.MakeSchemaRequest().Context(ctx), expected: ctx}, } for _, test := range tests { @@ -427,6 +431,12 @@ func TestRequestsDefaultValues(t *testing.T) { []interface{}{}), target: crud.MakeStatsRequest(), }, + { + name: "SchemaRequest", + ref: tarantool.NewCall17Request("crud.schema").Args( + []interface{}{nil, map[string]interface{}{}}), + target: crud.MakeSchemaRequest(), + }, } for _, tc := range testCases { @@ -589,6 +599,20 @@ func TestRequestsSetters(t *testing.T) { []interface{}{spaceName}), target: crud.MakeStatsRequest().Space(spaceName), }, + { + name: "SchemaRequest", + ref: tarantool.NewCall17Request("crud.schema").Args( + []interface{}{nil, schemaOpts}, + ), + target: crud.MakeSchemaRequest().Opts(schemaOpts), + }, + { + name: "SchemaRequestWithSpace", + ref: tarantool.NewCall17Request("crud.schema").Args( + []interface{}{spaceName, schemaOpts}, + ), + target: crud.MakeSchemaRequest().Space(spaceName).Opts(schemaOpts), + }, } for _, tc := range testCases { diff --git a/crud/schema.go b/crud/schema.go index 7f055e90a..6f3b94a97 100644 --- a/crud/schema.go +++ b/crud/schema.go @@ -14,11 +14,39 @@ func msgpackIsMap(code byte) bool { return code == msgpcode.Map16 || code == msgpcode.Map32 || msgpcode.IsFixedMap(code) } +// SchemaOpts describes options for `crud.schema` method. +type SchemaOpts struct { + // Timeout is a `vshard.call` timeout and vshard + // master discovery timeout (in seconds). + Timeout OptFloat64 + // VshardRouter is cartridge vshard group name or + // vshard router instance. + VshardRouter OptString + // Cached defines whether router should reload storage schema on call. + Cached OptBool +} + +// EncodeMsgpack provides custom msgpack encoder. +func (opts SchemaOpts) EncodeMsgpack(enc *msgpack.Encoder) error { + const optsCnt = 3 + + names := [optsCnt]string{timeoutOptName, vshardRouterOptName, + cachedOptName} + values := [optsCnt]interface{}{} + exists := [optsCnt]bool{} + values[0], exists[0] = opts.Timeout.Get() + values[1], exists[1] = opts.VshardRouter.Get() + values[2], exists[2] = opts.Cached.Get() + + return encodeOptions(enc, names[:], values[:], exists[:]) +} + // SchemaRequest helps you to create request object to call `crud.schema` // for execution by a Connection. type SchemaRequest struct { baseRequest space OptString + opts SchemaOpts } // MakeSchemaRequest returns a new empty SchemaRequest. @@ -35,12 +63,19 @@ func (req SchemaRequest) Space(space string) SchemaRequest { return req } +// Opts sets the options for the SchemaRequest request. +// Note: default value is nil. +func (req SchemaRequest) Opts(opts SchemaOpts) SchemaRequest { + req.opts = opts + return req +} + // Body fills an encoder with the call request body. func (req SchemaRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { if value, ok := req.space.Get(); ok { - req.impl = req.impl.Args([]interface{}{value}) + req.impl = req.impl.Args([]interface{}{value, req.opts}) } else { - req.impl = req.impl.Args([]interface{}{}) + req.impl = req.impl.Args([]interface{}{nil, req.opts}) } return req.impl.Body(res, enc) diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 399b0d48f..529f0201a 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -87,6 +87,11 @@ var opObjManyOpts = crud.OperationObjectManyOpts{ Timeout: crud.MakeOptFloat64(timeout), } +var schemaOpts = crud.SchemaOpts{ + Timeout: crud.MakeOptFloat64(timeout), + Cached: crud.MakeOptBool(false), +} + var conditions = []crud.Condition{ { Operator: crud.Lt, @@ -216,6 +221,11 @@ var testProcessDataCases = []struct { 1, crud.MakeStorageInfoRequest().Opts(baseOpts), }, + { + "Schema", + 1, + crud.MakeSchemaRequest().Opts(schemaOpts), + }, } var testResultWithErrCases = []struct { From 8d4fedd6e05b4035c585b5589753c4699a1412f8 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 14 Nov 2023 12:58:04 +0300 Subject: [PATCH 504/605] test: check crud vshard_router option encoding --- crud/request_test.go | 276 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/crud/request_test.go b/crud/request_test.go index d370dbdea..7c889cf40 100644 --- a/crud/request_test.go +++ b/crud/request_test.go @@ -621,3 +621,279 @@ func TestRequestsSetters(t *testing.T) { }) } } + +func TestRequestsVshardRouter(t *testing.T) { + testCases := []struct { + name string + ref tarantool.Request + target tarantool.Request + }{ + { + name: "InsertRequest", + ref: tarantool.NewCall17Request("crud.insert").Args([]interface{}{ + validSpace, + []interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeInsertRequest(validSpace).Opts(crud.InsertOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "InsertObjectRequest", + ref: tarantool.NewCall17Request("crud.insert_object").Args([]interface{}{ + validSpace, + map[string]interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeInsertObjectRequest(validSpace).Opts(crud.InsertObjectOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "InsertManyRequest", + ref: tarantool.NewCall17Request("crud.insert_many").Args([]interface{}{ + validSpace, + []interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeInsertManyRequest(validSpace).Opts(crud.InsertManyOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "InsertObjectManyRequest", + ref: tarantool.NewCall17Request("crud.insert_object_many").Args([]interface{}{ + validSpace, + []map[string]interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeInsertObjectManyRequest(validSpace).Opts(crud.InsertObjectManyOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "GetRequest", + ref: tarantool.NewCall17Request("crud.get").Args([]interface{}{ + validSpace, + []interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeGetRequest(validSpace).Opts(crud.GetOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "UpdateRequest", + ref: tarantool.NewCall17Request("crud.update").Args([]interface{}{ + validSpace, + []interface{}{}, + []interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeUpdateRequest(validSpace).Opts(crud.UpdateOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "DeleteRequest", + ref: tarantool.NewCall17Request("crud.delete").Args([]interface{}{ + validSpace, + []interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeDeleteRequest(validSpace).Opts(crud.DeleteOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "ReplaceRequest", + ref: tarantool.NewCall17Request("crud.replace").Args([]interface{}{ + validSpace, + []interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeReplaceRequest(validSpace).Opts(crud.ReplaceOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "ReplaceObjectRequest", + ref: tarantool.NewCall17Request("crud.replace_object").Args([]interface{}{ + validSpace, + map[string]interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeReplaceObjectRequest(validSpace).Opts(crud.ReplaceObjectOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "ReplaceManyRequest", + ref: tarantool.NewCall17Request("crud.replace_many").Args([]interface{}{ + validSpace, + []interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeReplaceManyRequest(validSpace).Opts(crud.ReplaceManyOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "ReplaceObjectManyRequest", + ref: tarantool.NewCall17Request("crud.replace_object_many").Args([]interface{}{ + validSpace, + []map[string]interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeReplaceObjectManyRequest(validSpace).Opts(crud.ReplaceObjectManyOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "UpsertRequest", + ref: tarantool.NewCall17Request("crud.upsert").Args([]interface{}{ + validSpace, + []interface{}{}, + []interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeUpsertRequest(validSpace).Opts(crud.UpsertOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "UpsertObjectRequest", + ref: tarantool.NewCall17Request("crud.upsert_object").Args([]interface{}{ + validSpace, + map[string]interface{}{}, + []interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeUpsertObjectRequest(validSpace).Opts(crud.UpsertObjectOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "UpsertManyRequest", + ref: tarantool.NewCall17Request("crud.upsert_many").Args([]interface{}{ + validSpace, + []interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeUpsertManyRequest(validSpace).Opts(crud.UpsertManyOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "UpsertObjectManyRequest", + ref: tarantool.NewCall17Request("crud.upsert_object_many").Args([]interface{}{ + validSpace, + []interface{}{}, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeUpsertObjectManyRequest(validSpace).Opts(crud.UpsertObjectManyOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "SelectRequest", + ref: tarantool.NewCall17Request("crud.select").Args([]interface{}{ + validSpace, + nil, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeSelectRequest(validSpace).Opts(crud.SelectOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "MinRequest", + ref: tarantool.NewCall17Request("crud.min").Args([]interface{}{ + validSpace, + nil, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeMinRequest(validSpace).Opts(crud.MinOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "MaxRequest", + ref: tarantool.NewCall17Request("crud.max").Args([]interface{}{ + validSpace, + nil, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeMaxRequest(validSpace).Opts(crud.MaxOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "TruncateRequest", + ref: tarantool.NewCall17Request("crud.truncate").Args([]interface{}{ + validSpace, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeTruncateRequest(validSpace).Opts(crud.TruncateOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "LenRequest", + ref: tarantool.NewCall17Request("crud.len").Args([]interface{}{ + validSpace, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeLenRequest(validSpace).Opts(crud.LenOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "CountRequest", + ref: tarantool.NewCall17Request("crud.count").Args([]interface{}{ + validSpace, + nil, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeCountRequest(validSpace).Opts(crud.CountOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "StorageInfoRequest", + ref: tarantool.NewCall17Request("crud.storage_info").Args([]interface{}{ + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeStorageInfoRequest().Opts(crud.StorageInfoOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "SchemaRequest", + ref: tarantool.NewCall17Request("crud.schema").Args([]interface{}{ + nil, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeSchemaRequest().Opts(crud.SchemaOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + { + name: "SchemaRequestWithSpace", + ref: tarantool.NewCall17Request("crud.schema").Args([]interface{}{ + validSpace, + map[string]interface{}{"vshard_router": "custom_router"}, + }), + target: crud.MakeSchemaRequest().Space(validSpace).Opts(crud.SchemaOpts{ + VshardRouter: crud.MakeOptString("custom_router"), + }), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assertBodyEqual(t, tc.ref, tc.target) + }) + } +} From a5de8f3242ca5085455162c66a406b447809c268 Mon Sep 17 00:00:00 2001 From: DerekBum Date: Tue, 14 Nov 2023 21:06:57 +0300 Subject: [PATCH 505/605] api: fix splice update operation Splice update operation (`:`) accepts 5 and only 5 arguments. It was parsed and encoded incorrectly with only 3 argument (as every other update operations). Also fixed the same operation for the crud. Part of #348 --- CHANGELOG.md | 1 + README.md | 5 ++ client_tools.go | 61 +++++++++++-------- crud/operations.go | 42 ++++++++++++- crud/tarantool_test.go | 132 +++++++++++++++++++++++++++++++++++++++++ example_test.go | 27 ++++----- request_test.go | 22 +++---- test_helpers/utils.go | 9 +++ 8 files changed, 245 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa34ce19..3875f125e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. `prefer_replica`, `balance`) setup for crud.GetRequest (#335) - Tests with crud 1.4.0 (#336) - Tests with case sensitive SQL (#341) +- Splice update operation accepts 3 arguments instead of 5 (#348) ## [1.12.0] - 2023-06-07 diff --git a/README.md b/README.md index f8483dc34..c4b1edeeb 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,11 @@ interface to get information if the usage of space and index names in requests is supported. * `Schema` structure no longer implements `SchemaResolver` interface. +#### Client tools changes + +* Remove `OpSplice` struct. +* `Operations.Splice` method now accepts 5 arguments instead of 3. + ## Contributing See [the contributing guide](CONTRIBUTING.md) for detailed instructions on how diff --git a/client_tools.go b/client_tools.go index 159f3d0ce..396645205 100644 --- a/client_tools.go +++ b/client_tools.go @@ -59,12 +59,38 @@ type Op struct { Op string Field int Arg interface{} + // Pos, Len, Replace fields used in the Splice operation. + Pos int + Len int + Replace string } func (o Op) EncodeMsgpack(enc *msgpack.Encoder) error { - enc.EncodeArrayLen(3) - enc.EncodeString(o.Op) - enc.EncodeInt(int64(o.Field)) + isSpliceOperation := o.Op == spliceOperator + argsLen := 3 + if isSpliceOperation { + argsLen = 5 + } + if err := enc.EncodeArrayLen(argsLen); err != nil { + return err + } + if err := enc.EncodeString(o.Op); err != nil { + return err + } + if err := enc.EncodeInt(int64(o.Field)); err != nil { + return err + } + + if isSpliceOperation { + if err := enc.EncodeInt(int64(o.Pos)); err != nil { + return err + } + if err := enc.EncodeInt(int64(o.Len)); err != nil { + return err + } + return enc.EncodeString(o.Replace) + } + return enc.Encode(o.Arg) } @@ -92,7 +118,12 @@ func NewOperations() *Operations { } func (ops *Operations) append(op string, field int, arg interface{}) *Operations { - ops.ops = append(ops.ops, Op{op, field, arg}) + ops.ops = append(ops.ops, Op{Op: op, Field: field, Arg: arg}) + return ops +} + +func (ops *Operations) appendSplice(op string, field, pos, len int, replace string) *Operations { + ops.ops = append(ops.ops, Op{Op: op, Field: field, Pos: pos, Len: len, Replace: replace}) return ops } @@ -122,8 +153,8 @@ func (ops *Operations) BitwiseXor(field int, arg interface{}) *Operations { } // Splice adds a splice operation to the collection of update operations. -func (ops *Operations) Splice(field int, arg interface{}) *Operations { - return ops.append(spliceOperator, field, arg) +func (ops *Operations) Splice(field, pos, len int, replace string) *Operations { + return ops.appendSplice(spliceOperator, field, pos, len, replace) } // Insert adds an insert operation to the collection of update operations. @@ -140,21 +171,3 @@ func (ops *Operations) Delete(field int, arg interface{}) *Operations { func (ops *Operations) Assign(field int, arg interface{}) *Operations { return ops.append(assignOperator, field, arg) } - -type OpSplice struct { - Op string - Field int - Pos int - Len int - Replace string -} - -func (o OpSplice) EncodeMsgpack(enc *msgpack.Encoder) error { - enc.EncodeArrayLen(5) - enc.EncodeString(o.Op) - enc.EncodeInt(int64(o.Field)) - enc.EncodeInt(int64(o.Pos)) - enc.EncodeInt(int64(o.Len)) - enc.EncodeString(o.Replace) - return nil -} diff --git a/crud/operations.go b/crud/operations.go index 5d55acc15..633afe7c6 100644 --- a/crud/operations.go +++ b/crud/operations.go @@ -1,5 +1,9 @@ package crud +import ( + "github.com/vmihailenco/msgpack/v5" +) + const ( // Add - operator for addition. Add Operator = "+" @@ -23,11 +27,43 @@ const ( // Operation describes CRUD operation as a table // {operator, field_identifier, value}. +// Splice operation described as a table +// {operator, field_identifier, position, length, replace_string}. type Operation struct { - // Instruct msgpack to pack this struct as array, so no custom packer - // is needed. - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused Operator Operator Field interface{} // Number or string. Value interface{} + // Pos, Len, Replace fields used in the Splice operation. + Pos int + Len int + Replace string +} + +func (o Operation) EncodeMsgpack(enc *msgpack.Encoder) error { + isSpliceOperation := o.Operator == Splice + argsLen := 3 + if isSpliceOperation { + argsLen = 5 + } + if err := enc.EncodeArrayLen(argsLen); err != nil { + return err + } + if err := enc.EncodeString(string(o.Operator)); err != nil { + return err + } + if err := enc.Encode(o.Field); err != nil { + return err + } + + if isSpliceOperation { + if err := enc.EncodeInt(int64(o.Pos)); err != nil { + return err + } + if err := enc.EncodeInt(int64(o.Len)); err != nil { + return err + } + return enc.EncodeString(o.Replace) + } + + return enc.Encode(o.Value) } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 529f0201a..9f6b5d948 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -40,6 +40,63 @@ var startOpts test_helpers.StartOpts = test_helpers.StartOpts{ var timeout = float64(1.1) var operations = []crud.Operation{ + // Insert new fields, + // because double update of the same field results in an error. + { + Operator: crud.Insert, + Field: 4, + Value: 0, + }, + { + Operator: crud.Insert, + Field: 5, + Value: 0, + }, + { + Operator: crud.Insert, + Field: 6, + Value: 0, + }, + { + Operator: crud.Insert, + Field: 7, + Value: 0, + }, + { + Operator: crud.Insert, + Field: 8, + Value: 0, + }, + { + Operator: crud.Add, + Field: 4, + Value: 1, + }, + { + Operator: crud.Sub, + Field: 5, + Value: 1, + }, + { + Operator: crud.And, + Field: 6, + Value: 1, + }, + { + Operator: crud.Or, + Field: 7, + Value: 1, + }, + { + Operator: crud.Xor, + Field: 8, + Value: 1, + }, + { + Operator: crud.Delete, + Field: 4, + Value: 5, + }, { Operator: crud.Assign, Field: "name", @@ -542,6 +599,81 @@ func TestCrudProcessData(t *testing.T) { } } +func TestCrudUpdateSplice(t *testing.T) { + test_helpers.SkipIfCrudSpliceBroken(t) + + conn := connect(t) + defer conn.Close() + + req := crud.MakeUpdateRequest(spaceName). + Key(key). + Operations([]crud.Operation{ + { + Operator: crud.Splice, + Field: "name", + Pos: 1, + Len: 1, + Replace: "!!", + }, + }). + Opts(simpleOperationOpts) + + testCrudRequestPrepareData(t, conn) + resp, err := conn.Do(req).Get() + testCrudRequestCheck(t, req, resp, + err, 2) +} + +func TestCrudUpsertSplice(t *testing.T) { + test_helpers.SkipIfCrudSpliceBroken(t) + + conn := connect(t) + defer conn.Close() + + req := crud.MakeUpsertRequest(spaceName). + Tuple(tuple). + Operations([]crud.Operation{ + { + Operator: crud.Splice, + Field: "name", + Pos: 1, + Len: 1, + Replace: "!!", + }, + }). + Opts(simpleOperationOpts) + + testCrudRequestPrepareData(t, conn) + resp, err := conn.Do(req).Get() + testCrudRequestCheck(t, req, resp, + err, 2) +} + +func TestCrudUpsertObjectSplice(t *testing.T) { + test_helpers.SkipIfCrudSpliceBroken(t) + + conn := connect(t) + defer conn.Close() + + req := crud.MakeUpsertObjectRequest(spaceName). + Object(object). + Operations([]crud.Operation{ + { + Operator: crud.Splice, + Field: "name", + Pos: 1, + Len: 1, + Replace: "!!", + }, + }). + Opts(simpleOperationOpts) + + testCrudRequestPrepareData(t, conn) + resp, err := conn.Do(req).Get() + testCrudRequestCheck(t, req, resp, + err, 2) +} + func TestUnflattenRows_IncorrectParams(t *testing.T) { invalidMetadata := []interface{}{ map[interface{}]interface{}{ diff --git a/example_test.go b/example_test.go index 9b66ea29b..7e689bcf3 100644 --- a/example_test.go +++ b/example_test.go @@ -439,34 +439,29 @@ func ExampleUpdateRequest() { for i := 1111; i <= 1112; i++ { conn.Do(tarantool.NewReplaceRequest(spaceNo). - Tuple([]interface{}{uint(i), "hello", "world"}), + Tuple([]interface{}{uint(i), "text", 1, 1, 1, 1, 1}), ).Get() } req := tarantool.NewUpdateRequest(617). Key(tarantool.IntKey{1111}). - Operations(tarantool.NewOperations().Assign(1, "bye")) + Operations(tarantool.NewOperations(). + Add(2, 1). + Subtract(3, 1). + BitwiseAnd(4, 1). + BitwiseOr(5, 1). + BitwiseXor(6, 1). + Splice(1, 1, 2, "!!"). + Insert(7, "new"). + Assign(7, "updated")) resp, err := conn.Do(req).Get() if err != nil { fmt.Printf("error in do update request is %v", err) return } fmt.Printf("response is %#v\n", resp.Data) - - req = tarantool.NewUpdateRequest("test"). - Index("primary"). - Key(tarantool.IntKey{1111}). - Operations(tarantool.NewOperations().Assign(1, "hello")) - fut := conn.Do(req) - resp, err = fut.Get() - if err != nil { - fmt.Printf("error in do async update request is %v", err) - return - } - fmt.Printf("response is %#v\n", resp.Data) // Output: - // response is []interface {}{[]interface {}{0x457, "bye", "world"}} - // response is []interface {}{[]interface {}{0x457, "hello", "world"}} + // response is []interface {}{[]interface {}{0x457, "t!!t", 2, 0, 1, 1, 0, "updated"}} } func ExampleUpdateRequest_spaceAndIndexNames() { diff --git a/request_test.go b/request_test.go index 7c2e5e514..af06e3c61 100644 --- a/request_test.go +++ b/request_test.go @@ -120,16 +120,16 @@ func assertBodyEqual(t testing.TB, reference []byte, req Request) { func getTestOps() ([]Op, *Operations) { ops := []Op{ - {"+", 1, 2}, - {"-", 3, 4}, - {"&", 5, 6}, - {"|", 7, 8}, - {"^", 9, 1}, - {"^", 9, 1}, // The duplication is for test purposes. - {":", 2, 3}, - {"!", 4, 5}, - {"#", 6, 7}, - {"=", 8, 9}, + {Op: "+", Field: 1, Arg: 2}, + {Op: "-", Field: 3, Arg: 4}, + {Op: "&", Field: 5, Arg: 6}, + {Op: "|", Field: 7, Arg: 8}, + {Op: "^", Field: 9, Arg: 1}, + {Op: "^", Field: 9, Arg: 1}, // The duplication is for test purposes. + {Op: ":", Field: 2, Pos: 3, Len: 1, Replace: "!!"}, + {Op: "!", Field: 4, Arg: 5}, + {Op: "#", Field: 6, Arg: 7}, + {Op: "=", Field: 8, Arg: 9}, } operations := NewOperations(). Add(1, 2). @@ -138,7 +138,7 @@ func getTestOps() ([]Op, *Operations) { BitwiseOr(7, 8). BitwiseXor(9, 1). BitwiseXor(9, 1). // The duplication is for test purposes. - Splice(2, 3). + Splice(2, 3, 1, "!!"). Insert(4, 5). Delete(6, 7). Assign(8, 9) diff --git a/test_helpers/utils.go b/test_helpers/utils.go index d2a941775..52c078c90 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -200,6 +200,15 @@ func SkipIfWatchOnceUnsupported(t *testing.T) { SkipIfFeatureUnsupported(t, "watch once", 3, 0, 0) } +// SkipIfCrudSpliceBroken skips test run if splice operation is broken +// on the crud side. +// https://github.com/tarantool/crud/issues/397 +func SkipIfCrudSpliceBroken(t *testing.T) { + t.Helper() + + SkipIfFeatureUnsupported(t, "crud update splice", 2, 0, 0) +} + // CheckEqualBoxErrors checks equivalence of tarantool.BoxError objects. // // Tarantool errors are not comparable by nature: From 6225ec495b92da3631e90c7d14b75061b6a72844 Mon Sep 17 00:00:00 2001 From: DerekBum Date: Wed, 15 Nov 2023 18:15:31 +0300 Subject: [PATCH 506/605] api: update all `Update` and `Upsert` requests `Op` struct made private. Change all `Upsert` and `Update` requests to accept `*tarantool.Operations` as `ops` parameters instead of `interface{}`. Closes #348 --- CHANGELOG.md | 2 + README.md | 15 +++-- client_tools.go | 20 +++--- client_tools_test.go | 51 +++++++++++++++ connector.go | 11 ++-- crud/operations.go | 1 + crud/operations_test.go | 123 +++++++++++++++++++++++++++++++++++ export_test.go | 4 +- pool/connection_pool.go | 20 +++--- pool/connection_pool_test.go | 8 +-- pool/connector.go | 14 ++-- pool/connector_test.go | 20 +++--- pool/pooler.go | 14 ++-- request.go | 39 +++++------ request_test.go | 34 ++++------ tarantool_test.go | 12 ++-- 16 files changed, 281 insertions(+), 107 deletions(-) create mode 100644 client_tools_test.go create mode 100644 crud/operations_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 3875f125e..182161846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. constants for `protocol` (#337) - Change `crud` operations `Timeout` option type to `crud.OptFloat64` instead of `crud.OptUint` (#342) +- Change all `Upsert` and `Update` requests to accept `*tarantool.Operations` + as `ops` parameters instead of `interface{}` (#348) ### Deprecated diff --git a/README.md b/README.md index c4b1edeeb..a0a470a86 100644 --- a/README.md +++ b/README.md @@ -229,9 +229,17 @@ of the requests is an array instead of array of arrays. IPROTO constants have been moved to a separate package [go-iproto](https://github.com/tarantool/go-iproto). -#### Request interface +#### Request changes * The method `Code() uint32` replaced by the `Type() iproto.Type`. +* `Op` struct for update operations made private. +* Removed `OpSplice` struct. +* `Operations.Splice` method now accepts 5 arguments instead of 3. +* Requests `Update`, `UpdateAsync`, `UpdateTyped`, `Upsert`, `UpsertAsync` no +longer accept `ops` argument (operations) as an `interface{}`. `*Operations` +needs to be passed instead. +* `UpdateRequest` and `UpsertRequest` structs no longer accept `interface{}` +for an `ops` field. `*Operations` needs to be used instead. #### Connect function @@ -253,11 +261,6 @@ interface to get information if the usage of space and index names in requests is supported. * `Schema` structure no longer implements `SchemaResolver` interface. -#### Client tools changes - -* Remove `OpSplice` struct. -* `Operations.Splice` method now accepts 5 arguments instead of 3. - ## Contributing See [the contributing guide](CONTRIBUTING.md) for detailed instructions on how diff --git a/client_tools.go b/client_tools.go index 396645205..351b07cae 100644 --- a/client_tools.go +++ b/client_tools.go @@ -54,8 +54,8 @@ func (k IntIntKey) EncodeMsgpack(enc *msgpack.Encoder) error { return nil } -// Op - is update operation. -type Op struct { +// operation - is update operation. +type operation struct { Op string Field int Arg interface{} @@ -65,7 +65,7 @@ type Op struct { Replace string } -func (o Op) EncodeMsgpack(enc *msgpack.Encoder) error { +func (o operation) EncodeMsgpack(enc *msgpack.Encoder) error { isSpliceOperation := o.Op == spliceOperator argsLen := 3 if isSpliceOperation { @@ -108,22 +108,26 @@ const ( // Operations is a collection of update operations. type Operations struct { - ops []Op + ops []operation +} + +// EncodeMsgpack encodes Operations as an array of operations. +func (ops *Operations) EncodeMsgpack(enc *msgpack.Encoder) error { + return enc.Encode(ops.ops) } // NewOperations returns a new empty collection of update operations. func NewOperations() *Operations { - ops := new(Operations) - return ops + return &Operations{[]operation{}} } func (ops *Operations) append(op string, field int, arg interface{}) *Operations { - ops.ops = append(ops.ops, Op{Op: op, Field: field, Arg: arg}) + ops.ops = append(ops.ops, operation{Op: op, Field: field, Arg: arg}) return ops } func (ops *Operations) appendSplice(op string, field, pos, len int, replace string) *Operations { - ops.ops = append(ops.ops, Op{Op: op, Field: field, Pos: pos, Len: len, Replace: replace}) + ops.ops = append(ops.ops, operation{Op: op, Field: field, Pos: pos, Len: len, Replace: replace}) return ops } diff --git a/client_tools_test.go b/client_tools_test.go new file mode 100644 index 000000000..fdd109152 --- /dev/null +++ b/client_tools_test.go @@ -0,0 +1,51 @@ +package tarantool_test + +import ( + "bytes" + "testing" + + "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v2" +) + +func TestOperations_EncodeMsgpack(t *testing.T) { + ops := tarantool.NewOperations(). + Add(1, 2). + Subtract(1, 2). + BitwiseAnd(1, 2). + BitwiseOr(1, 2). + BitwiseXor(1, 2). + Splice(1, 2, 3, "a"). + Insert(1, 2). + Delete(1, 2). + Assign(1, 2) + refOps := []interface{}{ + []interface{}{"+", 1, 2}, + []interface{}{"-", 1, 2}, + []interface{}{"&", 1, 2}, + []interface{}{"|", 1, 2}, + []interface{}{"^", 1, 2}, + []interface{}{":", 1, 2, 3, "a"}, + []interface{}{"!", 1, 2}, + []interface{}{"#", 1, 2}, + []interface{}{"=", 1, 2}, + } + + var refBuf bytes.Buffer + encRef := msgpack.NewEncoder(&refBuf) + if err := encRef.Encode(refOps); err != nil { + t.Errorf("error while encoding: %v", err.Error()) + } + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + + if err := enc.Encode(ops); err != nil { + t.Errorf("error while encoding: %v", err.Error()) + } + if !bytes.Equal(refBuf.Bytes(), buf.Bytes()) { + t.Errorf("encode response is wrong:\n expected %v\n got: %v", + refBuf, buf.Bytes()) + } +} diff --git a/connector.go b/connector.go index 5d4b5be39..9536116d7 100644 --- a/connector.go +++ b/connector.go @@ -29,10 +29,10 @@ type Connector interface { Delete(space, index interface{}, key interface{}) (*Response, error) // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. - Update(space, index interface{}, key, ops interface{}) (*Response, error) + Update(space, index interface{}, key interface{}, ops *Operations) (*Response, error) // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. - Upsert(space interface{}, tuple, ops interface{}) (*Response, error) + Upsert(space interface{}, tuple interface{}, ops *Operations) (*Response, error) // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. Call(functionName string, args interface{}) (*Response, error) @@ -67,7 +67,8 @@ type Connector interface { DeleteTyped(space, index interface{}, key interface{}, result interface{}) error // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. - UpdateTyped(space, index interface{}, key, ops interface{}, result interface{}) error + UpdateTyped(space, index interface{}, key interface{}, ops *Operations, + result interface{}) error // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. CallTyped(functionName string, args interface{}, result interface{}) error @@ -100,10 +101,10 @@ type Connector interface { DeleteAsync(space, index interface{}, key interface{}) *Future // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. - UpdateAsync(space, index interface{}, key, ops interface{}) *Future + UpdateAsync(space, index interface{}, key interface{}, ops *Operations) *Future // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. - UpsertAsync(space interface{}, tuple interface{}, ops interface{}) *Future + UpsertAsync(space interface{}, tuple interface{}, ops *Operations) *Future // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. CallAsync(functionName string, args interface{}) *Future diff --git a/crud/operations.go b/crud/operations.go index 633afe7c6..84953d590 100644 --- a/crud/operations.go +++ b/crud/operations.go @@ -39,6 +39,7 @@ type Operation struct { Replace string } +// EncodeMsgpack encodes Operation. func (o Operation) EncodeMsgpack(enc *msgpack.Encoder) error { isSpliceOperation := o.Operator == Splice argsLen := 3 diff --git a/crud/operations_test.go b/crud/operations_test.go new file mode 100644 index 000000000..0ff3e818a --- /dev/null +++ b/crud/operations_test.go @@ -0,0 +1,123 @@ +package crud_test + +import ( + "bytes" + "testing" + + "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v2/crud" +) + +func TestOperation_EncodeMsgpack(t *testing.T) { + testCases := []struct { + name string + op crud.Operation + ref []interface{} + }{ + { + "Add", + crud.Operation{ + Operator: crud.Add, + Field: 1, + Value: 2, + }, + []interface{}{"+", 1, 2}, + }, + { + "Sub", + crud.Operation{ + Operator: crud.Sub, + Field: 1, + Value: 2, + }, + []interface{}{"-", 1, 2}, + }, + { + "And", + crud.Operation{ + Operator: crud.And, + Field: 1, + Value: 2, + }, + []interface{}{"&", 1, 2}, + }, + { + "Or", + crud.Operation{ + Operator: crud.Or, + Field: 1, + Value: 2, + }, + []interface{}{"|", 1, 2}, + }, + { + "Xor", + crud.Operation{ + Operator: crud.Xor, + Field: 1, + Value: 2, + }, + []interface{}{"^", 1, 2}, + }, + { + "Splice", + crud.Operation{ + Operator: crud.Splice, + Field: 1, + Pos: 2, + Len: 3, + Replace: "a", + }, + []interface{}{":", 1, 2, 3, "a"}, + }, + { + "Insert", + crud.Operation{ + Operator: crud.Insert, + Field: 1, + Value: 2, + }, + []interface{}{"!", 1, 2}, + }, + { + "Delete", + crud.Operation{ + Operator: crud.Delete, + Field: 1, + Value: 2, + }, + []interface{}{"#", 1, 2}, + }, + { + "Assign", + crud.Operation{ + Operator: crud.Assign, + Field: 1, + Value: 2, + }, + []interface{}{"=", 1, 2}, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + var refBuf bytes.Buffer + encRef := msgpack.NewEncoder(&refBuf) + if err := encRef.Encode(test.ref); err != nil { + t.Errorf("error while encoding: %v", err.Error()) + } + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + + if err := enc.Encode(test.op); err != nil { + t.Errorf("error while encoding: %v", err.Error()) + } + if !bytes.Equal(refBuf.Bytes(), buf.Bytes()) { + t.Errorf("encode response is wrong:\n expected %v\n got: %v", + refBuf, buf.Bytes()) + } + }) + } +} diff --git a/export_test.go b/export_test.go index a52ef5b26..2c28472b9 100644 --- a/export_test.go +++ b/export_test.go @@ -78,7 +78,7 @@ func RefImplDeleteBody(enc *msgpack.Encoder, res SchemaResolver, space, index, // RefImplUpdateBody is reference implementation for filling of an update // request's body. func RefImplUpdateBody(enc *msgpack.Encoder, res SchemaResolver, space, index, - key, ops interface{}) error { + key interface{}, ops *Operations) error { spaceEnc, err := newSpaceEncoder(res, space) if err != nil { return err @@ -93,7 +93,7 @@ func RefImplUpdateBody(enc *msgpack.Encoder, res SchemaResolver, space, index, // RefImplUpsertBody is reference implementation for filling of an upsert // request's body. func RefImplUpsertBody(enc *msgpack.Encoder, res SchemaResolver, space, - tuple, ops interface{}) error { + tuple interface{}, ops *Operations) error { spaceEnc, err := newSpaceEncoder(res, space) if err != nil { return err diff --git a/pool/connection_pool.go b/pool/connection_pool.go index aa84d8c24..6186683b3 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -462,8 +462,8 @@ func (p *ConnectionPool) Delete(space, index interface{}, key interface{}, // // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. -func (p *ConnectionPool) Update(space, index interface{}, key, ops interface{}, - userMode ...Mode) (*tarantool.Response, error) { +func (p *ConnectionPool) Update(space, index interface{}, key interface{}, + ops *tarantool.Operations, userMode ...Mode) (*tarantool.Response, error) { conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err @@ -477,8 +477,8 @@ func (p *ConnectionPool) Update(space, index interface{}, key, ops interface{}, // // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. -func (p *ConnectionPool) Upsert(space interface{}, tuple, ops interface{}, - userMode ...Mode) (*tarantool.Response, error) { +func (p *ConnectionPool) Upsert(space interface{}, tuple interface{}, + ops *tarantool.Operations, userMode ...Mode) (*tarantool.Response, error) { conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err @@ -639,8 +639,8 @@ func (p *ConnectionPool) DeleteTyped(space, index interface{}, key interface{}, // // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. -func (p *ConnectionPool) UpdateTyped(space, index interface{}, key, ops interface{}, - result interface{}, userMode ...Mode) error { +func (p *ConnectionPool) UpdateTyped(space, index interface{}, key interface{}, + ops *tarantool.Operations, result interface{}, userMode ...Mode) error { conn, err := p.getConnByMode(RW, userMode) if err != nil { return err @@ -788,8 +788,8 @@ func (p *ConnectionPool) DeleteAsync(space, index interface{}, key interface{}, // // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. -func (p *ConnectionPool) UpdateAsync(space, index interface{}, key, ops interface{}, - userMode ...Mode) *tarantool.Future { +func (p *ConnectionPool) UpdateAsync(space, index interface{}, key interface{}, + ops *tarantool.Operations, userMode ...Mode) *tarantool.Future { conn, err := p.getConnByMode(RW, userMode) if err != nil { return newErrorFuture(err) @@ -803,8 +803,8 @@ func (p *ConnectionPool) UpdateAsync(space, index interface{}, key, ops interfac // // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. -func (p *ConnectionPool) UpsertAsync(space interface{}, tuple interface{}, ops interface{}, - userMode ...Mode) *tarantool.Future { +func (p *ConnectionPool) UpsertAsync(space interface{}, tuple interface{}, + ops *tarantool.Operations, userMode ...Mode) *tarantool.Future { conn, err := p.getConnByMode(RW, userMode) if err != nil { return newErrorFuture(err) diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index 27310be23..8b19e671b 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -1964,7 +1964,7 @@ func TestUpsert(t *testing.T) { // Mode is `RW` by default, we have only one RW instance (servers[2]) resp, err := connPool.Upsert(spaceName, []interface{}{"upsert_key", "upsert_value"}, - []interface{}{[]interface{}{"=", 1, "new_value"}}) + tarantool.NewOperations().Assign(1, "new_value")) require.Nilf(t, err, "failed to Upsert") require.NotNilf(t, resp, "response is nil after Upsert") @@ -1993,7 +1993,7 @@ func TestUpsert(t *testing.T) { // PreferRW resp, err = connPool.Upsert( spaceName, []interface{}{"upsert_key", "upsert_value"}, - []interface{}{[]interface{}{"=", 1, "new_value"}}, pool.PreferRW) + tarantool.NewOperations().Assign(1, "new_value"), pool.PreferRW) require.Nilf(t, err, "failed to Upsert") require.NotNilf(t, resp, "response is nil after Upsert") @@ -2056,7 +2056,7 @@ func TestUpdate(t *testing.T) { // Mode is `RW` by default, we have only one RW instance (servers[2]) resp, err = connPool.Update(spaceName, indexNo, - []interface{}{"update_key"}, []interface{}{[]interface{}{"=", 1, "new_value"}}) + []interface{}{"update_key"}, tarantool.NewOperations().Assign(1, "new_value")) require.Nilf(t, err, "failed to Update") require.NotNilf(t, resp, "response is nil after Update") @@ -2085,7 +2085,7 @@ func TestUpdate(t *testing.T) { // PreferRW resp, err = connPool.Update( spaceName, indexNo, []interface{}{"update_key"}, - []interface{}{[]interface{}{"=", 1, "another_value"}}, pool.PreferRW) + tarantool.NewOperations().Assign(1, "another_value"), pool.PreferRW) require.Nilf(t, err, "failed to Update") require.NotNilf(t, resp, "response is nil after Update") diff --git a/pool/connector.go b/pool/connector.go index acb9a9187..604e3921f 100644 --- a/pool/connector.go +++ b/pool/connector.go @@ -106,7 +106,7 @@ func (c *ConnectorAdapter) Delete(space, index interface{}, // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. func (c *ConnectorAdapter) Update(space, index interface{}, - key, ops interface{}) (*tarantool.Response, error) { + key interface{}, ops *tarantool.Operations) (*tarantool.Response, error) { return c.pool.Update(space, index, key, ops, c.mode) } @@ -114,8 +114,8 @@ func (c *ConnectorAdapter) Update(space, index interface{}, // // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. -func (c *ConnectorAdapter) Upsert(space interface{}, - tuple, ops interface{}) (*tarantool.Response, error) { +func (c *ConnectorAdapter) Upsert(space, tuple interface{}, + ops *tarantool.Operations) (*tarantool.Response, error) { return c.pool.Upsert(space, tuple, ops, c.mode) } @@ -220,7 +220,7 @@ func (c *ConnectorAdapter) DeleteTyped(space, index interface{}, // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. func (c *ConnectorAdapter) UpdateTyped(space, index interface{}, - key, ops interface{}, result interface{}) error { + key interface{}, ops *tarantool.Operations, result interface{}) error { return c.pool.UpdateTyped(space, index, key, ops, result, c.mode) } @@ -314,7 +314,7 @@ func (c *ConnectorAdapter) DeleteAsync(space, index interface{}, // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. func (c *ConnectorAdapter) UpdateAsync(space, index interface{}, - key, ops interface{}) *tarantool.Future { + key interface{}, ops *tarantool.Operations) *tarantool.Future { return c.pool.UpdateAsync(space, index, key, ops, c.mode) } @@ -322,8 +322,8 @@ func (c *ConnectorAdapter) UpdateAsync(space, index interface{}, // // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. -func (c *ConnectorAdapter) UpsertAsync(space interface{}, tuple interface{}, - ops interface{}) *tarantool.Future { +func (c *ConnectorAdapter) UpsertAsync(space, tuple interface{}, + ops *tarantool.Operations) *tarantool.Future { return c.pool.UpsertAsync(space, tuple, ops, c.mode) } diff --git a/pool/connector_test.go b/pool/connector_test.go index b667b0d34..fa107cc58 100644 --- a/pool/connector_test.go +++ b/pool/connector_test.go @@ -148,7 +148,7 @@ var reqIndex interface{} = []interface{}{2} var reqArgs interface{} = []interface{}{3} var reqTuple interface{} = []interface{}{4} var reqKey interface{} = []interface{}{5} -var reqOps interface{} = []interface{}{6} +var reqOps = tarantool.NewOperations() var reqResult interface{} = []interface{}{7} var reqSqlInfo = tarantool.SQLInfo{AffectedCount: 3} @@ -547,8 +547,8 @@ type updateMock struct { baseRequestMock } -func (m *updateMock) Update(space, index, key, ops interface{}, - mode ...Mode) (*tarantool.Response, error) { +func (m *updateMock) Update(space, index, key interface{}, + ops *tarantool.Operations, mode ...Mode) (*tarantool.Response, error) { m.called++ m.space = space m.index = index @@ -578,8 +578,8 @@ type updateTypedMock struct { baseRequestMock } -func (m *updateTypedMock) UpdateTyped(space, index, key, ops interface{}, - result interface{}, mode ...Mode) error { +func (m *updateTypedMock) UpdateTyped(space, index, key interface{}, + ops *tarantool.Operations, result interface{}, mode ...Mode) error { m.called++ m.space = space m.index = index @@ -610,8 +610,8 @@ type updateAsyncMock struct { baseRequestMock } -func (m *updateAsyncMock) UpdateAsync(space, index, key, ops interface{}, - mode ...Mode) *tarantool.Future { +func (m *updateAsyncMock) UpdateAsync(space, index, key interface{}, + ops *tarantool.Operations, mode ...Mode) *tarantool.Future { m.called++ m.space = space m.index = index @@ -640,7 +640,7 @@ type upsertMock struct { baseRequestMock } -func (m *upsertMock) Upsert(space, tuple, ops interface{}, +func (m *upsertMock) Upsert(space, tuple interface{}, ops *tarantool.Operations, mode ...Mode) (*tarantool.Response, error) { m.called++ m.space = space @@ -669,8 +669,8 @@ type upsertAsyncMock struct { baseRequestMock } -func (m *upsertAsyncMock) UpsertAsync(space, tuple, ops interface{}, - mode ...Mode) *tarantool.Future { +func (m *upsertAsyncMock) UpsertAsync(space, tuple interface{}, + ops *tarantool.Operations, mode ...Mode) *tarantool.Future { m.called++ m.space = space m.tuple = tuple diff --git a/pool/pooler.go b/pool/pooler.go index a588d67f4..0ff945bbb 100644 --- a/pool/pooler.go +++ b/pool/pooler.go @@ -38,11 +38,11 @@ type Pooler interface { mode ...Mode) (*tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. - Update(space, index interface{}, key, ops interface{}, + Update(space, index interface{}, key interface{}, ops *tarantool.Operations, mode ...Mode) (*tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. - Upsert(space interface{}, tuple, ops interface{}, + Upsert(space interface{}, tuple interface{}, ops *tarantool.Operations, mode ...Mode) (*tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. @@ -87,8 +87,8 @@ type Pooler interface { mode ...Mode) error // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. - UpdateTyped(space, index interface{}, key, ops interface{}, - result interface{}, mode ...Mode) error + UpdateTyped(space, index interface{}, key interface{}, + ops *tarantool.Operations, result interface{}, mode ...Mode) error // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. CallTyped(functionName string, args interface{}, result interface{}, @@ -128,11 +128,11 @@ type Pooler interface { mode ...Mode) *tarantool.Future // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. - UpdateAsync(space, index interface{}, key, ops interface{}, - mode ...Mode) *tarantool.Future + UpdateAsync(space, index interface{}, key interface{}, + ops *tarantool.Operations, mode ...Mode) *tarantool.Future // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. - UpsertAsync(space interface{}, tuple interface{}, ops interface{}, + UpsertAsync(space interface{}, tuple interface{}, ops *tarantool.Operations, mode ...Mode) *tarantool.Future // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. diff --git a/request.go b/request.go index c2e7cf96d..ac0f7e858 100644 --- a/request.go +++ b/request.go @@ -182,16 +182,20 @@ func fillSelect(enc *msgpack.Encoder, spaceEnc spaceEncoder, indexEnc indexEncod } func fillUpdate(enc *msgpack.Encoder, spaceEnc spaceEncoder, indexEnc indexEncoder, - key, ops interface{}) error { + key interface{}, ops *Operations) error { enc.EncodeMapLen(4) if err := fillSearch(enc, spaceEnc, indexEnc, key); err != nil { return err } enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)) + if ops == nil { + return enc.Encode([]interface{}{}) + } return enc.Encode(ops) } -func fillUpsert(enc *msgpack.Encoder, spaceEnc spaceEncoder, tuple, ops interface{}) error { +func fillUpsert(enc *msgpack.Encoder, spaceEnc spaceEncoder, tuple interface{}, + ops *Operations) error { enc.EncodeMapLen(3) if err := spaceEnc.Encode(enc); err != nil { return err @@ -202,6 +206,9 @@ func fillUpsert(enc *msgpack.Encoder, spaceEnc spaceEncoder, tuple, ops interfac return err } enc.EncodeUint(uint64(iproto.IPROTO_OPS)) + if ops == nil { + return enc.Encode([]interface{}{}) + } return enc.Encode(ops) } @@ -308,7 +315,7 @@ func (conn *Connection) Delete(space, index interface{}, key interface{}) (*Resp // // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. -func (conn *Connection) Update(space, index interface{}, key, ops interface{}) (*Response, error) { +func (conn *Connection) Update(space, index, key interface{}, ops *Operations) (*Response, error) { return conn.UpdateAsync(space, index, key, ops).Get() } @@ -319,7 +326,7 @@ func (conn *Connection) Update(space, index interface{}, key, ops interface{}) ( // // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. -func (conn *Connection) Upsert(space interface{}, tuple, ops interface{}) (*Response, error) { +func (conn *Connection) Upsert(space, tuple interface{}, ops *Operations) (*Response, error) { return conn.UpsertAsync(space, tuple, ops).Get() } @@ -464,8 +471,8 @@ func (conn *Connection) DeleteTyped(space, index interface{}, key interface{}, // // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. -func (conn *Connection) UpdateTyped(space, index interface{}, key, ops interface{}, - result interface{}) error { +func (conn *Connection) UpdateTyped(space, index interface{}, key interface{}, + ops *Operations, result interface{}) error { return conn.UpdateAsync(space, index, key, ops).GetTyped(result) } @@ -580,7 +587,8 @@ func (conn *Connection) DeleteAsync(space, index interface{}, key interface{}) * // // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. -func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface{}) *Future { +func (conn *Connection) UpdateAsync(space, index interface{}, key interface{}, + ops *Operations) *Future { req := NewUpdateRequest(space).Index(index).Key(key) req.ops = ops return conn.Do(req) @@ -591,8 +599,7 @@ func (conn *Connection) UpdateAsync(space, index interface{}, key, ops interface // // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. -func (conn *Connection) UpsertAsync(space interface{}, tuple interface{}, - ops interface{}) *Future { +func (conn *Connection) UpsertAsync(space, tuple interface{}, ops *Operations) *Future { req := NewUpsertRequest(space).Tuple(tuple) req.ops = ops return conn.Do(req) @@ -1193,7 +1200,7 @@ func (req *DeleteRequest) Context(ctx context.Context) *DeleteRequest { type UpdateRequest struct { spaceIndexRequest key interface{} - ops interface{} + ops *Operations } // NewUpdateRequest returns a new empty UpdateRequest. @@ -1202,7 +1209,6 @@ func NewUpdateRequest(space interface{}) *UpdateRequest { req.rtype = iproto.IPROTO_UPDATE req.setSpace(space) req.key = []interface{}{} - req.ops = []interface{}{} return req } @@ -1223,9 +1229,7 @@ func (req *UpdateRequest) Key(key interface{}) *UpdateRequest { // Operations sets operations to be performed on update. // Note: default value is empty. func (req *UpdateRequest) Operations(ops *Operations) *UpdateRequest { - if ops != nil { - req.ops = ops.ops - } + req.ops = ops return req } @@ -1259,7 +1263,7 @@ func (req *UpdateRequest) Context(ctx context.Context) *UpdateRequest { type UpsertRequest struct { spaceRequest tuple interface{} - ops interface{} + ops *Operations } // NewUpsertRequest returns a new empty UpsertRequest. @@ -1268,7 +1272,6 @@ func NewUpsertRequest(space interface{}) *UpsertRequest { req.rtype = iproto.IPROTO_UPSERT req.setSpace(space) req.tuple = []interface{}{} - req.ops = []interface{}{} return req } @@ -1282,9 +1285,7 @@ func (req *UpsertRequest) Tuple(tuple interface{}) *UpsertRequest { // Operations sets operations to be performed on update case by the upsert request. // Note: default value is empty. func (req *UpsertRequest) Operations(ops *Operations) *UpsertRequest { - if ops != nil { - req.ops = ops.ops - } + req.ops = ops return req } diff --git a/request_test.go b/request_test.go index af06e3c61..580259702 100644 --- a/request_test.go +++ b/request_test.go @@ -118,19 +118,7 @@ func assertBodyEqual(t testing.TB, reference []byte, req Request) { } } -func getTestOps() ([]Op, *Operations) { - ops := []Op{ - {Op: "+", Field: 1, Arg: 2}, - {Op: "-", Field: 3, Arg: 4}, - {Op: "&", Field: 5, Arg: 6}, - {Op: "|", Field: 7, Arg: 8}, - {Op: "^", Field: 9, Arg: 1}, - {Op: "^", Field: 9, Arg: 1}, // The duplication is for test purposes. - {Op: ":", Field: 2, Pos: 3, Len: 1, Replace: "!!"}, - {Op: "!", Field: 4, Arg: 5}, - {Op: "#", Field: 6, Arg: 7}, - {Op: "=", Field: 8, Arg: 9}, - } +func getTestOps() *Operations { operations := NewOperations(). Add(1, 2). Subtract(3, 4). @@ -142,7 +130,7 @@ func getTestOps() ([]Op, *Operations) { Insert(4, 5). Delete(6, 7). Assign(8, 9) - return ops, operations + return operations } func TestRequestsValidSpaceAndIndex(t *testing.T) { @@ -670,7 +658,7 @@ func TestUpdateRequestDefaultValues(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplUpdateBody(refEnc, &resolver, validSpace, defaultIndex, - []interface{}{}, []Op{}) + []interface{}{}, nil) if err != nil { t.Fatalf("An unexpected RefImplUpdateBody() error: %q", err.Error()) } @@ -686,7 +674,7 @@ func TestUpdateRequestSpaceByName(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplUpdateBody(refEnc, &resolver, "valid", defaultIndex, - []interface{}{}, []Op{}) + []interface{}{}, nil) if err != nil { t.Fatalf("An unexpected RefImplUpdateBody() error: %q", err.Error()) } @@ -702,7 +690,7 @@ func TestUpdateRequestIndexByName(t *testing.T) { refEnc := msgpack.NewEncoder(&refBuf) err := RefImplUpdateBody(refEnc, &resolver, defaultSpace, "valid", - []interface{}{}, []Op{}) + []interface{}{}, nil) if err != nil { t.Fatalf("An unexpected RefImplUpdateBody() error: %q", err.Error()) } @@ -714,11 +702,11 @@ func TestUpdateRequestIndexByName(t *testing.T) { func TestUpdateRequestSetters(t *testing.T) { key := []interface{}{uint(44)} - refOps, reqOps := getTestOps() + reqOps := getTestOps() var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpdateBody(refEnc, &resolver, validSpace, validIndex, key, refOps) + err := RefImplUpdateBody(refEnc, &resolver, validSpace, validIndex, key, reqOps) if err != nil { t.Fatalf("An unexpected RefImplUpdateBody() error: %q", err.Error()) } @@ -734,7 +722,7 @@ func TestUpsertRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpsertBody(refEnc, &resolver, validSpace, []interface{}{}, []Op{}) + err := RefImplUpsertBody(refEnc, &resolver, validSpace, []interface{}{}, nil) if err != nil { t.Fatalf("An unexpected RefImplUpsertBody() error: %q", err.Error()) } @@ -749,7 +737,7 @@ func TestUpsertRequestSpaceByName(t *testing.T) { resolver.nameUseSupported = true refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpsertBody(refEnc, &resolver, "valid", []interface{}{}, []Op{}) + err := RefImplUpsertBody(refEnc, &resolver, "valid", []interface{}{}, nil) if err != nil { t.Fatalf("An unexpected RefImplUpsertBody() error: %q", err.Error()) } @@ -760,11 +748,11 @@ func TestUpsertRequestSpaceByName(t *testing.T) { func TestUpsertRequestSetters(t *testing.T) { tuple := []interface{}{uint(64)} - refOps, reqOps := getTestOps() + reqOps := getTestOps() var refBuf bytes.Buffer refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpsertBody(refEnc, &resolver, validSpace, tuple, refOps) + err := RefImplUpsertBody(refEnc, &resolver, validSpace, tuple, reqOps) if err != nil { t.Fatalf("An unexpected RefImplUpsertBody() error: %q", err.Error()) } diff --git a/tarantool_test.go b/tarantool_test.go index 8be963474..f8da2bdb5 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -992,7 +992,7 @@ func TestClient(t *testing.T) { // Update resp, err = conn.Update(spaceNo, indexNo, []interface{}{uint(2)}, - []interface{}{[]interface{}{"=", 1, "bye"}, []interface{}{"#", 2, 1}}) + NewOperations().Assign(1, "bye").Delete(2, 1)) if err != nil { t.Fatalf("Failed to Update: %s", err.Error()) } @@ -1021,7 +1021,7 @@ func TestClient(t *testing.T) { // Upsert resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, - []interface{}{[]interface{}{"+", 1, 1}}) + NewOperations().Add(1, 1)) if err != nil { t.Fatalf("Failed to Upsert (insert): %s", err.Error()) } @@ -1032,7 +1032,7 @@ func TestClient(t *testing.T) { t.Errorf("Response should not have a position") } resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, - []interface{}{[]interface{}{"+", 1, 1}}) + NewOperations().Add(1, 1)) if err != nil { t.Fatalf("Failed to Upsert (update): %s", err.Error()) } @@ -2095,7 +2095,7 @@ func TestClientNamed(t *testing.T) { resp, err = conn.Update(spaceName, indexName, []interface{}{ uint(1002)}, - []interface{}{[]interface{}{"=", 1, "bye"}, []interface{}{"#", 2, 1}}) + NewOperations().Assign(1, "buy").Delete(2, 1)) if err != nil { t.Fatalf("Failed to Update: %s", err.Error()) } @@ -2105,7 +2105,7 @@ func TestClientNamed(t *testing.T) { // Upsert resp, err = conn.Upsert(spaceName, - []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + []interface{}{uint(1003), 1}, NewOperations().Add(1, 1)) if err != nil { t.Fatalf("Failed to Upsert (insert): %s", err.Error()) } @@ -2113,7 +2113,7 @@ func TestClientNamed(t *testing.T) { t.Errorf("Response is nil after Upsert (insert)") } resp, err = conn.Upsert(spaceName, - []interface{}{uint(1003), 1}, []interface{}{[]interface{}{"+", 1, 1}}) + []interface{}{uint(1003), 1}, NewOperations().Add(1, 1)) if err != nil { t.Fatalf("Failed to Upsert (update): %s", err.Error()) } From a0ce23284bcca04902d889b67b237928272316ee Mon Sep 17 00:00:00 2001 From: DerekBum Date: Sat, 18 Nov 2023 17:41:38 +0300 Subject: [PATCH 507/605] api: write a connection schema getter Write a helper function to load the actual schema for the user. Previously we stored actual schema in a private `schemaResolver` field and `Schema` field was used only to get a current schema. But now because of the new function, we don't need to store the `Schema` as a different field. So `Schema` was also removed. To update the schema, one needs to use `GetSchema` + `SetSchema` in pair. `SetSchema(Schema)` replacing the `OverrideSchema(*Schema)`. `Spaces` and `SpacesById` fields of the `Schema` struct store spaces by value. `Fields` and `FieldsById` fields of the `Space` struct store fields by value. `Index` and `IndexById` fields of the `Space` struct store indexes by value. `Fields` field of the `Index` struct store `IndexField` by value. Closes #7 --- CHANGELOG.md | 5 +++ README.md | 11 +++++ connection.go | 28 +++++++----- example_test.go | 26 +++++++++-- schema.go | 107 +++++++++++++++++++++++++++++----------------- tarantool_test.go | 85 ++++++++++++++++++++++++++++++------ 6 files changed, 195 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 182161846..e585d8e37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support `IPROTO_FEATURE_SPACE_AND_INDEX_NAMES` for Tarantool version >= 3.0.0-alpha1 (#338). It allows to use space and index names in requests instead of their IDs. +- `GetSchema` function to get the actual schema (#7) ### Changed @@ -51,6 +52,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. instead of `crud.OptUint` (#342) - Change all `Upsert` and `Update` requests to accept `*tarantool.Operations` as `ops` parameters instead of `interface{}` (#348) +- Change `OverrideSchema(*Schema)` to `SetSchema(Schema)` (#7) +- Change values, stored by pointers in the `Schema`, `Space`, `Index` structs, + to be stored by their values (#7) ### Deprecated @@ -70,6 +74,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - UUID_extId (#158) - IPROTO constants (#158) - Code() method from the Request interface (#158) +- `Schema` field from the `Connection` struct (#7) ### Fixed diff --git a/README.md b/README.md index a0a470a86..6a7a51f0c 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,13 @@ now does not attempt to reconnect and tries to establish a connection only once. Function might be canceled via context. Context accepted as first argument, and user may cancel it in process. +#### Connection schema + +* Removed `Schema` field from the `Connection` struct. Instead, new +`GetSchema(Connector)` function was added to get the actual connection +schema on demand. +* `OverrideSchema(*Schema)` method replaced with the `SetSchema(Schema)`. + #### Protocol changes * `iproto.Feature` type used instead of `ProtocolFeature`. @@ -260,6 +267,10 @@ and user may cancel it in process. interface to get information if the usage of space and index names in requests is supported. * `Schema` structure no longer implements `SchemaResolver` interface. +* `Spaces` and `SpacesById` fields of the `Schema` struct store spaces by value. +* `Fields` and `FieldsById` fields of the `Space` struct store fields by value. +`Index` and `IndexById` fields of the `Space` struct store indexes by value. +* `Fields` field of the `Index` struct store `IndexField` by value. ## Contributing diff --git a/connection.go b/connection.go index 217c153dd..48de5476b 100644 --- a/connection.go +++ b/connection.go @@ -160,8 +160,6 @@ type Connection struct { c Conn mutex sync.Mutex cond *sync.Cond - // Schema contains schema loaded on connection. - Schema *Schema // schemaResolver contains a SchemaResolver implementation. schemaResolver SchemaResolver // requestId contains the last request ID for requests with nil context. @@ -436,12 +434,14 @@ func Connect(ctx context.Context, addr string, opts Opts) (conn *Connection, err // TODO: reload schema after reconnect. if !conn.opts.SkipSchema { - if err = conn.loadSchema(); err != nil { + schema, err := GetSchema(conn) + if err != nil { conn.mutex.Lock() defer conn.mutex.Unlock() conn.closeConnection(err, true) return nil, err } + conn.SetSchema(schema) } return conn, err @@ -1302,15 +1302,21 @@ func (conn *Connection) ConfiguredTimeout() time.Duration { return conn.opts.Timeout } -// OverrideSchema sets Schema for the connection. -func (conn *Connection) OverrideSchema(s *Schema) { - if s != nil { - conn.mutex.Lock() - defer conn.mutex.Unlock() - conn.lockShards() - defer conn.unlockShards() +// SetSchema sets Schema for the connection. +func (conn *Connection) SetSchema(s Schema) { + sCopy := s.copy() + spaceAndIndexNamesSupported := + isFeatureInSlice(iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, + conn.serverProtocolInfo.Features) - conn.Schema = s + conn.mutex.Lock() + defer conn.mutex.Unlock() + conn.lockShards() + defer conn.unlockShards() + + conn.schemaResolver = &loadedSchemaResolver{ + Schema: sCopy, + SpaceAndIndexNamesSupported: spaceAndIndexNamesSupported, } } diff --git a/example_test.go b/example_test.go index 7e689bcf3..8e9cbf3a7 100644 --- a/example_test.go +++ b/example_test.go @@ -1063,7 +1063,10 @@ func ExampleSchema() { conn := exampleConnect(opts) defer conn.Close() - schema := conn.Schema + schema, err := tarantool.GetSchema(conn) + if err != nil { + fmt.Printf("unexpected error: %s\n", err.Error()) + } if schema.SpacesById == nil { fmt.Println("schema.SpacesById is nil") } @@ -1080,13 +1083,30 @@ func ExampleSchema() { // Space 2 ID 616 schematest } +// Example demonstrates how to update the connection schema. +func ExampleConnection_SetSchema() { + conn := exampleConnect(opts) + defer conn.Close() + + // Get the actual schema. + schema, err := tarantool.GetSchema(conn) + if err != nil { + fmt.Printf("unexpected error: %s\n", err.Error()) + } + // Update the current schema to match the actual one. + conn.SetSchema(schema) +} + // Example demonstrates how to retrieve information with space schema. func ExampleSpace() { conn := exampleConnect(opts) defer conn.Close() // Save Schema to a local variable to avoid races - schema := conn.Schema + schema, err := tarantool.GetSchema(conn) + if err != nil { + fmt.Printf("unexpected error: %s\n", err.Error()) + } if schema.SpacesById == nil { fmt.Println("schema.SpacesById is nil") } @@ -1120,7 +1140,7 @@ func ExampleSpace() { // Space 1 ID 617 test memtx // Space 1 ID 0 false // Index 0 primary - // &{0 unsigned} &{2 string} + // {0 unsigned} {2 string} // SpaceField 1 name0 unsigned // SpaceField 2 name3 unsigned } diff --git a/schema.go b/schema.go index f0be7a162..6066adf49 100644 --- a/schema.go +++ b/schema.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" - "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" "github.com/vmihailenco/msgpack/v5/msgpcode" ) @@ -58,9 +57,22 @@ type SchemaResolver interface { type Schema struct { Version uint // Spaces is map from space names to spaces. - Spaces map[string]*Space + Spaces map[string]Space // SpacesById is map from space numbers to spaces. - SpacesById map[uint32]*Space + SpacesById map[uint32]Space +} + +func (schema *Schema) copy() Schema { + schemaCopy := *schema + schemaCopy.Spaces = make(map[string]Space, len(schema.Spaces)) + for name, space := range schema.Spaces { + schemaCopy.Spaces[name] = space.copy() + } + schemaCopy.SpacesById = make(map[uint32]Space, len(schema.SpacesById)) + for id, space := range schema.SpacesById { + schemaCopy.SpacesById[id] = space.copy() + } + return schemaCopy } // Space contains information about Tarantool's space. @@ -72,12 +84,33 @@ type Space struct { Temporary bool // Is this space temporary? // Field configuration is not mandatory and not checked by Tarantool. FieldsCount uint32 - Fields map[string]*Field - FieldsById map[uint32]*Field + Fields map[string]Field + FieldsById map[uint32]Field // Indexes is map from index names to indexes. - Indexes map[string]*Index + Indexes map[string]Index // IndexesById is map from index numbers to indexes. - IndexesById map[uint32]*Index + IndexesById map[uint32]Index +} + +func (space *Space) copy() Space { + spaceCopy := *space + spaceCopy.Fields = make(map[string]Field, len(space.Fields)) + for name, field := range space.Fields { + spaceCopy.Fields[name] = field + } + spaceCopy.FieldsById = make(map[uint32]Field, len(space.FieldsById)) + for id, field := range space.FieldsById { + spaceCopy.FieldsById[id] = field + } + spaceCopy.Indexes = make(map[string]Index, len(space.Indexes)) + for name, index := range space.Indexes { + spaceCopy.Indexes[name] = index.copy() + } + spaceCopy.IndexesById = make(map[uint32]Index, len(space.IndexesById)) + for id, index := range space.IndexesById { + spaceCopy.IndexesById[id] = index.copy() + } + return spaceCopy } func (space *Space) DecodeMsgpack(d *msgpack.Decoder) error { @@ -135,17 +168,17 @@ func (space *Space) DecodeMsgpack(d *msgpack.Decoder) error { return errors.New("unexpected schema format (space flags)") } } - space.FieldsById = make(map[uint32]*Field) - space.Fields = make(map[string]*Field) - space.IndexesById = make(map[uint32]*Index) - space.Indexes = make(map[string]*Index) + space.FieldsById = make(map[uint32]Field) + space.Fields = make(map[string]Field) + space.IndexesById = make(map[uint32]Index) + space.Indexes = make(map[string]Index) if arrayLen >= vspaceSpFormatFieldNum { fieldCount, err := d.DecodeArrayLen() if err != nil { return err } for i := 0; i < fieldCount; i++ { - field := &Field{} + field := Field{} if err := field.DecodeMsgpack(d); err != nil { return err } @@ -206,7 +239,14 @@ type Index struct { Name string Type string Unique bool - Fields []*IndexField + Fields []IndexField +} + +func (index *Index) copy() Index { + indexCopy := *index + indexCopy.Fields = make([]IndexField, len(index.Fields)) + copy(indexCopy.Fields, index.Fields) + return indexCopy } func (index *Index) DecodeMsgpack(d *msgpack.Decoder) error { @@ -261,9 +301,9 @@ func (index *Index) DecodeMsgpack(d *msgpack.Decoder) error { if err != nil { return err } - index.Fields = make([]*IndexField, fieldCount) + index.Fields = make([]IndexField, fieldCount) for i := 0; i < int(fieldCount); i++ { - index.Fields[i] = new(IndexField) + index.Fields[i] = IndexField{} if index.Fields[i].Id, err = d.DecodeUint32(); err != nil { return err } @@ -340,16 +380,17 @@ func (indexField *IndexField) DecodeMsgpack(d *msgpack.Decoder) error { return errors.New("unexpected schema format (index fields)") } -func (conn *Connection) loadSchema() (err error) { - schema := new(Schema) - schema.SpacesById = make(map[uint32]*Space) - schema.Spaces = make(map[string]*Space) +// GetSchema returns the actual schema for the connection. +func GetSchema(conn Connector) (Schema, error) { + schema := Schema{} + schema.SpacesById = make(map[uint32]Space) + schema.Spaces = make(map[string]Space) // Reload spaces. - var spaces []*Space - err = conn.SelectTyped(vspaceSpId, 0, 0, maxSchemas, IterAll, []interface{}{}, &spaces) + var spaces []Space + err := conn.SelectTyped(vspaceSpId, 0, 0, maxSchemas, IterAll, []interface{}{}, &spaces) if err != nil { - return err + return Schema{}, err } for _, space := range spaces { schema.SpacesById[space.Id] = space @@ -357,10 +398,10 @@ func (conn *Connection) loadSchema() (err error) { } // Reload indexes. - var indexes []*Index + var indexes []Index err = conn.SelectTyped(vindexSpId, 0, 0, maxSchemas, IterAll, []interface{}{}, &indexes) if err != nil { - return err + return Schema{}, err } for _, index := range indexes { spaceId := index.SpaceId @@ -368,23 +409,11 @@ func (conn *Connection) loadSchema() (err error) { schema.SpacesById[spaceId].IndexesById[index.Id] = index schema.SpacesById[spaceId].Indexes[index.Name] = index } else { - return errors.New("concurrent schema update") + return Schema{}, errors.New("concurrent schema update") } } - spaceAndIndexNamesSupported := - isFeatureInSlice(iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, - conn.serverProtocolInfo.Features) - - conn.lockShards() - conn.Schema = schema - conn.schemaResolver = &loadedSchemaResolver{ - Schema: schema, - SpaceAndIndexNamesSupported: spaceAndIndexNamesSupported, - } - conn.unlockShards() - - return nil + return schema, nil } // resolveSpaceNumber tries to resolve a space number. @@ -462,7 +491,7 @@ func resolveIndexNumber(i interface{}) (uint32, error) { } type loadedSchemaResolver struct { - Schema *Schema + Schema Schema // SpaceAndIndexNamesSupported shows if a current Tarantool version supports // iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES. SpaceAndIndexNamesSupported bool diff --git a/tarantool_test.go b/tarantool_test.go index f8da2bdb5..f511376c2 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -1851,6 +1851,57 @@ func TestConnection_DoWithStrangerConn(t *testing.T) { } } +func TestGetSchema(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + s, err := GetSchema(conn) + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + if s.Version != 0 || s.Spaces[spaceName].Id != spaceNo { + t.Errorf("GetSchema() returns incorrect schema") + } +} + +func TestConnection_SetSchema_Changes(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + req := NewInsertRequest(spaceName) + req.Tuple([]interface{}{uint(1010), "Tarantool"}) + resp, err := conn.Do(req).Get() + if err != nil { + t.Fatalf("Failed to Insert: %s", err.Error()) + } + if resp.Code != OkCode { + t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) + } + + s, err := GetSchema(conn) + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + conn.SetSchema(s) + + // Check if changes of the SetSchema result will do nothing to the + // connection schema. + s.Spaces[spaceName] = Space{} + + reqS := NewSelectRequest(spaceName) + reqS.Key([]interface{}{uint(1010)}) + resp, err = conn.Do(reqS).Get() + if err != nil { + t.Fatalf("failed to Select: %s", err.Error()) + } + if resp.Code != OkCode { + t.Errorf("failed to Select: wrong code returned %d", resp.Code) + } + if resp.Data[0].([]interface{})[1] != "Tarantool" { + t.Errorf("wrong Select body: %v", resp.Data) + } +} + func TestNewPreparedFromResponse(t *testing.T) { var ( ErrNilResponsePassed = fmt.Errorf("passed nil response") @@ -1882,14 +1933,17 @@ func TestSchema(t *testing.T) { defer conn.Close() // Schema - schema := conn.Schema + schema, err := GetSchema(conn) + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } if schema.SpacesById == nil { t.Errorf("schema.SpacesById is nil") } if schema.Spaces == nil { t.Errorf("schema.Spaces is nil") } - var space, space2 *Space + var space, space2 Space var ok bool if space, ok = schema.SpacesById[616]; !ok { t.Errorf("space with id = 616 was not found in schema.SpacesById") @@ -1897,9 +1951,8 @@ func TestSchema(t *testing.T) { if space2, ok = schema.Spaces["schematest"]; !ok { t.Errorf("space with name 'schematest' was not found in schema.SpacesById") } - if space != space2 { - t.Errorf("space with id = 616 and space with name schematest are different") - } + assert.Equal(t, space, space2, + "space with id = 616 and space with name schematest are different") if space.Id != 616 { t.Errorf("space 616 has incorrect Id") } @@ -1929,7 +1982,7 @@ func TestSchema(t *testing.T) { t.Errorf("space.Fields len is incorrect") } - var field1, field2, field5, field1n, field5n *Field + var field1, field2, field5, field1n, field5n Field if field1, ok = space.FieldsById[1]; !ok { t.Errorf("field id = 1 was not found") } @@ -1975,7 +2028,7 @@ func TestSchema(t *testing.T) { t.Errorf("space.Indexes len is incorrect") } - var index0, index3, index0n, index3n *Index + var index0, index3, index0n, index3n Index if index0, ok = space.IndexesById[0]; !ok { t.Errorf("index id = 0 was not found") } @@ -1988,9 +2041,10 @@ func TestSchema(t *testing.T) { if index3n, ok = space.Indexes["secondary"]; !ok { t.Errorf("index name = secondary was not found") } - if index0 != index0n || index3 != index3n { - t.Errorf("index with id = 3 and index with name 'secondary' are different") - } + assert.Equal(t, index0, index0n, + "index with id = 0 and index with name 'primary' are different") + assert.Equal(t, index3, index3n, + "index with id = 3 and index with name 'secondary' are different") if index3.Id != 3 { t.Errorf("index has incorrect Id") } @@ -2012,7 +2066,7 @@ func TestSchema(t *testing.T) { ifield1 := index3.Fields[0] ifield2 := index3.Fields[1] - if ifield1 == nil || ifield2 == nil { + if (ifield1 == IndexField{}) || (ifield2 == IndexField{}) { t.Fatalf("index field is nil") } if ifield1.Id != 1 || ifield2.Id != 2 { @@ -2028,18 +2082,21 @@ func TestSchema_IsNullable(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, server, opts) defer conn.Close() - schema := conn.Schema + schema, err := GetSchema(conn) + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } if schema.Spaces == nil { t.Errorf("schema.Spaces is nil") } - var space *Space + var space Space var ok bool if space, ok = schema.SpacesById[616]; !ok { t.Errorf("space with id = 616 was not found in schema.SpacesById") } - var field, field_nullable *Field + var field, field_nullable Field for i := 0; i <= 5; i++ { name := fmt.Sprintf("name%d", i) if field, ok = space.Fields[name]; !ok { From 3dccc1c1d77578174759f5eb83c9925945e1f062 Mon Sep 17 00:00:00 2001 From: Albert Skalt Date: Tue, 7 Nov 2023 15:35:02 +0300 Subject: [PATCH 508/605] api: make dialer mandatory This patch modifies `Connect` api. Now, to connect to the Tarantool, you need to pass an object that satisfies `tarantool.Dialer` interface. You can use one of the existing implementations: `NetDialer` or `OpenSslDialer`. For example: ``` conn, err := tarantool.Connect(context.Background(), tarantool.NetDialer{ Address: "127.0.0.1:3301", User: "user", Password: "secret", }, tarantool.Opts{}) ``` To create a connection pool, you need to pass a `map[string]tarantool.Dialer`, where each dialer is associated with a unique ID (for example, it can be the server address). Dialers will be distinguished from each other using these IDs. For example: ``` connPool, err := pool.Connect(context.Background(), map[string]tarantool.Dialer{ "127.0.0.1": tarantool.NetDialer{ Address: "127.0.0.1", User: "user", Password: "secret", }, }, tarantool.Opts{}) ``` The `conn.RemoteAddr` and `conn.LocalAddr` functions have been removed. To obtain the connection address, you can use `conn.Addr`. Now, `NewWatcher` checks the actual features of the server, rather than relying on the features provided by the user during connection creation. In the case of connection pool, watchers are created for connections that support this feature. `ClientProtocolInfo`, `ServerProtocolInfo` were removed. Now, there is `ProtocolInfo`, which returns the server protocol info. `pool.GetPoolInfo` was renamed to `pool.GetInfo`. Return type changed to `map[string]ConnectionInfo`. Part of #321 --- box_error_test.go | 12 +- connection.go | 148 ++----- connection_test.go | 32 -- crud/example_test.go | 10 +- crud/tarantool_test.go | 14 +- datetime/datetime_test.go | 28 +- datetime/example_test.go | 10 +- datetime/interval_test.go | 2 +- decimal/decimal_test.go | 18 +- decimal/example_test.go | 9 +- dial.go | 254 ++++++++---- dial_test.go | 574 ++++++++++++++++++++++---- example_custom_unpacking_test.go | 13 +- example_test.go | 173 ++++---- export_test.go | 16 +- pool/connection_pool.go | 356 ++++++++-------- pool/connection_pool_test.go | 500 +++++++++++----------- pool/example_test.go | 87 ++-- pool/round_robin.go | 48 +-- pool/round_robin_test.go | 15 +- pool/watcher.go | 14 +- queue/example_connection_pool_test.go | 28 +- queue/example_msgpack_test.go | 9 +- queue/example_test.go | 9 +- queue/queue_test.go | 93 +++-- request.go | 26 +- settings/example_test.go | 16 +- settings/tarantool_test.go | 34 +- shutdown_test.go | 58 +-- ssl.go | 33 +- ssl_test.go | 367 ++++++---------- tarantool_test.go | 405 ++++++------------ test_helpers/main.go | 47 +-- test_helpers/pool_helper.go | 57 ++- test_helpers/utils.go | 10 +- uuid/example_test.go | 13 +- uuid/uuid_test.go | 14 +- 37 files changed, 1861 insertions(+), 1691 deletions(-) delete mode 100644 connection_test.go diff --git a/box_error_test.go b/box_error_test.go index 0d7d3f47b..b08b6cc52 100644 --- a/box_error_test.go +++ b/box_error_test.go @@ -299,7 +299,7 @@ func TestErrorTypeMPEncodeDecode(t *testing.T) { func TestErrorTypeEval(t *testing.T) { test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() for name, testcase := range tupleCases { @@ -318,7 +318,7 @@ func TestErrorTypeEval(t *testing.T) { func TestErrorTypeEvalTyped(t *testing.T) { test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() for name, testcase := range tupleCases { @@ -336,7 +336,7 @@ func TestErrorTypeEvalTyped(t *testing.T) { func TestErrorTypeInsert(t *testing.T) { test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() truncateEval := fmt.Sprintf("box.space[%q]:truncate()", space) @@ -374,7 +374,7 @@ func TestErrorTypeInsert(t *testing.T) { func TestErrorTypeInsertTyped(t *testing.T) { test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() truncateEval := fmt.Sprintf("box.space[%q]:truncate()", space) @@ -416,7 +416,7 @@ func TestErrorTypeInsertTyped(t *testing.T) { func TestErrorTypeSelect(t *testing.T) { test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() truncateEval := fmt.Sprintf("box.space[%q]:truncate()", space) @@ -461,7 +461,7 @@ func TestErrorTypeSelect(t *testing.T) { func TestErrorTypeSelectTyped(t *testing.T) { test_helpers.SkipIfErrorMessagePackTypeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() truncateEval := fmt.Sprintf("box.space[%q]:truncate()", space) diff --git a/connection.go b/connection.go index 48de5476b..a92a66d84 100644 --- a/connection.go +++ b/connection.go @@ -10,6 +10,7 @@ import ( "io" "log" "math" + "net" "runtime" "sync" "sync/atomic" @@ -90,15 +91,15 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac reconnects := v[0].(uint) err := v[1].(error) log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s", - reconnects, conn.opts.MaxReconnects, conn.addr, err) + reconnects, conn.opts.MaxReconnects, conn.Addr(), err) case LogLastReconnectFailed: err := v[0].(error) log.Printf("tarantool: last reconnect to %s failed: %s, giving it up", - conn.addr, err) + conn.Addr(), err) case LogUnexpectedResultId: resp := v[0].(*Response) log.Printf("tarantool: connection %s got unexpected resultId (%d) in response", - conn.addr, resp.RequestId) + conn.Addr(), resp.RequestId) case LogWatchEventReadFailed: err := v[0].(error) log.Printf("tarantool: unable to parse watch event: %s", err) @@ -156,10 +157,11 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac // More on graceful shutdown: // https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/ type Connection struct { - addr string - c Conn - mutex sync.Mutex - cond *sync.Cond + addr net.Addr + dialer Dialer + c Conn + mutex sync.Mutex + cond *sync.Cond // schemaResolver contains a SchemaResolver implementation. schemaResolver SchemaResolver // requestId contains the last request ID for requests with nil context. @@ -260,11 +262,6 @@ const ( // Opts is a way to configure Connection type Opts struct { - // Auth is an authentication method. - Auth Auth - // Dialer is a Dialer object used to create a new connection to a - // Tarantool instance. TtDialer is a default one. - Dialer Dialer // Timeout for response to a particular request. The timeout is reset when // push messages are received. If Timeout is zero, any request can be // blocked infinitely. @@ -287,10 +284,6 @@ type Opts struct { // endlessly. // After MaxReconnects attempts Connection becomes closed. MaxReconnects uint - // Username for logging in to Tarantool. - User string - // User password for logging in to Tarantool. - Pass string // RateLimit limits number of 'in-fly' request, i.e. already put into // requests queue, but not yet answered by server or timeouted. // It is disabled by default. @@ -315,83 +308,23 @@ type Opts struct { Handle interface{} // Logger is user specified logger used for error messages. Logger Logger - // Transport is the connection type, by default the connection is unencrypted. - 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. -type SslOpts struct { - // KeyFile is a path to a private SSL key file. - KeyFile string - // CertFile is a path to an SSL certificate file. - CertFile string - // CaFile is a path to a trusted certificate authorities (CA) file. - CaFile string - // Ciphers is a colon-separated (:) list of SSL cipher suites the connection - // can use. - // - // We don't provide a list of supported ciphers. This is what OpenSSL - // does. The only limitation is usage of TLSv1.2 (because other protocol - // versions don't seem to support the GOST cipher). To add additional - // ciphers (GOST cipher), you must configure OpenSSL. - // - // See also - // - // * https://www.openssl.org/docs/man1.1.1/man1/ciphers.html - Ciphers string - // Password is a password for decrypting the private SSL key file. - // The priority is as follows: try to decrypt with Password, then - // try PasswordFile. - Password string - // PasswordFile is a path to the list of passwords for decrypting - // the private SSL key file. The connection tries every line from the - // file as a password. - PasswordFile string -} - -// 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 } // Connect creates and configures a new Connection. -// -// Address could be specified in following ways: -// -// - TCP connections (tcp://192.168.1.1:3013, tcp://my.host:3013, -// tcp:192.168.1.1:3013, tcp:my.host:3013, 192.168.1.1:3013, my.host:3013) -// -// - Unix socket, first '/' or '.' indicates Unix socket -// (unix:///abs/path/tnt.sock, unix:path/tnt.sock, /abs/path/tnt.sock, -// ./rel/path/tnt.sock, unix/:path/tnt.sock) -func Connect(ctx context.Context, addr string, opts Opts) (conn *Connection, err error) { +func Connect(ctx context.Context, dialer Dialer, opts Opts) (conn *Connection, err error) { conn = &Connection{ - addr: addr, + dialer: dialer, requestId: 0, contextRequestId: 1, Greeting: &Greeting{}, control: make(chan struct{}), - opts: opts.Clone(), + opts: opts, dec: msgpack.NewDecoder(&smallBuf{}), } maxprocs := uint32(runtime.GOMAXPROCS(-1)) if conn.opts.Concurrency == 0 || conn.opts.Concurrency > maxprocs*128 { conn.opts.Concurrency = maxprocs * 4 } - if conn.opts.Dialer == nil { - conn.opts.Dialer = TtDialer{} - } if c := conn.opts.Concurrency; c&(c-1) != 0 { for i := uint(1); i < 32; i *= 2 { c |= c >> i @@ -474,30 +407,10 @@ func (conn *Connection) CloseGraceful() error { } // Addr returns a configured address of Tarantool socket. -func (conn *Connection) Addr() string { +func (conn *Connection) Addr() net.Addr { return conn.addr } -// RemoteAddr returns an address of Tarantool socket. -func (conn *Connection) RemoteAddr() string { - conn.mutex.Lock() - defer conn.mutex.Unlock() - if conn.c == nil { - return "" - } - return conn.c.RemoteAddr().String() -} - -// LocalAddr returns an address of outgoing socket. -func (conn *Connection) LocalAddr() string { - conn.mutex.Lock() - defer conn.mutex.Unlock() - if conn.c == nil { - return "" - } - return conn.c.LocalAddr().String() -} - // Handle returns a user-specified handle from Opts. func (conn *Connection) Handle() interface{} { return conn.opts.Handle @@ -514,19 +427,14 @@ func (conn *Connection) dial(ctx context.Context) error { opts := conn.opts var c Conn - c, err := conn.opts.Dialer.Dial(ctx, conn.addr, DialOpts{ - IoTimeout: opts.Timeout, - Transport: opts.Transport, - Ssl: opts.Ssl, - RequiredProtocol: opts.RequiredProtocolInfo, - Auth: opts.Auth, - User: opts.User, - Password: opts.Pass, + c, err := conn.dialer.Dial(ctx, DialOpts{ + IoTimeout: opts.Timeout, }) if err != nil { return err } + conn.addr = c.Addr() conn.Greeting.Version = c.Greeting().Version conn.serverProtocolInfo = c.ProtocolInfo() @@ -1453,8 +1361,7 @@ func isFeatureInSlice(expected iproto.Feature, actualSlice []iproto.Feature) boo // NewWatcher creates a new Watcher object for the connection. // -// You need to require IPROTO_FEATURE_WATCHERS to use watchers, see examples -// for the function. +// Server must support IPROTO_FEATURE_WATCHERS to use watchers. // // After watcher creation, the watcher callback is invoked for the first time. // In this case, the callback is triggered whether or not the key has already @@ -1490,9 +1397,9 @@ func (conn *Connection) NewWatcher(key string, callback WatchCallback) (Watcher, // That's why we can't just check the Tarantool response for an unsupported // request error. if !isFeatureInSlice(iproto.IPROTO_FEATURE_WATCHERS, - conn.opts.RequiredProtocolInfo.Features) { - err := fmt.Errorf("the feature %s must be required by connection "+ - "options to create a watcher", iproto.IPROTO_FEATURE_WATCHERS) + conn.c.ProtocolInfo().Features) { + err := fmt.Errorf("the feature %s must be supported by connection "+ + "to create a watcher", iproto.IPROTO_FEATURE_WATCHERS) return nil, err } @@ -1583,23 +1490,14 @@ func (conn *Connection) newWatcherImpl(key string, callback WatchCallback) (Watc }, nil } -// ServerProtocolVersion returns protocol version and protocol features +// ProtocolInfo 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 { +// Since 2.0.0 +func (conn *Connection) ProtocolInfo() 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 { - info := clientProtocolInfo.Clone() - info.Auth = conn.opts.Auth - return info -} - func shutdownEventCallback(event WatchEvent) { // Receives "true" on server shutdown. // See https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/ diff --git a/connection_test.go b/connection_test.go deleted file mode 100644 index 20d06b183..000000000 --- a/connection_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package tarantool_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/tarantool/go-iproto" - - . "github.com/tarantool/go-tarantool/v2" -) - -func TestOptsClonePreservesRequiredProtocolFeatures(t *testing.T) { - original := Opts{ - RequiredProtocolInfo: ProtocolInfo{ - Version: ProtocolVersion(100), - Features: []iproto.Feature{iproto.Feature(99), iproto.Feature(100)}, - }, - } - - origCopy := original.Clone() - - original.RequiredProtocolInfo.Features[1] = iproto.Feature(98) - - require.Equal(t, - origCopy, - Opts{ - RequiredProtocolInfo: ProtocolInfo{ - Version: ProtocolVersion(100), - Features: []iproto.Feature{iproto.Feature(99), iproto.Feature(100)}, - }, - }) -} diff --git a/crud/example_test.go b/crud/example_test.go index 763ab5deb..c043bd5f0 100644 --- a/crud/example_test.go +++ b/crud/example_test.go @@ -17,14 +17,18 @@ const ( var exampleOpts = tarantool.Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", +} + +var exampleDialer = tarantool.NetDialer{ + Address: exampleServer, + User: "test", + Password: "test", } func exampleConnect() *tarantool.Connection { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() - conn, err := tarantool.Connect(ctx, exampleServer, exampleOpts) + conn, err := tarantool.Connect(ctx, exampleDialer, exampleOpts) if err != nil { panic("Connection is not established: " + err.Error()) } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 9f6b5d948..dfc8d064e 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -21,17 +21,21 @@ var spaceName = "test" var invalidSpaceName = "invalid" var indexNo = uint32(0) var indexName = "primary_index" + +var dialer = tarantool.NetDialer{ + Address: server, + User: "test", + Password: "test", +} + var opts = tarantool.Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", } var startOpts test_helpers.StartOpts = test_helpers.StartOpts{ + Dialer: dialer, InitScript: "testdata/config.lua", Listen: server, - User: opts.User, - Pass: opts.Pass, WaitStart: 100 * time.Millisecond, ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, @@ -171,7 +175,7 @@ var object = crud.MapObject{ func connect(t testing.TB) *tarantool.Connection { for i := 0; i < 10; i++ { ctx, cancel := test_helpers.GetConnectContext() - conn, err := tarantool.Connect(ctx, server, opts) + conn, err := tarantool.Connect(ctx, dialer, opts) cancel() if err != nil { t.Fatalf("Failed to connect: %s", err) diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 858c7b84a..630d5f062 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -40,8 +40,11 @@ var isDatetimeSupported = false var server = "127.0.0.1:3013" var opts = Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", +} +var dialer = NetDialer{ + Address: server, + User: "test", + Password: "test", } var spaceTuple1 = "testDatetime_1" @@ -364,7 +367,7 @@ func TestDatetimeInterval(t *testing.T) { func TestDatetimeTarantoolInterval(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() dates := []string{ @@ -504,7 +507,7 @@ func TestInvalidOffset(t *testing.T) { t.Fatalf("Unexpected success: %v", dt) } if testcase.ok && isDatetimeSupported { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() tupleInsertSelectDelete(t, conn, tm) @@ -516,7 +519,7 @@ func TestInvalidOffset(t *testing.T) { func TestCustomTimezone(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() customZone := "Europe/Moscow" @@ -676,7 +679,7 @@ var datetimeSample = []struct { func TestDatetimeInsertSelectDelete(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() for _, testcase := range datetimeSample { @@ -705,7 +708,7 @@ func TestDatetimeInsertSelectDelete(t *testing.T) { func TestDatetimeBoundaryRange(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() for _, tm := range append(lesserBoundaryTimes, boundaryTimes...) { @@ -731,7 +734,7 @@ func TestDatetimeOutOfRange(t *testing.T) { func TestDatetimeReplace(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() tm, err := time.Parse(time.RFC3339, "2007-01-02T15:04:05Z") @@ -896,7 +899,7 @@ func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { func TestCustomEncodeDecodeTuple1(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() tm1, _ := time.Parse(time.RFC3339, "2010-05-24T17:51:56.000000009Z") @@ -966,7 +969,7 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { func TestCustomDecodeFunction(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Call function 'call_datetime_testdata' returning a custom tuples. @@ -1010,7 +1013,7 @@ func TestCustomDecodeFunction(t *testing.T) { func TestCustomEncodeDecodeTuple5(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() tm := time.Unix(500, 1000).In(time.FixedZone(NoTimezone, 0)) @@ -1166,10 +1169,9 @@ func runTestMain(m *testing.M) int { } instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + Dialer: dialer, InitScript: "config.lua", Listen: server, - User: opts.User, - Pass: opts.Pass, WaitStart: 100 * time.Millisecond, ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, diff --git a/datetime/example_test.go b/datetime/example_test.go index 954f43548..72d3448c2 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -20,13 +20,15 @@ import ( // Example demonstrates how to use tuples with datetime. To enable support of // datetime import tarantool/datetime package. func Example() { - opts := tarantool.Opts{ - User: "test", - Pass: "test", + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", } + opts := tarantool.Opts{} ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() - conn, err := tarantool.Connect(ctx, "127.0.0.1:3013", opts) + conn, err := tarantool.Connect(ctx, dialer, opts) if err != nil { fmt.Printf("Error in connect is %v", err) return diff --git a/datetime/interval_test.go b/datetime/interval_test.go index ae42e92c7..4e3cf5ab1 100644 --- a/datetime/interval_test.go +++ b/datetime/interval_test.go @@ -105,7 +105,7 @@ func TestIntervalSub(t *testing.T) { func TestIntervalTarantoolEncoding(t *testing.T) { skipIfDatetimeUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() cases := []Interval{ diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 57c70d1d7..14ce05b2a 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -20,10 +20,13 @@ import ( var isDecimalSupported = false var server = "127.0.0.1:3013" +var dialer = NetDialer{ + Address: server, + User: "test", + Password: "test", +} var opts = Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", } func skipIfDecimalUnsupported(t *testing.T) { @@ -526,7 +529,7 @@ func BenchmarkDecodeStringFromBCD(b *testing.B) { func TestSelect(t *testing.T) { skipIfDecimalUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() number, err := decimal.NewFromString("-12.34") @@ -572,7 +575,7 @@ func TestSelect(t *testing.T) { func TestUnmarshal_from_decimal_new(t *testing.T) { skipIfDecimalUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() samples := correctnessSamples @@ -627,7 +630,7 @@ func assertInsert(t *testing.T, conn *Connection, numString string) { func TestInsert(t *testing.T) { skipIfDecimalUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() samples := correctnessSamples @@ -642,7 +645,7 @@ func TestInsert(t *testing.T) { func TestReplace(t *testing.T) { skipIfDecimalUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() number, err := decimal.NewFromString("-12.34") @@ -695,10 +698,9 @@ func runTestMain(m *testing.M) int { } instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + Dialer: dialer, InitScript: "config.lua", Listen: server, - User: opts.User, - Pass: opts.Pass, WaitStart: 100 * time.Millisecond, ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, diff --git a/decimal/example_test.go b/decimal/example_test.go index f1984283c..3f7d4de06 100644 --- a/decimal/example_test.go +++ b/decimal/example_test.go @@ -22,13 +22,16 @@ import ( // import tarantool/decimal submodule. func Example() { server := "127.0.0.1:3013" + dialer := tarantool.NetDialer{ + Address: server, + User: "test", + Password: "test", + } opts := tarantool.Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", } ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - client, err := tarantool.Connect(ctx, server, opts) + client, err := tarantool.Connect(ctx, dialer, opts) cancel() if err != nil { log.Fatalf("Failed to connect: %s", err.Error()) diff --git a/dial.go b/dial.go index 5b17c0534..6b75adafa 100644 --- a/dial.go +++ b/dial.go @@ -15,10 +15,7 @@ import ( "github.com/vmihailenco/msgpack/v5" ) -const ( - dialTransportNone = "" - dialTransportSsl = "ssl" -) +const bufSize = 128 * 1024 // Greeting is a message sent by Tarantool on connect. type Greeting struct { @@ -45,47 +42,31 @@ type Conn interface { // Any blocked Read or Flush operations will be unblocked and return // errors. Close() error - // LocalAddr returns the local network address, if known. - LocalAddr() net.Addr - // RemoteAddr returns the remote network address, if known. - RemoteAddr() net.Addr // Greeting returns server greeting. Greeting() Greeting // ProtocolInfo returns server protocol info. ProtocolInfo() ProtocolInfo + // Addr returns the connection address. + Addr() net.Addr } // DialOpts is a way to configure a Dial method to create a new Conn. type DialOpts struct { // IoTimeout is a timeout per a network read/write. IoTimeout time.Duration - // Transport is a connect transport type. - Transport string - // Ssl configures "ssl" transport. - Ssl SslOpts - // RequiredProtocol contains minimal protocol version and - // list of protocol features that should be supported by - // Tarantool server. By default there are no restrictions. - RequiredProtocol ProtocolInfo - // Auth is an authentication method. - Auth Auth - // Username for logging in to Tarantool. - User string - // User password for logging in to Tarantool. - Password string } // Dialer is the interface that wraps a method to connect to a Tarantool // instance. The main idea is to provide a ready-to-work connection with // basic preparation, successful authorization and additional checks. // -// You can provide your own implementation to Connect() call via Opts.Dialer if -// some functionality is not implemented in the connector. See TtDialer.Dial() +// You can provide your own implementation to Connect() call if +// some functionality is not implemented in the connector. See NetDialer.Dial() // implementation as example. type Dialer interface { // Dial connects to a Tarantool instance to the address with specified // options. - Dial(ctx context.Context, address string, opts DialOpts) (Conn, error) + Dial(ctx context.Context, opts DialOpts) (Conn, error) } type tntConn struct { @@ -96,61 +77,186 @@ type tntConn struct { protocol ProtocolInfo } -// TtDialer is a default implementation of the Dialer interface which is -// used by the connector. -type TtDialer struct { +// rawDial does basic dial operations: +// reads greeting, identifies a protocol and validates it. +func rawDial(conn *tntConn, requiredProto ProtocolInfo) (string, error) { + version, salt, err := readGreeting(conn.reader) + if err != nil { + return "", fmt.Errorf("failed to read greeting: %w", err) + } + conn.greeting.Version = version + + if conn.protocol, err = identify(conn.writer, conn.reader); err != nil { + return "", fmt.Errorf("failed to identify: %w", err) + } + + if err = checkProtocolInfo(requiredProto, conn.protocol); err != nil { + return "", fmt.Errorf("invalid server protocol: %w", err) + } + return salt, err +} + +// NetDialer is a basic Dialer implementation. +type NetDialer struct { + // Address is an address to connect. + // It could be specified in following ways: + // + // - TCP connections (tcp://192.168.1.1:3013, tcp://my.host:3013, + // tcp:192.168.1.1:3013, tcp:my.host:3013, 192.168.1.1:3013, my.host:3013) + // + // - Unix socket, first '/' or '.' indicates Unix socket + // (unix:///abs/path/tnt.sock, unix:path/tnt.sock, /abs/path/tnt.sock, + // ./rel/path/tnt.sock, unix/:path/tnt.sock) + Address string + // Username for logging in to Tarantool. + User string + // User password for logging in to Tarantool. + Password string + // RequiredProtocol contains minimal protocol version and + // list of protocol features that should be supported by + // Tarantool server. By default, there are no restrictions. + RequiredProtocolInfo ProtocolInfo } -// Dial connects to a Tarantool instance to the address with specified -// options. -func (t TtDialer) Dial(ctx context.Context, address string, opts DialOpts) (Conn, error) { +// Dial makes NetDialer satisfy the Dialer interface. +func (d NetDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { var err error conn := new(tntConn) - if conn.net, err = dial(ctx, address, opts); err != nil { + network, address := parseAddress(d.Address) + dialer := net.Dialer{} + conn.net, err = dialer.DialContext(ctx, network, address) + if err != nil { return nil, fmt.Errorf("failed to dial: %w", err) } dc := &deadlineIO{to: opts.IoTimeout, c: conn.net} - conn.reader = bufio.NewReaderSize(dc, 128*1024) - conn.writer = bufio.NewWriterSize(dc, 128*1024) + conn.reader = bufio.NewReaderSize(dc, bufSize) + conn.writer = bufio.NewWriterSize(dc, bufSize) - var version, salt string - if version, salt, err = readGreeting(conn.reader); err != nil { + salt, err := rawDial(conn, d.RequiredProtocolInfo) + if err != nil { conn.net.Close() - return nil, fmt.Errorf("failed to read greeting: %w", err) + return nil, err } - conn.greeting.Version = version - if conn.protocol, err = identify(conn.writer, conn.reader); err != nil { + if d.User == "" { + return conn, nil + } + + conn.protocol.Auth = ChapSha1Auth + if err = authenticate(conn, ChapSha1Auth, d.User, d.Password, salt); err != nil { conn.net.Close() - return nil, fmt.Errorf("failed to identify: %w", err) + return nil, fmt.Errorf("failed to authenticate: %w", err) + } + + return conn, nil +} + +// OpenSslDialer allows to use SSL transport for connection. +type OpenSslDialer struct { + // Address is an address to connect. + // It could be specified in following ways: + // + // - TCP connections (tcp://192.168.1.1:3013, tcp://my.host:3013, + // tcp:192.168.1.1:3013, tcp:my.host:3013, 192.168.1.1:3013, my.host:3013) + // + // - Unix socket, first '/' or '.' indicates Unix socket + // (unix:///abs/path/tnt.sock, unix:path/tnt.sock, /abs/path/tnt.sock, + // ./rel/path/tnt.sock, unix/:path/tnt.sock) + Address string + // Auth is an authentication method. + Auth Auth + // Username for logging in to Tarantool. + User string + // User password for logging in to Tarantool. + Password string + // RequiredProtocol contains minimal protocol version and + // list of protocol features that should be supported by + // Tarantool server. By default, there are no restrictions. + RequiredProtocolInfo ProtocolInfo + // SslKeyFile is a path to a private SSL key file. + SslKeyFile string + // SslCertFile is a path to an SSL certificate file. + SslCertFile string + // SslCaFile is a path to a trusted certificate authorities (CA) file. + SslCaFile string + // SslCiphers is a colon-separated (:) list of SSL cipher suites the connection + // can use. + // + // We don't provide a list of supported ciphers. This is what OpenSSL + // does. The only limitation is usage of TLSv1.2 (because other protocol + // versions don't seem to support the GOST cipher). To add additional + // ciphers (GOST cipher), you must configure OpenSSL. + // + // See also + // + // * https://www.openssl.org/docs/man1.1.1/man1/ciphers.html + SslCiphers string + // SslPassword is a password for decrypting the private SSL key file. + // The priority is as follows: try to decrypt with SslPassword, then + // try SslPasswordFile. + SslPassword string + // SslPasswordFile is a path to the list of passwords for decrypting + // the private SSL key file. The connection tries every line from the + // file as a password. + SslPasswordFile string +} + +// Dial makes OpenSslDialer satisfy the Dialer interface. +func (d OpenSslDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { + var err error + conn := new(tntConn) + + network, address := parseAddress(d.Address) + conn.net, err = sslDialContext(ctx, network, address, sslOpts{ + KeyFile: d.SslKeyFile, + CertFile: d.SslCertFile, + CaFile: d.SslCaFile, + Ciphers: d.SslCiphers, + Password: d.SslPassword, + PasswordFile: d.SslPasswordFile, + }) + if err != nil { + return nil, fmt.Errorf("failed to dial: %w", err) } - if err = checkProtocolInfo(opts.RequiredProtocol, conn.protocol); err != nil { + dc := &deadlineIO{to: opts.IoTimeout, c: conn.net} + conn.reader = bufio.NewReaderSize(dc, bufSize) + conn.writer = bufio.NewWriterSize(dc, bufSize) + + salt, err := rawDial(conn, d.RequiredProtocolInfo) + if err != nil { conn.net.Close() - return nil, fmt.Errorf("invalid server protocol: %w", err) + return nil, err } - if opts.User != "" { - if opts.Auth == AutoAuth { - if conn.protocol.Auth != AutoAuth { - opts.Auth = conn.protocol.Auth - } else { - opts.Auth = ChapSha1Auth - } - } + if d.User == "" { + return conn, nil + } - err := authenticate(conn, opts, salt) - if err != nil { - conn.net.Close() - return nil, fmt.Errorf("failed to authenticate: %w", err) + if d.Auth == AutoAuth { + if conn.protocol.Auth != AutoAuth { + d.Auth = conn.protocol.Auth + } else { + d.Auth = ChapSha1Auth } } + conn.protocol.Auth = d.Auth + + if err = authenticate(conn, d.Auth, d.User, d.Password, salt); err != nil { + conn.net.Close() + return nil, fmt.Errorf("failed to authenticate: %w", err) + } return conn, nil } +// Addr makes tntConn satisfy the Conn interface. +func (c *tntConn) Addr() net.Addr { + return c.net.RemoteAddr() +} + // Read makes tntConn satisfy the Conn interface. func (c *tntConn) Read(p []byte) (int, error) { return c.reader.Read(p) @@ -177,16 +283,6 @@ func (c *tntConn) Close() error { return c.net.Close() } -// RemoteAddr makes tntConn satisfy the Conn interface. -func (c *tntConn) RemoteAddr() net.Addr { - return c.net.RemoteAddr() -} - -// LocalAddr makes tntConn satisfy the Conn interface. -func (c *tntConn) LocalAddr() net.Addr { - return c.net.LocalAddr() -} - // Greeting makes tntConn satisfy the Conn interface. func (c *tntConn) Greeting() Greeting { return c.greeting @@ -197,20 +293,6 @@ func (c *tntConn) ProtocolInfo() ProtocolInfo { return c.protocol } -// dial connects to a Tarantool instance. -func dial(ctx context.Context, address string, opts DialOpts) (net.Conn, error) { - network, address := parseAddress(address) - switch opts.Transport { - case dialTransportNone: - dialer := net.Dialer{} - return dialer.DialContext(ctx, network, address) - case dialTransportSsl: - return sslDialContext(ctx, network, address, opts.Ssl) - default: - return nil, fmt.Errorf("unsupported transport type: %s", opts.Transport) - } -} - // parseAddress split address into network and address parts. func parseAddress(address string) (string, string) { network := "tcp" @@ -316,29 +398,21 @@ func checkProtocolInfo(required ProtocolInfo, actual ProtocolInfo) error { } } -// authenticate authenticate for a connection. -func authenticate(c Conn, opts DialOpts, salt string) error { - auth := opts.Auth - user := opts.User - pass := opts.Password - +// authenticate authenticates for a connection. +func authenticate(c Conn, auth Auth, user string, pass string, salt string) error { var req Request var err error - switch opts.Auth { + switch auth { case ChapSha1Auth: req, err = newChapSha1AuthRequest(user, pass, salt) if err != nil { return err } case PapSha256Auth: - if opts.Transport != dialTransportSsl { - return errors.New("forbidden to use " + auth.String() + - " unless SSL is enabled for the connection") - } req = newPapSha256AuthRequest(user, pass) default: - return errors.New("unsupported method " + opts.Auth.String()) + return errors.New("unsupported method " + auth.String()) } if err = writeRequest(c, req); err != nil { diff --git a/dial_test.go b/dial_test.go index 209fc50cd..c8cf1c778 100644 --- a/dial_test.go +++ b/dial_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" + "github.com/tarantool/go-openssl" "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/test_helpers" @@ -23,7 +24,7 @@ type mockErrorDialer struct { err error } -func (m mockErrorDialer) Dial(ctx context.Context, address string, +func (m mockErrorDialer) Dial(ctx context.Context, opts tarantool.DialOpts) (tarantool.Conn, error) { return nil, m.err } @@ -36,22 +37,18 @@ func TestDialer_Dial_error(t *testing.T) { ctx, cancel := test_helpers.GetConnectContext() defer cancel() - conn, err := tarantool.Connect(ctx, "any", tarantool.Opts{ - Dialer: dialer, - }) + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) assert.Nil(t, conn) assert.ErrorContains(t, err, errMsg) } type mockPassedDialer struct { - ctx context.Context - address string - opts tarantool.DialOpts + ctx context.Context + opts tarantool.DialOpts } -func (m *mockPassedDialer) Dial(ctx context.Context, address string, +func (m *mockPassedDialer) Dial(ctx context.Context, opts tarantool.DialOpts) (tarantool.Conn, error) { - m.address = address m.opts = opts if ctx != m.ctx { return nil, errors.New("wrong context") @@ -60,53 +57,30 @@ func (m *mockPassedDialer) Dial(ctx context.Context, address string, } func TestDialer_Dial_passedOpts(t *testing.T) { - const addr = "127.0.0.1:8080" - opts := tarantool.DialOpts{ - IoTimeout: 2, - Transport: "any", - Ssl: tarantool.SslOpts{ - KeyFile: "a", - CertFile: "b", - CaFile: "c", - Ciphers: "d", - }, - RequiredProtocol: tarantool.ProtocolInfo{ - Auth: tarantool.ChapSha1Auth, - Version: 33, - Features: []iproto.Feature{ - iproto.IPROTO_FEATURE_ERROR_EXTENSION, - }, - }, - Auth: tarantool.ChapSha1Auth, - User: "user", - Password: "password", - } - dialer := &mockPassedDialer{} + ctx, cancel := test_helpers.GetConnectContext() defer cancel() - dialer.ctx = ctx - conn, err := tarantool.Connect(ctx, addr, tarantool.Opts{ - Dialer: dialer, - Timeout: opts.IoTimeout, - Transport: opts.Transport, - Ssl: opts.Ssl, - Auth: opts.Auth, - User: opts.User, - Pass: opts.Password, - RequiredProtocolInfo: opts.RequiredProtocol, + dialOpts := tarantool.DialOpts{ + IoTimeout: opts.Timeout, + } + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{ + Timeout: opts.Timeout, }) assert.Nil(t, conn) assert.NotNil(t, err) assert.NotEqual(t, err.Error(), "wrong context") - assert.Equal(t, addr, dialer.address) - assert.Equal(t, opts, dialer.opts) + assert.Equal(t, dialOpts, dialer.opts) } type mockIoConn struct { + addr net.Addr + // Addr calls count. + addrCnt int // Sends an event on Read()/Write()/Flush(). read, written chan struct{} // Read()/Write() buffers. @@ -117,12 +91,8 @@ type mockIoConn struct { readWgDelay, writeWgDelay int // Write()/Read()/Flush()/Close() calls count. writeCnt, readCnt, flushCnt, closeCnt int - // LocalAddr()/RemoteAddr() calls count. - localCnt, remoteCnt int // Greeting()/ProtocolInfo() calls count. greetingCnt, infoCnt int - // Values for LocalAddr()/RemoteAddr(). - local, remote net.Addr // Value for Greeting(). greeting tarantool.Greeting // Value for ProtocolInfo(). @@ -171,16 +141,6 @@ func (m *mockIoConn) Close() error { return nil } -func (m *mockIoConn) LocalAddr() net.Addr { - m.localCnt++ - return m.local -} - -func (m *mockIoConn) RemoteAddr() net.Addr { - m.remoteCnt++ - return m.remote -} - func (m *mockIoConn) Greeting() tarantool.Greeting { m.greetingCnt++ return m.greeting @@ -191,6 +151,11 @@ func (m *mockIoConn) ProtocolInfo() tarantool.ProtocolInfo { return m.info } +func (m *mockIoConn) Addr() net.Addr { + m.addrCnt++ + return m.addr +} + type mockIoDialer struct { init func(conn *mockIoConn) conn *mockIoConn @@ -203,8 +168,7 @@ func newMockIoConn() *mockIoConn { return conn } -func (m *mockIoDialer) Dial(ctx context.Context, address string, - opts tarantool.DialOpts) (tarantool.Conn, error) { +func (m *mockIoDialer) Dial(ctx context.Context, opts tarantool.DialOpts) (tarantool.Conn, error) { m.conn = newMockIoConn() if m.init != nil { m.init(m.conn) @@ -221,9 +185,8 @@ func dialIo(t *testing.T, } ctx, cancel := test_helpers.GetConnectContext() defer cancel() - conn, err := tarantool.Connect(ctx, "any", + conn, err := tarantool.Connect(ctx, &dialer, tarantool.Opts{ - Dialer: &dialer, Timeout: 1000 * time.Second, // Avoid pings. SkipSchema: true, }) @@ -244,7 +207,6 @@ func TestConn_Close(t *testing.T) { } type stubAddr struct { - net.Addr str string } @@ -252,25 +214,14 @@ func (a stubAddr) String() string { return a.str } -func TestConn_LocalAddr(t *testing.T) { - const addr = "any" - conn, dialer := dialIo(t, func(conn *mockIoConn) { - conn.local = stubAddr{str: addr} - }) - defer func() { - dialer.conn.readWg.Done() - dialer.conn.writeWg.Done() - conn.Close() - }() - - assert.Equal(t, addr, conn.LocalAddr()) - assert.Equal(t, 1, dialer.conn.localCnt) +func (a stubAddr) Network() string { + return "stub" } -func TestConn_RemoteAddr(t *testing.T) { +func TestConn_Addr(t *testing.T) { const addr = "any" conn, dialer := dialIo(t, func(conn *mockIoConn) { - conn.remote = stubAddr{str: addr} + conn.addr = stubAddr{str: addr} }) defer func() { dialer.conn.readWg.Done() @@ -278,8 +229,8 @@ func TestConn_RemoteAddr(t *testing.T) { conn.Close() }() - assert.Equal(t, addr, conn.RemoteAddr()) - assert.Equal(t, 1, dialer.conn.remoteCnt) + assert.Equal(t, addr, conn.Addr().String()) + assert.Equal(t, 1, dialer.conn.addrCnt) } func TestConn_Greeting(t *testing.T) { @@ -316,7 +267,7 @@ func TestConn_ProtocolInfo(t *testing.T) { conn.Close() }() - assert.Equal(t, info, conn.ServerProtocolInfo()) + assert.Equal(t, info, conn.ProtocolInfo()) assert.Equal(t, 1, dialer.conn.infoCnt) } @@ -359,13 +310,11 @@ func TestConn_ReadWrite(t *testing.T) { } func TestConn_ContextCancel(t *testing.T) { - const addr = "127.0.0.1:8080" - - dialer := tarantool.TtDialer{} + dialer := tarantool.NetDialer{Address: "127.0.0.1:8080"} ctx, cancel := context.WithCancel(context.Background()) cancel() - conn, err := dialer.Dial(ctx, addr, tarantool.DialOpts{}) + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) assert.Nil(t, conn) assert.NotNil(t, err) @@ -373,3 +322,460 @@ func TestConn_ContextCancel(t *testing.T) { fmt.Sprintf("unexpected error, expected to contain %s, got %v", "operation was canceled", err)) } + +func genSalt() [64]byte { + salt := [64]byte{} + for i := 0; i < 44; i++ { + salt[i] = 'a' + } + return salt +} + +var ( + testDialUser = "test" + testDialPass = "test" + testDialVersion = [64]byte{'t', 'e', 's', 't'} + + // Salt with end zeros. + testDialSalt = genSalt() + + idRequestExpected = []byte{ + 0xce, 0x00, 0x00, 0x00, 29, // Length. + 0x82, // Header map. + 0x00, 0x49, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x00, + + 0x82, // Data map. + 0x54, + 0xcf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, // Version. + 0x55, + 0x97, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // Features. + } + + idResponseTyped = tarantool.ProtocolInfo{ + Version: 6, + Features: []iproto.Feature{iproto.Feature(1), iproto.Feature(21)}, + Auth: tarantool.ChapSha1Auth, + } + + idResponse = []byte{ + 0xce, 0x00, 0x00, 0x00, 37, // Length. + 0x83, // Header map. + 0x00, 0xce, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x00, + 0x05, 0xce, 0x00, 0x00, 0x00, 0x61, + + 0x83, // Data map. + 0x54, + 0x06, // Version. + 0x55, + 0x92, 0x01, 0x15, // Features. + 0x5b, + 0xa9, 'c', 'h', 'a', 'p', '-', 's', 'h', 'a', '1', + } + + idResponseNotSupported = []byte{ + 0xce, 0x00, 0x00, 0x00, 25, // Length. + 0x83, // Header map. + 0x00, 0xce, 0x00, 0x00, 0x80, 0x30, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x00, + 0x05, 0xce, 0x00, 0x00, 0x00, 0x61, + 0x81, + 0x31, + 0xa3, 'e', 'r', 'r', + } + + authRequestExpectedChapSha1 = []byte{ + 0xce, 0x00, 0x00, 0x00, 57, // Length. + 0x82, // Header map. + 0x00, 0x07, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x00, + + 0x82, // Data map. + 0xce, 0x00, 0x00, 0x00, 0x23, + 0xa4, 't', 'e', 's', 't', // Login. + 0xce, 0x00, 0x00, 0x00, 0x21, + 0x92, // Tuple. + 0xa9, 'c', 'h', 'a', 'p', '-', 's', 'h', 'a', '1', + + // Scramble. + 0xb4, 0x1b, 0xd4, 0x20, 0x45, 0x73, 0x22, + 0xcf, 0xab, 0x05, 0x03, 0xf3, 0x89, 0x4b, + 0xfe, 0xc7, 0x24, 0x5a, 0xe6, 0xe8, 0x31, + } + + authRequestExpectedPapSha256 = []byte{ + 0xce, 0x00, 0x00, 0x00, 0x2a, // Length. + 0x82, // Header map. + 0x00, 0x07, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x00, + + 0x82, // Data map. + 0xce, 0x00, 0x00, 0x00, 0x23, + 0xa4, 't', 'e', 's', 't', // Login. + 0xce, 0x00, 0x00, 0x00, 0x21, + 0x92, // Tuple. + 0xaa, 'p', 'a', 'p', '-', 's', 'h', 'a', '2', '5', '6', + 0xa4, 't', 'e', 's', 't', + } + + okResponse = []byte{ + 0xce, 0x00, 0x00, 0x00, 19, // Length. + 0x83, // Header map. + 0x00, 0xce, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x00, + 0x05, 0xce, 0x00, 0x00, 0x00, 0x61, + } + + errResponse = []byte{0xce} +) + +type testDialOpts struct { + name string + wantErr bool + expectedErr string + expectedProtocolInfo tarantool.ProtocolInfo + + // These options configure the behavior of the server. + isErrGreeting bool + isErrId bool + isIdUnsupported bool + isPapSha256Auth bool + isErrAuth bool +} + +type dialServerActual struct { + IdRequest []byte + AuthRequest []byte +} + +func testDialAccept(opts testDialOpts, l net.Listener) chan dialServerActual { + ch := make(chan dialServerActual, 1) + + go func() { + client, err := l.Accept() + if err != nil { + return + } + defer client.Close() + if opts.isErrGreeting { + client.Write(errResponse) + return + } else { + // Write greeting. + client.Write(testDialVersion[:]) + client.Write(testDialSalt[:]) + } + + // Read Id request. + idRequestActual := make([]byte, len(idRequestExpected)) + client.Read(idRequestActual) + + // Make Id response. + if opts.isErrId { + client.Write(errResponse) + } else if opts.isIdUnsupported { + client.Write(idResponseNotSupported) + } else { + client.Write(idResponse) + } + + // Read Auth request. + authRequestExpected := authRequestExpectedChapSha1 + if opts.isPapSha256Auth { + authRequestExpected = authRequestExpectedPapSha256 + } + authRequestActual := make([]byte, len(authRequestExpected)) + client.Read(authRequestActual) + + // Make Auth response. + if opts.isErrAuth { + client.Write(errResponse) + } else { + client.Write(okResponse) + } + ch <- dialServerActual{ + IdRequest: idRequestActual, + AuthRequest: authRequestActual, + } + }() + + return ch +} + +func testDialer(t *testing.T, l net.Listener, dialer tarantool.Dialer, + opts testDialOpts) { + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + ch := testDialAccept(opts, l) + conn, err := dialer.Dial(ctx, tarantool.DialOpts{ + IoTimeout: time.Second * 2, + }) + if opts.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), opts.expectedErr) + return + } + require.NoError(t, err) + require.Equal(t, opts.expectedProtocolInfo, conn.ProtocolInfo()) + require.Equal(t, testDialVersion[:], []byte(conn.Greeting().Version)) + + actual := <-ch + require.Equal(t, idRequestExpected, actual.IdRequest) + + authRequestExpected := authRequestExpectedChapSha1 + if opts.isPapSha256Auth { + authRequestExpected = authRequestExpectedPapSha256 + } + require.Equal(t, authRequestExpected, actual.AuthRequest) + conn.Close() +} + +func TestNetDialer_Dial(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer l.Close() + dialer := tarantool.NetDialer{ + Address: l.Addr().String(), + User: testDialUser, + Password: testDialPass, + } + cases := []testDialOpts{ + { + name: "all is ok", + expectedProtocolInfo: idResponseTyped.Clone(), + }, + { + name: "id request unsupported", + // Dialer sets auth. + expectedProtocolInfo: tarantool.ProtocolInfo{Auth: tarantool.ChapSha1Auth}, + isIdUnsupported: true, + }, + { + name: "greeting response error", + wantErr: true, + expectedErr: "failed to read greeting", + isErrGreeting: true, + }, + { + name: "id response error", + wantErr: true, + expectedErr: "failed to identify", + isErrId: true, + }, + { + name: "auth response error", + wantErr: true, + expectedErr: "failed to authenticate", + isErrAuth: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + testDialer(t, l, dialer, tc) + }) + } +} + +func TestNetDialer_Dial_requirements(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer l.Close() + dialer := tarantool.NetDialer{ + Address: l.Addr().String(), + User: testDialUser, + Password: testDialPass, + RequiredProtocolInfo: tarantool.ProtocolInfo{ + Features: []iproto.Feature{42}, + }, + } + testDialAccept(testDialOpts{}, l) + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + if err == nil { + conn.Close() + } + require.Error(t, err) + require.Contains(t, err.Error(), "invalid server protocol") +} + +func createSslListener(t *testing.T, opts tarantool.SslTestOpts) net.Listener { + ctx, err := tarantool.SslCreateContext(opts) + require.NoError(t, err) + l, err := openssl.Listen("tcp", "127.0.0.1:0", ctx.(*openssl.Ctx)) + require.NoError(t, err) + return l +} + +func TestOpenSslDialer_Dial_basic(t *testing.T) { + l := createSslListener(t, tarantool.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }) + + defer l.Close() + addr := l.Addr().String() + + dialer := tarantool.OpenSslDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + } + + cases := []testDialOpts{ + { + name: "all is ok", + expectedProtocolInfo: idResponseTyped.Clone(), + }, + { + name: "id request unsupported", + // Dialer sets auth. + expectedProtocolInfo: tarantool.ProtocolInfo{Auth: tarantool.ChapSha1Auth}, + isIdUnsupported: true, + }, + { + name: "greeting response error", + wantErr: true, + expectedErr: "failed to read greeting", + isErrGreeting: true, + }, + { + name: "id response error", + wantErr: true, + expectedErr: "failed to identify", + isErrId: true, + }, + { + name: "auth response error", + wantErr: true, + expectedErr: "failed to authenticate", + isErrAuth: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + testDialer(t, l, dialer, tc) + }) + } +} + +func TestOpenSslDialer_Dial_requirements(t *testing.T) { + l := createSslListener(t, tarantool.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }) + + defer l.Close() + addr := l.Addr().String() + + dialer := tarantool.OpenSslDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + RequiredProtocolInfo: tarantool.ProtocolInfo{ + Features: []iproto.Feature{42}, + }, + } + + testDialAccept(testDialOpts{}, l) + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + if err == nil { + conn.Close() + } + require.Error(t, err) + require.Contains(t, err.Error(), "invalid server protocol") +} + +func TestOpenSslDialer_Dial_papSha256Auth(t *testing.T) { + l := createSslListener(t, tarantool.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }) + + defer l.Close() + addr := l.Addr().String() + + dialer := tarantool.OpenSslDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + Auth: tarantool.PapSha256Auth, + } + + protocol := idResponseTyped.Clone() + protocol.Auth = tarantool.PapSha256Auth + + testDialer(t, l, dialer, testDialOpts{ + expectedProtocolInfo: protocol, + isPapSha256Auth: true, + }) +} + +func TestOpenSslDialer_Dial_opts(t *testing.T) { + for _, test := range sslTests { + t.Run(test.name, func(t *testing.T) { + l := createSslListener(t, test.serverOpts) + defer l.Close() + addr := l.Addr().String() + + dialer := tarantool.OpenSslDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + SslKeyFile: test.clientOpts.KeyFile, + SslCertFile: test.clientOpts.CertFile, + SslCaFile: test.clientOpts.CaFile, + SslCiphers: test.clientOpts.Ciphers, + SslPassword: test.clientOpts.Password, + SslPasswordFile: test.clientOpts.PasswordFile, + } + testDialer(t, l, dialer, testDialOpts{ + wantErr: !test.ok, + expectedProtocolInfo: idResponseTyped.Clone(), + }) + }) + } +} + +func TestOpenSslDialer_Dial_ctx_cancel(t *testing.T) { + serverOpts := tarantool.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + } + clientOpts := tarantool.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + } + + l := createSslListener(t, serverOpts) + defer l.Close() + addr := l.Addr().String() + testDialAccept(testDialOpts{}, l) + + dialer := tarantool.OpenSslDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + SslKeyFile: clientOpts.KeyFile, + SslCertFile: clientOpts.CertFile, + SslCaFile: clientOpts.CaFile, + SslCiphers: clientOpts.Ciphers, + SslPassword: clientOpts.Password, + SslPasswordFile: clientOpts.PasswordFile, + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + if err == nil { + conn.Close() + } + require.Error(t, err) +} diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index 8fb243db0..316e0086f 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -77,14 +77,15 @@ func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { // a custom packer/unpacker, but it will work slower. func Example_customUnpacking() { // Establish a connection. - server := "127.0.0.1:3013" - opts := tarantool.Opts{ - Timeout: 500 * time.Millisecond, - User: "test", - Pass: "test", + + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", } + opts := tarantool.Opts{} ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - conn, err := tarantool.Connect(ctx, server, opts) + conn, err := tarantool.Connect(ctx, dialer, opts) cancel() if err != nil { log.Fatalf("Failed to connect: %s", err.Error()) diff --git a/example_test.go b/example_test.go index 8e9cbf3a7..00cd00467 100644 --- a/example_test.go +++ b/example_test.go @@ -20,10 +20,10 @@ type Tuple struct { Name string } -func exampleConnect(opts tarantool.Opts) *tarantool.Connection { +func exampleConnect(dialer tarantool.Dialer, opts tarantool.Opts) *tarantool.Connection { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() - conn, err := tarantool.Connect(ctx, server, opts) + conn, err := tarantool.Connect(ctx, dialer, opts) if err != nil { panic("Connection is not established: " + err.Error()) } @@ -31,27 +31,25 @@ func exampleConnect(opts tarantool.Opts) *tarantool.Connection { } // Example demonstrates how to use SSL transport. -func ExampleSslOpts() { - var opts = tarantool.Opts{ - User: "test", - Pass: "test", - Transport: "ssl", - Ssl: tarantool.SslOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, +func ExampleOpenSslDialer() { + sslDialer := tarantool.OpenSslDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + SslKeyFile: "testdata/localhost.key", + SslCertFile: "testdata/localhost.crt", + SslCaFile: "testdata/ca.crt", } ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() - _, err := tarantool.Connect(ctx, "127.0.0.1:3013", opts) + _, err := tarantool.Connect(ctx, sslDialer, opts) if err != nil { panic("Connection is not established: " + err.Error()) } } func ExampleIntKey() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() const space = "test" @@ -73,7 +71,7 @@ func ExampleIntKey() { } func ExampleUintKey() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() const space = "test" @@ -95,7 +93,7 @@ func ExampleUintKey() { } func ExampleStringKey() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() const space = "teststring" @@ -120,7 +118,7 @@ func ExampleStringKey() { } func ExampleIntIntKey() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() const space = "testintint" @@ -146,7 +144,7 @@ func ExampleIntIntKey() { } func ExamplePingRequest() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() // Ping a Tarantool instance to check connection. @@ -166,7 +164,7 @@ func ExamplePingRequest() { // of the request. For those purposes use context.WithTimeout() as // the root context. func ExamplePingRequest_Context() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() timeout := time.Nanosecond @@ -191,7 +189,7 @@ func ExamplePingRequest_Context() { } func ExampleSelectRequest() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() for i := 1111; i <= 1112; i++ { @@ -232,7 +230,7 @@ func ExampleSelectRequest() { } func ExampleSelectRequest_spaceAndIndexNames() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() req := tarantool.NewSelectRequest(spaceName) @@ -247,7 +245,7 @@ func ExampleSelectRequest_spaceAndIndexNames() { } func ExampleInsertRequest() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() // Insert a new tuple { 31, 1 }. @@ -289,7 +287,7 @@ func ExampleInsertRequest() { } func ExampleInsertRequest_spaceAndIndexNames() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() req := tarantool.NewInsertRequest(spaceName) @@ -303,7 +301,7 @@ func ExampleInsertRequest_spaceAndIndexNames() { } func ExampleDeleteRequest() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() // Insert a new tuple { 35, 1 }. @@ -346,7 +344,7 @@ func ExampleDeleteRequest() { } func ExampleDeleteRequest_spaceAndIndexNames() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() req := tarantool.NewDeleteRequest(spaceName) @@ -361,7 +359,7 @@ func ExampleDeleteRequest_spaceAndIndexNames() { } func ExampleReplaceRequest() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() // Insert a new tuple { 13, 1 }. @@ -420,7 +418,7 @@ func ExampleReplaceRequest() { } func ExampleReplaceRequest_spaceAndIndexNames() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() req := tarantool.NewReplaceRequest(spaceName) @@ -434,7 +432,7 @@ func ExampleReplaceRequest_spaceAndIndexNames() { } func ExampleUpdateRequest() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() for i := 1111; i <= 1112; i++ { @@ -465,7 +463,7 @@ func ExampleUpdateRequest() { } func ExampleUpdateRequest_spaceAndIndexNames() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() req := tarantool.NewUpdateRequest(spaceName) @@ -480,7 +478,7 @@ func ExampleUpdateRequest_spaceAndIndexNames() { } func ExampleUpsertRequest() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() var req tarantool.Request @@ -521,7 +519,7 @@ func ExampleUpsertRequest() { } func ExampleUpsertRequest_spaceAndIndexNames() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() req := tarantool.NewUpsertRequest(spaceName) @@ -535,7 +533,7 @@ func ExampleUpsertRequest_spaceAndIndexNames() { } func ExampleCallRequest() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() // Call a function 'simple_concat' with arguments. @@ -554,7 +552,7 @@ func ExampleCallRequest() { } func ExampleEvalRequest() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() // Run raw Lua code. @@ -582,7 +580,7 @@ func ExampleExecuteRequest() { return } - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() req := tarantool.NewExecuteRequest( @@ -700,31 +698,11 @@ func ExampleExecuteRequest() { fmt.Println("SQL Info", resp.SQLInfo) } -func ExampleProtocolVersion() { - conn := exampleConnect(opts) - defer conn.Close() +func getTestTxnDialer() tarantool.Dialer { + txnDialer := dialer - clientProtocolInfo := conn.ClientProtocolInfo() - fmt.Println("Connector client protocol version:", clientProtocolInfo.Version) - for _, f := range clientProtocolInfo.Features { - fmt.Println("Connector client protocol feature:", f) - } - // Output: - // Connector client protocol version: 6 - // Connector client protocol feature: IPROTO_FEATURE_STREAMS - // Connector client protocol feature: IPROTO_FEATURE_TRANSACTIONS - // Connector client protocol feature: IPROTO_FEATURE_ERROR_EXTENSION - // Connector client protocol feature: IPROTO_FEATURE_WATCHERS - // Connector client protocol feature: IPROTO_FEATURE_PAGINATION - // Connector client protocol feature: IPROTO_FEATURE_SPACE_AND_INDEX_NAMES - // Connector client protocol feature: IPROTO_FEATURE_WATCH_ONCE -} - -func getTestTxnOpts() tarantool.Opts { - txnOpts := opts.Clone() - - // Assert that server supports expected protocol features - txnOpts.RequiredProtocolInfo = tarantool.ProtocolInfo{ + // Assert that server supports expected protocol features. + txnDialer.RequiredProtocolInfo = tarantool.ProtocolInfo{ Version: tarantool.ProtocolVersion(1), Features: []iproto.Feature{ iproto.IPROTO_FEATURE_STREAMS, @@ -732,7 +710,7 @@ func getTestTxnOpts() tarantool.Opts { }, } - return txnOpts + return txnDialer } func ExampleCommitRequest() { @@ -746,8 +724,8 @@ func ExampleCommitRequest() { return } - txnOpts := getTestTxnOpts() - conn := exampleConnect(txnOpts) + txnDialer := getTestTxnDialer() + conn := exampleConnect(txnDialer, opts) defer conn.Close() stream, _ := conn.NewStream() @@ -823,8 +801,8 @@ func ExampleRollbackRequest() { return } - txnOpts := getTestTxnOpts() - conn := exampleConnect(txnOpts) + txnDialer := getTestTxnDialer() + conn := exampleConnect(txnDialer, opts) defer conn.Close() stream, _ := conn.NewStream() @@ -900,8 +878,8 @@ func ExampleBeginRequest_TxnIsolation() { return } - txnOpts := getTestTxnOpts() - conn := exampleConnect(txnOpts) + txnDialer := getTestTxnDialer() + conn := exampleConnect(txnDialer, opts) defer conn.Close() stream, _ := conn.NewStream() @@ -969,7 +947,7 @@ func ExampleBeginRequest_TxnIsolation() { } func ExampleFuture_GetIterator() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() const timeout = 3 * time.Second @@ -1006,10 +984,14 @@ func ExampleConnect() { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() - conn, err := tarantool.Connect(ctx, "127.0.0.1:3013", tarantool.Opts{ + dialer := tarantool.NetDialer{ + Address: server, + User: "test", + Password: "test", + } + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", Concurrency: 32, }) if err != nil { @@ -1025,10 +1007,14 @@ func ExampleConnect() { } func ExampleConnect_reconnects() { + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + } + opts := tarantool.Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", Concurrency: 32, Reconnect: time.Second, MaxReconnects: 10, @@ -1039,7 +1025,7 @@ func ExampleConnect_reconnects() { for i := uint(0); i < opts.MaxReconnects; i++ { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - conn, err = tarantool.Connect(ctx, "127.0.0.1:3013", opts) + conn, err = tarantool.Connect(ctx, dialer, opts) cancel() if err == nil { break @@ -1060,7 +1046,7 @@ func ExampleConnect_reconnects() { // Example demonstrates how to retrieve information with space schema. func ExampleSchema() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() schema, err := tarantool.GetSchema(conn) @@ -1085,7 +1071,7 @@ func ExampleSchema() { // Example demonstrates how to update the connection schema. func ExampleConnection_SetSchema() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() // Get the actual schema. @@ -1099,7 +1085,7 @@ func ExampleConnection_SetSchema() { // Example demonstrates how to retrieve information with space schema. func ExampleSpace() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() // Save Schema to a local variable to avoid races @@ -1148,7 +1134,7 @@ func ExampleSpace() { // ExampleConnection_Do demonstrates how to send a request and process // a response. func ExampleConnection_Do() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() // It could be any request. @@ -1175,7 +1161,7 @@ func ExampleConnection_Do() { // ExampleConnection_Do_failure demonstrates how to send a request and process // failure. func ExampleConnection_Do_failure() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() // It could be any request. @@ -1222,15 +1208,17 @@ func ExampleConnection_NewPrepared() { return } - server := "127.0.0.1:3013" + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + } opts := tarantool.Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", } ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() - conn, err := tarantool.Connect(ctx, server, opts) + conn, err := tarantool.Connect(ctx, dialer, opts) if err != nil { fmt.Printf("Failed to connect: %s", err.Error()) } @@ -1264,21 +1252,24 @@ func ExampleConnection_NewWatcher() { return } - server := "127.0.0.1:3013" + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + // You can require the feature explicitly. + RequiredProtocolInfo: tarantool.ProtocolInfo{ + Features: []iproto.Feature{iproto.IPROTO_FEATURE_WATCHERS}, + }, + } + opts := tarantool.Opts{ Timeout: 5 * time.Second, Reconnect: 5 * time.Second, MaxReconnects: 3, - User: "test", - Pass: "test", - // You need to require the feature to create a watcher. - RequiredProtocolInfo: tarantool.ProtocolInfo{ - Features: []iproto.Feature{iproto.IPROTO_FEATURE_WATCHERS}, - }, } ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() - conn, err := tarantool.Connect(ctx, server, opts) + conn, err := tarantool.Connect(ctx, dialer, opts) if err != nil { fmt.Printf("Failed to connect: %s\n", err) return @@ -1304,7 +1295,7 @@ func ExampleConnection_NewWatcher() { // ExampleConnection_CloseGraceful_force demonstrates how to force close // a connection with graceful close in progress after a while. func ExampleConnection_CloseGraceful_force() { - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) eval := `local fiber = require('fiber') local time = ... @@ -1347,7 +1338,7 @@ func ExampleWatchOnceRequest() { return } - conn := exampleConnect(opts) + conn := exampleConnect(dialer, opts) defer conn.Close() conn.Do(tarantool.NewBroadcastRequest(key).Value(value)).Get() diff --git a/export_test.go b/export_test.go index 2c28472b9..e92ce9bd9 100644 --- a/export_test.go +++ b/export_test.go @@ -1,20 +1,22 @@ package tarantool import ( - "context" - "net" "time" "github.com/vmihailenco/msgpack/v5" ) -func SslDialContext(ctx context.Context, network, address string, - opts SslOpts) (connection net.Conn, err error) { - return sslDialContext(ctx, network, address, opts) +type SslTestOpts struct { + KeyFile string + CertFile string + CaFile string + Ciphers string + Password string + PasswordFile string } -func SslCreateContext(opts SslOpts) (ctx interface{}, err error) { - return sslCreateContext(opts) +func SslCreateContext(opts SslTestOpts) (ctx interface{}, err error) { + return sslCreateContext(sslOpts(opts)) } // RefImplPingBody is reference implementation for filling of a ping diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 6186683b3..c272310bd 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -23,7 +23,7 @@ import ( ) var ( - ErrEmptyAddrs = errors.New("addrs (first argument) should not be empty") + ErrEmptyDialers = errors.New("dialers (second argument) should not be empty") ErrWrongCheckTimeout = errors.New("wrong check timeout, must be greater than 0") ErrNoConnection = errors.New("no active connections") ErrTooManyArgs = errors.New("too many arguments") @@ -50,7 +50,7 @@ type ConnectionHandler interface { // The client code may cancel adding a connection to the pool. The client // need to return an error from the Discovered call for that. In this case // the pool will close connection and will try to reopen it later. - Discovered(conn *tarantool.Connection, role Role) error + Discovered(id string, conn *tarantool.Connection, role Role) error // Deactivated is called when a connection with a role has become // unavaileble to send requests. It happens if the connection is closed or // the connection role is switched. @@ -61,7 +61,7 @@ type ConnectionHandler interface { // Deactivated will not be called if a previous Discovered() call returns // an error. Because in this case, the connection does not become available // for sending requests. - Deactivated(conn *tarantool.Connection, role Role) error + Deactivated(id string, conn *tarantool.Connection, role Role) error } // Opts provides additional options (configurable via ConnectWithOpts). @@ -94,8 +94,8 @@ Main features: - Automatic master discovery by mode parameter. */ type ConnectionPool struct { - addrs map[string]*endpoint - addrsMutex sync.RWMutex + ends map[string]*endpoint + endsMutex sync.RWMutex connOpts tarantool.Opts opts Opts @@ -112,7 +112,8 @@ type ConnectionPool struct { var _ Pooler = (*ConnectionPool)(nil) type endpoint struct { - addr string + id string + dialer tarantool.Dialer notify chan tarantool.ConnEvent conn *tarantool.Connection role Role @@ -124,9 +125,10 @@ type endpoint struct { closeErr error } -func newEndpoint(addr string) *endpoint { +func newEndpoint(id string, dialer tarantool.Dialer) *endpoint { return &endpoint{ - addr: addr, + id: id, + dialer: dialer, notify: make(chan tarantool.ConnEvent, 100), conn: nil, role: UnknownRole, @@ -137,25 +139,25 @@ func newEndpoint(addr string) *endpoint { } } -// ConnectWithOpts creates pool for instances with addresses addrs -// with options opts. -func ConnectWithOpts(ctx context.Context, addrs []string, +// ConnectWithOpts creates pool for instances with specified dialers and options opts. +// Each dialer corresponds to a certain id by which they will be distinguished. +func ConnectWithOpts(ctx context.Context, dialers map[string]tarantool.Dialer, connOpts tarantool.Opts, opts Opts) (*ConnectionPool, error) { - if len(addrs) == 0 { - return nil, ErrEmptyAddrs + if len(dialers) == 0 { + return nil, ErrEmptyDialers } if opts.CheckTimeout <= 0 { return nil, ErrWrongCheckTimeout } - size := len(addrs) + size := len(dialers) rwPool := newRoundRobinStrategy(size) roPool := newRoundRobinStrategy(size) anyPool := newRoundRobinStrategy(size) connPool := &ConnectionPool{ - addrs: make(map[string]*endpoint), - connOpts: connOpts.Clone(), + ends: make(map[string]*endpoint), + connOpts: connOpts, opts: opts, state: unknownState, done: make(chan struct{}), @@ -164,11 +166,7 @@ func ConnectWithOpts(ctx context.Context, addrs []string, anyPool: anyPool, } - for _, addr := range addrs { - connPool.addrs[addr] = nil - } - - somebodyAlive, ctxCanceled := connPool.fillPools(ctx) + somebodyAlive, ctxCanceled := connPool.fillPools(ctx, dialers) if !somebodyAlive { connPool.state.set(closedState) if ctxCanceled { @@ -179,7 +177,7 @@ func ConnectWithOpts(ctx context.Context, addrs []string, connPool.state.set(connectedState) - for _, s := range connPool.addrs { + for _, s := range connPool.ends { endpointCtx, cancel := context.WithCancel(context.Background()) s.cancel = cancel go connPool.controller(endpointCtx, s) @@ -188,17 +186,18 @@ func ConnectWithOpts(ctx context.Context, addrs []string, return connPool, nil } -// ConnectWithOpts creates pool for instances with addresses addrs. +// Connect creates pool for instances with specified dialers. +// Each dialer corresponds to a certain id by which they will be distinguished. // // It is useless to set up tarantool.Opts.Reconnect value for a connection. // The connection pool has its own reconnection logic. See // Opts.CheckTimeout description. -func Connect(ctx context.Context, addrs []string, +func Connect(ctx context.Context, dialers map[string]tarantool.Dialer, connOpts tarantool.Opts) (*ConnectionPool, error) { opts := Opts{ CheckTimeout: 1 * time.Second, } - return ConnectWithOpts(ctx, addrs, connOpts, opts) + return ConnectWithOpts(ctx, dialers, connOpts, opts) } // ConnectedNow gets connected status of pool. @@ -235,32 +234,32 @@ func (p *ConnectionPool) ConfiguredTimeout(mode Mode) (time.Duration, error) { return conn.ConfiguredTimeout(), nil } -// Add adds a new endpoint with the address into the pool. This function +// Add adds a new endpoint with the id into the pool. This function // adds the endpoint only after successful connection. -func (p *ConnectionPool) Add(ctx context.Context, addr string) error { - e := newEndpoint(addr) +func (p *ConnectionPool) Add(ctx context.Context, id string, dialer tarantool.Dialer) error { + e := newEndpoint(id, dialer) - p.addrsMutex.Lock() + p.endsMutex.Lock() // Ensure that Close()/CloseGraceful() not in progress/done. if p.state.get() != connectedState { - p.addrsMutex.Unlock() + p.endsMutex.Unlock() return ErrClosed } - if _, ok := p.addrs[addr]; ok { - p.addrsMutex.Unlock() + if _, ok := p.ends[id]; ok { + p.endsMutex.Unlock() return ErrExists } endpointCtx, cancel := context.WithCancel(context.Background()) e.cancel = cancel - p.addrs[addr] = e - p.addrsMutex.Unlock() + p.ends[id] = e + p.endsMutex.Unlock() if err := p.tryConnect(ctx, e); err != nil { - p.addrsMutex.Lock() - delete(p.addrs, addr) - p.addrsMutex.Unlock() + p.endsMutex.Lock() + delete(p.ends, id) + p.endsMutex.Unlock() e.cancel() close(e.closed) return err @@ -270,13 +269,13 @@ func (p *ConnectionPool) Add(ctx context.Context, addr string) error { return nil } -// Remove removes an endpoint with the address from the pool. The call +// Remove removes an endpoint with the id from the pool. The call // closes an active connection gracefully. -func (p *ConnectionPool) Remove(addr string) error { - p.addrsMutex.Lock() - endpoint, ok := p.addrs[addr] +func (p *ConnectionPool) Remove(id string) error { + p.endsMutex.Lock() + endpoint, ok := p.ends[id] if !ok { - p.addrsMutex.Unlock() + p.endsMutex.Unlock() return errors.New("endpoint not exist") } @@ -290,20 +289,20 @@ func (p *ConnectionPool) Remove(addr string) error { close(endpoint.shutdown) } - delete(p.addrs, addr) - p.addrsMutex.Unlock() + delete(p.ends, id) + p.endsMutex.Unlock() <-endpoint.closed return nil } func (p *ConnectionPool) waitClose() []error { - p.addrsMutex.RLock() - endpoints := make([]*endpoint, 0, len(p.addrs)) - for _, e := range p.addrs { + p.endsMutex.RLock() + endpoints := make([]*endpoint, 0, len(p.ends)) + for _, e := range p.ends { endpoints = append(endpoints, e) } - p.addrsMutex.RUnlock() + p.endsMutex.RUnlock() errs := make([]error, 0, len(endpoints)) for _, e := range endpoints { @@ -319,12 +318,12 @@ func (p *ConnectionPool) waitClose() []error { func (p *ConnectionPool) Close() []error { if p.state.cas(connectedState, closedState) || p.state.cas(shutdownState, closedState) { - p.addrsMutex.RLock() - for _, s := range p.addrs { + p.endsMutex.RLock() + for _, s := range p.ends { s.cancel() close(s.close) } - p.addrsMutex.RUnlock() + p.endsMutex.RUnlock() } return p.waitClose() @@ -334,39 +333,23 @@ func (p *ConnectionPool) Close() []error { // for all requests to complete. func (p *ConnectionPool) CloseGraceful() []error { if p.state.cas(connectedState, shutdownState) { - p.addrsMutex.RLock() - for _, s := range p.addrs { + p.endsMutex.RLock() + for _, s := range p.ends { s.cancel() close(s.shutdown) } - p.addrsMutex.RUnlock() + p.endsMutex.RUnlock() } return p.waitClose() } -// GetAddrs gets addresses of connections in pool. -func (p *ConnectionPool) GetAddrs() []string { - p.addrsMutex.RLock() - defer p.addrsMutex.RUnlock() - - cpy := make([]string, len(p.addrs)) - - i := 0 - for addr := range p.addrs { - cpy[i] = addr - i++ - } +// GetInfo gets information of connections (connected status, ro/rw role). +func (p *ConnectionPool) GetInfo() map[string]ConnectionInfo { + info := make(map[string]ConnectionInfo) - return cpy -} - -// GetPoolInfo gets information of connections (connected status, ro/rw role). -func (p *ConnectionPool) GetPoolInfo() map[string]*ConnectionInfo { - info := make(map[string]*ConnectionInfo) - - p.addrsMutex.RLock() - defer p.addrsMutex.RUnlock() + p.endsMutex.RLock() + defer p.endsMutex.RUnlock() p.poolsMutex.RLock() defer p.poolsMutex.RUnlock() @@ -374,10 +357,12 @@ func (p *ConnectionPool) GetPoolInfo() map[string]*ConnectionInfo { return info } - for addr := range p.addrs { - conn, role := p.getConnectionFromPool(addr) + for id := range p.ends { + conn, role := p.getConnectionFromPool(id) if conn != nil { - info[addr] = &ConnectionInfo{ConnectedNow: conn.ConnectedNow(), ConnRole: role} + info[id] = ConnectionInfo{ConnectedNow: conn.ConnectedNow(), ConnRole: role} + } else { + info[id] = ConnectionInfo{ConnectedNow: false, ConnRole: UnknownRole} } } @@ -912,12 +897,10 @@ func (p *ConnectionPool) NewPrepared(expr string, userMode Mode) (*tarantool.Pre } // NewWatcher creates a new Watcher object for the connection pool. -// -// You need to require IPROTO_FEATURE_WATCHERS to use watchers, see examples -// for the function. +// A watcher could be created only for instances with the support. // // The behavior is same as if Connection.NewWatcher() called for each -// connection with a suitable role. +// connection with a suitable mode. // // Keep in mind that garbage collection of a watcher handle doesn’t lead to the // watcher’s destruction. In this case, the watcher remains registered. You @@ -932,24 +915,13 @@ func (p *ConnectionPool) NewPrepared(expr string, userMode Mode) (*tarantool.Pre // Since 1.10.0 func (p *ConnectionPool) NewWatcher(key string, callback tarantool.WatchCallback, mode Mode) (tarantool.Watcher, error) { - watchersRequired := false - for _, feature := range p.connOpts.RequiredProtocolInfo.Features { - if iproto.IPROTO_FEATURE_WATCHERS == feature { - watchersRequired = true - break - } - } - if !watchersRequired { - return nil, errors.New("the feature IPROTO_FEATURE_WATCHERS must " + - "be required by connection options to create a watcher") - } watcher := &poolWatcher{ container: &p.watcherContainer, mode: mode, key: key, callback: callback, - watchers: make(map[string]tarantool.Watcher), + watchers: make(map[*tarantool.Connection]tarantool.Watcher), unregistered: false, } @@ -964,6 +936,10 @@ func (p *ConnectionPool) NewWatcher(key string, conns := rr.GetConnections() for _, conn := range conns { + // Check that connection supports watchers. + if !isFeatureInSlice(iproto.IPROTO_FEATURE_WATCHERS, conn.ProtocolInfo().Features) { + continue + } if err := watcher.watch(conn); err != nil { conn.Close() } @@ -977,8 +953,16 @@ func (p *ConnectionPool) NewWatcher(key string, // the argument of type Mode is unused. func (p *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarantool.Future { if connectedReq, ok := req.(tarantool.ConnectedRequest); ok { - conn, _ := p.getConnectionFromPool(connectedReq.Conn().Addr()) - if conn == nil { + conns := p.anyPool.GetConnections() + isOurConnection := false + for _, conn := range conns { + // Compare raw pointers. + if conn == connectedReq.Conn() { + isOurConnection = true + break + } + } + if !isOurConnection { return newErrorFuture(ErrUnknownRequest) } return connectedReq.Conn().Do(req) @@ -1030,22 +1014,22 @@ func (p *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (Role, er return UnknownRole, nil } -func (p *ConnectionPool) getConnectionFromPool(addr string) (*tarantool.Connection, Role) { - if conn := p.rwPool.GetConnByAddr(addr); conn != nil { +func (p *ConnectionPool) getConnectionFromPool(id string) (*tarantool.Connection, Role) { + if conn := p.rwPool.GetConnById(id); conn != nil { return conn, MasterRole } - if conn := p.roPool.GetConnByAddr(addr); conn != nil { + if conn := p.roPool.GetConnById(id); conn != nil { return conn, ReplicaRole } - return p.anyPool.GetConnByAddr(addr), UnknownRole + return p.anyPool.GetConnById(id), UnknownRole } -func (p *ConnectionPool) deleteConnection(addr string) { - if conn := p.anyPool.DeleteConnByAddr(addr); conn != nil { - if conn := p.rwPool.DeleteConnByAddr(addr); conn == nil { - p.roPool.DeleteConnByAddr(addr) +func (p *ConnectionPool) deleteConnection(id string) { + if conn := p.anyPool.DeleteConnById(id); conn != nil { + if conn := p.rwPool.DeleteConnById(id); conn == nil { + p.roPool.DeleteConnById(id) } // The internal connection deinitialization. p.watcherContainer.mutex.RLock() @@ -1058,109 +1042,108 @@ func (p *ConnectionPool) deleteConnection(addr string) { } } -func (p *ConnectionPool) addConnection(addr string, +func (p *ConnectionPool) addConnection(id string, conn *tarantool.Connection, role Role) error { // The internal connection initialization. p.watcherContainer.mutex.RLock() defer p.watcherContainer.mutex.RUnlock() - watched := []*poolWatcher{} - err := p.watcherContainer.foreach(func(watcher *poolWatcher) error { - watch := false - switch watcher.mode { - case RW: - watch = role == MasterRole - case RO: - watch = role == ReplicaRole - default: - watch = true - } - if watch { - if err := watcher.watch(conn); err != nil { - return err + if isFeatureInSlice(iproto.IPROTO_FEATURE_WATCHERS, conn.ProtocolInfo().Features) { + watched := []*poolWatcher{} + err := p.watcherContainer.foreach(func(watcher *poolWatcher) error { + watch := false + switch watcher.mode { + case RW: + watch = role == MasterRole + case RO: + watch = role == ReplicaRole + default: + watch = true } - watched = append(watched, watcher) - } - return nil - }) - if err != nil { - for _, watcher := range watched { - watcher.unwatch(conn) + if watch { + if err := watcher.watch(conn); err != nil { + return err + } + watched = append(watched, watcher) + } + return nil + }) + if err != nil { + for _, watcher := range watched { + watcher.unwatch(conn) + } + log.Printf("tarantool: failed initialize watchers for %s: %s", id, err) + return err } - log.Printf("tarantool: failed initialize watchers for %s: %s", addr, err) - return err } - p.anyPool.AddConn(addr, conn) + p.anyPool.AddConn(id, conn) switch role { case MasterRole: - p.rwPool.AddConn(addr, conn) + p.rwPool.AddConn(id, conn) case ReplicaRole: - p.roPool.AddConn(addr, conn) + p.roPool.AddConn(id, conn) } return nil } -func (p *ConnectionPool) handlerDiscovered(conn *tarantool.Connection, +func (p *ConnectionPool) handlerDiscovered(id string, conn *tarantool.Connection, role Role) bool { var err error if p.opts.ConnectionHandler != nil { - err = p.opts.ConnectionHandler.Discovered(conn, role) + err = p.opts.ConnectionHandler.Discovered(id, conn, role) } if err != nil { - addr := conn.Addr() - log.Printf("tarantool: storing connection to %s canceled: %s\n", addr, err) + log.Printf("tarantool: storing connection to %s canceled: %s\n", id, err) return false } return true } -func (p *ConnectionPool) handlerDeactivated(conn *tarantool.Connection, +func (p *ConnectionPool) handlerDeactivated(id string, conn *tarantool.Connection, role Role) { var err error if p.opts.ConnectionHandler != nil { - err = p.opts.ConnectionHandler.Deactivated(conn, role) + err = p.opts.ConnectionHandler.Deactivated(id, conn, role) } if err != nil { - addr := conn.Addr() - log.Printf("tarantool: deactivating connection to %s by user failed: %s\n", addr, err) + log.Printf("tarantool: deactivating connection to %s by user failed: %s\n", id, err) } } -func (p *ConnectionPool) deactivateConnection(addr string, - conn *tarantool.Connection, role Role) { - p.deleteConnection(addr) +func (p *ConnectionPool) deactivateConnection(id string, conn *tarantool.Connection, role Role) { + p.deleteConnection(id) conn.Close() - p.handlerDeactivated(conn, role) + p.handlerDeactivated(id, conn, role) } func (p *ConnectionPool) deactivateConnections() { - for address, endpoint := range p.addrs { + for id, endpoint := range p.ends { if endpoint != nil && endpoint.conn != nil { - p.deactivateConnection(address, endpoint.conn, endpoint.role) + p.deactivateConnection(id, endpoint.conn, endpoint.role) } } } func (p *ConnectionPool) processConnection(conn *tarantool.Connection, - addr string, end *endpoint) bool { + id string, end *endpoint) bool { role, err := p.getConnectionRole(conn) if err != nil { conn.Close() - log.Printf("tarantool: storing connection to %s failed: %s\n", addr, err) + log.Printf("tarantool: storing connection to %s failed: %s\n", id, err) return false } - if !p.handlerDiscovered(conn, role) { + if !p.handlerDiscovered(id, conn, role) { conn.Close() return false } - if p.addConnection(addr, conn, role) != nil { + if p.addConnection(id, conn, role) != nil { conn.Close() - p.handlerDeactivated(conn, role) + p.handlerDeactivated(id, conn, role) return false } @@ -1169,26 +1152,27 @@ func (p *ConnectionPool) processConnection(conn *tarantool.Connection, return true } -func (p *ConnectionPool) fillPools(ctx context.Context) (bool, bool) { +func (p *ConnectionPool) fillPools( + ctx context.Context, + dialers map[string]tarantool.Dialer) (bool, bool) { somebodyAlive := false ctxCanceled := false - // It is called before controller() goroutines so we don't expect + // It is called before controller() goroutines, so we don't expect // concurrency issues here. - for addr := range p.addrs { - end := newEndpoint(addr) - p.addrs[addr] = end - + for id, dialer := range dialers { + end := newEndpoint(id, dialer) + p.ends[id] = end connOpts := p.connOpts connOpts.Notify = end.notify - conn, err := tarantool.Connect(ctx, addr, connOpts) + conn, err := tarantool.Connect(ctx, dialer, connOpts) if err != nil { - log.Printf("tarantool: connect to %s failed: %s\n", addr, err.Error()) + log.Printf("tarantool: connect to %s failed: %s\n", id, err.Error()) select { case <-ctx.Done(): ctxCanceled = true - p.addrs[addr] = nil + p.ends[id] = nil log.Printf("tarantool: operation was canceled") p.deactivateConnections() @@ -1196,7 +1180,7 @@ func (p *ConnectionPool) fillPools(ctx context.Context) (bool, bool) { return false, ctxCanceled default: } - } else if p.processConnection(conn, addr, end) { + } else if p.processConnection(conn, id, end) { somebodyAlive = true } } @@ -1214,11 +1198,11 @@ func (p *ConnectionPool) updateConnection(e *endpoint) { if role, err := p.getConnectionRole(e.conn); err == nil { if e.role != role { - p.deleteConnection(e.addr) + p.deleteConnection(e.id) p.poolsMutex.Unlock() - p.handlerDeactivated(e.conn, e.role) - opened := p.handlerDiscovered(e.conn, role) + p.handlerDeactivated(e.id, e.conn, e.role) + opened := p.handlerDiscovered(e.id, e.conn, role) if !opened { e.conn.Close() e.conn = nil @@ -1231,17 +1215,17 @@ func (p *ConnectionPool) updateConnection(e *endpoint) { p.poolsMutex.Unlock() e.conn.Close() - p.handlerDeactivated(e.conn, role) + p.handlerDeactivated(e.id, e.conn, role) e.conn = nil e.role = UnknownRole return } - if p.addConnection(e.addr, e.conn, role) != nil { + if p.addConnection(e.id, e.conn, role) != nil { p.poolsMutex.Unlock() e.conn.Close() - p.handlerDeactivated(e.conn, role) + p.handlerDeactivated(e.id, e.conn, role) e.conn = nil e.role = UnknownRole return @@ -1251,11 +1235,11 @@ func (p *ConnectionPool) updateConnection(e *endpoint) { p.poolsMutex.Unlock() return } else { - p.deleteConnection(e.addr) + p.deleteConnection(e.id) p.poolsMutex.Unlock() e.conn.Close() - p.handlerDeactivated(e.conn, e.role) + p.handlerDeactivated(e.id, e.conn, e.role) e.conn = nil e.role = UnknownRole return @@ -1275,18 +1259,19 @@ func (p *ConnectionPool) tryConnect(ctx context.Context, e *endpoint) error { connOpts := p.connOpts connOpts.Notify = e.notify - conn, err := tarantool.Connect(ctx, e.addr, connOpts) + conn, err := tarantool.Connect(ctx, e.dialer, connOpts) if err == nil { role, err := p.getConnectionRole(conn) p.poolsMutex.Unlock() if err != nil { conn.Close() - log.Printf("tarantool: storing connection to %s failed: %s\n", e.addr, err) + log.Printf("tarantool: storing connection to %s failed: %s\n", + e.id, err) return err } - opened := p.handlerDiscovered(conn, role) + opened := p.handlerDiscovered(e.id, conn, role) if !opened { conn.Close() return errors.New("storing connection canceled") @@ -1296,14 +1281,14 @@ func (p *ConnectionPool) tryConnect(ctx context.Context, e *endpoint) error { if p.state.get() != connectedState { p.poolsMutex.Unlock() conn.Close() - p.handlerDeactivated(conn, role) + p.handlerDeactivated(e.id, conn, role) return ErrClosed } - if err = p.addConnection(e.addr, conn, role); err != nil { + if err = p.addConnection(e.id, conn, role); err != nil { p.poolsMutex.Unlock() conn.Close() - p.handlerDeactivated(conn, role) + p.handlerDeactivated(e.id, conn, role) return err } e.conn = conn @@ -1322,10 +1307,10 @@ func (p *ConnectionPool) reconnect(ctx context.Context, e *endpoint) { return } - p.deleteConnection(e.addr) + p.deleteConnection(e.id) p.poolsMutex.Unlock() - p.handlerDeactivated(e.conn, e.role) + p.handlerDeactivated(e.id, e.conn, e.role) e.conn = nil e.role = UnknownRole @@ -1358,12 +1343,12 @@ func (p *ConnectionPool) controller(ctx context.Context, e *endpoint) { case <-e.close: if e.conn != nil { p.poolsMutex.Lock() - p.deleteConnection(e.addr) + p.deleteConnection(e.id) p.poolsMutex.Unlock() if !shutdown { e.closeErr = e.conn.Close() - p.handlerDeactivated(e.conn, e.role) + p.handlerDeactivated(e.id, e.conn, e.role) close(e.closed) } else { // Force close the connection. @@ -1380,7 +1365,7 @@ func (p *ConnectionPool) controller(ctx context.Context, e *endpoint) { shutdown = true if e.conn != nil { p.poolsMutex.Lock() - p.deleteConnection(e.addr) + p.deleteConnection(e.id) p.poolsMutex.Unlock() // We need to catch s.close in the current goroutine, so @@ -1402,9 +1387,9 @@ func (p *ConnectionPool) controller(ctx context.Context, e *endpoint) { if e.conn != nil && e.conn.ClosedNow() { p.poolsMutex.Lock() if p.state.get() == connectedState { - p.deleteConnection(e.addr) + p.deleteConnection(e.id) p.poolsMutex.Unlock() - p.handlerDeactivated(e.conn, e.role) + p.handlerDeactivated(e.id, e.conn, e.role) e.conn = nil e.role = UnknownRole } else { @@ -1482,3 +1467,12 @@ func newErrorFuture(err error) *tarantool.Future { fut.SetError(err) return fut } + +func isFeatureInSlice(expected iproto.Feature, actualSlice []iproto.Feature) bool { + for _, actual := range actualSlice { + if expected == actual { + return true + } + } + return false +} diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index 8b19e671b..15e67b59d 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -13,21 +13,25 @@ import ( "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" + "github.com/stretchr/testify/require" "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/pool" "github.com/tarantool/go-tarantool/v2/test_helpers" ) +var user = "test" +var pass = "test" var spaceNo = uint32(520) var spaceName = "testPool" var indexNo = uint32(0) var ports = []string{"3013", "3014", "3015", "3016", "3017"} var host = "127.0.0.1" + +type DialersMap = map[string]tarantool.Dialer + var servers = []string{ strings.Join([]string{host, ports[0]}, ":"), strings.Join([]string{host, ports[1]}, ":"), @@ -36,10 +40,35 @@ var servers = []string{ strings.Join([]string{host, ports[4]}, ":"), } +func makeDialer(serv string) tarantool.Dialer { + return tarantool.NetDialer{ + Address: serv, + User: user, + Password: pass, + } +} + +func makeDialers(servers []string) []tarantool.Dialer { + dialers := make([]tarantool.Dialer, 0, len(servers)) + for _, serv := range servers { + dialers = append(dialers, makeDialer(serv)) + } + return dialers +} + +func makeDialersMap(servers []string) DialersMap { + dialersMap := DialersMap{} + for _, serv := range servers { + dialersMap[serv] = makeDialer(serv) + } + return dialersMap +} + +var dialers = makeDialers(servers) +var dialersMap = makeDialersMap(servers) + var connOpts = tarantool.Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", } var defaultCountRetry = 5 @@ -49,21 +78,24 @@ var instances []test_helpers.TarantoolInstance func TestConnError_IncorrectParams(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, []string{}, tarantool.Opts{}) + connPool, err := pool.Connect(ctx, DialersMap{}, tarantool.Opts{}) cancel() require.Nilf(t, connPool, "conn is not nil with incorrect param") require.NotNilf(t, err, "err is nil with incorrect params") - require.Equal(t, "addrs (first argument) should not be empty", err.Error()) + require.Equal(t, "dialers (second argument) should not be empty", err.Error()) ctx, cancel = test_helpers.GetPoolConnectContext() - connPool, err = pool.Connect(ctx, []string{"err1", "err2"}, connOpts) + connPool, err = pool.Connect(ctx, DialersMap{ + "err1": tarantool.NetDialer{Address: "err1"}, + "err2": tarantool.NetDialer{Address: "err2"}, + }, connOpts) cancel() require.Nilf(t, connPool, "conn is not nil with incorrect param") require.NotNilf(t, err, "err is nil with incorrect params") require.Equal(t, "no active connections", err.Error()) ctx, cancel = test_helpers.GetPoolConnectContext() - connPool, err = pool.ConnectWithOpts(ctx, servers, tarantool.Opts{}, pool.Opts{}) + connPool, err = pool.ConnectWithOpts(ctx, dialersMap, tarantool.Opts{}, pool.Opts{}) cancel() require.Nilf(t, connPool, "conn is not nil with incorrect param") require.NotNilf(t, err, "err is nil with incorrect params") @@ -71,10 +103,11 @@ func TestConnError_IncorrectParams(t *testing.T) { } func TestConnSuccessfully(t *testing.T) { - server := servers[0] + healthyServ := servers[0] + ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, []string{"err", server}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap([]string{healthyServ, "err"}), connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -83,10 +116,10 @@ func TestConnSuccessfully(t *testing.T) { args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, Mode: pool.ANY, - Servers: []string{server}, + Servers: []string{healthyServ}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ - server: true, + healthyServ: true, }, } @@ -97,8 +130,6 @@ func TestConnSuccessfully(t *testing.T) { func TestConnErrorAfterCtxCancel(t *testing.T) { var connLongReconnectOpts = tarantool.Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", Reconnect: time.Second, MaxReconnects: 100, } @@ -109,7 +140,7 @@ func TestConnErrorAfterCtxCancel(t *testing.T) { var err error cancel() - connPool, err = pool.Connect(ctx, servers, connLongReconnectOpts) + connPool, err = pool.Connect(ctx, dialersMap, connLongReconnectOpts) if connPool != nil || err == nil { t.Fatalf("ConnectionPool was created after cancel") @@ -121,24 +152,25 @@ func TestConnErrorAfterCtxCancel(t *testing.T) { } type mockClosingDialer struct { - cnt int + addr string + cnt *int ctx context.Context ctxCancel context.CancelFunc } -func (m *mockClosingDialer) Dial(ctx context.Context, address string, +func (m *mockClosingDialer) Dial(ctx context.Context, opts tarantool.DialOpts) (tarantool.Conn, error) { + dialer := tarantool.NetDialer{ + Address: m.addr, + User: user, + Password: pass, + } + conn, err := dialer.Dial(m.ctx, tarantool.DialOpts{}) - dialer := tarantool.TtDialer{} - conn, err := dialer.Dial(m.ctx, address, tarantool.DialOpts{ - User: "test", - Password: "test", - }) - - if m.cnt == 0 { + if *m.cnt == 0 { m.ctxCancel() } - m.cnt++ + *m.cnt++ return conn, err } @@ -147,11 +179,18 @@ func TestContextCancelInProgress(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - dialer := &mockClosingDialer{0, ctx, cancel} + cnt := new(int) + poolDialers := DialersMap{} + for _, serv := range servers { + poolDialers[serv] = &mockClosingDialer{ + addr: serv, + cnt: cnt, + ctx: ctx, + ctxCancel: cancel, + } + } - connPool, err := pool.Connect(ctx, servers, tarantool.Opts{ - Dialer: dialer, - }) + connPool, err := pool.Connect(ctx, poolDialers, tarantool.Opts{}) require.NotNilf(t, err, "expected err after ctx cancel") assert.Truef(t, strings.Contains(err.Error(), "operation was canceled"), fmt.Sprintf("unexpected error, expected to contain %s, got %v", @@ -161,10 +200,19 @@ func TestContextCancelInProgress(t *testing.T) { func TestConnSuccessfullyDuplicates(t *testing.T) { server := servers[0] + + poolDialers := DialersMap{} + for i := 0; i < 4; i++ { + poolDialers[fmt.Sprintf("c%d", i)] = tarantool.NetDialer{ + Address: server, + User: user, + Password: pass, + } + } + ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, []string{server, server, server, server}, - connOpts) + connPool, err := pool.Connect(ctx, poolDialers, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -173,18 +221,18 @@ func TestConnSuccessfullyDuplicates(t *testing.T) { args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, Mode: pool.ANY, - Servers: []string{server}, + Servers: []string{"c0", "c1", "c2", "c3"}, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ - server: true, + "c0": true, + "c1": true, + "c2": true, + "c3": true, }, } err = test_helpers.CheckPoolStatuses(args) require.Nil(t, err) - - addrs := connPool.GetAddrs() - require.Equalf(t, []string{server}, addrs, "should be only one address") } func TestReconnect(t *testing.T) { @@ -192,7 +240,7 @@ func TestReconnect(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -233,14 +281,15 @@ func TestReconnect(t *testing.T) { } func TestDisconnect_withReconnect(t *testing.T) { - const serverId = 0 + serverId := 0 + server := servers[serverId] opts := connOpts opts.Reconnect = 10 * time.Second ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, []string{servers[serverId]}, opts) + connPool, err := pool.Connect(ctx, makeDialersMap([]string{server}), opts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -286,7 +335,7 @@ func TestDisconnectAll(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, []string{server1, server2}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap([]string{server1, server2}), connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -335,7 +384,7 @@ func TestDisconnectAll(t *testing.T) { func TestAdd(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, []string{servers[0]}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap(servers[:1]), connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -343,7 +392,7 @@ func TestAdd(t *testing.T) { for _, server := range servers[1:] { ctx, cancel := test_helpers.GetConnectContext() - err = connPool.Add(ctx, server) + err = connPool.Add(ctx, server, dialersMap[server]) cancel() require.Nil(t, err) } @@ -368,8 +417,9 @@ func TestAdd(t *testing.T) { } func TestAdd_exist(t *testing.T) { + server := servers[0] ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, []string{servers[0]}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap([]string{server}), connOpts) cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -377,7 +427,7 @@ func TestAdd_exist(t *testing.T) { defer connPool.Close() ctx, cancel = test_helpers.GetConnectContext() - err = connPool.Add(ctx, servers[0]) + err = connPool.Add(ctx, server, makeDialer(server)) cancel() require.Equal(t, pool.ErrExists, err) @@ -387,7 +437,7 @@ func TestAdd_exist(t *testing.T) { Servers: servers, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ - servers[0]: true, + server: true, }, } @@ -397,16 +447,21 @@ func TestAdd_exist(t *testing.T) { } func TestAdd_unreachable(t *testing.T) { + server := servers[0] + ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, []string{servers[0]}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap([]string{server}), connOpts) cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() + unhealthyServ := "127.0.0.2:6667" ctx, cancel = test_helpers.GetConnectContext() - err = connPool.Add(ctx, "127.0.0.2:6667") + err = connPool.Add(ctx, unhealthyServ, tarantool.NetDialer{ + Address: unhealthyServ, + }) cancel() // The OS-dependent error so we just check for existence. require.NotNil(t, err) @@ -417,7 +472,7 @@ func TestAdd_unreachable(t *testing.T) { Servers: servers, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ - servers[0]: true, + server: true, }, } @@ -427,22 +482,26 @@ func TestAdd_unreachable(t *testing.T) { } func TestAdd_afterClose(t *testing.T) { + server := servers[0] ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, []string{servers[0]}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap([]string{server}), connOpts) cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") connPool.Close() ctx, cancel = test_helpers.GetConnectContext() - err = connPool.Add(ctx, servers[0]) + err = connPool.Add(ctx, server, dialersMap[server]) cancel() assert.Equal(t, err, pool.ErrClosed) } func TestAdd_Close_concurrent(t *testing.T) { + serv0 := servers[0] + serv1 := servers[1] + ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, []string{servers[0]}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap([]string{serv0}), connOpts) cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -453,7 +512,7 @@ func TestAdd_Close_concurrent(t *testing.T) { defer wg.Done() ctx, cancel := test_helpers.GetConnectContext() - err = connPool.Add(ctx, servers[1]) + err = connPool.Add(ctx, serv1, makeDialer(serv1)) cancel() if err != nil { assert.Equal(t, pool.ErrClosed, err) @@ -466,8 +525,11 @@ func TestAdd_Close_concurrent(t *testing.T) { } func TestAdd_CloseGraceful_concurrent(t *testing.T) { + serv0 := servers[0] + serv1 := servers[1] + ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, []string{servers[0]}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap([]string{serv0}), connOpts) cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -478,7 +540,7 @@ func TestAdd_CloseGraceful_concurrent(t *testing.T) { defer wg.Done() ctx, cancel := test_helpers.GetConnectContext() - err = connPool.Add(ctx, servers[1]) + err = connPool.Add(ctx, serv1, makeDialer(serv1)) cancel() if err != nil { assert.Equal(t, pool.ErrClosed, err) @@ -493,7 +555,7 @@ func TestAdd_CloseGraceful_concurrent(t *testing.T) { func TestRemove(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -522,7 +584,7 @@ func TestRemove(t *testing.T) { func TestRemove_double(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, []string{servers[0], servers[1]}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap(servers[:2]), connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -551,7 +613,7 @@ func TestRemove_double(t *testing.T) { func TestRemove_unknown(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, []string{servers[0], servers[1]}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap(servers[:2]), connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -579,7 +641,7 @@ func TestRemove_unknown(t *testing.T) { func TestRemove_concurrent(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, []string{servers[0], servers[1]}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap(servers[:2]), connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -628,7 +690,7 @@ func TestRemove_concurrent(t *testing.T) { func TestRemove_Close_concurrent(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, []string{servers[0], servers[1]}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap(servers[:2]), connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -649,7 +711,7 @@ func TestRemove_Close_concurrent(t *testing.T) { func TestRemove_CloseGraceful_concurrent(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, []string{servers[0], servers[1]}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap(servers[:2]), connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -673,7 +735,7 @@ func TestClose(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, []string{server1, server2}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap([]string{server1, server2}), connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -715,7 +777,7 @@ func TestCloseGraceful(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, []string{server1, server2}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap([]string{server1, server2}), connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -734,9 +796,11 @@ func TestCloseGraceful(t *testing.T) { require.Nil(t, err) eval := `local fiber = require('fiber') - local time = ... - fiber.sleep(time) + local time = ... + fiber.sleep(time) + ` + evalSleep := 3 // In seconds. req := tarantool.NewEvalRequest(eval).Args([]interface{}{evalSleep}) fut := connPool.Do(req, pool.ANY) @@ -779,7 +843,8 @@ func (h *testHandler) addErr(err error) { h.errs = append(h.errs, err) } -func (h *testHandler) Discovered(conn *tarantool.Connection, +func (h *testHandler) Discovered(id string, conn *tarantool.Connection, + role pool.Role) error { discovered := atomic.AddUint32(&h.discovered, 1) @@ -790,29 +855,29 @@ func (h *testHandler) Discovered(conn *tarantool.Connection, // discovered < 3 - initial open of connections // discovered >= 3 - update a connection after a role update - addr := conn.Addr() - if addr == servers[0] { + if id == servers[0] { if discovered < 3 && role != pool.MasterRole { - h.addErr(fmt.Errorf("unexpected init role %d for addr %s", role, addr)) + h.addErr(fmt.Errorf("unexpected init role %d for id %s", role, id)) } if discovered >= 3 && role != pool.ReplicaRole { - h.addErr(fmt.Errorf("unexpected updated role %d for addr %s", role, addr)) + h.addErr(fmt.Errorf("unexpected updated role %d for id %s", role, id)) } - } else if addr == servers[1] { + } else if id == servers[1] { if discovered >= 3 { - h.addErr(fmt.Errorf("unexpected discovery for addr %s", addr)) + h.addErr(fmt.Errorf("unexpected discovery for id %s", id)) } if role != pool.ReplicaRole { - h.addErr(fmt.Errorf("unexpected role %d for addr %s", role, addr)) + h.addErr(fmt.Errorf("unexpected role %d for id %s", role, id)) } } else { - h.addErr(fmt.Errorf("unexpected discovered addr %s", addr)) + h.addErr(fmt.Errorf("unexpected discovered id %s", id)) } return nil } -func (h *testHandler) Deactivated(conn *tarantool.Connection, +func (h *testHandler) Deactivated(id string, conn *tarantool.Connection, + role pool.Role) error { deactivated := atomic.AddUint32(&h.deactivated, 1) @@ -821,22 +886,21 @@ func (h *testHandler) Deactivated(conn *tarantool.Connection, return nil } - addr := conn.Addr() - if deactivated == 1 && addr == servers[0] { + if deactivated == 1 && id == servers[0] { // A first close is a role update. if role != pool.MasterRole { - h.addErr(fmt.Errorf("unexpected removed role %d for addr %s", role, addr)) + h.addErr(fmt.Errorf("unexpected removed role %d for id %s", role, id)) } return nil } - if addr == servers[0] || addr == servers[1] { + if id == servers[0] || id == servers[1] { // Close. if role != pool.ReplicaRole { - h.addErr(fmt.Errorf("unexpected removed role %d for addr %s", role, addr)) + h.addErr(fmt.Errorf("unexpected removed role %d for id %s", role, id)) } } else { - h.addErr(fmt.Errorf("unexpected removed addr %s", addr)) + h.addErr(fmt.Errorf("unexpected removed id %s", id)) } return nil @@ -844,9 +908,10 @@ func (h *testHandler) Deactivated(conn *tarantool.Connection, func TestConnectionHandlerOpenUpdateClose(t *testing.T) { poolServers := []string{servers[0], servers[1]} + poolDialers := makeDialersMap(poolServers) roles := []bool{false, true} - err := test_helpers.SetClusterRO(poolServers, connOpts, roles) + err := test_helpers.SetClusterRO(makeDialers(poolServers), connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") h := &testHandler{} @@ -856,7 +921,7 @@ func TestConnectionHandlerOpenUpdateClose(t *testing.T) { } ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.ConnectWithOpts(ctx, poolServers, connOpts, poolOpts) + connPool, err := pool.ConnectWithOpts(ctx, poolDialers, connOpts, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -910,13 +975,15 @@ type testAddErrorHandler struct { discovered, deactivated int } -func (h *testAddErrorHandler) Discovered(conn *tarantool.Connection, +func (h *testAddErrorHandler) Discovered(id string, conn *tarantool.Connection, + role pool.Role) error { h.discovered++ return fmt.Errorf("any error") } -func (h *testAddErrorHandler) Deactivated(conn *tarantool.Connection, +func (h *testAddErrorHandler) Deactivated(id string, conn *tarantool.Connection, + role pool.Role) error { h.deactivated++ return nil @@ -932,7 +999,7 @@ func TestConnectionHandlerOpenError(t *testing.T) { } ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.ConnectWithOpts(ctx, poolServers, connOpts, poolOpts) + connPool, err := pool.ConnectWithOpts(ctx, makeDialersMap(poolServers), connOpts, poolOpts) if err == nil { defer connPool.Close() } @@ -945,7 +1012,8 @@ type testUpdateErrorHandler struct { discovered, deactivated uint32 } -func (h *testUpdateErrorHandler) Discovered(conn *tarantool.Connection, +func (h *testUpdateErrorHandler) Discovered(id string, conn *tarantool.Connection, + role pool.Role) error { atomic.AddUint32(&h.discovered, 1) @@ -956,7 +1024,8 @@ func (h *testUpdateErrorHandler) Discovered(conn *tarantool.Connection, return nil } -func (h *testUpdateErrorHandler) Deactivated(conn *tarantool.Connection, +func (h *testUpdateErrorHandler) Deactivated(id string, conn *tarantool.Connection, + role pool.Role) error { atomic.AddUint32(&h.deactivated, 1) return nil @@ -964,9 +1033,10 @@ func (h *testUpdateErrorHandler) Deactivated(conn *tarantool.Connection, func TestConnectionHandlerUpdateError(t *testing.T) { poolServers := []string{servers[0], servers[1]} + poolDialers := makeDialersMap(poolServers) roles := []bool{false, false} - err := test_helpers.SetClusterRO(poolServers, connOpts, roles) + err := test_helpers.SetClusterRO(makeDialers(poolServers), connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") h := &testUpdateErrorHandler{} @@ -976,7 +1046,7 @@ func TestConnectionHandlerUpdateError(t *testing.T) { } ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.ConnectWithOpts(ctx, poolServers, connOpts, poolOpts) + connPool, err := pool.ConnectWithOpts(ctx, poolDialers, connOpts, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -1023,7 +1093,7 @@ func TestRequestOnClosed(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, []string{server1, server2}, connOpts) + connPool, err := pool.Connect(ctx, makeDialersMap([]string{server1, server2}), connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1056,35 +1126,15 @@ func TestRequestOnClosed(t *testing.T) { require.Nilf(t, err, "failed to restart tarantool") } -func TestGetPoolInfo(t *testing.T) { - server1 := servers[0] - server2 := servers[1] - - srvs := []string{server1, server2} - expected := []string{server1, server2} - ctx, cancel := test_helpers.GetPoolConnectContext() - defer cancel() - connPool, err := pool.Connect(ctx, srvs, connOpts) - require.Nilf(t, err, "failed to connect") - require.NotNilf(t, connPool, "conn is nil after Connect") - - defer connPool.Close() - - srvs[0] = "x" - connPool.GetAddrs()[1] = "y" - - require.ElementsMatch(t, expected, connPool.GetAddrs()) -} - func TestCall(t *testing.T) { roles := []bool{false, true, false, false, true} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1138,12 +1188,12 @@ func TestCall(t *testing.T) { func TestCall16(t *testing.T) { roles := []bool{false, true, false, false, true} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1197,12 +1247,12 @@ func TestCall16(t *testing.T) { func TestCall17(t *testing.T) { roles := []bool{false, true, false, false, true} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1256,12 +1306,12 @@ func TestCall17(t *testing.T) { func TestEval(t *testing.T) { roles := []bool{false, true, false, false, true} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1336,12 +1386,12 @@ func TestExecute(t *testing.T) { roles := []bool{false, true, false, false, true} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1395,12 +1445,12 @@ func TestRoundRobinStrategy(t *testing.T) { serversNumber := len(servers) - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1474,12 +1524,12 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { servers[4]: true, } - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1547,12 +1597,12 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { servers[4]: true, } - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1632,12 +1682,12 @@ func TestUpdateInstancesRoles(t *testing.T) { serversNumber := len(servers) - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1711,7 +1761,7 @@ func TestUpdateInstancesRoles(t *testing.T) { servers[3]: true, } - err = test_helpers.SetClusterRO(servers, connOpts, roles) + err = test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") // ANY @@ -1778,12 +1828,12 @@ func TestUpdateInstancesRoles(t *testing.T) { func TestInsert(t *testing.T) { roles := []bool{true, true, false, true, true} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1809,7 +1859,7 @@ func TestInsert(t *testing.T) { // Connect to servers[2] to check if tuple // was inserted on RW instance - conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) + conn := test_helpers.ConnectWithValidation(t, makeDialer(servers[2]), connOpts) defer conn.Close() sel := tarantool.NewSelectRequest(spaceNo). @@ -1879,12 +1929,12 @@ func TestInsert(t *testing.T) { func TestDelete(t *testing.T) { roles := []bool{true, true, false, true, true} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1892,7 +1942,7 @@ func TestDelete(t *testing.T) { // Connect to servers[2] to check if tuple // was inserted on RW instance - conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) + conn := test_helpers.ConnectWithValidation(t, makeDialer(servers[2]), connOpts) defer conn.Close() ins := tarantool.NewInsertRequest(spaceNo).Tuple([]interface{}{"delete_key", "delete_value"}) @@ -1945,12 +1995,12 @@ func TestDelete(t *testing.T) { func TestUpsert(t *testing.T) { roles := []bool{true, true, false, true, true} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1958,7 +2008,7 @@ func TestUpsert(t *testing.T) { // Connect to servers[2] to check if tuple // was inserted on RW instance - conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) + conn := test_helpers.ConnectWithValidation(t, makeDialer(servers[2]), connOpts) defer conn.Close() // Mode is `RW` by default, we have only one RW instance (servers[2]) @@ -2019,12 +2069,12 @@ func TestUpsert(t *testing.T) { func TestUpdate(t *testing.T) { roles := []bool{true, true, false, true, true} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2032,7 +2082,7 @@ func TestUpdate(t *testing.T) { // Connect to servers[2] to check if tuple // was inserted on RW instance - conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) + conn := test_helpers.ConnectWithValidation(t, makeDialer(servers[2]), connOpts) defer conn.Close() ins := tarantool.NewInsertRequest(spaceNo). @@ -2111,12 +2161,12 @@ func TestUpdate(t *testing.T) { func TestReplace(t *testing.T) { roles := []bool{true, true, false, true, true} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2124,7 +2174,7 @@ func TestReplace(t *testing.T) { // Connect to servers[2] to check if tuple // was inserted on RW instance - conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) + conn := test_helpers.ConnectWithValidation(t, makeDialer(servers[2]), connOpts) defer conn.Close() ins := tarantool.NewInsertRequest(spaceNo). @@ -2199,12 +2249,12 @@ func TestReplace(t *testing.T) { func TestSelect(t *testing.T) { roles := []bool{true, true, false, true, false} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2222,13 +2272,13 @@ func TestSelect(t *testing.T) { rwKey := []interface{}{"rw_select_key"} anyKey := []interface{}{"any_select_key"} - err = test_helpers.InsertOnInstances(roServers, connOpts, spaceNo, roTpl) + err = test_helpers.InsertOnInstances(makeDialers(roServers), connOpts, spaceNo, roTpl) require.Nil(t, err) - err = test_helpers.InsertOnInstances(rwServers, connOpts, spaceNo, rwTpl) + err = test_helpers.InsertOnInstances(makeDialers(rwServers), connOpts, spaceNo, rwTpl) require.Nil(t, err) - err = test_helpers.InsertOnInstances(allServers, connOpts, spaceNo, anyTpl) + err = test_helpers.InsertOnInstances(makeDialers(allServers), connOpts, spaceNo, anyTpl) require.Nil(t, err) //default: ANY @@ -2321,12 +2371,12 @@ func TestSelect(t *testing.T) { func TestPing(t *testing.T) { roles := []bool{true, true, false, true, false} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2361,12 +2411,12 @@ func TestPing(t *testing.T) { func TestDo(t *testing.T) { roles := []bool{true, true, false, true, false} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2402,12 +2452,12 @@ func TestDo(t *testing.T) { func TestDo_concurrent(t *testing.T) { roles := []bool{true, true, false, true, false} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2435,12 +2485,12 @@ func TestNewPrepared(t *testing.T) { roles := []bool{true, true, false, true, false} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2450,10 +2500,6 @@ func TestNewPrepared(t *testing.T) { "SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:id AND NAME1=:name;", pool.RO) require.Nilf(t, err, "fail to prepare statement: %v", err) - if connPool.GetPoolInfo()[stmt.Conn.Addr()].ConnRole != pool.ReplicaRole { - t.Errorf("wrong role for the statement's connection") - } - executeReq := tarantool.NewExecutePreparedRequest(stmt) unprepareReq := tarantool.NewUnprepareRequest(stmt) @@ -2505,12 +2551,12 @@ func TestDoWithStrangerConn(t *testing.T) { roles := []bool{true, true, false, true, false} - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2536,12 +2582,12 @@ func TestStream_Commit(t *testing.T) { roles := []bool{true, true, false, true, true} - err = test_helpers.SetClusterRO(servers, connOpts, roles) + err = test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2568,7 +2614,7 @@ func TestStream_Commit(t *testing.T) { // Connect to servers[2] to check if tuple // was inserted outside of stream on RW instance // before transaction commit - conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) + conn := test_helpers.ConnectWithValidation(t, makeDialer(servers[2]), connOpts) defer conn.Close() // Select not related to the transaction @@ -2637,12 +2683,12 @@ func TestStream_Rollback(t *testing.T) { roles := []bool{true, true, false, true, true} - err = test_helpers.SetClusterRO(servers, connOpts, roles) + err = test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2668,7 +2714,7 @@ func TestStream_Rollback(t *testing.T) { // Connect to servers[2] to check if tuple // was not inserted outside of stream on RW instance - conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) + conn := test_helpers.ConnectWithValidation(t, makeDialer(servers[2]), connOpts) defer conn.Close() // Select not related to the transaction @@ -2738,12 +2784,12 @@ func TestStream_TxnIsolationLevel(t *testing.T) { roles := []bool{true, true, false, true, true} - err = test_helpers.SetClusterRO(servers, connOpts, roles) + err = test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2754,7 +2800,7 @@ func TestStream_TxnIsolationLevel(t *testing.T) { // Connect to servers[2] to check if tuple // was not inserted outside of stream on RW instance - conn := test_helpers.ConnectWithValidation(t, servers[2], connOpts) + conn := test_helpers.ConnectWithValidation(t, makeDialer(servers[2]), connOpts) defer conn.Close() for _, level := range txnIsolationLevels { @@ -2827,30 +2873,29 @@ func TestStream_TxnIsolationLevel(t *testing.T) { } } -func TestConnectionPool_NewWatcher_noWatchersFeature(t *testing.T) { - const key = "TestConnectionPool_NewWatcher_noWatchersFeature" +func TestConnectionPool_NewWatcher_no_watchers(t *testing.T) { + test_helpers.SkipIfWatchersSupported(t) - roles := []bool{true, false, false, true, true} - - opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []iproto.Feature{} - err := test_helpers.SetClusterRO(servers, opts, roles) - require.Nilf(t, err, "fail to set roles for cluster") + const key = "TestConnectionPool_NewWatcher_no_watchers" ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, opts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") - require.NotNilf(t, connPool, "conn is nil after Connect") + require.NotNilf(t, connPool, "conn is nill after Connect") defer connPool.Close() - watcher, err := connPool.NewWatcher(key, - func(event tarantool.WatchEvent) {}, pool.ANY) - require.Nilf(t, watcher, "watcher must not be created") - require.NotNilf(t, err, "an error is expected") - expected := "the feature IPROTO_FEATURE_WATCHERS must be required by " + - "connection options to create a watcher" - require.Equal(t, expected, err.Error()) + ch := make(chan struct{}) + connPool.NewWatcher(key, func(event tarantool.WatchEvent) { + close(ch) + }, pool.ANY) + + select { + case <-time.After(time.Second): + break + case <-ch: + t.Fatalf("watcher was created for connection that doesn't support it") + } } func TestConnectionPool_NewWatcher_modes(t *testing.T) { @@ -2860,16 +2905,12 @@ func TestConnectionPool_NewWatcher_modes(t *testing.T) { roles := []bool{true, false, false, true, true} - opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - err := test_helpers.SetClusterRO(servers, opts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, opts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2910,7 +2951,7 @@ func TestConnectionPool_NewWatcher_modes(t *testing.T) { select { case event := <-events: require.NotNil(t, event.Conn) - addr := event.Conn.Addr() + addr := event.Conn.Addr().String() if val, ok := testMap[addr]; ok { testMap[addr] = val + 1 } else { @@ -2941,11 +2982,7 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { const initCnt = 2 roles := []bool{true, false, false, true, true} - opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - err := test_helpers.SetClusterRO(servers, opts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") poolOpts := pool.Opts{ @@ -2953,7 +2990,7 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { } ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - pool, err := pool.ConnectWithOpts(ctx, servers, opts, poolOpts) + pool, err := pool.ConnectWithOpts(ctx, dialersMap, connOpts, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, pool, "conn is nil after Connect") @@ -2976,7 +3013,7 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { select { case event := <-events: require.NotNil(t, event.Conn) - addr := event.Conn.Addr() + addr := event.Conn.Addr().String() if val, ok := testMap[addr]; ok { testMap[addr] = val + 1 } else { @@ -2992,7 +3029,7 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { for i, role := range roles { roles[i] = !role } - err = test_helpers.SetClusterRO(servers, connOpts, roles) + err = test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") // Wait for all updated events. @@ -3000,7 +3037,7 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { select { case event := <-events: require.NotNil(t, event.Conn) - addr := event.Conn.Addr() + addr := event.Conn.Addr().String() if val, ok := testMap[addr]; ok { testMap[addr] = val + 1 } else { @@ -3030,16 +3067,12 @@ func TestWatcher_Unregister(t *testing.T) { const expectedCnt = 2 roles := []bool{true, false, false, true, true} - opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - err := test_helpers.SetClusterRO(servers, opts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - pool, err := pool.Connect(ctx, servers, opts) + pool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, pool, "conn is nil after Connect") defer pool.Close() @@ -3091,16 +3124,12 @@ func TestConnectionPool_NewWatcher_concurrent(t *testing.T) { roles := []bool{true, false, false, true, true} - opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - err := test_helpers.SetClusterRO(servers, opts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, opts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -3133,16 +3162,12 @@ func TestWatcher_Unregister_concurrent(t *testing.T) { roles := []bool{true, false, false, true, true} - opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - err := test_helpers.SetClusterRO(servers, opts, roles) + err := test_helpers.SetClusterRO(dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, opts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -3178,18 +3203,27 @@ func runTestMain(m *testing.M) int { // Tarantool supports streams and interactive transactions since version 2.10.0 isStreamUnsupported, err := test_helpers.IsTarantoolVersionLess(2, 10, 0) if err != nil { - log.Fatalf("Could not check the Tarantool version") + log.Fatalf("Could not check the Tarantool version: %s", err) + } + + instsOpts := make([]test_helpers.StartOpts, 0, len(servers)) + for _, serv := range servers { + instsOpts = append(instsOpts, test_helpers.StartOpts{ + Listen: serv, + Dialer: tarantool.NetDialer{ + Address: serv, + User: user, + Password: pass, + }, + InitScript: initScript, + WaitStart: waitStart, + ConnectRetry: connectRetry, + RetryTimeout: retryTimeout, + MemtxUseMvccEngine: !isStreamUnsupported, + }) } - instances, err = test_helpers.StartTarantoolInstances(servers, nil, test_helpers.StartOpts{ - InitScript: initScript, - User: connOpts.User, - Pass: connOpts.Pass, - WaitStart: waitStart, - ConnectRetry: connectRetry, - RetryTimeout: retryTimeout, - MemtxUseMvccEngine: !isStreamUnsupported, - }) + instances, err = test_helpers.StartTarantoolInstances(instsOpts) if err != nil { log.Fatalf("Failed to prepare test tarantool: %s", err) diff --git a/pool/example_test.go b/pool/example_test.go index a38ae2831..c4a919a93 100644 --- a/pool/example_test.go +++ b/pool/example_test.go @@ -2,7 +2,6 @@ package pool_test import ( "fmt" - "strings" "time" "github.com/tarantool/go-iproto" @@ -22,14 +21,42 @@ type Tuple struct { var testRoles = []bool{true, true, false, true, true} -func examplePool(roles []bool, connOpts tarantool.Opts) (*pool.ConnectionPool, error) { - err := test_helpers.SetClusterRO(servers, connOpts, roles) +func examplePool(roles []bool, + connOpts tarantool.Opts) (*pool.ConnectionPool, error) { + err := test_helpers.SetClusterRO(dialers, connOpts, roles) if err != nil { return nil, fmt.Errorf("ConnectionPool is not established") } ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, servers, connOpts) + connPool, err := pool.Connect(ctx, dialersMap, connOpts) + if err != nil || connPool == nil { + return nil, fmt.Errorf("ConnectionPool is not established") + } + + return connPool, nil +} + +func exampleFeaturesPool(roles []bool, connOpts tarantool.Opts, + requiredProtocol tarantool.ProtocolInfo) (*pool.ConnectionPool, error) { + poolDialersMap := map[string]tarantool.Dialer{} + poolDialers := []tarantool.Dialer{} + for _, serv := range servers { + poolDialersMap[serv] = tarantool.NetDialer{ + Address: serv, + User: user, + Password: pass, + RequiredProtocolInfo: requiredProtocol, + } + poolDialers = append(poolDialers, poolDialersMap[serv]) + } + err := test_helpers.SetClusterRO(poolDialers, connOpts, roles) + if err != nil { + return nil, fmt.Errorf("ConnectionPool is not established") + } + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, poolDialersMap, connOpts) if err != nil || connPool == nil { return nil, fmt.Errorf("ConnectionPool is not established") } @@ -94,11 +121,6 @@ func ExampleConnectionPool_NewWatcher() { const key = "foo" const value = "bar" - opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - connPool, err := examplePool(testRoles, connOpts) if err != nil { fmt.Println(err) @@ -122,49 +144,15 @@ func ExampleConnectionPool_NewWatcher() { time.Sleep(time.Second) } -func ExampleConnectionPool_NewWatcher_noWatchersFeature() { - const key = "foo" - - opts := connOpts.Clone() - opts.RequiredProtocolInfo.Features = []iproto.Feature{} - - connPool, err := examplePool(testRoles, connOpts) - if err != nil { - fmt.Println(err) - } - defer connPool.Close() - - callback := func(event tarantool.WatchEvent) {} - watcher, err := connPool.NewWatcher(key, callback, pool.ANY) - fmt.Println(watcher) - if err != nil { - // Need to split the error message into two lines to pass - // golangci-lint. - str := err.Error() - fmt.Println(strings.Trim(str[:56], " ")) - fmt.Println(str[56:]) - } else { - fmt.Println(err) - } - // Output: - // - // the feature IPROTO_FEATURE_WATCHERS must be required by - // connection options to create a watcher -} - -func getTestTxnOpts() tarantool.Opts { - txnOpts := connOpts.Clone() - +func getTestTxnProtocol() tarantool.ProtocolInfo { // Assert that server supports expected protocol features - txnOpts.RequiredProtocolInfo = tarantool.ProtocolInfo{ + return tarantool.ProtocolInfo{ Version: tarantool.ProtocolVersion(1), Features: []iproto.Feature{ iproto.IPROTO_FEATURE_STREAMS, iproto.IPROTO_FEATURE_TRANSACTIONS, }, } - - return txnOpts } func ExampleCommitRequest() { @@ -178,8 +166,7 @@ func ExampleCommitRequest() { return } - txnOpts := getTestTxnOpts() - connPool, err := examplePool(testRoles, txnOpts) + connPool, err := exampleFeaturesPool(testRoles, connOpts, getTestTxnProtocol()) if err != nil { fmt.Println(err) return @@ -265,9 +252,8 @@ func ExampleRollbackRequest() { return } - txnOpts := getTestTxnOpts() // example pool has only one rw instance - connPool, err := examplePool(testRoles, txnOpts) + connPool, err := exampleFeaturesPool(testRoles, connOpts, getTestTxnProtocol()) if err != nil { fmt.Println(err) return @@ -352,9 +338,8 @@ func ExampleBeginRequest_TxnIsolation() { return } - txnOpts := getTestTxnOpts() // example pool has only one rw instance - connPool, err := examplePool(testRoles, txnOpts) + connPool, err := exampleFeaturesPool(testRoles, connOpts, getTestTxnProtocol()) if err != nil { fmt.Println(err) return diff --git a/pool/round_robin.go b/pool/round_robin.go index c3400d371..6e0b158f4 100644 --- a/pool/round_robin.go +++ b/pool/round_robin.go @@ -8,27 +8,27 @@ import ( ) type roundRobinStrategy struct { - conns []*tarantool.Connection - indexByAddr map[string]uint - mutex sync.RWMutex - size uint64 - current uint64 + conns []*tarantool.Connection + indexById map[string]uint + mutex sync.RWMutex + size uint64 + current uint64 } func newRoundRobinStrategy(size int) *roundRobinStrategy { return &roundRobinStrategy{ - conns: make([]*tarantool.Connection, 0, size), - indexByAddr: make(map[string]uint), - size: 0, - current: 0, + conns: make([]*tarantool.Connection, 0, size), + indexById: make(map[string]uint, size), + size: 0, + current: 0, } } -func (r *roundRobinStrategy) GetConnByAddr(addr string) *tarantool.Connection { +func (r *roundRobinStrategy) GetConnById(id string) *tarantool.Connection { r.mutex.RLock() defer r.mutex.RUnlock() - index, found := r.indexByAddr[addr] + index, found := r.indexById[id] if !found { return nil } @@ -36,7 +36,7 @@ func (r *roundRobinStrategy) GetConnByAddr(addr string) *tarantool.Connection { return r.conns[index] } -func (r *roundRobinStrategy) DeleteConnByAddr(addr string) *tarantool.Connection { +func (r *roundRobinStrategy) DeleteConnById(id string) *tarantool.Connection { r.mutex.Lock() defer r.mutex.Unlock() @@ -44,20 +44,20 @@ func (r *roundRobinStrategy) DeleteConnByAddr(addr string) *tarantool.Connection return nil } - index, found := r.indexByAddr[addr] + index, found := r.indexById[id] if !found { return nil } - delete(r.indexByAddr, addr) + delete(r.indexById, id) conn := r.conns[index] r.conns = append(r.conns[:index], r.conns[index+1:]...) r.size -= 1 - for k, v := range r.indexByAddr { + for k, v := range r.indexById { if v > index { - r.indexByAddr[k] = v - 1 + r.indexById[k] = v - 1 } } @@ -81,25 +81,27 @@ func (r *roundRobinStrategy) GetNextConnection() *tarantool.Connection { return r.conns[r.nextIndex()] } -func (r *roundRobinStrategy) GetConnections() []*tarantool.Connection { +func (r *roundRobinStrategy) GetConnections() map[string]*tarantool.Connection { r.mutex.RLock() defer r.mutex.RUnlock() - ret := make([]*tarantool.Connection, len(r.conns)) - copy(ret, r.conns) + conns := map[string]*tarantool.Connection{} + for id, index := range r.indexById { + conns[id] = r.conns[index] + } - return ret + return conns } -func (r *roundRobinStrategy) AddConn(addr string, conn *tarantool.Connection) { +func (r *roundRobinStrategy) AddConn(id string, conn *tarantool.Connection) { r.mutex.Lock() defer r.mutex.Unlock() - if idx, ok := r.indexByAddr[addr]; ok { + if idx, ok := r.indexById[id]; ok { r.conns[idx] = conn } else { r.conns = append(r.conns, conn) - r.indexByAddr[addr] = uint(r.size) + r.indexById[id] = uint(r.size) r.size += 1 } } diff --git a/pool/round_robin_test.go b/pool/round_robin_test.go index 4aacb1a26..6f133a799 100644 --- a/pool/round_robin_test.go +++ b/pool/round_robin_test.go @@ -22,7 +22,7 @@ func TestRoundRobinAddDelete(t *testing.T) { } for i, addr := range addrs { - if conn := rr.DeleteConnByAddr(addr); conn != conns[i] { + if conn := rr.DeleteConnById(addr); conn != conns[i] { t.Errorf("Unexpected connection on address %s", addr) } } @@ -40,13 +40,13 @@ func TestRoundRobinAddDuplicateDelete(t *testing.T) { rr.AddConn(validAddr1, conn1) rr.AddConn(validAddr1, conn2) - if rr.DeleteConnByAddr(validAddr1) != conn2 { + if rr.DeleteConnById(validAddr1) != conn2 { t.Errorf("Unexpected deleted connection") } if !rr.IsEmpty() { t.Errorf("RoundRobin does not empty") } - if rr.DeleteConnByAddr(validAddr1) != nil { + if rr.DeleteConnById(validAddr1) != nil { t.Errorf("Unexpected value after second deletion") } } @@ -79,11 +79,12 @@ func TestRoundRobinStrategy_GetConnections(t *testing.T) { rr.AddConn(addr, conns[i]) } - rr.GetConnections()[1] = conns[0] // GetConnections() returns a copy. + rr.GetConnections()[validAddr2] = conns[0] // GetConnections() returns a copy. rrConns := rr.GetConnections() - for i, expected := range conns { - if expected != rrConns[i] { - t.Errorf("Unexpected connection on %d call", i) + + for i, addr := range addrs { + if conns[i] != rrConns[addr] { + t.Errorf("Unexpected connection on %s addr", addr) } } } diff --git a/pool/watcher.go b/pool/watcher.go index 2d2b72e17..f7c08213e 100644 --- a/pool/watcher.go +++ b/pool/watcher.go @@ -76,7 +76,7 @@ type poolWatcher struct { key string callback tarantool.WatchCallback // watchers is a map connection -> connection watcher. - watchers map[string]tarantool.Watcher + watchers map[*tarantool.Connection]tarantool.Watcher // unregistered is true if the watcher already unregistered. unregistered bool // mutex for the pool watcher. @@ -101,18 +101,16 @@ func (w *poolWatcher) Unregister() { // watch adds a watcher for the connection. func (w *poolWatcher) watch(conn *tarantool.Connection) error { - addr := conn.Addr() - w.mutex.Lock() defer w.mutex.Unlock() if !w.unregistered { - if _, ok := w.watchers[addr]; ok { + if _, ok := w.watchers[conn]; ok { return nil } if watcher, err := conn.NewWatcher(w.key, w.callback); err == nil { - w.watchers[addr] = watcher + w.watchers[conn] = watcher return nil } else { return err @@ -123,15 +121,13 @@ func (w *poolWatcher) watch(conn *tarantool.Connection) error { // unwatch removes a watcher for the connection. func (w *poolWatcher) unwatch(conn *tarantool.Connection) { - addr := conn.Addr() - w.mutex.Lock() defer w.mutex.Unlock() if !w.unregistered { - if watcher, ok := w.watchers[addr]; ok { + if watcher, ok := w.watchers[conn]; ok { watcher.Unregister() - delete(w.watchers, addr) + delete(w.watchers, conn) } } } diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go index a3bfaf0a4..b6e89d3f8 100644 --- a/queue/example_connection_pool_test.go +++ b/queue/example_connection_pool_test.go @@ -44,7 +44,7 @@ func NewQueueConnectionHandler(name string, cfg queue.Cfg) *QueueConnectionHandl // // NOTE: the Queue supports only a master-replica cluster configuration. It // does not support a master-master configuration. -func (h *QueueConnectionHandler) Discovered(conn *tarantool.Connection, +func (h *QueueConnectionHandler) Discovered(id string, conn *tarantool.Connection, role pool.Role) error { h.mutex.Lock() defer h.mutex.Unlock() @@ -106,14 +106,14 @@ func (h *QueueConnectionHandler) Discovered(conn *tarantool.Connection, return h.err } - fmt.Printf("Master %s is ready to work!\n", conn.Addr()) + fmt.Printf("Master %s is ready to work!\n", id) atomic.AddInt32(&h.masterCnt, 1) return nil } // Deactivated doesn't do anything useful for the example. -func (h *QueueConnectionHandler) Deactivated(conn *tarantool.Connection, +func (h *QueueConnectionHandler) Deactivated(id string, conn *tarantool.Connection, role pool.Role) error { if role == pool.MasterRole { atomic.AddInt32(&h.masterCnt, -1) @@ -152,14 +152,22 @@ func Example_connectionPool() { defer h.Close() // Create a ConnectionPool object. - servers := []string{ - "127.0.0.1:3014", - "127.0.0.1:3015", + poolServers := []string{"127.0.0.1:3014", "127.0.0.1:3015"} + poolDialers := []tarantool.Dialer{} + poolDialersMap := map[string]tarantool.Dialer{} + + for _, serv := range poolServers { + dialer := tarantool.NetDialer{ + Address: serv, + User: "test", + Password: "test", + } + poolDialers = append(poolDialers, dialer) + poolDialersMap[serv] = dialer } + connOpts := tarantool.Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", } poolOpts := pool.Opts{ CheckTimeout: 5 * time.Second, @@ -167,7 +175,7 @@ func Example_connectionPool() { } ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() - connPool, err := pool.ConnectWithOpts(ctx, servers, connOpts, poolOpts) + connPool, err := pool.ConnectWithOpts(ctx, poolDialersMap, connOpts, poolOpts) if err != nil { fmt.Printf("Unable to connect to the pool: %s", err) return @@ -199,7 +207,7 @@ func Example_connectionPool() { // Switch a master instance in the pool. roles := []bool{true, false} for { - err := test_helpers.SetClusterRO(servers, connOpts, roles) + err := test_helpers.SetClusterRO(poolDialers, connOpts, roles) if err == nil { break } diff --git a/queue/example_msgpack_test.go b/queue/example_msgpack_test.go index 6d3637417..cdd1a4e1c 100644 --- a/queue/example_msgpack_test.go +++ b/queue/example_msgpack_test.go @@ -49,15 +49,18 @@ func (c *dummyData) EncodeMsgpack(e *msgpack.Encoder) error { // cannot take the task out of the queue within the time corresponding to the // connection timeout. func Example_simpleQueueCustomMsgPack() { + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + } opts := tarantool.Opts{ Reconnect: time.Second, Timeout: 5 * time.Second, MaxReconnects: 5, - User: "test", - Pass: "test", } ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - conn, err := tarantool.Connect(ctx, "127.0.0.1:3013", opts) + conn, err := tarantool.Connect(ctx, dialer, opts) cancel() if err != nil { log.Fatalf("connection: %s", err) diff --git a/queue/example_test.go b/queue/example_test.go index e81acca40..1b411603a 100644 --- a/queue/example_test.go +++ b/queue/example_test.go @@ -28,13 +28,16 @@ func Example_simpleQueue() { } opts := tarantool.Opts{ Timeout: 2500 * time.Millisecond, - User: "test", - Pass: "test", + } + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", } ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() - conn, err := tarantool.Connect(ctx, "127.0.0.1:3013", opts) + conn, err := tarantool.Connect(ctx, dialer, opts) if err != nil { fmt.Printf("error in prepare is %v", err) return diff --git a/queue/queue_test.go b/queue/queue_test.go index 575ce78a0..d23bd2c9c 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -15,18 +15,24 @@ import ( "github.com/tarantool/go-tarantool/v2/test_helpers" ) +const ( + user = "test" + pass = "test" +) + +var servers = []string{"127.0.0.1:3014", "127.0.0.1:3015"} var server = "127.0.0.1:3013" -var serversPool = []string{ - "127.0.0.1:3014", - "127.0.0.1:3015", -} var instances []test_helpers.TarantoolInstance +var dialer = NetDialer{ + Address: server, + User: user, + Password: pass, +} + var opts = Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", //Concurrency: 32, //RateLimit: 4*1024, } @@ -53,7 +59,7 @@ func dropQueue(t *testing.T, q queue.Queue) { /////////QUEUE///////// func TestFifoQueue(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -62,7 +68,7 @@ func TestFifoQueue(t *testing.T) { } func TestQueue_Cfg(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -76,7 +82,7 @@ func TestQueue_Cfg(t *testing.T) { } func TestQueue_Identify(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -99,7 +105,7 @@ func TestQueue_Identify(t *testing.T) { } func TestQueue_ReIdentify(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer func() { if conn != nil { conn.Close() @@ -152,7 +158,7 @@ func TestQueue_ReIdentify(t *testing.T) { conn.Close() conn = nil - conn = test_helpers.ConnectWithValidation(t, server, opts) + conn = test_helpers.ConnectWithValidation(t, dialer, opts) q = queue.New(conn, name) // Identify in another connection. @@ -182,7 +188,7 @@ func TestQueue_ReIdentify(t *testing.T) { } func TestQueue_State(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -199,7 +205,7 @@ func TestQueue_State(t *testing.T) { } func TestFifoQueue_GetExist_Statistic(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -229,7 +235,7 @@ func TestFifoQueue_GetExist_Statistic(t *testing.T) { } func TestFifoQueue_Put(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -249,7 +255,7 @@ func TestFifoQueue_Put(t *testing.T) { } func TestFifoQueue_Take(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -322,7 +328,7 @@ func (c *customData) EncodeMsgpack(e *msgpack.Encoder) error { } func TestFifoQueue_TakeTyped(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -385,7 +391,7 @@ func TestFifoQueue_TakeTyped(t *testing.T) { } func TestFifoQueue_Peek(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -417,7 +423,7 @@ func TestFifoQueue_Peek(t *testing.T) { } func TestFifoQueue_Bury_Kick(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -478,7 +484,7 @@ func TestFifoQueue_Bury_Kick(t *testing.T) { func TestFifoQueue_Delete(t *testing.T) { var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -530,7 +536,7 @@ func TestFifoQueue_Delete(t *testing.T) { } func TestFifoQueue_Release(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -590,7 +596,7 @@ func TestFifoQueue_Release(t *testing.T) { } func TestQueue_ReleaseAll(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -654,7 +660,7 @@ func TestQueue_ReleaseAll(t *testing.T) { } func TestTtlQueue(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -688,7 +694,7 @@ func TestTtlQueue(t *testing.T) { } func TestTtlQueue_Put(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_queue" @@ -737,7 +743,7 @@ func TestTtlQueue_Put(t *testing.T) { } func TestUtube_Put(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() name := "test_utube" @@ -810,7 +816,7 @@ func TestUtube_Put(t *testing.T) { } func TestTask_Touch(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() tests := []struct { @@ -896,10 +902,9 @@ func TestTask_Touch(t *testing.T) { // https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls func runTestMain(m *testing.M) int { inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + Dialer: dialer, InitScript: "testdata/config.lua", Listen: server, - User: opts.User, - Pass: opts.Pass, WaitStart: 100 * time.Millisecond, ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, @@ -911,14 +916,22 @@ func runTestMain(m *testing.M) int { defer test_helpers.StopTarantoolWithCleanup(inst) - poolOpts := test_helpers.StartOpts{ - InitScript: "testdata/pool.lua", - User: opts.User, - Pass: opts.Pass, - WaitStart: 3 * time.Second, // replication_timeout * 3 - ConnectRetry: -1, + poolInstsOpts := make([]test_helpers.StartOpts, 0, len(servers)) + for _, serv := range servers { + poolInstsOpts = append(poolInstsOpts, test_helpers.StartOpts{ + Listen: serv, + Dialer: NetDialer{ + Address: serv, + User: user, + Password: pass, + }, + InitScript: "testdata/pool.lua", + WaitStart: 3 * time.Second, // replication_timeout * 3 + ConnectRetry: -1, + }) } - instances, err = test_helpers.StartTarantoolInstances(serversPool, nil, poolOpts) + + instances, err = test_helpers.StartTarantoolInstances(poolInstsOpts) if err != nil { log.Printf("Failed to prepare test tarantool pool: %s", err) @@ -933,11 +946,17 @@ func runTestMain(m *testing.M) int { roles := []bool{false, true} connOpts := Opts{ Timeout: 500 * time.Millisecond, - User: "test", - Pass: "test", + } + dialers := make([]Dialer, 0, len(servers)) + for _, serv := range servers { + dialers = append(dialers, NetDialer{ + Address: serv, + User: user, + Password: pass, + }) } - err = test_helpers.SetClusterRO(serversPool, connOpts, roles) + err = test_helpers.SetClusterRO(dialers, connOpts, roles) if err == nil { break } diff --git a/request.go b/request.go index ac0f7e858..8c4f4acd2 100644 --- a/request.go +++ b/request.go @@ -904,10 +904,28 @@ func (req authRequest) Ctx() context.Context { // Body fills an encoder with the auth request body. func (req authRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - return enc.Encode(map[uint32]interface{}{ - uint32(iproto.IPROTO_USER_NAME): req.user, - uint32(iproto.IPROTO_TUPLE): []interface{}{req.auth.String(), req.pass}, - }) + if err := enc.EncodeMapLen(2); err != nil { + return err + } + if err := enc.EncodeUint32(uint32(iproto.IPROTO_USER_NAME)); err != nil { + return err + } + if err := enc.EncodeString(req.user); err != nil { + return err + } + if err := enc.EncodeUint32(uint32(iproto.IPROTO_TUPLE)); err != nil { + return err + } + if err := enc.EncodeArrayLen(2); err != nil { + return err + } + if err := enc.EncodeString(req.auth.String()); err != nil { + return err + } + if err := enc.EncodeString(req.pass); err != nil { + return err + } + return nil } // PingRequest helps you to create an execute request object for execution diff --git a/settings/example_test.go b/settings/example_test.go index 29be33bfe..47f8e8c43 100644 --- a/settings/example_test.go +++ b/settings/example_test.go @@ -10,10 +10,20 @@ import ( "github.com/tarantool/go-tarantool/v2/test_helpers" ) -func example_connect(opts tarantool.Opts) *tarantool.Connection { +var exampleDialer = tarantool.NetDialer{ + Address: "127.0.0.1", + User: "test", + Password: "test", +} + +var exampleOpts = tarantool.Opts{ + Timeout: 5 * time.Second, +} + +func example_connect(dialer tarantool.Dialer, opts tarantool.Opts) *tarantool.Connection { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() - conn, err := tarantool.Connect(ctx, server, opts) + conn, err := tarantool.Connect(ctx, dialer, opts) if err != nil { panic("Connection is not established: " + err.Error()) } @@ -25,7 +35,7 @@ func Example_sqlFullColumnNames() { var err error var isLess bool - conn := example_connect(opts) + conn := example_connect(exampleDialer, exampleOpts) defer conn.Close() // Tarantool supports session settings since version 2.3.1 diff --git a/settings/tarantool_test.go b/settings/tarantool_test.go index 2f7e01442..272693243 100644 --- a/settings/tarantool_test.go +++ b/settings/tarantool_test.go @@ -18,10 +18,13 @@ import ( var isSettingsSupported = false var server = "127.0.0.1:3013" +var dialer = tarantool.NetDialer{ + Address: server, + User: "test", + Password: "test", +} var opts = tarantool.Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", } func skipIfSettingsUnsupported(t *testing.T) { @@ -52,7 +55,7 @@ func TestErrorMarshalingEnabledSetting(t *testing.T) { var resp *tarantool.Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Disable receiving box.error as MP_EXT 3. @@ -101,7 +104,7 @@ func TestSQLDefaultEngineSetting(t *testing.T) { var resp *tarantool.Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Set default SQL "CREATE TABLE" engine to "vinyl". @@ -164,7 +167,7 @@ func TestSQLDeferForeignKeysSetting(t *testing.T) { var resp *tarantool.Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Create a parent space. @@ -237,7 +240,7 @@ func TestSQLFullColumnNamesSetting(t *testing.T) { var resp *tarantool.Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Create a space. @@ -299,7 +302,7 @@ func TestSQLFullMetadataSetting(t *testing.T) { var resp *tarantool.Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Create a space. @@ -360,7 +363,7 @@ func TestSQLParserDebugSetting(t *testing.T) { var resp *tarantool.Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Disable parser debug mode. @@ -398,7 +401,7 @@ func TestSQLRecursiveTriggersSetting(t *testing.T) { var resp *tarantool.Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Create a space. @@ -470,7 +473,7 @@ func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { var resp *tarantool.Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Create a space. @@ -510,7 +513,7 @@ func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { // Select multiple records. query := "SELECT * FROM seqscan data;" if isSeqScanOld, err := test_helpers.IsTarantoolVersionLess(3, 0, 0); err != nil { - t.Fatal("Could not check the Tarantool version") + t.Fatalf("Could not check the Tarantool version: %s", err) } else if isSeqScanOld { query = "SELECT * FROM data;" } @@ -549,7 +552,7 @@ func TestSQLSelectDebugSetting(t *testing.T) { var resp *tarantool.Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Disable select debug mode. @@ -586,7 +589,7 @@ func TestSQLVDBEDebugSetting(t *testing.T) { var resp *tarantool.Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Disable VDBE debug mode. @@ -623,7 +626,7 @@ func TestSessionSettings(t *testing.T) { var resp *tarantool.Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Set some settings values. @@ -668,10 +671,9 @@ func runTestMain(m *testing.M) int { isSettingsSupported = true inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + Dialer: dialer, InitScript: "testdata/config.lua", Listen: server, - User: opts.User, - Pass: opts.Pass, WaitStart: 100 * time.Millisecond, ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, diff --git a/shutdown_test.go b/shutdown_test.go index cf4894344..80996e3a9 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -2,7 +2,6 @@ // +build linux darwin,!cgo // Use OS build flags since signals are system-dependent. - package tarantool_test import ( @@ -14,25 +13,26 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tarantool/go-iproto" . "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/test_helpers" ) var shtdnServer = "127.0.0.1:3014" +var shtdnDialer = NetDialer{ + Address: shtdnServer, + User: dialer.User, + Password: dialer.Password, +} + var shtdnClntOpts = Opts{ - User: opts.User, - Pass: opts.Pass, - Timeout: 20 * time.Second, - Reconnect: 500 * time.Millisecond, - MaxReconnects: 10, - RequiredProtocolInfo: ProtocolInfo{Features: []iproto.Feature{iproto.IPROTO_FEATURE_WATCHERS}}, + Timeout: 20 * time.Second, + Reconnect: 500 * time.Millisecond, + MaxReconnects: 10, } var shtdnSrvOpts = test_helpers.StartOpts{ + Dialer: shtdnDialer, InitScript: "config.lua", Listen: shtdnServer, - User: shtdnClntOpts.User, - Pass: shtdnClntOpts.Pass, WaitStart: 100 * time.Millisecond, ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, @@ -139,7 +139,7 @@ func TestGracefulShutdown(t *testing.T) { require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) - conn = test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + conn = test_helpers.ConnectWithValidation(t, shtdnDialer, shtdnClntOpts) defer conn.Close() testGracefulShutdown(t, conn, &inst) @@ -147,16 +147,18 @@ func TestGracefulShutdown(t *testing.T) { func TestCloseGraceful(t *testing.T) { opts := Opts{ - User: shtdnClntOpts.User, - Pass: shtdnClntOpts.Pass, Timeout: shtdnClntOpts.Timeout, } + testDialer := shtdnDialer + testDialer.RequiredProtocolInfo = ProtocolInfo{} + testSrvOpts := shtdnSrvOpts + testSrvOpts.Dialer = testDialer - inst, err := test_helpers.StartTarantool(shtdnSrvOpts) + inst, err := test_helpers.StartTarantool(testSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) - conn := test_helpers.ConnectWithValidation(t, shtdnServer, opts) + conn := test_helpers.ConnectWithValidation(t, testDialer, opts) defer conn.Close() // Send request with sleep. @@ -197,7 +199,7 @@ func TestGracefulShutdownWithReconnect(t *testing.T) { require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) - conn := test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + conn := test_helpers.ConnectWithValidation(t, shtdnDialer, shtdnClntOpts) defer conn.Close() testGracefulShutdown(t, conn, &inst) @@ -214,19 +216,23 @@ func TestGracefulShutdownWithReconnect(t *testing.T) { func TestNoGracefulShutdown(t *testing.T) { // No watchers = no graceful shutdown. - noShtdnClntOpts := shtdnClntOpts.Clone() - noShtdnClntOpts.RequiredProtocolInfo = ProtocolInfo{} + noSthdClntOpts := opts + noShtdDialer := shtdnDialer + noShtdDialer.RequiredProtocolInfo = ProtocolInfo{} test_helpers.SkipIfWatchersSupported(t) var inst test_helpers.TarantoolInstance var conn *Connection var err error - inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + testSrvOpts := shtdnSrvOpts + testSrvOpts.Dialer = noShtdDialer + + inst, err = test_helpers.StartTarantool(testSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) - conn = test_helpers.ConnectWithValidation(t, shtdnServer, noShtdnClntOpts) + conn = test_helpers.ConnectWithValidation(t, noShtdDialer, noSthdClntOpts) defer conn.Close() evalSleep := 10 // in seconds @@ -278,7 +284,7 @@ func TestGracefulShutdownRespectsClose(t *testing.T) { require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) - conn = test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + conn = test_helpers.ConnectWithValidation(t, shtdnDialer, shtdnClntOpts) defer conn.Close() // Create a helper watcher to ensure that async @@ -358,7 +364,7 @@ func TestGracefulShutdownNotRacesWithRequestReconnect(t *testing.T) { require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) - conn = test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + conn = test_helpers.ConnectWithValidation(t, shtdnDialer, shtdnClntOpts) defer conn.Close() // Create a helper watcher to ensure that async @@ -429,7 +435,7 @@ func TestGracefulShutdownCloseConcurrent(t *testing.T) { require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) - conn := test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + conn := test_helpers.ConnectWithValidation(t, shtdnDialer, shtdnClntOpts) defer conn.Close() // Create a helper watcher to ensure that async @@ -466,7 +472,7 @@ func TestGracefulShutdownCloseConcurrent(t *testing.T) { // Do not wait till Tarantool register out watcher, // test everything is ok even on async. - conn, err := Connect(ctx, shtdnServer, shtdnClntOpts) + conn, err := Connect(ctx, shtdnDialer, shtdnClntOpts) if err != nil { t.Errorf("Failed to connect: %s", err) } else { @@ -519,7 +525,7 @@ func TestGracefulShutdownConcurrent(t *testing.T) { require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) - conn := test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + conn := test_helpers.ConnectWithValidation(t, shtdnDialer, shtdnClntOpts) defer conn.Close() // Set a big timeout so it would be easy to differ @@ -542,7 +548,7 @@ func TestGracefulShutdownConcurrent(t *testing.T) { go func(i int) { defer caseWg.Done() - conn := test_helpers.ConnectWithValidation(t, shtdnServer, shtdnClntOpts) + conn := test_helpers.ConnectWithValidation(t, shtdnDialer, shtdnClntOpts) defer conn.Close() // Create a helper watcher to ensure that async diff --git a/ssl.go b/ssl.go index 8ca430559..70f324df6 100644 --- a/ssl.go +++ b/ssl.go @@ -15,8 +15,37 @@ import ( "github.com/tarantool/go-openssl" ) +type sslOpts struct { + // KeyFile is a path to a private SSL key file. + KeyFile string + // CertFile is a path to an SSL certificate file. + CertFile string + // CaFile is a path to a trusted certificate authorities (CA) file. + CaFile string + // Ciphers is a colon-separated (:) list of SSL cipher suites the connection + // can use. + // + // We don't provide a list of supported ciphers. This is what OpenSSL + // does. The only limitation is usage of TLSv1.2 (because other protocol + // versions don't seem to support the GOST cipher). To add additional + // ciphers (GOST cipher), you must configure OpenSSL. + // + // See also + // + // * https://www.openssl.org/docs/man1.1.1/man1/ciphers.html + Ciphers string + // Password is a password for decrypting the private SSL key file. + // The priority is as follows: try to decrypt with Password, then + // try PasswordFile. + Password string + // PasswordFile is a path to the list of passwords for decrypting + // the private SSL key file. The connection tries every line from the + // file as a password. + PasswordFile string +} + func sslDialContext(ctx context.Context, network, address string, - opts SslOpts) (connection net.Conn, err error) { + opts sslOpts) (connection net.Conn, err error) { var sslCtx interface{} if sslCtx, err = sslCreateContext(opts); err != nil { return @@ -27,7 +56,7 @@ func sslDialContext(ctx context.Context, network, address string, // interface{} is a hack. It helps to avoid dependency of go-openssl in build // of tests with the tag 'go_tarantool_ssl_disable'. -func sslCreateContext(opts SslOpts) (ctx interface{}, err error) { +func sslCreateContext(opts sslOpts) (ctx interface{}, err error) { var sslCtx *openssl.Ctx // Require TLSv1.2, because other protocol versions don't seem to diff --git a/ssl_test.go b/ssl_test.go index 65be85504..61f6e131d 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -4,98 +4,19 @@ package tarantool_test import ( - "context" - "errors" "fmt" - "io/ioutil" - "net" "os" - "strconv" "strings" "testing" "time" - "github.com/tarantool/go-openssl" . "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/test_helpers" ) -const sslHost = "127.0.0.1" const tntHost = "127.0.0.1:3014" -func serverSsl(network, address string, opts SslOpts) (net.Listener, error) { - ctx, err := SslCreateContext(opts) - if err != nil { - return nil, errors.New("Unable to create SSL context: " + err.Error()) - } - - return openssl.Listen(network, address, ctx.(*openssl.Ctx)) -} - -func serverSslAccept(l net.Listener) (<-chan string, <-chan error) { - message := make(chan string, 1) - errors := make(chan error, 1) - - go func() { - conn, err := l.Accept() - if err != nil { - errors <- err - } else { - bytes, err := ioutil.ReadAll(conn) - if err != nil { - errors <- err - } else { - message <- string(bytes) - } - conn.Close() - } - - close(message) - close(errors) - }() - - return message, errors -} - -func serverSslRecv(msgs <-chan string, errs <-chan error) (string, error) { - return <-msgs, <-errs -} - -func clientSsl(ctx context.Context, network, address string, - opts SslOpts) (net.Conn, error) { - return SslDialContext(ctx, network, address, opts) -} - -func createClientServerSsl(ctx context.Context, t testing.TB, serverOpts, - clientOpts SslOpts) (net.Listener, net.Conn, <-chan string, <-chan error, error) { - t.Helper() - - l, err := serverSsl("tcp", sslHost+":0", serverOpts) - if err != nil { - t.Fatalf("Unable to create server, error %q", err.Error()) - } - - msgs, errs := serverSslAccept(l) - - port := l.Addr().(*net.TCPAddr).Port - c, err := clientSsl(ctx, "tcp", sslHost+":"+strconv.Itoa(port), clientOpts) - - return l, c, msgs, errs, err -} - -func createClientServerSslOk(ctx context.Context, t testing.TB, serverOpts, - clientOpts SslOpts) (net.Listener, net.Conn, <-chan string, <-chan error) { - t.Helper() - - l, c, msgs, errs, err := createClientServerSsl(ctx, t, serverOpts, clientOpts) - if err != nil { - t.Fatalf("Unable to create client, error %q", err.Error()) - } - - return l, c, msgs, errs -} - -func serverTnt(serverOpts SslOpts, auth Auth) (test_helpers.TarantoolInstance, error) { +func serverTnt(serverOpts SslTestOpts, auth Auth) (test_helpers.TarantoolInstance, error) { listen := tntHost + "?transport=ssl&" key := serverOpts.KeyFile @@ -130,37 +51,41 @@ func serverTnt(serverOpts SslOpts, auth Auth) (test_helpers.TarantoolInstance, e listen = listen[:len(listen)-1] - return test_helpers.StartTarantool(test_helpers.StartOpts{ - Auth: auth, - InitScript: "config.lua", - Listen: listen, - SslCertsDir: "testdata", - ClientServer: tntHost, - ClientTransport: "ssl", - ClientSsl: serverOpts, - User: "test", - Pass: "test", - WaitStart: 100 * time.Millisecond, - ConnectRetry: 10, - RetryTimeout: 500 * time.Millisecond, - }) + return test_helpers.StartTarantool( + test_helpers.StartOpts{ + Dialer: OpenSslDialer{ + Address: tntHost, + Auth: auth, + User: "test", + Password: "test", + SslKeyFile: serverOpts.KeyFile, + SslCertFile: serverOpts.CertFile, + SslCaFile: serverOpts.CaFile, + SslCiphers: serverOpts.Ciphers, + SslPassword: serverOpts.Password, + SslPasswordFile: serverOpts.PasswordFile, + }, + Auth: auth, + InitScript: "config.lua", + Listen: listen, + SslCertsDir: "testdata", + WaitStart: 100 * time.Millisecond, + ConnectRetry: 10, + RetryTimeout: 500 * time.Millisecond, + }, + ) } func serverTntStop(inst test_helpers.TarantoolInstance) { test_helpers.StopTarantoolWithCleanup(inst) } -func checkTntConn(clientOpts SslOpts) error { +func checkTntConn(dialer Dialer) error { ctx, cancel := test_helpers.GetConnectContext() defer cancel() - conn, err := Connect(ctx, tntHost, Opts{ - Auth: AutoAuth, + conn, err := Connect(ctx, dialer, Opts{ Timeout: 500 * time.Millisecond, - User: "test", - Pass: "test", SkipSchema: true, - Transport: "ssl", - Ssl: clientOpts, }) if err != nil { return err @@ -169,38 +94,7 @@ func checkTntConn(clientOpts SslOpts) error { return nil } -func assertConnectionSslFail(ctx context.Context, t testing.TB, serverOpts, - clientOpts SslOpts) { - t.Helper() - - l, c, _, _, err := createClientServerSsl(ctx, t, serverOpts, clientOpts) - l.Close() - if err == nil { - c.Close() - t.Errorf("An unexpected connection to the server.") - } -} - -func assertConnectionSslOk(ctx context.Context, t testing.TB, serverOpts, - clientOpts SslOpts) { - t.Helper() - - l, c, msgs, errs := createClientServerSslOk(ctx, t, serverOpts, clientOpts) - const message = "any test string" - c.Write([]byte(message)) - c.Close() - - recv, err := serverSslRecv(msgs, errs) - l.Close() - - if err != nil { - t.Errorf("An unexpected server error: %q", err.Error()) - } else if recv != message { - t.Errorf("An unexpected server message: %q, expected %q", recv, message) - } -} - -func assertConnectionTntFail(t testing.TB, serverOpts, clientOpts SslOpts) { +func assertConnectionTntFail(t testing.TB, serverOpts SslTestOpts, dialer OpenSslDialer) { t.Helper() inst, err := serverTnt(serverOpts, AutoAuth) @@ -209,13 +103,13 @@ func assertConnectionTntFail(t testing.TB, serverOpts, clientOpts SslOpts) { t.Fatalf("An unexpected server error %q", err.Error()) } - err = checkTntConn(clientOpts) + err = checkTntConn(dialer) if err == nil { t.Errorf("An unexpected connection to the server") } } -func assertConnectionTntOk(t testing.TB, serverOpts, clientOpts SslOpts) { +func assertConnectionTntOk(t testing.TB, serverOpts SslTestOpts, dialer OpenSslDialer) { t.Helper() inst, err := serverTnt(serverOpts, AutoAuth) @@ -224,17 +118,17 @@ func assertConnectionTntOk(t testing.TB, serverOpts, clientOpts SslOpts) { t.Fatalf("An unexpected server error %q", err.Error()) } - err = checkTntConn(clientOpts) + err = checkTntConn(dialer) if err != nil { t.Errorf("An unexpected connection error %q", err.Error()) } } -type test struct { +type sslTest struct { name string ok bool - serverOpts SslOpts - clientOpts SslOpts + serverOpts SslTestOpts + clientOpts SslTestOpts } /* @@ -253,24 +147,24 @@ CertFile - optional, mandatory if server.CaFile set CaFile - optional, Ciphers - optional */ -var tests = []test{ +var sslTests = []sslTest{ { "key_crt_server", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", }, - SslOpts{}, + SslTestOpts{}, }, { "key_crt_server_and_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", }, @@ -278,22 +172,22 @@ var tests = []test{ { "key_crt_ca_server", false, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{}, + SslTestOpts{}, }, { "key_crt_ca_server_key_crt_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", }, @@ -301,12 +195,12 @@ var tests = []test{ { "key_crt_ca_server_and_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", @@ -315,12 +209,12 @@ var tests = []test{ { "key_crt_ca_server_and_client_invalid_path_key", false, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "any_invalid_path", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", @@ -329,12 +223,12 @@ var tests = []test{ { "key_crt_ca_server_and_client_invalid_path_crt", false, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "any_invalid_path", CaFile: "testdata/ca.crt", @@ -343,12 +237,12 @@ var tests = []test{ { "key_crt_ca_server_and_client_invalid_path_ca", false, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "any_invalid_path", @@ -357,12 +251,12 @@ var tests = []test{ { "key_crt_ca_server_and_client_empty_key", false, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/empty", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", @@ -371,12 +265,12 @@ var tests = []test{ { "key_crt_ca_server_and_client_empty_crt", false, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/empty", CaFile: "testdata/ca.crt", @@ -385,12 +279,12 @@ var tests = []test{ { "key_crt_ca_server_and_client_empty_ca", false, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/empty", @@ -399,11 +293,11 @@ var tests = []test{ { "key_crt_server_and_key_crt_ca_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", @@ -412,13 +306,13 @@ var tests = []test{ { "key_crt_ca_ciphers_server_key_crt_ca_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", @@ -427,13 +321,13 @@ var tests = []test{ { "key_crt_ca_ciphers_server_and_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", @@ -443,13 +337,13 @@ var tests = []test{ { "non_equal_ciphers_client", false, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", @@ -459,12 +353,12 @@ var tests = []test{ { "pass_key_encrypt_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.enc.key", CertFile: "testdata/localhost.crt", Password: "mysslpassword", @@ -473,12 +367,12 @@ var tests = []test{ { "passfile_key_encrypt_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.enc.key", CertFile: "testdata/localhost.crt", PasswordFile: "testdata/passwords", @@ -487,12 +381,12 @@ var tests = []test{ { "pass_and_passfile_key_encrypt_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.enc.key", CertFile: "testdata/localhost.crt", Password: "mysslpassword", @@ -502,12 +396,12 @@ var tests = []test{ { "inv_pass_and_passfile_key_encrypt_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.enc.key", CertFile: "testdata/localhost.crt", Password: "invalidpassword", @@ -517,12 +411,12 @@ var tests = []test{ { "pass_and_inv_passfile_key_encrypt_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.enc.key", CertFile: "testdata/localhost.crt", Password: "mysslpassword", @@ -532,12 +426,12 @@ var tests = []test{ { "pass_and_not_existing_passfile_key_encrypt_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.enc.key", CertFile: "testdata/localhost.crt", Password: "mysslpassword", @@ -547,12 +441,12 @@ var tests = []test{ { "inv_pass_and_inv_passfile_key_encrypt_client", false, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.enc.key", CertFile: "testdata/localhost.crt", Password: "invalidpassword", @@ -562,12 +456,12 @@ var tests = []test{ { "not_existing_passfile_key_encrypt_client", false, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.enc.key", CertFile: "testdata/localhost.crt", PasswordFile: "testdata/notafile", @@ -576,12 +470,12 @@ var tests = []test{ { "no_pass_key_encrypt_client", false, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.enc.key", CertFile: "testdata/localhost.crt", }, @@ -589,12 +483,12 @@ var tests = []test{ { "pass_key_non_encrypt_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", Password: "invalidpassword", @@ -603,12 +497,12 @@ var tests = []test{ { "passfile_key_non_encrypt_client", true, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", CaFile: "testdata/ca.crt", }, - SslOpts{ + SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", PasswordFile: "testdata/invalidpasswords", @@ -622,67 +516,40 @@ func isTestTntSsl() bool { (testTntSsl == "1" || strings.ToUpper(testTntSsl) == "TRUE") } +func makeOpenSslDialer(opts SslTestOpts) OpenSslDialer { + return OpenSslDialer{ + Address: tntHost, + User: "test", + Password: "test", + SslKeyFile: opts.KeyFile, + SslCertFile: opts.CertFile, + SslCaFile: opts.CaFile, + SslCiphers: opts.Ciphers, + SslPassword: opts.Password, + SslPasswordFile: opts.PasswordFile, + } +} + func TestSslOpts(t *testing.T) { isTntSsl := isTestTntSsl() - for _, test := range tests { - var ctx context.Context - var cancel context.CancelFunc - ctx, cancel = test_helpers.GetConnectContext() - if test.ok { - t.Run("ok_ssl_"+test.name, func(t *testing.T) { - assertConnectionSslOk(ctx, t, test.serverOpts, test.clientOpts) - }) - } else { - t.Run("fail_ssl_"+test.name, func(t *testing.T) { - assertConnectionSslFail(ctx, t, test.serverOpts, test.clientOpts) - }) - } - cancel() + for _, test := range sslTests { if !isTntSsl { continue } + dialer := makeOpenSslDialer(test.clientOpts) if test.ok { t.Run("ok_tnt_"+test.name, func(t *testing.T) { - assertConnectionTntOk(t, test.serverOpts, test.clientOpts) + assertConnectionTntOk(t, test.serverOpts, dialer) }) } else { t.Run("fail_tnt_"+test.name, func(t *testing.T) { - assertConnectionTntFail(t, test.serverOpts, test.clientOpts) + assertConnectionTntFail(t, test.serverOpts, dialer) }) } } } -func TestSslDialContextCancel(t *testing.T) { - serverOpts := SslOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", - } - clientOpts := SslOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", - } - ctx, cancel := context.WithCancel(context.Background()) - cancel() - - l, c, _, _, err := createClientServerSsl(ctx, t, serverOpts, clientOpts) - l.Close() - - if err == nil { - c.Close() - t.Fatalf("Expected error, dial was not canceled") - } - if !strings.Contains(err.Error(), "operation was canceled") { - t.Fatalf("Unexpected error, expected to contain %s, got %v", - "operation was canceled", err) - } -} - func TestOpts_PapSha256Auth(t *testing.T) { isTntSsl := isTestTntSsl() if !isTntSsl { @@ -691,13 +558,13 @@ func TestOpts_PapSha256Auth(t *testing.T) { isLess, err := test_helpers.IsTarantoolVersionLess(2, 11, 0) if err != nil { - t.Fatalf("Could not check Tarantool version.") + t.Fatalf("Could not check Tarantool version: %s", err) } if isLess { - t.Skip("Skipping test for Tarantoo without pap-sha256 support") + t.Skip("Skipping test for Tarantool without pap-sha256 support") } - sslOpts := SslOpts{ + sslOpts := SslTestOpts{ KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", } @@ -708,14 +575,20 @@ func TestOpts_PapSha256Auth(t *testing.T) { t.Fatalf("An unexpected server error %q", err.Error()) } - clientOpts := opts - clientOpts.Transport = "ssl" - clientOpts.Ssl = sslOpts - clientOpts.Auth = PapSha256Auth - conn := test_helpers.ConnectWithValidation(t, tntHost, clientOpts) + client := OpenSslDialer{ + Address: tntHost, + Auth: PapSha256Auth, + User: "test", + Password: "test", + RequiredProtocolInfo: ProtocolInfo{}, + SslKeyFile: sslOpts.KeyFile, + SslCertFile: sslOpts.CertFile, + } + + conn := test_helpers.ConnectWithValidation(t, client, opts) conn.Close() - clientOpts.Auth = AutoAuth - conn = test_helpers.ConnectWithValidation(t, tntHost, clientOpts) + client.Auth = AutoAuth + conn = test_helpers.ConnectWithValidation(t, client, opts) conn.Close() } diff --git a/tarantool_test.go b/tarantool_test.go index f511376c2..e54c6a46d 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -25,15 +25,20 @@ import ( ) var startOpts test_helpers.StartOpts = test_helpers.StartOpts{ + Dialer: dialer, InitScript: "config.lua", Listen: server, - User: opts.User, - Pass: opts.Pass, WaitStart: 100 * time.Millisecond, ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, } +var dialer = NetDialer{ + Address: server, + User: "test", + Password: "test", +} + type Member struct { Name string Nonce string @@ -78,8 +83,6 @@ var indexNo = uint32(0) var indexName = "primary" var opts = Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", //Concurrency: 32, //RateLimit: 4*1024, } @@ -89,7 +92,7 @@ const N = 500 func BenchmarkClientSerial(b *testing.B) { var err error - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -109,7 +112,7 @@ func BenchmarkClientSerial(b *testing.B) { func BenchmarkClientSerialRequestObject(b *testing.B) { var err error - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -136,7 +139,7 @@ func BenchmarkClientSerialRequestObject(b *testing.B) { func BenchmarkClientSerialRequestObjectWithContext(b *testing.B) { var err error - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -165,7 +168,7 @@ func BenchmarkClientSerialRequestObjectWithContext(b *testing.B) { func BenchmarkClientSerialTyped(b *testing.B) { var err error - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -186,7 +189,7 @@ func BenchmarkClientSerialTyped(b *testing.B) { func BenchmarkClientSerialSQL(b *testing.B) { test_helpers.SkipIfSQLUnsupported(b) - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) @@ -208,7 +211,7 @@ func BenchmarkClientSerialSQL(b *testing.B) { func BenchmarkClientSerialSQLPrepared(b *testing.B) { test_helpers.SkipIfSQLUnsupported(b) - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) @@ -240,7 +243,7 @@ func BenchmarkClientSerialSQLPrepared(b *testing.B) { func BenchmarkClientFuture(b *testing.B) { var err error - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -267,7 +270,7 @@ func BenchmarkClientFuture(b *testing.B) { func BenchmarkClientFutureTyped(b *testing.B) { var err error - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -297,7 +300,7 @@ func BenchmarkClientFutureTyped(b *testing.B) { func BenchmarkClientFutureParallel(b *testing.B) { var err error - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -330,7 +333,7 @@ func BenchmarkClientFutureParallel(b *testing.B) { func BenchmarkClientFutureParallelTyped(b *testing.B) { var err error - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -366,7 +369,7 @@ func BenchmarkClientFutureParallelTyped(b *testing.B) { } func BenchmarkClientParallel(b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -387,7 +390,7 @@ func BenchmarkClientParallel(b *testing.B) { } func benchmarkClientParallelRequestObject(multiplier int, b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -415,7 +418,7 @@ func benchmarkClientParallelRequestObject(multiplier int, b *testing.B) { } func benchmarkClientParallelRequestObjectWithContext(multiplier int, b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -447,7 +450,7 @@ func benchmarkClientParallelRequestObjectWithContext(multiplier int, b *testing. } func benchmarkClientParallelRequestObjectMixed(multiplier int, b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -486,7 +489,7 @@ func benchmarkClientParallelRequestObjectMixed(multiplier int, b *testing.B) { func BenchmarkClientParallelRequestObject(b *testing.B) { multipliers := []int{10, 50, 500, 1000} - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -512,7 +515,7 @@ func BenchmarkClientParallelRequestObject(b *testing.B) { } func BenchmarkClientParallelMassive(b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -547,7 +550,7 @@ func BenchmarkClientParallelMassive(b *testing.B) { } func BenchmarkClientParallelMassiveUntyped(b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) @@ -582,7 +585,7 @@ func BenchmarkClientParallelMassiveUntyped(b *testing.B) { } func BenchmarkClientReplaceParallel(b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() b.ResetTimer() @@ -597,7 +600,7 @@ func BenchmarkClientReplaceParallel(b *testing.B) { } func BenchmarkClientLargeSelectParallel(b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() offset, limit := uint32(0), uint32(1000) @@ -616,7 +619,7 @@ func BenchmarkClientLargeSelectParallel(b *testing.B) { func BenchmarkClientParallelSQL(b *testing.B) { test_helpers.SkipIfSQLUnsupported(b) - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) @@ -640,7 +643,7 @@ func BenchmarkClientParallelSQL(b *testing.B) { func BenchmarkClientParallelSQLPrepared(b *testing.B) { test_helpers.SkipIfSQLUnsupported(b) - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) @@ -674,7 +677,7 @@ func BenchmarkClientParallelSQLPrepared(b *testing.B) { func BenchmarkSQLSerial(b *testing.B) { test_helpers.SkipIfSQLUnsupported(b) - conn := test_helpers.ConnectWithValidation(b, server, opts) + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) @@ -693,19 +696,18 @@ func BenchmarkSQLSerial(b *testing.B) { } } -func TestTtDialer(t *testing.T) { +func TestNetDialer(t *testing.T) { assert := assert.New(t) require := require.New(t) ctx, cancel := test_helpers.GetConnectContext() defer cancel() - conn, err := TtDialer{}.Dial(ctx, server, DialOpts{}) + conn, err := dialer.Dial(ctx, DialOpts{}) require.Nil(err) require.NotNil(conn) defer conn.Close() - assert.Contains(conn.LocalAddr().String(), "127.0.0.1") - assert.Equal(server, conn.RemoteAddr().String()) + assert.Equal(server, conn.Addr().String()) assert.NotEqual("", conn.Greeting().Version) // Write IPROTO_PING. @@ -738,53 +740,8 @@ func TestTtDialer(t *testing.T) { assert.Equal([]byte{0x83, 0x00, 0xce, 0x00, 0x00, 0x00, 0x00}, buf[:7]) } -func TestTtDialer_worksWithConnection(t *testing.T) { - defaultOpts := opts - defaultOpts.Dialer = TtDialer{} - - conn := test_helpers.ConnectWithValidation(t, server, defaultOpts) - defer conn.Close() - - _, err := conn.Do(NewPingRequest()).Get() - assert.Nil(t, err) -} - -func TestOptsAuth_Default(t *testing.T) { - defaultOpts := opts - defaultOpts.Auth = AutoAuth - - conn := test_helpers.ConnectWithValidation(t, server, defaultOpts) - defer conn.Close() -} - -func TestOptsAuth_ChapSha1Auth(t *testing.T) { - chapSha1Opts := opts - chapSha1Opts.Auth = ChapSha1Auth - - conn := test_helpers.ConnectWithValidation(t, server, chapSha1Opts) - defer conn.Close() -} - -func TestOptsAuth_PapSha256AuthForbit(t *testing.T) { - papSha256Opts := opts - papSha256Opts.Auth = PapSha256Auth - - ctx, cancel := test_helpers.GetConnectContext() - defer cancel() - conn, err := Connect(ctx, server, papSha256Opts) - if err == nil { - t.Error("An error expected.") - conn.Close() - } - - if err.Error() != "failed to authenticate: forbidden to use pap-sha256"+ - " unless SSL is enabled for the connection" { - t.Errorf("An unexpected error: %s", err) - } -} - func TestFutureMultipleGetGetTyped(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() fut := conn.Call17Async("simple_concat", []interface{}{"1"}) @@ -822,7 +779,7 @@ func TestFutureMultipleGetGetTyped(t *testing.T) { } func TestFutureMultipleGetWithError(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() fut := conn.Call17Async("non_exist", []interface{}{"1"}) @@ -835,7 +792,7 @@ func TestFutureMultipleGetWithError(t *testing.T) { } func TestFutureMultipleGetTypedWithError(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() fut := conn.Call17Async("simple_concat", []interface{}{"1"}) @@ -864,7 +821,7 @@ func TestClient(t *testing.T) { var resp *Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Ping @@ -1204,7 +1161,7 @@ func TestClient(t *testing.T) { } func TestClientSessionPush(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() var it ResponseIterator @@ -1358,7 +1315,7 @@ func TestSQL(t *testing.T) { selectSpanDifQuery := selectSpanDifQueryNew if isSeqScanOld, err := test_helpers.IsTarantoolVersionLess(3, 0, 0); err != nil { - t.Fatal("Could not check the Tarantool version") + t.Fatalf("Could not check the Tarantool version: %s", err) } else if isSeqScanOld { selectSpanDifQuery = selectSpanDifQueryOld } @@ -1504,7 +1461,7 @@ func TestSQL(t *testing.T) { }, } - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() for i, test := range testCases { @@ -1535,7 +1492,7 @@ func TestSQL(t *testing.T) { func TestSQLTyped(t *testing.T) { test_helpers.SkipIfSQLUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() mem := []Member{} @@ -1564,7 +1521,7 @@ func TestSQLBindings(t *testing.T) { var resp *Response - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // test all types of supported bindings @@ -1666,7 +1623,7 @@ func TestStressSQL(t *testing.T) { var resp *Response - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() resp, err := conn.Execute(createTableQuery, []interface{}{}) @@ -1763,7 +1720,7 @@ func TestStressSQL(t *testing.T) { func TestNewPrepared(t *testing.T) { test_helpers.SkipIfSQLUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() stmt, err := conn.NewPrepared(selectNamedQuery2) @@ -1852,7 +1809,7 @@ func TestConnection_DoWithStrangerConn(t *testing.T) { } func TestGetSchema(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() s, err := GetSchema(conn) @@ -1865,7 +1822,7 @@ func TestGetSchema(t *testing.T) { } func TestConnection_SetSchema_Changes(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() req := NewInsertRequest(spaceName) @@ -1929,7 +1886,7 @@ func TestNewPreparedFromResponse(t *testing.T) { } func TestSchema(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Schema @@ -2079,7 +2036,7 @@ func TestSchema(t *testing.T) { } func TestSchema_IsNullable(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() schema, err := GetSchema(conn) @@ -2118,7 +2075,7 @@ func TestClientNamed(t *testing.T) { var resp *Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Insert @@ -2215,7 +2172,7 @@ func TestClientRequestObjects(t *testing.T) { err error ) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Ping @@ -2506,7 +2463,7 @@ func TestClientRequestObjects(t *testing.T) { // Tarantool supports SQL since version 2.0.0 isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) if err != nil { - t.Fatalf("Could not check the Tarantool version") + t.Fatalf("Could not check the Tarantool version: %s", err) } if isLess { return @@ -2609,7 +2566,7 @@ func testConnectionDoSelectRequestCheck(t *testing.T, } func TestConnectionDoSelectRequest(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() testConnectionDoSelectRequestPrepare(t, conn) @@ -2627,7 +2584,7 @@ func TestConnectionDoSelectRequest(t *testing.T) { func TestConnectionDoWatchOnceRequest(t *testing.T) { test_helpers.SkipIfWatchOnceUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() _, err := conn.Do(NewBroadcastRequest("hello").Value("world")).Get() @@ -2650,13 +2607,13 @@ func TestConnectionDoWatchOnceRequest(t *testing.T) { func TestConnectionDoWatchOnceOnEmptyKey(t *testing.T) { watchOnceNotSupported, err := test_helpers.IsTarantoolVersionLess(3, 0, 0) if err != nil { - log.Fatalf("Could not check the Tarantool version") + log.Fatalf("Could not check the Tarantool version: %s", err) } if watchOnceNotSupported { return } - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() resp, err := conn.Do(NewWatchOnceRequest("notexists!")).Get() @@ -2674,7 +2631,7 @@ func TestConnectionDoWatchOnceOnEmptyKey(t *testing.T) { func TestConnectionDoSelectRequest_fetch_pos(t *testing.T) { test_helpers.SkipIfPaginationUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() testConnectionDoSelectRequestPrepare(t, conn) @@ -2693,7 +2650,7 @@ func TestConnectionDoSelectRequest_fetch_pos(t *testing.T) { func TestConnectDoSelectRequest_after_tuple(t *testing.T) { test_helpers.SkipIfPaginationUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() testConnectionDoSelectRequestPrepare(t, conn) @@ -2713,7 +2670,7 @@ func TestConnectDoSelectRequest_after_tuple(t *testing.T) { func TestConnectionDoSelectRequest_pagination_pos(t *testing.T) { test_helpers.SkipIfPaginationUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() testConnectionDoSelectRequestPrepare(t, conn) @@ -2737,7 +2694,7 @@ func TestConnection_Call(t *testing.T) { var resp *Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() resp, err = conn.Call("simple_concat", []interface{}{"1"}) @@ -2753,7 +2710,7 @@ func TestCallRequest(t *testing.T) { var resp *Response var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() req := NewCallRequest("simple_concat").Args([]interface{}{"1"}) @@ -2767,7 +2724,7 @@ func TestCallRequest(t *testing.T) { } func TestClientRequestObjectsWithNilContext(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() req := NewPingRequest().Context(nil) //nolint resp, err := conn.Do(req).Get() @@ -2783,7 +2740,7 @@ func TestClientRequestObjectsWithNilContext(t *testing.T) { } func TestClientRequestObjectsWithPassedCanceledContext(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() ctx, cancel := context.WithCancel(context.Background()) @@ -2825,7 +2782,7 @@ func (req *waitCtxRequest) Async() bool { func TestClientRequestObjectsWithContext(t *testing.T) { var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() ctx, cancel := context.WithCancel(context.Background()) @@ -2864,7 +2821,7 @@ func TestClientRequestObjectsWithContext(t *testing.T) { func TestComplexStructs(t *testing.T) { var err error - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} @@ -2895,7 +2852,7 @@ func TestComplexStructs(t *testing.T) { func TestStream_IdValues(t *testing.T) { test_helpers.SkipIfStreamsUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() cases := []uint64{ @@ -2932,7 +2889,7 @@ func TestStream_Commit(t *testing.T) { test_helpers.SkipIfStreamsUnsupported(t) - conn = test_helpers.ConnectWithValidation(t, server, opts) + conn = test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() stream, _ := conn.NewStream() @@ -3047,7 +3004,7 @@ func TestStream_Rollback(t *testing.T) { test_helpers.SkipIfStreamsUnsupported(t) - conn = test_helpers.ConnectWithValidation(t, server, opts) + conn = test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() stream, _ := conn.NewStream() @@ -3168,7 +3125,7 @@ func TestStream_TxnIsolationLevel(t *testing.T) { test_helpers.SkipIfStreamsUnsupported(t) - conn = test_helpers.ConnectWithValidation(t, server, opts) + conn = test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() stream, _ := conn.NewStream() @@ -3269,7 +3226,7 @@ func TestStream_DoWithClosedConn(t *testing.T) { test_helpers.SkipIfStreamsUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) stream, _ := conn.NewStream() conn.Close() @@ -3288,7 +3245,7 @@ func TestStream_DoWithClosedConn(t *testing.T) { func TestConnectionProtocolInfoSupported(t *testing.T) { test_helpers.SkipIfIdUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // First Tarantool protocol version (1, IPROTO_FEATURE_STREAMS and @@ -3309,23 +3266,7 @@ func TestConnectionProtocolInfoSupported(t *testing.T) { }, } - clientProtocolInfo := conn.ClientProtocolInfo() - require.Equal(t, - clientProtocolInfo, - ProtocolInfo{ - Version: ProtocolVersion(6), - Features: []iproto.Feature{ - iproto.IPROTO_FEATURE_STREAMS, - iproto.IPROTO_FEATURE_TRANSACTIONS, - iproto.IPROTO_FEATURE_ERROR_EXTENSION, - iproto.IPROTO_FEATURE_WATCHERS, - iproto.IPROTO_FEATURE_PAGINATION, - iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, - iproto.IPROTO_FEATURE_WATCH_ONCE, - }, - }) - - serverProtocolInfo := conn.ServerProtocolInfo() + serverProtocolInfo := conn.ProtocolInfo() require.GreaterOrEqual(t, serverProtocolInfo.Version, tarantool210ProtocolInfo.Version) @@ -3337,7 +3278,7 @@ func TestConnectionProtocolInfoSupported(t *testing.T) { func TestClientIdRequestObject(t *testing.T) { test_helpers.SkipIfIdUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() tarantool210ProtocolInfo := ProtocolInfo{ @@ -3373,7 +3314,7 @@ func TestClientIdRequestObject(t *testing.T) { func TestClientIdRequestObjectWithNilContext(t *testing.T) { test_helpers.SkipIfIdUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() tarantool210ProtocolInfo := ProtocolInfo{ @@ -3407,7 +3348,7 @@ func TestClientIdRequestObjectWithNilContext(t *testing.T) { } func TestClientIdRequestObjectWithPassedCanceledContext(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() ctx, cancel := context.WithCancel(context.Background()) @@ -3425,66 +3366,41 @@ func TestClientIdRequestObjectWithPassedCanceledContext(t *testing.T) { func TestConnectionProtocolInfoUnsupported(t *testing.T) { test_helpers.SkipIfIdSupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() - clientProtocolInfo := conn.ClientProtocolInfo() - require.Equal(t, - clientProtocolInfo, - ProtocolInfo{ - Version: ProtocolVersion(6), - Features: []iproto.Feature{ - iproto.IPROTO_FEATURE_STREAMS, - iproto.IPROTO_FEATURE_TRANSACTIONS, - iproto.IPROTO_FEATURE_ERROR_EXTENSION, - iproto.IPROTO_FEATURE_WATCHERS, - iproto.IPROTO_FEATURE_PAGINATION, - iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, - iproto.IPROTO_FEATURE_WATCH_ONCE, - }, - }) - - serverProtocolInfo := conn.ServerProtocolInfo() - require.Equal(t, serverProtocolInfo, ProtocolInfo{}) -} - -func TestConnectionClientFeaturesUmmutable(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) - defer conn.Close() - - info := conn.ClientProtocolInfo() - infoOrig := info.Clone() - info.Features[0] = iproto.Feature(15532) - - require.Equal(t, conn.ClientProtocolInfo(), infoOrig) - require.NotEqual(t, conn.ClientProtocolInfo(), info) + serverProtocolInfo := conn.ProtocolInfo() + expected := ProtocolInfo{ + Auth: ChapSha1Auth, + } + require.Equal(t, expected, serverProtocolInfo) } -func TestConnectionServerFeaturesUmmutable(t *testing.T) { +func TestConnectionServerFeaturesImmutable(t *testing.T) { test_helpers.SkipIfIdUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() - info := conn.ServerProtocolInfo() + info := conn.ProtocolInfo() infoOrig := info.Clone() info.Features[0] = iproto.Feature(15532) - require.Equal(t, conn.ServerProtocolInfo(), infoOrig) - require.NotEqual(t, conn.ServerProtocolInfo(), info) + require.Equal(t, conn.ProtocolInfo(), infoOrig) + require.NotEqual(t, conn.ProtocolInfo(), info) } func TestConnectionProtocolVersionRequirementSuccess(t *testing.T) { test_helpers.SkipIfIdUnsupported(t) - connOpts := opts.Clone() - connOpts.RequiredProtocolInfo = ProtocolInfo{ + testDialer := dialer + testDialer.RequiredProtocolInfo = ProtocolInfo{ Version: ProtocolVersion(3), } ctx, cancel := test_helpers.GetConnectContext() defer cancel() - conn, err := Connect(ctx, server, connOpts) + conn, err := Connect(ctx, testDialer, opts) require.Nilf(t, err, "No errors on connect") require.NotNilf(t, conn, "Connect success") @@ -3495,14 +3411,14 @@ func TestConnectionProtocolVersionRequirementSuccess(t *testing.T) { func TestConnectionProtocolVersionRequirementFail(t *testing.T) { test_helpers.SkipIfIdSupported(t) - connOpts := opts.Clone() - connOpts.RequiredProtocolInfo = ProtocolInfo{ + testDialer := dialer + testDialer.RequiredProtocolInfo = ProtocolInfo{ Version: ProtocolVersion(3), } ctx, cancel := test_helpers.GetConnectContext() defer cancel() - conn, err := Connect(ctx, server, connOpts) + conn, err := Connect(ctx, testDialer, opts) require.Nilf(t, conn, "Connect fail") require.NotNilf(t, err, "Got error on connect") @@ -3512,14 +3428,14 @@ func TestConnectionProtocolVersionRequirementFail(t *testing.T) { func TestConnectionProtocolFeatureRequirementSuccess(t *testing.T) { test_helpers.SkipIfIdUnsupported(t) - connOpts := opts.Clone() - connOpts.RequiredProtocolInfo = ProtocolInfo{ + testDialer := dialer + testDialer.RequiredProtocolInfo = ProtocolInfo{ Features: []iproto.Feature{iproto.IPROTO_FEATURE_TRANSACTIONS}, } ctx, cancel := test_helpers.GetConnectContext() defer cancel() - conn, err := Connect(ctx, server, connOpts) + conn, err := Connect(ctx, testDialer, opts) require.NotNilf(t, conn, "Connect success") require.Nilf(t, err, "No errors on connect") @@ -3530,14 +3446,14 @@ func TestConnectionProtocolFeatureRequirementSuccess(t *testing.T) { func TestConnectionProtocolFeatureRequirementFail(t *testing.T) { test_helpers.SkipIfIdSupported(t) - connOpts := opts.Clone() - connOpts.RequiredProtocolInfo = ProtocolInfo{ + testDialer := dialer + testDialer.RequiredProtocolInfo = ProtocolInfo{ Features: []iproto.Feature{iproto.IPROTO_FEATURE_TRANSACTIONS}, } ctx, cancel := test_helpers.GetConnectContext() defer cancel() - conn, err := Connect(ctx, server, connOpts) + conn, err := Connect(ctx, testDialer, opts) require.Nilf(t, conn, "Connect fail") require.NotNilf(t, err, "Got error on connect") @@ -3549,15 +3465,15 @@ func TestConnectionProtocolFeatureRequirementFail(t *testing.T) { func TestConnectionProtocolFeatureRequirementManyFail(t *testing.T) { test_helpers.SkipIfIdSupported(t) - connOpts := opts.Clone() - connOpts.RequiredProtocolInfo = ProtocolInfo{ + testDialer := dialer + testDialer.RequiredProtocolInfo = ProtocolInfo{ Features: []iproto.Feature{iproto.IPROTO_FEATURE_TRANSACTIONS, iproto.Feature(15532)}, } ctx, cancel := test_helpers.GetConnectContext() defer cancel() - conn, err := Connect(ctx, server, connOpts) + conn, err := Connect(ctx, testDialer, opts) require.Nilf(t, conn, "Connect fail") require.NotNilf(t, err, "Got error on connect") @@ -3567,49 +3483,10 @@ func TestConnectionProtocolFeatureRequirementManyFail(t *testing.T) { "Feature(15532) are not supported") } -func TestConnectionFeatureOptsImmutable(t *testing.T) { - test_helpers.SkipIfIdUnsupported(t) - - restartOpts := startOpts - restartOpts.Listen = "127.0.0.1:3014" - inst, err := test_helpers.StartTarantool(restartOpts) - defer test_helpers.StopTarantoolWithCleanup(inst) - - if err != nil { - log.Printf("Failed to prepare test tarantool: %s", err) - return - } - - retries := uint(10) - timeout := 100 * time.Millisecond - - connOpts := opts.Clone() - connOpts.Reconnect = timeout - connOpts.MaxReconnects = retries - connOpts.RequiredProtocolInfo = ProtocolInfo{ - Features: []iproto.Feature{iproto.IPROTO_FEATURE_TRANSACTIONS}, - } - - // Connect with valid opts - conn := test_helpers.ConnectWithValidation(t, server, connOpts) - defer conn.Close() - - // Change opts outside - connOpts.RequiredProtocolInfo.Features[0] = iproto.Feature(15532) - - // Trigger reconnect with opts re-check - test_helpers.StopTarantool(inst) - err = test_helpers.RestartTarantool(&inst) - require.Nilf(t, err, "Failed to restart tarantool") - - connected := test_helpers.WaitUntilReconnected(conn, retries, timeout) - require.True(t, connected, "Reconnect success") -} - func TestErrorExtendedInfoBasic(t *testing.T) { test_helpers.SkipIfErrorExtendedInfoUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() _, err := conn.Eval("not a Lua code", []interface{}{}) @@ -3637,7 +3514,7 @@ func TestErrorExtendedInfoBasic(t *testing.T) { func TestErrorExtendedInfoStack(t *testing.T) { test_helpers.SkipIfErrorExtendedInfoUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() _, err := conn.Eval("error(chained_error)", []interface{}{}) @@ -3673,7 +3550,7 @@ func TestErrorExtendedInfoStack(t *testing.T) { func TestErrorExtendedInfoFields(t *testing.T) { test_helpers.SkipIfErrorExtendedInfoUnsupported(t) - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() _, err := conn.Eval("error(access_denied_error)", []interface{}{}) @@ -3707,11 +3584,7 @@ func TestConnection_NewWatcher(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) const key = "TestConnection_NewWatcher" - connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - conn := test_helpers.ConnectWithValidation(t, server, connOpts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() events := make(chan WatchEvent) @@ -3742,17 +3615,19 @@ func TestConnection_NewWatcher(t *testing.T) { } func TestConnection_NewWatcher_noWatchersFeature(t *testing.T) { + test_helpers.SkipIfWatchersSupported(t) + const key = "TestConnection_NewWatcher_noWatchersFeature" - connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []iproto.Feature{} - conn := test_helpers.ConnectWithValidation(t, server, connOpts) + testDialer := dialer + testDialer.RequiredProtocolInfo = ProtocolInfo{Features: []iproto.Feature{}} + conn := test_helpers.ConnectWithValidation(t, testDialer, opts) defer conn.Close() watcher, err := conn.NewWatcher(key, func(event WatchEvent) {}) require.Nilf(t, watcher, "watcher must not be created") require.NotNilf(t, err, "an error is expected") - expected := "the feature IPROTO_FEATURE_WATCHERS must be required by " + - "connection options to create a watcher" + expected := "the feature IPROTO_FEATURE_WATCHERS must be supported by " + + "connection to create a watcher" require.Equal(t, expected, err.Error()) } @@ -3762,11 +3637,13 @@ func TestConnection_NewWatcher_reconnect(t *testing.T) { const key = "TestConnection_NewWatcher_reconnect" const server = "127.0.0.1:3014" + testDialer := dialer + testDialer.Address = server + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + Dialer: testDialer, InitScript: "config.lua", Listen: server, - User: opts.User, - Pass: opts.Pass, WaitStart: 100 * time.Millisecond, ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, @@ -3779,10 +3656,8 @@ func TestConnection_NewWatcher_reconnect(t *testing.T) { reconnectOpts := opts reconnectOpts.Reconnect = 100 * time.Millisecond reconnectOpts.MaxReconnects = 10 - reconnectOpts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - conn := test_helpers.ConnectWithValidation(t, server, reconnectOpts) + + conn := test_helpers.ConnectWithValidation(t, testDialer, reconnectOpts) defer conn.Close() events := make(chan WatchEvent) @@ -3816,11 +3691,7 @@ func TestBroadcastRequest(t *testing.T) { const key = "TestBroadcastRequest" const value = "bar" - connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - conn := test_helpers.ConnectWithValidation(t, server, connOpts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() resp, err := conn.Do(NewBroadcastRequest(key).Value(value)).Get() @@ -3866,11 +3737,7 @@ func TestBroadcastRequest_multi(t *testing.T) { const key = "TestBroadcastRequest_multi" - connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - conn := test_helpers.ConnectWithValidation(t, server, connOpts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() events := make(chan WatchEvent) @@ -3914,11 +3781,7 @@ func TestConnection_NewWatcher_multiOnKey(t *testing.T) { const key = "TestConnection_NewWatcher_multiOnKey" const value = "bar" - connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - conn := test_helpers.ConnectWithValidation(t, server, connOpts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() events := []chan WatchEvent{ @@ -3977,11 +3840,7 @@ func TestWatcher_Unregister(t *testing.T) { const key = "TestWatcher_Unregister" const value = "bar" - connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - conn := test_helpers.ConnectWithValidation(t, server, connOpts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() events := make(chan WatchEvent) @@ -4013,11 +3872,8 @@ func TestConnection_NewWatcher_concurrent(t *testing.T) { const testConcurrency = 1000 const key = "TestConnection_NewWatcher_concurrent" - connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - conn := test_helpers.ConnectWithValidation(t, server, connOpts) + + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() var wg sync.WaitGroup @@ -4058,11 +3914,8 @@ func TestWatcher_Unregister_concurrent(t *testing.T) { const testConcurrency = 1000 const key = "TestWatcher_Unregister_concurrent" - connOpts := opts.Clone() - connOpts.RequiredProtocolInfo.Features = []iproto.Feature{ - iproto.IPROTO_FEATURE_WATCHERS, - } - conn := test_helpers.ConnectWithValidation(t, server, connOpts) + + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() watcher, err := conn.NewWatcher(key, func(event WatchEvent) {}) @@ -4083,7 +3936,7 @@ func TestWatcher_Unregister_concurrent(t *testing.T) { } func TestConnect_schema_update(t *testing.T) { - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() for i := 0; i < 100; i++ { @@ -4091,7 +3944,7 @@ func TestConnect_schema_update(t *testing.T) { ctx, cancel := test_helpers.GetConnectContext() defer cancel() - if conn, err := Connect(ctx, server, opts); err != nil { + if conn, err := Connect(ctx, dialer, opts); err != nil { if err.Error() != "concurrent schema update" { t.Errorf("unexpected error: %s", err) } @@ -4110,8 +3963,6 @@ func TestConnect_schema_update(t *testing.T) { func TestConnect_context_cancel(t *testing.T) { var connLongReconnectOpts = Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", Reconnect: time.Second, MaxReconnects: 100, } @@ -4122,7 +3973,7 @@ func TestConnect_context_cancel(t *testing.T) { var err error cancel() - conn, err = Connect(ctx, server, connLongReconnectOpts) + conn, err = Connect(ctx, dialer, connLongReconnectOpts) if conn != nil || err == nil { t.Fatalf("Connection was created after cancel") @@ -4142,7 +3993,7 @@ func runTestMain(m *testing.M) int { // Tarantool supports streams and interactive transactions since version 2.10.0 isStreamUnsupported, err := test_helpers.IsTarantoolVersionLess(2, 10, 0) if err != nil { - log.Fatalf("Could not check the Tarantool version") + log.Fatalf("Could not check the Tarantool version: %s", err) } startOpts.MemtxUseMvccEngine = !isStreamUnsupported diff --git a/test_helpers/main.go b/test_helpers/main.go index cc806a7d2..d81ce3d1b 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -39,18 +39,6 @@ type StartOpts struct { // https://www.tarantool.io/en/doc/latest/reference/configuration/#cfg-basic-listen Listen string - // ClientServer changes a host to connect to test startup of a Tarantool - // instance. By default, it uses Listen value as the host for the connection. - ClientServer string - - // ClientTransport changes Opts.Transport for a connection that checks startup - // of a Tarantool instance. - ClientTransport string - - // ClientSsl changes Opts.Ssl for a connection that checks startup of - // a Tarantool instance. - ClientSsl tarantool.SslOpts - // WorkDir is box.cfg work_dir parameter for a Tarantool instance: // a folder to store data files. If not specified, helpers create a // new temporary directory. @@ -62,13 +50,6 @@ type StartOpts struct { // copied to the working directory. SslCertsDir string - // User is a username used to connect to tarantool. - // All required grants must be given in InitScript. - User string - - // Pass is a password for specified User. - Pass string - // WaitStart is a time to wait before starting to ping tarantool. WaitStart time.Duration @@ -82,6 +63,9 @@ type StartOpts struct { // MemtxUseMvccEngine is flag to enable transactional // manager if set to true. MemtxUseMvccEngine bool + + // Dialer to check that connection established. + Dialer tarantool.Dialer } // TarantoolInstance is a data for instance graceful shutdown and cleanup. @@ -91,16 +75,19 @@ type TarantoolInstance struct { // Options for restarting a tarantool instance. Opts StartOpts + + // Dialer to check that connection established. + Dialer tarantool.Dialer } -func isReady(server string, opts *tarantool.Opts) error { +func isReady(dialer tarantool.Dialer, opts *tarantool.Opts) error { var err error var conn *tarantool.Connection var resp *tarantool.Response ctx, cancel := GetConnectContext() defer cancel() - conn, err = tarantool.Connect(ctx, server, *opts) + conn, err = tarantool.Connect(ctx, dialer, *opts) if err != nil { return err } @@ -199,6 +186,8 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { var dir string var err error + inst.Dialer = startOpts.Dialer + if startOpts.WorkDir == "" { // Create work_dir for a new instance. // TO DO: replace with `os.MkdirTemp` when we drop support of @@ -255,24 +244,13 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { time.Sleep(startOpts.WaitStart) opts := tarantool.Opts{ - Auth: startOpts.Auth, Timeout: 500 * time.Millisecond, - User: startOpts.User, - Pass: startOpts.Pass, SkipSchema: true, - Transport: startOpts.ClientTransport, - Ssl: startOpts.ClientSsl, } var i int - var server string - if startOpts.ClientServer != "" { - server = startOpts.ClientServer - } else { - server = startOpts.Listen - } for i = 0; i <= startOpts.ConnectRetry; i++ { - err = isReady(server, &opts) + err = isReady(inst.Dialer, &opts) // Both connect and ping is ok. if err == nil { @@ -336,6 +314,9 @@ func copyDirectoryFiles(scrDir, dest string) error { return err } for _, entry := range entries { + if entry.IsDir() { + continue + } sourcePath := filepath.Join(scrDir, entry.Name()) destPath := filepath.Join(dest, entry.Name()) _, err := os.Stat(sourcePath) diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index b2340ccb8..fb59418d5 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -52,9 +52,9 @@ func CheckPoolStatuses(args interface{}) error { checkArgs.ExpectedPoolStatus, connected) } - poolInfo := checkArgs.ConnPool.GetPoolInfo() + poolInfo := checkArgs.ConnPool.GetInfo() for _, server := range checkArgs.Servers { - status := poolInfo[server] != nil && poolInfo[server].ConnectedNow + status := poolInfo[server].ConnectedNow if checkArgs.ExpectedStatuses[server] != status { return fmt.Errorf( "incorrect conn status: addr %s expected status %t actual status %t", @@ -106,7 +106,7 @@ func ProcessListenOnInstance(args interface{}) error { equal := reflect.DeepEqual(actualPorts, listenArgs.ExpectedPorts) if !equal { return fmt.Errorf("expected ports: %v, actual ports: %v", - actualPorts, listenArgs.ExpectedPorts) + listenArgs.ExpectedPorts, actualPorts) } return nil @@ -131,11 +131,11 @@ func Retry(f func(interface{}) error, args interface{}, count int, timeout time. return err } -func InsertOnInstance(ctx context.Context, server string, connOpts tarantool.Opts, +func InsertOnInstance(ctx context.Context, dialer tarantool.Dialer, connOpts tarantool.Opts, space interface{}, tuple interface{}) error { - conn, err := tarantool.Connect(ctx, server, connOpts) + conn, err := tarantool.Connect(ctx, dialer, connOpts) if err != nil { - return fmt.Errorf("fail to connect to %s: %s", server, err.Error()) + return fmt.Errorf("fail to connect: %s", err.Error()) } if conn == nil { return fmt.Errorf("conn is nil after Connect") @@ -169,22 +169,25 @@ func InsertOnInstance(ctx context.Context, server string, connOpts tarantool.Opt return nil } -func InsertOnInstances(servers []string, connOpts tarantool.Opts, space interface{}, +func InsertOnInstances( + dialers []tarantool.Dialer, + connOpts tarantool.Opts, + space interface{}, tuple interface{}) error { - serversNumber := len(servers) + serversNumber := len(dialers) roles := make([]bool, serversNumber) for i := 0; i < serversNumber; i++ { roles[i] = false } - err := SetClusterRO(servers, connOpts, roles) + err := SetClusterRO(dialers, connOpts, roles) if err != nil { return fmt.Errorf("fail to set roles for cluster: %s", err.Error()) } - for _, server := range servers { + for _, dialer := range dialers { ctx, cancel := GetConnectContext() - err := InsertOnInstance(ctx, server, connOpts, space, tuple) + err := InsertOnInstance(ctx, dialer, connOpts, space, tuple) cancel() if err != nil { return err @@ -194,9 +197,9 @@ func InsertOnInstances(servers []string, connOpts tarantool.Opts, space interfac return nil } -func SetInstanceRO(ctx context.Context, server string, connOpts tarantool.Opts, +func SetInstanceRO(ctx context.Context, dialer tarantool.Dialer, connOpts tarantool.Opts, isReplica bool) error { - conn, err := tarantool.Connect(ctx, server, connOpts) + conn, err := tarantool.Connect(ctx, dialer, connOpts) if err != nil { return err } @@ -212,14 +215,15 @@ func SetInstanceRO(ctx context.Context, server string, connOpts tarantool.Opts, return nil } -func SetClusterRO(servers []string, connOpts tarantool.Opts, roles []bool) error { - if len(servers) != len(roles) { +func SetClusterRO(dialers []tarantool.Dialer, connOpts tarantool.Opts, + roles []bool) error { + if len(dialers) != len(roles) { return fmt.Errorf("number of servers should be equal to number of roles") } - for i, server := range servers { + for i, dialer := range dialers { ctx, cancel := GetConnectContext() - err := SetInstanceRO(ctx, server, connOpts, roles[i]) + err := SetInstanceRO(ctx, dialer, connOpts, roles[i]) cancel() if err != nil { return err @@ -229,23 +233,10 @@ func SetClusterRO(servers []string, connOpts tarantool.Opts, roles []bool) error return nil } -func StartTarantoolInstances(servers []string, workDirs []string, - opts StartOpts) ([]TarantoolInstance, error) { - isUserWorkDirs := (workDirs != nil) - if isUserWorkDirs && (len(servers) != len(workDirs)) { - return nil, fmt.Errorf("number of servers should be equal to number of workDirs") - } - - instances := make([]TarantoolInstance, 0, len(servers)) - - for i, server := range servers { - opts.Listen = server - if isUserWorkDirs { - opts.WorkDir = workDirs[i] - } else { - opts.WorkDir = "" - } +func StartTarantoolInstances(instsOpts []StartOpts) ([]TarantoolInstance, error) { + instances := make([]TarantoolInstance, 0, len(instsOpts)) + for _, opts := range instsOpts { instance, err := StartTarantool(opts) if err != nil { StopTarantoolInstances(instances) diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 52c078c90..e962dc619 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -13,13 +13,13 @@ import ( // It returns a valid connection if it is successful, otherwise finishes a test // with an error. func ConnectWithValidation(t testing.TB, - server string, + dialer tarantool.Dialer, opts tarantool.Opts) *tarantool.Connection { t.Helper() ctx, cancel := GetConnectContext() defer cancel() - conn, err := tarantool.Connect(ctx, server, opts) + conn, err := tarantool.Connect(ctx, dialer, opts) if err != nil { t.Fatalf("Failed to connect: %s", err.Error()) } @@ -67,7 +67,7 @@ func SkipIfSQLUnsupported(t testing.TB) { // Tarantool supports SQL since version 2.0.0 isLess, err := IsTarantoolVersionLess(2, 0, 0) if err != nil { - t.Fatalf("Could not check the Tarantool version") + t.Fatalf("Could not check the Tarantool version: %s", err) } if isLess { t.Skip() @@ -80,7 +80,7 @@ func SkipIfLess(t *testing.T, reason string, major, minor, patch uint64) { isLess, err := IsTarantoolVersionLess(major, minor, patch) if err != nil { - t.Fatalf("Could not check the Tarantool version") + t.Fatalf("Could not check the Tarantool version: %s", err) } if isLess { @@ -95,7 +95,7 @@ func SkipIfGreaterOrEqual(t *testing.T, reason string, major, minor, patch uint6 isLess, err := IsTarantoolVersionLess(major, minor, patch) if err != nil { - t.Fatalf("Could not check the Tarantool version") + t.Fatalf("Could not check the Tarantool version: %s", err) } if !isLess { diff --git a/uuid/example_test.go b/uuid/example_test.go index 08bd64aae..ba90ea905 100644 --- a/uuid/example_test.go +++ b/uuid/example_test.go @@ -19,16 +19,21 @@ import ( _ "github.com/tarantool/go-tarantool/v2/uuid" ) +var exampleOpts = tarantool.Opts{ + Timeout: 5 * time.Second, +} + // Example demonstrates how to use tuples with UUID. To enable UUID support // in msgpack with google/uuid (https://github.com/google/uuid), import // tarantool/uuid submodule. func Example() { - opts := tarantool.Opts{ - User: "test", - Pass: "test", + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", } ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - client, err := tarantool.Connect(ctx, "127.0.0.1:3013", opts) + client, err := tarantool.Connect(ctx, dialer, exampleOpts) cancel() if err != nil { log.Fatalf("Failed to connect: %s", err.Error()) diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index 94baaa6ea..fdbf0cd82 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -23,8 +23,11 @@ var isUUIDSupported = false var server = "127.0.0.1:3013" var opts = Opts{ Timeout: 5 * time.Second, - User: "test", - Pass: "test", +} +var dialer = NetDialer{ + Address: server, + User: "test", + Password: "test", } var space = "testUUID" @@ -73,7 +76,7 @@ func TestSelect(t *testing.T) { t.Skip("Skipping test for Tarantool without UUID support in msgpack") } - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() id, uuidErr := uuid.Parse("c8f0fa1f-da29-438c-a040-393f1126ad39") @@ -113,7 +116,7 @@ func TestReplace(t *testing.T) { t.Skip("Skipping test for Tarantool without UUID support in msgpack") } - conn := test_helpers.ConnectWithValidation(t, server, opts) + conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() id, uuidErr := uuid.Parse("64d22e4d-ac92-4a23-899a-e59f34af5479") @@ -166,10 +169,9 @@ func runTestMain(m *testing.M) int { } inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + Dialer: dialer, InitScript: "config.lua", Listen: server, - User: opts.User, - Pass: opts.Pass, WaitStart: 100 * time.Millisecond, ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, From 4e067665571ea88cbea5c02d841553d9e19241a8 Mon Sep 17 00:00:00 2001 From: Albert Skalt Date: Thu, 16 Nov 2023 10:37:55 +0300 Subject: [PATCH 509/605] api: add the ability to connect via socket fd This patch introduces `FdDialer`, which connects to Tarantool using an existing socket file descriptor. `FdDialer` is not authenticated when creating a connection. Part of #321 --- .gitignore | 1 + dial.go | 56 +++++++++++++++++++++++++++ dial_test.go | 79 +++++++++++++++++++++++++++++++++++++ example_test.go | 32 +++++++++++++++ tarantool_test.go | 84 ++++++++++++++++++++++++++++++++++++++++ testdata/sidecar/main.go | 37 ++++++++++++++++++ 6 files changed, 289 insertions(+) create mode 100644 testdata/sidecar/main.go diff --git a/.gitignore b/.gitignore index fcd3c3236..c9f687eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ work_dir* .rocks bench* +testdata/sidecar/main diff --git a/dial.go b/dial.go index 6b75adafa..eae8e1283 100644 --- a/dial.go +++ b/dial.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net" + "os" "strings" "time" @@ -252,6 +253,61 @@ func (d OpenSslDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { return conn, nil } +// FdDialer allows to use an existing socket fd for connection. +type FdDialer struct { + // Fd is a socket file descrpitor. + Fd uintptr + // RequiredProtocol contains minimal protocol version and + // list of protocol features that should be supported by + // Tarantool server. By default, there are no restrictions. + RequiredProtocolInfo ProtocolInfo +} + +type fdAddr struct { + Fd uintptr +} + +func (a fdAddr) Network() string { + return "fd" +} + +func (a fdAddr) String() string { + return fmt.Sprintf("fd://%d", a.Fd) +} + +type fdConn struct { + net.Conn + Addr fdAddr +} + +func (c *fdConn) RemoteAddr() net.Addr { + return c.Addr +} + +// Dial makes FdDialer satisfy the Dialer interface. +func (d FdDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { + file := os.NewFile(d.Fd, "") + c, err := net.FileConn(file) + if err != nil { + return nil, fmt.Errorf("failed to dial: %w", err) + } + + conn := new(tntConn) + conn.net = &fdConn{Conn: c, Addr: fdAddr{Fd: d.Fd}} + + dc := &deadlineIO{to: opts.IoTimeout, c: conn.net} + conn.reader = bufio.NewReaderSize(dc, bufSize) + conn.writer = bufio.NewWriterSize(dc, bufSize) + + _, err = rawDial(conn, d.RequiredProtocolInfo) + if err != nil { + conn.net.Close() + return nil, err + } + + return conn, nil +} + // Addr makes tntConn satisfy the Conn interface. func (c *tntConn) Addr() net.Addr { return c.net.RemoteAddr() diff --git a/dial_test.go b/dial_test.go index c8cf1c778..ac8cab2aa 100644 --- a/dial_test.go +++ b/dial_test.go @@ -442,6 +442,7 @@ type testDialOpts struct { isIdUnsupported bool isPapSha256Auth bool isErrAuth bool + isEmptyAuth bool } type dialServerActual struct { @@ -484,6 +485,8 @@ func testDialAccept(opts testDialOpts, l net.Listener) chan dialServerActual { authRequestExpected := authRequestExpectedChapSha1 if opts.isPapSha256Auth { authRequestExpected = authRequestExpectedPapSha256 + } else if opts.isEmptyAuth { + authRequestExpected = []byte{} } authRequestActual := make([]byte, len(authRequestExpected)) client.Read(authRequestActual) @@ -526,6 +529,8 @@ func testDialer(t *testing.T, l net.Listener, dialer tarantool.Dialer, authRequestExpected := authRequestExpectedChapSha1 if opts.isPapSha256Auth { authRequestExpected = authRequestExpectedPapSha256 + } else if opts.isEmptyAuth { + authRequestExpected = []byte{} } require.Equal(t, authRequestExpected, actual.AuthRequest) conn.Close() @@ -779,3 +784,77 @@ func TestOpenSslDialer_Dial_ctx_cancel(t *testing.T) { } require.Error(t, err) } + +func TestFdDialer_Dial(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer l.Close() + addr := l.Addr().String() + + cases := []testDialOpts{ + { + name: "all is ok", + expectedProtocolInfo: idResponseTyped.Clone(), + isEmptyAuth: true, + }, + { + name: "id request unsupported", + expectedProtocolInfo: tarantool.ProtocolInfo{}, + isIdUnsupported: true, + isEmptyAuth: true, + }, + { + name: "greeting response error", + wantErr: true, + expectedErr: "failed to read greeting", + isErrGreeting: true, + }, + { + name: "id response error", + wantErr: true, + expectedErr: "failed to identify", + isErrId: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + sock, err := net.Dial("tcp", addr) + require.NoError(t, err) + f, err := sock.(*net.TCPConn).File() + require.NoError(t, err) + dialer := tarantool.FdDialer{ + Fd: f.Fd(), + } + testDialer(t, l, dialer, tc) + }) + } +} + +func TestFdDialer_Dial_requirements(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer l.Close() + addr := l.Addr().String() + + sock, err := net.Dial("tcp", addr) + require.NoError(t, err) + f, err := sock.(*net.TCPConn).File() + require.NoError(t, err) + dialer := tarantool.FdDialer{ + Fd: f.Fd(), + RequiredProtocolInfo: tarantool.ProtocolInfo{ + Features: []iproto.Feature{42}, + }, + } + + testDialAccept(testDialOpts{}, l) + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + if err == nil { + conn.Close() + } + require.Error(t, err) + require.Contains(t, err.Error(), "invalid server protocol") +} diff --git a/example_test.go b/example_test.go index 00cd00467..6500e18f4 100644 --- a/example_test.go +++ b/example_test.go @@ -3,6 +3,7 @@ package tarantool_test import ( "context" "fmt" + "net" "time" "github.com/tarantool/go-iproto" @@ -1350,3 +1351,34 @@ func ExampleWatchOnceRequest() { fmt.Println(resp.Data) } } + +// This example demonstrates how to use an existing socket file descriptor +// to establish a connection with Tarantool. This can be useful if the socket fd +// was inherited from the Tarantool process itself. +// For details, please see TestFdDialer in tarantool_test.go. +func ExampleFdDialer() { + addr := dialer.Address + c, err := net.Dial("tcp", addr) + if err != nil { + fmt.Printf("can't establish connection: %v\n", err) + return + } + f, err := c.(*net.TCPConn).File() + if err != nil { + fmt.Printf("unexpected error: %v\n", err) + return + } + dialer := tarantool.FdDialer{ + Fd: f.Fd(), + } + // Use an existing socket fd to create connection with Tarantool. + conn, err := tarantool.Connect(context.Background(), dialer, opts) + if err != nil { + fmt.Printf("connect error: %v\n", err) + return + } + resp, err := conn.Do(tarantool.NewPingRequest()).Get() + fmt.Println(resp.Code, err) + // Output: + // 0 +} diff --git a/tarantool_test.go b/tarantool_test.go index e54c6a46d..4d3f193f1 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -8,6 +8,8 @@ import ( "log" "math" "os" + "os/exec" + "path/filepath" "reflect" "runtime" "strings" @@ -77,6 +79,7 @@ func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { } var server = "127.0.0.1:3013" +var fdDialerTestServer = "127.0.0.1:3014" var spaceNo = uint32(617) var spaceName = "test" var indexNo = uint32(0) @@ -3984,6 +3987,87 @@ func TestConnect_context_cancel(t *testing.T) { } } +func buildSidecar(dir string) error { + goPath, err := exec.LookPath("go") + if err != nil { + return err + } + cmd := exec.Command(goPath, "build", "main.go") + cmd.Dir = filepath.Join(dir, "testdata", "sidecar") + return cmd.Run() +} + +func TestFdDialer(t *testing.T) { + isLess, err := test_helpers.IsTarantoolVersionLess(3, 0, 0) + if err != nil || isLess { + t.Skip("box.session.new present in Tarantool since version 3.0") + } + + wd, err := os.Getwd() + require.NoError(t, err) + + err = buildSidecar(wd) + require.NoErrorf(t, err, "failed to build sidecar: %v", err) + + instOpts := startOpts + instOpts.Listen = fdDialerTestServer + instOpts.Dialer = NetDialer{ + Address: fdDialerTestServer, + User: "test", + Password: "test", + } + + inst, err := test_helpers.StartTarantool(instOpts) + require.NoError(t, err) + defer test_helpers.StopTarantoolWithCleanup(inst) + + conn := test_helpers.ConnectWithValidation(t, dialer, opts) + defer conn.Close() + + sidecarExe := filepath.Join(wd, "testdata", "sidecar", "main") + + evalBody := fmt.Sprintf(` + local socket = require('socket') + local popen = require('popen') + local os = require('os') + local s1, s2 = socket.socketpair('AF_UNIX', 'SOCK_STREAM', 0) + + --[[ Tell sidecar which fd use to connect. --]] + os.setenv('SOCKET_FD', tostring(s2:fd())) + + box.session.new({ + type = 'binary', + fd = s1:fd(), + user = 'test', + }) + s1:detach() + + local ph, err = popen.new({'%s'}, { + stdout = popen.opts.PIPE, + stderr = popen.opts.PIPE, + inherit_fds = {s2:fd()}, + }) + + if err ~= nil then + return 1, err + end + + ph:wait() + + local status_code = ph:info().status.exit_code + local stderr = ph:read({stderr=true}):rstrip() + local stdout = ph:read({stdout=true}):rstrip() + return status_code, stderr, stdout + `, sidecarExe) + + var resp []interface{} + err = conn.EvalTyped(evalBody, []interface{}{}, &resp) + require.NoError(t, err) + require.Equal(t, "", resp[1], resp[1]) + require.Equal(t, "", resp[2], resp[2]) + require.Equal(t, int8(0), resp[0]) +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/testdata/sidecar/main.go b/testdata/sidecar/main.go new file mode 100644 index 000000000..971b8694c --- /dev/null +++ b/testdata/sidecar/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + "os" + "strconv" + + "github.com/tarantool/go-tarantool/v2" +) + +func main() { + fd, err := strconv.Atoi(os.Getenv("SOCKET_FD")) + if err != nil { + panic(err) + } + dialer := tarantool.FdDialer{ + Fd: uintptr(fd), + } + conn, err := tarantool.Connect(context.Background(), dialer, tarantool.Opts{}) + if err != nil { + panic(err) + } + if _, err := conn.Do(tarantool.NewPingRequest()).Get(); err != nil { + panic(err) + } + // Insert new tuple. + if _, err := conn.Do(tarantool.NewInsertRequest("test"). + Tuple([]interface{}{239})).Get(); err != nil { + panic(err) + } + // Delete inserted tuple. + if _, err := conn.Do(tarantool.NewDeleteRequest("test"). + Index("primary"). + Key([]interface{}{239})).Get(); err != nil { + panic(err) + } +} From 10a5cccc84ef884c039632d8b9a229340b9fa369 Mon Sep 17 00:00:00 2001 From: Albert Skalt Date: Fri, 17 Nov 2023 15:11:58 +0300 Subject: [PATCH 510/605] changelog: update according to the changes Part of #321 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e585d8e37..5eb1b0968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. version >= 3.0.0-alpha1 (#338). It allows to use space and index names in requests instead of their IDs. - `GetSchema` function to get the actual schema (#7) +- Support connection via an existing socket fd (#321) ### Changed @@ -55,6 +56,17 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Change `OverrideSchema(*Schema)` to `SetSchema(Schema)` (#7) - Change values, stored by pointers in the `Schema`, `Space`, `Index` structs, to be stored by their values (#7) +- Make `Dialer` mandatory for creation a single connection / connection pool (#321) +- Remove `Connection.RemoteAddr()`, `Connection.LocalAddr()`. + Add `Addr()` function instead (#321) +- Remove `Connection.ClientProtocolInfo`, `Connection.ServerProtocolInfo`. + Add `ProtocolInfo()` function, which returns the server protocol info (#321) +- `NewWatcher` checks the actual features of the server, rather than relying + on the features provided by the user during connection creation (#321) +- `pool.NewWatcher` does not create watchers for connections that do not support + it (#321) +- Rename `pool.GetPoolInfo` to `pool.GetInfo`. Change return type to + `map[string]ConnectionInfo` (#321) ### Deprecated From b2b800b09a8047345b81b8cd9b0a7c258637d7ca Mon Sep 17 00:00:00 2001 From: Albert Skalt Date: Fri, 24 Nov 2023 20:50:49 +0300 Subject: [PATCH 511/605] doc: update according to the changes Closes #321 --- README.md | 67 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 6a7a51f0c..b1d47ee84 100644 --- a/README.md +++ b/README.md @@ -109,16 +109,22 @@ import ( "context" "fmt" "time" - + "github.com/tarantool/go-tarantool/v2" ) func main() { - opts := tarantool.Opts{User: "guest"} - ctx, cancel := context.WithTimeout(context.Background(), + ctx, cancel := context.WithTimeout(context.Background(), 500 * time.Millisecond) defer cancel() - conn, err := tarantool.Connect(ctx, "127.0.0.1:3301", opts) + dialer := tarantool.NetDialer { + Address: "127.0.0.1:3301", + User: "guest", + } + opts := tarantool.Opts{ + Timeout: time.Second, + } + conn, err := tarantool.Connect(ctx, dialer, opts) if err != nil { fmt.Println("Connection refused:", err) } @@ -135,27 +141,30 @@ func main() { **Observation 1:** The line "`github.com/tarantool/go-tarantool/v2`" in the `import(...)` section brings in all Tarantool-related functions and structures. -**Observation 2:** The line starting with "`Opts :=`" sets up the options for +**Observation 2:** The line starting with "`dialer :=`" creates dialer for +`Connect()`. This structure contains fields required to establish a connection. + +**Observation 3:** The line starting with "`opts :=`" sets up the options for `Connect()`. In this example, the structure contains only a single value, the -username. The structure may also contain other settings, see more in +timeout. The structure may also contain other settings, see more in [documentation][godoc-opts-url] for the "`Opts`" structure. -**Observation 3:** The line containing "`tarantool.Connect`" is essential for +**Observation 4:** The line containing "`tarantool.Connect`" is essential for starting a session. There are three parameters: * a context, -* a string with `host:port` format, +* the dialer that was set up earlier, * the option structure that was set up earlier. -There will be only one attempt to connect. If multiple attempts needed, -"`tarantool.Connect`" could be placed inside the loop with some timeout -between each try. Example could be found in the [example_test](./example_test.go), +There will be only one attempt to connect. If multiple attempts needed, +"`tarantool.Connect`" could be placed inside the loop with some timeout +between each try. Example could be found in the [example_test](./example_test.go), name - `ExampleConnect_reconnects`. -**Observation 4:** The `err` structure will be `nil` if there is no error, +**Observation 5:** The `err` structure will be `nil` if there is no error, otherwise it will have a description which can be retrieved with `err.Error()`. -**Observation 5:** The `Insert` request, like almost all requests, is preceded +**Observation 6:** The `Insert` request, like almost all requests, is preceded by the method `Do` of object `conn` which is the object that was returned by `Connect()`. @@ -182,11 +191,16 @@ The subpackage has been deleted. You could use `pool` instead. * The `connection_pool` subpackage has been renamed to `pool`. * The type `PoolOpts` has been renamed to `Opts`. -* `pool.Connect` now accepts context as first argument, which user may cancel - in process. If it is canceled in progress, an error will be returned. +* `pool.Connect` now accepts context as first argument, which user may cancel + in process. If it is canceled in progress, an error will be returned. All created connections will be closed. -* `pool.Add` now accepts context as first argument, which user may cancel in +* `pool.Add` now accepts context as first argument, which user may cancel in process. +* Now you need to pass `map[string]Dialer` to the `pool.Connect` as the second + argument, instead of a list of addresses. Each dialer is associated with a + unique string ID, which allows them to be distinguished. +* `pool.GetPoolInfo` has been renamed to `pool.GetInfo`. Return type has been changed + to `map[string]ConnectionInfo`. #### crud package @@ -235,7 +249,7 @@ IPROTO constants have been moved to a separate package [go-iproto](https://githu * `Op` struct for update operations made private. * Removed `OpSplice` struct. * `Operations.Splice` method now accepts 5 arguments instead of 3. -* Requests `Update`, `UpdateAsync`, `UpdateTyped`, `Upsert`, `UpsertAsync` no +* Requests `Update`, `UpdateAsync`, `UpdateTyped`, `Upsert`, `UpsertAsync` no longer accept `ops` argument (operations) as an `interface{}`. `*Operations` needs to be passed instead. * `UpdateRequest` and `UpsertRequest` structs no longer accept `interface{}` @@ -243,15 +257,20 @@ for an `ops` field. `*Operations` needs to be used instead. #### Connect function -`connection.Connect` no longer return non-working connection objects. This function +`connection.Connect` no longer return non-working connection objects. This function now does not attempt to reconnect and tries to establish a connection only once. -Function might be canceled via context. Context accepted as first argument, +Function might be canceled via context. Context accepted as first argument, and user may cancel it in process. +Now you need to pass `Dialer` as the second argument instead of URI. +If you were using a non-SSL connection, you need to create `NetDialer`. +For SSL-enabled connections, use `OpenSslDialer`. Please note that the options +for creating a connection are now stored in corresponding `Dialer`, not in `Opts`. + #### Connection schema -* Removed `Schema` field from the `Connection` struct. Instead, new -`GetSchema(Connector)` function was added to get the actual connection +* Removed `Schema` field from the `Connection` struct. Instead, new +`GetSchema(Connector)` function was added to get the actual connection schema on demand. * `OverrideSchema(*Schema)` method replaced with the `SetSchema(Schema)`. @@ -262,9 +281,9 @@ schema on demand. #### Schema changes -* `ResolveSpaceIndex` function for `SchemaResolver` interface split into two: -`ResolveSpace` and `ResolveIndex`. `NamesUseSupported` function added into the -interface to get information if the usage of space and index names in requests +* `ResolveSpaceIndex` function for `SchemaResolver` interface split into two: +`ResolveSpace` and `ResolveIndex`. `NamesUseSupported` function added into the +interface to get information if the usage of space and index names in requests is supported. * `Schema` structure no longer implements `SchemaResolver` interface. * `Spaces` and `SpacesById` fields of the `Schema` struct store spaces by value. From cabe16e8ee52d80e17acd6856d96c84fd83e8bc4 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 29 Nov 2023 19:26:25 +0300 Subject: [PATCH 512/605] api: deprecate box.session.push() usage `box.session.push` is deprecated starting from Tarantool 3.0. We are going to remove the feature from the connector in a next major release. Part of #324 --- CHANGELOG.md | 2 ++ future.go | 6 ++++++ response_it.go | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eb1b0968..36f5c08db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - All ConnectionPool., ConnectionPool.Typed and ConnectionPool.Async methods. Instead you should use requests objects + ConnectionPool.Do() (#241) +- box.session.push() usage: Future.AppendPush() and Future.GetIterator() + methods, ResponseIterator and TimeoutResponseIterator types (#324) ### Removed diff --git a/future.go b/future.go index 09229788c..e7f7dae19 100644 --- a/future.go +++ b/future.go @@ -128,6 +128,9 @@ func NewFuture() (fut *Future) { // AppendPush appends the push response to the future. // Note: it works only before SetResponse() or SetError() +// +// Deprecated: the method will be removed in the next major version, +// use Connector.NewWatcher() instead of box.session.push(). func (fut *Future) AppendPush(resp *Response) { fut.mutex.Lock() defer fut.mutex.Unlock() @@ -208,6 +211,9 @@ func (fut *Future) GetTyped(result interface{}) error { // // - box.session.push(): // https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_session/push/ +// +// Deprecated: the method will be removed in the next major version, +// use Connector.NewWatcher() instead of box.session.push(). func (fut *Future) GetIterator() (it TimeoutResponseIterator) { futit := &asyncResponseIterator{ fut: fut, diff --git a/response_it.go b/response_it.go index 3ae296449..404c68a51 100644 --- a/response_it.go +++ b/response_it.go @@ -5,6 +5,9 @@ import ( ) // ResponseIterator is an interface for iteration over a set of responses. +// +// Deprecated: the method will be removed in the next major version, +// use Connector.NewWatcher() instead of box.session.push(). type ResponseIterator interface { // Next tries to switch to a next Response and returns true if it exists. Next() bool @@ -16,6 +19,9 @@ type ResponseIterator interface { // TimeoutResponseIterator is an interface that extends ResponseIterator // and adds the ability to change a timeout for the Next() call. +// +// Deprecated: the method will be removed in the next major version, +// use Connector.NewWatcher() instead of box.session.push(). type TimeoutResponseIterator interface { ResponseIterator // WithTimeout allows to set up a timeout for the Next() call. From 36b05f6759d9b0a1341ec145c43c6c19df8db443 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 1 Dec 2023 13:20:38 +0300 Subject: [PATCH 513/605] bugfix: build with -tags go_tarantool_ssl_disable The patch fixes build with the build tag `go_tarantool_ssl_disable`: 1. It moves tests with OpenSslDialer to a test file that executes only with the tag. 2. It defines structure `sslOpts` in the common place to use it in the code with/without the flag. Finally, it adds tests to CI with the build tag. Closes #357 --- .github/workflows/testing.yml | 17 ++++ dial_test.go | 181 --------------------------------- ssl.go | 143 -------------------------- ssl_disable.go | 4 +- ssl_enable.go | 144 ++++++++++++++++++++++++++ ssl_test.go | 186 ++++++++++++++++++++++++++++++++++ 6 files changed, 349 insertions(+), 326 deletions(-) create mode 100644 ssl_enable.go diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index c21344e27..3686a81f7 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -103,6 +103,11 @@ jobs: make test make testrace + - name: Run regression tests with disabled SSL + run: | + make test TAGS="go_tarantool_ssl_disable" + make testrace TAGS="go_tarantool_ssl_disable" + - name: Run fuzzing tests if: ${{ matrix.fuzzing }} run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" @@ -193,6 +198,12 @@ jobs: env: TEST_TNT_SSL: ${{matrix.ssl}} + - name: Run regression tests with disabled SSL + run: | + source tarantool-enterprise/env.sh + make test TAGS="go_tarantool_ssl_disable" + make testrace TAGS="go_tarantool_ssl_disable" + - name: Run fuzzing tests if: ${{ matrix.fuzzing }} run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" @@ -359,6 +370,12 @@ jobs: make test make testrace + - name: Run regression tests with disabled SSL + run: | + cd "${SRCDIR}" + make test TAGS="go_tarantool_ssl_disable" + make testrace TAGS="go_tarantool_ssl_disable" + - name: Run fuzzing tests if: ${{ matrix.fuzzing }} run: | diff --git a/dial_test.go b/dial_test.go index ac8cab2aa..17f037e00 100644 --- a/dial_test.go +++ b/dial_test.go @@ -14,7 +14,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-openssl" "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/test_helpers" @@ -605,186 +604,6 @@ func TestNetDialer_Dial_requirements(t *testing.T) { require.Contains(t, err.Error(), "invalid server protocol") } -func createSslListener(t *testing.T, opts tarantool.SslTestOpts) net.Listener { - ctx, err := tarantool.SslCreateContext(opts) - require.NoError(t, err) - l, err := openssl.Listen("tcp", "127.0.0.1:0", ctx.(*openssl.Ctx)) - require.NoError(t, err) - return l -} - -func TestOpenSslDialer_Dial_basic(t *testing.T) { - l := createSslListener(t, tarantool.SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - }) - - defer l.Close() - addr := l.Addr().String() - - dialer := tarantool.OpenSslDialer{ - Address: addr, - User: testDialUser, - Password: testDialPass, - } - - cases := []testDialOpts{ - { - name: "all is ok", - expectedProtocolInfo: idResponseTyped.Clone(), - }, - { - name: "id request unsupported", - // Dialer sets auth. - expectedProtocolInfo: tarantool.ProtocolInfo{Auth: tarantool.ChapSha1Auth}, - isIdUnsupported: true, - }, - { - name: "greeting response error", - wantErr: true, - expectedErr: "failed to read greeting", - isErrGreeting: true, - }, - { - name: "id response error", - wantErr: true, - expectedErr: "failed to identify", - isErrId: true, - }, - { - name: "auth response error", - wantErr: true, - expectedErr: "failed to authenticate", - isErrAuth: true, - }, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - testDialer(t, l, dialer, tc) - }) - } -} - -func TestOpenSslDialer_Dial_requirements(t *testing.T) { - l := createSslListener(t, tarantool.SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - }) - - defer l.Close() - addr := l.Addr().String() - - dialer := tarantool.OpenSslDialer{ - Address: addr, - User: testDialUser, - Password: testDialPass, - RequiredProtocolInfo: tarantool.ProtocolInfo{ - Features: []iproto.Feature{42}, - }, - } - - testDialAccept(testDialOpts{}, l) - ctx, cancel := test_helpers.GetConnectContext() - defer cancel() - conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) - if err == nil { - conn.Close() - } - require.Error(t, err) - require.Contains(t, err.Error(), "invalid server protocol") -} - -func TestOpenSslDialer_Dial_papSha256Auth(t *testing.T) { - l := createSslListener(t, tarantool.SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - }) - - defer l.Close() - addr := l.Addr().String() - - dialer := tarantool.OpenSslDialer{ - Address: addr, - User: testDialUser, - Password: testDialPass, - Auth: tarantool.PapSha256Auth, - } - - protocol := idResponseTyped.Clone() - protocol.Auth = tarantool.PapSha256Auth - - testDialer(t, l, dialer, testDialOpts{ - expectedProtocolInfo: protocol, - isPapSha256Auth: true, - }) -} - -func TestOpenSslDialer_Dial_opts(t *testing.T) { - for _, test := range sslTests { - t.Run(test.name, func(t *testing.T) { - l := createSslListener(t, test.serverOpts) - defer l.Close() - addr := l.Addr().String() - - dialer := tarantool.OpenSslDialer{ - Address: addr, - User: testDialUser, - Password: testDialPass, - SslKeyFile: test.clientOpts.KeyFile, - SslCertFile: test.clientOpts.CertFile, - SslCaFile: test.clientOpts.CaFile, - SslCiphers: test.clientOpts.Ciphers, - SslPassword: test.clientOpts.Password, - SslPasswordFile: test.clientOpts.PasswordFile, - } - testDialer(t, l, dialer, testDialOpts{ - wantErr: !test.ok, - expectedProtocolInfo: idResponseTyped.Clone(), - }) - }) - } -} - -func TestOpenSslDialer_Dial_ctx_cancel(t *testing.T) { - serverOpts := tarantool.SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", - } - clientOpts := tarantool.SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", - } - - l := createSslListener(t, serverOpts) - defer l.Close() - addr := l.Addr().String() - testDialAccept(testDialOpts{}, l) - - dialer := tarantool.OpenSslDialer{ - Address: addr, - User: testDialUser, - Password: testDialPass, - SslKeyFile: clientOpts.KeyFile, - SslCertFile: clientOpts.CertFile, - SslCaFile: clientOpts.CaFile, - SslCiphers: clientOpts.Ciphers, - SslPassword: clientOpts.Password, - SslPasswordFile: clientOpts.PasswordFile, - } - - ctx, cancel := context.WithCancel(context.Background()) - cancel() - conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) - if err == nil { - conn.Close() - } - require.Error(t, err) -} - func TestFdDialer_Dial(t *testing.T) { l, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) diff --git a/ssl.go b/ssl.go index 70f324df6..39cc57bd0 100644 --- a/ssl.go +++ b/ssl.go @@ -1,20 +1,5 @@ -//go:build !go_tarantool_ssl_disable -// +build !go_tarantool_ssl_disable - package tarantool -import ( - "bufio" - "context" - "errors" - "io/ioutil" - "net" - "os" - "strings" - - "github.com/tarantool/go-openssl" -) - type sslOpts struct { // KeyFile is a path to a private SSL key file. KeyFile string @@ -43,131 +28,3 @@ type sslOpts struct { // file as a password. PasswordFile string } - -func sslDialContext(ctx context.Context, network, address string, - opts sslOpts) (connection net.Conn, err error) { - var sslCtx interface{} - if sslCtx, err = sslCreateContext(opts); err != nil { - return - } - - return openssl.DialContext(ctx, network, address, sslCtx.(*openssl.Ctx), 0) -} - -// interface{} is a hack. It helps to avoid dependency of go-openssl in build -// of tests with the tag 'go_tarantool_ssl_disable'. -func sslCreateContext(opts sslOpts) (ctx interface{}, err error) { - var sslCtx *openssl.Ctx - - // Require TLSv1.2, because other protocol versions don't seem to - // support the GOST cipher. - if sslCtx, err = openssl.NewCtxWithVersion(openssl.TLSv1_2); err != nil { - return - } - ctx = sslCtx - sslCtx.SetMaxProtoVersion(openssl.TLS1_2_VERSION) - sslCtx.SetMinProtoVersion(openssl.TLS1_2_VERSION) - - if opts.CertFile != "" { - if err = sslLoadCert(sslCtx, opts.CertFile); err != nil { - return - } - } - - if opts.KeyFile != "" { - if err = sslLoadKey(sslCtx, opts.KeyFile, opts.Password, opts.PasswordFile); err != nil { - return - } - } - - if opts.CaFile != "" { - if err = sslCtx.LoadVerifyLocations(opts.CaFile, ""); err != nil { - return - } - verifyFlags := openssl.VerifyPeer | openssl.VerifyFailIfNoPeerCert - sslCtx.SetVerify(verifyFlags, nil) - } - - if opts.Ciphers != "" { - sslCtx.SetCipherList(opts.Ciphers) - } - - return -} - -func sslLoadCert(ctx *openssl.Ctx, certFile string) (err error) { - var certBytes []byte - if certBytes, err = ioutil.ReadFile(certFile); err != nil { - return - } - - certs := openssl.SplitPEM(certBytes) - if len(certs) == 0 { - err = errors.New("No PEM certificate found in " + certFile) - return - } - first, certs := certs[0], certs[1:] - - var cert *openssl.Certificate - if cert, err = openssl.LoadCertificateFromPEM(first); err != nil { - return - } - if err = ctx.UseCertificate(cert); err != nil { - return - } - - for _, pem := range certs { - if cert, err = openssl.LoadCertificateFromPEM(pem); err != nil { - break - } - if err = ctx.AddChainCertificate(cert); err != nil { - break - } - } - return -} - -func sslLoadKey(ctx *openssl.Ctx, keyFile string, password string, - passwordFile string) error { - var keyBytes []byte - var err, firstDecryptErr error - - if keyBytes, err = ioutil.ReadFile(keyFile); err != nil { - return err - } - - // If the key is encrypted and password is not provided, - // openssl.LoadPrivateKeyFromPEM(keyBytes) asks to enter PEM pass phrase - // interactively. On the other hand, - // openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) works fine - // for non-encrypted key with any password, including empty string. If - // the key is encrypted, we fast fail with password error instead of - // requesting the pass phrase interactively. - passwords := []string{password} - if passwordFile != "" { - file, err := os.Open(passwordFile) - if err == nil { - defer file.Close() - - scanner := bufio.NewScanner(file) - // Tarantool itself tries each password file line. - for scanner.Scan() { - password = strings.TrimSpace(scanner.Text()) - passwords = append(passwords, password) - } - } else { - firstDecryptErr = err - } - } - - for _, password := range passwords { - key, err := openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) - if err == nil { - return ctx.UsePrivateKey(key) - } else if firstDecryptErr == nil { - firstDecryptErr = err - } - } - - return firstDecryptErr -} diff --git a/ssl_disable.go b/ssl_disable.go index 6a2aa2163..21550b61a 100644 --- a/ssl_disable.go +++ b/ssl_disable.go @@ -10,10 +10,10 @@ import ( ) func sslDialContext(ctx context.Context, network, address string, - opts SslOpts) (connection net.Conn, err error) { + opts sslOpts) (connection net.Conn, err error) { return nil, errors.New("SSL support is disabled.") } -func sslCreateContext(opts SslOpts) (ctx interface{}, err error) { +func sslCreateContext(opts sslOpts) (ctx interface{}, err error) { return nil, errors.New("SSL support is disabled.") } diff --git a/ssl_enable.go b/ssl_enable.go new file mode 100644 index 000000000..a56dac1cc --- /dev/null +++ b/ssl_enable.go @@ -0,0 +1,144 @@ +//go:build !go_tarantool_ssl_disable +// +build !go_tarantool_ssl_disable + +package tarantool + +import ( + "bufio" + "context" + "errors" + "io/ioutil" + "net" + "os" + "strings" + + "github.com/tarantool/go-openssl" +) + +func sslDialContext(ctx context.Context, network, address string, + opts sslOpts) (connection net.Conn, err error) { + var sslCtx interface{} + if sslCtx, err = sslCreateContext(opts); err != nil { + return + } + + return openssl.DialContext(ctx, network, address, sslCtx.(*openssl.Ctx), 0) +} + +// interface{} is a hack. It helps to avoid dependency of go-openssl in build +// of tests with the tag 'go_tarantool_ssl_disable'. +func sslCreateContext(opts sslOpts) (ctx interface{}, err error) { + var sslCtx *openssl.Ctx + + // Require TLSv1.2, because other protocol versions don't seem to + // support the GOST cipher. + if sslCtx, err = openssl.NewCtxWithVersion(openssl.TLSv1_2); err != nil { + return + } + ctx = sslCtx + sslCtx.SetMaxProtoVersion(openssl.TLS1_2_VERSION) + sslCtx.SetMinProtoVersion(openssl.TLS1_2_VERSION) + + if opts.CertFile != "" { + if err = sslLoadCert(sslCtx, opts.CertFile); err != nil { + return + } + } + + if opts.KeyFile != "" { + if err = sslLoadKey(sslCtx, opts.KeyFile, opts.Password, opts.PasswordFile); err != nil { + return + } + } + + if opts.CaFile != "" { + if err = sslCtx.LoadVerifyLocations(opts.CaFile, ""); err != nil { + return + } + verifyFlags := openssl.VerifyPeer | openssl.VerifyFailIfNoPeerCert + sslCtx.SetVerify(verifyFlags, nil) + } + + if opts.Ciphers != "" { + sslCtx.SetCipherList(opts.Ciphers) + } + + return +} + +func sslLoadCert(ctx *openssl.Ctx, certFile string) (err error) { + var certBytes []byte + if certBytes, err = ioutil.ReadFile(certFile); err != nil { + return + } + + certs := openssl.SplitPEM(certBytes) + if len(certs) == 0 { + err = errors.New("No PEM certificate found in " + certFile) + return + } + first, certs := certs[0], certs[1:] + + var cert *openssl.Certificate + if cert, err = openssl.LoadCertificateFromPEM(first); err != nil { + return + } + if err = ctx.UseCertificate(cert); err != nil { + return + } + + for _, pem := range certs { + if cert, err = openssl.LoadCertificateFromPEM(pem); err != nil { + break + } + if err = ctx.AddChainCertificate(cert); err != nil { + break + } + } + return +} + +func sslLoadKey(ctx *openssl.Ctx, keyFile string, password string, + passwordFile string) error { + var keyBytes []byte + var err, firstDecryptErr error + + if keyBytes, err = ioutil.ReadFile(keyFile); err != nil { + return err + } + + // If the key is encrypted and password is not provided, + // openssl.LoadPrivateKeyFromPEM(keyBytes) asks to enter PEM pass phrase + // interactively. On the other hand, + // openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) works fine + // for non-encrypted key with any password, including empty string. If + // the key is encrypted, we fast fail with password error instead of + // requesting the pass phrase interactively. + passwords := []string{password} + if passwordFile != "" { + file, err := os.Open(passwordFile) + if err == nil { + defer file.Close() + + scanner := bufio.NewScanner(file) + // Tarantool itself tries each password file line. + for scanner.Scan() { + password = strings.TrimSpace(scanner.Text()) + passwords = append(passwords, password) + } + } else { + firstDecryptErr = err + } + } + + for _, password := range passwords { + key, err := openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) + if err == nil { + return ctx.UsePrivateKey(key) + } else if firstDecryptErr == nil { + firstDecryptErr = err + } + } + + return firstDecryptErr +} diff --git a/ssl_test.go b/ssl_test.go index 61f6e131d..44b26eb05 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -4,12 +4,18 @@ package tarantool_test import ( + "context" "fmt" + "net" "os" "strings" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" + "github.com/tarantool/go-openssl" + . "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/test_helpers" ) @@ -592,3 +598,183 @@ func TestOpts_PapSha256Auth(t *testing.T) { conn = test_helpers.ConnectWithValidation(t, client, opts) conn.Close() } + +func createSslListener(t *testing.T, opts SslTestOpts) net.Listener { + ctx, err := SslCreateContext(opts) + require.NoError(t, err) + l, err := openssl.Listen("tcp", "127.0.0.1:0", ctx.(*openssl.Ctx)) + require.NoError(t, err) + return l +} + +func TestOpenSslDialer_Dial_opts(t *testing.T) { + for _, test := range sslTests { + t.Run(test.name, func(t *testing.T) { + l := createSslListener(t, test.serverOpts) + defer l.Close() + addr := l.Addr().String() + + dialer := OpenSslDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + SslKeyFile: test.clientOpts.KeyFile, + SslCertFile: test.clientOpts.CertFile, + SslCaFile: test.clientOpts.CaFile, + SslCiphers: test.clientOpts.Ciphers, + SslPassword: test.clientOpts.Password, + SslPasswordFile: test.clientOpts.PasswordFile, + } + testDialer(t, l, dialer, testDialOpts{ + wantErr: !test.ok, + expectedProtocolInfo: idResponseTyped.Clone(), + }) + }) + } +} + +func TestOpenSslDialer_Dial_basic(t *testing.T) { + l := createSslListener(t, SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }) + + defer l.Close() + addr := l.Addr().String() + + dialer := OpenSslDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + } + + cases := []testDialOpts{ + { + name: "all is ok", + expectedProtocolInfo: idResponseTyped.Clone(), + }, + { + name: "id request unsupported", + // Dialer sets auth. + expectedProtocolInfo: ProtocolInfo{Auth: ChapSha1Auth}, + isIdUnsupported: true, + }, + { + name: "greeting response error", + wantErr: true, + expectedErr: "failed to read greeting", + isErrGreeting: true, + }, + { + name: "id response error", + wantErr: true, + expectedErr: "failed to identify", + isErrId: true, + }, + { + name: "auth response error", + wantErr: true, + expectedErr: "failed to authenticate", + isErrAuth: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + testDialer(t, l, dialer, tc) + }) + } +} + +func TestOpenSslDialer_Dial_requirements(t *testing.T) { + l := createSslListener(t, SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }) + + defer l.Close() + addr := l.Addr().String() + + dialer := OpenSslDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + RequiredProtocolInfo: ProtocolInfo{ + Features: []iproto.Feature{42}, + }, + } + + testDialAccept(testDialOpts{}, l) + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, DialOpts{}) + if err == nil { + conn.Close() + } + require.Error(t, err) + require.Contains(t, err.Error(), "invalid server protocol") +} + +func TestOpenSslDialer_Dial_papSha256Auth(t *testing.T) { + l := createSslListener(t, SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }) + + defer l.Close() + addr := l.Addr().String() + + dialer := OpenSslDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + Auth: PapSha256Auth, + } + + protocol := idResponseTyped.Clone() + protocol.Auth = PapSha256Auth + + testDialer(t, l, dialer, testDialOpts{ + expectedProtocolInfo: protocol, + isPapSha256Auth: true, + }) +} + +func TestOpenSslDialer_Dial_ctx_cancel(t *testing.T) { + serverOpts := SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + } + clientOpts := SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + } + + l := createSslListener(t, serverOpts) + defer l.Close() + addr := l.Addr().String() + testDialAccept(testDialOpts{}, l) + + dialer := OpenSslDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + SslKeyFile: clientOpts.KeyFile, + SslCertFile: clientOpts.CertFile, + SslCaFile: clientOpts.CaFile, + SslCiphers: clientOpts.Ciphers, + SslPassword: clientOpts.Password, + SslPasswordFile: clientOpts.PasswordFile, + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + conn, err := dialer.Dial(ctx, DialOpts{}) + if err == nil { + conn.Close() + } + require.Error(t, err) +} From 7d73f6a6962650a0aaffb9a1e1816dec7e47f49f Mon Sep 17 00:00:00 2001 From: better0fdead Date: Tue, 5 Dec 2023 14:30:19 +0300 Subject: [PATCH 514/605] connection: fix svacer issue Changed type of 'length' variable in 'read' function to avoid overflow when calculating it. --- connection.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/connection.go b/connection.go index a92a66d84..bf9f1554e 100644 --- a/connection.go +++ b/connection.go @@ -1158,7 +1158,7 @@ func (conn *Connection) timeouts() { } func read(r io.Reader, lenbuf []byte) (response []byte, err error) { - var length int + var length uint64 if _, err = io.ReadFull(r, lenbuf); err != nil { return @@ -1167,15 +1167,20 @@ func read(r io.Reader, lenbuf []byte) (response []byte, err error) { err = errors.New("wrong response header") return } - length = (int(lenbuf[1]) << 24) + - (int(lenbuf[2]) << 16) + - (int(lenbuf[3]) << 8) + - int(lenbuf[4]) + length = (uint64(lenbuf[1]) << 24) + + (uint64(lenbuf[2]) << 16) + + (uint64(lenbuf[3]) << 8) + + uint64(lenbuf[4]) - if length == 0 { + switch { + case length == 0: err = errors.New("response should not be 0 length") return + case length > math.MaxUint32: + err = errors.New("response is too big") + return } + response = make([]byte, length) _, err = io.ReadFull(r, response) From 02a8820b819e0cb72697f89f88ab07c910bc7fab Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 27 Dec 2023 21:49:23 +0300 Subject: [PATCH 515/605] mod: bump go-iproto to v1.0.0 The patch bumps the go-iproto dependency to the latest release version. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 440597972..f1a6af3b0 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.7.1 - github.com/tarantool/go-iproto v0.1.1-0.20231025103136-cb7894473931 + github.com/tarantool/go-iproto v1.0.0 github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca github.com/vmihailenco/msgpack/v5 v5.3.5 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect diff --git a/go.sum b/go.sum index 038e8af34..752c1029c 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tarantool/go-iproto v0.1.1-0.20231025103136-cb7894473931 h1:YrsRc1sDZ6HOZccvM2eJ3Nu2TMBq7NMZMsaT5KCu5qU= -github.com/tarantool/go-iproto v0.1.1-0.20231025103136-cb7894473931/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= +github.com/tarantool/go-iproto v1.0.0 h1:quC4hdFhCuFYaCqOFgUxH2foRkhAy+TlEy7gQLhdVjw= +github.com/tarantool/go-iproto v1.0.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca h1:oOrBh73tDDyooIXajfr+0pfnM+89404ClAhJpTTHI7E= github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= From 7ce27840782cfef23e402922e6f547fd25c4d1e5 Mon Sep 17 00:00:00 2001 From: Pavel Balaev Date: Thu, 18 Jan 2024 16:45:22 +0300 Subject: [PATCH 516/605] ci: remove tarantoolctl Part of tarantool/tarantool#9443 --- .github/workflows/check.yaml | 8 +++++++- .github/workflows/reusable_testing.yml | 6 ++++++ .github/workflows/testing.yml | 8 +++++++- Makefile | 9 +++------ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 88b6afa43..52675cfbd 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -19,8 +19,14 @@ jobs: with: tarantool-version: '2.8' + - name: Setup tt + run: | + curl -L https://tarantool.io/release/2/installer.sh | sudo bash + sudo apt install -y tt + tt version + - name: Setup luacheck - run: tarantoolctl rocks install luacheck 0.25.0 + run: tt rocks install luacheck 0.25.0 - name: Run luacheck run: ./.rocks/bin/luacheck . diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index 7a23e6f8a..c30171d87 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -38,6 +38,12 @@ jobs: with: go-version: 1.13 + - name: Setup tt + run: | + curl -L https://tarantool.io/release/2/installer.sh | sudo bash + sudo apt install -y tt + tt version + - name: Install test dependencies run: make deps diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 3686a81f7..34219a3b0 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -358,9 +358,15 @@ jobs: run: echo "MallocNanoZone=0" >> $GITHUB_ENV if: matrix.runs-on == 'macos-12' + # Workaround issue https://github.com/tarantool/tt/issues/640 + - name: Fix tt rocks + if: matrix.tarantool == 'brew' + run: | + brew ls --verbose tarantool | grep macosx.lua | xargs rm -f + - name: Install test dependencies run: | - brew install luarocks + brew install tt cd "${SRCDIR}" make deps diff --git a/Makefile b/Makefile index 3af9699ef..fb6817a2a 100644 --- a/Makefile +++ b/Makefile @@ -14,10 +14,6 @@ BENCH_OPTIONS := -bench=. -run=^Benchmark -benchmem -benchtime=${DURATION} -coun GO_TARANTOOL_URL := https://github.com/tarantool/go-tarantool GO_TARANTOOL_DIR := ${PROJECT_DIR}/${BENCH_PATH}/go-tarantool TAGS := -TTCTL := tt -ifeq (,$(shell which tt 2>/dev/null)) - TTCTL := tarantoolctl -endif .PHONY: clean clean: @@ -26,8 +22,9 @@ clean: .PHONY: deps deps: clean - ( cd ./queue/testdata; $(TTCTL) rocks install queue 1.3.0 ) - ( cd ./crud/testdata; $(TTCTL) rocks install crud 1.4.1 ) + @(command -v tt > /dev/null || (echo "error: tt not found" && exit 1)) + ( cd ./queue/testdata; tt rocks install queue 1.3.0 ) + ( cd ./crud/testdata; tt rocks install crud 1.4.1 ) .PHONY: datetime-timezones datetime-timezones: From 4784b558fa071d477d1f276c3f7d01c7aad78a2a Mon Sep 17 00:00:00 2001 From: DerekBum Date: Thu, 7 Dec 2023 18:20:57 +0300 Subject: [PATCH 517/605] api: change `Response` type to interface This commit creates a new `Response` interface. But still, only one `Response` implementation exists: `ConnResponse`. Custom responses (including mocks) are expected to implement this interface. Create a `Response` interface and its implementation `ConnResponse`. For the `Future` method `Get` now returns response data. To get the actual response new method `GetResponse` is added. `IsPush()` method is added to the response iterator. It returns the information if the current response is a `Push`. Right now it does not have a lot of usage, but it will be crucial once we create a separate response for pushes. Part of #237 --- CHANGELOG.md | 10 + README.md | 22 +- box_error_test.go | 18 +- connection.go | 26 +- connector.go | 24 +- const.go | 3 +- crud/tarantool_test.go | 63 +-- datetime/datetime_test.go | 50 +- datetime/example_test.go | 15 +- datetime/interval_test.go | 4 +- decimal/decimal_test.go | 50 +- decimal/example_test.go | 8 +- dial.go | 39 +- dial_test.go | 3 +- example_custom_unpacking_test.go | 6 +- example_test.go | 305 +++++----- future.go | 53 +- future_test.go | 61 +- header.go | 10 + pool/connection_pool.go | 35 +- pool/connection_pool_test.go | 388 +++++++------ pool/connector.go | 24 +- pool/connector_test.go | 26 +- pool/example_test.go | 83 ++- pool/pooler.go | 24 +- prepared.go | 12 +- queue/queue.go | 24 +- request.go | 50 +- response.go | 196 ++++--- response_it.go | 4 +- settings/example_test.go | 8 +- settings/tarantool_test.go | 345 +++++------- shutdown_test.go | 6 +- tarantool_test.go | 934 +++++++++++++------------------ test_helpers/main.go | 4 +- test_helpers/pool_helper.go | 18 +- uuid/example_test.go | 5 +- uuid/uuid_test.go | 21 +- 38 files changed, 1374 insertions(+), 1603 deletions(-) create mode 100644 header.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 36f5c08db..6b362a4d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. in requests instead of their IDs. - `GetSchema` function to get the actual schema (#7) - Support connection via an existing socket fd (#321) +- `Header` struct for the response header (#237). It can be accessed via + `Header()` method of the `Response` interface. ### Changed @@ -67,6 +69,13 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. it (#321) - Rename `pool.GetPoolInfo` to `pool.GetInfo`. Change return type to `map[string]ConnectionInfo` (#321) +- `Response` is now an interface (#237) +- All responses are now implementations of the `Response` interface (#237) +- `IsPush()` method is added to the response iterator (#237). It returns + the information if the current response is a `PushResponse`. + `PushCode` constant is removed. +- Method `Get` for `Future` now returns response data (#237). To get the actual + response new `GetResponse` method has been added. ### Deprecated @@ -89,6 +98,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - IPROTO constants (#158) - Code() method from the Request interface (#158) - `Schema` field from the `Connection` struct (#7) +- `PushCode` constant (#237) ### Fixed diff --git a/README.md b/README.md index b1d47ee84..a08ce1794 100644 --- a/README.md +++ b/README.md @@ -128,12 +128,13 @@ func main() { if err != nil { fmt.Println("Connection refused:", err) } - resp, err := conn.Do(tarantool.NewInsertRequest(999). + data, err := conn.Do(tarantool.NewInsertRequest(999). Tuple([]interface{}{99999, "BB"}), ).Get() if err != nil { fmt.Println("Error", err) - fmt.Println("Code", resp.Code) + } else { + fmt.Printf("Data: %v", data) } } ``` @@ -241,7 +242,9 @@ of the requests is an array instead of array of arrays. #### IPROTO constants -IPROTO constants have been moved to a separate package [go-iproto](https://github.com/tarantool/go-iproto). +* IPROTO constants have been moved to a separate package [go-iproto](https://github.com/tarantool/go-iproto). +* `PushCode` constant is removed. To check whether the current response is + a push response, use `IsPush()` method of the response iterator instead. #### Request changes @@ -255,6 +258,19 @@ needs to be passed instead. * `UpdateRequest` and `UpsertRequest` structs no longer accept `interface{}` for an `ops` field. `*Operations` needs to be used instead. +#### Response changes + +* `Response` is now an interface. +* Response header stored in a new `Header` struct. It could be accessed via + `Header()` method. +* `ResponseIterator` interface now has `IsPush()` method. + It returns true if the current response is a push response. + +#### Future changes + +* Method `Get` now returns response data instead of the actual response. +* New method `GetResponse` added to get an actual response. + #### Connect function `connection.Connect` no longer return non-working connection objects. This function diff --git a/box_error_test.go b/box_error_test.go index b08b6cc52..6839a1477 100644 --- a/box_error_test.go +++ b/box_error_test.go @@ -306,9 +306,11 @@ func TestErrorTypeEval(t *testing.T) { t.Run(name, func(t *testing.T) { resp, err := conn.Eval("return ...", []interface{}{&testcase.tuple.val}) require.Nil(t, err) - require.NotNil(t, resp.Data) - require.Equal(t, len(resp.Data), 1) - actual, ok := resp.Data[0].(*BoxError) + data, err := resp.Decode() + require.Nil(t, err) + require.NotNil(t, data) + require.Equal(t, len(data), 1) + actual, ok := data[0].(*BoxError) require.Truef(t, ok, "Response data has valid type") require.Equal(t, testcase.tuple.val, *actual) }) @@ -436,15 +438,17 @@ func TestErrorTypeSelect(t *testing.T) { _, err := conn.Eval(insertEval, []interface{}{}) require.Nilf(t, err, "Tuple has been successfully inserted") - var resp *Response + var resp Response var offset uint32 = 0 var limit uint32 = 1 resp, err = conn.Select(space, index, offset, limit, IterEq, []interface{}{testcase.tuple.pk}) require.Nil(t, err) - require.NotNil(t, resp.Data) - require.Equalf(t, len(resp.Data), 1, "Exactly one tuple had been found") - tpl, ok := resp.Data[0].([]interface{}) + data, err := resp.Decode() + require.Nil(t, err) + require.NotNil(t, data) + require.Equalf(t, len(data), 1, "Exactly one tuple had been found") + tpl, ok := data[0].([]interface{}) require.Truef(t, ok, "Tuple has valid type") require.Equal(t, testcase.tuple.pk, tpl[0]) actual, ok := tpl[1].(*BoxError) diff --git a/connection.go b/connection.go index bf9f1554e..1dab74187 100644 --- a/connection.go +++ b/connection.go @@ -97,9 +97,9 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac log.Printf("tarantool: last reconnect to %s failed: %s, giving it up", conn.Addr(), err) case LogUnexpectedResultId: - resp := v[0].(*Response) + header := v[0].(Header) log.Printf("tarantool: connection %s got unexpected resultId (%d) in response", - conn.Addr(), resp.RequestId) + conn.Addr(), header.RequestId) case LogWatchEventReadFailed: err := v[0].(error) log.Printf("tarantool: unable to parse watch event: %s", err) @@ -807,8 +807,8 @@ func (conn *Connection) reader(r io.Reader, c Conn) { conn.reconnect(err, c) return } - resp := &Response{buf: smallBuf{b: respBytes}} - err = resp.decodeHeader(conn.dec) + buf := smallBuf{b: respBytes} + header, err := decodeHeader(conn.dec, &buf) if err != nil { err = ClientError{ ErrProtocolError, @@ -818,8 +818,9 @@ func (conn *Connection) reader(r io.Reader, c Conn) { return } + resp := &ConnResponse{header: header, buf: buf} var fut *Future = nil - if iproto.Type(resp.Code) == iproto.IPROTO_EVENT { + if iproto.Type(header.Code) == iproto.IPROTO_EVENT { if event, err := readWatchEvent(&resp.buf); err == nil { events <- event } else { @@ -830,19 +831,19 @@ func (conn *Connection) reader(r io.Reader, c Conn) { conn.opts.Logger.Report(LogWatchEventReadFailed, conn, err) } continue - } else if resp.Code == PushCode { - if fut = conn.peekFuture(resp.RequestId); fut != nil { + } else if header.Code == uint32(iproto.IPROTO_CHUNK) { + if fut = conn.peekFuture(header.RequestId); fut != nil { fut.AppendPush(resp) } } else { - if fut = conn.fetchFuture(resp.RequestId); fut != nil { + if fut = conn.fetchFuture(header.RequestId); fut != nil { fut.SetResponse(resp) conn.markDone(fut) } } if fut == nil { - conn.opts.Logger.Report(LogUnexpectedResultId, conn, resp) + conn.opts.Logger.Report(LogUnexpectedResultId, conn, header) } } } @@ -1052,10 +1053,7 @@ func (conn *Connection) putFuture(fut *Future, req Request, streamId uint64) { if req.Async() { if fut = conn.fetchFuture(reqid); fut != nil { - resp := &Response{ - RequestId: reqid, - Code: OkCode, - } + resp := &ConnResponse{} fut.SetResponse(resp) conn.markDone(fut) } @@ -1236,7 +1234,7 @@ func (conn *Connection) SetSchema(s Schema) { // NewPrepared passes a sql statement to Tarantool for preparation synchronously. func (conn *Connection) NewPrepared(expr string) (*Prepared, error) { req := NewPrepareRequest(expr) - resp, err := conn.Do(req).Get() + resp, err := conn.Do(req).GetResponse() if err != nil { return nil, err } diff --git a/connector.go b/connector.go index 9536116d7..b7f5affed 100644 --- a/connector.go +++ b/connector.go @@ -13,41 +13,41 @@ type Connector interface { // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. - Ping() (*Response, error) + Ping() (Response, error) // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. Select(space, index interface{}, offset, limit uint32, iterator Iter, - key interface{}) (*Response, error) + key interface{}) (Response, error) // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. - Insert(space interface{}, tuple interface{}) (*Response, error) + Insert(space interface{}, tuple interface{}) (Response, error) // Deprecated: the method will be removed in the next major version, // use a ReplicaRequest object + Do() instead. - Replace(space interface{}, tuple interface{}) (*Response, error) + Replace(space interface{}, tuple interface{}) (Response, error) // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. - Delete(space, index interface{}, key interface{}) (*Response, error) + Delete(space, index interface{}, key interface{}) (Response, error) // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. - Update(space, index interface{}, key interface{}, ops *Operations) (*Response, error) + Update(space, index interface{}, key interface{}, ops *Operations) (Response, error) // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. - Upsert(space interface{}, tuple interface{}, ops *Operations) (*Response, error) + Upsert(space interface{}, tuple interface{}, ops *Operations) (Response, error) // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. - Call(functionName string, args interface{}) (*Response, error) + Call(functionName string, args interface{}) (Response, error) // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. - Call16(functionName string, args interface{}) (*Response, error) + Call16(functionName string, args interface{}) (Response, error) // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. - Call17(functionName string, args interface{}) (*Response, error) + Call17(functionName string, args interface{}) (Response, error) // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. - Eval(expr string, args interface{}) (*Response, error) + Eval(expr string, args interface{}) (Response, error) // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. - Execute(expr string, args interface{}) (*Response, error) + Execute(expr string, args interface{}) (Response, error) // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. diff --git a/const.go b/const.go index 62650b5be..ede4c988d 100644 --- a/const.go +++ b/const.go @@ -9,6 +9,5 @@ const ( ) const ( - OkCode = uint32(iproto.IPROTO_OK) - PushCode = uint32(iproto.IPROTO_CHUNK) + OkCode = uint32(iproto.IPROTO_OK) ) diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index dfc8d064e..8ee28cf09 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -517,39 +517,32 @@ func testSelectGeneratedData(t *testing.T, conn tarantool.Connector, Limit(20). Iterator(tarantool.IterGe). Key([]interface{}{uint(1010)}) - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Select: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != expectedTuplesCount { - t.Fatalf("Response Data len %d != %d", len(resp.Data), expectedTuplesCount) + if len(data) != expectedTuplesCount { + t.Fatalf("Response Data len %d != %d", len(data), expectedTuplesCount) } } func testCrudRequestCheck(t *testing.T, req tarantool.Request, - resp *tarantool.Response, err error, expectedLen int) { + data []interface{}, err error, expectedLen int) { t.Helper() if err != nil { t.Fatalf("Failed to Do CRUD request: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Do CRUD request") - } - - if len(resp.Data) < expectedLen { + if len(data) < expectedLen { t.Fatalf("Response Body len < %#v, actual len %#v", - expectedLen, len(resp.Data)) + expectedLen, len(data)) } // resp.Data[0] - CRUD res. // resp.Data[1] - CRUD err. - if expectedLen >= 2 && resp.Data[1] != nil { - if crudErr, err := getCrudError(req, resp.Data[1]); err != nil { + if expectedLen >= 2 && data[1] != nil { + if crudErr, err := getCrudError(req, data[1]); err != nil { t.Fatalf("Failed to get CRUD error: %#v", err) } else if crudErr != nil { t.Fatalf("Failed to perform CRUD request on CRUD side: %#v", crudErr) @@ -569,8 +562,8 @@ func TestCrudGenerateData(t *testing.T) { conn.Do(req).Get() } - resp, err := conn.Do(testCase.req).Get() - testCrudRequestCheck(t, testCase.req, resp, + data, err := conn.Do(testCase.req).Get() + testCrudRequestCheck(t, testCase.req, data, err, testCase.expectedRespLen) testSelectGeneratedData(t, conn, testCase.expectedTuplesCount) @@ -591,8 +584,8 @@ func TestCrudProcessData(t *testing.T) { for _, testCase := range testProcessDataCases { t.Run(testCase.name, func(t *testing.T) { testCrudRequestPrepareData(t, conn) - resp, err := conn.Do(testCase.req).Get() - testCrudRequestCheck(t, testCase.req, resp, + data, err := conn.Do(testCase.req).Get() + testCrudRequestCheck(t, testCase.req, data, err, testCase.expectedRespLen) for i := 1010; i < 1020; i++ { req := tarantool.NewDeleteRequest(spaceName). @@ -623,8 +616,8 @@ func TestCrudUpdateSplice(t *testing.T) { Opts(simpleOperationOpts) testCrudRequestPrepareData(t, conn) - resp, err := conn.Do(req).Get() - testCrudRequestCheck(t, req, resp, + data, err := conn.Do(req).Get() + testCrudRequestCheck(t, req, data, err, 2) } @@ -648,8 +641,8 @@ func TestCrudUpsertSplice(t *testing.T) { Opts(simpleOperationOpts) testCrudRequestPrepareData(t, conn) - resp, err := conn.Do(req).Get() - testCrudRequestCheck(t, req, resp, + data, err := conn.Do(req).Get() + testCrudRequestCheck(t, req, data, err, 2) } @@ -673,8 +666,8 @@ func TestCrudUpsertObjectSplice(t *testing.T) { Opts(simpleOperationOpts) testCrudRequestPrepareData(t, conn) - resp, err := conn.Do(req).Get() - testCrudRequestCheck(t, req, resp, + data, err := conn.Do(req).Get() + testCrudRequestCheck(t, req, data, err, 2) } @@ -719,11 +712,11 @@ func TestUnflattenRows(t *testing.T) { req := crud.MakeReplaceRequest(spaceName). Tuple(tuple). Opts(simpleOperationOpts) - resp, err := conn.Do(req).Get() - testCrudRequestCheck(t, req, resp, err, 2) + data, err := conn.Do(req).Get() + testCrudRequestCheck(t, req, data, err, 2) - if res, ok = resp.Data[0].(map[interface{}]interface{}); !ok { - t.Fatalf("Unexpected CRUD result: %#v", resp.Data[0]) + if res, ok = data[0].(map[interface{}]interface{}); !ok { + t.Fatalf("Unexpected CRUD result: %#v", data[0]) } if rawMetadata, ok := res["metadata"]; !ok { @@ -1293,21 +1286,21 @@ func TestNoreturnOption(t *testing.T) { conn.Do(req).Get() } - resp, err := conn.Do(testCase.req).Get() + data, err := conn.Do(testCase.req).Get() if err != nil { t.Fatalf("Failed to Do CRUD request: %s", err) } - if len(resp.Data) == 0 { + if len(data) == 0 { t.Fatalf("Expected explicit nil") } - if resp.Data[0] != nil { - t.Fatalf("Expected nil result, got %v", resp.Data[0]) + if data[0] != nil { + t.Fatalf("Expected nil result, got %v", data[0]) } - if len(resp.Data) >= 2 && resp.Data[1] != nil { - t.Fatalf("Expected no returned errors, got %v", resp.Data[1]) + if len(data) >= 2 && data[1] != nil { + t.Fatalf("Expected no returned errors, got %v", data[1]) } for i := 1010; i < 1020; i++ { diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 630d5f062..8be50e0e7 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -405,12 +405,12 @@ func TestDatetimeTarantoolInterval(t *testing.T) { func(t *testing.T) { req := NewCallRequest("call_datetime_interval"). Args([]interface{}{dti, dtj}) - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { t.Fatalf("Unable to call call_datetime_interval: %s", err) } ival := dti.Interval(dtj) - ret := resp.Data[0].(Interval) + ret := data[0].(Interval) if !reflect.DeepEqual(ival, ret) { t.Fatalf("%v != %v", ival, ret) } @@ -540,13 +540,13 @@ func TestCustomTimezone(t *testing.T) { } req := NewReplaceRequest(spaceTuple1).Tuple([]interface{}{dt, "payload"}) - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { t.Fatalf("Datetime replace failed %s", err.Error()) } - assertDatetimeIsEqual(t, resp.Data, tm) + assertDatetimeIsEqual(t, data, tm) - tpl := resp.Data[0].([]interface{}) + tpl := data[0].([]interface{}) if respDt, ok := tpl[0].(Datetime); ok { zone := respDt.ToTime().Location().String() _, offset := respDt.ToTime().Zone() @@ -592,25 +592,19 @@ func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) { Limit(limit). Iterator(IterEq). Key([]interface{}{dt}) - resp, err := conn.Do(sel).Get() + data, err := conn.Do(sel).Get() if err != nil { t.Fatalf("Datetime select failed: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - assertDatetimeIsEqual(t, resp.Data, tm) + assertDatetimeIsEqual(t, data, tm) // Delete tuple with datetime. del := NewDeleteRequest(spaceTuple1).Index(index).Key([]interface{}{dt}) - resp, err = conn.Do(del).Get() + data, err = conn.Do(del).Get() if err != nil { t.Fatalf("Datetime delete failed: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Delete") - } - assertDatetimeIsEqual(t, resp.Data, tm) + assertDatetimeIsEqual(t, data, tm) } var datetimeSample = []struct { @@ -747,28 +741,22 @@ func TestDatetimeReplace(t *testing.T) { t.Fatalf("Unable to create Datetime from %s: %s", tm, err) } rep := NewReplaceRequest(spaceTuple1).Tuple([]interface{}{dt, "payload"}) - resp, err := conn.Do(rep).Get() + data, err := conn.Do(rep).Get() if err != nil { t.Fatalf("Datetime replace failed: %s", err) } - if resp == nil { - t.Fatalf("Response is nil after Replace") - } - assertDatetimeIsEqual(t, resp.Data, tm) + assertDatetimeIsEqual(t, data, tm) sel := NewSelectRequest(spaceTuple1). Index(index). Limit(1). Iterator(IterEq). Key([]interface{}{dt}) - resp, err = conn.Do(sel).Get() + data, err = conn.Do(sel).Get() if err != nil { t.Fatalf("Datetime select failed: %s", err) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - assertDatetimeIsEqual(t, resp.Data, tm) + assertDatetimeIsEqual(t, data, tm) // Delete tuple with datetime. del := NewDeleteRequest(spaceTuple1).Index(index).Key([]interface{}{dt}) @@ -923,15 +911,15 @@ func TestCustomEncodeDecodeTuple1(t *testing.T) { }, } rep := NewReplaceRequest(spaceTuple2).Tuple(&tuple) - resp, err := conn.Do(rep).Get() - if err != nil || resp.Code != 0 { + data, err := conn.Do(rep).Get() + if err != nil { t.Fatalf("Failed to replace: %s", err.Error()) } - if len(resp.Data) != 1 { + if len(data) != 1 { t.Fatalf("Response Body len != 1") } - tpl, ok := resp.Data[0].([]interface{}) + tpl, ok := data[0].([]interface{}) if !ok { t.Fatalf("Unexpected body of Replace") } @@ -1033,11 +1021,11 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) { Limit(1). Iterator(IterEq). Key([]interface{}{dt}) - resp, errSel := conn.Do(sel).Get() + data, errSel := conn.Do(sel).Get() if errSel != nil { t.Errorf("Failed to Select: %s", errSel.Error()) } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { if val, ok := tpl[0].(Datetime); !ok || !val.ToTime().Equal(tm) { diff --git a/datetime/example_test.go b/datetime/example_test.go index 72d3448c2..ac5f40500 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -50,22 +50,21 @@ func Example() { index := "primary" // Replace a tuple with datetime. - resp, err := conn.Do(tarantool.NewReplaceRequest(space). + data, err := conn.Do(tarantool.NewReplaceRequest(space). Tuple([]interface{}{dt}), ).Get() if err != nil { fmt.Printf("Error in replace is %v", err) return } - respDt := resp.Data[0].([]interface{})[0].(Datetime) + respDt := data[0].([]interface{})[0].(Datetime) fmt.Println("Datetime tuple replace") - fmt.Printf("Code: %d\n", resp.Code) fmt.Printf("Data: %v\n", respDt.ToTime()) // Select a tuple with datetime. var offset uint32 = 0 var limit uint32 = 1 - resp, err = conn.Do(tarantool.NewSelectRequest(space). + data, err = conn.Do(tarantool.NewSelectRequest(space). Index(index). Offset(offset). Limit(limit). @@ -76,13 +75,12 @@ func Example() { fmt.Printf("Error in select is %v", err) return } - respDt = resp.Data[0].([]interface{})[0].(Datetime) + respDt = data[0].([]interface{})[0].(Datetime) fmt.Println("Datetime tuple select") - fmt.Printf("Code: %d\n", resp.Code) fmt.Printf("Data: %v\n", respDt.ToTime()) // Delete a tuple with datetime. - resp, err = conn.Do(tarantool.NewDeleteRequest(space). + data, err = conn.Do(tarantool.NewDeleteRequest(space). Index(index). Key([]interface{}{dt}), ).Get() @@ -90,9 +88,8 @@ func Example() { fmt.Printf("Error in delete is %v", err) return } - respDt = resp.Data[0].([]interface{})[0].(Datetime) + respDt = data[0].([]interface{})[0].(Datetime) fmt.Println("Datetime tuple delete") - fmt.Printf("Code: %d\n", resp.Code) fmt.Printf("Data: %v\n", respDt.ToTime()) } diff --git a/datetime/interval_test.go b/datetime/interval_test.go index 4e3cf5ab1..95142fe47 100644 --- a/datetime/interval_test.go +++ b/datetime/interval_test.go @@ -121,12 +121,12 @@ func TestIntervalTarantoolEncoding(t *testing.T) { t.Run(fmt.Sprintf("%v", tc), func(t *testing.T) { req := tarantool.NewCallRequest("call_interval_testdata"). Args([]interface{}{tc}) - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } - ret := resp.Data[0].(Interval) + ret := data[0].(Interval) if !reflect.DeepEqual(ret, tc) { t.Fatalf("Unexpected response: %v, expected %v", ret, tc) } diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 14ce05b2a..573daa8f6 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -538,14 +538,11 @@ func TestSelect(t *testing.T) { } ins := NewInsertRequest(space).Tuple([]interface{}{MakeDecimal(number)}) - resp, err := conn.Do(ins).Get() + data, err := conn.Do(ins).Get() if err != nil { t.Fatalf("Decimal insert failed: %s", err) } - if resp == nil { - t.Fatalf("Response is nil after Replace") - } - tupleValueIsDecimal(t, resp.Data, number) + tupleValueIsDecimal(t, data, number) var offset uint32 = 0 var limit uint32 = 1 @@ -555,21 +552,18 @@ func TestSelect(t *testing.T) { Limit(limit). Iterator(IterEq). Key([]interface{}{MakeDecimal(number)}) - resp, err = conn.Do(sel).Get() + data, err = conn.Do(sel).Get() if err != nil { t.Fatalf("Decimal select failed: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - tupleValueIsDecimal(t, resp.Data, number) + tupleValueIsDecimal(t, data, number) del := NewDeleteRequest(space).Index(index).Key([]interface{}{MakeDecimal(number)}) - resp, err = conn.Do(del).Get() + data, err = conn.Do(del).Get() if err != nil { t.Fatalf("Decimal delete failed: %s", err) } - tupleValueIsDecimal(t, resp.Data, number) + tupleValueIsDecimal(t, data, number) } func TestUnmarshal_from_decimal_new(t *testing.T) { @@ -591,14 +585,11 @@ func TestUnmarshal_from_decimal_new(t *testing.T) { call := NewEvalRequest("return require('decimal').new(...)"). Args([]interface{}{str}) - resp, err := conn.Do(call).Get() + data, err := conn.Do(call).Get() if err != nil { t.Fatalf("Decimal create failed: %s", err) } - if resp == nil { - t.Fatalf("Response is nil after Call") - } - tupleValueIsDecimal(t, []interface{}{resp.Data}, number) + tupleValueIsDecimal(t, []interface{}{data}, number) }) } } @@ -610,21 +601,18 @@ func assertInsert(t *testing.T, conn *Connection, numString string) { } ins := NewInsertRequest(space).Tuple([]interface{}{MakeDecimal(number)}) - resp, err := conn.Do(ins).Get() + data, err := conn.Do(ins).Get() if err != nil { t.Fatalf("Decimal insert failed: %s", err) } - if resp == nil { - t.Fatalf("Response is nil after Replace") - } - tupleValueIsDecimal(t, resp.Data, number) + tupleValueIsDecimal(t, data, number) del := NewDeleteRequest(space).Index(index).Key([]interface{}{MakeDecimal(number)}) - resp, err = conn.Do(del).Get() + data, err = conn.Do(del).Get() if err != nil { t.Fatalf("Decimal delete failed: %s", err) } - tupleValueIsDecimal(t, resp.Data, number) + tupleValueIsDecimal(t, data, number) } func TestInsert(t *testing.T) { @@ -654,28 +642,22 @@ func TestReplace(t *testing.T) { } rep := NewReplaceRequest(space).Tuple([]interface{}{MakeDecimal(number)}) - respRep, errRep := conn.Do(rep).Get() + dataRep, errRep := conn.Do(rep).Get() if errRep != nil { t.Fatalf("Decimal replace failed: %s", errRep) } - if respRep == nil { - t.Fatalf("Response is nil after Replace") - } - tupleValueIsDecimal(t, respRep.Data, number) + tupleValueIsDecimal(t, dataRep, number) sel := NewSelectRequest(space). Index(index). Limit(1). Iterator(IterEq). Key([]interface{}{MakeDecimal(number)}) - respSel, errSel := conn.Do(sel).Get() + dataSel, errSel := conn.Do(sel).Get() if errSel != nil { t.Fatalf("Decimal select failed: %s", errSel) } - if respSel == nil { - t.Fatalf("Response is nil after Select") - } - tupleValueIsDecimal(t, respSel.Data, number) + tupleValueIsDecimal(t, dataSel, number) } // runTestMain is a body of TestMain function diff --git a/decimal/example_test.go b/decimal/example_test.go index 3f7d4de06..5597590dc 100644 --- a/decimal/example_test.go +++ b/decimal/example_test.go @@ -44,18 +44,14 @@ func Example() { log.Fatalf("Failed to prepare test decimal: %s", err) } - resp, err := client.Do(tarantool.NewReplaceRequest(spaceNo). + data, err := client.Do(tarantool.NewReplaceRequest(spaceNo). Tuple([]interface{}{number}), ).Get() if err != nil { log.Fatalf("Decimal replace failed: %s", err) } - if resp == nil { - log.Fatalf("Response is nil after Replace") - } log.Println("Decimal tuple replace") log.Println("Error", err) - log.Println("Code", resp.Code) - log.Println("Data", resp.Data) + log.Println("Data", data) } diff --git a/dial.go b/dial.go index eae8e1283..37cbe0139 100644 --- a/dial.go +++ b/dial.go @@ -400,19 +400,27 @@ func identify(w writeFlusher, r io.Reader) (ProtocolInfo, error) { resp, err := readResponse(r) if err != nil { - if iproto.Error(resp.Code) == iproto.ER_UNKNOWN_REQUEST_TYPE { - // IPROTO_ID requests are not supported by server. - return info, nil - } - return info, err } + data, err := resp.Decode() + if err != nil { + switch err := err.(type) { + case Error: + if err.Code == iproto.ER_UNKNOWN_REQUEST_TYPE { + // IPROTO_ID requests are not supported by server. + return info, nil + } + return info, err + default: + return info, fmt.Errorf("decode response body error: %w", err) + } + } - if len(resp.Data) == 0 { + if len(data) == 0 { return info, errors.New("unexpected response: no data") } - info, ok := resp.Data[0].(ProtocolInfo) + info, ok := data[0].(ProtocolInfo) if !ok { return info, errors.New("unexpected response: wrong data") } @@ -503,23 +511,14 @@ func readResponse(r io.Reader) (Response, error) { respBytes, err := read(r, lenbuf[:]) if err != nil { - return Response{}, fmt.Errorf("read error: %w", err) + return &ConnResponse{}, fmt.Errorf("read error: %w", err) } - resp := Response{buf: smallBuf{b: respBytes}} - err = resp.decodeHeader(msgpack.NewDecoder(&smallBuf{})) + buf := smallBuf{b: respBytes} + header, err := decodeHeader(msgpack.NewDecoder(&smallBuf{}), &buf) + resp := &ConnResponse{header: header, buf: buf} if err != nil { return resp, fmt.Errorf("decode response header error: %w", err) } - - err = resp.decodeBody() - if err != nil { - switch err.(type) { - case Error: - return resp, err - default: - return resp, fmt.Errorf("decode response body error: %w", err) - } - } return resp, nil } diff --git a/dial_test.go b/dial_test.go index 17f037e00..88a582b07 100644 --- a/dial_test.go +++ b/dial_test.go @@ -303,9 +303,8 @@ func TestConn_ReadWrite(t *testing.T) { 0x80, // Empty map. }, dialer.conn.writebuf.Bytes()) - resp, err := fut.Get() + _, err := fut.Get() assert.Nil(t, err) - assert.NotNil(t, resp) } func TestConn_ContextCancel(t *testing.T) { diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index 316e0086f..a2706f3f6 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -97,13 +97,12 @@ func Example_customUnpacking() { tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} // Insert a structure itself. initReq := tarantool.NewReplaceRequest(spaceNo).Tuple(&tuple) - resp, err := conn.Do(initReq).Get() + data, err := conn.Do(initReq).Get() if err != nil { log.Fatalf("Failed to insert: %s", err.Error()) return } - fmt.Println("Data", resp.Data) - fmt.Println("Code", resp.Code) + fmt.Println("Data", data) var tuples1 []Tuple2 selectReq := tarantool.NewSelectRequest(spaceNo). @@ -139,7 +138,6 @@ func Example_customUnpacking() { // Output: // Data [[777 orig [[lol 1] [wut 3]]]] - // Code 0 // Tuples (tuples1) [{777 orig [{lol 1} {wut 3}]}] // Tuples (tuples2): [{{} 777 orig [{lol 1} {wut 3}]}] // Tuples (tuples3): [[{{} 221 [{Moscow 34} {Minsk 23} {Kiev 31}]}]] diff --git a/example_test.go b/example_test.go index 6500e18f4..a39b9d5a1 100644 --- a/example_test.go +++ b/example_test.go @@ -149,12 +149,10 @@ func ExamplePingRequest() { defer conn.Close() // Ping a Tarantool instance to check connection. - resp, err := conn.Do(tarantool.NewPingRequest()).Get() - fmt.Println("Ping Code", resp.Code) - fmt.Println("Ping Data", resp.Data) + data, err := conn.Do(tarantool.NewPingRequest()).Get() + fmt.Println("Ping Data", data) fmt.Println("Ping Error", err) // Output: - // Ping Code 0 // Ping Data [] // Ping Error } @@ -181,11 +179,11 @@ func ExamplePingRequest_Context() { req := tarantool.NewPingRequest().Context(ctx) // Ping a Tarantool instance to check connection. - resp, err := conn.Do(req).Get() - fmt.Println("Ping Resp", resp) + data, err := conn.Do(req).Get() + fmt.Println("Ping Resp data", data) fmt.Println("Ping Error", err) // Output: - // Ping Resp + // Ping Resp data [] // Ping Error context is done } @@ -200,7 +198,7 @@ func ExampleSelectRequest() { } key := []interface{}{uint(1111)} - resp, err := conn.Do(tarantool.NewSelectRequest(617). + data, err := conn.Do(tarantool.NewSelectRequest(617). Limit(100). Iterator(tarantool.IterEq). Key(key), @@ -210,7 +208,7 @@ func ExampleSelectRequest() { fmt.Printf("error in select is %v", err) return } - fmt.Printf("response is %#v\n", resp.Data) + fmt.Printf("response is %#v\n", data) var res []Tuple err = conn.Do(tarantool.NewSelectRequest("test"). @@ -236,12 +234,12 @@ func ExampleSelectRequest_spaceAndIndexNames() { req := tarantool.NewSelectRequest(spaceName) req.Index(indexName) - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { fmt.Printf("Failed to execute the request: %s\n", err) } else { - fmt.Println(resp.Data) + fmt.Println(data) } } @@ -250,21 +248,19 @@ func ExampleInsertRequest() { defer conn.Close() // Insert a new tuple { 31, 1 }. - resp, err := conn.Do(tarantool.NewInsertRequest(spaceNo). + data, err := conn.Do(tarantool.NewInsertRequest(spaceNo). Tuple([]interface{}{uint(31), "test", "one"}), ).Get() fmt.Println("Insert 31") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) + fmt.Println("Data", data) // Insert a new tuple { 32, 1 }. - resp, err = conn.Do(tarantool.NewInsertRequest("test"). + data, err = conn.Do(tarantool.NewInsertRequest("test"). Tuple(&Tuple{Id: 32, Msg: "test", Name: "one"}), ).Get() fmt.Println("Insert 32") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) + fmt.Println("Data", data) // Delete tuple with primary key { 31 }. conn.Do(tarantool.NewDeleteRequest("test"). @@ -279,11 +275,9 @@ func ExampleInsertRequest() { // Output: // Insert 31 // Error - // Code 0 // Data [[31 test one]] // Insert 32 // Error - // Code 0 // Data [[32 test one]] } @@ -292,12 +286,12 @@ func ExampleInsertRequest_spaceAndIndexNames() { defer conn.Close() req := tarantool.NewInsertRequest(spaceName) - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { fmt.Printf("Failed to execute the request: %s\n", err) } else { - fmt.Println(resp.Data) + fmt.Println(data) } } @@ -315,32 +309,28 @@ func ExampleDeleteRequest() { ).Get() // Delete tuple with primary key { 35 }. - resp, err := conn.Do(tarantool.NewDeleteRequest(spaceNo). + data, err := conn.Do(tarantool.NewDeleteRequest(spaceNo). Index(indexNo). Key([]interface{}{uint(35)}), ).Get() fmt.Println("Delete 35") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) + fmt.Println("Data", data) // Delete tuple with primary key { 36 }. - resp, err = conn.Do(tarantool.NewDeleteRequest("test"). + data, err = conn.Do(tarantool.NewDeleteRequest("test"). Index("primary"). Key([]interface{}{uint(36)}), ).Get() fmt.Println("Delete 36") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) + fmt.Println("Data", data) // Output: // Delete 35 // Error - // Code 0 // Data [[35 test one]] // Delete 36 // Error - // Code 0 // Data [[36 test one]] } @@ -350,12 +340,12 @@ func ExampleDeleteRequest_spaceAndIndexNames() { req := tarantool.NewDeleteRequest(spaceName) req.Index(indexName) - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { fmt.Printf("Failed to execute the request: %s\n", err) } else { - fmt.Println(resp.Data) + fmt.Println(data) } } @@ -371,50 +361,42 @@ func ExampleReplaceRequest() { // Replace a tuple with primary key 13. // Note, Tuple is defined within tests, and has EncdodeMsgpack and // DecodeMsgpack methods. - resp, err := conn.Do(tarantool.NewReplaceRequest(spaceNo). + data, err := conn.Do(tarantool.NewReplaceRequest(spaceNo). Tuple([]interface{}{uint(13), 1}), ).Get() fmt.Println("Replace 13") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - resp, err = conn.Do(tarantool.NewReplaceRequest("test"). + fmt.Println("Data", data) + data, err = conn.Do(tarantool.NewReplaceRequest("test"). Tuple([]interface{}{uint(13), 1}), ).Get() fmt.Println("Replace 13") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - resp, err = conn.Do(tarantool.NewReplaceRequest("test"). + fmt.Println("Data", data) + data, err = conn.Do(tarantool.NewReplaceRequest("test"). Tuple(&Tuple{Id: 13, Msg: "test", Name: "eleven"}), ).Get() fmt.Println("Replace 13") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - resp, err = conn.Do(tarantool.NewReplaceRequest("test"). + fmt.Println("Data", data) + data, err = conn.Do(tarantool.NewReplaceRequest("test"). Tuple(&Tuple{Id: 13, Msg: "test", Name: "twelve"}), ).Get() fmt.Println("Replace 13") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) + fmt.Println("Data", data) // Output: // Replace 13 // Error - // Code 0 // Data [[13 1]] // Replace 13 // Error - // Code 0 // Data [[13 1]] // Replace 13 // Error - // Code 0 // Data [[13 test eleven]] // Replace 13 // Error - // Code 0 // Data [[13 test twelve]] } @@ -423,12 +405,12 @@ func ExampleReplaceRequest_spaceAndIndexNames() { defer conn.Close() req := tarantool.NewReplaceRequest(spaceName) - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { fmt.Printf("Failed to execute the request: %s\n", err) } else { - fmt.Println(resp.Data) + fmt.Println(data) } } @@ -453,12 +435,12 @@ func ExampleUpdateRequest() { Splice(1, 1, 2, "!!"). Insert(7, "new"). Assign(7, "updated")) - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { fmt.Printf("error in do update request is %v", err) return } - fmt.Printf("response is %#v\n", resp.Data) + fmt.Printf("response is %#v\n", data) // Output: // response is []interface {}{[]interface {}{0x457, "t!!t", 2, 0, 1, 1, 0, "updated"}} } @@ -469,12 +451,12 @@ func ExampleUpdateRequest_spaceAndIndexNames() { req := tarantool.NewUpdateRequest(spaceName) req.Index(indexName) - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { fmt.Printf("Failed to execute the request: %s\n", err) } else { - fmt.Println(resp.Data) + fmt.Println(data) } } @@ -486,33 +468,33 @@ func ExampleUpsertRequest() { req = tarantool.NewUpsertRequest(617). Tuple([]interface{}{uint(1113), "first", "first"}). Operations(tarantool.NewOperations().Assign(1, "updated")) - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { fmt.Printf("error in do select upsert is %v", err) return } - fmt.Printf("response is %#v\n", resp.Data) + fmt.Printf("response is %#v\n", data) req = tarantool.NewUpsertRequest("test"). Tuple([]interface{}{uint(1113), "second", "second"}). Operations(tarantool.NewOperations().Assign(2, "updated")) fut := conn.Do(req) - resp, err = fut.Get() + data, err = fut.Get() if err != nil { fmt.Printf("error in do async upsert request is %v", err) return } - fmt.Printf("response is %#v\n", resp.Data) + fmt.Printf("response is %#v\n", data) req = tarantool.NewSelectRequest(617). Limit(100). Key(tarantool.IntKey{1113}) - resp, err = conn.Do(req).Get() + data, err = conn.Do(req).Get() if err != nil { fmt.Printf("error in do select request is %v", err) return } - fmt.Printf("response is %#v\n", resp.Data) + fmt.Printf("response is %#v\n", data) // Output: // response is []interface {}{} // response is []interface {}{} @@ -524,12 +506,12 @@ func ExampleUpsertRequest_spaceAndIndexNames() { defer conn.Close() req := tarantool.NewUpsertRequest(spaceName) - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { fmt.Printf("Failed to execute the request: %s\n", err) } else { - fmt.Println(resp.Data) + fmt.Println(data) } } @@ -538,17 +520,15 @@ func ExampleCallRequest() { defer conn.Close() // Call a function 'simple_concat' with arguments. - resp, err := conn.Do(tarantool.NewCallRequest("simple_concat"). + data, err := conn.Do(tarantool.NewCallRequest("simple_concat"). Args([]interface{}{"1"}), ).Get() fmt.Println("Call simple_concat()") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) + fmt.Println("Data", data) // Output: // Call simple_concat() // Error - // Code 0 // Data [11] } @@ -557,15 +537,13 @@ func ExampleEvalRequest() { defer conn.Close() // Run raw Lua code. - resp, err := conn.Do(tarantool.NewEvalRequest("return 1 + 2")).Get() + data, err := conn.Do(tarantool.NewEvalRequest("return 1 + 2")).Get() fmt.Println("Eval 'return 1 + 2'") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) + fmt.Println("Data", data) // Output: // Eval 'return 1 + 2' // Error - // Code 0 // Data [3] } @@ -586,13 +564,14 @@ func ExampleExecuteRequest() { req := tarantool.NewExecuteRequest( "CREATE TABLE SQL_TEST (id INTEGER PRIMARY KEY, name STRING)") - resp, err := conn.Do(req).Get() + resp, err := conn.Do(req).GetResponse() fmt.Println("Execute") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) + data, err := resp.Decode() + fmt.Println("Error", err) + fmt.Println("Data", data) + fmt.Println("MetaData", resp.MetaData()) + fmt.Println("SQL Info", resp.SQLInfo()) // There are 4 options to pass named parameters to an SQL query: // 1) The simple map; @@ -623,55 +602,60 @@ func ExampleExecuteRequest() { req = tarantool.NewExecuteRequest( "CREATE TABLE SQL_TEST (id INTEGER PRIMARY KEY, name STRING)") req = req.Args(sqlBind1) - resp, err = conn.Do(req).Get() + resp, err = conn.Do(req).GetResponse() fmt.Println("Execute") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) + data, err = resp.Decode() + fmt.Println("Error", err) + fmt.Println("Data", data) + fmt.Println("MetaData", resp.MetaData()) + fmt.Println("SQL Info", resp.SQLInfo()) // 2) req = req.Args(sqlBind2) - resp, err = conn.Do(req).Get() + resp, err = conn.Do(req).GetResponse() fmt.Println("Execute") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) + data, err = resp.Decode() + fmt.Println("Error", err) + fmt.Println("Data", data) + fmt.Println("MetaData", resp.MetaData()) + fmt.Println("SQL Info", resp.SQLInfo()) // 3) req = req.Args(sqlBind3) - resp, err = conn.Do(req).Get() + resp, err = conn.Do(req).GetResponse() fmt.Println("Execute") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) + data, err = resp.Decode() + fmt.Println("Error", err) + fmt.Println("Data", data) + fmt.Println("MetaData", resp.MetaData()) + fmt.Println("SQL Info", resp.SQLInfo()) // 4) req = req.Args(sqlBind4) - resp, err = conn.Do(req).Get() + resp, err = conn.Do(req).GetResponse() fmt.Println("Execute") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) + data, err = resp.Decode() + fmt.Println("Error", err) + fmt.Println("Data", data) + fmt.Println("MetaData", resp.MetaData()) + fmt.Println("SQL Info", resp.SQLInfo()) // The way to pass positional arguments to an SQL query. req = tarantool.NewExecuteRequest( "SELECT id FROM SQL_TEST WHERE id=? AND name=?"). Args([]interface{}{2, "test"}) - resp, err = conn.Do(req).Get() + resp, err = conn.Do(req).GetResponse() fmt.Println("Execute") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) + data, err = resp.Decode() + fmt.Println("Error", err) + fmt.Println("Data", data) + fmt.Println("MetaData", resp.MetaData()) + fmt.Println("SQL Info", resp.SQLInfo()) // The way to pass SQL expression with using custom packing/unpacking for // a type. @@ -690,13 +674,14 @@ func ExampleExecuteRequest() { req = tarantool.NewExecuteRequest( "SELECT id FROM SQL_TEST WHERE id=? AND name=?"). Args([]interface{}{tarantool.KeyValueBind{"id", 1}, "test"}) - resp, err = conn.Do(req).Get() + resp, err = conn.Do(req).GetResponse() fmt.Println("Execute") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) - fmt.Println("MetaData", resp.MetaData) - fmt.Println("SQL Info", resp.SQLInfo) + data, err = resp.Decode() + fmt.Println("Error", err) + fmt.Println("Data", data) + fmt.Println("MetaData", resp.MetaData()) + fmt.Println("SQL Info", resp.SQLInfo()) } func getTestTxnDialer() tarantool.Dialer { @@ -716,7 +701,6 @@ func getTestTxnDialer() tarantool.Dialer { func ExampleCommitRequest() { var req tarantool.Request - var resp *tarantool.Response var err error // Tarantool supports streams and interactive transactions since version 2.10.0 @@ -733,22 +717,22 @@ func ExampleCommitRequest() { // Begin transaction req = tarantool.NewBeginRequest() - resp, err = stream.Do(req).Get() + data, err := stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Begin: %s", err.Error()) return } - fmt.Printf("Begin transaction: response is %#v\n", resp.Code) + fmt.Printf("Begin transaction: response is %#v\n", data) // Insert in stream req = tarantool.NewInsertRequest(spaceName). Tuple([]interface{}{uint(1001), "commit_hello", "commit_world"}) - resp, err = stream.Do(req).Get() + data, err = stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Insert: %s", err.Error()) return } - fmt.Printf("Insert in stream: response is %#v\n", resp.Code) + fmt.Printf("Insert in stream: response is %#v\n", data) // Select not related to the transaction // while transaction is not committed @@ -758,42 +742,41 @@ func ExampleCommitRequest() { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{uint(1001)}) - resp, err = conn.Do(selectReq).Get() + data, err = conn.Do(selectReq).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select out of stream before commit: response is %#v\n", resp.Data) + fmt.Printf("Select out of stream before commit: response is %#v\n", data) // Select in stream - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select in stream: response is %#v\n", resp.Data) + fmt.Printf("Select in stream: response is %#v\n", data) // Commit transaction req = tarantool.NewCommitRequest() - resp, err = stream.Do(req).Get() + data, err = stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Commit: %s", err.Error()) return } - fmt.Printf("Commit transaction: response is %#v\n", resp.Code) + fmt.Printf("Commit transaction: response is %#v\n", data) // Select outside of transaction - resp, err = conn.Do(selectReq).Get() + data, err = conn.Do(selectReq).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select after commit: response is %#v\n", resp.Data) + fmt.Printf("Select after commit: response is %#v\n", data) } func ExampleRollbackRequest() { var req tarantool.Request - var resp *tarantool.Response var err error // Tarantool supports streams and interactive transactions since version 2.10.0 @@ -810,22 +793,22 @@ func ExampleRollbackRequest() { // Begin transaction req = tarantool.NewBeginRequest() - resp, err = stream.Do(req).Get() + data, err := stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Begin: %s", err.Error()) return } - fmt.Printf("Begin transaction: response is %#v\n", resp.Code) + fmt.Printf("Begin transaction: response is %#v\n", data) // Insert in stream req = tarantool.NewInsertRequest(spaceName). Tuple([]interface{}{uint(2001), "rollback_hello", "rollback_world"}) - resp, err = stream.Do(req).Get() + data, err = stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Insert: %s", err.Error()) return } - fmt.Printf("Insert in stream: response is %#v\n", resp.Code) + fmt.Printf("Insert in stream: response is %#v\n", data) // Select not related to the transaction // while transaction is not committed @@ -835,42 +818,41 @@ func ExampleRollbackRequest() { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{uint(2001)}) - resp, err = conn.Do(selectReq).Get() + data, err = conn.Do(selectReq).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select out of stream: response is %#v\n", resp.Data) + fmt.Printf("Select out of stream: response is %#v\n", data) // Select in stream - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select in stream: response is %#v\n", resp.Data) + fmt.Printf("Select in stream: response is %#v\n", data) // Rollback transaction req = tarantool.NewRollbackRequest() - resp, err = stream.Do(req).Get() + data, err = stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Rollback: %s", err.Error()) return } - fmt.Printf("Rollback transaction: response is %#v\n", resp.Code) + fmt.Printf("Rollback transaction: response is %#v\n", data) // Select outside of transaction - resp, err = conn.Do(selectReq).Get() + data, err = conn.Do(selectReq).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select after Rollback: response is %#v\n", resp.Data) + fmt.Printf("Select after Rollback: response is %#v\n", data) } func ExampleBeginRequest_TxnIsolation() { var req tarantool.Request - var resp *tarantool.Response var err error // Tarantool supports streams and interactive transactions since version 2.10.0 @@ -889,22 +871,22 @@ func ExampleBeginRequest_TxnIsolation() { req = tarantool.NewBeginRequest(). TxnIsolation(tarantool.ReadConfirmedLevel). Timeout(500 * time.Millisecond) - resp, err = stream.Do(req).Get() + data, err := stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Begin: %s", err.Error()) return } - fmt.Printf("Begin transaction: response is %#v\n", resp.Code) + fmt.Printf("Begin transaction: response is %#v\n", data) // Insert in stream req = tarantool.NewInsertRequest(spaceName). Tuple([]interface{}{uint(2001), "rollback_hello", "rollback_world"}) - resp, err = stream.Do(req).Get() + data, err = stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Insert: %s", err.Error()) return } - fmt.Printf("Insert in stream: response is %#v\n", resp.Code) + fmt.Printf("Insert in stream: response is %#v\n", data) // Select not related to the transaction // while transaction is not committed @@ -914,37 +896,37 @@ func ExampleBeginRequest_TxnIsolation() { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{uint(2001)}) - resp, err = conn.Do(selectReq).Get() + data, err = conn.Do(selectReq).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select out of stream: response is %#v\n", resp.Data) + fmt.Printf("Select out of stream: response is %#v\n", data) // Select in stream - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select in stream: response is %#v\n", resp.Data) + fmt.Printf("Select in stream: response is %#v\n", data) // Rollback transaction req = tarantool.NewRollbackRequest() - resp, err = stream.Do(req).Get() + data, err = stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Rollback: %s", err.Error()) return } - fmt.Printf("Rollback transaction: response is %#v\n", resp.Code) + fmt.Printf("Rollback transaction: response is %#v\n", data) // Select outside of transaction - resp, err = conn.Do(selectReq).Get() + data, err = conn.Do(selectReq).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select after Rollback: response is %#v\n", resp.Data) + fmt.Printf("Select after Rollback: response is %#v\n", data) } func ExampleFuture_GetIterator() { @@ -959,14 +941,15 @@ func ExampleFuture_GetIterator() { var it tarantool.ResponseIterator for it = fut.GetIterator().WithTimeout(timeout); it.Next(); { resp := it.Value() - if resp.Code == tarantool.PushCode { + data, _ := resp.Decode() + if it.IsPush() { // It is a push message. - fmt.Printf("push message: %v\n", resp.Data[0]) - } else if resp.Code == tarantool.OkCode { + fmt.Printf("push message: %v\n", data[0]) + } else if resp.Header().Code == tarantool.OkCode { // It is a regular response. - fmt.Printf("response: %v", resp.Data[0]) + fmt.Printf("response: %v", data[0]) } else { - fmt.Printf("an unexpected response code %d", resp.Code) + fmt.Printf("an unexpected response code %d", resp.Header().Code) } } if err := it.Err(); err != nil { @@ -1148,11 +1131,11 @@ func ExampleConnection_Do() { // When the future receives the response, the result of the Future is set // and becomes available. We could wait for that moment with Future.Get() // or Future.GetTyped() methods. - resp, err := future.Get() + data, err := future.Get() if err != nil { fmt.Printf("Failed to execute the request: %s\n", err) } else { - fmt.Println(resp.Data) + fmt.Println(data) } // Output: @@ -1172,9 +1155,9 @@ func ExampleConnection_Do_failure() { future := conn.Do(req) // When the future receives the response, the result of the Future is set - // and becomes available. We could wait for that moment with Future.Get() - // or Future.GetTyped() methods. - resp, err := future.Get() + // and becomes available. We could wait for that moment with Future.Get(), + // Future.GetResponse() or Future.GetTyped() methods. + resp, err := future.GetResponse() if err != nil { // We don't print the error here to keep the example reproducible. // fmt.Printf("Failed to execute the request: %s\n", err) @@ -1184,8 +1167,8 @@ func ExampleConnection_Do_failure() { } else { // Response exist. So it could be a Tarantool error or a decode // error. We need to check the error code. - fmt.Printf("Error code from the response: %d\n", resp.Code) - if resp.Code == tarantool.OkCode { + fmt.Printf("Error code from the response: %d\n", resp.Header().Code) + if resp.Header().Code == tarantool.OkCode { fmt.Printf("Decode error: %s\n", err) } else { code := err.(tarantool.Error).Code @@ -1326,7 +1309,7 @@ func ExampleConnection_CloseGraceful_force() { // Force Connection.Close()! // Connection.CloseGraceful() done! // Result: - // connection closed by client (0x4001) + // [] connection closed by client (0x4001) } func ExampleWatchOnceRequest() { @@ -1344,11 +1327,11 @@ func ExampleWatchOnceRequest() { conn.Do(tarantool.NewBroadcastRequest(key).Value(value)).Get() - resp, err := conn.Do(tarantool.NewWatchOnceRequest(key)).Get() + data, err := conn.Do(tarantool.NewWatchOnceRequest(key)).Get() if err != nil { fmt.Printf("Failed to execute the request: %s\n", err) } else { - fmt.Println(resp.Data) + fmt.Println(data) } } @@ -1377,8 +1360,8 @@ func ExampleFdDialer() { fmt.Printf("connect error: %v\n", err) return } - resp, err := conn.Do(tarantool.NewPingRequest()).Get() - fmt.Println(resp.Code, err) + _, err = conn.Do(tarantool.NewPingRequest()).Get() + fmt.Println(err) // Output: - // 0 + // } diff --git a/future.go b/future.go index e7f7dae19..7281b6149 100644 --- a/future.go +++ b/future.go @@ -11,8 +11,8 @@ type Future struct { next *Future timeout time.Duration mutex sync.Mutex - pushes []*Response - resp *Response + pushes []Response + resp Response err error ready chan struct{} done chan struct{} @@ -40,7 +40,7 @@ func (fut *Future) isDone() bool { type asyncResponseIterator struct { fut *Future timeout time.Duration - resp *Response + resp Response err error curPos int done bool @@ -77,24 +77,24 @@ func (it *asyncResponseIterator) Next() bool { return false } - if it.err = it.resp.decodeBody(); it.err != nil { - it.resp = nil - return false - } - if last { it.done = true } else { + it.err = nil it.curPos += 1 } return true } -func (it *asyncResponseIterator) Value() *Response { +func (it *asyncResponseIterator) Value() Response { return it.resp } +func (it *asyncResponseIterator) IsPush() bool { + return !it.done +} + func (it *asyncResponseIterator) Err() error { return it.err } @@ -104,7 +104,7 @@ func (it *asyncResponseIterator) WithTimeout(timeout time.Duration) TimeoutRespo return it } -func (it *asyncResponseIterator) nextResponse() (resp *Response) { +func (it *asyncResponseIterator) nextResponse() (resp Response) { fut := it.fut pushesLen := len(fut.pushes) @@ -122,7 +122,7 @@ func NewFuture() (fut *Future) { fut = &Future{} fut.ready = make(chan struct{}, 1000000000) fut.done = make(chan struct{}) - fut.pushes = make([]*Response, 0) + fut.pushes = make([]Response, 0) return fut } @@ -131,21 +131,20 @@ func NewFuture() (fut *Future) { // // Deprecated: the method will be removed in the next major version, // use Connector.NewWatcher() instead of box.session.push(). -func (fut *Future) AppendPush(resp *Response) { +func (fut *Future) AppendPush(resp Response) { fut.mutex.Lock() defer fut.mutex.Unlock() if fut.isDone() { return } - resp.Code = PushCode fut.pushes = append(fut.pushes, resp) fut.ready <- struct{}{} } // SetResponse sets a response for the future and finishes the future. -func (fut *Future) SetResponse(resp *Response) { +func (fut *Future) SetResponse(resp Response) { fut.mutex.Lock() defer fut.mutex.Unlock() @@ -172,24 +171,35 @@ func (fut *Future) SetError(err error) { close(fut.done) } -// Get waits for Future to be filled and returns Response and error. -// -// Response will contain deserialized result in Data field. -// It will be []interface{}, so if you want more performance, use GetTyped method. +// GetResponse waits for Future to be filled and returns Response and error. // // Note: Response could be equal to nil if ClientError is returned in error. // // "error" could be Error, if it is error returned by Tarantool, // or ClientError, if something bad happens in a client process. -func (fut *Future) Get() (*Response, error) { +func (fut *Future) GetResponse() (Response, error) { fut.wait() if fut.err != nil { return fut.resp, fut.err } - err := fut.resp.decodeBody() + _, err := fut.resp.Decode() return fut.resp, err } +// Get waits for Future to be filled and returns the data of the Response and error. +// +// The data will be []interface{}, so if you want more performance, use GetTyped method. +// +// "error" could be Error, if it is error returned by Tarantool, +// or ClientError, if something bad happens in a client process. +func (fut *Future) Get() ([]interface{}, error) { + fut.wait() + if fut.err != nil { + return nil, fut.err + } + return fut.resp.Decode() +} + // GetTyped waits for Future and calls msgpack.Decoder.Decode(result) if no error happens. // It is could be much faster than Get() function. // @@ -199,8 +209,7 @@ func (fut *Future) GetTyped(result interface{}) error { if fut.err != nil { return fut.err } - err := fut.resp.decodeBodyTyped(result) - return err + return fut.resp.DecodeTyped(result) } // GetIterator returns an iterator for iterating through push messages diff --git a/future_test.go b/future_test.go index 274bee7d5..3a3d8d01d 100644 --- a/future_test.go +++ b/future_test.go @@ -6,11 +6,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" . "github.com/tarantool/go-tarantool/v2" ) func assertResponseIteratorValue(t testing.TB, it ResponseIterator, - code uint32, resp *Response) { + isPush bool, resp Response) { t.Helper() if it.Err() != nil { @@ -19,13 +20,15 @@ func assertResponseIteratorValue(t testing.TB, it ResponseIterator, if it.Value() == nil { t.Errorf("An unexpected nil value") - } else if it.Value().Code != code { - t.Errorf("An unexpected response code %d, expected %d", it.Value().Code, code) + } else if it.IsPush() != isPush { + if isPush { + t.Errorf("An unexpected response type, expected to be push") + } else { + t.Errorf("An unexpected response type, expected not to be push") + } } - if it.Value() != resp { - t.Errorf("An unexpected response %v, expected %v", it.Value(), resp) - } + assert.Equalf(t, it.Value(), resp, "An unexpected response %v, expected %v", it.Value(), resp) } func assertResponseIteratorFinished(t testing.TB, it ResponseIterator) { @@ -51,12 +54,12 @@ func TestFutureGetIteratorNoItems(t *testing.T) { } func TestFutureGetIteratorNoResponse(t *testing.T) { - push := &Response{} + push := &ConnResponse{} fut := NewFuture() fut.AppendPush(push) if it := fut.GetIterator(); it.Next() { - assertResponseIteratorValue(t, it, PushCode, push) + assertResponseIteratorValue(t, it, true, push) if it.Next() == true { t.Errorf("An unexpected next value.") } @@ -67,12 +70,12 @@ func TestFutureGetIteratorNoResponse(t *testing.T) { } func TestFutureGetIteratorNoResponseTimeout(t *testing.T) { - push := &Response{} + push := &ConnResponse{} fut := NewFuture() fut.AppendPush(push) if it := fut.GetIterator().WithTimeout(1 * time.Nanosecond); it.Next() { - assertResponseIteratorValue(t, it, PushCode, push) + assertResponseIteratorValue(t, it, true, push) if it.Next() == true { t.Errorf("An unexpected next value.") } @@ -83,8 +86,8 @@ func TestFutureGetIteratorNoResponseTimeout(t *testing.T) { } func TestFutureGetIteratorResponseOnTimeout(t *testing.T) { - push := &Response{} - resp := &Response{} + push := &ConnResponse{} + resp := &ConnResponse{} fut := NewFuture() fut.AppendPush(push) @@ -99,13 +102,14 @@ func TestFutureGetIteratorResponseOnTimeout(t *testing.T) { var it ResponseIterator var cnt = 0 for it = fut.GetIterator().WithTimeout(5 * time.Second); it.Next(); { - code := PushCode - r := push + var r Response + isPush := true + r = push if cnt == 1 { - code = OkCode + isPush = false r = resp } - assertResponseIteratorValue(t, it, code, r) + assertResponseIteratorValue(t, it, isPush, r) cnt += 1 if cnt == 1 { wait.Done() @@ -124,14 +128,14 @@ func TestFutureGetIteratorResponseOnTimeout(t *testing.T) { } func TestFutureGetIteratorFirstResponse(t *testing.T) { - resp1 := &Response{} - resp2 := &Response{} + resp1 := &ConnResponse{} + resp2 := &ConnResponse{} fut := NewFuture() fut.SetResponse(resp1) fut.SetResponse(resp2) if it := fut.GetIterator(); it.Next() { - assertResponseIteratorValue(t, it, OkCode, resp1) + assertResponseIteratorValue(t, it, false, resp1) if it.Next() == true { t.Errorf("An unexpected next value.") } @@ -160,10 +164,10 @@ func TestFutureGetIteratorFirstError(t *testing.T) { } func TestFutureGetIteratorResponse(t *testing.T) { - responses := []*Response{ + responses := []*ConnResponse{ + {}, {}, {}, - {Code: OkCode}, } fut := NewFuture() for i, resp := range responses { @@ -181,11 +185,11 @@ func TestFutureGetIteratorResponse(t *testing.T) { for _, it := range its { var cnt = 0 for it.Next() { - code := PushCode + isPush := true if cnt == len(responses)-1 { - code = OkCode + isPush = false } - assertResponseIteratorValue(t, it, code, responses[cnt]) + assertResponseIteratorValue(t, it, isPush, responses[cnt]) cnt += 1 } assertResponseIteratorFinished(t, it) @@ -198,7 +202,7 @@ func TestFutureGetIteratorResponse(t *testing.T) { func TestFutureGetIteratorError(t *testing.T) { const errMsg = "error message" - responses := []*Response{ + responses := []*ConnResponse{ {}, {}, } @@ -216,8 +220,7 @@ func TestFutureGetIteratorError(t *testing.T) { for _, it := range its { var cnt = 0 for it.Next() { - code := PushCode - assertResponseIteratorValue(t, it, code, responses[cnt]) + assertResponseIteratorValue(t, it, true, responses[cnt]) cnt += 1 } if err = it.Err(); err != nil { @@ -236,14 +239,14 @@ func TestFutureGetIteratorError(t *testing.T) { func TestFutureSetStateRaceCondition(t *testing.T) { err := errors.New("any error") - resp := &Response{} + resp := &ConnResponse{} for i := 0; i < 1000; i++ { fut := NewFuture() for j := 0; j < 9; j++ { go func(opt int) { if opt%3 == 0 { - respAppend := &Response{} + respAppend := &ConnResponse{} fut.AppendPush(respAppend) } else if opt%3 == 1 { fut.SetError(err) diff --git a/header.go b/header.go new file mode 100644 index 000000000..d9069c23a --- /dev/null +++ b/header.go @@ -0,0 +1,10 @@ +package tarantool + +// Header is a response header. +type Header struct { + // RequestId is an id of a corresponding request. + RequestId uint32 + // Code is a response code. It could be used to check that response + // has or hasn't an error. + Code uint32 +} diff --git a/pool/connection_pool.go b/pool/connection_pool.go index c272310bd..747de45f4 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -373,7 +373,7 @@ func (p *ConnectionPool) GetInfo() map[string]ConnectionInfo { // // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. -func (p *ConnectionPool) Ping(userMode Mode) (*tarantool.Response, error) { +func (p *ConnectionPool) Ping(userMode Mode) (tarantool.Response, error) { conn, err := p.getNextConnection(userMode) if err != nil { return nil, err @@ -388,7 +388,7 @@ func (p *ConnectionPool) Ping(userMode Mode) (*tarantool.Response, error) { // use a SelectRequest object + Do() instead. func (p *ConnectionPool) Select(space, index interface{}, offset, limit uint32, - iterator tarantool.Iter, key interface{}, userMode ...Mode) (*tarantool.Response, error) { + iterator tarantool.Iter, key interface{}, userMode ...Mode) (tarantool.Response, error) { conn, err := p.getConnByMode(ANY, userMode) if err != nil { return nil, err @@ -403,7 +403,7 @@ func (p *ConnectionPool) Select(space, index interface{}, // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. func (p *ConnectionPool) Insert(space interface{}, tuple interface{}, - userMode ...Mode) (*tarantool.Response, error) { + userMode ...Mode) (tarantool.Response, error) { conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err @@ -418,7 +418,7 @@ func (p *ConnectionPool) Insert(space interface{}, tuple interface{}, // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. func (p *ConnectionPool) Replace(space interface{}, tuple interface{}, - userMode ...Mode) (*tarantool.Response, error) { + userMode ...Mode) (tarantool.Response, error) { conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err @@ -433,7 +433,7 @@ func (p *ConnectionPool) Replace(space interface{}, tuple interface{}, // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. func (p *ConnectionPool) Delete(space, index interface{}, key interface{}, - userMode ...Mode) (*tarantool.Response, error) { + userMode ...Mode) (tarantool.Response, error) { conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err @@ -448,7 +448,7 @@ func (p *ConnectionPool) Delete(space, index interface{}, key interface{}, // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. func (p *ConnectionPool) Update(space, index interface{}, key interface{}, - ops *tarantool.Operations, userMode ...Mode) (*tarantool.Response, error) { + ops *tarantool.Operations, userMode ...Mode) (tarantool.Response, error) { conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err @@ -463,7 +463,7 @@ func (p *ConnectionPool) Update(space, index interface{}, key interface{}, // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. func (p *ConnectionPool) Upsert(space interface{}, tuple interface{}, - ops *tarantool.Operations, userMode ...Mode) (*tarantool.Response, error) { + ops *tarantool.Operations, userMode ...Mode) (tarantool.Response, error) { conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err @@ -478,7 +478,7 @@ func (p *ConnectionPool) Upsert(space interface{}, tuple interface{}, // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. func (p *ConnectionPool) Call(functionName string, args interface{}, - userMode Mode) (*tarantool.Response, error) { + userMode Mode) (tarantool.Response, error) { conn, err := p.getNextConnection(userMode) if err != nil { return nil, err @@ -494,7 +494,7 @@ func (p *ConnectionPool) Call(functionName string, args interface{}, // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. func (p *ConnectionPool) Call16(functionName string, args interface{}, - userMode Mode) (*tarantool.Response, error) { + userMode Mode) (tarantool.Response, error) { conn, err := p.getNextConnection(userMode) if err != nil { return nil, err @@ -509,7 +509,7 @@ func (p *ConnectionPool) Call16(functionName string, args interface{}, // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. func (p *ConnectionPool) Call17(functionName string, args interface{}, - userMode Mode) (*tarantool.Response, error) { + userMode Mode) (tarantool.Response, error) { conn, err := p.getNextConnection(userMode) if err != nil { return nil, err @@ -523,7 +523,7 @@ func (p *ConnectionPool) Call17(functionName string, args interface{}, // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. func (p *ConnectionPool) Eval(expr string, args interface{}, - userMode Mode) (*tarantool.Response, error) { + userMode Mode) (tarantool.Response, error) { conn, err := p.getNextConnection(userMode) if err != nil { return nil, err @@ -537,7 +537,7 @@ func (p *ConnectionPool) Eval(expr string, args interface{}, // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. func (p *ConnectionPool) Execute(expr string, args interface{}, - userMode Mode) (*tarantool.Response, error) { + userMode Mode) (tarantool.Response, error) { conn, err := p.getNextConnection(userMode) if err != nil { return nil, err @@ -980,18 +980,15 @@ func (p *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarantool.Fut // func (p *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (Role, error) { - resp, err := conn.Do(tarantool.NewCallRequest("box.info")).Get() + data, err := conn.Do(tarantool.NewCallRequest("box.info")).Get() if err != nil { return UnknownRole, err } - if resp == nil { - return UnknownRole, ErrIncorrectResponse - } - if len(resp.Data) < 1 { + if len(data) < 1 { return UnknownRole, ErrIncorrectResponse } - instanceStatus, ok := resp.Data[0].(map[interface{}]interface{})["status"] + instanceStatus, ok := data[0].(map[interface{}]interface{})["status"] if !ok { return UnknownRole, ErrIncorrectResponse } @@ -999,7 +996,7 @@ func (p *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (Role, er return UnknownRole, ErrIncorrectStatus } - replicaRole, ok := resp.Data[0].(map[interface{}]interface{})["ro"] + replicaRole, ok := data[0].(map[interface{}]interface{})["ro"] if !ok { return UnknownRole, ErrIncorrectResponse } diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index 15e67b59d..50096cc4c 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -814,9 +814,8 @@ func TestCloseGraceful(t *testing.T) { require.ErrorContains(t, err, "can't find healthy instance in pool") // Check that a previous request was successful. - resp, err := fut.Get() + _, err = fut.Get() require.Nilf(t, err, "sleep request no error") - require.NotNilf(t, resp, "sleep response exists") args = test_helpers.CheckStatusesArgs{ ConnPool: connPool, @@ -1144,9 +1143,11 @@ func TestCall(t *testing.T) { resp, err := connPool.Call("box.info", []interface{}{}, pool.PreferRO) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + data, err := resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") - val := resp.Data[0].(map[interface{}]interface{})["ro"] + val := data[0].(map[interface{}]interface{})["ro"] ro, ok := val.(bool) require.Truef(t, ok, "expected `true` with mode `PreferRO`") require.Truef(t, ro, "expected `true` with mode `PreferRO`") @@ -1155,9 +1156,11 @@ func TestCall(t *testing.T) { resp, err = connPool.Call("box.info", []interface{}{}, pool.PreferRW) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") - val = resp.Data[0].(map[interface{}]interface{})["ro"] + val = data[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) require.Truef(t, ok, "expected `false` with mode `PreferRW`") require.Falsef(t, ro, "expected `false` with mode `PreferRW`") @@ -1166,9 +1169,11 @@ func TestCall(t *testing.T) { resp, err = connPool.Call("box.info", []interface{}{}, pool.RO) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") - val = resp.Data[0].(map[interface{}]interface{})["ro"] + val = data[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) require.Truef(t, ok, "expected `true` with mode `RO`") require.Truef(t, ro, "expected `true` with mode `RO`") @@ -1177,9 +1182,11 @@ func TestCall(t *testing.T) { resp, err = connPool.Call("box.info", []interface{}{}, pool.RW) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") - val = resp.Data[0].(map[interface{}]interface{})["ro"] + val = data[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) require.Truef(t, ok, "expected `false` with mode `RW`") require.Falsef(t, ro, "expected `false` with mode `RW`") @@ -1203,9 +1210,11 @@ func TestCall16(t *testing.T) { resp, err := connPool.Call16("box.info", []interface{}{}, pool.PreferRO) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + data, err := resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") - val := resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] + val := data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] ro, ok := val.(bool) require.Truef(t, ok, "expected `true` with mode `PreferRO`") require.Truef(t, ro, "expected `true` with mode `PreferRO`") @@ -1214,9 +1223,11 @@ func TestCall16(t *testing.T) { resp, err = connPool.Call16("box.info", []interface{}{}, pool.PreferRW) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") - val = resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] + val = data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) require.Truef(t, ok, "expected `false` with mode `PreferRW`") require.Falsef(t, ro, "expected `false` with mode `PreferRW`") @@ -1225,9 +1236,11 @@ func TestCall16(t *testing.T) { resp, err = connPool.Call16("box.info", []interface{}{}, pool.RO) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") - val = resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] + val = data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) require.Truef(t, ok, "expected `true` with mode `RO`") require.Truef(t, ro, "expected `true` with mode `RO`") @@ -1236,9 +1249,11 @@ func TestCall16(t *testing.T) { resp, err = connPool.Call16("box.info", []interface{}{}, pool.RW) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") - val = resp.Data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] + val = data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) require.Truef(t, ok, "expected `false` with mode `RW`") require.Falsef(t, ro, "expected `false` with mode `RW`") @@ -1262,9 +1277,11 @@ func TestCall17(t *testing.T) { resp, err := connPool.Call17("box.info", []interface{}{}, pool.PreferRO) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + data, err := resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") - val := resp.Data[0].(map[interface{}]interface{})["ro"] + val := data[0].(map[interface{}]interface{})["ro"] ro, ok := val.(bool) require.Truef(t, ok, "expected `true` with mode `PreferRO`") require.Truef(t, ro, "expected `true` with mode `PreferRO`") @@ -1273,9 +1290,11 @@ func TestCall17(t *testing.T) { resp, err = connPool.Call17("box.info", []interface{}{}, pool.PreferRW) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") - val = resp.Data[0].(map[interface{}]interface{})["ro"] + val = data[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) require.Truef(t, ok, "expected `false` with mode `PreferRW`") require.Falsef(t, ro, "expected `false` with mode `PreferRW`") @@ -1284,9 +1303,11 @@ func TestCall17(t *testing.T) { resp, err = connPool.Call17("box.info", []interface{}{}, pool.RO) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") - val = resp.Data[0].(map[interface{}]interface{})["ro"] + val = data[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) require.Truef(t, ok, "expected `true` with mode `RO`") require.Truef(t, ro, "expected `true` with mode `RO`") @@ -1295,9 +1316,11 @@ func TestCall17(t *testing.T) { resp, err = connPool.Call17("box.info", []interface{}{}, pool.RW) require.Nilf(t, err, "failed to Call") require.NotNilf(t, resp, "response is nil after Call") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Call") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") - val = resp.Data[0].(map[interface{}]interface{})["ro"] + val = data[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) require.Truef(t, ok, "expected `false` with mode `RW`") require.Falsef(t, ro, "expected `false` with mode `RW`") @@ -1321,9 +1344,11 @@ func TestEval(t *testing.T) { resp, err := connPool.Eval("return box.info().ro", []interface{}{}, pool.PreferRO) require.Nilf(t, err, "failed to Eval") require.NotNilf(t, resp, "response is nil after Eval") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Eval") + data, err := resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Eval") - val, ok := resp.Data[0].(bool) + val, ok := data[0].(bool) require.Truef(t, ok, "expected `true` with mode `PreferRO`") require.Truef(t, val, "expected `true` with mode `PreferRO`") @@ -1331,9 +1356,11 @@ func TestEval(t *testing.T) { resp, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.PreferRW) require.Nilf(t, err, "failed to Eval") require.NotNilf(t, resp, "response is nil after Eval") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Eval") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Eval") - val, ok = resp.Data[0].(bool) + val, ok = data[0].(bool) require.Truef(t, ok, "expected `false` with mode `PreferRW`") require.Falsef(t, val, "expected `false` with mode `PreferRW`") @@ -1341,9 +1368,11 @@ func TestEval(t *testing.T) { resp, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.RO) require.Nilf(t, err, "failed to Eval") require.NotNilf(t, resp, "response is nil after Eval") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Eval") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Eval") - val, ok = resp.Data[0].(bool) + val, ok = data[0].(bool) require.Truef(t, ok, "expected `true` with mode `RO`") require.Truef(t, val, "expected `true` with mode `RO`") @@ -1351,9 +1380,11 @@ func TestEval(t *testing.T) { resp, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.RW) require.Nilf(t, err, "failed to Eval") require.NotNilf(t, resp, "response is nil after Eval") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Eval") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Eval") - val, ok = resp.Data[0].(bool) + val, ok = data[0].(bool) require.Truef(t, ok, "expected `false` with mode `RW`") require.Falsef(t, val, "expected `false` with mode `RW`") } @@ -1402,8 +1433,10 @@ func TestExecute(t *testing.T) { resp, err := connPool.Execute(request, []interface{}{}, pool.ANY) require.Nilf(t, err, "failed to Execute") require.NotNilf(t, resp, "response is nil after Execute") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after Execute") - require.Equalf(t, len(resp.Data[0].([]interface{})), 2, "unexpected response") + data, err := resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Execute") + require.Equalf(t, len(data[0].([]interface{})), 2, "unexpected response") // ExecuteTyped mem := []Member{} @@ -1414,11 +1447,10 @@ func TestExecute(t *testing.T) { // ExecuteAsync fut := connPool.ExecuteAsync(request, []interface{}{}, pool.ANY) - resp, err = fut.Get() + data, err = fut.Get() require.Nilf(t, err, "failed to ExecuteAsync") - require.NotNilf(t, resp, "response is nil after ExecuteAsync") - require.GreaterOrEqualf(t, len(resp.Data), 1, "response.Data is empty after ExecuteAsync") - require.Equalf(t, len(resp.Data[0].([]interface{})), 2, "unexpected response") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after ExecuteAsync") + require.Equalf(t, len(data[0].([]interface{})), 2, "unexpected response") } func TestRoundRobinStrategy(t *testing.T) { @@ -1843,9 +1875,11 @@ func TestInsert(t *testing.T) { resp, err := connPool.Insert(spaceName, []interface{}{"rw_insert_key", "rw_insert_value"}) require.Nilf(t, err, "failed to Insert") require.NotNilf(t, resp, "response is nil after Insert") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") + data, err := resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.Equalf(t, len(data), 1, "response Body len != 1 after Insert") - tpl, ok := resp.Data[0].([]interface{}) + tpl, ok := data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Insert") require.Equalf(t, 2, len(tpl), "unexpected body of Insert") @@ -1867,12 +1901,11 @@ func TestInsert(t *testing.T) { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"rw_insert_key"}) - resp, err = conn.Do(sel).Get() + data, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -1889,9 +1922,11 @@ func TestInsert(t *testing.T) { []interface{}{"preferRW_insert_key", "preferRW_insert_value"}) require.Nilf(t, err, "failed to Insert") require.NotNilf(t, resp, "response is nil after Insert") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.Equalf(t, len(data), 1, "response Body len != 1 after Insert") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Insert") require.Equalf(t, 2, len(tpl), "unexpected body of Insert") @@ -1908,12 +1943,11 @@ func TestInsert(t *testing.T) { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"preferRW_insert_key"}) - resp, err = conn.Do(sel).Get() + data, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -1946,12 +1980,11 @@ func TestDelete(t *testing.T) { defer conn.Close() ins := tarantool.NewInsertRequest(spaceNo).Tuple([]interface{}{"delete_key", "delete_value"}) - resp, err := conn.Do(ins).Get() + data, err := conn.Do(ins).Get() require.Nilf(t, err, "failed to Insert") - require.NotNilf(t, resp, "response is nil after Insert") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") + require.Equalf(t, len(data), 1, "response Body len != 1 after Insert") - tpl, ok := resp.Data[0].([]interface{}) + tpl, ok := data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Insert") require.Equalf(t, 2, len(tpl), "unexpected body of Insert") @@ -1964,12 +1997,14 @@ func TestDelete(t *testing.T) { require.Equalf(t, "delete_value", value, "unexpected body of Insert (1)") // Mode is `RW` by default, we have only one RW instance (servers[2]) - resp, err = connPool.Delete(spaceName, indexNo, []interface{}{"delete_key"}) + resp, err := connPool.Delete(spaceName, indexNo, []interface{}{"delete_key"}) require.Nilf(t, err, "failed to Delete") require.NotNilf(t, resp, "response is nil after Delete") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Delete") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.Equalf(t, len(data), 1, "response Body len != 1 after Delete") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Delete") require.Equalf(t, 2, len(tpl), "unexpected body of Delete") @@ -1986,10 +2021,9 @@ func TestDelete(t *testing.T) { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"delete_key"}) - resp, err = conn.Do(sel).Get() + data, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 0, len(resp.Data), "response Body len != 0 after Select") + require.Equalf(t, 0, len(data), "response Body len != 0 after Select") } func TestUpsert(t *testing.T) { @@ -2023,12 +2057,11 @@ func TestUpsert(t *testing.T) { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"upsert_key"}) - resp, err = conn.Do(sel).Get() + data, err := conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok := resp.Data[0].([]interface{}) + tpl, ok := data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2048,12 +2081,11 @@ func TestUpsert(t *testing.T) { require.Nilf(t, err, "failed to Upsert") require.NotNilf(t, resp, "response is nil after Upsert") - resp, err = conn.Do(sel).Get() + data, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2087,12 +2119,11 @@ func TestUpdate(t *testing.T) { ins := tarantool.NewInsertRequest(spaceNo). Tuple([]interface{}{"update_key", "update_value"}) - resp, err := conn.Do(ins).Get() + data, err := conn.Do(ins).Get() require.Nilf(t, err, "failed to Insert") - require.NotNilf(t, resp, "response is nil after Insert") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") + require.Equalf(t, len(data), 1, "response Body len != 1 after Insert") - tpl, ok := resp.Data[0].([]interface{}) + tpl, ok := data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Insert") require.Equalf(t, 2, len(tpl), "unexpected body of Insert") @@ -2105,7 +2136,7 @@ func TestUpdate(t *testing.T) { require.Equalf(t, "update_value", value, "unexpected body of Insert (1)") // Mode is `RW` by default, we have only one RW instance (servers[2]) - resp, err = connPool.Update(spaceName, indexNo, + resp, err := connPool.Update(spaceName, indexNo, []interface{}{"update_key"}, tarantool.NewOperations().Assign(1, "new_value")) require.Nilf(t, err, "failed to Update") require.NotNilf(t, resp, "response is nil after Update") @@ -2115,12 +2146,11 @@ func TestUpdate(t *testing.T) { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"update_key"}) - resp, err = conn.Do(sel).Get() + data, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2140,12 +2170,11 @@ func TestUpdate(t *testing.T) { require.Nilf(t, err, "failed to Update") require.NotNilf(t, resp, "response is nil after Update") - resp, err = conn.Do(sel).Get() + data, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2179,12 +2208,11 @@ func TestReplace(t *testing.T) { ins := tarantool.NewInsertRequest(spaceNo). Tuple([]interface{}{"replace_key", "replace_value"}) - resp, err := conn.Do(ins).Get() + data, err := conn.Do(ins).Get() require.Nilf(t, err, "failed to Insert") - require.NotNilf(t, resp, "response is nil after Insert") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Insert") + require.Equalf(t, len(data), 1, "response Body len != 1 after Insert") - tpl, ok := resp.Data[0].([]interface{}) + tpl, ok := data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Insert") require.Equalf(t, 2, len(tpl), "unexpected body of Insert") @@ -2197,7 +2225,7 @@ func TestReplace(t *testing.T) { require.Equalf(t, "replace_value", value, "unexpected body of Insert (1)") // Mode is `RW` by default, we have only one RW instance (servers[2]) - resp, err = connPool.Replace(spaceNo, []interface{}{"new_key", "new_value"}) + resp, err := connPool.Replace(spaceNo, []interface{}{"new_key", "new_value"}) require.Nilf(t, err, "failed to Replace") require.NotNilf(t, resp, "response is nil after Replace") @@ -2206,12 +2234,11 @@ func TestReplace(t *testing.T) { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"new_key"}) - resp, err = conn.Do(sel).Get() + data, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2228,12 +2255,11 @@ func TestReplace(t *testing.T) { require.Nilf(t, err, "failed to Replace") require.NotNilf(t, resp, "response is nil after Replace") - resp, err = conn.Do(sel).Get() + data, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2285,9 +2311,11 @@ func TestSelect(t *testing.T) { resp, err := connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, anyKey) require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + data, err := resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok := resp.Data[0].([]interface{}) + tpl, ok := data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2303,9 +2331,11 @@ func TestSelect(t *testing.T) { resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, pool.PreferRO) require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2317,9 +2347,11 @@ func TestSelect(t *testing.T) { resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, pool.PreferRW) require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2335,9 +2367,11 @@ func TestSelect(t *testing.T) { resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, pool.RO) require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2353,9 +2387,11 @@ func TestSelect(t *testing.T) { resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, pool.RW) require.Nilf(t, err, "failed to Select") require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + data, err = resp.Decode() + require.Nilf(t, err, "failed to Decode") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2424,29 +2460,24 @@ func TestDo(t *testing.T) { req := tarantool.NewPingRequest() // ANY - resp, err := connPool.Do(req, pool.ANY).Get() + _, err = connPool.Do(req, pool.ANY).Get() require.Nilf(t, err, "failed to Ping") - require.NotNilf(t, resp, "response is nil after Ping") // RW - resp, err = connPool.Do(req, pool.RW).Get() + _, err = connPool.Do(req, pool.RW).Get() require.Nilf(t, err, "failed to Ping") - require.NotNilf(t, resp, "response is nil after Ping") // RO - resp, err = connPool.Do(req, pool.RO).Get() + _, err = connPool.Do(req, pool.RO).Get() require.Nilf(t, err, "failed to Ping") - require.NotNilf(t, resp, "response is nil after Ping") // PreferRW - resp, err = connPool.Do(req, pool.PreferRW).Get() + _, err = connPool.Do(req, pool.PreferRW).Get() require.Nilf(t, err, "failed to Ping") - require.NotNilf(t, resp, "response is nil after Ping") // PreferRO - resp, err = connPool.Do(req, pool.PreferRO).Get() + _, err = connPool.Do(req, pool.PreferRO).Get() require.Nilf(t, err, "failed to Ping") - require.NotNilf(t, resp, "response is nil after Ping") } func TestDo_concurrent(t *testing.T) { @@ -2503,34 +2534,33 @@ func TestNewPrepared(t *testing.T) { executeReq := tarantool.NewExecutePreparedRequest(stmt) unprepareReq := tarantool.NewUnprepareRequest(stmt) - resp, err := connPool.Do(executeReq.Args([]interface{}{1, "test"}), pool.ANY).Get() + resp, err := connPool.Do(executeReq.Args([]interface{}{1, "test"}), pool.ANY).GetResponse() if err != nil { t.Fatalf("failed to execute prepared: %v", err) } if resp == nil { t.Fatalf("nil response") } - if resp.Code != tarantool.OkCode { - t.Fatalf("failed to execute prepared: code %d", resp.Code) + data, err := resp.Decode() + if err != nil { + t.Fatalf("failed to Decode: %s", err.Error()) } - if reflect.DeepEqual(resp.Data[0], []interface{}{1, "test"}) { + if reflect.DeepEqual(data[0], []interface{}{1, "test"}) { t.Error("Select with named arguments failed") } - if resp.MetaData[0].FieldType != "unsigned" || - resp.MetaData[0].FieldName != "NAME0" || - resp.MetaData[1].FieldType != "string" || - resp.MetaData[1].FieldName != "NAME1" { + metaData := resp.MetaData() + if metaData[0].FieldType != "unsigned" || + metaData[0].FieldName != "NAME0" || + metaData[1].FieldType != "string" || + metaData[1].FieldName != "NAME1" { t.Error("Wrong metadata") } // the second argument for unprepare request is unused - it already belongs to some connection - resp, err = connPool.Do(unprepareReq, pool.ANY).Get() + _, err = connPool.Do(unprepareReq, pool.ANY).Get() if err != nil { t.Errorf("failed to unprepare prepared statement: %v", err) } - if resp.Code != tarantool.OkCode { - t.Errorf("failed to unprepare prepared statement: code %d", resp.Code) - } _, err = connPool.Do(unprepareReq, pool.ANY).Get() if err == nil { @@ -2575,7 +2605,6 @@ func TestDoWithStrangerConn(t *testing.T) { func TestStream_Commit(t *testing.T) { var req tarantool.Request - var resp *tarantool.Response var err error test_helpers.SkipIfStreamsUnsupported(t) @@ -2598,18 +2627,14 @@ func TestStream_Commit(t *testing.T) { // Begin transaction req = tarantool.NewBeginRequest() - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() require.Nilf(t, err, "failed to Begin") - require.NotNilf(t, resp, "response is nil after Begin") - require.Equalf(t, tarantool.OkCode, resp.Code, "failed to Begin: wrong code returned") // Insert in stream req = tarantool.NewInsertRequest(spaceName). Tuple([]interface{}{"commit_key", "commit_value"}) - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() require.Nilf(t, err, "failed to Insert") - require.NotNilf(t, resp, "response is nil after Insert") - require.Equalf(t, tarantool.OkCode, resp.Code, "failed to Insert: wrong code returned") // Connect to servers[2] to check if tuple // was inserted outside of stream on RW instance @@ -2625,18 +2650,16 @@ func TestStream_Commit(t *testing.T) { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"commit_key"}) - resp, err = conn.Do(selectReq).Get() + data, err := conn.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + require.Equalf(t, 0, len(data), "response Data len != 0") // Select in stream - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 1, len(resp.Data), "response Body len != 1 after Select") + require.Equalf(t, 1, len(data), "response Body len != 1 after Select") - tpl, ok := resp.Data[0].([]interface{}) + tpl, ok := data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2650,18 +2673,15 @@ func TestStream_Commit(t *testing.T) { // Commit transaction req = tarantool.NewCommitRequest() - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() require.Nilf(t, err, "failed to Commit") - require.NotNilf(t, resp, "response is nil after Commit") - require.Equalf(t, tarantool.OkCode, resp.Code, "failed to Commit: wrong code returned") // Select outside of transaction - resp, err = conn.Do(selectReq).Get() + data, err = conn.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, len(resp.Data), 1, "response Body len != 1 after Select") + require.Equalf(t, len(data), 1, "response Body len != 1 after Select") - tpl, ok = resp.Data[0].([]interface{}) + tpl, ok = data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2676,7 +2696,6 @@ func TestStream_Commit(t *testing.T) { func TestStream_Rollback(t *testing.T) { var req tarantool.Request - var resp *tarantool.Response var err error test_helpers.SkipIfStreamsUnsupported(t) @@ -2699,18 +2718,14 @@ func TestStream_Rollback(t *testing.T) { // Begin transaction req = tarantool.NewBeginRequest() - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() require.Nilf(t, err, "failed to Begin") - require.NotNilf(t, resp, "response is nil after Begin") - require.Equalf(t, tarantool.OkCode, resp.Code, "failed to Begin: wrong code returned") // Insert in stream req = tarantool.NewInsertRequest(spaceName). Tuple([]interface{}{"rollback_key", "rollback_value"}) - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() require.Nilf(t, err, "failed to Insert") - require.NotNilf(t, resp, "response is nil after Insert") - require.Equalf(t, tarantool.OkCode, resp.Code, "failed to Insert: wrong code returned") // Connect to servers[2] to check if tuple // was not inserted outside of stream on RW instance @@ -2725,18 +2740,16 @@ func TestStream_Rollback(t *testing.T) { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"rollback_key"}) - resp, err = conn.Do(selectReq).Get() + data, err := conn.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + require.Equalf(t, 0, len(data), "response Data len != 0") // Select in stream - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 1, len(resp.Data), "response Body len != 1 after Select") + require.Equalf(t, 1, len(data), "response Body len != 1 after Select") - tpl, ok := resp.Data[0].([]interface{}) + tpl, ok := data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2750,27 +2763,22 @@ func TestStream_Rollback(t *testing.T) { // Rollback transaction req = tarantool.NewRollbackRequest() - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() require.Nilf(t, err, "failed to Rollback") - require.NotNilf(t, resp, "response is nil after Rollback") - require.Equalf(t, tarantool.OkCode, resp.Code, "failed to Rollback: wrong code returned") // Select outside of transaction - resp, err = conn.Do(selectReq).Get() + data, err = conn.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 0, len(resp.Data), "response Body len != 0 after Select") + require.Equalf(t, 0, len(data), "response Body len != 0 after Select") // Select inside of stream after rollback - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 0, len(resp.Data), "response Body len != 0 after Select") + require.Equalf(t, 0, len(data), "response Body len != 0 after Select") } func TestStream_TxnIsolationLevel(t *testing.T) { var req tarantool.Request - var resp *tarantool.Response var err error txnIsolationLevels := []tarantool.TxnIsolationLevel{ @@ -2806,18 +2814,14 @@ func TestStream_TxnIsolationLevel(t *testing.T) { for _, level := range txnIsolationLevels { // Begin transaction req = tarantool.NewBeginRequest().TxnIsolation(level).Timeout(500 * time.Millisecond) - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() require.Nilf(t, err, "failed to Begin") - require.NotNilf(t, resp, "response is nil after Begin") - require.Equalf(t, tarantool.OkCode, resp.Code, "wrong code returned") // Insert in stream req = tarantool.NewInsertRequest(spaceName). Tuple([]interface{}{"level_key", "level_value"}) - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() require.Nilf(t, err, "failed to Insert") - require.NotNilf(t, resp, "response is nil after Insert") - require.Equalf(t, tarantool.OkCode, resp.Code, "wrong code returned") // Select not related to the transaction // while transaction is not committed @@ -2827,18 +2831,16 @@ func TestStream_TxnIsolationLevel(t *testing.T) { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"level_key"}) - resp, err = conn.Do(selectReq).Get() + data, err := conn.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + require.Equalf(t, 0, len(data), "response Data len != 0") // Select in stream - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 1, len(resp.Data), "response Body len != 1 after Select") + require.Equalf(t, 1, len(data), "response Body len != 1 after Select") - tpl, ok := resp.Data[0].([]interface{}) + tpl, ok := data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 2, len(tpl), "unexpected body of Select") @@ -2852,22 +2854,18 @@ func TestStream_TxnIsolationLevel(t *testing.T) { // Rollback transaction req = tarantool.NewRollbackRequest() - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() require.Nilf(t, err, "failed to Rollback") - require.NotNilf(t, resp, "response is nil after Rollback") - require.Equalf(t, tarantool.OkCode, resp.Code, "wrong code returned") // Select outside of transaction - resp, err = conn.Do(selectReq).Get() + data, err = conn.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + require.Equalf(t, 0, len(data), "response Data len != 0") // Select inside of stream after rollback - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + require.Equalf(t, 0, len(data), "response Data len != 0") test_helpers.DeleteRecordByKey(t, conn, spaceNo, indexNo, []interface{}{"level_key"}) } diff --git a/pool/connector.go b/pool/connector.go index 604e3921f..74a60bd74 100644 --- a/pool/connector.go +++ b/pool/connector.go @@ -60,7 +60,7 @@ func (c *ConnectorAdapter) ConfiguredTimeout() time.Duration { // // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. -func (c *ConnectorAdapter) Ping() (*tarantool.Response, error) { +func (c *ConnectorAdapter) Ping() (tarantool.Response, error) { return c.pool.Ping(c.mode) } @@ -70,7 +70,7 @@ func (c *ConnectorAdapter) Ping() (*tarantool.Response, error) { // use a SelectRequest object + Do() instead. func (c *ConnectorAdapter) Select(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, - key interface{}) (*tarantool.Response, error) { + key interface{}) (tarantool.Response, error) { return c.pool.Select(space, index, offset, limit, iterator, key, c.mode) } @@ -79,7 +79,7 @@ func (c *ConnectorAdapter) Select(space, index interface{}, // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. func (c *ConnectorAdapter) Insert(space interface{}, - tuple interface{}) (*tarantool.Response, error) { + tuple interface{}) (tarantool.Response, error) { return c.pool.Insert(space, tuple, c.mode) } @@ -88,7 +88,7 @@ func (c *ConnectorAdapter) Insert(space interface{}, // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. func (c *ConnectorAdapter) Replace(space interface{}, - tuple interface{}) (*tarantool.Response, error) { + tuple interface{}) (tarantool.Response, error) { return c.pool.Replace(space, tuple, c.mode) } @@ -97,7 +97,7 @@ func (c *ConnectorAdapter) Replace(space interface{}, // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. func (c *ConnectorAdapter) Delete(space, index interface{}, - key interface{}) (*tarantool.Response, error) { + key interface{}) (tarantool.Response, error) { return c.pool.Delete(space, index, key, c.mode) } @@ -106,7 +106,7 @@ func (c *ConnectorAdapter) Delete(space, index interface{}, // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. func (c *ConnectorAdapter) Update(space, index interface{}, - key interface{}, ops *tarantool.Operations) (*tarantool.Response, error) { + key interface{}, ops *tarantool.Operations) (tarantool.Response, error) { return c.pool.Update(space, index, key, ops, c.mode) } @@ -115,7 +115,7 @@ func (c *ConnectorAdapter) Update(space, index interface{}, // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. func (c *ConnectorAdapter) Upsert(space, tuple interface{}, - ops *tarantool.Operations) (*tarantool.Response, error) { + ops *tarantool.Operations) (tarantool.Response, error) { return c.pool.Upsert(space, tuple, ops, c.mode) } @@ -125,7 +125,7 @@ func (c *ConnectorAdapter) Upsert(space, tuple interface{}, // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. func (c *ConnectorAdapter) Call(functionName string, - args interface{}) (*tarantool.Response, error) { + args interface{}) (tarantool.Response, error) { return c.pool.Call(functionName, args, c.mode) } @@ -136,7 +136,7 @@ func (c *ConnectorAdapter) Call(functionName string, // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. func (c *ConnectorAdapter) Call16(functionName string, - args interface{}) (*tarantool.Response, error) { + args interface{}) (tarantool.Response, error) { return c.pool.Call16(functionName, args, c.mode) } @@ -146,7 +146,7 @@ func (c *ConnectorAdapter) Call16(functionName string, // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. func (c *ConnectorAdapter) Call17(functionName string, - args interface{}) (*tarantool.Response, error) { + args interface{}) (tarantool.Response, error) { return c.pool.Call17(functionName, args, c.mode) } @@ -155,7 +155,7 @@ func (c *ConnectorAdapter) Call17(functionName string, // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. func (c *ConnectorAdapter) Eval(expr string, - args interface{}) (*tarantool.Response, error) { + args interface{}) (tarantool.Response, error) { return c.pool.Eval(expr, args, c.mode) } @@ -164,7 +164,7 @@ func (c *ConnectorAdapter) Eval(expr string, // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. func (c *ConnectorAdapter) Execute(expr string, - args interface{}) (*tarantool.Response, error) { + args interface{}) (tarantool.Response, error) { return c.pool.Execute(expr, args, c.mode) } diff --git a/pool/connector_test.go b/pool/connector_test.go index fa107cc58..190d2f9cc 100644 --- a/pool/connector_test.go +++ b/pool/connector_test.go @@ -135,7 +135,7 @@ type baseRequestMock struct { mode Mode } -var reqResp *tarantool.Response = &tarantool.Response{} +var reqResp tarantool.Response = &tarantool.ConnResponse{} var errReq error = errors.New("response error") var reqFuture *tarantool.Future = &tarantool.Future{} @@ -190,7 +190,7 @@ type selectMock struct { func (m *selectMock) Select(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}, - mode ...Mode) (*tarantool.Response, error) { + mode ...Mode) (tarantool.Response, error) { m.called++ m.space = space m.index = index @@ -299,7 +299,7 @@ type insertMock struct { } func (m *insertMock) Insert(space, tuple interface{}, - mode ...Mode) (*tarantool.Response, error) { + mode ...Mode) (tarantool.Response, error) { m.called++ m.space = space m.tuple = tuple @@ -380,7 +380,7 @@ type replaceMock struct { } func (m *replaceMock) Replace(space, tuple interface{}, - mode ...Mode) (*tarantool.Response, error) { + mode ...Mode) (tarantool.Response, error) { m.called++ m.space = space m.tuple = tuple @@ -461,7 +461,7 @@ type deleteMock struct { } func (m *deleteMock) Delete(space, index, key interface{}, - mode ...Mode) (*tarantool.Response, error) { + mode ...Mode) (tarantool.Response, error) { m.called++ m.space = space m.index = index @@ -548,7 +548,7 @@ type updateMock struct { } func (m *updateMock) Update(space, index, key interface{}, - ops *tarantool.Operations, mode ...Mode) (*tarantool.Response, error) { + ops *tarantool.Operations, mode ...Mode) (tarantool.Response, error) { m.called++ m.space = space m.index = index @@ -641,7 +641,7 @@ type upsertMock struct { } func (m *upsertMock) Upsert(space, tuple interface{}, ops *tarantool.Operations, - mode ...Mode) (*tarantool.Response, error) { + mode ...Mode) (tarantool.Response, error) { m.called++ m.space = space m.tuple = tuple @@ -698,7 +698,7 @@ type baseCallMock struct { } func (m *baseCallMock) call(functionName string, args interface{}, - mode Mode) (*tarantool.Response, error) { + mode Mode) (tarantool.Response, error) { m.called++ m.functionName = functionName m.args = args @@ -730,7 +730,7 @@ type callMock struct { } func (m *callMock) Call(functionName string, args interface{}, - mode Mode) (*tarantool.Response, error) { + mode Mode) (tarantool.Response, error) { return m.call(functionName, args, mode) } @@ -801,7 +801,7 @@ type call16Mock struct { } func (m *call16Mock) Call16(functionName string, args interface{}, - mode Mode) (*tarantool.Response, error) { + mode Mode) (tarantool.Response, error) { return m.call(functionName, args, mode) } @@ -872,7 +872,7 @@ type call17Mock struct { } func (m *call17Mock) Call17(functionName string, args interface{}, - mode Mode) (*tarantool.Response, error) { + mode Mode) (tarantool.Response, error) { return m.call(functionName, args, mode) } @@ -943,7 +943,7 @@ type evalMock struct { } func (m *evalMock) Eval(functionName string, args interface{}, - mode Mode) (*tarantool.Response, error) { + mode Mode) (tarantool.Response, error) { return m.call(functionName, args, mode) } @@ -1014,7 +1014,7 @@ type executeMock struct { } func (m *executeMock) Execute(functionName string, args interface{}, - mode Mode) (*tarantool.Response, error) { + mode Mode) (tarantool.Response, error) { return m.call(functionName, args, mode) } diff --git a/pool/example_test.go b/pool/example_test.go index c4a919a93..7eb480177 100644 --- a/pool/example_test.go +++ b/pool/example_test.go @@ -157,7 +157,6 @@ func getTestTxnProtocol() tarantool.ProtocolInfo { func ExampleCommitRequest() { var req tarantool.Request - var resp *tarantool.Response var err error // Tarantool supports streams and interactive transactions since version 2.10.0 @@ -182,22 +181,22 @@ func ExampleCommitRequest() { // Begin transaction req = tarantool.NewBeginRequest() - resp, err = stream.Do(req).Get() + data, err := stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Begin: %s", err.Error()) return } - fmt.Printf("Begin transaction: response is %#v\n", resp.Code) + fmt.Printf("Begin transaction: response is %#v\n", data) // Insert in stream req = tarantool.NewInsertRequest(spaceName). Tuple([]interface{}{"example_commit_key", "example_commit_value"}) - resp, err = stream.Do(req).Get() + data, err = stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Insert: %s", err.Error()) return } - fmt.Printf("Insert in stream: response is %#v\n", resp.Code) + fmt.Printf("Insert in stream: response is %#v\n", data) // Select not related to the transaction // while transaction is not committed @@ -207,43 +206,42 @@ func ExampleCommitRequest() { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"example_commit_key"}) - resp, err = connPool.Do(selectReq, pool.RW).Get() + data, err = connPool.Do(selectReq, pool.RW).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select out of stream before commit: response is %#v\n", resp.Data) + fmt.Printf("Select out of stream before commit: response is %#v\n", data) // Select in stream - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select in stream: response is %#v\n", resp.Data) + fmt.Printf("Select in stream: response is %#v\n", data) // Commit transaction req = tarantool.NewCommitRequest() - resp, err = stream.Do(req).Get() + data, err = stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Commit: %s", err.Error()) return } - fmt.Printf("Commit transaction: response is %#v\n", resp.Code) + fmt.Printf("Commit transaction: response is %#v\n", data) // Select outside of transaction // example pool has only one rw instance - resp, err = connPool.Do(selectReq, pool.RW).Get() + data, err = connPool.Do(selectReq, pool.RW).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select after commit: response is %#v\n", resp.Data) + fmt.Printf("Select after commit: response is %#v\n", data) } func ExampleRollbackRequest() { var req tarantool.Request - var resp *tarantool.Response var err error // Tarantool supports streams and interactive transactions since version 2.10.0 @@ -268,22 +266,22 @@ func ExampleRollbackRequest() { // Begin transaction req = tarantool.NewBeginRequest() - resp, err = stream.Do(req).Get() + data, err := stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Begin: %s", err.Error()) return } - fmt.Printf("Begin transaction: response is %#v\n", resp.Code) + fmt.Printf("Begin transaction: response is %#v\n", data) // Insert in stream req = tarantool.NewInsertRequest(spaceName). Tuple([]interface{}{"example_rollback_key", "example_rollback_value"}) - resp, err = stream.Do(req).Get() + data, err = stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Insert: %s", err.Error()) return } - fmt.Printf("Insert in stream: response is %#v\n", resp.Code) + fmt.Printf("Insert in stream: response is %#v\n", data) // Select not related to the transaction // while transaction is not committed @@ -293,43 +291,42 @@ func ExampleRollbackRequest() { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"example_rollback_key"}) - resp, err = connPool.Do(selectReq, pool.RW).Get() + data, err = connPool.Do(selectReq, pool.RW).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select out of stream: response is %#v\n", resp.Data) + fmt.Printf("Select out of stream: response is %#v\n", data) // Select in stream - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select in stream: response is %#v\n", resp.Data) + fmt.Printf("Select in stream: response is %#v\n", data) // Rollback transaction req = tarantool.NewRollbackRequest() - resp, err = stream.Do(req).Get() + data, err = stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Rollback: %s", err.Error()) return } - fmt.Printf("Rollback transaction: response is %#v\n", resp.Code) + fmt.Printf("Rollback transaction: response is %#v\n", data) // Select outside of transaction // example pool has only one rw instance - resp, err = connPool.Do(selectReq, pool.RW).Get() + data, err = connPool.Do(selectReq, pool.RW).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select after Rollback: response is %#v\n", resp.Data) + fmt.Printf("Select after Rollback: response is %#v\n", data) } func ExampleBeginRequest_TxnIsolation() { var req tarantool.Request - var resp *tarantool.Response var err error // Tarantool supports streams and interactive transactions since version 2.10.0 @@ -356,22 +353,22 @@ func ExampleBeginRequest_TxnIsolation() { req = tarantool.NewBeginRequest(). TxnIsolation(tarantool.ReadConfirmedLevel). Timeout(500 * time.Millisecond) - resp, err = stream.Do(req).Get() + data, err := stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Begin: %s", err.Error()) return } - fmt.Printf("Begin transaction: response is %#v\n", resp.Code) + fmt.Printf("Begin transaction: response is %#v\n", data) // Insert in stream req = tarantool.NewInsertRequest(spaceName). Tuple([]interface{}{"isolation_level_key", "isolation_level_value"}) - resp, err = stream.Do(req).Get() + data, err = stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Insert: %s", err.Error()) return } - fmt.Printf("Insert in stream: response is %#v\n", resp.Code) + fmt.Printf("Insert in stream: response is %#v\n", data) // Select not related to the transaction // while transaction is not committed @@ -381,38 +378,38 @@ func ExampleBeginRequest_TxnIsolation() { Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"isolation_level_key"}) - resp, err = connPool.Do(selectReq, pool.RW).Get() + data, err = connPool.Do(selectReq, pool.RW).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select out of stream: response is %#v\n", resp.Data) + fmt.Printf("Select out of stream: response is %#v\n", data) // Select in stream - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select in stream: response is %#v\n", resp.Data) + fmt.Printf("Select in stream: response is %#v\n", data) // Rollback transaction req = tarantool.NewRollbackRequest() - resp, err = stream.Do(req).Get() + data, err = stream.Do(req).Get() if err != nil { fmt.Printf("Failed to Rollback: %s", err.Error()) return } - fmt.Printf("Rollback transaction: response is %#v\n", resp.Code) + fmt.Printf("Rollback transaction: response is %#v\n", data) // Select outside of transaction // example pool has only one rw instance - resp, err = connPool.Do(selectReq, pool.RW).Get() + data, err = connPool.Do(selectReq, pool.RW).Get() if err != nil { fmt.Printf("Failed to Select: %s", err.Error()) return } - fmt.Printf("Select after Rollback: response is %#v\n", resp.Data) + fmt.Printf("Select after Rollback: response is %#v\n", data) } func ExampleConnectorAdapter() { @@ -426,12 +423,10 @@ func ExampleConnectorAdapter() { var connector tarantool.Connector = adapter // Ping an RW instance to check connection. - resp, err := connector.Do(tarantool.NewPingRequest()).Get() - fmt.Println("Ping Code", resp.Code) - fmt.Println("Ping Data", resp.Data) + data, err := connector.Do(tarantool.NewPingRequest()).Get() + fmt.Println("Ping Data", data) fmt.Println("Ping Error", err) // Output: - // Ping Code 0 // Ping Data [] // Ping Error } @@ -473,5 +468,5 @@ func ExampleConnectionPool_CloseGraceful_force() { // Force ConnectionPool.Close()! // ConnectionPool.CloseGraceful() done! // Result: - // connection closed by client (0x4001) + // [] connection closed by client (0x4001) } diff --git a/pool/pooler.go b/pool/pooler.go index 0ff945bbb..6256c2d24 100644 --- a/pool/pooler.go +++ b/pool/pooler.go @@ -19,51 +19,51 @@ type Pooler interface { // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. - Ping(mode Mode) (*tarantool.Response, error) + Ping(mode Mode) (tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. Select(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, - key interface{}, mode ...Mode) (*tarantool.Response, error) + key interface{}, mode ...Mode) (tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. Insert(space interface{}, tuple interface{}, - mode ...Mode) (*tarantool.Response, error) + mode ...Mode) (tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. Replace(space interface{}, tuple interface{}, - mode ...Mode) (*tarantool.Response, error) + mode ...Mode) (tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. Delete(space, index interface{}, key interface{}, - mode ...Mode) (*tarantool.Response, error) + mode ...Mode) (tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. Update(space, index interface{}, key interface{}, ops *tarantool.Operations, - mode ...Mode) (*tarantool.Response, error) + mode ...Mode) (tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. Upsert(space interface{}, tuple interface{}, ops *tarantool.Operations, - mode ...Mode) (*tarantool.Response, error) + mode ...Mode) (tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. Call(functionName string, args interface{}, - mode Mode) (*tarantool.Response, error) + mode Mode) (tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. Call16(functionName string, args interface{}, - mode Mode) (*tarantool.Response, error) + mode Mode) (tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. Call17(functionName string, args interface{}, - mode Mode) (*tarantool.Response, error) + mode Mode) (tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. Eval(expr string, args interface{}, - mode Mode) (*tarantool.Response, error) + mode Mode) (tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. Execute(expr string, args interface{}, - mode Mode) (*tarantool.Response, error) + mode Mode) (tarantool.Response, error) // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. diff --git a/prepared.go b/prepared.go index 8f2d95519..f4fc1cdf1 100644 --- a/prepared.go +++ b/prepared.go @@ -42,17 +42,21 @@ func fillExecutePrepared(enc *msgpack.Encoder, stmt Prepared, args interface{}) } // NewPreparedFromResponse constructs a Prepared object. -func NewPreparedFromResponse(conn *Connection, resp *Response) (*Prepared, error) { +func NewPreparedFromResponse(conn *Connection, resp Response) (*Prepared, error) { if resp == nil { return nil, fmt.Errorf("passed nil response") } - if resp.Data == nil { + data, err := resp.Decode() + if err != nil { + return nil, fmt.Errorf("decode response body error: %s", err.Error()) + } + if data == nil { return nil, fmt.Errorf("response Data is nil") } - if len(resp.Data) == 0 { + if len(data) == 0 { return nil, fmt.Errorf("response Data format is wrong") } - stmt, ok := resp.Data[0].(*Prepared) + stmt, ok := data[0].(*Prepared) if !ok { return nil, fmt.Errorf("response Data format is wrong") } diff --git a/queue/queue.go b/queue/queue.go index 5c0506f3e..99b1a722f 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -213,14 +213,14 @@ func (q *queue) Cfg(opts CfgOpts) error { // Exists checks existence of a tube. func (q *queue) Exists() (bool, error) { cmd := "local name = ... ; return queue.tube[name] ~= nil" - resp, err := q.conn.Do(tarantool.NewEvalRequest(cmd). + data, err := q.conn.Do(tarantool.NewEvalRequest(cmd). Args([]string{q.name}), ).Get() if err != nil { return false, err } - exist := len(resp.Data) != 0 && resp.Data[0].(bool) + exist := len(data) != 0 && data[0].(bool) return exist, nil } @@ -244,11 +244,11 @@ func (q *queue) Identify(u *uuid.UUID) (uuid.UUID, error) { } req := tarantool.NewCallRequest(q.cmds.identify).Args(args) - if resp, err := q.conn.Do(req).Get(); err == nil { - if us, ok := resp.Data[0].(string); ok { + if data, err := q.conn.Do(req).Get(); err == nil { + if us, ok := data[0].(string); ok { return uuid.FromBytes([]byte(us)) } else { - return uuid.UUID{}, fmt.Errorf("unexpected response: %v", resp.Data) + return uuid.UUID{}, fmt.Errorf("unexpected response: %v", data) } } else { return uuid.UUID{}, err @@ -411,31 +411,31 @@ func (q *queue) Delete(taskId uint64) error { // State returns a current queue state. func (q *queue) State() (State, error) { - resp, err := q.conn.Do(tarantool.NewCallRequest(q.cmds.state)).Get() + data, err := q.conn.Do(tarantool.NewCallRequest(q.cmds.state)).Get() if err != nil { return UnknownState, err } - if respState, ok := resp.Data[0].(string); ok { + if respState, ok := data[0].(string); ok { if state, ok := strToState[respState]; ok { return state, nil } - return UnknownState, fmt.Errorf("unknown state: %v", resp.Data[0]) + return UnknownState, fmt.Errorf("unknown state: %v", data[0]) } - return UnknownState, fmt.Errorf("unexpected response: %v", resp.Data) + return UnknownState, fmt.Errorf("unexpected response: %v", data) } // Return the number of tasks in a queue broken down by task_state, and the // number of requests broken down by the type of request. func (q *queue) Statistic() (interface{}, error) { req := tarantool.NewCallRequest(q.cmds.statistics).Args([]interface{}{q.name}) - resp, err := q.conn.Do(req).Get() + data, err := q.conn.Do(req).Get() if err != nil { return nil, err } - if len(resp.Data) != 0 { - return resp.Data[0], nil + if len(data) != 0 { + return data[0], nil } return nil, nil diff --git a/request.go b/request.go index 8c4f4acd2..bdf1c50ea 100644 --- a/request.go +++ b/request.go @@ -260,8 +260,8 @@ func fillWatchOnce(enc *msgpack.Encoder, key string) error { // // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. -func (conn *Connection) Ping() (*Response, error) { - return conn.Do(NewPingRequest()).Get() +func (conn *Connection) Ping() (Response, error) { + return conn.Do(NewPingRequest()).GetResponse() } // Select performs select to box space. @@ -271,8 +271,8 @@ func (conn *Connection) Ping() (*Response, error) { // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. func (conn *Connection) Select(space, index interface{}, offset, limit uint32, iterator Iter, - key interface{}) (*Response, error) { - return conn.SelectAsync(space, index, offset, limit, iterator, key).Get() + key interface{}) (Response, error) { + return conn.SelectAsync(space, index, offset, limit, iterator, key).GetResponse() } // Insert performs insertion to box space. @@ -282,8 +282,8 @@ func (conn *Connection) Select(space, index interface{}, offset, limit uint32, i // // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. -func (conn *Connection) Insert(space interface{}, tuple interface{}) (*Response, error) { - return conn.InsertAsync(space, tuple).Get() +func (conn *Connection) Insert(space interface{}, tuple interface{}) (Response, error) { + return conn.InsertAsync(space, tuple).GetResponse() } // Replace performs "insert or replace" action to box space. @@ -293,8 +293,8 @@ func (conn *Connection) Insert(space interface{}, tuple interface{}) (*Response, // // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. -func (conn *Connection) Replace(space interface{}, tuple interface{}) (*Response, error) { - return conn.ReplaceAsync(space, tuple).Get() +func (conn *Connection) Replace(space interface{}, tuple interface{}) (Response, error) { + return conn.ReplaceAsync(space, tuple).GetResponse() } // Delete performs deletion of a tuple by key. @@ -304,8 +304,8 @@ func (conn *Connection) Replace(space interface{}, tuple interface{}) (*Response // // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. -func (conn *Connection) Delete(space, index interface{}, key interface{}) (*Response, error) { - return conn.DeleteAsync(space, index, key).Get() +func (conn *Connection) Delete(space, index interface{}, key interface{}) (Response, error) { + return conn.DeleteAsync(space, index, key).GetResponse() } // Update performs update of a tuple by key. @@ -315,8 +315,8 @@ func (conn *Connection) Delete(space, index interface{}, key interface{}) (*Resp // // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. -func (conn *Connection) Update(space, index, key interface{}, ops *Operations) (*Response, error) { - return conn.UpdateAsync(space, index, key, ops).Get() +func (conn *Connection) Update(space, index, key interface{}, ops *Operations) (Response, error) { + return conn.UpdateAsync(space, index, key, ops).GetResponse() } // Upsert performs "update or insert" action of a tuple by key. @@ -326,8 +326,8 @@ func (conn *Connection) Update(space, index, key interface{}, ops *Operations) ( // // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. -func (conn *Connection) Upsert(space, tuple interface{}, ops *Operations) (*Response, error) { - return conn.UpsertAsync(space, tuple, ops).Get() +func (conn *Connection) Upsert(space, tuple interface{}, ops *Operations) (Response, error) { + return conn.UpsertAsync(space, tuple, ops).GetResponse() } // Call calls registered Tarantool function. @@ -337,8 +337,8 @@ func (conn *Connection) Upsert(space, tuple interface{}, ops *Operations) (*Resp // // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. -func (conn *Connection) Call(functionName string, args interface{}) (*Response, error) { - return conn.CallAsync(functionName, args).Get() +func (conn *Connection) Call(functionName string, args interface{}) (Response, error) { + return conn.CallAsync(functionName, args).GetResponse() } // Call16 calls registered Tarantool function. @@ -349,8 +349,8 @@ func (conn *Connection) Call(functionName string, args interface{}) (*Response, // // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. -func (conn *Connection) Call16(functionName string, args interface{}) (*Response, error) { - return conn.Call16Async(functionName, args).Get() +func (conn *Connection) Call16(functionName string, args interface{}) (Response, error) { + return conn.Call16Async(functionName, args).GetResponse() } // Call17 calls registered Tarantool function. @@ -360,8 +360,8 @@ func (conn *Connection) Call16(functionName string, args interface{}) (*Response // // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. -func (conn *Connection) Call17(functionName string, args interface{}) (*Response, error) { - return conn.Call17Async(functionName, args).Get() +func (conn *Connection) Call17(functionName string, args interface{}) (Response, error) { + return conn.Call17Async(functionName, args).GetResponse() } // Eval passes Lua expression for evaluation. @@ -370,8 +370,8 @@ func (conn *Connection) Call17(functionName string, args interface{}) (*Response // // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. -func (conn *Connection) Eval(expr string, args interface{}) (*Response, error) { - return conn.EvalAsync(expr, args).Get() +func (conn *Connection) Eval(expr string, args interface{}) (Response, error) { + return conn.EvalAsync(expr, args).GetResponse() } // Execute passes sql expression to Tarantool for execution. @@ -381,8 +381,8 @@ func (conn *Connection) Eval(expr string, args interface{}) (*Response, error) { // // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. -func (conn *Connection) Execute(expr string, args interface{}) (*Response, error) { - return conn.ExecuteAsync(expr, args).Get() +func (conn *Connection) Execute(expr string, args interface{}) (Response, error) { + return conn.ExecuteAsync(expr, args).GetResponse() } // single used for conn.GetTyped for decode one tuple. @@ -534,7 +534,7 @@ func (conn *Connection) ExecuteTyped(expr string, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error) { fut := conn.ExecuteAsync(expr, args) err := fut.GetTyped(&result) - return fut.resp.SQLInfo, fut.resp.MetaData, err + return fut.resp.SQLInfo(), fut.resp.MetaData(), err } // SelectAsync sends select request to Tarantool and returns Future. diff --git a/response.go b/response.go index 0d6d062b8..09145ee9d 100644 --- a/response.go +++ b/response.go @@ -7,17 +7,33 @@ import ( "github.com/vmihailenco/msgpack/v5" ) -type Response struct { - RequestId uint32 - Code uint32 - // Error contains an error message. - Error string - // Data contains deserialized data for untyped requests. - Data []interface{} - // Pos contains a position descriptor of last selected tuple. - Pos []byte - MetaData []ColumnMetaData - SQLInfo SQLInfo +// Response is an interface with operations for the server responses. +type Response interface { + // Header returns a response header. + Header() Header + // Decode decodes a response. + Decode() ([]interface{}, error) + // DecodeTyped decodes a response into a given container res. + DecodeTyped(res interface{}) error + + // Pos returns a position descriptor of the last selected tuple. + Pos() []byte + // MetaData returns meta-data. + MetaData() []ColumnMetaData + // SQLInfo returns sql info. + SQLInfo() SQLInfo +} + +// ConnResponse is a Response interface implementation. +// It is used for all request types. +type ConnResponse struct { + header Header + // data contains deserialized data for untyped requests. + data []interface{} + // pos contains a position descriptor of last selected tuple. + pos []byte + metaData []ColumnMetaData + sqlInfo SQLInfo buf smallBuf } @@ -103,53 +119,58 @@ func (info *SQLInfo) DecodeMsgpack(d *msgpack.Decoder) error { return nil } -func (resp *Response) smallInt(d *msgpack.Decoder) (i int, err error) { - b, err := resp.buf.ReadByte() +func smallInt(d *msgpack.Decoder, buf *smallBuf) (i int, err error) { + b, err := buf.ReadByte() if err != nil { return } if b <= 127 { return int(b), nil } - resp.buf.UnreadByte() + buf.UnreadByte() return d.DecodeInt() } -func (resp *Response) decodeHeader(d *msgpack.Decoder) (err error) { +func decodeHeader(d *msgpack.Decoder, buf *smallBuf) (Header, error) { var l int - d.Reset(&resp.buf) + var err error + d.Reset(buf) if l, err = d.DecodeMapLen(); err != nil { - return + return Header{}, err } + decodedHeader := Header{} for ; l > 0; l-- { var cd int - if cd, err = resp.smallInt(d); err != nil { - return + if cd, err = smallInt(d, buf); err != nil { + return Header{}, err } switch iproto.Key(cd) { case iproto.IPROTO_SYNC: var rid uint64 if rid, err = d.DecodeUint64(); err != nil { - return + return Header{}, err } - resp.RequestId = uint32(rid) + decodedHeader.RequestId = uint32(rid) case iproto.IPROTO_REQUEST_TYPE: var rcode uint64 if rcode, err = d.DecodeUint64(); err != nil { - return + return Header{}, err } - resp.Code = uint32(rcode) + decodedHeader.Code = uint32(rcode) default: if err = d.Skip(); err != nil { - return + return Header{}, err } } } - return nil + return decodedHeader, nil } -func (resp *Response) decodeBody() (err error) { +func (resp *ConnResponse) Decode() ([]interface{}, error) { + var err error if resp.buf.Len() > 2 { + var decodedError string + offset := resp.buf.Offset() defer resp.buf.Seek(offset) @@ -165,67 +186,67 @@ func (resp *Response) decodeBody() (err error) { }) if l, err = d.DecodeMapLen(); err != nil { - return err + return nil, err } for ; l > 0; l-- { var cd int - if cd, err = resp.smallInt(d); err != nil { - return err + if cd, err = smallInt(d, &resp.buf); err != nil { + return nil, err } switch iproto.Key(cd) { case iproto.IPROTO_DATA: var res interface{} var ok bool if res, err = d.DecodeInterface(); err != nil { - return err + return nil, err } - if resp.Data, ok = res.([]interface{}); !ok { - return fmt.Errorf("result is not array: %v", res) + if resp.data, ok = res.([]interface{}); !ok { + return nil, fmt.Errorf("result is not array: %v", res) } case iproto.IPROTO_ERROR: if errorExtendedInfo, err = decodeBoxError(d); err != nil { - return err + return nil, err } case iproto.IPROTO_ERROR_24: - if resp.Error, err = d.DecodeString(); err != nil { - return err + if decodedError, err = d.DecodeString(); err != nil { + return nil, err } case iproto.IPROTO_SQL_INFO: - if err = d.Decode(&resp.SQLInfo); err != nil { - return err + if err = d.Decode(&resp.sqlInfo); err != nil { + return nil, err } case iproto.IPROTO_METADATA: - if err = d.Decode(&resp.MetaData); err != nil { - return err + if err = d.Decode(&resp.metaData); err != nil { + return nil, err } case iproto.IPROTO_STMT_ID: if stmtID, err = d.DecodeUint64(); err != nil { - return err + return nil, err } case iproto.IPROTO_BIND_COUNT: if bindCount, err = d.DecodeUint64(); err != nil { - return err + return nil, err } case iproto.IPROTO_VERSION: if err = d.Decode(&serverProtocolInfo.Version); err != nil { - return err + return nil, err } case iproto.IPROTO_FEATURES: if larr, err = d.DecodeArrayLen(); err != nil { - return err + return nil, err } serverProtocolInfo.Features = make([]iproto.Feature, larr) for i := 0; i < larr; i++ { if err = d.Decode(&feature); err != nil { - return err + return nil, err } serverProtocolInfo.Features[i] = feature } case iproto.IPROTO_AUTH_TYPE: var auth string if auth, err = d.DecodeString(); err != nil { - return err + return nil, err } found := false for _, a := range [...]Auth{ChapSha1Auth, PapSha256Auth} { @@ -235,15 +256,15 @@ func (resp *Response) decodeBody() (err error) { } } if !found { - return fmt.Errorf("unknown auth type %s", auth) + return nil, fmt.Errorf("unknown auth type %s", auth) } case iproto.IPROTO_POSITION: - if resp.Pos, err = d.DecodeBytes(); err != nil { - return fmt.Errorf("unable to decode a position: %w", err) + if resp.pos, err = d.DecodeBytes(); err != nil { + return nil, fmt.Errorf("unable to decode a position: %w", err) } default: if err = d.Skip(); err != nil { - return err + return nil, err } } } @@ -251,31 +272,32 @@ func (resp *Response) decodeBody() (err error) { stmt := &Prepared{ StatementID: PreparedID(stmtID), ParamCount: bindCount, - MetaData: resp.MetaData, + MetaData: resp.metaData, } - resp.Data = []interface{}{stmt} + resp.data = []interface{}{stmt} } // Tarantool may send only version >= 1 if serverProtocolInfo.Version != ProtocolVersion(0) || serverProtocolInfo.Features != nil { if serverProtocolInfo.Version == ProtocolVersion(0) { - return fmt.Errorf("no protocol version provided in Id response") + return nil, fmt.Errorf("no protocol version provided in Id response") } if serverProtocolInfo.Features == nil { - return fmt.Errorf("no features provided in Id response") + return nil, fmt.Errorf("no features provided in Id response") } - resp.Data = []interface{}{serverProtocolInfo} + resp.data = []interface{}{serverProtocolInfo} } - if resp.Code != OkCode && resp.Code != PushCode { - resp.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.Code), resp.Error, errorExtendedInfo} + if decodedError != "" { + resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) + err = Error{iproto.Error(resp.header.Code), decodedError, errorExtendedInfo} } } - return + return resp.data, err } -func (resp *Response) decodeBodyTyped(res interface{}) (err error) { +func (resp *ConnResponse) DecodeTyped(res interface{}) error { + var err error if resp.buf.Len() > 0 { offset := resp.buf.Offset() defer resp.buf.Seek(offset) @@ -292,9 +314,10 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if l, err = d.DecodeMapLen(); err != nil { return err } + var decodedError string for ; l > 0; l-- { var cd int - if cd, err = resp.smallInt(d); err != nil { + if cd, err = smallInt(d, &resp.buf); err != nil { return err } switch iproto.Key(cd) { @@ -307,19 +330,19 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { return err } case iproto.IPROTO_ERROR_24: - if resp.Error, err = d.DecodeString(); err != nil { + if decodedError, err = d.DecodeString(); err != nil { return err } case iproto.IPROTO_SQL_INFO: - if err = d.Decode(&resp.SQLInfo); err != nil { + if err = d.Decode(&resp.sqlInfo); err != nil { return err } case iproto.IPROTO_METADATA: - if err = d.Decode(&resp.MetaData); err != nil { + if err = d.Decode(&resp.metaData); err != nil { return err } case iproto.IPROTO_POSITION: - if resp.Pos, err = d.DecodeBytes(); err != nil { + if resp.pos, err = d.DecodeBytes(); err != nil { return fmt.Errorf("unable to decode a position: %w", err) } default: @@ -328,33 +351,34 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { } } } - if resp.Code != OkCode && resp.Code != PushCode { - resp.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.Code), resp.Error, errorExtendedInfo} + if decodedError != "" { + resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) + err = Error{iproto.Error(resp.header.Code), decodedError, errorExtendedInfo} } } - return + return err } -// String implements Stringer interface. -func (resp *Response) String() (str string) { - if resp.Code == OkCode { - return fmt.Sprintf("<%d OK %v>", resp.RequestId, resp.Data) - } - return fmt.Sprintf("<%d ERR 0x%x %s>", resp.RequestId, resp.Code, resp.Error) +func (resp *ConnResponse) Header() Header { + return resp.header } -// Tuples converts result of Eval and Call to same format -// as other actions returns (i.e. array of arrays). -func (resp *Response) Tuples() (res [][]interface{}) { - res = make([][]interface{}, len(resp.Data)) - for i, t := range resp.Data { - switch t := t.(type) { - case []interface{}: - res[i] = t - default: - res[i] = []interface{}{t} - } +func (resp *ConnResponse) Pos() []byte { + return resp.pos +} + +func (resp *ConnResponse) MetaData() []ColumnMetaData { + return resp.metaData +} + +func (resp *ConnResponse) SQLInfo() SQLInfo { + return resp.sqlInfo +} + +// String implements Stringer interface. +func (resp *ConnResponse) String() (str string) { + if resp.header.Code == OkCode { + return fmt.Sprintf("<%d OK %v>", resp.header.RequestId, resp.data) } - return res + return fmt.Sprintf("<%d ERR 0x%x>", resp.header.RequestId, resp.header.Code) } diff --git a/response_it.go b/response_it.go index 404c68a51..f5a4517e0 100644 --- a/response_it.go +++ b/response_it.go @@ -12,7 +12,9 @@ type ResponseIterator interface { // Next tries to switch to a next Response and returns true if it exists. Next() bool // Value returns a current Response if it exists, nil otherwise. - Value() *Response + Value() Response + // IsPush returns true if the current response is a push response. + IsPush() bool // Err returns error if it happens. Err() error } diff --git a/settings/example_test.go b/settings/example_test.go index 47f8e8c43..0a1dcc9b7 100644 --- a/settings/example_test.go +++ b/settings/example_test.go @@ -31,7 +31,7 @@ func example_connect(dialer tarantool.Dialer, opts tarantool.Opts) *tarantool.Co } func Example_sqlFullColumnNames() { - var resp *tarantool.Response + var resp tarantool.Response var err error var isLess bool @@ -74,8 +74,9 @@ func Example_sqlFullColumnNames() { fmt.Printf("error on select: %v\n", err) return } + metaData := resp.MetaData() // Show response metadata. - fmt.Printf("full column name: %v\n", resp.MetaData[0].FieldName) + fmt.Printf("full column name: %v\n", metaData[0].FieldName) // Disable showing full column names in SQL responses. _, err = conn.Do(settings.NewSQLFullColumnNamesSetRequest(false)).Get() @@ -90,6 +91,7 @@ func Example_sqlFullColumnNames() { fmt.Printf("error on select: %v\n", err) return } + metaData = resp.MetaData() // Show response metadata. - fmt.Printf("short column name: %v\n", resp.MetaData[0].FieldName) + fmt.Printf("short column name: %v\n", metaData[0].FieldName) } diff --git a/settings/tarantool_test.go b/settings/tarantool_test.go index 272693243..6ca64455a 100644 --- a/settings/tarantool_test.go +++ b/settings/tarantool_test.go @@ -52,48 +52,41 @@ func skipIfSQLDeferForeignKeysSettingUnsupported(t *testing.T) { func TestErrorMarshalingEnabledSetting(t *testing.T) { skipIfErrorMarshalingEnabledSettingUnsupported(t) - var resp *tarantool.Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Disable receiving box.error as MP_EXT 3. - resp, err = conn.Do(NewErrorMarshalingEnabledSetRequest(false)).Get() + data, err := conn.Do(NewErrorMarshalingEnabledSetRequest(false)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", false}}, data) // Fetch current setting value. - resp, err = conn.Do(NewErrorMarshalingEnabledGetRequest()).Get() + data, err = conn.Do(NewErrorMarshalingEnabledGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", false}}, data) // Get a box.Error value. eval := tarantool.NewEvalRequest("return box.error.new(box.error.UNKNOWN)") - resp, err = conn.Do(eval).Get() + data, err = conn.Do(eval).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.IsType(t, "string", resp.Data[0]) + require.IsType(t, "string", data[0]) // Enable receiving box.error as MP_EXT 3. - resp, err = conn.Do(NewErrorMarshalingEnabledSetRequest(true)).Get() + data, err = conn.Do(NewErrorMarshalingEnabledSetRequest(true)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", true}}, data) // Fetch current setting value. - resp, err = conn.Do(NewErrorMarshalingEnabledGetRequest()).Get() + data, err = conn.Do(NewErrorMarshalingEnabledGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"error_marshaling_enabled", true}}, data) // Get a box.Error value. - resp, err = conn.Do(eval).Get() + data, err = conn.Do(eval).Get() require.Nil(t, err) - require.NotNil(t, resp) - _, ok := resp.Data[0].(*tarantool.BoxError) + _, ok := data[0].(*tarantool.BoxError) require.True(t, ok) } @@ -101,70 +94,63 @@ func TestSQLDefaultEngineSetting(t *testing.T) { // https://github.com/tarantool/tarantool/blob/680990a082374e4790539215f69d9e9ee39c3307/test/sql/engine.test.lua skipIfSettingsUnsupported(t) - var resp *tarantool.Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Set default SQL "CREATE TABLE" engine to "vinyl". - resp, err = conn.Do(NewSQLDefaultEngineSetRequest("vinyl")).Get() + data, err := conn.Do(NewSQLDefaultEngineSetRequest("vinyl")).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.EqualValues(t, []interface{}{[]interface{}{"sql_default_engine", "vinyl"}}, resp.Data) + require.EqualValues(t, []interface{}{[]interface{}{"sql_default_engine", "vinyl"}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLDefaultEngineGetRequest()).Get() + data, err = conn.Do(NewSQLDefaultEngineGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "vinyl"}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "vinyl"}}, data) // Create a space with "CREATE TABLE". exec := tarantool.NewExecuteRequest("CREATE TABLE T1_VINYL(a INT PRIMARY KEY, b INT, c INT);") - resp, err = conn.Do(exec).Get() + resp, err := conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) // Check new space engine. eval := tarantool.NewEvalRequest("return box.space['T1_VINYL'].engine") - resp, err = conn.Do(eval).Get() + data, err = conn.Do(eval).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, "vinyl", resp.Data[0]) + require.Equal(t, "vinyl", data[0]) // Set default SQL "CREATE TABLE" engine to "memtx". - resp, err = conn.Do(NewSQLDefaultEngineSetRequest("memtx")).Get() + data, err = conn.Do(NewSQLDefaultEngineSetRequest("memtx")).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLDefaultEngineGetRequest()).Get() + data, err = conn.Do(NewSQLDefaultEngineGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, data) // Create a space with "CREATE TABLE". exec = tarantool.NewExecuteRequest("CREATE TABLE T2_MEMTX(a INT PRIMARY KEY, b INT, c INT);") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) // Check new space engine. eval = tarantool.NewEvalRequest("return box.space['T2_MEMTX'].engine") - resp, err = conn.Do(eval).Get() + data, err = conn.Do(eval).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, "memtx", resp.Data[0]) + require.Equal(t, "memtx", data[0]) } func TestSQLDeferForeignKeysSetting(t *testing.T) { // https://github.com/tarantool/tarantool/blob/eafadc13425f14446d7aaa49dea67dfc1d5f45e9/test/sql/transitive-transactions.result skipIfSQLDeferForeignKeysSettingUnsupported(t) - var resp *tarantool.Response + var resp tarantool.Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) @@ -172,18 +158,18 @@ func TestSQLDeferForeignKeysSetting(t *testing.T) { // Create a parent space. exec := tarantool.NewExecuteRequest("CREATE TABLE parent(id INT PRIMARY KEY, y INT UNIQUE);") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) // Create a space with reference to the parent space. exec = tarantool.NewExecuteRequest( "CREATE TABLE child(id INT PRIMARY KEY, x INT REFERENCES parent(y));") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) deferEval := ` box.begin() @@ -198,16 +184,14 @@ func TestSQLDeferForeignKeysSetting(t *testing.T) { ` // Disable foreign key constraint checks before commit. - resp, err = conn.Do(NewSQLDeferForeignKeysSetRequest(false)).Get() + data, err := conn.Do(NewSQLDeferForeignKeysSetRequest(false)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", false}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLDeferForeignKeysGetRequest()).Get() + data, err = conn.Do(NewSQLDeferForeignKeysGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", false}}, data) // Evaluate a scenario when foreign key not exists // on INSERT, but exists on commit. @@ -215,29 +199,26 @@ func TestSQLDeferForeignKeysSetting(t *testing.T) { require.NotNil(t, err) require.ErrorContains(t, err, "Failed to execute SQL statement: FOREIGN KEY constraint failed") - resp, err = conn.Do(NewSQLDeferForeignKeysSetRequest(true)).Get() + data, err = conn.Do(NewSQLDeferForeignKeysSetRequest(true)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", true}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLDeferForeignKeysGetRequest()).Get() + data, err = conn.Do(NewSQLDeferForeignKeysGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_defer_foreign_keys", true}}, data) // Evaluate a scenario when foreign key not exists // on INSERT, but exists on commit. - resp, err = conn.Do(tarantool.NewEvalRequest(deferEval)).Get() + data, err = conn.Do(tarantool.NewEvalRequest(deferEval)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, true, resp.Data[0]) + require.Equal(t, true, data[0]) } func TestSQLFullColumnNamesSetting(t *testing.T) { skipIfSettingsUnsupported(t) - var resp *tarantool.Response + var resp tarantool.Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) @@ -245,61 +226,57 @@ func TestSQLFullColumnNamesSetting(t *testing.T) { // Create a space. exec := tarantool.NewExecuteRequest("CREATE TABLE FKNAME(ID INT PRIMARY KEY, X INT);") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) // Fill it with some data. exec = tarantool.NewExecuteRequest("INSERT INTO FKNAME VALUES (1, 1);") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) // Disable displaying full column names in metadata. - resp, err = conn.Do(NewSQLFullColumnNamesSetRequest(false)).Get() + data, err := conn.Do(NewSQLFullColumnNamesSetRequest(false)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", false}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLFullColumnNamesGetRequest()).Get() + data, err = conn.Do(NewSQLFullColumnNamesGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", false}}, data) // Get a data with short column names in metadata. exec = tarantool.NewExecuteRequest("SELECT X FROM FKNAME WHERE ID = 1;") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, "X", resp.MetaData[0].FieldName) + require.Equal(t, "X", resp.MetaData()[0].FieldName) // Enable displaying full column names in metadata. - resp, err = conn.Do(NewSQLFullColumnNamesSetRequest(true)).Get() + data, err = conn.Do(NewSQLFullColumnNamesSetRequest(true)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLFullColumnNamesGetRequest()).Get() + data, err = conn.Do(NewSQLFullColumnNamesGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, data) // Get a data with full column names in metadata. exec = tarantool.NewExecuteRequest("SELECT X FROM FKNAME WHERE ID = 1;") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, "FKNAME.X", resp.MetaData[0].FieldName) + require.Equal(t, "FKNAME.X", resp.MetaData()[0].FieldName) } func TestSQLFullMetadataSetting(t *testing.T) { skipIfSettingsUnsupported(t) - var resp *tarantool.Response + var resp tarantool.Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) @@ -307,88 +284,80 @@ func TestSQLFullMetadataSetting(t *testing.T) { // Create a space. exec := tarantool.NewExecuteRequest("CREATE TABLE fmt(id INT PRIMARY KEY, x INT);") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) // Fill it with some data. exec = tarantool.NewExecuteRequest("INSERT INTO fmt VALUES (1, 1);") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) // Disable displaying additional fields in metadata. - resp, err = conn.Do(NewSQLFullMetadataSetRequest(false)).Get() + data, err := conn.Do(NewSQLFullMetadataSetRequest(false)).Get() require.Nil(t, err) - require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", false}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLFullMetadataGetRequest()).Get() + data, err = conn.Do(NewSQLFullMetadataGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", false}}, data) // Get a data without additional fields in metadata. exec = tarantool.NewExecuteRequest("SELECT x FROM fmt WHERE id = 1;") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, "", resp.MetaData[0].FieldSpan) + require.Equal(t, "", resp.MetaData()[0].FieldSpan) // Enable displaying full column names in metadata. - resp, err = conn.Do(NewSQLFullMetadataSetRequest(true)).Get() + data, err = conn.Do(NewSQLFullMetadataSetRequest(true)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", true}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLFullMetadataGetRequest()).Get() + data, err = conn.Do(NewSQLFullMetadataGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_full_metadata", true}}, data) // Get a data with additional fields in metadata. exec = tarantool.NewExecuteRequest("SELECT x FROM fmt WHERE id = 1;") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, "x", resp.MetaData[0].FieldSpan) + require.Equal(t, "x", resp.MetaData()[0].FieldSpan) } func TestSQLParserDebugSetting(t *testing.T) { skipIfSettingsUnsupported(t) - var resp *tarantool.Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Disable parser debug mode. - resp, err = conn.Do(NewSQLParserDebugSetRequest(false)).Get() + data, err := conn.Do(NewSQLParserDebugSetRequest(false)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", false}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLParserDebugGetRequest()).Get() + data, err = conn.Do(NewSQLParserDebugGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", false}}, data) // Enable parser debug mode. - resp, err = conn.Do(NewSQLParserDebugSetRequest(true)).Get() + data, err = conn.Do(NewSQLParserDebugSetRequest(true)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", true}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLParserDebugGetRequest()).Get() + data, err = conn.Do(NewSQLParserDebugGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_parser_debug", true}}, data) // To test real effect we need a Tarantool instance built with // `-DCMAKE_BUILD_TYPE=Debug`. @@ -398,7 +367,7 @@ func TestSQLRecursiveTriggersSetting(t *testing.T) { // https://github.com/tarantool/tarantool/blob/d11fb3061e15faf4e0eb5375fb8056b4e64348ae/test/sql-tap/triggerC.test.lua skipIfSettingsUnsupported(t) - var resp *tarantool.Response + var resp tarantool.Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) @@ -406,39 +375,37 @@ func TestSQLRecursiveTriggersSetting(t *testing.T) { // Create a space. exec := tarantool.NewExecuteRequest("CREATE TABLE rec(id INTEGER PRIMARY KEY, a INT, b INT);") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) // Fill it with some data. exec = tarantool.NewExecuteRequest("INSERT INTO rec VALUES(1, 1, 2);") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) // Create a recursive trigger (with infinite depth). exec = tarantool.NewExecuteRequest(` CREATE TRIGGER tr12 AFTER UPDATE ON rec FOR EACH ROW BEGIN UPDATE rec SET a=new.a+1, b=new.b+1; END;`) - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) // Enable SQL recursive triggers. - resp, err = conn.Do(NewSQLRecursiveTriggersSetRequest(true)).Get() + data, err := conn.Do(NewSQLRecursiveTriggersSetRequest(true)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", true}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLRecursiveTriggersGetRequest()).Get() + data, err = conn.Do(NewSQLRecursiveTriggersGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", true}}, data) // Trigger the recursion. exec = tarantool.NewExecuteRequest("UPDATE rec SET a=a+1, b=b+1;") @@ -448,29 +415,27 @@ func TestSQLRecursiveTriggersSetting(t *testing.T) { "Failed to execute SQL statement: too many levels of trigger recursion") // Disable SQL recursive triggers. - resp, err = conn.Do(NewSQLRecursiveTriggersSetRequest(false)).Get() + data, err = conn.Do(NewSQLRecursiveTriggersSetRequest(false)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", false}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLRecursiveTriggersGetRequest()).Get() + data, err = conn.Do(NewSQLRecursiveTriggersGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_recursive_triggers", false}}, data) // Trigger the recursion. exec = tarantool.NewExecuteRequest("UPDATE rec SET a=a+1, b=b+1;") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) } func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { skipIfSettingsUnsupported(t) - var resp *tarantool.Response + var resp tarantool.Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) @@ -478,37 +443,35 @@ func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { // Create a space. exec := tarantool.NewExecuteRequest("CREATE TABLE data(id STRING PRIMARY KEY);") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) // Fill it with some data. exec = tarantool.NewExecuteRequest("INSERT INTO data VALUES('1');") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) exec = tarantool.NewExecuteRequest("INSERT INTO data VALUES('2');") - resp, err = conn.Do(exec).Get() + resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo.AffectedCount) + require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) // Disable reverse order in unordered selects. - resp, err = conn.Do(NewSQLReverseUnorderedSelectsSetRequest(false)).Get() + data, err := conn.Do(NewSQLReverseUnorderedSelectsSetRequest(false)).Get() require.Nil(t, err) - require.NotNil(t, resp) require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", false}}, - resp.Data) + data) // Fetch current setting value. - resp, err = conn.Do(NewSQLReverseUnorderedSelectsGetRequest()).Get() + data, err = conn.Do(NewSQLReverseUnorderedSelectsGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", false}}, - resp.Data) + data) // Select multiple records. query := "SELECT * FROM seqscan data;" @@ -518,66 +481,57 @@ func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { query = "SELECT * FROM data;" } - resp, err = conn.Do(tarantool.NewExecuteRequest(query)).Get() + data, err = conn.Do(tarantool.NewExecuteRequest(query)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.EqualValues(t, []interface{}{"1"}, resp.Data[0]) - require.EqualValues(t, []interface{}{"2"}, resp.Data[1]) + require.EqualValues(t, []interface{}{"1"}, data[0]) + require.EqualValues(t, []interface{}{"2"}, data[1]) // Enable reverse order in unordered selects. - resp, err = conn.Do(NewSQLReverseUnorderedSelectsSetRequest(true)).Get() + data, err = conn.Do(NewSQLReverseUnorderedSelectsSetRequest(true)).Get() require.Nil(t, err) - require.NotNil(t, resp) require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", true}}, - resp.Data) + data) // Fetch current setting value. - resp, err = conn.Do(NewSQLReverseUnorderedSelectsGetRequest()).Get() + data, err = conn.Do(NewSQLReverseUnorderedSelectsGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) require.Equal(t, []interface{}{[]interface{}{"sql_reverse_unordered_selects", true}}, - resp.Data) + data) // Select multiple records. - resp, err = conn.Do(tarantool.NewExecuteRequest(query)).Get() + data, err = conn.Do(tarantool.NewExecuteRequest(query)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.EqualValues(t, []interface{}{"2"}, resp.Data[0]) - require.EqualValues(t, []interface{}{"1"}, resp.Data[1]) + require.EqualValues(t, []interface{}{"2"}, data[0]) + require.EqualValues(t, []interface{}{"1"}, data[1]) } func TestSQLSelectDebugSetting(t *testing.T) { skipIfSettingsUnsupported(t) - var resp *tarantool.Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Disable select debug mode. - resp, err = conn.Do(NewSQLSelectDebugSetRequest(false)).Get() + data, err := conn.Do(NewSQLSelectDebugSetRequest(false)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", false}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLSelectDebugGetRequest()).Get() + data, err = conn.Do(NewSQLSelectDebugGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", false}}, data) // Enable select debug mode. - resp, err = conn.Do(NewSQLSelectDebugSetRequest(true)).Get() + data, err = conn.Do(NewSQLSelectDebugSetRequest(true)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", true}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLSelectDebugGetRequest()).Get() + data, err = conn.Do(NewSQLSelectDebugGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_select_debug", true}}, data) // To test real effect we need a Tarantool instance built with // `-DCMAKE_BUILD_TYPE=Debug`. @@ -586,35 +540,30 @@ func TestSQLSelectDebugSetting(t *testing.T) { func TestSQLVDBEDebugSetting(t *testing.T) { skipIfSettingsUnsupported(t) - var resp *tarantool.Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Disable VDBE debug mode. - resp, err = conn.Do(NewSQLVDBEDebugSetRequest(false)).Get() + data, err := conn.Do(NewSQLVDBEDebugSetRequest(false)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", false}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLVDBEDebugGetRequest()).Get() + data, err = conn.Do(NewSQLVDBEDebugGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", false}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", false}}, data) // Enable VDBE debug mode. - resp, err = conn.Do(NewSQLVDBEDebugSetRequest(true)).Get() + data, err = conn.Do(NewSQLVDBEDebugSetRequest(true)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", true}}, data) // Fetch current setting value. - resp, err = conn.Do(NewSQLVDBEDebugGetRequest()).Get() + data, err = conn.Do(NewSQLVDBEDebugGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_vdbe_debug", true}}, data) // To test real effect we need a Tarantool instance built with // `-DCMAKE_BUILD_TYPE=Debug`. @@ -623,28 +572,24 @@ func TestSQLVDBEDebugSetting(t *testing.T) { func TestSessionSettings(t *testing.T) { skipIfSettingsUnsupported(t) - var resp *tarantool.Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Set some settings values. - resp, err = conn.Do(NewSQLDefaultEngineSetRequest("memtx")).Get() + data, err := conn.Do(NewSQLDefaultEngineSetRequest("memtx")).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_default_engine", "memtx"}}, data) - resp, err = conn.Do(NewSQLFullColumnNamesSetRequest(true)).Get() + data, err = conn.Do(NewSQLFullColumnNamesSetRequest(true)).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, resp.Data) + require.Equal(t, []interface{}{[]interface{}{"sql_full_column_names", true}}, data) // Fetch current settings values. - resp, err = conn.Do(NewSessionSettingsGetRequest()).Get() + data, err = conn.Do(NewSessionSettingsGetRequest()).Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Subset(t, resp.Data, + require.Subset(t, data, []interface{}{ []interface{}{"sql_default_engine", "memtx"}, []interface{}{"sql_full_column_names", true}, diff --git a/shutdown_test.go b/shutdown_test.go index 80996e3a9..b3a09eff0 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -47,7 +47,6 @@ var evalBody = ` ` func testGracefulShutdown(t *testing.T, conn *Connection, inst *test_helpers.TarantoolInstance) { - var resp *Response var err error // Set a big timeout so it would be easy to differ @@ -102,10 +101,9 @@ func testGracefulShutdown(t *testing.T, conn *Connection, inst *test_helpers.Tar require.Nil(t, err) // Check that requests started before the shutdown finish successfully. - resp, err = fut.Get() + data, err := fut.Get() require.Nil(t, err) - require.NotNil(t, resp) - require.Equal(t, resp.Data, []interface{}{evalMsg}) + require.Equal(t, data, []interface{}{evalMsg}) // Wait until server go down. // Server will go down only when it process all requests from our connection diff --git a/tarantool_test.go b/tarantool_test.go index 4d3f193f1..aa3f248f9 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -759,12 +759,12 @@ func TestFutureMultipleGetGetTyped(t *testing.T) { } if get { - resp, err := fut.Get() + data, err := fut.Get() if err != nil { t.Errorf("Failed to call Get(): %s", err) } - if val, ok := resp.Data[0].(string); !ok || val != "11" { - t.Errorf("Wrong Get() result: %v", resp.Data) + if val, ok := data[0].(string); !ok || val != "11" { + t.Errorf("Wrong Get() result: %v", data) } } else { tpl := struct { @@ -821,7 +821,7 @@ func TestFutureMultipleGetTypedWithError(t *testing.T) { /////////////////// func TestClient(t *testing.T) { - var resp *Response + var resp Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) @@ -835,7 +835,7 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Ping") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } @@ -847,13 +847,17 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Insert") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if len(resp.Data) != 1 { + data, err := resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) + } + if len(data) != 1 { t.Errorf("Response Body len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Insert") } else { if len(tpl) != 3 { @@ -870,7 +874,8 @@ func TestClient(t *testing.T) { if tntErr, ok := err.(Error); !ok || tntErr.Code != iproto.ER_TUPLE_FOUND { t.Errorf("Expected %s but got: %v", iproto.ER_TUPLE_FOUND, err) } - if len(resp.Data) != 0 { + data, _ = resp.Decode() + if len(data) != 0 { t.Errorf("Response Body len != 0") } @@ -882,13 +887,17 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Delete") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if len(resp.Data) != 1 { + data, err = resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) + } + if len(data) != 1 { t.Errorf("Response Body len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Delete") } else { if len(tpl) != 3 { @@ -908,10 +917,14 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Delete") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if len(resp.Data) != 0 { + data, err = resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) + } + if len(data) != 0 { t.Errorf("Response Data len != 0") } @@ -923,7 +936,7 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Replace") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } resp, err = conn.Replace(spaceNo, []interface{}{uint(2), "hi", "planet"}) @@ -933,10 +946,14 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Replace (duplicate)") } - if len(resp.Data) != 1 { + data, err = resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) + } + if len(data) != 1 { t.Errorf("Response Data len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Replace") } else { if len(tpl) != 3 { @@ -959,13 +976,17 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Update") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if len(resp.Data) != 1 { + data, err = resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) + } + if len(data) != 1 { t.Errorf("Response Data len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Update") } else { if len(tpl) != 2 { @@ -988,7 +1009,7 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Upsert (insert)") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, @@ -1006,11 +1027,12 @@ func TestClient(t *testing.T) { if err != nil { t.Fatalf("Failed to Replace: %s", err.Error()) } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if resp.Code != 0 { - t.Errorf("Failed to replace") + _, err := resp.Decode() + if err != nil { + t.Errorf("Failed to replace: %s", err.Error()) } } resp, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}) @@ -1020,13 +1042,17 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Select") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if len(resp.Data) != 1 { + data, err = resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) + } + if len(data) != 1 { t.Errorf("Response Data len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 10 { @@ -1045,10 +1071,14 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Select") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if len(resp.Data) != 0 { + data, err = resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) + } + if len(data) != 0 { t.Errorf("Response Data len != 0") } @@ -1114,10 +1144,14 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Call16") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if len(resp.Data) < 1 { + data, err = resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) + } + if len(data) < 1 { t.Errorf("Response.Data is empty after Eval") } @@ -1126,22 +1160,30 @@ func TestClient(t *testing.T) { if err != nil { t.Errorf("Failed to use Call16") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if val, ok := resp.Data[0].([]interface{})[0].(string); !ok || val != "11" { - t.Errorf("result is not {{1}} : %v", resp.Data) + data, err = resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) + } + if val, ok := data[0].([]interface{})[0].(string); !ok || val != "11" { + t.Errorf("result is not {{1}} : %v", data) } resp, err = conn.Call17("simple_concat", []interface{}{"1"}) if err != nil { t.Errorf("Failed to use Call") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if val, ok := resp.Data[0].(string); !ok || val != "11" { - t.Errorf("result is not {{1}} : %v", resp.Data) + data, err = resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) + } + if val, ok := data[0].(string); !ok || val != "11" { + t.Errorf("result is not {{1}} : %v", data) } // Eval @@ -1152,13 +1194,17 @@ func TestClient(t *testing.T) { if resp == nil { t.Fatalf("Response is nil after Eval") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if len(resp.Data) < 1 { + data, err = resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) + } + if len(data) < 1 { t.Errorf("Response.Data is empty after Eval") } - if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != 11 { + if val, err := test_helpers.ConvertUint64(data[0]); err != nil || val != 11 { t.Errorf("5 + 6 == 11, but got %v", val) } } @@ -1196,15 +1242,13 @@ func TestClientSessionPush(t *testing.T) { // It will wait a response before iteration. fut1 := conn.Call17Async("push_func", []interface{}{pushMax}) // Future.Get ignores push messages. - resp, err := fut1.Get() + data, err := fut1.Get() if err != nil { t.Errorf("Failed to Call17: %s", err.Error()) - } else if resp == nil { - t.Errorf("Response is nil after CallAsync") - } else if len(resp.Data) < 1 { + } else if len(data) < 1 { t.Errorf("Response.Data is empty after Call17Async") - } else if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != pushMax { - t.Errorf("Result is not %d: %v", pushMax, resp.Data) + } else if val, err := test_helpers.ConvertUint64(data[0]); err != nil || val != pushMax { + t.Errorf("Result is not %d: %v", pushMax, data) } // It will will be iterated with a timeout. @@ -1216,60 +1260,41 @@ func TestClientSessionPush(t *testing.T) { } for i := 0; i < len(its); i++ { - pushCnt := uint64(0) - respCnt := uint64(0) - it = its[i] for it.Next() { - resp = it.Value() + resp := it.Value() if resp == nil { t.Errorf("Response is empty after it.Next() == true") break } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if len(resp.Data) < 1 { - t.Errorf("Response.Data is empty after CallAsync") + data, err := resp.Decode() + if err != nil { + t.Errorf("Failed to Decode: %s", err.Error()) break } - if resp.Code == PushCode { - pushCnt += 1 - val, err := test_helpers.ConvertUint64(resp.Data[0]) - if err != nil || val != pushCnt { - t.Errorf("Unexpected push data = %v", resp.Data) - } - } else { - respCnt += 1 - val, err := test_helpers.ConvertUint64(resp.Data[0]) - if err != nil || val != pushMax { - t.Errorf("Result is not %d: %v", pushMax, resp.Data) - } + if len(data) < 1 { + t.Errorf("Response.Data is empty after CallAsync") + break } } if err = it.Err(); err != nil { t.Errorf("An unexpected iteration error: %s", err.Error()) } - - if pushCnt != pushMax { - t.Errorf("Expect %d pushes but got %d", pushMax, pushCnt) - } - - if respCnt != 1 { - t.Errorf("Expect %d responses but got %d", 1, respCnt) - } } // We can collect original responses after iterations. for _, fut := range []*Future{fut0, fut1, fut2} { - resp, err := fut.Get() + data, err := fut.Get() if err != nil { t.Errorf("Unable to call fut.Get(): %s", err) } - val, err := test_helpers.ConvertUint64(resp.Data[0]) + val, err := test_helpers.ConvertUint64(data[0]) if err != nil || val != pushMax { - t.Errorf("Result is not %d: %v", pushMax, resp.Data) + t.Errorf("Result is not %d: %v", pushMax, data) } tpl := struct { @@ -1311,9 +1336,11 @@ func TestSQL(t *testing.T) { test_helpers.SkipIfSQLUnsupported(t) type testCase struct { - Query string - Args interface{} - Resp Response + Query string + Args interface{} + sqlInfo SQLInfo + data []interface{} + metaData []ColumnMetaData } selectSpanDifQuery := selectSpanDifQueryNew @@ -1327,20 +1354,16 @@ func TestSQL(t *testing.T) { { createTableQuery, []interface{}{}, - Response{ - SQLInfo: SQLInfo{AffectedCount: 1}, - Data: []interface{}{}, - MetaData: nil, - }, + SQLInfo{AffectedCount: 1}, + []interface{}{}, + nil, }, { insertQuery, []interface{}{"1", "test"}, - Response{ - SQLInfo: SQLInfo{AffectedCount: 1}, - Data: []interface{}{}, - MetaData: nil, - }, + SQLInfo{AffectedCount: 1}, + []interface{}{}, + nil, }, { selectNamedQuery, @@ -1348,119 +1371,100 @@ func TestSQL(t *testing.T) { "ID": "1", "NAME": "test", }, - Response{ - SQLInfo: SQLInfo{AffectedCount: 0}, - Data: []interface{}{[]interface{}{"1", "test"}}, - MetaData: []ColumnMetaData{ - {FieldType: "string", FieldName: "ID"}, - {FieldType: "string", FieldName: "NAME"}}, - }, + SQLInfo{AffectedCount: 0}, + []interface{}{[]interface{}{"1", "test"}}, + []ColumnMetaData{ + {FieldType: "string", FieldName: "ID"}, + {FieldType: "string", FieldName: "NAME"}}, }, { selectPosQuery, []interface{}{"1", "test"}, - Response{ - SQLInfo: SQLInfo{AffectedCount: 0}, - Data: []interface{}{[]interface{}{"1", "test"}}, - MetaData: []ColumnMetaData{ - {FieldType: "string", FieldName: "ID"}, - {FieldType: "string", FieldName: "NAME"}}, - }, + SQLInfo{AffectedCount: 0}, + []interface{}{[]interface{}{"1", "test"}}, + []ColumnMetaData{ + {FieldType: "string", FieldName: "ID"}, + {FieldType: "string", FieldName: "NAME"}}, }, { updateQuery, []interface{}{"test_test", "1"}, - Response{ - SQLInfo: SQLInfo{AffectedCount: 1}, - Data: []interface{}{}, - MetaData: nil, - }, + SQLInfo{AffectedCount: 1}, + []interface{}{}, + nil, }, { enableFullMetaDataQuery, []interface{}{}, - Response{ - SQLInfo: SQLInfo{AffectedCount: 1}, - Data: []interface{}{}, - MetaData: nil, - }, + SQLInfo{AffectedCount: 1}, + []interface{}{}, + nil, }, { selectSpanDifQuery, []interface{}{"test_test"}, - Response{ - SQLInfo: SQLInfo{AffectedCount: 0}, - Data: []interface{}{[]interface{}{"11", "test_test", "1"}}, - MetaData: []ColumnMetaData{ - { - FieldType: "string", - FieldName: "COLUMN_1", - FieldIsNullable: false, - FieldIsAutoincrement: false, - FieldSpan: "ID||ID", - }, - { - FieldType: "string", - FieldName: "NAME", - FieldIsNullable: true, - FieldIsAutoincrement: false, - FieldSpan: "NAME", - FieldCollation: "unicode", - }, - { - FieldType: "string", - FieldName: "ID", - FieldIsNullable: false, - FieldIsAutoincrement: false, - FieldSpan: "ID", - FieldCollation: "", - }, - }}, + SQLInfo{AffectedCount: 0}, + []interface{}{[]interface{}{"11", "test_test", "1"}}, + []ColumnMetaData{ + { + FieldType: "string", + FieldName: "COLUMN_1", + FieldIsNullable: false, + FieldIsAutoincrement: false, + FieldSpan: "ID||ID", + }, + { + FieldType: "string", + FieldName: "NAME", + FieldIsNullable: true, + FieldIsAutoincrement: false, + FieldSpan: "NAME", + FieldCollation: "unicode", + }, + { + FieldType: "string", + FieldName: "ID", + FieldIsNullable: false, + FieldIsAutoincrement: false, + FieldSpan: "ID", + FieldCollation: "", + }, + }, }, { alterTableQuery, []interface{}{}, - Response{ - SQLInfo: SQLInfo{AffectedCount: 0}, - Data: []interface{}{}, - MetaData: nil, - }, + SQLInfo{AffectedCount: 0}, + []interface{}{}, + nil, }, { insertIncrQuery, []interface{}{"2", "test_2"}, - Response{ - SQLInfo: SQLInfo{AffectedCount: 1, InfoAutoincrementIds: []uint64{1}}, - Data: []interface{}{}, - MetaData: nil, - }, + SQLInfo{AffectedCount: 1, InfoAutoincrementIds: []uint64{1}}, + []interface{}{}, + nil, }, { deleteQuery, []interface{}{"test_2"}, - Response{ - SQLInfo: SQLInfo{AffectedCount: 1}, - Data: []interface{}{}, - MetaData: nil, - }, + SQLInfo{AffectedCount: 1}, + []interface{}{}, + nil, }, { dropQuery, []interface{}{}, - Response{ - SQLInfo: SQLInfo{AffectedCount: 1}, - Data: []interface{}{}, - MetaData: nil, - }, + SQLInfo{AffectedCount: 1}, + []interface{}{}, + nil, }, { disableFullMetaDataQuery, []interface{}{}, - Response{ - SQLInfo: SQLInfo{AffectedCount: 1}, - Data: []interface{}{}, - MetaData: nil, - }, + SQLInfo{AffectedCount: 1}, + []interface{}{}, + nil, }, } @@ -1471,23 +1475,26 @@ func TestSQL(t *testing.T) { resp, err := conn.Execute(test.Query, test.Args) assert.NoError(t, err, "Failed to Execute, query: %s", test.Query) assert.NotNil(t, resp, "Response is nil after Execute\nQuery number: %d", i) - for j := range resp.Data { - assert.Equal(t, resp.Data[j], test.Resp.Data[j], "Response data is wrong") + data, err := resp.Decode() + assert.Nil(t, err, "Failed to Decode") + for j := range data { + assert.Equal(t, data[j], test.data[j], "Response data is wrong") } - assert.Equal(t, resp.SQLInfo.AffectedCount, test.Resp.SQLInfo.AffectedCount, + assert.Equal(t, resp.SQLInfo().AffectedCount, test.sqlInfo.AffectedCount, "Affected count is wrong") errorMsg := "Response Metadata is wrong" - for j := range resp.MetaData { - assert.Equal(t, resp.MetaData[j].FieldIsAutoincrement, - test.Resp.MetaData[j].FieldIsAutoincrement, errorMsg) - assert.Equal(t, resp.MetaData[j].FieldIsNullable, - test.Resp.MetaData[j].FieldIsNullable, errorMsg) - assert.Equal(t, resp.MetaData[j].FieldCollation, - test.Resp.MetaData[j].FieldCollation, errorMsg) - assert.Equal(t, resp.MetaData[j].FieldName, test.Resp.MetaData[j].FieldName, errorMsg) - assert.Equal(t, resp.MetaData[j].FieldSpan, test.Resp.MetaData[j].FieldSpan, errorMsg) - assert.Equal(t, resp.MetaData[j].FieldType, test.Resp.MetaData[j].FieldType, errorMsg) + metaData := resp.MetaData() + for j := range metaData { + assert.Equal(t, metaData[j].FieldIsAutoincrement, + test.metaData[j].FieldIsAutoincrement, errorMsg) + assert.Equal(t, metaData[j].FieldIsNullable, + test.metaData[j].FieldIsNullable, errorMsg) + assert.Equal(t, metaData[j].FieldCollation, + test.metaData[j].FieldCollation, errorMsg) + assert.Equal(t, metaData[j].FieldName, test.metaData[j].FieldName, errorMsg) + assert.Equal(t, metaData[j].FieldSpan, test.metaData[j].FieldSpan, errorMsg) + assert.Equal(t, metaData[j].FieldType, test.metaData[j].FieldType, errorMsg) } } } @@ -1522,7 +1529,7 @@ func TestSQLBindings(t *testing.T) { 1: "test", } - var resp *Response + var resp Response conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() @@ -1575,13 +1582,18 @@ func TestSQLBindings(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } - if reflect.DeepEqual(resp.Data[0], []interface{}{1, testData[1]}) { + data, err := resp.Decode() + if err != nil { + t.Errorf("Failed to Decode: %s", err.Error()) + } + if reflect.DeepEqual(data[0], []interface{}{1, testData[1]}) { t.Error("Select with named arguments failed") } - if resp.MetaData[0].FieldType != "unsigned" || - resp.MetaData[0].FieldName != "NAME0" || - resp.MetaData[1].FieldType != "string" || - resp.MetaData[1].FieldName != "NAME1" { + metaData := resp.MetaData() + if metaData[0].FieldType != "unsigned" || + metaData[0].FieldName != "NAME0" || + metaData[1].FieldType != "string" || + metaData[1].FieldName != "NAME1" { t.Error("Wrong metadata") } } @@ -1593,13 +1605,18 @@ func TestSQLBindings(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } - if reflect.DeepEqual(resp.Data[0], []interface{}{1, testData[1]}) { + data, err := resp.Decode() + if err != nil { + t.Errorf("Failed to Decode: %s", err.Error()) + } + if reflect.DeepEqual(data[0], []interface{}{1, testData[1]}) { t.Error("Select with positioned arguments failed") } - if resp.MetaData[0].FieldType != "unsigned" || - resp.MetaData[0].FieldName != "NAME0" || - resp.MetaData[1].FieldType != "string" || - resp.MetaData[1].FieldName != "NAME1" { + metaData := resp.MetaData() + if metaData[0].FieldType != "unsigned" || + metaData[0].FieldName != "NAME0" || + metaData[1].FieldType != "string" || + metaData[1].FieldName != "NAME1" { t.Error("Wrong metadata") } @@ -1610,13 +1627,18 @@ func TestSQLBindings(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } - if reflect.DeepEqual(resp.Data[0], []interface{}{1, testData[1]}) { + data, err = resp.Decode() + if err != nil { + t.Errorf("Failed to Decode: %s", err.Error()) + } + if reflect.DeepEqual(data[0], []interface{}{1, testData[1]}) { t.Error("Select with positioned arguments failed") } - if resp.MetaData[0].FieldType != "unsigned" || - resp.MetaData[0].FieldName != "NAME0" || - resp.MetaData[1].FieldType != "string" || - resp.MetaData[1].FieldName != "NAME1" { + metaData = resp.MetaData() + if metaData[0].FieldType != "unsigned" || + metaData[0].FieldName != "NAME0" || + metaData[1].FieldType != "string" || + metaData[1].FieldName != "NAME1" { t.Error("Wrong metadata") } } @@ -1624,7 +1646,7 @@ func TestSQLBindings(t *testing.T) { func TestStressSQL(t *testing.T) { test_helpers.SkipIfSQLUnsupported(t) - var resp *Response + var resp Response conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() @@ -1636,11 +1658,8 @@ func TestStressSQL(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Code != 0 { - t.Fatalf("Failed to Execute: %d", resp.Code) - } - if resp.SQLInfo.AffectedCount != 1 { - t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + if resp.SQLInfo().AffectedCount != 1 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo().AffectedCount) } // create table with the same name @@ -1652,11 +1671,8 @@ func TestStressSQL(t *testing.T) { t.Fatal("Response is nil after Execute") } - if iproto.Error(resp.Code) != iproto.ER_SPACE_EXISTS { - t.Fatalf("Unexpected response code: %d", resp.Code) - } - if resp.SQLInfo.AffectedCount != 0 { - t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + if resp.SQLInfo().AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo().AffectedCount) } // execute with nil argument @@ -1667,11 +1683,8 @@ func TestStressSQL(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Code == 0 { - t.Fatalf("Unexpected response code: %d", resp.Code) - } - if resp.SQLInfo.AffectedCount != 0 { - t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + if resp.SQLInfo().AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo().AffectedCount) } // execute with zero string @@ -1682,11 +1695,8 @@ func TestStressSQL(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Code == 0 { - t.Fatalf("Unexpected response code: %d", resp.Code) - } - if resp.SQLInfo.AffectedCount != 0 { - t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + if resp.SQLInfo().AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo().AffectedCount) } // drop table query @@ -1697,11 +1707,8 @@ func TestStressSQL(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Code != 0 { - t.Fatalf("Failed to Execute: %d", resp.Code) - } - if resp.SQLInfo.AffectedCount != 1 { - t.Errorf("Incorrect count of dropped spaces: %d", resp.SQLInfo.AffectedCount) + if resp.SQLInfo().AffectedCount != 1 { + t.Errorf("Incorrect count of dropped spaces: %d", resp.SQLInfo().AffectedCount) } // drop the same table @@ -1712,11 +1719,8 @@ func TestStressSQL(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Code == 0 { - t.Fatalf("Unexpected response code: %d", resp.Code) - } - if resp.SQLInfo.AffectedCount != 0 { - t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + if resp.SQLInfo().AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo().AffectedCount) } } @@ -1734,30 +1738,29 @@ func TestNewPrepared(t *testing.T) { executeReq := NewExecutePreparedRequest(stmt) unprepareReq := NewUnprepareRequest(stmt) - resp, err := conn.Do(executeReq.Args([]interface{}{1, "test"})).Get() + resp, err := conn.Do(executeReq.Args([]interface{}{1, "test"})).GetResponse() if err != nil { t.Errorf("failed to execute prepared: %v", err) } - if resp.Code != OkCode { - t.Errorf("failed to execute prepared: code %d", resp.Code) + data, err := resp.Decode() + if err != nil { + t.Errorf("Failed to Decode: %s", err.Error()) } - if reflect.DeepEqual(resp.Data[0], []interface{}{1, "test"}) { + if reflect.DeepEqual(data[0], []interface{}{1, "test"}) { t.Error("Select with named arguments failed") } - if resp.MetaData[0].FieldType != "unsigned" || - resp.MetaData[0].FieldName != "NAME0" || - resp.MetaData[1].FieldType != "string" || - resp.MetaData[1].FieldName != "NAME1" { + metaData := resp.MetaData() + if metaData[0].FieldType != "unsigned" || + metaData[0].FieldName != "NAME0" || + metaData[1].FieldType != "string" || + metaData[1].FieldName != "NAME1" { t.Error("Wrong metadata") } - resp, err = conn.Do(unprepareReq).Get() + _, err = conn.Do(unprepareReq).Get() if err != nil { t.Errorf("failed to unprepare prepared statement: %v", err) } - if resp.Code != OkCode { - t.Errorf("failed to unprepare prepared statement: code %d", resp.Code) - } _, err = conn.Do(unprepareReq).Get() if err == nil { @@ -1772,21 +1775,18 @@ func TestNewPrepared(t *testing.T) { require.Contains(t, err.Error(), "Prepared statement with id") prepareReq := NewPrepareRequest(selectNamedQuery2) - resp, err = conn.Do(prepareReq).Get() + data, err = conn.Do(prepareReq).Get() if err != nil { t.Errorf("failed to prepare: %v", err) } - if resp.Data == nil { + if data == nil { t.Errorf("failed to prepare: Data is nil") } - if resp.Code != OkCode { - t.Errorf("failed to unprepare prepared statement: code %d", resp.Code) - } - if len(resp.Data) == 0 { + if len(data) == 0 { t.Errorf("failed to prepare: response Data has no elements") } - stmt, ok := resp.Data[0].(*Prepared) + stmt, ok := data[0].(*Prepared) if !ok { t.Errorf("failed to prepare: failed to cast the response Data to Prepared object") } @@ -1830,13 +1830,10 @@ func TestConnection_SetSchema_Changes(t *testing.T) { req := NewInsertRequest(spaceName) req.Tuple([]interface{}{uint(1010), "Tarantool"}) - resp, err := conn.Do(req).Get() + _, err := conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Insert: %s", err.Error()) } - if resp.Code != OkCode { - t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) - } s, err := GetSchema(conn) if err != nil { @@ -1850,41 +1847,12 @@ func TestConnection_SetSchema_Changes(t *testing.T) { reqS := NewSelectRequest(spaceName) reqS.Key([]interface{}{uint(1010)}) - resp, err = conn.Do(reqS).Get() + data, err := conn.Do(reqS).Get() if err != nil { t.Fatalf("failed to Select: %s", err.Error()) } - if resp.Code != OkCode { - t.Errorf("failed to Select: wrong code returned %d", resp.Code) - } - if resp.Data[0].([]interface{})[1] != "Tarantool" { - t.Errorf("wrong Select body: %v", resp.Data) - } -} - -func TestNewPreparedFromResponse(t *testing.T) { - var ( - ErrNilResponsePassed = fmt.Errorf("passed nil response") - ErrNilResponseData = fmt.Errorf("response Data is nil") - ErrWrongDataFormat = fmt.Errorf("response Data format is wrong") - ) - testConn := &Connection{} - testCases := []struct { - name string - resp *Response - expectedError error - }{ - {"ErrNilResponsePassed", nil, ErrNilResponsePassed}, - {"ErrNilResponseData", &Response{Data: nil}, ErrNilResponseData}, - {"ErrWrongDataFormat", &Response{Data: []interface{}{}}, ErrWrongDataFormat}, - {"ErrWrongDataFormat", &Response{Data: []interface{}{"test"}}, ErrWrongDataFormat}, - {"nil", &Response{Data: []interface{}{&Prepared{}}}, nil}, - } - for _, testCase := range testCases { - t.Run("Expecting error "+testCase.name, func(t *testing.T) { - _, err := NewPreparedFromResponse(testConn, testCase.resp) - assert.Equal(t, err, testCase.expectedError) - }) + if data[0].([]interface{})[1] != "Tarantool" { + t.Errorf("wrong Select body: %v", data) } } @@ -2075,7 +2043,7 @@ func TestSchema_IsNullable(t *testing.T) { } func TestClientNamed(t *testing.T) { - var resp *Response + var resp Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) @@ -2086,8 +2054,8 @@ func TestClientNamed(t *testing.T) { if err != nil { t.Fatalf("Failed to Insert: %s", err.Error()) } - if resp.Code != 0 { - t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) + if resp == nil { + t.Errorf("Response is nil after Insert") } // Delete @@ -2145,8 +2113,8 @@ func TestClientNamed(t *testing.T) { if err != nil { t.Fatalf("Failed to Replace: %s", err.Error()) } - if resp.Code != 0 { - t.Errorf("Failed to Replace: wrong code returned %d", resp.Code) + if resp == nil { + t.Errorf("Response is nil after Replace") } } resp, err = conn.Select(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)}) @@ -2170,9 +2138,8 @@ func TestClientNamed(t *testing.T) { func TestClientRequestObjects(t *testing.T) { var ( - req Request - resp *Response - err error + req Request + err error ) conn := test_helpers.ConnectWithValidation(t, dialer, opts) @@ -2180,14 +2147,11 @@ func TestClientRequestObjects(t *testing.T) { // Ping req = NewPingRequest() - resp, err = conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Ping: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Ping") - } - if len(resp.Data) != 0 { + if len(data) != 0 { t.Errorf("Response Body len != 0") } @@ -2200,23 +2164,14 @@ func TestClientRequestObjects(t *testing.T) { for i := 1010; i < 1020; i++ { req = NewInsertRequest(spaceName). Tuple([]interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) - resp, err = conn.Do(req).Get() + data, err = conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Insert: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Insert") - } - if resp.Pos != nil { - t.Errorf("Response should not have a position") - } - if resp.Data == nil { - t.Fatalf("Response data is nil after Insert") - } - if len(resp.Data) != 1 { + if len(data) != 1 { t.Fatalf("Response Body len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Insert") } else { if len(tpl) != 3 { @@ -2238,23 +2193,14 @@ func TestClientRequestObjects(t *testing.T) { for i := 1015; i < 1020; i++ { req = NewReplaceRequest(spaceName). Tuple([]interface{}{uint(i), fmt.Sprintf("val %d", i), "blar"}) - resp, err = conn.Do(req).Get() + data, err = conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Replace: %s", err.Error()) + t.Fatalf("Failed to Decode: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Replace") - } - if resp.Pos != nil { - t.Errorf("Response should not have a position") - } - if resp.Data == nil { - t.Fatalf("Response data is nil after Replace") - } - if len(resp.Data) != 1 { + if len(data) != 1 { t.Fatalf("Response Body len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Replace") } else { if len(tpl) != 3 { @@ -2275,23 +2221,17 @@ func TestClientRequestObjects(t *testing.T) { // Delete req = NewDeleteRequest(spaceName). Key([]interface{}{uint(1016)}) - resp, err = conn.Do(req).Get() + data, err = conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Delete: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Delete") - } - if resp.Pos != nil { - t.Errorf("Response should not have a position") - } - if resp.Data == nil { + if data == nil { t.Fatalf("Response data is nil after Delete") } - if len(resp.Data) != 1 { + if len(data) != 1 { t.Fatalf("Response Body len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Delete") } else { if len(tpl) != 3 { @@ -2312,23 +2252,17 @@ func TestClientRequestObjects(t *testing.T) { req = NewUpdateRequest(spaceName). Index(indexName). Key([]interface{}{uint(1010)}) - resp, err = conn.Do(req).Get() + data, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to Update: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Update") - } - if resp.Pos != nil { - t.Errorf("Response should not have a position") - } - if resp.Data == nil { + if data == nil { t.Fatalf("Response data is nil after Update") } - if len(resp.Data) != 1 { + if len(data) != 1 { t.Fatalf("Response Data len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Update") } else { if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != uint64(1010) { @@ -2347,23 +2281,14 @@ func TestClientRequestObjects(t *testing.T) { Index(indexName). Key([]interface{}{uint(1010)}). Operations(NewOperations().Assign(1, "bye").Insert(2, 1)) - resp, err = conn.Do(req).Get() + data, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to Update: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Update") - } - if resp.Pos != nil { - t.Errorf("Response should not have a position") - } - if resp.Data == nil { - t.Fatalf("Response data is nil after Update") - } - if len(resp.Data) != 1 { + if len(data) != 1 { t.Fatalf("Response Data len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1010 { @@ -2380,20 +2305,11 @@ func TestClientRequestObjects(t *testing.T) { // Upsert without operations. req = NewUpsertRequest(spaceNo). Tuple([]interface{}{uint(1010), "hi", "hi"}) - resp, err = conn.Do(req).Get() + data, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to Upsert (update): %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Upsert (update)") - } - if resp.Pos != nil { - t.Errorf("Response should not have a position") - } - if resp.Data == nil { - t.Fatalf("Response data is nil after Upsert") - } - if len(resp.Data) != 0 { + if len(data) != 0 { t.Fatalf("Response Data len != 0") } @@ -2401,65 +2317,44 @@ func TestClientRequestObjects(t *testing.T) { req = NewUpsertRequest(spaceNo). Tuple([]interface{}{uint(1010), "hi", "hi"}). Operations(NewOperations().Assign(2, "bye")) - resp, err = conn.Do(req).Get() + data, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to Upsert (update): %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Upsert (update)") - } - if resp.Pos != nil { - t.Errorf("Response should not have a position") - } - if resp.Data == nil { - t.Fatalf("Response data is nil after Upsert") - } - if len(resp.Data) != 0 { + if len(data) != 0 { t.Fatalf("Response Data len != 0") } // Call16 vs Call17 req = NewCall16Request("simple_concat").Args([]interface{}{"1"}) - resp, err = conn.Do(req).Get() + data, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to use Call") } - if resp.Pos != nil { - t.Errorf("Response should not have a position") - } - if val, ok := resp.Data[0].([]interface{})[0].(string); !ok || val != "11" { - t.Errorf("result is not {{1}} : %v", resp.Data) + if val, ok := data[0].([]interface{})[0].(string); !ok || val != "11" { + t.Errorf("result is not {{1}} : %v", data) } // Call17 req = NewCall17Request("simple_concat").Args([]interface{}{"1"}) - resp, err = conn.Do(req).Get() + data, err = conn.Do(req).Get() if err != nil { t.Errorf("Failed to use Call17") } - if resp.Pos != nil { - t.Errorf("Response should not have a position") - } - if val, ok := resp.Data[0].(string); !ok || val != "11" { - t.Errorf("result is not {{1}} : %v", resp.Data) + if val, ok := data[0].(string); !ok || val != "11" { + t.Errorf("result is not {{1}} : %v", data) } // Eval req = NewEvalRequest("return 5 + 6") - resp, err = conn.Do(req).Get() + data, err = conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Eval: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Eval") - } - if resp.Pos != nil { - t.Errorf("Response should not have a position") - } - if len(resp.Data) < 1 { + if len(data) < 1 { t.Errorf("Response.Data is empty after Eval") } - if val, err := test_helpers.ConvertUint64(resp.Data[0]); err != nil || val != 11 { + if val, err := test_helpers.ConvertUint64(data[0]); err != nil || val != 11 { t.Errorf("5 + 6 == 11, but got %v", val) } @@ -2473,45 +2368,47 @@ func TestClientRequestObjects(t *testing.T) { } req = NewExecuteRequest(createTableQuery) - resp, err = conn.Do(req).Get() + resp, err := conn.Do(req).GetResponse() if err != nil { t.Fatalf("Failed to Execute: %s", err.Error()) } if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if len(resp.Data) != 0 { - t.Fatalf("Response Body len != 0") + data, err = resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) } - if resp.Code != OkCode { - t.Fatalf("Failed to Execute: %d", resp.Code) + if len(data) != 0 { + t.Fatalf("Response Body len != 0") } - if resp.SQLInfo.AffectedCount != 1 { - t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + if resp.SQLInfo().AffectedCount != 1 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo().AffectedCount) } req = NewExecuteRequest(dropQuery2) - resp, err = conn.Do(req).Get() + resp, err = conn.Do(req).GetResponse() if err != nil { t.Fatalf("Failed to Execute: %s", err.Error()) } if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Pos != nil { + if resp.Pos() != nil { t.Errorf("Response should not have a position") } - if len(resp.Data) != 0 { - t.Fatalf("Response Body len != 0") + data, err = resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) } - if resp.Code != OkCode { - t.Fatalf("Failed to Execute: %d", resp.Code) + if len(data) != 0 { + t.Fatalf("Response Body len != 0") } - if resp.SQLInfo.AffectedCount != 1 { - t.Errorf("Incorrect count of dropped spaces: %d", resp.SQLInfo.AffectedCount) + if resp.SQLInfo().AffectedCount != 1 { + t.Errorf("Incorrect count of dropped spaces: %d", resp.SQLInfo().AffectedCount) } } @@ -2528,7 +2425,7 @@ func testConnectionDoSelectRequestPrepare(t *testing.T, conn Connector) { } func testConnectionDoSelectRequestCheck(t *testing.T, - resp *Response, err error, pos bool, dataLen int, firstKey uint64) { + resp Response, err error, pos bool, dataLen int, firstKey uint64) { t.Helper() if err != nil { @@ -2537,18 +2434,22 @@ func testConnectionDoSelectRequestCheck(t *testing.T, if resp == nil { t.Fatalf("Response is nil after Select") } - if !pos && resp.Pos != nil { + if !pos && resp.Pos() != nil { t.Errorf("Response should not have a position descriptor") } - if pos && resp.Pos == nil { + if pos && resp.Pos() == nil { t.Fatalf("A response must have a position descriptor") } - if len(resp.Data) != dataLen { - t.Fatalf("Response Data len %d != %d", len(resp.Data), dataLen) + data, err := resp.Decode() + if err != nil { + t.Fatalf("Failed to Decode: %s", err.Error()) + } + if len(data) != dataLen { + t.Fatalf("Response Data len %d != %d", len(data), dataLen) } for i := 0; i < dataLen; i++ { key := firstKey + uint64(i) - if tpl, ok := resp.Data[i].([]interface{}); !ok { + if tpl, ok := data[i].([]interface{}); !ok { t.Errorf("Unexpected body of Select") } else { if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != key { @@ -2579,7 +2480,7 @@ func TestConnectionDoSelectRequest(t *testing.T) { Limit(20). Iterator(IterGe). Key([]interface{}{uint(1010)}) - resp, err := conn.Do(req).Get() + resp, err := conn.Do(req).GetResponse() testConnectionDoSelectRequestCheck(t, resp, err, false, 10, 1010) } @@ -2595,15 +2496,12 @@ func TestConnectionDoWatchOnceRequest(t *testing.T) { t.Fatalf("Failed to create a broadcast : %s", err.Error()) } - resp, err := conn.Do(NewWatchOnceRequest("hello")).Get() + data, err := conn.Do(NewWatchOnceRequest("hello")).Get() if err != nil { t.Fatalf("Failed to WatchOnce: %s", err.Error()) } - if resp.Code != OkCode { - t.Errorf("Failed to WatchOnce: wrong code returned %d", resp.Code) - } - if len(resp.Data) < 1 || resp.Data[0] != "world" { - t.Errorf("Failed to WatchOnce: wrong value returned %v", resp.Data) + if len(data) < 1 || data[0] != "world" { + t.Errorf("Failed to WatchOnce: wrong value returned %v", data) } } @@ -2619,15 +2517,12 @@ func TestConnectionDoWatchOnceOnEmptyKey(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() - resp, err := conn.Do(NewWatchOnceRequest("notexists!")).Get() + data, err := conn.Do(NewWatchOnceRequest("notexists!")).Get() if err != nil { t.Fatalf("Failed to WatchOnce: %s", err.Error()) } - if resp.Code != OkCode { - t.Errorf("Failed to WatchOnce: wrong code returned %d", resp.Code) - } - if len(resp.Data) > 0 { - t.Errorf("Failed to WatchOnce: wrong value returned %v", resp.Data) + if len(data) > 0 { + t.Errorf("Failed to WatchOnce: wrong value returned %v", data) } } @@ -2645,7 +2540,7 @@ func TestConnectionDoSelectRequest_fetch_pos(t *testing.T) { Iterator(IterGe). FetchPos(true). Key([]interface{}{uint(1010)}) - resp, err := conn.Do(req).Get() + resp, err := conn.Do(req).GetResponse() testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1010) } @@ -2665,7 +2560,7 @@ func TestConnectDoSelectRequest_after_tuple(t *testing.T) { FetchPos(true). Key([]interface{}{uint(1010)}). After([]interface{}{uint(1012)}) - resp, err := conn.Do(req).Get() + resp, err := conn.Do(req).GetResponse() testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1013) } @@ -2684,17 +2579,17 @@ func TestConnectionDoSelectRequest_pagination_pos(t *testing.T) { Iterator(IterGe). FetchPos(true). Key([]interface{}{uint(1010)}) - resp, err := conn.Do(req).Get() + resp, err := conn.Do(req).GetResponse() testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1010) - resp, err = conn.Do(req.After(resp.Pos)).Get() + resp, err = conn.Do(req.After(resp.Pos())).GetResponse() testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1012) } func TestConnection_Call(t *testing.T) { - var resp *Response + var resp Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) @@ -2704,25 +2599,28 @@ func TestConnection_Call(t *testing.T) { if err != nil { t.Errorf("Failed to use Call") } - if val, ok := resp.Data[0].(string); !ok || val != "11" { - t.Errorf("result is not {{1}} : %v", resp.Data) + data, err := resp.Decode() + if err != nil { + t.Errorf("Failed to Decode: %s", err.Error()) + } + if val, ok := data[0].(string); !ok || val != "11" { + t.Errorf("result is not {{1}} : %v", data) } } func TestCallRequest(t *testing.T) { - var resp *Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() req := NewCallRequest("simple_concat").Args([]interface{}{"1"}) - resp, err = conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { t.Errorf("Failed to use Call") } - if val, ok := resp.Data[0].(string); !ok || val != "11" { - t.Errorf("result is not {{1}} : %v", resp.Data) + if val, ok := data[0].(string); !ok || val != "11" { + t.Errorf("result is not {{1}} : %v", data) } } @@ -2730,14 +2628,11 @@ func TestClientRequestObjectsWithNilContext(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() req := NewPingRequest().Context(nil) //nolint - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Ping: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Ping") - } - if len(resp.Data) != 0 { + if len(data) != 0 { t.Errorf("Response Body len != 0") } } @@ -2886,7 +2781,6 @@ func TestStream_IdValues(t *testing.T) { func TestStream_Commit(t *testing.T) { var req Request - var resp *Response var err error var conn *Connection @@ -2899,24 +2793,18 @@ func TestStream_Commit(t *testing.T) { // Begin transaction req = NewBeginRequest() - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() if err != nil { t.Fatalf("Failed to Begin: %s", err.Error()) } - if resp.Code != OkCode { - t.Fatalf("Failed to Begin: wrong code returned %d", resp.Code) - } // Insert in stream req = NewInsertRequest(spaceName). Tuple([]interface{}{uint(1001), "hello2", "world2"}) - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() if err != nil { t.Fatalf("Failed to Insert: %s", err.Error()) } - if resp.Code != OkCode { - t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) - } defer test_helpers.DeleteRecordByKey(t, conn, spaceNo, indexNo, []interface{}{uint(1001)}) // Select not related to the transaction @@ -2927,29 +2815,23 @@ func TestStream_Commit(t *testing.T) { Limit(1). Iterator(IterEq). Key([]interface{}{uint(1001)}) - resp, err = conn.Do(selectReq).Get() + data, err := conn.Do(selectReq).Get() if err != nil { t.Fatalf("Failed to Select: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != 0 { + if len(data) != 0 { t.Fatalf("Response Data len != 0") } // Select in stream - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() if err != nil { t.Fatalf("Failed to Select: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != 1 { + if len(data) != 1 { t.Fatalf("Response Data len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1001 { @@ -2965,26 +2847,20 @@ func TestStream_Commit(t *testing.T) { // Commit transaction req = NewCommitRequest() - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() if err != nil { t.Fatalf("Failed to Commit: %s", err.Error()) } - if resp.Code != OkCode { - t.Fatalf("Failed to Commit: wrong code returned %d", resp.Code) - } // Select outside of transaction - resp, err = conn.Do(selectReq).Get() + data, err = conn.Do(selectReq).Get() if err != nil { t.Fatalf("Failed to Select: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != 1 { + if len(data) != 1 { t.Fatalf("Response Data len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1001 { @@ -3001,7 +2877,6 @@ func TestStream_Commit(t *testing.T) { func TestStream_Rollback(t *testing.T) { var req Request - var resp *Response var err error var conn *Connection @@ -3014,24 +2889,18 @@ func TestStream_Rollback(t *testing.T) { // Begin transaction req = NewBeginRequest() - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() if err != nil { t.Fatalf("Failed to Begin: %s", err.Error()) } - if resp.Code != OkCode { - t.Fatalf("Failed to Begin: wrong code returned %d", resp.Code) - } // Insert in stream req = NewInsertRequest(spaceName). Tuple([]interface{}{uint(1001), "hello2", "world2"}) - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() if err != nil { t.Fatalf("Failed to Insert: %s", err.Error()) } - if resp.Code != OkCode { - t.Errorf("Failed to Insert: wrong code returned %d", resp.Code) - } defer test_helpers.DeleteRecordByKey(t, conn, spaceNo, indexNo, []interface{}{uint(1001)}) // Select not related to the transaction @@ -3042,29 +2911,23 @@ func TestStream_Rollback(t *testing.T) { Limit(1). Iterator(IterEq). Key([]interface{}{uint(1001)}) - resp, err = conn.Do(selectReq).Get() + data, err := conn.Do(selectReq).Get() if err != nil { t.Fatalf("Failed to Select: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != 0 { + if len(data) != 0 { t.Fatalf("Response Data len != 0") } // Select in stream - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() if err != nil { t.Fatalf("Failed to Select: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != 1 { + if len(data) != 1 { t.Fatalf("Response Data len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { t.Fatalf("Unexpected body of Select") } else { if id, err := test_helpers.ConvertUint64(tpl[0]); err != nil || id != 1001 { @@ -3080,42 +2943,32 @@ func TestStream_Rollback(t *testing.T) { // Rollback transaction req = NewRollbackRequest() - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() if err != nil { t.Fatalf("Failed to Rollback: %s", err.Error()) } - if resp.Code != OkCode { - t.Fatalf("Failed to Rollback: wrong code returned %d", resp.Code) - } // Select outside of transaction - resp, err = conn.Do(selectReq).Get() + data, err = conn.Do(selectReq).Get() if err != nil { t.Fatalf("Failed to Select: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != 0 { + if len(data) != 0 { t.Fatalf("Response Data len != 0") } // Select inside of stream after rollback - resp, err = stream.Do(selectReq).Get() + _, err = stream.Do(selectReq).Get() if err != nil { t.Fatalf("Failed to Select: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if len(resp.Data) != 0 { + if len(data) != 0 { t.Fatalf("Response Data len != 0") } } func TestStream_TxnIsolationLevel(t *testing.T) { var req Request - var resp *Response var err error var conn *Connection @@ -3136,18 +2989,14 @@ func TestStream_TxnIsolationLevel(t *testing.T) { for _, level := range txnIsolationLevels { // Begin transaction req = NewBeginRequest().TxnIsolation(level).Timeout(500 * time.Millisecond) - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() require.Nilf(t, err, "failed to Begin") - require.NotNilf(t, resp, "response is nil after Begin") - require.Equalf(t, OkCode, resp.Code, "wrong code returned") // Insert in stream req = NewInsertRequest(spaceName). Tuple([]interface{}{uint(1001), "hello2", "world2"}) - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() require.Nilf(t, err, "failed to Insert") - require.NotNilf(t, resp, "response is nil after Insert") - require.Equalf(t, OkCode, resp.Code, "wrong code returned") // Select not related to the transaction // while transaction is not committed @@ -3157,18 +3006,16 @@ func TestStream_TxnIsolationLevel(t *testing.T) { Limit(1). Iterator(IterEq). Key([]interface{}{uint(1001)}) - resp, err = conn.Do(selectReq).Get() + data, err := conn.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + require.Equalf(t, 0, len(data), "response Data len != 0") // Select in stream - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 1, len(resp.Data), "response Body len != 1 after Select") + require.Equalf(t, 1, len(data), "response Body len != 1 after Select") - tpl, ok := resp.Data[0].([]interface{}) + tpl, ok := data[0].([]interface{}) require.Truef(t, ok, "unexpected body of Select") require.Equalf(t, 3, len(tpl), "unexpected body of Select") @@ -3186,22 +3033,18 @@ func TestStream_TxnIsolationLevel(t *testing.T) { // Rollback transaction req = NewRollbackRequest() - resp, err = stream.Do(req).Get() + _, err = stream.Do(req).Get() require.Nilf(t, err, "failed to Rollback") - require.NotNilf(t, resp, "response is nil after Rollback") - require.Equalf(t, OkCode, resp.Code, "wrong code returned") // Select outside of transaction - resp, err = conn.Do(selectReq).Get() + data, err = conn.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + require.Equalf(t, 0, len(data), "response Data len != 0") // Select inside of stream after rollback - resp, err = stream.Do(selectReq).Get() + data, err = stream.Do(selectReq).Get() require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - require.Equalf(t, 0, len(resp.Data), "response Data len != 0") + require.Equalf(t, 0, len(data), "response Data len != 0") test_helpers.DeleteRecordByKey(t, conn, spaceNo, indexNo, []interface{}{uint(1001)}) } @@ -3298,13 +3141,12 @@ func TestClientIdRequestObject(t *testing.T) { Version: ProtocolVersion(1), Features: []iproto.Feature{iproto.IPROTO_FEATURE_STREAMS}, }) - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() require.Nilf(t, err, "No errors on Id request execution") - require.NotNilf(t, resp, "Response not empty") - require.NotNilf(t, resp.Data, "Response data not empty") - require.Equal(t, len(resp.Data), 1, "Response data contains exactly one object") + require.NotNilf(t, data, "Response data not empty") + require.Equal(t, len(data), 1, "Response data contains exactly one object") - serverProtocolInfo, ok := resp.Data[0].(ProtocolInfo) + serverProtocolInfo, ok := data[0].(ProtocolInfo) require.Truef(t, ok, "Response Data object is an ProtocolInfo object") require.GreaterOrEqual(t, serverProtocolInfo.Version, @@ -3334,13 +3176,12 @@ func TestClientIdRequestObjectWithNilContext(t *testing.T) { Version: ProtocolVersion(1), Features: []iproto.Feature{iproto.IPROTO_FEATURE_STREAMS}, }).Context(nil) //nolint - resp, err := conn.Do(req).Get() + data, err := conn.Do(req).Get() require.Nilf(t, err, "No errors on Id request execution") - require.NotNilf(t, resp, "Response not empty") - require.NotNilf(t, resp.Data, "Response data not empty") - require.Equal(t, len(resp.Data), 1, "Response data contains exactly one object") + require.NotNilf(t, data, "Response data not empty") + require.Equal(t, len(data), 1, "Response data contains exactly one object") - serverProtocolInfo, ok := resp.Data[0].(ProtocolInfo) + serverProtocolInfo, ok := data[0].(ProtocolInfo) require.Truef(t, ok, "Response Data object is an ProtocolInfo object") require.GreaterOrEqual(t, serverProtocolInfo.Version, @@ -3697,15 +3538,12 @@ func TestBroadcastRequest(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() - resp, err := conn.Do(NewBroadcastRequest(key).Value(value)).Get() + data, err := conn.Do(NewBroadcastRequest(key).Value(value)).Get() if err != nil { t.Fatalf("Got broadcast error: %s", err) } - if resp.Code != OkCode { - t.Errorf("Got unexpected broadcast response code: %d", resp.Code) - } - if !reflect.DeepEqual(resp.Data, []interface{}{}) { - t.Errorf("Got unexpected broadcast response data: %v", resp.Data) + if !reflect.DeepEqual(data, []interface{}{}) { + t.Errorf("Got unexpected broadcast response data: %v", data) } events := make(chan WatchEvent) diff --git a/test_helpers/main.go b/test_helpers/main.go index d81ce3d1b..0a0c7f1cd 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -83,7 +83,7 @@ type TarantoolInstance struct { func isReady(dialer tarantool.Dialer, opts *tarantool.Opts) error { var err error var conn *tarantool.Connection - var resp *tarantool.Response + var resp tarantool.Response ctx, cancel := GetConnectContext() defer cancel() @@ -96,7 +96,7 @@ func isReady(dialer tarantool.Dialer, opts *tarantool.Opts) error { } defer conn.Close() - resp, err = conn.Do(tarantool.NewPingRequest()).Get() + resp, err = conn.Do(tarantool.NewPingRequest()).GetResponse() if err != nil { return err } diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index fb59418d5..729a69e44 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -84,18 +84,15 @@ func ProcessListenOnInstance(args interface{}) error { for i := 0; i < listenArgs.ServersNumber; i++ { req := tarantool.NewEvalRequest("return box.cfg.listen") - resp, err := listenArgs.ConnPool.Do(req, listenArgs.Mode).Get() + data, err := listenArgs.ConnPool.Do(req, listenArgs.Mode).Get() if err != nil { return fmt.Errorf("fail to Eval: %s", err.Error()) } - if resp == nil { - return fmt.Errorf("response is nil after Eval") - } - if len(resp.Data) < 1 { + if len(data) < 1 { return fmt.Errorf("response.Data is empty after Eval") } - port, ok := resp.Data[0].(string) + port, ok := data[0].(string) if !ok { return fmt.Errorf("response.Data is incorrect after Eval") } @@ -142,17 +139,14 @@ func InsertOnInstance(ctx context.Context, dialer tarantool.Dialer, connOpts tar } defer conn.Close() - resp, err := conn.Do(tarantool.NewInsertRequest(space).Tuple(tuple)).Get() + data, err := conn.Do(tarantool.NewInsertRequest(space).Tuple(tuple)).Get() if err != nil { return fmt.Errorf("failed to Insert: %s", err.Error()) } - if resp == nil { - return fmt.Errorf("response is nil after Insert") - } - if len(resp.Data) != 1 { + if len(data) != 1 { return fmt.Errorf("response Body len != 1") } - if tpl, ok := resp.Data[0].([]interface{}); !ok { + if tpl, ok := data[0].([]interface{}); !ok { return fmt.Errorf("unexpected body of Insert") } else { expectedTpl, ok := tuple.([]interface{}) diff --git a/uuid/example_test.go b/uuid/example_test.go index ba90ea905..c79dc35be 100644 --- a/uuid/example_test.go +++ b/uuid/example_test.go @@ -46,12 +46,11 @@ func Example() { log.Fatalf("Failed to prepare uuid: %s", uuidErr) } - resp, err := client.Do(tarantool.NewReplaceRequest(spaceNo). + data, err := client.Do(tarantool.NewReplaceRequest(spaceNo). Tuple([]interface{}{id}), ).Get() fmt.Println("UUID tuple replace") fmt.Println("Error", err) - fmt.Println("Code", resp.Code) - fmt.Println("Data", resp.Data) + fmt.Println("Data", data) } diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index fdbf0cd82..0ce317979 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -89,14 +89,11 @@ func TestSelect(t *testing.T) { Limit(1). Iterator(IterEq). Key([]interface{}{id}) - resp, errSel := conn.Do(sel).Get() + data, errSel := conn.Do(sel).Get() if errSel != nil { t.Fatalf("UUID select failed: %s", errSel.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - tupleValueIsId(t, resp.Data, id) + tupleValueIsId(t, data, id) var tuples []TupleUUID errTyp := conn.Do(sel).GetTyped(&tuples) @@ -125,28 +122,22 @@ func TestReplace(t *testing.T) { } rep := NewReplaceRequest(space).Tuple([]interface{}{id}) - respRep, errRep := conn.Do(rep).Get() + dataRep, errRep := conn.Do(rep).Get() if errRep != nil { t.Errorf("UUID replace failed: %s", errRep) } - if respRep == nil { - t.Fatalf("Response is nil after Replace") - } - tupleValueIsId(t, respRep.Data, id) + tupleValueIsId(t, dataRep, id) sel := NewSelectRequest(space). Index(index). Limit(1). Iterator(IterEq). Key([]interface{}{id}) - respSel, errSel := conn.Do(sel).Get() + dataSel, errSel := conn.Do(sel).Get() if errSel != nil { t.Errorf("UUID select failed: %s", errSel) } - if respSel == nil { - t.Fatalf("Response is nil after Select") - } - tupleValueIsId(t, respSel.Data, id) + tupleValueIsId(t, dataSel, id) } // runTestMain is a body of TestMain function From c59e514d405bbb2157920d9da01c8519f79354e6 Mon Sep 17 00:00:00 2001 From: DerekBum Date: Thu, 14 Dec 2023 18:35:00 +0300 Subject: [PATCH 518/605] api: create different responses for different requests Different implementations of the `Response` interface created. Special types of responses are used with special requests. Thus `Response` interface was simplified: some special methods were moved to the corresponding implementations. This means that if a user wants to access this methods, the response should be casted to its actual type. `SelectResponse`, `ExecuteResponse`, `PrepareResponse`, `PushResponse` are part of a public API. `Pos()`, `MetaData()`, `SQLInfo()` methods created for them to get specific info. `Future` constructors now accept `Request` as their argument. `Future` methods `AppendPush` and `SetResponse` accept response `Header` and data as their arguments. `IsPush()` method is used to return the information if the current response is a `PushResponse`. `PushCode` constant is removed. To get information, if the current response is a push response, `IsPush()` method could be used instead. After this patch, operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` return response data instead of an actual responses. After this patch, operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Pooler` return response data instead of an actual responses. Part of #237 --- CHANGELOG.md | 16 +- README.md | 18 ++ box_error_test.go | 9 +- connection.go | 33 +- connector.go | 24 +- crud/common.go | 7 + dial.go | 17 +- example_test.go | 101 +++++- future.go | 45 ++- future_test.go | 61 ++-- pool/connection_pool.go | 24 +- pool/connection_pool_test.go | 189 +++++------- pool/connector.go | 24 +- pool/connector_test.go | 62 ++-- pool/pooler.go | 24 +- prepared.go | 19 ++ request.go | 101 ++++-- response.go | 575 ++++++++++++++++++++++++++--------- settings/example_test.go | 28 +- settings/request.go | 13 + settings/tarantool_test.go | 114 +++++-- tarantool_test.go | 506 +++++++++++++++--------------- test_helpers/main.go | 6 +- test_helpers/request_mock.go | 8 + watch.go | 10 + 25 files changed, 1326 insertions(+), 708 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b362a4d9..e15812510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Support connection via an existing socket fd (#321) - `Header` struct for the response header (#237). It can be accessed via `Header()` method of the `Response` interface. +- `Response` method added to the `Request` interface (#237) +- New `LogAppendPushFailed` connection log constant (#237). + It is logged when connection fails to append a push response. ### Changed @@ -70,12 +73,21 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Rename `pool.GetPoolInfo` to `pool.GetInfo`. Change return type to `map[string]ConnectionInfo` (#321) - `Response` is now an interface (#237) -- All responses are now implementations of the `Response` interface (#237) +- All responses are now implementations of the `Response` interface (#237). + `SelectResponse`, `ExecuteResponse`, `PrepareResponse`, `PushResponse` are part + of a public API. `Pos()`, `MetaData()`, `SQLInfo()` methods created for them + to get specific info. + Special types of responses are used with special requests. - `IsPush()` method is added to the response iterator (#237). It returns the information if the current response is a `PushResponse`. `PushCode` constant is removed. - Method `Get` for `Future` now returns response data (#237). To get the actual - response new `GetResponse` method has been added. + response new `GetResponse` method has been added. Methods `AppendPush` and + `SetResponse` accept response `Header` and data as their arguments. +- `Future` constructors now accept `Request` as their argument (#237) +- Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, + `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` and `Pooler` + return response data instead of an actual responses (#237) ### Deprecated diff --git a/README.md b/README.md index a08ce1794..4f82896de 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,9 @@ The subpackage has been deleted. You could use `pool` instead. unique string ID, which allows them to be distinguished. * `pool.GetPoolInfo` has been renamed to `pool.GetInfo`. Return type has been changed to `map[string]ConnectionInfo`. +* Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, + `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Pooler` return + response data instead of an actual responses. #### crud package @@ -257,6 +260,7 @@ longer accept `ops` argument (operations) as an `interface{}`. `*Operations` needs to be passed instead. * `UpdateRequest` and `UpsertRequest` structs no longer accept `interface{}` for an `ops` field. `*Operations` needs to be used instead. +* `Response` method added to the `Request` interface. #### Response changes @@ -265,11 +269,25 @@ for an `ops` field. `*Operations` needs to be used instead. `Header()` method. * `ResponseIterator` interface now has `IsPush()` method. It returns true if the current response is a push response. +* For each request type, a different response type is created. They all + implement a `Response` interface. `SelectResponse`, `PrepareResponse`, + `ExecuteResponse`, `PushResponse` are a part of a public API. + `Pos()`, `MetaData()`, `SQLInfo()` methods created for them to get specific info. + Special types of responses are used with special requests. #### Future changes * Method `Get` now returns response data instead of the actual response. * New method `GetResponse` added to get an actual response. +* `Future` constructors now accept `Request` as their argument. +* Methods `AppendPush` and `SetResponse` accepts response `Header` and data + as their arguments. + +#### Connector changes + +Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, +`Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` return +response data instead of an actual responses. #### Connect function diff --git a/box_error_test.go b/box_error_test.go index 6839a1477..c48303bba 100644 --- a/box_error_test.go +++ b/box_error_test.go @@ -304,9 +304,7 @@ func TestErrorTypeEval(t *testing.T) { for name, testcase := range tupleCases { t.Run(name, func(t *testing.T) { - resp, err := conn.Eval("return ...", []interface{}{&testcase.tuple.val}) - require.Nil(t, err) - data, err := resp.Decode() + data, err := conn.Eval("return ...", []interface{}{&testcase.tuple.val}) require.Nil(t, err) require.NotNil(t, data) require.Equal(t, len(data), 1) @@ -438,14 +436,11 @@ func TestErrorTypeSelect(t *testing.T) { _, err := conn.Eval(insertEval, []interface{}{}) require.Nilf(t, err, "Tuple has been successfully inserted") - var resp Response var offset uint32 = 0 var limit uint32 = 1 - resp, err = conn.Select(space, index, offset, limit, IterEq, + data, err := conn.Select(space, index, offset, limit, IterEq, []interface{}{testcase.tuple.pk}) require.Nil(t, err) - data, err := resp.Decode() - require.Nil(t, err) require.NotNil(t, data) require.Equalf(t, len(data), 1, "Exactly one tuple had been found") tpl, ok := data[0].([]interface{}) diff --git a/connection.go b/connection.go index 1dab74187..8e3939190 100644 --- a/connection.go +++ b/connection.go @@ -61,6 +61,8 @@ const ( LogUnexpectedResultId // LogWatchEventReadFailed is logged when failed to read a watch event. LogWatchEventReadFailed + // LogAppendPushFailed is logged when failed to append a push response. + LogAppendPushFailed ) // ConnEvent is sent throw Notify channel specified in Opts. @@ -103,6 +105,9 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac case LogWatchEventReadFailed: err := v[0].(error) log.Printf("tarantool: unable to parse watch event: %s", err) + case LogAppendPushFailed: + err := v[0].(error) + log.Printf("tarantool: unable to append a push response: %s", err) default: args := append([]interface{}{"tarantool: unexpected event ", event, conn}, v...) log.Print(args...) @@ -818,10 +823,9 @@ func (conn *Connection) reader(r io.Reader, c Conn) { return } - resp := &ConnResponse{header: header, buf: buf} var fut *Future = nil if iproto.Type(header.Code) == iproto.IPROTO_EVENT { - if event, err := readWatchEvent(&resp.buf); err == nil { + if event, err := readWatchEvent(&buf); err == nil { events <- event } else { err = ClientError{ @@ -833,11 +837,19 @@ func (conn *Connection) reader(r io.Reader, c Conn) { continue } else if header.Code == uint32(iproto.IPROTO_CHUNK) { if fut = conn.peekFuture(header.RequestId); fut != nil { - fut.AppendPush(resp) + if err := fut.AppendPush(header, &buf); err != nil { + err = ClientError{ + ErrProtocolError, + fmt.Sprintf("failed to append push response: %s", err), + } + conn.opts.Logger.Report(LogAppendPushFailed, conn, err) + } } } else { if fut = conn.fetchFuture(header.RequestId); fut != nil { - fut.SetResponse(resp) + if err := fut.SetResponse(header, &buf); err != nil { + fut.SetError(fmt.Errorf("failed to set response: %w", err)) + } conn.markDone(fut) } } @@ -873,8 +885,10 @@ func (conn *Connection) eventer(events <-chan connWatchEvent) { } } -func (conn *Connection) newFuture(ctx context.Context) (fut *Future) { +func (conn *Connection) newFuture(req Request) (fut *Future) { + ctx := req.Ctx() fut = NewFuture() + fut.SetRequest(req) if conn.rlimit != nil && conn.opts.RLimitAction == RLimitDrop { select { case conn.rlimit <- struct{}{}: @@ -984,7 +998,7 @@ func (conn *Connection) decrementRequestCnt() { func (conn *Connection) send(req Request, streamId uint64) *Future { conn.incrementRequestCnt() - fut := conn.newFuture(req.Ctx()) + fut := conn.newFuture(req) if fut.ready == nil { conn.decrementRequestCnt() return fut @@ -1053,8 +1067,11 @@ func (conn *Connection) putFuture(fut *Future, req Request, streamId uint64) { if req.Async() { if fut = conn.fetchFuture(reqid); fut != nil { - resp := &ConnResponse{} - fut.SetResponse(resp) + header := Header{ + RequestId: reqid, + Code: OkCode, + } + fut.SetResponse(header, nil) conn.markDone(fut) } } diff --git a/connector.go b/connector.go index b7f5affed..72b5d19a8 100644 --- a/connector.go +++ b/connector.go @@ -13,41 +13,41 @@ type Connector interface { // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. - Ping() (Response, error) + Ping() ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. Select(space, index interface{}, offset, limit uint32, iterator Iter, - key interface{}) (Response, error) + key interface{}) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. - Insert(space interface{}, tuple interface{}) (Response, error) + Insert(space interface{}, tuple interface{}) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a ReplicaRequest object + Do() instead. - Replace(space interface{}, tuple interface{}) (Response, error) + Replace(space interface{}, tuple interface{}) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. - Delete(space, index interface{}, key interface{}) (Response, error) + Delete(space, index interface{}, key interface{}) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. - Update(space, index interface{}, key interface{}, ops *Operations) (Response, error) + Update(space, index interface{}, key interface{}, ops *Operations) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. - Upsert(space interface{}, tuple interface{}, ops *Operations) (Response, error) + Upsert(space interface{}, tuple interface{}, ops *Operations) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. - Call(functionName string, args interface{}) (Response, error) + Call(functionName string, args interface{}) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. - Call16(functionName string, args interface{}) (Response, error) + Call16(functionName string, args interface{}) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. - Call17(functionName string, args interface{}) (Response, error) + Call17(functionName string, args interface{}) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. - Eval(expr string, args interface{}) (Response, error) + Eval(expr string, args interface{}) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. - Execute(expr string, args interface{}) (Response, error) + Execute(expr string, args interface{}) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. diff --git a/crud/common.go b/crud/common.go index 7a6f9e03b..061818487 100644 --- a/crud/common.go +++ b/crud/common.go @@ -55,6 +55,7 @@ package crud import ( "context" + "io" "github.com/tarantool/go-iproto" @@ -84,6 +85,12 @@ func (req baseRequest) Async() bool { return req.impl.Async() } +// Response creates a response for the baseRequest. +func (req baseRequest) Response(header tarantool.Header, + body io.Reader) (tarantool.Response, error) { + return req.impl.Response(header, body) +} + type spaceRequest struct { baseRequest space string diff --git a/dial.go b/dial.go index 37cbe0139..378687925 100644 --- a/dial.go +++ b/dial.go @@ -404,16 +404,11 @@ func identify(w writeFlusher, r io.Reader) (ProtocolInfo, error) { } data, err := resp.Decode() if err != nil { - switch err := err.(type) { - case Error: - if err.Code == iproto.ER_UNKNOWN_REQUEST_TYPE { - // IPROTO_ID requests are not supported by server. - return info, nil - } - return info, err - default: - return info, fmt.Errorf("decode response body error: %w", err) + if iproto.Error(resp.Header().Code) == iproto.ER_UNKNOWN_REQUEST_TYPE { + // IPROTO_ID requests are not supported by server. + return info, nil } + return info, err } if len(data) == 0 { @@ -511,12 +506,12 @@ func readResponse(r io.Reader) (Response, error) { respBytes, err := read(r, lenbuf[:]) if err != nil { - return &ConnResponse{}, fmt.Errorf("read error: %w", err) + return &BaseResponse{}, fmt.Errorf("read error: %w", err) } buf := smallBuf{b: respBytes} header, err := decodeHeader(msgpack.NewDecoder(&smallBuf{}), &buf) - resp := &ConnResponse{header: header, buf: buf} + resp := &BaseResponse{header: header, buf: buf} if err != nil { return resp, fmt.Errorf("decode response header error: %w", err) } diff --git a/example_test.go b/example_test.go index a39b9d5a1..3c4d12112 100644 --- a/example_test.go +++ b/example_test.go @@ -570,8 +570,17 @@ func ExampleExecuteRequest() { data, err := resp.Decode() fmt.Println("Error", err) fmt.Println("Data", data) - fmt.Println("MetaData", resp.MetaData()) - fmt.Println("SQL Info", resp.SQLInfo()) + exResp, ok := resp.(*tarantool.ExecuteResponse) + if !ok { + fmt.Printf("wrong response type") + return + } + metaData, err := exResp.MetaData() + fmt.Println("MetaData", metaData) + fmt.Println("Error", err) + sqlInfo, err := exResp.SQLInfo() + fmt.Println("SQL Info", sqlInfo) + fmt.Println("Error", err) // There are 4 options to pass named parameters to an SQL query: // 1) The simple map; @@ -608,8 +617,17 @@ func ExampleExecuteRequest() { data, err = resp.Decode() fmt.Println("Error", err) fmt.Println("Data", data) - fmt.Println("MetaData", resp.MetaData()) - fmt.Println("SQL Info", resp.SQLInfo()) + exResp, ok = resp.(*tarantool.ExecuteResponse) + if !ok { + fmt.Printf("wrong response type") + return + } + metaData, err = exResp.MetaData() + fmt.Println("MetaData", metaData) + fmt.Println("Error", err) + sqlInfo, err = exResp.SQLInfo() + fmt.Println("SQL Info", sqlInfo) + fmt.Println("Error", err) // 2) req = req.Args(sqlBind2) @@ -619,8 +637,17 @@ func ExampleExecuteRequest() { data, err = resp.Decode() fmt.Println("Error", err) fmt.Println("Data", data) - fmt.Println("MetaData", resp.MetaData()) - fmt.Println("SQL Info", resp.SQLInfo()) + exResp, ok = resp.(*tarantool.ExecuteResponse) + if !ok { + fmt.Printf("wrong response type") + return + } + metaData, err = exResp.MetaData() + fmt.Println("MetaData", metaData) + fmt.Println("Error", err) + sqlInfo, err = exResp.SQLInfo() + fmt.Println("SQL Info", sqlInfo) + fmt.Println("Error", err) // 3) req = req.Args(sqlBind3) @@ -630,8 +657,17 @@ func ExampleExecuteRequest() { data, err = resp.Decode() fmt.Println("Error", err) fmt.Println("Data", data) - fmt.Println("MetaData", resp.MetaData()) - fmt.Println("SQL Info", resp.SQLInfo()) + exResp, ok = resp.(*tarantool.ExecuteResponse) + if !ok { + fmt.Printf("wrong response type") + return + } + metaData, err = exResp.MetaData() + fmt.Println("MetaData", metaData) + fmt.Println("Error", err) + sqlInfo, err = exResp.SQLInfo() + fmt.Println("SQL Info", sqlInfo) + fmt.Println("Error", err) // 4) req = req.Args(sqlBind4) @@ -641,8 +677,17 @@ func ExampleExecuteRequest() { data, err = resp.Decode() fmt.Println("Error", err) fmt.Println("Data", data) - fmt.Println("MetaData", resp.MetaData()) - fmt.Println("SQL Info", resp.SQLInfo()) + exResp, ok = resp.(*tarantool.ExecuteResponse) + if !ok { + fmt.Printf("wrong response type") + return + } + metaData, err = exResp.MetaData() + fmt.Println("MetaData", metaData) + fmt.Println("Error", err) + sqlInfo, err = exResp.SQLInfo() + fmt.Println("SQL Info", sqlInfo) + fmt.Println("Error", err) // The way to pass positional arguments to an SQL query. req = tarantool.NewExecuteRequest( @@ -654,8 +699,17 @@ func ExampleExecuteRequest() { data, err = resp.Decode() fmt.Println("Error", err) fmt.Println("Data", data) - fmt.Println("MetaData", resp.MetaData()) - fmt.Println("SQL Info", resp.SQLInfo()) + exResp, ok = resp.(*tarantool.ExecuteResponse) + if !ok { + fmt.Printf("wrong response type") + return + } + metaData, err = exResp.MetaData() + fmt.Println("MetaData", metaData) + fmt.Println("Error", err) + sqlInfo, err = exResp.SQLInfo() + fmt.Println("SQL Info", sqlInfo) + fmt.Println("Error", err) // The way to pass SQL expression with using custom packing/unpacking for // a type. @@ -680,8 +734,17 @@ func ExampleExecuteRequest() { data, err = resp.Decode() fmt.Println("Error", err) fmt.Println("Data", data) - fmt.Println("MetaData", resp.MetaData()) - fmt.Println("SQL Info", resp.SQLInfo()) + exResp, ok = resp.(*tarantool.ExecuteResponse) + if !ok { + fmt.Printf("wrong response type") + return + } + metaData, err = exResp.MetaData() + fmt.Println("MetaData", metaData) + fmt.Println("Error", err) + sqlInfo, err = exResp.SQLInfo() + fmt.Println("SQL Info", sqlInfo) + fmt.Println("Error", err) } func getTestTxnDialer() tarantool.Dialer { @@ -1158,6 +1221,15 @@ func ExampleConnection_Do_failure() { // and becomes available. We could wait for that moment with Future.Get(), // Future.GetResponse() or Future.GetTyped() methods. resp, err := future.GetResponse() + if err != nil { + fmt.Printf("Error in the future: %s\n", err) + } + + data, err := future.Get() + if err != nil { + fmt.Printf("Data: %v\n", data) + } + if err != nil { // We don't print the error here to keep the example reproducible. // fmt.Printf("Failed to execute the request: %s\n", err) @@ -1179,6 +1251,7 @@ func ExampleConnection_Do_failure() { } // Output: + // Data: [] // Error code from the response: 33 // Error code from the error: 33 // Error short from the error: ER_NO_SUCH_PROC diff --git a/future.go b/future.go index 7281b6149..139782637 100644 --- a/future.go +++ b/future.go @@ -1,6 +1,7 @@ package tarantool import ( + "io" "sync" "time" ) @@ -8,6 +9,7 @@ import ( // Future is a handle for asynchronous request. type Future struct { requestId uint32 + req Request next *Future timeout time.Duration mutex sync.Mutex @@ -117,6 +119,19 @@ func (it *asyncResponseIterator) nextResponse() (resp Response) { return resp } +// PushResponse is used for push requests for the Future. +type PushResponse struct { + BaseResponse +} + +func createPushResponse(header Header, body io.Reader) (Response, error) { + resp, err := createBaseResponse(header, body) + if err != nil { + return nil, err + } + return &PushResponse{resp}, nil +} + // NewFuture creates a new empty Future. func NewFuture() (fut *Future) { fut = &Future{} @@ -131,30 +146,46 @@ func NewFuture() (fut *Future) { // // Deprecated: the method will be removed in the next major version, // use Connector.NewWatcher() instead of box.session.push(). -func (fut *Future) AppendPush(resp Response) { +func (fut *Future) AppendPush(header Header, body io.Reader) error { fut.mutex.Lock() defer fut.mutex.Unlock() if fut.isDone() { - return + return nil + } + resp, err := createPushResponse(header, body) + if err != nil { + return err } fut.pushes = append(fut.pushes, resp) fut.ready <- struct{}{} + return nil +} + +// SetRequest sets a request, for which the future was created. +func (fut *Future) SetRequest(req Request) { + fut.req = req } // SetResponse sets a response for the future and finishes the future. -func (fut *Future) SetResponse(resp Response) { +func (fut *Future) SetResponse(header Header, body io.Reader) error { fut.mutex.Lock() defer fut.mutex.Unlock() if fut.isDone() { - return + return nil + } + + resp, err := fut.req.Response(header, body) + if err != nil { + return err } fut.resp = resp close(fut.ready) close(fut.done) + return nil } // SetError sets an error for the future and finishes the future. @@ -179,11 +210,7 @@ func (fut *Future) SetError(err error) { // or ClientError, if something bad happens in a client process. func (fut *Future) GetResponse() (Response, error) { fut.wait() - if fut.err != nil { - return fut.resp, fut.err - } - _, err := fut.resp.Decode() - return fut.resp, err + return fut.resp, fut.err } // Get waits for Future to be filled and returns the data of the Response and error. diff --git a/future_test.go b/future_test.go index 3a3d8d01d..947e4ed28 100644 --- a/future_test.go +++ b/future_test.go @@ -54,9 +54,10 @@ func TestFutureGetIteratorNoItems(t *testing.T) { } func TestFutureGetIteratorNoResponse(t *testing.T) { - push := &ConnResponse{} + pushHeader := Header{} + push := &PushResponse{} fut := NewFuture() - fut.AppendPush(push) + fut.AppendPush(pushHeader, nil) if it := fut.GetIterator(); it.Next() { assertResponseIteratorValue(t, it, true, push) @@ -70,9 +71,10 @@ func TestFutureGetIteratorNoResponse(t *testing.T) { } func TestFutureGetIteratorNoResponseTimeout(t *testing.T) { - push := &ConnResponse{} + pushHeader := Header{} + push := &PushResponse{} fut := NewFuture() - fut.AppendPush(push) + fut.AppendPush(pushHeader, nil) if it := fut.GetIterator().WithTimeout(1 * time.Nanosecond); it.Next() { assertResponseIteratorValue(t, it, true, push) @@ -86,10 +88,12 @@ func TestFutureGetIteratorNoResponseTimeout(t *testing.T) { } func TestFutureGetIteratorResponseOnTimeout(t *testing.T) { - push := &ConnResponse{} - resp := &ConnResponse{} + pushHeader := Header{} + respHeader := Header{} + push := &PushResponse{} + resp := &BaseResponse{} fut := NewFuture() - fut.AppendPush(push) + fut.AppendPush(pushHeader, nil) var done sync.WaitGroup var wait sync.WaitGroup @@ -123,19 +127,21 @@ func TestFutureGetIteratorResponseOnTimeout(t *testing.T) { }() wait.Wait() - fut.SetResponse(resp) + + fut.SetRequest(&InsertRequest{}) + fut.SetResponse(respHeader, nil) done.Wait() } func TestFutureGetIteratorFirstResponse(t *testing.T) { - resp1 := &ConnResponse{} - resp2 := &ConnResponse{} + resp := &BaseResponse{} fut := NewFuture() - fut.SetResponse(resp1) - fut.SetResponse(resp2) + fut.SetRequest(&InsertRequest{}) + fut.SetResponse(Header{}, nil) + fut.SetResponse(Header{}, nil) if it := fut.GetIterator(); it.Next() { - assertResponseIteratorValue(t, it, false, resp1) + assertResponseIteratorValue(t, it, false, resp) if it.Next() == true { t.Errorf("An unexpected next value.") } @@ -164,17 +170,19 @@ func TestFutureGetIteratorFirstError(t *testing.T) { } func TestFutureGetIteratorResponse(t *testing.T) { - responses := []*ConnResponse{ - {}, - {}, - {}, + responses := []Response{ + &PushResponse{}, + &PushResponse{}, + &BaseResponse{}, } + header := Header{} fut := NewFuture() - for i, resp := range responses { + fut.SetRequest(&InsertRequest{}) + for i := range responses { if i == len(responses)-1 { - fut.SetResponse(resp) + fut.SetResponse(header, nil) } else { - fut.AppendPush(resp) + fut.AppendPush(header, nil) } } @@ -202,14 +210,14 @@ func TestFutureGetIteratorResponse(t *testing.T) { func TestFutureGetIteratorError(t *testing.T) { const errMsg = "error message" - responses := []*ConnResponse{ + responses := []*PushResponse{ {}, {}, } err := errors.New(errMsg) fut := NewFuture() - for _, resp := range responses { - fut.AppendPush(resp) + for range responses { + fut.AppendPush(Header{}, nil) } fut.SetError(err) @@ -239,19 +247,18 @@ func TestFutureGetIteratorError(t *testing.T) { func TestFutureSetStateRaceCondition(t *testing.T) { err := errors.New("any error") - resp := &ConnResponse{} for i := 0; i < 1000; i++ { fut := NewFuture() + fut.SetRequest(&InsertRequest{}) for j := 0; j < 9; j++ { go func(opt int) { if opt%3 == 0 { - respAppend := &ConnResponse{} - fut.AppendPush(respAppend) + fut.AppendPush(Header{}, nil) } else if opt%3 == 1 { fut.SetError(err) } else { - fut.SetResponse(resp) + fut.SetResponse(Header{}, nil) } }(j) } diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 747de45f4..3734c4c0a 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -373,7 +373,7 @@ func (p *ConnectionPool) GetInfo() map[string]ConnectionInfo { // // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. -func (p *ConnectionPool) Ping(userMode Mode) (tarantool.Response, error) { +func (p *ConnectionPool) Ping(userMode Mode) ([]interface{}, error) { conn, err := p.getNextConnection(userMode) if err != nil { return nil, err @@ -388,7 +388,7 @@ func (p *ConnectionPool) Ping(userMode Mode) (tarantool.Response, error) { // use a SelectRequest object + Do() instead. func (p *ConnectionPool) Select(space, index interface{}, offset, limit uint32, - iterator tarantool.Iter, key interface{}, userMode ...Mode) (tarantool.Response, error) { + iterator tarantool.Iter, key interface{}, userMode ...Mode) ([]interface{}, error) { conn, err := p.getConnByMode(ANY, userMode) if err != nil { return nil, err @@ -403,7 +403,7 @@ func (p *ConnectionPool) Select(space, index interface{}, // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. func (p *ConnectionPool) Insert(space interface{}, tuple interface{}, - userMode ...Mode) (tarantool.Response, error) { + userMode ...Mode) ([]interface{}, error) { conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err @@ -418,7 +418,7 @@ func (p *ConnectionPool) Insert(space interface{}, tuple interface{}, // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. func (p *ConnectionPool) Replace(space interface{}, tuple interface{}, - userMode ...Mode) (tarantool.Response, error) { + userMode ...Mode) ([]interface{}, error) { conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err @@ -433,7 +433,7 @@ func (p *ConnectionPool) Replace(space interface{}, tuple interface{}, // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. func (p *ConnectionPool) Delete(space, index interface{}, key interface{}, - userMode ...Mode) (tarantool.Response, error) { + userMode ...Mode) ([]interface{}, error) { conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err @@ -448,7 +448,7 @@ func (p *ConnectionPool) Delete(space, index interface{}, key interface{}, // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. func (p *ConnectionPool) Update(space, index interface{}, key interface{}, - ops *tarantool.Operations, userMode ...Mode) (tarantool.Response, error) { + ops *tarantool.Operations, userMode ...Mode) ([]interface{}, error) { conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err @@ -463,7 +463,7 @@ func (p *ConnectionPool) Update(space, index interface{}, key interface{}, // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. func (p *ConnectionPool) Upsert(space interface{}, tuple interface{}, - ops *tarantool.Operations, userMode ...Mode) (tarantool.Response, error) { + ops *tarantool.Operations, userMode ...Mode) ([]interface{}, error) { conn, err := p.getConnByMode(RW, userMode) if err != nil { return nil, err @@ -478,7 +478,7 @@ func (p *ConnectionPool) Upsert(space interface{}, tuple interface{}, // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. func (p *ConnectionPool) Call(functionName string, args interface{}, - userMode Mode) (tarantool.Response, error) { + userMode Mode) ([]interface{}, error) { conn, err := p.getNextConnection(userMode) if err != nil { return nil, err @@ -494,7 +494,7 @@ func (p *ConnectionPool) Call(functionName string, args interface{}, // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. func (p *ConnectionPool) Call16(functionName string, args interface{}, - userMode Mode) (tarantool.Response, error) { + userMode Mode) ([]interface{}, error) { conn, err := p.getNextConnection(userMode) if err != nil { return nil, err @@ -509,7 +509,7 @@ func (p *ConnectionPool) Call16(functionName string, args interface{}, // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. func (p *ConnectionPool) Call17(functionName string, args interface{}, - userMode Mode) (tarantool.Response, error) { + userMode Mode) ([]interface{}, error) { conn, err := p.getNextConnection(userMode) if err != nil { return nil, err @@ -523,7 +523,7 @@ func (p *ConnectionPool) Call17(functionName string, args interface{}, // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. func (p *ConnectionPool) Eval(expr string, args interface{}, - userMode Mode) (tarantool.Response, error) { + userMode Mode) ([]interface{}, error) { conn, err := p.getNextConnection(userMode) if err != nil { return nil, err @@ -537,7 +537,7 @@ func (p *ConnectionPool) Eval(expr string, args interface{}, // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. func (p *ConnectionPool) Execute(expr string, args interface{}, - userMode Mode) (tarantool.Response, error) { + userMode Mode) ([]interface{}, error) { conn, err := p.getNextConnection(userMode) if err != nil { return nil, err diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index 50096cc4c..ae1acd223 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -1140,11 +1140,9 @@ func TestCall(t *testing.T) { defer connPool.Close() // PreferRO - resp, err := connPool.Call("box.info", []interface{}{}, pool.PreferRO) + data, err := connPool.Call("box.info", []interface{}{}, pool.PreferRO) require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - data, err := resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Call") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") val := data[0].(map[interface{}]interface{})["ro"] @@ -1153,11 +1151,9 @@ func TestCall(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `PreferRO`") // PreferRW - resp, err = connPool.Call("box.info", []interface{}{}, pool.PreferRW) + data, err = connPool.Call("box.info", []interface{}{}, pool.PreferRW) require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Call") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") val = data[0].(map[interface{}]interface{})["ro"] @@ -1166,11 +1162,9 @@ func TestCall(t *testing.T) { require.Falsef(t, ro, "expected `false` with mode `PreferRW`") // RO - resp, err = connPool.Call("box.info", []interface{}{}, pool.RO) + data, err = connPool.Call("box.info", []interface{}{}, pool.RO) require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Call") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") val = data[0].(map[interface{}]interface{})["ro"] @@ -1179,11 +1173,9 @@ func TestCall(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `RO`") // RW - resp, err = connPool.Call("box.info", []interface{}{}, pool.RW) + data, err = connPool.Call("box.info", []interface{}{}, pool.RW) require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Call") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") val = data[0].(map[interface{}]interface{})["ro"] @@ -1207,11 +1199,9 @@ func TestCall16(t *testing.T) { defer connPool.Close() // PreferRO - resp, err := connPool.Call16("box.info", []interface{}{}, pool.PreferRO) + data, err := connPool.Call16("box.info", []interface{}{}, pool.PreferRO) require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - data, err := resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Call") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") val := data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] @@ -1220,11 +1210,9 @@ func TestCall16(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `PreferRO`") // PreferRW - resp, err = connPool.Call16("box.info", []interface{}{}, pool.PreferRW) + data, err = connPool.Call16("box.info", []interface{}{}, pool.PreferRW) require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Call") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") val = data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] @@ -1233,11 +1221,9 @@ func TestCall16(t *testing.T) { require.Falsef(t, ro, "expected `false` with mode `PreferRW`") // RO - resp, err = connPool.Call16("box.info", []interface{}{}, pool.RO) + data, err = connPool.Call16("box.info", []interface{}{}, pool.RO) require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Call") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") val = data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] @@ -1246,11 +1232,9 @@ func TestCall16(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `RO`") // RW - resp, err = connPool.Call16("box.info", []interface{}{}, pool.RW) + data, err = connPool.Call16("box.info", []interface{}{}, pool.RW) require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Call") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") val = data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] @@ -1274,11 +1258,9 @@ func TestCall17(t *testing.T) { defer connPool.Close() // PreferRO - resp, err := connPool.Call17("box.info", []interface{}{}, pool.PreferRO) + data, err := connPool.Call17("box.info", []interface{}{}, pool.PreferRO) require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - data, err := resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Call") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") val := data[0].(map[interface{}]interface{})["ro"] @@ -1287,11 +1269,9 @@ func TestCall17(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `PreferRO`") // PreferRW - resp, err = connPool.Call17("box.info", []interface{}{}, pool.PreferRW) + data, err = connPool.Call17("box.info", []interface{}{}, pool.PreferRW) require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Call") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") val = data[0].(map[interface{}]interface{})["ro"] @@ -1300,11 +1280,9 @@ func TestCall17(t *testing.T) { require.Falsef(t, ro, "expected `false` with mode `PreferRW`") // RO - resp, err = connPool.Call17("box.info", []interface{}{}, pool.RO) + data, err = connPool.Call17("box.info", []interface{}{}, pool.RO) require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Call") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") val = data[0].(map[interface{}]interface{})["ro"] @@ -1313,11 +1291,9 @@ func TestCall17(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `RO`") // RW - resp, err = connPool.Call17("box.info", []interface{}{}, pool.RW) + data, err = connPool.Call17("box.info", []interface{}{}, pool.RW) require.Nilf(t, err, "failed to Call") - require.NotNilf(t, resp, "response is nil after Call") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Call") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") val = data[0].(map[interface{}]interface{})["ro"] @@ -1341,11 +1317,9 @@ func TestEval(t *testing.T) { defer connPool.Close() // PreferRO - resp, err := connPool.Eval("return box.info().ro", []interface{}{}, pool.PreferRO) + data, err := connPool.Eval("return box.info().ro", []interface{}{}, pool.PreferRO) require.Nilf(t, err, "failed to Eval") - require.NotNilf(t, resp, "response is nil after Eval") - data, err := resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Eval") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Eval") val, ok := data[0].(bool) @@ -1353,11 +1327,9 @@ func TestEval(t *testing.T) { require.Truef(t, val, "expected `true` with mode `PreferRO`") // PreferRW - resp, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.PreferRW) + data, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.PreferRW) require.Nilf(t, err, "failed to Eval") - require.NotNilf(t, resp, "response is nil after Eval") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Eval") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Eval") val, ok = data[0].(bool) @@ -1365,11 +1337,9 @@ func TestEval(t *testing.T) { require.Falsef(t, val, "expected `false` with mode `PreferRW`") // RO - resp, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.RO) + data, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.RO) require.Nilf(t, err, "failed to Eval") - require.NotNilf(t, resp, "response is nil after Eval") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Eval") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Eval") val, ok = data[0].(bool) @@ -1377,11 +1347,9 @@ func TestEval(t *testing.T) { require.Truef(t, val, "expected `true` with mode `RO`") // RW - resp, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.RW) + data, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.RW) require.Nilf(t, err, "failed to Eval") - require.NotNilf(t, resp, "response is nil after Eval") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Eval") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Eval") val, ok = data[0].(bool) @@ -1430,11 +1398,9 @@ func TestExecute(t *testing.T) { request := "SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0 == 1;" // Execute - resp, err := connPool.Execute(request, []interface{}{}, pool.ANY) + data, err := connPool.Execute(request, []interface{}{}, pool.ANY) require.Nilf(t, err, "failed to Execute") - require.NotNilf(t, resp, "response is nil after Execute") - data, err := resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Execute") require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Execute") require.Equalf(t, len(data[0].([]interface{})), 2, "unexpected response") @@ -1872,11 +1838,9 @@ func TestInsert(t *testing.T) { defer connPool.Close() // Mode is `RW` by default, we have only one RW instance (servers[2]) - resp, err := connPool.Insert(spaceName, []interface{}{"rw_insert_key", "rw_insert_value"}) + data, err := connPool.Insert(spaceName, []interface{}{"rw_insert_key", "rw_insert_value"}) require.Nilf(t, err, "failed to Insert") - require.NotNilf(t, resp, "response is nil after Insert") - data, err := resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Insert") require.Equalf(t, len(data), 1, "response Body len != 1 after Insert") tpl, ok := data[0].([]interface{}) @@ -1918,12 +1882,10 @@ func TestInsert(t *testing.T) { require.Equalf(t, "rw_insert_value", value, "unexpected body of Select (1)") // PreferRW - resp, err = connPool.Insert(spaceName, + data, err = connPool.Insert(spaceName, []interface{}{"preferRW_insert_key", "preferRW_insert_value"}) require.Nilf(t, err, "failed to Insert") - require.NotNilf(t, resp, "response is nil after Insert") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Insert") require.Equalf(t, len(data), 1, "response Body len != 1 after Insert") tpl, ok = data[0].([]interface{}) @@ -1997,11 +1959,9 @@ func TestDelete(t *testing.T) { require.Equalf(t, "delete_value", value, "unexpected body of Insert (1)") // Mode is `RW` by default, we have only one RW instance (servers[2]) - resp, err := connPool.Delete(spaceName, indexNo, []interface{}{"delete_key"}) + data, err = connPool.Delete(spaceName, indexNo, []interface{}{"delete_key"}) require.Nilf(t, err, "failed to Delete") - require.NotNilf(t, resp, "response is nil after Delete") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Delete") require.Equalf(t, len(data), 1, "response Body len != 1 after Delete") tpl, ok = data[0].([]interface{}) @@ -2046,18 +2006,18 @@ func TestUpsert(t *testing.T) { defer conn.Close() // Mode is `RW` by default, we have only one RW instance (servers[2]) - resp, err := connPool.Upsert(spaceName, + data, err := connPool.Upsert(spaceName, []interface{}{"upsert_key", "upsert_value"}, tarantool.NewOperations().Assign(1, "new_value")) require.Nilf(t, err, "failed to Upsert") - require.NotNilf(t, resp, "response is nil after Upsert") + require.NotNilf(t, data, "response is nil after Upsert") sel := tarantool.NewSelectRequest(spaceNo). Index(indexNo). Limit(1). Iterator(tarantool.IterEq). Key([]interface{}{"upsert_key"}) - data, err := conn.Do(sel).Get() + data, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") require.Equalf(t, len(data), 1, "response Body len != 1 after Select") @@ -2074,12 +2034,12 @@ func TestUpsert(t *testing.T) { require.Equalf(t, "upsert_value", value, "unexpected body of Select (1)") // PreferRW - resp, err = connPool.Upsert( + data, err = connPool.Upsert( spaceName, []interface{}{"upsert_key", "upsert_value"}, tarantool.NewOperations().Assign(1, "new_value"), pool.PreferRW) require.Nilf(t, err, "failed to Upsert") - require.NotNilf(t, resp, "response is nil after Upsert") + require.NotNilf(t, data, "response is nil after Upsert") data, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") @@ -2308,11 +2268,9 @@ func TestSelect(t *testing.T) { require.Nil(t, err) //default: ANY - resp, err := connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, anyKey) + data, err := connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, anyKey) require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - data, err := resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Select") require.Equalf(t, len(data), 1, "response Body len != 1 after Select") tpl, ok := data[0].([]interface{}) @@ -2328,11 +2286,9 @@ func TestSelect(t *testing.T) { require.Equalf(t, "any_select_value", value, "unexpected body of Select (1)") // PreferRO - resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, pool.PreferRO) + data, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, pool.PreferRO) require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Select") require.Equalf(t, len(data), 1, "response Body len != 1 after Select") tpl, ok = data[0].([]interface{}) @@ -2344,11 +2300,9 @@ func TestSelect(t *testing.T) { require.Equalf(t, "ro_select_key", key, "unexpected body of Select (0)") // PreferRW - resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, pool.PreferRW) + data, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, pool.PreferRW) require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Select") require.Equalf(t, len(data), 1, "response Body len != 1 after Select") tpl, ok = data[0].([]interface{}) @@ -2364,11 +2318,9 @@ func TestSelect(t *testing.T) { require.Equalf(t, "rw_select_value", value, "unexpected body of Select (1)") // RO - resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, pool.RO) + data, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, pool.RO) require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Select") require.Equalf(t, len(data), 1, "response Body len != 1 after Select") tpl, ok = data[0].([]interface{}) @@ -2384,11 +2336,9 @@ func TestSelect(t *testing.T) { require.Equalf(t, "ro_select_value", value, "unexpected body of Select (1)") // RW - resp, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, pool.RW) + data, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, pool.RW) require.Nilf(t, err, "failed to Select") - require.NotNilf(t, resp, "response is nil after Select") - data, err = resp.Decode() - require.Nilf(t, err, "failed to Decode") + require.NotNilf(t, data, "response is nil after Select") require.Equalf(t, len(data), 1, "response Body len != 1 after Select") tpl, ok = data[0].([]interface{}) @@ -2419,29 +2369,29 @@ func TestPing(t *testing.T) { defer connPool.Close() // ANY - resp, err := connPool.Ping(pool.ANY) + data, err := connPool.Ping(pool.ANY) require.Nilf(t, err, "failed to Ping") - require.NotNilf(t, resp, "response is nil after Ping") + require.Nilf(t, data, "response data is not nil after Ping") // RW - resp, err = connPool.Ping(pool.RW) + data, err = connPool.Ping(pool.RW) require.Nilf(t, err, "failed to Ping") - require.NotNilf(t, resp, "response is nil after Ping") + require.Nilf(t, data, "response data is not nil after Ping") // RO - resp, err = connPool.Ping(pool.RO) + data, err = connPool.Ping(pool.RO) require.Nilf(t, err, "failed to Ping") - require.NotNilf(t, resp, "response is nil after Ping") + require.Nilf(t, data, "response data is not nil after Ping") // PreferRW - resp, err = connPool.Ping(pool.PreferRW) + data, err = connPool.Ping(pool.PreferRW) require.Nilf(t, err, "failed to Ping") - require.NotNilf(t, resp, "response is nil after Ping") + require.Nilf(t, data, "response data is not nil after Ping") // PreferRO - resp, err = connPool.Ping(pool.PreferRO) + data, err = connPool.Ping(pool.PreferRO) require.Nilf(t, err, "failed to Ping") - require.NotNilf(t, resp, "response is nil after Ping") + require.Nilf(t, data, "response data is not nil after Ping") } func TestDo(t *testing.T) { @@ -2548,7 +2498,14 @@ func TestNewPrepared(t *testing.T) { if reflect.DeepEqual(data[0], []interface{}{1, "test"}) { t.Error("Select with named arguments failed") } - metaData := resp.MetaData() + prepResp, ok := resp.(*tarantool.ExecuteResponse) + if !ok { + t.Fatalf("Not a Prepare response") + } + metaData, err := prepResp.MetaData() + if err != nil { + t.Errorf("Error while getting MetaData: %s", err.Error()) + } if metaData[0].FieldType != "unsigned" || metaData[0].FieldName != "NAME0" || metaData[1].FieldType != "string" || diff --git a/pool/connector.go b/pool/connector.go index 74a60bd74..23cc7275d 100644 --- a/pool/connector.go +++ b/pool/connector.go @@ -60,7 +60,7 @@ func (c *ConnectorAdapter) ConfiguredTimeout() time.Duration { // // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. -func (c *ConnectorAdapter) Ping() (tarantool.Response, error) { +func (c *ConnectorAdapter) Ping() ([]interface{}, error) { return c.pool.Ping(c.mode) } @@ -70,7 +70,7 @@ func (c *ConnectorAdapter) Ping() (tarantool.Response, error) { // use a SelectRequest object + Do() instead. func (c *ConnectorAdapter) Select(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, - key interface{}) (tarantool.Response, error) { + key interface{}) ([]interface{}, error) { return c.pool.Select(space, index, offset, limit, iterator, key, c.mode) } @@ -79,7 +79,7 @@ func (c *ConnectorAdapter) Select(space, index interface{}, // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. func (c *ConnectorAdapter) Insert(space interface{}, - tuple interface{}) (tarantool.Response, error) { + tuple interface{}) ([]interface{}, error) { return c.pool.Insert(space, tuple, c.mode) } @@ -88,7 +88,7 @@ func (c *ConnectorAdapter) Insert(space interface{}, // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. func (c *ConnectorAdapter) Replace(space interface{}, - tuple interface{}) (tarantool.Response, error) { + tuple interface{}) ([]interface{}, error) { return c.pool.Replace(space, tuple, c.mode) } @@ -97,7 +97,7 @@ func (c *ConnectorAdapter) Replace(space interface{}, // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. func (c *ConnectorAdapter) Delete(space, index interface{}, - key interface{}) (tarantool.Response, error) { + key interface{}) ([]interface{}, error) { return c.pool.Delete(space, index, key, c.mode) } @@ -106,7 +106,7 @@ func (c *ConnectorAdapter) Delete(space, index interface{}, // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. func (c *ConnectorAdapter) Update(space, index interface{}, - key interface{}, ops *tarantool.Operations) (tarantool.Response, error) { + key interface{}, ops *tarantool.Operations) ([]interface{}, error) { return c.pool.Update(space, index, key, ops, c.mode) } @@ -115,7 +115,7 @@ func (c *ConnectorAdapter) Update(space, index interface{}, // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. func (c *ConnectorAdapter) Upsert(space, tuple interface{}, - ops *tarantool.Operations) (tarantool.Response, error) { + ops *tarantool.Operations) ([]interface{}, error) { return c.pool.Upsert(space, tuple, ops, c.mode) } @@ -125,7 +125,7 @@ func (c *ConnectorAdapter) Upsert(space, tuple interface{}, // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. func (c *ConnectorAdapter) Call(functionName string, - args interface{}) (tarantool.Response, error) { + args interface{}) ([]interface{}, error) { return c.pool.Call(functionName, args, c.mode) } @@ -136,7 +136,7 @@ func (c *ConnectorAdapter) Call(functionName string, // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. func (c *ConnectorAdapter) Call16(functionName string, - args interface{}) (tarantool.Response, error) { + args interface{}) ([]interface{}, error) { return c.pool.Call16(functionName, args, c.mode) } @@ -146,7 +146,7 @@ func (c *ConnectorAdapter) Call16(functionName string, // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. func (c *ConnectorAdapter) Call17(functionName string, - args interface{}) (tarantool.Response, error) { + args interface{}) ([]interface{}, error) { return c.pool.Call17(functionName, args, c.mode) } @@ -155,7 +155,7 @@ func (c *ConnectorAdapter) Call17(functionName string, // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. func (c *ConnectorAdapter) Eval(expr string, - args interface{}) (tarantool.Response, error) { + args interface{}) ([]interface{}, error) { return c.pool.Eval(expr, args, c.mode) } @@ -164,7 +164,7 @@ func (c *ConnectorAdapter) Eval(expr string, // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. func (c *ConnectorAdapter) Execute(expr string, - args interface{}) (tarantool.Response, error) { + args interface{}) ([]interface{}, error) { return c.pool.Execute(expr, args, c.mode) } diff --git a/pool/connector_test.go b/pool/connector_test.go index 190d2f9cc..87bebbd53 100644 --- a/pool/connector_test.go +++ b/pool/connector_test.go @@ -135,7 +135,7 @@ type baseRequestMock struct { mode Mode } -var reqResp tarantool.Response = &tarantool.ConnResponse{} +var reqData []interface{} var errReq error = errors.New("response error") var reqFuture *tarantool.Future = &tarantool.Future{} @@ -190,7 +190,7 @@ type selectMock struct { func (m *selectMock) Select(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, key interface{}, - mode ...Mode) (tarantool.Response, error) { + mode ...Mode) ([]interface{}, error) { m.called++ m.space = space m.index = index @@ -199,7 +199,7 @@ func (m *selectMock) Select(space, index interface{}, m.iterator = iterator m.key = key m.mode = mode[0] - return reqResp, errReq + return reqData, errReq } func TestConnectorSelect(t *testing.T) { @@ -208,7 +208,7 @@ func TestConnectorSelect(t *testing.T) { resp, err := c.Select(reqSpace, reqIndex, reqOffset, reqLimit, reqIterator, reqKey) - require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqData, resp, "unexpected response") require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") @@ -299,12 +299,12 @@ type insertMock struct { } func (m *insertMock) Insert(space, tuple interface{}, - mode ...Mode) (tarantool.Response, error) { + mode ...Mode) ([]interface{}, error) { m.called++ m.space = space m.tuple = tuple m.mode = mode[0] - return reqResp, errReq + return reqData, errReq } func TestConnectorInsert(t *testing.T) { @@ -313,7 +313,7 @@ func TestConnectorInsert(t *testing.T) { resp, err := c.Insert(reqSpace, reqTuple) - require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqData, resp, "unexpected response") require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") @@ -380,12 +380,12 @@ type replaceMock struct { } func (m *replaceMock) Replace(space, tuple interface{}, - mode ...Mode) (tarantool.Response, error) { + mode ...Mode) ([]interface{}, error) { m.called++ m.space = space m.tuple = tuple m.mode = mode[0] - return reqResp, errReq + return reqData, errReq } func TestConnectorReplace(t *testing.T) { @@ -394,7 +394,7 @@ func TestConnectorReplace(t *testing.T) { resp, err := c.Replace(reqSpace, reqTuple) - require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqData, resp, "unexpected response") require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") @@ -461,13 +461,13 @@ type deleteMock struct { } func (m *deleteMock) Delete(space, index, key interface{}, - mode ...Mode) (tarantool.Response, error) { + mode ...Mode) ([]interface{}, error) { m.called++ m.space = space m.index = index m.key = key m.mode = mode[0] - return reqResp, errReq + return reqData, errReq } func TestConnectorDelete(t *testing.T) { @@ -476,7 +476,7 @@ func TestConnectorDelete(t *testing.T) { resp, err := c.Delete(reqSpace, reqIndex, reqKey) - require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqData, resp, "unexpected response") require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") @@ -548,14 +548,14 @@ type updateMock struct { } func (m *updateMock) Update(space, index, key interface{}, - ops *tarantool.Operations, mode ...Mode) (tarantool.Response, error) { + ops *tarantool.Operations, mode ...Mode) ([]interface{}, error) { m.called++ m.space = space m.index = index m.key = key m.ops = ops m.mode = mode[0] - return reqResp, errReq + return reqData, errReq } func TestConnectorUpdate(t *testing.T) { @@ -564,7 +564,7 @@ func TestConnectorUpdate(t *testing.T) { resp, err := c.Update(reqSpace, reqIndex, reqKey, reqOps) - require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqData, resp, "unexpected response") require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") @@ -641,13 +641,13 @@ type upsertMock struct { } func (m *upsertMock) Upsert(space, tuple interface{}, ops *tarantool.Operations, - mode ...Mode) (tarantool.Response, error) { + mode ...Mode) ([]interface{}, error) { m.called++ m.space = space m.tuple = tuple m.ops = ops m.mode = mode[0] - return reqResp, errReq + return reqData, errReq } func TestConnectorUpsert(t *testing.T) { @@ -656,7 +656,7 @@ func TestConnectorUpsert(t *testing.T) { resp, err := c.Upsert(reqSpace, reqTuple, reqOps) - require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqData, resp, "unexpected response") require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqSpace, m.space, "unexpected space was passed") @@ -698,12 +698,12 @@ type baseCallMock struct { } func (m *baseCallMock) call(functionName string, args interface{}, - mode Mode) (tarantool.Response, error) { + mode Mode) ([]interface{}, error) { m.called++ m.functionName = functionName m.args = args m.mode = mode - return reqResp, errReq + return reqData, errReq } func (m *baseCallMock) callTyped(functionName string, args interface{}, @@ -730,7 +730,7 @@ type callMock struct { } func (m *callMock) Call(functionName string, args interface{}, - mode Mode) (tarantool.Response, error) { + mode Mode) ([]interface{}, error) { return m.call(functionName, args, mode) } @@ -740,7 +740,7 @@ func TestConnectorCall(t *testing.T) { resp, err := c.Call(reqFunctionName, reqArgs) - require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqData, resp, "unexpected response") require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, @@ -801,7 +801,7 @@ type call16Mock struct { } func (m *call16Mock) Call16(functionName string, args interface{}, - mode Mode) (tarantool.Response, error) { + mode Mode) ([]interface{}, error) { return m.call(functionName, args, mode) } @@ -811,7 +811,7 @@ func TestConnectorCall16(t *testing.T) { resp, err := c.Call16(reqFunctionName, reqArgs) - require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqData, resp, "unexpected response") require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, @@ -872,7 +872,7 @@ type call17Mock struct { } func (m *call17Mock) Call17(functionName string, args interface{}, - mode Mode) (tarantool.Response, error) { + mode Mode) ([]interface{}, error) { return m.call(functionName, args, mode) } @@ -882,7 +882,7 @@ func TestConnectorCall17(t *testing.T) { resp, err := c.Call17(reqFunctionName, reqArgs) - require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqData, resp, "unexpected response") require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, @@ -943,7 +943,7 @@ type evalMock struct { } func (m *evalMock) Eval(functionName string, args interface{}, - mode Mode) (tarantool.Response, error) { + mode Mode) ([]interface{}, error) { return m.call(functionName, args, mode) } @@ -953,7 +953,7 @@ func TestConnectorEval(t *testing.T) { resp, err := c.Eval(reqFunctionName, reqArgs) - require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqData, resp, "unexpected response") require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, @@ -1014,7 +1014,7 @@ type executeMock struct { } func (m *executeMock) Execute(functionName string, args interface{}, - mode Mode) (tarantool.Response, error) { + mode Mode) ([]interface{}, error) { return m.call(functionName, args, mode) } @@ -1024,7 +1024,7 @@ func TestConnectorExecute(t *testing.T) { resp, err := c.Execute(reqFunctionName, reqArgs) - require.Equalf(t, reqResp, resp, "unexpected response") + require.Equalf(t, reqData, resp, "unexpected response") require.Equalf(t, errReq, err, "unexpected error") require.Equalf(t, 1, m.called, "should be called only once") require.Equalf(t, reqFunctionName, m.functionName, diff --git a/pool/pooler.go b/pool/pooler.go index 6256c2d24..975162d37 100644 --- a/pool/pooler.go +++ b/pool/pooler.go @@ -19,51 +19,51 @@ type Pooler interface { // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. - Ping(mode Mode) (tarantool.Response, error) + Ping(mode Mode) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. Select(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, - key interface{}, mode ...Mode) (tarantool.Response, error) + key interface{}, mode ...Mode) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. Insert(space interface{}, tuple interface{}, - mode ...Mode) (tarantool.Response, error) + mode ...Mode) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. Replace(space interface{}, tuple interface{}, - mode ...Mode) (tarantool.Response, error) + mode ...Mode) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. Delete(space, index interface{}, key interface{}, - mode ...Mode) (tarantool.Response, error) + mode ...Mode) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. Update(space, index interface{}, key interface{}, ops *tarantool.Operations, - mode ...Mode) (tarantool.Response, error) + mode ...Mode) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. Upsert(space interface{}, tuple interface{}, ops *tarantool.Operations, - mode ...Mode) (tarantool.Response, error) + mode ...Mode) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. Call(functionName string, args interface{}, - mode Mode) (tarantool.Response, error) + mode Mode) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. Call16(functionName string, args interface{}, - mode Mode) (tarantool.Response, error) + mode Mode) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. Call17(functionName string, args interface{}, - mode Mode) (tarantool.Response, error) + mode Mode) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. Eval(expr string, args interface{}, - mode Mode) (tarantool.Response, error) + mode Mode) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. Execute(expr string, args interface{}, - mode Mode) (tarantool.Response, error) + mode Mode) ([]interface{}, error) // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. diff --git a/prepared.go b/prepared.go index f4fc1cdf1..35b73272e 100644 --- a/prepared.go +++ b/prepared.go @@ -3,6 +3,7 @@ package tarantool import ( "context" "fmt" + "io" "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" @@ -95,6 +96,15 @@ func (req *PrepareRequest) Context(ctx context.Context) *PrepareRequest { return req } +// Response creates a response for the PrepareRequest. +func (req *PrepareRequest) Response(header Header, body io.Reader) (Response, error) { + baseResp, err := createBaseResponse(header, body) + if err != nil { + return nil, err + } + return &PrepareResponse{BaseResponse: baseResp}, nil +} + // UnprepareRequest helps you to create an unprepare request object for // execution by a Connection. type UnprepareRequest struct { @@ -175,3 +185,12 @@ func (req *ExecutePreparedRequest) Context(ctx context.Context) *ExecutePrepared req.ctx = ctx return req } + +// Response creates a response for the ExecutePreparedRequest. +func (req *ExecutePreparedRequest) Response(header Header, body io.Reader) (Response, error) { + baseResp, err := createBaseResponse(header, body) + if err != nil { + return nil, err + } + return &ExecuteResponse{BaseResponse: baseResp}, nil +} diff --git a/request.go b/request.go index bdf1c50ea..716e5ab2c 100644 --- a/request.go +++ b/request.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "reflect" "strings" "sync" @@ -260,8 +261,8 @@ func fillWatchOnce(enc *msgpack.Encoder, key string) error { // // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. -func (conn *Connection) Ping() (Response, error) { - return conn.Do(NewPingRequest()).GetResponse() +func (conn *Connection) Ping() ([]interface{}, error) { + return conn.Do(NewPingRequest()).Get() } // Select performs select to box space. @@ -271,8 +272,8 @@ func (conn *Connection) Ping() (Response, error) { // Deprecated: the method will be removed in the next major version, // use a SelectRequest object + Do() instead. func (conn *Connection) Select(space, index interface{}, offset, limit uint32, iterator Iter, - key interface{}) (Response, error) { - return conn.SelectAsync(space, index, offset, limit, iterator, key).GetResponse() + key interface{}) ([]interface{}, error) { + return conn.SelectAsync(space, index, offset, limit, iterator, key).Get() } // Insert performs insertion to box space. @@ -282,8 +283,8 @@ func (conn *Connection) Select(space, index interface{}, offset, limit uint32, i // // Deprecated: the method will be removed in the next major version, // use an InsertRequest object + Do() instead. -func (conn *Connection) Insert(space interface{}, tuple interface{}) (Response, error) { - return conn.InsertAsync(space, tuple).GetResponse() +func (conn *Connection) Insert(space interface{}, tuple interface{}) ([]interface{}, error) { + return conn.InsertAsync(space, tuple).Get() } // Replace performs "insert or replace" action to box space. @@ -293,8 +294,8 @@ func (conn *Connection) Insert(space interface{}, tuple interface{}) (Response, // // Deprecated: the method will be removed in the next major version, // use a ReplaceRequest object + Do() instead. -func (conn *Connection) Replace(space interface{}, tuple interface{}) (Response, error) { - return conn.ReplaceAsync(space, tuple).GetResponse() +func (conn *Connection) Replace(space interface{}, tuple interface{}) ([]interface{}, error) { + return conn.ReplaceAsync(space, tuple).Get() } // Delete performs deletion of a tuple by key. @@ -304,8 +305,8 @@ func (conn *Connection) Replace(space interface{}, tuple interface{}) (Response, // // Deprecated: the method will be removed in the next major version, // use a DeleteRequest object + Do() instead. -func (conn *Connection) Delete(space, index interface{}, key interface{}) (Response, error) { - return conn.DeleteAsync(space, index, key).GetResponse() +func (conn *Connection) Delete(space, index interface{}, key interface{}) ([]interface{}, error) { + return conn.DeleteAsync(space, index, key).Get() } // Update performs update of a tuple by key. @@ -315,8 +316,9 @@ func (conn *Connection) Delete(space, index interface{}, key interface{}) (Respo // // Deprecated: the method will be removed in the next major version, // use a UpdateRequest object + Do() instead. -func (conn *Connection) Update(space, index, key interface{}, ops *Operations) (Response, error) { - return conn.UpdateAsync(space, index, key, ops).GetResponse() +func (conn *Connection) Update(space, index, key interface{}, + ops *Operations) ([]interface{}, error) { + return conn.UpdateAsync(space, index, key, ops).Get() } // Upsert performs "update or insert" action of a tuple by key. @@ -326,8 +328,8 @@ func (conn *Connection) Update(space, index, key interface{}, ops *Operations) ( // // Deprecated: the method will be removed in the next major version, // use a UpsertRequest object + Do() instead. -func (conn *Connection) Upsert(space, tuple interface{}, ops *Operations) (Response, error) { - return conn.UpsertAsync(space, tuple, ops).GetResponse() +func (conn *Connection) Upsert(space, tuple interface{}, ops *Operations) ([]interface{}, error) { + return conn.UpsertAsync(space, tuple, ops).Get() } // Call calls registered Tarantool function. @@ -337,8 +339,8 @@ func (conn *Connection) Upsert(space, tuple interface{}, ops *Operations) (Respo // // Deprecated: the method will be removed in the next major version, // use a CallRequest object + Do() instead. -func (conn *Connection) Call(functionName string, args interface{}) (Response, error) { - return conn.CallAsync(functionName, args).GetResponse() +func (conn *Connection) Call(functionName string, args interface{}) ([]interface{}, error) { + return conn.CallAsync(functionName, args).Get() } // Call16 calls registered Tarantool function. @@ -349,8 +351,8 @@ func (conn *Connection) Call(functionName string, args interface{}) (Response, e // // Deprecated: the method will be removed in the next major version, // use a Call16Request object + Do() instead. -func (conn *Connection) Call16(functionName string, args interface{}) (Response, error) { - return conn.Call16Async(functionName, args).GetResponse() +func (conn *Connection) Call16(functionName string, args interface{}) ([]interface{}, error) { + return conn.Call16Async(functionName, args).Get() } // Call17 calls registered Tarantool function. @@ -360,8 +362,8 @@ func (conn *Connection) Call16(functionName string, args interface{}) (Response, // // Deprecated: the method will be removed in the next major version, // use a Call17Request object + Do() instead. -func (conn *Connection) Call17(functionName string, args interface{}) (Response, error) { - return conn.Call17Async(functionName, args).GetResponse() +func (conn *Connection) Call17(functionName string, args interface{}) ([]interface{}, error) { + return conn.Call17Async(functionName, args).Get() } // Eval passes Lua expression for evaluation. @@ -370,8 +372,8 @@ func (conn *Connection) Call17(functionName string, args interface{}) (Response, // // Deprecated: the method will be removed in the next major version, // use an EvalRequest object + Do() instead. -func (conn *Connection) Eval(expr string, args interface{}) (Response, error) { - return conn.EvalAsync(expr, args).GetResponse() +func (conn *Connection) Eval(expr string, args interface{}) ([]interface{}, error) { + return conn.EvalAsync(expr, args).Get() } // Execute passes sql expression to Tarantool for execution. @@ -381,8 +383,8 @@ func (conn *Connection) Eval(expr string, args interface{}) (Response, error) { // // Deprecated: the method will be removed in the next major version, // use an ExecuteRequest object + Do() instead. -func (conn *Connection) Execute(expr string, args interface{}) (Response, error) { - return conn.ExecuteAsync(expr, args).GetResponse() +func (conn *Connection) Execute(expr string, args interface{}) ([]interface{}, error) { + return conn.ExecuteAsync(expr, args).Get() } // single used for conn.GetTyped for decode one tuple. @@ -532,9 +534,20 @@ func (conn *Connection) EvalTyped(expr string, args interface{}, result interfac // use an ExecuteRequest object + Do() instead. func (conn *Connection) ExecuteTyped(expr string, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error) { + var ( + sqlInfo SQLInfo + metaData []ColumnMetaData + ) + fut := conn.ExecuteAsync(expr, args) err := fut.GetTyped(&result) - return fut.resp.SQLInfo(), fut.resp.MetaData(), err + if resp, ok := fut.resp.(*ExecuteResponse); ok { + sqlInfo = resp.sqlInfo + metaData = resp.metaData + } else if err == nil { + err = fmt.Errorf("unexpected response type %T, want: *ExecuteResponse", fut.resp) + } + return sqlInfo, metaData, err } // SelectAsync sends select request to Tarantool and returns Future. @@ -807,6 +820,8 @@ type Request interface { Ctx() context.Context // Async returns true if the request does not expect response. Async() bool + // Response creates a response for current request type. + Response(header Header, body io.Reader) (Response, error) } // ConnectedRequest is an interface that provides the info about a Connection @@ -838,6 +853,15 @@ func (req *baseRequest) Ctx() context.Context { return req.ctx } +// Response creates a response for the baseRequest. +func (req *baseRequest) Response(header Header, body io.Reader) (Response, error) { + resp, err := createBaseResponse(header, body) + if err != nil { + return nil, err + } + return &resp, nil +} + type spaceRequest struct { baseRequest space interface{} @@ -928,6 +952,15 @@ func (req authRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return nil } +// Response creates a response for the authRequest. +func (req authRequest) Response(header Header, body io.Reader) (Response, error) { + resp, err := createBaseResponse(header, body) + if err != nil { + return nil, err + } + return &resp, nil +} + // PingRequest helps you to create an execute request object for execution // by a Connection. type PingRequest struct { @@ -1070,6 +1103,15 @@ func (req *SelectRequest) Context(ctx context.Context) *SelectRequest { return req } +// Response creates a response for the SelectRequest. +func (req *SelectRequest) Response(header Header, body io.Reader) (Response, error) { + baseResp, err := createBaseResponse(header, body) + if err != nil { + return nil, err + } + return &SelectResponse{BaseResponse: baseResp}, nil +} + // InsertRequest helps you to create an insert request object for execution // by a Connection. type InsertRequest struct { @@ -1469,6 +1511,15 @@ func (req *ExecuteRequest) Context(ctx context.Context) *ExecuteRequest { return req } +// Response creates a response for the ExecuteRequest. +func (req *ExecuteRequest) Response(header Header, body io.Reader) (Response, error) { + baseResp, err := createBaseResponse(header, body) + if err != nil { + return nil, err + } + return &ExecuteResponse{BaseResponse: baseResp}, nil +} + // WatchOnceRequest synchronously fetches the value currently associated with a // specified notification key without subscribing to changes. type WatchOnceRequest struct { diff --git a/response.go b/response.go index 09145ee9d..61c77f413 100644 --- a/response.go +++ b/response.go @@ -2,6 +2,8 @@ package tarantool import ( "fmt" + "io" + "io/ioutil" "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" @@ -15,26 +17,62 @@ type Response interface { Decode() ([]interface{}, error) // DecodeTyped decodes a response into a given container res. DecodeTyped(res interface{}) error - - // Pos returns a position descriptor of the last selected tuple. - Pos() []byte - // MetaData returns meta-data. - MetaData() []ColumnMetaData - // SQLInfo returns sql info. - SQLInfo() SQLInfo } -// ConnResponse is a Response interface implementation. -// It is used for all request types. -type ConnResponse struct { +// BaseResponse is a base Response interface implementation. +type BaseResponse struct { header Header // data contains deserialized data for untyped requests. - data []interface{} + data []interface{} + buf smallBuf + decoded bool + decodedTyped bool +} + +func createBaseResponse(header Header, body io.Reader) (BaseResponse, error) { + if body == nil { + return BaseResponse{header: header}, nil + } + if buf, ok := body.(*smallBuf); ok { + return BaseResponse{header: header, buf: *buf}, nil + } + data, err := ioutil.ReadAll(body) + if err != nil { + return BaseResponse{}, err + } + return BaseResponse{header: header, buf: smallBuf{b: data}}, nil +} + +func (resp *BaseResponse) SetHeader(header Header) { + resp.header = header +} + +// SelectResponse is used for the select requests. +// It might contain a position descriptor of the last selected tuple. +// +// You need to cast to SelectResponse a response from SelectRequest. +type SelectResponse struct { + BaseResponse // pos contains a position descriptor of last selected tuple. - pos []byte + pos []byte +} + +// PrepareResponse is used for the prepare requests. +// It might contain meta-data and sql info. +// +// Be careful: now this is an alias for `ExecuteResponse`, +// but it could be changed in the future. +// You need to cast to PrepareResponse a response from PrepareRequest. +type PrepareResponse ExecuteResponse + +// ExecuteResponse is used for the execute requests. +// It might contain meta-data and sql info. +// +// You need to cast to ExecuteResponse a response from ExecuteRequest. +type ExecuteResponse struct { + BaseResponse metaData []ColumnMetaData sqlInfo SQLInfo - buf smallBuf } type ColumnMetaData struct { @@ -166,19 +204,118 @@ func decodeHeader(d *msgpack.Decoder, buf *smallBuf) (Header, error) { return decodedHeader, nil } -func (resp *ConnResponse) Decode() ([]interface{}, error) { +type decodeInfo struct { + stmtID uint64 + bindCount uint64 + serverProtocolInfo ProtocolInfo + errorExtendedInfo *BoxError + + decodedError string +} + +func (info *decodeInfo) parseData(resp *BaseResponse) error { + if info.stmtID != 0 { + stmt := &Prepared{ + StatementID: PreparedID(info.stmtID), + ParamCount: info.bindCount, + } + resp.data = []interface{}{stmt} + return nil + } + + // Tarantool may send only version >= 1. + if info.serverProtocolInfo.Version != ProtocolVersion(0) || + info.serverProtocolInfo.Features != nil { + if info.serverProtocolInfo.Version == ProtocolVersion(0) { + return fmt.Errorf("no protocol version provided in Id response") + } + if info.serverProtocolInfo.Features == nil { + return fmt.Errorf("no features provided in Id response") + } + resp.data = []interface{}{info.serverProtocolInfo} + return nil + } + return nil +} + +func decodeCommonField(d *msgpack.Decoder, cd int, data *[]interface{}, + info *decodeInfo) (bool, error) { + var feature iproto.Feature var err error - if resp.buf.Len() > 2 { - var decodedError string + switch iproto.Key(cd) { + case iproto.IPROTO_DATA: + var res interface{} + var ok bool + if res, err = d.DecodeInterface(); err != nil { + return false, err + } + if *data, ok = res.([]interface{}); !ok { + return false, fmt.Errorf("result is not array: %v", res) + } + case iproto.IPROTO_ERROR: + if info.errorExtendedInfo, err = decodeBoxError(d); err != nil { + return false, err + } + case iproto.IPROTO_ERROR_24: + if info.decodedError, err = d.DecodeString(); err != nil { + return false, err + } + case iproto.IPROTO_STMT_ID: + if info.stmtID, err = d.DecodeUint64(); err != nil { + return false, err + } + case iproto.IPROTO_BIND_COUNT: + if info.bindCount, err = d.DecodeUint64(); err != nil { + return false, err + } + case iproto.IPROTO_VERSION: + if err = d.Decode(&info.serverProtocolInfo.Version); err != nil { + return false, err + } + case iproto.IPROTO_FEATURES: + var larr int + if larr, err = d.DecodeArrayLen(); err != nil { + return false, err + } + + info.serverProtocolInfo.Features = make([]iproto.Feature, larr) + for i := 0; i < larr; i++ { + if err = d.Decode(&feature); err != nil { + return false, err + } + info.serverProtocolInfo.Features[i] = feature + } + case iproto.IPROTO_AUTH_TYPE: + var auth string + if auth, err = d.DecodeString(); err != nil { + return false, err + } + found := false + for _, a := range [...]Auth{ChapSha1Auth, PapSha256Auth} { + if auth == a.String() { + info.serverProtocolInfo.Auth = a + found = true + } + } + if !found { + return false, fmt.Errorf("unknown auth type %s", auth) + } + default: + return false, nil + } + return true, nil +} + +func (resp *BaseResponse) Decode() ([]interface{}, error) { + resp.decoded = true + var err error + if resp.buf.Len() > 2 { offset := resp.buf.Offset() defer resp.buf.Seek(offset) - var l, larr int - var stmtID, bindCount uint64 - var serverProtocolInfo ProtocolInfo - var feature iproto.Feature - var errorExtendedInfo *BoxError = nil + var l int + info := &decodeInfo{} d := msgpack.NewDecoder(&resp.buf) d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { @@ -193,117 +330,171 @@ func (resp *ConnResponse) Decode() ([]interface{}, error) { if cd, err = smallInt(d, &resp.buf); err != nil { return nil, err } - switch iproto.Key(cd) { - case iproto.IPROTO_DATA: - var res interface{} - var ok bool - if res, err = d.DecodeInterface(); err != nil { - return nil, err - } - if resp.data, ok = res.([]interface{}); !ok { - return nil, fmt.Errorf("result is not array: %v", res) - } - case iproto.IPROTO_ERROR: - if errorExtendedInfo, err = decodeBoxError(d); err != nil { - return nil, err - } - case iproto.IPROTO_ERROR_24: - if decodedError, err = d.DecodeString(); err != nil { - return nil, err - } - case iproto.IPROTO_SQL_INFO: - if err = d.Decode(&resp.sqlInfo); err != nil { - return nil, err - } - case iproto.IPROTO_METADATA: - if err = d.Decode(&resp.metaData); err != nil { - return nil, err - } - case iproto.IPROTO_STMT_ID: - if stmtID, err = d.DecodeUint64(); err != nil { - return nil, err - } - case iproto.IPROTO_BIND_COUNT: - if bindCount, err = d.DecodeUint64(); err != nil { - return nil, err - } - case iproto.IPROTO_VERSION: - if err = d.Decode(&serverProtocolInfo.Version); err != nil { - return nil, err - } - case iproto.IPROTO_FEATURES: - if larr, err = d.DecodeArrayLen(); err != nil { + decoded, err := decodeCommonField(d, cd, &resp.data, info) + if err != nil { + return nil, err + } + if !decoded { + if err = d.Skip(); err != nil { return nil, err } + } + } + err = info.parseData(resp) + if err != nil { + return nil, err + } - serverProtocolInfo.Features = make([]iproto.Feature, larr) - for i := 0; i < larr; i++ { - if err = d.Decode(&feature); err != nil { - return nil, err + if info.decodedError != "" { + resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) + err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + } + } + return resp.data, err +} + +func (resp *SelectResponse) Decode() ([]interface{}, error) { + resp.decoded = true + var err error + if resp.buf.Len() > 2 { + offset := resp.buf.Offset() + defer resp.buf.Seek(offset) + + var l int + info := &decodeInfo{} + + d := msgpack.NewDecoder(&resp.buf) + d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { + return dec.DecodeUntypedMap() + }) + + if l, err = d.DecodeMapLen(); err != nil { + return nil, err + } + for ; l > 0; l-- { + var cd int + if cd, err = smallInt(d, &resp.buf); err != nil { + return nil, err + } + decoded, err := decodeCommonField(d, cd, &resp.data, info) + if err != nil { + return nil, err + } + if !decoded { + switch iproto.Key(cd) { + case iproto.IPROTO_POSITION: + if resp.pos, err = d.DecodeBytes(); err != nil { + return nil, fmt.Errorf("unable to decode a position: %w", err) } - serverProtocolInfo.Features[i] = feature - } - case iproto.IPROTO_AUTH_TYPE: - var auth string - if auth, err = d.DecodeString(); err != nil { - return nil, err - } - found := false - for _, a := range [...]Auth{ChapSha1Auth, PapSha256Auth} { - if auth == a.String() { - serverProtocolInfo.Auth = a - found = true + default: + if err = d.Skip(); err != nil { + return nil, err } } - if !found { - return nil, fmt.Errorf("unknown auth type %s", auth) - } - case iproto.IPROTO_POSITION: - if resp.pos, err = d.DecodeBytes(); err != nil { - return nil, fmt.Errorf("unable to decode a position: %w", err) - } - default: - if err = d.Skip(); err != nil { - return nil, err - } } } - if stmtID != 0 { - stmt := &Prepared{ - StatementID: PreparedID(stmtID), - ParamCount: bindCount, - MetaData: resp.metaData, - } - resp.data = []interface{}{stmt} + err = info.parseData(&resp.BaseResponse) + if err != nil { + return nil, err + } + + if info.decodedError != "" { + resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) + err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} } + } + return resp.data, err +} + +func (resp *ExecuteResponse) Decode() ([]interface{}, error) { + resp.decoded = true + var err error + if resp.buf.Len() > 2 { + offset := resp.buf.Offset() + defer resp.buf.Seek(offset) + + var l int + info := &decodeInfo{} + + d := msgpack.NewDecoder(&resp.buf) + d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { + return dec.DecodeUntypedMap() + }) - // Tarantool may send only version >= 1 - if serverProtocolInfo.Version != ProtocolVersion(0) || serverProtocolInfo.Features != nil { - if serverProtocolInfo.Version == ProtocolVersion(0) { - return nil, fmt.Errorf("no protocol version provided in Id response") + if l, err = d.DecodeMapLen(); err != nil { + return nil, err + } + for ; l > 0; l-- { + var cd int + if cd, err = smallInt(d, &resp.buf); err != nil { + return nil, err } - if serverProtocolInfo.Features == nil { - return nil, fmt.Errorf("no features provided in Id response") + decoded, err := decodeCommonField(d, cd, &resp.data, info) + if err != nil { + return nil, err + } + if !decoded { + switch iproto.Key(cd) { + case iproto.IPROTO_SQL_INFO: + if err = d.Decode(&resp.sqlInfo); err != nil { + return nil, err + } + case iproto.IPROTO_METADATA: + if err = d.Decode(&resp.metaData); err != nil { + return nil, err + } + default: + if err = d.Skip(); err != nil { + return nil, err + } + } } - resp.data = []interface{}{serverProtocolInfo} + } + err = info.parseData(&resp.BaseResponse) + if err != nil { + return nil, err } - if decodedError != "" { + if info.decodedError != "" { resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), decodedError, errorExtendedInfo} + err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} } } return resp.data, err } -func (resp *ConnResponse) DecodeTyped(res interface{}) error { +func decodeTypedCommonField(d *msgpack.Decoder, res interface{}, cd int, + info *decodeInfo) (bool, error) { + var err error + + switch iproto.Key(cd) { + case iproto.IPROTO_DATA: + if err = d.Decode(res); err != nil { + return false, err + } + case iproto.IPROTO_ERROR: + if info.errorExtendedInfo, err = decodeBoxError(d); err != nil { + return false, err + } + case iproto.IPROTO_ERROR_24: + if info.decodedError, err = d.DecodeString(); err != nil { + return false, err + } + default: + return false, nil + } + return true, nil +} + +func (resp *BaseResponse) DecodeTyped(res interface{}) error { + resp.decodedTyped = true + var err error if resp.buf.Len() > 0 { offset := resp.buf.Offset() defer resp.buf.Seek(offset) - var errorExtendedInfo *BoxError = nil - + info := &decodeInfo{} var l int d := msgpack.NewDecoder(&resp.buf) @@ -314,69 +505,167 @@ func (resp *ConnResponse) DecodeTyped(res interface{}) error { if l, err = d.DecodeMapLen(); err != nil { return err } - var decodedError string for ; l > 0; l-- { var cd int if cd, err = smallInt(d, &resp.buf); err != nil { return err } - switch iproto.Key(cd) { - case iproto.IPROTO_DATA: - if err = d.Decode(res); err != nil { - return err - } - case iproto.IPROTO_ERROR: - if errorExtendedInfo, err = decodeBoxError(d); err != nil { - return err - } - case iproto.IPROTO_ERROR_24: - if decodedError, err = d.DecodeString(); err != nil { - return err - } - case iproto.IPROTO_SQL_INFO: - if err = d.Decode(&resp.sqlInfo); err != nil { - return err - } - case iproto.IPROTO_METADATA: - if err = d.Decode(&resp.metaData); err != nil { + decoded, err := decodeTypedCommonField(d, res, cd, info) + if err != nil { + return err + } + if !decoded { + if err = d.Skip(); err != nil { return err } - case iproto.IPROTO_POSITION: - if resp.pos, err = d.DecodeBytes(); err != nil { - return fmt.Errorf("unable to decode a position: %w", err) + } + } + if info.decodedError != "" { + resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) + err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + } + } + return err +} + +func (resp *SelectResponse) DecodeTyped(res interface{}) error { + resp.decodedTyped = true + + var err error + if resp.buf.Len() > 0 { + offset := resp.buf.Offset() + defer resp.buf.Seek(offset) + + info := &decodeInfo{} + var l int + + d := msgpack.NewDecoder(&resp.buf) + d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { + return dec.DecodeUntypedMap() + }) + + if l, err = d.DecodeMapLen(); err != nil { + return err + } + for ; l > 0; l-- { + var cd int + if cd, err = smallInt(d, &resp.buf); err != nil { + return err + } + decoded, err := decodeTypedCommonField(d, res, cd, info) + if err != nil { + return err + } + if !decoded { + switch iproto.Key(cd) { + case iproto.IPROTO_POSITION: + if resp.pos, err = d.DecodeBytes(); err != nil { + return fmt.Errorf("unable to decode a position: %w", err) + } + default: + if err = d.Skip(); err != nil { + return err + } } - default: - if err = d.Skip(); err != nil { - return err + } + } + if info.decodedError != "" { + resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) + err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + } + } + return err +} + +func (resp *ExecuteResponse) DecodeTyped(res interface{}) error { + resp.decodedTyped = true + + var err error + if resp.buf.Len() > 0 { + offset := resp.buf.Offset() + defer resp.buf.Seek(offset) + + info := &decodeInfo{} + var l int + + d := msgpack.NewDecoder(&resp.buf) + d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { + return dec.DecodeUntypedMap() + }) + + if l, err = d.DecodeMapLen(); err != nil { + return err + } + for ; l > 0; l-- { + var cd int + if cd, err = smallInt(d, &resp.buf); err != nil { + return err + } + decoded, err := decodeTypedCommonField(d, res, cd, info) + if err != nil { + return err + } + if !decoded { + switch iproto.Key(cd) { + case iproto.IPROTO_SQL_INFO: + if err = d.Decode(&resp.sqlInfo); err != nil { + return err + } + case iproto.IPROTO_METADATA: + if err = d.Decode(&resp.metaData); err != nil { + return err + } + default: + if err = d.Skip(); err != nil { + return err + } } } } - if decodedError != "" { + if info.decodedError != "" { resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), decodedError, errorExtendedInfo} + err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} } } return err } -func (resp *ConnResponse) Header() Header { +func (resp *BaseResponse) Header() Header { return resp.header } -func (resp *ConnResponse) Pos() []byte { - return resp.pos +// Pos returns a position descriptor of the last selected tuple for the SelectResponse. +// If the response was not decoded, this method will call Decode(). +func (resp *SelectResponse) Pos() ([]byte, error) { + var err error + if !resp.decoded && !resp.decodedTyped { + _, err = resp.Decode() + } + return resp.pos, err } -func (resp *ConnResponse) MetaData() []ColumnMetaData { - return resp.metaData +// MetaData returns ExecuteResponse meta-data. +// If the response was not decoded, this method will call Decode(). +func (resp *ExecuteResponse) MetaData() ([]ColumnMetaData, error) { + var err error + if !resp.decoded && !resp.decodedTyped { + _, err = resp.Decode() + } + return resp.metaData, err } -func (resp *ConnResponse) SQLInfo() SQLInfo { - return resp.sqlInfo +// SQLInfo returns ExecuteResponse sql info. +// If the response was not decoded, this method will call Decode(). +func (resp *ExecuteResponse) SQLInfo() (SQLInfo, error) { + var err error + if !resp.decoded && !resp.decodedTyped { + _, err = resp.Decode() + } + return resp.sqlInfo, err } // String implements Stringer interface. -func (resp *ConnResponse) String() (str string) { +func (resp *BaseResponse) String() (str string) { if resp.header.Code == OkCode { return fmt.Sprintf("<%d OK %v>", resp.header.RequestId, resp.data) } diff --git a/settings/example_test.go b/settings/example_test.go index 0a1dcc9b7..e51cadef0 100644 --- a/settings/example_test.go +++ b/settings/example_test.go @@ -69,12 +69,23 @@ func Example_sqlFullColumnNames() { // Get some data with SQL query. req = tarantool.NewExecuteRequest("SELECT x FROM example WHERE id = 1;") - _, err = conn.Do(req).Get() + resp, err = conn.Do(req).GetResponse() if err != nil { fmt.Printf("error on select: %v\n", err) return } - metaData := resp.MetaData() + + exResp, ok := resp.(*tarantool.ExecuteResponse) + if !ok { + fmt.Printf("wrong response type") + return + } + + metaData, err := exResp.MetaData() + if err != nil { + fmt.Printf("error on getting MetaData: %v\n", err) + return + } // Show response metadata. fmt.Printf("full column name: %v\n", metaData[0].FieldName) @@ -86,12 +97,21 @@ func Example_sqlFullColumnNames() { } // Get some data with SQL query. - _, err = conn.Do(req).Get() + resp, err = conn.Do(req).GetResponse() if err != nil { fmt.Printf("error on select: %v\n", err) return } - metaData = resp.MetaData() + exResp, ok = resp.(*tarantool.ExecuteResponse) + if !ok { + fmt.Printf("wrong response type") + return + } + metaData, err = exResp.MetaData() + if err != nil { + fmt.Printf("error on getting MetaData: %v\n", err) + return + } // Show response metadata. fmt.Printf("short column name: %v\n", metaData[0].FieldName) } diff --git a/settings/request.go b/settings/request.go index 02252fe47..1c106dc8d 100644 --- a/settings/request.go +++ b/settings/request.go @@ -60,6 +60,7 @@ package settings import ( "context" + "io" "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" @@ -107,6 +108,12 @@ func (req *SetRequest) Async() bool { return req.impl.Async() } +// Response creates a response for the SetRequest. +func (req *SetRequest) Response(header tarantool.Header, + body io.Reader) (tarantool.Response, error) { + return req.impl.Response(header, body) +} + // GetRequest helps to get session settings. type GetRequest struct { impl *tarantool.SelectRequest @@ -147,6 +154,12 @@ func (req *GetRequest) Async() bool { return req.impl.Async() } +// Response creates a response for the GetRequest. +func (req *GetRequest) Response(header tarantool.Header, + body io.Reader) (tarantool.Response, error) { + return req.impl.Response(header, body) +} + // NewErrorMarshalingEnabledSetRequest creates a request to // update current session ErrorMarshalingEnabled setting. func NewErrorMarshalingEnabledSetRequest(value bool) *SetRequest { diff --git a/settings/tarantool_test.go b/settings/tarantool_test.go index 6ca64455a..56cee33ce 100644 --- a/settings/tarantool_test.go +++ b/settings/tarantool_test.go @@ -114,7 +114,11 @@ func TestSQLDefaultEngineSetting(t *testing.T) { resp, err := conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok := resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err := exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) // Check new space engine. eval := tarantool.NewEvalRequest("return box.space['T1_VINYL'].engine") @@ -137,7 +141,11 @@ func TestSQLDefaultEngineSetting(t *testing.T) { resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok = resp.(*tarantool.ExecuteResponse) + sqlInfo, err = exResp.SQLInfo() + require.Nil(t, err) + require.True(t, ok, "wrong response type") + require.Equal(t, uint64(1), sqlInfo.AffectedCount) // Check new space engine. eval = tarantool.NewEvalRequest("return box.space['T2_MEMTX'].engine") @@ -161,7 +169,11 @@ func TestSQLDeferForeignKeysSetting(t *testing.T) { resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok := resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err := exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) // Create a space with reference to the parent space. exec = tarantool.NewExecuteRequest( @@ -169,7 +181,11 @@ func TestSQLDeferForeignKeysSetting(t *testing.T) { resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok = resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err = exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) deferEval := ` box.begin() @@ -229,14 +245,22 @@ func TestSQLFullColumnNamesSetting(t *testing.T) { resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok := resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err := exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) // Fill it with some data. exec = tarantool.NewExecuteRequest("INSERT INTO FKNAME VALUES (1, 1);") resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok = resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err = exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) // Disable displaying full column names in metadata. data, err := conn.Do(NewSQLFullColumnNamesSetRequest(false)).Get() @@ -253,7 +277,11 @@ func TestSQLFullColumnNamesSetting(t *testing.T) { resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, "X", resp.MetaData()[0].FieldName) + exResp, ok = resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + metaData, err := exResp.MetaData() + require.Nil(t, err) + require.Equal(t, "X", metaData[0].FieldName) // Enable displaying full column names in metadata. data, err = conn.Do(NewSQLFullColumnNamesSetRequest(true)).Get() @@ -270,7 +298,11 @@ func TestSQLFullColumnNamesSetting(t *testing.T) { resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, "FKNAME.X", resp.MetaData()[0].FieldName) + exResp, ok = resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + metaData, err = exResp.MetaData() + require.Nil(t, err) + require.Equal(t, "FKNAME.X", metaData[0].FieldName) } func TestSQLFullMetadataSetting(t *testing.T) { @@ -287,14 +319,22 @@ func TestSQLFullMetadataSetting(t *testing.T) { resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok := resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err := exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) // Fill it with some data. exec = tarantool.NewExecuteRequest("INSERT INTO fmt VALUES (1, 1);") resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok = resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err = exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) // Disable displaying additional fields in metadata. data, err := conn.Do(NewSQLFullMetadataSetRequest(false)).Get() @@ -311,7 +351,11 @@ func TestSQLFullMetadataSetting(t *testing.T) { resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, "", resp.MetaData()[0].FieldSpan) + exResp, ok = resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + metaData, err := exResp.MetaData() + require.Nil(t, err) + require.Equal(t, "", metaData[0].FieldSpan) // Enable displaying full column names in metadata. data, err = conn.Do(NewSQLFullMetadataSetRequest(true)).Get() @@ -328,7 +372,11 @@ func TestSQLFullMetadataSetting(t *testing.T) { resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, "x", resp.MetaData()[0].FieldSpan) + exResp, ok = resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + metaData, err = exResp.MetaData() + require.Nil(t, err) + require.Equal(t, "x", metaData[0].FieldSpan) } func TestSQLParserDebugSetting(t *testing.T) { @@ -378,14 +426,22 @@ func TestSQLRecursiveTriggersSetting(t *testing.T) { resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok := resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err := exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) // Fill it with some data. exec = tarantool.NewExecuteRequest("INSERT INTO rec VALUES(1, 1, 2);") resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok = resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err = exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) // Create a recursive trigger (with infinite depth). exec = tarantool.NewExecuteRequest(` @@ -395,7 +451,11 @@ func TestSQLRecursiveTriggersSetting(t *testing.T) { resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok = resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err = exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) // Enable SQL recursive triggers. data, err := conn.Do(NewSQLRecursiveTriggersSetRequest(true)).Get() @@ -429,7 +489,11 @@ func TestSQLRecursiveTriggersSetting(t *testing.T) { resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok = resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err = exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) } func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { @@ -446,20 +510,32 @@ func TestSQLReverseUnorderedSelectsSetting(t *testing.T) { resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok := resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err := exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) // Fill it with some data. exec = tarantool.NewExecuteRequest("INSERT INTO data VALUES('1');") resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok = resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err = exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) exec = tarantool.NewExecuteRequest("INSERT INTO data VALUES('2');") resp, err = conn.Do(exec).GetResponse() require.Nil(t, err) require.NotNil(t, resp) - require.Equal(t, uint64(1), resp.SQLInfo().AffectedCount) + exResp, ok = resp.(*tarantool.ExecuteResponse) + require.True(t, ok, "wrong response type") + sqlInfo, err = exResp.SQLInfo() + require.Nil(t, err) + require.Equal(t, uint64(1), sqlInfo.AffectedCount) // Disable reverse order in unordered selects. data, err := conn.Do(NewSQLReverseUnorderedSelectsSetRequest(false)).Get() diff --git a/tarantool_test.go b/tarantool_test.go index aa3f248f9..3b8bc4653 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -699,6 +699,35 @@ func BenchmarkSQLSerial(b *testing.B) { } } +type mockRequest struct { + conn *Connection +} + +func (req *mockRequest) Type() iproto.Type { + return iproto.Type(0) +} + +func (req *mockRequest) Async() bool { + return false +} + +func (req *mockRequest) Body(resolver SchemaResolver, enc *msgpack.Encoder) error { + return nil +} + +func (req *mockRequest) Conn() *Connection { + return req.conn +} + +func (req *mockRequest) Ctx() context.Context { + return nil +} + +func (req *mockRequest) Response(header Header, + body io.Reader) (Response, error) { + return nil, fmt.Errorf("some error") +} + func TestNetDialer(t *testing.T) { assert := assert.New(t) require := require.New(t) @@ -821,39 +850,25 @@ func TestFutureMultipleGetTypedWithError(t *testing.T) { /////////////////// func TestClient(t *testing.T) { - var resp Response var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Ping - resp, err = conn.Ping() + data, err := conn.Ping() if err != nil { t.Fatalf("Failed to Ping: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Ping") - } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") + if data != nil { + t.Fatalf("Response data is not nil after Ping") } // Insert - resp, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) + data, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) if err != nil { t.Fatalf("Failed to Insert: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Insert") - } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } - data, err := resp.Decode() - if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) - } if len(data) != 1 { t.Errorf("Response Body len != 1") } @@ -870,30 +885,19 @@ func TestClient(t *testing.T) { t.Errorf("Unexpected body of Insert (1)") } } - resp, err = conn.Insert(spaceNo, &Tuple{Id: 1, Msg: "hello", Name: "world"}) + data, err = conn.Insert(spaceNo, &Tuple{Id: 1, Msg: "hello", Name: "world"}) if tntErr, ok := err.(Error); !ok || tntErr.Code != iproto.ER_TUPLE_FOUND { t.Errorf("Expected %s but got: %v", iproto.ER_TUPLE_FOUND, err) } - data, _ = resp.Decode() if len(data) != 0 { t.Errorf("Response Body len != 0") } // Delete - resp, err = conn.Delete(spaceNo, indexNo, []interface{}{uint(1)}) + data, err = conn.Delete(spaceNo, indexNo, []interface{}{uint(1)}) if err != nil { t.Fatalf("Failed to Delete: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Delete") - } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } - data, err = resp.Decode() - if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) - } if len(data) != 1 { t.Errorf("Response Body len != 1") } @@ -910,46 +914,26 @@ func TestClient(t *testing.T) { t.Errorf("Unexpected body of Delete (1)") } } - resp, err = conn.Delete(spaceNo, indexNo, []interface{}{uint(101)}) + data, err = conn.Delete(spaceNo, indexNo, []interface{}{uint(101)}) if err != nil { t.Fatalf("Failed to Delete: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Delete") - } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } - data, err = resp.Decode() - if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) - } if len(data) != 0 { t.Errorf("Response Data len != 0") } // Replace - resp, err = conn.Replace(spaceNo, []interface{}{uint(2), "hello", "world"}) + data, err = conn.Replace(spaceNo, []interface{}{uint(2), "hello", "world"}) if err != nil { t.Fatalf("Failed to Replace: %s", err.Error()) } - if resp == nil { + if data == nil { t.Fatalf("Response is nil after Replace") } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } - resp, err = conn.Replace(spaceNo, []interface{}{uint(2), "hi", "planet"}) + data, err = conn.Replace(spaceNo, []interface{}{uint(2), "hi", "planet"}) if err != nil { t.Fatalf("Failed to Replace (duplicate): %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Replace (duplicate)") - } - data, err = resp.Decode() - if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) - } if len(data) != 1 { t.Errorf("Response Data len != 1") } @@ -968,21 +952,11 @@ func TestClient(t *testing.T) { } // Update - resp, err = conn.Update(spaceNo, indexNo, []interface{}{uint(2)}, + data, err = conn.Update(spaceNo, indexNo, []interface{}{uint(2)}, NewOperations().Assign(1, "bye").Delete(2, 1)) if err != nil { t.Fatalf("Failed to Update: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Update") - } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } - data, err = resp.Decode() - if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) - } if len(data) != 1 { t.Errorf("Response Data len != 1") } @@ -1001,56 +975,39 @@ func TestClient(t *testing.T) { } // Upsert - resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, + data, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, NewOperations().Add(1, 1)) if err != nil { t.Fatalf("Failed to Upsert (insert): %s", err.Error()) } - if resp == nil { + if data == nil { t.Fatalf("Response is nil after Upsert (insert)") } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } - resp, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, + data, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, NewOperations().Add(1, 1)) if err != nil { t.Fatalf("Failed to Upsert (update): %s", err.Error()) } - if resp == nil { + if data == nil { t.Errorf("Response is nil after Upsert (update)") } // Select for i := 10; i < 20; i++ { - resp, err = conn.Replace(spaceNo, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) + data, err = conn.Replace(spaceNo, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) if err != nil { t.Fatalf("Failed to Replace: %s", err.Error()) } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } - _, err := resp.Decode() - if err != nil { - t.Errorf("Failed to replace: %s", err.Error()) + if data == nil { + t.Errorf("Response is nil after Replace") } } - resp, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}) + data, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}) if err != nil { t.Fatalf("Failed to Select: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } - data, err = resp.Decode() - if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) - } if len(data) != 1 { - t.Errorf("Response Data len != 1") + t.Fatalf("Response Data len != 1") } if tpl, ok := data[0].([]interface{}); !ok { t.Errorf("Unexpected body of Select") @@ -1064,20 +1021,10 @@ func TestClient(t *testing.T) { } // Select empty - resp, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}) + data, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}) if err != nil { t.Fatalf("Failed to Select: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Select") - } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } - data, err = resp.Decode() - if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) - } if len(data) != 0 { t.Errorf("Response Data len != 0") } @@ -1137,70 +1084,36 @@ func TestClient(t *testing.T) { } // Call16 - resp, err = conn.Call16("box.info", []interface{}{"box.schema.SPACE_ID"}) + data, err = conn.Call16("box.info", []interface{}{"box.schema.SPACE_ID"}) if err != nil { t.Fatalf("Failed to Call16: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Call16") - } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } - data, err = resp.Decode() - if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) - } if len(data) < 1 { t.Errorf("Response.Data is empty after Eval") } // Call16 vs Call17 - resp, err = conn.Call16("simple_concat", []interface{}{"1"}) + data, err = conn.Call16("simple_concat", []interface{}{"1"}) if err != nil { t.Errorf("Failed to use Call16") } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } - data, err = resp.Decode() - if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) - } if val, ok := data[0].([]interface{})[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", data) } - resp, err = conn.Call17("simple_concat", []interface{}{"1"}) + data, err = conn.Call17("simple_concat", []interface{}{"1"}) if err != nil { t.Errorf("Failed to use Call") } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } - data, err = resp.Decode() - if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) - } if val, ok := data[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", data) } // Eval - resp, err = conn.Eval("return 5 + 6", []interface{}{}) + data, err = conn.Eval("return 5 + 6", []interface{}{}) if err != nil { t.Fatalf("Failed to Eval: %s", err.Error()) } - if resp == nil { - t.Fatalf("Response is nil after Eval") - } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } - data, err = resp.Decode() - if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) - } if len(data) < 1 { t.Errorf("Response.Data is empty after Eval") } @@ -1260,6 +1173,9 @@ func TestClientSessionPush(t *testing.T) { } for i := 0; i < len(its); i++ { + pushCnt := uint64(0) + respCnt := uint64(0) + it = its[i] for it.Next() { resp := it.Value() @@ -1267,9 +1183,6 @@ func TestClientSessionPush(t *testing.T) { t.Errorf("Response is empty after it.Next() == true") break } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } data, err := resp.Decode() if err != nil { t.Errorf("Failed to Decode: %s", err.Error()) @@ -1279,11 +1192,32 @@ func TestClientSessionPush(t *testing.T) { t.Errorf("Response.Data is empty after CallAsync") break } + if it.IsPush() { + pushCnt += 1 + val, err := test_helpers.ConvertUint64(data[0]) + if err != nil || val != pushCnt { + t.Errorf("Unexpected push data = %v", data) + } + } else { + respCnt += 1 + val, err := test_helpers.ConvertUint64(data[0]) + if err != nil || val != pushMax { + t.Errorf("Result is not %d: %v", pushMax, data) + } + } } if err = it.Err(); err != nil { t.Errorf("An unexpected iteration error: %s", err.Error()) } + + if pushCnt != pushMax { + t.Errorf("Expect %d pushes but got %d", pushMax, pushCnt) + } + + if respCnt != 1 { + t.Errorf("Expect %d responses but got %d", 1, respCnt) + } } // We can collect original responses after iterations. @@ -1472,7 +1406,8 @@ func TestSQL(t *testing.T) { defer conn.Close() for i, test := range testCases { - resp, err := conn.Execute(test.Query, test.Args) + req := NewExecuteRequest(test.Query).Args(test.Args) + resp, err := conn.Do(req).GetResponse() assert.NoError(t, err, "Failed to Execute, query: %s", test.Query) assert.NotNil(t, resp, "Response is nil after Execute\nQuery number: %d", i) data, err := resp.Decode() @@ -1480,21 +1415,18 @@ func TestSQL(t *testing.T) { for j := range data { assert.Equal(t, data[j], test.data[j], "Response data is wrong") } - assert.Equal(t, resp.SQLInfo().AffectedCount, test.sqlInfo.AffectedCount, + exResp, ok := resp.(*ExecuteResponse) + assert.True(t, ok, "Got wrong response type") + sqlInfo, err := exResp.SQLInfo() + assert.Nil(t, err, "Error while getting SQLInfo") + assert.Equal(t, sqlInfo.AffectedCount, test.sqlInfo.AffectedCount, "Affected count is wrong") errorMsg := "Response Metadata is wrong" - metaData := resp.MetaData() + metaData, err := exResp.MetaData() + assert.Nil(t, err, "Error while getting MetaData") for j := range metaData { - assert.Equal(t, metaData[j].FieldIsAutoincrement, - test.metaData[j].FieldIsAutoincrement, errorMsg) - assert.Equal(t, metaData[j].FieldIsNullable, - test.metaData[j].FieldIsNullable, errorMsg) - assert.Equal(t, metaData[j].FieldCollation, - test.metaData[j].FieldCollation, errorMsg) - assert.Equal(t, metaData[j].FieldName, test.metaData[j].FieldName, errorMsg) - assert.Equal(t, metaData[j].FieldSpan, test.metaData[j].FieldSpan, errorMsg) - assert.Equal(t, metaData[j].FieldType, test.metaData[j].FieldType, errorMsg) + assert.Equal(t, metaData[j], test.metaData[j], errorMsg) } } } @@ -1575,7 +1507,8 @@ func TestSQLBindings(t *testing.T) { } for _, bind := range namedSQLBinds { - resp, err := conn.Execute(selectNamedQuery2, bind) + req := NewExecuteRequest(selectNamedQuery2).Args(bind) + resp, err := conn.Do(req).GetResponse() if err != nil { t.Fatalf("Failed to Execute: %s", err.Error()) } @@ -1589,7 +1522,10 @@ func TestSQLBindings(t *testing.T) { if reflect.DeepEqual(data[0], []interface{}{1, testData[1]}) { t.Error("Select with named arguments failed") } - metaData := resp.MetaData() + exResp, ok := resp.(*ExecuteResponse) + assert.True(t, ok, "Got wrong response type") + metaData, err := exResp.MetaData() + assert.Nil(t, err, "Error while getting MetaData") if metaData[0].FieldType != "unsigned" || metaData[0].FieldName != "NAME0" || metaData[1].FieldType != "string" || @@ -1598,7 +1534,8 @@ func TestSQLBindings(t *testing.T) { } } - resp, err := conn.Execute(selectPosQuery2, sqlBind5) + req := NewExecuteRequest(selectPosQuery2).Args(sqlBind5) + resp, err := conn.Do(req).GetResponse() if err != nil { t.Fatalf("Failed to Execute: %s", err.Error()) } @@ -1612,7 +1549,10 @@ func TestSQLBindings(t *testing.T) { if reflect.DeepEqual(data[0], []interface{}{1, testData[1]}) { t.Error("Select with positioned arguments failed") } - metaData := resp.MetaData() + exResp, ok := resp.(*ExecuteResponse) + assert.True(t, ok, "Got wrong response type") + metaData, err := exResp.MetaData() + assert.Nil(t, err, "Error while getting MetaData") if metaData[0].FieldType != "unsigned" || metaData[0].FieldName != "NAME0" || metaData[1].FieldType != "string" || @@ -1620,7 +1560,8 @@ func TestSQLBindings(t *testing.T) { t.Error("Wrong metadata") } - resp, err = conn.Execute(mixedQuery, sqlBind6) + req = NewExecuteRequest(mixedQuery).Args(sqlBind6) + resp, err = conn.Do(req).GetResponse() if err != nil { t.Fatalf("Failed to Execute: %s", err.Error()) } @@ -1634,7 +1575,10 @@ func TestSQLBindings(t *testing.T) { if reflect.DeepEqual(data[0], []interface{}{1, testData[1]}) { t.Error("Select with positioned arguments failed") } - metaData = resp.MetaData() + exResp, ok = resp.(*ExecuteResponse) + assert.True(t, ok, "Got wrong response type") + metaData, err = exResp.MetaData() + assert.Nil(t, err, "Error while getting MetaData") if metaData[0].FieldType != "unsigned" || metaData[0].FieldName != "NAME0" || metaData[1].FieldType != "string" || @@ -1646,81 +1590,131 @@ func TestSQLBindings(t *testing.T) { func TestStressSQL(t *testing.T) { test_helpers.SkipIfSQLUnsupported(t) - var resp Response - conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() - resp, err := conn.Execute(createTableQuery, []interface{}{}) + req := NewExecuteRequest(createTableQuery) + resp, err := conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to Execute: %s", err.Error()) + t.Fatalf("Failed to create an Execute: %s", err.Error()) } if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.SQLInfo().AffectedCount != 1 { - t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo().AffectedCount) + exResp, ok := resp.(*ExecuteResponse) + assert.True(t, ok, "Got wrong response type") + sqlInfo, err := exResp.SQLInfo() + assert.Nil(t, err, "Error while getting SQLInfo") + if sqlInfo.AffectedCount != 1 { + t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } // create table with the same name - resp, err = conn.Execute(createTableQuery, []interface{}{}) - if err == nil { - t.Fatal("Unexpected lack of error") + req = NewExecuteRequest(createTableQuery) + resp, err = conn.Do(req).GetResponse() + if err != nil { + t.Fatalf("Failed to create an Execute: %s", err.Error()) } if resp == nil { t.Fatal("Response is nil after Execute") } + _, err = resp.Decode() + assert.NotNil(t, err, "Expected error while decoding") + + tntErr, ok := err.(Error) + assert.True(t, ok) + assert.Equal(t, iproto.ER_SPACE_EXISTS, tntErr.Code) + if iproto.Error(resp.Header().Code) != iproto.ER_SPACE_EXISTS { + t.Fatalf("Unexpected response code: %d", resp.Header().Code) + } - if resp.SQLInfo().AffectedCount != 0 { - t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo().AffectedCount) + exResp, ok = resp.(*ExecuteResponse) + assert.True(t, ok, "Got wrong response type") + sqlInfo, err = exResp.SQLInfo() + assert.Nil(t, err, "Unexpected error") + if sqlInfo.AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } // execute with nil argument - resp, err = conn.Execute(createTableQuery, nil) - if err == nil { - t.Fatal("Unexpected lack of error") + req = NewExecuteRequest(createTableQuery).Args(nil) + resp, err = conn.Do(req).GetResponse() + if err != nil { + t.Fatalf("Failed to create an Execute: %s", err.Error()) } if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.SQLInfo().AffectedCount != 0 { - t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo().AffectedCount) + if resp.Header().Code == OkCode { + t.Fatal("Unexpected successful Execute") + } + exResp, ok = resp.(*ExecuteResponse) + assert.True(t, ok, "Got wrong response type") + sqlInfo, err = exResp.SQLInfo() + assert.NotNil(t, err, "Expected an error") + if sqlInfo.AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } // execute with zero string - resp, err = conn.Execute("", []interface{}{}) - if err == nil { - t.Fatal("Unexpected lack of error") + req = NewExecuteRequest("") + resp, err = conn.Do(req).GetResponse() + if err != nil { + t.Fatalf("Failed to create an Execute: %s", err.Error()) } if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.SQLInfo().AffectedCount != 0 { - t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo().AffectedCount) + if resp.Header().Code == OkCode { + t.Fatal("Unexpected successful Execute") + } + exResp, ok = resp.(*ExecuteResponse) + assert.True(t, ok, "Got wrong response type") + sqlInfo, err = exResp.SQLInfo() + assert.NotNil(t, err, "Expected an error") + if sqlInfo.AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } // drop table query - resp, err = conn.Execute(dropQuery2, []interface{}{}) + req = NewExecuteRequest(dropQuery2) + resp, err = conn.Do(req).GetResponse() if err != nil { t.Fatalf("Failed to Execute: %s", err.Error()) } if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.SQLInfo().AffectedCount != 1 { - t.Errorf("Incorrect count of dropped spaces: %d", resp.SQLInfo().AffectedCount) + exResp, ok = resp.(*ExecuteResponse) + assert.True(t, ok, "Got wrong response type") + sqlInfo, err = exResp.SQLInfo() + assert.Nil(t, err, "Error while getting SQLInfo") + if sqlInfo.AffectedCount != 1 { + t.Errorf("Incorrect count of dropped spaces: %d", sqlInfo.AffectedCount) } // drop the same table - resp, err = conn.Execute(dropQuery2, []interface{}{}) - if err == nil { - t.Fatal("Unexpected lack of error") + req = NewExecuteRequest(dropQuery2) + resp, err = conn.Do(req).GetResponse() + if err != nil { + t.Fatalf("Failed to create an Execute: %s", err.Error()) } if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.SQLInfo().AffectedCount != 0 { - t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo().AffectedCount) + if resp.Header().Code == OkCode { + t.Fatal("Unexpected successful Execute") + } + _, err = resp.Decode() + if err == nil { + t.Fatal("Unexpected lack of error") + } + exResp, ok = resp.(*ExecuteResponse) + assert.True(t, ok, "Got wrong response type") + sqlInfo, err = exResp.SQLInfo() + assert.Nil(t, err, "Error while getting SQLInfo") + if sqlInfo.AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } } @@ -1749,7 +1743,10 @@ func TestNewPrepared(t *testing.T) { if reflect.DeepEqual(data[0], []interface{}{1, "test"}) { t.Error("Select with named arguments failed") } - metaData := resp.MetaData() + prepResp, ok := resp.(*ExecuteResponse) + assert.True(t, ok, "Got wrong response type") + metaData, err := prepResp.MetaData() + assert.Nil(t, err, "Error while getting MetaData") if metaData[0].FieldType != "unsigned" || metaData[0].FieldName != "NAME0" || metaData[1].FieldType != "string" || @@ -1786,7 +1783,7 @@ func TestNewPrepared(t *testing.T) { if len(data) == 0 { t.Errorf("failed to prepare: response Data has no elements") } - stmt, ok := data[0].(*Prepared) + stmt, ok = data[0].(*Prepared) if !ok { t.Errorf("failed to prepare: failed to cast the response Data to Prepared object") } @@ -1811,6 +1808,18 @@ func TestConnection_DoWithStrangerConn(t *testing.T) { } } +func TestConnection_SetResponse_failed(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, dialer, opts) + defer conn.Close() + + req := mockRequest{conn} + fut := conn.Do(&req) + + data, err := fut.Get() + assert.EqualError(t, err, "failed to set response: some error") + assert.Nil(t, data) +} + func TestGetSchema(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() @@ -2043,85 +2052,82 @@ func TestSchema_IsNullable(t *testing.T) { } func TestClientNamed(t *testing.T) { - var resp Response - var err error - conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() // Insert - resp, err = conn.Insert(spaceName, []interface{}{uint(1001), "hello2", "world2"}) + data, err := conn.Insert(spaceName, []interface{}{uint(1001), "hello2", "world2"}) if err != nil { t.Fatalf("Failed to Insert: %s", err.Error()) } - if resp == nil { + if data == nil { t.Errorf("Response is nil after Insert") } // Delete - resp, err = conn.Delete(spaceName, indexName, []interface{}{uint(1001)}) + data, err = conn.Delete(spaceName, indexName, []interface{}{uint(1001)}) if err != nil { t.Fatalf("Failed to Delete: %s", err.Error()) } - if resp == nil { + if data == nil { t.Errorf("Response is nil after Delete") } // Replace - resp, err = conn.Replace(spaceName, []interface{}{uint(1002), "hello", "world"}) + data, err = conn.Replace(spaceName, []interface{}{uint(1002), "hello", "world"}) if err != nil { t.Fatalf("Failed to Replace: %s", err.Error()) } - if resp == nil { + if data == nil { t.Errorf("Response is nil after Replace") } // Update - resp, err = conn.Update(spaceName, indexName, + data, err = conn.Update(spaceName, indexName, []interface{}{ uint(1002)}, NewOperations().Assign(1, "buy").Delete(2, 1)) if err != nil { t.Fatalf("Failed to Update: %s", err.Error()) } - if resp == nil { + if data == nil { t.Errorf("Response is nil after Update") } // Upsert - resp, err = conn.Upsert(spaceName, + data, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, NewOperations().Add(1, 1)) if err != nil { t.Fatalf("Failed to Upsert (insert): %s", err.Error()) } - if resp == nil { + if data == nil { t.Errorf("Response is nil after Upsert (insert)") } - resp, err = conn.Upsert(spaceName, + data, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, NewOperations().Add(1, 1)) if err != nil { t.Fatalf("Failed to Upsert (update): %s", err.Error()) } - if resp == nil { + if data == nil { t.Errorf("Response is nil after Upsert (update)") } // Select for i := 1010; i < 1020; i++ { - resp, err = conn.Replace(spaceName, + data, err = conn.Replace(spaceName, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) if err != nil { t.Fatalf("Failed to Replace: %s", err.Error()) } - if resp == nil { + if data == nil { t.Errorf("Response is nil after Replace") } } - resp, err = conn.Select(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)}) + data, err = conn.Select(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)}) if err != nil { t.Fatalf("Failed to Select: %s", err.Error()) } - if resp == nil { + if data == nil { t.Errorf("Response is nil after Select") } @@ -2375,9 +2381,6 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } data, err = resp.Decode() if err != nil { t.Fatalf("Failed to Decode: %s", err.Error()) @@ -2385,8 +2388,12 @@ func TestClientRequestObjects(t *testing.T) { if len(data) != 0 { t.Fatalf("Response Body len != 0") } - if resp.SQLInfo().AffectedCount != 1 { - t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo().AffectedCount) + exResp, ok := resp.(*ExecuteResponse) + assert.True(t, ok, "Got wrong response type") + sqlInfo, err := exResp.SQLInfo() + assert.Nil(t, err, "Error while getting SQLInfo") + if sqlInfo.AffectedCount != 1 { + t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } req = NewExecuteRequest(dropQuery2) @@ -2397,9 +2404,6 @@ func TestClientRequestObjects(t *testing.T) { if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Pos() != nil { - t.Errorf("Response should not have a position") - } data, err = resp.Decode() if err != nil { t.Fatalf("Failed to Decode: %s", err.Error()) @@ -2407,8 +2411,12 @@ func TestClientRequestObjects(t *testing.T) { if len(data) != 0 { t.Fatalf("Response Body len != 0") } - if resp.SQLInfo().AffectedCount != 1 { - t.Errorf("Incorrect count of dropped spaces: %d", resp.SQLInfo().AffectedCount) + exResp, ok = resp.(*ExecuteResponse) + assert.True(t, ok, "Got wrong response type") + sqlInfo, err = exResp.SQLInfo() + assert.Nil(t, err, "Error while getting SQLInfo") + if sqlInfo.AffectedCount != 1 { + t.Errorf("Incorrect count of dropped spaces: %d", sqlInfo.AffectedCount) } } @@ -2425,7 +2433,7 @@ func testConnectionDoSelectRequestPrepare(t *testing.T, conn Connector) { } func testConnectionDoSelectRequestCheck(t *testing.T, - resp Response, err error, pos bool, dataLen int, firstKey uint64) { + resp *SelectResponse, err error, pos bool, dataLen int, firstKey uint64) { t.Helper() if err != nil { @@ -2434,10 +2442,14 @@ func testConnectionDoSelectRequestCheck(t *testing.T, if resp == nil { t.Fatalf("Response is nil after Select") } - if !pos && resp.Pos() != nil { + respPos, err := resp.Pos() + if err != nil { + t.Errorf("Error while getting Pos: %s", err.Error()) + } + if !pos && respPos != nil { t.Errorf("Response should not have a position descriptor") } - if pos && resp.Pos() == nil { + if pos && respPos == nil { t.Fatalf("A response must have a position descriptor") } data, err := resp.Decode() @@ -2482,7 +2494,10 @@ func TestConnectionDoSelectRequest(t *testing.T) { Key([]interface{}{uint(1010)}) resp, err := conn.Do(req).GetResponse() - testConnectionDoSelectRequestCheck(t, resp, err, false, 10, 1010) + selResp, ok := resp.(*SelectResponse) + assert.True(t, ok, "Got wrong response type") + + testConnectionDoSelectRequestCheck(t, selResp, err, false, 10, 1010) } func TestConnectionDoWatchOnceRequest(t *testing.T) { @@ -2542,7 +2557,10 @@ func TestConnectionDoSelectRequest_fetch_pos(t *testing.T) { Key([]interface{}{uint(1010)}) resp, err := conn.Do(req).GetResponse() - testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1010) + selResp, ok := resp.(*SelectResponse) + assert.True(t, ok, "Got wrong response type") + + testConnectionDoSelectRequestCheck(t, selResp, err, true, 2, 1010) } func TestConnectDoSelectRequest_after_tuple(t *testing.T) { @@ -2562,7 +2580,10 @@ func TestConnectDoSelectRequest_after_tuple(t *testing.T) { After([]interface{}{uint(1012)}) resp, err := conn.Do(req).GetResponse() - testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1013) + selResp, ok := resp.(*SelectResponse) + assert.True(t, ok, "Got wrong response type") + + testConnectionDoSelectRequestCheck(t, selResp, err, true, 2, 1013) } func TestConnectionDoSelectRequest_pagination_pos(t *testing.T) { @@ -2581,28 +2602,29 @@ func TestConnectionDoSelectRequest_pagination_pos(t *testing.T) { Key([]interface{}{uint(1010)}) resp, err := conn.Do(req).GetResponse() - testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1010) + selResp, ok := resp.(*SelectResponse) + assert.True(t, ok, "Got wrong response type") + + testConnectionDoSelectRequestCheck(t, selResp, err, true, 2, 1010) + + selPos, err := selResp.Pos() + assert.Nil(t, err, "Error while getting Pos") - resp, err = conn.Do(req.After(resp.Pos())).GetResponse() + resp, err = conn.Do(req.After(selPos)).GetResponse() + selResp, ok = resp.(*SelectResponse) + assert.True(t, ok, "Got wrong response type") - testConnectionDoSelectRequestCheck(t, resp, err, true, 2, 1012) + testConnectionDoSelectRequestCheck(t, selResp, err, true, 2, 1012) } func TestConnection_Call(t *testing.T) { - var resp Response - var err error - conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() - resp, err = conn.Call("simple_concat", []interface{}{"1"}) + data, err := conn.Call("simple_concat", []interface{}{"1"}) if err != nil { t.Errorf("Failed to use Call") } - data, err := resp.Decode() - if err != nil { - t.Errorf("Failed to Decode: %s", err.Error()) - } if val, ok := data[0].(string); !ok || val != "11" { t.Errorf("result is not {{1}} : %v", data) } @@ -2678,6 +2700,12 @@ func (req *waitCtxRequest) Async() bool { return NewPingRequest().Async() } +func (req *waitCtxRequest) Response(header Header, body io.Reader) (Response, error) { + resp := BaseResponse{} + resp.SetHeader(header) + return &resp, nil +} + func TestClientRequestObjectsWithContext(t *testing.T) { var err error conn := test_helpers.ConnectWithValidation(t, dialer, opts) diff --git a/test_helpers/main.go b/test_helpers/main.go index 0a0c7f1cd..200c3f474 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -83,7 +83,6 @@ type TarantoolInstance struct { func isReady(dialer tarantool.Dialer, opts *tarantool.Opts) error { var err error var conn *tarantool.Connection - var resp tarantool.Response ctx, cancel := GetConnectContext() defer cancel() @@ -96,13 +95,10 @@ func isReady(dialer tarantool.Dialer, opts *tarantool.Opts) error { } defer conn.Close() - resp, err = conn.Do(tarantool.NewPingRequest()).GetResponse() + _, err = conn.Do(tarantool.NewPingRequest()).Get() if err != nil { return err } - if resp == nil { - return errors.New("response is nil after ping") - } return nil } diff --git a/test_helpers/request_mock.go b/test_helpers/request_mock.go index 80ca1b541..980c35db2 100644 --- a/test_helpers/request_mock.go +++ b/test_helpers/request_mock.go @@ -2,6 +2,7 @@ package test_helpers import ( "context" + "io" "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" @@ -35,3 +36,10 @@ func (sr *StrangerRequest) Conn() *tarantool.Connection { func (sr *StrangerRequest) Ctx() context.Context { return nil } + +func (sr *StrangerRequest) Response(header tarantool.Header, + body io.Reader) (tarantool.Response, error) { + resp := tarantool.BaseResponse{} + resp.SetHeader(header) + return &resp, nil +} diff --git a/watch.go b/watch.go index 0508899f0..c147b0399 100644 --- a/watch.go +++ b/watch.go @@ -2,6 +2,7 @@ package tarantool import ( "context" + "io" "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" @@ -55,6 +56,15 @@ func (req *BroadcastRequest) Async() bool { return req.call.Async() } +// Response creates a response for a BroadcastRequest. +func (req *BroadcastRequest) Response(header Header, body io.Reader) (Response, error) { + resp, err := createBaseResponse(header, body) + if err != nil { + return nil, err + } + return &resp, nil +} + // watchRequest subscribes to the updates of a specified key defined on the // server. After receiving the notification, you should send a new // watchRequest to acknowledge the notification. From 514b0d0601c8a9667940ad0536f50a73007076ca Mon Sep 17 00:00:00 2001 From: DerekBum Date: Tue, 26 Dec 2023 16:14:24 +0300 Subject: [PATCH 519/605] api: add ability to mock connections in tests Create a mock implementations `MockRequest`, `MockResponse` and `MockDoer`. The last one allows to mock not the full `Connection`, but its part -- a structure, that implements new `Doer` interface (a `Do` function). Also added missing comments, all the changes are recorded in the `CHANGELOG` and `README` files. Added new tests and examples. So this entity is easier to implement and it is enough to mock tests that require working `Connection`. All new mock structs and an example for `MockDoer` usage are added to the `test_helpers` package. Added new structs `MockDoer`, `MockRequest` to `test_helpers`. Renamed `StrangerResponse` to `MockResponse`. Added new connection log constant: `LogAppendPushFailed`. It is logged when connection fails to append a push response. Closes #237 --- CHANGELOG.md | 7 +- README.md | 17 ++- connection.go | 13 +- connector.go | 8 +- const.go | 4 +- dial.go | 35 ++++-- example_test.go | 61 ++++++++- future.go | 12 +- future_test.go | 163 +++++++++++++++++++++--- header.go | 10 +- pool/connection_pool.go | 2 +- pool/connection_pool_test.go | 5 +- prepared.go | 4 +- request.go | 4 +- request_test.go | 160 ++++++++++++++++-------- response.go | 181 +++++++++++++++------------ schema.go | 14 ++- schema_test.go | 162 ++++++++++++++++++++++++ stream.go | 2 +- tarantool_test.go | 235 ++++++++++++++++++++--------------- test_helpers/doer.go | 69 ++++++++++ test_helpers/example_test.go | 36 ++++++ test_helpers/request.go | 52 ++++++++ test_helpers/request_mock.go | 45 ------- test_helpers/response.go | 74 +++++++++++ 25 files changed, 1027 insertions(+), 348 deletions(-) create mode 100644 schema_test.go create mode 100644 test_helpers/doer.go create mode 100644 test_helpers/example_test.go create mode 100644 test_helpers/request.go delete mode 100644 test_helpers/request_mock.go create mode 100644 test_helpers/response.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e15812510..d0f696cc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,10 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - `Response` method added to the `Request` interface (#237) - New `LogAppendPushFailed` connection log constant (#237). It is logged when connection fails to append a push response. +- `ErrorNo` constant that indicates that no error has occurred while getting + the response (#237) +- Ability to mock connections for tests (#237). Added new types `MockDoer`, + `MockRequest` to `test_helpers`. ### Changed @@ -88,6 +92,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` and `Pooler` return response data instead of an actual responses (#237) +- Renamed `StrangerResponse` to `MockResponse` (#237) ### Deprecated @@ -110,7 +115,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - IPROTO constants (#158) - Code() method from the Request interface (#158) - `Schema` field from the `Connection` struct (#7) -- `PushCode` constant (#237) +- `OkCode` and `PushCode` constants (#237) ### Fixed diff --git a/README.md b/README.md index 4f82896de..5472e50a2 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,10 @@ The subpackage has been deleted. You could use `pool` instead. * `crud` operations `Timeout` option has `crud.OptFloat64` type instead of `crud.OptUint`. +#### test_helpers package + +Renamed `StrangerResponse` to `MockResponse`. + #### msgpack.v5 Most function names and argument types in `msgpack.v5` and `msgpack.v2` @@ -248,6 +252,8 @@ of the requests is an array instead of array of arrays. * IPROTO constants have been moved to a separate package [go-iproto](https://github.com/tarantool/go-iproto). * `PushCode` constant is removed. To check whether the current response is a push response, use `IsPush()` method of the response iterator instead. +* `ErrorNo` constant is added to indicate that no error has occurred while + getting the response. It should be used instead of the removed `OkCode`. #### Request changes @@ -285,9 +291,10 @@ for an `ops` field. `*Operations` needs to be used instead. #### Connector changes -Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, -`Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` return -response data instead of an actual responses. +* Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, + `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` return + response data instead of an actual responses. +* New interface `Doer` is added as a child-interface instead of a `Do` method. #### Connect function @@ -304,8 +311,8 @@ for creating a connection are now stored in corresponding `Dialer`, not in `Opts #### Connection schema * Removed `Schema` field from the `Connection` struct. Instead, new -`GetSchema(Connector)` function was added to get the actual connection -schema on demand. + `GetSchema(Doer)` function was added to get the actual connection + schema on demand. * `OverrideSchema(*Schema)` method replaced with the `SetSchema(Schema)`. #### Protocol changes diff --git a/connection.go b/connection.go index 8e3939190..8f8631a31 100644 --- a/connection.go +++ b/connection.go @@ -813,7 +813,7 @@ func (conn *Connection) reader(r io.Reader, c Conn) { return } buf := smallBuf{b: respBytes} - header, err := decodeHeader(conn.dec, &buf) + header, code, err := decodeHeader(conn.dec, &buf) if err != nil { err = ClientError{ ErrProtocolError, @@ -824,7 +824,7 @@ func (conn *Connection) reader(r io.Reader, c Conn) { } var fut *Future = nil - if iproto.Type(header.Code) == iproto.IPROTO_EVENT { + if code == iproto.IPROTO_EVENT { if event, err := readWatchEvent(&buf); err == nil { events <- event } else { @@ -835,7 +835,7 @@ func (conn *Connection) reader(r io.Reader, c Conn) { conn.opts.Logger.Report(LogWatchEventReadFailed, conn, err) } continue - } else if header.Code == uint32(iproto.IPROTO_CHUNK) { + } else if code == iproto.IPROTO_CHUNK { if fut = conn.peekFuture(header.RequestId); fut != nil { if err := fut.AppendPush(header, &buf); err != nil { err = ClientError{ @@ -887,8 +887,7 @@ func (conn *Connection) eventer(events <-chan connWatchEvent) { func (conn *Connection) newFuture(req Request) (fut *Future) { ctx := req.Ctx() - fut = NewFuture() - fut.SetRequest(req) + fut = NewFuture(req) if conn.rlimit != nil && conn.opts.RLimitAction == RLimitDrop { select { case conn.rlimit <- struct{}{}: @@ -1069,7 +1068,7 @@ func (conn *Connection) putFuture(fut *Future, req Request, streamId uint64) { if fut = conn.fetchFuture(reqid); fut != nil { header := Header{ RequestId: reqid, - Code: OkCode, + Error: ErrorNo, } fut.SetResponse(header, nil) conn.markDone(fut) @@ -1217,7 +1216,7 @@ func (conn *Connection) nextRequestId(context bool) (requestId uint32) { func (conn *Connection) Do(req Request) *Future { if connectedReq, ok := req.(ConnectedRequest); ok { if connectedReq.Conn() != conn { - fut := NewFuture() + fut := NewFuture(req) fut.SetError(errUnknownRequest) return fut } diff --git a/connector.go b/connector.go index 72b5d19a8..9917cff0f 100644 --- a/connector.go +++ b/connector.go @@ -2,14 +2,20 @@ package tarantool import "time" +// Doer is an interface that performs requests asynchronously. +type Doer interface { + // Do performs a request asynchronously. + Do(req Request) (fut *Future) +} + type Connector interface { + Doer ConnectedNow() bool Close() error ConfiguredTimeout() time.Duration NewPrepared(expr string) (*Prepared, error) NewStream() (*Stream, error) NewWatcher(key string, callback WatchCallback) (Watcher, error) - Do(req Request) (fut *Future) // Deprecated: the method will be removed in the next major version, // use a PingRequest object + Do() instead. diff --git a/const.go b/const.go index ede4c988d..e8f389253 100644 --- a/const.go +++ b/const.go @@ -9,5 +9,7 @@ const ( ) const ( - OkCode = uint32(iproto.IPROTO_OK) + // ErrorNo indicates that no error has occurred. It could be used to + // check that a response has an error without the response body decoding. + ErrorNo = iproto.ER_UNKNOWN ) diff --git a/dial.go b/dial.go index 378687925..ff5419760 100644 --- a/dial.go +++ b/dial.go @@ -398,16 +398,17 @@ func identify(w writeFlusher, r io.Reader) (ProtocolInfo, error) { return info, err } - resp, err := readResponse(r) + resp, err := readResponse(r, req) if err != nil { + if resp != nil && + resp.Header().Error == iproto.ER_UNKNOWN_REQUEST_TYPE { + // IPROTO_ID requests are not supported by server. + return info, nil + } return info, err } data, err := resp.Decode() if err != nil { - if iproto.Error(resp.Header().Code) == iproto.ER_UNKNOWN_REQUEST_TYPE { - // IPROTO_ID requests are not supported by server. - return info, nil - } return info, err } @@ -477,7 +478,7 @@ func authenticate(c Conn, auth Auth, user string, pass string, salt string) erro if err = writeRequest(c, req); err != nil { return err } - if _, err = readResponse(c); err != nil { + if _, err = readResponse(c, req); err != nil { return err } return nil @@ -501,19 +502,31 @@ func writeRequest(w writeFlusher, req Request) error { } // readResponse reads a response from the reader. -func readResponse(r io.Reader) (Response, error) { +func readResponse(r io.Reader, req Request) (Response, error) { var lenbuf [packetLengthBytes]byte respBytes, err := read(r, lenbuf[:]) if err != nil { - return &BaseResponse{}, fmt.Errorf("read error: %w", err) + return nil, fmt.Errorf("read error: %w", err) } buf := smallBuf{b: respBytes} - header, err := decodeHeader(msgpack.NewDecoder(&smallBuf{}), &buf) - resp := &BaseResponse{header: header, buf: buf} + header, _, err := decodeHeader(msgpack.NewDecoder(&smallBuf{}), &buf) if err != nil { - return resp, fmt.Errorf("decode response header error: %w", err) + return nil, fmt.Errorf("decode response header error: %w", err) + } + resp, err := req.Response(header, &buf) + if err != nil { + return nil, fmt.Errorf("creating response error: %w", err) + } + _, err = resp.Decode() + if err != nil { + switch err.(type) { + case Error: + return resp, err + default: + return resp, fmt.Errorf("decode response body error: %w", err) + } } return resp, nil } diff --git a/example_test.go b/example_test.go index 3c4d12112..965c25a82 100644 --- a/example_test.go +++ b/example_test.go @@ -198,16 +198,34 @@ func ExampleSelectRequest() { } key := []interface{}{uint(1111)} - data, err := conn.Do(tarantool.NewSelectRequest(617). + resp, err := conn.Do(tarantool.NewSelectRequest(617). Limit(100). Iterator(tarantool.IterEq). Key(key), - ).Get() + ).GetResponse() if err != nil { fmt.Printf("error in select is %v", err) return } + selResp, ok := resp.(*tarantool.SelectResponse) + if !ok { + fmt.Print("wrong response type") + return + } + + pos, err := selResp.Pos() + if err != nil { + fmt.Printf("error in Pos: %v", err) + return + } + fmt.Printf("pos for Select is %v\n", pos) + + data, err := resp.Decode() + if err != nil { + fmt.Printf("error while decoding: %v", err) + return + } fmt.Printf("response is %#v\n", data) var res []Tuple @@ -224,6 +242,7 @@ func ExampleSelectRequest() { fmt.Printf("response is %v\n", res) // Output: + // pos for Select is [] // response is []interface {}{[]interface {}{0x457, "hello", "world"}} // response is [{{} 1111 hello world}] } @@ -567,17 +586,21 @@ func ExampleExecuteRequest() { resp, err := conn.Do(req).GetResponse() fmt.Println("Execute") fmt.Println("Error", err) + data, err := resp.Decode() fmt.Println("Error", err) fmt.Println("Data", data) + exResp, ok := resp.(*tarantool.ExecuteResponse) if !ok { fmt.Printf("wrong response type") return } + metaData, err := exResp.MetaData() fmt.Println("MetaData", metaData) fmt.Println("Error", err) + sqlInfo, err := exResp.SQLInfo() fmt.Println("SQL Info", sqlInfo) fmt.Println("Error", err) @@ -992,6 +1015,26 @@ func ExampleBeginRequest_TxnIsolation() { fmt.Printf("Select after Rollback: response is %#v\n", data) } +func ExampleErrorNo() { + conn := exampleConnect(dialer, opts) + defer conn.Close() + + req := tarantool.NewPingRequest() + resp, err := conn.Do(req).GetResponse() + if err != nil { + fmt.Printf("error getting the response: %s\n", err) + return + } + + if resp.Header().Error != tarantool.ErrorNo { + fmt.Printf("response error code: %s\n", resp.Header().Error) + } else { + fmt.Println("Success.") + } + // Output: + // Success. +} + func ExampleFuture_GetIterator() { conn := exampleConnect(dialer, opts) defer conn.Close() @@ -1008,11 +1051,11 @@ func ExampleFuture_GetIterator() { if it.IsPush() { // It is a push message. fmt.Printf("push message: %v\n", data[0]) - } else if resp.Header().Code == tarantool.OkCode { + } else if resp.Header().Error == tarantool.ErrorNo { // It is a regular response. fmt.Printf("response: %v", data[0]) } else { - fmt.Printf("an unexpected response code %d", resp.Header().Code) + fmt.Printf("an unexpected response code %d", resp.Header().Error) } } if err := it.Err(); err != nil { @@ -1224,6 +1267,11 @@ func ExampleConnection_Do_failure() { if err != nil { fmt.Printf("Error in the future: %s\n", err) } + // Optional step: check a response error. + // It allows checking that response has or hasn't an error without decoding. + if resp.Header().Error != tarantool.ErrorNo { + fmt.Printf("Response error: %s\n", resp.Header().Error) + } data, err := future.Get() if err != nil { @@ -1239,8 +1287,8 @@ func ExampleConnection_Do_failure() { } else { // Response exist. So it could be a Tarantool error or a decode // error. We need to check the error code. - fmt.Printf("Error code from the response: %d\n", resp.Header().Code) - if resp.Header().Code == tarantool.OkCode { + fmt.Printf("Error code from the response: %d\n", resp.Header().Error) + if resp.Header().Error == tarantool.ErrorNo { fmt.Printf("Decode error: %s\n", err) } else { code := err.(tarantool.Error).Code @@ -1251,6 +1299,7 @@ func ExampleConnection_Do_failure() { } // Output: + // Response error: ER_NO_SUCH_PROC // Data: [] // Error code from the response: 33 // Error code from the error: 33 diff --git a/future.go b/future.go index 139782637..1b9f2ed14 100644 --- a/future.go +++ b/future.go @@ -121,7 +121,7 @@ func (it *asyncResponseIterator) nextResponse() (resp Response) { // PushResponse is used for push requests for the Future. type PushResponse struct { - BaseResponse + baseResponse } func createPushResponse(header Header, body io.Reader) (Response, error) { @@ -132,12 +132,13 @@ func createPushResponse(header Header, body io.Reader) (Response, error) { return &PushResponse{resp}, nil } -// NewFuture creates a new empty Future. -func NewFuture() (fut *Future) { +// NewFuture creates a new empty Future for a given Request. +func NewFuture(req Request) (fut *Future) { fut = &Future{} fut.ready = make(chan struct{}, 1000000000) fut.done = make(chan struct{}) fut.pushes = make([]Response, 0) + fut.req = req return fut } @@ -163,11 +164,6 @@ func (fut *Future) AppendPush(header Header, body io.Reader) error { return nil } -// SetRequest sets a request, for which the future was created. -func (fut *Future) SetRequest(req Request) { - fut.req = req -} - // SetResponse sets a response for the future and finishes the future. func (fut *Future) SetResponse(header Header, body io.Reader) error { fut.mutex.Lock() diff --git a/future_test.go b/future_test.go index 947e4ed28..6efda10a1 100644 --- a/future_test.go +++ b/future_test.go @@ -1,15 +1,86 @@ package tarantool_test import ( + "bytes" + "context" "errors" + "io" + "io/ioutil" "sync" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/tarantool/go-iproto" . "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/vmihailenco/msgpack/v5" ) +type futureMockRequest struct { +} + +func (req *futureMockRequest) Type() iproto.Type { + return iproto.Type(0) +} + +func (req *futureMockRequest) Async() bool { + return false +} + +func (req *futureMockRequest) Body(resolver SchemaResolver, enc *msgpack.Encoder) error { + return nil +} + +func (req *futureMockRequest) Conn() *Connection { + return &Connection{} +} + +func (req *futureMockRequest) Ctx() context.Context { + return nil +} + +func (req *futureMockRequest) Response(header Header, + body io.Reader) (Response, error) { + resp, err := createFutureMockResponse(header, body) + return resp, err +} + +type futureMockResponse struct { + header Header + data []byte + + decodeCnt int + decodeTypedCnt int +} + +func (resp *futureMockResponse) Header() Header { + return resp.header +} + +func (resp *futureMockResponse) Decode() ([]interface{}, error) { + resp.decodeCnt++ + + dataInt := make([]interface{}, len(resp.data)) + for i := range resp.data { + dataInt[i] = resp.data[i] + } + return dataInt, nil +} + +func (resp *futureMockResponse) DecodeTyped(res interface{}) error { + resp.decodeTypedCnt++ + return nil +} + +func createFutureMockResponse(header Header, body io.Reader) (Response, error) { + data, err := ioutil.ReadAll(body) + if err != nil { + return nil, err + } + return &futureMockResponse{header: header, data: data}, nil +} + func assertResponseIteratorValue(t testing.TB, it ResponseIterator, isPush bool, resp Response) { t.Helper() @@ -43,7 +114,7 @@ func assertResponseIteratorFinished(t testing.TB, it ResponseIterator) { } func TestFutureGetIteratorNoItems(t *testing.T) { - fut := NewFuture() + fut := NewFuture(test_helpers.NewMockRequest()) it := fut.GetIterator() if it.Next() { @@ -56,7 +127,7 @@ func TestFutureGetIteratorNoItems(t *testing.T) { func TestFutureGetIteratorNoResponse(t *testing.T) { pushHeader := Header{} push := &PushResponse{} - fut := NewFuture() + fut := NewFuture(test_helpers.NewMockRequest()) fut.AppendPush(pushHeader, nil) if it := fut.GetIterator(); it.Next() { @@ -73,7 +144,7 @@ func TestFutureGetIteratorNoResponse(t *testing.T) { func TestFutureGetIteratorNoResponseTimeout(t *testing.T) { pushHeader := Header{} push := &PushResponse{} - fut := NewFuture() + fut := NewFuture(test_helpers.NewMockRequest()) fut.AppendPush(pushHeader, nil) if it := fut.GetIterator().WithTimeout(1 * time.Nanosecond); it.Next() { @@ -91,8 +162,8 @@ func TestFutureGetIteratorResponseOnTimeout(t *testing.T) { pushHeader := Header{} respHeader := Header{} push := &PushResponse{} - resp := &BaseResponse{} - fut := NewFuture() + resp := &test_helpers.MockResponse{} + fut := NewFuture(test_helpers.NewMockRequest()) fut.AppendPush(pushHeader, nil) var done sync.WaitGroup @@ -128,15 +199,13 @@ func TestFutureGetIteratorResponseOnTimeout(t *testing.T) { wait.Wait() - fut.SetRequest(&InsertRequest{}) fut.SetResponse(respHeader, nil) done.Wait() } func TestFutureGetIteratorFirstResponse(t *testing.T) { - resp := &BaseResponse{} - fut := NewFuture() - fut.SetRequest(&InsertRequest{}) + resp := &test_helpers.MockResponse{} + fut := NewFuture(test_helpers.NewMockRequest()) fut.SetResponse(Header{}, nil) fut.SetResponse(Header{}, nil) @@ -155,7 +224,7 @@ func TestFutureGetIteratorFirstError(t *testing.T) { const errMsg1 = "error1" const errMsg2 = "error2" - fut := NewFuture() + fut := NewFuture(test_helpers.NewMockRequest()) fut.SetError(errors.New(errMsg1)) fut.SetError(errors.New(errMsg2)) @@ -173,11 +242,10 @@ func TestFutureGetIteratorResponse(t *testing.T) { responses := []Response{ &PushResponse{}, &PushResponse{}, - &BaseResponse{}, + &test_helpers.MockResponse{}, } header := Header{} - fut := NewFuture() - fut.SetRequest(&InsertRequest{}) + fut := NewFuture(test_helpers.NewMockRequest()) for i := range responses { if i == len(responses)-1 { fut.SetResponse(header, nil) @@ -215,7 +283,7 @@ func TestFutureGetIteratorError(t *testing.T) { {}, } err := errors.New(errMsg) - fut := NewFuture() + fut := NewFuture(test_helpers.NewMockRequest()) for range responses { fut.AppendPush(Header{}, nil) } @@ -249,8 +317,7 @@ func TestFutureSetStateRaceCondition(t *testing.T) { err := errors.New("any error") for i := 0; i < 1000; i++ { - fut := NewFuture() - fut.SetRequest(&InsertRequest{}) + fut := NewFuture(test_helpers.NewMockRequest()) for j := 0; j < 9; j++ { go func(opt int) { if opt%3 == 0 { @@ -266,3 +333,67 @@ func TestFutureSetStateRaceCondition(t *testing.T) { // It may be false-positive, but very rarely - it's ok for such very // simple race conditions tests. } + +func TestFutureGetIteratorIsPush(t *testing.T) { + fut := NewFuture(test_helpers.NewMockRequest()) + fut.AppendPush(Header{}, nil) + fut.SetResponse(Header{}, nil) + it := fut.GetIterator() + + it.Next() + assert.True(t, it.IsPush()) + it.Next() + assert.False(t, it.IsPush()) +} + +func TestFuture_Get(t *testing.T) { + fut := NewFuture(&futureMockRequest{}) + fut.SetResponse(Header{}, bytes.NewReader([]byte{'v', '2'})) + + resp, err := fut.GetResponse() + assert.NoError(t, err) + mockResp, ok := resp.(*futureMockResponse) + assert.True(t, ok) + + data, err := fut.Get() + assert.NoError(t, err) + assert.Equal(t, []interface{}{uint8('v'), uint8('2')}, data) + assert.Equal(t, 1, mockResp.decodeCnt) + assert.Equal(t, 0, mockResp.decodeTypedCnt) +} + +func TestFuture_GetTyped(t *testing.T) { + fut := NewFuture(&futureMockRequest{}) + fut.SetResponse(Header{}, bytes.NewReader([]byte{'v', '2'})) + + resp, err := fut.GetResponse() + assert.NoError(t, err) + mockResp, ok := resp.(*futureMockResponse) + assert.True(t, ok) + + var data []byte + + err = fut.GetTyped(&data) + assert.NoError(t, err) + assert.Equal(t, 0, mockResp.decodeCnt) + assert.Equal(t, 1, mockResp.decodeTypedCnt) +} + +func TestFuture_GetResponse(t *testing.T) { + mockResp, err := createFutureMockResponse(Header{}, + bytes.NewReader([]byte{'v', '2'})) + assert.NoError(t, err) + + fut := NewFuture(&futureMockRequest{}) + fut.SetResponse(Header{}, bytes.NewReader([]byte{'v', '2'})) + + resp, err := fut.GetResponse() + assert.NoError(t, err) + respConv, ok := resp.(*futureMockResponse) + assert.True(t, ok) + assert.Equal(t, mockResp, respConv) + + data, err := resp.Decode() + assert.NoError(t, err) + assert.Equal(t, []interface{}{uint8('v'), uint8('2')}, data) +} diff --git a/header.go b/header.go index d9069c23a..20a4a465f 100644 --- a/header.go +++ b/header.go @@ -1,10 +1,14 @@ package tarantool +import "github.com/tarantool/go-iproto" + // Header is a response header. type Header struct { // RequestId is an id of a corresponding request. RequestId uint32 - // Code is a response code. It could be used to check that response - // has or hasn't an error. - Code uint32 + // Error is a response error. It could be used + // to check that response has or hasn't an error without decoding. + // Error == ErrorNo (iproto.ER_UNKNOWN) if there is no error. + // Otherwise, it contains an error code from iproto.Error enumeration. + Error iproto.Error } diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 3734c4c0a..861221290 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -1460,7 +1460,7 @@ func (p *ConnectionPool) getConnByMode(defaultMode Mode, } func newErrorFuture(err error) *tarantool.Future { - fut := tarantool.NewFuture() + fut := tarantool.NewFuture(nil) fut.SetError(err) return fut } diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index ae1acd223..acdc756d3 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -2379,9 +2379,8 @@ func TestPing(t *testing.T) { require.Nilf(t, data, "response data is not nil after Ping") // RO - data, err = connPool.Ping(pool.RO) + _, err = connPool.Ping(pool.RO) require.Nilf(t, err, "failed to Ping") - require.Nilf(t, data, "response data is not nil after Ping") // PreferRW data, err = connPool.Ping(pool.PreferRW) @@ -2549,7 +2548,7 @@ func TestDoWithStrangerConn(t *testing.T) { defer connPool.Close() - req := test_helpers.NewStrangerRequest() + req := test_helpers.NewMockRequest() _, err = connPool.Do(req, pool.ANY).Get() if err == nil { diff --git a/prepared.go b/prepared.go index 35b73272e..3a03d740f 100644 --- a/prepared.go +++ b/prepared.go @@ -102,7 +102,7 @@ func (req *PrepareRequest) Response(header Header, body io.Reader) (Response, er if err != nil { return nil, err } - return &PrepareResponse{BaseResponse: baseResp}, nil + return &PrepareResponse{baseResponse: baseResp}, nil } // UnprepareRequest helps you to create an unprepare request object for @@ -192,5 +192,5 @@ func (req *ExecutePreparedRequest) Response(header Header, body io.Reader) (Resp if err != nil { return nil, err } - return &ExecuteResponse{BaseResponse: baseResp}, nil + return &ExecuteResponse{baseResponse: baseResp}, nil } diff --git a/request.go b/request.go index 716e5ab2c..8dbe250b5 100644 --- a/request.go +++ b/request.go @@ -1109,7 +1109,7 @@ func (req *SelectRequest) Response(header Header, body io.Reader) (Response, err if err != nil { return nil, err } - return &SelectResponse{BaseResponse: baseResp}, nil + return &SelectResponse{baseResponse: baseResp}, nil } // InsertRequest helps you to create an insert request object for execution @@ -1517,7 +1517,7 @@ func (req *ExecuteRequest) Response(header Header, body io.Reader) (Response, er if err != nil { return nil, err } - return &ExecuteResponse{BaseResponse: baseResp}, nil + return &ExecuteResponse{baseResponse: baseResp}, nil } // WatchOnceRequest synchronously fetches the value currently associated with a diff --git a/request_test.go b/request_test.go index 580259702..84ba23ef6 100644 --- a/request_test.go +++ b/request_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "fmt" "testing" "time" @@ -317,58 +318,6 @@ func TestRequestsCtx_setter(t *testing.T) { } } -func TestResolverCalledWithoutNameSupport(t *testing.T) { - resolver.nameUseSupported = false - resolver.spaceResolverCalls = 0 - resolver.indexResolverCalls = 0 - - req := NewSelectRequest("valid") - req.Index("valid") - - var reqBuf bytes.Buffer - reqEnc := msgpack.NewEncoder(&reqBuf) - - err := req.Body(&resolver, reqEnc) - if err != nil { - t.Errorf("An unexpected Response.Body() error: %q", err.Error()) - } - - if resolver.spaceResolverCalls != 1 { - t.Errorf("ResolveSpace was called %d times instead of 1.", - resolver.spaceResolverCalls) - } - if resolver.indexResolverCalls != 1 { - t.Errorf("ResolveIndex was called %d times instead of 1.", - resolver.indexResolverCalls) - } -} - -func TestResolverNotCalledWithNameSupport(t *testing.T) { - resolver.nameUseSupported = true - resolver.spaceResolverCalls = 0 - resolver.indexResolverCalls = 0 - - req := NewSelectRequest("valid") - req.Index("valid") - - var reqBuf bytes.Buffer - reqEnc := msgpack.NewEncoder(&reqBuf) - - err := req.Body(&resolver, reqEnc) - if err != nil { - t.Errorf("An unexpected Response.Body() error: %q", err.Error()) - } - - if resolver.spaceResolverCalls != 0 { - t.Errorf("ResolveSpace was called %d times instead of 0.", - resolver.spaceResolverCalls) - } - if resolver.indexResolverCalls != 0 { - t.Errorf("ResolveIndex was called %d times instead of 0.", - resolver.indexResolverCalls) - } -} - func TestPingRequestDefaultValues(t *testing.T) { var refBuf bytes.Buffer @@ -1007,3 +956,110 @@ func TestWatchOnceRequestDefaultValues(t *testing.T) { req := NewWatchOnceRequest(validKey) assertBodyEqual(t, refBuf.Bytes(), req) } + +func TestResponseDecode(t *testing.T) { + header := Header{} + data := bytes.NewBuffer([]byte{'v', '2'}) + baseExample, err := NewPingRequest().Response(header, data) + assert.NoError(t, err) + + tests := []struct { + req Request + expected Response + }{ + {req: NewSelectRequest(validSpace), expected: &SelectResponse{}}, + {req: NewUpdateRequest(validSpace), expected: baseExample}, + {req: NewUpsertRequest(validSpace), expected: baseExample}, + {req: NewInsertRequest(validSpace), expected: baseExample}, + {req: NewReplaceRequest(validSpace), expected: baseExample}, + {req: NewDeleteRequest(validSpace), expected: baseExample}, + {req: NewCallRequest(validExpr), expected: baseExample}, + {req: NewCall16Request(validExpr), expected: baseExample}, + {req: NewCall17Request(validExpr), expected: baseExample}, + {req: NewEvalRequest(validExpr), expected: baseExample}, + {req: NewExecuteRequest(validExpr), expected: &ExecuteResponse{}}, + {req: NewPingRequest(), expected: baseExample}, + {req: NewPrepareRequest(validExpr), expected: &PrepareResponse{}}, + {req: NewUnprepareRequest(validStmt), expected: baseExample}, + {req: NewExecutePreparedRequest(validStmt), expected: &ExecuteResponse{}}, + {req: NewBeginRequest(), expected: baseExample}, + {req: NewCommitRequest(), expected: baseExample}, + {req: NewRollbackRequest(), expected: baseExample}, + {req: NewIdRequest(validProtocolInfo), expected: baseExample}, + {req: NewBroadcastRequest(validKey), expected: baseExample}, + {req: NewWatchOnceRequest(validKey), expected: baseExample}, + } + + for _, test := range tests { + buf := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(buf) + + enc.EncodeMapLen(1) + enc.EncodeUint8(uint8(iproto.IPROTO_DATA)) + enc.Encode([]interface{}{'v', '2'}) + + resp, err := test.req.Response(header, bytes.NewBuffer(buf.Bytes())) + assert.NoError(t, err) + assert.True(t, fmt.Sprintf("%T", resp) == + fmt.Sprintf("%T", test.expected)) + assert.Equal(t, header, resp.Header()) + + decodedInterface, err := resp.Decode() + assert.NoError(t, err) + assert.Equal(t, []interface{}{'v', '2'}, decodedInterface) + } +} + +func TestResponseDecodeTyped(t *testing.T) { + header := Header{} + data := bytes.NewBuffer([]byte{'v', '2'}) + baseExample, err := NewPingRequest().Response(header, data) + assert.NoError(t, err) + + tests := []struct { + req Request + expected Response + }{ + {req: NewSelectRequest(validSpace), expected: &SelectResponse{}}, + {req: NewUpdateRequest(validSpace), expected: baseExample}, + {req: NewUpsertRequest(validSpace), expected: baseExample}, + {req: NewInsertRequest(validSpace), expected: baseExample}, + {req: NewReplaceRequest(validSpace), expected: baseExample}, + {req: NewDeleteRequest(validSpace), expected: baseExample}, + {req: NewCallRequest(validExpr), expected: baseExample}, + {req: NewCall16Request(validExpr), expected: baseExample}, + {req: NewCall17Request(validExpr), expected: baseExample}, + {req: NewEvalRequest(validExpr), expected: baseExample}, + {req: NewExecuteRequest(validExpr), expected: &ExecuteResponse{}}, + {req: NewPingRequest(), expected: baseExample}, + {req: NewPrepareRequest(validExpr), expected: &PrepareResponse{}}, + {req: NewUnprepareRequest(validStmt), expected: baseExample}, + {req: NewExecutePreparedRequest(validStmt), expected: &ExecuteResponse{}}, + {req: NewBeginRequest(), expected: baseExample}, + {req: NewCommitRequest(), expected: baseExample}, + {req: NewRollbackRequest(), expected: baseExample}, + {req: NewIdRequest(validProtocolInfo), expected: baseExample}, + {req: NewBroadcastRequest(validKey), expected: baseExample}, + {req: NewWatchOnceRequest(validKey), expected: baseExample}, + } + + for _, test := range tests { + buf := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(buf) + + enc.EncodeMapLen(1) + enc.EncodeUint8(uint8(iproto.IPROTO_DATA)) + enc.EncodeBytes([]byte{'v', '2'}) + + resp, err := test.req.Response(header, bytes.NewBuffer(buf.Bytes())) + assert.NoError(t, err) + assert.True(t, fmt.Sprintf("%T", resp) == + fmt.Sprintf("%T", test.expected)) + assert.Equal(t, header, resp.Header()) + + var decoded []byte + err = resp.DecodeTyped(&decoded) + assert.NoError(t, err) + assert.Equal(t, []byte{'v', '2'}, decoded) + } +} diff --git a/response.go b/response.go index 61c77f413..db88c743c 100644 --- a/response.go +++ b/response.go @@ -19,32 +19,31 @@ type Response interface { DecodeTyped(res interface{}) error } -// BaseResponse is a base Response interface implementation. -type BaseResponse struct { +type baseResponse struct { + // header is a response header. header Header // data contains deserialized data for untyped requests. - data []interface{} - buf smallBuf - decoded bool + data []interface{} + buf smallBuf + // Was the Decode() func called for this response. + decoded bool + // Was the DecodeTyped() func called for this response. decodedTyped bool + err error } -func createBaseResponse(header Header, body io.Reader) (BaseResponse, error) { +func createBaseResponse(header Header, body io.Reader) (baseResponse, error) { if body == nil { - return BaseResponse{header: header}, nil + return baseResponse{header: header}, nil } if buf, ok := body.(*smallBuf); ok { - return BaseResponse{header: header, buf: *buf}, nil + return baseResponse{header: header, buf: *buf}, nil } data, err := ioutil.ReadAll(body) if err != nil { - return BaseResponse{}, err + return baseResponse{}, err } - return BaseResponse{header: header, buf: smallBuf{b: data}}, nil -} - -func (resp *BaseResponse) SetHeader(header Header) { - resp.header = header + return baseResponse{header: header, buf: smallBuf{b: data}}, nil } // SelectResponse is used for the select requests. @@ -52,7 +51,7 @@ func (resp *BaseResponse) SetHeader(header Header) { // // You need to cast to SelectResponse a response from SelectRequest. type SelectResponse struct { - BaseResponse + baseResponse // pos contains a position descriptor of last selected tuple. pos []byte } @@ -70,7 +69,7 @@ type PrepareResponse ExecuteResponse // // You need to cast to ExecuteResponse a response from ExecuteRequest. type ExecuteResponse struct { - BaseResponse + baseResponse metaData []ColumnMetaData sqlInfo SQLInfo } @@ -169,39 +168,43 @@ func smallInt(d *msgpack.Decoder, buf *smallBuf) (i int, err error) { return d.DecodeInt() } -func decodeHeader(d *msgpack.Decoder, buf *smallBuf) (Header, error) { +func decodeHeader(d *msgpack.Decoder, buf *smallBuf) (Header, iproto.Type, error) { var l int + var code int var err error d.Reset(buf) if l, err = d.DecodeMapLen(); err != nil { - return Header{}, err + return Header{}, 0, err } - decodedHeader := Header{} + decodedHeader := Header{Error: ErrorNo} for ; l > 0; l-- { var cd int if cd, err = smallInt(d, buf); err != nil { - return Header{}, err + return Header{}, 0, err } switch iproto.Key(cd) { case iproto.IPROTO_SYNC: var rid uint64 if rid, err = d.DecodeUint64(); err != nil { - return Header{}, err + return Header{}, 0, err } decodedHeader.RequestId = uint32(rid) case iproto.IPROTO_REQUEST_TYPE: - var rcode uint64 - if rcode, err = d.DecodeUint64(); err != nil { - return Header{}, err + if code, err = d.DecodeInt(); err != nil { + return Header{}, 0, err + } + if code&int(iproto.IPROTO_TYPE_ERROR) != 0 { + decodedHeader.Error = iproto.Error(code &^ int(iproto.IPROTO_TYPE_ERROR)) + } else { + decodedHeader.Error = ErrorNo } - decodedHeader.Code = uint32(rcode) default: if err = d.Skip(); err != nil { - return Header{}, err + return Header{}, 0, err } } } - return decodedHeader, nil + return decodedHeader, iproto.Type(code), nil } type decodeInfo struct { @@ -213,7 +216,7 @@ type decodeInfo struct { decodedError string } -func (info *decodeInfo) parseData(resp *BaseResponse) error { +func (info *decodeInfo) parseData(resp *baseResponse) error { if info.stmtID != 0 { stmt := &Prepared{ StatementID: PreparedID(info.stmtID), @@ -307,7 +310,11 @@ func decodeCommonField(d *msgpack.Decoder, cd int, data *[]interface{}, return true, nil } -func (resp *BaseResponse) Decode() ([]interface{}, error) { +func (resp *baseResponse) Decode() ([]interface{}, error) { + if resp.decoded { + return resp.data, resp.err + } + resp.decoded = true var err error if resp.buf.Len() > 2 { @@ -323,37 +330,46 @@ func (resp *BaseResponse) Decode() ([]interface{}, error) { }) if l, err = d.DecodeMapLen(); err != nil { - return nil, err + resp.err = err + return nil, resp.err } for ; l > 0; l-- { var cd int if cd, err = smallInt(d, &resp.buf); err != nil { - return nil, err + resp.err = err + return nil, resp.err } decoded, err := decodeCommonField(d, cd, &resp.data, info) if err != nil { - return nil, err + resp.err = err + return nil, resp.err } if !decoded { if err = d.Skip(); err != nil { - return nil, err + resp.err = err + return nil, resp.err } } } err = info.parseData(resp) if err != nil { - return nil, err + resp.err = err + return nil, resp.err } if info.decodedError != "" { - resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + resp.err = Error{resp.header.Error, info.decodedError, + info.errorExtendedInfo} } } - return resp.data, err + return resp.data, resp.err } func (resp *SelectResponse) Decode() ([]interface{}, error) { + if resp.decoded { + return resp.data, resp.err + } + resp.decoded = true var err error if resp.buf.Len() > 2 { @@ -369,44 +385,54 @@ func (resp *SelectResponse) Decode() ([]interface{}, error) { }) if l, err = d.DecodeMapLen(); err != nil { - return nil, err + resp.err = err + return nil, resp.err } for ; l > 0; l-- { var cd int if cd, err = smallInt(d, &resp.buf); err != nil { - return nil, err + resp.err = err + return nil, resp.err } decoded, err := decodeCommonField(d, cd, &resp.data, info) if err != nil { + resp.err = err return nil, err } if !decoded { switch iproto.Key(cd) { case iproto.IPROTO_POSITION: if resp.pos, err = d.DecodeBytes(); err != nil { - return nil, fmt.Errorf("unable to decode a position: %w", err) + resp.err = err + return nil, fmt.Errorf("unable to decode a position: %w", resp.err) } default: if err = d.Skip(); err != nil { - return nil, err + resp.err = err + return nil, resp.err } } } } - err = info.parseData(&resp.BaseResponse) + err = info.parseData(&resp.baseResponse) if err != nil { - return nil, err + resp.err = err + return nil, resp.err } if info.decodedError != "" { - resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + resp.err = Error{resp.header.Error, info.decodedError, + info.errorExtendedInfo} } } - return resp.data, err + return resp.data, resp.err } func (resp *ExecuteResponse) Decode() ([]interface{}, error) { + if resp.decoded { + return resp.data, resp.err + } + resp.decoded = true var err error if resp.buf.Len() > 2 { @@ -422,45 +448,52 @@ func (resp *ExecuteResponse) Decode() ([]interface{}, error) { }) if l, err = d.DecodeMapLen(); err != nil { - return nil, err + resp.err = err + return nil, resp.err } for ; l > 0; l-- { var cd int if cd, err = smallInt(d, &resp.buf); err != nil { - return nil, err + resp.err = err + return nil, resp.err } decoded, err := decodeCommonField(d, cd, &resp.data, info) if err != nil { - return nil, err + resp.err = err + return nil, resp.err } if !decoded { switch iproto.Key(cd) { case iproto.IPROTO_SQL_INFO: if err = d.Decode(&resp.sqlInfo); err != nil { - return nil, err + resp.err = err + return nil, resp.err } case iproto.IPROTO_METADATA: if err = d.Decode(&resp.metaData); err != nil { - return nil, err + resp.err = err + return nil, resp.err } default: if err = d.Skip(); err != nil { - return nil, err + resp.err = err + return nil, resp.err } } } } - err = info.parseData(&resp.BaseResponse) + err = info.parseData(&resp.baseResponse) if err != nil { - return nil, err + resp.err = err + return nil, resp.err } if info.decodedError != "" { - resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + resp.err = Error{resp.header.Error, info.decodedError, + info.errorExtendedInfo} } } - return resp.data, err + return resp.data, resp.err } func decodeTypedCommonField(d *msgpack.Decoder, res interface{}, cd int, @@ -486,7 +519,7 @@ func decodeTypedCommonField(d *msgpack.Decoder, res interface{}, cd int, return true, nil } -func (resp *BaseResponse) DecodeTyped(res interface{}) error { +func (resp *baseResponse) DecodeTyped(res interface{}) error { resp.decodedTyped = true var err error @@ -521,8 +554,7 @@ func (resp *BaseResponse) DecodeTyped(res interface{}) error { } } if info.decodedError != "" { - resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + err = Error{resp.header.Error, info.decodedError, info.errorExtendedInfo} } } return err @@ -570,8 +602,7 @@ func (resp *SelectResponse) DecodeTyped(res interface{}) error { } } if info.decodedError != "" { - resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + err = Error{resp.header.Error, info.decodedError, info.errorExtendedInfo} } } return err @@ -623,51 +654,47 @@ func (resp *ExecuteResponse) DecodeTyped(res interface{}) error { } } if info.decodedError != "" { - resp.header.Code &^= uint32(iproto.IPROTO_TYPE_ERROR) - err = Error{iproto.Error(resp.header.Code), info.decodedError, info.errorExtendedInfo} + err = Error{resp.header.Error, info.decodedError, info.errorExtendedInfo} } } return err } -func (resp *BaseResponse) Header() Header { +func (resp *baseResponse) Header() Header { return resp.header } // Pos returns a position descriptor of the last selected tuple for the SelectResponse. // If the response was not decoded, this method will call Decode(). func (resp *SelectResponse) Pos() ([]byte, error) { - var err error if !resp.decoded && !resp.decodedTyped { - _, err = resp.Decode() + resp.Decode() } - return resp.pos, err + return resp.pos, resp.err } // MetaData returns ExecuteResponse meta-data. // If the response was not decoded, this method will call Decode(). func (resp *ExecuteResponse) MetaData() ([]ColumnMetaData, error) { - var err error if !resp.decoded && !resp.decodedTyped { - _, err = resp.Decode() + resp.Decode() } - return resp.metaData, err + return resp.metaData, resp.err } // SQLInfo returns ExecuteResponse sql info. // If the response was not decoded, this method will call Decode(). func (resp *ExecuteResponse) SQLInfo() (SQLInfo, error) { - var err error if !resp.decoded && !resp.decodedTyped { - _, err = resp.Decode() + resp.Decode() } - return resp.sqlInfo, err + return resp.sqlInfo, resp.err } // String implements Stringer interface. -func (resp *BaseResponse) String() (str string) { - if resp.header.Code == OkCode { +func (resp *baseResponse) String() (str string) { + if resp.header.Error == ErrorNo { return fmt.Sprintf("<%d OK %v>", resp.header.RequestId, resp.data) } - return fmt.Sprintf("<%d ERR 0x%x>", resp.header.RequestId, resp.header.Code) + return fmt.Sprintf("<%d ERR %s %v>", resp.header.RequestId, resp.header.Error, resp.err) } diff --git a/schema.go b/schema.go index 6066adf49..72b5e397f 100644 --- a/schema.go +++ b/schema.go @@ -380,15 +380,18 @@ func (indexField *IndexField) DecodeMsgpack(d *msgpack.Decoder) error { return errors.New("unexpected schema format (index fields)") } -// GetSchema returns the actual schema for the connection. -func GetSchema(conn Connector) (Schema, error) { +// GetSchema returns the actual schema for the Doer. +func GetSchema(doer Doer) (Schema, error) { schema := Schema{} schema.SpacesById = make(map[uint32]Space) schema.Spaces = make(map[string]Space) // Reload spaces. var spaces []Space - err := conn.SelectTyped(vspaceSpId, 0, 0, maxSchemas, IterAll, []interface{}{}, &spaces) + req := NewSelectRequest(vspaceSpId). + Index(0). + Limit(maxSchemas) + err := doer.Do(req).GetTyped(&spaces) if err != nil { return Schema{}, err } @@ -399,7 +402,10 @@ func GetSchema(conn Connector) (Schema, error) { // Reload indexes. var indexes []Index - err = conn.SelectTyped(vindexSpId, 0, 0, maxSchemas, IterAll, []interface{}{}, &indexes) + req = NewSelectRequest(vindexSpId). + Index(0). + Limit(maxSchemas) + err = doer.Do(req).GetTyped(&indexes) if err != nil { return Schema{}, err } diff --git a/schema_test.go b/schema_test.go new file mode 100644 index 000000000..631591cb2 --- /dev/null +++ b/schema_test.go @@ -0,0 +1,162 @@ +package tarantool_test + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" +) + +func TestGetSchema_ok(t *testing.T) { + space1 := tarantool.Space{ + Id: 1, + Name: "name1", + Indexes: make(map[string]tarantool.Index), + IndexesById: make(map[uint32]tarantool.Index), + Fields: make(map[string]tarantool.Field), + FieldsById: make(map[uint32]tarantool.Field), + } + index := tarantool.Index{ + Id: 1, + SpaceId: 2, + Name: "index_name", + Type: "index_type", + Unique: true, + Fields: make([]tarantool.IndexField, 0), + } + space2 := tarantool.Space{ + Id: 2, + Name: "name2", + Indexes: map[string]tarantool.Index{ + "index_name": index, + }, + IndexesById: map[uint32]tarantool.Index{ + 1: index, + }, + Fields: make(map[string]tarantool.Field), + FieldsById: make(map[uint32]tarantool.Field), + } + + mockDoer := test_helpers.NewMockDoer(t, + test_helpers.NewMockResponse(t, [][]interface{}{ + { + uint32(1), + "skip", + "name1", + "", + 0, + }, + { + uint32(2), + "skip", + "name2", + "", + 0, + }, + }), + test_helpers.NewMockResponse(t, [][]interface{}{ + { + uint32(2), + uint32(1), + "index_name", + "index_type", + uint8(1), + uint8(0), + }, + }), + ) + + expectedSchema := tarantool.Schema{ + SpacesById: map[uint32]tarantool.Space{ + 1: space1, + 2: space2, + }, + Spaces: map[string]tarantool.Space{ + "name1": space1, + "name2": space2, + }, + } + + schema, err := tarantool.GetSchema(&mockDoer) + require.NoError(t, err) + require.Equal(t, expectedSchema, schema) +} + +func TestGetSchema_spaces_select_error(t *testing.T) { + mockDoer := test_helpers.NewMockDoer(t, fmt.Errorf("some error")) + + schema, err := tarantool.GetSchema(&mockDoer) + require.EqualError(t, err, "some error") + require.Equal(t, tarantool.Schema{}, schema) +} + +func TestGetSchema_index_select_error(t *testing.T) { + mockDoer := test_helpers.NewMockDoer(t, + test_helpers.NewMockResponse(t, [][]interface{}{ + { + uint32(1), + "skip", + "name1", + "", + 0, + }, + }), + fmt.Errorf("some error")) + + schema, err := tarantool.GetSchema(&mockDoer) + require.EqualError(t, err, "some error") + require.Equal(t, tarantool.Schema{}, schema) +} + +func TestResolverCalledWithoutNameSupport(t *testing.T) { + resolver := ValidSchemeResolver{nameUseSupported: false} + + req := tarantool.NewSelectRequest("valid") + req.Index("valid") + + var reqBuf bytes.Buffer + reqEnc := msgpack.NewEncoder(&reqBuf) + + err := req.Body(&resolver, reqEnc) + if err != nil { + t.Errorf("An unexpected Response.Body() error: %q", err.Error()) + } + + if resolver.spaceResolverCalls != 1 { + t.Errorf("ResolveSpace was called %d times instead of 1.", + resolver.spaceResolverCalls) + } + if resolver.indexResolverCalls != 1 { + t.Errorf("ResolveIndex was called %d times instead of 1.", + resolver.indexResolverCalls) + } +} + +func TestResolverNotCalledWithNameSupport(t *testing.T) { + resolver := ValidSchemeResolver{nameUseSupported: true} + + req := tarantool.NewSelectRequest("valid") + req.Index("valid") + + var reqBuf bytes.Buffer + reqEnc := msgpack.NewEncoder(&reqBuf) + + err := req.Body(&resolver, reqEnc) + if err != nil { + t.Errorf("An unexpected Response.Body() error: %q", err.Error()) + } + + if resolver.spaceResolverCalls != 0 { + t.Errorf("ResolveSpace was called %d times instead of 0.", + resolver.spaceResolverCalls) + } + if resolver.indexResolverCalls != 0 { + t.Errorf("ResolveIndex was called %d times instead of 0.", + resolver.indexResolverCalls) + } +} diff --git a/stream.go b/stream.go index 5144ea6f1..43e80fc28 100644 --- a/stream.go +++ b/stream.go @@ -199,7 +199,7 @@ func (req *RollbackRequest) Context(ctx context.Context) *RollbackRequest { func (s *Stream) Do(req Request) *Future { if connectedReq, ok := req.(ConnectedRequest); ok { if connectedReq.Conn() != s.Conn { - fut := NewFuture() + fut := NewFuture(req) fut.SetError(errUnknownStreamRequest) return fut } diff --git a/tarantool_test.go b/tarantool_test.go index 3b8bc4653..c3f6b4c0b 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -858,7 +858,7 @@ func TestClient(t *testing.T) { // Ping data, err := conn.Ping() if err != nil { - t.Fatalf("Failed to Ping: %s", err.Error()) + t.Fatalf("Failed to Ping: %s", err) } if data != nil { t.Fatalf("Response data is not nil after Ping") @@ -867,7 +867,7 @@ func TestClient(t *testing.T) { // Insert data, err = conn.Insert(spaceNo, []interface{}{uint(1), "hello", "world"}) if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err) } if len(data) != 1 { t.Errorf("Response Body len != 1") @@ -896,7 +896,7 @@ func TestClient(t *testing.T) { // Delete data, err = conn.Delete(spaceNo, indexNo, []interface{}{uint(1)}) if err != nil { - t.Fatalf("Failed to Delete: %s", err.Error()) + t.Fatalf("Failed to Delete: %s", err) } if len(data) != 1 { t.Errorf("Response Body len != 1") @@ -916,7 +916,7 @@ func TestClient(t *testing.T) { } data, err = conn.Delete(spaceNo, indexNo, []interface{}{uint(101)}) if err != nil { - t.Fatalf("Failed to Delete: %s", err.Error()) + t.Fatalf("Failed to Delete: %s", err) } if len(data) != 0 { t.Errorf("Response Data len != 0") @@ -925,14 +925,14 @@ func TestClient(t *testing.T) { // Replace data, err = conn.Replace(spaceNo, []interface{}{uint(2), "hello", "world"}) if err != nil { - t.Fatalf("Failed to Replace: %s", err.Error()) + t.Fatalf("Failed to Replace: %s", err) } if data == nil { t.Fatalf("Response is nil after Replace") } data, err = conn.Replace(spaceNo, []interface{}{uint(2), "hi", "planet"}) if err != nil { - t.Fatalf("Failed to Replace (duplicate): %s", err.Error()) + t.Fatalf("Failed to Replace (duplicate): %s", err) } if len(data) != 1 { t.Errorf("Response Data len != 1") @@ -955,7 +955,7 @@ func TestClient(t *testing.T) { data, err = conn.Update(spaceNo, indexNo, []interface{}{uint(2)}, NewOperations().Assign(1, "bye").Delete(2, 1)) if err != nil { - t.Fatalf("Failed to Update: %s", err.Error()) + t.Fatalf("Failed to Update: %s", err) } if len(data) != 1 { t.Errorf("Response Data len != 1") @@ -978,7 +978,7 @@ func TestClient(t *testing.T) { data, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, NewOperations().Add(1, 1)) if err != nil { - t.Fatalf("Failed to Upsert (insert): %s", err.Error()) + t.Fatalf("Failed to Upsert (insert): %s", err) } if data == nil { t.Fatalf("Response is nil after Upsert (insert)") @@ -986,7 +986,7 @@ func TestClient(t *testing.T) { data, err = conn.Upsert(spaceNo, []interface{}{uint(3), 1}, NewOperations().Add(1, 1)) if err != nil { - t.Fatalf("Failed to Upsert (update): %s", err.Error()) + t.Fatalf("Failed to Upsert (update): %s", err) } if data == nil { t.Errorf("Response is nil after Upsert (update)") @@ -996,7 +996,7 @@ func TestClient(t *testing.T) { for i := 10; i < 20; i++ { data, err = conn.Replace(spaceNo, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) if err != nil { - t.Fatalf("Failed to Replace: %s", err.Error()) + t.Fatalf("Failed to Replace: %s", err) } if data == nil { t.Errorf("Response is nil after Replace") @@ -1004,7 +1004,7 @@ func TestClient(t *testing.T) { } data, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}) if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 1 { t.Fatalf("Response Data len != 1") @@ -1023,7 +1023,7 @@ func TestClient(t *testing.T) { // Select empty data, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}) if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 0 { t.Errorf("Response Data len != 0") @@ -1033,7 +1033,7 @@ func TestClient(t *testing.T) { var tpl []Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}, &tpl) if err != nil { - t.Fatalf("Failed to SelectTyped: %s", err.Error()) + t.Fatalf("Failed to SelectTyped: %s", err) } if len(tpl) != 1 { t.Errorf("Result len of SelectTyped != 1") @@ -1045,7 +1045,7 @@ func TestClient(t *testing.T) { var singleTpl = Tuple{} err = conn.GetTyped(spaceNo, indexNo, []interface{}{uint(10)}, &singleTpl) if err != nil { - t.Fatalf("Failed to GetTyped: %s", err.Error()) + t.Fatalf("Failed to GetTyped: %s", err) } if singleTpl.Id != 10 { t.Errorf("Bad value loaded from GetTyped") @@ -1055,7 +1055,7 @@ func TestClient(t *testing.T) { var tpl1 [1]Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}, &tpl1) if err != nil { - t.Fatalf("Failed to SelectTyped: %s", err.Error()) + t.Fatalf("Failed to SelectTyped: %s", err) } if len(tpl) != 1 { t.Errorf("Result len of SelectTyped != 1") @@ -1067,7 +1067,7 @@ func TestClient(t *testing.T) { var singleTpl2 Tuple err = conn.GetTyped(spaceNo, indexNo, []interface{}{uint(30)}, &singleTpl2) if err != nil { - t.Fatalf("Failed to GetTyped: %s", err.Error()) + t.Fatalf("Failed to GetTyped: %s", err) } if singleTpl2.Id != 0 { t.Errorf("Bad value loaded from GetTyped") @@ -1077,7 +1077,7 @@ func TestClient(t *testing.T) { var tpl2 []Tuple err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}, &tpl2) if err != nil { - t.Fatalf("Failed to SelectTyped: %s", err.Error()) + t.Fatalf("Failed to SelectTyped: %s", err) } if len(tpl2) != 0 { t.Errorf("Result len of SelectTyped != 1") @@ -1086,7 +1086,7 @@ func TestClient(t *testing.T) { // Call16 data, err = conn.Call16("box.info", []interface{}{"box.schema.SPACE_ID"}) if err != nil { - t.Fatalf("Failed to Call16: %s", err.Error()) + t.Fatalf("Failed to Call16: %s", err) } if len(data) < 1 { t.Errorf("Response.Data is empty after Eval") @@ -1112,7 +1112,7 @@ func TestClient(t *testing.T) { // Eval data, err = conn.Eval("return 5 + 6", []interface{}{}) if err != nil { - t.Fatalf("Failed to Eval: %s", err.Error()) + t.Fatalf("Failed to Eval: %s", err) } if len(data) < 1 { t.Errorf("Response.Data is empty after Eval") @@ -1157,7 +1157,7 @@ func TestClientSessionPush(t *testing.T) { // Future.Get ignores push messages. data, err := fut1.Get() if err != nil { - t.Errorf("Failed to Call17: %s", err.Error()) + t.Errorf("Failed to Call17: %s", err) } else if len(data) < 1 { t.Errorf("Response.Data is empty after Call17Async") } else if val, err := test_helpers.ConvertUint64(data[0]); err != nil || val != pushMax { @@ -1185,7 +1185,7 @@ func TestClientSessionPush(t *testing.T) { } data, err := resp.Decode() if err != nil { - t.Errorf("Failed to Decode: %s", err.Error()) + t.Errorf("Failed to Decode: %s", err) break } if len(data) < 1 { @@ -1411,20 +1411,20 @@ func TestSQL(t *testing.T) { assert.NoError(t, err, "Failed to Execute, query: %s", test.Query) assert.NotNil(t, resp, "Response is nil after Execute\nQuery number: %d", i) data, err := resp.Decode() - assert.Nil(t, err, "Failed to Decode") + assert.NoError(t, err, "Failed to Decode") for j := range data { assert.Equal(t, data[j], test.data[j], "Response data is wrong") } exResp, ok := resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err := exResp.SQLInfo() - assert.Nil(t, err, "Error while getting SQLInfo") + assert.NoError(t, err, "Error while getting SQLInfo") assert.Equal(t, sqlInfo.AffectedCount, test.sqlInfo.AffectedCount, "Affected count is wrong") errorMsg := "Response Metadata is wrong" metaData, err := exResp.MetaData() - assert.Nil(t, err, "Error while getting MetaData") + assert.NoError(t, err, "Error while getting MetaData") for j := range metaData { assert.Equal(t, metaData[j], test.metaData[j], errorMsg) } @@ -1510,14 +1510,14 @@ func TestSQLBindings(t *testing.T) { req := NewExecuteRequest(selectNamedQuery2).Args(bind) resp, err := conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to Execute: %s", err.Error()) + t.Fatalf("Failed to Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } data, err := resp.Decode() if err != nil { - t.Errorf("Failed to Decode: %s", err.Error()) + t.Errorf("Failed to Decode: %s", err) } if reflect.DeepEqual(data[0], []interface{}{1, testData[1]}) { t.Error("Select with named arguments failed") @@ -1525,7 +1525,7 @@ func TestSQLBindings(t *testing.T) { exResp, ok := resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") metaData, err := exResp.MetaData() - assert.Nil(t, err, "Error while getting MetaData") + assert.NoError(t, err, "Error while getting MetaData") if metaData[0].FieldType != "unsigned" || metaData[0].FieldName != "NAME0" || metaData[1].FieldType != "string" || @@ -1537,14 +1537,14 @@ func TestSQLBindings(t *testing.T) { req := NewExecuteRequest(selectPosQuery2).Args(sqlBind5) resp, err := conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to Execute: %s", err.Error()) + t.Fatalf("Failed to Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } data, err := resp.Decode() if err != nil { - t.Errorf("Failed to Decode: %s", err.Error()) + t.Errorf("Failed to Decode: %s", err) } if reflect.DeepEqual(data[0], []interface{}{1, testData[1]}) { t.Error("Select with positioned arguments failed") @@ -1552,7 +1552,7 @@ func TestSQLBindings(t *testing.T) { exResp, ok := resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") metaData, err := exResp.MetaData() - assert.Nil(t, err, "Error while getting MetaData") + assert.NoError(t, err, "Error while getting MetaData") if metaData[0].FieldType != "unsigned" || metaData[0].FieldName != "NAME0" || metaData[1].FieldType != "string" || @@ -1563,14 +1563,14 @@ func TestSQLBindings(t *testing.T) { req = NewExecuteRequest(mixedQuery).Args(sqlBind6) resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to Execute: %s", err.Error()) + t.Fatalf("Failed to Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } data, err = resp.Decode() if err != nil { - t.Errorf("Failed to Decode: %s", err.Error()) + t.Errorf("Failed to Decode: %s", err) } if reflect.DeepEqual(data[0], []interface{}{1, testData[1]}) { t.Error("Select with positioned arguments failed") @@ -1578,7 +1578,7 @@ func TestSQLBindings(t *testing.T) { exResp, ok = resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") metaData, err = exResp.MetaData() - assert.Nil(t, err, "Error while getting MetaData") + assert.NoError(t, err, "Error while getting MetaData") if metaData[0].FieldType != "unsigned" || metaData[0].FieldName != "NAME0" || metaData[1].FieldType != "string" || @@ -1596,7 +1596,7 @@ func TestStressSQL(t *testing.T) { req := NewExecuteRequest(createTableQuery) resp, err := conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to create an Execute: %s", err.Error()) + t.Fatalf("Failed to create an Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") @@ -1604,7 +1604,7 @@ func TestStressSQL(t *testing.T) { exResp, ok := resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err := exResp.SQLInfo() - assert.Nil(t, err, "Error while getting SQLInfo") + assert.NoError(t, err, "Error while getting SQLInfo") if sqlInfo.AffectedCount != 1 { t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } @@ -1613,7 +1613,7 @@ func TestStressSQL(t *testing.T) { req = NewExecuteRequest(createTableQuery) resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to create an Execute: %s", err.Error()) + t.Fatalf("Failed to create an Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") @@ -1624,14 +1624,15 @@ func TestStressSQL(t *testing.T) { tntErr, ok := err.(Error) assert.True(t, ok) assert.Equal(t, iproto.ER_SPACE_EXISTS, tntErr.Code) - if iproto.Error(resp.Header().Code) != iproto.ER_SPACE_EXISTS { - t.Fatalf("Unexpected response code: %d", resp.Header().Code) + if resp.Header().Error != iproto.ER_SPACE_EXISTS { + t.Fatalf("Unexpected response error: %d", resp.Header().Error) } + prevErr := err exResp, ok = resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err = exResp.SQLInfo() - assert.Nil(t, err, "Unexpected error") + assert.Equal(t, prevErr, err) if sqlInfo.AffectedCount != 0 { t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } @@ -1640,12 +1641,12 @@ func TestStressSQL(t *testing.T) { req = NewExecuteRequest(createTableQuery).Args(nil) resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to create an Execute: %s", err.Error()) + t.Fatalf("Failed to create an Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Header().Code == OkCode { + if resp.Header().Error == ErrorNo { t.Fatal("Unexpected successful Execute") } exResp, ok = resp.(*ExecuteResponse) @@ -1660,12 +1661,12 @@ func TestStressSQL(t *testing.T) { req = NewExecuteRequest("") resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to create an Execute: %s", err.Error()) + t.Fatalf("Failed to create an Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Header().Code == OkCode { + if resp.Header().Error == ErrorNo { t.Fatal("Unexpected successful Execute") } exResp, ok = resp.(*ExecuteResponse) @@ -1680,7 +1681,7 @@ func TestStressSQL(t *testing.T) { req = NewExecuteRequest(dropQuery2) resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to Execute: %s", err.Error()) + t.Fatalf("Failed to Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") @@ -1688,7 +1689,7 @@ func TestStressSQL(t *testing.T) { exResp, ok = resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err = exResp.SQLInfo() - assert.Nil(t, err, "Error while getting SQLInfo") + assert.NoError(t, err, "Error while getting SQLInfo") if sqlInfo.AffectedCount != 1 { t.Errorf("Incorrect count of dropped spaces: %d", sqlInfo.AffectedCount) } @@ -1697,12 +1698,12 @@ func TestStressSQL(t *testing.T) { req = NewExecuteRequest(dropQuery2) resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to create an Execute: %s", err.Error()) + t.Fatalf("Failed to create an Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } - if resp.Header().Code == OkCode { + if resp.Header().Error == ErrorNo { t.Fatal("Unexpected successful Execute") } _, err = resp.Decode() @@ -1712,7 +1713,9 @@ func TestStressSQL(t *testing.T) { exResp, ok = resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err = exResp.SQLInfo() - assert.Nil(t, err, "Error while getting SQLInfo") + if err == nil { + t.Fatal("Unexpected lack of error") + } if sqlInfo.AffectedCount != 0 { t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } @@ -1738,7 +1741,7 @@ func TestNewPrepared(t *testing.T) { } data, err := resp.Decode() if err != nil { - t.Errorf("Failed to Decode: %s", err.Error()) + t.Errorf("Failed to Decode: %s", err) } if reflect.DeepEqual(data[0], []interface{}{1, "test"}) { t.Error("Select with named arguments failed") @@ -1746,7 +1749,7 @@ func TestNewPrepared(t *testing.T) { prepResp, ok := resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") metaData, err := prepResp.MetaData() - assert.Nil(t, err, "Error while getting MetaData") + assert.NoError(t, err, "Error while getting MetaData") if metaData[0].FieldType != "unsigned" || metaData[0].FieldName != "NAME0" || metaData[1].FieldType != "string" || @@ -1797,7 +1800,7 @@ func TestConnection_DoWithStrangerConn(t *testing.T) { " connection or connection pool") conn1 := &Connection{} - req := test_helpers.NewStrangerRequest() + req := test_helpers.NewMockRequest() _, err := conn1.Do(req).Get() if err == nil { @@ -1841,7 +1844,7 @@ func TestConnection_SetSchema_Changes(t *testing.T) { req.Tuple([]interface{}{uint(1010), "Tarantool"}) _, err := conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err) } s, err := GetSchema(conn) @@ -1858,7 +1861,7 @@ func TestConnection_SetSchema_Changes(t *testing.T) { reqS.Key([]interface{}{uint(1010)}) data, err := conn.Do(reqS).Get() if err != nil { - t.Fatalf("failed to Select: %s", err.Error()) + t.Fatalf("failed to Select: %s", err) } if data[0].([]interface{})[1] != "Tarantool" { t.Errorf("wrong Select body: %v", data) @@ -2051,6 +2054,35 @@ func TestSchema_IsNullable(t *testing.T) { } } +func TestNewPreparedFromResponse(t *testing.T) { + var ( + ErrNilResponsePassed = fmt.Errorf("passed nil response") + ErrNilResponseData = fmt.Errorf("response Data is nil") + ErrWrongDataFormat = fmt.Errorf("response Data format is wrong") + ) + + testConn := &Connection{} + testCases := []struct { + name string + resp Response + expectedError error + }{ + {"ErrNilResponsePassed", nil, ErrNilResponsePassed}, + {"ErrNilResponseData", test_helpers.NewMockResponse(t, nil), + ErrNilResponseData}, + {"ErrWrongDataFormat", test_helpers.NewMockResponse(t, []interface{}{}), + ErrWrongDataFormat}, + {"ErrWrongDataFormat", test_helpers.NewMockResponse(t, []interface{}{"test"}), + ErrWrongDataFormat}, + } + for _, testCase := range testCases { + t.Run("Expecting error "+testCase.name, func(t *testing.T) { + _, err := NewPreparedFromResponse(testConn, testCase.resp) + assert.Equal(t, testCase.expectedError, err) + }) + } +} + func TestClientNamed(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() @@ -2058,7 +2090,7 @@ func TestClientNamed(t *testing.T) { // Insert data, err := conn.Insert(spaceName, []interface{}{uint(1001), "hello2", "world2"}) if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err) } if data == nil { t.Errorf("Response is nil after Insert") @@ -2067,7 +2099,7 @@ func TestClientNamed(t *testing.T) { // Delete data, err = conn.Delete(spaceName, indexName, []interface{}{uint(1001)}) if err != nil { - t.Fatalf("Failed to Delete: %s", err.Error()) + t.Fatalf("Failed to Delete: %s", err) } if data == nil { t.Errorf("Response is nil after Delete") @@ -2076,7 +2108,7 @@ func TestClientNamed(t *testing.T) { // Replace data, err = conn.Replace(spaceName, []interface{}{uint(1002), "hello", "world"}) if err != nil { - t.Fatalf("Failed to Replace: %s", err.Error()) + t.Fatalf("Failed to Replace: %s", err) } if data == nil { t.Errorf("Response is nil after Replace") @@ -2088,7 +2120,7 @@ func TestClientNamed(t *testing.T) { uint(1002)}, NewOperations().Assign(1, "buy").Delete(2, 1)) if err != nil { - t.Fatalf("Failed to Update: %s", err.Error()) + t.Fatalf("Failed to Update: %s", err) } if data == nil { t.Errorf("Response is nil after Update") @@ -2098,7 +2130,7 @@ func TestClientNamed(t *testing.T) { data, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, NewOperations().Add(1, 1)) if err != nil { - t.Fatalf("Failed to Upsert (insert): %s", err.Error()) + t.Fatalf("Failed to Upsert (insert): %s", err) } if data == nil { t.Errorf("Response is nil after Upsert (insert)") @@ -2106,7 +2138,7 @@ func TestClientNamed(t *testing.T) { data, err = conn.Upsert(spaceName, []interface{}{uint(1003), 1}, NewOperations().Add(1, 1)) if err != nil { - t.Fatalf("Failed to Upsert (update): %s", err.Error()) + t.Fatalf("Failed to Upsert (update): %s", err) } if data == nil { t.Errorf("Response is nil after Upsert (update)") @@ -2117,7 +2149,7 @@ func TestClientNamed(t *testing.T) { data, err = conn.Replace(spaceName, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) if err != nil { - t.Fatalf("Failed to Replace: %s", err.Error()) + t.Fatalf("Failed to Replace: %s", err) } if data == nil { t.Errorf("Response is nil after Replace") @@ -2125,7 +2157,7 @@ func TestClientNamed(t *testing.T) { } data, err = conn.Select(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)}) if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if data == nil { t.Errorf("Response is nil after Select") @@ -2135,7 +2167,7 @@ func TestClientNamed(t *testing.T) { var tpl []Tuple err = conn.SelectTyped(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)}, &tpl) if err != nil { - t.Fatalf("Failed to SelectTyped: %s", err.Error()) + t.Fatalf("Failed to SelectTyped: %s", err) } if len(tpl) != 1 { t.Errorf("Result len of SelectTyped != 1") @@ -2155,7 +2187,7 @@ func TestClientRequestObjects(t *testing.T) { req = NewPingRequest() data, err := conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Ping: %s", err.Error()) + t.Fatalf("Failed to Ping: %s", err) } if len(data) != 0 { t.Errorf("Response Body len != 0") @@ -2172,7 +2204,7 @@ func TestClientRequestObjects(t *testing.T) { Tuple([]interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"}) data, err = conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err) } if len(data) != 1 { t.Fatalf("Response Body len != 1") @@ -2201,7 +2233,7 @@ func TestClientRequestObjects(t *testing.T) { Tuple([]interface{}{uint(i), fmt.Sprintf("val %d", i), "blar"}) data, err = conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) + t.Fatalf("Failed to Decode: %s", err) } if len(data) != 1 { t.Fatalf("Response Body len != 1") @@ -2229,7 +2261,7 @@ func TestClientRequestObjects(t *testing.T) { Key([]interface{}{uint(1016)}) data, err = conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Delete: %s", err.Error()) + t.Fatalf("Failed to Delete: %s", err) } if data == nil { t.Fatalf("Response data is nil after Delete") @@ -2260,7 +2292,7 @@ func TestClientRequestObjects(t *testing.T) { Key([]interface{}{uint(1010)}) data, err = conn.Do(req).Get() if err != nil { - t.Errorf("Failed to Update: %s", err.Error()) + t.Errorf("Failed to Update: %s", err) } if data == nil { t.Fatalf("Response data is nil after Update") @@ -2289,7 +2321,7 @@ func TestClientRequestObjects(t *testing.T) { Operations(NewOperations().Assign(1, "bye").Insert(2, 1)) data, err = conn.Do(req).Get() if err != nil { - t.Errorf("Failed to Update: %s", err.Error()) + t.Errorf("Failed to Update: %s", err) } if len(data) != 1 { t.Fatalf("Response Data len != 1") @@ -2313,7 +2345,7 @@ func TestClientRequestObjects(t *testing.T) { Tuple([]interface{}{uint(1010), "hi", "hi"}) data, err = conn.Do(req).Get() if err != nil { - t.Errorf("Failed to Upsert (update): %s", err.Error()) + t.Errorf("Failed to Upsert (update): %s", err) } if len(data) != 0 { t.Fatalf("Response Data len != 0") @@ -2325,7 +2357,7 @@ func TestClientRequestObjects(t *testing.T) { Operations(NewOperations().Assign(2, "bye")) data, err = conn.Do(req).Get() if err != nil { - t.Errorf("Failed to Upsert (update): %s", err.Error()) + t.Errorf("Failed to Upsert (update): %s", err) } if len(data) != 0 { t.Fatalf("Response Data len != 0") @@ -2355,7 +2387,7 @@ func TestClientRequestObjects(t *testing.T) { req = NewEvalRequest("return 5 + 6") data, err = conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Eval: %s", err.Error()) + t.Fatalf("Failed to Eval: %s", err) } if len(data) < 1 { t.Errorf("Response.Data is empty after Eval") @@ -2376,14 +2408,14 @@ func TestClientRequestObjects(t *testing.T) { req = NewExecuteRequest(createTableQuery) resp, err := conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to Execute: %s", err.Error()) + t.Fatalf("Failed to Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } data, err = resp.Decode() if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) + t.Fatalf("Failed to Decode: %s", err) } if len(data) != 0 { t.Fatalf("Response Body len != 0") @@ -2391,7 +2423,7 @@ func TestClientRequestObjects(t *testing.T) { exResp, ok := resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err := exResp.SQLInfo() - assert.Nil(t, err, "Error while getting SQLInfo") + assert.NoError(t, err, "Error while getting SQLInfo") if sqlInfo.AffectedCount != 1 { t.Errorf("Incorrect count of created spaces: %d", sqlInfo.AffectedCount) } @@ -2399,14 +2431,14 @@ func TestClientRequestObjects(t *testing.T) { req = NewExecuteRequest(dropQuery2) resp, err = conn.Do(req).GetResponse() if err != nil { - t.Fatalf("Failed to Execute: %s", err.Error()) + t.Fatalf("Failed to Execute: %s", err) } if resp == nil { t.Fatal("Response is nil after Execute") } data, err = resp.Decode() if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) + t.Fatalf("Failed to Decode: %s", err) } if len(data) != 0 { t.Fatalf("Response Body len != 0") @@ -2414,7 +2446,7 @@ func TestClientRequestObjects(t *testing.T) { exResp, ok = resp.(*ExecuteResponse) assert.True(t, ok, "Got wrong response type") sqlInfo, err = exResp.SQLInfo() - assert.Nil(t, err, "Error while getting SQLInfo") + assert.NoError(t, err, "Error while getting SQLInfo") if sqlInfo.AffectedCount != 1 { t.Errorf("Incorrect count of dropped spaces: %d", sqlInfo.AffectedCount) } @@ -2437,14 +2469,14 @@ func testConnectionDoSelectRequestCheck(t *testing.T, t.Helper() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if resp == nil { t.Fatalf("Response is nil after Select") } respPos, err := resp.Pos() if err != nil { - t.Errorf("Error while getting Pos: %s", err.Error()) + t.Errorf("Error while getting Pos: %s", err) } if !pos && respPos != nil { t.Errorf("Response should not have a position descriptor") @@ -2454,7 +2486,7 @@ func testConnectionDoSelectRequestCheck(t *testing.T, } data, err := resp.Decode() if err != nil { - t.Fatalf("Failed to Decode: %s", err.Error()) + t.Fatalf("Failed to Decode: %s", err) } if len(data) != dataLen { t.Fatalf("Response Data len %d != %d", len(data), dataLen) @@ -2608,7 +2640,7 @@ func TestConnectionDoSelectRequest_pagination_pos(t *testing.T) { testConnectionDoSelectRequestCheck(t, selResp, err, true, 2, 1010) selPos, err := selResp.Pos() - assert.Nil(t, err, "Error while getting Pos") + assert.NoError(t, err, "Error while getting Pos") resp, err = conn.Do(req.After(selPos)).GetResponse() selResp, ok = resp.(*SelectResponse) @@ -2652,7 +2684,7 @@ func TestClientRequestObjectsWithNilContext(t *testing.T) { req := NewPingRequest().Context(nil) //nolint data, err := conn.Do(req).Get() if err != nil { - t.Fatalf("Failed to Ping: %s", err.Error()) + t.Fatalf("Failed to Ping: %s", err) } if len(data) != 0 { t.Errorf("Response Body len != 0") @@ -2701,9 +2733,8 @@ func (req *waitCtxRequest) Async() bool { } func (req *waitCtxRequest) Response(header Header, body io.Reader) (Response, error) { - resp := BaseResponse{} - resp.SetHeader(header) - return &resp, nil + resp, err := test_helpers.CreateMockResponse(header, body) + return resp, err } func TestClientRequestObjectsWithContext(t *testing.T) { @@ -2753,13 +2784,13 @@ func TestComplexStructs(t *testing.T) { tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} _, err = conn.Replace(spaceNo, &tuple) if err != nil { - t.Fatalf("Failed to insert: %s", err.Error()) + t.Fatalf("Failed to insert: %s", err) } var tuples [1]Tuple2 err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, []interface{}{777}, &tuples) if err != nil { - t.Fatalf("Failed to selectTyped: %s", err.Error()) + t.Fatalf("Failed to selectTyped: %s", err) } if len(tuples) != 1 { @@ -2801,7 +2832,7 @@ func TestStream_IdValues(t *testing.T) { stream.Id = id _, err := stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Ping: %s", err.Error()) + t.Fatalf("Failed to Ping: %s", err) } }) } @@ -2823,7 +2854,7 @@ func TestStream_Commit(t *testing.T) { req = NewBeginRequest() _, err = stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Begin: %s", err.Error()) + t.Fatalf("Failed to Begin: %s", err) } // Insert in stream @@ -2831,7 +2862,7 @@ func TestStream_Commit(t *testing.T) { Tuple([]interface{}{uint(1001), "hello2", "world2"}) _, err = stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err) } defer test_helpers.DeleteRecordByKey(t, conn, spaceNo, indexNo, []interface{}{uint(1001)}) @@ -2845,7 +2876,7 @@ func TestStream_Commit(t *testing.T) { Key([]interface{}{uint(1001)}) data, err := conn.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 0 { t.Fatalf("Response Data len != 0") @@ -2854,7 +2885,7 @@ func TestStream_Commit(t *testing.T) { // Select in stream data, err = stream.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 1 { t.Fatalf("Response Data len != 1") @@ -2877,13 +2908,13 @@ func TestStream_Commit(t *testing.T) { req = NewCommitRequest() _, err = stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Commit: %s", err.Error()) + t.Fatalf("Failed to Commit: %s", err) } // Select outside of transaction data, err = conn.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 1 { t.Fatalf("Response Data len != 1") @@ -2919,7 +2950,7 @@ func TestStream_Rollback(t *testing.T) { req = NewBeginRequest() _, err = stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Begin: %s", err.Error()) + t.Fatalf("Failed to Begin: %s", err) } // Insert in stream @@ -2927,7 +2958,7 @@ func TestStream_Rollback(t *testing.T) { Tuple([]interface{}{uint(1001), "hello2", "world2"}) _, err = stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Insert: %s", err.Error()) + t.Fatalf("Failed to Insert: %s", err) } defer test_helpers.DeleteRecordByKey(t, conn, spaceNo, indexNo, []interface{}{uint(1001)}) @@ -2941,7 +2972,7 @@ func TestStream_Rollback(t *testing.T) { Key([]interface{}{uint(1001)}) data, err := conn.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 0 { t.Fatalf("Response Data len != 0") @@ -2950,7 +2981,7 @@ func TestStream_Rollback(t *testing.T) { // Select in stream data, err = stream.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 1 { t.Fatalf("Response Data len != 1") @@ -2973,13 +3004,13 @@ func TestStream_Rollback(t *testing.T) { req = NewRollbackRequest() _, err = stream.Do(req).Get() if err != nil { - t.Fatalf("Failed to Rollback: %s", err.Error()) + t.Fatalf("Failed to Rollback: %s", err) } // Select outside of transaction data, err = conn.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 0 { t.Fatalf("Response Data len != 0") @@ -2988,7 +3019,7 @@ func TestStream_Rollback(t *testing.T) { // Select inside of stream after rollback _, err = stream.Do(selectReq).Get() if err != nil { - t.Fatalf("Failed to Select: %s", err.Error()) + t.Fatalf("Failed to Select: %s", err) } if len(data) != 0 { t.Fatalf("Response Data len != 0") @@ -3084,7 +3115,7 @@ func TestStream_DoWithStrangerConn(t *testing.T) { conn := &Connection{} stream, _ := conn.NewStream() - req := test_helpers.NewStrangerRequest() + req := test_helpers.NewMockRequest() _, err := stream.Do(req).Get() if err == nil { diff --git a/test_helpers/doer.go b/test_helpers/doer.go new file mode 100644 index 000000000..c33ff0e69 --- /dev/null +++ b/test_helpers/doer.go @@ -0,0 +1,69 @@ +package test_helpers + +import ( + "bytes" + "testing" + + "github.com/tarantool/go-tarantool/v2" +) + +type doerResponse struct { + resp *MockResponse + err error +} + +// MockDoer is an implementation of the Doer interface +// used for testing purposes. +type MockDoer struct { + // Requests is a slice of received requests. + // It could be used to compare incoming requests with expected. + Requests []tarantool.Request + responses []doerResponse + t *testing.T +} + +// NewMockDoer creates a MockDoer by given responses. +// Each response could be one of two types: MockResponse or error. +func NewMockDoer(t *testing.T, responses ...interface{}) MockDoer { + t.Helper() + + mockDoer := MockDoer{t: t} + for _, response := range responses { + doerResp := doerResponse{} + + switch resp := response.(type) { + case *MockResponse: + doerResp.resp = resp + case error: + doerResp.err = resp + default: + t.Fatalf("unsupported type: %T", response) + } + + mockDoer.responses = append(mockDoer.responses, doerResp) + } + return mockDoer +} + +// Do returns a future with the current response or an error. +// It saves the current request into MockDoer.Requests. +func (doer *MockDoer) Do(req tarantool.Request) *tarantool.Future { + doer.Requests = append(doer.Requests, req) + + mockReq := NewMockRequest() + fut := tarantool.NewFuture(mockReq) + + if len(doer.responses) == 0 { + doer.t.Fatalf("list of responses is empty") + } + response := doer.responses[0] + + if response.err != nil { + fut.SetError(response.err) + } else { + fut.SetResponse(response.resp.header, bytes.NewBuffer(response.resp.data)) + } + doer.responses = doer.responses[1:] + + return fut +} diff --git a/test_helpers/example_test.go b/test_helpers/example_test.go new file mode 100644 index 000000000..6272d737d --- /dev/null +++ b/test_helpers/example_test.go @@ -0,0 +1,36 @@ +package test_helpers_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" +) + +func TestExampleMockDoer(t *testing.T) { + mockDoer := test_helpers.NewMockDoer(t, + test_helpers.NewMockResponse(t, []interface{}{"some data"}), + fmt.Errorf("some error"), + test_helpers.NewMockResponse(t, "some typed data"), + fmt.Errorf("some error"), + ) + + data, err := mockDoer.Do(tarantool.NewPingRequest()).Get() + assert.NoError(t, err) + assert.Equal(t, []interface{}{"some data"}, data) + + data, err = mockDoer.Do(tarantool.NewSelectRequest("foo")).Get() + assert.EqualError(t, err, "some error") + assert.Nil(t, data) + + var stringData string + err = mockDoer.Do(tarantool.NewInsertRequest("space")).GetTyped(&stringData) + assert.NoError(t, err) + assert.Equal(t, "some typed data", stringData) + + err = mockDoer.Do(tarantool.NewPrepareRequest("expr")).GetTyped(&stringData) + assert.EqualError(t, err, "some error") + assert.Nil(t, data) +} diff --git a/test_helpers/request.go b/test_helpers/request.go new file mode 100644 index 000000000..3756a2b54 --- /dev/null +++ b/test_helpers/request.go @@ -0,0 +1,52 @@ +package test_helpers + +import ( + "context" + "io" + + "github.com/tarantool/go-iproto" + "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v2" +) + +// MockRequest is an empty mock request used for testing purposes. +type MockRequest struct { +} + +// NewMockRequest creates an empty MockRequest. +func NewMockRequest() *MockRequest { + return &MockRequest{} +} + +// Type returns an iproto type for MockRequest. +func (req *MockRequest) Type() iproto.Type { + return iproto.Type(0) +} + +// Async returns if MockRequest expects a response. +func (req *MockRequest) Async() bool { + return false +} + +// Body fills an msgpack.Encoder with the watch request body. +func (req *MockRequest) Body(resolver tarantool.SchemaResolver, enc *msgpack.Encoder) error { + return nil +} + +// Conn returns the Connection object the request belongs to. +func (req *MockRequest) Conn() *tarantool.Connection { + return &tarantool.Connection{} +} + +// Ctx returns a context of the MockRequest. +func (req *MockRequest) Ctx() context.Context { + return nil +} + +// Response creates a response for the MockRequest. +func (req *MockRequest) Response(header tarantool.Header, + body io.Reader) (tarantool.Response, error) { + resp, err := CreateMockResponse(header, body) + return resp, err +} diff --git a/test_helpers/request_mock.go b/test_helpers/request_mock.go deleted file mode 100644 index 980c35db2..000000000 --- a/test_helpers/request_mock.go +++ /dev/null @@ -1,45 +0,0 @@ -package test_helpers - -import ( - "context" - "io" - - "github.com/tarantool/go-iproto" - "github.com/vmihailenco/msgpack/v5" - - "github.com/tarantool/go-tarantool/v2" -) - -type StrangerRequest struct { -} - -func NewStrangerRequest() *StrangerRequest { - return &StrangerRequest{} -} - -func (sr *StrangerRequest) Type() iproto.Type { - return iproto.Type(0) -} - -func (sr *StrangerRequest) Async() bool { - return false -} - -func (sr *StrangerRequest) Body(resolver tarantool.SchemaResolver, enc *msgpack.Encoder) error { - return nil -} - -func (sr *StrangerRequest) Conn() *tarantool.Connection { - return &tarantool.Connection{} -} - -func (sr *StrangerRequest) Ctx() context.Context { - return nil -} - -func (sr *StrangerRequest) Response(header tarantool.Header, - body io.Reader) (tarantool.Response, error) { - resp := tarantool.BaseResponse{} - resp.SetHeader(header) - return &resp, nil -} diff --git a/test_helpers/response.go b/test_helpers/response.go new file mode 100644 index 000000000..4a28400c0 --- /dev/null +++ b/test_helpers/response.go @@ -0,0 +1,74 @@ +package test_helpers + +import ( + "bytes" + "io" + "io/ioutil" + "testing" + + "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v2" +) + +// MockResponse is a mock response used for testing purposes. +type MockResponse struct { + // header contains response header + header tarantool.Header + // data contains data inside a response. + data []byte +} + +// NewMockResponse creates a new MockResponse with an empty header and the given data. +// body should be passed as a structure to be encoded. +// The encoded body is served as response data and will be decoded once the +// response is decoded. +func NewMockResponse(t *testing.T, body interface{}) *MockResponse { + t.Helper() + + buf := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(buf) + + err := enc.Encode(body) + if err != nil { + t.Errorf("unexpected error while encoding: %s", err) + } + + return &MockResponse{data: buf.Bytes()} +} + +// CreateMockResponse creates a MockResponse from the header and a data, +// packed inside an io.Reader. +func CreateMockResponse(header tarantool.Header, body io.Reader) (*MockResponse, error) { + if body == nil { + return &MockResponse{header: header, data: nil}, nil + } + data, err := ioutil.ReadAll(body) + if err != nil { + return nil, err + } + return &MockResponse{header: header, data: data}, nil +} + +// Header returns a header for the MockResponse. +func (resp *MockResponse) Header() tarantool.Header { + return resp.header +} + +// Decode returns the result of decoding the response data as slice. +func (resp *MockResponse) Decode() ([]interface{}, error) { + if resp.data == nil { + return nil, nil + } + dec := msgpack.NewDecoder(bytes.NewBuffer(resp.data)) + return dec.DecodeSlice() +} + +// DecodeTyped returns the result of decoding the response data. +func (resp *MockResponse) DecodeTyped(res interface{}) error { + if resp.data == nil { + return nil + } + dec := msgpack.NewDecoder(bytes.NewBuffer(resp.data)) + return dec.Decode(res) +} From a2ad272821438d0f2b6c09adc498dc3f0a84bb0d Mon Sep 17 00:00:00 2001 From: DerekBum Date: Thu, 25 Jan 2024 11:43:09 +0300 Subject: [PATCH 520/605] ci: bump 1.10 master for macOS 1.10.14 build is flacky on macOS-12 runner. Bump to 1.10.15 hopefully helps with this issue. --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 34219a3b0..cba02c6ab 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -241,7 +241,7 @@ jobs: - macos-12 tarantool: - brew - - 1.10.14 + - 1.10.15 env: # Make sense only for non-brew jobs. From b6e0c4a4cc70b1c39b0bffbf412b672797ac12da Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 25 Jan 2024 17:40:14 +0300 Subject: [PATCH 521/605] bugfix: fix read/write goroutine alive after tests TestConn_ReadWrite causes endless read/write in goroutines after execution of the test. The patch fixes the problem. --- dial_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dial_test.go b/dial_test.go index 88a582b07..8e7ec8727 100644 --- a/dial_test.go +++ b/dial_test.go @@ -107,7 +107,7 @@ func (m *mockIoConn) Read(b []byte) (int, error) { ret, err := m.readbuf.Read(b) - if m.read != nil { + if ret != 0 && m.read != nil { m.read <- struct{}{} } @@ -282,15 +282,17 @@ func TestConn_ReadWrite(t *testing.T) { 0x01, 0xce, 0x00, 0x00, 0x00, 0x02, 0x80, // Body map. }) - conn.Close() }) defer func() { dialer.conn.writeWg.Done() + conn.Close() }() fut := conn.Do(tarantool.NewPingRequest()) <-dialer.conn.written + dialer.conn.written = nil + dialer.conn.readWg.Done() <-dialer.conn.read <-dialer.conn.read From b8d9914c472a8e6f3da749426087078942aa5fa8 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 25 Jan 2024 17:04:38 +0300 Subject: [PATCH 522/605] pool: add Instance type The type Instance: ``` type Instance struct { // Name is an unique name of the instance. Name string // Dialer will be used to create a connection to the instance. Dialer tarantool.Dialer // Opts will be used to specify a connection options. Opts tarantool.Opts } ``` The type allows to specify a dialer and connection options per a pool instance. It is used in `pool.Connect`, `pool.ConnectWithOpts` and `pool.Add` to specify an instance configuration now. Closes #356 --- CHANGELOG.md | 4 +- README.md | 23 +- pool/connection_pool.go | 230 ++++++++++--------- pool/connection_pool_test.go | 314 ++++++++++++++------------ pool/example_test.go | 19 +- pool/round_robin.go | 6 +- pool/round_robin_test.go | 16 +- queue/example_connection_pool_test.go | 31 +-- 8 files changed, 351 insertions(+), 292 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f696cc0..3c9def351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,7 +65,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Change `OverrideSchema(*Schema)` to `SetSchema(Schema)` (#7) - Change values, stored by pointers in the `Schema`, `Space`, `Index` structs, to be stored by their values (#7) -- Make `Dialer` mandatory for creation a single connection / connection pool (#321) +- Make `Dialer` mandatory for creation a single connection (#321) - Remove `Connection.RemoteAddr()`, `Connection.LocalAddr()`. Add `Addr()` function instead (#321) - Remove `Connection.ClientProtocolInfo`, `Connection.ServerProtocolInfo`. @@ -93,6 +93,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` and `Pooler` return response data instead of an actual responses (#237) - Renamed `StrangerResponse` to `MockResponse` (#237) +- `pool.Connect`, `pool.ConnetcWithOpts` and `pool.Add` use a new type + `pool.Instance` to determinate connection options (#356) ### Deprecated diff --git a/README.md b/README.md index 5472e50a2..f37b3be64 100644 --- a/README.md +++ b/README.md @@ -192,16 +192,19 @@ The subpackage has been deleted. You could use `pool` instead. * The `connection_pool` subpackage has been renamed to `pool`. * The type `PoolOpts` has been renamed to `Opts`. -* `pool.Connect` now accepts context as first argument, which user may cancel - in process. If it is canceled in progress, an error will be returned. - All created connections will be closed. -* `pool.Add` now accepts context as first argument, which user may cancel in - process. -* Now you need to pass `map[string]Dialer` to the `pool.Connect` as the second - argument, instead of a list of addresses. Each dialer is associated with a - unique string ID, which allows them to be distinguished. -* `pool.GetPoolInfo` has been renamed to `pool.GetInfo`. Return type has been changed - to `map[string]ConnectionInfo`. +* `pool.Connect` and `pool.ConnectWithOpts` now accept context as the first + argument, which user may cancel in process. If it is canceled in progress, + an error will be returned and all created connections will be closed. +* `pool.Connect` and `pool.ConnectWithOpts` now accept `[]pool.Instance` as + the second argument instead of a list of addresses. Each instance is + associated with a unique string name, `Dialer` and connection options which + allows instances to be independently configured. +* `pool.Add` now accepts context as the first argument, which user may cancel + in process. +* `pool.Add` now accepts `pool.Instance` as the second argument instead of + an address, it allows to configure a new instance more flexible. +* `pool.GetPoolInfo` has been renamed to `pool.GetInfo`. Return type has been + changed to `map[string]ConnectionInfo`. * Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Pooler` return response data instead of an actual responses. diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 861221290..7dd6e0c46 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -13,6 +13,7 @@ package pool import ( "context" "errors" + "fmt" "log" "sync" "time" @@ -23,7 +24,7 @@ import ( ) var ( - ErrEmptyDialers = errors.New("dialers (second argument) should not be empty") + ErrEmptyInstances = errors.New("instances (second argument) should not be empty") ErrWrongCheckTimeout = errors.New("wrong check timeout, must be greater than 0") ErrNoConnection = errors.New("no active connections") ErrTooManyArgs = errors.New("too many arguments") @@ -50,7 +51,7 @@ type ConnectionHandler interface { // The client code may cancel adding a connection to the pool. The client // need to return an error from the Discovered call for that. In this case // the pool will close connection and will try to reopen it later. - Discovered(id string, conn *tarantool.Connection, role Role) error + Discovered(name string, conn *tarantool.Connection, role Role) error // Deactivated is called when a connection with a role has become // unavaileble to send requests. It happens if the connection is closed or // the connection role is switched. @@ -61,7 +62,17 @@ type ConnectionHandler interface { // Deactivated will not be called if a previous Discovered() call returns // an error. Because in this case, the connection does not become available // for sending requests. - Deactivated(id string, conn *tarantool.Connection, role Role) error + Deactivated(name string, conn *tarantool.Connection, role Role) error +} + +// Instance describes a single instance configuration in the pool. +type Instance struct { + // Name is an instance name. The name must be unique. + Name string + // Dialer will be used to create a connection to the instance. + Dialer tarantool.Dialer + // Opts configures a connection to the instance. + Opts tarantool.Opts } // Opts provides additional options (configurable via ConnectWithOpts). @@ -97,8 +108,7 @@ type ConnectionPool struct { ends map[string]*endpoint endsMutex sync.RWMutex - connOpts tarantool.Opts - opts Opts + opts Opts state state done chan struct{} @@ -112,8 +122,9 @@ type ConnectionPool struct { var _ Pooler = (*ConnectionPool)(nil) type endpoint struct { - id string + name string dialer tarantool.Dialer + opts tarantool.Opts notify chan tarantool.ConnEvent conn *tarantool.Connection role Role @@ -125,10 +136,11 @@ type endpoint struct { closeErr error } -func newEndpoint(id string, dialer tarantool.Dialer) *endpoint { +func newEndpoint(name string, dialer tarantool.Dialer, opts tarantool.Opts) *endpoint { return &endpoint{ - id: id, + name: name, dialer: dialer, + opts: opts, notify: make(chan tarantool.ConnEvent, 100), conn: nil, role: UnknownRole, @@ -139,34 +151,41 @@ func newEndpoint(id string, dialer tarantool.Dialer) *endpoint { } } -// ConnectWithOpts creates pool for instances with specified dialers and options opts. -// Each dialer corresponds to a certain id by which they will be distinguished. -func ConnectWithOpts(ctx context.Context, dialers map[string]tarantool.Dialer, - connOpts tarantool.Opts, opts Opts) (*ConnectionPool, error) { - if len(dialers) == 0 { - return nil, ErrEmptyDialers +// ConnectWithOpts creates pool for instances with specified instances and +// opts. Instances must have unique names. +func ConnectWithOpts(ctx context.Context, instances []Instance, + opts Opts) (*ConnectionPool, error) { + if len(instances) == 0 { + return nil, ErrEmptyInstances } + unique := make(map[string]bool) + for _, instance := range instances { + if _, ok := unique[instance.Name]; ok { + return nil, fmt.Errorf("duplicate instance name: %q", instance.Name) + } + unique[instance.Name] = true + } + if opts.CheckTimeout <= 0 { return nil, ErrWrongCheckTimeout } - size := len(dialers) + size := len(instances) rwPool := newRoundRobinStrategy(size) roPool := newRoundRobinStrategy(size) anyPool := newRoundRobinStrategy(size) connPool := &ConnectionPool{ - ends: make(map[string]*endpoint), - connOpts: connOpts, - opts: opts, - state: unknownState, - done: make(chan struct{}), - rwPool: rwPool, - roPool: roPool, - anyPool: anyPool, + ends: make(map[string]*endpoint), + opts: opts, + state: unknownState, + done: make(chan struct{}), + rwPool: rwPool, + roPool: roPool, + anyPool: anyPool, } - somebodyAlive, ctxCanceled := connPool.fillPools(ctx, dialers) + somebodyAlive, ctxCanceled := connPool.fillPools(ctx, instances) if !somebodyAlive { connPool.state.set(closedState) if ctxCanceled { @@ -186,18 +205,17 @@ func ConnectWithOpts(ctx context.Context, dialers map[string]tarantool.Dialer, return connPool, nil } -// Connect creates pool for instances with specified dialers. -// Each dialer corresponds to a certain id by which they will be distinguished. +// Connect creates pool for instances with specified instances. Instances must +// have unique names. // // It is useless to set up tarantool.Opts.Reconnect value for a connection. // The connection pool has its own reconnection logic. See // Opts.CheckTimeout description. -func Connect(ctx context.Context, dialers map[string]tarantool.Dialer, - connOpts tarantool.Opts) (*ConnectionPool, error) { +func Connect(ctx context.Context, instances []Instance) (*ConnectionPool, error) { opts := Opts{ CheckTimeout: 1 * time.Second, } - return ConnectWithOpts(ctx, dialers, connOpts, opts) + return ConnectWithOpts(ctx, instances, opts) } // ConnectedNow gets connected status of pool. @@ -234,10 +252,10 @@ func (p *ConnectionPool) ConfiguredTimeout(mode Mode) (time.Duration, error) { return conn.ConfiguredTimeout(), nil } -// Add adds a new endpoint with the id into the pool. This function -// adds the endpoint only after successful connection. -func (p *ConnectionPool) Add(ctx context.Context, id string, dialer tarantool.Dialer) error { - e := newEndpoint(id, dialer) +// Add adds a new instance into the pool. This function adds the instance +// only after successful connection. +func (p *ConnectionPool) Add(ctx context.Context, instance Instance) error { + e := newEndpoint(instance.Name, instance.Dialer, instance.Opts) p.endsMutex.Lock() // Ensure that Close()/CloseGraceful() not in progress/done. @@ -245,7 +263,7 @@ func (p *ConnectionPool) Add(ctx context.Context, id string, dialer tarantool.Di p.endsMutex.Unlock() return ErrClosed } - if _, ok := p.ends[id]; ok { + if _, ok := p.ends[instance.Name]; ok { p.endsMutex.Unlock() return ErrExists } @@ -253,12 +271,12 @@ func (p *ConnectionPool) Add(ctx context.Context, id string, dialer tarantool.Di endpointCtx, cancel := context.WithCancel(context.Background()) e.cancel = cancel - p.ends[id] = e + p.ends[instance.Name] = e p.endsMutex.Unlock() if err := p.tryConnect(ctx, e); err != nil { p.endsMutex.Lock() - delete(p.ends, id) + delete(p.ends, instance.Name) p.endsMutex.Unlock() e.cancel() close(e.closed) @@ -269,11 +287,11 @@ func (p *ConnectionPool) Add(ctx context.Context, id string, dialer tarantool.Di return nil } -// Remove removes an endpoint with the id from the pool. The call +// Remove removes an endpoint with the name from the pool. The call // closes an active connection gracefully. -func (p *ConnectionPool) Remove(id string) error { +func (p *ConnectionPool) Remove(name string) error { p.endsMutex.Lock() - endpoint, ok := p.ends[id] + endpoint, ok := p.ends[name] if !ok { p.endsMutex.Unlock() return errors.New("endpoint not exist") @@ -289,7 +307,7 @@ func (p *ConnectionPool) Remove(id string) error { close(endpoint.shutdown) } - delete(p.ends, id) + delete(p.ends, name) p.endsMutex.Unlock() <-endpoint.closed @@ -357,12 +375,12 @@ func (p *ConnectionPool) GetInfo() map[string]ConnectionInfo { return info } - for id := range p.ends { - conn, role := p.getConnectionFromPool(id) + for name := range p.ends { + conn, role := p.getConnectionFromPool(name) if conn != nil { - info[id] = ConnectionInfo{ConnectedNow: conn.ConnectedNow(), ConnRole: role} + info[name] = ConnectionInfo{ConnectedNow: conn.ConnectedNow(), ConnRole: role} } else { - info[id] = ConnectionInfo{ConnectedNow: false, ConnRole: UnknownRole} + info[name] = ConnectionInfo{ConnectedNow: false, ConnRole: UnknownRole} } } @@ -1011,22 +1029,22 @@ func (p *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (Role, er return UnknownRole, nil } -func (p *ConnectionPool) getConnectionFromPool(id string) (*tarantool.Connection, Role) { - if conn := p.rwPool.GetConnById(id); conn != nil { +func (p *ConnectionPool) getConnectionFromPool(name string) (*tarantool.Connection, Role) { + if conn := p.rwPool.GetConnection(name); conn != nil { return conn, MasterRole } - if conn := p.roPool.GetConnById(id); conn != nil { + if conn := p.roPool.GetConnection(name); conn != nil { return conn, ReplicaRole } - return p.anyPool.GetConnById(id), UnknownRole + return p.anyPool.GetConnection(name), UnknownRole } -func (p *ConnectionPool) deleteConnection(id string) { - if conn := p.anyPool.DeleteConnById(id); conn != nil { - if conn := p.rwPool.DeleteConnById(id); conn == nil { - p.roPool.DeleteConnById(id) +func (p *ConnectionPool) deleteConnection(name string) { + if conn := p.anyPool.DeleteConnection(name); conn != nil { + if conn := p.rwPool.DeleteConnection(name); conn == nil { + p.roPool.DeleteConnection(name) } // The internal connection deinitialization. p.watcherContainer.mutex.RLock() @@ -1039,7 +1057,7 @@ func (p *ConnectionPool) deleteConnection(id string) { } } -func (p *ConnectionPool) addConnection(id string, +func (p *ConnectionPool) addConnection(name string, conn *tarantool.Connection, role Role) error { // The internal connection initialization. p.watcherContainer.mutex.RLock() @@ -1069,78 +1087,80 @@ func (p *ConnectionPool) addConnection(id string, for _, watcher := range watched { watcher.unwatch(conn) } - log.Printf("tarantool: failed initialize watchers for %s: %s", id, err) + log.Printf("tarantool: failed initialize watchers for %s: %s", name, err) return err } } - p.anyPool.AddConn(id, conn) + p.anyPool.AddConnection(name, conn) switch role { case MasterRole: - p.rwPool.AddConn(id, conn) + p.rwPool.AddConnection(name, conn) case ReplicaRole: - p.roPool.AddConn(id, conn) + p.roPool.AddConnection(name, conn) } return nil } -func (p *ConnectionPool) handlerDiscovered(id string, conn *tarantool.Connection, +func (p *ConnectionPool) handlerDiscovered(name string, conn *tarantool.Connection, role Role) bool { var err error if p.opts.ConnectionHandler != nil { - err = p.opts.ConnectionHandler.Discovered(id, conn, role) + err = p.opts.ConnectionHandler.Discovered(name, conn, role) } if err != nil { - log.Printf("tarantool: storing connection to %s canceled: %s\n", id, err) + log.Printf("tarantool: storing connection to %s canceled: %s\n", name, err) return false } return true } -func (p *ConnectionPool) handlerDeactivated(id string, conn *tarantool.Connection, +func (p *ConnectionPool) handlerDeactivated(name string, conn *tarantool.Connection, role Role) { var err error if p.opts.ConnectionHandler != nil { - err = p.opts.ConnectionHandler.Deactivated(id, conn, role) + err = p.opts.ConnectionHandler.Deactivated(name, conn, role) } if err != nil { - log.Printf("tarantool: deactivating connection to %s by user failed: %s\n", id, err) + log.Printf("tarantool: deactivating connection to %s by user failed: %s\n", + name, err) } } -func (p *ConnectionPool) deactivateConnection(id string, conn *tarantool.Connection, role Role) { - p.deleteConnection(id) +func (p *ConnectionPool) deactivateConnection(name string, + conn *tarantool.Connection, role Role) { + p.deleteConnection(name) conn.Close() - p.handlerDeactivated(id, conn, role) + p.handlerDeactivated(name, conn, role) } func (p *ConnectionPool) deactivateConnections() { - for id, endpoint := range p.ends { + for name, endpoint := range p.ends { if endpoint != nil && endpoint.conn != nil { - p.deactivateConnection(id, endpoint.conn, endpoint.role) + p.deactivateConnection(name, endpoint.conn, endpoint.role) } } } func (p *ConnectionPool) processConnection(conn *tarantool.Connection, - id string, end *endpoint) bool { + name string, end *endpoint) bool { role, err := p.getConnectionRole(conn) if err != nil { conn.Close() - log.Printf("tarantool: storing connection to %s failed: %s\n", id, err) + log.Printf("tarantool: storing connection to %s failed: %s\n", name, err) return false } - if !p.handlerDiscovered(id, conn, role) { + if !p.handlerDiscovered(name, conn, role) { conn.Close() return false } - if p.addConnection(id, conn, role) != nil { + if p.addConnection(name, conn, role) != nil { conn.Close() - p.handlerDeactivated(id, conn, role) + p.handlerDeactivated(name, conn, role) return false } @@ -1149,27 +1169,27 @@ func (p *ConnectionPool) processConnection(conn *tarantool.Connection, return true } -func (p *ConnectionPool) fillPools( - ctx context.Context, - dialers map[string]tarantool.Dialer) (bool, bool) { +func (p *ConnectionPool) fillPools(ctx context.Context, + instances []Instance) (bool, bool) { somebodyAlive := false ctxCanceled := false // It is called before controller() goroutines, so we don't expect // concurrency issues here. - for id, dialer := range dialers { - end := newEndpoint(id, dialer) - p.ends[id] = end - connOpts := p.connOpts + for _, instance := range instances { + end := newEndpoint(instance.Name, instance.Dialer, instance.Opts) + p.ends[instance.Name] = end + connOpts := instance.Opts connOpts.Notify = end.notify - conn, err := tarantool.Connect(ctx, dialer, connOpts) + conn, err := tarantool.Connect(ctx, instance.Dialer, connOpts) if err != nil { - log.Printf("tarantool: connect to %s failed: %s\n", id, err.Error()) + log.Printf("tarantool: connect to %s failed: %s\n", + instance.Name, err) select { case <-ctx.Done(): ctxCanceled = true - p.ends[id] = nil + p.ends[instance.Name] = nil log.Printf("tarantool: operation was canceled") p.deactivateConnections() @@ -1177,7 +1197,7 @@ func (p *ConnectionPool) fillPools( return false, ctxCanceled default: } - } else if p.processConnection(conn, id, end) { + } else if p.processConnection(conn, instance.Name, end) { somebodyAlive = true } } @@ -1195,11 +1215,11 @@ func (p *ConnectionPool) updateConnection(e *endpoint) { if role, err := p.getConnectionRole(e.conn); err == nil { if e.role != role { - p.deleteConnection(e.id) + p.deleteConnection(e.name) p.poolsMutex.Unlock() - p.handlerDeactivated(e.id, e.conn, e.role) - opened := p.handlerDiscovered(e.id, e.conn, role) + p.handlerDeactivated(e.name, e.conn, e.role) + opened := p.handlerDiscovered(e.name, e.conn, role) if !opened { e.conn.Close() e.conn = nil @@ -1212,17 +1232,17 @@ func (p *ConnectionPool) updateConnection(e *endpoint) { p.poolsMutex.Unlock() e.conn.Close() - p.handlerDeactivated(e.id, e.conn, role) + p.handlerDeactivated(e.name, e.conn, role) e.conn = nil e.role = UnknownRole return } - if p.addConnection(e.id, e.conn, role) != nil { + if p.addConnection(e.name, e.conn, role) != nil { p.poolsMutex.Unlock() e.conn.Close() - p.handlerDeactivated(e.id, e.conn, role) + p.handlerDeactivated(e.name, e.conn, role) e.conn = nil e.role = UnknownRole return @@ -1232,11 +1252,11 @@ func (p *ConnectionPool) updateConnection(e *endpoint) { p.poolsMutex.Unlock() return } else { - p.deleteConnection(e.id) + p.deleteConnection(e.name) p.poolsMutex.Unlock() e.conn.Close() - p.handlerDeactivated(e.id, e.conn, e.role) + p.handlerDeactivated(e.name, e.conn, e.role) e.conn = nil e.role = UnknownRole return @@ -1254,7 +1274,7 @@ func (p *ConnectionPool) tryConnect(ctx context.Context, e *endpoint) error { e.conn = nil e.role = UnknownRole - connOpts := p.connOpts + connOpts := e.opts connOpts.Notify = e.notify conn, err := tarantool.Connect(ctx, e.dialer, connOpts) if err == nil { @@ -1264,11 +1284,11 @@ func (p *ConnectionPool) tryConnect(ctx context.Context, e *endpoint) error { if err != nil { conn.Close() log.Printf("tarantool: storing connection to %s failed: %s\n", - e.id, err) + e.name, err) return err } - opened := p.handlerDiscovered(e.id, conn, role) + opened := p.handlerDiscovered(e.name, conn, role) if !opened { conn.Close() return errors.New("storing connection canceled") @@ -1278,14 +1298,14 @@ func (p *ConnectionPool) tryConnect(ctx context.Context, e *endpoint) error { if p.state.get() != connectedState { p.poolsMutex.Unlock() conn.Close() - p.handlerDeactivated(e.id, conn, role) + p.handlerDeactivated(e.name, conn, role) return ErrClosed } - if err = p.addConnection(e.id, conn, role); err != nil { + if err = p.addConnection(e.name, conn, role); err != nil { p.poolsMutex.Unlock() conn.Close() - p.handlerDeactivated(e.id, conn, role) + p.handlerDeactivated(e.name, conn, role) return err } e.conn = conn @@ -1304,10 +1324,10 @@ func (p *ConnectionPool) reconnect(ctx context.Context, e *endpoint) { return } - p.deleteConnection(e.id) + p.deleteConnection(e.name) p.poolsMutex.Unlock() - p.handlerDeactivated(e.id, e.conn, e.role) + p.handlerDeactivated(e.name, e.conn, e.role) e.conn = nil e.role = UnknownRole @@ -1340,12 +1360,12 @@ func (p *ConnectionPool) controller(ctx context.Context, e *endpoint) { case <-e.close: if e.conn != nil { p.poolsMutex.Lock() - p.deleteConnection(e.id) + p.deleteConnection(e.name) p.poolsMutex.Unlock() if !shutdown { e.closeErr = e.conn.Close() - p.handlerDeactivated(e.id, e.conn, e.role) + p.handlerDeactivated(e.name, e.conn, e.role) close(e.closed) } else { // Force close the connection. @@ -1362,7 +1382,7 @@ func (p *ConnectionPool) controller(ctx context.Context, e *endpoint) { shutdown = true if e.conn != nil { p.poolsMutex.Lock() - p.deleteConnection(e.id) + p.deleteConnection(e.name) p.poolsMutex.Unlock() // We need to catch s.close in the current goroutine, so @@ -1384,9 +1404,9 @@ func (p *ConnectionPool) controller(ctx context.Context, e *endpoint) { if e.conn != nil && e.conn.ClosedNow() { p.poolsMutex.Lock() if p.state.get() == connectedState { - p.deleteConnection(e.id) + p.deleteConnection(e.name) p.poolsMutex.Unlock() - p.handlerDeactivated(e.id, e.conn, e.role) + p.handlerDeactivated(e.name, e.conn, e.role) e.conn = nil e.role = UnknownRole } else { diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index acdc756d3..ea19225c8 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -30,8 +30,6 @@ var indexNo = uint32(0) var ports = []string{"3013", "3014", "3015", "3016", "3017"} var host = "127.0.0.1" -type DialersMap = map[string]tarantool.Dialer - var servers = []string{ strings.Join([]string{host, ports[0]}, ":"), strings.Join([]string{host, ports[1]}, ":"), @@ -40,9 +38,9 @@ var servers = []string{ strings.Join([]string{host, ports[4]}, ":"), } -func makeDialer(serv string) tarantool.Dialer { +func makeDialer(server string) tarantool.Dialer { return tarantool.NetDialer{ - Address: serv, + Address: server, User: user, Password: pass, } @@ -50,23 +48,35 @@ func makeDialer(serv string) tarantool.Dialer { func makeDialers(servers []string) []tarantool.Dialer { dialers := make([]tarantool.Dialer, 0, len(servers)) - for _, serv := range servers { - dialers = append(dialers, makeDialer(serv)) + for _, server := range servers { + dialers = append(dialers, makeDialer(server)) } return dialers } -func makeDialersMap(servers []string) DialersMap { - dialersMap := DialersMap{} - for _, serv := range servers { - dialersMap[serv] = makeDialer(serv) +var dialers = makeDialers(servers) + +func makeInstance(server string, opts tarantool.Opts) pool.Instance { + return pool.Instance{ + Name: server, + Dialer: tarantool.NetDialer{ + Address: server, + User: user, + Password: pass, + }, + Opts: opts, } - return dialersMap } -var dialers = makeDialers(servers) -var dialersMap = makeDialersMap(servers) +func makeInstances(servers []string, opts tarantool.Opts) []pool.Instance { + var instances []pool.Instance + for _, server := range servers { + instances = append(instances, makeInstance(server, opts)) + } + return instances +} +var instances = makeInstances(servers, connOpts) var connOpts = tarantool.Opts{ Timeout: 5 * time.Second, } @@ -74,32 +84,40 @@ var connOpts = tarantool.Opts{ var defaultCountRetry = 5 var defaultTimeoutRetry = 500 * time.Millisecond -var instances []test_helpers.TarantoolInstance +var helpInstances []test_helpers.TarantoolInstance -func TestConnError_IncorrectParams(t *testing.T) { +func TestConnect_error_empty_instances(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, DialersMap{}, tarantool.Opts{}) + connPool, err := pool.Connect(ctx, []pool.Instance{}) cancel() require.Nilf(t, connPool, "conn is not nil with incorrect param") - require.NotNilf(t, err, "err is nil with incorrect params") - require.Equal(t, "dialers (second argument) should not be empty", err.Error()) - - ctx, cancel = test_helpers.GetPoolConnectContext() - connPool, err = pool.Connect(ctx, DialersMap{ - "err1": tarantool.NetDialer{Address: "err1"}, - "err2": tarantool.NetDialer{Address: "err2"}, - }, connOpts) + require.ErrorIs(t, err, pool.ErrEmptyInstances) +} + +func TestConnect_error_unavailable(t *testing.T) { + ctx, cancel := test_helpers.GetPoolConnectContext() + connPool, err := pool.Connect(ctx, makeInstances([]string{"err1", "err2"}, connOpts)) cancel() require.Nilf(t, connPool, "conn is not nil with incorrect param") - require.NotNilf(t, err, "err is nil with incorrect params") - require.Equal(t, "no active connections", err.Error()) + require.ErrorIs(t, err, pool.ErrNoConnection) +} + +func TestConnect_error_duplicate(t *testing.T) { + ctx, cancel := test_helpers.GetPoolConnectContext() + connPool, err := pool.Connect(ctx, makeInstances([]string{"foo", "foo"}, connOpts)) + cancel() + + require.Nilf(t, connPool, "conn is not nil with incorrect param") + require.EqualError(t, err, "duplicate instance name: \"foo\"") +} - ctx, cancel = test_helpers.GetPoolConnectContext() - connPool, err = pool.ConnectWithOpts(ctx, dialersMap, tarantool.Opts{}, pool.Opts{}) +func TestConnectWithOpts_error_no_timeout(t *testing.T) { + ctx, cancel := test_helpers.GetPoolConnectContext() + connPool, err := pool.ConnectWithOpts(ctx, makeInstances([]string{"any"}, connOpts), + pool.Opts{}) cancel() require.Nilf(t, connPool, "conn is not nil with incorrect param") - require.NotNilf(t, err, "err is nil with incorrect params") - require.Equal(t, "wrong check timeout, must be greater than 0", err.Error()) + require.ErrorIs(t, err, pool.ErrWrongCheckTimeout) } func TestConnSuccessfully(t *testing.T) { @@ -107,7 +125,7 @@ func TestConnSuccessfully(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, makeDialersMap([]string{healthyServ, "err"}), connOpts) + connPool, err := pool.Connect(ctx, makeInstances([]string{healthyServ, "err"}, connOpts)) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -140,7 +158,7 @@ func TestConnErrorAfterCtxCancel(t *testing.T) { var err error cancel() - connPool, err = pool.Connect(ctx, dialersMap, connLongReconnectOpts) + connPool, err = pool.Connect(ctx, makeInstances(servers, connLongReconnectOpts)) if connPool != nil || err == nil { t.Fatalf("ConnectionPool was created after cancel") @@ -180,17 +198,21 @@ func TestContextCancelInProgress(t *testing.T) { defer cancel() cnt := new(int) - poolDialers := DialersMap{} - for _, serv := range servers { - poolDialers[serv] = &mockClosingDialer{ - addr: serv, - cnt: cnt, - ctx: ctx, - ctxCancel: cancel, - } + var instances []pool.Instance + for _, server := range servers { + instances = append(instances, pool.Instance{ + Name: server, + Dialer: &mockClosingDialer{ + addr: server, + cnt: cnt, + ctx: ctx, + ctxCancel: cancel, + }, + Opts: connOpts, + }) } - connPool, err := pool.Connect(ctx, poolDialers, tarantool.Opts{}) + connPool, err := pool.Connect(ctx, instances) require.NotNilf(t, err, "expected err after ctx cancel") assert.Truef(t, strings.Contains(err.Error(), "operation was canceled"), fmt.Sprintf("unexpected error, expected to contain %s, got %v", @@ -201,18 +223,22 @@ func TestContextCancelInProgress(t *testing.T) { func TestConnSuccessfullyDuplicates(t *testing.T) { server := servers[0] - poolDialers := DialersMap{} + var instances []pool.Instance for i := 0; i < 4; i++ { - poolDialers[fmt.Sprintf("c%d", i)] = tarantool.NetDialer{ - Address: server, - User: user, - Password: pass, - } + instances = append(instances, pool.Instance{ + Name: fmt.Sprintf("c%d", i), + Dialer: tarantool.NetDialer{ + Address: server, + User: user, + Password: pass, + }, + Opts: connOpts, + }) } ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, poolDialers, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -240,13 +266,13 @@ func TestReconnect(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() - test_helpers.StopTarantoolWithCleanup(instances[0]) + test_helpers.StopTarantoolWithCleanup(helpInstances[0]) args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, @@ -262,7 +288,7 @@ func TestReconnect(t *testing.T) { defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) - err = test_helpers.RestartTarantool(&instances[0]) + err = test_helpers.RestartTarantool(&helpInstances[0]) require.Nilf(t, err, "failed to restart tarantool") args = test_helpers.CheckStatusesArgs{ @@ -289,14 +315,14 @@ func TestDisconnect_withReconnect(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, makeDialersMap([]string{server}), opts) + connPool, err := pool.Connect(ctx, makeInstances([]string{server}, opts)) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() // Test. - test_helpers.StopTarantoolWithCleanup(instances[serverId]) + test_helpers.StopTarantoolWithCleanup(helpInstances[serverId]) args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, Mode: pool.ANY, @@ -311,7 +337,7 @@ func TestDisconnect_withReconnect(t *testing.T) { require.Nil(t, err) // Restart the server after success. - err = test_helpers.RestartTarantool(&instances[serverId]) + err = test_helpers.RestartTarantool(&helpInstances[serverId]) require.Nilf(t, err, "failed to restart tarantool") args = test_helpers.CheckStatusesArgs{ @@ -335,14 +361,14 @@ func TestDisconnectAll(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, makeDialersMap([]string{server1, server2}), connOpts) + connPool, err := pool.Connect(ctx, makeInstances([]string{server1, server2}, connOpts)) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() - test_helpers.StopTarantoolWithCleanup(instances[0]) - test_helpers.StopTarantoolWithCleanup(instances[1]) + test_helpers.StopTarantoolWithCleanup(helpInstances[0]) + test_helpers.StopTarantoolWithCleanup(helpInstances[1]) args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, @@ -359,10 +385,10 @@ func TestDisconnectAll(t *testing.T) { defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) - err = test_helpers.RestartTarantool(&instances[0]) + err = test_helpers.RestartTarantool(&helpInstances[0]) require.Nilf(t, err, "failed to restart tarantool") - err = test_helpers.RestartTarantool(&instances[1]) + err = test_helpers.RestartTarantool(&helpInstances[1]) require.Nilf(t, err, "failed to restart tarantool") args = test_helpers.CheckStatusesArgs{ @@ -384,7 +410,7 @@ func TestDisconnectAll(t *testing.T) { func TestAdd(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, makeDialersMap(servers[:1]), connOpts) + connPool, err := pool.Connect(ctx, makeInstances(servers[:1], connOpts)) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -392,7 +418,7 @@ func TestAdd(t *testing.T) { for _, server := range servers[1:] { ctx, cancel := test_helpers.GetConnectContext() - err = connPool.Add(ctx, server, dialersMap[server]) + err = connPool.Add(ctx, makeInstance(server, connOpts)) cancel() require.Nil(t, err) } @@ -419,7 +445,7 @@ func TestAdd(t *testing.T) { func TestAdd_exist(t *testing.T) { server := servers[0] ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, makeDialersMap([]string{server}), connOpts) + connPool, err := pool.Connect(ctx, makeInstances([]string{server}, connOpts)) cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -427,7 +453,7 @@ func TestAdd_exist(t *testing.T) { defer connPool.Close() ctx, cancel = test_helpers.GetConnectContext() - err = connPool.Add(ctx, server, makeDialer(server)) + err = connPool.Add(ctx, makeInstance(server, connOpts)) cancel() require.Equal(t, pool.ErrExists, err) @@ -450,7 +476,7 @@ func TestAdd_unreachable(t *testing.T) { server := servers[0] ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, makeDialersMap([]string{server}), connOpts) + connPool, err := pool.Connect(ctx, makeInstances([]string{server}, connOpts)) cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -459,8 +485,12 @@ func TestAdd_unreachable(t *testing.T) { unhealthyServ := "127.0.0.2:6667" ctx, cancel = test_helpers.GetConnectContext() - err = connPool.Add(ctx, unhealthyServ, tarantool.NetDialer{ - Address: unhealthyServ, + err = connPool.Add(ctx, pool.Instance{ + Name: unhealthyServ, + Dialer: tarantool.NetDialer{ + Address: unhealthyServ, + }, + Opts: connOpts, }) cancel() // The OS-dependent error so we just check for existence. @@ -484,14 +514,14 @@ func TestAdd_unreachable(t *testing.T) { func TestAdd_afterClose(t *testing.T) { server := servers[0] ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, makeDialersMap([]string{server}), connOpts) + connPool, err := pool.Connect(ctx, makeInstances([]string{server}, connOpts)) cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") connPool.Close() ctx, cancel = test_helpers.GetConnectContext() - err = connPool.Add(ctx, server, dialersMap[server]) + err = connPool.Add(ctx, makeInstance(server, connOpts)) cancel() assert.Equal(t, err, pool.ErrClosed) } @@ -501,7 +531,7 @@ func TestAdd_Close_concurrent(t *testing.T) { serv1 := servers[1] ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, makeDialersMap([]string{serv0}), connOpts) + connPool, err := pool.Connect(ctx, makeInstances([]string{serv0}, connOpts)) cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -512,7 +542,7 @@ func TestAdd_Close_concurrent(t *testing.T) { defer wg.Done() ctx, cancel := test_helpers.GetConnectContext() - err = connPool.Add(ctx, serv1, makeDialer(serv1)) + err = connPool.Add(ctx, makeInstance(serv1, connOpts)) cancel() if err != nil { assert.Equal(t, pool.ErrClosed, err) @@ -529,7 +559,7 @@ func TestAdd_CloseGraceful_concurrent(t *testing.T) { serv1 := servers[1] ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, makeDialersMap([]string{serv0}), connOpts) + connPool, err := pool.Connect(ctx, makeInstances([]string{serv0}, connOpts)) cancel() require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -540,7 +570,7 @@ func TestAdd_CloseGraceful_concurrent(t *testing.T) { defer wg.Done() ctx, cancel := test_helpers.GetConnectContext() - err = connPool.Add(ctx, serv1, makeDialer(serv1)) + err = connPool.Add(ctx, makeInstance(serv1, connOpts)) cancel() if err != nil { assert.Equal(t, pool.ErrClosed, err) @@ -555,7 +585,7 @@ func TestAdd_CloseGraceful_concurrent(t *testing.T) { func TestRemove(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -584,7 +614,7 @@ func TestRemove(t *testing.T) { func TestRemove_double(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, makeDialersMap(servers[:2]), connOpts) + connPool, err := pool.Connect(ctx, makeInstances(servers[:2], connOpts)) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -613,7 +643,7 @@ func TestRemove_double(t *testing.T) { func TestRemove_unknown(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, makeDialersMap(servers[:2]), connOpts) + connPool, err := pool.Connect(ctx, makeInstances(servers[:2], connOpts)) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -641,7 +671,7 @@ func TestRemove_unknown(t *testing.T) { func TestRemove_concurrent(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, makeDialersMap(servers[:2]), connOpts) + connPool, err := pool.Connect(ctx, makeInstances(servers[:2], connOpts)) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -690,7 +720,7 @@ func TestRemove_concurrent(t *testing.T) { func TestRemove_Close_concurrent(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, makeDialersMap(servers[:2]), connOpts) + connPool, err := pool.Connect(ctx, makeInstances(servers[:2], connOpts)) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -711,7 +741,7 @@ func TestRemove_Close_concurrent(t *testing.T) { func TestRemove_CloseGraceful_concurrent(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, makeDialersMap(servers[:2]), connOpts) + connPool, err := pool.Connect(ctx, makeInstances(servers[:2], connOpts)) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -735,7 +765,7 @@ func TestClose(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, makeDialersMap([]string{server1, server2}), connOpts) + connPool, err := pool.Connect(ctx, makeInstances([]string{server1, server2}, connOpts)) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -777,7 +807,7 @@ func TestCloseGraceful(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, makeDialersMap([]string{server1, server2}), connOpts) + connPool, err := pool.Connect(ctx, makeInstances([]string{server1, server2}, connOpts)) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -842,8 +872,7 @@ func (h *testHandler) addErr(err error) { h.errs = append(h.errs, err) } -func (h *testHandler) Discovered(id string, conn *tarantool.Connection, - +func (h *testHandler) Discovered(name string, conn *tarantool.Connection, role pool.Role) error { discovered := atomic.AddUint32(&h.discovered, 1) @@ -854,29 +883,28 @@ func (h *testHandler) Discovered(id string, conn *tarantool.Connection, // discovered < 3 - initial open of connections // discovered >= 3 - update a connection after a role update - if id == servers[0] { + if name == servers[0] { if discovered < 3 && role != pool.MasterRole { - h.addErr(fmt.Errorf("unexpected init role %d for id %s", role, id)) + h.addErr(fmt.Errorf("unexpected init role %d for name %s", role, name)) } if discovered >= 3 && role != pool.ReplicaRole { - h.addErr(fmt.Errorf("unexpected updated role %d for id %s", role, id)) + h.addErr(fmt.Errorf("unexpected updated role %d for name %s", role, name)) } - } else if id == servers[1] { + } else if name == servers[1] { if discovered >= 3 { - h.addErr(fmt.Errorf("unexpected discovery for id %s", id)) + h.addErr(fmt.Errorf("unexpected discovery for name %s", name)) } if role != pool.ReplicaRole { - h.addErr(fmt.Errorf("unexpected role %d for id %s", role, id)) + h.addErr(fmt.Errorf("unexpected role %d for name %s", role, name)) } } else { - h.addErr(fmt.Errorf("unexpected discovered id %s", id)) + h.addErr(fmt.Errorf("unexpected discovered name %s", name)) } return nil } -func (h *testHandler) Deactivated(id string, conn *tarantool.Connection, - +func (h *testHandler) Deactivated(name string, conn *tarantool.Connection, role pool.Role) error { deactivated := atomic.AddUint32(&h.deactivated, 1) @@ -885,21 +913,21 @@ func (h *testHandler) Deactivated(id string, conn *tarantool.Connection, return nil } - if deactivated == 1 && id == servers[0] { + if deactivated == 1 && name == servers[0] { // A first close is a role update. if role != pool.MasterRole { - h.addErr(fmt.Errorf("unexpected removed role %d for id %s", role, id)) + h.addErr(fmt.Errorf("unexpected removed role %d for name %s", role, name)) } return nil } - if id == servers[0] || id == servers[1] { + if name == servers[0] || name == servers[1] { // Close. if role != pool.ReplicaRole { - h.addErr(fmt.Errorf("unexpected removed role %d for id %s", role, id)) + h.addErr(fmt.Errorf("unexpected removed role %d for name %s", role, name)) } } else { - h.addErr(fmt.Errorf("unexpected removed id %s", id)) + h.addErr(fmt.Errorf("unexpected removed name %s", name)) } return nil @@ -907,7 +935,7 @@ func (h *testHandler) Deactivated(id string, conn *tarantool.Connection, func TestConnectionHandlerOpenUpdateClose(t *testing.T) { poolServers := []string{servers[0], servers[1]} - poolDialers := makeDialersMap(poolServers) + poolInstances := makeInstances(poolServers, connOpts) roles := []bool{false, true} err := test_helpers.SetClusterRO(makeDialers(poolServers), connOpts, roles) @@ -920,7 +948,7 @@ func TestConnectionHandlerOpenUpdateClose(t *testing.T) { } ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.ConnectWithOpts(ctx, poolDialers, connOpts, poolOpts) + connPool, err := pool.ConnectWithOpts(ctx, poolInstances, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -974,15 +1002,13 @@ type testAddErrorHandler struct { discovered, deactivated int } -func (h *testAddErrorHandler) Discovered(id string, conn *tarantool.Connection, - +func (h *testAddErrorHandler) Discovered(name string, conn *tarantool.Connection, role pool.Role) error { h.discovered++ return fmt.Errorf("any error") } -func (h *testAddErrorHandler) Deactivated(id string, conn *tarantool.Connection, - +func (h *testAddErrorHandler) Deactivated(name string, conn *tarantool.Connection, role pool.Role) error { h.deactivated++ return nil @@ -998,7 +1024,7 @@ func TestConnectionHandlerOpenError(t *testing.T) { } ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.ConnectWithOpts(ctx, makeDialersMap(poolServers), connOpts, poolOpts) + connPool, err := pool.ConnectWithOpts(ctx, makeInstances(poolServers, connOpts), poolOpts) if err == nil { defer connPool.Close() } @@ -1011,8 +1037,7 @@ type testUpdateErrorHandler struct { discovered, deactivated uint32 } -func (h *testUpdateErrorHandler) Discovered(id string, conn *tarantool.Connection, - +func (h *testUpdateErrorHandler) Discovered(name string, conn *tarantool.Connection, role pool.Role) error { atomic.AddUint32(&h.discovered, 1) @@ -1023,8 +1048,7 @@ func (h *testUpdateErrorHandler) Discovered(id string, conn *tarantool.Connectio return nil } -func (h *testUpdateErrorHandler) Deactivated(id string, conn *tarantool.Connection, - +func (h *testUpdateErrorHandler) Deactivated(name string, conn *tarantool.Connection, role pool.Role) error { atomic.AddUint32(&h.deactivated, 1) return nil @@ -1032,7 +1056,7 @@ func (h *testUpdateErrorHandler) Deactivated(id string, conn *tarantool.Connecti func TestConnectionHandlerUpdateError(t *testing.T) { poolServers := []string{servers[0], servers[1]} - poolDialers := makeDialersMap(poolServers) + poolInstances := makeInstances(poolServers, connOpts) roles := []bool{false, false} err := test_helpers.SetClusterRO(makeDialers(poolServers), connOpts, roles) @@ -1045,7 +1069,7 @@ func TestConnectionHandlerUpdateError(t *testing.T) { } ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.ConnectWithOpts(ctx, poolDialers, connOpts, poolOpts) + connPool, err := pool.ConnectWithOpts(ctx, poolInstances, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -1092,14 +1116,14 @@ func TestRequestOnClosed(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, makeDialersMap([]string{server1, server2}), connOpts) + connPool, err := pool.Connect(ctx, makeInstances([]string{server1, server2}, connOpts)) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() - test_helpers.StopTarantoolWithCleanup(instances[0]) - test_helpers.StopTarantoolWithCleanup(instances[1]) + test_helpers.StopTarantoolWithCleanup(helpInstances[0]) + test_helpers.StopTarantoolWithCleanup(helpInstances[1]) args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, @@ -1118,10 +1142,10 @@ func TestRequestOnClosed(t *testing.T) { _, err = connPool.Ping(pool.ANY) require.NotNilf(t, err, "err is nil after Ping") - err = test_helpers.RestartTarantool(&instances[0]) + err = test_helpers.RestartTarantool(&helpInstances[0]) require.Nilf(t, err, "failed to restart tarantool") - err = test_helpers.RestartTarantool(&instances[1]) + err = test_helpers.RestartTarantool(&helpInstances[1]) require.Nilf(t, err, "failed to restart tarantool") } @@ -1133,7 +1157,7 @@ func TestCall(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1192,7 +1216,7 @@ func TestCall16(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1251,7 +1275,7 @@ func TestCall17(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1310,7 +1334,7 @@ func TestEval(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1390,7 +1414,7 @@ func TestExecute(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1448,7 +1472,7 @@ func TestRoundRobinStrategy(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1527,7 +1551,7 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1600,7 +1624,7 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1685,7 +1709,7 @@ func TestUpdateInstancesRoles(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1831,7 +1855,7 @@ func TestInsert(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1930,7 +1954,7 @@ func TestDelete(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1994,7 +2018,7 @@ func TestUpsert(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2066,7 +2090,7 @@ func TestUpdate(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2155,7 +2179,7 @@ func TestReplace(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2240,7 +2264,7 @@ func TestSelect(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2362,7 +2386,7 @@ func TestPing(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2401,7 +2425,7 @@ func TestDo(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2437,7 +2461,7 @@ func TestDo_concurrent(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2470,7 +2494,7 @@ func TestNewPrepared(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2542,7 +2566,7 @@ func TestDoWithStrangerConn(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2572,7 +2596,7 @@ func TestStream_Commit(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2663,7 +2687,7 @@ func TestStream_Rollback(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2753,7 +2777,7 @@ func TestStream_TxnIsolationLevel(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2834,7 +2858,7 @@ func TestConnectionPool_NewWatcher_no_watchers(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nill after Connect") defer connPool.Close() @@ -2864,7 +2888,7 @@ func TestConnectionPool_NewWatcher_modes(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -2944,7 +2968,7 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { } ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - pool, err := pool.ConnectWithOpts(ctx, dialersMap, connOpts, poolOpts) + pool, err := pool.ConnectWithOpts(ctx, instances, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, pool, "conn is nil after Connect") @@ -3026,7 +3050,7 @@ func TestWatcher_Unregister(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - pool, err := pool.Connect(ctx, dialersMap, connOpts) + pool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, pool, "conn is nil after Connect") defer pool.Close() @@ -3083,7 +3107,7 @@ func TestConnectionPool_NewWatcher_concurrent(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -3121,7 +3145,7 @@ func TestWatcher_Unregister_concurrent(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() @@ -3177,14 +3201,14 @@ func runTestMain(m *testing.M) int { }) } - instances, err = test_helpers.StartTarantoolInstances(instsOpts) + helpInstances, err = test_helpers.StartTarantoolInstances(instsOpts) if err != nil { log.Fatalf("Failed to prepare test tarantool: %s", err) return -1 } - defer test_helpers.StopTarantoolInstances(instances) + defer test_helpers.StopTarantoolInstances(helpInstances) return m.Run() } diff --git a/pool/example_test.go b/pool/example_test.go index 7eb480177..a4d3d4ba1 100644 --- a/pool/example_test.go +++ b/pool/example_test.go @@ -29,7 +29,7 @@ func examplePool(roles []bool, } ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, dialersMap, connOpts) + connPool, err := pool.Connect(ctx, instances) if err != nil || connPool == nil { return nil, fmt.Errorf("ConnectionPool is not established") } @@ -39,16 +39,21 @@ func examplePool(roles []bool, func exampleFeaturesPool(roles []bool, connOpts tarantool.Opts, requiredProtocol tarantool.ProtocolInfo) (*pool.ConnectionPool, error) { - poolDialersMap := map[string]tarantool.Dialer{} + poolInstances := []pool.Instance{} poolDialers := []tarantool.Dialer{} - for _, serv := range servers { - poolDialersMap[serv] = tarantool.NetDialer{ - Address: serv, + for _, server := range servers { + dialer := tarantool.NetDialer{ + Address: server, User: user, Password: pass, RequiredProtocolInfo: requiredProtocol, } - poolDialers = append(poolDialers, poolDialersMap[serv]) + poolInstances = append(poolInstances, pool.Instance{ + Name: server, + Dialer: dialer, + Opts: connOpts, + }) + poolDialers = append(poolDialers, dialer) } err := test_helpers.SetClusterRO(poolDialers, connOpts, roles) if err != nil { @@ -56,7 +61,7 @@ func exampleFeaturesPool(roles []bool, connOpts tarantool.Opts, } ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, poolDialersMap, connOpts) + connPool, err := pool.Connect(ctx, poolInstances) if err != nil || connPool == nil { return nil, fmt.Errorf("ConnectionPool is not established") } diff --git a/pool/round_robin.go b/pool/round_robin.go index 6e0b158f4..82cf26f39 100644 --- a/pool/round_robin.go +++ b/pool/round_robin.go @@ -24,7 +24,7 @@ func newRoundRobinStrategy(size int) *roundRobinStrategy { } } -func (r *roundRobinStrategy) GetConnById(id string) *tarantool.Connection { +func (r *roundRobinStrategy) GetConnection(id string) *tarantool.Connection { r.mutex.RLock() defer r.mutex.RUnlock() @@ -36,7 +36,7 @@ func (r *roundRobinStrategy) GetConnById(id string) *tarantool.Connection { return r.conns[index] } -func (r *roundRobinStrategy) DeleteConnById(id string) *tarantool.Connection { +func (r *roundRobinStrategy) DeleteConnection(id string) *tarantool.Connection { r.mutex.Lock() defer r.mutex.Unlock() @@ -93,7 +93,7 @@ func (r *roundRobinStrategy) GetConnections() map[string]*tarantool.Connection { return conns } -func (r *roundRobinStrategy) AddConn(id string, conn *tarantool.Connection) { +func (r *roundRobinStrategy) AddConnection(id string, conn *tarantool.Connection) { r.mutex.Lock() defer r.mutex.Unlock() diff --git a/pool/round_robin_test.go b/pool/round_robin_test.go index 6f133a799..6f028f2de 100644 --- a/pool/round_robin_test.go +++ b/pool/round_robin_test.go @@ -18,11 +18,11 @@ func TestRoundRobinAddDelete(t *testing.T) { conns := []*tarantool.Connection{&tarantool.Connection{}, &tarantool.Connection{}} for i, addr := range addrs { - rr.AddConn(addr, conns[i]) + rr.AddConnection(addr, conns[i]) } for i, addr := range addrs { - if conn := rr.DeleteConnById(addr); conn != conns[i] { + if conn := rr.DeleteConnection(addr); conn != conns[i] { t.Errorf("Unexpected connection on address %s", addr) } } @@ -37,16 +37,16 @@ func TestRoundRobinAddDuplicateDelete(t *testing.T) { conn1 := &tarantool.Connection{} conn2 := &tarantool.Connection{} - rr.AddConn(validAddr1, conn1) - rr.AddConn(validAddr1, conn2) + rr.AddConnection(validAddr1, conn1) + rr.AddConnection(validAddr1, conn2) - if rr.DeleteConnById(validAddr1) != conn2 { + if rr.DeleteConnection(validAddr1) != conn2 { t.Errorf("Unexpected deleted connection") } if !rr.IsEmpty() { t.Errorf("RoundRobin does not empty") } - if rr.DeleteConnById(validAddr1) != nil { + if rr.DeleteConnection(validAddr1) != nil { t.Errorf("Unexpected value after second deletion") } } @@ -58,7 +58,7 @@ func TestRoundRobinGetNextConnection(t *testing.T) { conns := []*tarantool.Connection{&tarantool.Connection{}, &tarantool.Connection{}} for i, addr := range addrs { - rr.AddConn(addr, conns[i]) + rr.AddConnection(addr, conns[i]) } expectedConns := []*tarantool.Connection{conns[0], conns[1], conns[0], conns[1]} @@ -76,7 +76,7 @@ func TestRoundRobinStrategy_GetConnections(t *testing.T) { conns := []*tarantool.Connection{&tarantool.Connection{}, &tarantool.Connection{}} for i, addr := range addrs { - rr.AddConn(addr, conns[i]) + rr.AddConnection(addr, conns[i]) } rr.GetConnections()[validAddr2] = conns[0] // GetConnections() returns a copy. diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go index b6e89d3f8..355a491ed 100644 --- a/queue/example_connection_pool_test.go +++ b/queue/example_connection_pool_test.go @@ -44,7 +44,7 @@ func NewQueueConnectionHandler(name string, cfg queue.Cfg) *QueueConnectionHandl // // NOTE: the Queue supports only a master-replica cluster configuration. It // does not support a master-master configuration. -func (h *QueueConnectionHandler) Discovered(id string, conn *tarantool.Connection, +func (h *QueueConnectionHandler) Discovered(name string, conn *tarantool.Connection, role pool.Role) error { h.mutex.Lock() defer h.mutex.Unlock() @@ -106,14 +106,14 @@ func (h *QueueConnectionHandler) Discovered(id string, conn *tarantool.Connectio return h.err } - fmt.Printf("Master %s is ready to work!\n", id) + fmt.Printf("Master %s is ready to work!\n", name) atomic.AddInt32(&h.masterCnt, 1) return nil } // Deactivated doesn't do anything useful for the example. -func (h *QueueConnectionHandler) Deactivated(id string, conn *tarantool.Connection, +func (h *QueueConnectionHandler) Deactivated(name string, conn *tarantool.Connection, role pool.Role) error { if role == pool.MasterRole { atomic.AddInt32(&h.masterCnt, -1) @@ -154,28 +154,33 @@ func Example_connectionPool() { // Create a ConnectionPool object. poolServers := []string{"127.0.0.1:3014", "127.0.0.1:3015"} poolDialers := []tarantool.Dialer{} - poolDialersMap := map[string]tarantool.Dialer{} + poolInstances := []pool.Instance{} - for _, serv := range poolServers { + connOpts := tarantool.Opts{ + Timeout: 5 * time.Second, + } + for _, server := range poolServers { dialer := tarantool.NetDialer{ - Address: serv, + Address: server, User: "test", Password: "test", } poolDialers = append(poolDialers, dialer) - poolDialersMap[serv] = dialer + poolInstances = append(poolInstances, pool.Instance{ + Name: server, + Dialer: dialer, + Opts: connOpts, + }) } - connOpts := tarantool.Opts{ - Timeout: 5 * time.Second, - } + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + poolOpts := pool.Opts{ CheckTimeout: 5 * time.Second, ConnectionHandler: h, } - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - connPool, err := pool.ConnectWithOpts(ctx, poolDialersMap, connOpts, poolOpts) + connPool, err := pool.ConnectWithOpts(ctx, poolInstances, poolOpts) if err != nil { fmt.Printf("Unable to connect to the pool: %s", err) return From 6ba01ff28ee01dcc1f22f9fa508110cdb10e66a0 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 29 Jan 2024 09:24:41 +0300 Subject: [PATCH 523/605] crud: allow interface{} as values for *ManyRequest It was a mistake to use `[]interface{}` or `[]msgpack.CustomEncoder` as types for an array of tuples or an array of objects. Users were unable to use slices of custom types as incoming values. The patch now allows the use of `interface{}` as incoming values. It makes it easier to use the API, but users need to be more careful. Therefore, we have also added examples. Closes #365 --- CHANGELOG.md | 2 ++ README.md | 4 +++ crud/example_test.go | 83 ++++++++++++++++++++++++++++++++++++++++++++ crud/insert_many.go | 12 +++---- crud/object.go | 15 +++++--- crud/replace_many.go | 12 +++---- crud/tuple.go | 10 +++++- 7 files changed, 121 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c9def351..c1b1fc170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,6 +131,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Tests with crud 1.4.0 (#336) - Tests with case sensitive SQL (#341) - Splice update operation accepts 3 arguments instead of 5 (#348) +- Unable to use a slice of custom types as a slice of tuples or objects for + `crud.*ManyRequest/crud.*ObjectManyRequest` (#365) ## [1.12.0] - 2023-06-07 diff --git a/README.md b/README.md index f37b3be64..e9989a88a 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,10 @@ The subpackage has been deleted. You could use `pool` instead. * `crud` operations `Timeout` option has `crud.OptFloat64` type instead of `crud.OptUint`. +* A slice of a custom type could be used as tuples for `ReplaceManyRequest` and + `InsertManyRequest`, `ReplaceObjectManyRequest`. +* A slice of a custom type could be used as objects for `ReplaceObjectManyRequest` + and `InsertObjectManyRequest`. #### test_helpers package diff --git a/crud/example_test.go b/crud/example_test.go index c043bd5f0..7f9e34e0c 100644 --- a/crud/example_test.go +++ b/crud/example_test.go @@ -85,6 +85,89 @@ func ExampleResult_rowsCustomType() { // [{{} 2010 45 bla}] } +// ExampleTuples_customType demonstrates how to use a slice of objects of a +// custom type as Tuples to make a ReplaceManyRequest. +func ExampleTuples_customType() { + conn := exampleConnect() + + // The type will be encoded/decoded as an array. + type Tuple struct { + _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + Id uint64 + BucketId *uint64 + Name string + } + req := crud.MakeReplaceManyRequest(exampleSpace).Tuples([]Tuple{ + Tuple{ + Id: 2010, + BucketId: nil, + Name: "bla", + }, + }) + + ret := crud.MakeResult(reflect.TypeOf(Tuple{})) + if err := conn.Do(req).GetTyped(&ret); err != nil { + fmt.Printf("Failed to execute request: %s", err) + return + } + + fmt.Println(ret.Metadata) + rows := ret.Rows.([]Tuple) + if len(rows) == 1 { + fmt.Println(rows[0].Id) + fmt.Println(*rows[0].BucketId) + fmt.Println(rows[0].Name) + } else { + fmt.Printf("Unexpected result tuples count: %d", len(rows)) + } + // Output: + // [{id unsigned false} {bucket_id unsigned true} {name string false}] + // 2010 + // 45 + // bla +} + +// ExampleObjects_customType demonstrates how to use a slice of objects of +// a custom type as Objects to make a ReplaceObjectManyRequest. +func ExampleObjects_customType() { + conn := exampleConnect() + + // The type will be encoded/decoded as a map. + type Tuple struct { + Id uint64 `msgpack:"id,omitempty"` + BucketId *uint64 `msgpack:"bucket_id,omitempty"` + Name string `msgpack:"name,omitempty"` + } + req := crud.MakeReplaceObjectManyRequest(exampleSpace).Objects([]Tuple{ + Tuple{ + Id: 2010, + BucketId: nil, + Name: "bla", + }, + }) + + ret := crud.MakeResult(reflect.TypeOf(Tuple{})) + if err := conn.Do(req).GetTyped(&ret); err != nil { + fmt.Printf("Failed to execute request: %s", err) + return + } + + fmt.Println(ret.Metadata) + rows := ret.Rows.([]Tuple) + if len(rows) == 1 { + fmt.Println(rows[0].Id) + fmt.Println(*rows[0].BucketId) + fmt.Println(rows[0].Name) + } else { + fmt.Printf("Unexpected result tuples count: %d", len(rows)) + } + // Output: + // [{id unsigned false} {bucket_id unsigned true} {name string false}] + // 2010 + // 45 + // bla +} + // ExampleResult_operationData demonstrates how to obtain information // about erroneous objects from crud.Error using `OperationData` field. func ExampleResult_operationData() { diff --git a/crud/insert_many.go b/crud/insert_many.go index 602e210d5..866c1ceb5 100644 --- a/crud/insert_many.go +++ b/crud/insert_many.go @@ -15,14 +15,14 @@ type InsertManyOpts = OperationManyOpts // `crud.insert_many` for execution by a Connection. type InsertManyRequest struct { spaceRequest - tuples []Tuple + tuples Tuples opts InsertManyOpts } type insertManyArgs struct { _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused Space string - Tuples []Tuple + Tuples Tuples Opts InsertManyOpts } @@ -37,7 +37,7 @@ func MakeInsertManyRequest(space string) InsertManyRequest { // Tuples sets the tuples for the InsertManyRequest request. // Note: default value is nil. -func (req InsertManyRequest) Tuples(tuples []Tuple) InsertManyRequest { +func (req InsertManyRequest) Tuples(tuples Tuples) InsertManyRequest { req.tuples = tuples return req } @@ -73,14 +73,14 @@ type InsertObjectManyOpts = OperationObjectManyOpts // `crud.insert_object_many` for execution by a Connection. type InsertObjectManyRequest struct { spaceRequest - objects []Object + objects Objects opts InsertObjectManyOpts } type insertObjectManyArgs struct { _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused Space string - Objects []Object + Objects Objects Opts InsertObjectManyOpts } @@ -95,7 +95,7 @@ func MakeInsertObjectManyRequest(space string) InsertObjectManyRequest { // Objects sets the objects for the InsertObjectManyRequest request. // Note: default value is nil. -func (req InsertObjectManyRequest) Objects(objects []Object) InsertObjectManyRequest { +func (req InsertObjectManyRequest) Objects(objects Objects) InsertObjectManyRequest { req.objects = objects return req } diff --git a/crud/object.go b/crud/object.go index 3f266a7ee..8803d1268 100644 --- a/crud/object.go +++ b/crud/object.go @@ -4,10 +4,17 @@ import ( "github.com/vmihailenco/msgpack/v5" ) -// Object is an interface to describe object for CRUD methods. -type Object interface { - EncodeMsgpack(enc *msgpack.Encoder) -} +// Object is an interface to describe object for CRUD methods. It can be any +// type that msgpack can encode as a map. +type Object = interface{} + +// Objects is a type to describe an array of object for CRUD methods. It can be +// any type that msgpack can encode, but encoded data must be an array of +// objects. +// +// See the reason why not just []Object: +// https://github.com/tarantool/go-tarantool/issues/365 +type Objects = interface{} // MapObject is a type to describe object as a map. type MapObject map[string]interface{} diff --git a/crud/replace_many.go b/crud/replace_many.go index 77c947718..5a5143ef8 100644 --- a/crud/replace_many.go +++ b/crud/replace_many.go @@ -15,14 +15,14 @@ type ReplaceManyOpts = OperationManyOpts // `crud.replace_many` for execution by a Connection. type ReplaceManyRequest struct { spaceRequest - tuples []Tuple + tuples Tuples opts ReplaceManyOpts } type replaceManyArgs struct { _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused Space string - Tuples []Tuple + Tuples Tuples Opts ReplaceManyOpts } @@ -37,7 +37,7 @@ func MakeReplaceManyRequest(space string) ReplaceManyRequest { // Tuples sets the tuples for the ReplaceManyRequest request. // Note: default value is nil. -func (req ReplaceManyRequest) Tuples(tuples []Tuple) ReplaceManyRequest { +func (req ReplaceManyRequest) Tuples(tuples Tuples) ReplaceManyRequest { req.tuples = tuples return req } @@ -73,14 +73,14 @@ type ReplaceObjectManyOpts = OperationObjectManyOpts // `crud.replace_object_many` for execution by a Connection. type ReplaceObjectManyRequest struct { spaceRequest - objects []Object + objects Objects opts ReplaceObjectManyOpts } type replaceObjectManyArgs struct { _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused Space string - Objects []Object + Objects Objects Opts ReplaceObjectManyOpts } @@ -95,7 +95,7 @@ func MakeReplaceObjectManyRequest(space string) ReplaceObjectManyRequest { // Objects sets the tuple for the ReplaceObjectManyRequest request. // Note: default value is nil. -func (req ReplaceObjectManyRequest) Objects(objects []Object) ReplaceObjectManyRequest { +func (req ReplaceObjectManyRequest) Objects(objects Objects) ReplaceObjectManyRequest { req.objects = objects return req } diff --git a/crud/tuple.go b/crud/tuple.go index 61291cbb0..1f6850521 100644 --- a/crud/tuple.go +++ b/crud/tuple.go @@ -1,5 +1,13 @@ package crud // Tuple is a type to describe tuple for CRUD methods. It can be any type that -// msgpask can encode. +// msgpask can encode as an array. type Tuple = interface{} + +// Tuples is a type to describe an array of tuples for CRUD methods. It can be +// any type that msgpack can encode, but encoded data must be an array of +// tuples. +// +// See the reason why not just []Tuple: +// https://github.com/tarantool/go-tarantool/issues/365 +type Tuples = interface{} From f02579a563687a53600f9988b7eba7ebced293c8 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 26 Jan 2024 16:40:08 +0300 Subject: [PATCH 524/605] pool: add a connection even on connection error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From a user's perspective, it is useful to add all target instances to the pool, even some that are not currently unavailable. This way the user don’t have to keep track of the list of actually added instances. The patch make it possible. Closes #372 --- CHANGELOG.md | 6 +- README.md | 3 + pool/connection_pool.go | 109 ++++++++++++------------------ pool/connection_pool_test.go | 124 +++++++++++++++++++++++++---------- 4 files changed, 139 insertions(+), 103 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1b1fc170..5d6ad3acf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,8 +52,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. connection objects (#136). This function now does not attempt to reconnect and tries to establish a connection only once. Function might be canceled via context. Context accepted as first argument. - `pool.Connect` and `pool.Add` now accept context as first argument, which - user may cancel in process. If `pool.Connect` is canceled in progress, an + `pool.Connect` and `pool.Add` now accept context as the first argument, which + user may cancel in process. If `pool.Connect` is canceled in progress, an error will be returned. All created connections will be closed. - `iproto.Feature` type now used instead of `ProtocolFeature` (#337) - `iproto.IPROTO_FEATURE_` constants now used instead of local `Feature` @@ -95,6 +95,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Renamed `StrangerResponse` to `MockResponse` (#237) - `pool.Connect`, `pool.ConnetcWithOpts` and `pool.Add` use a new type `pool.Instance` to determinate connection options (#356) +- `pool.Connect`, `pool.ConnectWithOpts` and `pool.Add` add connections to + the pool even it is unable to connect to it (#372) ### Deprecated diff --git a/README.md b/README.md index e9989a88a..c025cfce8 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,9 @@ The subpackage has been deleted. You could use `pool` instead. the second argument instead of a list of addresses. Each instance is associated with a unique string name, `Dialer` and connection options which allows instances to be independently configured. +* `pool.Connect`, `pool.ConnectWithOpts` and `pool.Add` add instances into + the pool even it is unable to connect to it. The pool will try to connect to + the instance later. * `pool.Add` now accepts context as the first argument, which user may cancel in process. * `pool.Add` now accepts `pool.Instance` as the second argument instead of diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 7dd6e0c46..798a43af2 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -24,9 +24,7 @@ import ( ) var ( - ErrEmptyInstances = errors.New("instances (second argument) should not be empty") ErrWrongCheckTimeout = errors.New("wrong check timeout, must be greater than 0") - ErrNoConnection = errors.New("no active connections") ErrTooManyArgs = errors.New("too many arguments") ErrIncorrectResponse = errors.New("incorrect response format") ErrIncorrectStatus = errors.New("incorrect instance status: status should be `running`") @@ -155,9 +153,6 @@ func newEndpoint(name string, dialer tarantool.Dialer, opts tarantool.Opts) *end // opts. Instances must have unique names. func ConnectWithOpts(ctx context.Context, instances []Instance, opts Opts) (*ConnectionPool, error) { - if len(instances) == 0 { - return nil, ErrEmptyInstances - } unique := make(map[string]bool) for _, instance := range instances { if _, ok := unique[instance.Name]; ok { @@ -178,28 +173,23 @@ func ConnectWithOpts(ctx context.Context, instances []Instance, connPool := &ConnectionPool{ ends: make(map[string]*endpoint), opts: opts, - state: unknownState, + state: connectedState, done: make(chan struct{}), rwPool: rwPool, roPool: roPool, anyPool: anyPool, } - somebodyAlive, ctxCanceled := connPool.fillPools(ctx, instances) - if !somebodyAlive { + canceled := connPool.fillPools(ctx, instances) + if canceled { connPool.state.set(closedState) - if ctxCanceled { - return nil, ErrContextCanceled - } - return nil, ErrNoConnection + return nil, ErrContextCanceled } - connPool.state.set(connectedState) - - for _, s := range connPool.ends { + for _, endpoint := range connPool.ends { endpointCtx, cancel := context.WithCancel(context.Background()) - s.cancel = cancel - go connPool.controller(endpointCtx, s) + endpoint.cancel = cancel + go connPool.controller(endpointCtx, endpoint) } return connPool, nil @@ -252,8 +242,12 @@ func (p *ConnectionPool) ConfiguredTimeout(mode Mode) (time.Duration, error) { return conn.ConfiguredTimeout(), nil } -// Add adds a new instance into the pool. This function adds the instance -// only after successful connection. +// Add adds a new instance into the pool. The pool will try to connect to the +// instance later if it is unable to establish a connection. +// +// The function may return an error and don't add the instance into the pool +// if the context has been cancelled or on concurrent Close()/CloseGraceful() +// call. func (p *ConnectionPool) Add(ctx context.Context, instance Instance) error { e := newEndpoint(instance.Name, instance.Dialer, instance.Opts) @@ -268,19 +262,34 @@ func (p *ConnectionPool) Add(ctx context.Context, instance Instance) error { return ErrExists } - endpointCtx, cancel := context.WithCancel(context.Background()) - e.cancel = cancel + endpointCtx, endpointCancel := context.WithCancel(context.Background()) + connectCtx, connectCancel := context.WithCancel(ctx) + e.cancel = func() { + connectCancel() + endpointCancel() + } p.ends[instance.Name] = e p.endsMutex.Unlock() - if err := p.tryConnect(ctx, e); err != nil { - p.endsMutex.Lock() - delete(p.ends, instance.Name) - p.endsMutex.Unlock() - e.cancel() - close(e.closed) - return err + if err := p.tryConnect(connectCtx, e); err != nil { + var canceled bool + select { + case <-connectCtx.Done(): + canceled = true + case <-endpointCtx.Done(): + canceled = true + default: + canceled = false + } + if canceled { + p.endsMutex.Lock() + delete(p.ends, instance.Name) + p.endsMutex.Unlock() + e.cancel() + close(e.closed) + return err + } } go p.controller(endpointCtx, e) @@ -1145,64 +1154,30 @@ func (p *ConnectionPool) deactivateConnections() { } } -func (p *ConnectionPool) processConnection(conn *tarantool.Connection, - name string, end *endpoint) bool { - role, err := p.getConnectionRole(conn) - if err != nil { - conn.Close() - log.Printf("tarantool: storing connection to %s failed: %s\n", name, err) - return false - } - - if !p.handlerDiscovered(name, conn, role) { - conn.Close() - return false - } - if p.addConnection(name, conn, role) != nil { - conn.Close() - p.handlerDeactivated(name, conn, role) - return false - } - - end.conn = conn - end.role = role - return true -} - -func (p *ConnectionPool) fillPools(ctx context.Context, - instances []Instance) (bool, bool) { - somebodyAlive := false - ctxCanceled := false - +func (p *ConnectionPool) fillPools(ctx context.Context, instances []Instance) bool { // It is called before controller() goroutines, so we don't expect // concurrency issues here. for _, instance := range instances { end := newEndpoint(instance.Name, instance.Dialer, instance.Opts) p.ends[instance.Name] = end - connOpts := instance.Opts - connOpts.Notify = end.notify - conn, err := tarantool.Connect(ctx, instance.Dialer, connOpts) - if err != nil { + + if err := p.tryConnect(ctx, end); err != nil { log.Printf("tarantool: connect to %s failed: %s\n", instance.Name, err) select { case <-ctx.Done(): - ctxCanceled = true - p.ends[instance.Name] = nil log.Printf("tarantool: operation was canceled") p.deactivateConnections() - return false, ctxCanceled + return true default: } - } else if p.processConnection(conn, instance.Name, end) { - somebodyAlive = true } } - return somebodyAlive, ctxCanceled + return false } func (p *ConnectionPool) updateConnection(e *endpoint) { diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index ea19225c8..954277922 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -86,22 +86,6 @@ var defaultTimeoutRetry = 500 * time.Millisecond var helpInstances []test_helpers.TarantoolInstance -func TestConnect_error_empty_instances(t *testing.T) { - ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, []pool.Instance{}) - cancel() - require.Nilf(t, connPool, "conn is not nil with incorrect param") - require.ErrorIs(t, err, pool.ErrEmptyInstances) -} - -func TestConnect_error_unavailable(t *testing.T) { - ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, makeInstances([]string{"err1", "err2"}, connOpts)) - cancel() - require.Nilf(t, connPool, "conn is not nil with incorrect param") - require.ErrorIs(t, err, pool.ErrNoConnection) -} - func TestConnect_error_duplicate(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() connPool, err := pool.Connect(ctx, makeInstances([]string{"foo", "foo"}, connOpts)) @@ -138,6 +122,7 @@ func TestConnSuccessfully(t *testing.T) { ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ healthyServ: true, + "err": false, }, } @@ -145,6 +130,48 @@ func TestConnSuccessfully(t *testing.T) { require.Nil(t, err) } +func TestConnect_empty(t *testing.T) { + cases := []struct { + Name string + Instances []pool.Instance + }{ + {"nil", nil}, + {"empty", []pool.Instance{}}, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, tc.Instances) + if connPool != nil { + defer connPool.Close() + } + require.NoError(t, err, "failed to create a pool") + require.NotNilf(t, connPool, "pool is nil after Connect") + require.Lenf(t, connPool.GetInfo(), 0, "empty pool expected") + }) + } +} + +func TestConnect_unavailable(t *testing.T) { + servers := []string{"err1", "err2"} + ctx, cancel := test_helpers.GetPoolConnectContext() + connPool, err := pool.Connect(ctx, makeInstances([]string{"err1", "err2"}, connOpts)) + cancel() + + if connPool != nil { + defer connPool.Close() + } + + require.NoError(t, err, "failed to create a pool") + require.NotNilf(t, connPool, "pool is nil after Connect") + require.Equal(t, map[string]pool.ConnectionInfo{ + servers[0]: pool.ConnectionInfo{ConnectedNow: false, ConnRole: pool.UnknownRole}, + servers[1]: pool.ConnectionInfo{ConnectedNow: false, ConnRole: pool.UnknownRole}, + }, connPool.GetInfo()) +} + func TestConnErrorAfterCtxCancel(t *testing.T) { var connLongReconnectOpts = tarantool.Opts{ Timeout: 5 * time.Second, @@ -410,16 +437,17 @@ func TestDisconnectAll(t *testing.T) { func TestAdd(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.Connect(ctx, makeInstances(servers[:1], connOpts)) + connPool, err := pool.Connect(ctx, []pool.Instance{}) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") defer connPool.Close() - for _, server := range servers[1:] { + for _, server := range servers { ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + err = connPool.Add(ctx, makeInstance(server, connOpts)) - cancel() require.Nil(t, err) } @@ -442,6 +470,22 @@ func TestAdd(t *testing.T) { require.Nil(t, err) } +func TestAdd_canceled_ctx(t *testing.T) { + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, []pool.Instance{}) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + ctx, cancel = test_helpers.GetConnectContext() + cancel() + + err = connPool.Add(ctx, makeInstance(servers[0], connOpts)) + require.Error(t, err) +} + func TestAdd_exist(t *testing.T) { server := servers[0] ctx, cancel := test_helpers.GetPoolConnectContext() @@ -453,8 +497,9 @@ func TestAdd_exist(t *testing.T) { defer connPool.Close() ctx, cancel = test_helpers.GetConnectContext() + defer cancel() + err = connPool.Add(ctx, makeInstance(server, connOpts)) - cancel() require.Equal(t, pool.ErrExists, err) args := test_helpers.CheckStatusesArgs{ @@ -483,18 +528,15 @@ func TestAdd_unreachable(t *testing.T) { defer connPool.Close() - unhealthyServ := "127.0.0.2:6667" - ctx, cancel = test_helpers.GetConnectContext() - err = connPool.Add(ctx, pool.Instance{ + unhealthyServ := "unreachable:6667" + err = connPool.Add(context.Background(), pool.Instance{ Name: unhealthyServ, Dialer: tarantool.NetDialer{ Address: unhealthyServ, }, Opts: connOpts, }) - cancel() - // The OS-dependent error so we just check for existence. - require.NotNil(t, err) + require.NoError(t, err) args := test_helpers.CheckStatusesArgs{ ConnPool: connPool, @@ -502,7 +544,8 @@ func TestAdd_unreachable(t *testing.T) { Servers: servers, ExpectedPoolStatus: true, ExpectedStatuses: map[string]bool{ - server: true, + server: true, + unhealthyServ: false, }, } @@ -520,9 +563,11 @@ func TestAdd_afterClose(t *testing.T) { require.NotNilf(t, connPool, "conn is nil after Connect") connPool.Close() + ctx, cancel = test_helpers.GetConnectContext() + defer cancel() + err = connPool.Add(ctx, makeInstance(server, connOpts)) - cancel() assert.Equal(t, err, pool.ErrClosed) } @@ -541,9 +586,10 @@ func TestAdd_Close_concurrent(t *testing.T) { go func() { defer wg.Done() - ctx, cancel := test_helpers.GetConnectContext() + ctx, cancel = test_helpers.GetConnectContext() + defer cancel() + err = connPool.Add(ctx, makeInstance(serv1, connOpts)) - cancel() if err != nil { assert.Equal(t, pool.ErrClosed, err) } @@ -569,9 +615,10 @@ func TestAdd_CloseGraceful_concurrent(t *testing.T) { go func() { defer wg.Done() - ctx, cancel := test_helpers.GetConnectContext() + ctx, cancel = test_helpers.GetConnectContext() + defer cancel() + err = connPool.Add(ctx, makeInstance(serv1, connOpts)) - cancel() if err != nil { assert.Equal(t, pool.ErrClosed, err) } @@ -1028,8 +1075,17 @@ func TestConnectionHandlerOpenError(t *testing.T) { if err == nil { defer connPool.Close() } - require.NotNilf(t, err, "success to connect") - require.Equalf(t, 2, h.discovered, "unexpected discovered count") + require.NoError(t, err, "failed to connect") + require.NotNil(t, connPool, "pool expected") + require.Equal(t, map[string]pool.ConnectionInfo{ + servers[0]: pool.ConnectionInfo{ConnectedNow: false, ConnRole: pool.UnknownRole}, + servers[1]: pool.ConnectionInfo{ConnectedNow: false, ConnRole: pool.UnknownRole}, + }, connPool.GetInfo()) + connPool.Close() + + // It could happen additional reconnect attempts in the background, but + // at least 2 connects on start. + require.GreaterOrEqualf(t, h.discovered, 2, "unexpected discovered count") require.Equalf(t, 0, h.deactivated, "unexpected deactivated count") } From e765b0ab1424f0851c865b0f995cf7fc8c3cbeab Mon Sep 17 00:00:00 2001 From: DerekBum Date: Thu, 18 Jan 2024 15:51:57 +0300 Subject: [PATCH 525/605] api: add helper Dialer implementations To disable SSL by default we want to transfer `OpenSslDialer` to the go-openssl repository. In order to do so, we need to minimize the amount of copy-paste of the private functions. `AuthDialer` is created as a dialer-wrapper, that calls authentication methods. `ProtoDialer` is created to receive and check the `ProtocolInfo` in the created connection. `GreetingDialer` is created to fill the `Greeting` in the created connection. Part of #301 --- CHANGELOG.md | 5 + connection.go | 4 +- dial.go | 405 +++++++++++++++++++++++++++++++--------------- dial_test.go | 246 +++++++++++++++++++++++++++- ssl_test.go | 8 +- tarantool_test.go | 22 ++- 6 files changed, 549 insertions(+), 141 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d6ad3acf..28a20ed8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,11 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. the response (#237) - Ability to mock connections for tests (#237). Added new types `MockDoer`, `MockRequest` to `test_helpers`. +- `AuthDialer` type for creating a dialer with authentication (#301) +- `ProtocolDialer` type for creating a dialer with `ProtocolInfo` receiving and + check (#301) +- `GreetingDialer` type for creating a dialer, that fills `Greeting` of a + connection (#301) ### Changed diff --git a/connection.go b/connection.go index 8f8631a31..24edebece 100644 --- a/connection.go +++ b/connection.go @@ -440,7 +440,9 @@ func (conn *Connection) dial(ctx context.Context) error { } conn.addr = c.Addr() - conn.Greeting.Version = c.Greeting().Version + connGreeting := c.Greeting() + conn.Greeting.Version = connGreeting.Version + conn.Greeting.Salt = connGreeting.Salt conn.serverProtocolInfo = c.ProtocolInfo() spaceAndIndexNamesSupported := diff --git a/dial.go b/dial.go index ff5419760..c1f319549 100644 --- a/dial.go +++ b/dial.go @@ -20,7 +20,10 @@ const bufSize = 128 * 1024 // Greeting is a message sent by Tarantool on connect. type Greeting struct { + // Version is the supported protocol version. Version string + // Salt is used to authenticate a user. + Salt string } // writeFlusher is the interface that groups the basic Write and Flush methods. @@ -71,30 +74,94 @@ type Dialer interface { } type tntConn struct { - net net.Conn - reader io.Reader - writer writeFlusher + net net.Conn + reader io.Reader + writer writeFlusher +} + +// Addr makes tntConn satisfy the Conn interface. +func (c *tntConn) Addr() net.Addr { + return c.net.RemoteAddr() +} + +// Read makes tntConn satisfy the Conn interface. +func (c *tntConn) Read(p []byte) (int, error) { + return c.reader.Read(p) +} + +// Write makes tntConn satisfy the Conn interface. +func (c *tntConn) Write(p []byte) (int, error) { + if l, err := c.writer.Write(p); err != nil { + return l, err + } else if l != len(p) { + return l, errors.New("wrong length written") + } else { + return l, nil + } +} + +// Flush makes tntConn satisfy the Conn interface. +func (c *tntConn) Flush() error { + return c.writer.Flush() +} + +// Close makes tntConn satisfy the Conn interface. +func (c *tntConn) Close() error { + return c.net.Close() +} + +// Greeting makes tntConn satisfy the Conn interface. +func (c *tntConn) Greeting() Greeting { + return Greeting{} +} + +// ProtocolInfo makes tntConn satisfy the Conn interface. +func (c *tntConn) ProtocolInfo() ProtocolInfo { + return ProtocolInfo{} +} + +// protocolConn is a wrapper for connections, so they contain the ProtocolInfo. +type protocolConn struct { + Conn + protocolInfo ProtocolInfo +} + +// ProtocolInfo returns ProtocolInfo of a protocolConn. +func (c *protocolConn) ProtocolInfo() ProtocolInfo { + return c.protocolInfo +} + +// greetingConn is a wrapper for connections, so they contain the Greeting. +type greetingConn struct { + Conn greeting Greeting - protocol ProtocolInfo } -// rawDial does basic dial operations: -// reads greeting, identifies a protocol and validates it. -func rawDial(conn *tntConn, requiredProto ProtocolInfo) (string, error) { - version, salt, err := readGreeting(conn.reader) +// Greeting returns Greeting of a greetingConn. +func (c *greetingConn) Greeting() Greeting { + return c.greeting +} + +type netDialer struct { + address string +} + +func (d netDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { + var err error + conn := new(tntConn) + + network, address := parseAddress(d.address) + dialer := net.Dialer{} + conn.net, err = dialer.DialContext(ctx, network, address) if err != nil { - return "", fmt.Errorf("failed to read greeting: %w", err) + return nil, fmt.Errorf("failed to dial: %w", err) } - conn.greeting.Version = version - if conn.protocol, err = identify(conn.writer, conn.reader); err != nil { - return "", fmt.Errorf("failed to identify: %w", err) - } + dc := &deadlineIO{to: opts.IoTimeout, c: conn.net} + conn.reader = bufio.NewReaderSize(dc, bufSize) + conn.writer = bufio.NewWriterSize(dc, bufSize) - if err = checkProtocolInfo(requiredProto, conn.protocol); err != nil { - return "", fmt.Errorf("invalid server protocol: %w", err) - } - return salt, err + return conn, nil } // NetDialer is a basic Dialer implementation. @@ -121,12 +188,46 @@ type NetDialer struct { // Dial makes NetDialer satisfy the Dialer interface. func (d NetDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { + dialer := AuthDialer{ + Dialer: ProtocolDialer{ + Dialer: GreetingDialer{ + Dialer: netDialer{ + address: d.Address, + }, + }, + RequiredProtocolInfo: d.RequiredProtocolInfo, + }, + Auth: ChapSha1Auth, + Username: d.User, + Password: d.Password, + } + + return dialer.Dial(ctx, opts) +} + +type openSslDialer struct { + address string + sslKeyFile string + sslCertFile string + sslCaFile string + sslCiphers string + sslPassword string + sslPasswordFile string +} + +func (d openSslDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { var err error conn := new(tntConn) - network, address := parseAddress(d.Address) - dialer := net.Dialer{} - conn.net, err = dialer.DialContext(ctx, network, address) + network, address := parseAddress(d.address) + conn.net, err = sslDialContext(ctx, network, address, sslOpts{ + KeyFile: d.sslKeyFile, + CertFile: d.sslCertFile, + CaFile: d.sslCaFile, + Ciphers: d.sslCiphers, + Password: d.sslPassword, + PasswordFile: d.sslPasswordFile, + }) if err != nil { return nil, fmt.Errorf("failed to dial: %w", err) } @@ -135,22 +236,6 @@ func (d NetDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { conn.reader = bufio.NewReaderSize(dc, bufSize) conn.writer = bufio.NewWriterSize(dc, bufSize) - salt, err := rawDial(conn, d.RequiredProtocolInfo) - if err != nil { - conn.net.Close() - return nil, err - } - - if d.User == "" { - return conn, nil - } - - conn.protocol.Auth = ChapSha1Auth - if err = authenticate(conn, ChapSha1Auth, d.User, d.Password, salt); err != nil { - conn.net.Close() - return nil, fmt.Errorf("failed to authenticate: %w", err) - } - return conn, nil } @@ -206,61 +291,27 @@ type OpenSslDialer struct { // Dial makes OpenSslDialer satisfy the Dialer interface. func (d OpenSslDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { - var err error - conn := new(tntConn) - - network, address := parseAddress(d.Address) - conn.net, err = sslDialContext(ctx, network, address, sslOpts{ - KeyFile: d.SslKeyFile, - CertFile: d.SslCertFile, - CaFile: d.SslCaFile, - Ciphers: d.SslCiphers, - Password: d.SslPassword, - PasswordFile: d.SslPasswordFile, - }) - if err != nil { - return nil, fmt.Errorf("failed to dial: %w", err) - } - - dc := &deadlineIO{to: opts.IoTimeout, c: conn.net} - conn.reader = bufio.NewReaderSize(dc, bufSize) - conn.writer = bufio.NewWriterSize(dc, bufSize) - - salt, err := rawDial(conn, d.RequiredProtocolInfo) - if err != nil { - conn.net.Close() - return nil, err - } - - if d.User == "" { - return conn, nil - } - - if d.Auth == AutoAuth { - if conn.protocol.Auth != AutoAuth { - d.Auth = conn.protocol.Auth - } else { - d.Auth = ChapSha1Auth - } - } - conn.protocol.Auth = d.Auth - - if err = authenticate(conn, d.Auth, d.User, d.Password, salt); err != nil { - conn.net.Close() - return nil, fmt.Errorf("failed to authenticate: %w", err) - } - - return conn, nil -} - -// FdDialer allows to use an existing socket fd for connection. -type FdDialer struct { - // Fd is a socket file descrpitor. - Fd uintptr - // RequiredProtocol contains minimal protocol version and - // list of protocol features that should be supported by - // Tarantool server. By default, there are no restrictions. - RequiredProtocolInfo ProtocolInfo + dialer := AuthDialer{ + Dialer: ProtocolDialer{ + Dialer: GreetingDialer{ + Dialer: openSslDialer{ + address: d.Address, + sslKeyFile: d.SslKeyFile, + sslCertFile: d.SslCertFile, + sslCaFile: d.SslCaFile, + sslCiphers: d.SslCiphers, + sslPassword: d.SslPassword, + sslPasswordFile: d.SslPasswordFile, + }, + }, + RequiredProtocolInfo: d.RequiredProtocolInfo, + }, + Auth: d.Auth, + Username: d.User, + Password: d.Password, + } + + return dialer.Dial(ctx, opts) } type fdAddr struct { @@ -284,69 +335,163 @@ func (c *fdConn) RemoteAddr() net.Addr { return c.Addr } -// Dial makes FdDialer satisfy the Dialer interface. -func (d FdDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { - file := os.NewFile(d.Fd, "") +type fdDialer struct { + fd uintptr +} + +func (d fdDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { + file := os.NewFile(d.fd, "") c, err := net.FileConn(file) if err != nil { return nil, fmt.Errorf("failed to dial: %w", err) } conn := new(tntConn) - conn.net = &fdConn{Conn: c, Addr: fdAddr{Fd: d.Fd}} + conn.net = &fdConn{Conn: c, Addr: fdAddr{Fd: d.fd}} dc := &deadlineIO{to: opts.IoTimeout, c: conn.net} conn.reader = bufio.NewReaderSize(dc, bufSize) conn.writer = bufio.NewWriterSize(dc, bufSize) - _, err = rawDial(conn, d.RequiredProtocolInfo) - if err != nil { - conn.net.Close() - return nil, err - } - return conn, nil } -// Addr makes tntConn satisfy the Conn interface. -func (c *tntConn) Addr() net.Addr { - return c.net.RemoteAddr() +// FdDialer allows using an existing socket fd for connection. +type FdDialer struct { + // Fd is a socket file descriptor. + Fd uintptr + // RequiredProtocol contains minimal protocol version and + // list of protocol features that should be supported by + // Tarantool server. By default, there are no restrictions. + RequiredProtocolInfo ProtocolInfo } -// Read makes tntConn satisfy the Conn interface. -func (c *tntConn) Read(p []byte) (int, error) { - return c.reader.Read(p) +// Dial makes FdDialer satisfy the Dialer interface. +func (d FdDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { + dialer := ProtocolDialer{ + Dialer: GreetingDialer{ + Dialer: fdDialer{ + fd: d.Fd, + }, + }, + RequiredProtocolInfo: d.RequiredProtocolInfo, + } + + return dialer.Dial(ctx, opts) } -// Write makes tntConn satisfy the Conn interface. -func (c *tntConn) Write(p []byte) (int, error) { - if l, err := c.writer.Write(p); err != nil { - return l, err - } else if l != len(p) { - return l, errors.New("wrong length written") - } else { - return l, nil +// AuthDialer is a dialer-wrapper that does authentication of a user. +type AuthDialer struct { + // Dialer is a base dialer. + Dialer Dialer + // Authentication options. + Auth Auth + // Username is a name of a user for authentication. + Username string + // Password is a user password for authentication. + Password string +} + +// Dial makes AuthDialer satisfy the Dialer interface. +func (d AuthDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { + conn, err := d.Dialer.Dial(ctx, opts) + if err != nil { + return conn, err + } + greeting := conn.Greeting() + if greeting.Salt == "" { + conn.Close() + return nil, fmt.Errorf("failed to authenticate: " + + "an invalid connection without salt") } + + if d.Username == "" { + return conn, nil + } + + protocolAuth := conn.ProtocolInfo().Auth + if d.Auth == AutoAuth { + if protocolAuth != AutoAuth { + d.Auth = protocolAuth + } else { + d.Auth = ChapSha1Auth + } + } + + if err := authenticate(conn, d.Auth, d.Username, d.Password, + conn.Greeting().Salt); err != nil { + conn.Close() + return nil, fmt.Errorf("failed to authenticate: %w", err) + } + return conn, nil } -// Flush makes tntConn satisfy the Conn interface. -func (c *tntConn) Flush() error { - return c.writer.Flush() +// ProtocolDialer is a dialer-wrapper that reads and fills the ProtocolInfo +// of a connection. +type ProtocolDialer struct { + // Dialer is a base dialer. + Dialer Dialer + // RequiredProtocol contains minimal protocol version and + // list of protocol features that should be supported by + // Tarantool server. By default, there are no restrictions. + RequiredProtocolInfo ProtocolInfo } -// Close makes tntConn satisfy the Conn interface. -func (c *tntConn) Close() error { - return c.net.Close() +// Dial makes ProtocolDialer satisfy the Dialer interface. +func (d ProtocolDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { + conn, err := d.Dialer.Dial(ctx, opts) + if err != nil { + return conn, err + } + + protocolConn := protocolConn{ + Conn: conn, + protocolInfo: d.RequiredProtocolInfo, + } + + protocolConn.protocolInfo, err = identify(&protocolConn) + if err != nil { + protocolConn.Close() + return nil, fmt.Errorf("failed to identify: %w", err) + } + + err = checkProtocolInfo(d.RequiredProtocolInfo, protocolConn.protocolInfo) + if err != nil { + protocolConn.Close() + return nil, fmt.Errorf("invalid server protocol: %w", err) + } + + return &protocolConn, nil } -// Greeting makes tntConn satisfy the Conn interface. -func (c *tntConn) Greeting() Greeting { - return c.greeting +// GreetingDialer is a dialer-wrapper that reads and fills the Greeting +// of a connection. +type GreetingDialer struct { + // Dialer is a base dialer. + Dialer Dialer } -// ProtocolInfo makes tntConn satisfy the Conn interface. -func (c *tntConn) ProtocolInfo() ProtocolInfo { - return c.protocol +// Dial makes GreetingDialer satisfy the Dialer interface. +func (d GreetingDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { + conn, err := d.Dialer.Dial(ctx, opts) + if err != nil { + return conn, err + } + + greetingConn := greetingConn{ + Conn: conn, + } + version, salt, err := readGreeting(greetingConn) + if err != nil { + greetingConn.Close() + return nil, fmt.Errorf("failed to read greeting: %w", err) + } + greetingConn.greeting = Greeting{ + Version: version, + Salt: salt, + } + + return &greetingConn, err } // parseAddress split address into network and address parts. @@ -390,15 +535,15 @@ func readGreeting(reader io.Reader) (string, string, error) { // identify sends info about client protocol, receives info // about server protocol in response and stores it in the connection. -func identify(w writeFlusher, r io.Reader) (ProtocolInfo, error) { +func identify(conn Conn) (ProtocolInfo, error) { var info ProtocolInfo req := NewIdRequest(clientProtocolInfo) - if err := writeRequest(w, req); err != nil { + if err := writeRequest(conn, req); err != nil { return info, err } - resp, err := readResponse(r, req) + resp, err := readResponse(conn, req) if err != nil { if resp != nil && resp.Header().Error == iproto.ER_UNKNOWN_REQUEST_TYPE { diff --git a/dial_test.go b/dial_test.go index 8e7ec8727..b41a504df 100644 --- a/dial_test.go +++ b/dial_test.go @@ -3,6 +3,7 @@ package tarantool_test import ( "bytes" "context" + "encoding/base64" "errors" "fmt" "net" @@ -235,6 +236,7 @@ func TestConn_Addr(t *testing.T) { func TestConn_Greeting(t *testing.T) { greeting := tarantool.Greeting{ Version: "any", + Salt: "salt", } conn, dialer := dialIo(t, func(conn *mockIoConn) { conn.greeting = greeting @@ -522,6 +524,7 @@ func testDialer(t *testing.T, l net.Listener, dialer tarantool.Dialer, require.NoError(t, err) require.Equal(t, opts.expectedProtocolInfo, conn.ProtocolInfo()) require.Equal(t, testDialVersion[:], []byte(conn.Greeting().Version)) + require.Equal(t, testDialSalt[:44], []byte(conn.Greeting().Salt)) actual := <-ch require.Equal(t, idRequestExpected, actual.IdRequest) @@ -551,9 +554,8 @@ func TestNetDialer_Dial(t *testing.T) { expectedProtocolInfo: idResponseTyped.Clone(), }, { - name: "id request unsupported", - // Dialer sets auth. - expectedProtocolInfo: tarantool.ProtocolInfo{Auth: tarantool.ChapSha1Auth}, + name: "id request unsupported", + expectedProtocolInfo: tarantool.ProtocolInfo{}, isIdUnsupported: true, }, { @@ -678,3 +680,241 @@ func TestFdDialer_Dial_requirements(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "invalid server protocol") } + +func TestAuthDialer_Dial_DialerError(t *testing.T) { + dialer := tarantool.AuthDialer{ + Dialer: mockErrorDialer{ + err: fmt.Errorf("some error"), + }, + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + if conn != nil { + conn.Close() + } + + assert.NotNil(t, err) + assert.EqualError(t, err, "some error") +} + +func TestAuthDialer_Dial_NoSalt(t *testing.T) { + dialer := tarantool.AuthDialer{ + Dialer: &mockIoDialer{ + init: func(conn *mockIoConn) { + conn.greeting = tarantool.Greeting{ + Salt: "", + } + }, + }, + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + + assert.NotNil(t, err) + assert.ErrorContains(t, err, "an invalid connection without salt") + if conn != nil { + conn.Close() + t.Errorf("connection is not nil") + } +} + +func TestAuthDialer_Dial(t *testing.T) { + salt := fmt.Sprintf("%s", testDialSalt) + salt = base64.StdEncoding.EncodeToString([]byte(salt)) + dialer := mockIoDialer{ + init: func(conn *mockIoConn) { + conn.greeting.Salt = salt + conn.writeWgDelay = 1 + conn.readWgDelay = 2 + conn.readbuf.Write(okResponse) + }, + } + defer func() { + dialer.conn.writeWg.Done() + }() + + authDialer := tarantool.AuthDialer{ + Dialer: &dialer, + Username: "test", + Password: "test", + } + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := authDialer.Dial(ctx, tarantool.DialOpts{}) + if conn != nil { + conn.Close() + } + + assert.Nil(t, err) + assert.NotNil(t, conn) + assert.Equal(t, authRequestExpectedChapSha1[:41], dialer.conn.writebuf.Bytes()[:41]) +} + +func TestProtocolDialer_Dial_DialerError(t *testing.T) { + dialer := tarantool.ProtocolDialer{ + Dialer: mockErrorDialer{ + err: fmt.Errorf("some error"), + }, + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + if conn != nil { + conn.Close() + } + + assert.NotNil(t, err) + assert.EqualError(t, err, "some error") +} + +func TestProtocolDialer_Dial_IdentifyFailed(t *testing.T) { + dialer := tarantool.ProtocolDialer{ + Dialer: &mockIoDialer{ + init: func(conn *mockIoConn) { + conn.info = tarantool.ProtocolInfo{Version: 1} + conn.writeWgDelay = 1 + conn.readWgDelay = 2 + conn.readbuf.Write(errResponse) + }, + }, + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + + assert.NotNil(t, err) + assert.ErrorContains(t, err, "failed to identify") + if conn != nil { + conn.Close() + t.Errorf("connection is not nil") + } +} + +func TestProtocolDialer_Dial_WrongInfo(t *testing.T) { + dialer := tarantool.ProtocolDialer{ + Dialer: &mockIoDialer{ + init: func(conn *mockIoConn) { + conn.info = tarantool.ProtocolInfo{Version: 1} + conn.writeWgDelay = 1 + conn.readWgDelay = 2 + conn.readbuf.Write(idResponse) + }, + }, + RequiredProtocolInfo: validProtocolInfo, + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + + assert.NotNil(t, err) + assert.ErrorContains(t, err, "invalid server protocol") + if conn != nil { + conn.Close() + t.Errorf("connection is not nil") + } +} + +func TestProtocolDialer_Dial(t *testing.T) { + protoInfo := tarantool.ProtocolInfo{ + Auth: tarantool.ChapSha1Auth, + Version: 6, + Features: []iproto.Feature{0x01, 0x15}, + } + + dialer := tarantool.ProtocolDialer{ + Dialer: &mockIoDialer{ + init: func(conn *mockIoConn) { + conn.info = tarantool.ProtocolInfo{Version: 1} + conn.writeWgDelay = 1 + conn.readWgDelay = 2 + conn.readbuf.Write(idResponse) + }, + }, + RequiredProtocolInfo: protoInfo, + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + if conn != nil { + conn.Close() + } + + assert.Nil(t, err) + assert.NotNil(t, conn) + assert.Equal(t, protoInfo, conn.ProtocolInfo()) +} + +func TestGreetingDialer_Dial_DialerError(t *testing.T) { + dialer := tarantool.GreetingDialer{ + Dialer: mockErrorDialer{ + err: fmt.Errorf("some error"), + }, + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + if conn != nil { + conn.Close() + } + + assert.NotNil(t, err) + assert.EqualError(t, err, "some error") +} + +func TestGreetingDialer_Dial_GreetingFailed(t *testing.T) { + dialer := tarantool.GreetingDialer{ + Dialer: &mockIoDialer{ + init: func(conn *mockIoConn) { + conn.writeWgDelay = 1 + conn.readWgDelay = 2 + conn.readbuf.Write(errResponse) + }, + }, + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + + assert.NotNil(t, err) + assert.ErrorContains(t, err, "failed to read greeting") + if conn != nil { + conn.Close() + t.Errorf("connection is not nil") + } +} + +func TestGreetingDialer_Dial(t *testing.T) { + dialer := tarantool.GreetingDialer{ + Dialer: &mockIoDialer{ + init: func(conn *mockIoConn) { + conn.info = tarantool.ProtocolInfo{Version: 1} + conn.writeWgDelay = 1 + conn.readWgDelay = 3 + conn.readbuf.Write(append(testDialVersion[:], testDialSalt[:]...)) + conn.readbuf.Write(idResponse) + }, + }, + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + if conn != nil { + conn.Close() + } + + assert.Nil(t, err) + assert.NotNil(t, conn) + assert.Equal(t, string(testDialVersion[:]), conn.Greeting().Version) + assert.Equal(t, string(testDialSalt[:44]), conn.Greeting().Salt) +} diff --git a/ssl_test.go b/ssl_test.go index 44b26eb05..f161b98f0 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -654,9 +654,8 @@ func TestOpenSslDialer_Dial_basic(t *testing.T) { expectedProtocolInfo: idResponseTyped.Clone(), }, { - name: "id request unsupported", - // Dialer sets auth. - expectedProtocolInfo: ProtocolInfo{Auth: ChapSha1Auth}, + name: "id request unsupported", + expectedProtocolInfo: ProtocolInfo{}, isIdUnsupported: true, }, { @@ -730,8 +729,9 @@ func TestOpenSslDialer_Dial_papSha256Auth(t *testing.T) { Auth: PapSha256Auth, } + // Response from the server. protocol := idResponseTyped.Clone() - protocol.Auth = PapSha256Auth + protocol.Auth = ChapSha1Auth testDialer(t, l, dialer, testDialOpts{ expectedProtocolInfo: protocol, diff --git a/tarantool_test.go b/tarantool_test.go index c3f6b4c0b..c4db04cb3 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -772,6 +772,24 @@ func TestNetDialer(t *testing.T) { assert.Equal([]byte{0x83, 0x00, 0xce, 0x00, 0x00, 0x00, 0x00}, buf[:7]) } +func TestNetDialer_BadUser(t *testing.T) { + badDialer := NetDialer{ + Address: server, + User: "Cpt Smollett", + Password: "none", + } + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + + conn, err := Connect(ctx, badDialer, opts) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "failed to authenticate") + if conn != nil { + conn.Close() + t.Errorf("connection is not nil") + } +} + func TestFutureMultipleGetGetTyped(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() @@ -3273,9 +3291,7 @@ func TestConnectionProtocolInfoUnsupported(t *testing.T) { defer conn.Close() serverProtocolInfo := conn.ProtocolInfo() - expected := ProtocolInfo{ - Auth: ChapSha1Auth, - } + expected := ProtocolInfo{} require.Equal(t, expected, serverProtocolInfo) } From c83fcdc734c772c5c60f48a08dc70959223a5bdb Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 2 Feb 2024 15:38:45 +0300 Subject: [PATCH 526/605] pool: add a new method `Pool.DoInstance` The method allows to execute a request on the target instance in a pool. Closes #376 --- CHANGELOG.md | 2 ++ pool/connection_pool.go | 10 ++++++ pool/connection_pool_test.go | 70 ++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28a20ed8f..0c3b026ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. check (#301) - `GreetingDialer` type for creating a dialer, that fills `Greeting` of a connection (#301) +- New method `Pool.DoInstance` to execute a request on a target instance in + a pool (#376). ### Changed diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 798a43af2..9a79f9116 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -1002,6 +1002,16 @@ func (p *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarantool.Fut return conn.Do(req) } +// DoInstance sends the request into a target instance and returns a future. +func (p *ConnectionPool) DoInstance(req tarantool.Request, name string) *tarantool.Future { + conn := p.anyPool.GetConnection(name) + if conn == nil { + return newErrorFuture(ErrNoHealthyInstance) + } + + return conn.Do(req) +} + // // private // diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index 954277922..cadd83564 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -2540,6 +2540,76 @@ func TestDo_concurrent(t *testing.T) { wg.Wait() } +func TestDoInstance(t *testing.T) { + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + + connPool, err := pool.Connect(ctx, instances) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + req := tarantool.NewEvalRequest("return box.cfg.listen") + for _, server := range servers { + data, err := connPool.DoInstance(req, server).Get() + require.NoError(t, err) + assert.Equal(t, []interface{}{server}, data) + } +} + +func TestDoInstance_not_found(t *testing.T) { + roles := []bool{true, true, false, true, false} + + err := test_helpers.SetClusterRO(dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + + connPool, err := pool.Connect(ctx, []pool.Instance{}) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + data, err := connPool.DoInstance(tarantool.NewPingRequest(), "not_exist").Get() + assert.Nil(t, data) + require.ErrorIs(t, err, pool.ErrNoHealthyInstance) +} + +func TestDoInstance_concurrent(t *testing.T) { + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, instances) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + eval := tarantool.NewEvalRequest("return box.cfg.listen") + ping := tarantool.NewPingRequest() + const concurrency = 100 + var wg sync.WaitGroup + wg.Add(concurrency) + + for i := 0; i < concurrency; i++ { + go func() { + defer wg.Done() + + for _, server := range servers { + data, err := connPool.DoInstance(eval, server).Get() + require.NoError(t, err) + assert.Equal(t, []interface{}{server}, data) + } + _, err := connPool.DoInstance(ping, "not_exist").Get() + require.ErrorIs(t, err, pool.ErrNoHealthyInstance) + }() + } + + wg.Wait() +} + func TestNewPrepared(t *testing.T) { test_helpers.SkipIfSQLUnsupported(t) From df9254cc11c75fc2cc2ddafbb9dbb9d5ba39f700 Mon Sep 17 00:00:00 2001 From: DerekBum Date: Sat, 3 Feb 2024 21:44:15 +0300 Subject: [PATCH 527/605] api: remove ssl `OpenSslDialer` and all of its helper functions and tests were rellocated to the `go-tlsdialer` [1] package (and renamed to `OpenSSLDialer`). So now we can safely remove all the copy-pasted code from `go-tarantool`. This way, in order to use SSL, user should import the `go-tlsdialer` package and call functions from there. 1. https://github.com/tarantool/go-tlsdialer/ Part of #301 --- .github/workflows/testing.yml | 23 - CHANGELOG.md | 2 +- CONTRIBUTING.md | 14 - README.md | 66 ++- dial.go | 109 ----- dial_test.go | 48 ++- example_test.go | 18 - export_test.go | 13 - go.mod | 4 - go.sum | 17 +- ssl.go | 30 -- ssl_disable.go | 19 - ssl_enable.go | 144 ------- ssl_test.go | 780 ---------------------------------- tarantool_test.go | 20 + testdata/ca.crt | 20 - testdata/empty | 0 testdata/generate.sh | 38 -- testdata/invalidpasswords | 1 - testdata/localhost.crt | 22 - testdata/localhost.enc.key | 30 -- testdata/localhost.key | 28 -- testdata/passwords | 2 - 23 files changed, 119 insertions(+), 1329 deletions(-) delete mode 100644 ssl.go delete mode 100644 ssl_disable.go delete mode 100644 ssl_enable.go delete mode 100644 ssl_test.go delete mode 100644 testdata/ca.crt delete mode 100644 testdata/empty delete mode 100755 testdata/generate.sh delete mode 100644 testdata/invalidpasswords delete mode 100644 testdata/localhost.crt delete mode 100644 testdata/localhost.enc.key delete mode 100644 testdata/localhost.key delete mode 100644 testdata/passwords diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index cba02c6ab..3cf0d2808 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -103,11 +103,6 @@ jobs: make test make testrace - - name: Run regression tests with disabled SSL - run: | - make test TAGS="go_tarantool_ssl_disable" - make testrace TAGS="go_tarantool_ssl_disable" - - name: Run fuzzing tests if: ${{ matrix.fuzzing }} run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" @@ -149,16 +144,13 @@ jobs: - 'sdk-1.10.15-0-r598' coveralls: [false] fuzzing: [false] - ssl: [false] include: - sdk-path: 'release/linux/x86_64/2.10/' sdk-version: 'sdk-gc64-2.10.8-0-r598.linux.x86_64' coveralls: false - ssl: true - sdk-path: 'release/linux/x86_64/2.11/' sdk-version: 'sdk-gc64-2.11.1-0-r598.linux.x86_64' coveralls: true - ssl: true steps: - name: Clone the connector @@ -195,14 +187,6 @@ jobs: source tarantool-enterprise/env.sh make test make testrace - env: - TEST_TNT_SSL: ${{matrix.ssl}} - - - name: Run regression tests with disabled SSL - run: | - source tarantool-enterprise/env.sh - make test TAGS="go_tarantool_ssl_disable" - make testrace TAGS="go_tarantool_ssl_disable" - name: Run fuzzing tests if: ${{ matrix.fuzzing }} @@ -212,7 +196,6 @@ jobs: if: ${{ matrix.coveralls }} env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TEST_TNT_SSL: ${{matrix.ssl}} run: | source tarantool-enterprise/env.sh make coveralls @@ -376,12 +359,6 @@ jobs: make test make testrace - - name: Run regression tests with disabled SSL - run: | - cd "${SRCDIR}" - make test TAGS="go_tarantool_ssl_disable" - make testrace TAGS="go_tarantool_ssl_disable" - - name: Run fuzzing tests if: ${{ matrix.fuzzing }} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c3b026ec..c71a63146 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,6 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - IsNullable flag for Field (#302) - More linters on CI (#310) - Meaningful description for read/write socket errors (#129) -- Support password and password file to decrypt private SSL key file (#319) - Support `operation_data` in `crud.Error` (#330) - Support `fetch_latest_metadata` option for crud requests with metadata (#335) - Support `noreturn` option for data change crud requests (#335) @@ -127,6 +126,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Code() method from the Request interface (#158) - `Schema` field from the `Connection` struct (#7) - `OkCode` and `PushCode` constants (#237) +- SSL support (#301) ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fdfcc67a2..f3ffe4d96 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,20 +34,6 @@ make testrace The tests set up all required `tarantool` processes before run and clean up afterwards. -If you want to run the tests with specific build tags: -```bash -make test TAGS=go_tarantool_ssl_disable -make testrace TAGS=go_tarantool_ssl_disable -``` - -If you have Tarantool Enterprise Edition 2.10 or newer, you can run additional -SSL tests. To do this, you need to set an environment variable 'TEST_TNT_SSL': - -```bash -TEST_TNT_SSL=true make test -TEST_TNT_SSL=true make testrace -``` - If you want to run the tests for a specific package: ```bash make test- diff --git a/README.md b/README.md index c025cfce8..85ca776a1 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,7 @@ We define multiple [build tags](https://pkg.go.dev/go/build#hdr-Build_Constraint This allows us to introduce new features without losing backward compatibility. -1. To disable SSL support and linking with OpenSSL, you can use the tag: - ``` - go_tarantool_ssl_disable - ``` -2. To run fuzz tests with decimals, you can use the build tag: +1. To run fuzz tests with decimals, you can use the build tag: ``` go_tarantool_decimal_fuzzing ``` @@ -169,6 +165,60 @@ otherwise it will have a description which can be retrieved with `err.Error()`. by the method `Do` of object `conn` which is the object that was returned by `Connect()`. +### Example with encrypting traffic + +For SSL-enabled connections, use `OpenSSLDialer` from the [`go-tlsdialer`](https://github.com/tarantool/go-tlsdialer) +package. + +Here is small example with importing `go-tlsdialer` and using the +`OpenSSLDialer`: + +```go +package tarantool + +import ( + "context" + "fmt" + "time" + + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tlsdialer" +) + +func main() { + sslDialer := tlsdialer.OpenSSLDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + SslKeyFile: "testdata/localhost.key", + SslCertFile: "testdata/localhost.crt", + SslCaFile: "testdata/ca.crt", + } + opts := tarantool.Opts{ + Timeout: time.Second, + } + + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + conn, err := tarantool.Connect(ctx, sslDialer, opts) + if err != nil { + fmt.Printf("Connection refused: %s", err) + } + + data, err := conn.Do(tarantool.NewInsertRequest(999). + Tuple([]interface{}{99999, "BB"}), + ).Get() + if err != nil { + fmt.Printf("Error: %s", err) + } else { + fmt.Printf("Data: %v", data) + } +} +``` + +Note that [traffic encryption](https://www.tarantool.io/en/doc/latest/enterprise/security/#encrypting-traffic) +is only available in Tarantool Enterprise Edition 2.10 or newer. + ### Migration to v2 The article describes migration from go-tarantool to go-tarantool/v2. @@ -315,8 +365,10 @@ and user may cancel it in process. Now you need to pass `Dialer` as the second argument instead of URI. If you were using a non-SSL connection, you need to create `NetDialer`. -For SSL-enabled connections, use `OpenSslDialer`. Please note that the options -for creating a connection are now stored in corresponding `Dialer`, not in `Opts`. +For SSL-enabled connections, use `OpenSSLDialer` from the `go-tlsdialer` +package. +Please note that the options for creating a connection are now stored in +corresponding `Dialer`, not in `Opts`. #### Connection schema diff --git a/dial.go b/dial.go index c1f319549..ac1919030 100644 --- a/dial.go +++ b/dial.go @@ -205,115 +205,6 @@ func (d NetDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { return dialer.Dial(ctx, opts) } -type openSslDialer struct { - address string - sslKeyFile string - sslCertFile string - sslCaFile string - sslCiphers string - sslPassword string - sslPasswordFile string -} - -func (d openSslDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { - var err error - conn := new(tntConn) - - network, address := parseAddress(d.address) - conn.net, err = sslDialContext(ctx, network, address, sslOpts{ - KeyFile: d.sslKeyFile, - CertFile: d.sslCertFile, - CaFile: d.sslCaFile, - Ciphers: d.sslCiphers, - Password: d.sslPassword, - PasswordFile: d.sslPasswordFile, - }) - if err != nil { - return nil, fmt.Errorf("failed to dial: %w", err) - } - - dc := &deadlineIO{to: opts.IoTimeout, c: conn.net} - conn.reader = bufio.NewReaderSize(dc, bufSize) - conn.writer = bufio.NewWriterSize(dc, bufSize) - - return conn, nil -} - -// OpenSslDialer allows to use SSL transport for connection. -type OpenSslDialer struct { - // Address is an address to connect. - // It could be specified in following ways: - // - // - TCP connections (tcp://192.168.1.1:3013, tcp://my.host:3013, - // tcp:192.168.1.1:3013, tcp:my.host:3013, 192.168.1.1:3013, my.host:3013) - // - // - Unix socket, first '/' or '.' indicates Unix socket - // (unix:///abs/path/tnt.sock, unix:path/tnt.sock, /abs/path/tnt.sock, - // ./rel/path/tnt.sock, unix/:path/tnt.sock) - Address string - // Auth is an authentication method. - Auth Auth - // Username for logging in to Tarantool. - User string - // User password for logging in to Tarantool. - Password string - // RequiredProtocol contains minimal protocol version and - // list of protocol features that should be supported by - // Tarantool server. By default, there are no restrictions. - RequiredProtocolInfo ProtocolInfo - // SslKeyFile is a path to a private SSL key file. - SslKeyFile string - // SslCertFile is a path to an SSL certificate file. - SslCertFile string - // SslCaFile is a path to a trusted certificate authorities (CA) file. - SslCaFile string - // SslCiphers is a colon-separated (:) list of SSL cipher suites the connection - // can use. - // - // We don't provide a list of supported ciphers. This is what OpenSSL - // does. The only limitation is usage of TLSv1.2 (because other protocol - // versions don't seem to support the GOST cipher). To add additional - // ciphers (GOST cipher), you must configure OpenSSL. - // - // See also - // - // * https://www.openssl.org/docs/man1.1.1/man1/ciphers.html - SslCiphers string - // SslPassword is a password for decrypting the private SSL key file. - // The priority is as follows: try to decrypt with SslPassword, then - // try SslPasswordFile. - SslPassword string - // SslPasswordFile is a path to the list of passwords for decrypting - // the private SSL key file. The connection tries every line from the - // file as a password. - SslPasswordFile string -} - -// Dial makes OpenSslDialer satisfy the Dialer interface. -func (d OpenSslDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { - dialer := AuthDialer{ - Dialer: ProtocolDialer{ - Dialer: GreetingDialer{ - Dialer: openSslDialer{ - address: d.Address, - sslKeyFile: d.SslKeyFile, - sslCertFile: d.SslCertFile, - sslCaFile: d.SslCaFile, - sslCiphers: d.SslCiphers, - sslPassword: d.SslPassword, - sslPasswordFile: d.SslPasswordFile, - }, - }, - RequiredProtocolInfo: d.RequiredProtocolInfo, - }, - Auth: d.Auth, - Username: d.User, - Password: d.Password, - } - - return dialer.Dial(ctx, opts) -} - type fdAddr struct { Fd uintptr } diff --git a/dial_test.go b/dial_test.go index b41a504df..abfdf0d4f 100644 --- a/dial_test.go +++ b/dial_test.go @@ -442,7 +442,6 @@ type testDialOpts struct { isErrGreeting bool isErrId bool isIdUnsupported bool - isPapSha256Auth bool isErrAuth bool isEmptyAuth bool } @@ -485,9 +484,7 @@ func testDialAccept(opts testDialOpts, l net.Listener) chan dialServerActual { // Read Auth request. authRequestExpected := authRequestExpectedChapSha1 - if opts.isPapSha256Auth { - authRequestExpected = authRequestExpectedPapSha256 - } else if opts.isEmptyAuth { + if opts.isEmptyAuth { authRequestExpected = []byte{} } authRequestActual := make([]byte, len(authRequestExpected)) @@ -530,9 +527,7 @@ func testDialer(t *testing.T, l net.Listener, dialer tarantool.Dialer, require.Equal(t, idRequestExpected, actual.IdRequest) authRequestExpected := authRequestExpectedChapSha1 - if opts.isPapSha256Auth { - authRequestExpected = authRequestExpectedPapSha256 - } else if opts.isEmptyAuth { + if opts.isEmptyAuth { authRequestExpected = []byte{} } require.Equal(t, authRequestExpected, actual.AuthRequest) @@ -749,11 +744,44 @@ func TestAuthDialer_Dial(t *testing.T) { conn.Close() } - assert.Nil(t, err) + assert.NoError(t, err) assert.NotNil(t, conn) assert.Equal(t, authRequestExpectedChapSha1[:41], dialer.conn.writebuf.Bytes()[:41]) } +func TestAuthDialer_Dial_PapSha256Auth(t *testing.T) { + salt := fmt.Sprintf("%s", testDialSalt) + salt = base64.StdEncoding.EncodeToString([]byte(salt)) + dialer := mockIoDialer{ + init: func(conn *mockIoConn) { + conn.greeting.Salt = salt + conn.writeWgDelay = 1 + conn.readWgDelay = 2 + conn.readbuf.Write(okResponse) + }, + } + defer func() { + dialer.conn.writeWg.Done() + }() + + authDialer := tarantool.AuthDialer{ + Dialer: &dialer, + Username: "test", + Password: "test", + Auth: tarantool.PapSha256Auth, + } + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := authDialer.Dial(ctx, tarantool.DialOpts{}) + if conn != nil { + conn.Close() + } + + assert.NoError(t, err) + assert.NotNil(t, conn) + assert.Equal(t, authRequestExpectedPapSha256[:41], dialer.conn.writebuf.Bytes()[:41]) +} + func TestProtocolDialer_Dial_DialerError(t *testing.T) { dialer := tarantool.ProtocolDialer{ Dialer: mockErrorDialer{ @@ -847,7 +875,7 @@ func TestProtocolDialer_Dial(t *testing.T) { conn.Close() } - assert.Nil(t, err) + assert.NoError(t, err) assert.NotNil(t, conn) assert.Equal(t, protoInfo, conn.ProtocolInfo()) } @@ -913,7 +941,7 @@ func TestGreetingDialer_Dial(t *testing.T) { conn.Close() } - assert.Nil(t, err) + assert.NoError(t, err) assert.NotNil(t, conn) assert.Equal(t, string(testDialVersion[:]), conn.Greeting().Version) assert.Equal(t, string(testDialSalt[:44]), conn.Greeting().Salt) diff --git a/example_test.go b/example_test.go index 965c25a82..ec07b657b 100644 --- a/example_test.go +++ b/example_test.go @@ -31,24 +31,6 @@ func exampleConnect(dialer tarantool.Dialer, opts tarantool.Opts) *tarantool.Con return conn } -// Example demonstrates how to use SSL transport. -func ExampleOpenSslDialer() { - sslDialer := tarantool.OpenSslDialer{ - Address: "127.0.0.1:3013", - User: "test", - Password: "test", - SslKeyFile: "testdata/localhost.key", - SslCertFile: "testdata/localhost.crt", - SslCaFile: "testdata/ca.crt", - } - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - _, err := tarantool.Connect(ctx, sslDialer, opts) - if err != nil { - panic("Connection is not established: " + err.Error()) - } -} - func ExampleIntKey() { conn := exampleConnect(dialer, opts) defer conn.Close() diff --git a/export_test.go b/export_test.go index e92ce9bd9..ab3784b3b 100644 --- a/export_test.go +++ b/export_test.go @@ -6,19 +6,6 @@ import ( "github.com/vmihailenco/msgpack/v5" ) -type SslTestOpts struct { - KeyFile string - CertFile string - CaFile string - Ciphers string - Password string - PasswordFile string -} - -func SslCreateContext(opts SslTestOpts) (ctx interface{}, err error) { - return sslCreateContext(sslOpts(opts)) -} - // RefImplPingBody is reference implementation for filling of a ping // request's body. func RefImplPingBody(enc *msgpack.Encoder) error { diff --git a/go.mod b/go.mod index f1a6af3b0..4f97af1a6 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,8 @@ go 1.11 require ( github.com/google/uuid v1.3.0 - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.7.1 github.com/tarantool/go-iproto v1.0.0 - github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca github.com/vmihailenco/msgpack/v5 v5.3.5 - golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect ) diff --git a/go.sum b/go.sum index 752c1029c..ff942aa8f 100644 --- a/go.sum +++ b/go.sum @@ -2,36 +2,21 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= -github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= -github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tarantool/go-iproto v1.0.0 h1:quC4hdFhCuFYaCqOFgUxH2foRkhAy+TlEy7gQLhdVjw= github.com/tarantool/go-iproto v1.0.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= -github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca h1:oOrBh73tDDyooIXajfr+0pfnM+89404ClAhJpTTHI7E= -github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ssl.go b/ssl.go deleted file mode 100644 index 39cc57bd0..000000000 --- a/ssl.go +++ /dev/null @@ -1,30 +0,0 @@ -package tarantool - -type sslOpts struct { - // KeyFile is a path to a private SSL key file. - KeyFile string - // CertFile is a path to an SSL certificate file. - CertFile string - // CaFile is a path to a trusted certificate authorities (CA) file. - CaFile string - // Ciphers is a colon-separated (:) list of SSL cipher suites the connection - // can use. - // - // We don't provide a list of supported ciphers. This is what OpenSSL - // does. The only limitation is usage of TLSv1.2 (because other protocol - // versions don't seem to support the GOST cipher). To add additional - // ciphers (GOST cipher), you must configure OpenSSL. - // - // See also - // - // * https://www.openssl.org/docs/man1.1.1/man1/ciphers.html - Ciphers string - // Password is a password for decrypting the private SSL key file. - // The priority is as follows: try to decrypt with Password, then - // try PasswordFile. - Password string - // PasswordFile is a path to the list of passwords for decrypting - // the private SSL key file. The connection tries every line from the - // file as a password. - PasswordFile string -} diff --git a/ssl_disable.go b/ssl_disable.go deleted file mode 100644 index 21550b61a..000000000 --- a/ssl_disable.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build go_tarantool_ssl_disable -// +build go_tarantool_ssl_disable - -package tarantool - -import ( - "context" - "errors" - "net" -) - -func sslDialContext(ctx context.Context, network, address string, - opts sslOpts) (connection net.Conn, err error) { - return nil, errors.New("SSL support is disabled.") -} - -func sslCreateContext(opts sslOpts) (ctx interface{}, err error) { - return nil, errors.New("SSL support is disabled.") -} diff --git a/ssl_enable.go b/ssl_enable.go deleted file mode 100644 index a56dac1cc..000000000 --- a/ssl_enable.go +++ /dev/null @@ -1,144 +0,0 @@ -//go:build !go_tarantool_ssl_disable -// +build !go_tarantool_ssl_disable - -package tarantool - -import ( - "bufio" - "context" - "errors" - "io/ioutil" - "net" - "os" - "strings" - - "github.com/tarantool/go-openssl" -) - -func sslDialContext(ctx context.Context, network, address string, - opts sslOpts) (connection net.Conn, err error) { - var sslCtx interface{} - if sslCtx, err = sslCreateContext(opts); err != nil { - return - } - - return openssl.DialContext(ctx, network, address, sslCtx.(*openssl.Ctx), 0) -} - -// interface{} is a hack. It helps to avoid dependency of go-openssl in build -// of tests with the tag 'go_tarantool_ssl_disable'. -func sslCreateContext(opts sslOpts) (ctx interface{}, err error) { - var sslCtx *openssl.Ctx - - // Require TLSv1.2, because other protocol versions don't seem to - // support the GOST cipher. - if sslCtx, err = openssl.NewCtxWithVersion(openssl.TLSv1_2); err != nil { - return - } - ctx = sslCtx - sslCtx.SetMaxProtoVersion(openssl.TLS1_2_VERSION) - sslCtx.SetMinProtoVersion(openssl.TLS1_2_VERSION) - - if opts.CertFile != "" { - if err = sslLoadCert(sslCtx, opts.CertFile); err != nil { - return - } - } - - if opts.KeyFile != "" { - if err = sslLoadKey(sslCtx, opts.KeyFile, opts.Password, opts.PasswordFile); err != nil { - return - } - } - - if opts.CaFile != "" { - if err = sslCtx.LoadVerifyLocations(opts.CaFile, ""); err != nil { - return - } - verifyFlags := openssl.VerifyPeer | openssl.VerifyFailIfNoPeerCert - sslCtx.SetVerify(verifyFlags, nil) - } - - if opts.Ciphers != "" { - sslCtx.SetCipherList(opts.Ciphers) - } - - return -} - -func sslLoadCert(ctx *openssl.Ctx, certFile string) (err error) { - var certBytes []byte - if certBytes, err = ioutil.ReadFile(certFile); err != nil { - return - } - - certs := openssl.SplitPEM(certBytes) - if len(certs) == 0 { - err = errors.New("No PEM certificate found in " + certFile) - return - } - first, certs := certs[0], certs[1:] - - var cert *openssl.Certificate - if cert, err = openssl.LoadCertificateFromPEM(first); err != nil { - return - } - if err = ctx.UseCertificate(cert); err != nil { - return - } - - for _, pem := range certs { - if cert, err = openssl.LoadCertificateFromPEM(pem); err != nil { - break - } - if err = ctx.AddChainCertificate(cert); err != nil { - break - } - } - return -} - -func sslLoadKey(ctx *openssl.Ctx, keyFile string, password string, - passwordFile string) error { - var keyBytes []byte - var err, firstDecryptErr error - - if keyBytes, err = ioutil.ReadFile(keyFile); err != nil { - return err - } - - // If the key is encrypted and password is not provided, - // openssl.LoadPrivateKeyFromPEM(keyBytes) asks to enter PEM pass phrase - // interactively. On the other hand, - // openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) works fine - // for non-encrypted key with any password, including empty string. If - // the key is encrypted, we fast fail with password error instead of - // requesting the pass phrase interactively. - passwords := []string{password} - if passwordFile != "" { - file, err := os.Open(passwordFile) - if err == nil { - defer file.Close() - - scanner := bufio.NewScanner(file) - // Tarantool itself tries each password file line. - for scanner.Scan() { - password = strings.TrimSpace(scanner.Text()) - passwords = append(passwords, password) - } - } else { - firstDecryptErr = err - } - } - - for _, password := range passwords { - key, err := openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) - if err == nil { - return ctx.UsePrivateKey(key) - } else if firstDecryptErr == nil { - firstDecryptErr = err - } - } - - return firstDecryptErr -} diff --git a/ssl_test.go b/ssl_test.go deleted file mode 100644 index f161b98f0..000000000 --- a/ssl_test.go +++ /dev/null @@ -1,780 +0,0 @@ -//go:build !go_tarantool_ssl_disable -// +build !go_tarantool_ssl_disable - -package tarantool_test - -import ( - "context" - "fmt" - "net" - "os" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" - "github.com/tarantool/go-iproto" - "github.com/tarantool/go-openssl" - - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" -) - -const tntHost = "127.0.0.1:3014" - -func serverTnt(serverOpts SslTestOpts, auth Auth) (test_helpers.TarantoolInstance, error) { - listen := tntHost + "?transport=ssl&" - - key := serverOpts.KeyFile - if key != "" { - listen += fmt.Sprintf("ssl_key_file=%s&", key) - } - - cert := serverOpts.CertFile - if cert != "" { - listen += fmt.Sprintf("ssl_cert_file=%s&", cert) - } - - ca := serverOpts.CaFile - if ca != "" { - listen += fmt.Sprintf("ssl_ca_file=%s&", ca) - } - - ciphers := serverOpts.Ciphers - if ciphers != "" { - listen += fmt.Sprintf("ssl_ciphers=%s&", ciphers) - } - - password := serverOpts.Password - if password != "" { - listen += fmt.Sprintf("ssl_password=%s&", password) - } - - passwordFile := serverOpts.PasswordFile - if passwordFile != "" { - listen += fmt.Sprintf("ssl_password_file=%s&", passwordFile) - } - - listen = listen[:len(listen)-1] - - return test_helpers.StartTarantool( - test_helpers.StartOpts{ - Dialer: OpenSslDialer{ - Address: tntHost, - Auth: auth, - User: "test", - Password: "test", - SslKeyFile: serverOpts.KeyFile, - SslCertFile: serverOpts.CertFile, - SslCaFile: serverOpts.CaFile, - SslCiphers: serverOpts.Ciphers, - SslPassword: serverOpts.Password, - SslPasswordFile: serverOpts.PasswordFile, - }, - Auth: auth, - InitScript: "config.lua", - Listen: listen, - SslCertsDir: "testdata", - WaitStart: 100 * time.Millisecond, - ConnectRetry: 10, - RetryTimeout: 500 * time.Millisecond, - }, - ) -} - -func serverTntStop(inst test_helpers.TarantoolInstance) { - test_helpers.StopTarantoolWithCleanup(inst) -} - -func checkTntConn(dialer Dialer) error { - ctx, cancel := test_helpers.GetConnectContext() - defer cancel() - conn, err := Connect(ctx, dialer, Opts{ - Timeout: 500 * time.Millisecond, - SkipSchema: true, - }) - if err != nil { - return err - } - conn.Close() - return nil -} - -func assertConnectionTntFail(t testing.TB, serverOpts SslTestOpts, dialer OpenSslDialer) { - t.Helper() - - inst, err := serverTnt(serverOpts, AutoAuth) - defer serverTntStop(inst) - if err != nil { - t.Fatalf("An unexpected server error %q", err.Error()) - } - - err = checkTntConn(dialer) - if err == nil { - t.Errorf("An unexpected connection to the server") - } -} - -func assertConnectionTntOk(t testing.TB, serverOpts SslTestOpts, dialer OpenSslDialer) { - t.Helper() - - inst, err := serverTnt(serverOpts, AutoAuth) - defer serverTntStop(inst) - if err != nil { - t.Fatalf("An unexpected server error %q", err.Error()) - } - - err = checkTntConn(dialer) - if err != nil { - t.Errorf("An unexpected connection error %q", err.Error()) - } -} - -type sslTest struct { - name string - ok bool - serverOpts SslTestOpts - clientOpts SslTestOpts -} - -/* -Requirements from Tarantool Enterprise Edition manual: -https://www.tarantool.io/ru/enterprise_doc/security/#configuration - -For a server: -KeyFile - mandatory -CertFile - mandatory -CaFile - optional -Ciphers - optional - -For a client: -KeyFile - optional, mandatory if server.CaFile set -CertFile - optional, mandatory if server.CaFile set -CaFile - optional, -Ciphers - optional -*/ -var sslTests = []sslTest{ - { - "key_crt_server", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - }, - SslTestOpts{}, - }, - { - "key_crt_server_and_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - }, - }, - { - "key_crt_ca_server", - false, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{}, - }, - { - "key_crt_ca_server_key_crt_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - }, - }, - { - "key_crt_ca_server_and_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - }, - { - "key_crt_ca_server_and_client_invalid_path_key", - false, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "any_invalid_path", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - }, - { - "key_crt_ca_server_and_client_invalid_path_crt", - false, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "any_invalid_path", - CaFile: "testdata/ca.crt", - }, - }, - { - "key_crt_ca_server_and_client_invalid_path_ca", - false, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "any_invalid_path", - }, - }, - { - "key_crt_ca_server_and_client_empty_key", - false, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/empty", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - }, - { - "key_crt_ca_server_and_client_empty_crt", - false, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/empty", - CaFile: "testdata/ca.crt", - }, - }, - { - "key_crt_ca_server_and_client_empty_ca", - false, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/empty", - }, - }, - { - "key_crt_server_and_key_crt_ca_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - }, - { - "key_crt_ca_ciphers_server_key_crt_ca_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - }, - { - "key_crt_ca_ciphers_server_and_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", - }, - }, - { - "non_equal_ciphers_client", - false, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - Ciphers: "TLS_AES_128_GCM_SHA256", - }, - }, - { - "pass_key_encrypt_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.enc.key", - CertFile: "testdata/localhost.crt", - Password: "mysslpassword", - }, - }, - { - "passfile_key_encrypt_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.enc.key", - CertFile: "testdata/localhost.crt", - PasswordFile: "testdata/passwords", - }, - }, - { - "pass_and_passfile_key_encrypt_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.enc.key", - CertFile: "testdata/localhost.crt", - Password: "mysslpassword", - PasswordFile: "testdata/passwords", - }, - }, - { - "inv_pass_and_passfile_key_encrypt_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.enc.key", - CertFile: "testdata/localhost.crt", - Password: "invalidpassword", - PasswordFile: "testdata/passwords", - }, - }, - { - "pass_and_inv_passfile_key_encrypt_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.enc.key", - CertFile: "testdata/localhost.crt", - Password: "mysslpassword", - PasswordFile: "testdata/invalidpasswords", - }, - }, - { - "pass_and_not_existing_passfile_key_encrypt_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.enc.key", - CertFile: "testdata/localhost.crt", - Password: "mysslpassword", - PasswordFile: "testdata/notafile", - }, - }, - { - "inv_pass_and_inv_passfile_key_encrypt_client", - false, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.enc.key", - CertFile: "testdata/localhost.crt", - Password: "invalidpassword", - PasswordFile: "testdata/invalidpasswords", - }, - }, - { - "not_existing_passfile_key_encrypt_client", - false, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.enc.key", - CertFile: "testdata/localhost.crt", - PasswordFile: "testdata/notafile", - }, - }, - { - "no_pass_key_encrypt_client", - false, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.enc.key", - CertFile: "testdata/localhost.crt", - }, - }, - { - "pass_key_non_encrypt_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - Password: "invalidpassword", - }, - }, - { - "passfile_key_non_encrypt_client", - true, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - }, - SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - PasswordFile: "testdata/invalidpasswords", - }, - }, -} - -func isTestTntSsl() bool { - testTntSsl, exists := os.LookupEnv("TEST_TNT_SSL") - return exists && - (testTntSsl == "1" || strings.ToUpper(testTntSsl) == "TRUE") -} - -func makeOpenSslDialer(opts SslTestOpts) OpenSslDialer { - return OpenSslDialer{ - Address: tntHost, - User: "test", - Password: "test", - SslKeyFile: opts.KeyFile, - SslCertFile: opts.CertFile, - SslCaFile: opts.CaFile, - SslCiphers: opts.Ciphers, - SslPassword: opts.Password, - SslPasswordFile: opts.PasswordFile, - } -} - -func TestSslOpts(t *testing.T) { - isTntSsl := isTestTntSsl() - - for _, test := range sslTests { - if !isTntSsl { - continue - } - dialer := makeOpenSslDialer(test.clientOpts) - if test.ok { - t.Run("ok_tnt_"+test.name, func(t *testing.T) { - assertConnectionTntOk(t, test.serverOpts, dialer) - }) - } else { - t.Run("fail_tnt_"+test.name, func(t *testing.T) { - assertConnectionTntFail(t, test.serverOpts, dialer) - }) - } - } -} - -func TestOpts_PapSha256Auth(t *testing.T) { - isTntSsl := isTestTntSsl() - if !isTntSsl { - t.Skip("TEST_TNT_SSL is not set") - } - - isLess, err := test_helpers.IsTarantoolVersionLess(2, 11, 0) - if err != nil { - t.Fatalf("Could not check Tarantool version: %s", err) - } - if isLess { - t.Skip("Skipping test for Tarantool without pap-sha256 support") - } - - sslOpts := SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - } - - inst, err := serverTnt(sslOpts, PapSha256Auth) - defer serverTntStop(inst) - if err != nil { - t.Fatalf("An unexpected server error %q", err.Error()) - } - - client := OpenSslDialer{ - Address: tntHost, - Auth: PapSha256Auth, - User: "test", - Password: "test", - RequiredProtocolInfo: ProtocolInfo{}, - SslKeyFile: sslOpts.KeyFile, - SslCertFile: sslOpts.CertFile, - } - - conn := test_helpers.ConnectWithValidation(t, client, opts) - conn.Close() - - client.Auth = AutoAuth - conn = test_helpers.ConnectWithValidation(t, client, opts) - conn.Close() -} - -func createSslListener(t *testing.T, opts SslTestOpts) net.Listener { - ctx, err := SslCreateContext(opts) - require.NoError(t, err) - l, err := openssl.Listen("tcp", "127.0.0.1:0", ctx.(*openssl.Ctx)) - require.NoError(t, err) - return l -} - -func TestOpenSslDialer_Dial_opts(t *testing.T) { - for _, test := range sslTests { - t.Run(test.name, func(t *testing.T) { - l := createSslListener(t, test.serverOpts) - defer l.Close() - addr := l.Addr().String() - - dialer := OpenSslDialer{ - Address: addr, - User: testDialUser, - Password: testDialPass, - SslKeyFile: test.clientOpts.KeyFile, - SslCertFile: test.clientOpts.CertFile, - SslCaFile: test.clientOpts.CaFile, - SslCiphers: test.clientOpts.Ciphers, - SslPassword: test.clientOpts.Password, - SslPasswordFile: test.clientOpts.PasswordFile, - } - testDialer(t, l, dialer, testDialOpts{ - wantErr: !test.ok, - expectedProtocolInfo: idResponseTyped.Clone(), - }) - }) - } -} - -func TestOpenSslDialer_Dial_basic(t *testing.T) { - l := createSslListener(t, SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - }) - - defer l.Close() - addr := l.Addr().String() - - dialer := OpenSslDialer{ - Address: addr, - User: testDialUser, - Password: testDialPass, - } - - cases := []testDialOpts{ - { - name: "all is ok", - expectedProtocolInfo: idResponseTyped.Clone(), - }, - { - name: "id request unsupported", - expectedProtocolInfo: ProtocolInfo{}, - isIdUnsupported: true, - }, - { - name: "greeting response error", - wantErr: true, - expectedErr: "failed to read greeting", - isErrGreeting: true, - }, - { - name: "id response error", - wantErr: true, - expectedErr: "failed to identify", - isErrId: true, - }, - { - name: "auth response error", - wantErr: true, - expectedErr: "failed to authenticate", - isErrAuth: true, - }, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - testDialer(t, l, dialer, tc) - }) - } -} - -func TestOpenSslDialer_Dial_requirements(t *testing.T) { - l := createSslListener(t, SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - }) - - defer l.Close() - addr := l.Addr().String() - - dialer := OpenSslDialer{ - Address: addr, - User: testDialUser, - Password: testDialPass, - RequiredProtocolInfo: ProtocolInfo{ - Features: []iproto.Feature{42}, - }, - } - - testDialAccept(testDialOpts{}, l) - ctx, cancel := test_helpers.GetConnectContext() - defer cancel() - conn, err := dialer.Dial(ctx, DialOpts{}) - if err == nil { - conn.Close() - } - require.Error(t, err) - require.Contains(t, err.Error(), "invalid server protocol") -} - -func TestOpenSslDialer_Dial_papSha256Auth(t *testing.T) { - l := createSslListener(t, SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - }) - - defer l.Close() - addr := l.Addr().String() - - dialer := OpenSslDialer{ - Address: addr, - User: testDialUser, - Password: testDialPass, - Auth: PapSha256Auth, - } - - // Response from the server. - protocol := idResponseTyped.Clone() - protocol.Auth = ChapSha1Auth - - testDialer(t, l, dialer, testDialOpts{ - expectedProtocolInfo: protocol, - isPapSha256Auth: true, - }) -} - -func TestOpenSslDialer_Dial_ctx_cancel(t *testing.T) { - serverOpts := SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", - } - clientOpts := SslTestOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - CaFile: "testdata/ca.crt", - Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", - } - - l := createSslListener(t, serverOpts) - defer l.Close() - addr := l.Addr().String() - testDialAccept(testDialOpts{}, l) - - dialer := OpenSslDialer{ - Address: addr, - User: testDialUser, - Password: testDialPass, - SslKeyFile: clientOpts.KeyFile, - SslCertFile: clientOpts.CertFile, - SslCaFile: clientOpts.CaFile, - SslCiphers: clientOpts.Ciphers, - SslPassword: clientOpts.Password, - SslPasswordFile: clientOpts.PasswordFile, - } - - ctx, cancel := context.WithCancel(context.Background()) - cancel() - conn, err := dialer.Dial(ctx, DialOpts{}) - if err == nil { - conn.Close() - } - require.Error(t, err) -} diff --git a/tarantool_test.go b/tarantool_test.go index c4db04cb3..0bd3f394b 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -790,6 +790,26 @@ func TestNetDialer_BadUser(t *testing.T) { } } +// NetDialer does not work with PapSha256Auth, no matter the Tarantool version +// and edition. +func TestNetDialer_PapSha256Auth(t *testing.T) { + authDialer := AuthDialer{ + Dialer: dialer, + Username: "test", + Password: "test", + Auth: PapSha256Auth, + } + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := authDialer.Dial(ctx, DialOpts{}) + if conn != nil { + conn.Close() + t.Fatalf("Connection created successfully") + } + + assert.ErrorContains(t, err, "failed to authenticate") +} + func TestFutureMultipleGetGetTyped(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() diff --git a/testdata/ca.crt b/testdata/ca.crt deleted file mode 100644 index 2fa1a12ff..000000000 --- a/testdata/ca.crt +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDLzCCAhegAwIBAgIUMMZTmNkhr4qOfSwInVk2dAJvoBEwDQYJKoZIhvcNAQEL -BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y -MjA1MjYwNjE3NDBaFw00NDEwMjkwNjE3NDBaMCcxCzAJBgNVBAYTAlVTMRgwFgYD -VQQDDA9FeGFtcGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQCRq/eaA3I6CB8t770H2XDdzcp1yuC/+TZOxV5o0LuRkogTvL2kYULBrfx1 -rVZu8zQJTx1fmSRj1cN8j+IrmXN5goZ3mYFTnnIOgkyi+hJysVlo5s0Kp0qtLLGM -OuaVbxw2oAy75if5X3pFpiDaMvFBtJKsh8+SkncBIC5bbKC5AoLdFANLmPiH0CGr -Mv3rL3ycnbciI6J4uKHcWnYGGiMjBomaZ7jd/cOjcjmGfpI5d0nq13G11omkyEyR -wNX0eJRL02W+93Xu7tD+FEFMxFvak+70GvX+XWomwYw/Pjlio8KbTAlJxhfK2Lh6 -H798k17VfxIrOk0KjzZS7+a20hZ/AgMBAAGjUzBRMB0GA1UdDgQWBBT2f5o8r75C -PWST36akpkKRRTbhvjAfBgNVHSMEGDAWgBT2f5o8r75CPWST36akpkKRRTbhvjAP -BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA9pb75p6mnqp2MQHSr -5SKRf2UV4wQIUtXgF6V9vNfvVzJii+Lzrqir1YMk5QgavCzD96KlJcqJCcH559RY -5743AxI3tdWfA3wajBctoy35oYnT4M30qbkryYLTUlv7PmeNWvrksTURchyyDt5/ -3T73yj5ZalmzKN6+xLfUDdnudspfWlUMutKU50MU1iuQESf4Fwd53vOg9jMcWJ2E -vAgfVI0XAvYdU3ybJrUvBq5zokYR2RzGv14uHxwVPnLBjrBEHRnbrXvLZJhuIS2b -xZ3CqwWi+9bvNqHz09HvhkU2b6fCGweKaAUGSo8OfQ5FRkjTUomMI/ZLs/qtJ6JR -zzVt ------END CERTIFICATE----- diff --git a/testdata/empty b/testdata/empty deleted file mode 100644 index e69de29bb..000000000 diff --git a/testdata/generate.sh b/testdata/generate.sh deleted file mode 100755 index 4b8cf3630..000000000 --- a/testdata/generate.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -set -xeuo pipefail -# An example how-to re-generate testing certificates (because usually -# TLS certificates have expiration dates and some day they will expire). -# -# The instruction is valid for: -# -# $ openssl version -# OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022) - -cat < domains.ext -authorityKeyIdentifier=keyid,issuer -basicConstraints=CA:FALSE -keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment -subjectAltName = @alt_names -[alt_names] -DNS.1 = localhost -IP.1 = 127.0.0.1 -EOF - -openssl req -x509 -nodes -new -sha256 -days 8192 -newkey rsa:2048 -keyout ca.key -out ca.pem -subj "/C=US/CN=Example-Root-CA" -openssl x509 -outform pem -in ca.pem -out ca.crt - -openssl req -new -nodes -newkey rsa:2048 -keyout localhost.key -out localhost.csr -subj "/C=US/ST=YourState/L=YourCity/O=Example-Certificates/CN=localhost" -openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains.ext -out localhost.crt -password=mysslpassword - -# Tarantool tries every line from the password file. -cat < passwords -unusedpassword -$password -EOF - -cat < invalidpasswords -unusedpassword1 -EOF - -openssl rsa -aes256 -passout "pass:${password}" -in localhost.key -out localhost.enc.key diff --git a/testdata/invalidpasswords b/testdata/invalidpasswords deleted file mode 100644 index b09d795aa..000000000 --- a/testdata/invalidpasswords +++ /dev/null @@ -1 +0,0 @@ -unusedpassword1 diff --git a/testdata/localhost.crt b/testdata/localhost.crt deleted file mode 100644 index fd04b9900..000000000 --- a/testdata/localhost.crt +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDkjCCAnqgAwIBAgIUAvSBJ3nSv7kdKw1IQ7AjchzI7T8wDQYJKoZIhvcNAQEL -BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y -MjA1MjYwNjE3NDBaFw00NDEwMjkwNjE3NDBaMGcxCzAJBgNVBAYTAlVTMRIwEAYD -VQQIDAlZb3VyU3RhdGUxETAPBgNVBAcMCFlvdXJDaXR5MR0wGwYDVQQKDBRFeGFt -cGxlLUNlcnRpZmljYXRlczESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnbFY+BMqlddktbitgaZICws4Zyj8LFy9QzO+ -AYSQyqFuTCI+cGqbP5r6Qf4f3xHNGykHJGn18brpiFWhNMaVkkgU3dycU8fFayVN -hLEJAXd4acWP1h5/aH4cOZgl+xJlmU2iLHtP/TLYEDDiVkfqL/MgUIMxbndIaiU0 -/e81v+2gi8ydyI6aElN8KbAaFPzXCZ28/RmO/0m36YzF+FSMVD1Hx8xO5V+Q9N1q -dsyrMdh0nCxDDXGdBgDrKt5+U1uJkDpTHfjMAkf7oBoRd8DJ8O74bpue03W5WxKQ -NjNfvHSgkBaQSdnxR93FSCr/Gs6WcUd50Y8z+ZCTNkup0KROTwIDAQABo3YwdDAf -BgNVHSMEGDAWgBT2f5o8r75CPWST36akpkKRRTbhvjAJBgNVHRMEAjAAMAsGA1Ud -DwQEAwIE8DAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwHQYDVR0OBBYEFOwH -aHK6QrEfltP7wwldUWrQJ9viMA0GCSqGSIb3DQEBCwUAA4IBAQAGHGuloGJqLoPZ -2iRnb/NaiArowLnUz4Z3ENKMB2KbZFGijMJSXO9i9ZLCFL+O93vCTspKGtHqVX2o -dxcrF7EZ9EaHIijWjKGEp1PszunBIca+Te+zyRg9Z+F9gwRsJYB8ctBGjIhe4qEv -ZSlRY489UVNKLTcHcl6nlUled9hciBJHpXuitiiNhzUareP38hROyiUhrAy8L21L -t7Ww5YGRuSTxM5LQfPZcCy40++TlyvXs0DCQ8ZuUbqZv64bNHbaLOyxIqKfPypXa -nS3AYZzUJjHj7vZwHoL1SyvBjx/DQAsWaEv137d8FlMqCsWLXfCsuNpKeQYZOyDS -7ploP9Gl ------END CERTIFICATE----- diff --git a/testdata/localhost.enc.key b/testdata/localhost.enc.key deleted file mode 100644 index b881820a3..000000000 --- a/testdata/localhost.enc.key +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN ENCRYPTED PRIVATE KEY----- -MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIm+0WC9xe38cCAggA -MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBNOE4KD+yauMfsnOiNAaaZBIIE -0DtXaHGpacJ8MjjL6zciYhgJOD9SJHE4vwPxpNDWuS9mf6wk/cdBNFMqnYwJmlYw -J/eQ+Z8MsZUqjnhDQz9YXXd8JftexAAa1bHnmfv2N/czJCx57dAHVdmJzgibfp18 -GCpqR23tklEO2Nj2HCbR59rh7IsnW9mD6jh+mVtkOix5HMCUSxwc3bEUutIQE80P -JHG2BsEfAeeHZa+QgG3Y15c6uSXD6wY73ldPPOgZ3NFOqcw/RDqYf1zsohx7auxi -Y6zHA7LdYtQjbNJ5slIfxPhAh75Fws0g4QvWbAwqqdEOVmlamYYjAOdVBBxTvcRs -/63ZN55VTQ8rYhShNA3BVFOLHaRD4mnlKE5Xh7gJXltCED7EHdpHdT9K3uM9U7nW -b2JSylt2RzY+LDsio2U0xsQp9jHzRRw81p8P1jmo5alP8jPACMsE8nnNNSDF4p43 -fG7hNNBq/dhq80iOnaArY05TIBMsD079tB0VKrYyyfaL0RbsAdgtCEmF9bCpnsTM -y9ExcJGQQJx9WNAHkSyjdzJd0jR6Zc0MrgRuj26nJ3Ahq58zaQKdfFO9RfGWd38n -MH3jshEtAuF+jXFbMcM4rVdIBPSuhYgHzYIC6yteziy7+6hittpWeNGLKpC5oZ8R -oEwH3MVsjCbd6Pp3vdcR412vLMgy1ZUOraDoY08FXC82RBJViVX6LLltIJu96kiX -WWUcRZAwzlJsTvh1EGmDcNNKCgmvWQaojqTNgTjxjJ3SzD2/TV6uQrSLgZ6ulyNl -7vKWt/YMTvIgoJA9JeH8Aik/XNd4bRXL+VXfUHpLTgn+WKiq2irVYd9R/yITDunP -a/kzqxitjU4OGdf/LOtYxfxfoGvFw5ym4KikoHKVg4ILcIQ+W4roOQQlu4/yezAK -fwYCrMVJWq4ESuQh3rn7eFR+eyBV6YcNBLm4iUcQTMhnXMMYxQ3TnDNga5eYhmV1 -ByYx+nFQDrbDolXo5JfXs3x6kXhoT/7wMHgsXtmRSd5PSBbaeJTrbMGA0Op6YgWr -EpvX3Yt863s4h+JgDpg9ouH+OJGgn7LGGye+TjjuDds8CStFdcFDDOayBS3EH4Cr -jgJwzvTdTZl+1YLYJXB67M4zmVPRRs5H88+fZYYA9bhZACL/rQBj2wDq/sIxvrIM -SCjOhSJ4z5Sm3XaBKnRG2GBBt67MeHB0+T3HR3VHKR+zStbCnsbOLythsE/CIA8L -fBNXMvnWa5bLgaCaEcK6Q3LOamJiKaigbmhI+3U3NUdb9cT1GhE0rtx6/IO9eapz -IUDOrtX9U+1o6iW2dahezxwLo9ftRwQ7qwG4qOk/Co/1c2WuuQ+d4YPpj/JOO5mf -LanA35mQjQrr2MZII91psznx05ffb5xMp2pqNbC6DVuZq8ZlhvVHGk+wM9RK3kYP -/ITwpbUvLmmN892kvZgLAXadSupBV8R/L5ZjDUO9U2all9p4eGfWZBk/yiivOLmh -VQxKCqAmThTO1hRa56+AjgzRJO6cY85ra+4Mm3FhhdR4gYvap2QTq0o2Vn0WlCHh -1SIeaDKfw9v4aGBbhqyQU2mPlXO5JiLktO+lZ5styVq9Qm+b0ROZxHzL1lRUNbRA -VfQO4fRnINKPgyzgH3tNxJTzw4pLkrkBD/g+zxDZVqkx ------END ENCRYPTED PRIVATE KEY----- diff --git a/testdata/localhost.key b/testdata/localhost.key deleted file mode 100644 index ed0f55876..000000000 --- a/testdata/localhost.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCdsVj4EyqV12S1 -uK2BpkgLCzhnKPwsXL1DM74BhJDKoW5MIj5waps/mvpB/h/fEc0bKQckafXxuumI -VaE0xpWSSBTd3JxTx8VrJU2EsQkBd3hpxY/WHn9ofhw5mCX7EmWZTaIse0/9MtgQ -MOJWR+ov8yBQgzFud0hqJTT97zW/7aCLzJ3IjpoSU3wpsBoU/NcJnbz9GY7/Sbfp -jMX4VIxUPUfHzE7lX5D03Wp2zKsx2HScLEMNcZ0GAOsq3n5TW4mQOlMd+MwCR/ug -GhF3wMnw7vhum57TdblbEpA2M1+8dKCQFpBJ2fFH3cVIKv8azpZxR3nRjzP5kJM2 -S6nQpE5PAgMBAAECggEAFv81l9wHsll6pOu9VfJ/gCjPPXAjMn8F1OaXV5ZTHVHk -iXLXA0LwyBpcU8JxOHFapZLaqUtQpEObahf+zfkF+BLOBDr3i1pPZpxGjUraIt4e -7+HxY4sIDp+Rky6mn1JkAbLqKy2CkUzYaKgQYf/T3dFJjaRMUa1QoLYzX7MCdi5B -GnBICzi2UVsn3HU934l/gJKV+SlprdbrGJ+fRklP2AxLey3EOrwooUViy+k3+w5E -dzBH2HpLL0XuIHaBXQ01J6Mu3ud9ApFLC+Rh+2UFTW/WPnNe+B6BO5CGNN52Pfdr -Q5l+VzmRkXXo2fio+w4z/az8axT/DdhKGT2oBlp35QKBgQDZVGdKjkalH3QH2pdy -CWJIiybzY1R0CpimfgDLIdqEsps9qqgLXsUFB5yTcCRmg8RSWWHvhMVMyJtBcsdY -xGhmHxsFBxuray60UljxBcRQTwqvAX7mP8WEv8t80kbhyaxvOfkg8JD1n2hS7NjL -dOIG9Mh8L0YSOCRkbfv90OnYXQKBgQC5wGs35Ksnqi2swX5MLYfcBaImzoNde86n -cXJ0yyF82O1pk8DkmU2EDcUoQfkKxr3ANvVDG4vYaguIhYsJqPg/t8XQt/epDz/O -WZhqogn0ysaTv2FHrWcgPAkq82hpNII5NfPP8aRaYh8OUSfh4WHkW84m6+usqwjI -wbOq36qmmwKBgGMFFdreYEmzvwYlDoOiyukKncCfLUeB3HNfTbU/w3RafGjobJBh -qZrVEP4MRkl/F9/9YaXj9JE7haGYTkOfmYGOAp2T04OS3kDClEucuQluOgvqvorh -23jUej5xAGK3pJ046M2dTi7bZokB6PUqWCGbPg127JI4ijxH8FyA50rxAoGAQO2d -jMAFg6vco1JPT1lq7+GYOHBfQsIQDj99fo2yeu1or0rSVhWwHsShcdz9rGKj2Rhc -ysRKMa9/sIzdeNbzT3JxVu+3RgTqjLqMqFlTmZl3qBVxb5iRP5c8rSLAEGYmTtEp -FDqm9GDv8hU0F6SsjyH4AWrdylFOlL4Ai237PJkCgYBDC1wAwBD8WXJqRrYVGj7X -l4TQQ0hO7La/zgbasSgLNaJcYu32nut6D0O8IlmcQ2nO0BGPjQmJFGp6xawjViRu -np7fEkJQEf1pK0yeA8A3urjXccuUXEA9kKeqaSZYDzICPFaOlezPPPpW0hbkhnPe -dQn3DcoY6e6o0K5ltt1RvQ== ------END PRIVATE KEY----- diff --git a/testdata/passwords b/testdata/passwords deleted file mode 100644 index 58530047f..000000000 --- a/testdata/passwords +++ /dev/null @@ -1,2 +0,0 @@ -unusedpassword -mysslpassword From f33032e7d0ac4675a57488583c7d03384729c505 Mon Sep 17 00:00:00 2001 From: DerekBum Date: Tue, 6 Feb 2024 12:56:15 +0300 Subject: [PATCH 528/605] ci: remove Tarantool-EE jobs After all Tarantool-EE specific logic was moved to the `go-tlsdialer` [1], Tarantool-EE jobs could be removed from ci workflows. This commit removes ci jobs for Tarantool-EE. 1. https://github.com/tarantool/go-tlsdialer/ Closes #301 --- .github/workflows/testing.yml | 86 ----------------------------------- 1 file changed, 86 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 3cf0d2808..d70f3b962 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -117,92 +117,6 @@ jobs: - name: Check workability of benchmark tests run: make bench-deps bench DURATION=1x COUNT=1 - run-tests-ee: - # The same as for run-tests-ce, but it does not run on pull requests from - # forks and on forks by default. Tests from forks will run only when the - # pull request is labeled with `full-ci`. To avoid security problems, the - # label must be reset manually for every run. - # - # We need to use `pull_request_target` because it has access to base - # repository secrets unlike `pull_request`. - if: | - github.repository == 'tarantool/go-tarantool' && - (github.event_name == 'push' || - (github.event_name == 'pull_request_target' && - github.event.pull_request.head.repo.full_name != github.repository && - github.event.label.name == 'full-ci')) || - github.event_name == 'workflow_dispatch' - - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - sdk-path: - - 'release/linux/x86_64/1.10/' - sdk-version: - - 'sdk-1.10.15-0-r598' - coveralls: [false] - fuzzing: [false] - include: - - sdk-path: 'release/linux/x86_64/2.10/' - sdk-version: 'sdk-gc64-2.10.8-0-r598.linux.x86_64' - coveralls: false - - sdk-path: 'release/linux/x86_64/2.11/' - sdk-version: 'sdk-gc64-2.11.1-0-r598.linux.x86_64' - coveralls: true - - steps: - - name: Clone the connector - # `ref` as merge request is needed for pull_request_target because this - # target runs in the context of the base commit of the pull request. - uses: actions/checkout@v3 - if: github.event_name == 'pull_request_target' - with: - ref: refs/pull/${{ github.event.pull_request.number }}/merge - - - name: Clone the connector - if: github.event_name != 'pull_request_target' - uses: actions/checkout@v3 - - - name: Setup Tarantool ${{ matrix.sdk-version }} - run: | - ARCHIVE_NAME=tarantool-enterprise-${{ matrix.sdk-version }}.tar.gz - curl -O -L https://${{ secrets.SDK_DOWNLOAD_TOKEN }}@download.tarantool.io/enterprise/${{ matrix.sdk-path }}${ARCHIVE_NAME} - tar -xzf ${ARCHIVE_NAME} - rm -f ${ARCHIVE_NAME} - - - name: Setup golang for the connector and tests - uses: actions/setup-go@v3 - with: - go-version: 1.13 - - - name: Install test dependencies - run: | - source tarantool-enterprise/env.sh - make deps - - - name: Run regression tests - run: | - source tarantool-enterprise/env.sh - make test - make testrace - - - name: Run fuzzing tests - if: ${{ matrix.fuzzing }} - run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" - - - name: Run tests, collect code coverage data and send to Coveralls - if: ${{ matrix.coveralls }} - env: - COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - source tarantool-enterprise/env.sh - make coveralls - - - name: Check workability of benchmark tests - run: make bench-deps bench DURATION=1x COUNT=1 - testing_mac_os: # We want to run on external PRs, but not on our own internal # PRs as they'll be run by the push to the branch. From 49571b69d32c71bd50f4497e5e81e2ce2332b431 Mon Sep 17 00:00:00 2001 From: DerekBum Date: Thu, 8 Feb 2024 15:50:30 +0300 Subject: [PATCH 529/605] go: bump go to 1.20 Bump go version to 1.20. Update ci to use go version 1.20 and newer. Remove usage of the deprecated libraries. Part of #378 --- .github/workflows/reusable_testing.yml | 2 +- .github/workflows/testing.yml | 10 ++++++---- CHANGELOG.md | 1 + Makefile | 10 +++------- README.md | 10 +++++++--- connection.go | 9 ++++----- future_test.go | 3 +-- go.mod | 9 ++++++++- response.go | 3 +-- test_helpers/main.go | 15 ++++++++------- test_helpers/response.go | 3 +-- 11 files changed, 41 insertions(+), 34 deletions(-) diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index c30171d87..df60cd3f9 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -36,7 +36,7 @@ jobs: - name: Setup golang for connector and tests uses: actions/setup-go@v2 with: - go-version: 1.13 + go-version: '1.20' - name: Setup tt run: | diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index d70f3b962..536f8609d 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -29,7 +29,8 @@ jobs: fail-fast: false matrix: golang: - - 1.13 + - '1.20' + - 'stable' tarantool: - '1.10' - '2.8' @@ -40,10 +41,10 @@ jobs: include: - tarantool: 'master' coveralls: true - golang: 1.13 + golang: '1.20' - tarantool: 'master' fuzzing: true - golang: 1.18 + golang: '1.20' coveralls: false steps: @@ -132,7 +133,8 @@ jobs: fail-fast: false matrix: golang: - - 1.13 + - '1.20' + - 'stable' runs-on: - macos-11 - macos-12 diff --git a/CHANGELOG.md b/CHANGELOG.md index c71a63146..df833ee4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. `pool.Instance` to determinate connection options (#356) - `pool.Connect`, `pool.ConnectWithOpts` and `pool.Add` add connections to the pool even it is unable to connect to it (#372) +- Required Go version from `1.11` to `1.20` (#378) ### Deprecated diff --git a/Makefile b/Makefile index fb6817a2a..3a165ef82 100644 --- a/Makefile +++ b/Makefile @@ -114,6 +114,7 @@ coverage: .PHONY: coveralls coveralls: coverage go get github.com/mattn/goveralls + go install github.com/mattn/goveralls goveralls -coverprofile=$(COVERAGE_FILE) -service=github .PHONY: bench-deps @@ -122,14 +123,9 @@ ${BENCH_PATH} bench-deps: rm -rf ${BENCH_PATH} mkdir ${BENCH_PATH} go clean -testcache - # It is unable to build a latest version of benchstat with go 1.13. So - # we need to switch to an old commit. cd ${BENCH_PATH} && \ - git clone https://go.googlesource.com/perf && \ - cd perf && \ - git checkout 91a04616dc65ba76dbe9e5cf746b923b1402d303 && \ - go install ./cmd/benchstat - rm -rf ${BENCH_PATH}/perf + go get golang.org/x/perf/cmd/benchstat + go install golang.org/x/perf/cmd/benchstat .PHONY: bench ${BENCH_FILE} bench: ${BENCH_PATH} diff --git a/README.md b/README.md index 85ca776a1..39a169be3 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,10 @@ faster than other packages according to public benchmarks. We assume that you have Tarantool version 1.10+ and a modern Linux or BSD operating system. -You need a current version of `go`, version 1.13 or later (use `go version` to +You need a current version of `go`, version 1.20 or later (use `go version` to check the version number). Do not use `gccgo-go`. -**Note:** If your `go` version is older than 1.13 or if `go` is not installed, +**Note:** If your `go` version is older than 1.20 or if `go` is not installed, download and run the latest tarball from [golang.org][golang-dl]. The package `go-tarantool` is located in [tarantool/go-tarantool][go-tarantool] @@ -72,7 +72,7 @@ This allows us to introduce new features without losing backward compatibility. ``` go_tarantool_decimal_fuzzing ``` - **Note:** It crashes old Tarantool versions and requires Go 1.18+. + **Note:** It crashes old Tarantool versions. ## Documentation @@ -223,6 +223,10 @@ is only available in Tarantool Enterprise Edition 2.10 or newer. The article describes migration from go-tarantool to go-tarantool/v2. +#### Go version + +Required Go version is set to `1.20`. + #### datetime package Now you need to use objects of the Datetime type instead of pointers to it. A diff --git a/connection.go b/connection.go index 24edebece..53e6b874c 100644 --- a/connection.go +++ b/connection.go @@ -869,7 +869,7 @@ func (conn *Connection) eventer(events <-chan connWatchEvent) { st := value.(chan watchState) state := <-st state.value = event.value - if state.version == math.MaxUint64 { + if state.version == math.MaxUint { state.version = initWatchEventVersion + 1 } else { state.version += 1 @@ -1277,9 +1277,8 @@ func (conn *Connection) NewStream() (*Stream, error) { type watchState struct { // value is a current value. value interface{} - // version is a current version of the value. The only reason for uint64: - // go 1.13 has no math.Uint. - version uint64 + // version is a current version of the value. + version uint // ack true if the acknowledge is already sent. ack bool // cnt is a count of active watchers for the key. @@ -1292,7 +1291,7 @@ type watchState struct { } // initWatchEventVersion is an initial version until no events from Tarantool. -const initWatchEventVersion uint64 = 0 +const initWatchEventVersion uint = 0 // connWatcher is an internal implementation of the Watcher interface. type connWatcher struct { diff --git a/future_test.go b/future_test.go index 6efda10a1..0c8bb79cc 100644 --- a/future_test.go +++ b/future_test.go @@ -5,7 +5,6 @@ import ( "context" "errors" "io" - "io/ioutil" "sync" "testing" "time" @@ -74,7 +73,7 @@ func (resp *futureMockResponse) DecodeTyped(res interface{}) error { } func createFutureMockResponse(header Header, body io.Reader) (Response, error) { - data, err := ioutil.ReadAll(body) + data, err := io.ReadAll(body) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 4f97af1a6..802b8412c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tarantool/go-tarantool/v2 -go 1.11 +go 1.20 require ( github.com/google/uuid v1.3.0 @@ -9,3 +9,10 @@ require ( github.com/tarantool/go-iproto v1.0.0 github.com/vmihailenco/msgpack/v5 v5.3.5 ) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/response.go b/response.go index db88c743c..2c287f8bb 100644 --- a/response.go +++ b/response.go @@ -3,7 +3,6 @@ package tarantool import ( "fmt" "io" - "io/ioutil" "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" @@ -39,7 +38,7 @@ func createBaseResponse(header Header, body io.Reader) (baseResponse, error) { if buf, ok := body.(*smallBuf); ok { return baseResponse{header: header, buf: *buf}, nil } - data, err := ioutil.ReadAll(body) + data, err := io.ReadAll(body) if err != nil { return baseResponse{}, err } diff --git a/test_helpers/main.go b/test_helpers/main.go index 200c3f474..178f69921 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -15,7 +15,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "log" "os" "os/exec" @@ -185,10 +184,7 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { inst.Dialer = startOpts.Dialer if startOpts.WorkDir == "" { - // Create work_dir for a new instance. - // TO DO: replace with `os.MkdirTemp` when we drop support of - // Go 1.16 an older - dir, err = ioutil.TempDir("", "work_dir") + dir, err = os.MkdirTemp("", "work_dir") if err != nil { return inst, err } @@ -305,7 +301,7 @@ func copySslCerts(dst string, sslCertsDir string) (err error) { } func copyDirectoryFiles(scrDir, dest string) error { - entries, err := ioutil.ReadDir(scrDir) + entries, err := os.ReadDir(scrDir) if err != nil { return err } @@ -324,7 +320,12 @@ func copyDirectoryFiles(scrDir, dest string) error { return err } - if err := os.Chmod(destPath, entry.Mode()); err != nil { + info, err := entry.Info() + if err != nil { + return err + } + + if err := os.Chmod(destPath, info.Mode()); err != nil { return err } } diff --git a/test_helpers/response.go b/test_helpers/response.go index 4a28400c0..f8757f563 100644 --- a/test_helpers/response.go +++ b/test_helpers/response.go @@ -3,7 +3,6 @@ package test_helpers import ( "bytes" "io" - "io/ioutil" "testing" "github.com/vmihailenco/msgpack/v5" @@ -43,7 +42,7 @@ func CreateMockResponse(header tarantool.Header, body io.Reader) (*MockResponse, if body == nil { return &MockResponse{header: header, data: nil}, nil } - data, err := ioutil.ReadAll(body) + data, err := io.ReadAll(body) if err != nil { return nil, err } From 86830820a0bec820ad580b413493218b4d5552d7 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 8 Feb 2024 19:04:08 +0300 Subject: [PATCH 530/605] api: remove Future.Err() The method causes an improper error handling because it returns an error only from a client side. Therefore, it is not enough to simply check the error to find out that the request was not completed. A user should check an error from `Future.Get()` or `Future.GetTyped()`. In addition, the user can find out whether there was an error without decoding the response body with `Future.GetResponse()`, see `ExampleErrorNo`. --- CHANGELOG.md | 1 + README.md | 3 +++ future.go | 8 -------- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df833ee4c..1b9643846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,6 +128,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - `Schema` field from the `Connection` struct (#7) - `OkCode` and `PushCode` constants (#237) - SSL support (#301) +- `Future.Err()` method (#382) ### Fixed diff --git a/README.md b/README.md index 39a169be3..b784b3440 100644 --- a/README.md +++ b/README.md @@ -352,6 +352,9 @@ for an `ops` field. `*Operations` needs to be used instead. * `Future` constructors now accept `Request` as their argument. * Methods `AppendPush` and `SetResponse` accepts response `Header` and data as their arguments. +* Method `Err` was removed because it was causing improper error handling. You + You need to check an error from `Get`, `GetTyped` or `GetResponse` with + an addition check of a value `Response.Header().Error`, see `ExampleErrorNo`. #### Connector changes diff --git a/future.go b/future.go index 1b9f2ed14..64e2805f1 100644 --- a/future.go +++ b/future.go @@ -266,11 +266,3 @@ func (fut *Future) WaitChan() <-chan struct{} { } return fut.done } - -// Err returns error set on Future. -// It waits for future to be set. -// Note: it doesn't decode body, therefore decoding error are not set here. -func (fut *Future) Err() error { - fut.wait() - return fut.err -} From 6927d5548239e1c6cf04133272f6fa94478a0af5 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Sun, 11 Feb 2024 19:54:14 +0300 Subject: [PATCH 531/605] doc: add the migration guide to v2.0.0 --- MIGRATION.md | 300 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 252 +++++++------------------------------------ 2 files changed, 336 insertions(+), 216 deletions(-) create mode 100644 MIGRATION.md diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 000000000..1f07639af --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,300 @@ +# Migration guide + +## Migration from v1.x.x to v2.x.x + +* [Major changes](#major-changes) +* [Main package](#main-package) + * [Go version](#go-version) + * [msgpack/v5](#msgpackv5) + * [Call = Call17](#call--call17) + * [IPROTO constants](#iproto-constants) + * [Request interface](#request-interface) + * [Request changes](#request-changes) + * [Response interface](#response-interface) + * [Response changes](#response-changes) + * [Future type](#future-type) + * [Protocol types](#protocol-types) + * [Connector interface](#connector-interface) + * [Connect function](#connect-function) + * [Connection schema](#connection-schema) + * [Schema type](#schema-type) +* [datetime package](#datetime-package) +* [decimal package](#decimal-package) +* [multi package](#multi-package) +* [pool package](#pool-package) +* [crud package](#crud-package) +* [test_helpers package](#test_helpers-package) + +### Major changes + +* The `go_tarantool_call_17` build tag is no longer needed, since by default + the `CallRequest` is `Call17Request`. +* The `go_tarantool_msgpack_v5` build tag is no longer needed, since only the + `msgpack/v5` library is used. +* The `go_tarantool_ssl_disable` build tag is no longer needed, since the + connector is no longer depends on `OpenSSL` by default. You could use the + external library [go-tlsdialer](https://github.com/tarantool/go-tlsdialer) to + create a connection with the `ssl` transport. +* Required Go version is `1.20` now. +* The `Connect` function became more flexible. It now allows to create a + connection with cancellation and a custom `Dialer` implementation. +* It is required to use `Request` implementation types with the `Connection.Do` + method instead of `Connection.` methods. +* The `connection_pool` package renamed to `pool`. + +The basic code for the `v1.12.2` release: +```Go +package tarantool + +import ( + "fmt" + + "github.com/tarantool/go-tarantool" +) + +func main() { + opts := tarantool.Opts{User: "guest"} + conn, err := tarantool.Connect("127.0.0.1:3301", opts) + if err != nil { + fmt.Println("Connection refused:", err) + return + } + + resp, err := conn.Insert(999, []interface{}{99999, "BB"}) + if err != nil { + fmt.Println("Error:", err) + fmt.Println("Code:", resp.Code) + } else { + fmt.Println("Data:", resp.Data) + } +} +``` + +At now became: +```Go +package tarantool + +import ( + "context" + "fmt" + "time" + + "github.com/tarantool/go-tarantool/v2" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3301", + User: "guest", + } + opts := tarantool.Opts{ + Timeout: time.Second, + } + + conn, err := tarantool.Connect(ctx, dialer, opts) + if err != nil { + fmt.Println("Connection refused:", err) + return + } + + data, err := conn.Do( + tarantool.NewInsertRequest(999).Tuple([]interface{}{99999, "BB"})).Get() + if err != nil { + fmt.Println("Error:", err) + } else { + fmt.Println("Data:", data) + } +} +``` + +### Main package + +#### Go version + +Required Go version is updated from `1.13` to `1.20`. + +#### msgpack/v5 + +At now the `msgpack/v5` library is used for the `msgpack` encoding/decondig. + +Most function names and argument types in `msgpack/v5` and `msgpack.v2` +have not changed (in our code, we noticed changes in `EncodeInt`, `EncodeUint` +and `RegisterExt`). But there are a lot of changes in a logic of encoding and +decoding. On the plus side the migration seems easy, but on the minus side you +need to be very careful. + +First of all, `EncodeInt8`, `EncodeInt16`, `EncodeInt32`, `EncodeInt64` +and `EncodeUint*` analogues at `msgpack/v5` encode numbers as is without loss of +type. In `msgpack.v2` the type of a number is reduced to a value. + +Secondly, a base decoding function does not convert numbers to `int64` or +`uint64`. It converts numbers to an exact type defined by MessagePack. The +change makes manual type conversions much more difficult and can lead to +runtime errors with an old code. We do not recommend to use type conversions +and give preference to `*Typed` functions (besides, it's faster). + +There are also changes in the logic that can lead to errors in the old code, +[as example](https://github.com/vmihailenco/msgpack/issues/327). Although in +`msgpack/v5` some functions for the logic tuning were added (see +[UseLooseInterfaceDecoding](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5#Decoder.UseLooseInterfaceDecoding), [UseCompactInts](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5#Encoder.UseCompactInts) etc), it is still impossible +to achieve full compliance of behavior between `msgpack/v5` and `msgpack.v2`. +So we don't go this way. We use standard settings if it possible. + +#### Call = Call17 + +Call requests uses `IPROTO_CALL` instead of `IPROTO_CALL_16`. + +So now `Call` = `Call17` and `NewCallRequest` = `NewCall17Request`. A result +of the requests is an array instead of array of arrays. + +#### IPROTO constants + +* IPROTO constants have been moved to a separate package [go-iproto](https://github.com/tarantool/go-iproto). +* `PushCode` constant is removed. To check whether the current response is + a push response, use `IsPush()` method of the response iterator instead. +* `ErrorNo` constant is added to indicate that no error has occurred while + getting the response. It should be used instead of the removed `OkCode`. + See `ExampleErrorNo`. + +#### Request interface + +* The method `Code() uint32` replaced by the `Type() iproto.Type`. +* `Response` method added to the `Request` interface. + +#### Request changes + +* Requests `Update`, `UpdateAsync`, `UpdateTyped`, `Upsert`, `UpsertAsync` no +longer accept `ops` argument (operations) as an `interface{}`. `*Operations` +needs to be passed instead. +* `Op` struct for update operations made private. +* Removed `OpSplice` struct. +* `Operations.Splice` method now accepts 5 arguments instead of 3. +* `UpdateRequest` and `UpsertRequest` structs no longer accept `interface{}` +for an `ops` field. `*Operations` needs to be used instead. + +#### Response interface + +* `Response` is now an interface. +* Response header stored in a new `Header` struct. It could be accessed via + `Header()` method. + +#### Response changes + +* `ResponseIterator` interface now has `IsPush()` method. + It returns true if the current response is a push response. +* For each request type, a different response type is created. They all + implement a `Response` interface. `SelectResponse`, `PrepareResponse`, + `ExecuteResponse`, `PushResponse` are a part of a public API. + `Pos()`, `MetaData()`, `SQLInfo()` methods created for them to get specific + info. Special types of responses are used with special requests. + +#### Future type + +* Method `Get` now returns response data instead of the actual response. +* New method `GetResponse` added to get an actual response. +* `Future` constructors now accept `Request` as their argument. +* Methods `AppendPush` and `SetResponse` accepts response `Header` and data + as their arguments. +* Method `Err` was removed because it was causing improper error handling. + You need to check an error from `Get`, `GetTyped` or `GetResponse` with + an addition check of a value `Response.Header().Error`, see `ExampleErrorNo`. + +#### Connector interface + +* Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, + `Upsert`, `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` + return response data instead of an actual responses. +* New interface `Doer` is added as a child-interface instead of a `Do` method. + +#### Connect function + +`connection.Connect` no longer return non-working connection objects. This +function now does not attempt to reconnect and tries to establish a connection +only once. Function might be canceled via context. Context accepted as first +argument, and user may cancel it in process. + +Now you need to pass `Dialer` as the second argument instead of URI. +If you were using a non-SSL connection, you need to create `NetDialer`. +For SSL-enabled connections, use `OpenSSLDialer` from the +[go-tlsdialer](https://github.com/tarantool/go-tlsdialer) package. + +Please note that the options for creating a connection are now stored in +corresponding `Dialer`, not in `Opts`. + +#### Connection schema + +* Removed `Schema` field from the `Connection` struct. Instead, new + `GetSchema(Doer)` function was added to get the actual connection + schema on demand. +* `OverrideSchema(*Schema)` method replaced with the `SetSchema(Schema)`. + +#### Protocol types + +* `iproto.Feature` type used instead of `ProtocolFeature`. +* `iproto.IPROTO_FEATURE_` constants used instead of local ones. + +#### Schema type + +* `ResolveSpaceIndex` function for `SchemaResolver` interface split into two: +`ResolveSpace` and `ResolveIndex`. `NamesUseSupported` function added into the +interface to get information if the usage of space and index names in requests +is supported. +* `Schema` structure no longer implements `SchemaResolver` interface. +* `Spaces` and `SpacesById` fields of the `Schema` struct store spaces by value. +* `Fields` and `FieldsById` fields of the `Space` struct store fields by value. +`Index` and `IndexById` fields of the `Space` struct store indexes by value. +* `Fields` field of the `Index` struct store `IndexField` by value. + +### datetime package + +Now you need to use objects of the Datetime type instead of pointers to it. A +new constructor `MakeDatetime` returns an object. `NewDatetime` has been +removed. + +### decimal package + +Now you need to use objects of the Decimal type instead of pointers to it. A +new constructor `MakeDecimal` returns an object. `NewDecimal` has been removed. + +### multi package + +The subpackage has been deleted. You could use `pool` instead. + +### pool package + +* The `connection_pool` subpackage has been renamed to `pool`. +* The type `PoolOpts` has been renamed to `Opts`. +* `pool.Connect` and `pool.ConnectWithOpts` now accept context as the first + argument, which user may cancel in process. If it is canceled in progress, + an error will be returned and all created connections will be closed. +* `pool.Connect` and `pool.ConnectWithOpts` now accept `[]pool.Instance` as + the second argument instead of a list of addresses. Each instance is + associated with a unique string name, `Dialer` and connection options which + allows instances to be independently configured. +* `pool.Connect`, `pool.ConnectWithOpts` and `pool.Add` add instances into + the pool even it is unable to connect to it. The pool will try to connect to + the instance later. +* `pool.Add` now accepts context as the first argument, which user may cancel + in process. +* `pool.Add` now accepts `pool.Instance` as the second argument instead of + an address, it allows to configure a new instance more flexible. +* `pool.GetPoolInfo` has been renamed to `pool.GetInfo`. Return type has been + changed to `map[string]ConnectionInfo`. +* Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, + `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Pooler` return + response data instead of an actual responses. + +### crud package + +* `crud` operations `Timeout` option has `crud.OptFloat64` type + instead of `crud.OptUint`. +* A slice of a custom type could be used as tuples for `ReplaceManyRequest` and + `InsertManyRequest`, `ReplaceObjectManyRequest`. +* A slice of a custom type could be used as objects for `ReplaceObjectManyRequest` + and `InsertObjectManyRequest`. + +### test_helpers package + +* Renamed `StrangerResponse` to `MockResponse`. diff --git a/README.md b/README.md index b784b3440..db0885fb9 100644 --- a/README.md +++ b/README.md @@ -26,16 +26,8 @@ faster than other packages according to public benchmarks. * [Documentation](#documentation) * [API reference](#api-reference) * [Walking\-through example](#walking-through-example) - * [Migration to v2](#migration-to-v2) - * [datetime package](#datetime-package) - * [decimal package](#decimal-package) - * [multi package](#multi-package) - * [pool package](#pool-package) - * [crud package](#crud-package) - * [msgpack.v5](#msgpackv5) - * [Call = Call17](#call--call17) - * [IPROTO constants](#iproto-constants) - * [Request interface](#request-interface) + * [Example with encrypting traffic](#example-with-encrypting-traffic) +* [Migration guide](#migration-guide) * [Contributing](#contributing) * [Alternative connectors](#alternative-connectors) @@ -110,27 +102,28 @@ import ( ) func main() { - ctx, cancel := context.WithTimeout(context.Background(), - 500 * time.Millisecond) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - dialer := tarantool.NetDialer { + dialer := tarantool.NetDialer{ Address: "127.0.0.1:3301", User: "guest", } opts := tarantool.Opts{ Timeout: time.Second, } + conn, err := tarantool.Connect(ctx, dialer, opts) if err != nil { fmt.Println("Connection refused:", err) + return } - data, err := conn.Do(tarantool.NewInsertRequest(999). - Tuple([]interface{}{99999, "BB"}), - ).Get() + + data, err := conn.Do( + tarantool.NewInsertRequest(999).Tuple([]interface{}{99999, "BB"})).Get() if err != nil { - fmt.Println("Error", err) + fmt.Println("Error:", err) } else { - fmt.Printf("Data: %v", data) + fmt.Println("Data:", data) } } ``` @@ -138,15 +131,19 @@ func main() { **Observation 1:** The line "`github.com/tarantool/go-tarantool/v2`" in the `import(...)` section brings in all Tarantool-related functions and structures. -**Observation 2:** The line starting with "`dialer :=`" creates dialer for +**Observation 2:** The line starting with "`ctx, cancel :=`" creates a context +object for `Connect()`. The `Connect()` call will return an error when a +timeout expires before the connection is established. + +**Observation 3:** The line starting with "`dialer :=`" creates dialer for `Connect()`. This structure contains fields required to establish a connection. -**Observation 3:** The line starting with "`opts :=`" sets up the options for +**Observation 4:** The line starting with "`opts :=`" sets up the options for `Connect()`. In this example, the structure contains only a single value, the timeout. The structure may also contain other settings, see more in [documentation][godoc-opts-url] for the "`Opts`" structure. -**Observation 4:** The line containing "`tarantool.Connect`" is essential for +**Observation 5:** The line containing "`tarantool.Connect`" is essential for starting a session. There are three parameters: * a context, @@ -158,19 +155,19 @@ There will be only one attempt to connect. If multiple attempts needed, between each try. Example could be found in the [example_test](./example_test.go), name - `ExampleConnect_reconnects`. -**Observation 5:** The `err` structure will be `nil` if there is no error, +**Observation 6:** The `err` structure will be `nil` if there is no error, otherwise it will have a description which can be retrieved with `err.Error()`. -**Observation 6:** The `Insert` request, like almost all requests, is preceded +**Observation 7:** The `Insert` request, like almost all requests, is preceded by the method `Do` of object `conn` which is the object that was returned by `Connect()`. ### Example with encrypting traffic -For SSL-enabled connections, use `OpenSSLDialer` from the [`go-tlsdialer`](https://github.com/tarantool/go-tlsdialer) -package. +For SSL-enabled connections, use `OpenSSLDialer` from the +[go-tlsdialer](https://github.com/tarantool/go-tlsdialer) package. -Here is small example with importing `go-tlsdialer` and using the +Here is small example with importing the `go-tlsdialer` library and using the `OpenSSLDialer`: ```go @@ -186,7 +183,9 @@ import ( ) func main() { - sslDialer := tlsdialer.OpenSSLDialer{ + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + dialer := tlsdialer.OpenSSLDialer{ Address: "127.0.0.1:3013", User: "test", Password: "test", @@ -198,20 +197,18 @@ func main() { Timeout: time.Second, } - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - conn, err := tarantool.Connect(ctx, sslDialer, opts) + conn, err := tarantool.Connect(ctx, dialer, opts) if err != nil { - fmt.Printf("Connection refused: %s", err) + fmt.Println("Connection refused:", err) + return } - data, err := conn.Do(tarantool.NewInsertRequest(999). - Tuple([]interface{}{99999, "BB"}), - ).Get() + data, err := conn.Do( + tarantool.NewInsertRequest(999).Tuple([]interface{}{99999, "BB"})).Get() if err != nil { - fmt.Printf("Error: %s", err) + fmt.Println("Error:", err) } else { - fmt.Printf("Data: %v", data) + fmt.Println("Data:", data) } } ``` @@ -219,187 +216,10 @@ func main() { Note that [traffic encryption](https://www.tarantool.io/en/doc/latest/enterprise/security/#encrypting-traffic) is only available in Tarantool Enterprise Edition 2.10 or newer. -### Migration to v2 - -The article describes migration from go-tarantool to go-tarantool/v2. - -#### Go version - -Required Go version is set to `1.20`. - -#### datetime package - -Now you need to use objects of the Datetime type instead of pointers to it. A -new constructor `MakeDatetime` returns an object. `NewDatetime` has been -removed. - -#### decimal package - -Now you need to use objects of the Decimal type instead of pointers to it. A -new constructor `MakeDecimal` returns an object. `NewDecimal` has been removed. - -#### multi package - -The subpackage has been deleted. You could use `pool` instead. - -#### pool package - -* The `connection_pool` subpackage has been renamed to `pool`. -* The type `PoolOpts` has been renamed to `Opts`. -* `pool.Connect` and `pool.ConnectWithOpts` now accept context as the first - argument, which user may cancel in process. If it is canceled in progress, - an error will be returned and all created connections will be closed. -* `pool.Connect` and `pool.ConnectWithOpts` now accept `[]pool.Instance` as - the second argument instead of a list of addresses. Each instance is - associated with a unique string name, `Dialer` and connection options which - allows instances to be independently configured. -* `pool.Connect`, `pool.ConnectWithOpts` and `pool.Add` add instances into - the pool even it is unable to connect to it. The pool will try to connect to - the instance later. -* `pool.Add` now accepts context as the first argument, which user may cancel - in process. -* `pool.Add` now accepts `pool.Instance` as the second argument instead of - an address, it allows to configure a new instance more flexible. -* `pool.GetPoolInfo` has been renamed to `pool.GetInfo`. Return type has been - changed to `map[string]ConnectionInfo`. -* Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, - `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Pooler` return - response data instead of an actual responses. - -#### crud package - -* `crud` operations `Timeout` option has `crud.OptFloat64` type - instead of `crud.OptUint`. -* A slice of a custom type could be used as tuples for `ReplaceManyRequest` and - `InsertManyRequest`, `ReplaceObjectManyRequest`. -* A slice of a custom type could be used as objects for `ReplaceObjectManyRequest` - and `InsertObjectManyRequest`. - -#### test_helpers package - -Renamed `StrangerResponse` to `MockResponse`. - -#### msgpack.v5 - -Most function names and argument types in `msgpack.v5` and `msgpack.v2` -have not changed (in our code, we noticed changes in `EncodeInt`, `EncodeUint` -and `RegisterExt`). But there are a lot of changes in a logic of encoding and -decoding. On the plus side the migration seems easy, but on the minus side you -need to be very careful. - -First of all, `EncodeInt8`, `EncodeInt16`, `EncodeInt32`, `EncodeInt64` -and `EncodeUint*` analogues at `msgpack.v5` encode numbers as is without loss of -type. In `msgpack.v2` the type of a number is reduced to a value. +## Migration guide -Secondly, a base decoding function does not convert numbers to `int64` or -`uint64`. It converts numbers to an exact type defined by MessagePack. The -change makes manual type conversions much more difficult and can lead to -runtime errors with an old code. We do not recommend to use type conversions -and give preference to `*Typed` functions (besides, it's faster). - -There are also changes in the logic that can lead to errors in the old code, -[as example](https://github.com/vmihailenco/msgpack/issues/327). Although in -`msgpack.v5` some functions for the logic tuning were added (see -[UseLooseInterfaceDecoding](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5#Decoder.UseLooseInterfaceDecoding), [UseCompactInts](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5#Encoder.UseCompactInts) etc), it is still impossible -to achieve full compliance of behavior between `msgpack.v5` and `msgpack.v2`. So -we don't go this way. We use standard settings if it possible. - -#### Call = Call17 - -Call requests uses `IPROTO_CALL` instead of `IPROTO_CALL_16`. - -So now `Call` = `Call17` and `NewCallRequest` = `NewCall17Request`. A result -of the requests is an array instead of array of arrays. - -#### IPROTO constants - -* IPROTO constants have been moved to a separate package [go-iproto](https://github.com/tarantool/go-iproto). -* `PushCode` constant is removed. To check whether the current response is - a push response, use `IsPush()` method of the response iterator instead. -* `ErrorNo` constant is added to indicate that no error has occurred while - getting the response. It should be used instead of the removed `OkCode`. - -#### Request changes - -* The method `Code() uint32` replaced by the `Type() iproto.Type`. -* `Op` struct for update operations made private. -* Removed `OpSplice` struct. -* `Operations.Splice` method now accepts 5 arguments instead of 3. -* Requests `Update`, `UpdateAsync`, `UpdateTyped`, `Upsert`, `UpsertAsync` no -longer accept `ops` argument (operations) as an `interface{}`. `*Operations` -needs to be passed instead. -* `UpdateRequest` and `UpsertRequest` structs no longer accept `interface{}` -for an `ops` field. `*Operations` needs to be used instead. -* `Response` method added to the `Request` interface. - -#### Response changes - -* `Response` is now an interface. -* Response header stored in a new `Header` struct. It could be accessed via - `Header()` method. -* `ResponseIterator` interface now has `IsPush()` method. - It returns true if the current response is a push response. -* For each request type, a different response type is created. They all - implement a `Response` interface. `SelectResponse`, `PrepareResponse`, - `ExecuteResponse`, `PushResponse` are a part of a public API. - `Pos()`, `MetaData()`, `SQLInfo()` methods created for them to get specific info. - Special types of responses are used with special requests. - -#### Future changes - -* Method `Get` now returns response data instead of the actual response. -* New method `GetResponse` added to get an actual response. -* `Future` constructors now accept `Request` as their argument. -* Methods `AppendPush` and `SetResponse` accepts response `Header` and data - as their arguments. -* Method `Err` was removed because it was causing improper error handling. You - You need to check an error from `Get`, `GetTyped` or `GetResponse` with - an addition check of a value `Response.Header().Error`, see `ExampleErrorNo`. - -#### Connector changes - -* Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, - `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` return - response data instead of an actual responses. -* New interface `Doer` is added as a child-interface instead of a `Do` method. - -#### Connect function - -`connection.Connect` no longer return non-working connection objects. This function -now does not attempt to reconnect and tries to establish a connection only once. -Function might be canceled via context. Context accepted as first argument, -and user may cancel it in process. - -Now you need to pass `Dialer` as the second argument instead of URI. -If you were using a non-SSL connection, you need to create `NetDialer`. -For SSL-enabled connections, use `OpenSSLDialer` from the `go-tlsdialer` -package. -Please note that the options for creating a connection are now stored in -corresponding `Dialer`, not in `Opts`. - -#### Connection schema - -* Removed `Schema` field from the `Connection` struct. Instead, new - `GetSchema(Doer)` function was added to get the actual connection - schema on demand. -* `OverrideSchema(*Schema)` method replaced with the `SetSchema(Schema)`. - -#### Protocol changes - -* `iproto.Feature` type used instead of `ProtocolFeature`. -* `iproto.IPROTO_FEATURE_` constants used instead of local ones. - -#### Schema changes - -* `ResolveSpaceIndex` function for `SchemaResolver` interface split into two: -`ResolveSpace` and `ResolveIndex`. `NamesUseSupported` function added into the -interface to get information if the usage of space and index names in requests -is supported. -* `Schema` structure no longer implements `SchemaResolver` interface. -* `Spaces` and `SpacesById` fields of the `Schema` struct store spaces by value. -* `Fields` and `FieldsById` fields of the `Space` struct store fields by value. -`Index` and `IndexById` fields of the `Space` struct store indexes by value. -* `Fields` field of the `Index` struct store `IndexField` by value. +You can review the changes between major versions in the +[migration guide](./MIGRATION.md). ## Contributing From fbf6fe07e71abbd4ba0ee74840fdd75cee68dbca Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Sun, 11 Feb 2024 20:02:50 +0300 Subject: [PATCH 532/605] doc: add external types imports to main examples Closes #352 --- MIGRATION.md | 6 ++++++ README.md | 21 +++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 1f07639af..fdd4893dc 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -50,6 +50,9 @@ import ( "fmt" "github.com/tarantool/go-tarantool" + _ "github.com/tarantool/go-tarantool/v2/datetime" + _ "github.com/tarantool/go-tarantool/v2/decimal" + _ "github.com/tarantool/go-tarantool/v2/uuid" ) func main() { @@ -80,6 +83,9 @@ import ( "time" "github.com/tarantool/go-tarantool/v2" + _ "github.com/tarantool/go-tarantool/v2/datetime" + _ "github.com/tarantool/go-tarantool/v2/decimal" + _ "github.com/tarantool/go-tarantool/v2/uuid" ) func main() { diff --git a/README.md b/README.md index db0885fb9..71f526712 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,9 @@ import ( "time" "github.com/tarantool/go-tarantool/v2" + _ "github.com/tarantool/go-tarantool/v2/datetime" + _ "github.com/tarantool/go-tarantool/v2/decimal" + _ "github.com/tarantool/go-tarantool/v2/uuid" ) func main() { @@ -131,19 +134,22 @@ func main() { **Observation 1:** The line "`github.com/tarantool/go-tarantool/v2`" in the `import(...)` section brings in all Tarantool-related functions and structures. -**Observation 2:** The line starting with "`ctx, cancel :=`" creates a context +**Observation 2:** Unused import lines are required to initialize encoders and +decoders for external `msgpack` types. + +**Observation 3:** The line starting with "`ctx, cancel :=`" creates a context object for `Connect()`. The `Connect()` call will return an error when a timeout expires before the connection is established. -**Observation 3:** The line starting with "`dialer :=`" creates dialer for +**Observation 4:** The line starting with "`dialer :=`" creates dialer for `Connect()`. This structure contains fields required to establish a connection. -**Observation 4:** The line starting with "`opts :=`" sets up the options for +**Observation 5:** The line starting with "`opts :=`" sets up the options for `Connect()`. In this example, the structure contains only a single value, the timeout. The structure may also contain other settings, see more in [documentation][godoc-opts-url] for the "`Opts`" structure. -**Observation 5:** The line containing "`tarantool.Connect`" is essential for +**Observation 6:** The line containing "`tarantool.Connect`" is essential for starting a session. There are three parameters: * a context, @@ -155,10 +161,10 @@ There will be only one attempt to connect. If multiple attempts needed, between each try. Example could be found in the [example_test](./example_test.go), name - `ExampleConnect_reconnects`. -**Observation 6:** The `err` structure will be `nil` if there is no error, +**Observation 7:** The `err` structure will be `nil` if there is no error, otherwise it will have a description which can be retrieved with `err.Error()`. -**Observation 7:** The `Insert` request, like almost all requests, is preceded +**Observation 8:** The `Insert` request, like almost all requests, is preceded by the method `Do` of object `conn` which is the object that was returned by `Connect()`. @@ -179,6 +185,9 @@ import ( "time" "github.com/tarantool/go-tarantool/v2" + _ "github.com/tarantool/go-tarantool/v2/datetime" + _ "github.com/tarantool/go-tarantool/v2/decimal" + _ "github.com/tarantool/go-tarantool/v2/uuid" "github.com/tarantool/go-tlsdialer" ) From 23f0526464947d9f18dc3afba1a6d5e62f088f78 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Sun, 11 Feb 2024 21:49:26 +0300 Subject: [PATCH 533/605] doc: update pkg.go.dev links --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 71f526712..f5c533390 100644 --- a/README.md +++ b/README.md @@ -245,8 +245,8 @@ There are two other connectors available from the open source community: See feature comparison in the [documentation][tarantool-doc-connectors-comparison]. [tarantool-site]: https://tarantool.io/ -[godoc-badge]: https://pkg.go.dev/badge/github.com/tarantool/go-tarantool.svg -[godoc-url]: https://pkg.go.dev/github.com/tarantool/go-tarantool +[godoc-badge]: https://pkg.go.dev/badge/github.com/tarantool/go-tarantool/v2.svg +[godoc-url]: https://pkg.go.dev/github.com/tarantool/go-tarantool/v2 [actions-badge]: https://github.com/tarantool/go-tarantool/actions/workflows/testing.yml/badge.svg [actions-url]: https://github.com/tarantool/go-tarantool/actions/workflows/testing.yml [coverage-badge]: https://coveralls.io/repos/github/tarantool/go-tarantool/badge.svg?branch=master @@ -261,5 +261,5 @@ See feature comparison in the [documentation][tarantool-doc-connectors-compariso [go-tarantool]: https://github.com/tarantool/go-tarantool [tarantool-doc-data-model-url]: https://www.tarantool.io/en/doc/latest/book/box/data_model/ [tarantool-doc-box-space-url]: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/ -[godoc-opts-url]: https://pkg.go.dev/github.com/tarantool/go-tarantool#Opts +[godoc-opts-url]: https://pkg.go.dev/github.com/tarantool/go-tarantool/v2#Opts [tarantool-doc-connectors-comparison]: https://www.tarantool.io/en/doc/latest/book/connectors/#go-feature-comparison From 1046f295c04b256841e18c4234a5489426992917 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Sun, 11 Feb 2024 20:12:28 +0300 Subject: [PATCH 534/605] Release 2.0.0 Overview There are a lot of changes in the new major version. The main ones: * The `go_tarantool_call_17` build tag is no longer needed, since by default the `CallRequest` is `Call17Request`. * The `go_tarantool_msgpack_v5` build tag is no longer needed, since only the `msgpack/v5` library is used. * The `go_tarantool_ssl_disable` build tag is no longer needed, since the connector is no longer depends on `OpenSSL` by default. You could use the external library go-tlsdialer[1] to create a connection with the `ssl` transport. * Required Go version is `1.20` now. * The `Connect` function became more flexible. It now allows to create a connection with cancellation and a custom `Dialer` implementation. * It is required to use `Request` implementation types with the `Connection.Do` method instead of `Connection.` methods. * The `connection_pool` package renamed to `pool`. See the migration guide[2] for more details. Breaking changes connection_pool renamed to pool (#239). Use msgpack/v5 instead of msgpack.v2 (#236). Call/NewCallRequest = Call17/NewCall17Request (#235). Change encoding of the queue.Identify() UUID argument from binary blob to plain string. Needed for upgrade to Tarantool 3.0, where a binary blob is decoded to a varbinary object (#313). Use objects of the Decimal type instead of pointers (#238). Use objects of the Datetime type instead of pointers (#238). `connection.Connect` no longer return non-working connection objects (#136). This function now does not attempt to reconnect and tries to establish a connection only once. Function might be canceled via context. Context accepted as first argument. `pool.Connect` and `pool.Add` now accept context as the first argument, which user may cancel in process. If `pool.Connect` is canceled in progress, an error will be returned. All created connections will be closed. `iproto.Feature` type now used instead of `ProtocolFeature` (#337). `iproto.IPROTO_FEATURE_` constants now used instead of local `Feature` constants for `protocol` (#337). Change `crud` operations `Timeout` option type to `crud.OptFloat64` instead of `crud.OptUint` (#342). Change all `Upsert` and `Update` requests to accept `*tarantool.Operations` as `ops` parameters instead of `interface{}` (#348). Change `OverrideSchema(*Schema)` to `SetSchema(Schema)` (#7). Change values, stored by pointers in the `Schema`, `Space`, `Index` structs, to be stored by their values (#7). Make `Dialer` mandatory for creation a single connection (#321). Remove `Connection.RemoteAddr()`, `Connection.LocalAddr()`. Add `Addr()` function instead (#321). Remove `Connection.ClientProtocolInfo`, `Connection.ServerProtocolInfo`. Add `ProtocolInfo()` function instead, which returns the server protocol info (#321). `NewWatcher` checks the actual features of the server, rather than relying on the features provided by the user during connection creation (#321). `pool.NewWatcher` does not create watchers for connections that do not support it (#321). Rename `pool.GetPoolInfo` to `pool.GetInfo`. Change return type to `map[string]ConnectionInfo` (#321). `Response` is now an interface (#237). All responses are now implementations of the `Response` interface (#237). `SelectResponse`, `ExecuteResponse`, `PrepareResponse`, `PushResponse` are part of a public API. `Pos()`, `MetaData()`, `SQLInfo()` methods created for them to get specific info. Special types of responses are used with special requests. `IsPush()` method is added to the response iterator (#237). It returns the information if the current response is a `PushResponse`. `PushCode` constant is removed. Method `Get` for `Future` now returns response data (#237). To get the actual response new `GetResponse` method has been added. Methods `AppendPush` and `SetResponse` accept response `Header` and data as their arguments. `Future` constructors now accept `Request` as their argument (#237). Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` and `Pooler` return response data instead of an actual responses (#237). `pool.Connect`, `pool.ConnetcWithOpts` and `pool.Add` use a new type `pool.Instance` to determinate connection options (#356). `pool.Connect`, `pool.ConnectWithOpts` and `pool.Add` add connections to the pool even it is unable to connect to it (#372). Required Go version from `1.13` to `1.20` (#378). multi subpackage is removed (#240). msgpack.v2 support is removed (#236). pool/RoundRobinStrategy is removed (#158). DeadlineIO is removed (#158). UUID_extId is removed (#158). IPROTO constants are removed (#158). Code() method from the Request interface is removed (#158). `Schema` field from the `Connection` struct is removed (#7). `OkCode` and `PushCode` constants is removed (#237). SSL support is removed (#301). `Future.Err()` method is removed (#382). New features Type() method to the Request interface (#158). Enumeration types for RLimitAction/iterators (#158). IsNullable flag for Field (#302). Meaningful description for read/write socket errors (#129). Support `operation_data` in `crud.Error` (#330). Support `fetch_latest_metadata` option for crud requests with metadata (#335). Support `noreturn` option for data change crud requests (#335). Support `crud.schema` request (#336, #351). Support `IPROTO_WATCH_ONCE` request type for Tarantool version >= 3.0.0-alpha1 (#337). Support `yield_every` option for crud select requests (#350). Support `IPROTO_FEATURE_SPACE_AND_INDEX_NAMES` for Tarantool version >= 3.0.0-alpha1 (#338). It allows to use space and index names in requests instead of their IDs. `GetSchema` function to get the actual schema (#7). Support connection via an existing socket fd (#321). `Header` struct for the response header (#237). It can be accessed via `Header()` method of the `Response` interface. `Response` method added to the `Request` interface (#237). New `LogAppendPushFailed` connection log constant (#237). It is logged when connection fails to append a push response. `ErrorNo` constant that indicates that no error has occurred while getting the response (#237). `AuthDialer` type for creating a dialer with authentication (#301). `ProtocolDialer` type for creating a dialer with `ProtocolInfo` receiving and check (#301). `GreetingDialer` type for creating a dialer, that fills `Greeting` of a connection (#301). New method `Pool.DoInstance` to execute a request on a target instance in a pool (#376). Bugfixes Race condition at roundRobinStrategy.GetNextConnection() (#309). Incorrect decoding of an MP_DECIMAL when the `scale` value is negative (#314). Incorrect options (`after`, `batch_size` and `force_map_call`) setup for crud.SelectRequest (#320). Incorrect options (`vshard_router`, `fields`, `bucket_id`, `mode`, `prefer_replica`, `balance`) setup for crud.GetRequest (#335). Splice update operation accepts 3 arguments instead of 5 (#348). Unable to use a slice of custom types as a slice of tuples or objects for `crud.*ManyRequest/crud.*ObjectManyRequest` (#365). Testing More linters on CI (#310). Added an ability to mock connections for tests (#237). Added new types `MockDoer`, `MockRequest` to `test_helpers`. Fixed flaky decimal/TestSelect (#300). Fixed tests with crud 1.4.0 (#336). Fixed tests with case sensitive SQL (#341). Renamed `StrangerResponse` to `MockResponse` (#237). Other All Connection., Connection.Typed and Connection.Async methods are now deprecated. Instead you should use requests objects + Connection.Do() (#241). All ConnectionPool., ConnectionPool.Typed and ConnectionPool.Async methods are now deprecated. Instead you should use requests objects + ConnectionPool.Do() (#241). box.session.push() usage is deprecated: Future.AppendPush() and Future.GetIterator() methods, ResponseIterator and TimeoutResponseIterator types (#324). 1. https://github.com/tarantool/go-tlsdialer 2. https://github.com/tarantool/go-tarantool/blob/master/MIGRATION.md --- CHANGELOG.md | 51 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b9643846..30aa750f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,20 +10,47 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +### Changed + +### Fixed + +## [2.0.0] - 2024-02-12 + +There are a lot of changes in the new major version. The main ones: + +* The `go_tarantool_call_17` build tag is no longer needed, since by default + the `CallRequest` is `Call17Request`. +* The `go_tarantool_msgpack_v5` build tag is no longer needed, since only the + `msgpack/v5` library is used. +* The `go_tarantool_ssl_disable` build tag is no longer needed, since the + connector is no longer depends on `OpenSSL` by default. You could use the + external library [go-tlsdialer](https://github.com/tarantool/go-tlsdialer) to + create a connection with the `ssl` transport. +* Required Go version is `1.20` now. +* The `Connect` function became more flexible. It now allows to create a + connection with cancellation and a custom `Dialer` implementation. +* It is required to use `Request` implementation types with the `Connection.Do` + method instead of `Connection.` methods. +* The `connection_pool` package renamed to `pool`. + +See the [migration guide](./MIGRATION.md) for more details. + +### Added + - Type() method to the Request interface (#158) - Enumeration types for RLimitAction/iterators (#158) - IsNullable flag for Field (#302) - More linters on CI (#310) - Meaningful description for read/write socket errors (#129) -- Support `operation_data` in `crud.Error` (#330) +- Support `operation_data` in `crud.Error` (#330) - Support `fetch_latest_metadata` option for crud requests with metadata (#335) - Support `noreturn` option for data change crud requests (#335) - Support `crud.schema` request (#336, #351) -- Support `IPROTO_WATCH_ONCE` request type for Tarantool +- Support `IPROTO_WATCH_ONCE` request type for Tarantool version >= 3.0.0-alpha1 (#337) - Support `yield_every` option for crud select requests (#350) - Support `IPROTO_FEATURE_SPACE_AND_INDEX_NAMES` for Tarantool - version >= 3.0.0-alpha1 (#338). It allows to use space and index names + version >= 3.0.0-alpha1 (#338). It allows to use space and index names in requests instead of their IDs. - `GetSchema` function to get the actual schema (#7) - Support connection via an existing socket fd (#321) @@ -37,12 +64,12 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Ability to mock connections for tests (#237). Added new types `MockDoer`, `MockRequest` to `test_helpers`. - `AuthDialer` type for creating a dialer with authentication (#301) -- `ProtocolDialer` type for creating a dialer with `ProtocolInfo` receiving and +- `ProtocolDialer` type for creating a dialer with `ProtocolInfo` receiving and check (#301) - `GreetingDialer` type for creating a dialer, that fills `Greeting` of a connection (#301) - New method `Pool.DoInstance` to execute a request on a target instance in - a pool (#376). + a pool (#376) ### Changed @@ -54,22 +81,22 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. decoded to a varbinary object (#313). - Use objects of the Decimal type instead of pointers (#238) - Use objects of the Datetime type instead of pointers (#238) -- `connection.Connect` no longer return non-working - connection objects (#136). This function now does not attempt to reconnect - and tries to establish a connection only once. Function might be canceled +- `connection.Connect` no longer return non-working + connection objects (#136). This function now does not attempt to reconnect + and tries to establish a connection only once. Function might be canceled via context. Context accepted as first argument. `pool.Connect` and `pool.Add` now accept context as the first argument, which user may cancel in process. If `pool.Connect` is canceled in progress, an error will be returned. All created connections will be closed. - `iproto.Feature` type now used instead of `ProtocolFeature` (#337) -- `iproto.IPROTO_FEATURE_` constants now used instead of local `Feature` +- `iproto.IPROTO_FEATURE_` constants now used instead of local `Feature` constants for `protocol` (#337) - Change `crud` operations `Timeout` option type to `crud.OptFloat64` instead of `crud.OptUint` (#342) -- Change all `Upsert` and `Update` requests to accept `*tarantool.Operations` +- Change all `Upsert` and `Update` requests to accept `*tarantool.Operations` as `ops` parameters instead of `interface{}` (#348) - Change `OverrideSchema(*Schema)` to `SetSchema(Schema)` (#7) -- Change values, stored by pointers in the `Schema`, `Space`, `Index` structs, +- Change values, stored by pointers in the `Schema`, `Space`, `Index` structs, to be stored by their values (#7) - Make `Dialer` mandatory for creation a single connection (#321) - Remove `Connection.RemoteAddr()`, `Connection.LocalAddr()`. @@ -103,7 +130,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. `pool.Instance` to determinate connection options (#356) - `pool.Connect`, `pool.ConnectWithOpts` and `pool.Add` add connections to the pool even it is unable to connect to it (#372) -- Required Go version from `1.11` to `1.20` (#378) +- Required Go version updated from `1.13` to `1.20` (#378) ### Deprecated From c070b26c8853dd57723331516fbc9410f1cd3245 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 29 Feb 2024 10:34:54 +0300 Subject: [PATCH 535/605] pool: fix notify on remove a connection ConnectionPool.Remove() does not notify a ConnectionHandler on an instance removing from the pool. --- CHANGELOG.md | 3 ++ pool/connection_pool.go | 1 + pool/connection_pool_test.go | 71 ++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30aa750f2..aecdf37ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +- `ConnectionPool.Remove()` does not notify a `ConnectionHandler` after + an instance is already removed from the pool (#385) + ## [2.0.0] - 2024-02-12 There are a lot of changes in the new major version. The main ones: diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 9a79f9116..b4062c387 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -1374,6 +1374,7 @@ func (p *ConnectionPool) controller(ctx context.Context, e *endpoint) { // we need to start an another one for the shutdown. go func() { e.closeErr = e.conn.CloseGraceful() + p.handlerDeactivated(e.name, e.conn, e.role) close(e.closed) }() } else { diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index cadd83564..2592c7117 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -1166,6 +1166,77 @@ func TestConnectionHandlerUpdateError(t *testing.T) { require.Nilf(t, err, "failed to get ConnectedNow()") } +type testDeactivatedErrorHandler struct { + mut sync.Mutex + deactivated []string +} + +func (h *testDeactivatedErrorHandler) Discovered(name string, conn *tarantool.Connection, + role pool.Role) error { + return nil +} + +func (h *testDeactivatedErrorHandler) Deactivated(name string, conn *tarantool.Connection, + role pool.Role) error { + h.mut.Lock() + defer h.mut.Unlock() + + h.deactivated = append(h.deactivated, name) + return nil +} + +func TestConnectionHandlerDeactivated_on_remove(t *testing.T) { + poolServers := []string{servers[0], servers[1]} + poolInstances := makeInstances(poolServers, connOpts) + roles := []bool{false, false} + + err := test_helpers.SetClusterRO(makeDialers(poolServers), connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + + h := &testDeactivatedErrorHandler{} + poolOpts := pool.Opts{ + CheckTimeout: 100 * time.Microsecond, + ConnectionHandler: h, + } + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.ConnectWithOpts(ctx, poolInstances, poolOpts) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + defer connPool.Close() + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: pool.ANY, + Servers: servers, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + servers[0]: true, + servers[1]: true, + }, + } + err = test_helpers.CheckPoolStatuses(args) + require.Nil(t, err) + + for _, server := range poolServers { + connPool.Remove(server) + connPool.Remove(server) + } + + args = test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: pool.ANY, + Servers: servers, + ExpectedPoolStatus: false, + } + err = test_helpers.CheckPoolStatuses(args) + require.Nil(t, err) + + h.mut.Lock() + defer h.mut.Unlock() + require.ElementsMatch(t, poolServers, h.deactivated) +} + func TestRequestOnClosed(t *testing.T) { server1 := servers[0] server2 := servers[1] From dc1fe5dfddc95052523fb58e3a9fb849426592f2 Mon Sep 17 00:00:00 2001 From: DerekBum Date: Tue, 5 Mar 2024 15:29:05 +0300 Subject: [PATCH 536/605] pool: get instance status from `WatchOnce` request Starting from Tarantool version >= 3.0.0 `WatchOnce` requset is supported. So we can get instance status using this request instead of calling `box.info`. This way user can add instances to the ConnectionPool without the `execute` access. Closes #380 --- CHANGELOG.md | 3 ++ pool/config.lua | 3 ++ pool/connection_pool.go | 17 +++++++- pool/connection_pool_test.go | 85 ++++++++++++++++++++++++++++++++++++ tarantool_test.go | 8 +--- test_helpers/utils.go | 8 ++++ 6 files changed, 115 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aecdf37ea..24f4daacf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Changed +- `execute` access for `box.info` is no longer required for ConnectionPool + for a Tarantool version >= 3.0.0 (#380) + ### Fixed - `ConnectionPool.Remove()` does not notify a `ConnectionHandler` after diff --git a/pool/config.lua b/pool/config.lua index 4df392ba8..4e91a0d83 100644 --- a/pool/config.lua +++ b/pool/config.lua @@ -9,6 +9,9 @@ box.once("init", function() box.schema.user.create('test', { password = 'test' }) box.schema.user.grant('test', 'read,write,execute', 'universe') + box.schema.user.create('test_noexec', { password = 'test' }) + box.schema.user.grant('test_noexec', 'read,write', 'universe') + local s = box.schema.space.create('testPool', { id = 520, if_not_exists = true, diff --git a/pool/connection_pool.go b/pool/connection_pool.go index b4062c387..949b468bc 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -1017,7 +1017,20 @@ func (p *ConnectionPool) DoInstance(req tarantool.Request, name string) *taranto // func (p *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (Role, error) { - data, err := conn.Do(tarantool.NewCallRequest("box.info")).Get() + var ( + roFieldName string + data []interface{} + err error + ) + + if isFeatureInSlice(iproto.IPROTO_FEATURE_WATCH_ONCE, conn.ProtocolInfo().Features) { + roFieldName = "is_ro" + data, err = conn.Do(tarantool.NewWatchOnceRequest("box.status")).Get() + } else { + roFieldName = "ro" + data, err = conn.Do(tarantool.NewCallRequest("box.info")).Get() + } + if err != nil { return UnknownRole, err } @@ -1033,7 +1046,7 @@ func (p *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (Role, er return UnknownRole, ErrIncorrectStatus } - replicaRole, ok := data[0].(map[interface{}]interface{})["ro"] + replicaRole, ok := data[0].(map[interface{}]interface{})[roFieldName] if !ok { return UnknownRole, ErrIncorrectResponse } diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index 2592c7117..c4e43e2d3 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -1,6 +1,7 @@ package pool_test import ( + "bytes" "context" "fmt" "log" @@ -22,6 +23,7 @@ import ( ) var user = "test" +var userNoExec = "test_noexec" var pass = "test" var spaceNo = uint32(520) var spaceName = "testPool" @@ -68,6 +70,18 @@ func makeInstance(server string, opts tarantool.Opts) pool.Instance { } } +func makeNoExecuteInstance(server string, opts tarantool.Opts) pool.Instance { + return pool.Instance{ + Name: server, + Dialer: tarantool.NetDialer{ + Address: server, + User: userNoExec, + Password: pass, + }, + Opts: opts, + } +} + func makeInstances(servers []string, opts tarantool.Opts) []pool.Instance { var instances []pool.Instance for _, server := range servers { @@ -130,6 +144,77 @@ func TestConnSuccessfully(t *testing.T) { require.Nil(t, err) } +func TestConn_no_execute_supported(t *testing.T) { + test_helpers.SkipIfWatchOnceUnsupported(t) + + healthyServ := servers[0] + + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, + []pool.Instance{makeNoExecuteInstance(healthyServ, connOpts)}) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: pool.ANY, + Servers: []string{healthyServ}, + ExpectedPoolStatus: true, + ExpectedStatuses: map[string]bool{ + healthyServ: true, + }, + } + + err = test_helpers.CheckPoolStatuses(args) + require.Nil(t, err) + + _, err = connPool.Do(tarantool.NewPingRequest(), pool.ANY).Get() + require.Nil(t, err) +} + +func TestConn_no_execute_unsupported(t *testing.T) { + test_helpers.SkipIfWatchOnceSupported(t) + + var buf bytes.Buffer + log.SetOutput(&buf) + defer log.SetOutput(os.Stderr) + + healthyServ := servers[0] + + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + connPool, err := pool.Connect(ctx, + []pool.Instance{makeNoExecuteInstance(healthyServ, connOpts)}) + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + defer connPool.Close() + + require.Contains(t, buf.String(), + fmt.Sprintf("connect to %s failed: Execute access to function "+ + "'box.info' is denied for user '%s'", servers[0], userNoExec)) + + args := test_helpers.CheckStatusesArgs{ + ConnPool: connPool, + Mode: pool.ANY, + Servers: []string{healthyServ}, + ExpectedPoolStatus: false, + ExpectedStatuses: map[string]bool{ + healthyServ: false, + }, + } + + err = test_helpers.CheckPoolStatuses(args) + require.Nil(t, err) + + _, err = connPool.Do(tarantool.NewPingRequest(), pool.ANY).Get() + require.Error(t, err) + require.Equal(t, "can't find healthy instance in pool", err.Error()) +} + func TestConnect_empty(t *testing.T) { cases := []struct { Name string diff --git a/tarantool_test.go b/tarantool_test.go index 0bd3f394b..03b786f33 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2591,13 +2591,7 @@ func TestConnectionDoWatchOnceRequest(t *testing.T) { } func TestConnectionDoWatchOnceOnEmptyKey(t *testing.T) { - watchOnceNotSupported, err := test_helpers.IsTarantoolVersionLess(3, 0, 0) - if err != nil { - log.Fatalf("Could not check the Tarantool version: %s", err) - } - if watchOnceNotSupported { - return - } + test_helpers.SkipIfWatchOnceUnsupported(t) conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() diff --git a/test_helpers/utils.go b/test_helpers/utils.go index e962dc619..5dba76ff7 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -200,6 +200,14 @@ func SkipIfWatchOnceUnsupported(t *testing.T) { SkipIfFeatureUnsupported(t, "watch once", 3, 0, 0) } +// SkipIfWatchOnceSupported skips test run if Tarantool with WatchOnce +// request type is used. +func SkipIfWatchOnceSupported(t *testing.T) { + t.Helper() + + SkipIfFeatureSupported(t, "watch once", 3, 0, 0) +} + // SkipIfCrudSpliceBroken skips test run if splice operation is broken // on the crud side. // https://github.com/tarantool/crud/issues/397 From 95c7afc7b4e72e91144fe2c452b9f4a79325c43c Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 6 Mar 2024 08:35:52 +0300 Subject: [PATCH 537/605] Release v2.1.0 Overview The small release improves the ConnectionPool. The ConnectionPool is no longer required execute access for `box.info` from a user for Tarantool >= 3.0.0. Breaking changes There are no breaking changes in the release. New features `execute` access for `box.info` is no longer required for ConnectionPool for a Tarantool version >= 3.0.0 (#380). Bugfixes `ConnectionPool.Remove()` does not notify a `ConnectionHandler` after an instance is already removed from the pool (#385). --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24f4daacf..b15b46080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,15 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Changed +### Fixed + +## [2.1.0] - 2024-03-06 + +The small release improves the ConnectionPool. The ConnectionPool now does not +require execute access for `box.info` from a user for Tarantool >= 3.0.0. + +### Changed + - `execute` access for `box.info` is no longer required for ConnectionPool for a Tarantool version >= 3.0.0 (#380) From be0f71e4ede457e9bb7dd05fd4e8fa3db499cfbc Mon Sep 17 00:00:00 2001 From: Yaroslav Lobankov Date: Thu, 14 Mar 2024 17:13:41 +0400 Subject: [PATCH 538/605] ci: bump actions in reusable_testing.yml Bump version of the `actions/checkout` and `actions/download-artifact` actions to v4. Bump version of the `actions/setup-go` action to v5. It is needed for fixing the following GitHub warnings: Node.js 16 actions are deprecated. Please update the following actions to use Node.js 20 The following actions uses node12 which is deprecated and will be forced to run on node16 --- .github/workflows/reusable_testing.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index df60cd3f9..352653101 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -14,12 +14,12 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Clone the go-tarantool connector - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: ${{ github.repository_owner }}/go-tarantool - name: Download the tarantool build artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ${{ inputs.artifact_name }} @@ -34,7 +34,7 @@ jobs: echo "TNT_VERSION=$TNT_VERSION" >> $GITHUB_ENV - name: Setup golang for connector and tests - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: '1.20' From edde459335c196d28fc3e0dc8999237835859516 Mon Sep 17 00:00:00 2001 From: Max Maximov Date: Mon, 25 Mar 2024 18:30:13 +0300 Subject: [PATCH 539/605] pool: add log messages on connect error Add err log to ConnectionPool.Add() in case, when unable to establish connection and ctx is not canceled; also added logs for error case of ConnectionPool.tryConnect() calls in ConnectionPool.controller() and ConnectionPool.reconnect() Part of #389 --- CHANGELOG.md | 5 ++++- pool/connection_pool.go | 11 +++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b15b46080..a5cc700ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,10 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ## [Unreleased] ### Added - +- Add err log to `ConnectionPool.Add()` in case, when unable to establish + connection and ctx is not canceled; + also added logs for error case of `ConnectionPool.tryConnect()` calls in + `ConnectionPool.controller()` and `ConnectionPool.reconnect()` ### Changed ### Fixed diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 949b468bc..1c9d85f44 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -289,6 +289,8 @@ func (p *ConnectionPool) Add(ctx context.Context, instance Instance) error { e.cancel() close(e.closed) return err + } else { + log.Printf("tarantool: connect to %s failed: %s\n", instance.Name, err) } } @@ -1329,7 +1331,9 @@ func (p *ConnectionPool) reconnect(ctx context.Context, e *endpoint) { e.conn = nil e.role = UnknownRole - p.tryConnect(ctx, e) + if err := p.tryConnect(ctx, e); err != nil { + log.Printf("tarantool: reconnect to %s failed: %s\n", e.name, err) + } } func (p *ConnectionPool) controller(ctx context.Context, e *endpoint) { @@ -1417,7 +1421,10 @@ func (p *ConnectionPool) controller(ctx context.Context, e *endpoint) { // Relocate connection between subpools // if ro/rw was updated. if e.conn == nil { - p.tryConnect(ctx, e) + if err := p.tryConnect(ctx, e); err != nil { + log.Printf("tarantool: reopen connection to %s failed: %s\n", + e.name, err) + } } else if !e.conn.ClosedNow() { p.updateConnection(e) } else { From 8db4e215558275b89609f3260ae1c41bfc213061 Mon Sep 17 00:00:00 2001 From: Nikolay Shirokovskiy Date: Fri, 29 Mar 2024 19:21:23 +0300 Subject: [PATCH 540/605] mod: bump stretchr/testify to v1.9.0 In this version (latest currently) require.Subset supports maps correctly. Also it has the feature documented. --- go.mod | 6 +++--- go.sum | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 802b8412c..30381a8be 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,14 @@ go 1.20 require ( github.com/google/uuid v1.3.0 github.com/shopspring/decimal v1.3.1 - github.com/stretchr/testify v1.7.1 + github.com/stretchr/testify v1.9.0 github.com/tarantool/go-iproto v1.0.0 github.com/vmihailenco/msgpack/v5 v5.3.5 ) require ( - github.com/davecgh/go-spew v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ff942aa8f..bf5926673 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -10,6 +12,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tarantool/go-iproto v1.0.0 h1:quC4hdFhCuFYaCqOFgUxH2foRkhAy+TlEy7gQLhdVjw= github.com/tarantool/go-iproto v1.0.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= @@ -20,3 +24,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 3f44945b615ee85030b24f30d00476b85ad24eac Mon Sep 17 00:00:00 2001 From: Nikolay Shirokovskiy Date: Fri, 29 Mar 2024 18:26:50 +0300 Subject: [PATCH 541/605] test: use coverage instead of equality to test error payload We are going to add missing 'user' payload field for ACCESS_DENIED error which will break current tests. Let fix tests to allow adding new payload fields for errors. Need for https://github.com/tarantool/tarantool/issues/9108 --- test_helpers/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 5dba76ff7..049dff92d 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -244,7 +244,7 @@ func CheckEqualBoxErrors(t *testing.T, expected tarantool.BoxError, actual taran require.Equal(t, expected.Msg, actual.Msg) require.Equal(t, expected.Errno, actual.Errno) require.Equal(t, expected.Code, actual.Code) - require.Equal(t, expected.Fields, actual.Fields) + require.Subset(t, actual.Fields, expected.Fields) if expected.Prev != nil { // Stack depth is the same From dcd093403658c84e360f7269ee0873a230caf029 Mon Sep 17 00:00:00 2001 From: Nikolay Shirokovskiy Date: Wed, 3 Apr 2024 14:11:46 +0300 Subject: [PATCH 542/605] test: use coverage instead of equality to test error payload 2 We are going to add missing 'user' payload field for ACCESS_DENIED error which will break current tests. Let fix tests to allow adding new payload fields for errors. This is another place where we test for payload equality. Need for https://github.com/tarantool/tarantool/issues/9108 --- config.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/config.lua b/config.lua index 2553b94c8..f0bf05b65 100644 --- a/config.lua +++ b/config.lua @@ -248,8 +248,8 @@ if tarantool_version_at_least(2, 4, 1) then -- - file: val -- line: val - local function compare_box_error_attributes(expected, actual, attr_provider) - for attr, _ in pairs(attr_provider:unpack()) do + local function compare_box_error_attributes(expected, actual) + for attr, _ in pairs(expected:unpack()) do if (attr ~= 'prev') and (attr ~= 'trace') then if expected[attr] ~= actual[attr] then error(('%s expected %s is not equal to actual %s'):format( @@ -272,8 +272,7 @@ if tarantool_version_at_least(2, 4, 1) then expected.type, expected.message)) end - compare_box_error_attributes(expected, actual, expected) - compare_box_error_attributes(expected, actual, actual) + compare_box_error_attributes(expected, actual) if (expected.prev ~= nil) or (actual.prev ~= nil) then return compare_box_errors(expected.prev, actual.prev) From a7d9162002e0598abfa678f48df78956748ba993 Mon Sep 17 00:00:00 2001 From: Nikolay Shirokovskiy Date: Wed, 3 Apr 2024 16:49:45 +0300 Subject: [PATCH 543/605] test: fix compare_box_errors invocation The function signature is (expected, actual) so expected should be called first. It is significant as the function checks that all payload fields of expected are present in actual. The reverse inclusion is not true generally speaking. Need for https://github.com/tarantool/tarantool/issues/9108 --- box_error_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box_error_test.go b/box_error_test.go index c48303bba..43f3c6956 100644 --- a/box_error_test.go +++ b/box_error_test.go @@ -358,7 +358,7 @@ func TestErrorTypeInsert(t *testing.T) { local tuple_err = tuple[2] assert(tuple_err ~= nil) - return compare_box_errors(err, tuple_err) + return compare_box_errors(tuple_err, err) `, testcase.ttObj, space, testcase.tuple.pk) // In fact, compare_box_errors does not check than File and Line @@ -400,7 +400,7 @@ func TestErrorTypeInsertTyped(t *testing.T) { local tuple_err = tuple[2] assert(tuple_err ~= nil) - return compare_box_errors(err, tuple_err) + return compare_box_errors(tuple_err, err) `, testcase.ttObj, space, testcase.tuple.pk) // In fact, compare_box_errors does not check than File and Line From 8b2be0133e9d4105491a90dcccd2945dd1e0558d Mon Sep 17 00:00:00 2001 From: maxim-konovalov Date: Wed, 1 May 2024 12:39:32 +0300 Subject: [PATCH 544/605] pool: add missing methods to the pooler interface I added methods that are implemented but not included in the pooler interface. --- CHANGELOG.md | 2 ++ pool/pooler.go | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5cc700ea..4ef3cb4e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. connection and ctx is not canceled; also added logs for error case of `ConnectionPool.tryConnect()` calls in `ConnectionPool.controller()` and `ConnectionPool.reconnect()` +- Methods that are implemented but not included in the pooler interface (#395). + ### Changed ### Fixed diff --git a/pool/pooler.go b/pool/pooler.go index 975162d37..d4c0f1b5e 100644 --- a/pool/pooler.go +++ b/pool/pooler.go @@ -1,15 +1,28 @@ package pool import ( + "context" "time" "github.com/tarantool/go-tarantool/v2" ) +// TopologyEditor is the interface that must be implemented by a connection pool. +// It describes edit topology methods. +type TopologyEditor interface { + Add(ctx context.Context, instance Instance) error + Remove(name string) error +} + // Pooler is the interface that must be implemented by a connection pool. type Pooler interface { + TopologyEditor + ConnectedNow(mode Mode) (bool, error) Close() []error + // CloseGraceful closes connections in the ConnectionPool gracefully. It waits + // for all requests to complete. + CloseGraceful() []error ConfiguredTimeout(mode Mode) (time.Duration, error) NewPrepared(expr string, mode Mode) (*tarantool.Prepared, error) NewStream(mode Mode) (*tarantool.Stream, error) From 9bb5124240419c7d9339fa80f1b4bc6b07f96ce4 Mon Sep 17 00:00:00 2001 From: Andrey Saranchin Date: Tue, 6 Aug 2024 12:06:39 +0300 Subject: [PATCH 545/605] test: install crud from master Currently `make deps`, which is used for tests, installs specified version of `crud`. Such approach complicates integration with Tarantool: when a patch in Tarantool breaks integration with both `crud` and `go-tarantool` (because it uses `crud`), one needs to fix `crud`, then release it and then bump it to the new version in makefile of `go-tarantool`. Let's simplify this process - simply install `crud` from its master for tests. Note that such approach has a drawback - any commit in `crud` can break integration with `go-tarantool`. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3a165ef82..ce3505afc 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ clean: deps: clean @(command -v tt > /dev/null || (echo "error: tt not found" && exit 1)) ( cd ./queue/testdata; tt rocks install queue 1.3.0 ) - ( cd ./crud/testdata; tt rocks install crud 1.4.1 ) + ( cd ./crud/testdata; tt rocks install crud ) .PHONY: datetime-timezones datetime-timezones: From f1d88dcd1108799cf26d952df921119fb06d5e2c Mon Sep 17 00:00:00 2001 From: Andrey Saranchin Date: Tue, 6 Aug 2024 16:04:21 +0300 Subject: [PATCH 546/605] ci: replace macos-11 runners with macos-14 Since GitHub does not support `macos-11` runners anymore, jobs using these runners just hang. Let's replace them with the most actual `macos-14` runners. Now we test the project against 12 and 14 versions: we could fill the gap, but let's not create new runners without actual need. Tarantool 1.10 fails to build on `macos-14` runner, so this combination is excluded from testing matrix. Closes #402 --- .github/workflows/testing.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 536f8609d..17fe6370c 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -136,11 +136,14 @@ jobs: - '1.20' - 'stable' runs-on: - - macos-11 - macos-12 + - macos-14 tarantool: - brew - 1.10.15 + exclude: + - runs-on: macos-14 + tarantool: 1.10.15 env: # Make sense only for non-brew jobs. From 0ccdee28735fa907a7934b66678eeb1783b4fdb0 Mon Sep 17 00:00:00 2001 From: Maksim Konovalov Date: Mon, 9 Sep 2024 12:40:35 +0300 Subject: [PATCH 547/605] pool: implemented Role Stringer interface implemented Role Stringer interface for human-readable printing --- CHANGELOG.md | 1 + pool/const.go | 10 +++++++--- pool/const_test.go | 13 +++++++++++++ pool/role_string.go | 25 +++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 pool/const_test.go create mode 100644 pool/role_string.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ef3cb4e0..aa0bfc0e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. also added logs for error case of `ConnectionPool.tryConnect()` calls in `ConnectionPool.controller()` and `ConnectionPool.reconnect()` - Methods that are implemented but not included in the pooler interface (#395). +- Implemented stringer methods for pool.Role (#405). ### Changed diff --git a/pool/const.go b/pool/const.go index 7ec239f7d..b1748ae52 100644 --- a/pool/const.go +++ b/pool/const.go @@ -1,3 +1,4 @@ +//go:generate stringer -type Role -linecomment package pool /* @@ -31,7 +32,10 @@ const ( type Role uint32 const ( - UnknownRole Role = iota // A connection pool failed to discover mode of the instance. - MasterRole // The instance is read-write mode. - ReplicaRole // The instance is in read-only mode. + // UnknownRole - the connection pool was unable to detect the instance mode. + UnknownRole Role = iota // unknown + // MasterRole - the instance is in read-write mode. + MasterRole // master + // ReplicaRole - the instance is in read-only mode. + ReplicaRole // replica ) diff --git a/pool/const_test.go b/pool/const_test.go new file mode 100644 index 000000000..83e2678de --- /dev/null +++ b/pool/const_test.go @@ -0,0 +1,13 @@ +package pool + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRole_String(t *testing.T) { + require.Equal(t, "unknown", UnknownRole.String()) + require.Equal(t, "master", MasterRole.String()) + require.Equal(t, "replica", ReplicaRole.String()) +} diff --git a/pool/role_string.go b/pool/role_string.go new file mode 100644 index 000000000..162ebd698 --- /dev/null +++ b/pool/role_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type Role -linecomment"; DO NOT EDIT. + +package pool + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[UnknownRole-0] + _ = x[MasterRole-1] + _ = x[ReplicaRole-2] +} + +const _Role_name = "unknownmasterreplica" + +var _Role_index = [...]uint8{0, 7, 13, 20} + +func (i Role) String() string { + if i >= Role(len(_Role_index)-1) { + return "Role(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Role_name[_Role_index[i]:_Role_index[i+1]] +} From 592db69eed8649b82ce432b930c27daeee98c52f Mon Sep 17 00:00:00 2001 From: Nurzhan Saktaganov Date: Thu, 19 Sep 2024 20:11:43 +0300 Subject: [PATCH 548/605] api: more informative request canceling Log the probable reason for unexpected requestId. Add requestId info to context done error message. --- CHANGELOG.md | 3 +++ connection.go | 9 +++++---- example_test.go | 5 +++-- tarantool_test.go | 9 ++++++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa0bfc0e1..a2f9983e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Changed +- More informative request canceling: log the probable reason for unexpected request ID + and add request ID info to context done error message (#407). + ### Fixed ## [2.1.0] - 2024-03-06 diff --git a/connection.go b/connection.go index 53e6b874c..7c8e1294d 100644 --- a/connection.go +++ b/connection.go @@ -100,7 +100,8 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac conn.Addr(), err) case LogUnexpectedResultId: header := v[0].(Header) - log.Printf("tarantool: connection %s got unexpected resultId (%d) in response", + log.Printf("tarantool: connection %s got unexpected request ID (%d) in response "+ + "(probably cancelled request)", conn.Addr(), header.RequestId) case LogWatchEventReadFailed: err := v[0].(error) @@ -940,7 +941,7 @@ func (conn *Connection) newFuture(req Request) (fut *Future) { if ctx != nil { select { case <-ctx.Done(): - fut.SetError(fmt.Errorf("context is done")) + fut.SetError(fmt.Errorf("context is done (request ID %d)", fut.requestId)) shard.rmut.Unlock() return default: @@ -982,7 +983,7 @@ func (conn *Connection) contextWatchdog(fut *Future, ctx context.Context) { case <-fut.done: return default: - conn.cancelFuture(fut, fmt.Errorf("context is done")) + conn.cancelFuture(fut, fmt.Errorf("context is done (request ID %d)", fut.requestId)) } } @@ -1008,7 +1009,7 @@ func (conn *Connection) send(req Request, streamId uint64) *Future { if req.Ctx() != nil { select { case <-req.Ctx().Done(): - conn.cancelFuture(fut, fmt.Errorf("context is done")) + conn.cancelFuture(fut, fmt.Errorf("context is done (request ID %d)", fut.requestId)) return fut default: } diff --git a/example_test.go b/example_test.go index ec07b657b..d4480cea4 100644 --- a/example_test.go +++ b/example_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "regexp" "time" "github.com/tarantool/go-iproto" @@ -163,10 +164,10 @@ func ExamplePingRequest_Context() { // Ping a Tarantool instance to check connection. data, err := conn.Do(req).Get() fmt.Println("Ping Resp data", data) - fmt.Println("Ping Error", err) + fmt.Println("Ping Error", regexp.MustCompile("[0-9]+").ReplaceAllString(err.Error(), "N")) // Output: // Ping Resp data [] - // Ping Error context is done + // Ping Error context is done (request ID N) } func ExampleSelectRequest() { diff --git a/tarantool_test.go b/tarantool_test.go index 03b786f33..98f04a045 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -11,6 +11,7 @@ import ( "os/exec" "path/filepath" "reflect" + "regexp" "runtime" "strings" "sync" @@ -47,6 +48,8 @@ type Member struct { Val uint } +var contextDoneErrRegexp = regexp.MustCompile(`^context is done \(request ID [0-9]+\)$`) + func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeArrayLen(2); err != nil { return err @@ -2731,7 +2734,7 @@ func TestClientRequestObjectsWithPassedCanceledContext(t *testing.T) { req := NewPingRequest().Context(ctx) cancel() resp, err := conn.Do(req).Get() - if err.Error() != "context is done" { + if !contextDoneErrRegexp.Match([]byte(err.Error())) { t.Fatalf("Failed to catch an error from done context") } if resp != nil { @@ -2802,7 +2805,7 @@ func TestClientRequestObjectsWithContext(t *testing.T) { if err == nil { t.Fatalf("caught nil error") } - if err.Error() != "context is done" { + if !contextDoneErrRegexp.Match([]byte(err.Error())) { t.Fatalf("wrong error caught: %v", err) } } @@ -3295,7 +3298,7 @@ func TestClientIdRequestObjectWithPassedCanceledContext(t *testing.T) { resp, err := conn.Do(req).Get() require.Nilf(t, resp, "Response is empty") require.NotNilf(t, err, "Error is not empty") - require.Equal(t, err.Error(), "context is done") + require.Regexp(t, contextDoneErrRegexp, err.Error()) } func TestConnectionProtocolInfoUnsupported(t *testing.T) { From d2ff87cd8be089f0dc94e574e3de5421eb0c5d54 Mon Sep 17 00:00:00 2001 From: Dmitriy Gertsog Date: Tue, 8 Oct 2024 16:52:10 +0300 Subject: [PATCH 549/605] ci: update lint rule for long lines --- .golangci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.golangci.yaml b/.golangci.yaml index 32f87cb0d..7fedeac31 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -27,4 +27,4 @@ issues: exclude-rules: - linters: - lll - source: "\t?// *(see )?https://" + source: "^\\s*//\\s*(\\S+\\s){0,3}https?://\\S+$" From 8d1494eae72cc3490393cd38fa7d347d575d0cbb Mon Sep 17 00:00:00 2001 From: Dmitriy Gertsog Date: Tue, 8 Oct 2024 17:12:50 +0300 Subject: [PATCH 550/605] test: Allow use environment variable TARANTOOL_BIN Environment variable TARANTOOL_BIN can be used to specify path of Tarantool executable. --- test_helpers/main.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test_helpers/main.go b/test_helpers/main.go index 178f69921..1ced629f3 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -120,13 +120,20 @@ func atoiUint64(str string) (uint64, error) { return res, nil } +func getTarantoolExec() string { + if tar_bin := os.Getenv("TARANTOOL_BIN"); tar_bin != "" { + return tar_bin + } + return "tarantool" +} + // IsTarantoolVersionLess checks if tarantool version is less // than passed . Returns error if failed // to extract version. func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) (bool, error) { var major, minor, patch uint64 - out, err := exec.Command("tarantool", "--version").Output() + out, err := exec.Command(getTarantoolExec(), "--version").Output() if err != nil { return true, err @@ -203,7 +210,7 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { } } - inst.Cmd = exec.Command("tarantool", startOpts.InitScript) + inst.Cmd = exec.Command(getTarantoolExec(), startOpts.InitScript) inst.Cmd.Env = append( os.Environ(), From cbc81509a9feaa10968669ab1d80bbdf4643c00f Mon Sep 17 00:00:00 2001 From: Dmitriy Gertsog Date: Wed, 9 Oct 2024 12:38:44 +0300 Subject: [PATCH 551/605] test: locking the Ubuntu version as 22.04 to run --- .github/workflows/check.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 52675cfbd..02d561c9b 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -6,7 +6,7 @@ on: jobs: luacheck: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 if: | github.event_name == 'push' || github.event_name == 'pull_request' && @@ -32,7 +32,7 @@ jobs: run: ./.rocks/bin/luacheck . golangci-lint: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 if: | github.event_name == 'push' || github.event_name == 'pull_request' && @@ -57,7 +57,7 @@ jobs: args: --out-${NO_FUTURE}format colored-line-number --config=.golangci.yaml codespell: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 if: | github.event_name == 'push' || github.event_name == 'pull_request' && From d4b5d04d45b12e7f77461468e01f7830e9269e94 Mon Sep 17 00:00:00 2001 From: Dmitriy Gertsog Date: Wed, 2 Oct 2024 17:45:02 +0300 Subject: [PATCH 552/605] api: add support of a batch insert request Add support the IPROTO_INSERT_ARROW request and message pack type MP_ARROW. Closes #399 --- CHANGELOG.md | 7 +- arrow/arrow.go | 56 ++++++++++++ arrow/arrow_test.go | 100 ++++++++++++++++++++++ arrow/example_test.go | 61 +++++++++++++ arrow/request.go | 93 ++++++++++++++++++++ arrow/request_test.go | 146 ++++++++++++++++++++++++++++++++ arrow/tarantool_test.go | 120 ++++++++++++++++++++++++++ arrow/testdata/config-memcs.lua | 31 +++++++ arrow/testdata/config-memtx.lua | 35 ++++++++ request.go | 23 ++--- request_test.go | 83 +++++++++++++++++- response.go | 6 ++ response_test.go | 55 ++++++++++++ watch.go | 6 +- 14 files changed, 802 insertions(+), 20 deletions(-) create mode 100644 arrow/arrow.go create mode 100644 arrow/arrow_test.go create mode 100644 arrow/example_test.go create mode 100644 arrow/request.go create mode 100644 arrow/request_test.go create mode 100644 arrow/tarantool_test.go create mode 100644 arrow/testdata/config-memcs.lua create mode 100644 arrow/testdata/config-memtx.lua create mode 100644 response_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a2f9983e2..b0eb0f9c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,13 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ## [Unreleased] ### Added -- Add err log to `ConnectionPool.Add()` in case, when unable to establish - connection and ctx is not canceled; - also added logs for error case of `ConnectionPool.tryConnect()` calls in +- Add err log to `ConnectionPool.Add()` in case, when unable to establish + connection and ctx is not canceled; + also added logs for error case of `ConnectionPool.tryConnect()` calls in `ConnectionPool.controller()` and `ConnectionPool.reconnect()` - Methods that are implemented but not included in the pooler interface (#395). - Implemented stringer methods for pool.Role (#405). +- Support the IPROTO_INSERT_ARROW request (#399). ### Changed diff --git a/arrow/arrow.go b/arrow/arrow.go new file mode 100644 index 000000000..aaeaccca9 --- /dev/null +++ b/arrow/arrow.go @@ -0,0 +1,56 @@ +package arrow + +import ( + "fmt" + "reflect" + + "github.com/vmihailenco/msgpack/v5" +) + +// Arrow MessagePack extension type. +const arrowExtId = 8 + +// Arrow struct wraps a raw arrow data buffer. +type Arrow struct { + data []byte +} + +// MakeArrow returns a new arrow.Arrow object that contains +// wrapped a raw arrow data buffer. +func MakeArrow(arrow []byte) (Arrow, error) { + return Arrow{arrow}, nil +} + +// Raw returns a []byte that contains Arrow raw data. +func (a Arrow) Raw() []byte { + return a.data +} + +func arrowDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { + arrow := Arrow{ + data: make([]byte, extLen), + } + n, err := d.Buffered().Read(arrow.data) + if err != nil { + return fmt.Errorf("arrowDecoder: can't read bytes on Arrow decode: %w", err) + } + if n < extLen || n != len(arrow.data) { + return fmt.Errorf("arrowDecoder: unexpected end of stream after %d Arrow bytes", n) + } + + v.Set(reflect.ValueOf(arrow)) + return nil +} + +func arrowEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { + arr, ok := v.Interface().(Arrow) + if !ok { + return []byte{}, fmt.Errorf("arrowEncoder: not an Arrow type") + } + return arr.data, nil +} + +func init() { + msgpack.RegisterExtDecoder(arrowExtId, Arrow{}, arrowDecoder) + msgpack.RegisterExtEncoder(arrowExtId, Arrow{}, arrowEncoder) +} diff --git a/arrow/arrow_test.go b/arrow/arrow_test.go new file mode 100644 index 000000000..5e8b440c4 --- /dev/null +++ b/arrow/arrow_test.go @@ -0,0 +1,100 @@ +package arrow_test + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/v2/arrow" + "github.com/vmihailenco/msgpack/v5" +) + +var longArrow, _ = hex.DecodeString("ffffffff70000000040000009effffff0400010004000000" + + "b6ffffff0c00000004000000000000000100000004000000daffffff140000000202" + + "000004000000f0ffffff4000000001000000610000000600080004000c0010000400" + + "080009000c000c000c0000000400000008000a000c00040006000800ffffffff8800" + + "0000040000008affffff0400030010000000080000000000000000000000acffffff" + + "01000000000000003400000008000000000000000200000000000000000000000000" + + "00000000000000000000000000000800000000000000000000000100000001000000" + + "0000000000000000000000000a00140004000c0010000c0014000400060008000c00" + + "00000000000000000000") + +var tests = []struct { + name string + arr []byte + enc []byte +}{ + { + "abc", + []byte{'a', 'b', 'c'}, + []byte{0xc7, 0x3, 0x8, 'a', 'b', 'c'}, + }, + { + "empty", + []byte{}, + []byte{0xc7, 0x0, 0x8}, + }, + { + "one", + []byte{1}, + []byte{0xd4, 0x8, 0x1}, + }, + { + "long", + longArrow, + []byte{ + 0xc8, 0x1, 0x10, 0x8, 0xff, 0xff, 0xff, 0xff, 0x70, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, + 0x0, 0x9e, 0xff, 0xff, 0xff, 0x4, 0x0, 0x1, 0x0, 0x4, 0x0, 0x0, 0x0, 0xb6, 0xff, 0xff, + 0xff, 0xc, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x4, 0x0, 0x0, 0x0, 0xda, 0xff, 0xff, 0xff, 0x14, 0x0, 0x0, 0x0, 0x2, 0x2, 0x0, 0x0, + 0x4, 0x0, 0x0, 0x0, 0xf0, 0xff, 0xff, 0xff, 0x40, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x61, 0x0, 0x0, 0x0, 0x6, 0x0, 0x8, 0x0, 0x4, 0x0, 0xc, 0x0, 0x10, 0x0, 0x4, 0x0, 0x8, + 0x0, 0x9, 0x0, 0xc, 0x0, 0xc, 0x0, 0xc, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x8, 0x0, + 0xa, 0x0, 0xc, 0x0, 0x4, 0x0, 0x6, 0x0, 0x8, 0x0, 0xff, 0xff, 0xff, 0xff, 0x88, 0x0, + 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x8a, 0xff, 0xff, 0xff, 0x4, 0x0, 0x3, 0x0, 0x10, 0x0, + 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xac, 0xff, 0xff, + 0xff, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x14, 0x0, + 0x4, 0x0, 0xc, 0x0, 0x10, 0x0, 0xc, 0x0, 0x14, 0x0, 0x4, 0x0, 0x6, 0x0, 0x8, 0x0, 0xc, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }, + }, +} + +func TestEncodeArrow(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(buf) + + arr, err := arrow.MakeArrow(tt.arr) + require.NoError(t, err) + + err = enc.Encode(arr) + require.NoError(t, err) + + require.Equal(t, tt.enc, buf.Bytes()) + }) + + } +} + +func TestDecodeArrow(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + buf := bytes.NewBuffer(tt.enc) + dec := msgpack.NewDecoder(buf) + + var arr arrow.Arrow + err := dec.Decode(&arr) + require.NoError(t, err) + + require.Equal(t, tt.arr, arr.Raw()) + }) + } +} diff --git a/arrow/example_test.go b/arrow/example_test.go new file mode 100644 index 000000000..e85d195cd --- /dev/null +++ b/arrow/example_test.go @@ -0,0 +1,61 @@ +// Run Tarantool Enterprise Edition instance before example execution: +// +// Terminal 1: +// $ cd arrow +// $ TEST_TNT_WORK_DIR=$(mktemp -d -t 'tarantool.XXX') tarantool testdata/config-memcs.lua +// +// Terminal 2: +// $ go test -v example_test.go +package arrow_test + +import ( + "context" + "encoding/hex" + "fmt" + "log" + "time" + + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/arrow" +) + +var arrowBinData, _ = hex.DecodeString("ffffffff70000000040000009effffff0400010004000000" + + "b6ffffff0c00000004000000000000000100000004000000daffffff140000000202" + + "000004000000f0ffffff4000000001000000610000000600080004000c0010000400" + + "080009000c000c000c0000000400000008000a000c00040006000800ffffffff8800" + + "0000040000008affffff0400030010000000080000000000000000000000acffffff" + + "01000000000000003400000008000000000000000200000000000000000000000000" + + "00000000000000000000000000000800000000000000000000000100000001000000" + + "0000000000000000000000000a00140004000c0010000c0014000400060008000c00" + + "00000000000000000000") + +func Example() { + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + } + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + cancel() + if err != nil { + log.Fatalf("Failed to connect: %s", err) + } + + arr, err := arrow.MakeArrow(arrowBinData) + if err != nil { + log.Fatalf("Failed prepare Arrow data: %s", err) + } + + req := arrow.NewInsertRequest("testArrow", arr) + + resp, err := client.Do(req).Get() + if err != nil { + log.Fatalf("Failed insert Arrow: %s", err) + } + if len(resp) > 0 { + log.Fatalf("Unexpected response") + } else { + fmt.Printf("Batch arrow inserted") + } +} diff --git a/arrow/request.go b/arrow/request.go new file mode 100644 index 000000000..2b6a9e295 --- /dev/null +++ b/arrow/request.go @@ -0,0 +1,93 @@ +package arrow + +import ( + "context" + "io" + + "github.com/tarantool/go-iproto" + "github.com/tarantool/go-tarantool/v2" + "github.com/vmihailenco/msgpack/v5" +) + +// INSERT Arrow request. +// +// FIXME: replace with iproto.IPROTO_INSERT_ARROW when iproto will released. +// https://github.com/tarantool/go-tarantool/issues/412 +const iprotoInsertArrowType = iproto.Type(17) + +// The data in Arrow format. +// +// FIXME: replace with iproto.IPROTO_ARROW when iproto will released. +// https://github.com/tarantool/go-tarantool/issues/412 +const iprotoArrowKey = iproto.Key(0x36) + +// InsertRequest helps you to create an insert request object for execution +// by a Connection. +type InsertRequest struct { + arrow Arrow + space interface{} + ctx context.Context +} + +// NewInsertRequest returns a new InsertRequest. +func NewInsertRequest(space interface{}, arrow Arrow) *InsertRequest { + return &InsertRequest{ + space: space, + arrow: arrow, + } +} + +// Type returns a IPROTO_INSERT_ARROW type for the request. +func (r *InsertRequest) Type() iproto.Type { + return iprotoInsertArrowType +} + +// Async returns false to the request return a response. +func (r *InsertRequest) Async() bool { + return false +} + +// Ctx returns a context of the request. +func (r *InsertRequest) Ctx() context.Context { + return r.ctx +} + +// Context sets a passed context to the request. +// +// Pay attention that when using context with request objects, +// the timeout option for Connection does not affect the lifetime +// of the request. For those purposes use context.WithTimeout() as +// the root context. +func (r *InsertRequest) Context(ctx context.Context) *InsertRequest { + r.ctx = ctx + return r +} + +// Arrow sets the arrow for insertion the insert arrow request. +// Note: default value is nil. +func (r *InsertRequest) Arrow(arrow Arrow) *InsertRequest { + r.arrow = arrow + return r +} + +// Body fills an msgpack.Encoder with the insert arrow request body. +func (r *InsertRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { + if err := enc.EncodeMapLen(2); err != nil { + return err + } + if err := tarantool.EncodeSpace(res, enc, r.space); err != nil { + return err + } + if err := enc.EncodeUint(uint64(iprotoArrowKey)); err != nil { + return err + } + return enc.Encode(r.arrow) +} + +// Response creates a response for the InsertRequest. +func (r *InsertRequest) Response( + header tarantool.Header, + body io.Reader, +) (tarantool.Response, error) { + return tarantool.DecodeBaseResponse(header, body) +} diff --git a/arrow/request_test.go b/arrow/request_test.go new file mode 100644 index 000000000..5cddaefb9 --- /dev/null +++ b/arrow/request_test.go @@ -0,0 +1,146 @@ +package arrow_test + +import ( + "bytes" + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/arrow" + "github.com/vmihailenco/msgpack/v5" +) + +// INSERT Arrow request. +// +// FIXME: replace with iproto.IPROTO_INSERT_ARROW when iproto will released. +// https://github.com/tarantool/go-tarantool/issues/412 +const iprotoInsertArrowType = iproto.Type(17) + +const validSpace uint32 = 1 // Any valid value != default. + +func TestInsertRequestType(t *testing.T) { + request := arrow.NewInsertRequest(validSpace, arrow.Arrow{}) + require.Equal(t, iprotoInsertArrowType, request.Type()) +} + +func TestInsertRequestAsync(t *testing.T) { + request := arrow.NewInsertRequest(validSpace, arrow.Arrow{}) + require.Equal(t, false, request.Async()) +} + +func TestInsertRequestCtx_default(t *testing.T) { + request := arrow.NewInsertRequest(validSpace, arrow.Arrow{}) + require.Equal(t, nil, request.Ctx()) +} + +func TestInsertRequestCtx_setter(t *testing.T) { + ctx := context.Background() + request := arrow.NewInsertRequest(validSpace, arrow.Arrow{}).Context(ctx) + require.Equal(t, ctx, request.Ctx()) +} + +func TestResponseDecode(t *testing.T) { + header := tarantool.Header{} + buf := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(buf) + + enc.EncodeMapLen(1) + enc.EncodeUint8(uint8(iproto.IPROTO_DATA)) + enc.Encode([]interface{}{'v', '2'}) + + request := arrow.NewInsertRequest(validSpace, arrow.Arrow{}) + resp, err := request.Response(header, bytes.NewBuffer(buf.Bytes())) + require.NoError(t, err) + require.Equal(t, header, resp.Header()) + + decodedInterface, err := resp.Decode() + require.NoError(t, err) + require.Equal(t, []interface{}{'v', '2'}, decodedInterface) +} + +func TestResponseDecodeTyped(t *testing.T) { + header := tarantool.Header{} + buf := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(buf) + + enc.EncodeMapLen(1) + enc.EncodeUint8(uint8(iproto.IPROTO_DATA)) + enc.EncodeBytes([]byte{'v', '2'}) + + request := arrow.NewInsertRequest(validSpace, arrow.Arrow{}) + resp, err := request.Response(header, bytes.NewBuffer(buf.Bytes())) + require.NoError(t, err) + require.Equal(t, header, resp.Header()) + + var decoded []byte + err = resp.DecodeTyped(&decoded) + require.NoError(t, err) + require.Equal(t, []byte{'v', '2'}, decoded) +} + +type stubSchemeResolver struct { + space interface{} +} + +func (r stubSchemeResolver) ResolveSpace(s interface{}) (uint32, error) { + if id, ok := r.space.(uint32); ok { + return id, nil + } + if _, ok := r.space.(string); ok { + return 0, nil + } + return 0, fmt.Errorf("stub error message: %v", r.space) +} + +func (stubSchemeResolver) ResolveIndex(i interface{}, spaceNo uint32) (uint32, error) { + return 0, nil +} + +func (r stubSchemeResolver) NamesUseSupported() bool { + _, ok := r.space.(string) + return ok +} + +func TestInsertRequestDefaultValues(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(buf) + + resolver := stubSchemeResolver{validSpace} + req := arrow.NewInsertRequest(resolver.space, arrow.Arrow{}) + err := req.Body(&resolver, enc) + require.NoError(t, err) + + require.Equal(t, []byte{0x82, 0x10, 0x1, 0x36, 0xc7, 0x0, 0x8}, buf.Bytes()) +} + +func TestInsertRequestSpaceByName(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(buf) + + resolver := stubSchemeResolver{"valid"} + req := arrow.NewInsertRequest(resolver.space, arrow.Arrow{}) + err := req.Body(&resolver, enc) + require.NoError(t, err) + + require.Equal(t, + []byte{0x82, 0x5e, 0xa5, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x36, 0xc7, 0x0, 0x8}, + buf.Bytes()) +} + +func TestInsertRequestSetters(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(buf) + + arr, err := arrow.MakeArrow([]byte{'a', 'b', 'c'}) + require.NoError(t, err) + + resolver := stubSchemeResolver{validSpace} + req := arrow.NewInsertRequest(resolver.space, arr) + err = req.Body(&resolver, enc) + require.NoError(t, err) + + require.Equal(t, []byte{0x82, 0x10, 0x1, 0x36, 0xc7, 0x3, 0x8, 'a', 'b', 'c'}, buf.Bytes()) +} diff --git a/arrow/tarantool_test.go b/arrow/tarantool_test.go new file mode 100644 index 000000000..2ff29fc8e --- /dev/null +++ b/arrow/tarantool_test.go @@ -0,0 +1,120 @@ +package arrow_test + +import ( + "encoding/hex" + "log" + "os" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/arrow" + "github.com/tarantool/go-tarantool/v2/test_helpers" +) + +var isArrowSupported = false + +var server = "127.0.0.1:3013" +var dialer = tarantool.NetDialer{ + Address: server, + User: "test", + Password: "test", +} +var space = "testArrow" + +var opts = tarantool.Opts{ + Timeout: 5 * time.Second, +} + +// TestInsert uses Arrow sequence from Tarantool's test. +// See: https://github.com/tarantool/tarantool/blob/d628b71bc537a75b69c253f45ec790462cf1a5cd/test/box-luatest/gh_10508_iproto_insert_arrow_test.lua#L56 +func TestInsert_invalid(t *testing.T) { + arrows := []struct { + arrow string + expected string + }{ + { + "", + "Failed to decode Arrow IPC data", + }, + { + "00", + "Failed to decode Arrow IPC data", + }, + { + "ffffffff70000000040000009effffff0400010004000000" + + "b6ffffff0c00000004000000000000000100000004000000daffffff140000000202" + + "000004000000f0ffffff4000000001000000610000000600080004000c0010000400" + + "080009000c000c000c0000000400000008000a000c00040006000800ffffffff8800" + + "0000040000008affffff0400030010000000080000000000000000000000acffffff" + + "01000000000000003400000008000000000000000200000000000000000000000000" + + "00000000000000000000000000000800000000000000000000000100000001000000" + + "0000000000000000000000000a00140004000c0010000c0014000400060008000c00" + + "00000000000000000000", + "memtx does not support arrow format", + }, + } + + conn := test_helpers.ConnectWithValidation(t, dialer, opts) + defer conn.Close() + + for i, a := range arrows { + t.Run(strconv.Itoa(i), func(t *testing.T) { + data, err := hex.DecodeString(a.arrow) + require.NoError(t, err) + + arr, err := arrow.MakeArrow(data) + if err != nil { + require.ErrorContains(t, err, a.expected) + return + } + req := arrow.NewInsertRequest(space, arr) + + _, err = conn.Do(req).Get() + require.ErrorContains(t, err, a.expected) + }) + } + +} + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + isLess, err := test_helpers.IsTarantoolVersionLess(3, 3, 0) + if err != nil { + log.Fatalf("Failed to extract Tarantool version: %s", err) + } + isArrowSupported = !isLess + + if !isArrowSupported { + log.Println("Skipping insert Arrow tests...") + return 0 + } + + instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + Dialer: dialer, + InitScript: "testdata/config-memtx.lua", + Listen: server, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 10, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(instance) + + if err != nil { + log.Printf("Failed to prepare test Tarantool: %s", err) + return 1 + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/arrow/testdata/config-memcs.lua b/arrow/testdata/config-memcs.lua new file mode 100644 index 000000000..ae1cc430e --- /dev/null +++ b/arrow/testdata/config-memcs.lua @@ -0,0 +1,31 @@ +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +box.cfg { + work_dir = os.getenv("TEST_TNT_WORK_DIR") +} + +box.schema.user.create('test', { + password = 'test', + if_not_exists = true +}) +box.schema.user.grant('test', 'execute', 'universe', nil, { + if_not_exists = true +}) + +local s = box.schema.space.create('testArrow', { + engine = 'memcs', + field_count = 1, + format = {{'a', 'uint64'}}, + if_not_exists = true +}) +s:create_index('primary') +s:truncate() + +box.schema.user.grant('test', 'read,write', 'space', 'testArrow', { + if_not_exists = true +}) + +-- Set listen only when every other thing is configured. +box.cfg { + listen = 3013 +} diff --git a/arrow/testdata/config-memtx.lua b/arrow/testdata/config-memtx.lua new file mode 100644 index 000000000..92c0af096 --- /dev/null +++ b/arrow/testdata/config-memtx.lua @@ -0,0 +1,35 @@ +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +box.cfg { + work_dir = os.getenv("TEST_TNT_WORK_DIR") +} + +box.schema.user.create('test', { + password = 'test', + if_not_exists = true +}) +box.schema.user.grant('test', 'execute', 'universe', nil, { + if_not_exists = true +}) + +local s = box.schema.space.create('testArrow', { + if_not_exists = true +}) +s:create_index('primary', { + type = 'tree', + parts = {{ + field = 1, + type = 'integer' + }}, + if_not_exists = true +}) +s:truncate() + +box.schema.user.grant('test', 'read,write', 'space', 'testArrow', { + if_not_exists = true +}) + +-- Set listen only when every other thing is configured. +box.cfg { + listen = os.getenv("TEST_TNT_LISTEN") +} diff --git a/request.go b/request.go index 8dbe250b5..21ed0eba1 100644 --- a/request.go +++ b/request.go @@ -855,11 +855,7 @@ func (req *baseRequest) Ctx() context.Context { // Response creates a response for the baseRequest. func (req *baseRequest) Response(header Header, body io.Reader) (Response, error) { - resp, err := createBaseResponse(header, body) - if err != nil { - return nil, err - } - return &resp, nil + return DecodeBaseResponse(header, body) } type spaceRequest struct { @@ -871,6 +867,17 @@ func (req *spaceRequest) setSpace(space interface{}) { req.space = space } +func EncodeSpace(res SchemaResolver, enc *msgpack.Encoder, space interface{}) error { + spaceEnc, err := newSpaceEncoder(res, space) + if err != nil { + return err + } + if err := spaceEnc.Encode(enc); err != nil { + return err + } + return nil +} + type spaceIndexRequest struct { spaceRequest index interface{} @@ -954,11 +961,7 @@ func (req authRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { // Response creates a response for the authRequest. func (req authRequest) Response(header Header, body io.Reader) (Response, error) { - resp, err := createBaseResponse(header, body) - if err != nil { - return nil, err - } - return &resp, nil + return DecodeBaseResponse(header, body) } // PingRequest helps you to create an execute request object for execution diff --git a/request_test.go b/request_test.go index 84ba23ef6..8ced455cd 100644 --- a/request_test.go +++ b/request_test.go @@ -9,10 +9,10 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" + "github.com/vmihailenco/msgpack/v5" ) const invalidSpaceMsg = "invalid space" @@ -1063,3 +1063,82 @@ func TestResponseDecodeTyped(t *testing.T) { assert.Equal(t, []byte{'v', '2'}, decoded) } } + +type stubSchemeResolver struct { + space interface{} +} + +func (r stubSchemeResolver) ResolveSpace(s interface{}) (uint32, error) { + if id, ok := r.space.(uint32); ok { + return id, nil + } + if _, ok := r.space.(string); ok { + return 0, nil + } + return 0, fmt.Errorf("stub error message: %v", r.space) +} + +func (stubSchemeResolver) ResolveIndex(i interface{}, spaceNo uint32) (uint32, error) { + return 0, nil +} + +func (r stubSchemeResolver) NamesUseSupported() bool { + _, ok := r.space.(string) + return ok +} + +func TestEncodeSpace(t *testing.T) { + tests := []struct { + name string + res stubSchemeResolver + err string + out []byte + }{ + { + name: "string space", + res: stubSchemeResolver{"test"}, + out: []byte{0x5E, 0xA4, 0x74, 0x65, 0x73, 0x74}, + }, + { + name: "empty string", + res: stubSchemeResolver{""}, + out: []byte{0x5E, 0xA0}, + }, + { + name: "numeric 524", + res: stubSchemeResolver{uint32(524)}, + out: []byte{0x10, 0xCD, 0x02, 0x0C}, + }, + { + name: "numeric zero", + res: stubSchemeResolver{uint32(0)}, + out: []byte{0x10, 0x00}, + }, + { + name: "numeric max value", + res: stubSchemeResolver{^uint32(0)}, + out: []byte{0x10, 0xCE, 0xFF, 0xFF, 0xFF, 0xFF}, + }, + { + name: "resolve error", + res: stubSchemeResolver{false}, + err: "stub error message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + + err := EncodeSpace(tt.res, enc, tt.res.space) + if tt.err != "" { + require.ErrorContains(t, err, tt.err) + return + } else { + require.NoError(t, err) + } + + require.Equal(t, tt.out, buf.Bytes()) + }) + } +} diff --git a/response.go b/response.go index 2c287f8bb..90a02a1ce 100644 --- a/response.go +++ b/response.go @@ -45,6 +45,12 @@ func createBaseResponse(header Header, body io.Reader) (baseResponse, error) { return baseResponse{header: header, buf: smallBuf{b: data}}, nil } +// DecodeBaseResponse parse response header and body. +func DecodeBaseResponse(header Header, body io.Reader) (Response, error) { + resp, err := createBaseResponse(header, body) + return &resp, err +} + // SelectResponse is used for the select requests. // It might contain a position descriptor of the last selected tuple. // diff --git a/response_test.go b/response_test.go new file mode 100644 index 000000000..1edbf018e --- /dev/null +++ b/response_test.go @@ -0,0 +1,55 @@ +package tarantool_test + +import ( + "bytes" + "io" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" + "github.com/tarantool/go-tarantool/v2" + "github.com/vmihailenco/msgpack/v5" +) + +func encodeResponseData(t *testing.T, data interface{}) io.Reader { + t.Helper() + + buf := bytes.NewBuffer([]byte{}) + enc := msgpack.NewEncoder(buf) + + enc.EncodeMapLen(1) + enc.EncodeUint8(uint8(iproto.IPROTO_DATA)) + enc.Encode([]interface{}{data}) + return buf + +} + +func TestDecodeBaseResponse(t *testing.T) { + tests := []struct { + name string + header tarantool.Header + body interface{} + }{ + { + "test1", + tarantool.Header{}, + nil, + }, + { + "test2", + tarantool.Header{RequestId: 123}, + []byte{'v', '2'}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := tarantool.DecodeBaseResponse(tt.header, encodeResponseData(t, tt.body)) + require.NoError(t, err) + require.Equal(t, tt.header, res.Header()) + + got, err := res.Decode() + require.NoError(t, err) + require.Equal(t, []interface{}{tt.body}, got) + }) + } +} diff --git a/watch.go b/watch.go index c147b0399..9f1273134 100644 --- a/watch.go +++ b/watch.go @@ -58,11 +58,7 @@ func (req *BroadcastRequest) Async() bool { // Response creates a response for a BroadcastRequest. func (req *BroadcastRequest) Response(header Header, body io.Reader) (Response, error) { - resp, err := createBaseResponse(header, body) - if err != nil { - return nil, err - } - return &resp, nil + return DecodeBaseResponse(header, body) } // watchRequest subscribes to the updates of a specified key defined on the From 8cf8673dacf5ca12e55bc809ad4ab147bde93613 Mon Sep 17 00:00:00 2001 From: Dmitriy Gertsog Date: Fri, 11 Oct 2024 13:13:11 +0300 Subject: [PATCH 553/605] api: update iproto.Arrow constants Closes #412 --- arrow/request.go | 16 ++-------------- arrow/request_test.go | 8 +------- go.mod | 2 +- go.sum | 8 ++------ 4 files changed, 6 insertions(+), 28 deletions(-) diff --git a/arrow/request.go b/arrow/request.go index 2b6a9e295..332720d3e 100644 --- a/arrow/request.go +++ b/arrow/request.go @@ -9,18 +9,6 @@ import ( "github.com/vmihailenco/msgpack/v5" ) -// INSERT Arrow request. -// -// FIXME: replace with iproto.IPROTO_INSERT_ARROW when iproto will released. -// https://github.com/tarantool/go-tarantool/issues/412 -const iprotoInsertArrowType = iproto.Type(17) - -// The data in Arrow format. -// -// FIXME: replace with iproto.IPROTO_ARROW when iproto will released. -// https://github.com/tarantool/go-tarantool/issues/412 -const iprotoArrowKey = iproto.Key(0x36) - // InsertRequest helps you to create an insert request object for execution // by a Connection. type InsertRequest struct { @@ -39,7 +27,7 @@ func NewInsertRequest(space interface{}, arrow Arrow) *InsertRequest { // Type returns a IPROTO_INSERT_ARROW type for the request. func (r *InsertRequest) Type() iproto.Type { - return iprotoInsertArrowType + return iproto.IPROTO_INSERT_ARROW } // Async returns false to the request return a response. @@ -78,7 +66,7 @@ func (r *InsertRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) if err := tarantool.EncodeSpace(res, enc, r.space); err != nil { return err } - if err := enc.EncodeUint(uint64(iprotoArrowKey)); err != nil { + if err := enc.EncodeUint(uint64(iproto.IPROTO_ARROW)); err != nil { return err } return enc.Encode(r.arrow) diff --git a/arrow/request_test.go b/arrow/request_test.go index 5cddaefb9..fba6b5563 100644 --- a/arrow/request_test.go +++ b/arrow/request_test.go @@ -13,17 +13,11 @@ import ( "github.com/vmihailenco/msgpack/v5" ) -// INSERT Arrow request. -// -// FIXME: replace with iproto.IPROTO_INSERT_ARROW when iproto will released. -// https://github.com/tarantool/go-tarantool/issues/412 -const iprotoInsertArrowType = iproto.Type(17) - const validSpace uint32 = 1 // Any valid value != default. func TestInsertRequestType(t *testing.T) { request := arrow.NewInsertRequest(validSpace, arrow.Arrow{}) - require.Equal(t, iprotoInsertArrowType, request.Type()) + require.Equal(t, iproto.IPROTO_INSERT_ARROW, request.Type()) } func TestInsertRequestAsync(t *testing.T) { diff --git a/go.mod b/go.mod index 30381a8be..ba396f15c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/google/uuid v1.3.0 github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.9.0 - github.com/tarantool/go-iproto v1.0.0 + github.com/tarantool/go-iproto v1.0.1-0.20241010173538-44b6566ef267 github.com/vmihailenco/msgpack/v5 v5.3.5 ) diff --git a/go.sum b/go.sum index bf5926673..e532373e6 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -10,19 +9,16 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tarantool/go-iproto v1.0.0 h1:quC4hdFhCuFYaCqOFgUxH2foRkhAy+TlEy7gQLhdVjw= -github.com/tarantool/go-iproto v1.0.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= +github.com/tarantool/go-iproto v1.0.1-0.20241010173538-44b6566ef267 h1:GenzvYfP9io9aEdZFmnopfcBOXmJg6MgBWyd4t8z4oI= +github.com/tarantool/go-iproto v1.0.1-0.20241010173538-44b6566ef267/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From f9eb9518e070833c7bcb94b8a84c37f7c8af7f15 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Wed, 23 Oct 2024 21:48:39 +0300 Subject: [PATCH 554/605] tests: fix decode arrow check after update The pull request changes [1] changes an error message on arrow decoding. The patch replaces a check of error message to a check of error code as it is a more stable check. However, the code is changed too. So as a temporary solution we need to work with both codes at the same time. 1. https://github.com/tarantool/tarantool/pull/10665 Part of #415 --- arrow/tarantool_test.go | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/arrow/tarantool_test.go b/arrow/tarantool_test.go index 2ff29fc8e..c02ce4f0f 100644 --- a/arrow/tarantool_test.go +++ b/arrow/tarantool_test.go @@ -9,6 +9,8 @@ import ( "time" "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" + "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/arrow" "github.com/tarantool/go-tarantool/v2/test_helpers" @@ -33,15 +35,19 @@ var opts = tarantool.Opts{ func TestInsert_invalid(t *testing.T) { arrows := []struct { arrow string - expected string + expected []iproto.Error }{ { "", - "Failed to decode Arrow IPC data", + // TODO: delete iproto.ER_ARROW_IPC_DECODE, see: + // https://github.com/tarantool/go-tarantool/issues/415 + []iproto.Error{iproto.ER_INVALID_MSGPACK, iproto.ER_ARROW_IPC_DECODE}, }, { "00", - "Failed to decode Arrow IPC data", + // TODO: delete iproto.ER_ARROW_IPC_DECODE, see: + // https://github.com/tarantool/go-tarantool/issues/415 + []iproto.Error{iproto.ER_INVALID_MSGPACK, iproto.ER_ARROW_IPC_DECODE}, }, { "ffffffff70000000040000009effffff0400010004000000" + @@ -53,7 +59,7 @@ func TestInsert_invalid(t *testing.T) { "00000000000000000000000000000800000000000000000000000100000001000000" + "0000000000000000000000000a00140004000c0010000c0014000400060008000c00" + "00000000000000000000", - "memtx does not support arrow format", + []iproto.Error{iproto.ER_UNSUPPORTED}, }, } @@ -66,14 +72,19 @@ func TestInsert_invalid(t *testing.T) { require.NoError(t, err) arr, err := arrow.MakeArrow(data) - if err != nil { - require.ErrorContains(t, err, a.expected) - return - } - req := arrow.NewInsertRequest(space, arr) + require.NoError(t, err) + req := arrow.NewInsertRequest(space, arr) _, err = conn.Do(req).Get() - require.ErrorContains(t, err, a.expected) + ttErr := err.(tarantool.Error) + + require.Contains(t, a.expected, ttErr.Code) + // TODO: replace the check with: + // + // require.Equal(t, a.expected, ttErr.Code) + // + // See: + // https://github.com/tarantool/go-tarantool/issues/415 }) } From 893934053d0d4db6acdee0a9a67e135edc531857 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 5 Dec 2024 09:17:16 +0300 Subject: [PATCH 555/605] tests: add output to a version parse error Now it is difficult to determine a reason of a `tarantool --version` parsing error. The patch adds a command output to the error to make it easier. --- test_helpers/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test_helpers/main.go b/test_helpers/main.go index 1ced629f3..f452f3e97 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -142,19 +142,19 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) ( parsed := tarantoolVersionRegexp.FindStringSubmatch(string(out)) if parsed == nil { - return true, errors.New("regexp parse failed") + return true, fmt.Errorf("failed to parse output %q", out) } if major, err = atoiUint64(parsed[1]); err != nil { - return true, fmt.Errorf("failed to parse major: %s", err) + return true, fmt.Errorf("failed to parse major from output %q: %w", out, err) } if minor, err = atoiUint64(parsed[2]); err != nil { - return true, fmt.Errorf("failed to parse minor: %s", err) + return true, fmt.Errorf("failed to parse minor from output %q: %w", out, err) } if patch, err = atoiUint64(parsed[3]); err != nil { - return true, fmt.Errorf("failed to parse patch: %s", err) + return true, fmt.Errorf("failed to parse patch from output %q: %w", out, err) } if major != majorMin { From ee82151093e49eb5d8de2695b04049c1b2ff3a5e Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 5 Dec 2024 09:35:16 +0300 Subject: [PATCH 556/605] ci: replace macos-12 with macos-13 The macos-12 is no longer supported by GitHub. --- .github/workflows/testing.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 17fe6370c..d82dc3ef8 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -136,7 +136,7 @@ jobs: - '1.20' - 'stable' runs-on: - - macos-12 + - macos-13 - macos-14 tarantool: - brew @@ -254,12 +254,6 @@ jobs: with: go-version: ${{ matrix.golang }} - # Workaround for Mac OS 12 testrace failure - # https://github.com/golang/go/issues/49138 - - name: disable MallocNanoZone for macos-12 - run: echo "MallocNanoZone=0" >> $GITHUB_ENV - if: matrix.runs-on == 'macos-12' - # Workaround issue https://github.com/tarantool/tt/issues/640 - name: Fix tt rocks if: matrix.tarantool == 'brew' From fbc69d4a2267e9fd62b3a297413799c122eacf17 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 5 Dec 2024 09:50:12 +0300 Subject: [PATCH 557/605] tests: update a negative check of MP_ARROW decoding The patch updates a check according to a latest Tarantool 3.3.0 release. Closes #415 --- arrow/tarantool_test.go | 20 +++++--------------- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/arrow/tarantool_test.go b/arrow/tarantool_test.go index c02ce4f0f..728b38f8b 100644 --- a/arrow/tarantool_test.go +++ b/arrow/tarantool_test.go @@ -35,19 +35,15 @@ var opts = tarantool.Opts{ func TestInsert_invalid(t *testing.T) { arrows := []struct { arrow string - expected []iproto.Error + expected iproto.Error }{ { "", - // TODO: delete iproto.ER_ARROW_IPC_DECODE, see: - // https://github.com/tarantool/go-tarantool/issues/415 - []iproto.Error{iproto.ER_INVALID_MSGPACK, iproto.ER_ARROW_IPC_DECODE}, + iproto.ER_INVALID_MSGPACK, }, { "00", - // TODO: delete iproto.ER_ARROW_IPC_DECODE, see: - // https://github.com/tarantool/go-tarantool/issues/415 - []iproto.Error{iproto.ER_INVALID_MSGPACK, iproto.ER_ARROW_IPC_DECODE}, + iproto.ER_INVALID_MSGPACK, }, { "ffffffff70000000040000009effffff0400010004000000" + @@ -59,7 +55,7 @@ func TestInsert_invalid(t *testing.T) { "00000000000000000000000000000800000000000000000000000100000001000000" + "0000000000000000000000000a00140004000c0010000c0014000400060008000c00" + "00000000000000000000", - []iproto.Error{iproto.ER_UNSUPPORTED}, + iproto.ER_UNSUPPORTED, }, } @@ -78,13 +74,7 @@ func TestInsert_invalid(t *testing.T) { _, err = conn.Do(req).Get() ttErr := err.(tarantool.Error) - require.Contains(t, a.expected, ttErr.Code) - // TODO: replace the check with: - // - // require.Equal(t, a.expected, ttErr.Code) - // - // See: - // https://github.com/tarantool/go-tarantool/issues/415 + require.Equal(t, a.expected, ttErr.Code) }) } diff --git a/go.mod b/go.mod index ba396f15c..237d390cc 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/google/uuid v1.3.0 github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.9.0 - github.com/tarantool/go-iproto v1.0.1-0.20241010173538-44b6566ef267 + github.com/tarantool/go-iproto v1.1.0 github.com/vmihailenco/msgpack/v5 v5.3.5 ) diff --git a/go.sum b/go.sum index e532373e6..a66c68ef0 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tarantool/go-iproto v1.0.1-0.20241010173538-44b6566ef267 h1:GenzvYfP9io9aEdZFmnopfcBOXmJg6MgBWyd4t8z4oI= -github.com/tarantool/go-iproto v1.0.1-0.20241010173538-44b6566ef267/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= +github.com/tarantool/go-iproto v1.1.0 h1:HULVOIHsiehI+FnHfM7wMDntuzUddO09DKqu2WnFQ5A= +github.com/tarantool/go-iproto v1.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= From ebdf9863cbec3bc7a4ebf3da496392422b5f1c1b Mon Sep 17 00:00:00 2001 From: "maksim.konovalov" Date: Tue, 10 Dec 2024 11:10:04 +0300 Subject: [PATCH 558/605] box: first box and box.info implementation Implemented the box interface for tarantool with a small number of fields, which in the future can be supplemented. --- CHANGELOG.md | 1 + box/box.go | 36 +++++++++++++++++ box/box_test.go | 29 ++++++++++++++ box/example_test.go | 60 ++++++++++++++++++++++++++++ box/info.go | 76 ++++++++++++++++++++++++++++++++++++ box/request.go | 38 ++++++++++++++++++ box/tarantool_test.go | 86 +++++++++++++++++++++++++++++++++++++++++ box/testdata/config.lua | 13 +++++++ 8 files changed, 339 insertions(+) create mode 100644 box/box.go create mode 100644 box/box_test.go create mode 100644 box/example_test.go create mode 100644 box/info.go create mode 100644 box/request.go create mode 100644 box/tarantool_test.go create mode 100644 box/testdata/config.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index b0eb0f9c9..4ae4da136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Methods that are implemented but not included in the pooler interface (#395). - Implemented stringer methods for pool.Role (#405). - Support the IPROTO_INSERT_ARROW request (#399). +- A simple implementation of using the box interface was written (#410). ### Changed diff --git a/box/box.go b/box/box.go new file mode 100644 index 000000000..be7f288ad --- /dev/null +++ b/box/box.go @@ -0,0 +1,36 @@ +package box + +import ( + "github.com/tarantool/go-tarantool/v2" +) + +// Box is a helper that wraps box.* requests. +// It holds a connection to the Tarantool instance via the Doer interface. +type Box struct { + conn tarantool.Doer // Connection interface for interacting with Tarantool. +} + +// New returns a new instance of the box structure, which implements the Box interface. +func New(conn tarantool.Doer) *Box { + return &Box{ + conn: conn, // Assigns the provided Tarantool connection. + } +} + +// Info retrieves the current information of the Tarantool instance. +// It calls the "box.info" function and parses the result into the Info structure. +func (b *Box) Info() (Info, error) { + var infoResp InfoResponse + + // Call "box.info" to get instance information from Tarantool. + fut := b.conn.Do(NewInfoRequest()) + + // Parse the result into the Info structure. + err := fut.GetTyped(&infoResp) + if err != nil { + return Info{}, err + } + + // Return the parsed info and any potential error. + return infoResp.Info, err +} diff --git a/box/box_test.go b/box/box_test.go new file mode 100644 index 000000000..31e614c15 --- /dev/null +++ b/box/box_test.go @@ -0,0 +1,29 @@ +package box_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/v2/box" +) + +func TestNew(t *testing.T) { + // Create a box instance with a nil connection. This should lead to a panic later. + b := box.New(nil) + + // Ensure the box instance is not nil (which it shouldn't be), but this is not meaningful + // since we will panic when we call the Info method with the nil connection. + require.NotNil(t, b) + + // We expect a panic because we are passing a nil connection (nil Doer) to the By function. + // The library does not control this zone, and the nil connection would cause a runtime error + // when we attempt to call methods (like Info) on it. + // This test ensures that such an invalid state is correctly handled by causing a panic, + // as it's outside the library's responsibility. + require.Panics(t, func() { + + // Calling Info on a box with a nil connection will result in a panic, since the underlying + // connection (Doer) cannot perform the requested action (it's nil). + _, _ = b.Info() + }) +} diff --git a/box/example_test.go b/box/example_test.go new file mode 100644 index 000000000..461949760 --- /dev/null +++ b/box/example_test.go @@ -0,0 +1,60 @@ +// Run Tarantool Common Edition before example execution: +// +// Terminal 1: +// $ cd box +// $ TEST_TNT_LISTEN=127.0.0.1:3013 tarantool testdata/config.lua +// +// Terminal 2: +// $ go test -v example_test.go +package box_test + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/box" +) + +func Example() { + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + } + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + cancel() + if err != nil { + log.Fatalf("Failed to connect: %s", err) + } + + // You can use Info Request type. + + fut := client.Do(box.NewInfoRequest()) + + resp := &box.InfoResponse{} + + err = fut.GetTyped(resp) + if err != nil { + log.Fatalf("Failed get box info: %s", err) + } + + // Or use simple Box implementation. + + b := box.New(client) + + info, err := b.Info() + if err != nil { + log.Fatalf("Failed get box info: %s", err) + } + + if info.UUID != resp.Info.UUID { + log.Fatalf("Box info uuids are not equal") + } + + fmt.Printf("Box info uuids are equal") + fmt.Printf("Current box info: %+v\n", resp.Info) +} diff --git a/box/info.go b/box/info.go new file mode 100644 index 000000000..6e5ed1c92 --- /dev/null +++ b/box/info.go @@ -0,0 +1,76 @@ +package box + +import ( + "fmt" + + "github.com/tarantool/go-tarantool/v2" + "github.com/vmihailenco/msgpack/v5" +) + +var _ tarantool.Request = (*InfoRequest)(nil) + +// Info represents detailed information about the Tarantool instance. +// It includes version, node ID, read-only status, process ID, cluster information, and more. +type Info struct { + // The Version of the Tarantool instance. + Version string `msgpack:"version"` + // The node ID (nullable). + ID *int `msgpack:"id"` + // Read-only (RO) status of the instance. + RO bool `msgpack:"ro"` + // UUID - Unique identifier of the instance. + UUID string `msgpack:"uuid"` + // Process ID of the instance. + PID int `msgpack:"pid"` + // Status - Current status of the instance (e.g., running, unconfigured). + Status string `msgpack:"status"` + // LSN - Log sequence number of the instance. + LSN uint64 `msgpack:"lsn"` +} + +// InfoResponse represents the response structure +// that holds the information of the Tarantool instance. +// It contains a single field: Info, which holds the instance details (version, UUID, PID, etc.). +type InfoResponse struct { + Info Info +} + +func (ir *InfoResponse) DecodeMsgpack(d *msgpack.Decoder) error { + arrayLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + if arrayLen != 1 { + return fmt.Errorf("protocol violation; expected 1 array entry, got %d", arrayLen) + } + + i := Info{} + err = d.Decode(&i) + if err != nil { + return err + } + + ir.Info = i + + return nil +} + +// InfoRequest represents a request to retrieve information about the Tarantool instance. +// It implements the tarantool.Request interface. +type InfoRequest struct { + baseRequest +} + +// Body method is used to serialize the request's body. +// It is part of the tarantool.Request interface implementation. +func (i InfoRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { + return i.impl.Body(res, enc) +} + +// NewInfoRequest returns a new empty info request. +func NewInfoRequest() InfoRequest { + req := InfoRequest{} + req.impl = newCall("box.info") + return req +} diff --git a/box/request.go b/box/request.go new file mode 100644 index 000000000..bf51a72f6 --- /dev/null +++ b/box/request.go @@ -0,0 +1,38 @@ +package box + +import ( + "context" + "io" + + "github.com/tarantool/go-iproto" + "github.com/tarantool/go-tarantool/v2" +) + +type baseRequest struct { + impl *tarantool.CallRequest +} + +func newCall(method string) *tarantool.CallRequest { + return tarantool.NewCallRequest(method) +} + +// Type returns IPROTO type for request. +func (req baseRequest) Type() iproto.Type { + return req.impl.Type() +} + +// Ctx returns a context of request. +func (req baseRequest) Ctx() context.Context { + return req.impl.Ctx() +} + +// Async returns request expects a response. +func (req baseRequest) Async() bool { + return req.impl.Async() +} + +// Response creates a response for the baseRequest. +func (req baseRequest) Response(header tarantool.Header, + body io.Reader) (tarantool.Response, error) { + return req.impl.Response(header, body) +} diff --git a/box/tarantool_test.go b/box/tarantool_test.go new file mode 100644 index 000000000..3d638b5b5 --- /dev/null +++ b/box/tarantool_test.go @@ -0,0 +1,86 @@ +package box_test + +import ( + "context" + "log" + "os" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/box" + "github.com/tarantool/go-tarantool/v2/test_helpers" +) + +var server = "127.0.0.1:3013" +var dialer = tarantool.NetDialer{ + Address: server, + User: "test", + Password: "test", +} + +func validateInfo(t testing.TB, info box.Info) { + var err error + + // Check all fields run correctly. + _, err = uuid.Parse(info.UUID) + require.NoErrorf(t, err, "validate instance uuid is valid") + + require.NotEmpty(t, info.Version) + // Check that pid parsed correctly. + require.NotEqual(t, info.PID, 0) +} + +func TestBox_Sugar_Info(t *testing.T) { + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + info, err := box.New(conn).Info() + require.NoError(t, err) + + validateInfo(t, info) +} + +func TestBox_Info(t *testing.T) { + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + fut := conn.Do(box.NewInfoRequest()) + require.NotNil(t, fut) + + resp := &box.InfoResponse{} + err = fut.GetTyped(resp) + require.NoError(t, err) + + validateInfo(t, resp.Info) +} + +func runTestMain(m *testing.M) int { + instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + Dialer: dialer, + InitScript: "testdata/config.lua", + Listen: server, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 10, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(instance) + + if err != nil { + log.Printf("Failed to prepare test Tarantool: %s", err) + return 1 + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/box/testdata/config.lua b/box/testdata/config.lua new file mode 100644 index 000000000..f3ee1a7b2 --- /dev/null +++ b/box/testdata/config.lua @@ -0,0 +1,13 @@ +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +box.cfg{ + work_dir = os.getenv("TEST_TNT_WORK_DIR"), +} + +box.schema.user.create('test', { password = 'test' , if_not_exists = true }) +box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +} From dba8c8505d5debb6b963b11da55785dee50c1631 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 10 Dec 2024 19:16:17 +0300 Subject: [PATCH 559/605] test: fix flaky TestAdd_Close_concurrent We need to reset an error to pool.ErrClosed in a Pool.Add() method if a pool already closed or closing in progress to make the behavior deterministic. --- pool/connection_pool.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 1c9d85f44..9be46665e 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -283,6 +283,13 @@ func (p *ConnectionPool) Add(ctx context.Context, instance Instance) error { canceled = false } if canceled { + if p.state.get() != connectedState { + // If it is canceled (or could be canceled) due to a + // Close()/CloseGraceful() call we overwrite the error + // to make behavior expected. + err = ErrClosed + } + p.endsMutex.Lock() delete(p.ends, instance.Name) p.endsMutex.Unlock() From 3a2fa7e12a9d04035c018899cc6b2416323be54c Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Sat, 14 Dec 2024 18:54:06 +0300 Subject: [PATCH 560/605] changelog: add dots at the end of records The non-critical change helps to unify a changelog format across team's repositories. --- CHANGELOG.md | 467 ++++++++++++++++++++++++++------------------------- 1 file changed, 235 insertions(+), 232 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ae4da136..b6c5c251b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,19 +9,20 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ## [Unreleased] ### Added -- Add err log to `ConnectionPool.Add()` in case, when unable to establish - connection and ctx is not canceled; - also added logs for error case of `ConnectionPool.tryConnect()` calls in - `ConnectionPool.controller()` and `ConnectionPool.reconnect()` + +- Error logging to `ConnectionPool.Add()` in case, when unable to establish + connection and ctx is not canceled (#389). +- Error logging for error case of `ConnectionPool.tryConnect()` calls in + `ConnectionPool.controller()` and `ConnectionPool.reconnect()` (#389). - Methods that are implemented but not included in the pooler interface (#395). - Implemented stringer methods for pool.Role (#405). - Support the IPROTO_INSERT_ARROW request (#399). -- A simple implementation of using the box interface was written (#410). +- A simple implementation of using the box interface (#410). ### Changed -- More informative request canceling: log the probable reason for unexpected request ID - and add request ID info to context done error message (#407). +- More informative request canceling: log the probable reason for unexpected + request ID and add request ID info to context done error message (#407). ### Fixed @@ -33,12 +34,12 @@ require execute access for `box.info` from a user for Tarantool >= 3.0.0. ### Changed - `execute` access for `box.info` is no longer required for ConnectionPool - for a Tarantool version >= 3.0.0 (#380) + for a Tarantool version >= 3.0.0 (#380). ### Fixed - `ConnectionPool.Remove()` does not notify a `ConnectionHandler` after - an instance is already removed from the pool (#385) + an instance is already removed from the pool (#385). ## [2.0.0] - 2024-02-12 @@ -63,50 +64,51 @@ See the [migration guide](./MIGRATION.md) for more details. ### Added -- Type() method to the Request interface (#158) -- Enumeration types for RLimitAction/iterators (#158) -- IsNullable flag for Field (#302) -- More linters on CI (#310) -- Meaningful description for read/write socket errors (#129) -- Support `operation_data` in `crud.Error` (#330) -- Support `fetch_latest_metadata` option for crud requests with metadata (#335) -- Support `noreturn` option for data change crud requests (#335) -- Support `crud.schema` request (#336, #351) +- Type() method to the Request interface (#158). +- Enumeration types for RLimitAction/iterators (#158). +- IsNullable flag for Field (#302). +- More linters on CI (#310). +- Meaningful description for read/write socket errors (#129). +- Support `operation_data` in `crud.Error` (#330). +- Support `fetch_latest_metadata` option for crud requests with + metadata (#335). +- Support `noreturn` option for data change crud requests (#335). +- Support `crud.schema` request (#336, #351). - Support `IPROTO_WATCH_ONCE` request type for Tarantool - version >= 3.0.0-alpha1 (#337) -- Support `yield_every` option for crud select requests (#350) + version >= 3.0.0-alpha1 (#337). +- Support `yield_every` option for crud select requests (#350). - Support `IPROTO_FEATURE_SPACE_AND_INDEX_NAMES` for Tarantool version >= 3.0.0-alpha1 (#338). It allows to use space and index names in requests instead of their IDs. -- `GetSchema` function to get the actual schema (#7) -- Support connection via an existing socket fd (#321) +- `GetSchema` function to get the actual schema (#7). +- Support connection via an existing socket fd (#321). - `Header` struct for the response header (#237). It can be accessed via `Header()` method of the `Response` interface. -- `Response` method added to the `Request` interface (#237) +- `Response` method added to the `Request` interface (#237). - New `LogAppendPushFailed` connection log constant (#237). It is logged when connection fails to append a push response. - `ErrorNo` constant that indicates that no error has occurred while getting - the response (#237) + the response (#237). - Ability to mock connections for tests (#237). Added new types `MockDoer`, `MockRequest` to `test_helpers`. -- `AuthDialer` type for creating a dialer with authentication (#301) +- `AuthDialer` type for creating a dialer with authentication (#301). - `ProtocolDialer` type for creating a dialer with `ProtocolInfo` receiving and - check (#301) + check (#301). - `GreetingDialer` type for creating a dialer, that fills `Greeting` of a - connection (#301) + connection (#301). - New method `Pool.DoInstance` to execute a request on a target instance in - a pool (#376) + a pool (#376). ### Changed -- connection_pool renamed to pool (#239) -- Use msgpack/v5 instead of msgpack.v2 (#236) -- Call/NewCallRequest = Call17/NewCall17Request (#235) +- connection_pool renamed to pool (#239). +- Use msgpack/v5 instead of msgpack.v2 (#236). +- Call/NewCallRequest = Call17/NewCall17Request (#235). - Change encoding of the queue.Identify() UUID argument from binary blob to plain string. Needed for upgrade to Tarantool 3.0, where a binary blob is decoded to a varbinary object (#313). -- Use objects of the Decimal type instead of pointers (#238) -- Use objects of the Datetime type instead of pointers (#238) +- Use objects of the Decimal type instead of pointers (#238). +- Use objects of the Datetime type instead of pointers (#238). - `connection.Connect` no longer return non-working connection objects (#136). This function now does not attempt to reconnect and tries to establish a connection only once. Function might be canceled @@ -114,28 +116,28 @@ See the [migration guide](./MIGRATION.md) for more details. `pool.Connect` and `pool.Add` now accept context as the first argument, which user may cancel in process. If `pool.Connect` is canceled in progress, an error will be returned. All created connections will be closed. -- `iproto.Feature` type now used instead of `ProtocolFeature` (#337) +- `iproto.Feature` type now used instead of `ProtocolFeature` (#337). - `iproto.IPROTO_FEATURE_` constants now used instead of local `Feature` - constants for `protocol` (#337) + constants for `protocol` (#337). - Change `crud` operations `Timeout` option type to `crud.OptFloat64` - instead of `crud.OptUint` (#342) + instead of `crud.OptUint` (#342). - Change all `Upsert` and `Update` requests to accept `*tarantool.Operations` - as `ops` parameters instead of `interface{}` (#348) -- Change `OverrideSchema(*Schema)` to `SetSchema(Schema)` (#7) + as `ops` parameters instead of `interface{}` (#348). +- Change `OverrideSchema(*Schema)` to `SetSchema(Schema)` (#7). - Change values, stored by pointers in the `Schema`, `Space`, `Index` structs, - to be stored by their values (#7) -- Make `Dialer` mandatory for creation a single connection (#321) + to be stored by their values (#7). +- Make `Dialer` mandatory for creation a single connection (#321). - Remove `Connection.RemoteAddr()`, `Connection.LocalAddr()`. - Add `Addr()` function instead (#321) + Add `Addr()` function instead (#321). - Remove `Connection.ClientProtocolInfo`, `Connection.ServerProtocolInfo`. - Add `ProtocolInfo()` function, which returns the server protocol info (#321) + Add `ProtocolInfo()` function, which returns the server protocol info (#321). - `NewWatcher` checks the actual features of the server, rather than relying - on the features provided by the user during connection creation (#321) + on the features provided by the user during connection creation (#321). - `pool.NewWatcher` does not create watchers for connections that do not support - it (#321) + it (#321). - Rename `pool.GetPoolInfo` to `pool.GetInfo`. Change return type to - `map[string]ConnectionInfo` (#321) -- `Response` is now an interface (#237) + `map[string]ConnectionInfo` (#321). +- `Response` is now an interface (#237). - All responses are now implementations of the `Response` interface (#237). `SelectResponse`, `ExecuteResponse`, `PrepareResponse`, `PushResponse` are part of a public API. `Pos()`, `MetaData()`, `SQLInfo()` methods created for them @@ -147,56 +149,57 @@ See the [migration guide](./MIGRATION.md) for more details. - Method `Get` for `Future` now returns response data (#237). To get the actual response new `GetResponse` method has been added. Methods `AppendPush` and `SetResponse` accept response `Header` and data as their arguments. -- `Future` constructors now accept `Request` as their argument (#237) +- `Future` constructors now accept `Request` as their argument (#237). - Operations `Ping`, `Select`, `Insert`, `Replace`, `Delete`, `Update`, `Upsert`, `Call`, `Call16`, `Call17`, `Eval`, `Execute` of a `Connector` and `Pooler` - return response data instead of an actual responses (#237) -- Renamed `StrangerResponse` to `MockResponse` (#237) + return response data instead of an actual responses (#237). +- Renamed `StrangerResponse` to `MockResponse` (#237). - `pool.Connect`, `pool.ConnetcWithOpts` and `pool.Add` use a new type - `pool.Instance` to determinate connection options (#356) + `pool.Instance` to determinate connection options (#356). - `pool.Connect`, `pool.ConnectWithOpts` and `pool.Add` add connections to - the pool even it is unable to connect to it (#372) -- Required Go version updated from `1.13` to `1.20` (#378) + the pool even it is unable to connect to it (#372). +- Required Go version updated from `1.13` to `1.20` (#378). ### Deprecated - All Connection., Connection.Typed and Connection.Async methods. Instead you should use requests objects + - Connection.Do() (#241) + Connection.Do() (#241). - All ConnectionPool., ConnectionPool.Typed and ConnectionPool.Async methods. Instead you should use requests - objects + ConnectionPool.Do() (#241) + objects + ConnectionPool.Do() (#241). - box.session.push() usage: Future.AppendPush() and Future.GetIterator() - methods, ResponseIterator and TimeoutResponseIterator types (#324) + methods, ResponseIterator and TimeoutResponseIterator types (#324). ### Removed -- multi subpackage (#240) -- msgpack.v2 support (#236) -- pool/RoundRobinStrategy (#158) -- DeadlineIO (#158) -- UUID_extId (#158) -- IPROTO constants (#158) -- Code() method from the Request interface (#158) -- `Schema` field from the `Connection` struct (#7) -- `OkCode` and `PushCode` constants (#237) -- SSL support (#301) -- `Future.Err()` method (#382) +- multi subpackage (#240). +- msgpack.v2 support (#236). +- pool/RoundRobinStrategy (#158). +- DeadlineIO (#158). +- UUID_extId (#158). +- IPROTO constants (#158). +- Code() method from the Request interface (#158). +- `Schema` field from the `Connection` struct (#7). +- `OkCode` and `PushCode` constants (#237). +- SSL support (#301). +- `Future.Err()` method (#382). ### Fixed -- Flaky decimal/TestSelect (#300) -- Race condition at roundRobinStrategy.GetNextConnection() (#309) -- Incorrect decoding of an MP_DECIMAL when the `scale` value is negative (#314) +- Flaky decimal/TestSelect (#300). +- Race condition at roundRobinStrategy.GetNextConnection() (#309). +- Incorrect decoding of an MP_DECIMAL when the `scale` value is + negative (#314). - Incorrect options (`after`, `batch_size` and `force_map_call`) setup for - crud.SelectRequest (#320) + crud.SelectRequest (#320). - Incorrect options (`vshard_router`, `fields`, `bucket_id`, `mode`, - `prefer_replica`, `balance`) setup for crud.GetRequest (#335) -- Tests with crud 1.4.0 (#336) -- Tests with case sensitive SQL (#341) -- Splice update operation accepts 3 arguments instead of 5 (#348) + `prefer_replica`, `balance`) setup for crud.GetRequest (#335). +- Tests with crud 1.4.0 (#336). +- Tests with case sensitive SQL (#341). +- Splice update operation accepts 3 arguments instead of 5 (#348). - Unable to use a slice of custom types as a slice of tuples or objects for - `crud.*ManyRequest/crud.*ObjectManyRequest` (#365) + `crud.*ManyRequest/crud.*ObjectManyRequest` (#365). ## [1.12.0] - 2023-06-07 @@ -207,18 +210,18 @@ from a ConnectionPool. ### Added - Connection.CloseGraceful() unlike Connection.Close() waits for all - requests to complete (#257) + requests to complete (#257). - ConnectionPool.CloseGraceful() unlike ConnectionPool.Close() waits for all - requests to complete (#257) + requests to complete (#257). - ConnectionPool.Add()/ConnectionPool.Remove() to add/remove endpoints - from a pool (#290) + from a pool (#290). ### Changed ### Fixed -- crud tests with Tarantool 3.0 (#293) -- SQL tests with Tarantool 3.0 (#295) +- crud tests with Tarantool 3.0 (#293). +- SQL tests with Tarantool 3.0 (#295). ## [1.11.0] - 2023-05-18 @@ -227,28 +230,28 @@ The release adds pagination support and wrappers for the ### Added -- Support pagination (#246) -- A Makefile target to test with race detector (#218) -- Support CRUD API (#108) +- Support pagination (#246). +- A Makefile target to test with race detector (#218). +- Support CRUD API (#108). - An ability to replace a base network connection to a Tarantool - instance (#265) -- Missed iterator constant (#285) + instance (#265). +- Missed iterator constant (#285). ### Changed -- queue module version bumped to 1.3.0 (#278) +- queue module version bumped to 1.3.0 (#278). ### Fixed -- Several non-critical data race issues (#218) -- Build on Apple M1 with OpenSSL (#260) +- Several non-critical data race issues (#218). +- Build on Apple M1 with OpenSSL (#260). - ConnectionPool does not properly handle disconnection with Opts.Reconnect - set (#272) -- Watcher events loss with a small per-request timeout (#284) -- Connect() panics on concurrent schema update (#278) -- Wrong Ttr setup by Queue.Cfg() (#278) -- Flaky queue/Example_connectionPool (#278) -- Flaky queue/Example_simpleQueueCustomMsgPack (#277) + set (#272). +- Watcher events loss with a small per-request timeout (#284). +- Connect() panics on concurrent schema update (#278). +- Wrong Ttr setup by Queue.Cfg() (#278). +- Flaky queue/Example_connectionPool (#278). +- Flaky queue/Example_simpleQueueCustomMsgPack (#277). ## [1.10.0] - 2022-12-31 @@ -256,21 +259,21 @@ The release improves compatibility with new Tarantool versions. ### Added -- Support iproto feature discovery (#120) -- Support errors extended information (#209) -- Support error type in MessagePack (#209) -- Support event subscription (#119) -- Support session settings (#215) -- Support pap-sha256 authorization method (Tarantool EE feature) (#243) -- Support graceful shutdown (#214) +- Support iproto feature discovery (#120). +- Support errors extended information (#209). +- Support error type in MessagePack (#209). +- Support event subscription (#119). +- Support session settings (#215). +- Support pap-sha256 authorization method (Tarantool EE feature) (#243). +- Support graceful shutdown (#214). ### Fixed - Decimal package uses a test variable DecimalPrecision instead of a - package-level variable decimalPrecision (#233) -- Flaky test TestClientRequestObjectsWithContext (#244) -- Flaky test multi/TestDisconnectAll (#234) -- Build on macOS with Apple M1 (#260) + package-level variable decimalPrecision (#233). +- Flaky test TestClientRequestObjectsWithContext (#244). +- Flaky test multi/TestDisconnectAll (#234). +- Build on macOS with Apple M1 (#260). ## [1.9.0] - 2022-11-02 @@ -280,30 +283,30 @@ switching. ### Added -- Support the queue 1.2.1 (#177) +- Support the queue 1.2.1 (#177). - ConnectionHandler interface for handling changes of connections in - ConnectionPool (#178) -- Execute, ExecuteTyped and ExecuteAsync methods to ConnectionPool (#176) -- ConnectorAdapter type to use ConnectionPool as Connector interface (#176) -- An example how to use queue and connection_pool subpackages together (#176) + ConnectionPool (#178). +- Execute, ExecuteTyped and ExecuteAsync methods to ConnectionPool (#176). +- ConnectorAdapter type to use ConnectionPool as Connector interface (#176). +- An example how to use queue and connection_pool subpackages together (#176). ### Fixed -- Mode type description in the connection_pool subpackage (#208) -- Missed Role type constants in the connection_pool subpackage (#208) -- ConnectionPool does not close UnknownRole connections (#208) -- Segmentation faults in ConnectionPool requests after disconnect (#208) -- Addresses in ConnectionPool may be changed from an external code (#208) -- ConnectionPool recreates connections too often (#208) -- A connection is still opened after ConnectionPool.Close() (#208) +- Mode type description in the connection_pool subpackage (#208). +- Missed Role type constants in the connection_pool subpackage (#208). +- ConnectionPool does not close UnknownRole connections (#208). +- Segmentation faults in ConnectionPool requests after disconnect (#208). +- Addresses in ConnectionPool may be changed from an external code (#208). +- ConnectionPool recreates connections too often (#208). +- A connection is still opened after ConnectionPool.Close() (#208). - Future.GetTyped() after Future.Get() does not decode response - correctly (#213) + correctly (#213). - Decimal package uses a test function GetNumberLength instead of a - package-level function getNumberLength (#219) -- Datetime location after encode + decode is unequal (#217) -- Wrong interval arithmetic with timezones (#221) -- Invalid MsgPack if STREAM_ID > 127 (#224) -- queue.Take() returns an invalid task (#222) + package-level function getNumberLength (#219). +- Datetime location after encode + decode is unequal (#217). +- Wrong interval arithmetic with timezones (#221). +- Invalid MsgPack if STREAM_ID > 127 (#224). +- queue.Take() returns an invalid task (#222). ## [1.8.0] - 2022-08-17 @@ -311,15 +314,13 @@ The minor release with time zones and interval support for datetime. ### Added -- Optional msgpack.v5 usage (#124) -- TZ support for datetime (#163) -- Interval support for datetime (#165) - -### Changed +- Optional msgpack.v5 usage (#124). +- TZ support for datetime (#163). +- Interval support for datetime (#165). ### Fixed -- Markdown of documentation for the decimal subpackage (#201) +- Markdown of documentation for the decimal subpackage (#201). ## [1.7.0] - 2022-08-02 @@ -330,29 +331,29 @@ based on this idea. ### Added -- SSL support (#155) -- IPROTO_PUSH messages support (#67) -- Public API with request object types (#126) -- Support decimal type in msgpack (#96) -- Support datetime type in msgpack (#118) -- Prepared SQL statements (#117) -- Context support for request objects (#48) -- Streams and interactive transactions support (#101) +- SSL support (#155). +- IPROTO_PUSH messages support (#67). +- Public API with request object types (#126). +- Support decimal type in msgpack (#96). +- Support datetime type in msgpack (#118). +- Prepared SQL statements (#117). +- Context support for request objects (#48). +- Streams and interactive transactions support (#101). - `Call16` method, support build tag `go_tarantool_call_17` to choose - default behavior for `Call` method as Call17 (#125) + default behavior for `Call` method as Call17 (#125). ### Changed - `IPROTO_*` constants that identify requests renamed from `Request` to - `RequestCode` (#126) + `RequestCode` (#126). ### Removed -- NewErrorFuture function (#190) +- NewErrorFuture function (#190). ### Fixed -- Add `ExecuteAsync` and `ExecuteTyped` to common connector interface (#62) +- Add `ExecuteAsync` and `ExecuteTyped` to common connector interface (#62). ## [1.6.0] - 2022-06-01 @@ -361,26 +362,26 @@ CI and documentation. ### Added -- Coveralls support (#149) -- Reusable testing workflow (integration testing with latest Tarantool) (#112) -- Simple CI based on GitHub actions (#114) -- Support UUID type in msgpack (#90) -- Go modules support (#91) -- queue-utube handling (#85) -- Master discovery (#113) -- SQL support (#62) +- Coveralls support (#149). +- Reusable testing workflow (integration testing with latest Tarantool) (#112). +- Simple CI based on GitHub actions (#114). +- Support UUID type in msgpack (#90). +- Go modules support (#91). +- queue-utube handling (#85). +- Master discovery (#113). +- SQL support (#62). ### Changed -- Handle everything with `go test` (#115) -- Use plain package instead of module for UUID submodule (#134) -- Reset buffer if its average use size smaller than quarter of capacity (#95) -- Update API documentation: comments and examples (#123) +- Handle everything with `go test` (#115). +- Use plain package instead of module for UUID submodule (#134). +- Reset buffer if its average use size smaller than quarter of capacity (#95). +- Update API documentation: comments and examples (#123). ### Fixed -- Fix queue tests (#107) -- Make test case consistent with comments (#105) +- Fix queue tests (#107). +- Make test case consistent with comments (#105). ## [1.5] - 2019-12-29 @@ -388,90 +389,92 @@ First release. ### Fixed -- Fix infinite recursive call of `Upsert` method for `ConnectionMulti` -- Fix index out of range panic on `dial()` to short address -- Fix cast in `defaultLogger.Report` (#49) -- Fix race condition on extremely small request timeouts (#43) -- Fix notify for `Connected` transition -- Fix reconnection logic and add `Opts.SkipSchema` method -- Fix future sending -- Fix panic on disconnect + timeout -- Fix block on msgpack error -- Fix ratelimit -- Fix `timeouts` method for `Connection` -- Fix possible race condition on extremely small request timeouts -- Fix race condition on future channel creation -- Fix block on forever closed connection -- Fix race condition in `Connection` -- Fix extra map fields -- Fix response header parsing -- Fix reconnect logic in `Connection` +- Fix infinite recursive call of `Upsert` method for `ConnectionMulti`. +- Fix index out of range panic on `dial()` to short address. +- Fix cast in `defaultLogger.Report` (#49). +- Fix race condition on extremely small request timeouts (#43). +- Fix notify for `Connected` transition. +- Fix reconnection logic and add `Opts.SkipSchema` method. +- Fix future sending. +- Fix panic on disconnect + timeout. +- Fix block on msgpack error. +- Fix ratelimit. +- Fix `timeouts` method for `Connection`. +- Fix possible race condition on extremely small request timeouts. +- Fix race condition on future channel creation. +- Fix block on forever closed connection. +- Fix race condition in `Connection`. +- Fix extra map fields. +- Fix response header parsing. +- Fix reconnect logic in `Connection`. ### Changed -- Make logger configurable -- Report user mismatch error immediately -- Set limit timeout by 0.9 of connection to queue request timeout -- Update fields could be negative -- Require `RLimitAction` to be specified if `RateLimit` is specified -- Use newer typed msgpack interface -- Do not start timeouts goroutine if no timeout specified -- Clear buffers on connection close -- Update `BenchmarkClientParallelMassive` -- Remove array requirements for keys and opts -- Do not allocate `Response` inplace -- Respect timeout on request sending -- Use `AfterFunc(fut.timeouted)` instead of `time.NewTimer()` -- Use `_vspace`/`_vindex` for introspection -- Method `Tuples()` always returns table for response +- Make logger configurable. +- Report user mismatch error immediately. +- Set limit timeout by 0.9 of connection to queue request timeout. +- Update fields could be negative. +- Require `RLimitAction` to be specified if `RateLimit` is specified. +- Use newer typed msgpack interface. +- Do not start timeouts goroutine if no timeout specified. +- Clear buffers on connection close. +- Update `BenchmarkClientParallelMassive`. +- Remove array requirements for keys and opts. +- Do not allocate `Response` inplace. +- Respect timeout on request sending. +- Use `AfterFunc(fut.timeouted)` instead of `time.NewTimer()`. +- Use `_vspace`/`_vindex` for introspection. +- Method `Tuples()` always returns table for response. ### Removed -- Remove `UpsertTyped()` method (#23) +- Remove `UpsertTyped()` method (#23). ### Added -- Add methods `Future.WaitChan` and `Future.Err` (#86) -- Get node list from nodes (#81) -- Add method `deleteConnectionFromPool` -- Add multiconnections support -- Add `Addr` method for the connection (#64) -- Add `Delete` method for the queue -- Implemented typed taking from queue (#55) -- Add `OverrideSchema` method for the connection -- Add default case to default logger -- Add license (BSD-2 clause as for Tarantool) -- Add `GetTyped` method for the connection (#40) -- Add `ConfiguredTimeout` method for the connection, change queue interface -- Add an example for queue -- Add `GetQueue` method for the queue -- Add queue support -- Add support of Unix socket address -- Add check for prefix "tcp:" -- Add the ability to work with the Tarantool via Unix socket -- Add note about magic way to pack tuples -- Add notification about connection state change -- Add workaround for tarantool/tarantool#2060 (#32) -- Add `ConnectedNow` method for the connection -- Add IO deadline and use `net.Conn.Set(Read|Write)Deadline` -- Add a couple of benchmarks -- Add timeout on connection attempt -- Add `RLimitAction` option -- Add `Call17` method for the connection to make a call compatible with Tarantool 1.7 -- Add `ClientParallelMassive` benchmark -- Add `runtime.Gosched` for decreasing `writer.flush` count -- Add `Eval`, `EvalTyped`, `SelectTyped`, `InsertTyped`, `ReplaceTyped`, `DeleteRequest`, `UpdateTyped`, `UpsertTyped` methods -- Add `UpdateTyped` method -- Add `CallTyped` method +- Add methods `Future.WaitChan` and `Future.Err` (#86). +- Get node list from nodes (#81). +- Add method `deleteConnectionFromPool`. +- Add multiconnections support. +- Add `Addr` method for the connection (#64). +- Add `Delete` method for the queue. +- Implemented typed taking from queue (#55). +- Add `OverrideSchema` method for the connection. +- Add default case to default logger. +- Add license (BSD-2 clause as for Tarantool). +- Add `GetTyped` method for the connection (#40). +- Add `ConfiguredTimeout` method for the connection, change queue interface. +- Add an example for queue. +- Add `GetQueue` method for the queue. +- Add queue support. +- Add support of Unix socket address. +- Add check for prefix "tcp:". +- Add the ability to work with the Tarantool via Unix socket. +- Add note about magic way to pack tuples. +- Add notification about connection state change. +- Add workaround for tarantool/tarantool#2060 (#32). +- Add `ConnectedNow` method for the connection. +- Add IO deadline and use `net.Conn.Set(Read|Write)Deadline`. +- Add a couple of benchmarks. +- Add timeout on connection attempt. +- Add `RLimitAction` option. +- Add `Call17` method for the connection to make a call compatible with + Tarantool 1.7. +- Add `ClientParallelMassive` benchmark. +- Add `runtime.Gosched` for decreasing `writer.flush` count. +- Add `Eval`, `EvalTyped`, `SelectTyped`, `InsertTyped`, `ReplaceTyped`, + `DeleteRequest`, `UpdateTyped`, `UpsertTyped` methods. +- Add `UpdateTyped` method. +- Add `CallTyped` method. - Add possibility to pass `Space` and `Index` objects into `Select` etc. -- Add custom MsgPack pack/unpack functions -- Add support of Tarantool 1.6.8 schema format -- Add support of Tarantool 1.6.5 schema format -- Add schema loading -- Add `LocalAddr` and `RemoteAddr` methods for the connection -- Add `Upsert` method for the connection -- Add `Eval` and `EvalAsync` methods for the connection -- Add Tarantool error codes -- Add auth support -- Add auth during reconnect -- Add auth request +- Add custom MsgPack pack/unpack functions. +- Add support of Tarantool 1.6.8 schema format. +- Add support of Tarantool 1.6.5 schema format. +- Add schema loading. +- Add `LocalAddr` and `RemoteAddr` methods for the connection. +- Add `Upsert` method for the connection. +- Add `Eval` and `EvalAsync` methods for the connection. +- Add Tarantool error codes. +- Add auth support. +- Add auth during reconnect. +- Add auth request. From 10b3c8ff3556b9d5b99bd88ac34739d9e8296747 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Sat, 14 Dec 2024 18:27:58 +0300 Subject: [PATCH 561/605] Release v2.2.0 --- CHANGELOG.md | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6c5c251b..f62080c5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,18 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +### Changed + +### Fixed + +## [v2.2.0] - 2024-12-16 + +The release introduces the IPROTO_INSERT_ARROW request (arrow.InsertRequest) +and a request to archive `box.info` values (box.InfoRequest). Additionally, it +includes some improvements to logging. + +### Added + - Error logging to `ConnectionPool.Add()` in case, when unable to establish connection and ctx is not canceled (#389). - Error logging for error case of `ConnectionPool.tryConnect()` calls in @@ -24,9 +36,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - More informative request canceling: log the probable reason for unexpected request ID and add request ID info to context done error message (#407). -### Fixed - -## [2.1.0] - 2024-03-06 +## [v2.1.0] - 2024-03-06 The small release improves the ConnectionPool. The ConnectionPool now does not require execute access for `box.info` from a user for Tarantool >= 3.0.0. @@ -41,7 +51,7 @@ require execute access for `box.info` from a user for Tarantool >= 3.0.0. - `ConnectionPool.Remove()` does not notify a `ConnectionHandler` after an instance is already removed from the pool (#385). -## [2.0.0] - 2024-02-12 +## [v2.0.0] - 2024-02-12 There are a lot of changes in the new major version. The main ones: @@ -201,7 +211,7 @@ See the [migration guide](./MIGRATION.md) for more details. - Unable to use a slice of custom types as a slice of tuples or objects for `crud.*ManyRequest/crud.*ObjectManyRequest` (#365). -## [1.12.0] - 2023-06-07 +## [v1.12.0] - 2023-06-07 The release introduces the ability to gracefully close Connection and ConnectionPool and also provides methods for adding or removing an endpoint @@ -223,7 +233,7 @@ from a ConnectionPool. - crud tests with Tarantool 3.0 (#293). - SQL tests with Tarantool 3.0 (#295). -## [1.11.0] - 2023-05-18 +## [v1.11.0] - 2023-05-18 The release adds pagination support and wrappers for the [crud](https://github.com/tarantool/crud) module. @@ -253,7 +263,7 @@ The release adds pagination support and wrappers for the - Flaky queue/Example_connectionPool (#278). - Flaky queue/Example_simpleQueueCustomMsgPack (#277). -## [1.10.0] - 2022-12-31 +## [v1.10.0] - 2022-12-31 The release improves compatibility with new Tarantool versions. @@ -275,7 +285,7 @@ The release improves compatibility with new Tarantool versions. - Flaky test multi/TestDisconnectAll (#234). - Build on macOS with Apple M1 (#260). -## [1.9.0] - 2022-11-02 +## [v1.9.0] - 2022-11-02 The release adds support for the latest version of the [queue package](https://github.com/tarantool/queue) with master-replica @@ -308,7 +318,7 @@ switching. - Invalid MsgPack if STREAM_ID > 127 (#224). - queue.Take() returns an invalid task (#222). -## [1.8.0] - 2022-08-17 +## [v1.8.0] - 2022-08-17 The minor release with time zones and interval support for datetime. @@ -322,7 +332,7 @@ The minor release with time zones and interval support for datetime. - Markdown of documentation for the decimal subpackage (#201). -## [1.7.0] - 2022-08-02 +## [v1.7.0] - 2022-08-02 This release adds a number of features. The extending of the public API has become possible with a new way of creating requests. New types of requests are @@ -355,7 +365,7 @@ based on this idea. - Add `ExecuteAsync` and `ExecuteTyped` to common connector interface (#62). -## [1.6.0] - 2022-06-01 +## [v1.6.0] - 2022-06-01 This release adds a number of features. Also it significantly improves testing, CI and documentation. From c7dce8139000121ee0223e6d3c88134dd0a9f146 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 26 Dec 2024 12:25:00 +0300 Subject: [PATCH 562/605] bugfix: schema is reset after a reconnect We need to keep an existing schema resolver to avoid a schema reset after a reconnect. An another approach could be to get a schema on each reconnect, but we will change the expected behavior in this case: load a schema once on a first connect. --- CHANGELOG.md | 3 +++ connection.go | 9 ++++---- tarantool_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f62080c5d..f9ca3519d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +- `unable to use an index name because schema is not loaded` error after + a reconnect (#424). + ## [v2.2.0] - 2024-12-16 The release introduces the IPROTO_INSERT_ARROW request (arrow.InsertRequest) diff --git a/connection.go b/connection.go index 7c8e1294d..d00b52a57 100644 --- a/connection.go +++ b/connection.go @@ -446,12 +446,13 @@ func (conn *Connection) dial(ctx context.Context) error { conn.Greeting.Salt = connGreeting.Salt conn.serverProtocolInfo = c.ProtocolInfo() - spaceAndIndexNamesSupported := - isFeatureInSlice(iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, + if conn.schemaResolver == nil { + namesSupported := isFeatureInSlice(iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, conn.serverProtocolInfo.Features) - conn.schemaResolver = &noSchemaResolver{ - SpaceAndIndexNamesSupported: spaceAndIndexNamesSupported, + conn.schemaResolver = &noSchemaResolver{ + SpaceAndIndexNamesSupported: namesSupported, + } } // Watchers. diff --git a/tarantool_test.go b/tarantool_test.go index 98f04a045..e6adebc6d 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3868,6 +3868,61 @@ func TestWatcher_Unregister_concurrent(t *testing.T) { wg.Wait() } +func TestConnection_named_index_after_reconnect(t *testing.T) { + const server = "127.0.0.1:3015" + + testDialer := dialer + testDialer.Address = server + + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + Dialer: testDialer, + InitScript: "config.lua", + Listen: server, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 10, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(inst) + if err != nil { + t.Fatalf("Unable to start Tarantool: %s", err) + } + + reconnectOpts := opts + reconnectOpts.Reconnect = 100 * time.Millisecond + reconnectOpts.MaxReconnects = 10 + + conn := test_helpers.ConnectWithValidation(t, testDialer, reconnectOpts) + defer conn.Close() + + test_helpers.StopTarantool(inst) + + request := NewSelectRequest("test").Index("primary").Limit(1) + _, err = conn.Do(request).Get() + if err == nil { + t.Fatalf("An error expected.") + } + + if err := test_helpers.RestartTarantool(&inst); err != nil { + t.Fatalf("Unable to restart Tarantool: %s", err) + } + + maxTime := reconnectOpts.Reconnect * time.Duration(reconnectOpts.MaxReconnects) + timeout := time.After(maxTime) + + for { + select { + case <-timeout: + t.Fatalf("Failed to execute request without an error, last error: %s", err) + default: + } + + _, err = conn.Do(request).Get() + if err == nil { + return + } + } +} + func TestConnect_schema_update(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() From 7eae014b832a2c1a34d368efdd510ecabb9487f4 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Fri, 27 Dec 2024 10:13:55 +0300 Subject: [PATCH 563/605] Release v2.2.1 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ca3519d..c515f54c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +## [v2.2.1] - 2024-12-17 + +The release fixes a schema lost after a reconnect. + +### Fixed + - `unable to use an index name because schema is not loaded` error after a reconnect (#424). From d8e2284e4f0faf90231d678e68dee7817bb8adb5 Mon Sep 17 00:00:00 2001 From: Vladimir Fidunin Date: Wed, 15 Jan 2025 11:56:54 +0300 Subject: [PATCH 564/605] box: add replication information --- CHANGELOG.md | 2 + box/info.go | 53 +++++++++++++++++ box/info_test.go | 126 ++++++++++++++++++++++++++++++++++++++++ box/tarantool_test.go | 6 ++ box/testdata/config.lua | 3 + 5 files changed, 190 insertions(+) create mode 100644 box/info_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index c515f54c2..82f9766f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +- Extend box with replication information (#427). + ### Changed ### Fixed diff --git a/box/info.go b/box/info.go index 6e5ed1c92..aabfd65e3 100644 --- a/box/info.go +++ b/box/info.go @@ -26,6 +26,59 @@ type Info struct { Status string `msgpack:"status"` // LSN - Log sequence number of the instance. LSN uint64 `msgpack:"lsn"` + // Replication - replication status. + Replication map[int]Replication `msgpack:"replication,omitempty"` +} + +// Replication section of box.info() is a table with statistics for all instances +// in the replica set that the current instance belongs to. +type Replication struct { + // ID is a short numeric identifier of instance n within the replica set. + ID int `msgpack:"id"` + // UUID - Unique identifier of the instance. + UUID string `msgpack:"uuid"` + // LSN - Log sequence number of the instance. + LSN uint64 `msgpack:"lsn"` + // Upstream - information about upstream. + Upstream Upstream `msgpack:"upstream,omitempty"` + // Downstream - information about downstream. + Downstream Downstream `msgpack:"downstream,omitempty"` +} + +// Upstream information. +type Upstream struct { + // Status is replication status of the connection with the instance. + Status string `msgpack:"status"` + // Idle is the time (in seconds) since the last event was received. + Idle float64 `msgpack:"idle"` + // Peer contains instance n’s URI. + Peer string `msgpack:"peer"` + // Lag is the time difference between the local time of instance n, + // recorded when the event was received, and the local time at another master + // recorded when the event was written to the write-ahead log on that master. + Lag float64 `msgpack:"lag"` + // Message contains an error message in case of a degraded state; otherwise, it is nil. + Message string `msgpack:"message,omitempty"` + // SystemMessage contains an error message in case of a degraded state; otherwise, it is nil. + SystemMessage string `msgpack:"system_message,omitempty"` +} + +// Downstream information. +type Downstream struct { + // Status is replication status of the connection with the instance. + Status string `msgpack:"status"` + // Idle is the time (in seconds) since the last event was received. + Idle float64 `msgpack:"idle"` + // VClock contains the vector clock, which is a table of ‘id, lsn’ pairs. + VClock map[int]uint64 `msgpack:"vclock"` + // Lag is the time difference between the local time of instance n, + // recorded when the event was received, and the local time at another master + // recorded when the event was written to the write-ahead log on that master. + Lag float64 `msgpack:"lag"` + // Message contains an error message in case of a degraded state; otherwise, it is nil. + Message string `msgpack:"message,omitempty"` + // SystemMessage contains an error message in case of a degraded state; otherwise, it is nil. + SystemMessage string `msgpack:"system_message,omitempty"` } // InfoResponse represents the response structure diff --git a/box/info_test.go b/box/info_test.go new file mode 100644 index 000000000..818555b69 --- /dev/null +++ b/box/info_test.go @@ -0,0 +1,126 @@ +package box + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack/v5" +) + +func TestInfo(t *testing.T) { + id := 1 + cases := []struct { + Name string + Struct Info + Data map[string]interface{} + }{ + { + Name: "Case: base info struct", + Struct: Info{ + Version: "2.11.4-0-g8cebbf2cad", + ID: &id, + RO: false, + UUID: "69360e9b-4641-4ec3-ab51-297f46749849", + PID: 1, + Status: "running", + LSN: 8, + }, + Data: map[string]interface{}{ + "version": "2.11.4-0-g8cebbf2cad", + "id": 1, + "ro": false, + "uuid": "69360e9b-4641-4ec3-ab51-297f46749849", + "pid": 1, + "status": "running", + "lsn": 8, + }, + }, + { + Name: "Case: info struct with replication", + Struct: Info{ + Version: "2.11.4-0-g8cebbf2cad", + ID: &id, + RO: false, + UUID: "69360e9b-4641-4ec3-ab51-297f46749849", + PID: 1, + Status: "running", + LSN: 8, + Replication: map[int]Replication{ + 1: { + ID: 1, + UUID: "69360e9b-4641-4ec3-ab51-297f46749849", + LSN: 8, + }, + 2: { + ID: 2, + UUID: "75f5f5aa-89f0-4d95-b5a9-96a0eaa0ce36", + LSN: 0, + Upstream: Upstream{ + Status: "follow", + Idle: 2.4564633660484, + Peer: "other.tarantool:3301", + Lag: 0.00011920928955078, + Message: "'getaddrinfo: Name or service not known'", + SystemMessage: "Input/output error", + }, + Downstream: Downstream{ + Status: "follow", + Idle: 2.8306158290943, + VClock: map[int]uint64{1: 8}, + Lag: 0, + Message: "'unexpected EOF when reading from socket'", + SystemMessage: "Broken pipe", + }, + }, + }, + }, + Data: map[string]interface{}{ + "version": "2.11.4-0-g8cebbf2cad", + "id": 1, + "ro": false, + "uuid": "69360e9b-4641-4ec3-ab51-297f46749849", + "pid": 1, + "status": "running", + "lsn": 8, + "replication": map[interface{}]interface{}{ + 1: map[string]interface{}{ + "id": 1, + "uuid": "69360e9b-4641-4ec3-ab51-297f46749849", + "lsn": 8, + }, + 2: map[string]interface{}{ + "id": 2, + "uuid": "75f5f5aa-89f0-4d95-b5a9-96a0eaa0ce36", + "lsn": 0, + "upstream": map[string]interface{}{ + "status": "follow", + "idle": 2.4564633660484, + "peer": "other.tarantool:3301", + "lag": 0.00011920928955078, + "message": "'getaddrinfo: Name or service not known'", + "system_message": "Input/output error", + }, + "downstream": map[string]interface{}{ + "status": "follow", + "idle": 2.8306158290943, + "vclock": map[interface{}]interface{}{1: 8}, + "lag": 0, + "message": "'unexpected EOF when reading from socket'", + "system_message": "Broken pipe", + }, + }, + }, + }, + }, + } + for _, tc := range cases { + data, err := msgpack.Marshal(tc.Data) + require.NoError(t, err, tc.Name) + + var result Info + err = msgpack.Unmarshal(data, &result) + require.NoError(t, err, tc.Name) + + require.Equal(t, tc.Struct, result) + } +} diff --git a/box/tarantool_test.go b/box/tarantool_test.go index 3d638b5b5..515eac3d0 100644 --- a/box/tarantool_test.go +++ b/box/tarantool_test.go @@ -31,6 +31,12 @@ func validateInfo(t testing.TB, info box.Info) { require.NotEmpty(t, info.Version) // Check that pid parsed correctly. require.NotEqual(t, info.PID, 0) + + // Check replication is parsed correctly. + require.NotEmpty(t, info.Replication) + + // Check one replica uuid is equal system uuid. + require.Equal(t, info.UUID, info.Replication[1].UUID) } func TestBox_Sugar_Info(t *testing.T) { diff --git a/box/testdata/config.lua b/box/testdata/config.lua index f3ee1a7b2..061439aa1 100644 --- a/box/testdata/config.lua +++ b/box/testdata/config.lua @@ -10,4 +10,7 @@ box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true -- Set listen only when every other thing is configured. box.cfg{ listen = os.getenv("TEST_TNT_LISTEN"), + replication = { + os.getenv("TEST_TNT_LISTEN"), + }, } From 536864682eaaad428cb2491131ad2c8ab8ae0506 Mon Sep 17 00:00:00 2001 From: "maksim.konovalov" Date: Tue, 4 Feb 2025 13:38:44 +0300 Subject: [PATCH 565/605] pool: add Instance info to connection ConnectionInfo Instance info in ConnectionInfo enables pool state monitoring and dynamic tarantool topology tracking. --- CHANGELOG.md | 1 + pool/connection_pool.go | 21 ++++++++++++--- pool/connection_pool_test.go | 51 +++++++++++++++++++++++++++++++----- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82f9766f7..6dc5bdd5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added - Extend box with replication information (#427). +- The Instance info has been added to ConnectionInfo for GetInfo response (#429). ### Changed diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 9be46665e..fa5537a6a 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -93,6 +93,7 @@ ConnectionInfo structure for information about connection statuses: type ConnectionInfo struct { ConnectedNow bool ConnRole Role + Instance Instance } /* @@ -393,13 +394,25 @@ func (p *ConnectionPool) GetInfo() map[string]ConnectionInfo { return info } - for name := range p.ends { + for name, end := range p.ends { conn, role := p.getConnectionFromPool(name) + + connInfo := ConnectionInfo{ + ConnectedNow: false, + ConnRole: UnknownRole, + Instance: Instance{ + Name: name, + Dialer: end.dialer, + Opts: end.opts, + }, + } + if conn != nil { - info[name] = ConnectionInfo{ConnectedNow: conn.ConnectedNow(), ConnRole: role} - } else { - info[name] = ConnectionInfo{ConnectedNow: false, ConnRole: UnknownRole} + connInfo.ConnRole = role + connInfo.ConnectedNow = conn.ConnectedNow() } + + info[name] = connInfo } return info diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index c4e43e2d3..e6f1f50c2 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -242,7 +242,9 @@ func TestConnect_empty(t *testing.T) { func TestConnect_unavailable(t *testing.T) { servers := []string{"err1", "err2"} ctx, cancel := test_helpers.GetPoolConnectContext() - connPool, err := pool.Connect(ctx, makeInstances([]string{"err1", "err2"}, connOpts)) + insts := makeInstances([]string{"err1", "err2"}, connOpts) + + connPool, err := pool.Connect(ctx, insts) cancel() if connPool != nil { @@ -252,8 +254,10 @@ func TestConnect_unavailable(t *testing.T) { require.NoError(t, err, "failed to create a pool") require.NotNilf(t, connPool, "pool is nil after Connect") require.Equal(t, map[string]pool.ConnectionInfo{ - servers[0]: pool.ConnectionInfo{ConnectedNow: false, ConnRole: pool.UnknownRole}, - servers[1]: pool.ConnectionInfo{ConnectedNow: false, ConnRole: pool.UnknownRole}, + servers[0]: pool.ConnectionInfo{ + ConnectedNow: false, ConnRole: pool.UnknownRole, Instance: insts[0]}, + servers[1]: pool.ConnectionInfo{ + ConnectedNow: false, ConnRole: pool.UnknownRole, Instance: insts[1]}, }, connPool.GetInfo()) } @@ -1156,15 +1160,19 @@ func TestConnectionHandlerOpenError(t *testing.T) { } ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() - connPool, err := pool.ConnectWithOpts(ctx, makeInstances(poolServers, connOpts), poolOpts) + + insts := makeInstances(poolServers, connOpts) + connPool, err := pool.ConnectWithOpts(ctx, insts, poolOpts) if err == nil { defer connPool.Close() } require.NoError(t, err, "failed to connect") require.NotNil(t, connPool, "pool expected") require.Equal(t, map[string]pool.ConnectionInfo{ - servers[0]: pool.ConnectionInfo{ConnectedNow: false, ConnRole: pool.UnknownRole}, - servers[1]: pool.ConnectionInfo{ConnectedNow: false, ConnRole: pool.UnknownRole}, + servers[0]: pool.ConnectionInfo{ + ConnectedNow: false, ConnRole: pool.UnknownRole, Instance: insts[0]}, + servers[1]: pool.ConnectionInfo{ + ConnectedNow: false, ConnRole: pool.UnknownRole, Instance: insts[1]}, }, connPool.GetInfo()) connPool.Close() @@ -3495,6 +3503,37 @@ func runTestMain(m *testing.M) int { return m.Run() } +func TestConnectionPool_GetInfo_equal_instance_info(t *testing.T) { + var tCases [][]pool.Instance + + tCases = append(tCases, makeInstances([]string{servers[0], servers[1]}, connOpts)) + tCases = append(tCases, makeInstances([]string{ + servers[0], + servers[1], + servers[3]}, + connOpts)) + tCases = append(tCases, makeInstances([]string{servers[0]}, connOpts)) + + for _, tc := range tCases { + ctx, cancel := test_helpers.GetPoolConnectContext() + connPool, err := pool.Connect(ctx, tc) + cancel() + require.Nilf(t, err, "failed to connect") + require.NotNilf(t, connPool, "conn is nil after Connect") + + info := connPool.GetInfo() + + var infoInstances []pool.Instance + + for _, infoInst := range info { + infoInstances = append(infoInstances, infoInst.Instance) + } + + require.ElementsMatch(t, tc, infoInstances) + connPool.Close() + } +} + func TestMain(m *testing.M) { code := runTestMain(m) os.Exit(code) From a1dc029cab36803e95c7c8a43838eda6396548b4 Mon Sep 17 00:00:00 2001 From: Dmitriy Gertsog Date: Thu, 6 Mar 2025 15:50:36 +0300 Subject: [PATCH 566/605] ci: bump Ubuntu up to 22.04 --- .github/workflows/reusable_testing.yml | 2 +- .github/workflows/testing.yml | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index 352653101..6c821fe51 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -11,7 +11,7 @@ on: jobs: run_tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Clone the go-tarantool connector uses: actions/checkout@v4 diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index d82dc3ef8..758bc89a1 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -23,7 +23,7 @@ jobs: # We could replace it with ubuntu-latest after fixing the bug: # https://github.com/tarantool/setup-tarantool/issues/37 - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false @@ -100,9 +100,10 @@ jobs: run: make deps - name: Run regression tests - run: | - make test - make testrace + run: make test + + - name: Run race tests + run: make testrace - name: Run fuzzing tests if: ${{ matrix.fuzzing }} @@ -116,6 +117,7 @@ jobs: make coveralls - name: Check workability of benchmark tests + if: matrix.golang == 'stable' run: make bench-deps bench DURATION=1x COUNT=1 testing_mac_os: @@ -270,6 +272,10 @@ jobs: run: | cd "${SRCDIR}" make test + + - name: Run race tests + run: | + cd "${SRCDIR}" make testrace - name: Run fuzzing tests @@ -279,6 +285,7 @@ jobs: make fuzzing TAGS="go_tarantool_decimal_fuzzing" - name: Check workability of benchmark tests + if: matrix.golang == 'stable' run: | cd "${SRCDIR}" make bench-deps bench DURATION=1x COUNT=1 From 344cc02be811b9c5a05d7e169b99455bcdaab3a1 Mon Sep 17 00:00:00 2001 From: Dmitriy Gertsog Date: Fri, 7 Mar 2025 16:04:28 +0300 Subject: [PATCH 567/605] tests: turn off coverage checks for test_helpers --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ce3505afc..556fad418 100644 --- a/Makefile +++ b/Makefile @@ -108,7 +108,7 @@ test-main: coverage: go clean -testcache go get golang.org/x/tools/cmd/cover - go test -tags "$(TAGS)" ./... -v -p 1 -covermode=atomic -coverprofile=$(COVERAGE_FILE) + go test -tags "$(TAGS)" $(go list ./... | grep -v test_helpers) -v -p 1 -covermode=atomic -coverprofile=$(COVERAGE_FILE) go tool cover -func=$(COVERAGE_FILE) .PHONY: coveralls From 09a9e011cedc1d656abe3d3e5d73ad374ba27ba9 Mon Sep 17 00:00:00 2001 From: Dmitriy Gertsog Date: Thu, 6 Mar 2025 15:49:02 +0300 Subject: [PATCH 568/605] tests: add helpers for TcS Simple helpers to make easy create tests required Taranatool centralized configuration storage. --- CHANGELOG.md | 12 ++ pool/connection_pool_test.go | 14 +- queue/queue_test.go | 4 +- shutdown_test.go | 83 +++------ tarantool_test.go | 4 +- test_helpers/main.go | 259 ++++++++++++++++++++++---- test_helpers/pool_helper.go | 6 +- test_helpers/tcs/prepare.go | 66 +++++++ test_helpers/tcs/tcs.go | 180 ++++++++++++++++++ test_helpers/tcs/testdata/config.yaml | 39 ++++ test_helpers/utils.go | 26 +++ 11 files changed, 579 insertions(+), 114 deletions(-) create mode 100644 test_helpers/tcs/prepare.go create mode 100644 test_helpers/tcs/tcs.go create mode 100644 test_helpers/tcs/testdata/config.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc5bdd5b..1d595e230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,23 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Extend box with replication information (#427). - The Instance info has been added to ConnectionInfo for GetInfo response (#429). +- Added helpers to run Tarantool config storage (#431). ### Changed +- Changed helpers API `StartTarantool` and `StopTarantool`, now it uses + pointer on `TarantoolInstance`: + * `StartTarantool()` returns `*TarantoolInstance`; + * `StopTarantool()` and `StopTarantoolWithCleanup()` accepts + `*TarantoolInstance` as arguments. +- Field `Cmd` in `TarantoolInstance` struct declared as deprecated. + Suggested `Wait()`, `Stop()` and `Signal()` methods as safer to use + instead of direct `Cmd.Process` access (#431). + ### Fixed +- Fixed flaky test detection on fail start Tarantool instance (#431). + ## [v2.2.1] - 2024-12-17 The release fixes a schema lost after a reconnect. diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index e6f1f50c2..323ce37c3 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -98,7 +98,7 @@ var connOpts = tarantool.Opts{ var defaultCountRetry = 5 var defaultTimeoutRetry = 500 * time.Millisecond -var helpInstances []test_helpers.TarantoolInstance +var helpInstances []*test_helpers.TarantoolInstance func TestConnect_error_duplicate(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() @@ -404,7 +404,7 @@ func TestReconnect(t *testing.T) { defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) - err = test_helpers.RestartTarantool(&helpInstances[0]) + err = test_helpers.RestartTarantool(helpInstances[0]) require.Nilf(t, err, "failed to restart tarantool") args = test_helpers.CheckStatusesArgs{ @@ -453,7 +453,7 @@ func TestDisconnect_withReconnect(t *testing.T) { require.Nil(t, err) // Restart the server after success. - err = test_helpers.RestartTarantool(&helpInstances[serverId]) + err = test_helpers.RestartTarantool(helpInstances[serverId]) require.Nilf(t, err, "failed to restart tarantool") args = test_helpers.CheckStatusesArgs{ @@ -501,10 +501,10 @@ func TestDisconnectAll(t *testing.T) { defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) - err = test_helpers.RestartTarantool(&helpInstances[0]) + err = test_helpers.RestartTarantool(helpInstances[0]) require.Nilf(t, err, "failed to restart tarantool") - err = test_helpers.RestartTarantool(&helpInstances[1]) + err = test_helpers.RestartTarantool(helpInstances[1]) require.Nilf(t, err, "failed to restart tarantool") args = test_helpers.CheckStatusesArgs{ @@ -1362,10 +1362,10 @@ func TestRequestOnClosed(t *testing.T) { _, err = connPool.Ping(pool.ANY) require.NotNilf(t, err, "err is nil after Ping") - err = test_helpers.RestartTarantool(&helpInstances[0]) + err = test_helpers.RestartTarantool(helpInstances[0]) require.Nilf(t, err, "failed to restart tarantool") - err = test_helpers.RestartTarantool(&helpInstances[1]) + err = test_helpers.RestartTarantool(helpInstances[1]) require.Nilf(t, err, "failed to restart tarantool") } diff --git a/queue/queue_test.go b/queue/queue_test.go index d23bd2c9c..e43c47113 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -23,8 +23,6 @@ const ( var servers = []string{"127.0.0.1:3014", "127.0.0.1:3015"} var server = "127.0.0.1:3013" -var instances []test_helpers.TarantoolInstance - var dialer = NetDialer{ Address: server, User: user, @@ -931,7 +929,7 @@ func runTestMain(m *testing.M) int { }) } - instances, err = test_helpers.StartTarantoolInstances(poolInstsOpts) + instances, err := test_helpers.StartTarantoolInstances(poolInstsOpts) if err != nil { log.Printf("Failed to prepare test tarantool pool: %s", err) diff --git a/shutdown_test.go b/shutdown_test.go index b3a09eff0..4df34aef5 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -78,7 +78,7 @@ func testGracefulShutdown(t *testing.T, conn *Connection, inst *test_helpers.Tar // SIGTERM the server. shutdownStart := time.Now() - require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM)) + require.Nil(t, inst.Signal(syscall.SIGTERM)) // Check that we can't send new requests after shutdown starts. // Retry helps to wait a bit until server starts to shutdown @@ -108,14 +108,11 @@ func testGracefulShutdown(t *testing.T, conn *Connection, inst *test_helpers.Tar // Wait until server go down. // Server will go down only when it process all requests from our connection // (or on timeout). - _, err = inst.Cmd.Process.Wait() + err = inst.Wait() require.Nil(t, err) shutdownFinish := time.Now() shutdownTime := shutdownFinish.Sub(shutdownStart) - // Help test helpers to properly clean up. - inst.Cmd.Process = nil - // Check that it wasn't a timeout. require.Lessf(t, shutdownTime, @@ -129,18 +126,16 @@ func testGracefulShutdown(t *testing.T, conn *Connection, inst *test_helpers.Tar func TestGracefulShutdown(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) - var inst test_helpers.TarantoolInstance var conn *Connection - var err error - inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + inst, err := test_helpers.StartTarantool(shtdnSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) conn = test_helpers.ConnectWithValidation(t, shtdnDialer, shtdnClntOpts) defer conn.Close() - testGracefulShutdown(t, conn, &inst) + testGracefulShutdown(t, conn, inst) } func TestCloseGraceful(t *testing.T) { @@ -190,26 +185,23 @@ func TestCloseGraceful(t *testing.T) { func TestGracefulShutdownWithReconnect(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) - var inst test_helpers.TarantoolInstance - var err error - - inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + inst, err := test_helpers.StartTarantool(shtdnSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) conn := test_helpers.ConnectWithValidation(t, shtdnDialer, shtdnClntOpts) defer conn.Close() - testGracefulShutdown(t, conn, &inst) + testGracefulShutdown(t, conn, inst) - err = test_helpers.RestartTarantool(&inst) + err = test_helpers.RestartTarantool(inst) require.Nilf(t, err, "Failed to restart tarantool") connected := test_helpers.WaitUntilReconnected(conn, shtdnClntOpts.MaxReconnects, shtdnClntOpts.Reconnect) require.Truef(t, connected, "Reconnect success") - testGracefulShutdown(t, conn, &inst) + testGracefulShutdown(t, conn, inst) } func TestNoGracefulShutdown(t *testing.T) { @@ -219,14 +211,12 @@ func TestNoGracefulShutdown(t *testing.T) { noShtdDialer.RequiredProtocolInfo = ProtocolInfo{} test_helpers.SkipIfWatchersSupported(t) - var inst test_helpers.TarantoolInstance var conn *Connection - var err error testSrvOpts := shtdnSrvOpts testSrvOpts.Dialer = noShtdDialer - inst, err = test_helpers.StartTarantool(testSrvOpts) + inst, err := test_helpers.StartTarantool(testSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) @@ -249,21 +239,18 @@ func TestNoGracefulShutdown(t *testing.T) { // SIGTERM the server. shutdownStart := time.Now() - require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM)) + require.Nil(t, inst.Signal(syscall.SIGTERM)) // Check that request was interrupted. _, err = fut.Get() require.NotNilf(t, err, "sleep request error") // Wait until server go down. - _, err = inst.Cmd.Process.Wait() + err = inst.Wait() require.Nil(t, err) shutdownFinish := time.Now() shutdownTime := shutdownFinish.Sub(shutdownStart) - // Help test helpers to properly clean up. - inst.Cmd.Process = nil - // Check that server finished without waiting for eval to finish. require.Lessf(t, shutdownTime, @@ -274,11 +261,9 @@ func TestNoGracefulShutdown(t *testing.T) { func TestGracefulShutdownRespectsClose(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) - var inst test_helpers.TarantoolInstance var conn *Connection - var err error - inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + inst, err := test_helpers.StartTarantool(shtdnSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) @@ -314,7 +299,7 @@ func TestGracefulShutdownRespectsClose(t *testing.T) { // SIGTERM the server. shutdownStart := time.Now() - require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM)) + require.Nil(t, inst.Signal(syscall.SIGTERM)) // Close the connection. conn.Close() @@ -327,14 +312,11 @@ func TestGracefulShutdownRespectsClose(t *testing.T) { require.NotNilf(t, err, "sleep request error") // Wait until server go down. - _, err = inst.Cmd.Process.Wait() + err = inst.Wait() require.Nil(t, err) shutdownFinish := time.Now() shutdownTime := shutdownFinish.Sub(shutdownStart) - // Help test helpers to properly clean up. - inst.Cmd.Process = nil - // Check that server finished without waiting for eval to finish. require.Lessf(t, shutdownTime, @@ -354,11 +336,9 @@ func TestGracefulShutdownRespectsClose(t *testing.T) { func TestGracefulShutdownNotRacesWithRequestReconnect(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) - var inst test_helpers.TarantoolInstance var conn *Connection - var err error - inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + inst, err := test_helpers.StartTarantool(shtdnSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) @@ -397,16 +377,13 @@ func TestGracefulShutdownNotRacesWithRequestReconnect(t *testing.T) { fut := conn.Do(req) // SIGTERM the server. - require.Nil(t, inst.Cmd.Process.Signal(syscall.SIGTERM)) + require.Nil(t, inst.Signal(syscall.SIGTERM)) // Wait until server go down. // Server is expected to go down on timeout. - _, err = inst.Cmd.Process.Wait() + err = inst.Wait() require.Nil(t, err) - // Help test helpers to properly clean up. - inst.Cmd.Process = nil - // Check that request failed by server disconnect, not a client timeout. _, err = fut.Get() require.NotNil(t, err) @@ -425,11 +402,9 @@ func TestGracefulShutdownNotRacesWithRequestReconnect(t *testing.T) { func TestGracefulShutdownCloseConcurrent(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) - var inst test_helpers.TarantoolInstance - var err error var srvShtdnStart, srvShtdnFinish time.Time - inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + inst, err := test_helpers.StartTarantool(shtdnSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) @@ -487,22 +462,19 @@ func TestGracefulShutdownCloseConcurrent(t *testing.T) { go func(inst *test_helpers.TarantoolInstance) { srvToStop.Wait() srvShtdnStart = time.Now() - cerr := inst.Cmd.Process.Signal(syscall.SIGTERM) + cerr := inst.Signal(syscall.SIGTERM) if cerr != nil { sret = cerr } srvStop.Done() - }(&inst) + }(inst) srvStop.Wait() require.Nil(t, sret, "No errors on server SIGTERM") - _, err = inst.Cmd.Process.Wait() + err = inst.Wait() require.Nil(t, err) - // Help test helpers to properly clean up. - inst.Cmd.Process = nil - srvShtdnFinish = time.Now() srvShtdnTime := srvShtdnFinish.Sub(srvShtdnStart) @@ -515,11 +487,9 @@ func TestGracefulShutdownCloseConcurrent(t *testing.T) { func TestGracefulShutdownConcurrent(t *testing.T) { test_helpers.SkipIfWatchersUnsupported(t) - var inst test_helpers.TarantoolInstance - var err error var srvShtdnStart, srvShtdnFinish time.Time - inst, err = test_helpers.StartTarantool(shtdnSrvOpts) + inst, err := test_helpers.StartTarantool(shtdnSrvOpts) require.Nil(t, err) defer test_helpers.StopTarantoolWithCleanup(inst) @@ -577,12 +547,12 @@ func TestGracefulShutdownConcurrent(t *testing.T) { go func(inst *test_helpers.TarantoolInstance) { srvToStop.Wait() srvShtdnStart = time.Now() - cerr := inst.Cmd.Process.Signal(syscall.SIGTERM) + cerr := inst.Signal(syscall.SIGTERM) if cerr != nil { sret = cerr } srvStop.Done() - }(&inst) + }(inst) srvStop.Wait() require.Nil(t, sret, "No errors on server SIGTERM") @@ -590,12 +560,9 @@ func TestGracefulShutdownConcurrent(t *testing.T) { caseWg.Wait() require.Nil(t, ret, "No errors on concurrent wait") - _, err = inst.Cmd.Process.Wait() + err = inst.Wait() require.Nil(t, err) - // Help test helpers to properly clean up. - inst.Cmd.Process = nil - srvShtdnFinish = time.Now() srvShtdnTime := srvShtdnFinish.Sub(srvShtdnStart) diff --git a/tarantool_test.go b/tarantool_test.go index e6adebc6d..2be3e793b 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3609,7 +3609,7 @@ func TestConnection_NewWatcher_reconnect(t *testing.T) { <-events test_helpers.StopTarantool(inst) - if err := test_helpers.RestartTarantool(&inst); err != nil { + if err := test_helpers.RestartTarantool(inst); err != nil { t.Fatalf("Unable to restart Tarantool: %s", err) } @@ -3902,7 +3902,7 @@ func TestConnection_named_index_after_reconnect(t *testing.T) { t.Fatalf("An error expected.") } - if err := test_helpers.RestartTarantool(&inst); err != nil { + if err := test_helpers.RestartTarantool(inst); err != nil { t.Fatalf("Unable to restart Tarantool: %s", err) } diff --git a/test_helpers/main.go b/test_helpers/main.go index f452f3e97..4ebe4c622 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -15,6 +15,7 @@ import ( "errors" "fmt" "io" + "io/fs" "log" "os" "os/exec" @@ -33,6 +34,14 @@ type StartOpts struct { // InitScript is a Lua script for tarantool to run on start. InitScript string + // ConfigFile is a path to a configuration file for a Tarantool instance. + // Required in pair with InstanceName. + ConfigFile string + + // InstanceName is a name of an instance to run. + // Required in pair with ConfigFile. + InstanceName string + // Listen is box.cfg listen parameter for tarantool. // Use this address to connect to tarantool after configuration. // https://www.tarantool.io/en/doc/latest/reference/configuration/#cfg-basic-listen @@ -67,9 +76,19 @@ type StartOpts struct { Dialer tarantool.Dialer } +type state struct { + done chan struct{} + ret error + stopped bool +} + // TarantoolInstance is a data for instance graceful shutdown and cleanup. type TarantoolInstance struct { // Cmd is a Tarantool command. Used to kill Tarantool process. + // + // Deprecated: Cmd field will be removed in the next major version. + // Use `Wait()` and `Stop()` methods, instead of calling `Cmd.Process.Wait()` or + // `Cmd.Process.Kill()` directly. Cmd *exec.Cmd // Options for restarting a tarantool instance. @@ -77,6 +96,89 @@ type TarantoolInstance struct { // Dialer to check that connection established. Dialer tarantool.Dialer + + st chan state +} + +// IsExit checks if Tarantool process was terminated. +func (t *TarantoolInstance) IsExit() bool { + st := <-t.st + t.st <- st + + select { + case <-st.done: + default: + return false + } + + return st.ret != nil +} + +func (t *TarantoolInstance) result() error { + st := <-t.st + t.st <- st + + select { + case <-st.done: + default: + return nil + } + + return st.ret +} + +func (t *TarantoolInstance) checkDone() { + ret := t.Cmd.Wait() + + st := <-t.st + + st.ret = ret + close(st.done) + + t.st <- st + + if !st.stopped { + log.Printf("Tarantool %q was unexpectedly terminated: %v", t.Opts.Listen, t.result()) + } +} + +// Wait waits until Tarantool process is terminated. +// Returns error as process result status. +func (t *TarantoolInstance) Wait() error { + st := <-t.st + t.st <- st + + <-st.done + t.Cmd.Process = nil + + st = <-t.st + t.st <- st + + return st.ret +} + +// Stop terminates Tarantool process and waits until it exit. +func (t *TarantoolInstance) Stop() error { + st := <-t.st + st.stopped = true + t.st <- st + + if t.IsExit() { + return nil + } + if t.Cmd != nil && t.Cmd.Process != nil { + if err := t.Cmd.Process.Kill(); err != nil && !t.IsExit() { + return fmt.Errorf("failed to kill tarantool %q (pid %d), got %s", + t.Opts.Listen, t.Cmd.Process.Pid, err) + } + t.Wait() + } + return nil +} + +// Signal sends a signal to the Tarantool instance. +func (t *TarantoolInstance) Signal(sig os.Signal) error { + return t.Cmd.Process.Signal(sig) } func isReady(dialer tarantool.Dialer, opts *tarantool.Opts) error { @@ -108,7 +210,7 @@ var ( ) func init() { - tarantoolVersionRegexp = regexp.MustCompile(`Tarantool (?:Enterprise )?(\d+)\.(\d+)\.(\d+).*`) + tarantoolVersionRegexp = regexp.MustCompile(`Tarantool (Enterprise )?(\d+)\.(\d+)\.(\d+).*`) } // atoiUint64 parses string to uint64. @@ -145,15 +247,15 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) ( return true, fmt.Errorf("failed to parse output %q", out) } - if major, err = atoiUint64(parsed[1]); err != nil { + if major, err = atoiUint64(parsed[2]); err != nil { return true, fmt.Errorf("failed to parse major from output %q: %w", out, err) } - if minor, err = atoiUint64(parsed[2]); err != nil { + if minor, err = atoiUint64(parsed[3]); err != nil { return true, fmt.Errorf("failed to parse minor from output %q: %w", out, err) } - if patch, err = atoiUint64(parsed[3]); err != nil { + if patch, err = atoiUint64(parsed[4]); err != nil { return true, fmt.Errorf("failed to parse patch from output %q: %w", out, err) } @@ -166,6 +268,21 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) ( } } +// IsTarantoolEE checks if Tarantool is Enterprise edition. +func IsTarantoolEE() (bool, error) { + out, err := exec.Command(getTarantoolExec(), "--version").Output() + if err != nil { + return true, err + } + + parsed := tarantoolVersionRegexp.FindStringSubmatch(string(out)) + if parsed == nil { + return true, fmt.Errorf("failed to parse output %q", out) + } + + return parsed[1] != "", nil +} + // RestartTarantool restarts a tarantool instance for tests // with specifies parameters (refer to StartOpts) // which were specified in inst parameter. @@ -175,42 +292,96 @@ func IsTarantoolVersionLess(majorMin uint64, minorMin uint64, patchMin uint64) ( // Process must be stopped with StopTarantool. func RestartTarantool(inst *TarantoolInstance) error { startedInst, err := StartTarantool(inst.Opts) + inst.Cmd.Process = startedInst.Cmd.Process + inst.st = startedInst.st + return err } +func removeByMask(dir string, masks ...string) error { + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if d.IsDir() { + return nil + } + + for _, mask := range masks { + if ok, err := filepath.Match(mask, d.Name()); err != nil { + return err + } else if ok { + if err = os.Remove(path); err != nil { + return err + } + } + } + return nil + }) + + if err != nil { + return err + } + return nil +} + +func prepareDir(workDir string) (string, error) { + if workDir == "" { + dir, err := os.MkdirTemp("", "work_dir") + if err != nil { + return "", err + } + return dir, nil + } + // Create work_dir. + err := os.MkdirAll(workDir, 0755) + if err != nil { + return "", err + } + + // Clean up existing work_dir. + err = removeByMask(workDir, "*.snap", "*.xlog") + if err != nil { + return "", err + } + return workDir, nil +} + // StartTarantool starts a tarantool instance for tests // with specifies parameters (refer to StartOpts). // Process must be stopped with StopTarantool. -func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { +func StartTarantool(startOpts StartOpts) (*TarantoolInstance, error) { // Prepare tarantool command. - var inst TarantoolInstance - var dir string - var err error + inst := &TarantoolInstance{ + st: make(chan state, 1), + } + init := state{ + done: make(chan struct{}), + } + inst.st <- init + var err error inst.Dialer = startOpts.Dialer + startOpts.WorkDir, err = prepareDir(startOpts.WorkDir) + if err != nil { + return inst, fmt.Errorf("failed to prepare working dir %q: %w", startOpts.WorkDir, err) + } - if startOpts.WorkDir == "" { - dir, err = os.MkdirTemp("", "work_dir") - if err != nil { - return inst, err - } - startOpts.WorkDir = dir - } else { - // Clean up existing work_dir. - err = os.RemoveAll(startOpts.WorkDir) - if err != nil { - return inst, err - } - - // Create work_dir. - err = os.Mkdir(startOpts.WorkDir, 0755) - if err != nil { - return inst, err + args := []string{} + if startOpts.InitScript != "" { + if !filepath.IsAbs(startOpts.InitScript) { + cwd, err := os.Getwd() + if err != nil { + return inst, fmt.Errorf("failed to get current working directory: %w", err) + } + startOpts.InitScript = filepath.Join(cwd, startOpts.InitScript) } + args = append(args, startOpts.InitScript) } - - inst.Cmd = exec.Command(getTarantoolExec(), startOpts.InitScript) + if startOpts.ConfigFile != "" && startOpts.InstanceName != "" { + args = append(args, "--config", startOpts.ConfigFile) + args = append(args, "--name", startOpts.InstanceName) + } + inst.Cmd = exec.Command(getTarantoolExec(), args...) + inst.Cmd.Dir = startOpts.WorkDir inst.Cmd.Env = append( os.Environ(), @@ -242,6 +413,8 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { // see https://github.com/tarantool/go-tarantool/issues/136 time.Sleep(startOpts.WaitStart) + go inst.checkDone() + opts := tarantool.Opts{ Timeout: 500 * time.Millisecond, SkipSchema: true, @@ -261,24 +434,28 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { } } - return inst, err + if inst.IsExit() && inst.result() != nil { + StopTarantool(inst) + return nil, fmt.Errorf("unexpected terminated Tarantool %q: %w", + inst.Opts.Listen, inst.result()) + } + + if err != nil { + StopTarantool(inst) + return nil, fmt.Errorf("failed to connect Tarantool %q: %w", + inst.Opts.Listen, err) + } + + return inst, nil } // StopTarantool stops a tarantool instance started // with StartTarantool. Waits until any resources // associated with the process is released. If something went wrong, fails. -func StopTarantool(inst TarantoolInstance) { - if inst.Cmd != nil && inst.Cmd.Process != nil { - if err := inst.Cmd.Process.Kill(); err != nil { - log.Fatalf("Failed to kill tarantool (pid %d), got %s", inst.Cmd.Process.Pid, err) - } - - // Wait releases any resources associated with the Process. - if _, err := inst.Cmd.Process.Wait(); err != nil { - log.Fatalf("Failed to wait for Tarantool process to exit, got %s", err) - } - - inst.Cmd.Process = nil +func StopTarantool(inst *TarantoolInstance) { + err := inst.Stop() + if err != nil { + log.Fatal(err) } } @@ -286,7 +463,7 @@ func StopTarantool(inst TarantoolInstance) { // with StartTarantool. Waits until any resources // associated with the process is released. // Cleans work directory after stop. If something went wrong, fails. -func StopTarantoolWithCleanup(inst TarantoolInstance) { +func StopTarantoolWithCleanup(inst *TarantoolInstance) { StopTarantool(inst) if inst.Opts.WorkDir != "" { diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index 729a69e44..b67d05f02 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -227,8 +227,8 @@ func SetClusterRO(dialers []tarantool.Dialer, connOpts tarantool.Opts, return nil } -func StartTarantoolInstances(instsOpts []StartOpts) ([]TarantoolInstance, error) { - instances := make([]TarantoolInstance, 0, len(instsOpts)) +func StartTarantoolInstances(instsOpts []StartOpts) ([]*TarantoolInstance, error) { + instances := make([]*TarantoolInstance, 0, len(instsOpts)) for _, opts := range instsOpts { instance, err := StartTarantool(opts) @@ -243,7 +243,7 @@ func StartTarantoolInstances(instsOpts []StartOpts) ([]TarantoolInstance, error) return instances, nil } -func StopTarantoolInstances(instances []TarantoolInstance) { +func StopTarantoolInstances(instances []*TarantoolInstance) { for _, instance := range instances { StopTarantoolWithCleanup(instance) } diff --git a/test_helpers/tcs/prepare.go b/test_helpers/tcs/prepare.go new file mode 100644 index 000000000..c7b87d0f6 --- /dev/null +++ b/test_helpers/tcs/prepare.go @@ -0,0 +1,66 @@ +package tcs + +import ( + _ "embed" + "fmt" + "os" + "path/filepath" + "text/template" + "time" + + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" +) + +const ( + waitTimeout = 500 * time.Millisecond + connectRetry = 3 + tcsUser = "client" + tcsPassword = "secret" +) + +//go:embed testdata/config.yaml +var tcsConfig []byte + +func writeConfig(name string, port int) error { + cfg, err := os.Create(name) + if err != nil { + return err + } + defer cfg.Close() + + cfg.Chmod(0644) + + t := template.Must(template.New("config").Parse(string(tcsConfig))) + return t.Execute(cfg, map[string]interface{}{ + "host": "localhost", + "port": port, + }) +} + +func makeOpts(port int) (test_helpers.StartOpts, error) { + opts := test_helpers.StartOpts{} + var err error + opts.WorkDir, err = os.MkdirTemp("", "tcs_dir") + if err != nil { + return opts, err + } + + opts.ConfigFile = filepath.Join(opts.WorkDir, "config.yaml") + err = writeConfig(opts.ConfigFile, port) + if err != nil { + return opts, fmt.Errorf("can't save file %q: %w", opts.ConfigFile, err) + } + + opts.Listen = fmt.Sprintf("localhost:%d", port) + opts.WaitStart = waitTimeout + opts.ConnectRetry = connectRetry + opts.RetryTimeout = waitTimeout + opts.InstanceName = "master" + opts.Dialer = tarantool.NetDialer{ + Address: opts.Listen, + User: tcsUser, + Password: tcsPassword, + } + return opts, nil +} diff --git a/test_helpers/tcs/tcs.go b/test_helpers/tcs/tcs.go new file mode 100644 index 000000000..1ba26a19e --- /dev/null +++ b/test_helpers/tcs/tcs.go @@ -0,0 +1,180 @@ +package tcs + +import ( + "context" + "errors" + "fmt" + "net" + "testing" + + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" +) + +// ErrNotSupported identifies result of `Start()` why storage was not started. +var ErrNotSupported = errors.New("required Tarantool EE 3.3+") + +// ErrNoValue used to show that `Get()` was successful, but no values were found. +var ErrNoValue = errors.New("required value not found") + +// TCS is a Tarantool centralized configuration storage connection. +type TCS struct { + inst *test_helpers.TarantoolInstance + conn *tarantool.Connection + tb testing.TB + port int +} + +// dataResponse content of TcS response in data array. +type dataResponse struct { + Path string `msgpack:"path"` + Value string `msgpack:"value"` + ModRevision int64 `msgpack:"mod_revision"` +} + +// findEmptyPort returns some random unused port if @port is passed with zero. +func findEmptyPort(port int) (int, error) { + listener, err := net.Listen("tcp4", fmt.Sprintf(":%d", port)) + if err != nil { + return 0, err + } + defer listener.Close() + + addr := listener.Addr().(*net.TCPAddr) + return addr.Port, nil +} + +// Start starts a Tarantool centralized configuration storage. +// Use `port = 0` to use any unused port. +// Returns a Tcs instance and a cleanup function. +func Start(port int) (TCS, error) { + tcs := TCS{} + if ok, err := test_helpers.IsTcsSupported(); !ok || err != nil { + return tcs, errors.Join(ErrNotSupported, err) + } + var err error + tcs.port, err = findEmptyPort(port) + if err != nil { + if port == 0 { + return tcs, fmt.Errorf("failed to detect an empty port: %w", err) + } else { + return tcs, fmt.Errorf("port %d can't be used: %w", port, err) + } + } + + opts, err := makeOpts(tcs.port) + if err != nil { + return tcs, err + } + + tcs.inst, err = test_helpers.StartTarantool(opts) + if err != nil { + return tcs, fmt.Errorf("failed to start Tarantool config storage: %w", err) + } + + tcs.conn, err = tarantool.Connect(context.Background(), tcs.inst.Dialer, tarantool.Opts{}) + if err != nil { + return tcs, fmt.Errorf("failed to connect to Tarantool config storage: %w", err) + } + + return tcs, nil +} + +// Start starts a Tarantool centralized configuration storage. +// Returns a Tcs instance and a cleanup function. +func StartTesting(tb testing.TB, port int) TCS { + tcs, err := Start(port) + if err != nil { + tb.Fatal(err) + } + return tcs +} + +// Doer returns interface for interacting with Tarantool. +func (t *TCS) Doer() tarantool.Doer { + return t.conn +} + +// Dialer returns a dialer to connect to Tarantool. +func (t *TCS) Dialer() tarantool.Dialer { + return t.inst.Dialer +} + +// Endpoints returns a list of addresses to connect. +func (t *TCS) Endpoints() []string { + return []string{fmt.Sprintf("127.0.0.1:%d", t.port)} +} + +// Credentials returns a user name and password to connect. +func (t *TCS) Credentials() (string, string) { + return tcsUser, tcsPassword +} + +// Stop stops the Tarantool centralized configuration storage. +func (t *TCS) Stop() { + if t.tb != nil { + t.tb.Helper() + } + if t.conn != nil { + t.conn.Close() + } + test_helpers.StopTarantoolWithCleanup(t.inst) +} + +// Put implements "config.storage.put" method. +func (t *TCS) Put(ctx context.Context, path string, value string) error { + if t.tb != nil { + t.tb.Helper() + } + req := tarantool.NewCallRequest("config.storage.put"). + Args([]any{path, value}). + Context(ctx) + if _, err := t.conn.Do(req).GetResponse(); err != nil { + return fmt.Errorf("failed to save data to tarantool: %w", err) + } + return nil +} + +// Delete implements "config.storage.delete" method. +func (t *TCS) Delete(ctx context.Context, path string) error { + if t.tb != nil { + t.tb.Helper() + } + req := tarantool.NewCallRequest("config.storage.delete"). + Args([]any{path}). + Context(ctx) + if _, err := t.conn.Do(req).GetResponse(); err != nil { + return fmt.Errorf("failed to delete data from tarantool: %w", err) + } + return nil +} + +// Get implements "config.storage.get" method. +func (t *TCS) Get(ctx context.Context, path string) (string, error) { + if t.tb != nil { + t.tb.Helper() + } + req := tarantool.NewCallRequest("config.storage.get"). + Args([]any{path}). + Context(ctx) + + resp := []struct { + Data []dataResponse `msgpack:"data"` + }{} + + err := t.conn.Do(req).GetTyped(&resp) + if err != nil { + return "", fmt.Errorf("failed to fetch data from tarantool: %w", err) + } + if len(resp) != 1 { + return "", errors.New("unexpected response from tarantool") + } + if len(resp[0].Data) == 0 { + return "", ErrNoValue + } + if len(resp[0].Data) != 1 { + return "", errors.New("too much data in response from tarantool") + } + + return resp[0].Data[0].Value, nil +} diff --git a/test_helpers/tcs/testdata/config.yaml b/test_helpers/tcs/testdata/config.yaml new file mode 100644 index 000000000..5d42e32aa --- /dev/null +++ b/test_helpers/tcs/testdata/config.yaml @@ -0,0 +1,39 @@ +credentials: + users: + replicator: + password: "topsecret" + roles: [replication] + client: + password: "secret" + privileges: + - permissions: [execute] + universe: true + - permissions: [read, write] + spaces: [config_storage, config_storage_meta] + +iproto: + advertise: + peer: + login: replicator + +replication: + failover: election + +database: + use_mvcc_engine: true + +groups: + group-001: + replicasets: + replicaset-001: + roles: [config.storage] + roles_cfg: + config_storage: + status_check_interval: 3 + instances: + master: + iproto: + listen: + - uri: "{{.host}}:{{.port}}" + params: + transport: plain diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 049dff92d..eff7e1dbe 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -217,6 +217,32 @@ func SkipIfCrudSpliceBroken(t *testing.T) { SkipIfFeatureUnsupported(t, "crud update splice", 2, 0, 0) } +// IsTcsSupported checks if Tarantool supports centralized storage. +// Tarantool supports centralized storage with Enterprise since 3.3.0 version. +func IsTcsSupported() (bool, error) { + + if isEe, err := IsTarantoolEE(); !isEe || err != nil { + return false, err + } + if isLess, err := IsTarantoolVersionLess(3, 3, 0); isLess || err != nil { + return false, err + } + return true, nil +} + +// SkipIfTCSUnsupported skips test if no centralized storage support. +func SkipIfTcsUnsupported(t testing.TB) { + t.Helper() + + ok, err := IsTcsSupported() + if err != nil { + t.Fatalf("Could not check the Tarantool version: %s", err) + } + if !ok { + t.Skip("not found Tarantool EE 3.3+") + } +} + // CheckEqualBoxErrors checks equivalence of tarantool.BoxError objects. // // Tarantool errors are not comparable by nature: From 252c3b783db122b2824bc51bf702506274067580 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 10 Mar 2025 17:09:39 +0300 Subject: [PATCH 569/605] Release v2.3.0 --- CHANGELOG.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d595e230..c839c1747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. -## [Unreleased] +## [v2.3.0] - 2025-03-11 + +The release extends box.info responses and ConnectionPool.GetInfo return data. + +Be careful, we have changed the test_helpers package a little since we do not +support backward compatibility for it. ### Added - Extend box with replication information (#427). -- The Instance info has been added to ConnectionInfo for GetInfo response (#429). +- The Instance info has been added to ConnectionInfo for ConnectionPool.GetInfo + response (#429). - Added helpers to run Tarantool config storage (#431). ### Changed @@ -27,7 +33,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed -- Fixed flaky test detection on fail start Tarantool instance (#431). +- Test helpers does not detect a fail to start a Tarantool instance if + another Tarantool instance already listens a port (#431). ## [v2.2.1] - 2024-12-17 From c0a8ad307468471132652fb3829525b813762bc7 Mon Sep 17 00:00:00 2001 From: Mergen Imeev Date: Wed, 26 Mar 2025 14:02:49 +0300 Subject: [PATCH 570/605] api: block Connect() on failure if Reconnect > 0 This patch makes Connect() retry connection attempts if opts.Reconnect is greater than 0. The delay between connection attempts is opts.Reconnect. If opts.MaxReconnects > 0 then the maximum number of attempts is equal to it, otherwise the maximum number of attempts is unlimited. Connect() now also blocks until a connection is established, provided context is cancelled or the number of attempts is exhausted. Closes #436 --- CHANGELOG.md | 13 ++++++++ connection.go | 68 +++++++++++++++++++++++++++++++--------- tarantool_test.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c839c1747..00049a9c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. +## [Unreleased] + +### Added + +### Changed + +- Connect() now retry the connection if a failure occurs and opts.Reconnect > 0. + The number of attempts is equal to opts.MaxReconnects or unlimited if + opts.MaxReconnects == 0. Connect() blocks until a connection is established, + the context is cancelled, or the number of attempts is exhausted (#436). + +### Fixed + ## [v2.3.0] - 2025-03-11 The release extends box.info responses and ConnectionPool.GetInfo return data. diff --git a/connection.go b/connection.go index d00b52a57..97f9dbbca 100644 --- a/connection.go +++ b/connection.go @@ -92,12 +92,24 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac case LogReconnectFailed: reconnects := v[0].(uint) err := v[1].(error) - log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s", - reconnects, conn.opts.MaxReconnects, conn.Addr(), err) + addr := conn.Addr() + if addr == nil { + log.Printf("tarantool: connect (%d/%d) failed: %s", + reconnects, conn.opts.MaxReconnects, err) + } else { + log.Printf("tarantool: reconnect (%d/%d) to %s failed: %s", + reconnects, conn.opts.MaxReconnects, addr, err) + } case LogLastReconnectFailed: err := v[0].(error) - log.Printf("tarantool: last reconnect to %s failed: %s, giving it up", - conn.Addr(), err) + addr := conn.Addr() + if addr == nil { + log.Printf("tarantool: last connect failed: %s, giving it up", + err) + } else { + log.Printf("tarantool: last reconnect to %s failed: %s, giving it up", + addr, err) + } case LogUnexpectedResultId: header := v[0].(Header) log.Printf("tarantool: connection %s got unexpected request ID (%d) in response "+ @@ -362,8 +374,20 @@ func Connect(ctx context.Context, dialer Dialer, opts Opts) (conn *Connection, e conn.cond = sync.NewCond(&conn.mutex) - if err = conn.createConnection(ctx); err != nil { - return nil, err + if conn.opts.Reconnect > 0 { + // We don't need these mutex.Lock()/mutex.Unlock() here, but + // runReconnects() expects mutex.Lock() to be set, so it's + // easier to add them instead of reworking runReconnects(). + conn.mutex.Lock() + err = conn.runReconnects(ctx) + conn.mutex.Unlock() + if err != nil { + return nil, err + } + } else { + if err = conn.connect(ctx); err != nil { + return nil, err + } } go conn.pinger() @@ -553,7 +577,7 @@ func pack(h *smallWBuf, enc *msgpack.Encoder, reqid uint32, return } -func (conn *Connection) createConnection(ctx context.Context) error { +func (conn *Connection) connect(ctx context.Context) error { var err error if conn.c == nil && conn.state == connDisconnected { if err = conn.dial(ctx); err == nil { @@ -616,19 +640,30 @@ func (conn *Connection) getDialTimeout() time.Duration { return dialTimeout } -func (conn *Connection) runReconnects() error { +func (conn *Connection) runReconnects(ctx context.Context) error { dialTimeout := conn.getDialTimeout() var reconnects uint var err error + t := time.NewTicker(conn.opts.Reconnect) + defer t.Stop() for conn.opts.MaxReconnects == 0 || reconnects <= conn.opts.MaxReconnects { - now := time.Now() - - ctx, cancel := context.WithTimeout(context.Background(), dialTimeout) - err = conn.createConnection(ctx) + localCtx, cancel := context.WithTimeout(ctx, dialTimeout) + err = conn.connect(localCtx) cancel() if err != nil { + // The error will most likely be the one that Dialer + // returns to us due to the context being cancelled. + // Although this is not guaranteed. For example, + // if the dialer may throw another error before checking + // the context, and the context has already been + // canceled. Or the context was not canceled after + // the error was thrown, but before the context was + // checked here. + if ctx.Err() != nil { + return err + } if clientErr, ok := err.(ClientError); ok && clientErr.Code == ErrConnectionClosed { return err @@ -642,7 +677,12 @@ func (conn *Connection) runReconnects() error { reconnects++ conn.mutex.Unlock() - time.Sleep(time.Until(now.Add(conn.opts.Reconnect))) + select { + case <-ctx.Done(): + // Since the context is cancelled, we don't need to do anything. + // Conn.connect() will return the correct error. + case <-t.C: + } conn.mutex.Lock() } @@ -656,7 +696,7 @@ func (conn *Connection) reconnectImpl(neterr error, c Conn) { if conn.opts.Reconnect > 0 { if c == conn.c { conn.closeConnection(neterr, false) - if err := conn.runReconnects(); err != nil { + if err := conn.runReconnects(context.Background()); err != nil { conn.closeConnection(err, true) } } diff --git a/tarantool_test.go b/tarantool_test.go index 2be3e793b..3f1e90ef2 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3972,6 +3972,86 @@ func TestConnect_context_cancel(t *testing.T) { } } +// A dialer that rejects the first few connection requests. +type mockSlowDialer struct { + counter *int + original NetDialer +} + +func (m mockSlowDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { + *m.counter++ + if *m.counter < 10 { + return nil, fmt.Errorf("Too early: %v", *m.counter) + } + return m.original.Dial(ctx, opts) +} + +func TestConnectIsBlocked(t *testing.T) { + const server = "127.0.0.1:3015" + testDialer := dialer + testDialer.Address = server + + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + Dialer: testDialer, + InitScript: "config.lua", + Listen: server, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 10, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(inst) + if err != nil { + t.Fatalf("Unable to start Tarantool: %s", err) + } + + var counter int + mockDialer := mockSlowDialer{original: testDialer, counter: &counter} + ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second) + defer cancel() + + reconnectOpts := opts + reconnectOpts.Reconnect = 100 * time.Millisecond + reconnectOpts.MaxReconnects = 100 + conn, err := Connect(ctx, mockDialer, reconnectOpts) + assert.Nil(t, err) + conn.Close() + assert.GreaterOrEqual(t, counter, 10) +} + +func TestConnectIsBlockedUntilContextExpires(t *testing.T) { + const server = "127.0.0.1:3015" + + testDialer := dialer + testDialer.Address = server + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + + reconnectOpts := opts + reconnectOpts.Reconnect = 100 * time.Millisecond + reconnectOpts.MaxReconnects = 100 + _, err := Connect(ctx, testDialer, reconnectOpts) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "failed to dial: dial tcp 127.0.0.1:3015: i/o timeout") +} + +func TestConnectIsUnblockedAfterMaxAttempts(t *testing.T) { + const server = "127.0.0.1:3015" + + testDialer := dialer + testDialer.Address = server + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + + reconnectOpts := opts + reconnectOpts.Reconnect = 100 * time.Millisecond + reconnectOpts.MaxReconnects = 1 + _, err := Connect(ctx, testDialer, reconnectOpts) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "last reconnect failed") +} + func buildSidecar(dir string) error { goPath, err := exec.LookPath("go") if err != nil { From 7926576639c3aafdc2135ad3489a3c196905cd05 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Sun, 30 Mar 2025 00:06:46 +0300 Subject: [PATCH 571/605] internal: use a sync.Pool of msgpack.Decoder The change helps to avoid 2 allocations per a response decoding. You could check the change with the command: $ go test -v . -bench . -run fff -benchmem --- CHANGELOG.md | 3 +++ box_error.go | 4 +++- connection.go | 4 +++- decoder.go | 24 ++++++++++++++++++++++++ dial.go | 6 +++++- go.mod | 2 +- go.sum | 8 ++------ response.go | 36 ++++++++++++------------------------ 8 files changed, 53 insertions(+), 34 deletions(-) create mode 100644 decoder.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 00049a9c0..3b87725d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +- A usage of sync.Pool of msgpack.Decoder saves 2 object allocations per + a response decoding. + ### Changed - Connect() now retry the connection if a failure occurs and opts.Reconnect > 0. diff --git a/box_error.go b/box_error.go index 3d76a942c..59bfb4a05 100644 --- a/box_error.go +++ b/box_error.go @@ -278,7 +278,9 @@ func (e *BoxError) UnmarshalMsgpack(b []byte) error { } buf := bytes.NewBuffer(b) - dec := msgpack.NewDecoder(buf) + + dec := getDecoder(buf) + defer putDecoder(dec) if val, err := decodeBoxError(dec); err != nil { return err diff --git a/connection.go b/connection.go index 97f9dbbca..8c8552e60 100644 --- a/connection.go +++ b/connection.go @@ -803,7 +803,9 @@ func (conn *Connection) writer(w writeFlusher, c Conn) { func readWatchEvent(reader io.Reader) (connWatchEvent, error) { keyExist := false event := connWatchEvent{} - d := msgpack.NewDecoder(reader) + + d := getDecoder(reader) + defer putDecoder(d) l, err := d.DecodeMapLen() if err != nil { diff --git a/decoder.go b/decoder.go new file mode 100644 index 000000000..d1c682889 --- /dev/null +++ b/decoder.go @@ -0,0 +1,24 @@ +package tarantool + +import ( + "io" + + "github.com/vmihailenco/msgpack/v5" +) + +func untypedMapDecoder(dec *msgpack.Decoder) (interface{}, error) { + return dec.DecodeUntypedMap() +} + +func getDecoder(r io.Reader) *msgpack.Decoder { + d := msgpack.GetDecoder() + + d.Reset(r) + d.SetMapDecoder(untypedMapDecoder) + + return d +} + +func putDecoder(dec *msgpack.Decoder) { + msgpack.PutDecoder(dec) +} diff --git a/dial.go b/dial.go index ac1919030..d0acf69c0 100644 --- a/dial.go +++ b/dial.go @@ -547,7 +547,11 @@ func readResponse(r io.Reader, req Request) (Response, error) { } buf := smallBuf{b: respBytes} - header, _, err := decodeHeader(msgpack.NewDecoder(&smallBuf{}), &buf) + + d := getDecoder(&buf) + defer putDecoder(d) + + header, _, err := decodeHeader(d, &buf) if err != nil { return nil, fmt.Errorf("decode response header error: %w", err) } diff --git a/go.mod b/go.mod index 237d390cc..5b44eb1c5 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.9.0 github.com/tarantool/go-iproto v1.1.0 - github.com/vmihailenco/msgpack/v5 v5.3.5 + github.com/vmihailenco/msgpack/v5 v5.4.1 ) require ( diff --git a/go.sum b/go.sum index a66c68ef0..099647b8c 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -7,18 +6,15 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tarantool/go-iproto v1.1.0 h1:HULVOIHsiehI+FnHfM7wMDntuzUddO09DKqu2WnFQ5A= github.com/tarantool/go-iproto v1.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= -github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/response.go b/response.go index 90a02a1ce..36aad66a0 100644 --- a/response.go +++ b/response.go @@ -329,10 +329,8 @@ func (resp *baseResponse) Decode() ([]interface{}, error) { var l int info := &decodeInfo{} - d := msgpack.NewDecoder(&resp.buf) - d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { - return dec.DecodeUntypedMap() - }) + d := getDecoder(&resp.buf) + defer putDecoder(d) if l, err = d.DecodeMapLen(); err != nil { resp.err = err @@ -384,10 +382,8 @@ func (resp *SelectResponse) Decode() ([]interface{}, error) { var l int info := &decodeInfo{} - d := msgpack.NewDecoder(&resp.buf) - d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { - return dec.DecodeUntypedMap() - }) + d := getDecoder(&resp.buf) + defer putDecoder(d) if l, err = d.DecodeMapLen(); err != nil { resp.err = err @@ -447,10 +443,8 @@ func (resp *ExecuteResponse) Decode() ([]interface{}, error) { var l int info := &decodeInfo{} - d := msgpack.NewDecoder(&resp.buf) - d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { - return dec.DecodeUntypedMap() - }) + d := getDecoder(&resp.buf) + defer putDecoder(d) if l, err = d.DecodeMapLen(); err != nil { resp.err = err @@ -535,10 +529,8 @@ func (resp *baseResponse) DecodeTyped(res interface{}) error { info := &decodeInfo{} var l int - d := msgpack.NewDecoder(&resp.buf) - d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { - return dec.DecodeUntypedMap() - }) + d := getDecoder(&resp.buf) + defer putDecoder(d) if l, err = d.DecodeMapLen(); err != nil { return err @@ -576,10 +568,8 @@ func (resp *SelectResponse) DecodeTyped(res interface{}) error { info := &decodeInfo{} var l int - d := msgpack.NewDecoder(&resp.buf) - d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { - return dec.DecodeUntypedMap() - }) + d := getDecoder(&resp.buf) + defer putDecoder(d) if l, err = d.DecodeMapLen(); err != nil { return err @@ -624,10 +614,8 @@ func (resp *ExecuteResponse) DecodeTyped(res interface{}) error { info := &decodeInfo{} var l int - d := msgpack.NewDecoder(&resp.buf) - d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { - return dec.DecodeUntypedMap() - }) + d := getDecoder(&resp.buf) + defer putDecoder(d) if l, err = d.DecodeMapLen(); err != nil { return err From 19eb1f93629ca47b8421bb2fae61234a1c8ffc5d Mon Sep 17 00:00:00 2001 From: Alexey Potapenko Date: Wed, 2 Apr 2025 11:36:35 +0300 Subject: [PATCH 572/605] ci: hotfix to build checks module Part of #TNTP-1918 --- .github/workflows/testing.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 758bc89a1..433ed206b 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -59,6 +59,10 @@ jobs: - name: Setup tt environment run: tt init + # https://github.com/tarantool/checks/issues/64 + - name: Install specific CMake version + run: pip3 install cmake==3.15.3 + - name: Setup Tarantool ${{ matrix.tarantool }} if: matrix.tarantool != 'master' uses: tarantool/setup-tarantool@v2 @@ -85,6 +89,7 @@ jobs: - name: Setup Tarantool master if: matrix.tarantool == 'master' && steps.cache-latest.outputs.cache-hit != 'true' run: | + sudo pip3 install cmake==3.15.3 sudo tt install tarantool master - name: Add Tarantool master to PATH From c04377183768546f7d31ff26bdd060e564d07008 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 3 Apr 2025 08:19:31 +0300 Subject: [PATCH 573/605] Release v2.3.1 --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b87725d4..aebbd1d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,16 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +### Changed + +### Fixed + +## [v2.3.1] - 2025-04-03 + +The patch releases fixes expected Connect() behavior and reduces allocations. + +### Added + - A usage of sync.Pool of msgpack.Decoder saves 2 object allocations per a response decoding. @@ -20,8 +30,6 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. opts.MaxReconnects == 0. Connect() blocks until a connection is established, the context is cancelled, or the number of attempts is exhausted (#436). -### Fixed - ## [v2.3.0] - 2025-03-11 The release extends box.info responses and ConnectionPool.GetInfo return data. From 07ea1d15c1c0fe680c8e9b077bb4fa3337ea8d77 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 10 Apr 2025 10:18:34 +0300 Subject: [PATCH 574/605] dial: Connect() does not cancel by context if no i/o NetDialer, GreetingDialer, AuthDialer and ProtocolDialer may not cancel Dial() on context expiration when network connection hangs. The issue occurred because context wasn't properly handled during network I/O operations, potentially causing infinite waiting. Part of #TNTP-2018 --- CHANGELOG.md | 5 +- connection.go | 2 +- dial.go | 109 ++++++++++++++++++++++++++++++++++++------- dial_test.go | 125 ++++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 214 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aebbd1d3d..f9c59d95b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +- Connect() may not cancel Dial() call on context expiration if network + connection hangs (#443). + ## [v2.3.1] - 2025-04-03 The patch releases fixes expected Connect() behavior and reduces allocations. @@ -21,7 +24,7 @@ The patch releases fixes expected Connect() behavior and reduces allocations. ### Added - A usage of sync.Pool of msgpack.Decoder saves 2 object allocations per - a response decoding. + a response decoding (#440). ### Changed diff --git a/connection.go b/connection.go index 8c8552e60..2b43f2ec0 100644 --- a/connection.go +++ b/connection.go @@ -489,7 +489,7 @@ func (conn *Connection) dial(ctx context.Context) error { } req := newWatchRequest(key.(string)) - if err = writeRequest(c, req); err != nil { + if err = writeRequest(ctx, c, req); err != nil { st <- state return false } diff --git a/dial.go b/dial.go index d0acf69c0..9faeaa98f 100644 --- a/dial.go +++ b/dial.go @@ -289,6 +289,7 @@ func (d AuthDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { if err != nil { return conn, err } + greeting := conn.Greeting() if greeting.Salt == "" { conn.Close() @@ -309,7 +310,7 @@ func (d AuthDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { } } - if err := authenticate(conn, d.Auth, d.Username, d.Password, + if err := authenticate(ctx, conn, d.Auth, d.Username, d.Password, conn.Greeting().Salt); err != nil { conn.Close() return nil, fmt.Errorf("failed to authenticate: %w", err) @@ -340,7 +341,7 @@ func (d ProtocolDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { protocolInfo: d.RequiredProtocolInfo, } - protocolConn.protocolInfo, err = identify(&protocolConn) + protocolConn.protocolInfo, err = identify(ctx, &protocolConn) if err != nil { protocolConn.Close() return nil, fmt.Errorf("failed to identify: %w", err) @@ -372,11 +373,12 @@ func (d GreetingDialer) Dial(ctx context.Context, opts DialOpts) (Conn, error) { greetingConn := greetingConn{ Conn: conn, } - version, salt, err := readGreeting(greetingConn) + version, salt, err := readGreeting(ctx, &greetingConn) if err != nil { greetingConn.Close() return nil, fmt.Errorf("failed to read greeting: %w", err) } + greetingConn.greeting = Greeting{ Version: version, Salt: salt, @@ -410,31 +412,67 @@ func parseAddress(address string) (string, string) { return network, address } +// ioWaiter waits in a background until an io operation done or a context +// is expired. It closes the connection and writes a context error into the +// output channel on context expiration. +// +// A user of the helper should close the first output channel after an IO +// operation done and read an error from a second channel to get the result +// of waiting. +func ioWaiter(ctx context.Context, conn Conn) (chan<- struct{}, <-chan error) { + doneIO := make(chan struct{}) + doneWait := make(chan error, 1) + + go func() { + defer close(doneWait) + + select { + case <-ctx.Done(): + conn.Close() + <-doneIO + doneWait <- ctx.Err() + case <-doneIO: + doneWait <- nil + } + }() + + return doneIO, doneWait +} + // readGreeting reads a greeting message. -func readGreeting(reader io.Reader) (string, string, error) { +func readGreeting(ctx context.Context, conn Conn) (string, string, error) { var version, salt string + doneRead, doneWait := ioWaiter(ctx, conn) + data := make([]byte, 128) - _, err := io.ReadFull(reader, data) + _, err := io.ReadFull(conn, data) + + close(doneRead) + if err == nil { version = bytes.NewBuffer(data[:64]).String() salt = bytes.NewBuffer(data[64:108]).String() } + if waitErr := <-doneWait; waitErr != nil { + err = waitErr + } + return version, salt, err } // identify sends info about client protocol, receives info // about server protocol in response and stores it in the connection. -func identify(conn Conn) (ProtocolInfo, error) { +func identify(ctx context.Context, conn Conn) (ProtocolInfo, error) { var info ProtocolInfo req := NewIdRequest(clientProtocolInfo) - if err := writeRequest(conn, req); err != nil { + if err := writeRequest(ctx, conn, req); err != nil { return info, err } - resp, err := readResponse(conn, req) + resp, err := readResponse(ctx, conn, req) if err != nil { if resp != nil && resp.Header().Error == iproto.ER_UNKNOWN_REQUEST_TYPE { @@ -495,7 +533,7 @@ func checkProtocolInfo(required ProtocolInfo, actual ProtocolInfo) error { } // authenticate authenticates for a connection. -func authenticate(c Conn, auth Auth, user string, pass string, salt string) error { +func authenticate(ctx context.Context, c Conn, auth Auth, user, pass, salt string) error { var req Request var err error @@ -511,37 +549,73 @@ func authenticate(c Conn, auth Auth, user string, pass string, salt string) erro return errors.New("unsupported method " + auth.String()) } - if err = writeRequest(c, req); err != nil { + if err = writeRequest(ctx, c, req); err != nil { return err } - if _, err = readResponse(c, req); err != nil { + if _, err = readResponse(ctx, c, req); err != nil { return err } return nil } // writeRequest writes a request to the writer. -func writeRequest(w writeFlusher, req Request) error { +func writeRequest(ctx context.Context, conn Conn, req Request) error { var packet smallWBuf err := pack(&packet, msgpack.NewEncoder(&packet), 0, req, ignoreStreamId, nil) if err != nil { return fmt.Errorf("pack error: %w", err) } - if _, err = w.Write(packet.b); err != nil { + + doneWrite, doneWait := ioWaiter(ctx, conn) + + _, err = conn.Write(packet.b) + + close(doneWrite) + + if waitErr := <-doneWait; waitErr != nil { + err = waitErr + } + + if err != nil { return fmt.Errorf("write error: %w", err) } - if err = w.Flush(); err != nil { + + doneWrite, doneWait = ioWaiter(ctx, conn) + + err = conn.Flush() + + close(doneWrite) + + if waitErr := <-doneWait; waitErr != nil { + err = waitErr + } + + if err != nil { return fmt.Errorf("flush error: %w", err) } + + if waitErr := <-doneWait; waitErr != nil { + err = waitErr + } + return err } // readResponse reads a response from the reader. -func readResponse(r io.Reader, req Request) (Response, error) { +func readResponse(ctx context.Context, conn Conn, req Request) (Response, error) { var lenbuf [packetLengthBytes]byte - respBytes, err := read(r, lenbuf[:]) + doneRead, doneWait := ioWaiter(ctx, conn) + + respBytes, err := read(conn, lenbuf[:]) + + close(doneRead) + + if waitErr := <-doneWait; waitErr != nil { + err = waitErr + } + if err != nil { return nil, fmt.Errorf("read error: %w", err) } @@ -555,10 +629,12 @@ func readResponse(r io.Reader, req Request) (Response, error) { if err != nil { return nil, fmt.Errorf("decode response header error: %w", err) } + resp, err := req.Response(header, &buf) if err != nil { return nil, fmt.Errorf("creating response error: %w", err) } + _, err = resp.Decode() if err != nil { switch err.(type) { @@ -568,5 +644,6 @@ func readResponse(r io.Reader, req Request) (Response, error) { return resp, fmt.Errorf("decode response body error: %w", err) } } + return resp, nil } diff --git a/dial_test.go b/dial_test.go index abfdf0d4f..87b9af5d8 100644 --- a/dial_test.go +++ b/dial_test.go @@ -87,6 +87,8 @@ type mockIoConn struct { readbuf, writebuf bytes.Buffer // Calls readWg/writeWg.Wait() in Read()/Flush(). readWg, writeWg sync.WaitGroup + // wgDoneOnClose call Done() on the wait groups on Close(). + wgDoneOnClose bool // How many times to wait before a wg.Wait() call. readWgDelay, writeWgDelay int // Write()/Read()/Flush()/Close() calls count. @@ -137,6 +139,12 @@ func (m *mockIoConn) Flush() error { } func (m *mockIoConn) Close() error { + if m.wgDoneOnClose { + m.readWg.Done() + m.writeWg.Done() + m.wgDoneOnClose = false + } + m.closeCnt++ return nil } @@ -165,6 +173,7 @@ func newMockIoConn() *mockIoConn { conn := new(mockIoConn) conn.readWg.Add(1) conn.writeWg.Add(1) + conn.wgDoneOnClose = true return conn } @@ -201,9 +210,6 @@ func TestConn_Close(t *testing.T) { conn.Close() assert.Equal(t, 1, dialer.conn.closeCnt) - - dialer.conn.readWg.Done() - dialer.conn.writeWg.Done() } type stubAddr struct { @@ -224,8 +230,6 @@ func TestConn_Addr(t *testing.T) { conn.addr = stubAddr{str: addr} }) defer func() { - dialer.conn.readWg.Done() - dialer.conn.writeWg.Done() conn.Close() }() @@ -242,8 +246,6 @@ func TestConn_Greeting(t *testing.T) { conn.greeting = greeting }) defer func() { - dialer.conn.readWg.Done() - dialer.conn.writeWg.Done() conn.Close() }() @@ -263,8 +265,6 @@ func TestConn_ProtocolInfo(t *testing.T) { conn.info = info }) defer func() { - dialer.conn.readWg.Done() - dialer.conn.writeWg.Done() conn.Close() }() @@ -284,6 +284,7 @@ func TestConn_ReadWrite(t *testing.T) { 0x01, 0xce, 0x00, 0x00, 0x00, 0x02, 0x80, // Body map. }) + conn.wgDoneOnClose = false }) defer func() { dialer.conn.writeWg.Done() @@ -579,6 +580,24 @@ func TestNetDialer_Dial(t *testing.T) { } } +func TestNetDialer_Dial_hang_connection(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer l.Close() + + dialer := tarantool.NetDialer{ + Address: l.Addr().String(), + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + + require.Nil(t, conn) + require.Error(t, err, context.DeadlineExceeded) +} + func TestNetDialer_Dial_requirements(t *testing.T) { l, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) @@ -685,6 +704,7 @@ func TestAuthDialer_Dial_DialerError(t *testing.T) { ctx, cancel := test_helpers.GetConnectContext() defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) if conn != nil { conn.Close() @@ -717,6 +737,38 @@ func TestAuthDialer_Dial_NoSalt(t *testing.T) { } } +func TestConn_AuthDialer_hang_connection(t *testing.T) { + salt := fmt.Sprintf("%s", testDialSalt) + salt = base64.StdEncoding.EncodeToString([]byte(salt)) + mock := &mockIoDialer{ + init: func(conn *mockIoConn) { + conn.greeting.Salt = salt + conn.readWgDelay = 0 + conn.writeWgDelay = 0 + }, + } + dialer := tarantool.AuthDialer{ + Dialer: mock, + Username: "test", + Password: "test", + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + + conn, err := tarantool.Connect(ctx, &dialer, + tarantool.Opts{ + Timeout: 1000 * time.Second, // Avoid pings. + SkipSchema: true, + }) + + require.Nil(t, conn) + require.Error(t, err, context.DeadlineExceeded) + require.Equal(t, mock.conn.writeCnt, 1) + require.Equal(t, mock.conn.readCnt, 0) + require.Greater(t, mock.conn.closeCnt, 1) +} + func TestAuthDialer_Dial(t *testing.T) { salt := fmt.Sprintf("%s", testDialSalt) salt = base64.StdEncoding.EncodeToString([]byte(salt)) @@ -726,6 +778,7 @@ func TestAuthDialer_Dial(t *testing.T) { conn.writeWgDelay = 1 conn.readWgDelay = 2 conn.readbuf.Write(okResponse) + conn.wgDoneOnClose = false }, } defer func() { @@ -758,6 +811,7 @@ func TestAuthDialer_Dial_PapSha256Auth(t *testing.T) { conn.writeWgDelay = 1 conn.readWgDelay = 2 conn.readbuf.Write(okResponse) + conn.wgDoneOnClose = false }, } defer func() { @@ -800,6 +854,34 @@ func TestProtocolDialer_Dial_DialerError(t *testing.T) { assert.EqualError(t, err, "some error") } +func TestConn_ProtocolDialer_hang_connection(t *testing.T) { + mock := &mockIoDialer{ + init: func(conn *mockIoConn) { + conn.info = tarantool.ProtocolInfo{Version: 1} + conn.readWgDelay = 0 + conn.writeWgDelay = 0 + }, + } + dialer := tarantool.ProtocolDialer{ + Dialer: mock, + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + + conn, err := tarantool.Connect(ctx, &dialer, + tarantool.Opts{ + Timeout: 1000 * time.Second, // Avoid pings. + SkipSchema: true, + }) + + require.Nil(t, conn) + require.Error(t, err, context.DeadlineExceeded) + require.Equal(t, mock.conn.writeCnt, 1) + require.Equal(t, mock.conn.readCnt, 0) + require.Greater(t, mock.conn.closeCnt, 1) +} + func TestProtocolDialer_Dial_IdentifyFailed(t *testing.T) { dialer := tarantool.ProtocolDialer{ Dialer: &mockIoDialer{ @@ -898,6 +980,31 @@ func TestGreetingDialer_Dial_DialerError(t *testing.T) { assert.EqualError(t, err, "some error") } +func TestConn_GreetingDialer_hang_connection(t *testing.T) { + mock := &mockIoDialer{ + init: func(conn *mockIoConn) { + conn.readWgDelay = 0 + }, + } + dialer := tarantool.GreetingDialer{ + Dialer: mock, + } + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + + conn, err := tarantool.Connect(ctx, &dialer, + tarantool.Opts{ + Timeout: 1000 * time.Second, // Avoid pings. + SkipSchema: true, + }) + + require.Nil(t, conn) + require.Error(t, err, context.DeadlineExceeded) + require.Equal(t, mock.conn.readCnt, 1) + require.Greater(t, mock.conn.closeCnt, 1) +} + func TestGreetingDialer_Dial_GreetingFailed(t *testing.T) { dialer := tarantool.GreetingDialer{ Dialer: &mockIoDialer{ From a255bb73bc7e2f22db9bdf02aca16d97255d159e Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 14 Apr 2025 00:48:14 +0300 Subject: [PATCH 575/605] tests: fix flaky TestConnect_schema_update The tests flaks sometime with the error: ``` failed to identify: read error: context deadline exceeded ``` It was decided just to increase a connect timeout. --- tarantool_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tarantool_test.go b/tarantool_test.go index 3f1e90ef2..ded7ba452 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3927,11 +3927,12 @@ func TestConnect_schema_update(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + for i := 0; i < 100; i++ { fut := conn.Do(NewCallRequest("create_spaces")) - ctx, cancel := test_helpers.GetConnectContext() - defer cancel() if conn, err := Connect(ctx, dialer, opts); err != nil { if err.Error() != "concurrent schema update" { t.Errorf("unexpected error: %s", err) From e85e137a849d44c89fc8f2a66120810e3c0229bc Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 14 Apr 2025 00:18:25 +0300 Subject: [PATCH 576/605] pool: fix pool.Connect if a server i/o hangs Previously, `pool.Connect` attempted to establish a connection one after another instance. It could cause the entire chain to hang if one connection hanged. Now connections are established in parallel. After the first successful connection, the remaining connections wait with a timeout of `pool.Opts.CheckTimeout`. Closes #TNTP-2018 --- CHANGELOG.md | 8 +++ pool/connection_pool.go | 100 +++++++++++++++++++++-------------- pool/connection_pool_test.go | 75 ++++++++++++++++++++------ 3 files changed, 125 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9c59d95b..c5a161d7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,18 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Changed +- Previously, `pool.Connect` attempted to establish a connection one after + another instance. It could cause the entire chain to hang if one connection + hanged. Now connections are established in parallel. After the first + successful connection, the remaining connections wait with a timeout of + `pool.Opts.CheckTimeout` (#444). + ### Fixed - Connect() may not cancel Dial() call on context expiration if network connection hangs (#443). +- pool.Connect() failed to connect to any instance if a first instance + connection hangs (#444). ## [v2.3.1] - 2025-04-03 diff --git a/pool/connection_pool.go b/pool/connection_pool.go index fa5537a6a..a47ec19a5 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -171,7 +171,7 @@ func ConnectWithOpts(ctx context.Context, instances []Instance, roPool := newRoundRobinStrategy(size) anyPool := newRoundRobinStrategy(size) - connPool := &ConnectionPool{ + p := &ConnectionPool{ ends: make(map[string]*endpoint), opts: opts, state: connectedState, @@ -181,19 +181,44 @@ func ConnectWithOpts(ctx context.Context, instances []Instance, anyPool: anyPool, } - canceled := connPool.fillPools(ctx, instances) - if canceled { - connPool.state.set(closedState) - return nil, ErrContextCanceled + fillCtx, fillCancel := context.WithCancel(ctx) + defer fillCancel() + + var timeout <-chan time.Time + + timeout = make(chan time.Time) + filled := p.fillPools(fillCtx, instances) + done := 0 + success := len(instances) == 0 + + for done < len(instances) { + select { + case <-timeout: + fillCancel() + // To be sure that the branch is called only once. + timeout = make(chan time.Time) + case err := <-filled: + done++ + + if err == nil && !success { + timeout = time.After(opts.CheckTimeout) + success = true + } + } + } + + if !success && ctx.Err() != nil { + p.state.set(closedState) + return nil, ctx.Err() } - for _, endpoint := range connPool.ends { + for _, endpoint := range p.ends { endpointCtx, cancel := context.WithCancel(context.Background()) endpoint.cancel = cancel - go connPool.controller(endpointCtx, endpoint) + go p.controller(endpointCtx, endpoint) } - return connPool, nil + return p, nil } // Connect creates pool for instances with specified instances. Instances must @@ -1184,45 +1209,33 @@ func (p *ConnectionPool) handlerDeactivated(name string, conn *tarantool.Connect } } -func (p *ConnectionPool) deactivateConnection(name string, - conn *tarantool.Connection, role Role) { - p.deleteConnection(name) - conn.Close() - p.handlerDeactivated(name, conn, role) -} +func (p *ConnectionPool) fillPools(ctx context.Context, instances []Instance) <-chan error { + done := make(chan error, len(instances)) -func (p *ConnectionPool) deactivateConnections() { - for name, endpoint := range p.ends { - if endpoint != nil && endpoint.conn != nil { - p.deactivateConnection(name, endpoint.conn, endpoint.role) - } - } -} - -func (p *ConnectionPool) fillPools(ctx context.Context, instances []Instance) bool { // It is called before controller() goroutines, so we don't expect // concurrency issues here. for _, instance := range instances { end := newEndpoint(instance.Name, instance.Dialer, instance.Opts) p.ends[instance.Name] = end + } - if err := p.tryConnect(ctx, end); err != nil { - log.Printf("tarantool: connect to %s failed: %s\n", - instance.Name, err) - select { - case <-ctx.Done(): - p.ends[instance.Name] = nil - log.Printf("tarantool: operation was canceled") + for _, instance := range instances { + name := instance.Name + end := p.ends[name] - p.deactivateConnections() + go func() { + if err := p.tryConnect(ctx, end); err != nil { + log.Printf("tarantool: connect to %s failed: %s\n", name, err) + done <- fmt.Errorf("failed to connect to %s :%w", name, err) - return true - default: + return } - } + + done <- nil + }() } - return false + return done } func (p *ConnectionPool) updateConnection(e *endpoint) { @@ -1284,19 +1297,24 @@ func (p *ConnectionPool) updateConnection(e *endpoint) { } func (p *ConnectionPool) tryConnect(ctx context.Context, e *endpoint) error { + e.conn = nil + e.role = UnknownRole + + connOpts := e.opts + connOpts.Notify = e.notify + conn, err := tarantool.Connect(ctx, e.dialer, connOpts) + p.poolsMutex.Lock() if p.state.get() != connectedState { + if err == nil { + conn.Close() + } + p.poolsMutex.Unlock() return ErrClosed } - e.conn = nil - e.role = UnknownRole - - connOpts := e.opts - connOpts.Notify = e.notify - conn, err := tarantool.Connect(ctx, e.dialer, connOpts) if err == nil { role, err := p.getConnectionRole(conn) p.poolsMutex.Unlock() diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index 323ce37c3..f3bf5f556 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "log" + "net" "os" "reflect" "strings" @@ -141,7 +142,7 @@ func TestConnSuccessfully(t *testing.T) { } err = test_helpers.CheckPoolStatuses(args) - require.Nil(t, err) + require.NoError(t, err) } func TestConn_no_execute_supported(t *testing.T) { @@ -261,6 +262,51 @@ func TestConnect_unavailable(t *testing.T) { }, connPool.GetInfo()) } +func TestConnect_single_server_hang(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer l.Close() + + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + + insts := makeInstances([]string{l.Addr().String()}, connOpts) + + connPool, err := pool.Connect(ctx, insts) + if connPool != nil { + defer connPool.Close() + } + + require.ErrorIs(t, err, context.DeadlineExceeded) + require.Nil(t, connPool) +} + +func TestConnect_server_hang(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer l.Close() + + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + + servers := []string{l.Addr().String(), servers[0]} + insts := makeInstances(servers, connOpts) + + connPool, err := pool.Connect(ctx, insts) + if connPool != nil { + defer connPool.Close() + } + + require.NoError(t, err, "failed to create a pool") + require.NotNil(t, connPool, "pool is nil after Connect") + require.Equal(t, map[string]pool.ConnectionInfo{ + servers[0]: pool.ConnectionInfo{ + ConnectedNow: false, ConnRole: pool.UnknownRole, Instance: insts[0]}, + servers[1]: pool.ConnectionInfo{ + ConnectedNow: true, ConnRole: pool.MasterRole, Instance: insts[1]}, + }, connPool.GetInfo()) +} + func TestConnErrorAfterCtxCancel(t *testing.T) { var connLongReconnectOpts = tarantool.Opts{ Timeout: 5 * time.Second, @@ -279,7 +325,7 @@ func TestConnErrorAfterCtxCancel(t *testing.T) { if connPool != nil || err == nil { t.Fatalf("ConnectionPool was created after cancel") } - if !strings.Contains(err.Error(), "operation was canceled") { + if !strings.Contains(err.Error(), "context canceled") { t.Fatalf("Unexpected error, expected to contain %s, got %v", "operation was canceled", err) } @@ -287,7 +333,6 @@ func TestConnErrorAfterCtxCancel(t *testing.T) { type mockClosingDialer struct { addr string - cnt *int ctx context.Context ctxCancel context.CancelFunc } @@ -301,26 +346,21 @@ func (m *mockClosingDialer) Dial(ctx context.Context, } conn, err := dialer.Dial(m.ctx, tarantool.DialOpts{}) - if *m.cnt == 0 { - m.ctxCancel() - } - *m.cnt++ + m.ctxCancel() return conn, err } -func TestContextCancelInProgress(t *testing.T) { +func TestConnectContextCancelAfterConnect(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - cnt := new(int) var instances []pool.Instance for _, server := range servers { instances = append(instances, pool.Instance{ Name: server, Dialer: &mockClosingDialer{ addr: server, - cnt: cnt, ctx: ctx, ctxCancel: cancel, }, @@ -329,11 +369,12 @@ func TestContextCancelInProgress(t *testing.T) { } connPool, err := pool.Connect(ctx, instances) - require.NotNilf(t, err, "expected err after ctx cancel") - assert.Truef(t, strings.Contains(err.Error(), "operation was canceled"), - fmt.Sprintf("unexpected error, expected to contain %s, got %v", - "operation was canceled", err)) - require.Nilf(t, connPool, "conn is not nil after ctx cancel") + if connPool != nil { + defer connPool.Close() + } + + assert.NoError(t, err, "expected err after ctx cancel") + assert.NotNil(t, connPool) } func TestConnSuccessfullyDuplicates(t *testing.T) { @@ -527,8 +568,8 @@ func TestAdd(t *testing.T) { ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() connPool, err := pool.Connect(ctx, []pool.Instance{}) - require.Nilf(t, err, "failed to connect") - require.NotNilf(t, connPool, "conn is nil after Connect") + require.NoError(t, err, "failed to connect") + require.NotNil(t, connPool, "conn is nil after Connect") defer connPool.Close() From 66ef7216ea83750162f3b8a5b09573d16276a393 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 14 Apr 2025 16:33:44 +0300 Subject: [PATCH 577/605] Release v2.3.2 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5a161d7a..2c3900ee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,15 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Changed +### Fixed + +## [v2.3.2] - 2025-04-14 + +This release improves the logic of `Connect` and `pool.Connect` in case of a +hung I/O connection. + +### Changed + - Previously, `pool.Connect` attempted to establish a connection one after another instance. It could cause the entire chain to hang if one connection hanged. Now connections are established in parallel. After the first From 953a5cfafa36b7d73c14519ff22b51721b1228ca Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Fri, 20 Jun 2025 19:45:53 +0300 Subject: [PATCH 578/605] box: added logic for working with Tarantool schema Implemented the `box.Schema()` method that returns a `Schema` object for schema-related operations. Co-authored-by: maksim.konovalov Closes #TNTP-3331 --- CHANGELOG.md | 3 + box/box.go | 12 + box/box_test.go | 95 +++++-- box/example_test.go | 189 ++++++++++++- box/info.go | 17 +- box/info_test.go | 18 +- box/request.go | 38 --- box/schema.go | 21 ++ box/schema_user.go | 545 ++++++++++++++++++++++++++++++++++++ box/schema_user_test.go | 195 +++++++++++++ box/session.go | 57 ++++ box/session_test.go | 20 ++ box/tarantool_test.go | 595 +++++++++++++++++++++++++++++++++++++++- box/testdata/config.lua | 4 +- box_error_test.go | 2 +- test_helpers/utils.go | 10 + 16 files changed, 1739 insertions(+), 82 deletions(-) delete mode 100644 box/request.go create mode 100644 box/schema.go create mode 100644 box/schema_user.go create mode 100644 box/schema_user_test.go create mode 100644 box/session.go create mode 100644 box/session_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c3900ee2..5af49c730 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +- Implemented all box.schema.user operations requests and sugar interface (#426). +- Implemented box.session.su request and sugar interface only for current session granting (#426). + ### Changed ### Fixed diff --git a/box/box.go b/box/box.go index be7f288ad..c9b0e71dd 100644 --- a/box/box.go +++ b/box/box.go @@ -12,11 +12,23 @@ type Box struct { // New returns a new instance of the box structure, which implements the Box interface. func New(conn tarantool.Doer) *Box { + if conn == nil { + // Check if the provided Tarantool connection is nil, and if it is, panic with an error + // message. panic early helps to catch and fix nil pointer issues in the code. + panic("tarantool connection cannot be nil") + } + return &Box{ conn: conn, // Assigns the provided Tarantool connection. } } +// Schema returns a new Schema instance, providing access to schema-related operations. +// It uses the connection from the Box instance to communicate with Tarantool. +func (b *Box) Schema() *Schema { + return newSchema(b.conn) +} + // Info retrieves the current information of the Tarantool instance. // It calls the "box.info" function and parses the result into the Info structure. func (b *Box) Info() (Info, error) { diff --git a/box/box_test.go b/box/box_test.go index 31e614c15..0180473ed 100644 --- a/box/box_test.go +++ b/box/box_test.go @@ -1,29 +1,88 @@ package box_test import ( + "context" + "errors" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tarantool/go-tarantool/v2/box" + "github.com/tarantool/go-tarantool/v2/test_helpers" ) func TestNew(t *testing.T) { - // Create a box instance with a nil connection. This should lead to a panic later. - b := box.New(nil) - - // Ensure the box instance is not nil (which it shouldn't be), but this is not meaningful - // since we will panic when we call the Info method with the nil connection. - require.NotNil(t, b) - - // We expect a panic because we are passing a nil connection (nil Doer) to the By function. - // The library does not control this zone, and the nil connection would cause a runtime error - // when we attempt to call methods (like Info) on it. - // This test ensures that such an invalid state is correctly handled by causing a panic, - // as it's outside the library's responsibility. - require.Panics(t, func() { - - // Calling Info on a box with a nil connection will result in a panic, since the underlying - // connection (Doer) cannot perform the requested action (it's nil). - _, _ = b.Info() - }) + t.Parallel() + + // Create a box instance with a nil connection. This should lead to a panic. + require.Panics(t, func() { box.New(nil) }) +} + +func TestMocked_BoxInfo(t *testing.T) { + t.Parallel() + + data := []interface{}{ + map[string]interface{}{ + "version": "1.0.0", + "id": nil, + "ro": false, + "uuid": "uuid", + "pid": 456, + "status": "status", + "lsn": 123, + "replication": nil, + }, + } + mock := test_helpers.NewMockDoer(t, + test_helpers.NewMockResponse(t, data), + ) + b := box.New(&mock) + + info, err := b.Info() + require.NoError(t, err) + + assert.Equal(t, "1.0.0", info.Version) + assert.Equal(t, 456, info.PID) +} + +func TestMocked_BoxSchemaUserInfo(t *testing.T) { + t.Parallel() + + data := []interface{}{ + []interface{}{ + []interface{}{"read,write,execute", "universe", ""}, + }, + } + mock := test_helpers.NewMockDoer(t, + test_helpers.NewMockResponse(t, data), + ) + b := box.New(&mock) + + privs, err := b.Schema().User().Info(context.Background(), "username") + require.NoError(t, err) + + assert.Equal(t, []box.Privilege{ + { + Permissions: []box.Permission{ + box.PermissionRead, + box.PermissionWrite, + box.PermissionExecute, + }, + Type: box.PrivilegeUniverse, + Name: "", + }, + }, privs) +} + +func TestMocked_BoxSessionSu(t *testing.T) { + t.Parallel() + + mock := test_helpers.NewMockDoer(t, + test_helpers.NewMockResponse(t, []interface{}{}), + errors.New("user not found or supplied credentials are invalid"), + ) + b := box.New(&mock) + + err := b.Session().Su(context.Background(), "admin") + require.NoError(t, err) } diff --git a/box/example_test.go b/box/example_test.go index 461949760..eac3f5e1a 100644 --- a/box/example_test.go +++ b/box/example_test.go @@ -6,6 +6,7 @@ // // Terminal 2: // $ go test -v example_test.go + package box_test import ( @@ -18,7 +19,7 @@ import ( "github.com/tarantool/go-tarantool/v2/box" ) -func Example() { +func ExampleBox_Info() { dialer := tarantool.NetDialer{ Address: "127.0.0.1:3013", User: "test", @@ -55,6 +56,188 @@ func Example() { log.Fatalf("Box info uuids are not equal") } - fmt.Printf("Box info uuids are equal") - fmt.Printf("Current box info: %+v\n", resp.Info) + fmt.Printf("Box info uuids are equal\n") + fmt.Printf("Current box ro: %+v", resp.Info.RO) + // Output: + // Box info uuids are equal + // Current box ro: false +} + +func ExampleSchemaUser_Exists() { + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + } + ctx := context.Background() + + client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + + if err != nil { + log.Fatalf("Failed to connect: %s", err) + } + + // You can use UserExistsRequest type and call it directly. + fut := client.Do(box.NewUserExistsRequest("user")) + + resp := &box.UserExistsResponse{} + + err = fut.GetTyped(resp) + if err != nil { + log.Fatalf("Failed get box schema user exists with error: %s", err) + } + + // Or use simple User implementation. + b := box.New(client) + exists, err := b.Schema().User().Exists(ctx, "user") + if err != nil { + log.Fatalf("Failed get box schema user exists with error: %s", err) + } + + if exists != resp.Exists { + log.Fatalf("Box schema users exists are not equal") + } + + fmt.Printf("Box schema users exists are equal\n") + fmt.Printf("Current exists state: %+v", exists) + // Output: + // Box schema users exists are equal + // Current exists state: false +} + +func ExampleSchemaUser_Create() { + // Connect to Tarantool. + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + } + ctx := context.Background() + + client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + if err != nil { + log.Fatalf("Failed to connect: %s", err) + } + + // Create SchemaUser. + schemaUser := box.New(client).Schema().User() + + // Create a new user. + username := "new_user" + options := box.UserCreateOptions{ + IfNotExists: true, + Password: "secure_password", + } + err = schemaUser.Create(ctx, username, options) + if err != nil { + log.Fatalf("Failed to create user: %s", err) + } + + fmt.Printf("User '%s' created successfully\n", username) + // Output: + // User 'new_user' created successfully +} + +func ExampleSchemaUser_Drop() { + // Connect to Tarantool. + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + } + ctx := context.Background() + + client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + if err != nil { + log.Fatalf("Failed to connect: %s", err) + } + + // Create SchemaUser. + schemaUser := box.New(client).Schema().User() + + // Drop an existing user. + username := "new_user" + options := box.UserDropOptions{ + IfExists: true, + } + err = schemaUser.Drop(ctx, username, options) + if err != nil { + log.Fatalf("Failed to drop user: %s", err) + } + + exists, err := schemaUser.Exists(ctx, username) + if err != nil { + log.Fatalf("Failed to get user exists: %s", err) + } + + fmt.Printf("User '%s' dropped successfully\n", username) + fmt.Printf("User '%s' exists status: %v \n", username, exists) + // Output: + // User 'new_user' dropped successfully + // User 'new_user' exists status: false +} + +func ExampleSchemaUser_Password() { + // Connect to Tarantool. + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + } + ctx := context.Background() + + client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + if err != nil { + log.Fatalf("Failed to connect: %s", err) + } + + // Create SchemaUser. + schemaUser := box.New(client).Schema().User() + + // Get the password hash. + password := "my-password" + passwordHash, err := schemaUser.Password(ctx, password) + if err != nil { + log.Fatalf("Failed to get password hash: %s", err) + } + + fmt.Printf("Password '%s' hash: %s", password, passwordHash) + // Output: + // Password 'my-password' hash: 3PHNAQGFWFo0KRfToxNgDXHj2i8= +} + +func ExampleSchemaUser_Info() { + // Connect to Tarantool. + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + } + ctx := context.Background() + + client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + if err != nil { + log.Fatalf("Failed to connect: %s", err) + } + + // Create SchemaUser. + schemaUser := box.New(client).Schema().User() + + info, err := schemaUser.Info(ctx, "test") + if err != nil { + log.Fatalf("Failed to get password hash: %s", err) + } + + hasSuper := false + for _, i := range info { + if i.Name == "super" && i.Type == box.PrivilegeRole { + hasSuper = true + } + } + + if hasSuper { + fmt.Printf("User have super privileges") + } + // Output: + // User have super privileges } diff --git a/box/info.go b/box/info.go index aabfd65e3..aa682822c 100644 --- a/box/info.go +++ b/box/info.go @@ -105,25 +105,20 @@ func (ir *InfoResponse) DecodeMsgpack(d *msgpack.Decoder) error { } ir.Info = i - return nil } // InfoRequest represents a request to retrieve information about the Tarantool instance. // It implements the tarantool.Request interface. type InfoRequest struct { - baseRequest -} - -// Body method is used to serialize the request's body. -// It is part of the tarantool.Request interface implementation. -func (i InfoRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { - return i.impl.Body(res, enc) + *tarantool.CallRequest // Underlying Tarantool call request. } // NewInfoRequest returns a new empty info request. func NewInfoRequest() InfoRequest { - req := InfoRequest{} - req.impl = newCall("box.info") - return req + callReq := tarantool.NewCallRequest("box.info") + + return InfoRequest{ + callReq, + } } diff --git a/box/info_test.go b/box/info_test.go index 818555b69..2cf00f69b 100644 --- a/box/info_test.go +++ b/box/info_test.go @@ -1,22 +1,24 @@ -package box +package box_test import ( "testing" "github.com/stretchr/testify/require" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v2/box" ) func TestInfo(t *testing.T) { id := 1 cases := []struct { Name string - Struct Info + Struct box.Info Data map[string]interface{} }{ { Name: "Case: base info struct", - Struct: Info{ + Struct: box.Info{ Version: "2.11.4-0-g8cebbf2cad", ID: &id, RO: false, @@ -37,7 +39,7 @@ func TestInfo(t *testing.T) { }, { Name: "Case: info struct with replication", - Struct: Info{ + Struct: box.Info{ Version: "2.11.4-0-g8cebbf2cad", ID: &id, RO: false, @@ -45,7 +47,7 @@ func TestInfo(t *testing.T) { PID: 1, Status: "running", LSN: 8, - Replication: map[int]Replication{ + Replication: map[int]box.Replication{ 1: { ID: 1, UUID: "69360e9b-4641-4ec3-ab51-297f46749849", @@ -55,7 +57,7 @@ func TestInfo(t *testing.T) { ID: 2, UUID: "75f5f5aa-89f0-4d95-b5a9-96a0eaa0ce36", LSN: 0, - Upstream: Upstream{ + Upstream: box.Upstream{ Status: "follow", Idle: 2.4564633660484, Peer: "other.tarantool:3301", @@ -63,7 +65,7 @@ func TestInfo(t *testing.T) { Message: "'getaddrinfo: Name or service not known'", SystemMessage: "Input/output error", }, - Downstream: Downstream{ + Downstream: box.Downstream{ Status: "follow", Idle: 2.8306158290943, VClock: map[int]uint64{1: 8}, @@ -117,7 +119,7 @@ func TestInfo(t *testing.T) { data, err := msgpack.Marshal(tc.Data) require.NoError(t, err, tc.Name) - var result Info + var result box.Info err = msgpack.Unmarshal(data, &result) require.NoError(t, err, tc.Name) diff --git a/box/request.go b/box/request.go deleted file mode 100644 index bf51a72f6..000000000 --- a/box/request.go +++ /dev/null @@ -1,38 +0,0 @@ -package box - -import ( - "context" - "io" - - "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" -) - -type baseRequest struct { - impl *tarantool.CallRequest -} - -func newCall(method string) *tarantool.CallRequest { - return tarantool.NewCallRequest(method) -} - -// Type returns IPROTO type for request. -func (req baseRequest) Type() iproto.Type { - return req.impl.Type() -} - -// Ctx returns a context of request. -func (req baseRequest) Ctx() context.Context { - return req.impl.Ctx() -} - -// Async returns request expects a response. -func (req baseRequest) Async() bool { - return req.impl.Async() -} - -// Response creates a response for the baseRequest. -func (req baseRequest) Response(header tarantool.Header, - body io.Reader) (tarantool.Response, error) { - return req.impl.Response(header, body) -} diff --git a/box/schema.go b/box/schema.go new file mode 100644 index 000000000..25017e5a4 --- /dev/null +++ b/box/schema.go @@ -0,0 +1,21 @@ +package box + +import "github.com/tarantool/go-tarantool/v2" + +// Schema represents the schema-related operations in Tarantool. +// It holds a connection to interact with the Tarantool instance. +type Schema struct { + conn tarantool.Doer // Connection interface for interacting with Tarantool. +} + +// newSchema creates a new Schema instance with the provided Tarantool connection. +// It initializes a Schema object that can be used for schema-related operations +// such as managing users, tables, and other schema elements in the Tarantool instance. +func newSchema(conn tarantool.Doer) *Schema { + return &Schema{conn: conn} // Pass the connection to the Schema. +} + +// User returns a new SchemaUser instance, allowing schema-related user operations. +func (s *Schema) User() *SchemaUser { + return newSchemaUser(s.conn) +} diff --git a/box/schema_user.go b/box/schema_user.go new file mode 100644 index 000000000..80874da92 --- /dev/null +++ b/box/schema_user.go @@ -0,0 +1,545 @@ +package box + +import ( + "context" + "fmt" + "strings" + + "github.com/tarantool/go-tarantool/v2" + "github.com/vmihailenco/msgpack/v5" +) + +// SchemaUser provides methods to interact with schema-related user operations in Tarantool. +type SchemaUser struct { + conn tarantool.Doer // Connection interface for interacting with Tarantool. +} + +// newSchemaUser creates a new SchemaUser instance with the provided Tarantool connection. +// It initializes a SchemaUser object, which provides methods to perform user-related +// schema operations (such as creating, modifying, or deleting users) in the Tarantool instance. +func newSchemaUser(conn tarantool.Doer) *SchemaUser { + return &SchemaUser{conn: conn} +} + +// UserExistsRequest represents a request to check if a user exists in Tarantool. +type UserExistsRequest struct { + *tarantool.CallRequest // Underlying Tarantool call request. +} + +// UserExistsResponse represents the response to a user existence check. +type UserExistsResponse struct { + Exists bool // True if the user exists, false otherwise. +} + +// DecodeMsgpack decodes the response from a Msgpack-encoded byte slice. +func (uer *UserExistsResponse) DecodeMsgpack(d *msgpack.Decoder) error { + arrayLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + // Ensure that the response array contains exactly 1 element (the "Exists" field). + if arrayLen != 1 { + return fmt.Errorf("protocol violation; expected 1 array entry, got %d", arrayLen) + } + + // Decode the boolean value indicating whether the user exists. + uer.Exists, err = d.DecodeBool() + + return err +} + +// NewUserExistsRequest creates a new request to check if a user exists. +func NewUserExistsRequest(username string) UserExistsRequest { + callReq := tarantool.NewCallRequest("box.schema.user.exists").Args([]interface{}{username}) + + return UserExistsRequest{ + callReq, + } +} + +// Exists checks if the specified user exists in Tarantool. +func (u *SchemaUser) Exists(ctx context.Context, username string) (bool, error) { + // Create a request and send it to Tarantool. + req := NewUserExistsRequest(username).Context(ctx) + resp := &UserExistsResponse{} + + // Execute the request and parse the response. + err := u.conn.Do(req).GetTyped(resp) + + return resp.Exists, err +} + +// UserCreateOptions represents options for creating a user in Tarantool. +type UserCreateOptions struct { + // IfNotExists - if true, prevents an error if the user already exists. + IfNotExists bool `msgpack:"if_not_exists"` + // Password for the new user. + Password string `msgpack:"password"` +} + +// UserCreateRequest represents a request to create a new user in Tarantool. +type UserCreateRequest struct { + *tarantool.CallRequest // Underlying Tarantool call request. +} + +// NewUserCreateRequest creates a new request to create a user with specified options. +func NewUserCreateRequest(username string, options UserCreateOptions) UserCreateRequest { + callReq := tarantool.NewCallRequest("box.schema.user.create"). + Args([]interface{}{username, options}) + + return UserCreateRequest{ + callReq, + } +} + +// UserCreateResponse represents the response to a user creation request. +type UserCreateResponse struct{} + +// DecodeMsgpack decodes the response for a user creation request. +// In this case, the response does not contain any data. +func (uer *UserCreateResponse) DecodeMsgpack(_ *msgpack.Decoder) error { + return nil +} + +// Create creates a new user in Tarantool with the given username and options. +func (u *SchemaUser) Create(ctx context.Context, username string, options UserCreateOptions) error { + // Create a request and send it to Tarantool. + req := NewUserCreateRequest(username, options).Context(ctx) + resp := &UserCreateResponse{} + + // Execute the request and handle the response. + fut := u.conn.Do(req) + + err := fut.GetTyped(resp) + if err != nil { + return err + } + + return nil +} + +// UserDropOptions represents options for dropping a user in Tarantool. +type UserDropOptions struct { + IfExists bool `msgpack:"if_exists"` // If true, prevents an error if the user does not exist. +} + +// UserDropRequest represents a request to drop a user from Tarantool. +type UserDropRequest struct { + *tarantool.CallRequest // Underlying Tarantool call request. +} + +// NewUserDropRequest creates a new request to drop a user with specified options. +func NewUserDropRequest(username string, options UserDropOptions) UserDropRequest { + callReq := tarantool.NewCallRequest("box.schema.user.drop"). + Args([]interface{}{username, options}) + + return UserDropRequest{ + callReq, + } +} + +// UserDropResponse represents the response to a user drop request. +type UserDropResponse struct{} + +// Drop drops the specified user from Tarantool, with optional conditions. +func (u *SchemaUser) Drop(ctx context.Context, username string, options UserDropOptions) error { + // Create a request and send it to Tarantool. + req := NewUserDropRequest(username, options).Context(ctx) + resp := &UserCreateResponse{} + + // Execute the request and handle the response. + fut := u.conn.Do(req) + + err := fut.GetTyped(resp) + if err != nil { + return err + } + + return nil +} + +// UserPasswordRequest represents a request to retrieve a user's password from Tarantool. +type UserPasswordRequest struct { + *tarantool.CallRequest // Underlying Tarantool call request. +} + +// NewUserPasswordRequest creates a new request to fetch the user's password. +// It takes the username and constructs the request to Tarantool. +func NewUserPasswordRequest(username string) UserPasswordRequest { + // Create a request to get the user's password. + callReq := tarantool.NewCallRequest("box.schema.user.password").Args([]interface{}{username}) + + return UserPasswordRequest{ + callReq, + } +} + +// UserPasswordResponse represents the response to the user password request. +// It contains the password hash. +type UserPasswordResponse struct { + Hash string // The password hash of the user. +} + +// DecodeMsgpack decodes the response from Tarantool in Msgpack format. +// It expects the response to be an array of length 1, containing the password hash string. +func (upr *UserPasswordResponse) DecodeMsgpack(d *msgpack.Decoder) error { + // Decode the array length. + arrayLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + // Ensure the array contains exactly 1 element (the password hash). + if arrayLen != 1 { + return fmt.Errorf("protocol violation; expected 1 array entry, got %d", arrayLen) + } + + // Decode the string containing the password hash. + upr.Hash, err = d.DecodeString() + + return err +} + +// Password sends a request to retrieve the user's password from Tarantool. +// It returns the password hash as a string or an error if the request fails. +// It works just like hash function. +func (u *SchemaUser) Password(ctx context.Context, password string) (string, error) { + // Create the request and send it to Tarantool. + req := NewUserPasswordRequest(password).Context(ctx) + resp := &UserPasswordResponse{} + + // Execute the request and handle the response. + fut := u.conn.Do(req) + + // Get the decoded response. + err := fut.GetTyped(resp) + if err != nil { + return "", err + } + + // Return the password hash. + return resp.Hash, nil +} + +// UserPasswdRequest represents a request to change a user's password in Tarantool. +type UserPasswdRequest struct { + *tarantool.CallRequest // Underlying Tarantool call request. +} + +// NewUserPasswdRequest creates a new request to change a user's password in Tarantool. +func NewUserPasswdRequest(args ...string) (UserPasswdRequest, error) { + callReq := tarantool.NewCallRequest("box.schema.user.passwd") + + switch len(args) { + case 1: + callReq.Args([]interface{}{args[0]}) + case 2: + callReq.Args([]interface{}{args[0], args[1]}) + default: + return UserPasswdRequest{}, fmt.Errorf("len of fields must be 1 or 2, got %d", len(args)) + + } + + return UserPasswdRequest{callReq}, nil +} + +// UserPasswdResponse represents the response to a user passwd request. +type UserPasswdResponse struct{} + +// Passwd sends a request to set a password for a currently logged in or a specified user. +// A currently logged-in user can change their password using box.schema.user.passwd(password). +// An administrator can change the password of another user +// with box.schema.user.passwd(username, password). +func (u *SchemaUser) Passwd(ctx context.Context, args ...string) error { + req, err := NewUserPasswdRequest(args...) + if err != nil { + return err + } + + req.Context(ctx) + + resp := &UserPasswdResponse{} + + // Execute the request and handle the response. + fut := u.conn.Do(req) + + err = fut.GetTyped(resp) + if err != nil { + return err + } + + return nil +} + +// UserInfoRequest represents a request to get a user's info in Tarantool. +type UserInfoRequest struct { + *tarantool.CallRequest // Underlying Tarantool call request. +} + +// NewUserInfoRequest creates a new request to get user privileges. +func NewUserInfoRequest(username string) UserInfoRequest { + callReq := tarantool.NewCallRequest("box.schema.user.info").Args([]interface{}{username}) + + return UserInfoRequest{ + callReq, + } +} + +// PrivilegeType is a struct based on privilege object types list +// https://www.tarantool.io/en/doc/latest/admin/access_control/#all-object-types-and-permissions +type PrivilegeType string + +const ( + // PrivilegeUniverse - privilege type based on universe. + // A database (box.schema) that contains database objects, including spaces, + // indexes, users, roles, sequences, and functions. + // Granting privileges to universe gives a user access to any object in the database. + PrivilegeUniverse PrivilegeType = "universe" + // PrivilegeTypeUser - privilege type based on user. + // A user identifies a person or program that interacts with a Tarantool instance. + PrivilegeTypeUser PrivilegeType = "user" + // PrivilegeRole - privilege type based on role. + // A role is a container for privileges that can be granted to users. + // Roles can also be assigned to other roles, creating a role hierarchy. + PrivilegeRole PrivilegeType = "role" + // PrivilegeSpace - privilege type based on space. + // Tarantool stores tuples in containers called spaces. + PrivilegeSpace PrivilegeType = "space" + // PrivilegeFunction - privilege type based on functions. + // This allows access control based on function access. + PrivilegeFunction PrivilegeType = "function" + // PrivilegeSequence - privilege type based on sequences. + // A sequence is a generator of ordered integer values. + PrivilegeSequence PrivilegeType = "sequence" + // PrivilegeLuaEval - privilege type based on executing arbitrary Lua code. + PrivilegeLuaEval PrivilegeType = "lua_eval" + // PrivilegeLuaCall - privilege type based on + // calling any global user-defined Lua function. + PrivilegeLuaCall PrivilegeType = "lua_call" + // PrivilegeSQL - privilege type based on + // executing an arbitrary SQL expression. + PrivilegeSQL PrivilegeType = "sql" +) + +// Permission is a struct based on permission tarantool object +// https://www.tarantool.io/en/doc/latest/admin/access_control/#permissions +type Permission string + +const ( + // PermissionRead allows reading data of the specified object. + // For example, this permission can be used to allow a user + // to select data from the specified space. + PermissionRead Permission = "read" + // PermissionWrite allows updating data of the specified object. + // For example, this permission can be used to allow + // a user to modify data in the specified space. + PermissionWrite Permission = "write" + // PermissionCreate allows creating objects of the specified type. + // For example, this permission can be used to allow a user to create new spaces. + // Note that this permission requires read and write access to certain system spaces. + PermissionCreate Permission = "create" + // PermissionAlter allows altering objects of the specified type. + // Note that this permission requires read and write access to certain system spaces. + PermissionAlter Permission = "alter" + // PermissionDrop allows dropping objects of the specified type. + // Note that this permission requires read and write access to certain system spaces. + PermissionDrop Permission = "drop" + // PermissionExecute for role, + // allows using the specified role. For other object types, allows calling a function. + // Can be used only for role, universe, function, lua_eval, lua_call, sql. + PermissionExecute Permission = "execute" + // PermissionSession allows a user to connect to an instance over IPROTO. + PermissionSession Permission = "session" + // PermissionUsage allows a user to use their privileges on database objects + // (for example, read, write, and alter spaces). + PermissionUsage Permission = "usage" +) + +// Privilege is a structure that is used to create new rights, +// as well as obtain information for rights. +type Privilege struct { + // Permissions is a list of privileges that apply to the privileges object type. + Permissions []Permission + // Type - one of privilege object types (it might be space,function, etc.). + Type PrivilegeType + // Name - can be the name of a function or space, + // and can also be empty in case of universe access + Name string +} + +// UserInfoResponse represents the response to a user info request. +type UserInfoResponse struct { + Privileges []Privilege +} + +// DecodeMsgpack decodes the response from Tarantool in Msgpack format. +func (uer *UserInfoResponse) DecodeMsgpack(d *msgpack.Decoder) error { + rawArr := make([][][3]string, 0) + + err := d.Decode(&rawArr) + switch { + case err != nil: + return err + case len(rawArr) != 1: + return fmt.Errorf("protocol violation; expected 1 array, got %d", len(rawArr)) + } + + privileges := make([]Privilege, len(rawArr[0])) + + for i, rawPrivileges := range rawArr[0] { + strPerms := strings.Split(rawPrivileges[0], ",") + + perms := make([]Permission, len(strPerms)) + for j, strPerm := range strPerms { + perms[j] = Permission(strPerm) + } + + privileges[i] = Privilege{ + Permissions: perms, + Type: PrivilegeType(rawPrivileges[1]), + Name: rawPrivileges[2], + } + } + + uer.Privileges = privileges + + return nil +} + +// Info returns a list of user privileges according to the box.schema.user.info method call. +func (u *SchemaUser) Info(ctx context.Context, username string) ([]Privilege, error) { + req := NewUserInfoRequest(username).Context(ctx) + + resp := &UserInfoResponse{} + fut := u.conn.Do(req) + + err := fut.GetTyped(resp) + if err != nil { + return nil, err + } + + return resp.Privileges, nil +} + +// prepareGrantAndRevokeArgs prepares the arguments for granting or revoking user permissions. +// It accepts a username, a privilege, and options for either granting or revoking. +// The generic type T can be UserGrantOptions or UserRevokeOptions. +func prepareGrantAndRevokeArgs[T UserGrantOptions | UserRevokeOptions](username string, + privilege Privilege, opts T) []interface{} { + + args := []interface{}{username} // Initialize args slice with the username. + + switch privilege.Type { + case PrivilegeUniverse: + // Preparing arguments for granting permissions at the universe level. + // box.schema.user.grant(username, permissions, 'universe'[, nil, {options}]) + strPerms := make([]string, len(privilege.Permissions)) + for i, perm := range privilege.Permissions { + strPerms[i] = string(perm) // Convert each Permission to a string. + } + + reqPerms := strings.Join(strPerms, ",") // Join permissions into a single string. + + // Append universe-specific arguments to args. + args = append(args, reqPerms, string(privilege.Type), nil, opts) + case PrivilegeRole: + // Handling the case where the object type is a role name. + // Append role-specific arguments to args. + args = append(args, privilege.Name, nil, nil, opts) + default: + // Preparing arguments for granting permissions on a specific object. + strPerms := make([]string, len(privilege.Permissions)) + for i, perm := range privilege.Permissions { + strPerms[i] = string(perm) // Convert each Permission to a string. + } + + reqPerms := strings.Join(strPerms, ",") // Join permissions into a single string. + // box.schema.user.grant(username, permissions, object-type, object-name[, {options}]) + // Example: box.schema.user.grant('testuser', 'read', 'space', 'writers') + args = append(args, reqPerms, string(privilege.Type), privilege.Name, opts) + } + + return args // Return the prepared arguments. +} + +// UserGrantOptions holds options for granting permissions to a user. +type UserGrantOptions struct { + Grantor string `msgpack:"grantor,omitempty"` // Optional grantor name. + IfNotExists bool `msgpack:"if_not_exists"` // Option to skip if the grant already exists. +} + +// UserGrantRequest wraps a Tarantool call request for granting user permissions. +type UserGrantRequest struct { + *tarantool.CallRequest // Underlying Tarantool call request. +} + +// NewUserGrantRequest creates a new UserGrantRequest based on provided parameters. +func NewUserGrantRequest(username string, privilege Privilege, + opts UserGrantOptions) UserGrantRequest { + args := prepareGrantAndRevokeArgs[UserGrantOptions](username, privilege, opts) + + // Create a new call request for the box.schema.user.grant method with the given args. + callReq := tarantool.NewCallRequest("box.schema.user.grant").Args(args) + + return UserGrantRequest{callReq} // Return the UserGrantRequest. +} + +// UserGrantResponse represents the response from a user grant request. +type UserGrantResponse struct{} + +// Grant executes the user grant operation in Tarantool, returning an error if it fails. +func (u *SchemaUser) Grant(ctx context.Context, username string, privilege Privilege, + opts UserGrantOptions) error { + req := NewUserGrantRequest(username, privilege, opts).Context(ctx) + + resp := &UserGrantResponse{} // Initialize a response object. + fut := u.conn.Do(req) // Execute the request. + + err := fut.GetTyped(resp) // Get the typed response and check for errors. + if err != nil { + return err // Return any errors encountered. + } + + return nil // Return nil if the operation was successful. +} + +// UserRevokeOptions holds options for revoking permissions from a user. +type UserRevokeOptions struct { + IfExists bool `msgpack:"if_exists"` // Option to skip if the revoke does not exist. +} + +// UserRevokeRequest wraps a Tarantool call request for revoking user permissions. +type UserRevokeRequest struct { + *tarantool.CallRequest // Underlying Tarantool call request. +} + +// UserRevokeResponse represents the response from a user revoke request. +type UserRevokeResponse struct{} + +// NewUserRevokeRequest creates a new UserRevokeRequest based on provided parameters. +func NewUserRevokeRequest(username string, privilege Privilege, + opts UserRevokeOptions) UserRevokeRequest { + args := prepareGrantAndRevokeArgs[UserRevokeOptions](username, privilege, opts) + + // Create a new call request for the box.schema.user.revoke method with the given args. + callReq := tarantool.NewCallRequest("box.schema.user.revoke").Args(args) + + return UserRevokeRequest{callReq} +} + +// Revoke executes the user revoke operation in Tarantool, returning an error if it fails. +func (u *SchemaUser) Revoke(ctx context.Context, username string, privilege Privilege, + opts UserRevokeOptions) error { + req := NewUserRevokeRequest(username, privilege, opts).Context(ctx) + + resp := &UserRevokeResponse{} // Initialize a response object. + fut := u.conn.Do(req) // Execute the request. + + err := fut.GetTyped(resp) // Get the typed response and check for errors. + if err != nil { + return err + } + + return nil +} diff --git a/box/schema_user_test.go b/box/schema_user_test.go new file mode 100644 index 000000000..b9f3e18de --- /dev/null +++ b/box/schema_user_test.go @@ -0,0 +1,195 @@ +package box_test + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/v2" + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-tarantool/v2/box" +) + +func TestUserExistsResponse_DecodeMsgpack(t *testing.T) { + tCases := map[bool]func() *bytes.Buffer{ + true: func() *bytes.Buffer { + buf := bytes.NewBuffer(nil) + buf.WriteByte(msgpcode.FixedArrayLow | byte(1)) + buf.WriteByte(msgpcode.True) + + return buf + }, + false: func() *bytes.Buffer { + buf := bytes.NewBuffer(nil) + buf.WriteByte(msgpcode.FixedArrayLow | byte(1)) + buf.WriteByte(msgpcode.False) + + return buf + }, + } + + for tCaseBool, tCaseBuf := range tCases { + tCaseBool := tCaseBool + tCaseBuf := tCaseBuf() + + t.Run(fmt.Sprintf("case: %t", tCaseBool), func(t *testing.T) { + t.Parallel() + + resp := box.UserExistsResponse{} + + require.NoError(t, resp.DecodeMsgpack(msgpack.NewDecoder(tCaseBuf))) + require.Equal(t, tCaseBool, resp.Exists) + }) + } + +} + +func TestUserPasswordResponse_DecodeMsgpack(t *testing.T) { + tCases := []string{ + "test", + "$tr0ng_pass", + } + + for _, tCase := range tCases { + tCase := tCase + + t.Run(tCase, func(t *testing.T) { + t.Parallel() + buf := bytes.NewBuffer(nil) + buf.WriteByte(msgpcode.FixedArrayLow | byte(1)) + + bts, err := msgpack.Marshal(tCase) + require.NoError(t, err) + buf.Write(bts) + + resp := box.UserPasswordResponse{} + + err = resp.DecodeMsgpack(msgpack.NewDecoder(buf)) + require.NoError(t, err) + require.Equal(t, tCase, resp.Hash) + }) + } + +} + +func FuzzUserPasswordResponse_DecodeMsgpack(f *testing.F) { + f.Fuzz(func(t *testing.T, orig string) { + buf := bytes.NewBuffer(nil) + buf.WriteByte(msgpcode.FixedArrayLow | byte(1)) + + bts, err := msgpack.Marshal(orig) + require.NoError(t, err) + buf.Write(bts) + + resp := box.UserPasswordResponse{} + + err = resp.DecodeMsgpack(msgpack.NewDecoder(buf)) + require.NoError(t, err) + require.Equal(t, orig, resp.Hash) + }) +} + +func TestNewUserExistsRequest(t *testing.T) { + t.Parallel() + + req := box.UserExistsRequest{} + + require.NotPanics(t, func() { + req = box.NewUserExistsRequest("test") + }) + + require.Implements(t, (*tarantool.Request)(nil), req) +} + +func TestNewUserCreateRequest(t *testing.T) { + t.Parallel() + + req := box.UserCreateRequest{} + + require.NotPanics(t, func() { + req = box.NewUserCreateRequest("test", box.UserCreateOptions{}) + }) + + require.Implements(t, (*tarantool.Request)(nil), req) +} + +func TestNewUserDropRequest(t *testing.T) { + t.Parallel() + + req := box.UserDropRequest{} + + require.NotPanics(t, func() { + req = box.NewUserDropRequest("test", box.UserDropOptions{}) + }) + + require.Implements(t, (*tarantool.Request)(nil), req) +} + +func TestNewUserPasswordRequest(t *testing.T) { + t.Parallel() + + req := box.UserPasswordRequest{} + + require.NotPanics(t, func() { + req = box.NewUserPasswordRequest("test") + }) + + require.Implements(t, (*tarantool.Request)(nil), req) +} + +func TestNewUserPasswdRequest(t *testing.T) { + t.Parallel() + + var err error + req := box.UserPasswdRequest{} + + require.NotPanics(t, func() { + req, err = box.NewUserPasswdRequest("test") + require.NoError(t, err) + }) + + _, err = box.NewUserPasswdRequest() + require.Errorf(t, err, "invalid arguments count") + + require.Implements(t, (*tarantool.Request)(nil), req) +} + +func TestNewUserInfoRequest(t *testing.T) { + t.Parallel() + + var err error + req := box.UserInfoRequest{} + + require.NotPanics(t, func() { + req = box.NewUserInfoRequest("test") + require.NoError(t, err) + }) + + require.Implements(t, (*tarantool.Request)(nil), req) +} + +func TestNewUserGrantRequest(t *testing.T) { + t.Parallel() + + var err error + req := box.UserGrantRequest{} + + require.NotPanics(t, func() { + req = box.NewUserGrantRequest("test", box.Privilege{ + Permissions: []box.Permission{ + box.PermissionAlter, + box.PermissionCreate, + box.PermissionDrop, + }, + Type: box.PrivilegeUniverse, + Name: "test", + }, box.UserGrantOptions{IfNotExists: true}) + require.NoError(t, err) + }) + + assert.Implements(t, (*tarantool.Request)(nil), req) +} diff --git a/box/session.go b/box/session.go new file mode 100644 index 000000000..b63113581 --- /dev/null +++ b/box/session.go @@ -0,0 +1,57 @@ +package box + +import ( + "context" + + "github.com/tarantool/go-tarantool/v2" +) + +// Session struct represents a connection session to Tarantool. +type Session struct { + conn tarantool.Doer // Connection interface for interacting with Tarantool. +} + +// newSession creates a new Session instance, taking a Tarantool connection as an argument. +func newSession(conn tarantool.Doer) *Session { + return &Session{conn: conn} // Pass the connection to the Session structure. +} + +// Session method returns a new Session object associated with the Box instance. +func (b *Box) Session() *Session { + return newSession(b.conn) +} + +// SessionSuRequest struct wraps a Tarantool call request specifically for session switching. +type SessionSuRequest struct { + *tarantool.CallRequest // Underlying Tarantool call request. +} + +// NewSessionSuRequest creates a new SessionSuRequest for switching session to a specified username. +// It returns an error if any execute functions are provided, as they are not supported now. +func NewSessionSuRequest(username string) (SessionSuRequest, error) { + args := []interface{}{username} // Create args slice with the username. + + // Create a new call request for the box.session.su method with the given args. + callReq := tarantool.NewCallRequest("box.session.su").Args(args) + + return SessionSuRequest{ + callReq, // Return the new SessionSuRequest containing the call request. + }, nil +} + +// Su method is used to switch the session to the specified username. +// It sends the request to Tarantool and returns an error. +func (s *Session) Su(ctx context.Context, username string) error { + // Create a request and send it to Tarantool. + req, err := NewSessionSuRequest(username) + if err != nil { + return err // Return any errors encountered while creating the request. + } + + req.Context(ctx) // Attach the context to the request for cancellation and timeout. + + // Execute the request and return the future response, or an error. + fut := s.conn.Do(req) + _, err = fut.GetResponse() + return err +} diff --git a/box/session_test.go b/box/session_test.go new file mode 100644 index 000000000..07ada0436 --- /dev/null +++ b/box/session_test.go @@ -0,0 +1,20 @@ +package box_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/tarantool/go-tarantool/v2/box" + th "github.com/tarantool/go-tarantool/v2/test_helpers" +) + +func TestBox_Session(t *testing.T) { + b := box.New(th.Ptr(th.NewMockDoer(t))) + require.NotNil(t, b.Session()) +} + +func TestNewSessionSuRequest(t *testing.T) { + _, err := box.NewSessionSuRequest("admin") + require.NoError(t, err) +} diff --git a/box/tarantool_test.go b/box/tarantool_test.go index 515eac3d0..0eb9e94b9 100644 --- a/box/tarantool_test.go +++ b/box/tarantool_test.go @@ -2,6 +2,7 @@ package box_test import ( "context" + "errors" "log" "os" "testing" @@ -9,6 +10,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" "github.com/tarantool/go-tarantool/v2" "github.com/tarantool/go-tarantool/v2/box" "github.com/tarantool/go-tarantool/v2/test_helpers" @@ -67,6 +69,595 @@ func TestBox_Info(t *testing.T) { validateInfo(t, resp.Info) } +func TestBox_Sugar_Schema_UserCreate_NoError(t *testing.T) { + const ( + username = "user_create_no_error" + password = "user_create_no_error" + ) + + defer cleanupUser(username) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + // Create new user. + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) + require.NoError(t, err) +} + +func TestBox_Sugar_Schema_UserCreate_CanConnectWithNewCred(t *testing.T) { + const ( + username = "can_connect_with_new_cred" + password = "can_connect_with_new_cred" + ) + + defer cleanupUser(username) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + // Create new user. + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) + require.NoError(t, err) + + // Can connect with new credentials + // Check that password is valid, and we can connect to tarantool with such credentials + var newUserDialer = tarantool.NetDialer{ + Address: server, + User: username, + Password: password, + } + + // We can connect with our new credentials + newUserConn, err := tarantool.Connect(ctx, newUserDialer, tarantool.Opts{}) + require.NoError(t, err) + require.NotNil(t, newUserConn) + require.NoError(t, newUserConn.Close()) +} + +func TestBox_Sugar_Schema_UserCreate_AlreadyExists(t *testing.T) { + const ( + username = "create_already_exists" + password = "create_already_exists" + ) + + defer cleanupUser(username) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + // Create new user. + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) + require.NoError(t, err) + + // Create user already exists error. + // Get error that user already exists. + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) + require.Error(t, err) + + // Require that error code is ER_USER_EXISTS. + var boxErr tarantool.Error + errors.As(err, &boxErr) + require.Equal(t, iproto.ER_USER_EXISTS, boxErr.Code) +} + +func TestBox_Sugar_Schema_UserCreate_ExistsTrue(t *testing.T) { + const ( + username = "exists_check" + password = "exists_check" + ) + + defer cleanupUser(username) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + // Create new user. + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) + require.NoError(t, err) + + // Check that already exists by exists call procedure + exists, err := b.Schema().User().Exists(ctx, username) + require.True(t, exists) + require.NoError(t, err) + +} + +func TestBox_Sugar_Schema_UserCreate_IfNotExistsNoErr(t *testing.T) { + const ( + username = "if_not_exists_no_err" + password = "if_not_exists_no_err" + ) + + defer cleanupUser(username) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + // Create new user. + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) + require.NoError(t, err) + + // Again create such user. + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{ + Password: password, + IfNotExists: true, + }) + require.NoError(t, err) +} + +func TestBox_Sugar_Schema_UserPassword(t *testing.T) { + const ( + password = "passwd" + ) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + // Require password hash. + hash, err := b.Schema().User().Password(ctx, password) + require.NoError(t, err) + require.NotEmpty(t, hash) +} + +func TestBox_Sugar_Schema_UserDrop_AfterCreate(t *testing.T) { + const ( + username = "to_drop_after_create" + password = "to_drop_after_create" + ) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + // Create new user + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) + require.NoError(t, err) + + // Try to drop user + err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{}) + require.NoError(t, err) +} + +func TestBox_Sugar_Schema_UserDrop_DoubleDrop(t *testing.T) { + const ( + username = "to_drop_double_drop" + password = "to_drop_double_drop" + ) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + // Create new user + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) + require.NoError(t, err) + + // Try to drop user first time + err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{}) + require.NoError(t, err) + + // Error double drop with IfExists: false option + err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{}) + require.Error(t, err) + + // Require no error with IfExists: true option. + err = b.Schema().User().Drop(ctx, username, + box.UserDropOptions{IfExists: true}) + require.NoError(t, err) +} + +func TestBox_Sugar_Schema_UserDrop_UnknownUser(t *testing.T) { + t.Parallel() + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + // Require error cause user not exists + err = b.Schema().User().Drop(ctx, "some_strange_not_existing_name", box.UserDropOptions{}) + require.Error(t, err) + + var boxErr tarantool.Error + + // Require that error code is ER_NO_SUCH_USER + errors.As(err, &boxErr) + require.Equal(t, iproto.ER_NO_SUCH_USER, boxErr.Code) +} + +func TestSchemaUser_Passwd_NotFound(t *testing.T) { + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + err = b.Schema().User().Passwd(ctx, "not-exists-passwd", "new_password") + require.Error(t, err) + // Require that error code is ER_USER_EXISTS. + var boxErr tarantool.Error + errors.As(err, &boxErr) + require.Equal(t, iproto.ER_NO_SUCH_USER, boxErr.Code) +} + +func TestSchemaUser_Passwd_Ok(t *testing.T) { + const ( + username = "new_password_user" + startPassword = "new_password" + endPassword = "end_password" + ) + + defer cleanupUser(username) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + // New user change password and connect + + err = b.Schema().User().Create(ctx, username, + box.UserCreateOptions{Password: startPassword, IfNotExists: true}) + require.NoError(t, err) + + err = b.Schema().User().Passwd(ctx, username, endPassword) + require.NoError(t, err) + + dialer := dialer + dialer.User = username + dialer.Password = startPassword + + // Can't connect with old password. + _, err = tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.Error(t, err, "can't connect with old password") + + // Ok connection with new password. + dialer.Password = endPassword + _, err = tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err, "ok connection with new password") +} + +func TestSchemaUser_Passwd_WithoutGrants(t *testing.T) { + const ( + username = "new_password_user_fail_conn" + startPassword = "new_password" + endPassword = "end_password" + ) + defer cleanupUser(username) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + err = b.Schema().User().Create(ctx, username, + box.UserCreateOptions{Password: startPassword, IfNotExists: true}) + require.NoError(t, err) + + dialer := dialer + dialer.User = username + dialer.Password = startPassword + + conn2Fail, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + require.NotNil(t, conn2Fail) + + bFail := box.New(conn2Fail) + // can't change self user password without grants + err = bFail.Schema().User().Passwd(ctx, endPassword) + require.Error(t, err) + + // Require that error code is AccessDeniedError, + var boxErr tarantool.Error + errors.As(err, &boxErr) + require.Equal(t, iproto.ER_ACCESS_DENIED, boxErr.Code) + +} + +func TestSchemaUser_Info_TestUserCorrect(t *testing.T) { + t.Parallel() + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + privileges, err := b.Schema().User().Info(ctx, dialer.User) + require.NoError(t, err) + require.NotNil(t, privileges) + + require.Len(t, privileges, 4) +} + +func TestSchemaUser_Info_NonExistsUser(t *testing.T) { + t.Parallel() + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + privileges, err := b.Schema().User().Info(ctx, "non-existing") + require.Error(t, err) + require.Nil(t, privileges) +} + +func TestBox_Sugar_Schema_UserGrant_NoSu(t *testing.T) { + const ( + username = "to_grant_no_su" + password = "to_grant_no_su" + ) + + defer cleanupUser(username) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) + require.NoError(t, err) + + err = b.Schema().User().Grant(ctx, username, box.Privilege{ + Permissions: []box.Permission{ + box.PermissionRead, + }, + Type: box.PrivilegeSpace, + Name: "space1", + }, box.UserGrantOptions{IfNotExists: false}) + require.Error(t, err) + + // Require that error code is ER_ACCESS_DENIED. + var boxErr tarantool.Error + errors.As(err, &boxErr) + require.Equal(t, iproto.ER_ACCESS_DENIED, boxErr.Code) +} + +func TestBox_Sugar_Schema_UserGrant_WithSu(t *testing.T) { + const ( + username = "to_grant_with_su" + password = "to_grant_with_su" + ) + + defer cleanupUser(username) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) + require.NoError(t, err) + + startPrivilages, err := b.Schema().User().Info(ctx, username) + require.NoError(t, err) + + err = b.Session().Su(ctx, "admin") + require.NoError(t, err) + + require.NoError(t, err, "default user in super group") + + newPrivilege := box.Privilege{ + Permissions: []box.Permission{ + box.PermissionRead, + }, + Type: box.PrivilegeSpace, + Name: "space1", + } + + require.NotContains(t, startPrivilages, newPrivilege) + + err = b.Schema().User().Grant(ctx, + username, + newPrivilege, + box.UserGrantOptions{ + IfNotExists: false, + }) + require.NoError(t, err) + + endPrivileges, err := b.Schema().User().Info(ctx, username) + require.NoError(t, err) + require.NotEqual(t, startPrivilages, endPrivileges) + require.Contains(t, endPrivileges, newPrivilege) +} + +func TestSchemaUser_Revoke_WithoutSu(t *testing.T) { + const ( + username = "to_revoke_without_su" + password = "to_revoke_without_su" + ) + + defer cleanupUser(username) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) + require.NoError(t, err) + + // Can`t revoke without su permissions. + err = b.Schema().User().Grant(ctx, username, box.Privilege{ + Permissions: []box.Permission{ + box.PermissionRead, + }, + Type: box.PrivilegeSpace, + Name: "space1", + }, box.UserGrantOptions{IfNotExists: false}) + require.Error(t, err) + + // Require that error code is ER_ACCESS_DENIED. + var boxErr tarantool.Error + errors.As(err, &boxErr) + require.Equal(t, iproto.ER_ACCESS_DENIED, boxErr.Code) +} + +func TestSchemaUser_Revoke_WithSu(t *testing.T) { + const ( + username = "to_revoke_with_su" + password = "to_revoke_with_su" + ) + + defer cleanupUser(username) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) + require.NoError(t, err) + + // Can revoke with su admin permissions. + startPrivileges, err := b.Schema().User().Info(ctx, username) + require.NoError(t, err) + + err = b.Session().Su(ctx, "admin") + require.NoError(t, err) + + require.NoError(t, err, "dialer user in super group") + + require.NotEmpty(t, startPrivileges) + // Let's choose random first privilege. + examplePriv := startPrivileges[0] + + // Revoke it. + err = b.Schema().User().Revoke(ctx, + username, + examplePriv, + box.UserRevokeOptions{ + IfExists: false, + }) + + require.NoError(t, err) + + privileges, err := b.Schema().User().Info(ctx, username) + require.NoError(t, err) + + require.NotEqual(t, startPrivileges, privileges) + require.NotContains(t, privileges, examplePriv) +} + +func TestSchemaUser_Revoke_NonExistsPermission(t *testing.T) { + const ( + username = "to_revoke_non_exists_permission" + password = "to_revoke_non_exists_permission" + ) + + defer cleanupUser(username) + + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) + require.NoError(t, err) + + startPrivileges, err := b.Schema().User().Info(ctx, username) + require.NoError(t, err) + + err = b.Session().Su(ctx, "admin") + require.NoError(t, err) + + require.NoError(t, err, "dialer user in super group") + + require.NotEmpty(t, startPrivileges) + examplePriv := box.Privilege{ + Permissions: []box.Permission{box.PermissionRead}, + Name: "non_existing_space", + Type: box.PrivilegeSpace, + } + + err = b.Schema().User().Revoke(ctx, + username, + examplePriv, + box.UserRevokeOptions{ + IfExists: false, + }) + + require.Error(t, err) +} + +func TestSession_Su_AdminPermissions(t *testing.T) { + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + b := box.New(conn) + + err = b.Session().Su(ctx, "admin") + require.NoError(t, err) +} + +func cleanupUser(username string) { + ctx := context.TODO() + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + if err != nil { + log.Fatal(err) + } + + b := box.New(conn) + err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{}) + if err != nil { + log.Fatal(err) + } +} + func runTestMain(m *testing.M) int { instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{ Dialer: dialer, @@ -76,13 +667,13 @@ func runTestMain(m *testing.M) int { ConnectRetry: 10, RetryTimeout: 500 * time.Millisecond, }) - defer test_helpers.StopTarantoolWithCleanup(instance) - if err != nil { log.Printf("Failed to prepare test Tarantool: %s", err) return 1 } + defer test_helpers.StopTarantoolWithCleanup(instance) + return m.Run() } diff --git a/box/testdata/config.lua b/box/testdata/config.lua index 061439aa1..3d0db6acb 100644 --- a/box/testdata/config.lua +++ b/box/testdata/config.lua @@ -4,8 +4,10 @@ box.cfg{ work_dir = os.getenv("TEST_TNT_WORK_DIR"), } +box.schema.space.create('space1') + box.schema.user.create('test', { password = 'test' , if_not_exists = true }) -box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) +box.schema.user.grant('test', 'super', nil, nil, { if_not_exists = true }) -- Set listen only when every other thing is configured. box.cfg{ diff --git a/box_error_test.go b/box_error_test.go index 43f3c6956..3d7f7345d 100644 --- a/box_error_test.go +++ b/box_error_test.go @@ -190,7 +190,7 @@ var mpDecodeSamples = map[string]struct { func TestMessagePackDecode(t *testing.T) { for name, testcase := range mpDecodeSamples { t.Run(name, func(t *testing.T) { - var val *BoxError = &BoxError{} + var val = &BoxError{} err := val.UnmarshalMsgpack(testcase.b) if testcase.ok { require.Nilf(t, err, "No errors on decode") diff --git a/test_helpers/utils.go b/test_helpers/utils.go index eff7e1dbe..d5a65be10 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -281,3 +281,13 @@ func CheckEqualBoxErrors(t *testing.T, expected tarantool.BoxError, actual taran } } } + +// Ptr returns a pointer to an existing value. +// +// Example: +// +// func NewInt() int { return 1 } +// var b *int = Ptr(NewInt()) +func Ptr[T any](val T) *T { + return &val +} From 20494e9755aaf7c79e3763ecb683e61298822c3d Mon Sep 17 00:00:00 2001 From: Mergen Imeev Date: Fri, 28 Mar 2025 14:29:10 +0300 Subject: [PATCH 579/605] api: fix panic in conn.NewWatcher() Before this patch, `conn.c` was not checked for `nil` before calling its method. This could cause a panic if the connection was lost or closed. Closes #438 --- CHANGELOG.md | 3 ++ connection.go | 2 +- tarantool_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5af49c730..1ea36601b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +- Fixed panic when calling NewWatcher() during reconnection or after + connection is closed (#438). + ## [v2.3.2] - 2025-04-14 This release improves the logic of `Connect` and `pool.Connect` in case of a diff --git a/connection.go b/connection.go index 2b43f2ec0..5f976fbf8 100644 --- a/connection.go +++ b/connection.go @@ -1461,7 +1461,7 @@ func (conn *Connection) NewWatcher(key string, callback WatchCallback) (Watcher, // That's why we can't just check the Tarantool response for an unsupported // request error. if !isFeatureInSlice(iproto.IPROTO_FEATURE_WATCHERS, - conn.c.ProtocolInfo().Features) { + conn.serverProtocolInfo.Features) { err := fmt.Errorf("the feature %s must be supported by connection "+ "to create a watcher", iproto.IPROTO_FEATURE_WATCHERS) return nil, err diff --git a/tarantool_test.go b/tarantool_test.go index ded7ba452..b053c5aef 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3550,6 +3550,72 @@ func TestConnection_NewWatcher(t *testing.T) { } } +func newWatcherReconnectionPrepareTestConnection(t *testing.T) (*Connection, context.CancelFunc) { + t.Helper() + + const server = "127.0.0.1:3015" + testDialer := dialer + testDialer.Address = server + + inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + Dialer: testDialer, + InitScript: "config.lua", + Listen: server, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 10, + RetryTimeout: 500 * time.Millisecond, + }) + t.Cleanup(func() { test_helpers.StopTarantoolWithCleanup(inst) }) + if err != nil { + t.Fatalf("Unable to start Tarantool: %s", err) + } + + ctx, cancel := test_helpers.GetConnectContext() + + reconnectOpts := opts + reconnectOpts.Reconnect = 100 * time.Millisecond + reconnectOpts.MaxReconnects = 0 + reconnectOpts.Notify = make(chan ConnEvent) + conn, err := Connect(ctx, testDialer, reconnectOpts) + if err != nil { + t.Fatalf("Connection was not established: %v", err) + } + + test_helpers.StopTarantool(inst) + + // Wait for reconnection process to be started. + for conn.ConnectedNow() { + time.Sleep(100 * time.Millisecond) + } + + return conn, cancel +} + +func TestNewWatcherDuringReconnect(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + conn, cancel := newWatcherReconnectionPrepareTestConnection(t) + defer conn.Close() + defer cancel() + + _, err := conn.NewWatcher("one", func(event WatchEvent) {}) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "client connection is not ready") +} + +func TestNewWatcherAfterClose(t *testing.T) { + test_helpers.SkipIfWatchersUnsupported(t) + + conn, cancel := newWatcherReconnectionPrepareTestConnection(t) + defer cancel() + + _ = conn.Close() + + _, err := conn.NewWatcher("one", func(event WatchEvent) {}) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "using closed connection") +} + func TestConnection_NewWatcher_noWatchersFeature(t *testing.T) { test_helpers.SkipIfWatchersSupported(t) @@ -4149,13 +4215,13 @@ func runTestMain(m *testing.M) int { startOpts.MemtxUseMvccEngine = !isStreamUnsupported inst, err := test_helpers.StartTarantool(startOpts) - defer test_helpers.StopTarantoolWithCleanup(inst) - if err != nil { log.Printf("Failed to prepare test tarantool: %s", err) return 1 } + defer test_helpers.StopTarantoolWithCleanup(inst) + return m.Run() } From 4567442d28b7f0363c49e2bf20ddfa41265bfc31 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Mon, 30 Jun 2025 20:14:57 +0300 Subject: [PATCH 580/605] const: add 'concurrent schema update' error constant Closes #404, TNTP-3336 --- CHANGELOG.md | 2 ++ schema.go | 6 +++++- schema_test.go | 5 +++++ tarantool_test.go | 15 +++++++-------- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ea36601b..f9b6d549e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Implemented all box.schema.user operations requests and sugar interface (#426). - Implemented box.session.su request and sugar interface only for current session granting (#426). +- Defined `ErrConcurrentSchemaUpdate` constant for "concurrent schema update" error. + Now you can check this error with `errors.Is(err, tarantool.ErrConcurrentSchemaUpdate)`. ### Changed diff --git a/schema.go b/schema.go index 72b5e397f..e7c09f80e 100644 --- a/schema.go +++ b/schema.go @@ -19,6 +19,10 @@ const ( vspaceSpFormatFieldNum = 7 ) +var ( + ErrConcurrentSchemaUpdate = errors.New("concurrent schema update") +) + func msgpackIsUint(code byte) bool { return code == msgpcode.Uint8 || code == msgpcode.Uint16 || code == msgpcode.Uint32 || code == msgpcode.Uint64 || @@ -415,7 +419,7 @@ func GetSchema(doer Doer) (Schema, error) { schema.SpacesById[spaceId].IndexesById[index.Id] = index schema.SpacesById[spaceId].Indexes[index.Name] = index } else { - return Schema{}, errors.New("concurrent schema update") + return Schema{}, ErrConcurrentSchemaUpdate } } diff --git a/schema_test.go b/schema_test.go index 631591cb2..cbc863917 100644 --- a/schema_test.go +++ b/schema_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vmihailenco/msgpack/v5" @@ -160,3 +161,7 @@ func TestResolverNotCalledWithNameSupport(t *testing.T) { resolver.indexResolverCalls) } } + +func TestErrConcurrentSchemaUpdate(t *testing.T) { + assert.EqualError(t, tarantool.ErrConcurrentSchemaUpdate, "concurrent schema update") +} diff --git a/tarantool_test.go b/tarantool_test.go index b053c5aef..14bb63ef7 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3999,14 +3999,13 @@ func TestConnect_schema_update(t *testing.T) { for i := 0; i < 100; i++ { fut := conn.Do(NewCallRequest("create_spaces")) - if conn, err := Connect(ctx, dialer, opts); err != nil { - if err.Error() != "concurrent schema update" { - t.Errorf("unexpected error: %s", err) - } - } else if conn == nil { - t.Errorf("conn is nil") - } else { - conn.Close() + switch conn, err := Connect(ctx, dialer, opts); { + case err != nil: + assert.ErrorIs(t, err, ErrConcurrentSchemaUpdate) + case conn == nil: + assert.Fail(t, "conn is nil") + default: + _ = conn.Close() } if _, err := fut.Get(); err != nil { From ccc856fb51f8a40e16293768aa2bc7515f86d74d Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Wed, 25 Jun 2025 20:25:07 +0300 Subject: [PATCH 581/605] refactor: get rid of fillXXX functions, in favor of Body method NB: all deleted tests were checking that functions passed from constructor object to fillXXX function were passed in right order, so removing them is not a problem. --- export_test.go | 158 ----------- future_test.go | 2 +- prepared.go | 64 +++-- protocol.go | 44 +-- request.go | 403 ++++++++++++++++------------ request_test.go | 693 +----------------------------------------------- stream.go | 101 +++---- watch.go | 4 + 8 files changed, 347 insertions(+), 1122 deletions(-) delete mode 100644 export_test.go diff --git a/export_test.go b/export_test.go deleted file mode 100644 index ab3784b3b..000000000 --- a/export_test.go +++ /dev/null @@ -1,158 +0,0 @@ -package tarantool - -import ( - "time" - - "github.com/vmihailenco/msgpack/v5" -) - -// RefImplPingBody is reference implementation for filling of a ping -// request's body. -func RefImplPingBody(enc *msgpack.Encoder) error { - return fillPing(enc) -} - -// RefImplSelectBody is reference implementation for filling of a select -// request's body. -func RefImplSelectBody(enc *msgpack.Encoder, res SchemaResolver, space, index interface{}, - offset, limit uint32, iterator Iter, key, after interface{}, fetchPos bool) error { - spaceEnc, err := newSpaceEncoder(res, space) - if err != nil { - return err - } - indexEnc, err := newIndexEncoder(res, index, spaceEnc.Id) - if err != nil { - return err - } - return fillSelect(enc, spaceEnc, indexEnc, offset, limit, iterator, key, after, fetchPos) -} - -// RefImplInsertBody is reference implementation for filling of an insert -// request's body. -func RefImplInsertBody(enc *msgpack.Encoder, res SchemaResolver, space, - tuple interface{}) error { - spaceEnc, err := newSpaceEncoder(res, space) - if err != nil { - return err - } - return fillInsert(enc, spaceEnc, tuple) -} - -// RefImplReplaceBody is reference implementation for filling of a replace -// request's body. -func RefImplReplaceBody(enc *msgpack.Encoder, res SchemaResolver, space, - tuple interface{}) error { - spaceEnc, err := newSpaceEncoder(res, space) - if err != nil { - return err - } - return fillInsert(enc, spaceEnc, tuple) -} - -// RefImplDeleteBody is reference implementation for filling of a delete -// request's body. -func RefImplDeleteBody(enc *msgpack.Encoder, res SchemaResolver, space, index, - key interface{}) error { - spaceEnc, err := newSpaceEncoder(res, space) - if err != nil { - return err - } - indexEnc, err := newIndexEncoder(res, index, spaceEnc.Id) - if err != nil { - return err - } - return fillDelete(enc, spaceEnc, indexEnc, key) -} - -// RefImplUpdateBody is reference implementation for filling of an update -// request's body. -func RefImplUpdateBody(enc *msgpack.Encoder, res SchemaResolver, space, index, - key interface{}, ops *Operations) error { - spaceEnc, err := newSpaceEncoder(res, space) - if err != nil { - return err - } - indexEnc, err := newIndexEncoder(res, index, spaceEnc.Id) - if err != nil { - return err - } - return fillUpdate(enc, spaceEnc, indexEnc, key, ops) -} - -// RefImplUpsertBody is reference implementation for filling of an upsert -// request's body. -func RefImplUpsertBody(enc *msgpack.Encoder, res SchemaResolver, space, - tuple interface{}, ops *Operations) error { - spaceEnc, err := newSpaceEncoder(res, space) - if err != nil { - return err - } - return fillUpsert(enc, spaceEnc, tuple, ops) -} - -// RefImplCallBody is reference implementation for filling of a call or call17 -// request's body. -func RefImplCallBody(enc *msgpack.Encoder, function string, args interface{}) error { - return fillCall(enc, function, args) -} - -// RefImplEvalBody is reference implementation for filling of an eval -// request's body. -func RefImplEvalBody(enc *msgpack.Encoder, expr string, args interface{}) error { - return fillEval(enc, expr, args) -} - -// RefImplExecuteBody is reference implementation for filling of an execute -// request's body. -func RefImplExecuteBody(enc *msgpack.Encoder, expr string, args interface{}) error { - return fillExecute(enc, expr, args) -} - -// RefImplPrepareBody is reference implementation for filling of an prepare -// request's body. -func RefImplPrepareBody(enc *msgpack.Encoder, expr string) error { - return fillPrepare(enc, expr) -} - -// RefImplUnprepareBody is reference implementation for filling of an execute prepared -// request's body. -func RefImplExecutePreparedBody(enc *msgpack.Encoder, stmt Prepared, args interface{}) error { - return fillExecutePrepared(enc, stmt, args) -} - -// RefImplUnprepareBody is reference implementation for filling of an unprepare -// request's body. -func RefImplUnprepareBody(enc *msgpack.Encoder, stmt Prepared) error { - return fillUnprepare(enc, stmt) -} - -// RefImplBeginBody is reference implementation for filling of an begin -// request's body. -func RefImplBeginBody(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, - timeout time.Duration) error { - return fillBegin(enc, txnIsolation, timeout) -} - -// RefImplCommitBody is reference implementation for filling of an commit -// request's body. -func RefImplCommitBody(enc *msgpack.Encoder) error { - return fillCommit(enc) -} - -// RefImplRollbackBody is reference implementation for filling of an rollback -// request's body. -func RefImplRollbackBody(enc *msgpack.Encoder) error { - return fillRollback(enc) -} - -// RefImplIdBody is reference implementation for filling of an id -// request's body. -func RefImplIdBody(enc *msgpack.Encoder, protocolInfo ProtocolInfo) error { - return fillId(enc, protocolInfo) -} - -// RefImplWatchOnceBody is reference implementation for filling of an watchOnce -// request's body. -func RefImplWatchOnceBody(enc *msgpack.Encoder, key string) error { - return fillWatchOnce(enc, key) -} diff --git a/future_test.go b/future_test.go index 0c8bb79cc..fbb30fe62 100644 --- a/future_test.go +++ b/future_test.go @@ -27,7 +27,7 @@ func (req *futureMockRequest) Async() bool { return false } -func (req *futureMockRequest) Body(resolver SchemaResolver, enc *msgpack.Encoder) error { +func (req *futureMockRequest) Body(_ SchemaResolver, _ *msgpack.Encoder) error { return nil } diff --git a/prepared.go b/prepared.go index 3a03d740f..6f7ace911 100644 --- a/prepared.go +++ b/prepared.go @@ -22,26 +22,6 @@ type Prepared struct { Conn *Connection } -func fillPrepare(enc *msgpack.Encoder, expr string) error { - enc.EncodeMapLen(1) - enc.EncodeUint(uint64(iproto.IPROTO_SQL_TEXT)) - return enc.EncodeString(expr) -} - -func fillUnprepare(enc *msgpack.Encoder, stmt Prepared) error { - enc.EncodeMapLen(1) - enc.EncodeUint(uint64(iproto.IPROTO_STMT_ID)) - return enc.EncodeUint(uint64(stmt.StatementID)) -} - -func fillExecutePrepared(enc *msgpack.Encoder, stmt Prepared, args interface{}) error { - enc.EncodeMapLen(2) - enc.EncodeUint(uint64(iproto.IPROTO_STMT_ID)) - enc.EncodeUint(uint64(stmt.StatementID)) - enc.EncodeUint(uint64(iproto.IPROTO_SQL_BIND)) - return encodeSQLBind(enc, args) -} - // NewPreparedFromResponse constructs a Prepared object. func NewPreparedFromResponse(conn *Connection, resp Response) (*Prepared, error) { if resp == nil { @@ -81,8 +61,16 @@ func NewPrepareRequest(expr string) *PrepareRequest { } // Body fills an msgpack.Encoder with the execute request body. -func (req *PrepareRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - return fillPrepare(enc, req.expr) +func (req *PrepareRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { + if err := enc.EncodeMapLen(1); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_SQL_TEXT)); err != nil { + return err + } + + return enc.EncodeString(req.expr) } // Context sets a passed context to the request. @@ -126,8 +114,16 @@ func (req *UnprepareRequest) Conn() *Connection { } // Body fills an msgpack.Encoder with the execute request body. -func (req *UnprepareRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - return fillUnprepare(enc, *req.stmt) +func (req *UnprepareRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { + if err := enc.EncodeMapLen(1); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_STMT_ID)); err != nil { + return err + } + + return enc.EncodeUint(uint64(req.stmt.StatementID)) } // Context sets a passed context to the request. @@ -171,8 +167,24 @@ func (req *ExecutePreparedRequest) Args(args interface{}) *ExecutePreparedReques } // Body fills an msgpack.Encoder with the execute request body. -func (req *ExecutePreparedRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - return fillExecutePrepared(enc, *req.stmt, req.args) +func (req *ExecutePreparedRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { + if err := enc.EncodeMapLen(2); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_STMT_ID)); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(req.stmt.StatementID)); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_SQL_BIND)); err != nil { + return err + } + + return encodeSQLBind(enc, req.args) } // Context sets a passed context to the request. diff --git a/protocol.go b/protocol.go index 9296943ce..9f4d6a1b9 100644 --- a/protocol.go +++ b/protocol.go @@ -68,22 +68,37 @@ type IdRequest struct { protocolInfo ProtocolInfo } -func fillId(enc *msgpack.Encoder, protocolInfo ProtocolInfo) error { - enc.EncodeMapLen(2) +// NewIdRequest returns a new IdRequest. +func NewIdRequest(protocolInfo ProtocolInfo) *IdRequest { + req := new(IdRequest) + req.rtype = iproto.IPROTO_ID + req.protocolInfo = protocolInfo.Clone() + return req +} - enc.EncodeUint(uint64(iproto.IPROTO_VERSION)) - if err := enc.Encode(protocolInfo.Version); err != nil { +// Body fills an msgpack.Encoder with the id request body. +func (req *IdRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { + if err := enc.EncodeMapLen(2); err != nil { return err } - enc.EncodeUint(uint64(iproto.IPROTO_FEATURES)) + if err := enc.EncodeUint(uint64(iproto.IPROTO_VERSION)); err != nil { + return err + } - t := len(protocolInfo.Features) - if err := enc.EncodeArrayLen(t); err != nil { + if err := enc.Encode(req.protocolInfo.Version); err != nil { return err } - for _, feature := range protocolInfo.Features { + if err := enc.EncodeUint(uint64(iproto.IPROTO_FEATURES)); err != nil { + return err + } + + if err := enc.EncodeArrayLen(len(req.protocolInfo.Features)); err != nil { + return err + } + + for _, feature := range req.protocolInfo.Features { if err := enc.Encode(feature); err != nil { return err } @@ -92,19 +107,6 @@ func fillId(enc *msgpack.Encoder, protocolInfo ProtocolInfo) error { return nil } -// NewIdRequest returns a new IdRequest. -func NewIdRequest(protocolInfo ProtocolInfo) *IdRequest { - req := new(IdRequest) - req.rtype = iproto.IPROTO_ID - req.protocolInfo = protocolInfo.Clone() - return req -} - -// Body fills an msgpack.Encoder with the id request body. -func (req *IdRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - return fillId(enc, req.protocolInfo) -} - // Context sets a passed context to the request. // // Pay attention that when using context with request objects, diff --git a/request.go b/request.go index 21ed0eba1..c18b3aeb2 100644 --- a/request.go +++ b/request.go @@ -103,160 +103,6 @@ func fillSearch(enc *msgpack.Encoder, spaceEnc spaceEncoder, indexEnc indexEncod return enc.Encode(key) } -func fillIterator(enc *msgpack.Encoder, offset, limit uint32, iterator Iter) error { - if err := enc.EncodeUint(uint64(iproto.IPROTO_ITERATOR)); err != nil { - return err - } - if err := enc.EncodeUint(uint64(iterator)); err != nil { - return err - } - if err := enc.EncodeUint(uint64(iproto.IPROTO_OFFSET)); err != nil { - return err - } - if err := enc.EncodeUint(uint64(offset)); err != nil { - return err - } - if err := enc.EncodeUint(uint64(iproto.IPROTO_LIMIT)); err != nil { - return err - } - return enc.EncodeUint(uint64(limit)) -} - -func fillInsert(enc *msgpack.Encoder, spaceEnc spaceEncoder, tuple interface{}) error { - if err := enc.EncodeMapLen(2); err != nil { - return err - } - if err := spaceEnc.Encode(enc); err != nil { - return err - } - - if err := enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)); err != nil { - return err - } - return enc.Encode(tuple) -} - -func fillSelect(enc *msgpack.Encoder, spaceEnc spaceEncoder, indexEnc indexEncoder, - offset, limit uint32, iterator Iter, key, after interface{}, fetchPos bool) error { - mapLen := 6 - if fetchPos { - mapLen += 1 - } - if after != nil { - mapLen += 1 - } - if err := enc.EncodeMapLen(mapLen); err != nil { - return err - } - if err := fillIterator(enc, offset, limit, iterator); err != nil { - return err - } - if err := fillSearch(enc, spaceEnc, indexEnc, key); err != nil { - return err - } - if fetchPos { - if err := enc.EncodeUint(uint64(iproto.IPROTO_FETCH_POSITION)); err != nil { - return err - } - if err := enc.EncodeBool(fetchPos); err != nil { - return err - } - } - if after != nil { - if pos, ok := after.([]byte); ok { - if err := enc.EncodeUint(uint64(iproto.IPROTO_AFTER_POSITION)); err != nil { - return err - } - if err := enc.EncodeString(string(pos)); err != nil { - return err - } - } else { - if err := enc.EncodeUint(uint64(iproto.IPROTO_AFTER_TUPLE)); err != nil { - return err - } - if err := enc.Encode(after); err != nil { - return err - } - } - } - return nil -} - -func fillUpdate(enc *msgpack.Encoder, spaceEnc spaceEncoder, indexEnc indexEncoder, - key interface{}, ops *Operations) error { - enc.EncodeMapLen(4) - if err := fillSearch(enc, spaceEnc, indexEnc, key); err != nil { - return err - } - enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)) - if ops == nil { - return enc.Encode([]interface{}{}) - } - return enc.Encode(ops) -} - -func fillUpsert(enc *msgpack.Encoder, spaceEnc spaceEncoder, tuple interface{}, - ops *Operations) error { - enc.EncodeMapLen(3) - if err := spaceEnc.Encode(enc); err != nil { - return err - } - - enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)) - if err := enc.Encode(tuple); err != nil { - return err - } - enc.EncodeUint(uint64(iproto.IPROTO_OPS)) - if ops == nil { - return enc.Encode([]interface{}{}) - } - return enc.Encode(ops) -} - -func fillDelete(enc *msgpack.Encoder, spaceEnc spaceEncoder, indexEnc indexEncoder, - key interface{}) error { - enc.EncodeMapLen(3) - return fillSearch(enc, spaceEnc, indexEnc, key) -} - -func fillCall(enc *msgpack.Encoder, functionName string, args interface{}) error { - enc.EncodeMapLen(2) - enc.EncodeUint(uint64(iproto.IPROTO_FUNCTION_NAME)) - enc.EncodeString(functionName) - enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)) - return enc.Encode(args) -} - -func fillEval(enc *msgpack.Encoder, expr string, args interface{}) error { - enc.EncodeMapLen(2) - enc.EncodeUint(uint64(iproto.IPROTO_EXPR)) - enc.EncodeString(expr) - enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)) - return enc.Encode(args) -} - -func fillExecute(enc *msgpack.Encoder, expr string, args interface{}) error { - enc.EncodeMapLen(2) - enc.EncodeUint(uint64(iproto.IPROTO_SQL_TEXT)) - enc.EncodeString(expr) - enc.EncodeUint(uint64(iproto.IPROTO_SQL_BIND)) - return encodeSQLBind(enc, args) -} - -func fillPing(enc *msgpack.Encoder) error { - return enc.EncodeMapLen(0) -} - -func fillWatchOnce(enc *msgpack.Encoder, key string) error { - if err := enc.EncodeMapLen(1); err != nil { - return err - } - if err := enc.EncodeUint(uint64(iproto.IPROTO_EVENT_KEY)); err != nil { - return err - } - return enc.EncodeString(key) -} - // Ping sends empty request to Tarantool to check connection. // // Deprecated: the method will be removed in the next major version, @@ -934,28 +780,35 @@ func (req authRequest) Ctx() context.Context { } // Body fills an encoder with the auth request body. -func (req authRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { +func (req authRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { if err := enc.EncodeMapLen(2); err != nil { return err } + if err := enc.EncodeUint32(uint32(iproto.IPROTO_USER_NAME)); err != nil { return err } + if err := enc.EncodeString(req.user); err != nil { return err } + if err := enc.EncodeUint32(uint32(iproto.IPROTO_TUPLE)); err != nil { return err } + if err := enc.EncodeArrayLen(2); err != nil { return err } + if err := enc.EncodeString(req.auth.String()); err != nil { return err } + if err := enc.EncodeString(req.pass); err != nil { return err } + return nil } @@ -978,8 +831,8 @@ func NewPingRequest() *PingRequest { } // Body fills an msgpack.Encoder with the ping request body. -func (req *PingRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - return fillPing(enc) +func (req *PingRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { + return enc.EncodeMapLen(0) } // Context sets a passed context to the request. @@ -1086,13 +939,83 @@ func (req *SelectRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { if err != nil { return err } + indexEnc, err := newIndexEncoder(res, req.index, spaceEnc.Id) if err != nil { return err } - return fillSelect(enc, spaceEnc, indexEnc, req.offset, req.limit, req.iterator, - req.key, req.after, req.fetchPos) + mapLen := 6 + if req.fetchPos { + mapLen++ + } + if req.after != nil { + mapLen++ + } + + if err := enc.EncodeMapLen(mapLen); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_ITERATOR)); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(req.iterator)); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_OFFSET)); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(req.offset)); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_LIMIT)); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(req.limit)); err != nil { + return err + } + + if err := fillSearch(enc, spaceEnc, indexEnc, req.key); err != nil { + return err + } + + if req.fetchPos { + if err := enc.EncodeUint(uint64(iproto.IPROTO_FETCH_POSITION)); err != nil { + return err + } + + if err := enc.EncodeBool(req.fetchPos); err != nil { + return err + } + } + + if req.after != nil { + if pos, ok := req.after.([]byte); ok { + if err := enc.EncodeUint(uint64(iproto.IPROTO_AFTER_POSITION)); err != nil { + return err + } + + if err := enc.EncodeString(string(pos)); err != nil { + return err + } + } else { + if err := enc.EncodeUint(uint64(iproto.IPROTO_AFTER_TUPLE)); err != nil { + return err + } + + if err := enc.Encode(req.after); err != nil { + return err + } + } + } + + return nil } // Context sets a passed context to the request. @@ -1145,7 +1068,19 @@ func (req *InsertRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return err } - return fillInsert(enc, spaceEnc, req.tuple) + if err := enc.EncodeMapLen(2); err != nil { + return err + } + + if err := spaceEnc.Encode(enc); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)); err != nil { + return err + } + + return enc.Encode(req.tuple) } // Context sets a passed context to the request. @@ -1189,7 +1124,19 @@ func (req *ReplaceRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error return err } - return fillInsert(enc, spaceEnc, req.tuple) + if err := enc.EncodeMapLen(2); err != nil { + return err + } + + if err := spaceEnc.Encode(enc); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)); err != nil { + return err + } + + return enc.Encode(req.tuple) } // Context sets a passed context to the request. @@ -1239,12 +1186,17 @@ func (req *DeleteRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { if err != nil { return err } + indexEnc, err := newIndexEncoder(res, req.index, spaceEnc.Id) if err != nil { return err } - return fillDelete(enc, spaceEnc, indexEnc, req.key) + if err := enc.EncodeMapLen(3); err != nil { + return err + } + + return fillSearch(enc, spaceEnc, indexEnc, req.key) } // Context sets a passed context to the request. @@ -1302,12 +1254,29 @@ func (req *UpdateRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { if err != nil { return err } + indexEnc, err := newIndexEncoder(res, req.index, spaceEnc.Id) if err != nil { return err } - return fillUpdate(enc, spaceEnc, indexEnc, req.key, req.ops) + if err := enc.EncodeMapLen(4); err != nil { + return err + } + + if err := fillSearch(enc, spaceEnc, indexEnc, req.key); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)); err != nil { + return err + } + + if req.ops == nil { + return enc.EncodeArrayLen(0) + } else { + return enc.Encode(req.ops) + } } // Context sets a passed context to the request. @@ -1359,7 +1328,31 @@ func (req *UpsertRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { return err } - return fillUpsert(enc, spaceEnc, req.tuple, req.ops) + if err := enc.EncodeMapLen(3); err != nil { + return err + } + + if err := spaceEnc.Encode(enc); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)); err != nil { + return err + } + + if err := enc.Encode(req.tuple); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_OPS)); err != nil { + return err + } + + if req.ops == nil { + return enc.EncodeArrayLen(0) + } else { + return enc.Encode(req.ops) + } } // Context sets a passed context to the request. @@ -1398,12 +1391,28 @@ func (req *CallRequest) Args(args interface{}) *CallRequest { } // Body fills an encoder with the call request body. -func (req *CallRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - args := req.args - if args == nil { - args = []interface{}{} +func (req *CallRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { + if err := enc.EncodeMapLen(2); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_FUNCTION_NAME)); err != nil { + return err + } + + if err := enc.EncodeString(req.function); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)); err != nil { + return err + } + + if req.args == nil { + return enc.EncodeArrayLen(0) + } else { + return enc.Encode(req.args) } - return fillCall(enc, req.function, args) } // Context sets a passed context to the request. @@ -1459,8 +1468,28 @@ func (req *EvalRequest) Args(args interface{}) *EvalRequest { } // Body fills an msgpack.Encoder with the eval request body. -func (req *EvalRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - return fillEval(enc, req.expr, req.args) +func (req *EvalRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { + if err := enc.EncodeMapLen(2); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_EXPR)); err != nil { + return err + } + + if err := enc.EncodeString(req.expr); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_TUPLE)); err != nil { + return err + } + + if req.args == nil { + return enc.EncodeArrayLen(0) + } else { + return enc.Encode(req.args) + } } // Context sets a passed context to the request. @@ -1499,8 +1528,24 @@ func (req *ExecuteRequest) Args(args interface{}) *ExecuteRequest { } // Body fills an msgpack.Encoder with the execute request body. -func (req *ExecuteRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - return fillExecute(enc, req.expr, req.args) +func (req *ExecuteRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { + if err := enc.EncodeMapLen(2); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_SQL_TEXT)); err != nil { + return err + } + + if err := enc.EncodeString(req.expr); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_SQL_BIND)); err != nil { + return err + } + + return encodeSQLBind(enc, req.args) } // Context sets a passed context to the request. @@ -1539,8 +1584,16 @@ func NewWatchOnceRequest(key string) *WatchOnceRequest { } // Body fills an msgpack.Encoder with the watchOnce request body. -func (req *WatchOnceRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - return fillWatchOnce(enc, req.key) +func (req *WatchOnceRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { + if err := enc.EncodeMapLen(1); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(iproto.IPROTO_EVENT_KEY)); err != nil { + return err + } + + return enc.EncodeString(req.key) } // Context sets a passed context to the request. diff --git a/request_test.go b/request_test.go index 8ced455cd..62baffb32 100644 --- a/request_test.go +++ b/request_test.go @@ -6,13 +6,13 @@ import ( "errors" "fmt" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack/v5" + "github.com/tarantool/go-iproto" . "github.com/tarantool/go-tarantool/v2" - "github.com/vmihailenco/msgpack/v5" ) const invalidSpaceMsg = "invalid space" @@ -27,17 +27,13 @@ const validKey = "foo" // Any string. const defaultSpace uint32 = 0 // And valid too. const defaultIndex uint32 = 0 // And valid too. -const defaultIsolationLevel = DefaultIsolationLevel -const defaultTimeout = 0 - -const validTimeout = 500 * time.Millisecond - -var validStmt *Prepared = &Prepared{StatementID: 1, Conn: &Connection{}} - -var validProtocolInfo ProtocolInfo = ProtocolInfo{ - Version: ProtocolVersion(3), - Features: []iproto.Feature{iproto.IPROTO_FEATURE_STREAMS}, -} +var ( + validStmt = &Prepared{StatementID: 1, Conn: &Connection{}} + validProtocolInfo = ProtocolInfo{ + Version: ProtocolVersion(3), + Features: []iproto.Feature{iproto.IPROTO_FEATURE_STREAMS}, + } +) type ValidSchemeResolver struct { nameUseSupported bool @@ -102,38 +98,6 @@ func assertBodyCall(t testing.TB, requests []Request, errorMsg string) { } } -func assertBodyEqual(t testing.TB, reference []byte, req Request) { - t.Helper() - - var reqBuf bytes.Buffer - reqEnc := msgpack.NewEncoder(&reqBuf) - - err := req.Body(&resolver, reqEnc) - if err != nil { - t.Fatalf("An unexpected Response.Body() error: %q", err.Error()) - } - - reqBody := reqBuf.Bytes() - if !bytes.Equal(reqBody, reference) { - t.Errorf("Encoded request %v != reference %v", reqBody, reference) - } -} - -func getTestOps() *Operations { - operations := NewOperations(). - Add(1, 2). - Subtract(3, 4). - BitwiseAnd(5, 6). - BitwiseOr(7, 8). - BitwiseXor(9, 1). - BitwiseXor(9, 1). // The duplication is for test purposes. - Splice(2, 3, 1, "!!"). - Insert(4, 5). - Delete(6, 7). - Assign(8, 9) - return operations -} - func TestRequestsValidSpaceAndIndex(t *testing.T) { requests := []Request{ NewSelectRequest(validSpace), @@ -318,645 +282,6 @@ func TestRequestsCtx_setter(t *testing.T) { } } -func TestPingRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplPingBody(refEnc) - if err != nil { - t.Fatalf("An unexpected RefImplPingBody() error: %q", err.Error()) - } - - req := NewPingRequest() - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestSelectRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, &resolver, validSpace, defaultIndex, 0, 0xFFFFFFFF, - IterAll, []interface{}{}, nil, false) - if err != nil { - t.Fatalf("An unexpected RefImplSelectBody() error %q", err.Error()) - } - - req := NewSelectRequest(validSpace) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestSelectRequestSpaceByName(t *testing.T) { - var refBuf bytes.Buffer - - resolver.nameUseSupported = true - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, &resolver, "valid", defaultIndex, 0, 0xFFFFFFFF, - IterAll, []interface{}{}, nil, false) - if err != nil { - t.Fatalf("An unexpected RefImplSelectBody() error %q", err.Error()) - } - - req := NewSelectRequest("valid") - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestSelectRequestIndexByName(t *testing.T) { - var refBuf bytes.Buffer - - resolver.nameUseSupported = true - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, &resolver, defaultSpace, "valid", 0, 0xFFFFFFFF, - IterAll, []interface{}{}, nil, false) - if err != nil { - t.Fatalf("An unexpected RefImplSelectBody() error %q", err.Error()) - } - - req := NewSelectRequest(defaultSpace) - req.Index("valid") - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestSelectRequestDefaultIteratorEqIfKey(t *testing.T) { - var refBuf bytes.Buffer - key := []interface{}{uint(18)} - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, &resolver, validSpace, defaultIndex, 0, 0xFFFFFFFF, - IterEq, key, nil, false) - if err != nil { - t.Fatalf("An unexpected RefImplSelectBody() error %q", err.Error()) - } - - req := NewSelectRequest(validSpace). - Key(key) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestSelectRequestIteratorNotChangedIfKey(t *testing.T) { - var refBuf bytes.Buffer - key := []interface{}{uint(678)} - const iter = IterGe - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplSelectBody(refEnc, &resolver, validSpace, defaultIndex, 0, 0xFFFFFFFF, - iter, key, nil, false) - if err != nil { - t.Fatalf("An unexpected RefImplSelectBody() error %q", err.Error()) - } - - req := NewSelectRequest(validSpace). - Iterator(iter). - Key(key) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestSelectRequestSetters(t *testing.T) { - const offset = 4 - const limit = 5 - const iter = IterLt - key := []interface{}{uint(36)} - afterBytes := []byte{0x1, 0x2, 0x3} - afterKey := []interface{}{uint(13)} - var refBufAfterBytes, refBufAfterKey bytes.Buffer - - refEncAfterBytes := msgpack.NewEncoder(&refBufAfterBytes) - err := RefImplSelectBody(refEncAfterBytes, &resolver, validSpace, validIndex, offset, - limit, iter, key, afterBytes, true) - if err != nil { - t.Fatalf("An unexpected RefImplSelectBody() error %s", err) - } - - refEncAfterKey := msgpack.NewEncoder(&refBufAfterKey) - err = RefImplSelectBody(refEncAfterKey, &resolver, validSpace, validIndex, offset, - limit, iter, key, afterKey, true) - if err != nil { - t.Fatalf("An unexpected RefImplSelectBody() error %s", err) - } - - reqAfterBytes := NewSelectRequest(validSpace). - Index(validIndex). - Offset(offset). - Limit(limit). - Iterator(iter). - Key(key). - After(afterBytes). - FetchPos(true) - reqAfterKey := NewSelectRequest(validSpace). - Index(validIndex). - Offset(offset). - Limit(limit). - Iterator(iter). - Key(key). - After(afterKey). - FetchPos(true) - - assertBodyEqual(t, refBufAfterBytes.Bytes(), reqAfterBytes) - assertBodyEqual(t, refBufAfterKey.Bytes(), reqAfterKey) -} - -func TestInsertRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplInsertBody(refEnc, &resolver, validSpace, []interface{}{}) - if err != nil { - t.Fatalf("An unexpected RefImplInsertBody() error: %q", err.Error()) - } - - req := NewInsertRequest(validSpace) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestInsertRequestSpaceByName(t *testing.T) { - var refBuf bytes.Buffer - - resolver.nameUseSupported = true - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplInsertBody(refEnc, &resolver, "valid", []interface{}{}) - if err != nil { - t.Fatalf("An unexpected RefImplInsertBody() error: %q", err.Error()) - } - - req := NewInsertRequest("valid") - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestInsertRequestSetters(t *testing.T) { - tuple := []interface{}{uint(24)} - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplInsertBody(refEnc, &resolver, validSpace, tuple) - if err != nil { - t.Fatalf("An unexpected RefImplInsertBody() error: %q", err.Error()) - } - - req := NewInsertRequest(validSpace). - Tuple(tuple) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestReplaceRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplReplaceBody(refEnc, &resolver, validSpace, []interface{}{}) - if err != nil { - t.Fatalf("An unexpected RefImplReplaceBody() error: %q", err.Error()) - } - - req := NewReplaceRequest(validSpace) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestReplaceRequestSpaceByName(t *testing.T) { - var refBuf bytes.Buffer - - resolver.nameUseSupported = true - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplReplaceBody(refEnc, &resolver, "valid", []interface{}{}) - if err != nil { - t.Fatalf("An unexpected RefImplReplaceBody() error: %q", err.Error()) - } - - req := NewReplaceRequest("valid") - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestReplaceRequestSetters(t *testing.T) { - tuple := []interface{}{uint(99)} - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplReplaceBody(refEnc, &resolver, validSpace, tuple) - if err != nil { - t.Fatalf("An unexpected RefImplReplaceBody() error: %q", err.Error()) - } - - req := NewReplaceRequest(validSpace). - Tuple(tuple) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestDeleteRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplDeleteBody(refEnc, &resolver, validSpace, defaultIndex, []interface{}{}) - if err != nil { - t.Fatalf("An unexpected RefImplDeleteBody() error: %q", err.Error()) - } - - req := NewDeleteRequest(validSpace) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestDeleteRequestSpaceByName(t *testing.T) { - var refBuf bytes.Buffer - - resolver.nameUseSupported = true - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplDeleteBody(refEnc, &resolver, "valid", defaultIndex, []interface{}{}) - if err != nil { - t.Fatalf("An unexpected RefImplDeleteBody() error: %q", err.Error()) - } - - req := NewDeleteRequest("valid") - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestDeleteRequestIndexByName(t *testing.T) { - var refBuf bytes.Buffer - - resolver.nameUseSupported = true - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplDeleteBody(refEnc, &resolver, defaultSpace, "valid", []interface{}{}) - if err != nil { - t.Fatalf("An unexpected RefImplDeleteBody() error: %q", err.Error()) - } - - req := NewDeleteRequest(defaultSpace) - req.Index("valid") - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestDeleteRequestSetters(t *testing.T) { - key := []interface{}{uint(923)} - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplDeleteBody(refEnc, &resolver, validSpace, validIndex, key) - if err != nil { - t.Fatalf("An unexpected RefImplDeleteBody() error: %q", err.Error()) - } - - req := NewDeleteRequest(validSpace). - Index(validIndex). - Key(key) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestUpdateRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpdateBody(refEnc, &resolver, validSpace, defaultIndex, - []interface{}{}, nil) - if err != nil { - t.Fatalf("An unexpected RefImplUpdateBody() error: %q", err.Error()) - } - - req := NewUpdateRequest(validSpace) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestUpdateRequestSpaceByName(t *testing.T) { - var refBuf bytes.Buffer - - resolver.nameUseSupported = true - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpdateBody(refEnc, &resolver, "valid", defaultIndex, - []interface{}{}, nil) - if err != nil { - t.Fatalf("An unexpected RefImplUpdateBody() error: %q", err.Error()) - } - - req := NewUpdateRequest("valid") - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestUpdateRequestIndexByName(t *testing.T) { - var refBuf bytes.Buffer - - resolver.nameUseSupported = true - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpdateBody(refEnc, &resolver, defaultSpace, "valid", - []interface{}{}, nil) - if err != nil { - t.Fatalf("An unexpected RefImplUpdateBody() error: %q", err.Error()) - } - - req := NewUpdateRequest(defaultSpace) - req.Index("valid") - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestUpdateRequestSetters(t *testing.T) { - key := []interface{}{uint(44)} - reqOps := getTestOps() - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpdateBody(refEnc, &resolver, validSpace, validIndex, key, reqOps) - if err != nil { - t.Fatalf("An unexpected RefImplUpdateBody() error: %q", err.Error()) - } - - req := NewUpdateRequest(validSpace). - Index(validIndex). - Key(key). - Operations(reqOps) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestUpsertRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpsertBody(refEnc, &resolver, validSpace, []interface{}{}, nil) - if err != nil { - t.Fatalf("An unexpected RefImplUpsertBody() error: %q", err.Error()) - } - - req := NewUpsertRequest(validSpace) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestUpsertRequestSpaceByName(t *testing.T) { - var refBuf bytes.Buffer - - resolver.nameUseSupported = true - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpsertBody(refEnc, &resolver, "valid", []interface{}{}, nil) - if err != nil { - t.Fatalf("An unexpected RefImplUpsertBody() error: %q", err.Error()) - } - - req := NewUpsertRequest("valid") - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestUpsertRequestSetters(t *testing.T) { - tuple := []interface{}{uint(64)} - reqOps := getTestOps() - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUpsertBody(refEnc, &resolver, validSpace, tuple, reqOps) - if err != nil { - t.Fatalf("An unexpected RefImplUpsertBody() error: %q", err.Error()) - } - - req := NewUpsertRequest(validSpace). - Tuple(tuple). - Operations(reqOps) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestCallRequestsDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplCallBody(refEnc, validExpr, []interface{}{}) - if err != nil { - t.Fatalf("An unexpected RefImplCallBody() error: %q", err.Error()) - } - - req := NewCallRequest(validExpr) - req16 := NewCall16Request(validExpr) - req17 := NewCall17Request(validExpr) - assertBodyEqual(t, refBuf.Bytes(), req) - assertBodyEqual(t, refBuf.Bytes(), req16) - assertBodyEqual(t, refBuf.Bytes(), req17) -} - -func TestCallRequestsSetters(t *testing.T) { - args := []interface{}{uint(34)} - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplCallBody(refEnc, validExpr, args) - if err != nil { - t.Fatalf("An unexpected RefImplCallBody() error: %q", err.Error()) - } - - req := NewCallRequest(validExpr). - Args(args) - req16 := NewCall16Request(validExpr). - Args(args) - req17 := NewCall17Request(validExpr). - Args(args) - assertBodyEqual(t, refBuf.Bytes(), req) - assertBodyEqual(t, refBuf.Bytes(), req16) - assertBodyEqual(t, refBuf.Bytes(), req17) -} - -func TestEvalRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplEvalBody(refEnc, validExpr, []interface{}{}) - if err != nil { - t.Fatalf("An unexpected RefImplEvalBody() error: %q", err.Error()) - } - - req := NewEvalRequest(validExpr) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestEvalRequestSetters(t *testing.T) { - args := []interface{}{uint(34), int(12)} - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplEvalBody(refEnc, validExpr, args) - if err != nil { - t.Fatalf("An unexpected RefImplEvalBody() error: %q", err.Error()) - } - - req := NewEvalRequest(validExpr). - Args(args) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestExecuteRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplExecuteBody(refEnc, validExpr, []interface{}{}) - if err != nil { - t.Fatalf("An unexpected RefImplExecuteBody() error: %q", err.Error()) - } - - req := NewExecuteRequest(validExpr) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestExecuteRequestSetters(t *testing.T) { - args := []interface{}{uint(11)} - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplExecuteBody(refEnc, validExpr, args) - if err != nil { - t.Fatalf("An unexpected RefImplExecuteBody() error: %q", err.Error()) - } - - req := NewExecuteRequest(validExpr). - Args(args) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestPrepareRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplPrepareBody(refEnc, validExpr) - if err != nil { - t.Fatalf("An unexpected RefImplPrepareBody() error: %q", err.Error()) - } - - req := NewPrepareRequest(validExpr) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestUnprepareRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplUnprepareBody(refEnc, *validStmt) - if err != nil { - t.Fatalf("An unexpected RefImplUnprepareBody() error: %q", err.Error()) - } - - req := NewUnprepareRequest(validStmt) - assert.Equal(t, req.Conn(), validStmt.Conn) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestExecutePreparedRequestSetters(t *testing.T) { - args := []interface{}{uint(11)} - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplExecutePreparedBody(refEnc, *validStmt, args) - if err != nil { - t.Fatalf("An unexpected RefImplExecutePreparedBody() error: %q", err.Error()) - } - - req := NewExecutePreparedRequest(validStmt). - Args(args) - assert.Equal(t, req.Conn(), validStmt.Conn) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestExecutePreparedRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplExecutePreparedBody(refEnc, *validStmt, []interface{}{}) - if err != nil { - t.Fatalf("An unexpected RefImplExecutePreparedBody() error: %q", err.Error()) - } - - req := NewExecutePreparedRequest(validStmt) - assert.Equal(t, req.Conn(), validStmt.Conn) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestBeginRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplBeginBody(refEnc, defaultIsolationLevel, defaultTimeout) - if err != nil { - t.Fatalf("An unexpected RefImplBeginBody() error: %q", err.Error()) - } - - req := NewBeginRequest() - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestBeginRequestSetters(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplBeginBody(refEnc, ReadConfirmedLevel, validTimeout) - if err != nil { - t.Fatalf("An unexpected RefImplBeginBody() error: %q", err.Error()) - } - - req := NewBeginRequest().TxnIsolation(ReadConfirmedLevel).Timeout(validTimeout) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestCommitRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplCommitBody(refEnc) - if err != nil { - t.Fatalf("An unexpected RefImplCommitBody() error: %q", err.Error()) - } - - req := NewCommitRequest() - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestRollbackRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplRollbackBody(refEnc) - if err != nil { - t.Fatalf("An unexpected RefImplRollbackBody() error: %q", err.Error()) - } - - req := NewRollbackRequest() - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestBroadcastRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - expectedArgs := []interface{}{validKey} - err := RefImplCallBody(refEnc, "box.broadcast", expectedArgs) - if err != nil { - t.Fatalf("An unexpected RefImplCallBody() error: %q", err.Error()) - } - - req := NewBroadcastRequest(validKey) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestBroadcastRequestSetters(t *testing.T) { - value := []interface{}{uint(34), int(12)} - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - expectedArgs := []interface{}{validKey, value} - err := RefImplCallBody(refEnc, "box.broadcast", expectedArgs) - if err != nil { - t.Fatalf("An unexpected RefImplCallBody() error: %q", err.Error()) - } - - req := NewBroadcastRequest(validKey).Value(value) - assertBodyEqual(t, refBuf.Bytes(), req) -} - -func TestWatchOnceRequestDefaultValues(t *testing.T) { - var refBuf bytes.Buffer - - refEnc := msgpack.NewEncoder(&refBuf) - err := RefImplWatchOnceBody(refEnc, validKey) - if err != nil { - t.Fatalf("An unexpected RefImplCallBody() error: %q", err.Error()) - } - - req := NewWatchOnceRequest(validKey) - assertBodyEqual(t, refBuf.Bytes(), req) -} - func TestResponseDecode(t *testing.T) { header := Header{} data := bytes.NewBuffer([]byte{'v', '2'}) diff --git a/stream.go b/stream.go index 43e80fc28..e3c70865a 100644 --- a/stream.go +++ b/stream.go @@ -35,57 +35,6 @@ type Stream struct { Conn *Connection } -func fillBegin(enc *msgpack.Encoder, txnIsolation TxnIsolationLevel, timeout time.Duration) error { - hasTimeout := timeout > 0 - hasIsolationLevel := txnIsolation != DefaultIsolationLevel - mapLen := 0 - if hasTimeout { - mapLen += 1 - } - if hasIsolationLevel { - mapLen += 1 - } - - err := enc.EncodeMapLen(mapLen) - if err != nil { - return err - } - - if hasTimeout { - err = enc.EncodeUint(uint64(iproto.IPROTO_TIMEOUT)) - if err != nil { - return err - } - - err = enc.Encode(timeout.Seconds()) - if err != nil { - return err - } - } - - if hasIsolationLevel { - err = enc.EncodeUint(uint64(iproto.IPROTO_TXN_ISOLATION)) - if err != nil { - return err - } - - err = enc.EncodeUint(uint64(txnIsolation)) - if err != nil { - return err - } - } - - return err -} - -func fillCommit(enc *msgpack.Encoder) error { - return enc.EncodeMapLen(0) -} - -func fillRollback(enc *msgpack.Encoder) error { - return enc.EncodeMapLen(0) -} - // BeginRequest helps you to create a begin request object for execution // by a Stream. // Begin request can not be processed out of stream. @@ -117,8 +66,46 @@ func (req *BeginRequest) Timeout(timeout time.Duration) *BeginRequest { } // Body fills an msgpack.Encoder with the begin request body. -func (req *BeginRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - return fillBegin(enc, req.txnIsolation, req.timeout) +func (req *BeginRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { + var ( + mapLen = 0 + hasTimeout = req.timeout > 0 + hasIsolationLevel = req.txnIsolation != DefaultIsolationLevel + ) + + if hasTimeout { + mapLen++ + } + + if hasIsolationLevel { + mapLen++ + } + + if err := enc.EncodeMapLen(mapLen); err != nil { + return err + } + + if hasTimeout { + if err := enc.EncodeUint(uint64(iproto.IPROTO_TIMEOUT)); err != nil { + return err + } + + if err := enc.Encode(req.timeout.Seconds()); err != nil { + return err + } + } + + if hasIsolationLevel { + if err := enc.EncodeUint(uint64(iproto.IPROTO_TXN_ISOLATION)); err != nil { + return err + } + + if err := enc.EncodeUint(uint64(req.txnIsolation)); err != nil { + return err + } + } + + return nil } // Context sets a passed context to the request. @@ -147,8 +134,8 @@ func NewCommitRequest() *CommitRequest { } // Body fills an msgpack.Encoder with the commit request body. -func (req *CommitRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - return fillCommit(enc) +func (req *CommitRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { + return enc.EncodeMapLen(0) } // Context sets a passed context to the request. @@ -177,8 +164,8 @@ func NewRollbackRequest() *RollbackRequest { } // Body fills an msgpack.Encoder with the rollback request body. -func (req *RollbackRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { - return fillRollback(enc) +func (req *RollbackRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { + return enc.EncodeMapLen(0) } // Context sets a passed context to the request. diff --git a/watch.go b/watch.go index 9f1273134..9628b96ac 100644 --- a/watch.go +++ b/watch.go @@ -84,9 +84,11 @@ func (req *watchRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error { if err := enc.EncodeMapLen(1); err != nil { return err } + if err := enc.EncodeUint(uint64(iproto.IPROTO_EVENT_KEY)); err != nil { return err } + return enc.EncodeString(req.key) } @@ -118,9 +120,11 @@ func (req *unwatchRequest) Body(res SchemaResolver, enc *msgpack.Encoder) error if err := enc.EncodeMapLen(1); err != nil { return err } + if err := enc.EncodeUint(uint64(iproto.IPROTO_EVENT_KEY)); err != nil { return err } + return enc.EncodeString(req.key) } From fba773c132af24d033837bd835f0bbe3d246849c Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Thu, 26 Jun 2025 11:25:37 +0300 Subject: [PATCH 582/605] stream: added is_sync flag for begin/commit Part of #TNTP-3334 Closes #366 --- CHANGELOG.md | 2 ++ example_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++ stream.go | 59 ++++++++++++++++++++++++++++-- tarantool_test.go | 69 +++++++++++++++++++++++++++++++++++ test_helpers/utils.go | 8 +++++ 5 files changed, 220 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9b6d549e..db043f583 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Implemented box.session.su request and sugar interface only for current session granting (#426). - Defined `ErrConcurrentSchemaUpdate` constant for "concurrent schema update" error. Now you can check this error with `errors.Is(err, tarantool.ErrConcurrentSchemaUpdate)`. +- Implemented support for `IPROTO_IS_SYNC` flag in stream transactions, + added `IsSync(bool)` method for `BeginRequest`/`CommitRequest` (#447). ### Changed diff --git a/example_test.go b/example_test.go index d4480cea4..463f3289c 100644 --- a/example_test.go +++ b/example_test.go @@ -998,6 +998,90 @@ func ExampleBeginRequest_TxnIsolation() { fmt.Printf("Select after Rollback: response is %#v\n", data) } +func ExampleBeginRequest_IsSync() { + conn := exampleConnect(dialer, opts) + defer conn.Close() + + // Tarantool supports IS_SYNC flag for BeginRequest since version 3.1.0. + isLess, err := test_helpers.IsTarantoolVersionLess(3, 1, 0) + if err != nil || isLess { + return + } + + stream, err := conn.NewStream() + if err != nil { + fmt.Printf("error getting the stream: %s\n", err) + return + } + + // Begin transaction with synchronous mode. + req := tarantool.NewBeginRequest().IsSync(true) + resp, err := stream.Do(req).GetResponse() + switch { + case err != nil: + fmt.Printf("error getting the response: %s\n", err) + case resp.Header().Error != tarantool.ErrorNo: + fmt.Printf("response error code: %s\n", resp.Header().Error) + default: + fmt.Println("Success.") + } +} + +func ExampleCommitRequest_IsSync() { + conn := exampleConnect(dialer, opts) + defer conn.Close() + + // Tarantool supports IS_SYNC flag for CommitRequest since version 3.1.0. + isLess, err := test_helpers.IsTarantoolVersionLess(3, 1, 0) + if err != nil || isLess { + return + } + + var req tarantool.Request + + stream, err := conn.NewStream() + if err != nil { + fmt.Printf("error getting the stream: %s\n", err) + return + } + + // Begin transaction. + req = tarantool.NewBeginRequest() + resp, err := stream.Do(req).GetResponse() + switch { + case err != nil: + fmt.Printf("error getting the response: %s\n", err) + return + case resp.Header().Error != tarantool.ErrorNo: + fmt.Printf("response error code: %s\n", resp.Header().Error) + return + } + + // Insert in stream. + req = tarantool.NewReplaceRequest("test").Tuple([]interface{}{1, "test"}) + resp, err = stream.Do(req).GetResponse() + switch { + case err != nil: + fmt.Printf("error getting the response: %s\n", err) + return + case resp.Header().Error != tarantool.ErrorNo: + fmt.Printf("response error code: %s\n", resp.Header().Error) + return + } + + // Commit transaction in sync mode. + req = tarantool.NewCommitRequest().IsSync(true) + resp, err = stream.Do(req).GetResponse() + switch { + case err != nil: + fmt.Printf("error getting the response: %s\n", err) + case resp.Header().Error != tarantool.ErrorNo: + fmt.Printf("response error code: %s\n", resp.Header().Error) + default: + fmt.Println("Success.") + } +} + func ExampleErrorNo() { conn := exampleConnect(dialer, opts) defer conn.Close() diff --git a/stream.go b/stream.go index e3c70865a..cdabf12fa 100644 --- a/stream.go +++ b/stream.go @@ -42,6 +42,8 @@ type BeginRequest struct { baseRequest txnIsolation TxnIsolationLevel timeout time.Duration + isSync bool + isSyncSet bool } // NewBeginRequest returns a new BeginRequest. @@ -59,12 +61,19 @@ func (req *BeginRequest) TxnIsolation(txnIsolation TxnIsolationLevel) *BeginRequ return req } -// WithTimeout allows to set up a timeout for call BeginRequest. +// Timeout allows to set up a timeout for call BeginRequest. func (req *BeginRequest) Timeout(timeout time.Duration) *BeginRequest { req.timeout = timeout return req } +// IsSync allows to set up a IsSync flag for call BeginRequest. +func (req *BeginRequest) IsSync(isSync bool) *BeginRequest { + req.isSync = isSync + req.isSyncSet = true + return req +} + // Body fills an msgpack.Encoder with the begin request body. func (req *BeginRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { var ( @@ -81,6 +90,10 @@ func (req *BeginRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { mapLen++ } + if req.isSyncSet { + mapLen++ + } + if err := enc.EncodeMapLen(mapLen); err != nil { return err } @@ -105,6 +118,16 @@ func (req *BeginRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { } } + if req.isSyncSet { + if err := enc.EncodeUint(uint64(iproto.IPROTO_IS_SYNC)); err != nil { + return err + } + + if err := enc.EncodeBool(req.isSync); err != nil { + return err + } + } + return nil } @@ -124,6 +147,9 @@ func (req *BeginRequest) Context(ctx context.Context) *BeginRequest { // Commit request can not be processed out of stream. type CommitRequest struct { baseRequest + + isSync bool + isSyncSet bool } // NewCommitRequest returns a new CommitRequest. @@ -133,9 +159,38 @@ func NewCommitRequest() *CommitRequest { return req } +// IsSync allows to set up a IsSync flag for call BeginRequest. +func (req *CommitRequest) IsSync(isSync bool) *CommitRequest { + req.isSync = isSync + req.isSyncSet = true + return req +} + // Body fills an msgpack.Encoder with the commit request body. func (req *CommitRequest) Body(_ SchemaResolver, enc *msgpack.Encoder) error { - return enc.EncodeMapLen(0) + var ( + mapLen = 0 + ) + + if req.isSyncSet { + mapLen++ + } + + if err := enc.EncodeMapLen(mapLen); err != nil { + return err + } + + if req.isSyncSet { + if err := enc.EncodeUint(uint64(iproto.IPROTO_IS_SYNC)); err != nil { + return err + } + + if err := enc.EncodeBool(req.isSync); err != nil { + return err + } + } + + return nil } // Context sets a passed context to the request. diff --git a/tarantool_test.go b/tarantool_test.go index 14bb63ef7..4902e96f4 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -4199,6 +4199,75 @@ func TestFdDialer(t *testing.T) { require.Equal(t, int8(0), resp[0]) } +const ( + errNoSyncTransactionQueue = "The synchronous transaction queue doesn't belong to any instance" +) + +func TestDoBeginRequest_IsSync(t *testing.T) { + test_helpers.SkipIfIsSyncUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, dialer, opts) + defer conn.Close() + + stream, err := conn.NewStream() + require.NoError(t, err) + + _, err = stream.Do(NewBeginRequest().IsSync(true)).Get() + assert.Nil(t, err) + + _, err = stream.Do( + NewReplaceRequest("test").Tuple([]interface{}{1, "foo"}), + ).Get() + require.Nil(t, err) + + _, err = stream.Do(NewCommitRequest()).Get() + require.NotNil(t, err) + assert.Contains(t, err.Error(), errNoSyncTransactionQueue) +} + +func TestDoCommitRequest_IsSync(t *testing.T) { + test_helpers.SkipIfIsSyncUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, dialer, opts) + defer conn.Close() + + stream, err := conn.NewStream() + require.NoError(t, err) + + _, err = stream.Do(NewBeginRequest()).Get() + require.Nil(t, err) + + _, err = stream.Do( + NewReplaceRequest("test").Tuple([]interface{}{1, "foo"}), + ).Get() + require.Nil(t, err) + + _, err = stream.Do(NewCommitRequest().IsSync(true)).Get() + require.NotNil(t, err) + assert.Contains(t, err.Error(), errNoSyncTransactionQueue) +} + +func TestDoCommitRequest_NoSync(t *testing.T) { + test_helpers.SkipIfIsSyncUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, dialer, opts) + defer conn.Close() + + stream, err := conn.NewStream() + require.NoError(t, err) + + _, err = stream.Do(NewBeginRequest()).Get() + require.Nil(t, err) + + _, err = stream.Do( + NewReplaceRequest("test").Tuple([]interface{}{1, "foo"}), + ).Get() + require.Nil(t, err) + + _, err = stream.Do(NewCommitRequest()).Get() + assert.Nil(t, err) +} + // runTestMain is a body of TestMain function // (see https://pkg.go.dev/testing#hdr-Main). // Using defer + os.Exit is not works so TestMain body diff --git a/test_helpers/utils.go b/test_helpers/utils.go index d5a65be10..579f507c9 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -217,6 +217,14 @@ func SkipIfCrudSpliceBroken(t *testing.T) { SkipIfFeatureUnsupported(t, "crud update splice", 2, 0, 0) } +// SkipIfIsSyncUnsupported skips test run if Tarantool without +// IS_SYNC support is used. +func SkipIfIsSyncUnsupported(t *testing.T) { + t.Helper() + + SkipIfFeatureUnsupported(t, "is sync", 3, 1, 0) +} + // IsTcsSupported checks if Tarantool supports centralized storage. // Tarantool supports centralized storage with Enterprise since 3.3.0 version. func IsTcsSupported() (bool, error) { From 774665230feace1f7fd5964c57f49f2d003dd4ae Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Thu, 3 Jul 2025 12:20:08 +0300 Subject: [PATCH 583/605] tests: added test for Body() methods of requests using golden files --- golden_test.go | 536 ++++++++++++++++++ request_test.go | 2 +- ...with-txn-isolation-is-sync-timeout.msgpack | Bin 0 -> 15 bytes .../begin-with-txn-isolation-is-sync.msgpack | 1 + .../requests/begin-with-txn-isolation.msgpack | 1 + testdata/requests/begin.msgpack | 1 + testdata/requests/call-no-args.msgpack | 1 + .../call-with-args-empty-array.msgpack | 1 + .../requests/call-with-args-mixed.msgpack | Bin 0 -> 58 bytes testdata/requests/call-with-args-nil.msgpack | 1 + testdata/requests/call-with-args.msgpack | 1 + .../requests/call16-with-args-nil.msgpack | 1 + testdata/requests/call16-with-args.msgpack | Bin 0 -> 58 bytes testdata/requests/call16.msgpack | 1 + .../requests/call17-with-args-nil.msgpack | 1 + testdata/requests/call17-with-args.msgpack | Bin 0 -> 58 bytes testdata/requests/call17.msgpack | 1 + testdata/requests/commit-raw.msgpack | 1 + .../requests/commit-with-sync-false.msgpack | 1 + testdata/requests/commit-with-sync.msgpack | 1 + testdata/requests/delete-raw.msgpack | Bin 0 -> 17 bytes testdata/requests/delete-sname-iname.msgpack | 1 + .../requests/delete-sname-inumber.msgpack | 1 + .../requests/delete-snumber-iname.msgpack | 1 + .../requests/delete-snumber-inumber.msgpack | 1 + testdata/requests/delete.msgpack | Bin 0 -> 17 bytes testdata/requests/eval-with-args.msgpack | Bin 0 -> 63 bytes .../requests/eval-with-empty-array.msgpack | 1 + testdata/requests/eval-with-nil.msgpack | 1 + .../requests/eval-with-single-number.msgpack | 1 + testdata/requests/eval.msgpack | 1 + testdata/requests/insert-sname.msgpack | Bin 0 -> 55 bytes testdata/requests/insert-snumber.msgpack | Bin 0 -> 45 bytes testdata/requests/ping.msgpack | 1 + testdata/requests/replace-sname.msgpack | Bin 0 -> 55 bytes testdata/requests/replace-snumber.msgpack | Bin 0 -> 45 bytes testdata/requests/rollback.msgpack | 1 + testdata/requests/select | Bin 0 -> 27 bytes .../requests/select-key-sname-iname.msgpack | Bin 0 -> 77 bytes .../requests/select-key-sname-inumber.msgpack | Bin 0 -> 67 bytes .../requests/select-key-snumber-iname.msgpack | Bin 0 -> 67 bytes .../select-key-snumber-inumber.msgpack | Bin 0 -> 57 bytes testdata/requests/select-sname-iname.msgpack | Bin 0 -> 37 bytes .../requests/select-sname-inumber.msgpack | Bin 0 -> 27 bytes .../requests/select-snumber-iname.msgpack | Bin 0 -> 27 bytes .../requests/select-snumber-inumber.msgpack | Bin 0 -> 17 bytes testdata/requests/select-with-after.msgpack | Bin 0 -> 105 bytes testdata/requests/select-with-key.msgpack | Bin 0 -> 67 bytes .../requests/select-with-optionals.msgpack | Bin 0 -> 25 bytes testdata/requests/update-sname-iname.msgpack | 2 + .../requests/update-sname-inumber.msgpack | 2 + .../requests/update-snumber-iname.msgpack | 2 + testdata/requests/update.msgpack | Bin 0 -> 19 bytes testdata/requests/upsert-sname.msgpack | Bin 0 -> 132 bytes testdata/requests/upsert-snumber.msgpack | Bin 0 -> 122 bytes testdata/requests/upsert.msgpack | 1 + 56 files changed, 568 insertions(+), 1 deletion(-) create mode 100644 golden_test.go create mode 100644 testdata/requests/begin-with-txn-isolation-is-sync-timeout.msgpack create mode 100644 testdata/requests/begin-with-txn-isolation-is-sync.msgpack create mode 100644 testdata/requests/begin-with-txn-isolation.msgpack create mode 100644 testdata/requests/begin.msgpack create mode 100644 testdata/requests/call-no-args.msgpack create mode 100644 testdata/requests/call-with-args-empty-array.msgpack create mode 100644 testdata/requests/call-with-args-mixed.msgpack create mode 100644 testdata/requests/call-with-args-nil.msgpack create mode 100644 testdata/requests/call-with-args.msgpack create mode 100644 testdata/requests/call16-with-args-nil.msgpack create mode 100644 testdata/requests/call16-with-args.msgpack create mode 100644 testdata/requests/call16.msgpack create mode 100644 testdata/requests/call17-with-args-nil.msgpack create mode 100644 testdata/requests/call17-with-args.msgpack create mode 100644 testdata/requests/call17.msgpack create mode 100644 testdata/requests/commit-raw.msgpack create mode 100644 testdata/requests/commit-with-sync-false.msgpack create mode 100644 testdata/requests/commit-with-sync.msgpack create mode 100644 testdata/requests/delete-raw.msgpack create mode 100644 testdata/requests/delete-sname-iname.msgpack create mode 100644 testdata/requests/delete-sname-inumber.msgpack create mode 100644 testdata/requests/delete-snumber-iname.msgpack create mode 100644 testdata/requests/delete-snumber-inumber.msgpack create mode 100644 testdata/requests/delete.msgpack create mode 100644 testdata/requests/eval-with-args.msgpack create mode 100644 testdata/requests/eval-with-empty-array.msgpack create mode 100644 testdata/requests/eval-with-nil.msgpack create mode 100644 testdata/requests/eval-with-single-number.msgpack create mode 100644 testdata/requests/eval.msgpack create mode 100644 testdata/requests/insert-sname.msgpack create mode 100644 testdata/requests/insert-snumber.msgpack create mode 100644 testdata/requests/ping.msgpack create mode 100644 testdata/requests/replace-sname.msgpack create mode 100644 testdata/requests/replace-snumber.msgpack create mode 100644 testdata/requests/rollback.msgpack create mode 100644 testdata/requests/select create mode 100644 testdata/requests/select-key-sname-iname.msgpack create mode 100644 testdata/requests/select-key-sname-inumber.msgpack create mode 100644 testdata/requests/select-key-snumber-iname.msgpack create mode 100644 testdata/requests/select-key-snumber-inumber.msgpack create mode 100644 testdata/requests/select-sname-iname.msgpack create mode 100644 testdata/requests/select-sname-inumber.msgpack create mode 100644 testdata/requests/select-snumber-iname.msgpack create mode 100644 testdata/requests/select-snumber-inumber.msgpack create mode 100644 testdata/requests/select-with-after.msgpack create mode 100644 testdata/requests/select-with-key.msgpack create mode 100644 testdata/requests/select-with-optionals.msgpack create mode 100644 testdata/requests/update-sname-iname.msgpack create mode 100644 testdata/requests/update-sname-inumber.msgpack create mode 100644 testdata/requests/update-snumber-iname.msgpack create mode 100644 testdata/requests/update.msgpack create mode 100644 testdata/requests/upsert-sname.msgpack create mode 100644 testdata/requests/upsert-snumber.msgpack create mode 100644 testdata/requests/upsert.msgpack diff --git a/golden_test.go b/golden_test.go new file mode 100644 index 000000000..271a98ce3 --- /dev/null +++ b/golden_test.go @@ -0,0 +1,536 @@ +package tarantool_test + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "os" + "path" + "reflect" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" + "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v2" +) + +// golden_test.go contains tests that will check that the msgpack +// encoding of the requests body matches the golden files. +// +// Algorithm to add new test: +// 1. Create a new test record in TestGolden (name + Request). +// 2. Run the test with the flag -generate-golden to generate the golden file. +// (for example: `go test . -run=TestGolden -v -generate-golden`) +// 3. Verify that JSON representation of the msgpack is the same as expected. +// 4. Commit the test record. +// +// Example of JSON representation of the msgpack +// ``` +// golden_test.go:80: writing golden file testdata/requests/select-with-optionals.msgpack +// golden_test.go:38: { +// golden_test.go:38: "IPROTO_FETCH_POSITION[31]": true, +// golden_test.go:38: "IPROTO_INDEX_ID[17]": 0, +// golden_test.go:38: "IPROTO_ITERATOR[20]": 5, +// golden_test.go:38: "IPROTO_KEY[32]": [], +// golden_test.go:38: "IPROTO_LIMIT[18]": 123, +// golden_test.go:38: "IPROTO_OFFSET[19]": 123, +// golden_test.go:38: "IPROTO_SPACE_NAME[94]": "table_name" +// golden_test.go:38: } +// ``` +// +// +// In case of any changes in the msgpack encoding/tests, the test will fail with next error: +// +// ``` +// === RUN TestGolden/testdata/requests/select-with-after.msgpack +// golden_test.go:109: +// Error Trace: ../go-tarantool/golden_test.go:73 +// ../go-tarantool/golden_test.go:109 +// Error: Not equal: +// expected: []byte{0x87, 0x14, 0x5, 0x13, 0x0, ..., 0x33, 0x33, 0x33} +// actual : []byte{0x87, 0x14, 0x5, 0x13, 0x0, ..., 0x33, 0x33, 0x33} +// +// Diff: +// --- Expected +// +++ Actual +// @@ -1,3 +1,3 @@ +// ([]uint8) (len=105) { +// - 00000000 87 14 05 13 ... 6e |......{^.table_n| +// + 00000000 87 14 05 13 ... 6e |......|^.table_n| +// 00000010 61 6d 65 11 ... ff |ame.. ...args...| +// Test: TestGolden/testdata/requests/select-with-after.msgpack +// Messages: golden file content is not equal to actual +// golden_test.go:109: expected: +// golden_test.go:63: { +// golden_test.go:63: "IPROTO_AFTER_TUPLE[47]": [ +// golden_test.go:63: 1, +// golden_test.go:63: "args", +// golden_test.go:63: 3, +// golden_test.go:63: "2024-01-01T03:00:00+03:00", +// golden_test.go:63: "gZMIqvDBS3SYYcSrWiZjCA==", +// golden_test.go:63: 1.2 +// golden_test.go:63: ], +// golden_test.go:63: "IPROTO_INDEX_ID[17]": 0, +// golden_test.go:63: "IPROTO_ITERATOR[20]": 5, +// golden_test.go:63: "IPROTO_KEY[32]": [ +// golden_test.go:63: 1, +// golden_test.go:63: "args", +// golden_test.go:63: 3, +// golden_test.go:63: "2024-01-01T03:00:00+03:00", +// golden_test.go:63: "gZMIqvDBS3SYYcSrWiZjCA==", +// golden_test.go:63: 1.2 +// golden_test.go:63: ], +// golden_test.go:63: "IPROTO_LIMIT[18]": 123, +// golden_test.go:63: "IPROTO_OFFSET[19]": 0, +// golden_test.go:63: "IPROTO_SPACE_NAME[94]": "table_name" +// golden_test.go:63: } +// golden_test.go:109: actual: +// golden_test.go:63: { +// golden_test.go:63: "IPROTO_AFTER_TUPLE[47]": [ +// golden_test.go:63: 1, +// golden_test.go:63: "args", +// golden_test.go:63: 3, +// golden_test.go:63: "2024-01-01T03:00:00+03:00", +// golden_test.go:63: "gZMIqvDBS3SYYcSrWiZjCA==", +// golden_test.go:63: 1.2 +// golden_test.go:63: ], +// golden_test.go:63: "IPROTO_INDEX_ID[17]": 0, +// golden_test.go:63: "IPROTO_ITERATOR[20]": 5, +// golden_test.go:63: "IPROTO_KEY[32]": [ +// golden_test.go:63: 1, +// golden_test.go:63: "args", +// golden_test.go:63: 3, +// golden_test.go:63: "2024-01-01T03:00:00+03:00", +// golden_test.go:63: "gZMIqvDBS3SYYcSrWiZjCA==", +// golden_test.go:63: 1.2 +// golden_test.go:63: ], +// golden_test.go:63: "IPROTO_LIMIT[18]": 124, +// golden_test.go:63: "IPROTO_OFFSET[19]": 0, +// golden_test.go:63: "IPROTO_SPACE_NAME[94]": "table_name" +// golden_test.go:63: } +// --- FAIL: TestGolden/testdata/requests/select-with-after.msgpack (0.00s) +// ``` +// Use it to debug the test. +// +// If you want to update the golden file, run delete old file and rerun the test. + +func logMsgpackAsJsonConvert(t *testing.T, data []byte) { + t.Helper() + + var decodedMsgpack map[int]interface{} + + decoder := msgpack.NewDecoder(bytes.NewReader(data)) + require.NoError(t, decoder.Decode(&decodedMsgpack)) + + decodedConvertedMsgpack := map[string]interface{}{} + for k, v := range decodedMsgpack { + decodedConvertedMsgpack[fmt.Sprintf("%s[%d]", iproto.Key(k).String(), k)] = v + } + + encodedJson, err := json.MarshalIndent(decodedConvertedMsgpack, "", " ") + require.NoError(t, err, "failed to convert msgpack to json") + + for _, line := range bytes.Split(encodedJson, []byte("\n")) { + t.Log(string(line)) + } +} + +func compareGoldenMsgpackAndPrintDiff(t *testing.T, name string, data []byte) { + t.Helper() + + testContent, err := os.ReadFile(name) + require.NoError(t, err, "failed to read golden file", name) + + if assert.Equal(t, testContent, data, "golden file content is not equal to actual") { + return + } + + t.Logf("expected:\n") + logMsgpackAsJsonConvert(t, testContent) + t.Logf("actual:\n") + logMsgpackAsJsonConvert(t, data) +} + +func fileExists(name string) bool { + _, err := os.Stat(name) + return !os.IsNotExist(err) +} + +const ( + pathPrefix = "testdata/requests" +) + +var ( + generateGolden = flag.Bool("generate-golden", false, + "generate golden files if they do not exist") +) + +type goldenTestCase struct { + Name string + Test func(t *testing.T) tarantool.Request + Request tarantool.Request + Resolver tarantool.SchemaResolver +} + +func (tc goldenTestCase) Execute(t *testing.T) { + t.Helper() + + if tc.Request != nil { + if tc.Test != nil { + require.FailNow(t, "both Test and Request must not be set") + } + + tc.Test = func(t *testing.T) tarantool.Request { + return tc.Request + } + } + if tc.Resolver == nil { + tc.Resolver = &dummySchemaResolver{} + } + + name := path.Join(pathPrefix, tc.Name) + + t.Run(name, func(t *testing.T) { + var out bytes.Buffer + encoder := msgpack.NewEncoder(&out) + + req := tc.Test(t) + require.NotNil(t, req, "failed to create request") + + err := req.Body(&dummySchemaResolver{}, encoder) + require.NoError(t, err, "failed to encode request") + + goldenFileExists := fileExists(name) + generateGoldenIsSet := *generateGolden && !goldenFileExists + + switch { + case !goldenFileExists && generateGoldenIsSet: + t.Logf("writing golden file %s", name) + err := os.WriteFile(name, out.Bytes(), 0644) + require.NoError(t, err, "failed to write golden file", name) + logMsgpackAsJsonConvert(t, out.Bytes()) + case !goldenFileExists && !generateGoldenIsSet: + assert.FailNow(t, "golden file does not exist") + } + + compareGoldenMsgpackAndPrintDiff(t, name, out.Bytes()) + }) +} + +type dummySchemaResolver struct{} + +func interfaceToUint32(in interface{}) (uint32, bool) { + switch val := reflect.ValueOf(in); val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return uint32(val.Int()), true + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return uint32(val.Uint()), true + default: + return 0, false + } +} + +func (d dummySchemaResolver) ResolveSpace(in interface{}) (uint32, error) { + if num, ok := interfaceToUint32(in); ok { + return num, nil + } + return 0, fmt.Errorf("unexpected space type %T", in) +} + +func (d dummySchemaResolver) ResolveIndex(in interface{}, _ uint32) (uint32, error) { + if in == nil { + return 0, nil + } else if num, ok := interfaceToUint32(in); ok { + return num, nil + } + return 0, fmt.Errorf("unexpected index type %T", in) +} + +func (d dummySchemaResolver) NamesUseSupported() bool { + return true +} + +func TestGolden(t *testing.T) { + precachedUUID := uuid.MustParse("819308aa-f0c1-4b74-9861-c4ab5a266308") + precachedTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + precachedTuple := []interface{}{1, "args", 3, precachedTime, precachedUUID, 1.2} + + precachedUpdateOps := tarantool.NewOperations(). + Add(1, "test"). + Assign(2, "fest"). + Delete(3, ""). + Insert(4, "insert"). + Splice(5, 6, 7, "splice"). + Subtract(6, "subtract"). + BitwiseAnd(7, 10). + BitwiseOr(8, 11). + BitwiseXor(9, 12) + + testCases := []goldenTestCase{ + { + Name: "commit-raw.msgpack", + Request: tarantool.NewCommitRequest(), + }, + { + Name: "commit-with-sync.msgpack", + Request: tarantool.NewCommitRequest().IsSync(true), + }, + { + Name: "commit-with-sync-false.msgpack", + Request: tarantool.NewCommitRequest().IsSync(false), + }, + { + Name: "begin.msgpack", + Request: tarantool.NewBeginRequest(), + }, + { + Name: "begin-with-txn-isolation.msgpack", + Request: tarantool.NewBeginRequest(). + TxnIsolation(tarantool.ReadCommittedLevel), + }, + { + Name: "begin-with-txn-isolation-is-sync.msgpack", + Request: tarantool.NewBeginRequest(). + TxnIsolation(tarantool.ReadCommittedLevel). + IsSync(true), + }, + { + Name: "begin-with-txn-isolation-is-sync-timeout.msgpack", + Request: tarantool.NewBeginRequest(). + TxnIsolation(tarantool.ReadCommittedLevel). + IsSync(true). + Timeout(2 * time.Second), + }, + { + Name: "rollback.msgpack", + Request: tarantool.NewRollbackRequest(), + }, + { + Name: "ping.msgpack", + Request: tarantool.NewPingRequest(), + }, + { + Name: "call-no-args.msgpack", + Request: tarantool.NewCallRequest("function.name"), + }, + { + Name: "call-with-args.msgpack", + Request: tarantool.NewCallRequest("function.name").Args( + []interface{}{1, 2, 3}, + ), + }, + { + Name: "call-with-args-mixed.msgpack", + Request: tarantool.NewCallRequest("function.name").Args( + []interface{}{1, "args", 3, precachedTime, precachedUUID, 1.2}, + ), + }, + { + Name: "call-with-args-nil.msgpack", + Request: tarantool.NewCallRequest("function.name").Args(nil), + }, + { + Name: "call-with-args-empty-array.msgpack", + Request: tarantool.NewCallRequest("function.name").Args([]int{}), + }, + { + Name: "eval.msgpack", + Request: tarantool.NewEvalRequest("function_name()"), + }, + { + Name: "eval-with-nil.msgpack", + Request: tarantool.NewEvalRequest("function_name()").Args(nil), + }, + { + Name: "eval-with-empty-array.msgpack", + Request: tarantool.NewEvalRequest("function_name()").Args([]int{}), + }, + { + Name: "eval-with-args.msgpack", + Request: tarantool.NewEvalRequest("function_name(...)").Args(precachedTuple), + }, + { + Name: "eval-with-single-number.msgpack", + Request: tarantool.NewEvalRequest("function_name()").Args(1), + }, + { + Name: "delete-raw.msgpack", + Request: tarantool.NewDeleteRequest("table_name"), + }, + { + Name: "delete-snumber-inumber.msgpack", + Request: tarantool.NewDeleteRequest(246). + Index(123).Key([]interface{}{123}), + }, + { + Name: "delete-snumber-iname.msgpack", + Request: tarantool.NewDeleteRequest(246). + Index("index_name").Key([]interface{}{123}), + }, + { + Name: "delete-sname-inumber.msgpack", + Request: tarantool.NewDeleteRequest("table_name"). + Index(123).Key([]interface{}{123}), + }, + { + Name: "delete-sname-iname.msgpack", + Request: tarantool.NewDeleteRequest("table_name"). + Index("index_name").Key([]interface{}{123}), + }, + { + Name: "replace-sname.msgpack", + Request: tarantool.NewReplaceRequest("table_name"). + Tuple(precachedTuple), + }, + { + Name: "replace-snumber.msgpack", + Request: tarantool.NewReplaceRequest(123). + Tuple(precachedTuple), + }, + { + Name: "insert-sname.msgpack", + Request: tarantool.NewReplaceRequest("table_name"). + Tuple(precachedTuple), + }, + { + Name: "insert-snumber.msgpack", + Request: tarantool.NewReplaceRequest(123). + Tuple(precachedTuple), + }, + { + Name: "call16.msgpack", + Request: tarantool.NewCall16Request("function.name"), + }, + { + Name: "call16-with-args.msgpack", + Request: tarantool.NewCall16Request("function.name"). + Args(precachedTuple), + }, + { + Name: "call16-with-args-nil.msgpack", + Request: tarantool.NewCall16Request("function.name").Args(nil), + }, + { + Name: "call17.msgpack", + Request: tarantool.NewCall17Request("function.name"), + }, + { + Name: "call17-with-args-nil.msgpack", + Request: tarantool.NewCall17Request("function.name").Args(nil), + }, + { + Name: "call17-with-args.msgpack", + Request: tarantool.NewCall17Request("function.name"). + Args(precachedTuple), + }, + { + Name: "update.msgpack", + Request: tarantool.NewUpdateRequest("table_name"), + }, + { + Name: "update-snumber-iname.msgpack", + Request: tarantool.NewUpdateRequest(123). + Index("index_name").Key([]interface{}{123}). + Operations(precachedUpdateOps), + }, + { + Name: "update-sname-iname.msgpack", + Request: tarantool.NewUpdateRequest("table_name"). + Index("index_name").Key([]interface{}{123}). + Operations(precachedUpdateOps), + }, + { + Name: "update-sname-inumber.msgpack", + Request: tarantool.NewUpdateRequest("table_name"). + Index(123).Key([]interface{}{123}). + Operations(precachedUpdateOps), + }, + { + Name: "upsert.msgpack", + Request: tarantool.NewUpsertRequest("table_name"), + }, + { + Name: "upsert-snumber.msgpack", + Request: tarantool.NewUpsertRequest(123). + Operations(precachedUpdateOps). + Tuple(precachedTuple), + }, + { + Name: "upsert-sname.msgpack", + Request: tarantool.NewUpsertRequest("table_name"). + Operations(precachedUpdateOps). + Tuple(precachedTuple), + }, + { + Name: "select", + Request: tarantool.NewSelectRequest("table_name"), + }, + { + Name: "select-sname-iname.msgpack", + Request: tarantool.NewSelectRequest("table_name"). + Index("index_name"), + }, + { + Name: "select-sname-inumber.msgpack", + Request: tarantool.NewSelectRequest("table_name"). + Index(123), + }, + { + Name: "select-snumber-iname.msgpack", + Request: tarantool.NewSelectRequest(123). + Index("index_name"), + }, + { + Name: "select-snumber-inumber.msgpack", + Request: tarantool.NewSelectRequest(123). + Index(123), + }, + { + Name: "select-with-key.msgpack", + Request: tarantool.NewSelectRequest("table_name"). + Key(precachedTuple), + }, + { + Name: "select-key-sname-iname.msgpack", + Request: tarantool.NewSelectRequest("table_name"). + Index("index_name").Key(precachedTuple), + }, + { + Name: "select-key-sname-inumber.msgpack", + Request: tarantool.NewSelectRequest("table_name"). + Index(123).Key(precachedTuple), + }, + { + Name: "select-key-snumber-iname.msgpack", + Request: tarantool.NewSelectRequest(123). + Index("index_name").Key(precachedTuple), + }, + { + Name: "select-key-snumber-inumber.msgpack", + Request: tarantool.NewSelectRequest(123). + Index(123).Key(precachedTuple), + }, + { + Name: "select-with-optionals.msgpack", + Request: tarantool.NewSelectRequest("table_name"). + Offset(123).Limit(123).Iterator(tarantool.IterGe). + FetchPos(true), + }, + { + Name: "select-with-after.msgpack", + Request: tarantool.NewSelectRequest("table_name"). + After(precachedTuple).Limit(123).Iterator(tarantool.IterGe). + Key(precachedTuple), + }, + } + + for _, tc := range testCases { + tc.Execute(t) + } +} diff --git a/request_test.go b/request_test.go index 62baffb32..43e22d311 100644 --- a/request_test.go +++ b/request_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-iproto" . "github.com/tarantool/go-tarantool/v2" ) diff --git a/testdata/requests/begin-with-txn-isolation-is-sync-timeout.msgpack b/testdata/requests/begin-with-txn-isolation-is-sync-timeout.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..dc04ebcd0833c9e2df8b6c3252ce05075497dd66 GIT binary patch literal 15 RcmZn;JMF*#0g;S}hXEX`17iRH literal 0 HcmV?d00001 diff --git a/testdata/requests/begin-with-txn-isolation-is-sync.msgpack b/testdata/requests/begin-with-txn-isolation-is-sync.msgpack new file mode 100644 index 000000000..962d8b233 --- /dev/null +++ b/testdata/requests/begin-with-txn-isolation-is-sync.msgpack @@ -0,0 +1 @@ +�Ya� \ No newline at end of file diff --git a/testdata/requests/begin-with-txn-isolation.msgpack b/testdata/requests/begin-with-txn-isolation.msgpack new file mode 100644 index 000000000..bce57fe8c --- /dev/null +++ b/testdata/requests/begin-with-txn-isolation.msgpack @@ -0,0 +1 @@ +�Y \ No newline at end of file diff --git a/testdata/requests/begin.msgpack b/testdata/requests/begin.msgpack new file mode 100644 index 000000000..5416677bc --- /dev/null +++ b/testdata/requests/begin.msgpack @@ -0,0 +1 @@ +� \ No newline at end of file diff --git a/testdata/requests/call-no-args.msgpack b/testdata/requests/call-no-args.msgpack new file mode 100644 index 000000000..17e7abbb0 --- /dev/null +++ b/testdata/requests/call-no-args.msgpack @@ -0,0 +1 @@ +�"�function.name!� \ No newline at end of file diff --git a/testdata/requests/call-with-args-empty-array.msgpack b/testdata/requests/call-with-args-empty-array.msgpack new file mode 100644 index 000000000..17e7abbb0 --- /dev/null +++ b/testdata/requests/call-with-args-empty-array.msgpack @@ -0,0 +1 @@ +�"�function.name!� \ No newline at end of file diff --git a/testdata/requests/call-with-args-mixed.msgpack b/testdata/requests/call-with-args-mixed.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..8a10ddcfac0a65f07edaf0f80db226e65c142c95 GIT binary patch literal 58 zcmZotTANmymt2yWpQo3Xn479Njd4k0QF<}+wg0J;7#fZUG*0GN_2Hm*$&AD!tE1GC LIZoSuHU$e=I*06-Q7RR910 literal 0 HcmV?d00001 diff --git a/testdata/requests/delete-sname-iname.msgpack b/testdata/requests/delete-sname-iname.msgpack new file mode 100644 index 000000000..49acbef2f --- /dev/null +++ b/testdata/requests/delete-sname-iname.msgpack @@ -0,0 +1 @@ +�^�table_name_�index_name �{ \ No newline at end of file diff --git a/testdata/requests/delete-sname-inumber.msgpack b/testdata/requests/delete-sname-inumber.msgpack new file mode 100644 index 000000000..9390f6e9b --- /dev/null +++ b/testdata/requests/delete-sname-inumber.msgpack @@ -0,0 +1 @@ +�^�table_name{ �{ \ No newline at end of file diff --git a/testdata/requests/delete-snumber-iname.msgpack b/testdata/requests/delete-snumber-iname.msgpack new file mode 100644 index 000000000..5805ea28e --- /dev/null +++ b/testdata/requests/delete-snumber-iname.msgpack @@ -0,0 +1 @@ +���_�index_name �{ \ No newline at end of file diff --git a/testdata/requests/delete-snumber-inumber.msgpack b/testdata/requests/delete-snumber-inumber.msgpack new file mode 100644 index 000000000..39c85022e --- /dev/null +++ b/testdata/requests/delete-snumber-inumber.msgpack @@ -0,0 +1 @@ +���{ �{ \ No newline at end of file diff --git a/testdata/requests/delete.msgpack b/testdata/requests/delete.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..8d001f87ccaa5a3cada8a17d3c339e8cce829c4e GIT binary patch literal 17 YcmZpUTUC;nl#?2tmzbL>$e=I*06-Q7RR910 literal 0 HcmV?d00001 diff --git a/testdata/requests/eval-with-args.msgpack b/testdata/requests/eval-with-args.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..6ab7b9953b7768ac496ff5038d6c8abc141df9f7 GIT binary patch literal 63 zcmZot-;`FGmt2yWpBJB(n47Ahr>Cc>IE`^hVo`cA^R@q}lNcI~2sBRSSoPtccgc*z QBdep-k~vP>e>MgK04?7cfdBvi literal 0 HcmV?d00001 diff --git a/testdata/requests/eval-with-empty-array.msgpack b/testdata/requests/eval-with-empty-array.msgpack new file mode 100644 index 000000000..152e20ef1 --- /dev/null +++ b/testdata/requests/eval-with-empty-array.msgpack @@ -0,0 +1 @@ +�'�function_name()!� \ No newline at end of file diff --git a/testdata/requests/eval-with-nil.msgpack b/testdata/requests/eval-with-nil.msgpack new file mode 100644 index 000000000..152e20ef1 --- /dev/null +++ b/testdata/requests/eval-with-nil.msgpack @@ -0,0 +1 @@ +�'�function_name()!� \ No newline at end of file diff --git a/testdata/requests/eval-with-single-number.msgpack b/testdata/requests/eval-with-single-number.msgpack new file mode 100644 index 000000000..0d75cc10e --- /dev/null +++ b/testdata/requests/eval-with-single-number.msgpack @@ -0,0 +1 @@ +�'�function_name()! \ No newline at end of file diff --git a/testdata/requests/eval.msgpack b/testdata/requests/eval.msgpack new file mode 100644 index 000000000..152e20ef1 --- /dev/null +++ b/testdata/requests/eval.msgpack @@ -0,0 +1 @@ +�'�function_name()!� \ No newline at end of file diff --git a/testdata/requests/insert-sname.msgpack b/testdata/requests/insert-sname.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..6e5f221a7afe588db3063379c99fc5c6f78b9886 GIT binary patch literal 55 zcmZpQTUC;nl#?2tmzbNXIE`^hVo`cA^R@q}lNcI~2sBRSSoPtccgc*zBdep-k~vP> Ie>MgK0HPBYcK`qY literal 0 HcmV?d00001 diff --git a/testdata/requests/insert-snumber.msgpack b/testdata/requests/insert-snumber.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..2fd7d3af3bda56bbc3cb846055247fc4dc0f0ff7 GIT binary patch literal 45 ycmZn?s8*cDxFoSCy_osh|I|qg4MzkTCv&X&aL~JCM&gmxQEJH?r|mx*g8=}&u@dqC literal 0 HcmV?d00001 diff --git a/testdata/requests/ping.msgpack b/testdata/requests/ping.msgpack new file mode 100644 index 000000000..5416677bc --- /dev/null +++ b/testdata/requests/ping.msgpack @@ -0,0 +1 @@ +� \ No newline at end of file diff --git a/testdata/requests/replace-sname.msgpack b/testdata/requests/replace-sname.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..6e5f221a7afe588db3063379c99fc5c6f78b9886 GIT binary patch literal 55 zcmZpQTUC;nl#?2tmzbNXIE`^hVo`cA^R@q}lNcI~2sBRSSoPtccgc*zBdep-k~vP> Ie>MgK0HPBYcK`qY literal 0 HcmV?d00001 diff --git a/testdata/requests/replace-snumber.msgpack b/testdata/requests/replace-snumber.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..2fd7d3af3bda56bbc3cb846055247fc4dc0f0ff7 GIT binary patch literal 45 ycmZn?s8*cDxFoSCy_osh|I|qg4MzkTCv&X&aL~JCM&gmxQEJH?r|mx*g8=}&u@dqC literal 0 HcmV?d00001 diff --git a/testdata/requests/rollback.msgpack b/testdata/requests/rollback.msgpack new file mode 100644 index 000000000..5416677bc --- /dev/null +++ b/testdata/requests/rollback.msgpack @@ -0,0 +1 @@ +� \ No newline at end of file diff --git a/testdata/requests/select b/testdata/requests/select new file mode 100644 index 0000000000000000000000000000000000000000..ebed86cadbed1b6f2786b7f3f36b8bcec29d26a5 GIT binary patch literal 27 hcmZn@VG?E#I`E=ep(FJ`{>KXnpA!x4eT a$sDUb9P}=kk$7Zvlv*;!Y5UK{U;qF=rXijH literal 0 HcmV?d00001 diff --git a/testdata/requests/select-key-sname-inumber.msgpack b/testdata/requests/select-key-sname-inumber.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..e8cdb0bd9a355f5a0a62b6f7cb98bf3390b3cd43 GIT binary patch literal 67 zcmZn@VGw2zI`L|5jj??y^jllo_^GzKI literal 0 HcmV?d00001 diff --git a/testdata/requests/select-key-snumber-iname.msgpack b/testdata/requests/select-key-snumber-iname.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..f686f9afc0de27646c6387c5998766dca86a6a99 GIT binary patch literal 67 zcmZn@VGw2zI`bEe=B6r4V_cG0lwQny?SJYdhK3^ojgvW6eK_b{ TG9&TG>L|5jj??y^jllo_^0OTo literal 0 HcmV?d00001 diff --git a/testdata/requests/select-key-snumber-inumber.msgpack b/testdata/requests/select-key-snumber-inumber.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..29ea666d4ed4e5d7caee9642df0ca50c3d834099 GIT binary patch literal 57 zcmZn@VGw2zI`bEe=B6r4005*A3+Mm< literal 0 HcmV?d00001 diff --git a/testdata/requests/select-snumber-inumber.msgpack b/testdata/requests/select-snumber-inumber.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..8470a2045cb539f13685f64fb15313cbbc754602 GIT binary patch literal 17 XcmZn@VG?E#I`e>MgKeWFzY0BdF{Hvj+t literal 0 HcmV?d00001 diff --git a/testdata/requests/select-with-key.msgpack b/testdata/requests/select-with-key.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..f0976da10db6c07d99e4da5945d9d66d542c9995 GIT binary patch literal 67 zcmZn@VGw2zI`L|5jj??y^jllo_-lQCk literal 0 HcmV?d00001 diff --git a/testdata/requests/select-with-optionals.msgpack b/testdata/requests/select-with-optionals.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..4915ece9768e869248787828758605d84dcb87b7 GIT binary patch literal 25 gcmZn{VHK_xs*YP#l9-f}8lRV#n<~hlFhTw>0BO<)ZvX%Q literal 0 HcmV?d00001 diff --git a/testdata/requests/update-sname-iname.msgpack b/testdata/requests/update-sname-iname.msgpack new file mode 100644 index 000000000..b085c995b --- /dev/null +++ b/testdata/requests/update-sname-iname.msgpack @@ -0,0 +1,2 @@ +�^�table_name_�index_name �{!���+�test��=�fest��#���!�insert��:�splice��-�subtract��& +��| ��^ \ No newline at end of file diff --git a/testdata/requests/update-sname-inumber.msgpack b/testdata/requests/update-sname-inumber.msgpack new file mode 100644 index 000000000..f603ddf34 --- /dev/null +++ b/testdata/requests/update-sname-inumber.msgpack @@ -0,0 +1,2 @@ +�^�table_name{ �{!���+�test��=�fest��#���!�insert��:�splice��-�subtract��& +��| ��^ \ No newline at end of file diff --git a/testdata/requests/update-snumber-iname.msgpack b/testdata/requests/update-snumber-iname.msgpack new file mode 100644 index 000000000..cd3ec4317 --- /dev/null +++ b/testdata/requests/update-snumber-iname.msgpack @@ -0,0 +1,2 @@ +�{_�index_name �{!���+�test��=�fest��#���!�insert��:�splice��-�subtract��& +��| ��^ \ No newline at end of file diff --git a/testdata/requests/update.msgpack b/testdata/requests/update.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..3749bbcade764e358229730f2e728decc0923fa4 GIT binary patch literal 19 acmZpPTUC;nl#?2tmzbL>$e=JmaRLBMr3VK9 literal 0 HcmV?d00001 diff --git a/testdata/requests/upsert-sname.msgpack b/testdata/requests/upsert-sname.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..3620cbc1e2b178779e45e0fdf38d8fb3cec25f8e GIT binary patch literal 132 zcmZpUTUC;nl#?2tmzbNXIE`^hVo`cA^R@q}lNcI~2sBRSSoPtccgc*zBdep-k~vP> ze>MgKjhT}dYBMe=Ni8m!ywH|uNg9};%)DUoLPeHknR&&jMI}=gTCuXRFDov{$xKe2 gyik{IMR93TNl{`lP>UKn*W`sY9Nd!^#&Plh0BdtKNB{r; literal 0 HcmV?d00001 diff --git a/testdata/requests/upsert-snumber.msgpack b/testdata/requests/upsert-snumber.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..6e34b0c2fe7ceb0895e1d6e1c9d2cf8b1dbee621 GIT binary patch literal 122 zcmZn`s8*cDxFoSCy_osh|I|qg4MzkTCv&X&aL~JCM&gmxQEJH?r|mx*gMr4($qTg^ zmz1OymrP!0%d{j7%ur@tFnOUO%d*V8;?$y&sSB-G+1QsA7vy9nr%qm|%eJDpG^wO0 VF&U^ujh$=q!Ws_l$qVB+c>pT+Fv9=< literal 0 HcmV?d00001 diff --git a/testdata/requests/upsert.msgpack b/testdata/requests/upsert.msgpack new file mode 100644 index 000000000..06a3800f8 --- /dev/null +++ b/testdata/requests/upsert.msgpack @@ -0,0 +1 @@ +�^�table_name!�(� \ No newline at end of file From 20dc742184d7de84ff3466b572976ce80a900100 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Fri, 11 Jul 2025 15:12:29 +0300 Subject: [PATCH 584/605] Release v2.4.0 --- CHANGELOG.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db043f583..9e536967e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,15 +10,24 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +### Changed + +### Fixed + +## [v2.4.0] - 2025-07-11 + +This release focuses on adding schema/user/session operations, synchronous transaction +flag handling, and fixes watcher panic. + +### Added + - Implemented all box.schema.user operations requests and sugar interface (#426). - Implemented box.session.su request and sugar interface only for current session granting (#426). -- Defined `ErrConcurrentSchemaUpdate` constant for "concurrent schema update" error. +- Defined `ErrConcurrentSchemaUpdate` constant for "concurrent schema update" error (#404). Now you can check this error with `errors.Is(err, tarantool.ErrConcurrentSchemaUpdate)`. - Implemented support for `IPROTO_IS_SYNC` flag in stream transactions, added `IsSync(bool)` method for `BeginRequest`/`CommitRequest` (#447). -### Changed - ### Fixed - Fixed panic when calling NewWatcher() during reconnection or after From 5d29e17bff1d7dcf3f0e7ebde41c2e30de9297d7 Mon Sep 17 00:00:00 2001 From: Mikhail Elhimov Date: Fri, 8 Aug 2025 14:22:30 +0300 Subject: [PATCH 585/605] test: handle multiple instances in parallel --- test_helpers/pool_helper.go | 45 ++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index b67d05f02..335496ff7 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -2,8 +2,10 @@ package test_helpers import ( "context" + "errors" "fmt" "reflect" + "sync" "time" "github.com/tarantool/go-tarantool/v2" @@ -179,16 +181,22 @@ func InsertOnInstances( return fmt.Errorf("fail to set roles for cluster: %s", err.Error()) } - for _, dialer := range dialers { - ctx, cancel := GetConnectContext() - err := InsertOnInstance(ctx, dialer, connOpts, space, tuple) - cancel() - if err != nil { - return err - } + ctx, cancel := GetConnectContext() + defer cancel() + + errs := make([]error, len(dialers)) + var wg sync.WaitGroup + wg.Add(len(dialers)) + for i, dialer := range dialers { + // Pass loop variable(s) to avoid its capturing by reference (not needed since Go 1.22). + go func(i int, dialer tarantool.Dialer) { + defer wg.Done() + errs[i] = InsertOnInstance(ctx, dialer, connOpts, space, tuple) + }(i, dialer) } + wg.Wait() - return nil + return errors.Join(errs...) } func SetInstanceRO(ctx context.Context, dialer tarantool.Dialer, connOpts tarantool.Opts, @@ -215,16 +223,23 @@ func SetClusterRO(dialers []tarantool.Dialer, connOpts tarantool.Opts, return fmt.Errorf("number of servers should be equal to number of roles") } + ctx, cancel := GetConnectContext() + defer cancel() + + // Apply roles in parallel. + errs := make([]error, len(dialers)) + var wg sync.WaitGroup + wg.Add(len(dialers)) for i, dialer := range dialers { - ctx, cancel := GetConnectContext() - err := SetInstanceRO(ctx, dialer, connOpts, roles[i]) - cancel() - if err != nil { - return err - } + // Pass loop variable(s) to avoid its capturing by reference (not needed since Go 1.22). + go func(i int, dialer tarantool.Dialer) { + defer wg.Done() + errs[i] = SetInstanceRO(ctx, dialer, connOpts, roles[i]) + }(i, dialer) } + wg.Wait() - return nil + return errors.Join(errs...) } func StartTarantoolInstances(instsOpts []StartOpts) ([]*TarantoolInstance, error) { From f581fcda067e3bfc7a0a6271dcb10503de397808 Mon Sep 17 00:00:00 2001 From: Mikhail Elhimov Date: Fri, 8 Aug 2025 15:48:19 +0300 Subject: [PATCH 586/605] test: improve code readability General context approach is used in multi-instances functions -- context is passed as a first argument. --- pool/connection_pool_test.go | 237 +++++++++++++++----------- pool/example_test.go | 12 +- queue/example_connection_pool_test.go | 4 +- queue/queue_test.go | 4 +- test_helpers/pool_helper.go | 11 +- 5 files changed, 151 insertions(+), 117 deletions(-) diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index f3bf5f556..8120c613d 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -1115,7 +1115,10 @@ func TestConnectionHandlerOpenUpdateClose(t *testing.T) { poolInstances := makeInstances(poolServers, connOpts) roles := []bool{false, true} - err := test_helpers.SetClusterRO(makeDialers(poolServers), connOpts, roles) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + + err := test_helpers.SetClusterRO(ctx, makeDialers(poolServers), connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") h := &testHandler{} @@ -1123,8 +1126,6 @@ func TestConnectionHandlerOpenUpdateClose(t *testing.T) { CheckTimeout: 100 * time.Microsecond, ConnectionHandler: h, } - ctx, cancel := test_helpers.GetPoolConnectContext() - defer cancel() connPool, err := pool.ConnectWithOpts(ctx, poolInstances, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1249,7 +1250,10 @@ func TestConnectionHandlerUpdateError(t *testing.T) { poolInstances := makeInstances(poolServers, connOpts) roles := []bool{false, false} - err := test_helpers.SetClusterRO(makeDialers(poolServers), connOpts, roles) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + + err := test_helpers.SetClusterRO(ctx, makeDialers(poolServers), connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") h := &testUpdateErrorHandler{} @@ -1257,8 +1261,6 @@ func TestConnectionHandlerUpdateError(t *testing.T) { CheckTimeout: 100 * time.Microsecond, ConnectionHandler: h, } - ctx, cancel := test_helpers.GetPoolConnectContext() - defer cancel() connPool, err := pool.ConnectWithOpts(ctx, poolInstances, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1324,7 +1326,10 @@ func TestConnectionHandlerDeactivated_on_remove(t *testing.T) { poolInstances := makeInstances(poolServers, connOpts) roles := []bool{false, false} - err := test_helpers.SetClusterRO(makeDialers(poolServers), connOpts, roles) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + + err := test_helpers.SetClusterRO(ctx, makeDialers(poolServers), connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") h := &testDeactivatedErrorHandler{} @@ -1332,8 +1337,6 @@ func TestConnectionHandlerDeactivated_on_remove(t *testing.T) { CheckTimeout: 100 * time.Microsecond, ConnectionHandler: h, } - ctx, cancel := test_helpers.GetPoolConnectContext() - defer cancel() connPool, err := pool.ConnectWithOpts(ctx, poolInstances, poolOpts) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1413,11 +1416,12 @@ func TestRequestOnClosed(t *testing.T) { func TestCall(t *testing.T) { roles := []bool{false, true, false, false, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1472,11 +1476,12 @@ func TestCall(t *testing.T) { func TestCall16(t *testing.T) { roles := []bool{false, true, false, false, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1531,11 +1536,12 @@ func TestCall16(t *testing.T) { func TestCall17(t *testing.T) { roles := []bool{false, true, false, false, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1590,11 +1596,12 @@ func TestCall17(t *testing.T) { func TestEval(t *testing.T) { roles := []bool{false, true, false, false, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1670,11 +1677,12 @@ func TestExecute(t *testing.T) { roles := []bool{false, true, false, false, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1728,11 +1736,12 @@ func TestRoundRobinStrategy(t *testing.T) { serversNumber := len(servers) - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1807,11 +1816,12 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { servers[4]: true, } - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1880,11 +1890,12 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { servers[4]: true, } - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -1965,11 +1976,12 @@ func TestUpdateInstancesRoles(t *testing.T) { serversNumber := len(servers) - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2044,7 +2056,9 @@ func TestUpdateInstancesRoles(t *testing.T) { servers[3]: true, } - err = test_helpers.SetClusterRO(dialers, connOpts, roles) + ctxSetRoles, cancelSetRoles := test_helpers.GetPoolConnectContext() + err = test_helpers.SetClusterRO(ctxSetRoles, dialers, connOpts, roles) + cancelSetRoles() require.Nilf(t, err, "fail to set roles for cluster") // ANY @@ -2111,11 +2125,12 @@ func TestUpdateInstancesRoles(t *testing.T) { func TestInsert(t *testing.T) { roles := []bool{true, true, false, true, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2210,11 +2225,12 @@ func TestInsert(t *testing.T) { func TestDelete(t *testing.T) { roles := []bool{true, true, false, true, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2274,11 +2290,12 @@ func TestDelete(t *testing.T) { func TestUpsert(t *testing.T) { roles := []bool{true, true, false, true, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2346,11 +2363,12 @@ func TestUpsert(t *testing.T) { func TestUpdate(t *testing.T) { roles := []bool{true, true, false, true, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2435,11 +2453,12 @@ func TestUpdate(t *testing.T) { func TestReplace(t *testing.T) { roles := []bool{true, true, false, true, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2520,11 +2539,12 @@ func TestReplace(t *testing.T) { func TestSelect(t *testing.T) { roles := []bool{true, true, false, true, false} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2543,13 +2563,13 @@ func TestSelect(t *testing.T) { rwKey := []interface{}{"rw_select_key"} anyKey := []interface{}{"any_select_key"} - err = test_helpers.InsertOnInstances(makeDialers(roServers), connOpts, spaceNo, roTpl) + err = test_helpers.InsertOnInstances(ctx, makeDialers(roServers), connOpts, spaceNo, roTpl) require.Nil(t, err) - err = test_helpers.InsertOnInstances(makeDialers(rwServers), connOpts, spaceNo, rwTpl) + err = test_helpers.InsertOnInstances(ctx, makeDialers(rwServers), connOpts, spaceNo, rwTpl) require.Nil(t, err) - err = test_helpers.InsertOnInstances(makeDialers(allServers), connOpts, spaceNo, anyTpl) + err = test_helpers.InsertOnInstances(ctx, makeDialers(allServers), connOpts, spaceNo, anyTpl) require.Nil(t, err) //default: ANY @@ -2642,11 +2662,12 @@ func TestSelect(t *testing.T) { func TestPing(t *testing.T) { roles := []bool{true, true, false, true, false} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2681,11 +2702,12 @@ func TestPing(t *testing.T) { func TestDo(t *testing.T) { roles := []bool{true, true, false, true, false} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2717,11 +2739,12 @@ func TestDo(t *testing.T) { func TestDo_concurrent(t *testing.T) { roles := []bool{true, true, false, true, false} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2766,12 +2789,12 @@ func TestDoInstance(t *testing.T) { func TestDoInstance_not_found(t *testing.T) { roles := []bool{true, true, false, true, false} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, []pool.Instance{}) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2820,11 +2843,12 @@ func TestNewPrepared(t *testing.T) { roles := []bool{true, true, false, true, false} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2892,11 +2916,12 @@ func TestDoWithStrangerConn(t *testing.T) { roles := []bool{true, true, false, true, false} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -2922,11 +2947,12 @@ func TestStream_Commit(t *testing.T) { roles := []bool{true, true, false, true, true} - err = test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err = test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -3013,11 +3039,12 @@ func TestStream_Rollback(t *testing.T) { roles := []bool{true, true, false, true, true} - err = test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err = test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -3103,11 +3130,12 @@ func TestStream_TxnIsolationLevel(t *testing.T) { roles := []bool{true, true, false, true, true} - err = test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err = test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -3214,11 +3242,12 @@ func TestConnectionPool_NewWatcher_modes(t *testing.T) { roles := []bool{true, false, false, true, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -3291,14 +3320,15 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { const initCnt = 2 roles := []bool{true, false, false, true, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) require.Nilf(t, err, "fail to set roles for cluster") poolOpts := pool.Opts{ CheckTimeout: 500 * time.Millisecond, } - ctx, cancel := test_helpers.GetPoolConnectContext() - defer cancel() pool, err := pool.ConnectWithOpts(ctx, instances, poolOpts) require.Nilf(t, err, "failed to connect") @@ -3338,7 +3368,9 @@ func TestConnectionPool_NewWatcher_update(t *testing.T) { for i, role := range roles { roles[i] = !role } - err = test_helpers.SetClusterRO(dialers, connOpts, roles) + ctxSetRoles, cancelSetRoles := test_helpers.GetPoolConnectContext() + err = test_helpers.SetClusterRO(ctxSetRoles, dialers, connOpts, roles) + cancelSetRoles() require.Nilf(t, err, "fail to set roles for cluster") // Wait for all updated events. @@ -3376,11 +3408,12 @@ func TestWatcher_Unregister(t *testing.T) { const expectedCnt = 2 roles := []bool{true, false, false, true, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + pool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, pool, "conn is nil after Connect") @@ -3433,11 +3466,12 @@ func TestConnectionPool_NewWatcher_concurrent(t *testing.T) { roles := []bool{true, false, false, true, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") @@ -3471,11 +3505,12 @@ func TestWatcher_Unregister_concurrent(t *testing.T) { roles := []bool{true, false, false, true, true} - err := test_helpers.SetClusterRO(dialers, connOpts, roles) - require.Nilf(t, err, "fail to set roles for cluster") - ctx, cancel := test_helpers.GetPoolConnectContext() defer cancel() + + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + require.Nilf(t, err, "fail to set roles for cluster") + connPool, err := pool.Connect(ctx, instances) require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") diff --git a/pool/example_test.go b/pool/example_test.go index a4d3d4ba1..dce8bb1af 100644 --- a/pool/example_test.go +++ b/pool/example_test.go @@ -23,12 +23,12 @@ var testRoles = []bool{true, true, false, true, true} func examplePool(roles []bool, connOpts tarantool.Opts) (*pool.ConnectionPool, error) { - err := test_helpers.SetClusterRO(dialers, connOpts, roles) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + err := test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) if err != nil { return nil, fmt.Errorf("ConnectionPool is not established") } - ctx, cancel := test_helpers.GetPoolConnectContext() - defer cancel() connPool, err := pool.Connect(ctx, instances) if err != nil || connPool == nil { return nil, fmt.Errorf("ConnectionPool is not established") @@ -55,12 +55,12 @@ func exampleFeaturesPool(roles []bool, connOpts tarantool.Opts, }) poolDialers = append(poolDialers, dialer) } - err := test_helpers.SetClusterRO(poolDialers, connOpts, roles) + ctx, cancel := test_helpers.GetPoolConnectContext() + defer cancel() + err := test_helpers.SetClusterRO(ctx, poolDialers, connOpts, roles) if err != nil { return nil, fmt.Errorf("ConnectionPool is not established") } - ctx, cancel := test_helpers.GetPoolConnectContext() - defer cancel() connPool, err := pool.Connect(ctx, poolInstances) if err != nil || connPool == nil { return nil, fmt.Errorf("ConnectionPool is not established") diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go index 355a491ed..a126e13a1 100644 --- a/queue/example_connection_pool_test.go +++ b/queue/example_connection_pool_test.go @@ -212,7 +212,9 @@ func Example_connectionPool() { // Switch a master instance in the pool. roles := []bool{true, false} for { - err := test_helpers.SetClusterRO(poolDialers, connOpts, roles) + ctx, cancel := test_helpers.GetPoolConnectContext() + err := test_helpers.SetClusterRO(ctx, poolDialers, connOpts, roles) + cancel() if err == nil { break } diff --git a/queue/queue_test.go b/queue/queue_test.go index e43c47113..840c18b4f 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -954,7 +954,9 @@ func runTestMain(m *testing.M) int { }) } - err = test_helpers.SetClusterRO(dialers, connOpts, roles) + ctx, cancel := test_helpers.GetPoolConnectContext() + err = test_helpers.SetClusterRO(ctx, dialers, connOpts, roles) + cancel() if err == nil { break } diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index 335496ff7..4386596ce 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -166,6 +166,7 @@ func InsertOnInstance(ctx context.Context, dialer tarantool.Dialer, connOpts tar } func InsertOnInstances( + ctx context.Context, dialers []tarantool.Dialer, connOpts tarantool.Opts, space interface{}, @@ -176,14 +177,11 @@ func InsertOnInstances( roles[i] = false } - err := SetClusterRO(dialers, connOpts, roles) + err := SetClusterRO(ctx, dialers, connOpts, roles) if err != nil { return fmt.Errorf("fail to set roles for cluster: %s", err.Error()) } - ctx, cancel := GetConnectContext() - defer cancel() - errs := make([]error, len(dialers)) var wg sync.WaitGroup wg.Add(len(dialers)) @@ -217,15 +215,12 @@ func SetInstanceRO(ctx context.Context, dialer tarantool.Dialer, connOpts tarant return nil } -func SetClusterRO(dialers []tarantool.Dialer, connOpts tarantool.Opts, +func SetClusterRO(ctx context.Context, dialers []tarantool.Dialer, connOpts tarantool.Opts, roles []bool) error { if len(dialers) != len(roles) { return fmt.Errorf("number of servers should be equal to number of roles") } - ctx, cancel := GetConnectContext() - defer cancel() - // Apply roles in parallel. errs := make([]error, len(dialers)) var wg sync.WaitGroup From 325040dbef7a731a303d0ef93665f6ab2a4f6122 Mon Sep 17 00:00:00 2001 From: Mikhail Elhimov Date: Thu, 7 Aug 2025 21:31:21 +0300 Subject: [PATCH 587/605] pool: use safe type assertion --- pool/connection_pool.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pool/connection_pool.go b/pool/connection_pool.go index a47ec19a5..e62cb2b3e 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -1085,7 +1085,12 @@ func (p *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (Role, er return UnknownRole, ErrIncorrectResponse } - instanceStatus, ok := data[0].(map[interface{}]interface{})["status"] + respFields, ok := data[0].(map[interface{}]interface{}) + if !ok { + return UnknownRole, ErrIncorrectResponse + } + + instanceStatus, ok := respFields["status"] if !ok { return UnknownRole, ErrIncorrectResponse } @@ -1093,7 +1098,7 @@ func (p *ConnectionPool) getConnectionRole(conn *tarantool.Connection) (Role, er return UnknownRole, ErrIncorrectStatus } - replicaRole, ok := data[0].(map[interface{}]interface{})[roFieldName] + replicaRole, ok := respFields[roFieldName] if !ok { return UnknownRole, ErrIncorrectResponse } From 44917a0575b9725b3594ed20c183ef0b3157ad78 Mon Sep 17 00:00:00 2001 From: Mikhail Elhimov Date: Wed, 30 Jul 2025 17:37:05 +0300 Subject: [PATCH 588/605] test: fix flaky test Helper method that performs initial assigning of master/replica roles and is widely used in ConnectionPool tests was adjusted to wait for the roles to be applied successfully. Prior to this patch it doesn't, so sometimes subsequent test code might work unexpectedly (the problem was caught with TestConnectionHandlerOpenUpdateClose) Closes #452 --- test_helpers/pool_helper.go | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index 4386596ce..599e56594 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -212,6 +212,50 @@ func SetInstanceRO(ctx context.Context, dialer tarantool.Dialer, connOpts tarant return err } + checkRole := func(conn *tarantool.Connection, isReplica bool) string { + data, err := conn.Do(tarantool.NewCallRequest("box.info")).Get() + switch { + case err != nil: + return fmt.Sprintf("failed to get box.info: %s", err) + case len(data) < 1: + return "box.info is empty" + } + + boxInfo, ok := data[0].(map[interface{}]interface{}) + if !ok { + return "unexpected type in box.info response" + } + + status, statusFound := boxInfo["status"] + readonly, readonlyFound := boxInfo["ro"] + switch { + case !statusFound: + return "box.info.status is missing" + case status != "running": + return fmt.Sprintf("box.info.status='%s' (waiting for 'running')", status) + case !readonlyFound: + return "box.info.ro is missing" + case readonly != isReplica: + return fmt.Sprintf("box.info.ro='%v' (waiting for '%v')", readonly, isReplica) + default: + return "" + } + } + + problem := "not checked yet" + + // Wait for the role to be applied. + for len(problem) != 0 { + select { + case <-time.After(10 * time.Millisecond): + case <-ctx.Done(): + return fmt.Errorf("%w: failed to apply role, the last problem: %s", + ctx.Err(), problem) + } + + problem = checkRole(conn, isReplica) + } + return nil } From e50fbb5c497bcaf32b425b0bdaad8caf7ea00a28 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Fri, 5 Sep 2025 17:54:25 +0300 Subject: [PATCH 589/605] ci: update GitHub Actions workflows `check.yaml`: * All jobs now run on ubuntu-latest instead of ubuntu-22.04. * Updated actions/checkout to v5, tarantool/setup-tarantool to v4, and actions/setup-go to v5. * Upgraded luacheck's Tarantool version to 3.4. `reusable-run.yml`: * Created a new reusable workflow to centralize testing logic, accepting os, tarantool-version, go-version, coveralls, and fuzzing as inputs. * Uses actions/checkout@v5 and actions/setup-go@v5. * Removed alternative cmake version. `reusable_testing.yml`: * Updated actions versions and version of tt. `testing.yml`: * Split run-tests-ce into run-tests-tarantool-1-10 (for Tarantool 1.10) and run-tests (for Tarantool 2.11, 3.4, and master). * Both new jobs leverage the reusable-run.yml workflow. * Updated actions/checkout to v5, actions/cache to v4, and actions/setup-go to v5. remove alternative cmake version --- .github/workflows/check.yaml | 20 ++-- .github/workflows/reusable-run.yml | 98 ++++++++++++++++++ .github/workflows/reusable_testing.yml | 8 +- .github/workflows/testing.yml | 137 ++++++------------------- 4 files changed, 146 insertions(+), 117 deletions(-) create mode 100644 .github/workflows/reusable-run.yml diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 02d561c9b..29b6f21d2 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -6,22 +6,22 @@ on: jobs: luacheck: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: | github.event_name == 'push' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository steps: - - uses: actions/checkout@master + - uses: actions/checkout@v5 - name: Setup Tarantool - uses: tarantool/setup-tarantool@v2 + uses: tarantool/setup-tarantool@v4 with: - tarantool-version: '2.8' + tarantool-version: '3.4' - name: Setup tt run: | - curl -L https://tarantool.io/release/2/installer.sh | sudo bash + curl -L https://tarantool.io/release/3/installer.sh | sudo bash sudo apt install -y tt tt version @@ -32,15 +32,15 @@ jobs: run: ./.rocks/bin/luacheck . golangci-lint: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: | github.event_name == 'push' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository steps: - - uses: actions/setup-go@v2 + - uses: actions/setup-go@v5 - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - name: golangci-lint uses: golangci/golangci-lint-action@v3 @@ -57,13 +57,13 @@ jobs: args: --out-${NO_FUTURE}format colored-line-number --config=.golangci.yaml codespell: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: | github.event_name == 'push' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository steps: - - uses: actions/checkout@master + - uses: actions/checkout@v5 - name: Install codespell run: pip3 install codespell diff --git a/.github/workflows/reusable-run.yml b/.github/workflows/reusable-run.yml new file mode 100644 index 000000000..e6ec05228 --- /dev/null +++ b/.github/workflows/reusable-run.yml @@ -0,0 +1,98 @@ +name: Reusable Test Run + +on: + workflow_call: + inputs: + os: + required: true + type: string + tarantool-version: + required: true + type: string + go-version: + required: true + type: string + coveralls: + required: false + type: boolean + default: false + fuzzing: + required: false + type: boolean + default: false + +jobs: + run-tests: + runs-on: ${{ inputs.os }} + steps: + - name: Clone the connector + uses: actions/checkout@v5 + + - name: Setup tt + run: | + curl -L https://tarantool.io/release/3/installer.sh | sudo bash + sudo apt install -y tt + + - name: Setup tt environment + run: tt init + + - name: Setup Tarantool ${{ inputs.tarantool-version }} + if: inputs.tarantool-version != 'master' + uses: tarantool/setup-tarantool@v4 + with: + tarantool-version: ${{ inputs.tarantool-version }} + + - name: Get Tarantool master commit + if: inputs.tarantool-version == 'master' + run: | + commit_hash=$(git ls-remote https://github.com/tarantool/tarantool.git --branch master | head -c 8) + echo "LATEST_COMMIT=${commit_hash}" >> $GITHUB_ENV + shell: bash + + - name: Cache Tarantool master + if: inputs.tarantool-version == 'master' + id: cache-latest + uses: actions/cache@v4 + with: + path: | + ${{ github.workspace }}/bin + ${{ github.workspace }}/include + key: cache-latest-${{ env.LATEST_COMMIT }} + + - name: Setup Tarantool master + if: inputs.tarantool-version == 'master' && steps.cache-latest.outputs.cache-hit != 'true' + run: | + sudo tt install tarantool master + + - name: Add Tarantool master to PATH + if: inputs.tarantool-version == 'master' + run: echo "${GITHUB_WORKSPACE}/bin" >> $GITHUB_PATH + + - name: Setup golang for the connector and tests + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + + - name: Install test dependencies + run: make deps + + - name: Run regression tests + run: make test + + - name: Run race tests + run: make testrace + + - name: Run fuzzing tests + if: ${{ inputs.fuzzing }} + run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" + + - name: Run tests, collect code coverage data and send to Coveralls + if: ${{ inputs.coveralls }} + env: + COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + make coveralls + + - name: Check workability of benchmark tests + if: inputs.go-version == 'stable' + run: make bench-deps bench DURATION=1x COUNT=1 diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index 6c821fe51..895859314 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -14,12 +14,12 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Clone the go-tarantool connector - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: ${{ github.repository_owner }}/go-tarantool - name: Download the tarantool build artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: ${{ inputs.artifact_name }} @@ -32,7 +32,7 @@ jobs: run: | TNT_VERSION=$(tarantool --version | grep -e '^Tarantool') echo "TNT_VERSION=$TNT_VERSION" >> $GITHUB_ENV - +п - name: Setup golang for connector and tests uses: actions/setup-go@v5 with: @@ -40,7 +40,7 @@ jobs: - name: Setup tt run: | - curl -L https://tarantool.io/release/2/installer.sh | sudo bash + curl -L https://tarantool.io/release/3/installer.sh | sudo bash sudo apt install -y tt tt version diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 433ed206b..bf0b0dd2b 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -8,34 +8,36 @@ on: workflow_dispatch: jobs: - run-tests-ce: - # We want to run on external PRs, but not on our own internal - # PRs as they'll be run by the push to the branch. - # - # The main trick is described here: - # https://github.com/Dart-Code/Dart-Code/pull/2375 - # - # Also we want to run it always for manually triggered workflows. + run-tests-tarantool-1-10: if: (github.event_name == 'push') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) || (github.event_name == 'workflow_dispatch') - - # We could replace it with ubuntu-latest after fixing the bug: - # https://github.com/tarantool/setup-tarantool/issues/37 - runs-on: ubuntu-22.04 - strategy: fail-fast: false matrix: - golang: - - '1.20' - - 'stable' - tarantool: - - '1.10' - - '2.8' - - '2.10' - - 'master' + golang: ['1.20', 'stable'] + tarantool: ['1.10'] + coveralls: [false] + fuzzing: [false] + uses: ./.github/workflows/reusable-run.yml + with: + os: ubuntu-22.04 + go-version: ${{ matrix.golang }} + tarantool-version: ${{ matrix.tarantool }} + coveralls: ${{ matrix.coveralls }} + fuzzing: ${{ matrix.fuzzing }} + + run-tests: + if: (github.event_name == 'push') || + (github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.repository) || + (github.event_name == 'workflow_dispatch') + strategy: + fail-fast: false + matrix: + golang: ['1.20', 'stable'] + tarantool: ['2.11', '3.4', 'master'] coveralls: [false] fuzzing: [false] include: @@ -46,84 +48,13 @@ jobs: fuzzing: true golang: '1.20' coveralls: false - - steps: - - name: Clone the connector - uses: actions/checkout@v3 - - - name: Setup tt - run: | - curl -L https://tarantool.io/release/2/installer.sh | sudo bash - sudo apt install -y tt - - - name: Setup tt environment - run: tt init - - # https://github.com/tarantool/checks/issues/64 - - name: Install specific CMake version - run: pip3 install cmake==3.15.3 - - - name: Setup Tarantool ${{ matrix.tarantool }} - if: matrix.tarantool != 'master' - uses: tarantool/setup-tarantool@v2 - with: - tarantool-version: ${{ matrix.tarantool }} - - - name: Get Tarantool master commit - if: matrix.tarantool == 'master' - run: | - commit_hash=$(git ls-remote https://github.com/tarantool/tarantool.git --branch master | head -c 8) - echo "LATEST_COMMIT=${commit_hash}" >> $GITHUB_ENV - shell: bash - - - name: Cache Tarantool master - if: matrix.tarantool == 'master' - id: cache-latest - uses: actions/cache@v3 - with: - path: | - ${{ github.workspace }}/bin - ${{ github.workspace }}/include - key: cache-latest-${{ env.LATEST_COMMIT }} - - - name: Setup Tarantool master - if: matrix.tarantool == 'master' && steps.cache-latest.outputs.cache-hit != 'true' - run: | - sudo pip3 install cmake==3.15.3 - sudo tt install tarantool master - - - name: Add Tarantool master to PATH - if: matrix.tarantool == 'master' - run: echo "${GITHUB_WORKSPACE}/bin" >> $GITHUB_PATH - - - name: Setup golang for the connector and tests - uses: actions/setup-go@v3 - with: - go-version: ${{ matrix.golang }} - - - name: Install test dependencies - run: make deps - - - name: Run regression tests - run: make test - - - name: Run race tests - run: make testrace - - - name: Run fuzzing tests - if: ${{ matrix.fuzzing }} - run: make fuzzing TAGS="go_tarantool_decimal_fuzzing" - - - name: Run tests, collect code coverage data and send to Coveralls - if: ${{ matrix.coveralls }} - env: - COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - make coveralls - - - name: Check workability of benchmark tests - if: matrix.golang == 'stable' - run: make bench-deps bench DURATION=1x COUNT=1 + uses: ./.github/workflows/reusable-run.yml + with: + os: ubuntu-24.04 + go-version: ${{ matrix.golang }} + tarantool-version: ${{ matrix.tarantool }} + coveralls: ${{ matrix.coveralls }} + fuzzing: ${{ matrix.fuzzing }} testing_mac_os: # We want to run on external PRs, but not on our own internal @@ -165,12 +96,12 @@ jobs: runs-on: ${{ matrix.runs-on }} steps: - name: Clone the connector - uses: actions/checkout@v3 + uses: actions/checkout@v5 with: path: ${{ env.SRCDIR }} - name: Restore cache of tarantool ${{ env.T_VERSION }} - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache with: path: ${{ env.T_TARDIR }} @@ -186,7 +117,7 @@ jobs: if: matrix.tarantool != 'brew' && steps.cache.outputs.cache-hit != 'true' - name: Clone tarantool ${{ env.T_VERSION }} - uses: actions/checkout@v3 + uses: actions/checkout@v5 with: repository: tarantool/tarantool ref: ${{ env.T_VERSION }} @@ -257,7 +188,7 @@ jobs: if: matrix.tarantool != 'brew' && matrix.tarantool != 'master' - name: Setup golang for the connector and tests - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.golang }} From 871efa053c8a50517adfcbfc3bde90579d3d6f87 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Mon, 8 Sep 2025 17:07:44 +0300 Subject: [PATCH 590/605] ci: fixing workflow definitions * `reusable_testing.yml` contained bad symbol in the steps. * `run-tests` failed to run coveralls=true,fuzzing=false job. --- .github/workflows/reusable_testing.yml | 2 +- .github/workflows/testing.yml | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index 895859314..94c671543 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -32,7 +32,7 @@ jobs: run: | TNT_VERSION=$(tarantool --version | grep -e '^Tarantool') echo "TNT_VERSION=$TNT_VERSION" >> $GITHUB_ENV -п + - name: Setup golang for connector and tests uses: actions/setup-go@v5 with: diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index bf0b0dd2b..f08aa9fc0 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -41,12 +41,13 @@ jobs: coveralls: [false] fuzzing: [false] include: - - tarantool: 'master' + - golang: '1.20' + tarantool: 'master' coveralls: true - golang: '1.20' - - tarantool: 'master' + fuzzing: false + - golang: '1.20' + tarantool: 'master' fuzzing: true - golang: '1.20' coveralls: false uses: ./.github/workflows/reusable-run.yml with: From 47f0e5d7b9f124e40c108e9988bed393962cb3ba Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Tue, 16 Sep 2025 12:26:30 +0300 Subject: [PATCH 591/605] ci: fix actions on macOS flows * Removed unsupported macos-13 from CI actions. * Added new and supported by apple/github macos-15 and macos-26. * Removed tests for 1.10 for macOS since they won't even build on new versions of llvm that are shipped with macos-14 and newer ones. * Use actions-setup-cmake@v2 for new versions of macOS, since they are shipped with cmake 4.* or cmake 3.31, but supported cmake version for macOS is 3.29.x. --- .github/workflows/testing.yml | 95 ++--------------------------------- 1 file changed, 5 insertions(+), 90 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index f08aa9fc0..2fce2f5ec 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -75,14 +75,9 @@ jobs: - '1.20' - 'stable' runs-on: - - macos-13 - macos-14 - tarantool: - - brew - - 1.10.15 - exclude: - - runs-on: macos-14 - tarantool: 1.10.15 + - macos-15 + - macos-26 env: # Make sense only for non-brew jobs. @@ -101,92 +96,13 @@ jobs: with: path: ${{ env.SRCDIR }} - - name: Restore cache of tarantool ${{ env.T_VERSION }} - uses: actions/cache@v4 - id: cache + - name: Setup cmake + uses: jwlawson/actions-setup-cmake@v2 with: - path: ${{ env.T_TARDIR }} - key: ${{ matrix.runs-on }}-${{ matrix.tarantool }} - if: matrix.tarantool != 'brew' + cmake-version: '3.29.x' - name: Install latest tarantool from brew run: brew install tarantool - if: matrix.tarantool == 'brew' - - - name: Install tarantool build dependencies - run: brew install autoconf automake libtool openssl@1.1 - if: matrix.tarantool != 'brew' && steps.cache.outputs.cache-hit != 'true' - - - name: Clone tarantool ${{ env.T_VERSION }} - uses: actions/checkout@v5 - with: - repository: tarantool/tarantool - ref: ${{ env.T_VERSION }} - path: ${{ env.T_TARDIR }} - submodules: true - # fetch-depth is 1 by default and it is okay for - # building from a tag. However we have master in - # the version list. - fetch-depth: 0 - if: matrix.tarantool != 'brew' && steps.cache.outputs.cache-hit != 'true' - - - name: Build tarantool ${{ env.T_VERSION }} from sources - run: | - cd "${T_TARDIR}" - # Set RelWithDebInfo just to disable -Werror. - # - # There are tarantool releases on which AppleClang - # complains about the problem that was fixed later in - # https://github.com/tarantool/tarantool/commit/7e8688ff8885cc7813d12225e03694eb8886de29 - # - # Set OpenSSL root directory for linking tarantool with OpenSSL of version 1.1 - # This is related to #49. There are too much deprecations which affect the build and tests. - # Must be revisited after fixing https://github.com/tarantool/tarantool/issues/6477 - cmake . -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_DIST=ON -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl@1.1 -DOPENSSL_LIBRARIES=/usr/local/opt/openssl@1.1/lib - # {{{ Workaround Mac OS build failure (gh-6076) - # - # https://github.com/tarantool/tarantool/issues/6076 - # - # In brief: when "src/lib/small" is in include paths, - # `#include ` from inside Mac OS SDK headers - # attempts to include "src/lib/small/VERSION" as a - # header file that leads to a syntax error. - # - # It was fixed in the following commits: - # - # * 1.10.10-24-g7bce4abd1 - # * 2.7.2-44-gbb1d32903 - # * 2.8.1-56-ga6c29c5af - # * 2.9.0-84-gc5ae543f3 - # - # However applying the workaround for all versions looks - # harmless. - # - # Added -f just in case: I guess we'll drop this useless - # obsoleted VERSION file from the git repository sooner - # or later. - rm -f src/lib/small/VERSION - # The same as above, but for the VERSION file generated - # by tarantool's CMake script. - rm VERSION - # }}} Workaround Mac OS build failure (gh-6076) - # Continue the build. - make -j$(sysctl -n hw.logicalcpu) - make install - if: matrix.tarantool != 'brew' && steps.cache.outputs.cache-hit != 'true' - - - name: Install tarantool - run: | - cd "${T_TARDIR}" - make install - if: matrix.tarantool != 'brew' && steps.cache.outputs.cache-hit == 'true' - - - name: Verify tarantool version - run: | - # Workaround https://github.com/tarantool/tarantool/issues/4983 - # Workaround https://github.com/tarantool/tarantool/issues/5040 - tarantool -e "require('fiber').sleep(0) assert(_TARANTOOL:startswith('${T_VERSION}'), _TARANTOOL) os.exit()" - if: matrix.tarantool != 'brew' && matrix.tarantool != 'master' - name: Setup golang for the connector and tests uses: actions/setup-go@v5 @@ -195,7 +111,6 @@ jobs: # Workaround issue https://github.com/tarantool/tt/issues/640 - name: Fix tt rocks - if: matrix.tarantool == 'brew' run: | brew ls --verbose tarantool | grep macosx.lua | xargs rm -f From a2c8f8ed090801982ee6387c8ee227d00a8bb52c Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 16 Oct 2025 13:51:00 +0300 Subject: [PATCH 592/605] Release v2.4.1 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e536967e..737db9029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +## [v2.4.1] - 2025-10-16 + +This maintenance release marks the end of active development on the `v2` +branch. + ## [v2.4.0] - 2025-07-11 This release focuses on adding schema/user/session operations, synchronous transaction From 7432972207b64f899a8a9a4d4daa49f7172421c1 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Fri, 5 Sep 2025 15:39:47 +0300 Subject: [PATCH 593/605] package: update go and package versions * Module version update from v2 to v3, * Go version dep updated from 1.20 to 1.24, * Added stub record for migration guide. --- .github/workflows/reusable_testing.yml | 2 +- .github/workflows/testing.yml | 12 +++++------ CHANGELOG.md | 2 ++ MIGRATION.md | 28 +++++++++++++++++--------- README.md | 20 +++++++++--------- arrow/arrow_test.go | 3 ++- arrow/example_test.go | 4 ++-- arrow/request.go | 3 ++- arrow/request_test.go | 5 +++-- arrow/tarantool_test.go | 6 +++--- auth_test.go | 2 +- box/box.go | 2 +- box/box_test.go | 5 +++-- box/example_test.go | 4 ++-- box/info.go | 3 ++- box/info_test.go | 2 +- box/schema.go | 4 +++- box/schema_user.go | 3 ++- box/schema_user_test.go | 4 ++-- box/session.go | 2 +- box/session_test.go | 4 ++-- box/tarantool_test.go | 7 ++++--- box_error_test.go | 4 ++-- client_tools_test.go | 2 +- crud/common.go | 2 +- crud/count.go | 4 ++-- crud/delete.go | 4 ++-- crud/error_test.go | 3 ++- crud/example_test.go | 8 ++++---- crud/get.go | 4 ++-- crud/insert.go | 6 +++--- crud/insert_many.go | 6 +++--- crud/len.go | 4 ++-- crud/max.go | 4 ++-- crud/min.go | 4 ++-- crud/operations_test.go | 2 +- crud/replace.go | 6 +++--- crud/replace_many.go | 6 +++--- crud/request_test.go | 4 ++-- crud/result_test.go | 3 ++- crud/schema.go | 2 +- crud/select.go | 4 ++-- crud/stats.go | 2 +- crud/storage_info.go | 4 ++-- crud/tarantool_test.go | 8 ++++---- crud/truncate.go | 4 ++-- crud/update.go | 4 ++-- crud/upsert.go | 6 +++--- crud/upsert_many.go | 10 ++++----- datetime/datetime_test.go | 6 +++--- datetime/example_test.go | 4 ++-- datetime/interval_test.go | 6 +++--- decimal/decimal_test.go | 6 +++--- decimal/example_test.go | 4 ++-- decimal/fuzzing_test.go | 3 ++- dial_test.go | 4 ++-- example_custom_unpacking_test.go | 4 ++-- example_test.go | 6 +++--- future_test.go | 4 ++-- go.mod | 4 ++-- golden_test.go | 2 +- pool/connection_pool.go | 2 +- pool/connection_pool_test.go | 10 ++++----- pool/connector.go | 2 +- pool/connector_test.go | 5 +++-- pool/example_test.go | 8 ++++---- pool/pooler.go | 2 +- pool/round_robin.go | 2 +- pool/round_robin_test.go | 2 +- pool/watcher.go | 2 +- protocol_test.go | 2 +- queue/example_connection_pool_test.go | 9 +++++---- queue/example_msgpack_test.go | 4 ++-- queue/example_test.go | 4 ++-- queue/queue.go | 2 +- queue/queue_test.go | 12 +++++------ request_test.go | 2 +- response_test.go | 3 ++- schema_test.go | 4 ++-- settings/example_test.go | 6 +++--- settings/request.go | 2 +- settings/request_test.go | 4 ++-- settings/tarantool_test.go | 7 ++++--- shutdown_test.go | 5 +++-- tarantool_test.go | 16 +++++++-------- test_helpers/doer.go | 2 +- test_helpers/example_test.go | 5 +++-- test_helpers/main.go | 2 +- test_helpers/pool_helper.go | 4 ++-- test_helpers/request.go | 2 +- test_helpers/response.go | 2 +- test_helpers/tcs/prepare.go | 4 ++-- test_helpers/tcs/tcs.go | 4 ++-- test_helpers/utils.go | 3 ++- testdata/sidecar/main.go | 2 +- uuid/example_test.go | 5 +++-- uuid/uuid_test.go | 6 +++--- 97 files changed, 245 insertions(+), 213 deletions(-) diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index 94c671543..beb4c84c8 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -36,7 +36,7 @@ jobs: - name: Setup golang for connector and tests uses: actions/setup-go@v5 with: - go-version: '1.20' + go-version: '1.24' - name: Setup tt run: | diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 2fce2f5ec..12e41dae6 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - golang: ['1.20', 'stable'] + golang: ['1.24', 'stable'] tarantool: ['1.10'] coveralls: [false] fuzzing: [false] @@ -36,19 +36,19 @@ jobs: strategy: fail-fast: false matrix: - golang: ['1.20', 'stable'] + golang: ['1.24', 'stable'] tarantool: ['2.11', '3.4', 'master'] coveralls: [false] fuzzing: [false] include: - - golang: '1.20' + - golang: '1.24' tarantool: 'master' coveralls: true fuzzing: false - - golang: '1.20' + - golang: '1.24' tarantool: 'master' - fuzzing: true coveralls: false + fuzzing: true uses: ./.github/workflows/reusable-run.yml with: os: ubuntu-24.04 @@ -72,7 +72,7 @@ jobs: fail-fast: false matrix: golang: - - '1.20' + - '1.24' - 'stable' runs-on: - macos-14 diff --git a/CHANGELOG.md b/CHANGELOG.md index 737db9029..41abba294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Changed +* Required Go version is `1.24` now. + ### Fixed ## [v2.4.1] - 2025-10-16 diff --git a/MIGRATION.md b/MIGRATION.md index fdd4893dc..54f4e3034 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,8 +1,18 @@ # Migration guide +## Migration from v2.x.x to v3.x.x + +* [Major changes](#major-changes-v3) + +TODO + +### Major changes + +* Required Go version is `1.24` now. + ## Migration from v1.x.x to v2.x.x -* [Major changes](#major-changes) +* [Major changes](#major-changes-v2) * [Main package](#main-package) * [Go version](#go-version) * [msgpack/v5](#msgpackv5) @@ -25,7 +35,7 @@ * [crud package](#crud-package) * [test_helpers package](#test_helpers-package) -### Major changes +### Major changes * The `go_tarantool_call_17` build tag is no longer needed, since by default the `CallRequest` is `Call17Request`. @@ -50,9 +60,9 @@ import ( "fmt" "github.com/tarantool/go-tarantool" - _ "github.com/tarantool/go-tarantool/v2/datetime" - _ "github.com/tarantool/go-tarantool/v2/decimal" - _ "github.com/tarantool/go-tarantool/v2/uuid" + _ "github.com/tarantool/go-tarantool/v3/datetime" + _ "github.com/tarantool/go-tarantool/v3/decimal" + _ "github.com/tarantool/go-tarantool/v3/uuid" ) func main() { @@ -82,10 +92,10 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" - _ "github.com/tarantool/go-tarantool/v2/datetime" - _ "github.com/tarantool/go-tarantool/v2/decimal" - _ "github.com/tarantool/go-tarantool/v2/uuid" + "github.com/tarantool/go-tarantool/v3" + _ "github.com/tarantool/go-tarantool/v3/datetime" + _ "github.com/tarantool/go-tarantool/v3/decimal" + _ "github.com/tarantool/go-tarantool/v3/uuid" ) func main() { diff --git a/README.md b/README.md index f5c533390..6950724ca 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,10 @@ faster than other packages according to public benchmarks. We assume that you have Tarantool version 1.10+ and a modern Linux or BSD operating system. -You need a current version of `go`, version 1.20 or later (use `go version` to +You need a current version of `go`, version 1.24 or later (use `go version` to check the version number). Do not use `gccgo-go`. -**Note:** If your `go` version is older than 1.20 or if `go` is not installed, +**Note:** If your `go` version is older than 1.24 or if `go` is not installed, download and run the latest tarball from [golang.org][golang-dl]. The package `go-tarantool` is located in [tarantool/go-tarantool][go-tarantool] @@ -98,10 +98,10 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" - _ "github.com/tarantool/go-tarantool/v2/datetime" - _ "github.com/tarantool/go-tarantool/v2/decimal" - _ "github.com/tarantool/go-tarantool/v2/uuid" + "github.com/tarantool/go-tarantool/v3" + _ "github.com/tarantool/go-tarantool/v3/datetime" + _ "github.com/tarantool/go-tarantool/v3/decimal" + _ "github.com/tarantool/go-tarantool/v3/uuid" ) func main() { @@ -184,10 +184,10 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" - _ "github.com/tarantool/go-tarantool/v2/datetime" - _ "github.com/tarantool/go-tarantool/v2/decimal" - _ "github.com/tarantool/go-tarantool/v2/uuid" + "github.com/tarantool/go-tarantool/v3" + _ "github.com/tarantool/go-tarantool/v3/datetime" + _ "github.com/tarantool/go-tarantool/v3/decimal" + _ "github.com/tarantool/go-tarantool/v3/uuid" "github.com/tarantool/go-tlsdialer" ) diff --git a/arrow/arrow_test.go b/arrow/arrow_test.go index 5e8b440c4..c3f40090c 100644 --- a/arrow/arrow_test.go +++ b/arrow/arrow_test.go @@ -6,8 +6,9 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/arrow" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3/arrow" ) var longArrow, _ = hex.DecodeString("ffffffff70000000040000009effffff0400010004000000" + diff --git a/arrow/example_test.go b/arrow/example_test.go index e85d195cd..3510a777d 100644 --- a/arrow/example_test.go +++ b/arrow/example_test.go @@ -15,8 +15,8 @@ import ( "log" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/arrow" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/arrow" ) var arrowBinData, _ = hex.DecodeString("ffffffff70000000040000009effffff0400010004000000" + diff --git a/arrow/request.go b/arrow/request.go index 332720d3e..82b55f399 100644 --- a/arrow/request.go +++ b/arrow/request.go @@ -5,8 +5,9 @@ import ( "io" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3" ) // InsertRequest helps you to create an insert request object for execution diff --git a/arrow/request_test.go b/arrow/request_test.go index fba6b5563..251b2b31d 100644 --- a/arrow/request_test.go +++ b/arrow/request_test.go @@ -8,9 +8,10 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/arrow" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/arrow" ) const validSpace uint32 = 1 // Any valid value != default. diff --git a/arrow/tarantool_test.go b/arrow/tarantool_test.go index 728b38f8b..cc2ad552e 100644 --- a/arrow/tarantool_test.go +++ b/arrow/tarantool_test.go @@ -11,9 +11,9 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/arrow" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/arrow" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var isArrowSupported = false diff --git a/auth_test.go b/auth_test.go index 712226d63..a9a0b34e9 100644 --- a/auth_test.go +++ b/auth_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - . "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v3" ) func TestAuth_String(t *testing.T) { diff --git a/box/box.go b/box/box.go index c9b0e71dd..a4cc1780e 100644 --- a/box/box.go +++ b/box/box.go @@ -1,7 +1,7 @@ package box import ( - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // Box is a helper that wraps box.* requests. diff --git a/box/box_test.go b/box/box_test.go index 0180473ed..59ceaf6e3 100644 --- a/box/box_test.go +++ b/box/box_test.go @@ -7,8 +7,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/box" - "github.com/tarantool/go-tarantool/v2/test_helpers" + + "github.com/tarantool/go-tarantool/v3/box" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) func TestNew(t *testing.T) { diff --git a/box/example_test.go b/box/example_test.go index eac3f5e1a..fa65189a8 100644 --- a/box/example_test.go +++ b/box/example_test.go @@ -15,8 +15,8 @@ import ( "log" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/box" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/box" ) func ExampleBox_Info() { diff --git a/box/info.go b/box/info.go index aa682822c..edd4894cd 100644 --- a/box/info.go +++ b/box/info.go @@ -3,8 +3,9 @@ package box import ( "fmt" - "github.com/tarantool/go-tarantool/v2" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3" ) var _ tarantool.Request = (*InfoRequest)(nil) diff --git a/box/info_test.go b/box/info_test.go index 2cf00f69b..d4e8e9539 100644 --- a/box/info_test.go +++ b/box/info_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2/box" + "github.com/tarantool/go-tarantool/v3/box" ) func TestInfo(t *testing.T) { diff --git a/box/schema.go b/box/schema.go index 25017e5a4..036c76c6d 100644 --- a/box/schema.go +++ b/box/schema.go @@ -1,6 +1,8 @@ package box -import "github.com/tarantool/go-tarantool/v2" +import ( + "github.com/tarantool/go-tarantool/v3" +) // Schema represents the schema-related operations in Tarantool. // It holds a connection to interact with the Tarantool instance. diff --git a/box/schema_user.go b/box/schema_user.go index 80874da92..6e9f558a5 100644 --- a/box/schema_user.go +++ b/box/schema_user.go @@ -5,8 +5,9 @@ import ( "fmt" "strings" - "github.com/tarantool/go-tarantool/v2" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3" ) // SchemaUser provides methods to interact with schema-related user operations in Tarantool. diff --git a/box/schema_user_test.go b/box/schema_user_test.go index b9f3e18de..985e9fe86 100644 --- a/box/schema_user_test.go +++ b/box/schema_user_test.go @@ -7,11 +7,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2" "github.com/vmihailenco/msgpack/v5" "github.com/vmihailenco/msgpack/v5/msgpcode" - "github.com/tarantool/go-tarantool/v2/box" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/box" ) func TestUserExistsResponse_DecodeMsgpack(t *testing.T) { diff --git a/box/session.go b/box/session.go index b63113581..5a01b32a3 100644 --- a/box/session.go +++ b/box/session.go @@ -3,7 +3,7 @@ package box import ( "context" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // Session struct represents a connection session to Tarantool. diff --git a/box/session_test.go b/box/session_test.go index 07ada0436..39b80d1d4 100644 --- a/box/session_test.go +++ b/box/session_test.go @@ -5,8 +5,8 @@ import ( "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/box" - th "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3/box" + th "github.com/tarantool/go-tarantool/v3/test_helpers" ) func TestBox_Session(t *testing.T) { diff --git a/box/tarantool_test.go b/box/tarantool_test.go index 0eb9e94b9..4244a98e9 100644 --- a/box/tarantool_test.go +++ b/box/tarantool_test.go @@ -11,9 +11,10 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/box" - "github.com/tarantool/go-tarantool/v2/test_helpers" + + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/box" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var server = "127.0.0.1:3013" diff --git a/box_error_test.go b/box_error_test.go index 3d7f7345d..acb051d31 100644 --- a/box_error_test.go +++ b/box_error_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/require" "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + . "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var samples = map[string]BoxError{ diff --git a/client_tools_test.go b/client_tools_test.go index fdd109152..fc911acf9 100644 --- a/client_tools_test.go +++ b/client_tools_test.go @@ -6,7 +6,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) func TestOperations_EncodeMsgpack(t *testing.T) { diff --git a/crud/common.go b/crud/common.go index 061818487..df3c4a795 100644 --- a/crud/common.go +++ b/crud/common.go @@ -59,7 +59,7 @@ import ( "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) type baseRequest struct { diff --git a/crud/count.go b/crud/count.go index 9deb1e44c..b90198658 100644 --- a/crud/count.go +++ b/crud/count.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // CountResult describes result for `crud.count` method. @@ -73,7 +73,7 @@ type CountRequest struct { } type countArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Conditions []Condition Opts CountOpts diff --git a/crud/delete.go b/crud/delete.go index f37da7ac5..075b25b3c 100644 --- a/crud/delete.go +++ b/crud/delete.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // DeleteOpts describes options for `crud.delete` method. @@ -20,7 +20,7 @@ type DeleteRequest struct { } type deleteArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Key Tuple Opts DeleteOpts diff --git a/crud/error_test.go b/crud/error_test.go index a3db035bf..71fda30d4 100644 --- a/crud/error_test.go +++ b/crud/error_test.go @@ -4,7 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/crud" + + "github.com/tarantool/go-tarantool/v3/crud" ) func TestErrorMany(t *testing.T) { diff --git a/crud/example_test.go b/crud/example_test.go index 7f9e34e0c..1b97308ae 100644 --- a/crud/example_test.go +++ b/crud/example_test.go @@ -6,8 +6,8 @@ import ( "reflect" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/crud" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/crud" ) const ( @@ -65,7 +65,7 @@ func ExampleResult_rowsCustomType() { Tuple([]interface{}{uint(2010), nil, "bla"}) type Tuple struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Id uint64 BucketId uint64 Name string @@ -92,7 +92,7 @@ func ExampleTuples_customType() { // The type will be encoded/decoded as an array. type Tuple struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Id uint64 BucketId *uint64 Name string diff --git a/crud/get.go b/crud/get.go index 6ab91440e..5a31473ef 100644 --- a/crud/get.go +++ b/crud/get.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // GetOpts describes options for `crud.get` method. @@ -66,7 +66,7 @@ type GetRequest struct { } type getArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Key Tuple Opts GetOpts diff --git a/crud/insert.go b/crud/insert.go index c696d201f..4e56c6d91 100644 --- a/crud/insert.go +++ b/crud/insert.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // InsertOpts describes options for `crud.insert` method. @@ -20,7 +20,7 @@ type InsertRequest struct { } type insertArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Tuple Tuple Opts InsertOpts @@ -78,7 +78,7 @@ type InsertObjectRequest struct { } type insertObjectArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Object Object Opts InsertObjectOpts diff --git a/crud/insert_many.go b/crud/insert_many.go index 866c1ceb5..17748564d 100644 --- a/crud/insert_many.go +++ b/crud/insert_many.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // InsertManyOpts describes options for `crud.insert_many` method. @@ -20,7 +20,7 @@ type InsertManyRequest struct { } type insertManyArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Tuples Tuples Opts InsertManyOpts @@ -78,7 +78,7 @@ type InsertObjectManyRequest struct { } type insertObjectManyArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Objects Objects Opts InsertObjectManyOpts diff --git a/crud/len.go b/crud/len.go index 5fef700d7..a1da72f72 100644 --- a/crud/len.go +++ b/crud/len.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // LenResult describes result for `crud.len` method. @@ -22,7 +22,7 @@ type LenRequest struct { } type lenArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Opts LenOpts } diff --git a/crud/max.go b/crud/max.go index 727a17ac5..961e7724b 100644 --- a/crud/max.go +++ b/crud/max.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // MaxOpts describes options for `crud.max` method. @@ -20,7 +20,7 @@ type MaxRequest struct { } type maxArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Index interface{} Opts MaxOpts diff --git a/crud/min.go b/crud/min.go index ab3bcfe07..2bbf9b816 100644 --- a/crud/min.go +++ b/crud/min.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // MinOpts describes options for `crud.min` method. @@ -20,7 +20,7 @@ type MinRequest struct { } type minArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Index interface{} Opts MinOpts diff --git a/crud/operations_test.go b/crud/operations_test.go index 0ff3e818a..a7f61a8a7 100644 --- a/crud/operations_test.go +++ b/crud/operations_test.go @@ -6,7 +6,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2/crud" + "github.com/tarantool/go-tarantool/v3/crud" ) func TestOperation_EncodeMsgpack(t *testing.T) { diff --git a/crud/replace.go b/crud/replace.go index 8231c9aa5..b47bba9ab 100644 --- a/crud/replace.go +++ b/crud/replace.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // ReplaceOpts describes options for `crud.replace` method. @@ -20,7 +20,7 @@ type ReplaceRequest struct { } type replaceArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Tuple Tuple Opts ReplaceOpts @@ -78,7 +78,7 @@ type ReplaceObjectRequest struct { } type replaceObjectArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Object Object Opts ReplaceObjectOpts diff --git a/crud/replace_many.go b/crud/replace_many.go index 5a5143ef8..024b863b7 100644 --- a/crud/replace_many.go +++ b/crud/replace_many.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // ReplaceManyOpts describes options for `crud.replace_many` method. @@ -20,7 +20,7 @@ type ReplaceManyRequest struct { } type replaceManyArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Tuples Tuples Opts ReplaceManyOpts @@ -78,7 +78,7 @@ type ReplaceObjectManyRequest struct { } type replaceObjectManyArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Objects Objects Opts ReplaceObjectManyOpts diff --git a/crud/request_test.go b/crud/request_test.go index 7c889cf40..ba2bae859 100644 --- a/crud/request_test.go +++ b/crud/request_test.go @@ -9,8 +9,8 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/crud" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/crud" ) const validSpace = "test" // Any valid value != default. diff --git a/crud/result_test.go b/crud/result_test.go index c67649f96..578eebed1 100644 --- a/crud/result_test.go +++ b/crud/result_test.go @@ -6,8 +6,9 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2/crud" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3/crud" ) func TestResult_DecodeMsgpack(t *testing.T) { diff --git a/crud/schema.go b/crud/schema.go index 6f3b94a97..4c2d661ec 100644 --- a/crud/schema.go +++ b/crud/schema.go @@ -7,7 +7,7 @@ import ( "github.com/vmihailenco/msgpack/v5" "github.com/vmihailenco/msgpack/v5/msgpcode" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) func msgpackIsMap(code byte) bool { diff --git a/crud/select.go b/crud/select.go index 24dbd0cb0..b52eb7003 100644 --- a/crud/select.go +++ b/crud/select.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // SelectOpts describes options for `crud.select` method. @@ -90,7 +90,7 @@ type SelectRequest struct { } type selectArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Conditions []Condition Opts SelectOpts diff --git a/crud/stats.go b/crud/stats.go index 47737f33a..c4f6988a0 100644 --- a/crud/stats.go +++ b/crud/stats.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // StatsRequest helps you to create request object to call `crud.stats` diff --git a/crud/storage_info.go b/crud/storage_info.go index e2d67aadb..b39bf37a5 100644 --- a/crud/storage_info.go +++ b/crud/storage_info.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // StatusTable describes information for instance. @@ -98,7 +98,7 @@ type StorageInfoRequest struct { } type storageInfoArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Opts StorageInfoOpts } diff --git a/crud/tarantool_test.go b/crud/tarantool_test.go index 8ee28cf09..0e1c1791a 100644 --- a/crud/tarantool_test.go +++ b/crud/tarantool_test.go @@ -10,9 +10,9 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/crud" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/crud" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var server = "127.0.0.1:3013" @@ -182,7 +182,7 @@ func connect(t testing.TB) *tarantool.Connection { } ret := struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Result bool }{} err = conn.Do(tarantool.NewCall17Request("is_ready")).GetTyped(&ret) diff --git a/crud/truncate.go b/crud/truncate.go index 9f80063d1..8313785d9 100644 --- a/crud/truncate.go +++ b/crud/truncate.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // TruncateResult describes result for `crud.truncate` method. @@ -22,7 +22,7 @@ type TruncateRequest struct { } type truncateArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Opts TruncateOpts } diff --git a/crud/update.go b/crud/update.go index 41ebd2c09..4bdeb01ce 100644 --- a/crud/update.go +++ b/crud/update.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // UpdateOpts describes options for `crud.update` method. @@ -21,7 +21,7 @@ type UpdateRequest struct { } type updateArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Key Tuple Operations []Operation diff --git a/crud/upsert.go b/crud/upsert.go index e44523d45..d55d1da1b 100644 --- a/crud/upsert.go +++ b/crud/upsert.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // UpsertOpts describes options for `crud.upsert` method. @@ -21,7 +21,7 @@ type UpsertRequest struct { } type upsertArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Tuple Tuple Operations []Operation @@ -90,7 +90,7 @@ type UpsertObjectRequest struct { } type upsertObjectArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string Object Object Operations []Operation diff --git a/crud/upsert_many.go b/crud/upsert_many.go index b0ccedf09..dad7dd158 100644 --- a/crud/upsert_many.go +++ b/crud/upsert_many.go @@ -5,7 +5,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // UpsertManyOpts describes options for `crud.upsert_many` method. @@ -13,7 +13,7 @@ type UpsertManyOpts = OperationManyOpts // TupleOperationsData contains tuple with operations to be applied to tuple. type TupleOperationsData struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Tuple Tuple Operations []Operation } @@ -27,7 +27,7 @@ type UpsertManyRequest struct { } type upsertManyArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string TuplesOperationsData []TupleOperationsData Opts UpsertManyOpts @@ -79,7 +79,7 @@ type UpsertObjectManyOpts = OperationManyOpts // ObjectOperationsData contains object with operations to be applied to object. type ObjectOperationsData struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Object Object Operations []Operation } @@ -93,7 +93,7 @@ type UpsertObjectManyRequest struct { } type upsertObjectManyArgs struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Space string ObjectsOperationsData []ObjectOperationsData Opts UpsertObjectManyOpts diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 8be50e0e7..d01153892 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -11,9 +11,9 @@ import ( "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/datetime" - "github.com/tarantool/go-tarantool/v2/test_helpers" + . "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/datetime" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var noTimezoneLoc = time.FixedZone(NoTimezone, 0) diff --git a/datetime/example_test.go b/datetime/example_test.go index ac5f40500..df5d55563 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -13,8 +13,8 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/datetime" + "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/datetime" ) // Example demonstrates how to use tuples with datetime. To enable support of diff --git a/datetime/interval_test.go b/datetime/interval_test.go index 95142fe47..2f4bb8a66 100644 --- a/datetime/interval_test.go +++ b/datetime/interval_test.go @@ -5,9 +5,9 @@ import ( "reflect" "testing" - "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/datetime" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/datetime" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) func TestIntervalAdd(t *testing.T) { diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 573daa8f6..f75494204 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -12,9 +12,9 @@ import ( "github.com/shopspring/decimal" "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/decimal" - "github.com/tarantool/go-tarantool/v2/test_helpers" + . "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/decimal" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var isDecimalSupported = false diff --git a/decimal/example_test.go b/decimal/example_test.go index 5597590dc..7903b077e 100644 --- a/decimal/example_test.go +++ b/decimal/example_test.go @@ -13,8 +13,8 @@ import ( "log" "time" - "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/decimal" + "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/decimal" ) // To enable support of decimal in msgpack with diff --git a/decimal/fuzzing_test.go b/decimal/fuzzing_test.go index 5ec2a2b8f..b6c49dcd9 100644 --- a/decimal/fuzzing_test.go +++ b/decimal/fuzzing_test.go @@ -7,7 +7,8 @@ import ( "testing" "github.com/shopspring/decimal" - . "github.com/tarantool/go-tarantool/v2/decimal" + + . "github.com/tarantool/go-tarantool/v3/decimal" ) func strToDecimal(t *testing.T, buf string, exp int) decimal.Decimal { diff --git a/dial_test.go b/dial_test.go index 87b9af5d8..2d0dee8ce 100644 --- a/dial_test.go +++ b/dial_test.go @@ -16,8 +16,8 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) type mockErrorDialer struct { diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index a2706f3f6..d8c790a25 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -8,7 +8,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) type Tuple2 struct { @@ -19,7 +19,7 @@ type Tuple2 struct { // Same effect in a "magic" way, but slower. type Tuple3 struct { - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Cid uint Orig string diff --git a/example_test.go b/example_test.go index 463f3289c..e1b9d5766 100644 --- a/example_test.go +++ b/example_test.go @@ -9,14 +9,14 @@ import ( "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) type Tuple struct { // Instruct msgpack to pack this struct as array, so no custom packer // is needed. - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Id uint Msg string Name string diff --git a/future_test.go b/future_test.go index fbb30fe62..fe13bc103 100644 --- a/future_test.go +++ b/future_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/tarantool/go-iproto" - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + . "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" "github.com/vmihailenco/msgpack/v5" ) diff --git a/go.mod b/go.mod index 5b44eb1c5..c1d57fa18 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module github.com/tarantool/go-tarantool/v2 +module github.com/tarantool/go-tarantool/v3 -go 1.20 +go 1.24 require ( github.com/google/uuid v1.3.0 diff --git a/golden_test.go b/golden_test.go index 271a98ce3..c2ee52f89 100644 --- a/golden_test.go +++ b/golden_test.go @@ -17,7 +17,7 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // golden_test.go contains tests that will check that the msgpack diff --git a/pool/connection_pool.go b/pool/connection_pool.go index e62cb2b3e..572fe6cde 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -20,7 +20,7 @@ import ( "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) var ( diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index 8120c613d..e0fcc8c47 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -15,12 +15,12 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/vmihailenco/msgpack/v5" - "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/pool" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/pool" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var user = "test" @@ -2572,7 +2572,7 @@ func TestSelect(t *testing.T) { err = test_helpers.InsertOnInstances(ctx, makeDialers(allServers), connOpts, spaceNo, anyTpl) require.Nil(t, err) - //default: ANY + // default: ANY data, err := connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, anyKey) require.Nilf(t, err, "failed to Select") require.NotNilf(t, data, "response is nil after Select") diff --git a/pool/connector.go b/pool/connector.go index 23cc7275d..391b83b75 100644 --- a/pool/connector.go +++ b/pool/connector.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // ConnectorAdapter allows to use Pooler as Connector. diff --git a/pool/connector_test.go b/pool/connector_test.go index 87bebbd53..9b8106c12 100644 --- a/pool/connector_test.go +++ b/pool/connector_test.go @@ -7,8 +7,9 @@ import ( "time" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/pool" + + "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/pool" ) var testMode Mode = RW diff --git a/pool/example_test.go b/pool/example_test.go index dce8bb1af..6cf339baf 100644 --- a/pool/example_test.go +++ b/pool/example_test.go @@ -6,15 +6,15 @@ import ( "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/pool" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/pool" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) type Tuple struct { // Instruct msgpack to pack this struct as array, so no custom packer // is needed. - _msgpack struct{} `msgpack:",asArray"` //nolint: structcheck,unused + _msgpack struct{} `msgpack:",asArray"` // nolint: structcheck,unused Key string Value string } diff --git a/pool/pooler.go b/pool/pooler.go index d4c0f1b5e..fd3df3401 100644 --- a/pool/pooler.go +++ b/pool/pooler.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // TopologyEditor is the interface that must be implemented by a connection pool. diff --git a/pool/round_robin.go b/pool/round_robin.go index 82cf26f39..f3ccb014c 100644 --- a/pool/round_robin.go +++ b/pool/round_robin.go @@ -4,7 +4,7 @@ import ( "sync" "sync/atomic" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) type roundRobinStrategy struct { diff --git a/pool/round_robin_test.go b/pool/round_robin_test.go index 6f028f2de..dcc219fd4 100644 --- a/pool/round_robin_test.go +++ b/pool/round_robin_test.go @@ -3,7 +3,7 @@ package pool import ( "testing" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) const ( diff --git a/pool/watcher.go b/pool/watcher.go index f7c08213e..aee3103fd 100644 --- a/pool/watcher.go +++ b/pool/watcher.go @@ -3,7 +3,7 @@ package pool import ( "sync" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // watcherContainer is a very simple implementation of a thread-safe container diff --git a/protocol_test.go b/protocol_test.go index 81ed2d3b5..c79a8afd3 100644 --- a/protocol_test.go +++ b/protocol_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - . "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v3" ) func TestProtocolInfoClonePreservesFeatures(t *testing.T) { diff --git a/queue/example_connection_pool_test.go b/queue/example_connection_pool_test.go index a126e13a1..8b5aab7cb 100644 --- a/queue/example_connection_pool_test.go +++ b/queue/example_connection_pool_test.go @@ -8,10 +8,11 @@ import ( "time" "github.com/google/uuid" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/pool" - "github.com/tarantool/go-tarantool/v2/queue" - "github.com/tarantool/go-tarantool/v2/test_helpers" + + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/pool" + "github.com/tarantool/go-tarantool/v3/queue" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) // QueueConnectionHandler handles new connections in a ConnectionPool. diff --git a/queue/example_msgpack_test.go b/queue/example_msgpack_test.go index cdd1a4e1c..53e54dc72 100644 --- a/queue/example_msgpack_test.go +++ b/queue/example_msgpack_test.go @@ -16,8 +16,8 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/queue" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/queue" ) type dummyData struct { diff --git a/queue/example_test.go b/queue/example_test.go index 1b411603a..99efa769b 100644 --- a/queue/example_test.go +++ b/queue/example_test.go @@ -13,8 +13,8 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/queue" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/queue" ) // Example demonstrates an operations like Put and Take with queue. diff --git a/queue/queue.go b/queue/queue.go index 99b1a722f..c8f968dff 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -15,7 +15,7 @@ import ( "github.com/google/uuid" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // Queue is a handle to Tarantool queue's tube. diff --git a/queue/queue_test.go b/queue/queue_test.go index 840c18b4f..81f768e18 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -10,9 +10,9 @@ import ( "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/queue" - "github.com/tarantool/go-tarantool/v2/test_helpers" + . "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/queue" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) const ( @@ -31,8 +31,8 @@ var dialer = NetDialer{ var opts = Opts{ Timeout: 5 * time.Second, - //Concurrency: 32, - //RateLimit: 4*1024, + // Concurrency: 32, + // RateLimit: 4*1024, } func createQueue(t *testing.T, conn *Connection, name string, cfg queue.Cfg) queue.Queue { @@ -54,7 +54,7 @@ func dropQueue(t *testing.T, q queue.Queue) { } } -/////////QUEUE///////// +// ///////QUEUE///////// func TestFifoQueue(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) diff --git a/request_test.go b/request_test.go index 43e22d311..fb4290299 100644 --- a/request_test.go +++ b/request_test.go @@ -12,7 +12,7 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" + . "github.com/tarantool/go-tarantool/v3" ) const invalidSpaceMsg = "invalid space" diff --git a/response_test.go b/response_test.go index 1edbf018e..e58b4d47c 100644 --- a/response_test.go +++ b/response_test.go @@ -7,8 +7,9 @@ import ( "github.com/stretchr/testify/require" "github.com/tarantool/go-iproto" - "github.com/tarantool/go-tarantool/v2" "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-tarantool/v3" ) func encodeResponseData(t *testing.T, data interface{}) io.Reader { diff --git a/schema_test.go b/schema_test.go index cbc863917..e30d52690 100644 --- a/schema_test.go +++ b/schema_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/require" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) func TestGetSchema_ok(t *testing.T) { diff --git a/settings/example_test.go b/settings/example_test.go index e51cadef0..fdad495f3 100644 --- a/settings/example_test.go +++ b/settings/example_test.go @@ -5,9 +5,9 @@ import ( "fmt" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/settings" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/settings" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var exampleDialer = tarantool.NetDialer{ diff --git a/settings/request.go b/settings/request.go index 1c106dc8d..10c6cac25 100644 --- a/settings/request.go +++ b/settings/request.go @@ -65,7 +65,7 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // SetRequest helps to set session settings. diff --git a/settings/request_test.go b/settings/request_test.go index b4c537a29..bb6c9e5c5 100644 --- a/settings/request_test.go +++ b/settings/request_test.go @@ -9,8 +9,8 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/settings" + "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/settings" ) type ValidSchemeResolver struct { diff --git a/settings/tarantool_test.go b/settings/tarantool_test.go index 56cee33ce..891959397 100644 --- a/settings/tarantool_test.go +++ b/settings/tarantool_test.go @@ -7,9 +7,10 @@ import ( "time" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2" - . "github.com/tarantool/go-tarantool/v2/settings" - "github.com/tarantool/go-tarantool/v2/test_helpers" + + "github.com/tarantool/go-tarantool/v3" + . "github.com/tarantool/go-tarantool/v3/settings" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) // There is no way to skip tests in testing.M, diff --git a/shutdown_test.go b/shutdown_test.go index 4df34aef5..434600824 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -13,8 +13,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + + . "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var shtdnServer = "127.0.0.1:3014" diff --git a/tarantool_test.go b/tarantool_test.go index 4902e96f4..437f7f96e 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -23,8 +23,8 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + . "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) var startOpts test_helpers.StartOpts = test_helpers.StartOpts{ @@ -89,8 +89,8 @@ var indexNo = uint32(0) var indexName = "primary" var opts = Opts{ Timeout: 5 * time.Second, - //Concurrency: 32, - //RateLimit: 4*1024, + // Concurrency: 32, + // RateLimit: 4*1024, } const N = 500 @@ -888,7 +888,7 @@ func TestFutureMultipleGetTypedWithError(t *testing.T) { } } -/////////////////// +// ///////////////// func TestClient(t *testing.T) { var err error @@ -2716,7 +2716,7 @@ func TestCallRequest(t *testing.T) { func TestClientRequestObjectsWithNilContext(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, dialer, opts) defer conn.Close() - req := NewPingRequest().Context(nil) //nolint + req := NewPingRequest().Context(nil) // nolint data, err := conn.Do(req).Get() if err != nil { t.Fatalf("Failed to Ping: %s", err) @@ -3269,7 +3269,7 @@ func TestClientIdRequestObjectWithNilContext(t *testing.T) { req := NewIdRequest(ProtocolInfo{ Version: ProtocolVersion(1), Features: []iproto.Feature{iproto.IPROTO_FEATURE_STREAMS}, - }).Context(nil) //nolint + }).Context(nil) // nolint data, err := conn.Do(req).Get() require.Nilf(t, err, "No errors on Id request execution") require.NotNilf(t, data, "Response data not empty") @@ -3293,7 +3293,7 @@ func TestClientIdRequestObjectWithPassedCanceledContext(t *testing.T) { req := NewIdRequest(ProtocolInfo{ Version: ProtocolVersion(1), Features: []iproto.Feature{iproto.IPROTO_FEATURE_STREAMS}, - }).Context(ctx) //nolint + }).Context(ctx) // nolint cancel() resp, err := conn.Do(req).Get() require.Nilf(t, resp, "Response is empty") diff --git a/test_helpers/doer.go b/test_helpers/doer.go index c33ff0e69..b61692c43 100644 --- a/test_helpers/doer.go +++ b/test_helpers/doer.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) type doerResponse struct { diff --git a/test_helpers/example_test.go b/test_helpers/example_test.go index 6272d737d..3b1ed5d64 100644 --- a/test_helpers/example_test.go +++ b/test_helpers/example_test.go @@ -5,8 +5,9 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) func TestExampleMockDoer(t *testing.T) { diff --git a/test_helpers/main.go b/test_helpers/main.go index 4ebe4c622..35c57f810 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -24,7 +24,7 @@ import ( "strconv" "time" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) type StartOpts struct { diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index 599e56594..7831b9f6e 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -8,8 +8,8 @@ import ( "sync" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/pool" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/pool" ) type ListenOnInstanceArgs struct { diff --git a/test_helpers/request.go b/test_helpers/request.go index 3756a2b54..003a97ab3 100644 --- a/test_helpers/request.go +++ b/test_helpers/request.go @@ -7,7 +7,7 @@ import ( "github.com/tarantool/go-iproto" "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // MockRequest is an empty mock request used for testing purposes. diff --git a/test_helpers/response.go b/test_helpers/response.go index f8757f563..630ac7726 100644 --- a/test_helpers/response.go +++ b/test_helpers/response.go @@ -7,7 +7,7 @@ import ( "github.com/vmihailenco/msgpack/v5" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) // MockResponse is a mock response used for testing purposes. diff --git a/test_helpers/tcs/prepare.go b/test_helpers/tcs/prepare.go index c7b87d0f6..92a13afb1 100644 --- a/test_helpers/tcs/prepare.go +++ b/test_helpers/tcs/prepare.go @@ -8,8 +8,8 @@ import ( "text/template" "time" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) const ( diff --git a/test_helpers/tcs/tcs.go b/test_helpers/tcs/tcs.go index 1ba26a19e..a54ba5fda 100644 --- a/test_helpers/tcs/tcs.go +++ b/test_helpers/tcs/tcs.go @@ -7,8 +7,8 @@ import ( "net" "testing" - "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" ) // ErrNotSupported identifies result of `Start()` why storage was not started. diff --git a/test_helpers/utils.go b/test_helpers/utils.go index 579f507c9..d844a822a 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -6,7 +6,8 @@ import ( "time" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool/v2" + + "github.com/tarantool/go-tarantool/v3" ) // ConnectWithValidation tries to connect to a Tarantool instance. diff --git a/testdata/sidecar/main.go b/testdata/sidecar/main.go index 971b8694c..a2c571b87 100644 --- a/testdata/sidecar/main.go +++ b/testdata/sidecar/main.go @@ -5,7 +5,7 @@ import ( "os" "strconv" - "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v3" ) func main() { diff --git a/uuid/example_test.go b/uuid/example_test.go index c79dc35be..8673d390c 100644 --- a/uuid/example_test.go +++ b/uuid/example_test.go @@ -15,8 +15,9 @@ import ( "time" "github.com/google/uuid" - "github.com/tarantool/go-tarantool/v2" - _ "github.com/tarantool/go-tarantool/v2/uuid" + + "github.com/tarantool/go-tarantool/v3" + _ "github.com/tarantool/go-tarantool/v3/uuid" ) var exampleOpts = tarantool.Opts{ diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go index 0ce317979..22ffd7eb5 100644 --- a/uuid/uuid_test.go +++ b/uuid/uuid_test.go @@ -10,9 +10,9 @@ import ( "github.com/google/uuid" "github.com/vmihailenco/msgpack/v5" - . "github.com/tarantool/go-tarantool/v2" - "github.com/tarantool/go-tarantool/v2/test_helpers" - _ "github.com/tarantool/go-tarantool/v2/uuid" + . "github.com/tarantool/go-tarantool/v3" + "github.com/tarantool/go-tarantool/v3/test_helpers" + _ "github.com/tarantool/go-tarantool/v3/uuid" ) // There is no way to skip tests in testing.M, From 1a38e95c1f4fffd64012dc8e6e0b4876870eae59 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Thu, 4 Sep 2025 12:08:27 +0300 Subject: [PATCH 594/605] feat(arrow, datetime, decimal, uuid): add support for go-option Add support for `go-option` to the `arrow`, `datetime`, `decimal`, and `uuid` packages. This allows for handling optional values of the `Arrow`, `Datetime`, `Interval`, `Decimal`, `UUID` and `BoxError` types. The following changes have been made: - Added `go:generate` directives to the `Arrow`, `Datetime`, `Interval`, `Decimal`, `UUID` and `BoxError` types to generate optional types using `github.com/tarantool/go-option/cmd/gentypes`. - Implemented `MarshalMsgpack` and `UnmarshalMsgpack` methods for the `Arrow`, `Datetime`, `Interval`, and `Decimal` types. - Added `marshalUUID` and `unmarshalUUID` functions for the `UUID` type. - The generated `_gen.go` files contain the `Optional*` types that wrap the original types and provide methods to handle optional values, including `EncodeMsgpack` and `DecodeMsgpack` for `msgpack` serialization. - Refactored the `datetime` and `decimal` decoders to use the new `UnmarshalMsgpack` methods. --- arrow/arrow.go | 13 ++ arrow/arrow_gen.go | 241 +++++++++++++++++++++++++ arrow/arrow_gen_test.go | 124 +++++++++++++ box_error.go => boxerror.go | 2 + boxerror_gen.go | 241 +++++++++++++++++++++++++ boxerror_gen_test.go | 116 ++++++++++++ box_error_test.go => boxerror_test.go | 0 datetime/datetime.go | 160 +++++++++-------- datetime/datetime_gen.go | 241 +++++++++++++++++++++++++ datetime/datetime_gen_test.go | 125 +++++++++++++ datetime/interval.go | 175 +++++++++++-------- datetime/interval_gen.go | 241 +++++++++++++++++++++++++ datetime/interval_gen_test.go | 116 ++++++++++++ decimal/decimal.go | 88 ++++++---- decimal/decimal_gen.go | 241 +++++++++++++++++++++++++ decimal/decimal_gen_test.go | 117 +++++++++++++ go.mod | 15 +- go.sum | 28 ++- pool/const.go | 2 +- uuid/uuid.go | 14 +- uuid/uuid_gen.go | 243 ++++++++++++++++++++++++++ uuid/uuid_gen_test.go | 117 +++++++++++++ 22 files changed, 2473 insertions(+), 187 deletions(-) create mode 100644 arrow/arrow_gen.go create mode 100644 arrow/arrow_gen_test.go rename box_error.go => boxerror.go (99%) create mode 100644 boxerror_gen.go create mode 100644 boxerror_gen_test.go rename box_error_test.go => boxerror_test.go (100%) create mode 100644 datetime/datetime_gen.go create mode 100644 datetime/datetime_gen_test.go create mode 100644 datetime/interval_gen.go create mode 100644 datetime/interval_gen_test.go create mode 100644 decimal/decimal_gen.go create mode 100644 decimal/decimal_gen_test.go create mode 100644 uuid/uuid_gen.go create mode 100644 uuid/uuid_gen_test.go diff --git a/arrow/arrow.go b/arrow/arrow.go index aaeaccca9..a7767c459 100644 --- a/arrow/arrow.go +++ b/arrow/arrow.go @@ -7,6 +7,8 @@ import ( "github.com/vmihailenco/msgpack/v5" ) +//go:generate go tool gentypes -ext-code 8 Arrow + // Arrow MessagePack extension type. const arrowExtId = 8 @@ -26,6 +28,17 @@ func (a Arrow) Raw() []byte { return a.data } +// MarshalMsgpack implements a custom msgpack marshaler for extension type. +func (a Arrow) MarshalMsgpack() ([]byte, error) { + return a.data, nil +} + +// UnmarshalMsgpack implements a custom msgpack unmarshaler for extension type. +func (a *Arrow) UnmarshalMsgpack(data []byte) error { + a.data = data + return nil +} + func arrowDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { arrow := Arrow{ data: make([]byte, extLen), diff --git a/arrow/arrow_gen.go b/arrow/arrow_gen.go new file mode 100644 index 000000000..c86c72778 --- /dev/null +++ b/arrow/arrow_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package arrow + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalArrow represents an optional value of type Arrow. +// It can either hold a valid Arrow (IsSome == true) or be empty (IsZero == true). +type OptionalArrow struct { + value Arrow + exists bool +} + +// SomeOptionalArrow creates an optional OptionalArrow with the given Arrow value. +// The returned OptionalArrow will have IsSome() == true and IsZero() == false. +func SomeOptionalArrow(value Arrow) OptionalArrow { + return OptionalArrow{ + value: value, + exists: true, + } +} + +// NoneOptionalArrow creates an empty optional OptionalArrow value. +// The returned OptionalArrow will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalArrow() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalArrow() OptionalArrow { + return OptionalArrow{} +} + +func (o OptionalArrow) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalArrow", + Parent: err, + } +} + +func (o OptionalArrow) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalArrow", + Parent: err, + } +} + +// IsSome returns true if the OptionalArrow contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalArrow) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalArrow does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o OptionalArrow) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalArrow) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of Arrow, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalArrow) Get() (Arrow, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o OptionalArrow) MustGet() Arrow { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for Arrow. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalArrow) Unwrap() Arrow { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalArrow() +// v := o.UnwrapOr(someDefaultOptionalArrow) +func (o OptionalArrow) UnwrapOr(defaultValue Arrow) Arrow { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneOptionalArrow() +// v := o.UnwrapOrElse(func() Arrow { return computeDefault() }) +func (o OptionalArrow) UnwrapOrElse(defaultValue func() Arrow) Arrow { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalArrow) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(8, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalArrow value using MessagePack format. +// - If the value is present, it is encoded as Arrow. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalArrow) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalArrow) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 8: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalArrow) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalArrow value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalArrow) +// - Arrow: interpreted as a present value (SomeOptionalArrow) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on Arrow: exists = true, value = decoded value +func (o *OptionalArrow) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/arrow/arrow_gen_test.go b/arrow/arrow_gen_test.go new file mode 100644 index 000000000..d990499f4 --- /dev/null +++ b/arrow/arrow_gen_test.go @@ -0,0 +1,124 @@ +package arrow + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalArrow(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + opt := SomeOptionalArrow(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalArrow(t *testing.T) { + opt := NoneOptionalArrow() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalArrow_MustGet(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + optSome := SomeOptionalArrow(val) + optNone := NoneOptionalArrow() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalArrow_Unwrap(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + optSome := SomeOptionalArrow(val) + optNone := NoneOptionalArrow() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, Arrow{}, optNone.Unwrap()) +} + +func TestOptionalArrow_UnwrapOr(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + def, err := MakeArrow([]byte{4, 5, 6}) + assert.NoError(t, err) + optSome := SomeOptionalArrow(val) + optNone := NoneOptionalArrow() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalArrow_UnwrapOrElse(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + def, err := MakeArrow([]byte{4, 5, 6}) + assert.NoError(t, err) + optSome := SomeOptionalArrow(val) + optNone := NoneOptionalArrow() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() Arrow { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() Arrow { return def })) +} + +func TestOptionalArrow_EncodeDecodeMsgpack_Some(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + some := SomeOptionalArrow(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err = enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalArrow + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalArrow_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalArrow() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalArrow + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalArrow_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalArrow + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/box_error.go b/boxerror.go similarity index 99% rename from box_error.go rename to boxerror.go index 59bfb4a05..2fb18268f 100644 --- a/box_error.go +++ b/boxerror.go @@ -9,6 +9,8 @@ import ( const errorExtID = 3 +//go:generate go tool gentypes -ext-code 3 BoxError + const ( keyErrorStack = 0x00 keyErrorType = 0x00 diff --git a/boxerror_gen.go b/boxerror_gen.go new file mode 100644 index 000000000..07a1be695 --- /dev/null +++ b/boxerror_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package tarantool + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalBoxError represents an optional value of type BoxError. +// It can either hold a valid BoxError (IsSome == true) or be empty (IsZero == true). +type OptionalBoxError struct { + value BoxError + exists bool +} + +// SomeOptionalBoxError creates an optional OptionalBoxError with the given BoxError value. +// The returned OptionalBoxError will have IsSome() == true and IsZero() == false. +func SomeOptionalBoxError(value BoxError) OptionalBoxError { + return OptionalBoxError{ + value: value, + exists: true, + } +} + +// NoneOptionalBoxError creates an empty optional OptionalBoxError value. +// The returned OptionalBoxError will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalBoxError() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalBoxError() OptionalBoxError { + return OptionalBoxError{} +} + +func (o OptionalBoxError) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalBoxError", + Parent: err, + } +} + +func (o OptionalBoxError) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalBoxError", + Parent: err, + } +} + +// IsSome returns true if the OptionalBoxError contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalBoxError) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalBoxError does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o OptionalBoxError) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalBoxError) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of BoxError, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalBoxError) Get() (BoxError, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o OptionalBoxError) MustGet() BoxError { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for BoxError. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalBoxError) Unwrap() BoxError { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalBoxError() +// v := o.UnwrapOr(someDefaultOptionalBoxError) +func (o OptionalBoxError) UnwrapOr(defaultValue BoxError) BoxError { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneOptionalBoxError() +// v := o.UnwrapOrElse(func() BoxError { return computeDefault() }) +func (o OptionalBoxError) UnwrapOrElse(defaultValue func() BoxError) BoxError { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalBoxError) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(3, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalBoxError value using MessagePack format. +// - If the value is present, it is encoded as BoxError. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalBoxError) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalBoxError) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 3: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalBoxError) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalBoxError value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalBoxError) +// - BoxError: interpreted as a present value (SomeOptionalBoxError) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on BoxError: exists = true, value = decoded value +func (o *OptionalBoxError) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/boxerror_gen_test.go b/boxerror_gen_test.go new file mode 100644 index 000000000..4d7fada3a --- /dev/null +++ b/boxerror_gen_test.go @@ -0,0 +1,116 @@ +package tarantool + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalBoxError(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + opt := SomeOptionalBoxError(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalBoxError(t *testing.T) { + opt := NoneOptionalBoxError() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalBoxError_MustGet(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + optSome := SomeOptionalBoxError(val) + optNone := NoneOptionalBoxError() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalBoxError_Unwrap(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + optSome := SomeOptionalBoxError(val) + optNone := NoneOptionalBoxError() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, BoxError{}, optNone.Unwrap()) +} + +func TestOptionalBoxError_UnwrapOr(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + def := BoxError{Code: 2, Msg: "default"} + optSome := SomeOptionalBoxError(val) + optNone := NoneOptionalBoxError() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalBoxError_UnwrapOrElse(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + def := BoxError{Code: 2, Msg: "default"} + optSome := SomeOptionalBoxError(val) + optNone := NoneOptionalBoxError() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() BoxError { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() BoxError { return def })) +} + +func TestOptionalBoxError_EncodeDecodeMsgpack_Some(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + some := SomeOptionalBoxError(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalBoxError + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalBoxError_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalBoxError() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalBoxError + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalBoxError_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalBoxError + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/box_error_test.go b/boxerror_test.go similarity index 100% rename from box_error_test.go rename to boxerror_test.go diff --git a/datetime/datetime.go b/datetime/datetime.go index f5a2a8278..23901305b 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -76,6 +76,7 @@ const ( const maxSize = secondsSize + nsecSize + tzIndexSize + tzOffsetSize +//go:generate go tool gentypes -ext-code 4 Datetime type Datetime struct { time time.Time } @@ -183,6 +184,89 @@ func addMonth(ival Interval, delta int64, adjust Adjust) Interval { return ival } +// MarshalMsgpack implements a custom msgpack marshaler. +func (d Datetime) MarshalMsgpack() ([]byte, error) { + tm := d.ToTime() + + var dt datetime + dt.seconds = tm.Unix() + dt.nsec = int32(tm.Nanosecond()) + + zone := tm.Location().String() + _, offset := tm.Zone() + if zone != NoTimezone { + // The zone value already checked in MakeDatetime() or + // UnmarshalMsgpack() calls. + dt.tzIndex = int16(timezoneToIndex[zone]) + } + dt.tzOffset = int16(offset / 60) + + var bytesSize = secondsSize + if dt.nsec != 0 || dt.tzOffset != 0 || dt.tzIndex != 0 { + bytesSize += nsecSize + tzIndexSize + tzOffsetSize + } + + buf := make([]byte, bytesSize) + binary.LittleEndian.PutUint64(buf, uint64(dt.seconds)) + if bytesSize == maxSize { + binary.LittleEndian.PutUint32(buf[secondsSize:], uint32(dt.nsec)) + binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize:], uint16(dt.tzOffset)) + binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize+tzOffsetSize:], uint16(dt.tzIndex)) + } + + return buf, nil +} + +// UnmarshalMsgpack implements a custom msgpack unmarshaler. +func (d *Datetime) UnmarshalMsgpack(data []byte) error { + var dt datetime + + sec := binary.LittleEndian.Uint64(data) + dt.seconds = int64(sec) + dt.nsec = 0 + if len(data) == maxSize { + dt.nsec = int32(binary.LittleEndian.Uint32(data[secondsSize:])) + dt.tzOffset = int16(binary.LittleEndian.Uint16(data[secondsSize+nsecSize:])) + dt.tzIndex = int16(binary.LittleEndian.Uint16(data[secondsSize+nsecSize+tzOffsetSize:])) + } + + tt := time.Unix(dt.seconds, int64(dt.nsec)) + + loc := noTimezoneLoc + if dt.tzIndex != 0 || dt.tzOffset != 0 { + zone := NoTimezone + offset := int(dt.tzOffset) * 60 + + if dt.tzIndex != 0 { + if _, ok := indexToTimezone[int(dt.tzIndex)]; !ok { + return fmt.Errorf("unknown timezone index %d", dt.tzIndex) + } + zone = indexToTimezone[int(dt.tzIndex)] + } + if zone != NoTimezone { + if loadLoc, err := time.LoadLocation(zone); err == nil { + loc = loadLoc + } else { + // Unable to load location. + loc = time.FixedZone(zone, offset) + } + } else { + // Only offset. + loc = time.FixedZone(zone, offset) + } + } + tt = tt.In(loc) + + newDatetime, err := MakeDatetime(tt) + if err != nil { + return err + } + + *d = newDatetime + + return nil +} + func (d Datetime) add(ival Interval, positive bool) (Datetime, error) { newVal := intervalFromDatetime(d) @@ -244,35 +328,8 @@ func (d *Datetime) ToTime() time.Time { func datetimeEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { dtime := v.Interface().(Datetime) - tm := dtime.ToTime() - - var dt datetime - dt.seconds = tm.Unix() - dt.nsec = int32(tm.Nanosecond()) - - zone := tm.Location().String() - _, offset := tm.Zone() - if zone != NoTimezone { - // The zone value already checked in MakeDatetime() or - // UnmarshalMsgpack() calls. - dt.tzIndex = int16(timezoneToIndex[zone]) - } - dt.tzOffset = int16(offset / 60) - var bytesSize = secondsSize - if dt.nsec != 0 || dt.tzOffset != 0 || dt.tzIndex != 0 { - bytesSize += nsecSize + tzIndexSize + tzOffsetSize - } - - buf := make([]byte, bytesSize) - binary.LittleEndian.PutUint64(buf, uint64(dt.seconds)) - if bytesSize == maxSize { - binary.LittleEndian.PutUint32(buf[secondsSize:], uint32(dt.nsec)) - binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize:], uint16(dt.tzOffset)) - binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize+tzOffsetSize:], uint16(dt.tzIndex)) - } - - return buf, nil + return dtime.MarshalMsgpack() } func datetimeDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { @@ -282,54 +339,15 @@ func datetimeDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { } b := make([]byte, extLen) - n, err := d.Buffered().Read(b) - if err != nil { + switch n, err := d.Buffered().Read(b); { + case err != nil: return err - } - if n < extLen { + case n < extLen: return fmt.Errorf("msgpack: unexpected end of stream after %d datetime bytes", n) } - var dt datetime - sec := binary.LittleEndian.Uint64(b) - dt.seconds = int64(sec) - dt.nsec = 0 - if extLen == maxSize { - dt.nsec = int32(binary.LittleEndian.Uint32(b[secondsSize:])) - dt.tzOffset = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize:])) - dt.tzIndex = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize+tzOffsetSize:])) - } - - tt := time.Unix(dt.seconds, int64(dt.nsec)) - - loc := noTimezoneLoc - if dt.tzIndex != 0 || dt.tzOffset != 0 { - zone := NoTimezone - offset := int(dt.tzOffset) * 60 - - if dt.tzIndex != 0 { - if _, ok := indexToTimezone[int(dt.tzIndex)]; !ok { - return fmt.Errorf("unknown timezone index %d", dt.tzIndex) - } - zone = indexToTimezone[int(dt.tzIndex)] - } - if zone != NoTimezone { - if loadLoc, err := time.LoadLocation(zone); err == nil { - loc = loadLoc - } else { - // Unable to load location. - loc = time.FixedZone(zone, offset) - } - } else { - // Only offset. - loc = time.FixedZone(zone, offset) - } - } - tt = tt.In(loc) - ptr := v.Addr().Interface().(*Datetime) - *ptr, err = MakeDatetime(tt) - return err + return ptr.UnmarshalMsgpack(b) } func init() { diff --git a/datetime/datetime_gen.go b/datetime/datetime_gen.go new file mode 100644 index 000000000..753d9c371 --- /dev/null +++ b/datetime/datetime_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package datetime + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalDatetime represents an optional value of type Datetime. +// It can either hold a valid Datetime (IsSome == true) or be empty (IsZero == true). +type OptionalDatetime struct { + value Datetime + exists bool +} + +// SomeOptionalDatetime creates an optional OptionalDatetime with the given Datetime value. +// The returned OptionalDatetime will have IsSome() == true and IsZero() == false. +func SomeOptionalDatetime(value Datetime) OptionalDatetime { + return OptionalDatetime{ + value: value, + exists: true, + } +} + +// NoneOptionalDatetime creates an empty optional OptionalDatetime value. +// The returned OptionalDatetime will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalDatetime() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalDatetime() OptionalDatetime { + return OptionalDatetime{} +} + +func (o OptionalDatetime) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalDatetime", + Parent: err, + } +} + +func (o OptionalDatetime) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalDatetime", + Parent: err, + } +} + +// IsSome returns true if the OptionalDatetime contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalDatetime) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalDatetime does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o OptionalDatetime) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalDatetime) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of Datetime, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalDatetime) Get() (Datetime, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o OptionalDatetime) MustGet() Datetime { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for Datetime. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalDatetime) Unwrap() Datetime { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalDatetime() +// v := o.UnwrapOr(someDefaultOptionalDatetime) +func (o OptionalDatetime) UnwrapOr(defaultValue Datetime) Datetime { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneOptionalDatetime() +// v := o.UnwrapOrElse(func() Datetime { return computeDefault() }) +func (o OptionalDatetime) UnwrapOrElse(defaultValue func() Datetime) Datetime { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalDatetime) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(4, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalDatetime value using MessagePack format. +// - If the value is present, it is encoded as Datetime. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalDatetime) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalDatetime) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 4: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalDatetime) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalDatetime value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalDatetime) +// - Datetime: interpreted as a present value (SomeOptionalDatetime) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on Datetime: exists = true, value = decoded value +func (o *OptionalDatetime) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/datetime/datetime_gen_test.go b/datetime/datetime_gen_test.go new file mode 100644 index 000000000..6b45b6fb1 --- /dev/null +++ b/datetime/datetime_gen_test.go @@ -0,0 +1,125 @@ +package datetime + +import ( + "bytes" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalDatetime(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + opt := SomeOptionalDatetime(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalDatetime(t *testing.T) { + opt := NoneOptionalDatetime() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalDatetime_MustGet(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + optSome := SomeOptionalDatetime(val) + optNone := NoneOptionalDatetime() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalDatetime_Unwrap(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + optSome := SomeOptionalDatetime(val) + optNone := NoneOptionalDatetime() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, Datetime{}, optNone.Unwrap()) +} + +func TestOptionalDatetime_UnwrapOr(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + def, err := MakeDatetime(time.Now().Add(1 * time.Hour).In(time.UTC)) + assert.NoError(t, err) + optSome := SomeOptionalDatetime(val) + optNone := NoneOptionalDatetime() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalDatetime_UnwrapOrElse(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + def, err := MakeDatetime(time.Now().Add(1 * time.Hour).In(time.UTC)) + assert.NoError(t, err) + optSome := SomeOptionalDatetime(val) + optNone := NoneOptionalDatetime() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() Datetime { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() Datetime { return def })) +} + +func TestOptionalDatetime_EncodeDecodeMsgpack_Some(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + some := SomeOptionalDatetime(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err = enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalDatetime + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalDatetime_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalDatetime() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalDatetime + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalDatetime_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalDatetime + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/datetime/interval.go b/datetime/interval.go index bcd052383..e6d39e4d8 100644 --- a/datetime/interval.go +++ b/datetime/interval.go @@ -2,7 +2,6 @@ package datetime import ( "bytes" - "fmt" "reflect" "github.com/vmihailenco/msgpack/v5" @@ -23,6 +22,8 @@ const ( ) // Interval type is GoLang implementation of Tarantool intervals. +// +//go:generate go tool gentypes -ext-code 6 Interval type Interval struct { Year int64 Month int64 @@ -35,6 +36,21 @@ type Interval struct { Adjust Adjust } +func (ival Interval) countNonZeroFields() int { + count := 0 + + for _, field := range []int64{ + ival.Year, ival.Month, ival.Week, ival.Day, ival.Hour, + ival.Min, ival.Sec, ival.Nsec, adjustToDt[ival.Adjust], + } { + if field != 0 { + count++ + } + } + + return count +} + // We use int64 for every field to avoid changes in the future, see: // https://github.com/tarantool/tarantool/blob/943ce3caf8401510ced4f074bca7006c3d73f9b3/src/lib/core/datetime.h#L106 @@ -66,115 +82,134 @@ func (ival Interval) Sub(sub Interval) Interval { return ival } -func encodeIntervalValue(e *msgpack.Encoder, typ uint64, value int64) (err error) { - if value == 0 { - return - } - err = e.EncodeUint(typ) - if err == nil { - if value > 0 { - err = e.EncodeUint(uint64(value)) - } else if value < 0 { - err = e.EncodeInt(value) - } +// MarshalMsgpack implements a custom msgpack marshaler. +func (ival Interval) MarshalMsgpack() ([]byte, error) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + + if err := ival.MarshalMsgpackTo(enc); err != nil { + return nil, err } - return -} -func encodeInterval(e *msgpack.Encoder, v reflect.Value) (err error) { - val := v.Interface().(Interval) + return buf.Bytes(), nil +} - var fieldNum uint64 - for _, val := range []int64{val.Year, val.Month, val.Week, val.Day, - val.Hour, val.Min, val.Sec, val.Nsec, - adjustToDt[val.Adjust]} { - if val != 0 { - fieldNum++ - } - } - if err = e.EncodeUint(fieldNum); err != nil { - return +// MarshalMsgpackTo implements a custom msgpack marshaler. +func (ival Interval) MarshalMsgpackTo(e *msgpack.Encoder) error { + var fieldNum = uint64(ival.countNonZeroFields()) + if err := e.EncodeUint(fieldNum); err != nil { + return err } - if err = encodeIntervalValue(e, fieldYear, val.Year); err != nil { - return + if err := encodeIntervalValue(e, fieldYear, ival.Year); err != nil { + return err } - if err = encodeIntervalValue(e, fieldMonth, val.Month); err != nil { - return + if err := encodeIntervalValue(e, fieldMonth, ival.Month); err != nil { + return err } - if err = encodeIntervalValue(e, fieldWeek, val.Week); err != nil { - return + if err := encodeIntervalValue(e, fieldWeek, ival.Week); err != nil { + return err } - if err = encodeIntervalValue(e, fieldDay, val.Day); err != nil { - return + if err := encodeIntervalValue(e, fieldDay, ival.Day); err != nil { + return err } - if err = encodeIntervalValue(e, fieldHour, val.Hour); err != nil { - return + if err := encodeIntervalValue(e, fieldHour, ival.Hour); err != nil { + return err } - if err = encodeIntervalValue(e, fieldMin, val.Min); err != nil { - return + if err := encodeIntervalValue(e, fieldMin, ival.Min); err != nil { + return err } - if err = encodeIntervalValue(e, fieldSec, val.Sec); err != nil { - return + if err := encodeIntervalValue(e, fieldSec, ival.Sec); err != nil { + return err } - if err = encodeIntervalValue(e, fieldNSec, val.Nsec); err != nil { - return + if err := encodeIntervalValue(e, fieldNSec, ival.Nsec); err != nil { + return err } - if err = encodeIntervalValue(e, fieldAdjust, adjustToDt[val.Adjust]); err != nil { - return + if err := encodeIntervalValue(e, fieldAdjust, adjustToDt[ival.Adjust]); err != nil { + return err } + return nil } -func decodeInterval(d *msgpack.Decoder, v reflect.Value) (err error) { - var fieldNum uint - if fieldNum, err = d.DecodeUint(); err != nil { - return +// UnmarshalMsgpackFrom implements a custom msgpack unmarshaler. +func (ival *Interval) UnmarshalMsgpackFrom(d *msgpack.Decoder) error { + fieldNum, err := d.DecodeUint() + if err != nil { + return err } - var val Interval + ival.Adjust = dtToAdjust[int64(NoneAdjust)] - hasAdjust := false for i := 0; i < int(fieldNum); i++ { var fieldType uint if fieldType, err = d.DecodeUint(); err != nil { - return + return err } + var fieldVal int64 if fieldVal, err = d.DecodeInt64(); err != nil { - return + return err } + switch fieldType { case fieldYear: - val.Year = fieldVal + ival.Year = fieldVal case fieldMonth: - val.Month = fieldVal + ival.Month = fieldVal case fieldWeek: - val.Week = fieldVal + ival.Week = fieldVal case fieldDay: - val.Day = fieldVal + ival.Day = fieldVal case fieldHour: - val.Hour = fieldVal + ival.Hour = fieldVal case fieldMin: - val.Min = fieldVal + ival.Min = fieldVal case fieldSec: - val.Sec = fieldVal + ival.Sec = fieldVal case fieldNSec: - val.Nsec = fieldVal + ival.Nsec = fieldVal case fieldAdjust: - hasAdjust = true - if adjust, ok := dtToAdjust[fieldVal]; ok { - val.Adjust = adjust - } else { - return fmt.Errorf("unsupported Adjust: %d", fieldVal) - } - default: - return fmt.Errorf("unsupported interval field type: %d", fieldType) + ival.Adjust = dtToAdjust[fieldVal] } } - if !hasAdjust { - val.Adjust = dtToAdjust[0] + return nil +} + +// UnmarshalMsgpack implements a custom msgpack unmarshaler. +func (ival *Interval) UnmarshalMsgpack(data []byte) error { + dec := msgpack.NewDecoder(bytes.NewReader(data)) + return ival.UnmarshalMsgpackFrom(dec) +} + +func encodeIntervalValue(e *msgpack.Encoder, typ uint64, value int64) error { + if value == 0 { + return nil + } + + err := e.EncodeUint(typ) + if err != nil { + return err + } + + switch { + case value > 0: + return e.EncodeUint(uint64(value)) + default: + return e.EncodeInt(value) + } +} + +func encodeInterval(e *msgpack.Encoder, v reflect.Value) (err error) { + val := v.Interface().(Interval) + return val.MarshalMsgpackTo(e) +} + +func decodeInterval(d *msgpack.Decoder, v reflect.Value) (err error) { + val := Interval{} + if err = val.UnmarshalMsgpackFrom(d); err != nil { + return } v.Set(reflect.ValueOf(val)) diff --git a/datetime/interval_gen.go b/datetime/interval_gen.go new file mode 100644 index 000000000..2cccaaca0 --- /dev/null +++ b/datetime/interval_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package datetime + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalInterval represents an optional value of type Interval. +// It can either hold a valid Interval (IsSome == true) or be empty (IsZero == true). +type OptionalInterval struct { + value Interval + exists bool +} + +// SomeOptionalInterval creates an optional OptionalInterval with the given Interval value. +// The returned OptionalInterval will have IsSome() == true and IsZero() == false. +func SomeOptionalInterval(value Interval) OptionalInterval { + return OptionalInterval{ + value: value, + exists: true, + } +} + +// NoneOptionalInterval creates an empty optional OptionalInterval value. +// The returned OptionalInterval will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalInterval() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalInterval() OptionalInterval { + return OptionalInterval{} +} + +func (o OptionalInterval) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalInterval", + Parent: err, + } +} + +func (o OptionalInterval) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalInterval", + Parent: err, + } +} + +// IsSome returns true if the OptionalInterval contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalInterval) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalInterval does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o OptionalInterval) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalInterval) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of Interval, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalInterval) Get() (Interval, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o OptionalInterval) MustGet() Interval { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for Interval. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalInterval) Unwrap() Interval { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalInterval() +// v := o.UnwrapOr(someDefaultOptionalInterval) +func (o OptionalInterval) UnwrapOr(defaultValue Interval) Interval { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneOptionalInterval() +// v := o.UnwrapOrElse(func() Interval { return computeDefault() }) +func (o OptionalInterval) UnwrapOrElse(defaultValue func() Interval) Interval { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalInterval) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(6, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalInterval value using MessagePack format. +// - If the value is present, it is encoded as Interval. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalInterval) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalInterval) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 6: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalInterval) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalInterval value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalInterval) +// - Interval: interpreted as a present value (SomeOptionalInterval) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on Interval: exists = true, value = decoded value +func (o *OptionalInterval) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/datetime/interval_gen_test.go b/datetime/interval_gen_test.go new file mode 100644 index 000000000..162db3336 --- /dev/null +++ b/datetime/interval_gen_test.go @@ -0,0 +1,116 @@ +package datetime + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalInterval(t *testing.T) { + val := Interval{Year: 1} + opt := SomeOptionalInterval(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalInterval(t *testing.T) { + opt := NoneOptionalInterval() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalInterval_MustGet(t *testing.T) { + val := Interval{Year: 1} + optSome := SomeOptionalInterval(val) + optNone := NoneOptionalInterval() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalInterval_Unwrap(t *testing.T) { + val := Interval{Year: 1} + optSome := SomeOptionalInterval(val) + optNone := NoneOptionalInterval() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, Interval{}, optNone.Unwrap()) +} + +func TestOptionalInterval_UnwrapOr(t *testing.T) { + val := Interval{Year: 1} + def := Interval{Year: 2} + optSome := SomeOptionalInterval(val) + optNone := NoneOptionalInterval() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalInterval_UnwrapOrElse(t *testing.T) { + val := Interval{Year: 1} + def := Interval{Year: 2} + optSome := SomeOptionalInterval(val) + optNone := NoneOptionalInterval() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() Interval { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() Interval { return def })) +} + +func TestOptionalInterval_EncodeDecodeMsgpack_Some(t *testing.T) { + val := Interval{Year: 1} + some := SomeOptionalInterval(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalInterval + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalInterval_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalInterval() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalInterval + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalInterval_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalInterval + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/decimal/decimal.go b/decimal/decimal.go index 3a1abb76e..3c2681238 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -44,13 +44,14 @@ const ( ) var ( - one decimal.Decimal = decimal.NewFromInt(1) + one = decimal.NewFromInt(1) // -10^decimalPrecision - 1 - minSupportedDecimal decimal.Decimal = maxSupportedDecimal.Neg().Sub(one) + minSupportedDecimal = maxSupportedDecimal.Neg().Sub(one) // 10^decimalPrecision - 1 - maxSupportedDecimal decimal.Decimal = decimal.New(1, decimalPrecision).Sub(one) + maxSupportedDecimal = decimal.New(1, decimalPrecision).Sub(one) ) +//go:generate go tool gentypes -ext-code 1 Decimal type Decimal struct { decimal.Decimal } @@ -71,37 +72,20 @@ func MakeDecimalFromString(src string) (Decimal, error) { return result, nil } -func decimalEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { - dec := v.Interface().(Decimal) - if dec.GreaterThan(maxSupportedDecimal) { - return nil, - fmt.Errorf( - "msgpack: decimal number is bigger than maximum supported number (10^%d - 1)", - decimalPrecision) - } - if dec.LessThan(minSupportedDecimal) { - return nil, - fmt.Errorf( - "msgpack: decimal number is lesser than minimum supported number (-10^%d - 1)", - decimalPrecision) - } - - strBuf := dec.String() - bcdBuf, err := encodeStringToBCD(strBuf) - if err != nil { - return nil, fmt.Errorf("msgpack: can't encode string (%s) to a BCD buffer: %w", strBuf, err) - } - return bcdBuf, nil -} +var ( + ErrDecimalOverflow = fmt.Errorf("msgpack: decimal number is bigger than"+ + " maximum supported number (10^%d - 1)", decimalPrecision) + ErrDecimalUnderflow = fmt.Errorf("msgpack: decimal number is lesser than"+ + " minimum supported number (-10^%d - 1)", decimalPrecision) +) -func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { - b := make([]byte, extLen) - n, err := d.Buffered().Read(b) - if err != nil { - return err - } - if n < extLen { - return fmt.Errorf("msgpack: unexpected end of stream after %d decimal bytes", n) +// MarshalMsgpack implements a custom msgpack marshaler. +func (d Decimal) MarshalMsgpack() ([]byte, error) { + switch { + case d.GreaterThan(maxSupportedDecimal): + return nil, ErrDecimalOverflow + case d.LessThan(minSupportedDecimal): + return nil, ErrDecimalUnderflow } // Decimal values can be encoded to fixext MessagePack, where buffer @@ -112,9 +96,19 @@ func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { // +--------+-------------------+------------+===============+ // | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal | // +--------+-------------------+------------+===============+ - digits, exp, err := decodeStringFromBCD(b) + strBuf := d.String() + bcdBuf, err := encodeStringToBCD(strBuf) if err != nil { - return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", b, err) + return nil, fmt.Errorf("msgpack: can't encode string (%s) to a BCD buffer: %w", strBuf, err) + } + return bcdBuf, nil +} + +// UnmarshalMsgpack implements a custom msgpack unmarshaler. +func (d *Decimal) UnmarshalMsgpack(data []byte) error { + digits, exp, err := decodeStringFromBCD(data) + if err != nil { + return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", data, err) } dec, err := decimal.NewFromString(digits) @@ -125,11 +119,31 @@ func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { if exp != 0 { dec = dec.Shift(int32(exp)) } - ptr := v.Addr().Interface().(*Decimal) - *ptr = MakeDecimal(dec) + + *d = MakeDecimal(dec) return nil } +func decimalEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { + dec := v.Interface().(Decimal) + + return dec.MarshalMsgpack() +} + +func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { + b := make([]byte, extLen) + + switch n, err := d.Buffered().Read(b); { + case err != nil: + return err + case n < extLen: + return fmt.Errorf("msgpack: unexpected end of stream after %d decimal bytes", n) + } + + ptr := v.Addr().Interface().(*Decimal) + return ptr.UnmarshalMsgpack(b) +} + func init() { msgpack.RegisterExtDecoder(decimalExtID, Decimal{}, decimalDecoder) msgpack.RegisterExtEncoder(decimalExtID, Decimal{}, decimalEncoder) diff --git a/decimal/decimal_gen.go b/decimal/decimal_gen.go new file mode 100644 index 000000000..0f9b18e3a --- /dev/null +++ b/decimal/decimal_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package decimal + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalDecimal represents an optional value of type Decimal. +// It can either hold a valid Decimal (IsSome == true) or be empty (IsZero == true). +type OptionalDecimal struct { + value Decimal + exists bool +} + +// SomeOptionalDecimal creates an optional OptionalDecimal with the given Decimal value. +// The returned OptionalDecimal will have IsSome() == true and IsZero() == false. +func SomeOptionalDecimal(value Decimal) OptionalDecimal { + return OptionalDecimal{ + value: value, + exists: true, + } +} + +// NoneOptionalDecimal creates an empty optional OptionalDecimal value. +// The returned OptionalDecimal will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalDecimal() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalDecimal() OptionalDecimal { + return OptionalDecimal{} +} + +func (o OptionalDecimal) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalDecimal", + Parent: err, + } +} + +func (o OptionalDecimal) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalDecimal", + Parent: err, + } +} + +// IsSome returns true if the OptionalDecimal contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalDecimal) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalDecimal does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o OptionalDecimal) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalDecimal) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of Decimal, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalDecimal) Get() (Decimal, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o OptionalDecimal) MustGet() Decimal { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for Decimal. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalDecimal) Unwrap() Decimal { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalDecimal() +// v := o.UnwrapOr(someDefaultOptionalDecimal) +func (o OptionalDecimal) UnwrapOr(defaultValue Decimal) Decimal { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneOptionalDecimal() +// v := o.UnwrapOrElse(func() Decimal { return computeDefault() }) +func (o OptionalDecimal) UnwrapOrElse(defaultValue func() Decimal) Decimal { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalDecimal) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(1, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalDecimal value using MessagePack format. +// - If the value is present, it is encoded as Decimal. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalDecimal) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalDecimal) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 1: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalDecimal) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalDecimal value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalDecimal) +// - Decimal: interpreted as a present value (SomeOptionalDecimal) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on Decimal: exists = true, value = decoded value +func (o *OptionalDecimal) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/decimal/decimal_gen_test.go b/decimal/decimal_gen_test.go new file mode 100644 index 000000000..50f22bf23 --- /dev/null +++ b/decimal/decimal_gen_test.go @@ -0,0 +1,117 @@ +package decimal + +import ( + "bytes" + "testing" + + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalDecimal(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + opt := SomeOptionalDecimal(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalDecimal(t *testing.T) { + opt := NoneOptionalDecimal() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalDecimal_MustGet(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + optSome := SomeOptionalDecimal(val) + optNone := NoneOptionalDecimal() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalDecimal_Unwrap(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + optSome := SomeOptionalDecimal(val) + optNone := NoneOptionalDecimal() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, Decimal{}, optNone.Unwrap()) +} + +func TestOptionalDecimal_UnwrapOr(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + def := MakeDecimal(decimal.NewFromFloat(4.56)) + optSome := SomeOptionalDecimal(val) + optNone := NoneOptionalDecimal() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalDecimal_UnwrapOrElse(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + def := MakeDecimal(decimal.NewFromFloat(4.56)) + optSome := SomeOptionalDecimal(val) + optNone := NoneOptionalDecimal() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() Decimal { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() Decimal { return def })) +} + +func TestOptionalDecimal_EncodeDecodeMsgpack_Some(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + some := SomeOptionalDecimal(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalDecimal + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalDecimal_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalDecimal() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalDecimal + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalDecimal_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalDecimal + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/go.mod b/go.mod index c1d57fa18..7582412da 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,27 @@ module github.com/tarantool/go-tarantool/v3 go 1.24 require ( - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.6.0 github.com/shopspring/decimal v1.3.1 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.11.1 github.com/tarantool/go-iproto v1.1.0 + github.com/tarantool/go-option v1.0.0 github.com/vmihailenco/msgpack/v5 v5.4.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/tools v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +tool ( + github.com/tarantool/go-option/cmd/gentypes + golang.org/x/tools/cmd/stringer +) diff --git a/go.sum b/go.sum index 099647b8c..91e1c4f25 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,38 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tarantool/go-iproto v1.1.0 h1:HULVOIHsiehI+FnHfM7wMDntuzUddO09DKqu2WnFQ5A= github.com/tarantool/go-iproto v1.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= +github.com/tarantool/go-option v1.0.0 h1:+Etw0i3TjsXvADTo5rfZNCfsXe3BfHOs+iVfIrl0Nlo= +github.com/tarantool/go-option v1.0.0/go.mod h1:lXzzeZtL+rPUtLOCDP6ny3FemFBjruG9aHKzNN2bS08= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pool/const.go b/pool/const.go index b1748ae52..d15490928 100644 --- a/pool/const.go +++ b/pool/const.go @@ -1,4 +1,4 @@ -//go:generate stringer -type Role -linecomment +//go:generate go tool stringer -type Role -linecomment package pool /* diff --git a/uuid/uuid.go b/uuid/uuid.go index cc2be736f..ca7b0ad05 100644 --- a/uuid/uuid.go +++ b/uuid/uuid.go @@ -1,4 +1,4 @@ -// Package with support of Tarantool's UUID data type. +// Package uuid with support of Tarantool's UUID data type. // // UUID data type supported in Tarantool since 2.4.1. // @@ -27,6 +27,17 @@ import ( // UUID external type. const uuid_extID = 2 +//go:generate go tool gentypes -ext-code 2 -marshal-func marshalUUID -unmarshal-func unmarshalUUID -imports "github.com/google/uuid" uuid.UUID + +func marshalUUID(id uuid.UUID) ([]byte, error) { + return id.MarshalBinary() +} + +func unmarshalUUID(uuid *uuid.UUID, data []byte) error { + return uuid.UnmarshalBinary(data) +} + +// encodeUUID encodes a uuid.UUID value into the msgpack format. func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { id := v.Interface().(uuid.UUID) @@ -43,6 +54,7 @@ func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { return nil } +// decodeUUID decodes a uuid.UUID value from the msgpack format. func decodeUUID(d *msgpack.Decoder, v reflect.Value) error { var bytesCount = 16 bytes := make([]byte, bytesCount) diff --git a/uuid/uuid_gen.go b/uuid/uuid_gen.go new file mode 100644 index 000000000..f1b1992ce --- /dev/null +++ b/uuid/uuid_gen.go @@ -0,0 +1,243 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package uuid + +import ( + "github.com/google/uuid" + + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalUUID represents an optional value of type uuid.UUID. +// It can either hold a valid uuid.UUID (IsSome == true) or be empty (IsZero == true). +type OptionalUUID struct { + value uuid.UUID + exists bool +} + +// SomeOptionalUUID creates an optional OptionalUUID with the given uuid.UUID value. +// The returned OptionalUUID will have IsSome() == true and IsZero() == false. +func SomeOptionalUUID(value uuid.UUID) OptionalUUID { + return OptionalUUID{ + value: value, + exists: true, + } +} + +// NoneOptionalUUID creates an empty optional OptionalUUID value. +// The returned OptionalUUID will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalUUID() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalUUID() OptionalUUID { + return OptionalUUID{} +} + +func (o OptionalUUID) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalUUID", + Parent: err, + } +} + +func (o OptionalUUID) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalUUID", + Parent: err, + } +} + +// IsSome returns true if the OptionalUUID contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalUUID) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalUUID does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o OptionalUUID) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalUUID) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of uuid.UUID, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalUUID) Get() (uuid.UUID, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o OptionalUUID) MustGet() uuid.UUID { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for uuid.UUID. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalUUID) Unwrap() uuid.UUID { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalUUID() +// v := o.UnwrapOr(someDefaultOptionalUUID) +func (o OptionalUUID) UnwrapOr(defaultValue uuid.UUID) uuid.UUID { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneOptionalUUID() +// v := o.UnwrapOrElse(func() uuid.UUID { return computeDefault() }) +func (o OptionalUUID) UnwrapOrElse(defaultValue func() uuid.UUID) uuid.UUID { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalUUID) encodeValue(encoder *msgpack.Encoder) error { + value, err := marshalUUID(o.value) + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(2, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalUUID value using MessagePack format. +// - If the value is present, it is encoded as uuid.UUID. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalUUID) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalUUID) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 2: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := unmarshalUUID(&o.value, a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalUUID) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalUUID value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalUUID) +// - uuid.UUID: interpreted as a present value (SomeOptionalUUID) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on uuid.UUID: exists = true, value = decoded value +func (o *OptionalUUID) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/uuid/uuid_gen_test.go b/uuid/uuid_gen_test.go new file mode 100644 index 000000000..616bb2314 --- /dev/null +++ b/uuid/uuid_gen_test.go @@ -0,0 +1,117 @@ +package uuid + +import ( + "bytes" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalUUID(t *testing.T) { + val := uuid.New() + opt := SomeOptionalUUID(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalUUID(t *testing.T) { + opt := NoneOptionalUUID() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalUUID_MustGet(t *testing.T) { + val := uuid.New() + optSome := SomeOptionalUUID(val) + optNone := NoneOptionalUUID() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalUUID_Unwrap(t *testing.T) { + val := uuid.New() + optSome := SomeOptionalUUID(val) + optNone := NoneOptionalUUID() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, uuid.Nil, optNone.Unwrap()) +} + +func TestOptionalUUID_UnwrapOr(t *testing.T) { + val := uuid.New() + def := uuid.New() + optSome := SomeOptionalUUID(val) + optNone := NoneOptionalUUID() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalUUID_UnwrapOrElse(t *testing.T) { + val := uuid.New() + def := uuid.New() + optSome := SomeOptionalUUID(val) + optNone := NoneOptionalUUID() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() uuid.UUID { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() uuid.UUID { return def })) +} + +func TestOptionalUUID_EncodeDecodeMsgpack_Some(t *testing.T) { + val := uuid.New() + some := SomeOptionalUUID(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalUUID + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalUUID_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalUUID() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalUUID + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalUUID_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalUUID + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} From 6111b7918c589faac52b1993a352fcb658d488a2 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 16 Oct 2025 14:11:20 +0300 Subject: [PATCH 595/605] doc: fix changelog entries --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41abba294..ab3c73982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,11 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +* New types for MessagePack extensions compatible with go-option (#459). + ### Changed -* Required Go version is `1.24` now. +* Required Go version is `1.24` now (#456). ### Fixed From e727a3bc9d63e72559f70a34807c0e3dc4f3df03 Mon Sep 17 00:00:00 2001 From: Viktor Tsapovskiy Date: Thu, 23 Oct 2025 17:46:15 +0300 Subject: [PATCH 596/605] box: box.New returns an error instead of panic Method `box.New` now returns (*Box, error), because avoiding panics entirely in third-party libraries is ideal, since we can't break compatibility. Nonetheless, there is still a way to panic on error, via implemented `box.MustNew` wrapper. Part of #448 --- CHANGELOG.md | 3 ++ box/box.go | 19 ++++++++--- box/box_test.go | 20 ++++++++--- box/example_test.go | 35 +++++++++++++++---- box/session_test.go | 3 +- box/tarantool_test.go | 78 ++++++++++++++++++++++++++++++------------- 6 files changed, 119 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab3c73982..cb9e9587c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added * New types for MessagePack extensions compatible with go-option (#459). +* Added `box.MustNew` wrapper for `box.New`: panics when `box.New` returns an error (#448). ### Changed @@ -18,6 +19,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Fixed +* `box.New` returns an error instead of panic (#448). + ## [v2.4.1] - 2025-10-16 This maintenance release marks the end of active development on the `v2` diff --git a/box/box.go b/box/box.go index a4cc1780e..d28f8a876 100644 --- a/box/box.go +++ b/box/box.go @@ -1,6 +1,8 @@ package box import ( + "errors" + "github.com/tarantool/go-tarantool/v3" ) @@ -11,16 +13,25 @@ type Box struct { } // New returns a new instance of the box structure, which implements the Box interface. -func New(conn tarantool.Doer) *Box { +func New(conn tarantool.Doer) (*Box, error) { if conn == nil { - // Check if the provided Tarantool connection is nil, and if it is, panic with an error - // message. panic early helps to catch and fix nil pointer issues in the code. - panic("tarantool connection cannot be nil") + return nil, errors.New("tarantool connection cannot be nil") } return &Box{ conn: conn, // Assigns the provided Tarantool connection. + }, nil +} + +// MustNew returns a new instance of the box structure, which implements the Box interface. +func MustNew(conn tarantool.Doer) *Box { + b, err := New(conn) + if err != nil { + // Check if the provided Tarantool connection is nil, and if it is, panic with an error + // message. panic early helps to catch and fix nil pointer issues in the code + panic(err) } + return b } // Schema returns a new Schema instance, providing access to schema-related operations. diff --git a/box/box_test.go b/box/box_test.go index 59ceaf6e3..e9732e108 100644 --- a/box/box_test.go +++ b/box/box_test.go @@ -15,8 +15,15 @@ import ( func TestNew(t *testing.T) { t.Parallel() + _, err := box.New(nil) + require.Error(t, err) +} + +func TestMustNew(t *testing.T) { + t.Parallel() + // Create a box instance with a nil connection. This should lead to a panic. - require.Panics(t, func() { box.New(nil) }) + require.Panics(t, func() { box.MustNew(nil) }) } func TestMocked_BoxInfo(t *testing.T) { @@ -37,7 +44,8 @@ func TestMocked_BoxInfo(t *testing.T) { mock := test_helpers.NewMockDoer(t, test_helpers.NewMockResponse(t, data), ) - b := box.New(&mock) + b, err := box.New(&mock) + require.NoError(t, err) info, err := b.Info() require.NoError(t, err) @@ -57,7 +65,8 @@ func TestMocked_BoxSchemaUserInfo(t *testing.T) { mock := test_helpers.NewMockDoer(t, test_helpers.NewMockResponse(t, data), ) - b := box.New(&mock) + b, err := box.New(&mock) + require.NoError(t, err) privs, err := b.Schema().User().Info(context.Background(), "username") require.NoError(t, err) @@ -82,8 +91,9 @@ func TestMocked_BoxSessionSu(t *testing.T) { test_helpers.NewMockResponse(t, []interface{}{}), errors.New("user not found or supplied credentials are invalid"), ) - b := box.New(&mock) + b, err := box.New(&mock) + require.NoError(t, err) - err := b.Session().Su(context.Background(), "admin") + err = b.Session().Su(context.Background(), "admin") require.NoError(t, err) } diff --git a/box/example_test.go b/box/example_test.go index fa65189a8..578f88876 100644 --- a/box/example_test.go +++ b/box/example_test.go @@ -45,7 +45,10 @@ func ExampleBox_Info() { // Or use simple Box implementation. - b := box.New(client) + b, err := box.New(client) + if err != nil { + log.Fatalf("Failed get box info: %s", err) + } info, err := b.Info() if err != nil { @@ -88,7 +91,11 @@ func ExampleSchemaUser_Exists() { } // Or use simple User implementation. - b := box.New(client) + b, err := box.New(client) + if err != nil { + log.Fatalf("Failed get box info: %s", err) + } + exists, err := b.Schema().User().Exists(ctx, "user") if err != nil { log.Fatalf("Failed get box schema user exists with error: %s", err) @@ -120,7 +127,11 @@ func ExampleSchemaUser_Create() { } // Create SchemaUser. - schemaUser := box.New(client).Schema().User() + b, err := box.New(client) + if err != nil { + log.Fatalf("Failed to connect: %s", err) + } + schemaUser := b.Schema().User() // Create a new user. username := "new_user" @@ -153,7 +164,11 @@ func ExampleSchemaUser_Drop() { } // Create SchemaUser. - schemaUser := box.New(client).Schema().User() + b, err := box.New(client) + if err != nil { + log.Fatalf("Failed to connect: %s", err) + } + schemaUser := b.Schema().User() // Drop an existing user. username := "new_user" @@ -192,7 +207,11 @@ func ExampleSchemaUser_Password() { } // Create SchemaUser. - schemaUser := box.New(client).Schema().User() + b, err := box.New(client) + if err != nil { + log.Fatalf("Failed to connect: %s", err) + } + schemaUser := b.Schema().User() // Get the password hash. password := "my-password" @@ -221,7 +240,11 @@ func ExampleSchemaUser_Info() { } // Create SchemaUser. - schemaUser := box.New(client).Schema().User() + b, err := box.New(client) + if err != nil { + log.Fatalf("Failed to connect: %s", err) + } + schemaUser := b.Schema().User() info, err := schemaUser.Info(ctx, "test") if err != nil { diff --git a/box/session_test.go b/box/session_test.go index 39b80d1d4..8348dd11c 100644 --- a/box/session_test.go +++ b/box/session_test.go @@ -10,7 +10,8 @@ import ( ) func TestBox_Session(t *testing.T) { - b := box.New(th.Ptr(th.NewMockDoer(t))) + b, err := box.New(th.Ptr(th.NewMockDoer(t))) + require.NoError(t, err) require.NotNil(t, b.Session()) } diff --git a/box/tarantool_test.go b/box/tarantool_test.go index 4244a98e9..56092859d 100644 --- a/box/tarantool_test.go +++ b/box/tarantool_test.go @@ -48,7 +48,10 @@ func TestBox_Sugar_Info(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - info, err := box.New(conn).Info() + b, err := box.New(conn) + require.NoError(t, err) + + info, err := b.Info() require.NoError(t, err) validateInfo(t, info) @@ -70,6 +73,10 @@ func TestBox_Info(t *testing.T) { validateInfo(t, resp.Info) } +func TestBox_Connection_NotNil(t *testing.T) { + +} + func TestBox_Sugar_Schema_UserCreate_NoError(t *testing.T) { const ( username = "user_create_no_error" @@ -83,7 +90,8 @@ func TestBox_Sugar_Schema_UserCreate_NoError(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) // Create new user. err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -103,7 +111,8 @@ func TestBox_Sugar_Schema_UserCreate_CanConnectWithNewCred(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) // Create new user. err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -137,7 +146,8 @@ func TestBox_Sugar_Schema_UserCreate_AlreadyExists(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) // Create new user. err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -167,7 +177,8 @@ func TestBox_Sugar_Schema_UserCreate_ExistsTrue(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) // Create new user. err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -193,7 +204,8 @@ func TestBox_Sugar_Schema_UserCreate_IfNotExistsNoErr(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) // Create new user. err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -217,7 +229,8 @@ func TestBox_Sugar_Schema_UserPassword(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) // Require password hash. hash, err := b.Schema().User().Password(ctx, password) @@ -236,7 +249,8 @@ func TestBox_Sugar_Schema_UserDrop_AfterCreate(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) // Create new user err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -258,7 +272,8 @@ func TestBox_Sugar_Schema_UserDrop_DoubleDrop(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) // Create new user err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -286,7 +301,8 @@ func TestBox_Sugar_Schema_UserDrop_UnknownUser(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) // Require error cause user not exists err = b.Schema().User().Drop(ctx, "some_strange_not_existing_name", box.UserDropOptions{}) @@ -305,7 +321,8 @@ func TestSchemaUser_Passwd_NotFound(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) err = b.Schema().User().Passwd(ctx, "not-exists-passwd", "new_password") require.Error(t, err) @@ -329,7 +346,8 @@ func TestSchemaUser_Passwd_Ok(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) // New user change password and connect @@ -367,7 +385,8 @@ func TestSchemaUser_Passwd_WithoutGrants(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: startPassword, IfNotExists: true}) @@ -381,7 +400,8 @@ func TestSchemaUser_Passwd_WithoutGrants(t *testing.T) { require.NoError(t, err) require.NotNil(t, conn2Fail) - bFail := box.New(conn2Fail) + bFail, err := box.New(conn2Fail) + require.NoError(t, err) // can't change self user password without grants err = bFail.Schema().User().Passwd(ctx, endPassword) require.Error(t, err) @@ -401,7 +421,8 @@ func TestSchemaUser_Info_TestUserCorrect(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) privileges, err := b.Schema().User().Info(ctx, dialer.User) require.NoError(t, err) @@ -418,7 +439,8 @@ func TestSchemaUser_Info_NonExistsUser(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) privileges, err := b.Schema().User().Info(ctx, "non-existing") require.Error(t, err) @@ -438,7 +460,8 @@ func TestBox_Sugar_Schema_UserGrant_NoSu(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) require.NoError(t, err) @@ -471,7 +494,8 @@ func TestBox_Sugar_Schema_UserGrant_WithSu(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) require.NoError(t, err) @@ -521,7 +545,8 @@ func TestSchemaUser_Revoke_WithoutSu(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) require.NoError(t, err) @@ -555,7 +580,8 @@ func TestSchemaUser_Revoke_WithSu(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) require.NoError(t, err) @@ -603,7 +629,8 @@ func TestSchemaUser_Revoke_NonExistsPermission(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) require.NoError(t, err) @@ -639,7 +666,8 @@ func TestSession_Su_AdminPermissions(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b := box.New(conn) + b, err := box.New(conn) + require.NoError(t, err) err = b.Session().Su(ctx, "admin") require.NoError(t, err) @@ -652,7 +680,11 @@ func cleanupUser(username string) { log.Fatal(err) } - b := box.New(conn) + b, err := box.New(conn) + if err != nil { + log.Fatal(err) + } + err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{}) if err != nil { log.Fatal(err) From cbcf6a47367cdf9506da7779e0f7540831236aaa Mon Sep 17 00:00:00 2001 From: Viktor Tsapovskiy Date: Sat, 25 Oct 2025 23:53:29 +0300 Subject: [PATCH 597/605] box: refactored tests with box.New Tests now use `box.MustNew` instead of `box.New`. Added TestMocked_BoxNew for box.New creating a workable object by means of checking length of Request[] in mockDoer after subrequest. Closes #448 --- CHANGELOG.md | 5 ++- MIGRATION.md | 2 ++ box/box.go | 3 +- box/box_test.go | 27 +++++++++++---- box/example_test.go | 34 ++++--------------- box/session_test.go | 3 +- box/tarantool_test.go | 76 ++++++++++++++----------------------------- 7 files changed, 56 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb9e9587c..c2dc4fc19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,16 +11,15 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added * New types for MessagePack extensions compatible with go-option (#459). -* Added `box.MustNew` wrapper for `box.New`: panics when `box.New` returns an error (#448). +* Added `box.MustNew` wrapper for `box.New` without an error (#448). ### Changed * Required Go version is `1.24` now (#456). +* `box.New` returns an error instead of panic (#448). ### Fixed -* `box.New` returns an error instead of panic (#448). - ## [v2.4.1] - 2025-10-16 This maintenance release marks the end of active development on the `v2` diff --git a/MIGRATION.md b/MIGRATION.md index 54f4e3034..ea82c8815 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -9,6 +9,8 @@ TODO ### Major changes * Required Go version is `1.24` now. +* `box.New` returns an error instead of panic +* Added `box.MustNew` wrapper for `box.New` without an error ## Migration from v1.x.x to v2.x.x diff --git a/box/box.go b/box/box.go index d28f8a876..4341768cf 100644 --- a/box/box.go +++ b/box/box.go @@ -24,11 +24,10 @@ func New(conn tarantool.Doer) (*Box, error) { } // MustNew returns a new instance of the box structure, which implements the Box interface. +// It panics if conn == nil. func MustNew(conn tarantool.Doer) *Box { b, err := New(conn) if err != nil { - // Check if the provided Tarantool connection is nil, and if it is, panic with an error - // message. panic early helps to catch and fix nil pointer issues in the code panic(err) } return b diff --git a/box/box_test.go b/box/box_test.go index e9732e108..57d0d5526 100644 --- a/box/box_test.go +++ b/box/box_test.go @@ -26,6 +26,22 @@ func TestMustNew(t *testing.T) { require.Panics(t, func() { box.MustNew(nil) }) } +func TestMocked_BoxNew(t *testing.T) { + t.Parallel() + + mock := test_helpers.NewMockDoer(t, + test_helpers.NewMockResponse(t, "valid"), + ) + + b, err := box.New(&mock) + require.NoError(t, err) + require.NotNil(t, b) + + assert.Len(t, mock.Requests, 0) + b.Schema().User().Exists(box.NewInfoRequest().Ctx(), "") + require.Len(t, mock.Requests, 1) +} + func TestMocked_BoxInfo(t *testing.T) { t.Parallel() @@ -44,8 +60,7 @@ func TestMocked_BoxInfo(t *testing.T) { mock := test_helpers.NewMockDoer(t, test_helpers.NewMockResponse(t, data), ) - b, err := box.New(&mock) - require.NoError(t, err) + b := box.MustNew(&mock) info, err := b.Info() require.NoError(t, err) @@ -65,8 +80,7 @@ func TestMocked_BoxSchemaUserInfo(t *testing.T) { mock := test_helpers.NewMockDoer(t, test_helpers.NewMockResponse(t, data), ) - b, err := box.New(&mock) - require.NoError(t, err) + b := box.MustNew(&mock) privs, err := b.Schema().User().Info(context.Background(), "username") require.NoError(t, err) @@ -91,9 +105,8 @@ func TestMocked_BoxSessionSu(t *testing.T) { test_helpers.NewMockResponse(t, []interface{}{}), errors.New("user not found or supplied credentials are invalid"), ) - b, err := box.New(&mock) - require.NoError(t, err) + b := box.MustNew(&mock) - err = b.Session().Su(context.Background(), "admin") + err := b.Session().Su(context.Background(), "admin") require.NoError(t, err) } diff --git a/box/example_test.go b/box/example_test.go index 578f88876..39474d8aa 100644 --- a/box/example_test.go +++ b/box/example_test.go @@ -45,10 +45,7 @@ func ExampleBox_Info() { // Or use simple Box implementation. - b, err := box.New(client) - if err != nil { - log.Fatalf("Failed get box info: %s", err) - } + b := box.MustNew(client) info, err := b.Info() if err != nil { @@ -91,10 +88,7 @@ func ExampleSchemaUser_Exists() { } // Or use simple User implementation. - b, err := box.New(client) - if err != nil { - log.Fatalf("Failed get box info: %s", err) - } + b := box.MustNew(client) exists, err := b.Schema().User().Exists(ctx, "user") if err != nil { @@ -127,11 +121,7 @@ func ExampleSchemaUser_Create() { } // Create SchemaUser. - b, err := box.New(client) - if err != nil { - log.Fatalf("Failed to connect: %s", err) - } - schemaUser := b.Schema().User() + schemaUser := box.MustNew(client).Schema().User() // Create a new user. username := "new_user" @@ -164,11 +154,7 @@ func ExampleSchemaUser_Drop() { } // Create SchemaUser. - b, err := box.New(client) - if err != nil { - log.Fatalf("Failed to connect: %s", err) - } - schemaUser := b.Schema().User() + schemaUser := box.MustNew(client).Schema().User() // Drop an existing user. username := "new_user" @@ -207,11 +193,7 @@ func ExampleSchemaUser_Password() { } // Create SchemaUser. - b, err := box.New(client) - if err != nil { - log.Fatalf("Failed to connect: %s", err) - } - schemaUser := b.Schema().User() + schemaUser := box.MustNew(client).Schema().User() // Get the password hash. password := "my-password" @@ -240,11 +222,7 @@ func ExampleSchemaUser_Info() { } // Create SchemaUser. - b, err := box.New(client) - if err != nil { - log.Fatalf("Failed to connect: %s", err) - } - schemaUser := b.Schema().User() + schemaUser := box.MustNew(client).Schema().User() info, err := schemaUser.Info(ctx, "test") if err != nil { diff --git a/box/session_test.go b/box/session_test.go index 8348dd11c..188360251 100644 --- a/box/session_test.go +++ b/box/session_test.go @@ -10,8 +10,7 @@ import ( ) func TestBox_Session(t *testing.T) { - b, err := box.New(th.Ptr(th.NewMockDoer(t))) - require.NoError(t, err) + b := box.MustNew(th.Ptr(th.NewMockDoer(t))) require.NotNil(t, b.Session()) } diff --git a/box/tarantool_test.go b/box/tarantool_test.go index 56092859d..ae47932d8 100644 --- a/box/tarantool_test.go +++ b/box/tarantool_test.go @@ -48,8 +48,7 @@ func TestBox_Sugar_Info(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) info, err := b.Info() require.NoError(t, err) @@ -73,10 +72,6 @@ func TestBox_Info(t *testing.T) { validateInfo(t, resp.Info) } -func TestBox_Connection_NotNil(t *testing.T) { - -} - func TestBox_Sugar_Schema_UserCreate_NoError(t *testing.T) { const ( username = "user_create_no_error" @@ -90,8 +85,7 @@ func TestBox_Sugar_Schema_UserCreate_NoError(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) // Create new user. err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -111,8 +105,7 @@ func TestBox_Sugar_Schema_UserCreate_CanConnectWithNewCred(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) // Create new user. err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -146,8 +139,7 @@ func TestBox_Sugar_Schema_UserCreate_AlreadyExists(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) // Create new user. err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -177,8 +169,7 @@ func TestBox_Sugar_Schema_UserCreate_ExistsTrue(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) // Create new user. err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -204,8 +195,7 @@ func TestBox_Sugar_Schema_UserCreate_IfNotExistsNoErr(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) // Create new user. err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -229,8 +219,7 @@ func TestBox_Sugar_Schema_UserPassword(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) // Require password hash. hash, err := b.Schema().User().Password(ctx, password) @@ -249,8 +238,7 @@ func TestBox_Sugar_Schema_UserDrop_AfterCreate(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) // Create new user err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -272,8 +260,7 @@ func TestBox_Sugar_Schema_UserDrop_DoubleDrop(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) // Create new user err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) @@ -301,8 +288,7 @@ func TestBox_Sugar_Schema_UserDrop_UnknownUser(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) // Require error cause user not exists err = b.Schema().User().Drop(ctx, "some_strange_not_existing_name", box.UserDropOptions{}) @@ -321,8 +307,7 @@ func TestSchemaUser_Passwd_NotFound(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) err = b.Schema().User().Passwd(ctx, "not-exists-passwd", "new_password") require.Error(t, err) @@ -346,8 +331,7 @@ func TestSchemaUser_Passwd_Ok(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) // New user change password and connect @@ -385,8 +369,7 @@ func TestSchemaUser_Passwd_WithoutGrants(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: startPassword, IfNotExists: true}) @@ -400,8 +383,8 @@ func TestSchemaUser_Passwd_WithoutGrants(t *testing.T) { require.NoError(t, err) require.NotNil(t, conn2Fail) - bFail, err := box.New(conn2Fail) - require.NoError(t, err) + bFail := box.MustNew(conn2Fail) + // can't change self user password without grants err = bFail.Schema().User().Passwd(ctx, endPassword) require.Error(t, err) @@ -421,8 +404,7 @@ func TestSchemaUser_Info_TestUserCorrect(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) privileges, err := b.Schema().User().Info(ctx, dialer.User) require.NoError(t, err) @@ -439,8 +421,7 @@ func TestSchemaUser_Info_NonExistsUser(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) privileges, err := b.Schema().User().Info(ctx, "non-existing") require.Error(t, err) @@ -460,8 +441,7 @@ func TestBox_Sugar_Schema_UserGrant_NoSu(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) require.NoError(t, err) @@ -494,8 +474,7 @@ func TestBox_Sugar_Schema_UserGrant_WithSu(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) require.NoError(t, err) @@ -545,8 +524,7 @@ func TestSchemaUser_Revoke_WithoutSu(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) require.NoError(t, err) @@ -580,8 +558,7 @@ func TestSchemaUser_Revoke_WithSu(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) require.NoError(t, err) @@ -629,8 +606,7 @@ func TestSchemaUser_Revoke_NonExistsPermission(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) err = b.Schema().User().Create(ctx, username, box.UserCreateOptions{Password: password}) require.NoError(t, err) @@ -666,8 +642,7 @@ func TestSession_Su_AdminPermissions(t *testing.T) { conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) require.NoError(t, err) - b, err := box.New(conn) - require.NoError(t, err) + b := box.MustNew(conn) err = b.Session().Su(ctx, "admin") require.NoError(t, err) @@ -680,10 +655,7 @@ func cleanupUser(username string) { log.Fatal(err) } - b, err := box.New(conn) - if err != nil { - log.Fatal(err) - } + b := box.MustNew(conn) err = b.Schema().User().Drop(ctx, username, box.UserDropOptions{}) if err != nil { From 57dc8338c70d25aaeb3e5d7afef8bbf4049aff2b Mon Sep 17 00:00:00 2001 From: Ermachkov Yaroslav Date: Fri, 24 Oct 2025 13:25:00 +0300 Subject: [PATCH 598/605] connection: returing ctx.Cause error in canceled case Returing wrapped context.Cause(ctx) error in <-ctx.Done() case. Allows compare errors using errors.Is/As. Add 3 tests errors checking comparabily. Fixes #457 --- CHANGELOG.md | 2 ++ connection.go | 6 ++++-- example_test.go | 2 +- tarantool_test.go | 43 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2dc4fc19..eb283b863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. * Required Go version is `1.24` now (#456). * `box.New` returns an error instead of panic (#448). +* Now cases of `<-ctx.Done()` returns wrapped error provided by `ctx.Cause()`. + Allows you compare it using `errors.Is/As` (#457). ### Fixed diff --git a/connection.go b/connection.go index 5f976fbf8..f8f04d014 100644 --- a/connection.go +++ b/connection.go @@ -984,7 +984,8 @@ func (conn *Connection) newFuture(req Request) (fut *Future) { if ctx != nil { select { case <-ctx.Done(): - fut.SetError(fmt.Errorf("context is done (request ID %d)", fut.requestId)) + fut.SetError(fmt.Errorf("context is done (request ID %d): %w", + fut.requestId, context.Cause(ctx))) shard.rmut.Unlock() return default: @@ -1026,7 +1027,8 @@ func (conn *Connection) contextWatchdog(fut *Future, ctx context.Context) { case <-fut.done: return default: - conn.cancelFuture(fut, fmt.Errorf("context is done (request ID %d)", fut.requestId)) + conn.cancelFuture(fut, fmt.Errorf("context is done (request ID %d): %w", + fut.requestId, context.Cause(ctx))) } } diff --git a/example_test.go b/example_test.go index e1b9d5766..b4411fb69 100644 --- a/example_test.go +++ b/example_test.go @@ -167,7 +167,7 @@ func ExamplePingRequest_Context() { fmt.Println("Ping Error", regexp.MustCompile("[0-9]+").ReplaceAllString(err.Error(), "N")) // Output: // Ping Resp data [] - // Ping Error context is done (request ID N) + // Ping Error context is done (request ID N): context deadline exceeded } func ExampleSelectRequest() { diff --git a/tarantool_test.go b/tarantool_test.go index 437f7f96e..4b8873eb7 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -3,6 +3,7 @@ package tarantool_test import ( "context" "encoding/binary" + "errors" "fmt" "io" "log" @@ -48,7 +49,8 @@ type Member struct { Val uint } -var contextDoneErrRegexp = regexp.MustCompile(`^context is done \(request ID [0-9]+\)$`) +var contextDoneErrRegexp = regexp.MustCompile( + `^context is done \(request ID [0-9]+\): context canceled$`) func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { if err := e.EncodeArrayLen(2); err != nil { @@ -2742,6 +2744,45 @@ func TestClientRequestObjectsWithPassedCanceledContext(t *testing.T) { } } +// Checking comparable with simple context.WithCancel. +func TestComparableErrorsCanceledContext(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, dialer, opts) + defer conn.Close() + + ctx, cancel := context.WithCancel(context.Background()) + req := NewPingRequest().Context(ctx) + cancel() + _, err := conn.Do(req).Get() + require.True(t, errors.Is(err, context.Canceled), err.Error()) +} + +// Checking comparable with simple context.WithTimeout. +func TestComparableErrorsTimeoutContext(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, dialer, opts) + defer conn.Close() + + timeout := time.Nanosecond + ctx, cancel := context.WithTimeout(context.Background(), timeout) + req := NewPingRequest().Context(ctx) + defer cancel() + _, err := conn.Do(req).Get() + require.True(t, errors.Is(err, context.DeadlineExceeded), err.Error()) +} + +// Checking comparable with context.WithCancelCause. +// Shows ability to compare with custom errors (also with ClientError). +func TestComparableErrorsCancelCauseContext(t *testing.T) { + conn := test_helpers.ConnectWithValidation(t, dialer, opts) + defer conn.Close() + + ctxCause, cancelCause := context.WithCancelCause(context.Background()) + req := NewPingRequest().Context(ctxCause) + cancelCause(ClientError{ErrConnectionClosed, "something went wrong"}) + _, err := conn.Do(req).Get() + var tmpErr ClientError + require.True(t, errors.As(err, &tmpErr), tmpErr.Error()) +} + // waitCtxRequest waits for the WaitGroup in Body() call and returns // the context from Ctx() call. The request helps us to make sure that // the context's cancel() call is called before a response received. From 02dff5ac590f035fe2b2adf901fe8d5f75cad793 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 30 Oct 2025 11:26:28 +0300 Subject: [PATCH 599/605] doc: fix links to v3 in the README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6950724ca..8b3ff5e7a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The package `go-tarantool` is located in [tarantool/go-tarantool][go-tarantool] repository. To download and install, say: ``` -$ go get github.com/tarantool/go-tarantool/v2 +$ go get github.com/tarantool/go-tarantool/v3 ``` This should put the source and binary files in subdirectories of @@ -131,7 +131,7 @@ func main() { } ``` -**Observation 1:** The line "`github.com/tarantool/go-tarantool/v2`" in the +**Observation 1:** The line "`github.com/tarantool/go-tarantool/v3`" in the `import(...)` section brings in all Tarantool-related functions and structures. **Observation 2:** Unused import lines are required to initialize encoders and @@ -245,8 +245,8 @@ There are two other connectors available from the open source community: See feature comparison in the [documentation][tarantool-doc-connectors-comparison]. [tarantool-site]: https://tarantool.io/ -[godoc-badge]: https://pkg.go.dev/badge/github.com/tarantool/go-tarantool/v2.svg -[godoc-url]: https://pkg.go.dev/github.com/tarantool/go-tarantool/v2 +[godoc-badge]: https://pkg.go.dev/badge/github.com/tarantool/go-tarantool/v3.svg +[godoc-url]: https://pkg.go.dev/github.com/tarantool/go-tarantool/v3 [actions-badge]: https://github.com/tarantool/go-tarantool/actions/workflows/testing.yml/badge.svg [actions-url]: https://github.com/tarantool/go-tarantool/actions/workflows/testing.yml [coverage-badge]: https://coveralls.io/repos/github/tarantool/go-tarantool/badge.svg?branch=master @@ -261,5 +261,5 @@ See feature comparison in the [documentation][tarantool-doc-connectors-compariso [go-tarantool]: https://github.com/tarantool/go-tarantool [tarantool-doc-data-model-url]: https://www.tarantool.io/en/doc/latest/book/box/data_model/ [tarantool-doc-box-space-url]: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/ -[godoc-opts-url]: https://pkg.go.dev/github.com/tarantool/go-tarantool/v2#Opts +[godoc-opts-url]: https://pkg.go.dev/github.com/tarantool/go-tarantool/v3#Opts [tarantool-doc-connectors-comparison]: https://www.tarantool.io/en/doc/latest/book/connectors/#go-feature-comparison From 8384443805923e35d4b3838cdcb6d2c8ce9080f5 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 30 Oct 2025 11:27:17 +0300 Subject: [PATCH 600/605] doc: add a note about unstable v3 to the README.md --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 8b3ff5e7a..3c31d2506 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,17 @@ [![GitHub Discussions][discussions-badge]][discussions-url] [![Stack Overflow][stackoverflow-badge]][stackoverflow-url] +# ⚠️ Development Status Notice + +**The current `main` branch is under active development for the next major +release (v3).** + +The API on this branch is **unstable and subject to change**. + +**For production use and stable API, please use the +[`v2`](https://github.com/tarantool/go-tarantool/tree/v2) branch of the +repository.** + # Client in Go for Tarantool The package `go-tarantool` contains everything you need to connect to From 3e758681df46b268ef4d246d5aa682edf09668e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D1=89=D0=B0=D0=BD=D0=BE=D0=B2=D0=B0=20=D0=A1=D0=B0?= =?UTF-8?q?=D1=80=D0=B0?= Date: Thu, 30 Oct 2025 21:05:27 +0300 Subject: [PATCH 601/605] iproto: add missing IPROTO greeting flags The greeting flags of functions IPROTO_FEATURE_IS_SYNC and IPROTO_FEATURE_INSERT_ARROW protocol function flags were added. Closes #466 --- CHANGELOG.md | 2 ++ dial_test.go | 5 +++-- protocol.go | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb283b863..3c3d119b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ flag handling, and fixes watcher panic. Now you can check this error with `errors.Is(err, tarantool.ErrConcurrentSchemaUpdate)`. - Implemented support for `IPROTO_IS_SYNC` flag in stream transactions, added `IsSync(bool)` method for `BeginRequest`/`CommitRequest` (#447). +- Added missing IPROTO feature flags to greeting negotiation + (iproto.IPROTO_FEATURE_IS_SYNC, iproto.IPROTO_FEATURE_INSERT_ARROW) (#466). ### Fixed diff --git a/dial_test.go b/dial_test.go index 2d0dee8ce..f2cacc4cf 100644 --- a/dial_test.go +++ b/dial_test.go @@ -343,7 +343,7 @@ var ( testDialSalt = genSalt() idRequestExpected = []byte{ - 0xce, 0x00, 0x00, 0x00, 29, // Length. + 0xce, 0x00, 0x00, 0x00, 31, // Length. 0x82, // Header map. 0x00, 0x49, 0x01, 0xce, 0x00, 0x00, 0x00, 0x00, @@ -352,7 +352,8 @@ var ( 0x54, 0xcf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, // Version. 0x55, - 0x97, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // Features. + 0x99, // Fixed arrау with 9 elements. + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x0b, 0x0c, // Features (9 elements). } idResponseTyped = tarantool.ProtocolInfo{ diff --git a/protocol.go b/protocol.go index 9f4d6a1b9..e4e93e7c7 100644 --- a/protocol.go +++ b/protocol.go @@ -58,6 +58,8 @@ var clientProtocolInfo ProtocolInfo = ProtocolInfo{ iproto.IPROTO_FEATURE_PAGINATION, iproto.IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, iproto.IPROTO_FEATURE_WATCH_ONCE, + iproto.IPROTO_FEATURE_IS_SYNC, + iproto.IPROTO_FEATURE_INSERT_ARROW, }, } From c972ce4c1ab72e923d61164c92a22cad6ff7b49e Mon Sep 17 00:00:00 2001 From: Ermachkov Yaroslav Date: Thu, 30 Oct 2025 16:16:10 +0300 Subject: [PATCH 602/605] pool: removed deprecated methods Removed deprecated pool's methods. Related tests are updated (deleted or refactored). Also removed mentioned methods in interfaces declarations. Fixes #478 --- CHANGELOG.md | 1 + MIGRATION.md | 1 + connector.go | 110 ---- pool/connection_pool.go | 518 ------------------- pool/connection_pool_test.go | 500 ++++++++++-------- pool/connector.go | 320 ------------ pool/connector_test.go | 956 ----------------------------------- pool/pooler.go | 138 ----- 8 files changed, 298 insertions(+), 2246 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c3d119b9..a0d43749b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. * `box.New` returns an error instead of panic (#448). * Now cases of `<-ctx.Done()` returns wrapped error provided by `ctx.Cause()`. Allows you compare it using `errors.Is/As` (#457). +* Removed deprecated `pool` methods, related interfaces and tests are updated (#478). ### Fixed diff --git a/MIGRATION.md b/MIGRATION.md index ea82c8815..3044dbffc 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -11,6 +11,7 @@ TODO * Required Go version is `1.24` now. * `box.New` returns an error instead of panic * Added `box.MustNew` wrapper for `box.New` without an error +* Removed deprecated `pool` methods, related interfaces and tests are updated. ## Migration from v1.x.x to v2.x.x diff --git a/connector.go b/connector.go index 9917cff0f..7112eb099 100644 --- a/connector.go +++ b/connector.go @@ -16,114 +16,4 @@ type Connector interface { NewPrepared(expr string) (*Prepared, error) NewStream() (*Stream, error) NewWatcher(key string, callback WatchCallback) (Watcher, error) - - // Deprecated: the method will be removed in the next major version, - // use a PingRequest object + Do() instead. - Ping() ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a SelectRequest object + Do() instead. - Select(space, index interface{}, offset, limit uint32, iterator Iter, - key interface{}) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use an InsertRequest object + Do() instead. - Insert(space interface{}, tuple interface{}) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a ReplicaRequest object + Do() instead. - Replace(space interface{}, tuple interface{}) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a DeleteRequest object + Do() instead. - Delete(space, index interface{}, key interface{}) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a UpdateRequest object + Do() instead. - Update(space, index interface{}, key interface{}, ops *Operations) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a UpsertRequest object + Do() instead. - Upsert(space interface{}, tuple interface{}, ops *Operations) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a CallRequest object + Do() instead. - Call(functionName string, args interface{}) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a Call16Request object + Do() instead. - Call16(functionName string, args interface{}) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a Call17Request object + Do() instead. - Call17(functionName string, args interface{}) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use an EvalRequest object + Do() instead. - Eval(expr string, args interface{}) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use an ExecuteRequest object + Do() instead. - Execute(expr string, args interface{}) ([]interface{}, error) - - // Deprecated: the method will be removed in the next major version, - // use a SelectRequest object + Do() instead. - GetTyped(space, index interface{}, key interface{}, result interface{}) error - // Deprecated: the method will be removed in the next major version, - // use a SelectRequest object + Do() instead. - SelectTyped(space, index interface{}, offset, limit uint32, iterator Iter, key interface{}, - result interface{}) error - // Deprecated: the method will be removed in the next major version, - // use an InsertRequest object + Do() instead. - InsertTyped(space interface{}, tuple interface{}, result interface{}) error - // Deprecated: the method will be removed in the next major version, - // use a ReplaceRequest object + Do() instead. - ReplaceTyped(space interface{}, tuple interface{}, result interface{}) error - // Deprecated: the method will be removed in the next major version, - // use a DeleteRequest object + Do() instead. - DeleteTyped(space, index interface{}, key interface{}, result interface{}) error - // Deprecated: the method will be removed in the next major version, - // use a UpdateRequest object + Do() instead. - UpdateTyped(space, index interface{}, key interface{}, ops *Operations, - result interface{}) error - // Deprecated: the method will be removed in the next major version, - // use a CallRequest object + Do() instead. - CallTyped(functionName string, args interface{}, result interface{}) error - // Deprecated: the method will be removed in the next major version, - // use a Call16Request object + Do() instead. - Call16Typed(functionName string, args interface{}, result interface{}) error - // Deprecated: the method will be removed in the next major version, - // use a Call17Request object + Do() instead. - Call17Typed(functionName string, args interface{}, result interface{}) error - // Deprecated: the method will be removed in the next major version, - // use an EvalRequest object + Do() instead. - EvalTyped(expr string, args interface{}, result interface{}) error - // Deprecated: the method will be removed in the next major version, - // use an ExecuteRequest object + Do() instead. - ExecuteTyped(expr string, args interface{}, - result interface{}) (SQLInfo, []ColumnMetaData, error) - - // Deprecated: the method will be removed in the next major version, - // use a SelectRequest object + Do() instead. - SelectAsync(space, index interface{}, offset, limit uint32, iterator Iter, - key interface{}) *Future - // Deprecated: the method will be removed in the next major version, - // use an InsertRequest object + Do() instead. - InsertAsync(space interface{}, tuple interface{}) *Future - // Deprecated: the method will be removed in the next major version, - // use a ReplaceRequest object + Do() instead. - ReplaceAsync(space interface{}, tuple interface{}) *Future - // Deprecated: the method will be removed in the next major version, - // use a DeleteRequest object + Do() instead. - DeleteAsync(space, index interface{}, key interface{}) *Future - // Deprecated: the method will be removed in the next major version, - // use a UpdateRequest object + Do() instead. - UpdateAsync(space, index interface{}, key interface{}, ops *Operations) *Future - // Deprecated: the method will be removed in the next major version, - // use a UpsertRequest object + Do() instead. - UpsertAsync(space interface{}, tuple interface{}, ops *Operations) *Future - // Deprecated: the method will be removed in the next major version, - // use a CallRequest object + Do() instead. - CallAsync(functionName string, args interface{}) *Future - // Deprecated: the method will be removed in the next major version, - // use a Call16Request object + Do() instead. - Call16Async(functionName string, args interface{}) *Future - // Deprecated: the method will be removed in the next major version, - // use a Call17Request object + Do() instead. - Call17Async(functionName string, args interface{}) *Future - // Deprecated: the method will be removed in the next major version, - // use an EvalRequest object + Do() instead. - EvalAsync(expr string, args interface{}) *Future - // Deprecated: the method will be removed in the next major version, - // use an ExecuteRequest object + Do() instead. - ExecuteAsync(expr string, args interface{}) *Future } diff --git a/pool/connection_pool.go b/pool/connection_pool.go index 572fe6cde..9d020ba8e 100644 --- a/pool/connection_pool.go +++ b/pool/connection_pool.go @@ -443,510 +443,6 @@ func (p *ConnectionPool) GetInfo() map[string]ConnectionInfo { return info } -// Ping sends empty request to Tarantool to check connection. -// -// Deprecated: the method will be removed in the next major version, -// use a PingRequest object + Do() instead. -func (p *ConnectionPool) Ping(userMode Mode) ([]interface{}, error) { - conn, err := p.getNextConnection(userMode) - if err != nil { - return nil, err - } - - return conn.Ping() -} - -// Select performs select to box space. -// -// Deprecated: the method will be removed in the next major version, -// use a SelectRequest object + Do() instead. -func (p *ConnectionPool) Select(space, index interface{}, - offset, limit uint32, - iterator tarantool.Iter, key interface{}, userMode ...Mode) ([]interface{}, error) { - conn, err := p.getConnByMode(ANY, userMode) - if err != nil { - return nil, err - } - - return conn.Select(space, index, offset, limit, iterator, key) -} - -// Insert performs insertion to box space. -// Tarantool will reject Insert when tuple with same primary key exists. -// -// Deprecated: the method will be removed in the next major version, -// use an InsertRequest object + Do() instead. -func (p *ConnectionPool) Insert(space interface{}, tuple interface{}, - userMode ...Mode) ([]interface{}, error) { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return nil, err - } - - return conn.Insert(space, tuple) -} - -// Replace performs "insert or replace" action to box space. -// If tuple with same primary key exists, it will be replaced. -// -// Deprecated: the method will be removed in the next major version, -// use a ReplaceRequest object + Do() instead. -func (p *ConnectionPool) Replace(space interface{}, tuple interface{}, - userMode ...Mode) ([]interface{}, error) { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return nil, err - } - - return conn.Replace(space, tuple) -} - -// Delete performs deletion of a tuple by key. -// Result will contain array with deleted tuple. -// -// Deprecated: the method will be removed in the next major version, -// use a DeleteRequest object + Do() instead. -func (p *ConnectionPool) Delete(space, index interface{}, key interface{}, - userMode ...Mode) ([]interface{}, error) { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return nil, err - } - - return conn.Delete(space, index, key) -} - -// Update performs update of a tuple by key. -// Result will contain array with updated tuple. -// -// Deprecated: the method will be removed in the next major version, -// use a UpdateRequest object + Do() instead. -func (p *ConnectionPool) Update(space, index interface{}, key interface{}, - ops *tarantool.Operations, userMode ...Mode) ([]interface{}, error) { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return nil, err - } - - return conn.Update(space, index, key, ops) -} - -// Upsert performs "update or insert" action of a tuple by key. -// Result will not contain any tuple. -// -// Deprecated: the method will be removed in the next major version, -// use a UpsertRequest object + Do() instead. -func (p *ConnectionPool) Upsert(space interface{}, tuple interface{}, - ops *tarantool.Operations, userMode ...Mode) ([]interface{}, error) { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return nil, err - } - - return conn.Upsert(space, tuple, ops) -} - -// Call calls registered Tarantool function. -// It uses request code for Tarantool >= 1.7, result is an array. -// -// Deprecated: the method will be removed in the next major version, -// use a CallRequest object + Do() instead. -func (p *ConnectionPool) Call(functionName string, args interface{}, - userMode Mode) ([]interface{}, error) { - conn, err := p.getNextConnection(userMode) - if err != nil { - return nil, err - } - - return conn.Call(functionName, args) -} - -// Call16 calls registered Tarantool function. -// It uses request code for Tarantool 1.6, result is an array of arrays. -// Deprecated since Tarantool 1.7.2. -// -// Deprecated: the method will be removed in the next major version, -// use a Call16Request object + Do() instead. -func (p *ConnectionPool) Call16(functionName string, args interface{}, - userMode Mode) ([]interface{}, error) { - conn, err := p.getNextConnection(userMode) - if err != nil { - return nil, err - } - - return conn.Call16(functionName, args) -} - -// Call17 calls registered Tarantool function. -// It uses request code for Tarantool >= 1.7, result is an array. -// -// Deprecated: the method will be removed in the next major version, -// use a Call17Request object + Do() instead. -func (p *ConnectionPool) Call17(functionName string, args interface{}, - userMode Mode) ([]interface{}, error) { - conn, err := p.getNextConnection(userMode) - if err != nil { - return nil, err - } - - return conn.Call17(functionName, args) -} - -// Eval passes lua expression for evaluation. -// -// Deprecated: the method will be removed in the next major version, -// use an EvalRequest object + Do() instead. -func (p *ConnectionPool) Eval(expr string, args interface{}, - userMode Mode) ([]interface{}, error) { - conn, err := p.getNextConnection(userMode) - if err != nil { - return nil, err - } - - return conn.Eval(expr, args) -} - -// Execute passes sql expression to Tarantool for execution. -// -// Deprecated: the method will be removed in the next major version, -// use an ExecuteRequest object + Do() instead. -func (p *ConnectionPool) Execute(expr string, args interface{}, - userMode Mode) ([]interface{}, error) { - conn, err := p.getNextConnection(userMode) - if err != nil { - return nil, err - } - - return conn.Execute(expr, args) -} - -// GetTyped performs select (with limit = 1 and offset = 0) -// to box space and fills typed result. -// -// Deprecated: the method will be removed in the next major version, -// use a SelectRequest object + Do() instead. -func (p *ConnectionPool) GetTyped(space, index interface{}, key interface{}, result interface{}, - userMode ...Mode) error { - conn, err := p.getConnByMode(ANY, userMode) - if err != nil { - return err - } - - return conn.GetTyped(space, index, key, result) -} - -// SelectTyped performs select to box space and fills typed result. -// -// Deprecated: the method will be removed in the next major version, -// use a SelectRequest object + Do() instead. -func (p *ConnectionPool) SelectTyped(space, index interface{}, - offset, limit uint32, - iterator tarantool.Iter, key interface{}, result interface{}, userMode ...Mode) error { - conn, err := p.getConnByMode(ANY, userMode) - if err != nil { - return err - } - - return conn.SelectTyped(space, index, offset, limit, iterator, key, result) -} - -// InsertTyped performs insertion to box space. -// Tarantool will reject Insert when tuple with same primary key exists. -// -// Deprecated: the method will be removed in the next major version, -// use an InsertRequest object + Do() instead. -func (p *ConnectionPool) InsertTyped(space interface{}, tuple interface{}, result interface{}, - userMode ...Mode) error { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return err - } - - return conn.InsertTyped(space, tuple, result) -} - -// ReplaceTyped performs "insert or replace" action to box space. -// If tuple with same primary key exists, it will be replaced. -// -// Deprecated: the method will be removed in the next major version, -// use a ReplaceRequest object + Do() instead. -func (p *ConnectionPool) ReplaceTyped(space interface{}, tuple interface{}, result interface{}, - userMode ...Mode) error { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return err - } - - return conn.ReplaceTyped(space, tuple, result) -} - -// DeleteTyped performs deletion of a tuple by key and fills result with deleted tuple. -// -// Deprecated: the method will be removed in the next major version, -// use a DeleteRequest object + Do() instead. -func (p *ConnectionPool) DeleteTyped(space, index interface{}, key interface{}, result interface{}, - userMode ...Mode) error { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return err - } - - return conn.DeleteTyped(space, index, key, result) -} - -// UpdateTyped performs update of a tuple by key and fills result with updated tuple. -// -// Deprecated: the method will be removed in the next major version, -// use a UpdateRequest object + Do() instead. -func (p *ConnectionPool) UpdateTyped(space, index interface{}, key interface{}, - ops *tarantool.Operations, result interface{}, userMode ...Mode) error { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return err - } - - return conn.UpdateTyped(space, index, key, ops, result) -} - -// CallTyped calls registered function. -// It uses request code for Tarantool >= 1.7, result is an array. -// -// Deprecated: the method will be removed in the next major version, -// use a CallRequest object + Do() instead. -func (p *ConnectionPool) CallTyped(functionName string, args interface{}, result interface{}, - userMode Mode) error { - conn, err := p.getNextConnection(userMode) - if err != nil { - return err - } - - return conn.CallTyped(functionName, args, result) -} - -// Call16Typed calls registered function. -// It uses request code for Tarantool 1.6, result is an array of arrays. -// Deprecated since Tarantool 1.7.2. -// -// Deprecated: the method will be removed in the next major version, -// use a Call16Request object + Do() instead. -func (p *ConnectionPool) Call16Typed(functionName string, args interface{}, result interface{}, - userMode Mode) error { - conn, err := p.getNextConnection(userMode) - if err != nil { - return err - } - - return conn.Call16Typed(functionName, args, result) -} - -// Call17Typed calls registered function. -// It uses request code for Tarantool >= 1.7, result is an array. -// -// Deprecated: the method will be removed in the next major version, -// use a Call17Request object + Do() instead. -func (p *ConnectionPool) Call17Typed(functionName string, args interface{}, result interface{}, - userMode Mode) error { - conn, err := p.getNextConnection(userMode) - if err != nil { - return err - } - - return conn.Call17Typed(functionName, args, result) -} - -// EvalTyped passes lua expression for evaluation. -// -// Deprecated: the method will be removed in the next major version, -// use an EvalRequest object + Do() instead. -func (p *ConnectionPool) EvalTyped(expr string, args interface{}, result interface{}, - userMode Mode) error { - conn, err := p.getNextConnection(userMode) - if err != nil { - return err - } - - return conn.EvalTyped(expr, args, result) -} - -// ExecuteTyped passes sql expression to Tarantool for execution. -// -// Deprecated: the method will be removed in the next major version, -// use an ExecuteRequest object + Do() instead. -func (p *ConnectionPool) ExecuteTyped(expr string, args interface{}, result interface{}, - userMode Mode) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) { - conn, err := p.getNextConnection(userMode) - if err != nil { - return tarantool.SQLInfo{}, nil, err - } - - return conn.ExecuteTyped(expr, args, result) -} - -// SelectAsync sends select request to Tarantool and returns Future. -// -// Deprecated: the method will be removed in the next major version, -// use a SelectRequest object + Do() instead. -func (p *ConnectionPool) SelectAsync(space, index interface{}, - offset, limit uint32, - iterator tarantool.Iter, key interface{}, userMode ...Mode) *tarantool.Future { - conn, err := p.getConnByMode(ANY, userMode) - if err != nil { - return newErrorFuture(err) - } - - return conn.SelectAsync(space, index, offset, limit, iterator, key) -} - -// InsertAsync sends insert action to Tarantool and returns Future. -// Tarantool will reject Insert when tuple with same primary key exists. -// -// Deprecated: the method will be removed in the next major version, -// use an InsertRequest object + Do() instead. -func (p *ConnectionPool) InsertAsync(space interface{}, tuple interface{}, - userMode ...Mode) *tarantool.Future { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return newErrorFuture(err) - } - - return conn.InsertAsync(space, tuple) -} - -// ReplaceAsync sends "insert or replace" action to Tarantool and returns Future. -// If tuple with same primary key exists, it will be replaced. -// -// Deprecated: the method will be removed in the next major version, -// use a ReplaceRequest object + Do() instead. -func (p *ConnectionPool) ReplaceAsync(space interface{}, tuple interface{}, - userMode ...Mode) *tarantool.Future { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return newErrorFuture(err) - } - - return conn.ReplaceAsync(space, tuple) -} - -// DeleteAsync sends deletion action to Tarantool and returns Future. -// Future's result will contain array with deleted tuple. -// -// Deprecated: the method will be removed in the next major version, -// use a DeleteRequest object + Do() instead. -func (p *ConnectionPool) DeleteAsync(space, index interface{}, key interface{}, - userMode ...Mode) *tarantool.Future { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return newErrorFuture(err) - } - - return conn.DeleteAsync(space, index, key) -} - -// UpdateAsync sends deletion of a tuple by key and returns Future. -// Future's result will contain array with updated tuple. -// -// Deprecated: the method will be removed in the next major version, -// use a UpdateRequest object + Do() instead. -func (p *ConnectionPool) UpdateAsync(space, index interface{}, key interface{}, - ops *tarantool.Operations, userMode ...Mode) *tarantool.Future { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return newErrorFuture(err) - } - - return conn.UpdateAsync(space, index, key, ops) -} - -// UpsertAsync sends "update or insert" action to Tarantool and returns Future. -// Future's sesult will not contain any tuple. -// -// Deprecated: the method will be removed in the next major version, -// use a UpsertRequest object + Do() instead. -func (p *ConnectionPool) UpsertAsync(space interface{}, tuple interface{}, - ops *tarantool.Operations, userMode ...Mode) *tarantool.Future { - conn, err := p.getConnByMode(RW, userMode) - if err != nil { - return newErrorFuture(err) - } - - return conn.UpsertAsync(space, tuple, ops) -} - -// CallAsync sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool >= 1.7, future's result is an array. -// -// Deprecated: the method will be removed in the next major version, -// use a CallRequest object + Do() instead. -func (p *ConnectionPool) CallAsync(functionName string, args interface{}, - userMode Mode) *tarantool.Future { - conn, err := p.getNextConnection(userMode) - if err != nil { - return newErrorFuture(err) - } - - return conn.CallAsync(functionName, args) -} - -// Call16Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool 1.6, so future's result is an array of arrays. -// Deprecated since Tarantool 1.7.2. -// -// Deprecated: the method will be removed in the next major version, -// use a Call16Request object + Do() instead. -func (p *ConnectionPool) Call16Async(functionName string, args interface{}, - userMode Mode) *tarantool.Future { - conn, err := p.getNextConnection(userMode) - if err != nil { - return newErrorFuture(err) - } - - return conn.Call16Async(functionName, args) -} - -// Call17Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool >= 1.7, future's result is an array. -// -// Deprecated: the method will be removed in the next major version, -// use a Call17Request object + Do() instead. -func (p *ConnectionPool) Call17Async(functionName string, args interface{}, - userMode Mode) *tarantool.Future { - conn, err := p.getNextConnection(userMode) - if err != nil { - return newErrorFuture(err) - } - - return conn.Call17Async(functionName, args) -} - -// EvalAsync sends a lua expression for evaluation and returns Future. -// -// Deprecated: the method will be removed in the next major version, -// use an EvalRequest object + Do() instead. -func (p *ConnectionPool) EvalAsync(expr string, args interface{}, - userMode Mode) *tarantool.Future { - conn, err := p.getNextConnection(userMode) - if err != nil { - return newErrorFuture(err) - } - - return conn.EvalAsync(expr, args) -} - -// ExecuteAsync sends sql expression to Tarantool for execution and returns -// Future. -// -// Deprecated: the method will be removed in the next major version, -// use an ExecuteRequest object + Do() instead. -func (p *ConnectionPool) ExecuteAsync(expr string, args interface{}, - userMode Mode) *tarantool.Future { - conn, err := p.getNextConnection(userMode) - if err != nil { - return newErrorFuture(err) - } - - return conn.ExecuteAsync(expr, args) -} - // NewStream creates new Stream object for connection selected // by userMode from pool. // @@ -1514,20 +1010,6 @@ func (p *ConnectionPool) getNextConnection(mode Mode) (*tarantool.Connection, er return nil, ErrNoHealthyInstance } -func (p *ConnectionPool) getConnByMode(defaultMode Mode, - userMode []Mode) (*tarantool.Connection, error) { - if len(userMode) > 1 { - return nil, ErrTooManyArgs - } - - mode := defaultMode - if len(userMode) > 0 { - mode = userMode[0] - } - - return p.getNextConnection(mode) -} - func newErrorFuture(err error) *tarantool.Future { fut := tarantool.NewFuture(nil) fut.SetError(err) diff --git a/pool/connection_pool_test.go b/pool/connection_pool_test.go index e0fcc8c47..f4a0376d4 100644 --- a/pool/connection_pool_test.go +++ b/pool/connection_pool_test.go @@ -1130,9 +1130,11 @@ func TestConnectionHandlerOpenUpdateClose(t *testing.T) { require.Nilf(t, err, "failed to connect") require.NotNilf(t, connPool, "conn is nil after Connect") - _, err = connPool.Call17("box.cfg", []interface{}{map[string]bool{ - "read_only": true, - }}, pool.RW) + _, err = connPool.Do(tarantool.NewCall17Request("box.cfg"). + Args([]interface{}{map[string]bool{ + "read_only": true, + }}), + pool.RW).GetResponse() require.Nilf(t, err, "failed to make ro") for i := 0; i < 100; i++ { @@ -1271,9 +1273,10 @@ func TestConnectionHandlerUpdateError(t *testing.T) { require.Truef(t, connected, "should be connected") for i := 0; i < len(poolServers); i++ { - _, err = connPool.Call17("box.cfg", []interface{}{map[string]bool{ - "read_only": true, - }}, pool.RW) + _, err = connPool.Do(tarantool.NewCall17Request("box.cfg"). + Args([]interface{}{map[string]bool{ + "read_only": true, + }}), pool.RW).Get() require.Nilf(t, err, "failed to make ro") } @@ -1403,8 +1406,8 @@ func TestRequestOnClosed(t *testing.T) { defaultCountRetry, defaultTimeoutRetry) require.Nil(t, err) - _, err = connPool.Ping(pool.ANY) - require.NotNilf(t, err, "err is nil after Ping") + _, err = connPool.Do(tarantool.NewPingRequest(), pool.ANY).Get() + require.NotNilf(t, err, "err is nil after Do with PingRequest") err = test_helpers.RestartTarantool(helpInstances[0]) require.Nilf(t, err, "failed to restart tarantool") @@ -1413,7 +1416,7 @@ func TestRequestOnClosed(t *testing.T) { require.Nilf(t, err, "failed to restart tarantool") } -func TestCall(t *testing.T) { +func TestDoWithCallRequest(t *testing.T) { roles := []bool{false, true, false, false, true} ctx, cancel := test_helpers.GetPoolConnectContext() @@ -1429,10 +1432,13 @@ func TestCall(t *testing.T) { defer connPool.Close() // PreferRO - data, err := connPool.Call("box.info", []interface{}{}, pool.PreferRO) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, data, "response is nil after Call") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") + data, err := connPool.Do( + tarantool.NewCallRequest("box.info"). + Args([]interface{}{}), + pool.PreferRO).Get() + require.Nilf(t, err, "failed to Do with CallRequest") + require.NotNilf(t, data, "response is nil after Do with CallRequest") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with CallRequest") val := data[0].(map[interface{}]interface{})["ro"] ro, ok := val.(bool) @@ -1440,10 +1446,13 @@ func TestCall(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `PreferRO`") // PreferRW - data, err = connPool.Call("box.info", []interface{}{}, pool.PreferRW) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, data, "response is nil after Call") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") + data, err = connPool.Do( + tarantool.NewCallRequest("box.info"). + Args([]interface{}{}), + pool.PreferRW).Get() + require.Nilf(t, err, "failed to Do with CallRequest") + require.NotNilf(t, data, "response is nil after Do with CallRequest") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with CallRequest") val = data[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) @@ -1451,10 +1460,13 @@ func TestCall(t *testing.T) { require.Falsef(t, ro, "expected `false` with mode `PreferRW`") // RO - data, err = connPool.Call("box.info", []interface{}{}, pool.RO) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, data, "response is nil after Call") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") + data, err = connPool.Do( + tarantool.NewCallRequest("box.info"). + Args([]interface{}{}), + pool.RO).Get() + require.Nilf(t, err, "failed to Do with CallRequest") + require.NotNilf(t, data, "response is nil after Do with CallRequest") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with CallRequest") val = data[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) @@ -1462,10 +1474,13 @@ func TestCall(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `RO`") // RW - data, err = connPool.Call("box.info", []interface{}{}, pool.RW) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, data, "response is nil after Call") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") + data, err = connPool.Do( + tarantool.NewCallRequest("box.info"). + Args([]interface{}{}), + pool.RW).Get() + require.Nilf(t, err, "failed to Do with CallRequest") + require.NotNilf(t, data, "response is nil after Do with CallRequest") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with CallRequest") val = data[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) @@ -1473,7 +1488,7 @@ func TestCall(t *testing.T) { require.Falsef(t, ro, "expected `false` with mode `RW`") } -func TestCall16(t *testing.T) { +func TestDoWithCall16Request(t *testing.T) { roles := []bool{false, true, false, false, true} ctx, cancel := test_helpers.GetPoolConnectContext() @@ -1489,10 +1504,13 @@ func TestCall16(t *testing.T) { defer connPool.Close() // PreferRO - data, err := connPool.Call16("box.info", []interface{}{}, pool.PreferRO) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, data, "response is nil after Call") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") + data, err := connPool.Do( + tarantool.NewCall16Request("box.info"). + Args([]interface{}{}), + pool.PreferRO).Get() + require.Nilf(t, err, "failed to Do with Call16Request") + require.NotNilf(t, data, "response is nil after Do with Call16Request") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with Call16Request") val := data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] ro, ok := val.(bool) @@ -1500,10 +1518,13 @@ func TestCall16(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `PreferRO`") // PreferRW - data, err = connPool.Call16("box.info", []interface{}{}, pool.PreferRW) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, data, "response is nil after Call") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") + data, err = connPool.Do( + tarantool.NewCall16Request("box.info"). + Args([]interface{}{}), + pool.PreferRW).Get() + require.Nilf(t, err, "failed to Do with Call16Request") + require.NotNilf(t, data, "response is nil after Do with Call16Request") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with Call16Request") val = data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) @@ -1511,10 +1532,13 @@ func TestCall16(t *testing.T) { require.Falsef(t, ro, "expected `false` with mode `PreferRW`") // RO - data, err = connPool.Call16("box.info", []interface{}{}, pool.RO) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, data, "response is nil after Call") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") + data, err = connPool.Do( + tarantool.NewCall16Request("box.info"). + Args([]interface{}{}), + pool.RO).Get() + require.Nilf(t, err, "failed to Do with Call16Request") + require.NotNilf(t, data, "response is nil after Do with Call16Request") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with Call16Request") val = data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) @@ -1522,10 +1546,13 @@ func TestCall16(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `RO`") // RW - data, err = connPool.Call16("box.info", []interface{}{}, pool.RW) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, data, "response is nil after Call") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") + data, err = connPool.Do( + tarantool.NewCall16Request("box.info"). + Args([]interface{}{}), + pool.RW).Get() + require.Nilf(t, err, "failed to Do with Call16Request") + require.NotNilf(t, data, "response is nil after Do with Call16Request") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with Call16Request") val = data[0].([]interface{})[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) @@ -1533,7 +1560,7 @@ func TestCall16(t *testing.T) { require.Falsef(t, ro, "expected `false` with mode `RW`") } -func TestCall17(t *testing.T) { +func TestDoWithCall17Request(t *testing.T) { roles := []bool{false, true, false, false, true} ctx, cancel := test_helpers.GetPoolConnectContext() @@ -1549,10 +1576,13 @@ func TestCall17(t *testing.T) { defer connPool.Close() // PreferRO - data, err := connPool.Call17("box.info", []interface{}{}, pool.PreferRO) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, data, "response is nil after Call") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") + data, err := connPool.Do( + tarantool.NewCall17Request("box.info"). + Args([]interface{}{}), + pool.PreferRO).Get() + require.Nilf(t, err, "failed to Do with Call17Request") + require.NotNilf(t, data, "response is nil after Do with Call17Request") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with Call17Request") val := data[0].(map[interface{}]interface{})["ro"] ro, ok := val.(bool) @@ -1560,10 +1590,13 @@ func TestCall17(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `PreferRO`") // PreferRW - data, err = connPool.Call17("box.info", []interface{}{}, pool.PreferRW) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, data, "response is nil after Call") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") + data, err = connPool.Do( + tarantool.NewCall17Request("box.info"). + Args([]interface{}{}), + pool.PreferRW).Get() + require.Nilf(t, err, "failed to Do with Call17Request") + require.NotNilf(t, data, "response is nil after Do with Call17Request") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with Call17Request") val = data[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) @@ -1571,10 +1604,13 @@ func TestCall17(t *testing.T) { require.Falsef(t, ro, "expected `false` with mode `PreferRW`") // RO - data, err = connPool.Call17("box.info", []interface{}{}, pool.RO) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, data, "response is nil after Call") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") + data, err = connPool.Do( + tarantool.NewCall17Request("box.info"). + Args([]interface{}{}), + pool.RO).Get() + require.Nilf(t, err, "failed to Do with Call17Request") + require.NotNilf(t, data, "response is nil after Do with Call17Request") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with Call17Request") val = data[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) @@ -1582,10 +1618,13 @@ func TestCall17(t *testing.T) { require.Truef(t, ro, "expected `true` with mode `RO`") // RW - data, err = connPool.Call17("box.info", []interface{}{}, pool.RW) - require.Nilf(t, err, "failed to Call") - require.NotNilf(t, data, "response is nil after Call") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Call") + data, err = connPool.Do( + tarantool.NewCall17Request("box.info"). + Args([]interface{}{}), + pool.RW).Get() + require.Nilf(t, err, "failed to Do with Call17Request") + require.NotNilf(t, data, "response is nil after Do with Call17Request") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with Call17Request") val = data[0].(map[interface{}]interface{})["ro"] ro, ok = val.(bool) @@ -1593,7 +1632,7 @@ func TestCall17(t *testing.T) { require.Falsef(t, ro, "expected `false` with mode `RW`") } -func TestEval(t *testing.T) { +func TestDoWithEvalRequest(t *testing.T) { roles := []bool{false, true, false, false, true} ctx, cancel := test_helpers.GetPoolConnectContext() @@ -1609,40 +1648,52 @@ func TestEval(t *testing.T) { defer connPool.Close() // PreferRO - data, err := connPool.Eval("return box.info().ro", []interface{}{}, pool.PreferRO) - require.Nilf(t, err, "failed to Eval") - require.NotNilf(t, data, "response is nil after Eval") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Eval") + data, err := connPool.Do( + tarantool.NewEvalRequest("return box.info().ro"). + Args([]interface{}{}), + pool.PreferRO).Get() + require.Nilf(t, err, "failed to Do with EvalRequest") + require.NotNilf(t, data, "response is nil after Do with EvalRequest") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with EvalRequest") val, ok := data[0].(bool) require.Truef(t, ok, "expected `true` with mode `PreferRO`") require.Truef(t, val, "expected `true` with mode `PreferRO`") // PreferRW - data, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.PreferRW) - require.Nilf(t, err, "failed to Eval") - require.NotNilf(t, data, "response is nil after Eval") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Eval") + data, err = connPool.Do( + tarantool.NewEvalRequest("return box.info().ro"). + Args([]interface{}{}), + pool.PreferRW).Get() + require.Nilf(t, err, "failed to Do with EvalRequest") + require.NotNilf(t, data, "response is nil after Do with EvalRequest") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with EvalRequest") val, ok = data[0].(bool) require.Truef(t, ok, "expected `false` with mode `PreferRW`") require.Falsef(t, val, "expected `false` with mode `PreferRW`") // RO - data, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.RO) - require.Nilf(t, err, "failed to Eval") - require.NotNilf(t, data, "response is nil after Eval") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Eval") + data, err = connPool.Do( + tarantool.NewEvalRequest("return box.info().ro"). + Args([]interface{}{}), + pool.RO).Get() + require.Nilf(t, err, "failed to Do with EvalRequest") + require.NotNilf(t, data, "response is nil after Do with EvalRequest") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with EvalRequest") val, ok = data[0].(bool) require.Truef(t, ok, "expected `true` with mode `RO`") require.Truef(t, val, "expected `true` with mode `RO`") // RW - data, err = connPool.Eval("return box.info().ro", []interface{}{}, pool.RW) - require.Nilf(t, err, "failed to Eval") - require.NotNilf(t, data, "response is nil after Eval") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Eval") + data, err = connPool.Do( + tarantool.NewEvalRequest("return box.info().ro"). + Args([]interface{}{}), + pool.RW).Get() + require.Nilf(t, err, "failed to Do with EvalRequest") + require.NotNilf(t, data, "response is nil after Do with EvalRequest") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with EvalRequest") val, ok = data[0].(bool) require.Truef(t, ok, "expected `false` with mode `RW`") @@ -1672,7 +1723,7 @@ func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { return nil } -func TestExecute(t *testing.T) { +func TestDoWithExecuteRequest(t *testing.T) { test_helpers.SkipIfSQLUnsupported(t) roles := []bool{false, true, false, false, true} @@ -1690,26 +1741,17 @@ func TestExecute(t *testing.T) { defer connPool.Close() request := "SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0 == 1;" - // Execute - data, err := connPool.Execute(request, []interface{}{}, pool.ANY) - require.Nilf(t, err, "failed to Execute") - require.NotNilf(t, data, "response is nil after Execute") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Execute") - require.Equalf(t, len(data[0].([]interface{})), 2, "unexpected response") - - // ExecuteTyped mem := []Member{} - info, _, err := connPool.ExecuteTyped(request, []interface{}{}, &mem, pool.ANY) - require.Nilf(t, err, "failed to ExecuteTyped") - require.Equalf(t, info.AffectedCount, uint64(0), "unexpected info.AffectedCount") - require.Equalf(t, len(mem), 1, "wrong count of results") - - // ExecuteAsync - fut := connPool.ExecuteAsync(request, []interface{}{}, pool.ANY) - data, err = fut.Get() - require.Nilf(t, err, "failed to ExecuteAsync") - require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after ExecuteAsync") + + fut := connPool.Do(tarantool.NewExecuteRequest(request).Args([]interface{}{}), pool.ANY) + data, err := fut.Get() + require.Nilf(t, err, "failed to Do with ExecuteRequest") + require.NotNilf(t, data, "response is nil after Execute") + require.GreaterOrEqualf(t, len(data), 1, "response.Data is empty after Do with ExecuteRequest") require.Equalf(t, len(data[0].([]interface{})), 2, "unexpected response") + err = fut.GetTyped(&mem) + require.Nilf(t, err, "Unable to GetTyped of fut") + require.Equalf(t, len(mem), 1, "wrong count of result") } func TestRoundRobinStrategy(t *testing.T) { @@ -1829,8 +1871,11 @@ func TestRoundRobinStrategy_NoReplica(t *testing.T) { defer connPool.Close() // RO - _, err = connPool.Eval("return box.cfg.listen", []interface{}{}, pool.RO) - require.NotNilf(t, err, "expected to fail after Eval, but error is nil") + _, err = connPool.Do( + tarantool.NewEvalRequest("return box.cfg.listen"). + Args([]interface{}{}), + pool.RO).Get() + require.NotNilf(t, err, "expected to fail after Do with EvalRequest, but error is nil") require.Equal(t, "can't find ro instance in pool", err.Error()) // ANY @@ -1903,8 +1948,11 @@ func TestRoundRobinStrategy_NoMaster(t *testing.T) { defer connPool.Close() // RW - _, err = connPool.Eval("return box.cfg.listen", []interface{}{}, pool.RW) - require.NotNilf(t, err, "expected to fail after Eval, but error is nil") + _, err = connPool.Do( + tarantool.NewEvalRequest("return box.cfg.listen"). + Args([]interface{}{}), + pool.RW).Get() + require.NotNilf(t, err, "expected to fail after Do with EvalRequest, but error is nil") require.Equal(t, "can't find rw instance in pool", err.Error()) // ANY @@ -2122,7 +2170,7 @@ func TestUpdateInstancesRoles(t *testing.T) { require.Nil(t, err) } -func TestInsert(t *testing.T) { +func TestDoWithInsertRequest(t *testing.T) { roles := []bool{true, true, false, true, true} ctx, cancel := test_helpers.GetPoolConnectContext() @@ -2137,8 +2185,10 @@ func TestInsert(t *testing.T) { defer connPool.Close() - // Mode is `RW` by default, we have only one RW instance (servers[2]) - data, err := connPool.Insert(spaceName, []interface{}{"rw_insert_key", "rw_insert_value"}) + // RW + data, err := connPool.Do(tarantool.NewInsertRequest(spaceName). + Tuple([]interface{}{"rw_insert_key", "rw_insert_value"}), + pool.RW).Get() require.Nilf(t, err, "failed to Insert") require.NotNilf(t, data, "response is nil after Insert") require.Equalf(t, len(data), 1, "response Body len != 1 after Insert") @@ -2182,8 +2232,8 @@ func TestInsert(t *testing.T) { require.Equalf(t, "rw_insert_value", value, "unexpected body of Select (1)") // PreferRW - data, err = connPool.Insert(spaceName, - []interface{}{"preferRW_insert_key", "preferRW_insert_value"}) + data, err = connPool.Do(tarantool.NewInsertRequest(spaceName).Tuple( + []interface{}{"preferRW_insert_key", "preferRW_insert_value"}), pool.PreferRW).Get() require.Nilf(t, err, "failed to Insert") require.NotNilf(t, data, "response is nil after Insert") require.Equalf(t, len(data), 1, "response Body len != 1 after Insert") @@ -2222,7 +2272,7 @@ func TestInsert(t *testing.T) { require.Equalf(t, "preferRW_insert_value", value, "unexpected body of Select (1)") } -func TestDelete(t *testing.T) { +func TestDoWithDeleteRequest(t *testing.T) { roles := []bool{true, true, false, true, true} ctx, cancel := test_helpers.GetPoolConnectContext() @@ -2259,23 +2309,26 @@ func TestDelete(t *testing.T) { require.Truef(t, ok, "unexpected body of Insert (1)") require.Equalf(t, "delete_value", value, "unexpected body of Insert (1)") - // Mode is `RW` by default, we have only one RW instance (servers[2]) - data, err = connPool.Delete(spaceName, indexNo, []interface{}{"delete_key"}) - require.Nilf(t, err, "failed to Delete") - require.NotNilf(t, data, "response is nil after Delete") - require.Equalf(t, len(data), 1, "response Body len != 1 after Delete") + data, err = connPool.Do( + tarantool.NewDeleteRequest(spaceName). + Index(indexNo). + Key([]interface{}{"delete_key"}), + pool.RW).Get() + require.Nilf(t, err, "failed to Do with DeleteRequest") + require.NotNilf(t, data, "response is nil after Do with DeleteRequest") + require.Equalf(t, len(data), 1, "response Body len != 1 after Do with DeleteRequest") tpl, ok = data[0].([]interface{}) - require.Truef(t, ok, "unexpected body of Delete") - require.Equalf(t, 2, len(tpl), "unexpected body of Delete") + require.Truef(t, ok, "unexpected body of Do with DeleteRequest") + require.Equalf(t, 2, len(tpl), "unexpected body of Do with DeleteRequest") key, ok = tpl[0].(string) - require.Truef(t, ok, "unexpected body of Delete (0)") - require.Equalf(t, "delete_key", key, "unexpected body of Delete (0)") + require.Truef(t, ok, "unexpected body of Do with DeleteRequest (0)") + require.Equalf(t, "delete_key", key, "unexpected body of Do with DeleteRequest (0)") value, ok = tpl[1].(string) - require.Truef(t, ok, "unexpected body of Delete (1)") - require.Equalf(t, "delete_value", value, "unexpected body of Delete (1)") + require.Truef(t, ok, "unexpected body of Do with DeleteRequest (1)") + require.Equalf(t, "delete_value", value, "unexpected body of Do with DeleteRequest (1)") sel := tarantool.NewSelectRequest(spaceNo). Index(indexNo). @@ -2287,7 +2340,7 @@ func TestDelete(t *testing.T) { require.Equalf(t, 0, len(data), "response Body len != 0 after Select") } -func TestUpsert(t *testing.T) { +func TestDoWithUpsertRequest(t *testing.T) { roles := []bool{true, true, false, true, true} ctx, cancel := test_helpers.GetPoolConnectContext() @@ -2307,10 +2360,10 @@ func TestUpsert(t *testing.T) { conn := test_helpers.ConnectWithValidation(t, makeDialer(servers[2]), connOpts) defer conn.Close() - // Mode is `RW` by default, we have only one RW instance (servers[2]) - data, err := connPool.Upsert(spaceName, - []interface{}{"upsert_key", "upsert_value"}, - tarantool.NewOperations().Assign(1, "new_value")) + // RW + data, err := connPool.Do(tarantool.NewUpsertRequest(spaceName).Tuple( + []interface{}{"upsert_key", "upsert_value"}).Operations( + tarantool.NewOperations().Assign(1, "new_value")), pool.RW).Get() require.Nilf(t, err, "failed to Upsert") require.NotNilf(t, data, "response is nil after Upsert") @@ -2336,9 +2389,9 @@ func TestUpsert(t *testing.T) { require.Equalf(t, "upsert_value", value, "unexpected body of Select (1)") // PreferRW - data, err = connPool.Upsert( - spaceName, []interface{}{"upsert_key", "upsert_value"}, - tarantool.NewOperations().Assign(1, "new_value"), pool.PreferRW) + data, err = connPool.Do(tarantool.NewUpsertRequest( + spaceName).Tuple([]interface{}{"upsert_key", "upsert_value"}).Operations( + tarantool.NewOperations().Assign(1, "new_value")), pool.PreferRW).Get() require.Nilf(t, err, "failed to Upsert") require.NotNilf(t, data, "response is nil after Upsert") @@ -2360,7 +2413,7 @@ func TestUpsert(t *testing.T) { require.Equalf(t, "new_value", value, "unexpected body of Select (1)") } -func TestUpdate(t *testing.T) { +func TestDoWithUpdateRequest(t *testing.T) { roles := []bool{true, true, false, true, true} ctx, cancel := test_helpers.GetPoolConnectContext() @@ -2398,9 +2451,12 @@ func TestUpdate(t *testing.T) { require.Truef(t, ok, "unexpected body of Insert (1)") require.Equalf(t, "update_value", value, "unexpected body of Insert (1)") - // Mode is `RW` by default, we have only one RW instance (servers[2]) - resp, err := connPool.Update(spaceName, indexNo, - []interface{}{"update_key"}, tarantool.NewOperations().Assign(1, "new_value")) + // RW + resp, err := connPool.Do(tarantool.NewUpdateRequest(spaceName). + Index(indexNo). + Key([]interface{}{"update_key"}). + Operations(tarantool.NewOperations().Assign(1, "new_value")), + pool.RW).Get() require.Nilf(t, err, "failed to Update") require.NotNilf(t, resp, "response is nil after Update") @@ -2426,9 +2482,11 @@ func TestUpdate(t *testing.T) { require.Equalf(t, "new_value", value, "unexpected body of Select (1)") // PreferRW - resp, err = connPool.Update( - spaceName, indexNo, []interface{}{"update_key"}, - tarantool.NewOperations().Assign(1, "another_value"), pool.PreferRW) + resp, err = connPool.Do(tarantool.NewUpdateRequest(spaceName). + Index(indexNo). + Key([]interface{}{"update_key"}). + Operations(tarantool.NewOperations().Assign(1, "another_value")), + pool.PreferRW).Get() require.Nilf(t, err, "failed to Update") require.NotNilf(t, resp, "response is nil after Update") @@ -2450,7 +2508,7 @@ func TestUpdate(t *testing.T) { require.Equalf(t, "another_value", value, "unexpected body of Select (1)") } -func TestReplace(t *testing.T) { +func TestDoWithReplaceRequest(t *testing.T) { roles := []bool{true, true, false, true, true} ctx, cancel := test_helpers.GetPoolConnectContext() @@ -2488,10 +2546,12 @@ func TestReplace(t *testing.T) { require.Truef(t, ok, "unexpected body of Insert (1)") require.Equalf(t, "replace_value", value, "unexpected body of Insert (1)") - // Mode is `RW` by default, we have only one RW instance (servers[2]) - resp, err := connPool.Replace(spaceNo, []interface{}{"new_key", "new_value"}) - require.Nilf(t, err, "failed to Replace") - require.NotNilf(t, resp, "response is nil after Replace") + // RW + resp, err := connPool.Do(tarantool.NewReplaceRequest(spaceNo). + Tuple([]interface{}{"new_key", "new_value"}), + pool.RW).Get() + require.Nilf(t, err, "failed to Do with ReplaceRequest") + require.NotNilf(t, resp, "response is nil after Do with ReplaceRequest") sel := tarantool.NewSelectRequest(spaceNo). Index(indexNo). @@ -2515,9 +2575,11 @@ func TestReplace(t *testing.T) { require.Equalf(t, "new_value", value, "unexpected body of Select (1)") // PreferRW - resp, err = connPool.Replace(spaceNo, []interface{}{"new_key", "new_value"}, pool.PreferRW) - require.Nilf(t, err, "failed to Replace") - require.NotNilf(t, resp, "response is nil after Replace") + resp, err = connPool.Do(tarantool.NewReplaceRequest(spaceNo). + Tuple([]interface{}{"new_key", "new_value"}), + pool.PreferRW).Get() + require.Nilf(t, err, "failed to Do with ReplaceRequest") + require.NotNilf(t, resp, "response is nil after Do with ReplaceRequest") data, err = conn.Do(sel).Get() require.Nilf(t, err, "failed to Select") @@ -2536,7 +2598,7 @@ func TestReplace(t *testing.T) { require.Equalf(t, "new_value", value, "unexpected body of Select (1)") } -func TestSelect(t *testing.T) { +func TestDoWithSelectRequest(t *testing.T) { roles := []bool{true, true, false, true, false} ctx, cancel := test_helpers.GetPoolConnectContext() @@ -2572,94 +2634,124 @@ func TestSelect(t *testing.T) { err = test_helpers.InsertOnInstances(ctx, makeDialers(allServers), connOpts, spaceNo, anyTpl) require.Nil(t, err) - // default: ANY - data, err := connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, anyKey) - require.Nilf(t, err, "failed to Select") - require.NotNilf(t, data, "response is nil after Select") - require.Equalf(t, len(data), 1, "response Body len != 1 after Select") + // ANY + data, err := connPool.Do(tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Offset(0). + Limit(1). + Iterator(tarantool.IterEq). + Key(anyKey), + pool.ANY).Get() + require.Nilf(t, err, "failed to Do with SelectRequest") + require.NotNilf(t, data, "response is nil after Do with SelectRequest") + require.Equalf(t, len(data), 1, "response Body len != 1 after Do with SelectRequest") tpl, ok := data[0].([]interface{}) - require.Truef(t, ok, "unexpected body of Select") - require.Equalf(t, 2, len(tpl), "unexpected body of Select") + require.Truef(t, ok, "unexpected body of Do with SelectRequest") + require.Equalf(t, 2, len(tpl), "unexpected body of Do with SelectRequest") key, ok := tpl[0].(string) - require.Truef(t, ok, "unexpected body of Select (0)") - require.Equalf(t, "any_select_key", key, "unexpected body of Select (0)") + require.Truef(t, ok, "unexpected body of Do with SelectRequest (0)") + require.Equalf(t, "any_select_key", key, "unexpected body of Do with SelectRequest (0)") value, ok := tpl[1].(string) - require.Truef(t, ok, "unexpected body of Select (1)") - require.Equalf(t, "any_select_value", value, "unexpected body of Select (1)") + require.Truef(t, ok, "unexpected body of Do with SelectRequest (1)") + require.Equalf(t, "any_select_value", value, "unexpected body of Do with SelectRequest (1)") // PreferRO - data, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, pool.PreferRO) - require.Nilf(t, err, "failed to Select") - require.NotNilf(t, data, "response is nil after Select") - require.Equalf(t, len(data), 1, "response Body len != 1 after Select") + data, err = connPool.Do(tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Offset(0). + Limit(1). + Iterator(tarantool.IterEq). + Key(roKey), + pool.PreferRO).Get() + require.Nilf(t, err, "failed to Do with SelectRequest") + require.NotNilf(t, data, "response is nil after Do with SelectRequest") + require.Equalf(t, len(data), 1, "response Body len != 1 after Do with SelectRequest") tpl, ok = data[0].([]interface{}) - require.Truef(t, ok, "unexpected body of Select") - require.Equalf(t, 2, len(tpl), "unexpected body of Select") + require.Truef(t, ok, "unexpected body of Do with SelectRequest") + require.Equalf(t, 2, len(tpl), "unexpected body of Do with SelectRequest") key, ok = tpl[0].(string) - require.Truef(t, ok, "unexpected body of Select (0)") - require.Equalf(t, "ro_select_key", key, "unexpected body of Select (0)") + require.Truef(t, ok, "unexpected body of Do with SelectRequest (0)") + require.Equalf(t, "ro_select_key", key, "unexpected body of Do with SelectRequest (0)") // PreferRW - data, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, pool.PreferRW) - require.Nilf(t, err, "failed to Select") - require.NotNilf(t, data, "response is nil after Select") - require.Equalf(t, len(data), 1, "response Body len != 1 after Select") + data, err = connPool.Do(tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Offset(0). + Limit(1). + Iterator(tarantool.IterEq). + Key(rwKey), + pool.PreferRW).Get() + require.Nilf(t, err, "failed to Do with SelectRequest") + require.NotNilf(t, data, "response is nil after Do with SelectRequest") + require.Equalf(t, len(data), 1, "response Body len != 1 after Do with SelectRequest") tpl, ok = data[0].([]interface{}) - require.Truef(t, ok, "unexpected body of Select") - require.Equalf(t, 2, len(tpl), "unexpected body of Select") + require.Truef(t, ok, "unexpected body of Do with SelectRequest") + require.Equalf(t, 2, len(tpl), "unexpected body of Do with SelectRequest") key, ok = tpl[0].(string) - require.Truef(t, ok, "unexpected body of Select (0)") - require.Equalf(t, "rw_select_key", key, "unexpected body of Select (0)") + require.Truef(t, ok, "unexpected body of Do with SelectRequest (0)") + require.Equalf(t, "rw_select_key", key, "unexpected body of Do with SelectRequest (0)") value, ok = tpl[1].(string) - require.Truef(t, ok, "unexpected body of Select (1)") - require.Equalf(t, "rw_select_value", value, "unexpected body of Select (1)") + require.Truef(t, ok, "unexpected body of Do with SelectRequest (1)") + require.Equalf(t, "rw_select_value", value, "unexpected body of Do with SelectRequest (1)") // RO - data, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, roKey, pool.RO) - require.Nilf(t, err, "failed to Select") - require.NotNilf(t, data, "response is nil after Select") - require.Equalf(t, len(data), 1, "response Body len != 1 after Select") + data, err = connPool.Do(tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Offset(0). + Limit(1). + Iterator(tarantool.IterEq). + Key(roKey), + pool.RO).Get() + require.Nilf(t, err, "failed to Do with SelectRequest") + require.NotNilf(t, data, "response is nil after Do with SelectRequest") + require.Equalf(t, len(data), 1, "response Body len != 1 after Do with SelectRequest") tpl, ok = data[0].([]interface{}) - require.Truef(t, ok, "unexpected body of Select") - require.Equalf(t, 2, len(tpl), "unexpected body of Select") + require.Truef(t, ok, "unexpected body of Do with SelectRequest") + require.Equalf(t, 2, len(tpl), "unexpected body of Do with SelectRequest") key, ok = tpl[0].(string) - require.Truef(t, ok, "unexpected body of Select (0)") - require.Equalf(t, "ro_select_key", key, "unexpected body of Select (0)") + require.Truef(t, ok, "unexpected body of Do with SelectRequest (0)") + require.Equalf(t, "ro_select_key", key, "unexpected body of Do with SelectRequest (0)") value, ok = tpl[1].(string) - require.Truef(t, ok, "unexpected body of Select (1)") - require.Equalf(t, "ro_select_value", value, "unexpected body of Select (1)") + require.Truef(t, ok, "unexpected body of Do with SelectRequest (1)") + require.Equalf(t, "ro_select_value", value, "unexpected body of Do with SelectRequest (1)") // RW - data, err = connPool.Select(spaceNo, indexNo, 0, 1, tarantool.IterEq, rwKey, pool.RW) - require.Nilf(t, err, "failed to Select") - require.NotNilf(t, data, "response is nil after Select") - require.Equalf(t, len(data), 1, "response Body len != 1 after Select") + data, err = connPool.Do(tarantool.NewSelectRequest(spaceNo). + Index(indexNo). + Offset(0). + Limit(1). + Iterator(tarantool.IterEq). + Key(rwKey), + pool.RW).Get() + require.Nilf(t, err, "failed to Do with SelectRequest") + require.NotNilf(t, data, "response is nil after Do with SelectRequest") + require.Equalf(t, len(data), 1, "response Body len != 1 after Do with SelectRequest") tpl, ok = data[0].([]interface{}) - require.Truef(t, ok, "unexpected body of Select") - require.Equalf(t, 2, len(tpl), "unexpected body of Select") + require.Truef(t, ok, "unexpected body of Do with SelectRequest") + require.Equalf(t, 2, len(tpl), "unexpected body of Do with SelectRequest") key, ok = tpl[0].(string) - require.Truef(t, ok, "unexpected body of Select (0)") - require.Equalf(t, "rw_select_key", key, "unexpected body of Select (0)") + require.Truef(t, ok, "unexpected body of Do with SelectRequest (0)") + require.Equalf(t, "rw_select_key", key, "unexpected body of Do with SelectRequest (0)") value, ok = tpl[1].(string) - require.Truef(t, ok, "unexpected body of Select (1)") - require.Equalf(t, "rw_select_value", value, "unexpected body of Select (1)") + require.Truef(t, ok, "unexpected body of Do with SelectRequest (1)") + require.Equalf(t, "rw_select_value", value, "unexpected body of Do with SelectRequest (1)") } -func TestPing(t *testing.T) { +func TestDoWithPingRequest(t *testing.T) { roles := []bool{true, true, false, true, false} ctx, cancel := test_helpers.GetPoolConnectContext() @@ -2675,28 +2767,28 @@ func TestPing(t *testing.T) { defer connPool.Close() // ANY - data, err := connPool.Ping(pool.ANY) - require.Nilf(t, err, "failed to Ping") - require.Nilf(t, data, "response data is not nil after Ping") + data, err := connPool.Do(tarantool.NewPingRequest(), pool.ANY).Get() + require.Nilf(t, err, "failed to Do with Ping Request") + require.Nilf(t, data, "response data is not nil after Do with Ping Request") // RW - data, err = connPool.Ping(pool.RW) - require.Nilf(t, err, "failed to Ping") - require.Nilf(t, data, "response data is not nil after Ping") + data, err = connPool.Do(tarantool.NewPingRequest(), pool.RW).Get() + require.Nilf(t, err, "failed to Do with Ping Request") + require.Nilf(t, data, "response data is not nil after Do with Ping Request") // RO - _, err = connPool.Ping(pool.RO) - require.Nilf(t, err, "failed to Ping") + _, err = connPool.Do(tarantool.NewPingRequest(), pool.RO).Get() + require.Nilf(t, err, "failed to Do with Ping Request") // PreferRW - data, err = connPool.Ping(pool.PreferRW) - require.Nilf(t, err, "failed to Ping") - require.Nilf(t, data, "response data is not nil after Ping") + data, err = connPool.Do(tarantool.NewPingRequest(), pool.PreferRW).Get() + require.Nilf(t, err, "failed to Do with Ping Request") + require.Nilf(t, data, "response data is not nil after Do with Ping Request") // PreferRO - data, err = connPool.Ping(pool.PreferRO) - require.Nilf(t, err, "failed to Ping") - require.Nilf(t, data, "response data is not nil after Ping") + data, err = connPool.Do(tarantool.NewPingRequest(), pool.PreferRO).Get() + require.Nilf(t, err, "failed to Do with Ping Request") + require.Nilf(t, data, "response data is not nil after Do with Ping Request") } func TestDo(t *testing.T) { diff --git a/pool/connector.go b/pool/connector.go index 391b83b75..c984bfd32 100644 --- a/pool/connector.go +++ b/pool/connector.go @@ -56,326 +56,6 @@ func (c *ConnectorAdapter) ConfiguredTimeout() time.Duration { return ret } -// Ping sends empty request to Tarantool to check connection. -// -// Deprecated: the method will be removed in the next major version, -// use a PingRequest object + Do() instead. -func (c *ConnectorAdapter) Ping() ([]interface{}, error) { - return c.pool.Ping(c.mode) -} - -// Select performs select to box space. -// -// Deprecated: the method will be removed in the next major version, -// use a SelectRequest object + Do() instead. -func (c *ConnectorAdapter) Select(space, index interface{}, - offset, limit uint32, iterator tarantool.Iter, - key interface{}) ([]interface{}, error) { - return c.pool.Select(space, index, offset, limit, iterator, key, c.mode) -} - -// Insert performs insertion to box space. -// -// Deprecated: the method will be removed in the next major version, -// use an InsertRequest object + Do() instead. -func (c *ConnectorAdapter) Insert(space interface{}, - tuple interface{}) ([]interface{}, error) { - return c.pool.Insert(space, tuple, c.mode) -} - -// Replace performs "insert or replace" action to box space. -// -// Deprecated: the method will be removed in the next major version, -// use a ReplaceRequest object + Do() instead. -func (c *ConnectorAdapter) Replace(space interface{}, - tuple interface{}) ([]interface{}, error) { - return c.pool.Replace(space, tuple, c.mode) -} - -// Delete performs deletion of a tuple by key. -// -// Deprecated: the method will be removed in the next major version, -// use a DeleteRequest object + Do() instead. -func (c *ConnectorAdapter) Delete(space, index interface{}, - key interface{}) ([]interface{}, error) { - return c.pool.Delete(space, index, key, c.mode) -} - -// Update performs update of a tuple by key. -// -// Deprecated: the method will be removed in the next major version, -// use a UpdateRequest object + Do() instead. -func (c *ConnectorAdapter) Update(space, index interface{}, - key interface{}, ops *tarantool.Operations) ([]interface{}, error) { - return c.pool.Update(space, index, key, ops, c.mode) -} - -// Upsert performs "update or insert" action of a tuple by key. -// -// Deprecated: the method will be removed in the next major version, -// use a UpsertRequest object + Do() instead. -func (c *ConnectorAdapter) Upsert(space, tuple interface{}, - ops *tarantool.Operations) ([]interface{}, error) { - return c.pool.Upsert(space, tuple, ops, c.mode) -} - -// Call calls registered Tarantool function. -// It uses request code for Tarantool >= 1.7, result is an array. -// -// Deprecated: the method will be removed in the next major version, -// use a CallRequest object + Do() instead. -func (c *ConnectorAdapter) Call(functionName string, - args interface{}) ([]interface{}, error) { - return c.pool.Call(functionName, args, c.mode) -} - -// Call16 calls registered Tarantool function. -// It uses request code for Tarantool 1.6, result is an array of arrays. -// Deprecated since Tarantool 1.7.2. -// -// Deprecated: the method will be removed in the next major version, -// use a Call16Request object + Do() instead. -func (c *ConnectorAdapter) Call16(functionName string, - args interface{}) ([]interface{}, error) { - return c.pool.Call16(functionName, args, c.mode) -} - -// Call17 calls registered Tarantool function. -// It uses request code for Tarantool >= 1.7, result is an array. -// -// Deprecated: the method will be removed in the next major version, -// use a Call17Request object + Do() instead. -func (c *ConnectorAdapter) Call17(functionName string, - args interface{}) ([]interface{}, error) { - return c.pool.Call17(functionName, args, c.mode) -} - -// Eval passes Lua expression for evaluation. -// -// Deprecated: the method will be removed in the next major version, -// use an EvalRequest object + Do() instead. -func (c *ConnectorAdapter) Eval(expr string, - args interface{}) ([]interface{}, error) { - return c.pool.Eval(expr, args, c.mode) -} - -// Execute passes sql expression to Tarantool for execution. -// -// Deprecated: the method will be removed in the next major version, -// use an ExecuteRequest object + Do() instead. -func (c *ConnectorAdapter) Execute(expr string, - args interface{}) ([]interface{}, error) { - return c.pool.Execute(expr, args, c.mode) -} - -// GetTyped performs select (with limit = 1 and offset = 0) -// to box space and fills typed result. -// -// Deprecated: the method will be removed in the next major version, -// use a SelectRequest object + Do() instead. -func (c *ConnectorAdapter) GetTyped(space, index interface{}, - key interface{}, result interface{}) error { - return c.pool.GetTyped(space, index, key, result, c.mode) -} - -// SelectTyped performs select to box space and fills typed result. -// -// Deprecated: the method will be removed in the next major version, -// use a SelectRequest object + Do() instead. -func (c *ConnectorAdapter) SelectTyped(space, index interface{}, - offset, limit uint32, iterator tarantool.Iter, - key interface{}, result interface{}) error { - return c.pool.SelectTyped(space, index, offset, limit, iterator, key, result, c.mode) -} - -// InsertTyped performs insertion to box space. -// -// Deprecated: the method will be removed in the next major version, -// use an InsertRequest object + Do() instead. -func (c *ConnectorAdapter) InsertTyped(space interface{}, - tuple interface{}, result interface{}) error { - return c.pool.InsertTyped(space, tuple, result, c.mode) -} - -// ReplaceTyped performs "insert or replace" action to box space. -// -// Deprecated: the method will be removed in the next major version, -// use a ReplaceRequest object + Do() instead. -func (c *ConnectorAdapter) ReplaceTyped(space interface{}, - tuple interface{}, result interface{}) error { - return c.pool.ReplaceTyped(space, tuple, result, c.mode) -} - -// DeleteTyped performs deletion of a tuple by key and fills result with deleted tuple. -// -// Deprecated: the method will be removed in the next major version, -// use a DeleteRequest object + Do() instead. -func (c *ConnectorAdapter) DeleteTyped(space, index interface{}, - key interface{}, result interface{}) error { - return c.pool.DeleteTyped(space, index, key, result, c.mode) -} - -// UpdateTyped performs update of a tuple by key and fills result with updated tuple. -// -// Deprecated: the method will be removed in the next major version, -// use a UpdateRequest object + Do() instead. -func (c *ConnectorAdapter) UpdateTyped(space, index interface{}, - key interface{}, ops *tarantool.Operations, result interface{}) error { - return c.pool.UpdateTyped(space, index, key, ops, result, c.mode) -} - -// CallTyped calls registered function. -// It uses request code for Tarantool >= 1.7, result is an array. -// -// Deprecated: the method will be removed in the next major version, -// use a CallRequest object + Do() instead. -func (c *ConnectorAdapter) CallTyped(functionName string, - args interface{}, result interface{}) error { - return c.pool.CallTyped(functionName, args, result, c.mode) -} - -// Call16Typed calls registered function. -// It uses request code for Tarantool 1.6, result is an array of arrays. -// Deprecated since Tarantool 1.7.2. -// -// Deprecated: the method will be removed in the next major version, -// use a Call16Request object + Do() instead. -func (c *ConnectorAdapter) Call16Typed(functionName string, - args interface{}, result interface{}) error { - return c.pool.Call16Typed(functionName, args, result, c.mode) -} - -// Call17Typed calls registered function. -// It uses request code for Tarantool >= 1.7, result is an array. -// -// Deprecated: the method will be removed in the next major version, -// use a Call17Request object + Do() instead. -func (c *ConnectorAdapter) Call17Typed(functionName string, - args interface{}, result interface{}) error { - return c.pool.Call17Typed(functionName, args, result, c.mode) -} - -// EvalTyped passes Lua expression for evaluation. -// -// Deprecated: the method will be removed in the next major version, -// use an EvalRequest object + Do() instead. -func (c *ConnectorAdapter) EvalTyped(expr string, args interface{}, - result interface{}) error { - return c.pool.EvalTyped(expr, args, result, c.mode) -} - -// ExecuteTyped passes sql expression to Tarantool for execution. -// -// Deprecated: the method will be removed in the next major version, -// use an ExecuteRequest object + Do() instead. -func (c *ConnectorAdapter) ExecuteTyped(expr string, args interface{}, - result interface{}) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) { - return c.pool.ExecuteTyped(expr, args, result, c.mode) -} - -// SelectAsync sends select request to Tarantool and returns Future. -// -// Deprecated: the method will be removed in the next major version, -// use a SelectRequest object + Do() instead. -func (c *ConnectorAdapter) SelectAsync(space, index interface{}, - offset, limit uint32, iterator tarantool.Iter, key interface{}) *tarantool.Future { - return c.pool.SelectAsync(space, index, offset, limit, iterator, key, c.mode) -} - -// InsertAsync sends insert action to Tarantool and returns Future. -// -// Deprecated: the method will be removed in the next major version, -// use an InsertRequest object + Do() instead. -func (c *ConnectorAdapter) InsertAsync(space interface{}, - tuple interface{}) *tarantool.Future { - return c.pool.InsertAsync(space, tuple, c.mode) -} - -// ReplaceAsync sends "insert or replace" action to Tarantool and returns Future. -// -// Deprecated: the method will be removed in the next major version, -// use a ReplaceRequest object + Do() instead. -func (c *ConnectorAdapter) ReplaceAsync(space interface{}, - tuple interface{}) *tarantool.Future { - return c.pool.ReplaceAsync(space, tuple, c.mode) -} - -// DeleteAsync sends deletion action to Tarantool and returns Future. -// -// Deprecated: the method will be removed in the next major version, -// use a DeleteRequest object + Do() instead. -func (c *ConnectorAdapter) DeleteAsync(space, index interface{}, - key interface{}) *tarantool.Future { - return c.pool.DeleteAsync(space, index, key, c.mode) -} - -// Update sends deletion of a tuple by key and returns Future. -// -// Deprecated: the method will be removed in the next major version, -// use a UpdateRequest object + Do() instead. -func (c *ConnectorAdapter) UpdateAsync(space, index interface{}, - key interface{}, ops *tarantool.Operations) *tarantool.Future { - return c.pool.UpdateAsync(space, index, key, ops, c.mode) -} - -// UpsertAsync sends "update or insert" action to Tarantool and returns Future. -// -// Deprecated: the method will be removed in the next major version, -// use a UpsertRequest object + Do() instead. -func (c *ConnectorAdapter) UpsertAsync(space, tuple interface{}, - ops *tarantool.Operations) *tarantool.Future { - return c.pool.UpsertAsync(space, tuple, ops, c.mode) -} - -// CallAsync sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool >= 1.7, future's result is an array. -// -// Deprecated: the method will be removed in the next major version, -// use a CallRequest object + Do() instead. -func (c *ConnectorAdapter) CallAsync(functionName string, - args interface{}) *tarantool.Future { - return c.pool.CallAsync(functionName, args, c.mode) -} - -// Call16Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool 1.6, so future's result is an array of arrays. -// Deprecated since Tarantool 1.7.2. -// -// Deprecated: the method will be removed in the next major version, -// use a Call16Request object + Do() instead. -func (c *ConnectorAdapter) Call16Async(functionName string, - args interface{}) *tarantool.Future { - return c.pool.Call16Async(functionName, args, c.mode) -} - -// Call17Async sends a call to registered Tarantool function and returns Future. -// It uses request code for Tarantool >= 1.7, future's result is an array. -// -// Deprecated: the method will be removed in the next major version, -// use a Call17Request object + Do() instead. -func (c *ConnectorAdapter) Call17Async(functionName string, - args interface{}) *tarantool.Future { - return c.pool.Call17Async(functionName, args, c.mode) -} - -// EvalAsync sends a Lua expression for evaluation and returns Future. -// -// Deprecated: the method will be removed in the next major version, -// use an EvalRequest object + Do() instead. -func (c *ConnectorAdapter) EvalAsync(expr string, - args interface{}) *tarantool.Future { - return c.pool.EvalAsync(expr, args, c.mode) -} - -// ExecuteAsync sends a sql expression for execution and returns Future. -// -// Deprecated: the method will be removed in the next major version, -// use an EvalRequest object + Do() instead. -func (c *ConnectorAdapter) ExecuteAsync(expr string, - args interface{}) *tarantool.Future { - return c.pool.ExecuteAsync(expr, args, c.mode) -} - // NewPrepared passes a sql statement to Tarantool for preparation // synchronously. func (c *ConnectorAdapter) NewPrepared(expr string) (*tarantool.Prepared, error) { diff --git a/pool/connector_test.go b/pool/connector_test.go index 9b8106c12..3ed8ea81d 100644 --- a/pool/connector_test.go +++ b/pool/connector_test.go @@ -124,966 +124,10 @@ func TestConnectorConfiguredTimeoutWithError(t *testing.T) { // Tests for that ConnectorAdapter is just a proxy for requests. -type baseRequestMock struct { - Pooler - called int - functionName string - offset, limit uint32 - iterator tarantool.Iter - space, index interface{} - args, tuple, key, ops interface{} - result interface{} - mode Mode -} - -var reqData []interface{} var errReq error = errors.New("response error") var reqFuture *tarantool.Future = &tarantool.Future{} var reqFunctionName string = "any_name" -var reqOffset uint32 = 1 -var reqLimit uint32 = 2 -var reqIterator tarantool.Iter = tarantool.IterLt -var reqSpace interface{} = []interface{}{1} -var reqIndex interface{} = []interface{}{2} -var reqArgs interface{} = []interface{}{3} -var reqTuple interface{} = []interface{}{4} -var reqKey interface{} = []interface{}{5} -var reqOps = tarantool.NewOperations() - -var reqResult interface{} = []interface{}{7} -var reqSqlInfo = tarantool.SQLInfo{AffectedCount: 3} -var reqMeta = []tarantool.ColumnMetaData{{FieldIsNullable: false}} - -type getTypedMock struct { - baseRequestMock -} - -func (m *getTypedMock) GetTyped(space, index, key interface{}, - result interface{}, mode ...Mode) error { - m.called++ - m.space = space - m.index = index - m.key = key - m.result = result - m.mode = mode[0] - return errReq -} - -func TestConnectorGetTyped(t *testing.T) { - m := &getTypedMock{} - c := NewConnectorAdapter(m, testMode) - - err := c.GetTyped(reqSpace, reqIndex, reqKey, reqResult) - - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqIndex, m.index, "unexpected index was passed") - require.Equalf(t, reqKey, m.key, "unexpected key was passed") - require.Equalf(t, reqResult, m.result, "unexpected result was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type selectMock struct { - baseRequestMock -} - -func (m *selectMock) Select(space, index interface{}, - offset, limit uint32, iterator tarantool.Iter, key interface{}, - mode ...Mode) ([]interface{}, error) { - m.called++ - m.space = space - m.index = index - m.offset = offset - m.limit = limit - m.iterator = iterator - m.key = key - m.mode = mode[0] - return reqData, errReq -} - -func TestConnectorSelect(t *testing.T) { - m := &selectMock{} - c := NewConnectorAdapter(m, testMode) - - resp, err := c.Select(reqSpace, reqIndex, reqOffset, reqLimit, reqIterator, reqKey) - - require.Equalf(t, reqData, resp, "unexpected response") - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqIndex, m.index, "unexpected index was passed") - require.Equalf(t, reqOffset, m.offset, "unexpected offset was passed") - require.Equalf(t, reqLimit, m.limit, "unexpected limit was passed") - require.Equalf(t, reqIterator, m.iterator, "unexpected iterator was passed") - require.Equalf(t, reqKey, m.key, "unexpected key was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type selectTypedMock struct { - baseRequestMock -} - -func (m *selectTypedMock) SelectTyped(space, index interface{}, - offset, limit uint32, iterator tarantool.Iter, key interface{}, - result interface{}, mode ...Mode) error { - m.called++ - m.space = space - m.index = index - m.offset = offset - m.limit = limit - m.iterator = iterator - m.key = key - m.result = result - m.mode = mode[0] - return errReq -} - -func TestConnectorSelectTyped(t *testing.T) { - m := &selectTypedMock{} - c := NewConnectorAdapter(m, testMode) - - err := c.SelectTyped(reqSpace, reqIndex, reqOffset, reqLimit, - reqIterator, reqKey, reqResult) - - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqIndex, m.index, "unexpected index was passed") - require.Equalf(t, reqOffset, m.offset, "unexpected offset was passed") - require.Equalf(t, reqLimit, m.limit, "unexpected limit was passed") - require.Equalf(t, reqIterator, m.iterator, "unexpected iterator was passed") - require.Equalf(t, reqKey, m.key, "unexpected key was passed") - require.Equalf(t, reqResult, m.result, "unexpected result was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type selectAsyncMock struct { - baseRequestMock -} - -func (m *selectAsyncMock) SelectAsync(space, index interface{}, - offset, limit uint32, iterator tarantool.Iter, key interface{}, - mode ...Mode) *tarantool.Future { - m.called++ - m.space = space - m.index = index - m.offset = offset - m.limit = limit - m.iterator = iterator - m.key = key - m.mode = mode[0] - return reqFuture -} - -func TestConnectorSelectAsync(t *testing.T) { - m := &selectAsyncMock{} - c := NewConnectorAdapter(m, testMode) - - fut := c.SelectAsync(reqSpace, reqIndex, reqOffset, reqLimit, - reqIterator, reqKey) - - require.Equalf(t, reqFuture, fut, "unexpected future") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqIndex, m.index, "unexpected index was passed") - require.Equalf(t, reqOffset, m.offset, "unexpected offset was passed") - require.Equalf(t, reqLimit, m.limit, "unexpected limit was passed") - require.Equalf(t, reqIterator, m.iterator, "unexpected iterator was passed") - require.Equalf(t, reqKey, m.key, "unexpected key was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type insertMock struct { - baseRequestMock -} - -func (m *insertMock) Insert(space, tuple interface{}, - mode ...Mode) ([]interface{}, error) { - m.called++ - m.space = space - m.tuple = tuple - m.mode = mode[0] - return reqData, errReq -} - -func TestConnectorInsert(t *testing.T) { - m := &insertMock{} - c := NewConnectorAdapter(m, testMode) - - resp, err := c.Insert(reqSpace, reqTuple) - - require.Equalf(t, reqData, resp, "unexpected response") - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type insertTypedMock struct { - baseRequestMock -} - -func (m *insertTypedMock) InsertTyped(space, tuple interface{}, - result interface{}, mode ...Mode) error { - m.called++ - m.space = space - m.tuple = tuple - m.result = result - m.mode = mode[0] - return errReq -} - -func TestConnectorInsertTyped(t *testing.T) { - m := &insertTypedMock{} - c := NewConnectorAdapter(m, testMode) - - err := c.InsertTyped(reqSpace, reqTuple, reqResult) - - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") - require.Equalf(t, reqResult, m.result, "unexpected result was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type insertAsyncMock struct { - baseRequestMock -} - -func (m *insertAsyncMock) InsertAsync(space, tuple interface{}, - mode ...Mode) *tarantool.Future { - m.called++ - m.space = space - m.tuple = tuple - m.mode = mode[0] - return reqFuture -} - -func TestConnectorInsertAsync(t *testing.T) { - m := &insertAsyncMock{} - c := NewConnectorAdapter(m, testMode) - - fut := c.InsertAsync(reqSpace, reqTuple) - - require.Equalf(t, reqFuture, fut, "unexpected future") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type replaceMock struct { - baseRequestMock -} - -func (m *replaceMock) Replace(space, tuple interface{}, - mode ...Mode) ([]interface{}, error) { - m.called++ - m.space = space - m.tuple = tuple - m.mode = mode[0] - return reqData, errReq -} - -func TestConnectorReplace(t *testing.T) { - m := &replaceMock{} - c := NewConnectorAdapter(m, testMode) - - resp, err := c.Replace(reqSpace, reqTuple) - - require.Equalf(t, reqData, resp, "unexpected response") - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type replaceTypedMock struct { - baseRequestMock -} - -func (m *replaceTypedMock) ReplaceTyped(space, tuple interface{}, - result interface{}, mode ...Mode) error { - m.called++ - m.space = space - m.tuple = tuple - m.result = result - m.mode = mode[0] - return errReq -} - -func TestConnectorReplaceTyped(t *testing.T) { - m := &replaceTypedMock{} - c := NewConnectorAdapter(m, testMode) - - err := c.ReplaceTyped(reqSpace, reqTuple, reqResult) - - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") - require.Equalf(t, reqResult, m.result, "unexpected result was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type replaceAsyncMock struct { - baseRequestMock -} - -func (m *replaceAsyncMock) ReplaceAsync(space, tuple interface{}, - mode ...Mode) *tarantool.Future { - m.called++ - m.space = space - m.tuple = tuple - m.mode = mode[0] - return reqFuture -} - -func TestConnectorReplaceAsync(t *testing.T) { - m := &replaceAsyncMock{} - c := NewConnectorAdapter(m, testMode) - - fut := c.ReplaceAsync(reqSpace, reqTuple) - - require.Equalf(t, reqFuture, fut, "unexpected future") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type deleteMock struct { - baseRequestMock -} - -func (m *deleteMock) Delete(space, index, key interface{}, - mode ...Mode) ([]interface{}, error) { - m.called++ - m.space = space - m.index = index - m.key = key - m.mode = mode[0] - return reqData, errReq -} - -func TestConnectorDelete(t *testing.T) { - m := &deleteMock{} - c := NewConnectorAdapter(m, testMode) - - resp, err := c.Delete(reqSpace, reqIndex, reqKey) - - require.Equalf(t, reqData, resp, "unexpected response") - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqIndex, m.index, "unexpected index was passed") - require.Equalf(t, reqKey, m.key, "unexpected key was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type deleteTypedMock struct { - baseRequestMock -} - -func (m *deleteTypedMock) DeleteTyped(space, index, key interface{}, - result interface{}, mode ...Mode) error { - m.called++ - m.space = space - m.index = index - m.key = key - m.result = result - m.mode = mode[0] - return errReq -} - -func TestConnectorDeleteTyped(t *testing.T) { - m := &deleteTypedMock{} - c := NewConnectorAdapter(m, testMode) - - err := c.DeleteTyped(reqSpace, reqIndex, reqKey, reqResult) - - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqIndex, m.index, "unexpected index was passed") - require.Equalf(t, reqKey, m.key, "unexpected key was passed") - require.Equalf(t, reqResult, m.result, "unexpected result was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type deleteAsyncMock struct { - baseRequestMock -} - -func (m *deleteAsyncMock) DeleteAsync(space, index, key interface{}, - mode ...Mode) *tarantool.Future { - m.called++ - m.space = space - m.index = index - m.key = key - m.mode = mode[0] - return reqFuture -} - -func TestConnectorDeleteAsync(t *testing.T) { - m := &deleteAsyncMock{} - c := NewConnectorAdapter(m, testMode) - - fut := c.DeleteAsync(reqSpace, reqIndex, reqKey) - - require.Equalf(t, reqFuture, fut, "unexpected future") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqIndex, m.index, "unexpected index was passed") - require.Equalf(t, reqKey, m.key, "unexpected key was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type updateMock struct { - baseRequestMock -} - -func (m *updateMock) Update(space, index, key interface{}, - ops *tarantool.Operations, mode ...Mode) ([]interface{}, error) { - m.called++ - m.space = space - m.index = index - m.key = key - m.ops = ops - m.mode = mode[0] - return reqData, errReq -} - -func TestConnectorUpdate(t *testing.T) { - m := &updateMock{} - c := NewConnectorAdapter(m, testMode) - - resp, err := c.Update(reqSpace, reqIndex, reqKey, reqOps) - - require.Equalf(t, reqData, resp, "unexpected response") - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqIndex, m.index, "unexpected index was passed") - require.Equalf(t, reqKey, m.key, "unexpected key was passed") - require.Equalf(t, reqOps, m.ops, "unexpected ops was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type updateTypedMock struct { - baseRequestMock -} - -func (m *updateTypedMock) UpdateTyped(space, index, key interface{}, - ops *tarantool.Operations, result interface{}, mode ...Mode) error { - m.called++ - m.space = space - m.index = index - m.key = key - m.ops = ops - m.result = result - m.mode = mode[0] - return errReq -} - -func TestConnectorUpdateTyped(t *testing.T) { - m := &updateTypedMock{} - c := NewConnectorAdapter(m, testMode) - - err := c.UpdateTyped(reqSpace, reqIndex, reqKey, reqOps, reqResult) - - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqIndex, m.index, "unexpected index was passed") - require.Equalf(t, reqKey, m.key, "unexpected key was passed") - require.Equalf(t, reqOps, m.ops, "unexpected ops was passed") - require.Equalf(t, reqResult, m.result, "unexpected result was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type updateAsyncMock struct { - baseRequestMock -} - -func (m *updateAsyncMock) UpdateAsync(space, index, key interface{}, - ops *tarantool.Operations, mode ...Mode) *tarantool.Future { - m.called++ - m.space = space - m.index = index - m.key = key - m.ops = ops - m.mode = mode[0] - return reqFuture -} - -func TestConnectorUpdateAsync(t *testing.T) { - m := &updateAsyncMock{} - c := NewConnectorAdapter(m, testMode) - - fut := c.UpdateAsync(reqSpace, reqIndex, reqKey, reqOps) - - require.Equalf(t, reqFuture, fut, "unexpected future") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqIndex, m.index, "unexpected index was passed") - require.Equalf(t, reqKey, m.key, "unexpected key was passed") - require.Equalf(t, reqOps, m.ops, "unexpected ops was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type upsertMock struct { - baseRequestMock -} - -func (m *upsertMock) Upsert(space, tuple interface{}, ops *tarantool.Operations, - mode ...Mode) ([]interface{}, error) { - m.called++ - m.space = space - m.tuple = tuple - m.ops = ops - m.mode = mode[0] - return reqData, errReq -} - -func TestConnectorUpsert(t *testing.T) { - m := &upsertMock{} - c := NewConnectorAdapter(m, testMode) - - resp, err := c.Upsert(reqSpace, reqTuple, reqOps) - - require.Equalf(t, reqData, resp, "unexpected response") - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") - require.Equalf(t, reqOps, m.ops, "unexpected ops was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type upsertAsyncMock struct { - baseRequestMock -} - -func (m *upsertAsyncMock) UpsertAsync(space, tuple interface{}, - ops *tarantool.Operations, mode ...Mode) *tarantool.Future { - m.called++ - m.space = space - m.tuple = tuple - m.ops = ops - m.mode = mode[0] - return reqFuture -} - -func TestConnectorUpsertAsync(t *testing.T) { - m := &upsertAsyncMock{} - c := NewConnectorAdapter(m, testMode) - - fut := c.UpsertAsync(reqSpace, reqTuple, reqOps) - - require.Equalf(t, reqFuture, fut, "unexpected future") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqSpace, m.space, "unexpected space was passed") - require.Equalf(t, reqTuple, m.tuple, "unexpected tuple was passed") - require.Equalf(t, reqOps, m.ops, "unexpected ops was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type baseCallMock struct { - baseRequestMock -} - -func (m *baseCallMock) call(functionName string, args interface{}, - mode Mode) ([]interface{}, error) { - m.called++ - m.functionName = functionName - m.args = args - m.mode = mode - return reqData, errReq -} - -func (m *baseCallMock) callTyped(functionName string, args interface{}, - result interface{}, mode Mode) error { - m.called++ - m.functionName = functionName - m.args = args - m.result = result - m.mode = mode - return errReq -} - -func (m *baseCallMock) callAsync(functionName string, args interface{}, - mode Mode) *tarantool.Future { - m.called++ - m.functionName = functionName - m.args = args - m.mode = mode - return reqFuture -} - -type callMock struct { - baseCallMock -} - -func (m *callMock) Call(functionName string, args interface{}, - mode Mode) ([]interface{}, error) { - return m.call(functionName, args, mode) -} - -func TestConnectorCall(t *testing.T) { - m := &callMock{} - c := NewConnectorAdapter(m, testMode) - - resp, err := c.Call(reqFunctionName, reqArgs) - - require.Equalf(t, reqData, resp, "unexpected response") - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected functionName was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type callTypedMock struct { - baseCallMock -} - -func (m *callTypedMock) CallTyped(functionName string, args interface{}, - result interface{}, mode Mode) error { - return m.callTyped(functionName, args, result, mode) -} - -func TestConnectorCallTyped(t *testing.T) { - m := &callTypedMock{} - c := NewConnectorAdapter(m, testMode) - - err := c.CallTyped(reqFunctionName, reqArgs, reqResult) - - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected functionName was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, reqResult, m.result, "unexpected result was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type callAsyncMock struct { - baseCallMock -} - -func (m *callAsyncMock) CallAsync(functionName string, args interface{}, - mode Mode) *tarantool.Future { - return m.callAsync(functionName, args, mode) -} - -func TestConnectorCallAsync(t *testing.T) { - m := &callAsyncMock{} - c := NewConnectorAdapter(m, testMode) - - fut := c.CallAsync(reqFunctionName, reqArgs) - - require.Equalf(t, reqFuture, fut, "unexpected future") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected functionName was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type call16Mock struct { - baseCallMock -} - -func (m *call16Mock) Call16(functionName string, args interface{}, - mode Mode) ([]interface{}, error) { - return m.call(functionName, args, mode) -} - -func TestConnectorCall16(t *testing.T) { - m := &call16Mock{} - c := NewConnectorAdapter(m, testMode) - - resp, err := c.Call16(reqFunctionName, reqArgs) - - require.Equalf(t, reqData, resp, "unexpected response") - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected functionName was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type call16TypedMock struct { - baseCallMock -} - -func (m *call16TypedMock) Call16Typed(functionName string, args interface{}, - result interface{}, mode Mode) error { - return m.callTyped(functionName, args, result, mode) -} - -func TestConnectorCall16Typed(t *testing.T) { - m := &call16TypedMock{} - c := NewConnectorAdapter(m, testMode) - - err := c.Call16Typed(reqFunctionName, reqArgs, reqResult) - - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected functionName was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, reqResult, m.result, "unexpected result was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type call16AsyncMock struct { - baseCallMock -} - -func (m *call16AsyncMock) Call16Async(functionName string, args interface{}, - mode Mode) *tarantool.Future { - return m.callAsync(functionName, args, mode) -} - -func TestConnectorCall16Async(t *testing.T) { - m := &call16AsyncMock{} - c := NewConnectorAdapter(m, testMode) - - fut := c.Call16Async(reqFunctionName, reqArgs) - - require.Equalf(t, reqFuture, fut, "unexpected future") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected functionName was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type call17Mock struct { - baseCallMock -} - -func (m *call17Mock) Call17(functionName string, args interface{}, - mode Mode) ([]interface{}, error) { - return m.call(functionName, args, mode) -} - -func TestConnectorCall17(t *testing.T) { - m := &call17Mock{} - c := NewConnectorAdapter(m, testMode) - - resp, err := c.Call17(reqFunctionName, reqArgs) - - require.Equalf(t, reqData, resp, "unexpected response") - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected functionName was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type call17TypedMock struct { - baseCallMock -} - -func (m *call17TypedMock) Call17Typed(functionName string, args interface{}, - result interface{}, mode Mode) error { - return m.callTyped(functionName, args, result, mode) -} - -func TestConnectorCall17Typed(t *testing.T) { - m := &call17TypedMock{} - c := NewConnectorAdapter(m, testMode) - - err := c.Call17Typed(reqFunctionName, reqArgs, reqResult) - - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected functionName was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, reqResult, m.result, "unexpected result was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type call17AsyncMock struct { - baseCallMock -} - -func (m *call17AsyncMock) Call17Async(functionName string, args interface{}, - mode Mode) *tarantool.Future { - return m.callAsync(functionName, args, mode) -} - -func TestConnectorCall17Async(t *testing.T) { - m := &call17AsyncMock{} - c := NewConnectorAdapter(m, testMode) - - fut := c.Call17Async(reqFunctionName, reqArgs) - - require.Equalf(t, reqFuture, fut, "unexpected future") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected functionName was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type evalMock struct { - baseCallMock -} - -func (m *evalMock) Eval(functionName string, args interface{}, - mode Mode) ([]interface{}, error) { - return m.call(functionName, args, mode) -} - -func TestConnectorEval(t *testing.T) { - m := &evalMock{} - c := NewConnectorAdapter(m, testMode) - - resp, err := c.Eval(reqFunctionName, reqArgs) - - require.Equalf(t, reqData, resp, "unexpected response") - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected expr was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type evalTypedMock struct { - baseCallMock -} - -func (m *evalTypedMock) EvalTyped(functionName string, args interface{}, - result interface{}, mode Mode) error { - return m.callTyped(functionName, args, result, mode) -} - -func TestConnectorEvalTyped(t *testing.T) { - m := &evalTypedMock{} - c := NewConnectorAdapter(m, testMode) - - err := c.EvalTyped(reqFunctionName, reqArgs, reqResult) - - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected expr was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, reqResult, m.result, "unexpected result was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type evalAsyncMock struct { - baseCallMock -} - -func (m *evalAsyncMock) EvalAsync(functionName string, args interface{}, - mode Mode) *tarantool.Future { - return m.callAsync(functionName, args, mode) -} - -func TestConnectorEvalAsync(t *testing.T) { - m := &evalAsyncMock{} - c := NewConnectorAdapter(m, testMode) - - fut := c.EvalAsync(reqFunctionName, reqArgs) - - require.Equalf(t, reqFuture, fut, "unexpected future") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected expr was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type executeMock struct { - baseCallMock -} - -func (m *executeMock) Execute(functionName string, args interface{}, - mode Mode) ([]interface{}, error) { - return m.call(functionName, args, mode) -} - -func TestConnectorExecute(t *testing.T) { - m := &executeMock{} - c := NewConnectorAdapter(m, testMode) - - resp, err := c.Execute(reqFunctionName, reqArgs) - - require.Equalf(t, reqData, resp, "unexpected response") - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected expr was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type executeTypedMock struct { - baseCallMock -} - -func (m *executeTypedMock) ExecuteTyped(functionName string, args, result interface{}, - mode Mode) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) { - m.callTyped(functionName, args, result, mode) - return reqSqlInfo, reqMeta, errReq -} - -func TestConnectorExecuteTyped(t *testing.T) { - m := &executeTypedMock{} - c := NewConnectorAdapter(m, testMode) - - info, meta, err := c.ExecuteTyped(reqFunctionName, reqArgs, reqResult) - - require.Equalf(t, reqSqlInfo, info, "unexpected info") - require.Equalf(t, reqMeta, meta, "unexpected meta") - require.Equalf(t, errReq, err, "unexpected error") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected expr was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, reqResult, m.result, "unexpected result was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - -type executeAsyncMock struct { - baseCallMock -} - -func (m *executeAsyncMock) ExecuteAsync(functionName string, args interface{}, - mode Mode) *tarantool.Future { - return m.callAsync(functionName, args, mode) -} - -func TestConnectorExecuteAsync(t *testing.T) { - m := &executeAsyncMock{} - c := NewConnectorAdapter(m, testMode) - - fut := c.ExecuteAsync(reqFunctionName, reqArgs) - - require.Equalf(t, reqFuture, fut, "unexpected future") - require.Equalf(t, 1, m.called, "should be called only once") - require.Equalf(t, reqFunctionName, m.functionName, - "unexpected expr was passed") - require.Equalf(t, reqArgs, m.args, "unexpected args was passed") - require.Equalf(t, testMode, m.mode, "unexpected proxy mode") -} - var reqPrepared *tarantool.Prepared = &tarantool.Prepared{} type newPreparedMock struct { diff --git a/pool/pooler.go b/pool/pooler.go index fd3df3401..1d05d1eb9 100644 --- a/pool/pooler.go +++ b/pool/pooler.go @@ -29,142 +29,4 @@ type Pooler interface { NewWatcher(key string, callback tarantool.WatchCallback, mode Mode) (tarantool.Watcher, error) Do(req tarantool.Request, mode Mode) (fut *tarantool.Future) - - // Deprecated: the method will be removed in the next major version, - // use a PingRequest object + Do() instead. - Ping(mode Mode) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a SelectRequest object + Do() instead. - Select(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, - key interface{}, mode ...Mode) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use an InsertRequest object + Do() instead. - Insert(space interface{}, tuple interface{}, - mode ...Mode) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a ReplaceRequest object + Do() instead. - Replace(space interface{}, tuple interface{}, - mode ...Mode) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a DeleteRequest object + Do() instead. - Delete(space, index interface{}, key interface{}, - mode ...Mode) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a UpdateRequest object + Do() instead. - Update(space, index interface{}, key interface{}, ops *tarantool.Operations, - mode ...Mode) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a UpsertRequest object + Do() instead. - Upsert(space interface{}, tuple interface{}, ops *tarantool.Operations, - mode ...Mode) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a CallRequest object + Do() instead. - Call(functionName string, args interface{}, - mode Mode) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a Call16Request object + Do() instead. - Call16(functionName string, args interface{}, - mode Mode) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use a Call17Request object + Do() instead. - Call17(functionName string, args interface{}, - mode Mode) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use an EvalRequest object + Do() instead. - Eval(expr string, args interface{}, - mode Mode) ([]interface{}, error) - // Deprecated: the method will be removed in the next major version, - // use an ExecuteRequest object + Do() instead. - Execute(expr string, args interface{}, - mode Mode) ([]interface{}, error) - - // Deprecated: the method will be removed in the next major version, - // use a SelectRequest object + Do() instead. - GetTyped(space, index interface{}, key interface{}, result interface{}, - mode ...Mode) error - // Deprecated: the method will be removed in the next major version, - // use a SelectRequest object + Do() instead. - SelectTyped(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, - key interface{}, result interface{}, mode ...Mode) error - // Deprecated: the method will be removed in the next major version, - // use an InsertRequest object + Do() instead. - InsertTyped(space interface{}, tuple interface{}, result interface{}, - mode ...Mode) error - // Deprecated: the method will be removed in the next major version, - // use a ReplaceRequest object + Do() instead. - ReplaceTyped(space interface{}, tuple interface{}, result interface{}, - mode ...Mode) error - // Deprecated: the method will be removed in the next major version, - // use a DeleteRequest object + Do() instead. - DeleteTyped(space, index interface{}, key interface{}, result interface{}, - mode ...Mode) error - // Deprecated: the method will be removed in the next major version, - // use a UpdateRequest object + Do() instead. - UpdateTyped(space, index interface{}, key interface{}, - ops *tarantool.Operations, result interface{}, mode ...Mode) error - // Deprecated: the method will be removed in the next major version, - // use a CallRequest object + Do() instead. - CallTyped(functionName string, args interface{}, result interface{}, - mode Mode) error - // Deprecated: the method will be removed in the next major version, - // use a Call16Request object + Do() instead. - Call16Typed(functionName string, args interface{}, result interface{}, - mode Mode) error - // Deprecated: the method will be removed in the next major version, - // use a Call17Request object + Do() instead. - Call17Typed(functionName string, args interface{}, result interface{}, - mode Mode) error - // Deprecated: the method will be removed in the next major version, - // use an EvalRequest object + Do() instead. - EvalTyped(expr string, args interface{}, result interface{}, - mode Mode) error - // Deprecated: the method will be removed in the next major version, - // use an ExecuteRequest object + Do() instead. - ExecuteTyped(expr string, args interface{}, result interface{}, - mode Mode) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) - - // Deprecated: the method will be removed in the next major version, - // use a SelectRequest object + Do() instead. - SelectAsync(space, index interface{}, offset, limit uint32, iterator tarantool.Iter, - key interface{}, mode ...Mode) *tarantool.Future - // Deprecated: the method will be removed in the next major version, - // use an InsertRequest object + Do() instead. - InsertAsync(space interface{}, tuple interface{}, - mode ...Mode) *tarantool.Future - // Deprecated: the method will be removed in the next major version, - // use a ReplaceRequest object + Do() instead. - ReplaceAsync(space interface{}, tuple interface{}, - mode ...Mode) *tarantool.Future - // Deprecated: the method will be removed in the next major version, - // use a DeleteRequest object + Do() instead. - DeleteAsync(space, index interface{}, key interface{}, - mode ...Mode) *tarantool.Future - // Deprecated: the method will be removed in the next major version, - // use a UpdateRequest object + Do() instead. - UpdateAsync(space, index interface{}, key interface{}, - ops *tarantool.Operations, mode ...Mode) *tarantool.Future - // Deprecated: the method will be removed in the next major version, - // use a UpsertRequest object + Do() instead. - UpsertAsync(space interface{}, tuple interface{}, ops *tarantool.Operations, - mode ...Mode) *tarantool.Future - // Deprecated: the method will be removed in the next major version, - // use a CallRequest object + Do() instead. - CallAsync(functionName string, args interface{}, - mode Mode) *tarantool.Future - // Deprecated: the method will be removed in the next major version, - // use a Call16Request object + Do() instead. - Call16Async(functionName string, args interface{}, - mode Mode) *tarantool.Future - // Deprecated: the method will be removed in the next major version, - // use a Call17Request object + Do() instead. - Call17Async(functionName string, args interface{}, - mode Mode) *tarantool.Future - // Deprecated: the method will be removed in the next major version, - // use an EvalRequest object + Do() instead. - EvalAsync(expr string, args interface{}, - mode Mode) *tarantool.Future - // Deprecated: the method will be removed in the next major version, - // use an ExecuteRequest object + Do() instead. - ExecuteAsync(expr string, args interface{}, - mode Mode) *tarantool.Future } From c2db82e5fcd2ea667303464f766db73741ca86d3 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Thu, 23 Oct 2025 23:40:57 +0300 Subject: [PATCH 603/605] tests: replace benchmarks Existing benchmarks are no longer relevant - no one knows what they're testing. Instead, benchmarks have been added for the following metrics: * Number of allocations. * RPS on synchronous and asynchronous modes. --- tarantool_test.go | 652 +++++++++++++--------------------------------- 1 file changed, 180 insertions(+), 472 deletions(-) diff --git a/tarantool_test.go b/tarantool_test.go index 4b8873eb7..08cbd4a89 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -13,9 +13,9 @@ import ( "path/filepath" "reflect" "regexp" - "runtime" "strings" "sync" + "sync/atomic" "testing" "time" @@ -97,611 +97,319 @@ var opts = Opts{ const N = 500 -func BenchmarkClientSerial(b *testing.B) { +func BenchmarkSync_naive(b *testing.B) { var err error conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err = conn.Do( + NewReplaceRequest(spaceNo). + Tuple([]interface{}{uint(1111), "hello", "world"}), + ).Get() if err != nil { - b.Errorf("No connection available") + b.Fatalf("failed to initialize database: %s", err) } b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) + + for b.Loop() { + req := NewSelectRequest(spaceNo). + Index(indexNo). + Iterator(IterEq). + Key([]interface{}{uint(1111)}) + data, err := conn.Do(req).Get() if err != nil { - b.Errorf("No connection available") + b.Errorf("request error: %s", err) + } + + tuple := data[0].([]any) + if tuple[0].(uint16) != uint16(1111) { + b.Errorf("invalid result") } } } -func BenchmarkClientSerialRequestObject(b *testing.B) { +func BenchmarkSync_naive_with_single_request(b *testing.B) { var err error conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err = conn.Do( + NewReplaceRequest(spaceNo). + Tuple([]interface{}{uint(1111), "hello", "world"}), + ).Get() if err != nil { - b.Error(err) + b.Fatalf("failed to initialize database: %s", err) } + req := NewSelectRequest(spaceNo). Index(indexNo). - Offset(0). - Limit(1). Iterator(IterEq). - Key([]interface{}{uint(1111)}) + Key(UintKey{I: 1111}) b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := conn.Do(req).Get() + for b.Loop() { + data, err := conn.Do(req).Get() if err != nil { - b.Error(err) + b.Errorf("request error: %s", err) } - } -} - -func BenchmarkClientSerialRequestObjectWithContext(b *testing.B) { - var err error - - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() - - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) - if err != nil { - b.Error(err) - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - req := NewSelectRequest(spaceNo). - Index(indexNo). - Limit(1). - Iterator(IterEq). - Key([]interface{}{uint(1111)}). - Context(ctx) - _, err := conn.Do(req).Get() - if err != nil { - b.Error(err) + tuple := data[0].([]any) + if tuple[0].(uint16) != uint16(1111) { + b.Errorf("invalid result") } } } -func BenchmarkClientSerialTyped(b *testing.B) { - var err error - - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() - - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) - if err != nil { - b.Fatal("No connection available") - } - - var r []Tuple - b.ResetTimer() - for i := 0; i < b.N; i++ { - err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, IntKey{1111}, &r) - if err != nil { - b.Errorf("No connection available") - } - } +type benchTuple struct { + id uint } -func BenchmarkClientSerialSQL(b *testing.B) { - test_helpers.SkipIfSQLUnsupported(b) - - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() - - _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) +func (t *benchTuple) DecodeMsgpack(dec *msgpack.Decoder) error { + l, err := dec.DecodeArrayLen() if err != nil { - b.Errorf("Failed to replace: %s", err) + return fmt.Errorf("failed to decode tuples array: %w", err) } - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", - []interface{}{uint(1111)}) - if err != nil { - b.Errorf("Select failed: %s", err.Error()) - break - } - } -} - -func BenchmarkClientSerialSQLPrepared(b *testing.B) { - test_helpers.SkipIfSQLUnsupported(b) - - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() - - _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) - if err != nil { - b.Errorf("Failed to replace: %s", err) + if l != 1 { + return fmt.Errorf("unexpected tuples array with len %d", l) } - stmt, err := conn.NewPrepared("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?") + l, err = dec.DecodeArrayLen() if err != nil { - b.Fatalf("failed to prepare a SQL statement") + return fmt.Errorf("failed to decode tuple array: %w", err) } - executeReq := NewExecutePreparedRequest(stmt) - unprepareReq := NewUnprepareRequest(stmt) - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := conn.Do(executeReq.Args([]interface{}{uint(1111)})).Get() - if err != nil { - b.Errorf("Select failed: %s", err.Error()) - break - } + if l < 1 { + return fmt.Errorf("too small tuple have 0 fields") } - _, err = conn.Do(unprepareReq).Get() - if err != nil { - b.Fatalf("failed to unprepare a SQL statement") - } -} - -func BenchmarkClientFuture(b *testing.B) { - var err error - - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + t.id, err = dec.DecodeUint() if err != nil { - b.Error(err) + return fmt.Errorf("failed to decode id: %w", err) } - b.ResetTimer() - for i := 0; i < b.N; i += N { - var fs [N]*Future - for j := 0; j < N; j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) - } - for j := 0; j < N; j++ { - _, err = fs[j].Get() - if err != nil { - b.Error(err) - } - } - - } + return nil } -func BenchmarkClientFutureTyped(b *testing.B) { +func BenchmarkSync_naive_with_custom_type(b *testing.B) { var err error conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err = conn.Do( + NewReplaceRequest(spaceNo). + Tuple([]interface{}{uint(1111), "hello", "world"}), + ).Get() if err != nil { - b.Errorf("No connection available") + b.Fatalf("failed to initialize database: %s", err) } - b.ResetTimer() - for i := 0; i < b.N; i += N { - var fs [N]*Future - for j := 0; j < N; j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, IntKey{1111}) - } - var r []Tuple - for j := 0; j < N; j++ { - err = fs[j].GetTyped(&r) - if err != nil { - b.Error(err) - } - if len(r) != 1 || r[0].Id != 1111 { - b.Errorf("Doesn't match %v", r) - } - } - } -} - -func BenchmarkClientFutureParallel(b *testing.B) { - var err error - - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() + req := NewSelectRequest(spaceNo). + Index(indexNo). + Iterator(IterEq). + Key(UintKey{I: 1111}) - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) - if err != nil { - b.Errorf("No connection available") - } + var tuple benchTuple b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - exit := false - for !exit { - var fs [N]*Future - var j int - for j = 0; j < N && pb.Next(); j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) - } - exit = j < N - for j > 0 { - j-- - _, err := fs[j].Get() - if err != nil { - b.Error(err) - break - } - } - } - }) -} - -func BenchmarkClientFutureParallelTyped(b *testing.B) { - var err error - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() - - _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) - if err != nil { - b.Fatal("No connection available") - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - exit := false - for !exit { - var fs [N]*Future - var j int - for j = 0; j < N && pb.Next(); j++ { - fs[j] = conn.SelectAsync(spaceNo, indexNo, 0, 1, IterEq, IntKey{1111}) - } - exit = j < N - var r []Tuple - for j > 0 { - j-- - err := fs[j].GetTyped(&r) - if err != nil { - b.Error(err) - break - } - if len(r) != 1 || r[0].Id != 1111 { - b.Errorf("Doesn't match %v", r) - break - } - } + for b.Loop() { + err := conn.Do(req).GetTyped(&tuple) + if err != nil { + b.Errorf("request error: %s", err) } - }) -} - -func BenchmarkClientParallel(b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() - _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) - if err != nil { - b.Fatal("No connection available") - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _, err := conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) - if err != nil { - b.Errorf("No connection available") - break - } + if tuple.id != 1111 { + b.Errorf("invalid result") } - }) + } } -func benchmarkClientParallelRequestObject(multiplier int, b *testing.B) { +func BenchmarkSync_multithread(b *testing.B) { + var err error + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() - _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err = conn.Do( + NewReplaceRequest(spaceNo). + Tuple([]interface{}{uint(1111), "hello", "world"}), + ).Get() if err != nil { - b.Fatal("No connection available") + b.Fatalf("failed to initialize database: %s", err) } req := NewSelectRequest(spaceNo). Index(indexNo). - Limit(1). Iterator(IterEq). - Key([]interface{}{uint(1111)}) + Key(UintKey{I: 1111}) - b.SetParallelism(multiplier) b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + var tuple benchTuple + for pb.Next() { - _ = conn.Do(req) - _, err := conn.Do(req).Get() + err := conn.Do(req).GetTyped(&tuple) if err != nil { - b.Error(err) + b.Errorf("request error: %s", err) } - } - }) -} - -func benchmarkClientParallelRequestObjectWithContext(multiplier int, b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() - - _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) - if err != nil { - b.Fatal("No connection available") - } - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - req := NewSelectRequest(spaceNo). - Index(indexNo). - Limit(1). - Iterator(IterEq). - Key([]interface{}{uint(1111)}). - Context(ctx) - - b.SetParallelism(multiplier) - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = conn.Do(req) - _, err := conn.Do(req).Get() - if err != nil { - b.Error(err) + if tuple.id != 1111 { + b.Errorf("invalid result") } } }) } -func benchmarkClientParallelRequestObjectMixed(multiplier int, b *testing.B) { +func BenchmarkAsync_multithread_parallelism(b *testing.B) { + var err error + conn := test_helpers.ConnectWithValidation(b, dialer, opts) defer conn.Close() - _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + _, err = conn.Do( + NewReplaceRequest(spaceNo). + Tuple([]interface{}{uint(1111), "hello", "world"}), + ).Get() if err != nil { - b.Fatal("No connection available") + b.Fatalf("failed to initialize database: %s", err) } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - req := NewSelectRequest(spaceNo). Index(indexNo). - Limit(1). - Iterator(IterEq). - Key([]interface{}{uint(1111)}) - - reqWithCtx := NewSelectRequest(spaceNo). - Index(indexNo). - Limit(1). Iterator(IterEq). - Key([]interface{}{uint(1111)}). - Context(ctx) + Key(UintKey{I: 1111}) - b.SetParallelism(multiplier) b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = conn.Do(req) - _, err := conn.Do(reqWithCtx).Get() - if err != nil { - b.Error(err) - } - } - }) -} -func BenchmarkClientParallelRequestObject(b *testing.B) { - multipliers := []int{10, 50, 500, 1000} - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() + for p := 1; p <= 1024; p *= 2 { + b.Run(fmt.Sprintf("%d", p), func(b *testing.B) { + b.SetParallelism(p) + b.ResetTimer() - _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) - if err != nil { - b.Fatal("No connection available") - } - - for _, m := range multipliers { - goroutinesNum := runtime.GOMAXPROCS(0) * m - - b.Run(fmt.Sprintf("Plain %d goroutines", goroutinesNum), func(b *testing.B) { - benchmarkClientParallelRequestObject(m, b) - }) + b.RunParallel(func(pb *testing.PB) { + var tuple benchTuple - b.Run(fmt.Sprintf("With Context %d goroutines", goroutinesNum), func(b *testing.B) { - benchmarkClientParallelRequestObjectWithContext(m, b) - }) + for pb.Next() { + err := conn.Do(req).GetTyped(&tuple) + if err != nil { + b.Errorf("request error: %s", err) + } - b.Run(fmt.Sprintf("Mixed %d goroutines", goroutinesNum), func(b *testing.B) { - benchmarkClientParallelRequestObjectMixed(m, b) + if tuple.id != 1111 { + b.Errorf("invalid result") + } + } + }) }) } } -func BenchmarkClientParallelMassive(b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() +// TestBenchmarkAsync is a benchmark for the async API that is unable to +// implement with a Go-benchmark. It can be used to test performance with +// different numbers of connections and processing goroutines. +func TestBenchmarkAsync(t *testing.T) { + t.Skip() - _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) - if err != nil { - b.Fatal("No connection available") - } + requests := int64(10_000_000) + connections := 16 - var wg sync.WaitGroup - limit := make(chan struct{}, 128*1024) - for i := 0; i < 512; i++ { - go func() { - var r []Tuple - for { - if _, ok := <-limit; !ok { - break - } - err = conn.SelectTyped(spaceNo, indexNo, 0, 1, IterEq, IntKey{1111}, &r) - wg.Done() - if err != nil { - b.Errorf("No connection available") - } - } - }() - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - wg.Add(1) - limit <- struct{}{} - } - wg.Wait() - close(limit) -} + ops := opts + // ops.Concurrency = 2 // 4 max. // 2 max. -func BenchmarkClientParallelMassiveUntyped(b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() + conns := make([]*Connection, 0, connections) + for range connections { + conn := test_helpers.ConnectWithValidation(t, dialer, ops) + defer conn.Close() - _, err := conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) - if err != nil { - b.Errorf("No connection available") + conns = append(conns, conn) } - var wg sync.WaitGroup - limit := make(chan struct{}, 128*1024) - for i := 0; i < 512; i++ { - go func() { - for { - if _, ok := <-limit; !ok { - break - } - _, err = conn.Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)}) - wg.Done() - if err != nil { - b.Errorf("No connection available") - } - } - }() + _, err := conns[0].Do( + NewReplaceRequest(spaceNo). + Tuple([]interface{}{uint(1111), "hello", "world"}), + ).Get() + if err != nil { + t.Fatalf("failed to initialize database: %s", err) } - b.ResetTimer() - for i := 0; i < b.N; i++ { - wg.Add(1) - limit <- struct{}{} - } - wg.Wait() - close(limit) -} + req := NewSelectRequest(spaceNo). + Index(indexNo). + Iterator(IterEq). + Key(UintKey{I: 1111}) -func BenchmarkClientReplaceParallel(b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() + maxRps := float64(0) + maxConnections := 0 + maxConcurrency := 0 - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _, err := conn.Replace("test_perf", []interface{}{uint(1), "hello", []interface{}{}}) - if err != nil { - b.Error(err) - } - } - }) -} + for cn := 1; cn <= connections; cn *= 2 { + for cc := 1; cc <= 512; cc *= 2 { + var wg sync.WaitGroup -func BenchmarkClientLargeSelectParallel(b *testing.B) { - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() + curRequests := requests - offset, limit := uint32(0), uint32(1000) - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _, err := conn.Select("test_perf", "secondary", offset, limit, IterEq, - []interface{}{"test_name"}) - if err != nil { - b.Fatal(err) - } - } - }) -} + start := time.Now() -func BenchmarkClientParallelSQL(b *testing.B) { - test_helpers.SkipIfSQLUnsupported(b) + for i := range cc { + wg.Add(1) - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() + ch := make(chan *Future, 1024) - _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) - if err != nil { - b.Errorf("No connection available") - } + go func(i int) { + defer close(ch) - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", - []interface{}{uint(1111)}) - if err != nil { - b.Errorf("Select failed: %s", err.Error()) - break - } - } - }) -} - -func BenchmarkClientParallelSQLPrepared(b *testing.B) { - test_helpers.SkipIfSQLUnsupported(b) + for atomic.AddInt64(&curRequests, -1) >= 0 { + ch <- conns[i%cn].Do(req) + } + }(i) - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() + go func() { + defer wg.Done() - _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) - if err != nil { - b.Errorf("No connection available") - } + var tuple benchTuple - stmt, err := conn.NewPrepared("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?") - if err != nil { - b.Fatalf("failed to prepare a SQL statement") - } - executeReq := NewExecutePreparedRequest(stmt) - unprepareReq := NewUnprepareRequest(stmt) + for fut := range ch { + err := fut.GetTyped(&tuple) + if err != nil { + t.Errorf("request error: %s", err) + } - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _, err := conn.Do(executeReq.Args([]interface{}{uint(1111)})).Get() - if err != nil { - b.Errorf("Select failed: %s", err.Error()) - break + if tuple.id != 1111 { + t.Errorf("invalid result") + } + } + }() } - } - }) - _, err = conn.Do(unprepareReq).Get() - if err != nil { - b.Fatalf("failed to unprepare a SQL statement") - } -} -func BenchmarkSQLSerial(b *testing.B) { - test_helpers.SkipIfSQLUnsupported(b) + wg.Wait() - conn := test_helpers.ConnectWithValidation(b, dialer, opts) - defer conn.Close() + duration := time.Since(start) - _, err := conn.Replace("SQL_TEST", []interface{}{uint(1111), "hello", "world"}) - if err != nil { - b.Errorf("Failed to replace: %s", err) - } + rps := float64(requests) / duration.Seconds() + t.Log("requests :", requests) + t.Log("concurrency:", cc) + t.Log("connections:", cn) + t.Logf("duration : %.2f\n", duration.Seconds()) + t.Logf("requests/s : %.2f\n", rps) + t.Log("============") - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", - []interface{}{uint(1111)}) - if err != nil { - b.Errorf("Select failed: %s", err.Error()) - break + if maxRps < rps { + maxRps = rps + maxConnections = cn + maxConcurrency = cc + } } } + + t.Log("max connections:", maxConnections) + t.Log("max concurrency:", maxConcurrency) + t.Logf("max requests/s : %.2f\n", maxRps) } type mockRequest struct { @@ -2564,7 +2272,7 @@ func TestConnectionDoSelectRequest(t *testing.T) { req := NewSelectRequest(spaceNo). Index(indexNo). - Limit(20). + Limit(10). Iterator(IterGe). Key([]interface{}{uint(1010)}) resp, err := conn.Do(req).GetResponse() From 29b97f09272a0b46b901c5fe6f1b4ce32396435c Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 3 Nov 2025 11:46:15 +0300 Subject: [PATCH 604/605] tests: increase initial connect timeout It should help to fix flaky tests on CI. --- test_helpers/main.go | 2 +- test_helpers/pool_helper.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test_helpers/main.go b/test_helpers/main.go index 35c57f810..c80683d94 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -569,5 +569,5 @@ func ConvertUint64(v interface{}) (result uint64, err error) { } func GetConnectContext() (context.Context, context.CancelFunc) { - return context.WithTimeout(context.Background(), 500*time.Millisecond) + return context.WithTimeout(context.Background(), time.Second) } diff --git a/test_helpers/pool_helper.go b/test_helpers/pool_helper.go index 7831b9f6e..81eb8e2f3 100644 --- a/test_helpers/pool_helper.go +++ b/test_helpers/pool_helper.go @@ -304,5 +304,5 @@ func StopTarantoolInstances(instances []*TarantoolInstance) { } func GetPoolConnectContext() (context.Context, context.CancelFunc) { - return context.WithTimeout(context.Background(), 500*time.Millisecond) + return context.WithTimeout(context.Background(), time.Second) } From 802aa244d26017a901cf7247119515f346ac8eb6 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Mon, 3 Nov 2025 13:13:10 +0300 Subject: [PATCH 605/605] tests: fix flaky TestFdDialer_Dial_requirements It seems that a connection's file descriptor is not always fully ready after the connection is created. The hack helps to avoid the "bad file descriptor" flaky errors for the test. --- dial_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/dial_test.go b/dial_test.go index f2cacc4cf..ad55bb95a 100644 --- a/dial_test.go +++ b/dial_test.go @@ -658,8 +658,21 @@ func TestFdDialer_Dial(t *testing.T) { t.Run(tc.name, func(t *testing.T) { sock, err := net.Dial("tcp", addr) require.NoError(t, err) + defer sock.Close() + + // It seems that the file descriptor is not always fully ready + // after the connection is created. These lines help to avoid the + // "bad file descriptor" errors. + // + // We already tried to use the SyscallConn(), but it has the same + // issue. + time.Sleep(time.Millisecond) + sock.(*net.TCPConn).SetLinger(0) + f, err := sock.(*net.TCPConn).File() require.NoError(t, err) + defer f.Close() + dialer := tarantool.FdDialer{ Fd: f.Fd(), } @@ -676,8 +689,21 @@ func TestFdDialer_Dial_requirements(t *testing.T) { sock, err := net.Dial("tcp", addr) require.NoError(t, err) + defer sock.Close() + + // It seems that the file descriptor is not always fully ready after the + // connection is created. These lines help to avoid the + // "bad file descriptor" errors. + // + // We already tried to use the SyscallConn(), but it has the same + // issue. + time.Sleep(time.Millisecond) + sock.(*net.TCPConn).SetLinger(0) + f, err := sock.(*net.TCPConn).File() require.NoError(t, err) + defer f.Close() + dialer := tarantool.FdDialer{ Fd: f.Fd(), RequiredProtocolInfo: tarantool.ProtocolInfo{ @@ -688,6 +714,7 @@ func TestFdDialer_Dial_requirements(t *testing.T) { testDialAccept(testDialOpts{}, l) ctx, cancel := test_helpers.GetConnectContext() defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) if err == nil { conn.Close()