├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── auth.go ├── changelog.md ├── config.lua ├── connection.go ├── connection_test.go ├── const.go ├── deadline_io.go ├── decoder_pool.go ├── errors.go ├── example_test.go ├── examples ├── go.mod ├── go.sum └── readme_example │ ├── example.lua │ └── main.go ├── future.go ├── go.mod ├── go.sum ├── helpers.go ├── operator.go ├── request.go ├── response.go ├── schema.go ├── smallbuf.go └── tarantool_test.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | golangci: 5 | name: Lint 6 | runs-on: ubuntu-latest 7 | # Prevent duplicate builds on internal PRs. 8 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-go@v3 12 | with: 13 | go-version: '1.17.x' 14 | - name: golangci-lint 15 | uses: golangci/golangci-lint-action@v3 16 | with: 17 | version: v1.45.2 18 | args: --timeout 3m0s 19 | build: 20 | name: Test Go ${{ matrix.go-version }} / Tarantool ${{ matrix.tarantool-version }} 21 | runs-on: ubuntu-latest 22 | # Prevent duplicate builds on internal PRs. 23 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 24 | strategy: 25 | matrix: 26 | go-version: [1.17] 27 | tarantool-version: [2.7.0, 2.5.2, 1.10.8] 28 | steps: 29 | - name: Install Go stable version 30 | uses: actions/setup-go@v2 31 | with: 32 | go-version: ${{ matrix.go-version }} 33 | 34 | - name: Checkout code 35 | uses: actions/checkout@v2 36 | 37 | - name: Start Tarantool 38 | run: docker run -d -p 3301:3301 -v $(pwd):/opt/tarantool tarantool/tarantool:${{ matrix.tarantool-version }} tarantool /opt/tarantool/config.lua 39 | 40 | - name: Test 41 | run: go test -v -race 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .vscode 3 | .idea 4 | *.snap 5 | *.xlog 6 | *.snap.inprogress 7 | .rocks/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2014-2017, Tarantool AUTHORS 4 | Copyright (c) 2014-2017, Dmitry Smal 5 | Copyright (c) 2014-2017, Yura Sokolov aka funny_falcon 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/FZambia/tarantool/workflows/build/badge.svg?branch=master)](https://github.com/FZambia/tarantool/actions) 2 | [![GoDoc](https://pkg.go.dev/badge/FZambia/tarantool)](https://pkg.go.dev/github.com/FZambia/tarantool) 3 | 4 | # Tarantool client in Go language 5 | 6 | The `tarantool` package allows communicating with [Tarantool 1.7.1+](http://tarantool.org/). 7 | 8 | This is an opinionated modification of [github.com/tarantool/go-tarantool](https://github.com/tarantool/go-tarantool) package. The original license kept unchanged here at the moment. 9 | 10 | ## Differences from the original package 11 | 12 | * API changed, some non-obvious (mostly to me personally) API removed. 13 | * This package uses the latest msgpack library [github.com/vmihailenco/msgpack/v5](https://github.com/vmihailenco/msgpack) instead of `v2` in original. 14 | * Uses `UseArrayEncodedStructs(true)` for `msgpack.Encoder` by default so there is no need to define `msgpack:",as_array"` struct tags. If you need to disable this (for example when using nested structs) then this behavior can be disabled using `DisableArrayEncodedStructs` option. 15 | * Uses `UseLooseInterfaceDecoding(true)` for `msgpack.Decoder` to decode response into untyped `[]interface{}` result. See [decoding rules](https://pkg.go.dev/github.com/vmihailenco/msgpack/v5#Decoder.DecodeInterfaceLoose). 16 | * Supports out-of-bound pushes (see [box.session.push](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_session/#box-session-push)) 17 | * Adds optional support for `context.Context` (though performance will suffer a bit, if you want a maximum performance then use non-context methods which use per-connection timeout). Context cancellation does not cancel a query (Tarantool has no such functionality) - just stops waiting for request future resolving. 18 | * Uses sync.Pool for `*msgpack.Decoder` to reduce allocations on decoding stage a bit. Actually this package allocates a bit more than the original one, but allocations are small and overall performance is comparable to the original (based on observations from internal benchmarks). 19 | * No `multi` and `queue` packages. 20 | * Only one version of `Call` which uses Tarantool 1.7 request code. 21 | * Modified connection address behavior: refer to `Connect` function docs to see details. 22 | * Per-request timeout detached from underlying connection read and write timeouts. 23 | * `Op` type to express different update/upsert operations. 24 | * Some other cosmetic changes including several linter fixes. 25 | * No default `Logger` – developer needs to provide custom implementation explicitly. 26 | 27 | The networking core of `github.com/tarantool/go-tarantool` kept mostly unchanged at the moment so this package should behave in similar way. 28 | 29 | ## Installation 30 | 31 | ``` 32 | $ go get github.com/FZambia/tarantool 33 | ``` 34 | 35 | ## Status 36 | 37 | This library is a prototype for [Centrifuge](https://github.com/centrifugal/centrifuge)/[Centrifugo](https://github.com/centrifugal/centrifugo) ongoing Tarantool Engine experiment. 38 | 39 | **API is not stable here** and can have changes as experiment evolves. Also, there are no concrete plans at the moment regarding the package maintenance. 40 | 41 | The versioning politics before v1 will be the following: patch version updates will only contain backwards compatible changes, minor version updates may have backwards incompatible changes. 42 | 43 | ## Quick start 44 | 45 | Create `example.lua` file with content: 46 | 47 | ```lua 48 | box.cfg{listen = 3301} 49 | box.schema.space.create('examples', {id = 999}) 50 | box.space.examples:create_index('primary', {type = 'hash', parts = {1, 'unsigned'}}) 51 | box.schema.user.grant('guest', 'read,write', 'space', 'examples') 52 | ``` 53 | 54 | Run it with Tarantool: 55 | 56 | ``` 57 | tarantool example.lua 58 | ``` 59 | 60 | Then create `main.go` file: 61 | 62 | ```go 63 | package main 64 | 65 | import ( 66 | "log" 67 | "time" 68 | 69 | "github.com/FZambia/tarantool" 70 | ) 71 | 72 | type Row struct { 73 | ID uint64 74 | Value string 75 | } 76 | 77 | func main() { 78 | opts := tarantool.Opts{ 79 | RequestTimeout: 500 * time.Millisecond, 80 | User: "guest", 81 | } 82 | conn, err := tarantool.Connect("127.0.0.1:3301", opts) 83 | if err != nil { 84 | log.Fatalf("Connection refused: %v", err) 85 | } 86 | defer func() { _ = conn.Close() }() 87 | 88 | _, err = conn.Exec(tarantool.Insert("examples", Row{ID: 999, Value: "hello"})) 89 | if err != nil { 90 | log.Fatalf("Insert failed: %v", err) 91 | } 92 | log.Println("Insert succeeded") 93 | } 94 | ``` 95 | 96 | Finally, run it with: 97 | 98 | ``` 99 | go run main.go 100 | ``` 101 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/base64" 6 | ) 7 | 8 | func scramble(encodedSalt, pass string) (scramble []byte, err error) { 9 | /* ================================================================== 10 | According to: http://tarantool.org/doc/dev_guide/box-protocol.html 11 | 12 | salt = base64_decode(encodedSalt); 13 | step1 = sha1(password); 14 | step2 = sha1(step1); 15 | step3 = sha1(salt, step2); 16 | scramble = xor(step1, step3); 17 | return scramble; 18 | 19 | ===================================================================== */ 20 | scrambleSize := sha1.Size // == 20 21 | 22 | salt, err := base64.StdEncoding.DecodeString(encodedSalt) 23 | if err != nil { 24 | return 25 | } 26 | step1 := sha1.Sum([]byte(pass)) 27 | step2 := sha1.Sum(step1[0:]) 28 | hash := sha1.New() // may be create it once per connection ? 29 | _, err = hash.Write(salt[0:scrambleSize]) 30 | if err != nil { 31 | return 32 | } 33 | _, err = hash.Write(step2[0:]) 34 | if err != nil { 35 | return 36 | } 37 | step3 := hash.Sum(nil) 38 | 39 | return xor(step1[0:], step3[0:], scrambleSize), nil 40 | } 41 | 42 | func xor(left, right []byte, size int) []byte { 43 | result := make([]byte, size) 44 | for i := 0; i < size; i++ { 45 | result[i] = left[i] ^ right[i] 46 | } 47 | return result 48 | } 49 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | v0.3.1 2 | ====== 3 | 4 | * Fix `panic: close of nil channel` after closing connection after an error - [#10](https://github.com/FZambia/tarantool/issues/10) 5 | 6 | v0.3.0 7 | ====== 8 | 9 | * Removing `Response` type from public API – see [#8](https://github.com/FZambia/tarantool/pull/8) for details. 10 | 11 | ``` 12 | > gorelease -base v0.2.3 -version v0.3.0 13 | # github.com/FZambia/tarantool 14 | ## incompatible changes 15 | (*Connection).Exec: changed from func(*Request) (*Response, error) to func(*Request) ([]interface{}, error) 16 | (*Connection).ExecContext: changed from func(context.Context, *Request) (*Response, error) to func(context.Context, *Request) ([]interface{}, error) 17 | (*Request).WithPush: changed from func(func(*Response)) *Request to func(func([]interface{})) *Request 18 | ErrorCodeBit: removed 19 | Future.Get: changed from func() (*Response, error) to func() ([]interface{}, error) 20 | FutureContext.GetContext: changed from func(context.Context) (*Response, error) to func(context.Context) ([]interface{}, error) 21 | OkCode: removed 22 | Response: removed 23 | 24 | # summary 25 | v0.3.0 is a valid semantic version for this release. 26 | ``` 27 | 28 | v0.2.3 29 | ====== 30 | 31 | * Add `DisableArrayEncodedStructs` option. 32 | 33 | ``` 34 | > gorelease -base v0.2.2 -version v0.2.3 35 | github.com/FZambia/tarantool 36 | ---------------------------- 37 | Compatible changes: 38 | - Opts.DisableArrayEncodedStructs: added 39 | 40 | v0.2.3 is a valid semantic version for this release. 41 | ``` 42 | 43 | v0.2.2 44 | ====== 45 | 46 | * Up msgpack dependency 47 | 48 | v0.2.1 49 | ====== 50 | 51 | * Fix type conversion causing panic on schema load - see [#3](https://github.com/FZambia/tarantool/issues/3) 52 | 53 | v0.2.0 54 | ====== 55 | 56 | * Fix parsing connection address with non-ip host and without `tcp://` scheme, like `localhost:3301` - previously connecting with such an address resulted in configuration error 57 | 58 | v0.1.1 59 | ====== 60 | 61 | * Fix calling ExecTypedContext w/o timeout - see [#1](https://github.com/FZambia/tarantool/pull/1) 62 | 63 | v0.1.0 64 | ====== 65 | 66 | * Remove default logger - user must explicitly provide logger to get logs from a package 67 | 68 | v0.0.1 69 | ====== 70 | 71 | This is an opinionated modification of [github.com/tarantool/go-tarantool](https://github.com/tarantool/go-tarantool) package. 72 | 73 | Changes from the original: 74 | 75 | * API changed, some non-obvious (mostly to me personally) API removed. 76 | * This package uses the latest msgpack library [github.com/vmihailenco/msgpack/v5](https://github.com/vmihailenco/msgpack) instead of `v2` in original. 77 | * Uses `enc.UseArrayEncodedStructs(true)` for `msgpack.Encoder` internally so there is no need to define `msgpack:",as_array"` struct tags. 78 | * Supports out-of-bound pushes (see [box.session.push](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_session/#box-session-push)) 79 | * Adds optional support for `context.Context` (though performance will suffer a bit, if you want a maximum performance then use non-context methods which use per-connection timeout). 80 | * Uses sync.Pool for `*msgpack.Decoder` to reduce allocations on decoding stage a bit. Actually this package allocates a bit more than the original one, but allocations are small and overall performance is comparable to the original (based on observations from internal benchmarks). 81 | * No `multi` and `queue` packages. 82 | * Only one version of `Call` which uses Tarantool 1.7 request code. 83 | * Modified connection address behavior: refer to `Connect` function docs to see details. 84 | * Per-request timeout detached from underlying connection read and write timeouts. 85 | * `Op` type to express different update/upsert operations. 86 | * Some other cosmetic changes including several linter fixes. 87 | -------------------------------------------------------------------------------- /config.lua: -------------------------------------------------------------------------------- 1 | fiber = require 'fiber' 2 | 3 | box.cfg{ 4 | listen = '0.0.0.0:3301', 5 | readahead = 10 * 1024 * 1024, -- to keep up with benchmark load. 6 | net_msg_max = 10 * 1024, -- to keep up with benchmark load. 7 | } 8 | 9 | box.once("init", function() 10 | local s = box.schema.space.create('test', { 11 | id = 512, 12 | if_not_exists = true, 13 | }) 14 | s:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) 15 | 16 | local st = box.schema.space.create('schematest', { 17 | id = 514, 18 | temporary = true, 19 | if_not_exists = true, 20 | field_count = 7, 21 | format = { 22 | {name = "name0", type = "unsigned"}, 23 | {name = "name1", type = "unsigned"}, 24 | {name = "name2", type = "string"}, 25 | {name = "name3", type = "unsigned"}, 26 | {name = "name4", type = "unsigned"}, 27 | {name = "name5", type = "string"}, 28 | }, 29 | }) 30 | st:create_index('primary', { 31 | type = 'hash', 32 | parts = {1, 'uint'}, 33 | unique = true, 34 | if_not_exists = true, 35 | }) 36 | st:create_index('secondary', { 37 | id = 3, 38 | type = 'tree', 39 | unique = false, 40 | parts = { 2, 'uint', 3, 'string' }, 41 | if_not_exists = true, 42 | }) 43 | st:truncate() 44 | 45 | box.schema.func.create('box.info') 46 | box.schema.func.create('simple_incr') 47 | 48 | -- auth testing: access control 49 | box.schema.user.create('test', {password = 'test'}) 50 | box.schema.user.grant('test', 'execute', 'universe') 51 | box.schema.user.grant('test', 'read,write', 'space', 'test') 52 | box.schema.user.grant('test', 'read,write', 'space', 'schematest') 53 | end) 54 | 55 | function timeout() 56 | fiber.sleep(1) 57 | return 1 58 | end 59 | 60 | function simple_incr(a) 61 | return a+1 62 | end 63 | 64 | box.space.test:truncate() 65 | -------------------------------------------------------------------------------- /connection.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net" 11 | "net/url" 12 | "runtime" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | "sync/atomic" 17 | "time" 18 | 19 | "github.com/vmihailenco/msgpack/v5" 20 | ) 21 | 22 | const requestsMap = 128 23 | 24 | const packetLengthBytes = 5 25 | 26 | const ( 27 | connDisconnected = 0 28 | connConnected = 1 29 | connClosed = 2 30 | ) 31 | 32 | type ConnEventKind int 33 | 34 | const ( 35 | // Connected signals that connection is established or reestablished. 36 | Connected ConnEventKind = iota + 1 37 | // Disconnected signals that connection is broken. 38 | Disconnected 39 | // ReconnectFailed signals that attempt to reconnect has failed. 40 | ReconnectFailed 41 | // Closed means either reconnect attempts exhausted, or explicit Close is called. 42 | Closed 43 | ) 44 | 45 | type ConnLogKind int 46 | 47 | const ( 48 | // LogReconnectFailed is logged when reconnect attempt failed. 49 | LogReconnectFailed ConnLogKind = iota + 1 50 | // LogLastReconnectFailed is logged when last reconnect attempt failed, 51 | // connection will be closed after that. 52 | LogLastReconnectFailed 53 | // LogUnexpectedResultID is logged when response with unknown id were received. 54 | // Most probably it is due to request timeout. 55 | LogUnexpectedResultID 56 | ) 57 | 58 | // DefaultConnectTimeout to Tarantool. 59 | const DefaultConnectTimeout = time.Second 60 | 61 | // DefaultReadTimeout to Tarantool. 62 | const DefaultReadTimeout = 30 * time.Second 63 | 64 | // DefaultWriteTimeout to Tarantool. 65 | const DefaultWriteTimeout = 5 * time.Second 66 | 67 | // ConnEvent is sent throw Notify channel specified in Opts. 68 | type ConnEvent struct { 69 | Conn *Connection 70 | Kind ConnEventKind 71 | When time.Time 72 | } 73 | 74 | // Logger is logger type expected to be passed in options. 75 | type Logger interface { 76 | Report(event ConnLogKind, conn *Connection, v ...interface{}) 77 | } 78 | 79 | // Connection to Tarantool. 80 | // 81 | // It is created and configured with Connect function, and could not be 82 | // reconfigured later. 83 | // 84 | // Connection can be "Connected", "Disconnected", and "Closed". 85 | // 86 | // When "Connected" it sends queries to Tarantool. 87 | // 88 | // When "Disconnected" it rejects queries with ClientError{Code: ErrConnectionNotReady} 89 | // 90 | // When "Closed" it rejects queries with ClientError{Code: ErrConnectionClosed} 91 | // 92 | // Connection could become "Closed" when Connection.Close() method called, 93 | // or when Tarantool disconnected and ReconnectDelay pause is not specified or 94 | // MaxReconnects is specified and MaxReconnect reconnect attempts already performed. 95 | // 96 | // You may perform data manipulation operation by executing: 97 | // Call, Insert, Replace, Update, Upsert, Eval. 98 | // 99 | // In any method that accepts `space` you may pass either space number or 100 | // space name (in this case it will be looked up in schema). Same is true for `index`. 101 | // 102 | // A Connection can be used simultaneously from multiple goroutines. 103 | // 104 | // ATTENTION: `tuple`, `key`, `ops` and `args` arguments for any method should be 105 | // and array or should serialize to msgpack array. 106 | type Connection struct { 107 | state uint32 // Keep atomics on top to work on 32-bit architectures. 108 | requestID uint32 // Keep atomics on top to work on 32-bit architectures. 109 | 110 | c net.Conn 111 | mutex sync.Mutex 112 | 113 | schema *Schema 114 | greeting *Greeting 115 | 116 | shard []connShard 117 | dirtyShard chan uint32 118 | 119 | control chan struct{} 120 | rLimit chan struct{} 121 | opts Opts 122 | dec *msgpack.Decoder 123 | lenBuf [packetLengthBytes]byte 124 | } 125 | 126 | type connShard struct { 127 | mu sync.Mutex 128 | requests [requestsMap]struct { 129 | first *futureImpl 130 | last **futureImpl 131 | } 132 | bufMu sync.Mutex 133 | buf smallWBuf 134 | enc *msgpack.Encoder 135 | _ [16]uint64 136 | } 137 | 138 | // Greeting is a message sent by tarantool on connect. 139 | type Greeting struct { 140 | Version string 141 | auth string 142 | } 143 | 144 | // Opts is a way to configure Connection. 145 | type Opts struct { 146 | // User for auth. 147 | User string 148 | // Password for auth. 149 | Password string 150 | 151 | // ConnectTimeout sets connect timeout. If not set then DefaultConnectTimeout will be used. 152 | ConnectTimeout time.Duration 153 | // RequestTimeout is a default requests timeout. Can be overridden 154 | // on per-operation basis using context.Context. 155 | RequestTimeout time.Duration 156 | // ReadTimeout used to set up underlying connection ReadDeadline. 157 | ReadTimeout time.Duration 158 | // WriteTimeout used to set up underlying connection WriteDeadline. 159 | WriteTimeout time.Duration 160 | // ReconnectDelay is a pause between reconnection attempts. 161 | // If specified, then when tarantool is not reachable or disconnected, 162 | // new connect attempt is performed after pause. 163 | // By default, no reconnection attempts are performed, 164 | // so once disconnected, connection becomes Closed. 165 | ReconnectDelay time.Duration 166 | // MaxReconnects is a maximum reconnect attempts. 167 | // After MaxReconnects attempts Connection becomes closed. 168 | MaxReconnects uint64 169 | 170 | // SkipSchema disables schema loading. Without disabling schema loading, there 171 | // is no way to create Connection for currently not accessible tarantool. 172 | SkipSchema bool 173 | 174 | // RateLimit limits number of 'in-fly' request, ie already put into 175 | // requests queue, but not yet answered by server or timed out. 176 | // It is disabled by default. 177 | // See RLimitAction for possible actions when RateLimit.reached. 178 | RateLimit uint32 179 | // RLimitAction tells what to do when RateLimit reached: 180 | // RLimitDrop - immediately abort request, 181 | // RLimitWait - waitContext during timeout period for some request to be answered. 182 | // If no request answered during timeout period, this request 183 | // is aborted. 184 | // If no timeout period is set, it will waitContext forever. 185 | // It is required if RateLimit is specified. 186 | RLimitAction uint32 187 | // Concurrency is amount of separate mutexes for request 188 | // queues and buffers inside of connection. 189 | // It is rounded upto the nearest power of 2. 190 | // By default, it is runtime.GOMAXPROCS(-1) * 4 191 | Concurrency uint32 192 | 193 | // Notify is a channel which receives notifications about Connection status changes. 194 | Notify chan<- ConnEvent 195 | // Handle is user specified value, that could be retrieved with Handle() method. 196 | Handle interface{} 197 | // Logger is user specified logger used for log messages. 198 | Logger Logger 199 | 200 | // DisableArrayEncodedStructs allows disabling usage of UseArrayEncodedStructs option 201 | // of msgpack Encoder. 202 | DisableArrayEncodedStructs bool 203 | 204 | network string 205 | address string 206 | } 207 | 208 | // Connect creates and configures new Connection. 209 | // 210 | // Address could be specified in following ways: 211 | // 212 | // host:port (no way to provide other options over DSN in this case) 213 | // tcp://[[username[:password]@]host[:port][/?option1=value1&optionN=valueN] 214 | // unix://[[username[:password]@]path[?option1=value1&optionN=valueN] 215 | // 216 | // TCP connections: 217 | // - 127.0.0.1:3301 218 | // - tcp://[fe80::1]:3301 219 | // - tcp://user:pass@example.com:3301 220 | // - tcp://user@example.com/?connect_timeout=5s&request_timeout=1s 221 | // Unix socket: 222 | // - unix:///var/run/tarantool/my_instance.sock 223 | // - unix://user:pass@/var/run/tarantool/my_instance.sock?connect_timeout=5s 224 | func Connect(addr string, opts Opts) (conn *Connection, err error) { 225 | opts, err = optsFromAddr(addr, opts) 226 | if err != nil { 227 | return 228 | } 229 | conn = &Connection{ 230 | requestID: 0, 231 | greeting: &Greeting{}, 232 | control: make(chan struct{}), 233 | opts: opts, 234 | dec: getDecoder(&smallBuf{}), 235 | } 236 | maxProc := uint32(runtime.GOMAXPROCS(-1)) 237 | if conn.opts.Concurrency == 0 || conn.opts.Concurrency > maxProc*128 { 238 | conn.opts.Concurrency = maxProc * 4 239 | } 240 | if c := conn.opts.Concurrency; c&(c-1) != 0 { 241 | for i := uint(1); i < 32; i *= 2 { 242 | c |= c >> i 243 | } 244 | conn.opts.Concurrency = c + 1 245 | } 246 | conn.dirtyShard = make(chan uint32, conn.opts.Concurrency*2) 247 | conn.shard = make([]connShard, conn.opts.Concurrency) 248 | for i := range conn.shard { 249 | shard := &conn.shard[i] 250 | for j := range shard.requests { 251 | shard.requests[j].last = &shard.requests[j].first 252 | } 253 | } 254 | 255 | if opts.RateLimit > 0 { 256 | conn.rLimit = make(chan struct{}, opts.RateLimit) 257 | if opts.RLimitAction != RLimitDrop && opts.RLimitAction != RLimitWait { 258 | return nil, errors.New("RLimitAction should be specified to RLimitDone nor RLimitWait") 259 | } 260 | } 261 | 262 | if err = conn.createConnection(false); err != nil { 263 | ter, ok := err.(Error) 264 | if conn.opts.ReconnectDelay <= 0 { 265 | return nil, err 266 | } else if ok && (ter.Code == ErrNoSuchUser || ter.Code == ErrPasswordMismatch) { 267 | // Report auth errors immediately. 268 | return nil, err 269 | } else { 270 | // Without SkipSchema it is useless. 271 | go func(conn *Connection) { 272 | conn.mutex.Lock() 273 | defer conn.mutex.Unlock() 274 | if err := conn.createConnection(true); err != nil { 275 | _ = conn.closeConnection(err, true) 276 | } 277 | }(conn) 278 | err = nil 279 | } 280 | } 281 | 282 | go conn.pingRoutine() 283 | if conn.opts.RequestTimeout != 0 { 284 | go conn.timeouts() 285 | } 286 | 287 | return conn, err 288 | } 289 | 290 | // Greeting sent by Tarantool on connect. 291 | func (conn *Connection) Greeting() *Greeting { 292 | return conn.greeting 293 | } 294 | 295 | // Schema loaded from Tarantool. 296 | func (conn *Connection) Schema() *Schema { 297 | return conn.schema 298 | } 299 | 300 | // NetConn returns underlying net.Conn. 301 | func (conn *Connection) NetConn() net.Conn { 302 | conn.mutex.Lock() 303 | defer conn.mutex.Unlock() 304 | return conn.c 305 | } 306 | 307 | // Exec Request on Tarantool server. Return untyped result and error. 308 | // Use ExecTyped for more performance and convenience. 309 | func (conn *Connection) Exec(req *Request) ([]interface{}, error) { 310 | return conn.newFuture(req, true).Get() 311 | } 312 | 313 | // ExecTyped execs request and decodes it to a typed result. 314 | func (conn *Connection) ExecTyped(req *Request, result interface{}) error { 315 | return conn.newFuture(req, true).GetTyped(result) 316 | } 317 | 318 | // ExecContext execs Request with context.Context. Note, that context 319 | // cancellation/timeout won't result into ongoing request cancellation 320 | // on Tarantool side. 321 | func (conn *Connection) ExecContext(ctx context.Context, req *Request) ([]interface{}, error) { 322 | if _, ok := ctx.Deadline(); !ok && conn.opts.RequestTimeout > 0 { 323 | var cancel func() 324 | ctx, cancel = context.WithTimeout(ctx, conn.opts.RequestTimeout) 325 | defer cancel() 326 | } 327 | return conn.newFuture(req, false).GetContext(ctx) 328 | } 329 | 330 | // ExecTypedContext execs Request with context.Context and decodes it to a typed result. 331 | // Note, that context cancellation/timeout won't result into ongoing request cancellation 332 | // on Tarantool side. 333 | func (conn *Connection) ExecTypedContext(ctx context.Context, req *Request, result interface{}) error { 334 | if _, ok := ctx.Deadline(); !ok && conn.opts.RequestTimeout > 0 { 335 | var cancel func() 336 | ctx, cancel = context.WithTimeout(ctx, conn.opts.RequestTimeout) 337 | defer cancel() 338 | } 339 | return conn.newFuture(req, false).GetTypedContext(ctx, result) 340 | } 341 | 342 | // ExecAsync execs Request but does not wait for a result - it's then possible to get 343 | // a result from the returned Future. 344 | func (conn *Connection) ExecAsync(req *Request) Future { 345 | return conn.newFuture(req, true) 346 | } 347 | 348 | // ExecAsyncContext execs Request but does not wait for a result - it's then possible 349 | // to get a result from the returned FutureContext. Note, that context cancellation/timeout 350 | // won't result into ongoing request cancellation on Tarantool side. 351 | func (conn *Connection) ExecAsyncContext(req *Request) FutureContext { 352 | return conn.newFuture(req, false) 353 | } 354 | 355 | // Close closes Connection. 356 | // After this method called, there is no way to reopen this Connection. 357 | func (conn *Connection) Close() error { 358 | err := ClientError{ErrConnectionClosed, "connection closed by client"} 359 | conn.mutex.Lock() 360 | defer conn.mutex.Unlock() 361 | return conn.closeConnection(err, true) 362 | } 363 | 364 | var epoch = time.Now() 365 | 366 | func (conn *Connection) timeouts() { 367 | timeout := conn.opts.RequestTimeout 368 | t := time.NewTimer(timeout) 369 | for { 370 | var nowEpoch time.Duration 371 | select { 372 | case <-conn.control: 373 | t.Stop() 374 | return 375 | case <-t.C: 376 | } 377 | minNext := time.Since(epoch) + timeout 378 | for i := range conn.shard { 379 | nowEpoch = time.Since(epoch) 380 | shard := &conn.shard[i] 381 | for pos := range shard.requests { 382 | shard.mu.Lock() 383 | pair := &shard.requests[pos] 384 | for pair.first != nil && pair.first.timeout > 0 && pair.first.timeout < nowEpoch { 385 | shard.bufMu.Lock() 386 | fut := pair.first 387 | pair.first = fut.next 388 | if fut.next == nil { 389 | pair.last = &pair.first 390 | } else { 391 | fut.next = nil 392 | } 393 | fut.err = ClientError{ 394 | Code: ErrTimedOut, 395 | Msg: "request timeout", 396 | } 397 | fut.markReady(conn) 398 | shard.bufMu.Unlock() 399 | } 400 | if pair.first != nil && pair.first.timeout > 0 && pair.first.timeout < minNext { 401 | minNext = pair.first.timeout 402 | } 403 | shard.mu.Unlock() 404 | } 405 | } 406 | nowEpoch = time.Since(epoch) 407 | if nowEpoch+time.Microsecond < minNext { 408 | t.Reset(minNext - nowEpoch) 409 | } else { 410 | t.Reset(time.Microsecond) 411 | } 412 | } 413 | } 414 | 415 | func optsFromAddr(addr string, opts Opts) (Opts, error) { 416 | if !strings.HasPrefix(addr, "tcp://") && !strings.HasPrefix(addr, "unix://") { 417 | if host, port, err := net.SplitHostPort(addr); err == nil && host != "" && port != "" { 418 | opts.network = "tcp" 419 | opts.address = addr 420 | return opts, nil 421 | } 422 | return opts, errors.New("malformed connection address") 423 | } 424 | u, err := url.Parse(addr) 425 | if err != nil { 426 | return opts, fmt.Errorf("malformed connection address URL: %w", err) 427 | } 428 | switch u.Scheme { 429 | case "tcp": 430 | opts.network = "tcp" 431 | opts.address = u.Host 432 | case "unix": 433 | opts.network = "unix" 434 | opts.address = u.Path 435 | default: 436 | return opts, errors.New("connection address should have tcp:// or unix:// scheme") 437 | } 438 | if u.User != nil { 439 | opts.User = u.User.Username() 440 | if pass, ok := u.User.Password(); ok { 441 | opts.Password = pass 442 | } 443 | } 444 | connectTimeout := u.Query().Get("connect_timeout") 445 | if connectTimeout != "" { 446 | if v, err := time.ParseDuration(connectTimeout); err != nil { 447 | return opts, errors.New("malformed connect_timeout parameter") 448 | } else { 449 | opts.ConnectTimeout = v 450 | } 451 | } 452 | requestTimeout := u.Query().Get("request_timeout") 453 | if requestTimeout != "" { 454 | if v, err := time.ParseDuration(requestTimeout); err != nil { 455 | return opts, errors.New("malformed request_timeout parameter") 456 | } else { 457 | opts.RequestTimeout = v 458 | } 459 | } 460 | writeTimeout := u.Query().Get("write_timeout") 461 | if writeTimeout != "" { 462 | if v, err := time.ParseDuration(writeTimeout); err != nil { 463 | return opts, errors.New("malformed write_timeout parameter") 464 | } else { 465 | opts.WriteTimeout = v 466 | } 467 | } 468 | readTimeout := u.Query().Get("read_timeout") 469 | if readTimeout != "" { 470 | if v, err := time.ParseDuration(readTimeout); err != nil { 471 | return opts, errors.New("malformed read_timeout parameter") 472 | } else { 473 | opts.ReadTimeout = v 474 | } 475 | } 476 | reconnectDelay := u.Query().Get("reconnect_delay") 477 | if reconnectDelay != "" { 478 | if v, err := time.ParseDuration(reconnectDelay); err != nil { 479 | return opts, errors.New("malformed reconnect_delay parameter") 480 | } else { 481 | opts.ReconnectDelay = v 482 | } 483 | } 484 | maxReconnects := u.Query().Get("max_reconnects") 485 | if maxReconnects != "" { 486 | if v, err := strconv.ParseUint(maxReconnects, 10, 64); err != nil { 487 | return opts, errors.New("malformed max_reconnects parameter") 488 | } else { 489 | opts.MaxReconnects = v 490 | } 491 | } 492 | skipSchema := u.Query().Get("skip_schema") 493 | if skipSchema == "true" { 494 | opts.SkipSchema = true 495 | } 496 | return opts, nil 497 | } 498 | 499 | func (conn *Connection) dial() (err error) { 500 | var connection net.Conn 501 | timeout := DefaultConnectTimeout 502 | if conn.opts.ConnectTimeout != 0 { 503 | timeout = conn.opts.ConnectTimeout 504 | } 505 | connection, err = net.DialTimeout(conn.opts.network, conn.opts.address, timeout) 506 | if err != nil { 507 | return 508 | } 509 | readTimeout := DefaultReadTimeout 510 | if conn.opts.ReadTimeout != 0 { 511 | readTimeout = conn.opts.ReadTimeout 512 | } 513 | writeTimeout := DefaultWriteTimeout 514 | if conn.opts.WriteTimeout != 0 { 515 | writeTimeout = conn.opts.WriteTimeout 516 | } 517 | dc := &deadlineIO{rto: readTimeout, wto: writeTimeout, c: connection} 518 | r := bufio.NewReaderSize(dc, 128*1024) 519 | w := bufio.NewWriterSize(dc, 128*1024) 520 | greeting := make([]byte, 128) 521 | _, err = io.ReadFull(r, greeting) 522 | if err != nil { 523 | _ = connection.Close() 524 | return 525 | } 526 | conn.greeting.Version = bytes.NewBuffer(greeting[:64]).String() 527 | conn.greeting.auth = bytes.NewBuffer(greeting[64:108]).String() 528 | 529 | // Auth. 530 | if conn.opts.User != "" { 531 | scr, err := scramble(conn.greeting.auth, conn.opts.Password) 532 | if err != nil { 533 | err = errors.New("auth: scrambling failure " + err.Error()) 534 | _ = connection.Close() 535 | return err 536 | } 537 | if err = conn.writeAuthRequest(w, scr); err != nil { 538 | _ = connection.Close() 539 | return err 540 | } 541 | if err = conn.readAuthResponse(r); err != nil { 542 | _ = connection.Close() 543 | return err 544 | } 545 | } 546 | 547 | // Only if connected and authenticated. 548 | conn.lockShards() 549 | conn.c = connection 550 | atomic.StoreUint32(&conn.state, connConnected) 551 | conn.unlockShards() 552 | go conn.writer(w, connection) 553 | go conn.reader(r, connection) 554 | 555 | if !conn.opts.SkipSchema { 556 | if err = conn.loadSchema(); err != nil { 557 | _ = connection.Close() 558 | return err 559 | } 560 | } 561 | 562 | return 563 | } 564 | 565 | func (conn *Connection) writeAuthRequest(w *bufio.Writer, scramble []byte) (err error) { 566 | request := &Request{ 567 | requestCode: AuthRequest, 568 | sendFunc: func(conn *Connection) (func(enc *msgpack.Encoder) error, error) { 569 | return func(enc *msgpack.Encoder) error { 570 | return enc.Encode(map[uint32]interface{}{ 571 | KeyUserName: conn.opts.User, 572 | KeyTuple: []interface{}{"chap-sha1", string(scramble)}, 573 | }) 574 | }, nil 575 | }, 576 | } 577 | var packet smallWBuf 578 | err = request.pack(0, &packet, msgpack.NewEncoder(&packet), conn) 579 | if err != nil { 580 | return errors.New("auth: pack error " + err.Error()) 581 | } 582 | if err := write(w, packet); err != nil { 583 | return errors.New("auth: write error " + err.Error()) 584 | } 585 | if err = w.Flush(); err != nil { 586 | return errors.New("auth: flush error " + err.Error()) 587 | } 588 | return 589 | } 590 | 591 | func (conn *Connection) readAuthResponse(r io.Reader) (err error) { 592 | respBytes, err := conn.read(r) 593 | if err != nil { 594 | return errors.New("auth: read error " + err.Error()) 595 | } 596 | resp := response{buf: smallBuf{b: respBytes}} 597 | err = resp.decodeHeader(conn.dec) 598 | if err != nil { 599 | return errors.New("auth: decode response header error " + err.Error()) 600 | } 601 | err = resp.decodeBody() 602 | if err != nil { 603 | switch err.(type) { 604 | case Error: 605 | return err 606 | default: 607 | return errors.New("auth: decode response body error " + err.Error()) 608 | } 609 | } 610 | return 611 | } 612 | 613 | func (conn *Connection) createConnection(reconnect bool) (err error) { 614 | var reconnects uint64 615 | for conn.c == nil && atomic.LoadUint32(&conn.state) == connDisconnected { 616 | now := time.Now() 617 | err = conn.dial() 618 | if err == nil || !reconnect { 619 | if err == nil { 620 | conn.notify(Connected) 621 | } 622 | return 623 | } 624 | if conn.opts.MaxReconnects > 0 && reconnects > conn.opts.MaxReconnects { 625 | if conn.opts.Logger != nil { 626 | conn.opts.Logger.Report(LogLastReconnectFailed, conn, err) 627 | } 628 | err = ClientError{ErrConnectionClosed, "last reconnect failed"} 629 | // mark connection as closed to avoid reopening by another goroutine. 630 | return 631 | } 632 | if conn.opts.Logger != nil { 633 | conn.opts.Logger.Report(LogReconnectFailed, conn, reconnects, err) 634 | } 635 | conn.notify(ReconnectFailed) 636 | reconnects++ 637 | conn.mutex.Unlock() 638 | time.Sleep(time.Until(now.Add(conn.opts.ReconnectDelay))) 639 | conn.mutex.Lock() 640 | } 641 | if atomic.LoadUint32(&conn.state) == connClosed { 642 | err = ClientError{ErrConnectionClosed, "using closed connection"} 643 | } 644 | return 645 | } 646 | 647 | func (conn *Connection) closeConnection(netErr error, forever bool) (err error) { 648 | conn.lockShards() 649 | defer conn.unlockShards() 650 | if forever { 651 | if atomic.LoadUint32(&conn.state) != connClosed { 652 | close(conn.control) 653 | atomic.StoreUint32(&conn.state, connClosed) 654 | conn.notify(Closed) 655 | } 656 | } else { 657 | atomic.StoreUint32(&conn.state, connDisconnected) 658 | conn.notify(Disconnected) 659 | } 660 | if conn.c != nil { 661 | err = conn.c.Close() 662 | conn.c = nil 663 | } 664 | for i := range conn.shard { 665 | conn.shard[i].buf = conn.shard[i].buf[:0] 666 | requests := &conn.shard[i].requests 667 | for pos := range requests { 668 | fut := requests[pos].first 669 | requests[pos].first = nil 670 | requests[pos].last = &requests[pos].first 671 | for fut != nil { 672 | fut.err = netErr 673 | fut.markReady(conn) 674 | fut, fut.next = fut.next, nil 675 | } 676 | } 677 | } 678 | return 679 | } 680 | 681 | func (conn *Connection) reconnect(netErr error, c net.Conn) { 682 | conn.mutex.Lock() 683 | defer conn.mutex.Unlock() 684 | if conn.opts.ReconnectDelay > 0 { 685 | if c == conn.c { 686 | _ = conn.closeConnection(netErr, false) 687 | if err := conn.createConnection(true); err != nil { 688 | _ = conn.closeConnection(err, true) 689 | } 690 | } 691 | } else { 692 | _ = conn.closeConnection(netErr, true) 693 | } 694 | } 695 | 696 | func (conn *Connection) lockShards() { 697 | for i := range conn.shard { 698 | conn.shard[i].mu.Lock() 699 | conn.shard[i].bufMu.Lock() 700 | } 701 | } 702 | 703 | func (conn *Connection) unlockShards() { 704 | for i := range conn.shard { 705 | conn.shard[i].mu.Unlock() 706 | conn.shard[i].bufMu.Unlock() 707 | } 708 | } 709 | 710 | func (conn *Connection) pingRoutine() { 711 | var pingCmd = Ping() 712 | to := conn.opts.ReadTimeout 713 | if to == 0 { 714 | to = 3 * time.Second 715 | } 716 | t := time.NewTicker(to / 3) 717 | defer t.Stop() 718 | for { 719 | select { 720 | case <-conn.control: 721 | return 722 | case <-t.C: 723 | } 724 | _, _ = conn.Exec(pingCmd) 725 | } 726 | } 727 | 728 | func (conn *Connection) notify(kind ConnEventKind) { 729 | if conn.opts.Notify != nil { 730 | select { 731 | case conn.opts.Notify <- ConnEvent{Kind: kind, Conn: conn, When: time.Now()}: 732 | default: 733 | } 734 | } 735 | } 736 | 737 | func (conn *Connection) writer(w *bufio.Writer, c net.Conn) { 738 | var shardNum uint32 739 | var packet smallWBuf 740 | for atomic.LoadUint32(&conn.state) != connClosed { 741 | select { 742 | case shardNum = <-conn.dirtyShard: 743 | default: 744 | runtime.Gosched() 745 | if len(conn.dirtyShard) == 0 { 746 | if err := w.Flush(); err != nil { 747 | conn.reconnect(err, c) 748 | return 749 | } 750 | } 751 | select { 752 | case shardNum = <-conn.dirtyShard: 753 | case <-conn.control: 754 | return 755 | } 756 | } 757 | shard := &conn.shard[shardNum] 758 | shard.bufMu.Lock() 759 | if conn.c != c { 760 | conn.dirtyShard <- shardNum 761 | shard.bufMu.Unlock() 762 | return 763 | } 764 | packet, shard.buf = shard.buf, packet 765 | shard.bufMu.Unlock() 766 | if len(packet) == 0 { 767 | continue 768 | } 769 | if err := write(w, packet); err != nil { 770 | conn.reconnect(err, c) 771 | return 772 | } 773 | packet = packet[0:0] 774 | } 775 | } 776 | 777 | func (conn *Connection) reader(r *bufio.Reader, c net.Conn) { 778 | for atomic.LoadUint32(&conn.state) != connClosed { 779 | respBytes, err := conn.read(r) 780 | if err != nil { 781 | conn.reconnect(err, c) 782 | return 783 | } 784 | resp := &response{buf: smallBuf{b: respBytes}} 785 | err = resp.decodeHeader(conn.dec) 786 | if err != nil { 787 | conn.reconnect(err, c) 788 | return 789 | } 790 | if resp.code == KeyPush { 791 | if fut := conn.peekFuture(resp.requestID); fut != nil { 792 | fut.markPushReady(resp) 793 | } else { 794 | if conn.opts.Logger != nil { 795 | conn.opts.Logger.Report(LogUnexpectedResultID, conn, resp) 796 | } 797 | } 798 | continue 799 | } 800 | if fut := conn.fetchFuture(resp.requestID); fut != nil { 801 | fut.resp = resp 802 | fut.markReady(conn) 803 | } else { 804 | if conn.opts.Logger != nil { 805 | conn.opts.Logger.Report(LogUnexpectedResultID, conn, resp) 806 | } 807 | } 808 | } 809 | } 810 | 811 | func (conn *Connection) newFuture(req *Request, withTimeout bool) (fut *futureImpl) { 812 | fut = &futureImpl{req: req, conn: conn} 813 | if conn.rLimit != nil && conn.opts.RLimitAction == RLimitDrop { 814 | select { 815 | case conn.rLimit <- struct{}{}: 816 | default: 817 | fut.err = ClientError{ErrRateLimited, "request is rate limited on client"} 818 | return 819 | } 820 | } 821 | fut.ready = make(chan struct{}) 822 | fut.requestID = conn.nextRequestID() 823 | shardNum := fut.requestID & (conn.opts.Concurrency - 1) 824 | shard := &conn.shard[shardNum] 825 | shard.mu.Lock() 826 | 827 | switch atomic.LoadUint32(&conn.state) { 828 | case connClosed: 829 | fut.err = ClientError{ErrConnectionClosed, "using closed connection"} 830 | fut.ready = nil 831 | shard.mu.Unlock() 832 | return 833 | case connDisconnected: 834 | fut.err = ClientError{ErrConnectionNotReady, "client connection is not ready"} 835 | fut.ready = nil 836 | shard.mu.Unlock() 837 | return 838 | } 839 | pos := (fut.requestID / conn.opts.Concurrency) & (requestsMap - 1) 840 | pair := &shard.requests[pos] 841 | *pair.last = fut 842 | pair.last = &fut.next 843 | if withTimeout && conn.opts.RequestTimeout > 0 { 844 | fut.timeout = time.Since(epoch) + conn.opts.RequestTimeout 845 | } 846 | shard.mu.Unlock() 847 | if conn.rLimit != nil && conn.opts.RLimitAction == RLimitWait { 848 | select { 849 | case conn.rLimit <- struct{}{}: 850 | default: 851 | runtime.Gosched() 852 | select { 853 | case conn.rLimit <- struct{}{}: 854 | case <-fut.ready: 855 | if fut.err == nil { 856 | panic("fut.ready is closed, but err is nil") 857 | } 858 | } 859 | } 860 | } 861 | conn.putFuture(fut) 862 | return 863 | } 864 | 865 | func (conn *Connection) putFuture(fut *futureImpl) { 866 | shardNum := fut.requestID & (conn.opts.Concurrency - 1) 867 | shard := &conn.shard[shardNum] 868 | shard.bufMu.Lock() 869 | select { 870 | case <-fut.ready: 871 | shard.bufMu.Unlock() 872 | return 873 | default: 874 | } 875 | firstWritten := len(shard.buf) == 0 876 | if cap(shard.buf) == 0 { 877 | shard.buf = make(smallWBuf, 0, 128) 878 | enc := msgpack.NewEncoder(&shard.buf) 879 | enc.UseArrayEncodedStructs(!conn.opts.DisableArrayEncodedStructs) 880 | shard.enc = enc 881 | } 882 | bufLen := len(shard.buf) 883 | if err := fut.req.pack(fut.requestID, &shard.buf, shard.enc, fut.conn); err != nil { 884 | shard.buf = shard.buf[:bufLen] 885 | shard.bufMu.Unlock() 886 | if f := conn.fetchFuture(fut.requestID); f == fut { 887 | fut.markReady(conn) 888 | fut.err = err 889 | } else if f != nil { 890 | /* in theory, it is possible. In practice, you have 891 | * to have race condition that lasts hours */ 892 | panic("unknown future") 893 | } else { 894 | fut.wait() 895 | if fut.err == nil { 896 | panic("future removed from queue without error") 897 | } 898 | if _, ok := fut.err.(ClientError); ok { 899 | // packing error is more important than connection 900 | // error, because it is indication of programmer's 901 | // mistake. 902 | fut.err = err 903 | } 904 | } 905 | return 906 | } 907 | shard.bufMu.Unlock() 908 | if firstWritten { 909 | conn.dirtyShard <- shardNum 910 | } 911 | } 912 | 913 | func (conn *Connection) fetchFuture(reqID uint32) (fut *futureImpl) { 914 | shard := &conn.shard[reqID&(conn.opts.Concurrency-1)] 915 | shard.mu.Lock() 916 | fut = conn.fetchFutureImp(reqID) 917 | shard.mu.Unlock() 918 | return fut 919 | } 920 | 921 | func (conn *Connection) peekFuture(reqID uint32) (fut *futureImpl) { 922 | shard := &conn.shard[reqID&(conn.opts.Concurrency-1)] 923 | shard.mu.Lock() 924 | fut = conn.peekFutureImp(reqID) 925 | shard.mu.Unlock() 926 | return fut 927 | } 928 | 929 | func (conn *Connection) peekFutureImp(reqID uint32) *futureImpl { 930 | shard := &conn.shard[reqID&(conn.opts.Concurrency-1)] 931 | pos := (reqID / conn.opts.Concurrency) & (requestsMap - 1) 932 | pair := &shard.requests[pos] 933 | root := &pair.first 934 | for { 935 | fut := *root 936 | if fut == nil { 937 | return nil 938 | } 939 | if fut.requestID == reqID { 940 | return fut 941 | } 942 | root = &fut.next 943 | } 944 | } 945 | 946 | func (conn *Connection) fetchFutureImp(reqID uint32) *futureImpl { 947 | shard := &conn.shard[reqID&(conn.opts.Concurrency-1)] 948 | pos := (reqID / conn.opts.Concurrency) & (requestsMap - 1) 949 | pair := &shard.requests[pos] 950 | root := &pair.first 951 | for { 952 | fut := *root 953 | if fut == nil { 954 | return nil 955 | } 956 | if fut.requestID == reqID { 957 | *root = fut.next 958 | if fut.next == nil { 959 | pair.last = root 960 | } else { 961 | fut.next = nil 962 | } 963 | return fut 964 | } 965 | root = &fut.next 966 | } 967 | } 968 | 969 | func write(w io.Writer, data []byte) (err error) { 970 | l, err := w.Write(data) 971 | if err != nil { 972 | return 973 | } 974 | if l != len(data) { 975 | return errors.New("wrong length written") 976 | } 977 | return 978 | } 979 | 980 | func (conn *Connection) read(r io.Reader) (response []byte, err error) { 981 | var length int 982 | 983 | if _, err = io.ReadFull(r, conn.lenBuf[:]); err != nil { 984 | return 985 | } 986 | if conn.lenBuf[0] != 0xce { 987 | err = errors.New("wrong response header") 988 | return 989 | } 990 | length = (int(conn.lenBuf[1]) << 24) + 991 | (int(conn.lenBuf[2]) << 16) + 992 | (int(conn.lenBuf[3]) << 8) + 993 | int(conn.lenBuf[4]) 994 | 995 | if length == 0 { 996 | err = errors.New("response should not be 0 length") 997 | return 998 | } 999 | response = make([]byte, length) 1000 | _, err = io.ReadFull(r, response) 1001 | 1002 | return 1003 | } 1004 | 1005 | func (conn *Connection) nextRequestID() (requestID uint32) { 1006 | return atomic.AddUint32(&conn.requestID, 1) 1007 | } 1008 | -------------------------------------------------------------------------------- /connection_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestRedisShard_OptsFromAddr(t *testing.T) { 11 | opts, err := optsFromAddr("127.0.0.1:3301", Opts{}) 12 | require.NoError(t, err) 13 | require.Equal(t, "tcp", opts.network) 14 | require.Equal(t, "127.0.0.1:3301", opts.address) 15 | 16 | opts, err = optsFromAddr("localhost:3301", Opts{}) 17 | require.NoError(t, err) 18 | require.Equal(t, "tcp", opts.network) 19 | require.Equal(t, "localhost:3301", opts.address) 20 | 21 | _, err = optsFromAddr("localhost:", Opts{}) 22 | require.Error(t, err) 23 | 24 | opts, err = optsFromAddr("tcp://localhost:3301", Opts{}) 25 | require.NoError(t, err) 26 | require.Equal(t, "tcp", opts.network) 27 | require.Equal(t, "localhost:3301", opts.address) 28 | 29 | opts, err = optsFromAddr("tcp://user:pass@localhost:3301", Opts{}) 30 | require.NoError(t, err) 31 | require.Equal(t, "tcp", opts.network) 32 | require.Equal(t, "localhost:3301", opts.address) 33 | require.Equal(t, "user", opts.User) 34 | require.Equal(t, "pass", opts.Password) 35 | 36 | opts, err = optsFromAddr("tcp://localhost:3301/rest", Opts{}) 37 | require.NoError(t, err) 38 | require.Equal(t, "tcp", opts.network) 39 | require.Equal(t, "localhost:3301", opts.address) 40 | require.Equal(t, "", opts.Password) 41 | 42 | opts, err = optsFromAddr("unix://user:pass@/var/run/tarantool/my_instance.sock", Opts{}) 43 | require.NoError(t, err) 44 | require.Equal(t, "unix", opts.network) 45 | require.Equal(t, "/var/run/tarantool/my_instance.sock", opts.address) 46 | require.Equal(t, "pass", opts.Password) 47 | require.Equal(t, "user", opts.User) 48 | 49 | opts, err = optsFromAddr("tcp://[fe80::1]:3301", Opts{}) 50 | require.NoError(t, err) 51 | require.Equal(t, "tcp", opts.network) 52 | require.Equal(t, "[fe80::1]:3301", opts.address) 53 | 54 | opts, err = optsFromAddr("redis://127.0.0.1:3301", Opts{}) 55 | require.Error(t, err) 56 | 57 | opts, err = optsFromAddr("tcp://:pass@localhost:3301?connect_timeout=2s&read_timeout=3s&write_timeout=4s&request_timeout=5s&reconnect_delay=6s&max_reconnects=7&skip_schema=true", Opts{}) 58 | require.NoError(t, err) 59 | require.Equal(t, "tcp", opts.network) 60 | require.Equal(t, "localhost:3301", opts.address) 61 | require.Equal(t, "pass", opts.Password) 62 | require.Equal(t, "", opts.User) 63 | require.Equal(t, 2*time.Second, opts.ConnectTimeout) 64 | require.Equal(t, 3*time.Second, opts.ReadTimeout) 65 | require.Equal(t, 4*time.Second, opts.WriteTimeout) 66 | require.Equal(t, 5*time.Second, opts.RequestTimeout) 67 | require.Equal(t, 6*time.Second, opts.ReconnectDelay) 68 | require.Equal(t, uint64(7), opts.MaxReconnects) 69 | require.True(t, opts.SkipSchema) 70 | } 71 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | // Request code possible values. 4 | const ( 5 | SelectRequest = 1 6 | InsertRequest = 2 7 | ReplaceRequest = 3 8 | UpdateRequest = 4 9 | DeleteRequest = 5 10 | AuthRequest = 7 11 | EvalRequest = 8 12 | UpsertRequest = 9 13 | Call17Request = 10 14 | PingRequest = 64 15 | ) 16 | 17 | // Key possible values. 18 | const ( 19 | KeyCode = 0x00 20 | KeySync = 0x01 21 | KeySpaceNo = 0x10 22 | KeyIndexNo = 0x11 23 | KeyLimit = 0x12 24 | KeyOffset = 0x13 25 | KeyIterator = 0x14 26 | KeyKey = 0x20 27 | KeyTuple = 0x21 28 | KeyFunctionName = 0x22 29 | KeyUserName = 0x23 30 | KeyExpression = 0x27 31 | KeyDefTuple = 0x28 32 | KeyData = 0x30 33 | KeyError = 0x31 34 | KeyPush = 0x80 35 | ) 36 | 37 | // Iter op possible values. 38 | const ( 39 | IterEq = uint32(0) // key == x ASC order 40 | IterReq = uint32(1) // key == x DESC order 41 | IterAll = uint32(2) // all tuples 42 | IterLt = uint32(3) // key < x 43 | IterLe = uint32(4) // key <= x 44 | IterGe = uint32(5) // key >= x 45 | IterGt = uint32(6) // key > x 46 | IterBitsAllSet = uint32(7) // all bits from x are set in key 47 | IterBitsAnySet = uint32(8) // at least one x's bit is set 48 | IterBitsAllNotSet = uint32(9) // all bits are not set 49 | ) 50 | 51 | // RLimit possible values. 52 | const ( 53 | RLimitDrop = 1 54 | RLimitWait = 2 55 | ) 56 | 57 | // response related const. 58 | const ( 59 | okCode = uint32(0) 60 | errorCodeBit = 0x8000 61 | ) 62 | -------------------------------------------------------------------------------- /deadline_io.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | type deadlineIO struct { 9 | rto time.Duration 10 | wto time.Duration 11 | c net.Conn 12 | } 13 | 14 | func (d *deadlineIO) Write(b []byte) (n int, err error) { 15 | if d.wto > 0 { 16 | if err = d.c.SetWriteDeadline(time.Now().Add(d.wto)); err != nil { 17 | return 0, err 18 | } 19 | } 20 | n, err = d.c.Write(b) 21 | return 22 | } 23 | 24 | func (d *deadlineIO) Read(b []byte) (n int, err error) { 25 | if d.rto > 0 { 26 | if err = d.c.SetReadDeadline(time.Now().Add(d.rto)); err != nil { 27 | return 0, err 28 | } 29 | } 30 | n, err = d.c.Read(b) 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /decoder_pool.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | 7 | "github.com/vmihailenco/msgpack/v5" 8 | ) 9 | 10 | var decoderPool sync.Pool 11 | 12 | func getDecoder(r io.Reader) *msgpack.Decoder { 13 | v := decoderPool.Get() 14 | if v == nil { 15 | d := msgpack.NewDecoder(r) 16 | d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { 17 | return dec.DecodeUntypedMap() 18 | }) 19 | d.UseLooseInterfaceDecoding(true) 20 | return d 21 | } 22 | d := v.(*msgpack.Decoder) 23 | d.Reset(r) 24 | d.SetMapDecoder(func(dec *msgpack.Decoder) (interface{}, error) { 25 | return dec.DecodeUntypedMap() 26 | }) 27 | d.UseLooseInterfaceDecoding(true) 28 | return d 29 | } 30 | 31 | func putDecoder(d *msgpack.Decoder) { 32 | decoderPool.Put(d) 33 | } 34 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Error is wrapper around error returned by Tarantool. 8 | type Error struct { 9 | Code uint32 10 | Msg string 11 | } 12 | 13 | func (err Error) Error() string { 14 | return fmt.Sprintf("%s (0x%x)", err.Msg, err.Code) 15 | } 16 | 17 | // ClientError is connection produced by this client – i.e. connection failures or timeouts. 18 | type ClientError struct { 19 | Code uint32 20 | Msg string 21 | } 22 | 23 | func (err ClientError) Error() string { 24 | return fmt.Sprintf("%s (0x%x)", err.Msg, err.Code) 25 | } 26 | 27 | // Temporary returns true if next attempt to perform request may succeed. 28 | // Currently, it returns true when: 29 | // - Connection is not connected at the moment, 30 | // - or request is timed out, 31 | // - or request is aborted due to rate limit. 32 | func (err ClientError) Temporary() bool { 33 | switch err.Code { 34 | case ErrConnectionNotReady, ErrTimedOut, ErrRateLimited: 35 | return true 36 | default: 37 | return false 38 | } 39 | } 40 | 41 | // Tarantool client error codes. 42 | const ( 43 | ErrConnectionNotReady = 0x4000 + iota 44 | ErrConnectionClosed = 0x4000 + iota 45 | ErrProtocolError = 0x4000 + iota 46 | ErrTimedOut = 0x4000 + iota 47 | ErrRateLimited = 0x4000 + iota 48 | ) 49 | 50 | // Tarantool server error codes. 51 | const ( 52 | ErrUnknown = 0 // Unknown error 53 | ErrIllegalParams = 1 // Illegal parameters, %s 54 | ErrMemoryIssue = 2 // Failed to allocate %u bytes in %s for %s 55 | ErrTupleFound = 3 // Duplicate key exists in unique index '%s' in space '%s' 56 | ErrTupleNotFound = 4 // Tuple doesn't exist in index '%s' in space '%s' 57 | ErrUnsupported = 5 // %s does not support %s 58 | ErrNonMaster = 6 // Can't modify data on a replication slave. My master is: %s 59 | ErrReadonly = 7 // Can't modify data because this server is in read-only mode. 60 | ErrInjection = 8 // Error injection '%s' 61 | ErrCreateSpace = 9 // Failed to create space '%s': %s 62 | ErrSpaceExists = 10 // Space '%s' already exists 63 | ErrDropSpace = 11 // Can't drop space '%s': %s 64 | ErrAlterSpace = 12 // Can't modify space '%s': %s 65 | ErrIndexType = 13 // Unsupported index type supplied for index '%s' in space '%s' 66 | ErrModifyIndex = 14 // Can't create or modify index '%s' in space '%s': %s 67 | ErrLastDrop = 15 // Can't drop the primary key in a system space, space '%s' 68 | ErrTupleFormatLimit = 16 // Tuple format limit reached: %u 69 | ErrDropPrimaryKey = 17 // Can't drop primary key in space '%s' while secondary keys exist 70 | ErrKeyPartType = 18 // Supplied key type of part %u does not match index part type: expected %s 71 | ErrExactMatch = 19 // Invalid key part count in an exact match (expected %u, got %u) 72 | ErrInvalidMsgpack = 20 // Invalid MsgPack - %s 73 | ErrProcRet = 21 // msgpack.encode: can not encode Lua type '%s' 74 | ErrTupleNotArray = 22 // Tuple/Key must be MsgPack array 75 | ErrFieldType = 23 // Tuple field %u type does not match one required by operation: expected %s 76 | ErrFieldTypeMismatch = 24 // Ambiguous field type in index '%s', key part %u. Requested type is %s but the field has previously been defined as %s 77 | ErrSplice = 25 // SPLICE error on field %u: %s 78 | ErrArgType = 26 // Argument type in operation '%c' on field %u does not match field type: expected a %s 79 | ErrTupleIsTooLong = 27 // Tuple is too long %u 80 | ErrUnknownUpdateOp = 28 // Unknown UPDATE operation 81 | ErrUpdateField = 29 // Field %u UPDATE error: %s 82 | ErrFiberStack = 30 // Can not create a new fiber: recursion limit reached 83 | ErrKeyPartCount = 31 // Invalid key part count (expected [0..%u], got %u) 84 | ErrProcLua = 32 // %s 85 | ErrNoSuchProc = 33 // Procedure '%.*s' is not defined 86 | ErrNoSuchTrigger = 34 // Trigger is not found 87 | ErrNoSuchIndex = 35 // No index #%u is defined in space '%s' 88 | ErrNoSuchSpace = 36 // Space '%s' does not exist 89 | ErrNoSuchField = 37 // Field %d was not found in the tuple 90 | ErrSpaceFieldCount = 38 // Tuple field count %u does not match space '%s' field count %u 91 | ErrIndexFieldCount = 39 // Tuple field count %u is less than required by a defined index (expected %u) 92 | ErrWalIO = 40 // Failed to write to disk 93 | ErrMoreThanOneTuple = 41 // More than one tuple found by GetContext() 94 | ErrAccessDenied = 42 // %s access denied for user '%s' 95 | ErrCreateUser = 43 // Failed to create user '%s': %s 96 | ErrDropUser = 44 // Failed to drop user '%s': %s 97 | ErrNoSuchUser = 45 // User '%s' is not found 98 | ErrUserExists = 46 // User '%s' already exists 99 | ErrPasswordMismatch = 47 // Incorrect password supplied for user '%s' 100 | ErrUnknownRequestType = 48 // Unknown request type %u 101 | ErrUnknownSchemaObject = 49 // Unknown object type '%s' 102 | ErrCreateFunction = 50 // Failed to create function '%s': %s 103 | ErrNoSuchFunction = 51 // Function '%s' does not exist 104 | ErrFunctionExists = 52 // Function '%s' already exists 105 | ErrFunctionAccessDenied = 53 // %s access denied for user '%s' to function '%s' 106 | ErrFunctionMax = 54 // A limit on the total number of functions has been reached: %u 107 | ErrSpaceAccessDenied = 55 // %s access denied for user '%s' to space '%s' 108 | ErrUserMax = 56 // A limit on the total number of users has been reached: %u 109 | ErrNoSuchEngine = 57 // Space engine '%s' does not exist 110 | ErrReloadCfg = 58 // Can't set option '%s' dynamically 111 | ErrCfg = 59 // Incorrect value for option '%s': %s 112 | ErrSophia = 60 // %s 113 | ErrLocalServerIsNotActive = 61 // Local server is not active 114 | ErrUnknownServer = 62 // Server %s is not registered with the cluster 115 | ErrClusterIdMismatch = 63 // Cluster id of the replica %s doesn't match cluster id of the master %s 116 | ErrInvalidUUID = 64 // Invalid UUID: %s 117 | ErrClusterIdIsRo = 65 // Can't reset cluster id: it is already assigned 118 | ErrReserved66 = 66 // Reserved66 119 | ErrServerIdIsReserved = 67 // Can't initialize server id with a reserved value %u 120 | ErrInvalidOrder = 68 // Invalid LSN order for server %u: previous LSN = %llu, new lsn = %llu 121 | ErrMissingRequestField = 69 // Missing mandatory field '%s' in request 122 | ErrIdentifier = 70 // Invalid identifier '%s' (expected letters, digits or an underscore) 123 | ErrDropFunction = 71 // Can't drop function %u: %s 124 | ErrIteratorType = 72 // Unknown iterator type '%s' 125 | ErrReplicaMax = 73 // Replica count limit reached: %u 126 | ErrInvalidXlog = 74 // Failed to read xlog: %lld 127 | ErrInvalidXlogName = 75 // Invalid xlog name: expected %lld got %lld 128 | ErrInvalidXlogOrder = 76 // Invalid xlog order: %lld and %lld 129 | ErrNoConnection = 77 // Connection is not established 130 | ErrTimeout = 78 // Timeout exceeded 131 | ErrActiveTransaction = 79 // Operation is not permitted when there is an active transaction 132 | ErrNoActiveTransaction = 80 // Operation is not permitted when there is no active transaction 133 | ErrCrossEngineTransaction = 81 // A multi-statement transaction can not use multiple storage engines 134 | ErrNoSuchRole = 82 // Role '%s' is not found 135 | ErrRoleExists = 83 // Role '%s' already exists 136 | ErrCreateRole = 84 // Failed to create role '%s': %s 137 | ErrIndexExists = 85 // Index '%s' already exists 138 | ErrTupleRefOverflow = 86 // Tuple reference counter overflow 139 | ErrRoleLoop = 87 // Granting role '%s' to role '%s' would create a loop 140 | ErrGrant = 88 // Incorrect grant arguments: %s 141 | ErrPrivilegeGranted = 89 // User '%s' already has %s access on %s '%s' 142 | ErrRoleGranted = 90 // User '%s' already has role '%s' 143 | ErrPrivilegeNotGranted = 91 // User '%s' does not have %s access on %s '%s' 144 | ErrRoleNotGranted = 92 // User '%s' does not have role '%s' 145 | ErrMissingSnapshot = 93 // Can't find snapshot 146 | ErrCantUpdatePrimaryKey = 94 // Attempt to modify a tuple field which is part of index '%s' in space '%s' 147 | ErrUpdateIntegerOverflow = 95 // Integer overflow when performing '%c' operation on field %u 148 | ErrGuestUserPassword = 96 // Setting password for guest user has no effect 149 | ErrTransactionConflict = 97 // Transaction has been aborted by conflict 150 | ErrUnsupportedRolePrivilege = 98 // Unsupported role privilege '%s' 151 | ErrLoadFunction = 99 // Failed to dynamically load function '%s': %s 152 | ErrFunctionLanguage = 100 // Unsupported language '%s' specified for function '%s' 153 | ErrRtreeRect = 101 // RTree: %s must be an array with %u (point) or %u (rectangle/box) numeric coordinates 154 | ErrProcC = 102 // ??? 155 | ErrUnknownRtreeIndexDistanceType = 103 // Unknown RTREE index distance type %s 156 | ErrProtocol = 104 // %s 157 | ErrUpsertUniqueSecondaryKey = 105 // Space %s has a unique secondary index and does not support UPSERT 158 | ErrWrongIndexRecord = 106 // Wrong record in _index space: got {%s}, expected {%s} 159 | ErrWrongIndexParts = 107 // Wrong index parts (field %u): %s; expected field1 id (number), field1 type (string), ... 160 | ErrWrongIndexOptions = 108 // Wrong index options (field %u): %s 161 | ErrWrongSchemaVersion = 109 // Wrong schema version, current: %d, in request: %u 162 | ErrSlabAllocMax = 110 // Failed to allocate %u bytes for tuple in the slab allocator: tuple is too large. Check 'slab_alloc_maximal' configuration option. 163 | ) 164 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type Tuple struct { 9 | ID uint 10 | Msg string 11 | Name string 12 | } 13 | 14 | func exampleConnect() (*Connection, error) { 15 | conn, err := Connect(server, opts) 16 | if err != nil { 17 | return nil, err 18 | } 19 | _, err = conn.Exec(Replace(spaceNo, []interface{}{uint(1111), "hello", "world"})) 20 | if err != nil { 21 | _ = conn.Close() 22 | return nil, err 23 | } 24 | _, err = conn.Exec(Replace(spaceNo, []interface{}{uint(1112), "hallo", "werld"})) 25 | if err != nil { 26 | _ = conn.Close() 27 | return nil, err 28 | } 29 | return conn, nil 30 | } 31 | 32 | func ExampleConnection_Exec() { 33 | var conn *Connection 34 | conn, err := exampleConnect() 35 | if err != nil { 36 | fmt.Printf("error in prepare is %v", err) 37 | return 38 | } 39 | defer func() { _ = conn.Close() }() 40 | result, err := conn.Exec(Select(512, 0, 0, 100, IterEq, []interface{}{uint(1111)})) 41 | if err != nil { 42 | fmt.Printf("error in select is %v", err) 43 | return 44 | } 45 | fmt.Printf("result is %#v\n", result) 46 | result, err = conn.Exec(Select("test", "primary", 0, 100, IterEq, IntKey{1111})) 47 | if err != nil { 48 | fmt.Printf("error in select is %v", err) 49 | return 50 | } 51 | fmt.Printf("result is %#v\n", result) 52 | // Output: 53 | // result is []interface {}{[]interface {}{0x457, "hello", "world"}} 54 | // result is []interface {}{[]interface {}{0x457, "hello", "world"}} 55 | } 56 | 57 | func ExampleConnection_ExecTyped() { 58 | var conn *Connection 59 | conn, err := exampleConnect() 60 | if err != nil { 61 | fmt.Printf("error in prepare is %v", err) 62 | return 63 | } 64 | defer func() { _ = conn.Close() }() 65 | var res []Tuple 66 | err = conn.ExecTyped(Select(512, 0, 0, 100, IterEq, IntKey{1111}), &res) 67 | if err != nil { 68 | fmt.Printf("error in select is %v", err) 69 | return 70 | } 71 | fmt.Printf("response is %v\n", res) 72 | err = conn.ExecTyped(Select("test", "primary", 0, 100, IterEq, IntKey{1111}), &res) 73 | if err != nil { 74 | fmt.Printf("error in select is %v", err) 75 | return 76 | } 77 | fmt.Printf("response is %v\n", res) 78 | // Output: 79 | // response is [{1111 hello world}] 80 | // response is [{1111 hello world}] 81 | } 82 | 83 | func Example() { 84 | spaceNo := uint32(512) 85 | indexNo := uint32(0) 86 | 87 | server := "127.0.0.1:3301" 88 | opts := Opts{ 89 | RequestTimeout: 50 * time.Millisecond, 90 | ReconnectDelay: 100 * time.Millisecond, 91 | MaxReconnects: 3, 92 | User: "test", 93 | Password: "test", 94 | } 95 | client, err := Connect(server, opts) 96 | if err != nil { 97 | fmt.Printf("failed to connect: %s", err.Error()) 98 | return 99 | } 100 | 101 | result, err := client.Exec(Ping()) 102 | if err != nil { 103 | fmt.Printf("failed to ping: %s", err.Error()) 104 | return 105 | } 106 | fmt.Println("Ping Result", result) 107 | 108 | // Delete tuple for cleaning. 109 | _, _ = client.Exec(Delete(spaceNo, indexNo, []interface{}{uint(10)})) 110 | _, _ = client.Exec(Delete(spaceNo, indexNo, []interface{}{uint(11)})) 111 | 112 | // Insert new tuple { 10, 1 }. 113 | result, err = client.Exec(Insert(spaceNo, []interface{}{uint(10), "test", "one"})) 114 | fmt.Println("Insert Error", err) 115 | fmt.Println("Insert Result", result) 116 | 117 | // Insert new tuple { 11, 1 }. 118 | result, err = client.Exec(Insert("test", &Tuple{ID: 10, Msg: "test", Name: "one"})) 119 | fmt.Println("Insert Error", err) 120 | fmt.Println("Insert Result", result) 121 | 122 | // Delete tuple with primary key { 10 }. 123 | result, err = client.Exec(Delete(spaceNo, indexNo, []interface{}{uint(10)})) 124 | // or 125 | // result, err = client.Exec(Delete("test", "primary", UintKey{10}})) 126 | fmt.Println("Delete Error", err) 127 | fmt.Println("Delete Result", result) 128 | 129 | // Replace tuple with primary key 13. 130 | result, err = client.Exec(Replace(spaceNo, []interface{}{uint(13), 1})) 131 | fmt.Println("Replace Error", err) 132 | fmt.Println("Replace Result", result) 133 | 134 | // Update tuple with primary key { 13 }, incrementing second field by 3. 135 | result, err = client.Exec(Update("test", "primary", UintKey{13}, []Op{OpAdd(1, 3)})) 136 | // or 137 | // resp, err = client.Exec(Update(spaceNo, indexNo, []interface{}{uint(13)}, []Op{OpAdd(1, 3)})) 138 | fmt.Println("Update Error", err) 139 | fmt.Println("Update Result", result) 140 | 141 | // Select just one tuple with primary key { 15 }. 142 | result, err = client.Exec(Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(15)})) 143 | // or 144 | // resp, err = client.Exec(Select("test", "primary", 0, 1, IterEq, UintKey{15})) 145 | fmt.Println("Select Error", err) 146 | fmt.Println("Select Result", result) 147 | 148 | // Call function 'func_name' with arguments. 149 | result, err = client.Exec(Call("simple_incr", []interface{}{1})) 150 | fmt.Println("Call Error", err) 151 | fmt.Println("Call Result", result) 152 | 153 | // Run raw lua code. 154 | result, err = client.Exec(Eval("return 1 + 2", []interface{}{})) 155 | fmt.Println("Eval Error", err) 156 | fmt.Println("Eval Result", result) 157 | 158 | // Output: 159 | // Ping Result [] 160 | // Insert Error 161 | // Insert Result [[10 test one]] 162 | // Insert Error Duplicate key exists in unique index 'primary' in space 'test' (0x3) 163 | // Insert Result [] 164 | // Delete Error 165 | // Delete Result [[10 test one]] 166 | // Replace Error 167 | // Replace Result [[13 1]] 168 | // Update Error 169 | // Update Result [[13 4]] 170 | // Select Error 171 | // Select Result [[15 val 15 bla]] 172 | // Call Error 173 | // Call Result [2] 174 | // Eval Error 175 | // Eval Result [3] 176 | } 177 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/FZambia/tarantool/examples 2 | 3 | replace github.com/FZambia/tarantool => ../ 4 | 5 | go 1.15 6 | 7 | require github.com/FZambia/tarantool v0.0.0-00010101000000-000000000000 8 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 7 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | github.com/vmihailenco/msgpack/v5 v5.1.0 h1:+od5YbEXxW95SPlW6beocmt8nOtlh83zqat5Ip9Hwdc= 9 | github.com/vmihailenco/msgpack/v5 v5.1.0/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI= 10 | github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= 11 | github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | -------------------------------------------------------------------------------- /examples/readme_example/example.lua: -------------------------------------------------------------------------------- 1 | fiber = require 'fiber' 2 | 3 | box.cfg{listen = 3301} 4 | box.schema.space.create('examples', {id = 999}) 5 | box.space.examples:create_index('primary', {type = 'hash', parts = {1, 'unsigned'}}) 6 | box.schema.user.grant('guest', 'read,write', 'space', 'examples') 7 | 8 | if not fiber.self().storage.console then 9 | require 'console'.start() 10 | os.exit() 11 | end 12 | -------------------------------------------------------------------------------- /examples/readme_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/FZambia/tarantool" 8 | ) 9 | 10 | type Row struct { 11 | ID uint64 12 | Value string 13 | } 14 | 15 | func main() { 16 | opts := tarantool.Opts{ 17 | RequestTimeout: 500 * time.Millisecond, 18 | User: "guest", 19 | } 20 | conn, err := tarantool.Connect("127.0.0.1:3301", opts) 21 | if err != nil { 22 | log.Fatalf("Connection refused: %v", err) 23 | } 24 | defer func() { _ = conn.Close() }() 25 | 26 | _, err = conn.Exec(tarantool.Insert("examples", Row{ID: 999, Value: "hello"})) 27 | if err != nil { 28 | log.Fatalf("Insert failed: %v", err) 29 | } 30 | log.Println("Insert succeeded") 31 | } 32 | -------------------------------------------------------------------------------- /future.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/vmihailenco/msgpack/v5" 8 | ) 9 | 10 | // Future allows to extract response from server as soon as it's ready. 11 | type Future interface { 12 | Get() ([]interface{}, error) 13 | GetTyped(result interface{}) error 14 | } 15 | 16 | // FutureContext allows extracting response from server as soon as it's ready with Context. 17 | type FutureContext interface { 18 | GetContext(ctx context.Context) ([]interface{}, error) 19 | GetTypedContext(ctx context.Context, result interface{}) error 20 | } 21 | 22 | // futureImpl is a handle for asynchronous request. 23 | type futureImpl struct { 24 | requestID uint32 25 | timeout time.Duration 26 | conn *Connection 27 | req *Request 28 | resp *response 29 | err error 30 | ready chan struct{} 31 | next *futureImpl 32 | } 33 | 34 | // Get waits for future to be filled and returns result and error. 35 | // 36 | // Result will contain data deserialized into []interface{}. if you want more 37 | // performance, use GetTyped method. 38 | // 39 | // Note: Response could be equal to nil if ClientError is returned in error. 40 | // 41 | // Error could be Error, if it is error returned by Tarantool, or ClientError, if 42 | // something bad happens in a client process. 43 | func (fut *futureImpl) Get() ([]interface{}, error) { 44 | fut.wait() 45 | if fut.err != nil { 46 | return nil, fut.err 47 | } 48 | fut.err = fut.resp.decodeBody() 49 | if fut.err != nil { 50 | return nil, fut.err 51 | } 52 | return fut.resp.data, nil 53 | } 54 | 55 | // GetTyped waits for future and decodes response into result if no error happens. 56 | // This could be much faster than Get() function. 57 | func (fut *futureImpl) GetTyped(result interface{}) error { 58 | fut.wait() 59 | if fut.err != nil { 60 | return fut.err 61 | } 62 | fut.err = fut.resp.decodeBodyTyped(result) 63 | return fut.err 64 | } 65 | 66 | // GetContext waits for future to be filled and returns result and error. 67 | func (fut *futureImpl) GetContext(ctx context.Context) ([]interface{}, error) { 68 | fut.waitContext(ctx) 69 | if fut.err != nil { 70 | if fut.err == context.DeadlineExceeded || fut.err == context.Canceled { 71 | fut.conn.fetchFuture(fut.requestID) 72 | } 73 | return nil, fut.err 74 | } 75 | fut.err = fut.resp.decodeBody() 76 | if fut.err != nil { 77 | return nil, fut.err 78 | } 79 | return fut.resp.data, nil 80 | } 81 | 82 | // GetTypedContext waits for futureImpl and calls msgpack.Decoder.Decode(result) if 83 | // no error happens. It could be much faster than GetContext() function. 84 | func (fut *futureImpl) GetTypedContext(ctx context.Context, result interface{}) error { 85 | fut.waitContext(ctx) 86 | if fut.err != nil { 87 | if fut.err == context.DeadlineExceeded || fut.err == context.Canceled { 88 | fut.conn.fetchFuture(fut.requestID) 89 | } 90 | return fut.err 91 | } 92 | fut.err = fut.resp.decodeBodyTyped(result) 93 | return fut.err 94 | } 95 | 96 | func (fut *futureImpl) markPushReady(resp *response) { 97 | if fut.req.push == nil && fut.req.pushTyped == nil { 98 | return 99 | } 100 | if fut.req.push != nil { 101 | err := resp.decodeBody() 102 | if err == nil { 103 | fut.req.push(resp.data) 104 | } 105 | return 106 | } 107 | fut.req.pushTyped(func(i interface{}) error { 108 | return resp.decodeBodyTyped(i) 109 | }) 110 | } 111 | 112 | func (fut *futureImpl) markReady(conn *Connection) { 113 | close(fut.ready) 114 | if conn.rLimit != nil { 115 | <-conn.rLimit 116 | } 117 | } 118 | 119 | func (fut *futureImpl) waitContext(ctx context.Context) { 120 | if fut.ready == nil { 121 | return 122 | } 123 | select { 124 | case <-fut.ready: 125 | case <-ctx.Done(): 126 | fut.err = ctx.Err() 127 | } 128 | } 129 | 130 | func (fut *futureImpl) wait() { 131 | if fut.ready == nil { 132 | return 133 | } 134 | <-fut.ready 135 | } 136 | 137 | func fillSearch(enc *msgpack.Encoder, spaceNo, indexNo uint32, key interface{}) error { 138 | _ = enc.EncodeInt(KeySpaceNo) 139 | _ = enc.EncodeInt(int64(spaceNo)) 140 | _ = enc.EncodeInt(KeyIndexNo) 141 | _ = enc.EncodeInt(int64(indexNo)) 142 | _ = enc.EncodeInt(KeyKey) 143 | return enc.Encode(key) 144 | } 145 | 146 | func fillIterator(enc *msgpack.Encoder, offset, limit, iterator uint32) { 147 | _ = enc.EncodeInt(KeyIterator) 148 | _ = enc.EncodeInt(int64(iterator)) 149 | _ = enc.EncodeInt(KeyOffset) 150 | _ = enc.EncodeInt(int64(offset)) 151 | _ = enc.EncodeInt(KeyLimit) 152 | _ = enc.EncodeInt(int64(limit)) 153 | } 154 | 155 | func fillInsert(enc *msgpack.Encoder, spaceNo uint32, tuple interface{}) error { 156 | _ = enc.EncodeInt(KeySpaceNo) 157 | _ = enc.EncodeInt(int64(spaceNo)) 158 | _ = enc.EncodeInt(KeyTuple) 159 | return enc.Encode(tuple) 160 | } 161 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/FZambia/tarantool 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/stretchr/testify v1.6.1 7 | github.com/vmihailenco/msgpack/v5 v5.3.5 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 7 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= 9 | github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= 10 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 11 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "github.com/vmihailenco/msgpack/v5" 5 | ) 6 | 7 | // IntKey is utility type for passing integer key to Select, Update and Delete. 8 | // It serializes to array with single integer element. 9 | type IntKey struct { 10 | I int64 11 | } 12 | 13 | func (k IntKey) EncodeMsgpack(enc *msgpack.Encoder) error { 14 | _ = enc.EncodeArrayLen(1) 15 | _ = enc.EncodeInt(k.I) 16 | return nil 17 | } 18 | 19 | // UintKey is utility type for passing unsigned integer key to Select, Update and Delete. 20 | // It serializes to array with single integer element. 21 | type UintKey struct { 22 | I uint64 23 | } 24 | 25 | func (k UintKey) EncodeMsgpack(enc *msgpack.Encoder) error { 26 | _ = enc.EncodeArrayLen(1) 27 | _ = enc.EncodeUint(k.I) 28 | return nil 29 | } 30 | 31 | // StringKey is utility type for passing string key to Select, Update and Delete. 32 | // It serializes to array with single string element. 33 | type StringKey struct { 34 | S string 35 | } 36 | 37 | func (k StringKey) EncodeMsgpack(enc *msgpack.Encoder) error { 38 | _ = enc.EncodeArrayLen(1) 39 | _ = enc.EncodeString(k.S) 40 | return nil 41 | } 42 | 43 | // IntIntKey is utility type for passing two integer keys to Select, Update and Delete. 44 | // It serializes to array with two integer elements 45 | type IntIntKey struct { 46 | I1, I2 int64 47 | } 48 | 49 | func (k IntIntKey) EncodeMsgpack(enc *msgpack.Encoder) error { 50 | _ = enc.EncodeArrayLen(2) 51 | _ = enc.EncodeInt(k.I1) 52 | _ = enc.EncodeInt(k.I2) 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /operator.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "github.com/vmihailenco/msgpack/v5" 5 | ) 6 | 7 | type Op struct { 8 | encode func(enc *msgpack.Encoder) error 9 | } 10 | 11 | func (op Op) EncodeMsgpack(enc *msgpack.Encoder) error { 12 | return op.encode(enc) 13 | } 14 | 15 | // OpAdd ... 16 | func OpAdd(field uint64, val interface{}) Op { 17 | return Op{encode: func(enc *msgpack.Encoder) error { 18 | _ = enc.EncodeArrayLen(3) 19 | _ = enc.EncodeString("+") 20 | _ = enc.EncodeUint(field) 21 | return enc.Encode(val) 22 | }} 23 | } 24 | 25 | // OpSub ... 26 | func OpSub(field uint64, val interface{}) Op { 27 | return Op{encode: func(enc *msgpack.Encoder) error { 28 | _ = enc.EncodeArrayLen(3) 29 | _ = enc.EncodeString("-") 30 | _ = enc.EncodeUint(field) 31 | return enc.Encode(val) 32 | }} 33 | } 34 | 35 | // OpBitAND ... 36 | func OpBitAND(field, val uint64) Op { 37 | return Op{encode: func(enc *msgpack.Encoder) error { 38 | _ = enc.EncodeArrayLen(3) 39 | _ = enc.EncodeString("&") 40 | _ = enc.EncodeUint(field) 41 | return enc.EncodeUint(val) 42 | }} 43 | } 44 | 45 | // OpBitXOR ... 46 | func OpBitXOR(field, val uint64) Op { 47 | return Op{encode: func(enc *msgpack.Encoder) error { 48 | _ = enc.EncodeArrayLen(3) 49 | _ = enc.EncodeString("^") 50 | _ = enc.EncodeUint(field) 51 | return enc.EncodeUint(val) 52 | }} 53 | } 54 | 55 | // OpBitOR ... 56 | func OpBitOR(field, val uint64) Op { 57 | return Op{encode: func(enc *msgpack.Encoder) error { 58 | _ = enc.EncodeArrayLen(3) 59 | _ = enc.EncodeString("|") 60 | _ = enc.EncodeUint(field) 61 | return enc.EncodeUint(val) 62 | }} 63 | } 64 | 65 | // OpDelete ... 66 | func OpDelete(from, count uint64) Op { 67 | return Op{encode: func(enc *msgpack.Encoder) error { 68 | _ = enc.EncodeArrayLen(3) 69 | _ = enc.EncodeString("#") 70 | _ = enc.EncodeUint(from) 71 | return enc.EncodeUint(count) 72 | }} 73 | } 74 | 75 | // OpInsert ... 76 | func OpInsert(before uint64, val interface{}) Op { 77 | return Op{encode: func(enc *msgpack.Encoder) error { 78 | _ = enc.EncodeArrayLen(3) 79 | _ = enc.EncodeString("!") 80 | _ = enc.EncodeUint(before) 81 | return enc.Encode(val) 82 | }} 83 | } 84 | 85 | // OpAssign ... 86 | func OpAssign(field uint64, val interface{}) Op { 87 | return Op{encode: func(enc *msgpack.Encoder) error { 88 | _ = enc.EncodeArrayLen(3) 89 | _ = enc.EncodeString("=") 90 | _ = enc.EncodeUint(field) 91 | return enc.Encode(val) 92 | }} 93 | } 94 | 95 | // OpSplice ... 96 | func OpSplice(field, offset, position uint64, replace string) Op { 97 | return Op{encode: func(enc *msgpack.Encoder) error { 98 | _ = enc.EncodeArrayLen(4) 99 | _ = enc.EncodeString(":") 100 | _ = enc.EncodeUint(field) 101 | _ = enc.EncodeUint(offset) 102 | _ = enc.EncodeUint(position) 103 | return enc.EncodeString(replace) 104 | }} 105 | } 106 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "github.com/vmihailenco/msgpack/v5" 5 | ) 6 | 7 | // Request to be executed in Tarantool. 8 | type Request struct { 9 | requestCode int32 10 | sendFunc func(conn *Connection) (func(enc *msgpack.Encoder) error, error) 11 | push func([]interface{}) 12 | pushTyped func(func(interface{}) error) 13 | } 14 | 15 | func newRequest(requestCode int32, cb func(conn *Connection) (func(enc *msgpack.Encoder) error, error)) *Request { 16 | return &Request{ 17 | requestCode: requestCode, 18 | sendFunc: cb, 19 | } 20 | } 21 | 22 | // WithPush allows setting Push handler to Request. 23 | func (req *Request) WithPush(pushCB func([]interface{})) *Request { 24 | req.push = pushCB 25 | return req 26 | } 27 | 28 | // WithPushTyped allows setting typed Push handler to Request. 29 | func (req *Request) WithPushTyped(pushTypedCB func(func(interface{}) error)) *Request { 30 | req.pushTyped = pushTypedCB 31 | return req 32 | } 33 | 34 | func (req *Request) pack(requestID uint32, h *smallWBuf, enc *msgpack.Encoder, conn *Connection) error { 35 | hl := len(*h) 36 | *h = append(*h, smallWBuf{ 37 | 0xce, 0, 0, 0, 0, // length 38 | 0x82, // 2 element map 39 | KeyCode, byte(req.requestCode), // request code 40 | KeySync, 0xce, 41 | byte(requestID >> 24), byte(requestID >> 16), 42 | byte(requestID >> 8), byte(requestID), 43 | }...) 44 | 45 | body, err := req.sendFunc(conn) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | if err = body(enc); err != nil { 51 | return err 52 | } 53 | 54 | l := uint32(len(*h) - 5 - hl) 55 | (*h)[hl+1] = byte(l >> 24) 56 | (*h)[hl+2] = byte(l >> 16) 57 | (*h)[hl+3] = byte(l >> 8) 58 | (*h)[hl+4] = byte(l) 59 | 60 | return nil 61 | } 62 | 63 | // Ping sends empty request to Tarantool to check connection. 64 | func Ping() *Request { 65 | return newRequest(PingRequest, func(conn *Connection) (func(enc *msgpack.Encoder) error, error) { 66 | return func(enc *msgpack.Encoder) error { 67 | _ = enc.EncodeMapLen(0) 68 | return nil 69 | }, nil 70 | }) 71 | } 72 | 73 | // Select sends select request to Tarantool. 74 | func Select(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Request { 75 | return newRequest(SelectRequest, func(conn *Connection) (func(enc *msgpack.Encoder) error, error) { 76 | spaceNo, indexNo, err := conn.schema.resolveSpaceIndex(space, index) 77 | if err != nil { 78 | return nil, err 79 | } 80 | return func(enc *msgpack.Encoder) error { 81 | _ = enc.EncodeMapLen(6) 82 | fillIterator(enc, offset, limit, iterator) 83 | return fillSearch(enc, spaceNo, indexNo, key) 84 | }, nil 85 | }) 86 | } 87 | 88 | // Insert sends insert action to Tarantool. 89 | // Tarantool will reject Insert when tuple with same primary key exists. 90 | func Insert(space interface{}, tuple interface{}) *Request { 91 | return newRequest(InsertRequest, func(conn *Connection) (func(enc *msgpack.Encoder) error, error) { 92 | spaceNo, _, err := conn.schema.resolveSpaceIndex(space, nil) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return func(enc *msgpack.Encoder) error { 97 | _ = enc.EncodeMapLen(2) 98 | return fillInsert(enc, spaceNo, tuple) 99 | }, nil 100 | }) 101 | } 102 | 103 | // Replace sends "insert or replace" action to Tarantool. 104 | // If tuple with same primary key exists, it will be replaced. 105 | func Replace(space interface{}, tuple interface{}) *Request { 106 | return newRequest(ReplaceRequest, func(conn *Connection) (func(enc *msgpack.Encoder) error, error) { 107 | spaceNo, _, err := conn.schema.resolveSpaceIndex(space, nil) 108 | if err != nil { 109 | return nil, err 110 | } 111 | return func(enc *msgpack.Encoder) error { 112 | _ = enc.EncodeMapLen(2) 113 | return fillInsert(enc, spaceNo, tuple) 114 | }, nil 115 | }) 116 | } 117 | 118 | // Delete sends deletion action to Tarantool. 119 | // Result will contain array with deleted tuple. 120 | func Delete(space, index interface{}, key interface{}) *Request { 121 | return newRequest(DeleteRequest, func(conn *Connection) (func(enc *msgpack.Encoder) error, error) { 122 | spaceNo, indexNo, err := conn.schema.resolveSpaceIndex(space, index) 123 | if err != nil { 124 | return nil, err 125 | } 126 | return func(enc *msgpack.Encoder) error { 127 | _ = enc.EncodeMapLen(3) 128 | return fillSearch(enc, spaceNo, indexNo, key) 129 | }, nil 130 | }) 131 | } 132 | 133 | // Update sends deletion of a tuple by key. 134 | // Result will contain array with updated tuple. 135 | func Update(space, index interface{}, key interface{}, ops []Op) *Request { 136 | return newRequest(UpdateRequest, func(conn *Connection) (func(enc *msgpack.Encoder) error, error) { 137 | spaceNo, indexNo, err := conn.schema.resolveSpaceIndex(space, index) 138 | if err != nil { 139 | return nil, err 140 | } 141 | return func(enc *msgpack.Encoder) error { 142 | _ = enc.EncodeMapLen(4) 143 | if err := fillSearch(enc, spaceNo, indexNo, key); err != nil { 144 | return err 145 | } 146 | _ = enc.EncodeInt(KeyTuple) 147 | return enc.Encode(ops) 148 | }, nil 149 | }) 150 | } 151 | 152 | // Upsert sends "update or insert" action to Tarantool. 153 | // Result will not contain any tuple. 154 | func Upsert(space interface{}, tuple interface{}, ops []Op) *Request { 155 | return newRequest(UpsertRequest, func(conn *Connection) (func(enc *msgpack.Encoder) error, error) { 156 | spaceNo, _, err := conn.schema.resolveSpaceIndex(space, nil) 157 | if err != nil { 158 | return nil, err 159 | } 160 | return func(enc *msgpack.Encoder) error { 161 | _ = enc.EncodeMapLen(3) 162 | _ = enc.EncodeInt(KeySpaceNo) 163 | _ = enc.EncodeInt(int64(spaceNo)) 164 | _ = enc.EncodeInt(KeyTuple) 165 | if err := enc.Encode(tuple); err != nil { 166 | return err 167 | } 168 | _ = enc.EncodeInt(KeyDefTuple) 169 | return enc.Encode(ops) 170 | }, nil 171 | }) 172 | } 173 | 174 | // Call sends a call to registered Tarantool function. 175 | // It uses request code for Tarantool 1.7, so future's result will not be converted 176 | // (though, keep in mind, result is always array). 177 | func Call(functionName string, args interface{}) *Request { 178 | return newRequest(Call17Request, func(conn *Connection) (func(enc *msgpack.Encoder) error, error) { 179 | return func(enc *msgpack.Encoder) error { 180 | _ = enc.EncodeMapLen(2) 181 | _ = enc.EncodeInt(KeyFunctionName) 182 | _ = enc.EncodeString(functionName) 183 | _ = enc.EncodeInt(KeyTuple) 184 | return enc.Encode(args) 185 | }, nil 186 | }) 187 | } 188 | 189 | // Eval sends a lua expression for evaluation. 190 | func Eval(expr string, args interface{}) *Request { 191 | return newRequest(EvalRequest, func(conn *Connection) (func(enc *msgpack.Encoder) error, error) { 192 | return func(enc *msgpack.Encoder) error { 193 | _ = enc.EncodeMapLen(2) 194 | _ = enc.EncodeInt(KeyExpression) 195 | _ = enc.EncodeString(expr) 196 | _ = enc.EncodeInt(KeyTuple) 197 | return enc.Encode(args) 198 | }, nil 199 | }) 200 | } 201 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | ) 8 | 9 | type response struct { 10 | requestID uint32 11 | code uint32 12 | error string 13 | buf smallBuf 14 | 15 | // Deserialized data for untyped requests. 16 | data []interface{} 17 | } 18 | 19 | func (resp *response) smallInt(d *msgpack.Decoder) (i int, err error) { 20 | b, err := resp.buf.ReadByte() 21 | if err != nil { 22 | return 23 | } 24 | if b <= 127 { 25 | return int(b), nil 26 | } 27 | err = resp.buf.UnreadByte() 28 | if err != nil { 29 | return 30 | } 31 | return d.DecodeInt() 32 | } 33 | 34 | func (resp *response) decodeHeader(d *msgpack.Decoder) (err error) { 35 | var l int 36 | d.Reset(&resp.buf) 37 | if l, err = d.DecodeMapLen(); err != nil { 38 | return 39 | } 40 | for ; l > 0; l-- { 41 | var cd int 42 | if cd, err = resp.smallInt(d); err != nil { 43 | return 44 | } 45 | switch cd { 46 | case KeySync: 47 | var rid uint64 48 | if rid, err = d.DecodeUint64(); err != nil { 49 | return 50 | } 51 | resp.requestID = uint32(rid) 52 | case KeyCode: 53 | var respCode uint64 54 | if respCode, err = d.DecodeUint64(); err != nil { 55 | return 56 | } 57 | resp.code = uint32(respCode) 58 | default: 59 | if err = d.Skip(); err != nil { 60 | return 61 | } 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | func (resp *response) decodeBody() (err error) { 68 | if resp.buf.Len() > 2 { 69 | var l int 70 | d := getDecoder(&resp.buf) 71 | defer putDecoder(d) 72 | if l, err = d.DecodeMapLen(); err != nil { 73 | return err 74 | } 75 | for ; l > 0; l-- { 76 | var cd int 77 | if cd, err = resp.smallInt(d); err != nil { 78 | return err 79 | } 80 | switch cd { 81 | case KeyData: 82 | var res interface{} 83 | var ok bool 84 | if res, err = d.DecodeInterface(); err != nil { 85 | return err 86 | } 87 | if resp.data, ok = res.([]interface{}); !ok { 88 | return fmt.Errorf("result is not array: %v", res) 89 | } 90 | case KeyError: 91 | if resp.error, err = d.DecodeString(); err != nil { 92 | return err 93 | } 94 | default: 95 | if err = d.Skip(); err != nil { 96 | return err 97 | } 98 | } 99 | } 100 | if resp.code == KeyPush { 101 | return 102 | } 103 | if resp.code != okCode { 104 | resp.code &^= errorCodeBit 105 | err = Error{resp.code, resp.error} 106 | } 107 | } 108 | return 109 | } 110 | 111 | func (resp *response) decodeBodyTyped(res interface{}) (err error) { 112 | if resp.buf.Len() > 0 { 113 | var l int 114 | d := getDecoder(&resp.buf) 115 | defer putDecoder(d) 116 | if l, err = d.DecodeMapLen(); err != nil { 117 | return err 118 | } 119 | for ; l > 0; l-- { 120 | var cd int 121 | if cd, err = resp.smallInt(d); err != nil { 122 | return err 123 | } 124 | switch cd { 125 | case KeyData: 126 | if err = d.Decode(res); err != nil { 127 | return err 128 | } 129 | case KeyError: 130 | if resp.error, err = d.DecodeString(); err != nil { 131 | return err 132 | } 133 | default: 134 | if err = d.Skip(); err != nil { 135 | return err 136 | } 137 | } 138 | } 139 | if resp.code == KeyPush { 140 | return 141 | } 142 | if resp.code != okCode { 143 | resp.code &^= errorCodeBit 144 | err = Error{resp.code, resp.error} 145 | } 146 | } 147 | return 148 | } 149 | -------------------------------------------------------------------------------- /schema.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // Schema contains information about spaces and indexes. 10 | type Schema struct { 11 | // Spaces is map from space names to spaces. 12 | Spaces map[string]*Space 13 | // SpacesByID is map from space numbers to spaces. 14 | SpacesByID map[uint32]*Space 15 | } 16 | 17 | // Space contains information about tarantool space. 18 | type Space struct { 19 | ID uint32 20 | Name string 21 | Engine string 22 | Temporary bool 23 | 24 | // Field configuration is not mandatory and not checked by tarantool. 25 | FieldsCount uint32 26 | Fields map[string]*Field 27 | FieldsByID map[uint32]*Field 28 | 29 | // Indexes is map from index names to indexes. 30 | Indexes map[string]*Index 31 | // IndexesByID is map from index numbers to indexes. 32 | IndexesByID map[uint32]*Index 33 | } 34 | 35 | type Field struct { 36 | ID uint32 37 | Name string 38 | Type string 39 | } 40 | 41 | // Index contains information about index. 42 | type Index struct { 43 | ID uint32 44 | Name string 45 | Type string 46 | Unique bool 47 | Fields []*IndexField 48 | } 49 | 50 | type IndexField struct { 51 | ID uint32 52 | Type string 53 | } 54 | 55 | const ( 56 | maxSchemas = 10000 57 | vSpaceSpID = 281 58 | vIndexSpID = 289 59 | ) 60 | 61 | func (conn *Connection) loadSchema() (err error) { 62 | schema := new(Schema) 63 | schema.SpacesByID = make(map[uint32]*Space) 64 | schema.Spaces = make(map[string]*Space) 65 | 66 | // Reload spaces. 67 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 68 | defer cancel() 69 | resp, err := conn.ExecContext(ctx, Select(vSpaceSpID, 0, 0, maxSchemas, IterAll, []interface{}{})) 70 | if err != nil { 71 | return err 72 | } 73 | for _, row := range resp { 74 | row := row.([]interface{}) 75 | space := new(Space) 76 | space.ID = uint32(row[0].(uint64)) 77 | space.Name = row[2].(string) 78 | space.Engine = row[3].(string) 79 | space.FieldsCount = uint32(row[4].(int64)) 80 | if len(row) >= 6 { 81 | switch row5 := row[5].(type) { 82 | case string: 83 | space.Temporary = row5 == "temporary" 84 | case map[interface{}]interface{}: 85 | if temp, ok := row5["temporary"]; ok { 86 | space.Temporary = temp.(bool) 87 | } 88 | default: 89 | panic("unexpected schema format (space flags)") 90 | } 91 | } 92 | space.FieldsByID = make(map[uint32]*Field) 93 | space.Fields = make(map[string]*Field) 94 | space.IndexesByID = make(map[uint32]*Index) 95 | space.Indexes = make(map[string]*Index) 96 | if len(row) >= 7 { 97 | for i, f := range row[6].([]interface{}) { 98 | if f == nil { 99 | continue 100 | } 101 | f := f.(map[interface{}]interface{}) 102 | field := new(Field) 103 | field.ID = uint32(i) 104 | if name, ok := f["name"]; ok && name != nil { 105 | field.Name = name.(string) 106 | } 107 | if type1, ok := f["type"]; ok && type1 != nil { 108 | field.Type = type1.(string) 109 | } 110 | space.FieldsByID[field.ID] = field 111 | if field.Name != "" { 112 | space.Fields[field.Name] = field 113 | } 114 | } 115 | } 116 | 117 | schema.SpacesByID[space.ID] = space 118 | schema.Spaces[space.Name] = space 119 | } 120 | 121 | // Reload indexes. 122 | ctx, cancel2 := context.WithTimeout(context.Background(), time.Second) 123 | defer cancel2() 124 | resp, err = conn.ExecContext(ctx, Select(vIndexSpID, 0, 0, maxSchemas, IterAll, []interface{}{})) 125 | if err != nil { 126 | return err 127 | } 128 | for _, row := range resp { 129 | row := row.([]interface{}) 130 | index := new(Index) 131 | index.ID = uint32(row[1].(int64)) 132 | index.Name = row[2].(string) 133 | index.Type = row[3].(string) 134 | switch row[4].(type) { 135 | case uint64: 136 | index.Unique = row[4].(uint64) > 0 137 | case map[interface{}]interface{}: 138 | opts := row[4].(map[interface{}]interface{}) 139 | var ok bool 140 | if index.Unique, ok = opts["unique"].(bool); !ok { 141 | /* see bug https://github.com/tarantool/tarantool/issues/2060 */ 142 | index.Unique = true 143 | } 144 | default: 145 | panic("unexpected schema format (index flags)") 146 | } 147 | switch fields := row[5].(type) { 148 | case uint64: 149 | cnt := int(fields) 150 | for i := 0; i < cnt; i++ { 151 | field := new(IndexField) 152 | field.ID = uint32(row[6+i*2].(int64)) 153 | field.Type = row[7+i*2].(string) 154 | index.Fields = append(index.Fields, field) 155 | } 156 | case []interface{}: 157 | for _, f := range fields { 158 | field := new(IndexField) 159 | switch f := f.(type) { 160 | case []interface{}: 161 | field.ID = uint32(f[0].(int64)) 162 | field.Type = f[1].(string) 163 | case map[interface{}]interface{}: 164 | field.ID = uint32(f["field"].(int64)) 165 | field.Type = f["type"].(string) 166 | } 167 | index.Fields = append(index.Fields, field) 168 | } 169 | default: 170 | panic("unexpected schema format (index fields)") 171 | } 172 | spaceID := uint32(row[0].(uint64)) 173 | schema.SpacesByID[spaceID].IndexesByID[index.ID] = index 174 | schema.SpacesByID[spaceID].Indexes[index.Name] = index 175 | } 176 | conn.schema = schema 177 | return nil 178 | } 179 | 180 | func (schema *Schema) resolveSpaceIndex(s interface{}, i interface{}) (spaceNo, indexNo uint32, err error) { 181 | var space *Space 182 | var index *Index 183 | var ok bool 184 | 185 | switch s := s.(type) { 186 | case string: 187 | if schema == nil { 188 | err = fmt.Errorf("schema is not loaded") 189 | return 190 | } 191 | if space, ok = schema.Spaces[s]; !ok { 192 | err = fmt.Errorf("there is no space with name %s", s) 193 | return 194 | } 195 | spaceNo = space.ID 196 | case uint: 197 | spaceNo = uint32(s) 198 | case uint64: 199 | spaceNo = uint32(s) 200 | case uint32: 201 | spaceNo = s 202 | case uint16: 203 | spaceNo = uint32(s) 204 | case uint8: 205 | spaceNo = uint32(s) 206 | case int: 207 | spaceNo = uint32(s) 208 | case int64: 209 | spaceNo = uint32(s) 210 | case int32: 211 | spaceNo = uint32(s) 212 | case int16: 213 | spaceNo = uint32(s) 214 | case int8: 215 | spaceNo = uint32(s) 216 | case Space: 217 | spaceNo = s.ID 218 | case *Space: 219 | spaceNo = s.ID 220 | default: 221 | panic("unexpected type of space param") 222 | } 223 | 224 | if i != nil { 225 | switch i := i.(type) { 226 | case string: 227 | if schema == nil { 228 | err = fmt.Errorf("schema is not loaded") 229 | return 230 | } 231 | if space == nil { 232 | if space, ok = schema.SpacesByID[spaceNo]; !ok { 233 | err = fmt.Errorf("there is no space with id %d", spaceNo) 234 | return 235 | } 236 | } 237 | if index, ok = space.Indexes[i]; !ok { 238 | err = fmt.Errorf("space %s has not index with name %s", space.Name, i) 239 | return 240 | } 241 | indexNo = index.ID 242 | case uint: 243 | indexNo = uint32(i) 244 | case uint64: 245 | indexNo = uint32(i) 246 | case uint32: 247 | indexNo = i 248 | case uint16: 249 | indexNo = uint32(i) 250 | case uint8: 251 | indexNo = uint32(i) 252 | case int: 253 | indexNo = uint32(i) 254 | case int64: 255 | indexNo = uint32(i) 256 | case int32: 257 | indexNo = uint32(i) 258 | case int16: 259 | indexNo = uint32(i) 260 | case int8: 261 | indexNo = uint32(i) 262 | case Index: 263 | indexNo = i.ID 264 | case *Index: 265 | indexNo = i.ID 266 | default: 267 | panic("unexpected type of index param") 268 | } 269 | } 270 | return 271 | } 272 | -------------------------------------------------------------------------------- /smallbuf.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | ) 7 | 8 | type smallBuf struct { 9 | b []byte 10 | p int 11 | } 12 | 13 | func (s *smallBuf) Read(d []byte) (l int, err error) { 14 | l = len(s.b) - s.p 15 | if l == 0 && len(d) > 0 { 16 | return 0, io.EOF 17 | } 18 | if l > len(d) { 19 | l = len(d) 20 | } 21 | copy(d, s.b[s.p:]) 22 | s.p += l 23 | return l, nil 24 | } 25 | 26 | func (s *smallBuf) ReadByte() (b byte, err error) { 27 | if s.p == len(s.b) { 28 | return 0, io.EOF 29 | } 30 | b = s.b[s.p] 31 | s.p++ 32 | return b, nil 33 | } 34 | 35 | func (s *smallBuf) UnreadByte() error { 36 | if s.p == 0 { 37 | return errors.New("could not unread") 38 | } 39 | s.p-- 40 | return nil 41 | } 42 | 43 | func (s *smallBuf) Len() int { 44 | return len(s.b) - s.p 45 | } 46 | 47 | func (s *smallBuf) Bytes() []byte { 48 | if len(s.b) > s.p { 49 | return s.b[s.p:] 50 | } 51 | return nil 52 | } 53 | 54 | type smallWBuf []byte 55 | 56 | func (s *smallWBuf) Write(b []byte) (int, error) { 57 | *s = append(*s, b...) 58 | return len(b), nil 59 | } 60 | 61 | func (s *smallWBuf) WriteByte(b byte) error { 62 | *s = append(*s, b) 63 | return nil 64 | } 65 | 66 | func (s *smallWBuf) WriteString(ss string) (int, error) { 67 | *s = append(*s, ss...) 68 | return len(ss), nil 69 | } 70 | -------------------------------------------------------------------------------- /tarantool_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/require" 13 | "github.com/vmihailenco/msgpack/v5" 14 | ) 15 | 16 | type Member struct { 17 | Name string 18 | Nonce string 19 | Val uint 20 | } 21 | 22 | type Tuple2 struct { 23 | Cid uint 24 | Orig string 25 | Members []Member 26 | } 27 | 28 | func (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { 29 | _ = e.EncodeArrayLen(2) 30 | _ = e.EncodeString(m.Name) 31 | _ = e.EncodeUint(uint64(m.Val)) 32 | return nil 33 | } 34 | 35 | func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { 36 | var err error 37 | var l int 38 | if l, err = d.DecodeArrayLen(); err != nil { 39 | return err 40 | } 41 | if l != 2 { 42 | return fmt.Errorf("array len doesn't match: %d", l) 43 | } 44 | if m.Name, err = d.DecodeString(); err != nil { 45 | return err 46 | } 47 | if m.Val, err = d.DecodeUint(); err != nil { 48 | return err 49 | } 50 | return nil 51 | } 52 | 53 | func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { 54 | _ = e.EncodeArrayLen(3) 55 | _ = e.EncodeUint(uint64(c.Cid)) 56 | _ = e.EncodeString(c.Orig) 57 | return e.Encode(c.Members) 58 | } 59 | 60 | func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { 61 | var err error 62 | var l int 63 | if l, err = d.DecodeArrayLen(); err != nil { 64 | return err 65 | } 66 | if l != 3 { 67 | return fmt.Errorf("array len doesn't match: %d", l) 68 | } 69 | if c.Cid, err = d.DecodeUint(); err != nil { 70 | return err 71 | } 72 | if c.Orig, err = d.DecodeString(); err != nil { 73 | return err 74 | } 75 | if l, err = d.DecodeArrayLen(); err != nil { 76 | return err 77 | } 78 | c.Members = make([]Member, l) 79 | for i := 0; i < l; i++ { 80 | if err := d.Decode(&c.Members[i]); err != nil { 81 | return err 82 | } 83 | } 84 | return nil 85 | } 86 | 87 | var server = "127.0.0.1:3301" 88 | var spaceNo = uint32(512) 89 | var spaceName = "test" 90 | var indexNo = uint32(0) 91 | var indexName = "primary" 92 | 93 | var opts = Opts{ 94 | RequestTimeout: 500 * time.Millisecond, 95 | User: "test", 96 | Password: "test", 97 | } 98 | 99 | const N = 500 100 | 101 | func BenchmarkClientSerial(b *testing.B) { 102 | var err error 103 | 104 | conn, err := Connect(server, opts) 105 | if err != nil { 106 | b.Errorf("No connection available") 107 | return 108 | } 109 | defer func() { _ = conn.Close() }() 110 | 111 | _, err = conn.Exec(Replace(spaceNo, []interface{}{uint(1111), "hello", "world"})) 112 | if err != nil { 113 | b.Errorf("No connection available") 114 | } 115 | 116 | b.ResetTimer() 117 | for i := 0; i < b.N; i++ { 118 | _, err = conn.Exec(Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)})) 119 | if err != nil { 120 | b.Errorf("No connection available") 121 | } 122 | } 123 | } 124 | 125 | func BenchmarkClientSerialTyped(b *testing.B) { 126 | var err error 127 | 128 | conn, err := Connect(server, opts) 129 | if err != nil { 130 | b.Errorf("No connection available") 131 | return 132 | } 133 | defer func() { _ = conn.Close() }() 134 | 135 | _, err = conn.Exec(Replace(spaceNo, []interface{}{uint(1111), "hello", "world"})) 136 | if err != nil { 137 | b.Errorf("No connection available") 138 | } 139 | 140 | var r []Tuple 141 | b.ResetTimer() 142 | for i := 0; i < b.N; i++ { 143 | err = conn.ExecTyped(Select(spaceNo, indexNo, 0, 1, IterEq, IntKey{I: 1111}), &r) 144 | if err != nil { 145 | b.Errorf("No connection available") 146 | } 147 | } 148 | } 149 | 150 | func BenchmarkClientFuture(b *testing.B) { 151 | var err error 152 | 153 | conn, err := Connect(server, opts) 154 | if err != nil { 155 | b.Error(err) 156 | return 157 | } 158 | defer func() { _ = conn.Close() }() 159 | 160 | _, err = conn.Exec(Replace(spaceNo, []interface{}{uint(1111), "hello", "world"})) 161 | if err != nil { 162 | b.Error(err) 163 | } 164 | b.ResetTimer() 165 | for i := 0; i < b.N; i += N { 166 | var fs [N]Future 167 | for j := 0; j < N; j++ { 168 | fs[j] = conn.ExecAsync(Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)})) 169 | } 170 | for j := 0; j < N; j++ { 171 | _, err = fs[j].Get() 172 | if err != nil { 173 | b.Error(err) 174 | } 175 | } 176 | 177 | } 178 | } 179 | 180 | func BenchmarkClientFutureTyped(b *testing.B) { 181 | var err error 182 | 183 | conn, err := Connect(server, opts) 184 | if err != nil { 185 | b.Errorf("No connection available") 186 | return 187 | } 188 | defer func() { _ = conn.Close() }() 189 | 190 | _, err = conn.Exec(Replace(spaceNo, []interface{}{uint(1111), "hello", "world"})) 191 | if err != nil { 192 | b.Errorf("No connection available") 193 | } 194 | b.ResetTimer() 195 | for i := 0; i < b.N; i += N { 196 | var fs [N]Future 197 | for j := 0; j < N; j++ { 198 | fs[j] = conn.ExecAsync(Select(spaceNo, indexNo, 0, 1, IterEq, IntKey{I: 1111})) 199 | } 200 | var r []Tuple 201 | for j := 0; j < N; j++ { 202 | err = fs[j].GetTyped(&r) 203 | if err != nil { 204 | b.Error(err) 205 | } 206 | if len(r) != 1 || r[0].ID != 1111 { 207 | b.Errorf("Doesn't match %v", r) 208 | } 209 | } 210 | } 211 | } 212 | 213 | func BenchmarkClientFutureParallel(b *testing.B) { 214 | var err error 215 | 216 | conn, err := Connect(server, opts) 217 | if err != nil { 218 | b.Errorf("No connection available") 219 | return 220 | } 221 | defer func() { _ = conn.Close() }() 222 | 223 | _, err = conn.Exec(Replace(spaceNo, []interface{}{uint(1111), "hello", "world"})) 224 | if err != nil { 225 | b.Errorf("No connection available") 226 | } 227 | b.ResetTimer() 228 | b.RunParallel(func(pb *testing.PB) { 229 | exit := false 230 | for !exit { 231 | var fs [N]Future 232 | var j int 233 | for j = 0; j < N && pb.Next(); j++ { 234 | fs[j] = conn.ExecAsync(Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)})) 235 | } 236 | exit = j < N 237 | for j > 0 { 238 | j-- 239 | _, err := fs[j].Get() 240 | if err != nil { 241 | b.Error(err) 242 | break 243 | } 244 | } 245 | } 246 | }) 247 | } 248 | 249 | func BenchmarkClientFutureParallelTyped(b *testing.B) { 250 | var err error 251 | 252 | conn, err := Connect(server, opts) 253 | if err != nil { 254 | b.Errorf("No connection available") 255 | return 256 | } 257 | defer func() { _ = conn.Close() }() 258 | 259 | _, err = conn.Exec(Replace(spaceNo, []interface{}{uint(1111), "hello", "world"})) 260 | if err != nil { 261 | b.Errorf("No connection available") 262 | } 263 | b.ResetTimer() 264 | b.RunParallel(func(pb *testing.PB) { 265 | exit := false 266 | for !exit { 267 | var fs [N]Future 268 | var j int 269 | for j = 0; j < N && pb.Next(); j++ { 270 | fs[j] = conn.ExecAsync(Select(spaceNo, indexNo, 0, 1, IterEq, IntKey{I: 1111})) 271 | } 272 | exit = j < N 273 | var r []Tuple 274 | for j > 0 { 275 | j-- 276 | err := fs[j].GetTyped(&r) 277 | if err != nil { 278 | b.Error(err) 279 | break 280 | } 281 | if len(r) != 1 || r[0].ID != 1111 { 282 | b.Errorf("Doesn't match %v", r) 283 | break 284 | } 285 | } 286 | } 287 | }) 288 | } 289 | 290 | func BenchmarkClientParallelTimeouts(b *testing.B) { 291 | var err error 292 | 293 | var options = Opts{ 294 | RequestTimeout: time.Millisecond, 295 | User: "test", 296 | Password: "test", 297 | SkipSchema: true, 298 | } 299 | 300 | conn, err := Connect(server, options) 301 | if err != nil { 302 | b.Errorf("No connection available") 303 | return 304 | } 305 | defer func() { _ = conn.Close() }() 306 | 307 | b.SetParallelism(1024) 308 | b.ResetTimer() 309 | b.RunParallel(func(pb *testing.PB) { 310 | for pb.Next() { 311 | _, err := conn.Exec(Call("timeout", [][]interface{}{})) 312 | if err != nil { 313 | if err.(ClientError).Code != ErrTimedOut { 314 | b.Fatal(err.Error()) 315 | } 316 | } else { 317 | b.Fatal("timeout error expected") 318 | } 319 | } 320 | }) 321 | } 322 | 323 | func BenchmarkClientParallel(b *testing.B) { 324 | conn, err := Connect(server, opts) 325 | if err != nil { 326 | b.Errorf("No connection available") 327 | return 328 | } 329 | defer func() { _ = conn.Close() }() 330 | 331 | _, err = conn.Exec(Replace(spaceNo, []interface{}{uint(1111), "hello", "world"})) 332 | if err != nil { 333 | b.Errorf("No connection available") 334 | } 335 | b.ResetTimer() 336 | b.RunParallel(func(pb *testing.PB) { 337 | for pb.Next() { 338 | _, err := conn.Exec(Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)})) 339 | if err != nil { 340 | b.Errorf("No connection available") 341 | break 342 | } 343 | } 344 | }) 345 | } 346 | 347 | func BenchmarkClientParallelMassive(b *testing.B) { 348 | conn, err := Connect(server, opts) 349 | if err != nil { 350 | b.Errorf("No connection available") 351 | return 352 | } 353 | defer func() { _ = conn.Close() }() 354 | 355 | _, err = conn.Exec(Replace(spaceNo, []interface{}{uint(1111), "hello", "world"})) 356 | if err != nil { 357 | b.Errorf("No connection available") 358 | } 359 | 360 | var wg sync.WaitGroup 361 | limit := make(chan struct{}, 128*1024) 362 | for i := 0; i < 512; i++ { 363 | go func() { 364 | var r []Tuple 365 | for { 366 | if _, ok := <-limit; !ok { 367 | break 368 | } 369 | err = conn.ExecTyped(Select(spaceNo, indexNo, 0, 1, IterEq, IntKey{I: 1111}), &r) 370 | wg.Done() 371 | if err != nil { 372 | b.Errorf("No connection available") 373 | } 374 | } 375 | }() 376 | } 377 | for i := 0; i < b.N; i++ { 378 | wg.Add(1) 379 | limit <- struct{}{} 380 | } 381 | wg.Wait() 382 | close(limit) 383 | } 384 | 385 | func BenchmarkClientParallelMassiveUntyped(b *testing.B) { 386 | conn, err := Connect(server, opts) 387 | if err != nil { 388 | b.Errorf("No connection available") 389 | return 390 | } 391 | defer func() { _ = conn.Close() }() 392 | 393 | _, err = conn.Exec(Replace(spaceNo, []interface{}{uint(1111), "hello", "world"})) 394 | if err != nil { 395 | b.Errorf("No connection available") 396 | } 397 | 398 | var wg sync.WaitGroup 399 | limit := make(chan struct{}, 128*1024) 400 | for i := 0; i < 512; i++ { 401 | go func() { 402 | for { 403 | if _, ok := <-limit; !ok { 404 | break 405 | } 406 | _, err = conn.Exec(Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(1111)})) 407 | wg.Done() 408 | if err != nil { 409 | b.Errorf("No connection available") 410 | } 411 | } 412 | }() 413 | } 414 | for i := 0; i < b.N; i++ { 415 | wg.Add(1) 416 | limit <- struct{}{} 417 | } 418 | wg.Wait() 419 | close(limit) 420 | } 421 | 422 | func TestClientBoxInfoCall(t *testing.T) { 423 | var err error 424 | var conn *Connection 425 | 426 | conn, err = Connect(server, opts) 427 | if err != nil { 428 | t.Errorf("Failed to connect: %s", err.Error()) 429 | return 430 | } 431 | if conn == nil { 432 | t.Errorf("conn is nil after Connect") 433 | return 434 | } 435 | defer func() { _ = conn.Close() }() 436 | 437 | result, err := conn.Exec(Call("box.info", []interface{}{"box.schema.SPACE_ID"})) 438 | if err != nil { 439 | t.Errorf("Failed to Call: %s", err.Error()) 440 | return 441 | } 442 | if result == nil { 443 | t.Errorf("Result is nil after Call") 444 | return 445 | } 446 | if len(result) < 1 { 447 | t.Errorf("Result is empty after Call") 448 | } 449 | } 450 | 451 | func TestClient(t *testing.T) { 452 | var err error 453 | var conn *Connection 454 | 455 | conn, err = Connect(server, opts) 456 | if err != nil { 457 | t.Errorf("Failed to connect: %s", err.Error()) 458 | return 459 | } 460 | if conn == nil { 461 | t.Errorf("conn is nil after Connect") 462 | return 463 | } 464 | defer func() { _ = conn.Close() }() 465 | 466 | // Ping. 467 | _, err = conn.Exec(Ping()) 468 | if err != nil { 469 | t.Errorf("Failed to Ping: %s", err.Error()) 470 | return 471 | } 472 | 473 | // Insert. 474 | result, err := conn.Exec(Insert(spaceNo, []interface{}{uint(1), "hello", "world"})) 475 | if err != nil { 476 | t.Errorf("Failed to Insert: %s", err.Error()) 477 | return 478 | } 479 | if result == nil { 480 | t.Errorf("Data is nil after Insert") 481 | return 482 | } 483 | if len(result) != 1 { 484 | t.Errorf("Response Body len != 1") 485 | } 486 | if tpl, ok := result[0].([]interface{}); !ok { 487 | t.Errorf("Unexpected body of Insert") 488 | } else { 489 | if len(tpl) != 3 { 490 | t.Errorf("Unexpected body of Insert (tuple len)") 491 | } 492 | if id, ok := tpl[0].(int64); !ok || id != 1 { 493 | t.Errorf("Unexpected body of Insert (0)") 494 | } 495 | if h, ok := tpl[1].(string); !ok || h != "hello" { 496 | t.Errorf("Unexpected body of Insert (1)") 497 | } 498 | } 499 | _, err = conn.Exec(Insert(spaceNo, &Tuple{ID: 1, Msg: "hello", Name: "world"})) 500 | if tntErr, ok := err.(Error); !ok || tntErr.Code != ErrTupleFound { 501 | t.Errorf("Expected ErrTupleFound but got: %v", err) 502 | return 503 | } 504 | 505 | // Delete. 506 | result, err = conn.Exec(Delete(spaceNo, indexNo, []interface{}{uint(1)})) 507 | if err != nil { 508 | t.Errorf("Failed to Delete: %s", err.Error()) 509 | return 510 | } 511 | if result == nil { 512 | t.Errorf("Data is nil after Delete") 513 | return 514 | } 515 | if len(result) != 1 { 516 | t.Errorf("Response Body len != 1") 517 | } 518 | if tpl, ok := result[0].([]interface{}); !ok { 519 | t.Errorf("Unexpected body of Delete") 520 | } else { 521 | if len(tpl) != 3 { 522 | t.Errorf("Unexpected body of Delete (tuple len)") 523 | } 524 | if id, ok := tpl[0].(int64); !ok || id != 1 { 525 | t.Errorf("Unexpected body of Delete (0)") 526 | } 527 | if h, ok := tpl[1].(string); !ok || h != "hello" { 528 | t.Errorf("Unexpected body of Delete (1)") 529 | } 530 | } 531 | result, err = conn.Exec(Delete(spaceNo, indexNo, []interface{}{uint(101)})) 532 | if err != nil { 533 | t.Errorf("Failed to Delete: %s", err.Error()) 534 | return 535 | } 536 | if result == nil { 537 | t.Errorf("Data is nil after Insert") 538 | return 539 | } 540 | if len(result) != 0 { 541 | t.Errorf("Response Data len != 0") 542 | } 543 | 544 | // Replace. 545 | result, err = conn.Exec(Replace(spaceNo, []interface{}{uint(2), "hello", "world"})) 546 | if err != nil { 547 | t.Errorf("Failed to Replace: %s", err.Error()) 548 | } 549 | if err != nil { 550 | t.Errorf("Failed to Replace: %s", err.Error()) 551 | return 552 | } 553 | if result == nil { 554 | t.Errorf("Result is nil after Replace") 555 | } 556 | result, err = conn.Exec(Replace(spaceNo, []interface{}{uint(2), "hi", "planet"})) 557 | if err != nil { 558 | t.Errorf("Failed to Replace (duplicate): %s", err.Error()) 559 | return 560 | } 561 | if result == nil { 562 | t.Errorf("Data is nil after Insert") 563 | return 564 | } 565 | if len(result) != 1 { 566 | t.Errorf("Response Data len != 1") 567 | } 568 | if tpl, ok := result[0].([]interface{}); !ok { 569 | t.Errorf("Unexpected body of Replace") 570 | } else { 571 | if len(tpl) != 3 { 572 | t.Errorf("Unexpected body of Replace (tuple len)") 573 | } 574 | if id, ok := tpl[0].(int64); !ok || id != 2 { 575 | t.Errorf("Unexpected body of Replace (0)") 576 | } 577 | if h, ok := tpl[1].(string); !ok || h != "hi" { 578 | t.Errorf("Unexpected body of Replace (1)") 579 | } 580 | } 581 | 582 | // Update. 583 | result, err = conn.Exec(Update(spaceNo, indexNo, []interface{}{uint(2)}, []Op{OpAssign(1, "bye"), OpDelete(2, 1)})) 584 | if err != nil { 585 | t.Errorf("Failed to Update: %s", err.Error()) 586 | return 587 | } 588 | if result == nil { 589 | t.Errorf("Data is nil after Insert") 590 | return 591 | } 592 | if len(result) != 1 { 593 | t.Errorf("Response Data len != 1") 594 | } 595 | if tpl, ok := result[0].([]interface{}); !ok { 596 | t.Errorf("Unexpected body of Update") 597 | } else { 598 | if len(tpl) != 2 { 599 | t.Errorf("Unexpected body of Update (tuple len)") 600 | } 601 | if id, ok := tpl[0].(int64); !ok || id != 2 { 602 | t.Errorf("Unexpected body of Update (0)") 603 | } 604 | if h, ok := tpl[1].(string); !ok || h != "bye" { 605 | t.Errorf("Unexpected body of Update (1)") 606 | } 607 | } 608 | 609 | // Upsert. 610 | if strings.Compare(conn.greeting.Version, "Tarantool 1.6.7") >= 0 { 611 | result, err = conn.Exec(Upsert(spaceNo, []interface{}{uint(3), 1}, []Op{OpAdd(1, 1)})) 612 | if err != nil { 613 | t.Errorf("Failed to Upsert (insert): %s", err.Error()) 614 | } 615 | if result == nil { 616 | t.Errorf("Result is nil after Upsert (insert)") 617 | } 618 | result, err = conn.Exec(Upsert(spaceNo, []interface{}{uint(3), 1}, []Op{OpAdd(1, 1)})) 619 | if err != nil { 620 | t.Errorf("Failed to Upsert (update): %s", err.Error()) 621 | } 622 | if result == nil { 623 | t.Errorf("Result is nil after Upsert (update)") 624 | } 625 | } 626 | 627 | // Select. 628 | for i := 10; i < 20; i++ { 629 | _, err = conn.Exec(Replace(spaceNo, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"})) 630 | if err != nil { 631 | t.Errorf("Failed to Replace: %s", err.Error()) 632 | } 633 | } 634 | result, err = conn.Exec(Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)})) 635 | if err != nil { 636 | t.Errorf("Failed to Select: %s", err.Error()) 637 | return 638 | } 639 | if result == nil { 640 | t.Errorf("Result is nil after Select") 641 | return 642 | } 643 | if len(result) != 1 { 644 | t.Errorf("Response Data len != 1") 645 | } 646 | if tpl, ok := result[0].([]interface{}); !ok { 647 | t.Errorf("Unexpected body of Select") 648 | } else { 649 | if id, ok := tpl[0].(int64); !ok || id != 10 { 650 | t.Errorf("Unexpected body of Select (0)") 651 | } 652 | if h, ok := tpl[1].(string); !ok || h != "val 10" { 653 | t.Errorf("Unexpected body of Select (1)") 654 | } 655 | } 656 | 657 | // Select empty. 658 | result, err = conn.Exec(Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)})) 659 | if err != nil { 660 | t.Errorf("Failed to Select: %s", err.Error()) 661 | return 662 | } 663 | if result == nil { 664 | t.Errorf("Result is nil after Select") 665 | return 666 | } 667 | if len(result) != 0 { 668 | t.Errorf("Response Data len != 0") 669 | } 670 | 671 | // Select Typed. 672 | var tpl []Tuple 673 | err = conn.ExecTypedContext(context.Background(), Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}), &tpl) 674 | if err != nil { 675 | t.Errorf("Failed to SelectTyped: %s", err.Error()) 676 | } 677 | if len(tpl) != 1 { 678 | t.Errorf("Result len of SelectTyped != 1") 679 | } else { 680 | if tpl[0].ID != 10 { 681 | t.Errorf("Bad value loaded from SelectTyped") 682 | } 683 | } 684 | 685 | // Select Typed for one tuple. 686 | var tpl1 [1]Tuple 687 | err = conn.ExecTypedContext(context.Background(), Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(10)}), &tpl1) 688 | if err != nil { 689 | t.Errorf("Failed to SelectTyped: %s", err.Error()) 690 | } 691 | if len(tpl) != 1 { 692 | t.Errorf("Result len of SelectTyped != 1") 693 | } else { 694 | if tpl[0].ID != 10 { 695 | t.Errorf("Bad value loaded from SelectTyped") 696 | } 697 | } 698 | 699 | // Select Typed Empty. 700 | var tpl2 []Tuple 701 | err = conn.ExecTypedContext(context.Background(), Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{uint(30)}), &tpl2) 702 | if err != nil { 703 | t.Errorf("Failed to SelectTyped: %s", err.Error()) 704 | } 705 | if len(tpl2) != 0 { 706 | t.Errorf("Result len of SelectTyped != 1") 707 | } 708 | 709 | // Call. 710 | result, err = conn.Exec(Call("simple_incr", []interface{}{1})) 711 | if err != nil { 712 | t.Errorf("Failed to Call: %s", err.Error()) 713 | return 714 | } 715 | if result[0].(int64) != 2 { 716 | t.Errorf("result is not {{1}} : %v", result) 717 | } 718 | 719 | // Eval. 720 | result, err = conn.Exec(Eval("return 5 + 6", []interface{}{})) 721 | if err != nil { 722 | t.Errorf("Failed to Eval: %s", err.Error()) 723 | return 724 | } 725 | if result == nil { 726 | t.Errorf("Result is nil after Eval") 727 | return 728 | } 729 | if len(result) < 1 { 730 | t.Errorf("Response.Data is empty after Eval") 731 | } 732 | val := result[0].(int64) 733 | if val != 11 { 734 | t.Errorf("5 + 6 == 11, but got %v", val) 735 | } 736 | } 737 | 738 | func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (spaceNo, indexNo uint32, err error) { 739 | return schema.resolveSpaceIndex(s, i) 740 | } 741 | 742 | func TestSchema(t *testing.T) { 743 | var err error 744 | var conn *Connection 745 | 746 | conn, err = Connect(server, opts) 747 | if err != nil { 748 | t.Errorf("Failed to connect: %s", err.Error()) 749 | return 750 | } 751 | if conn == nil { 752 | t.Errorf("conn is nil after Connect") 753 | return 754 | } 755 | defer func() { _ = conn.Close() }() 756 | 757 | schema := conn.schema 758 | if schema.SpacesByID == nil { 759 | t.Errorf("schema.SpacesByID is nil") 760 | } 761 | if schema.Spaces == nil { 762 | t.Errorf("schema.Spaces is nil") 763 | } 764 | var space, space2 *Space 765 | var ok bool 766 | if space, ok = schema.SpacesByID[514]; !ok { 767 | t.Errorf("space with id = 514 was not found in schema.SpacesByID") 768 | } 769 | if space2, ok = schema.Spaces["schematest"]; !ok { 770 | t.Errorf("space with name 'schematest' was not found in schema.SpacesByID") 771 | } 772 | if space != space2 { 773 | t.Errorf("space with id = 514 and space with name schematest are different") 774 | } 775 | if space.ID != 514 { 776 | t.Errorf("space 514 has incorrect ID") 777 | } 778 | if space.Name != "schematest" { 779 | t.Errorf("space 514 has incorrect Name") 780 | } 781 | if !space.Temporary { 782 | t.Errorf("space 514 should be temporary") 783 | } 784 | if space.Engine != "memtx" { 785 | t.Errorf("space 514 engine should be memtx") 786 | } 787 | if space.FieldsCount != 7 { 788 | t.Errorf("space 514 has incorrect fields count") 789 | } 790 | 791 | if space.FieldsByID == nil { 792 | t.Errorf("space.FieldsByID is nill") 793 | } 794 | if space.Fields == nil { 795 | t.Errorf("space.Fields is nill") 796 | } 797 | if len(space.FieldsByID) != 6 { 798 | t.Errorf("space.FieldsByID len is incorrect") 799 | } 800 | if len(space.Fields) != 6 { 801 | t.Errorf("space.Fields len is incorrect") 802 | } 803 | 804 | var field1, field2, field5, field1n, field5n *Field 805 | if field1, ok = space.FieldsByID[1]; !ok { 806 | t.Errorf("field id = 1 was not found") 807 | } 808 | if field2, ok = space.FieldsByID[2]; !ok { 809 | t.Errorf("field id = 2 was not found") 810 | } 811 | if field5, ok = space.FieldsByID[5]; !ok { 812 | t.Errorf("field id = 5 was not found") 813 | } 814 | 815 | if field1n, ok = space.Fields["name1"]; !ok { 816 | t.Errorf("field name = name1 was not found") 817 | } 818 | if field5n, ok = space.Fields["name5"]; !ok { 819 | t.Errorf("field name = name5 was not found") 820 | } 821 | if field1 != field1n || field5 != field5n { 822 | t.Errorf("field with id = 1 and field with name 'name1' are different") 823 | } 824 | if field1.Name != "name1" { 825 | t.Errorf("field 1 has incorrect Name") 826 | } 827 | if field1.Type != "unsigned" { 828 | t.Errorf("field 1 has incorrect Type") 829 | } 830 | if field2.Name != "name2" { 831 | t.Errorf("field 2 has incorrect Name") 832 | } 833 | if field2.Type != "string" { 834 | t.Errorf("field 2 has incorrect Type") 835 | } 836 | 837 | if space.IndexesByID == nil { 838 | t.Errorf("space.IndexesByID is nill") 839 | } 840 | if space.Indexes == nil { 841 | t.Errorf("space.Indexes is nill") 842 | } 843 | if len(space.IndexesByID) != 2 { 844 | t.Errorf("space.IndexesByID len is incorrect") 845 | } 846 | if len(space.Indexes) != 2 { 847 | t.Errorf("space.Indexes len is incorrect") 848 | } 849 | 850 | var index0, index3, index0n, index3n *Index 851 | if index0, ok = space.IndexesByID[0]; !ok { 852 | t.Errorf("index id = 0 was not found") 853 | } 854 | if index3, ok = space.IndexesByID[3]; !ok { 855 | t.Errorf("index id = 3 was not found") 856 | } 857 | if index0n, ok = space.Indexes["primary"]; !ok { 858 | t.Errorf("index name = primary was not found") 859 | } 860 | if index3n, ok = space.Indexes["secondary"]; !ok { 861 | t.Errorf("index name = secondary was not found") 862 | } 863 | if index0 != index0n || index3 != index3n { 864 | t.Errorf("index with id = 3 and index with name 'secondary' are different") 865 | } 866 | if index3.ID != 3 { 867 | t.Errorf("index has incorrect ID") 868 | } 869 | if index0.Name != "primary" { 870 | t.Errorf("index has incorrect Name") 871 | } 872 | if index0.Type != "hash" || index3.Type != "tree" { 873 | t.Errorf("index has incorrect Type") 874 | } 875 | if !index0.Unique || index3.Unique { 876 | t.Errorf("index has incorrect Unique") 877 | } 878 | if index3.Fields == nil { 879 | t.Errorf("index.Fields is nil") 880 | } 881 | if len(index3.Fields) != 2 { 882 | t.Errorf("index.Fields len is incorrect") 883 | } 884 | 885 | iField1 := index3.Fields[0] 886 | iField2 := index3.Fields[1] 887 | if iField1 == nil || iField2 == nil { 888 | t.Errorf("index field is nil") 889 | return 890 | } 891 | if iField1.ID != 1 || iField2.ID != 2 { 892 | t.Errorf("index field has incorrect ID") 893 | } 894 | if (iField1.Type != "num" && iField1.Type != "unsigned") || (iField2.Type != "STR" && iField2.Type != "string") { 895 | t.Errorf("index field has incorrect Type '%s'", iField2.Type) 896 | } 897 | 898 | var rSpaceNo, rIndexNo uint32 899 | rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex(514, 3) 900 | if err != nil || rSpaceNo != 514 || rIndexNo != 3 { 901 | t.Errorf("numeric space and index params not resolved as-is") 902 | } 903 | rSpaceNo, _, err = schema.ResolveSpaceIndex(514, nil) 904 | if err != nil || rSpaceNo != 514 { 905 | t.Errorf("numeric space param not resolved as-is") 906 | } 907 | rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex("schematest", "secondary") 908 | if err != nil || rSpaceNo != 514 || rIndexNo != 3 { 909 | t.Errorf("symbolic space and index params not resolved") 910 | } 911 | rSpaceNo, _, err = schema.ResolveSpaceIndex("schematest", nil) 912 | if err != nil || rSpaceNo != 514 { 913 | t.Errorf("symbolic space param not resolved") 914 | } 915 | _, _, err = schema.ResolveSpaceIndex("schematest22", "secondary") 916 | if err == nil { 917 | t.Errorf("resolveSpaceIndex didn't returned error with not existing space name") 918 | } 919 | _, _, err = schema.ResolveSpaceIndex("schematest", "secondary22") 920 | if err == nil { 921 | t.Errorf("resolveSpaceIndex didn't returned error with not existing index name") 922 | } 923 | } 924 | 925 | func TestClientNamed(t *testing.T) { 926 | var err error 927 | var conn *Connection 928 | 929 | conn, err = Connect(server, opts) 930 | if err != nil { 931 | t.Errorf("Failed to connect: %s", err.Error()) 932 | return 933 | } 934 | if conn == nil { 935 | t.Errorf("conn is nil after Connect") 936 | return 937 | } 938 | defer func() { _ = conn.Close() }() 939 | 940 | // Insert. 941 | _, err = conn.Exec(Insert(spaceName, []interface{}{uint(1001), "hello2", "world2"})) 942 | if err != nil { 943 | t.Errorf("Failed to Insert: %s", err.Error()) 944 | } 945 | 946 | // Delete. 947 | result, err := conn.Exec(Delete(spaceName, indexName, []interface{}{uint(1001)})) 948 | if err != nil { 949 | t.Errorf("Failed to Delete: %s", err.Error()) 950 | } 951 | if result == nil { 952 | t.Errorf("Result is nil after Delete") 953 | } 954 | 955 | // Replace. 956 | result, err = conn.Exec(Replace(spaceName, []interface{}{uint(1002), "hello", "world"})) 957 | if err != nil { 958 | t.Errorf("Failed to Replace: %s", err.Error()) 959 | } 960 | if result == nil { 961 | t.Errorf("Result is nil after Replace") 962 | } 963 | 964 | // Update. 965 | result, err = conn.Exec(Update(spaceName, indexName, []interface{}{uint(1002)}, []Op{OpAssign(1, "bye"), OpDelete(2, 1)})) 966 | if err != nil { 967 | t.Errorf("Failed to Update: %s", err.Error()) 968 | } 969 | if result == nil { 970 | t.Errorf("Result is nil after Update") 971 | } 972 | 973 | // Upsert. 974 | if strings.Compare(conn.greeting.Version, "Tarantool 1.6.7") >= 0 { 975 | result, err = conn.Exec(Upsert(spaceName, []interface{}{uint(1003), 1}, []Op{OpAdd(1, 1)})) 976 | if err != nil { 977 | t.Errorf("Failed to Upsert (insert): %s", err.Error()) 978 | } 979 | if result == nil { 980 | t.Errorf("Result is nil after Upsert (insert)") 981 | } 982 | result, err = conn.Exec(Upsert(spaceName, []interface{}{uint(1003), 1}, []Op{OpAdd(1, 1)})) 983 | if err != nil { 984 | t.Errorf("Failed to Upsert (update): %s", err.Error()) 985 | } 986 | if result == nil { 987 | t.Errorf("Result is nil after Upsert (update)") 988 | } 989 | } 990 | 991 | // Select. 992 | for i := 1010; i < 1020; i++ { 993 | _, err = conn.Exec(Replace(spaceName, []interface{}{uint(i), fmt.Sprintf("val %d", i), "bla"})) 994 | if err != nil { 995 | t.Errorf("Failed to Replace: %s", err.Error()) 996 | } 997 | } 998 | result, err = conn.Exec(Select(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)})) 999 | if err != nil { 1000 | t.Errorf("Failed to Select: %s", err.Error()) 1001 | } 1002 | if result == nil { 1003 | t.Errorf("Result is nil after Select") 1004 | } 1005 | 1006 | // Select Typed. 1007 | var tpl []Tuple 1008 | err = conn.ExecTypedContext(context.Background(), Select(spaceName, indexName, 0, 1, IterEq, []interface{}{uint(1010)}), &tpl) 1009 | if err != nil { 1010 | t.Errorf("Failed to SelectTyped: %s", err.Error()) 1011 | } 1012 | if len(tpl) != 1 { 1013 | t.Errorf("Result len of SelectTyped != 1") 1014 | } 1015 | } 1016 | 1017 | func TestComplexStructs(t *testing.T) { 1018 | var err error 1019 | var conn *Connection 1020 | 1021 | conn, err = Connect(server, opts) 1022 | if err != nil { 1023 | t.Errorf("Failed to connect: %s", err.Error()) 1024 | return 1025 | } 1026 | if conn == nil { 1027 | t.Errorf("conn is nil after Connect") 1028 | return 1029 | } 1030 | defer func() { _ = conn.Close() }() 1031 | 1032 | tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} 1033 | _, err = conn.Exec(Replace(spaceNo, &tuple)) 1034 | if err != nil { 1035 | t.Errorf("Failed to insert: %s", err.Error()) 1036 | return 1037 | } 1038 | 1039 | var tuples [1]Tuple2 1040 | err = conn.ExecTypedContext(context.Background(), Select(spaceNo, indexNo, 0, 1, IterEq, []interface{}{777}), &tuples) 1041 | if err != nil { 1042 | t.Errorf("Failed to selectTyped: %s", err.Error()) 1043 | return 1044 | } 1045 | 1046 | if tuple.Cid != tuples[0].Cid || len(tuple.Members) != len(tuples[0].Members) || tuple.Members[1].Name != tuples[0].Members[1].Name { 1047 | t.Errorf("Failed to selectTyped: incorrect data") 1048 | return 1049 | } 1050 | } 1051 | 1052 | func TestExecContext(t *testing.T) { 1053 | var err error 1054 | var connWithTimeout *Connection 1055 | var connNoTimeout *Connection 1056 | var result []interface{} 1057 | 1058 | var ctx context.Context 1059 | var cancel context.CancelFunc 1060 | 1061 | // long request 1062 | req := Eval("require('fiber').sleep(0.5)", []interface{}{}) 1063 | 1064 | // connection w/o request timeout 1065 | connNoTimeout, err = Connect(server, Opts{ 1066 | User: opts.User, 1067 | Password: opts.Password, 1068 | }) 1069 | require.NoError(t, err) 1070 | require.NotNil(t, connNoTimeout) 1071 | 1072 | defer func() { _ = connNoTimeout.Close() }() 1073 | 1074 | // exec without timeout - shouldn't fail 1075 | err = connNoTimeout.ExecTypedContext( 1076 | context.Background(), 1077 | req, 1078 | &result, 1079 | ) 1080 | require.NoError(t, err) 1081 | 1082 | _, err = connNoTimeout.ExecContext( 1083 | context.Background(), 1084 | req, 1085 | ) 1086 | require.NoError(t, err) 1087 | 1088 | // exec with timeout - should fail 1089 | ctx, cancel = context.WithTimeout(context.Background(), 200*time.Millisecond) 1090 | defer cancel() 1091 | 1092 | err = connNoTimeout.ExecTypedContext( 1093 | ctx, 1094 | req, 1095 | &result, 1096 | ) 1097 | 1098 | require.Error(t, err) 1099 | require.True(t, errors.Is(err, context.DeadlineExceeded)) 1100 | 1101 | ctx, cancel = context.WithTimeout(context.Background(), 200*time.Millisecond) 1102 | defer cancel() 1103 | 1104 | result, err = connNoTimeout.ExecContext( 1105 | ctx, 1106 | req, 1107 | ) 1108 | require.Error(t, err) 1109 | require.True(t, errors.Is(err, context.DeadlineExceeded)) 1110 | require.Nil(t, result) 1111 | 1112 | // connection w/ request timeout 1113 | connWithTimeout, err = Connect(server, Opts{ 1114 | User: opts.User, 1115 | Password: opts.Password, 1116 | RequestTimeout: 200 * time.Millisecond, 1117 | }) 1118 | require.NoError(t, err) 1119 | require.NotNil(t, connWithTimeout) 1120 | 1121 | defer func() { _ = connWithTimeout.Close() }() 1122 | 1123 | // exec without timeout - should fail 1124 | err = connWithTimeout.ExecTypedContext( 1125 | context.Background(), 1126 | req, 1127 | &result, 1128 | ) 1129 | require.Error(t, err) 1130 | require.True(t, errors.Is(err, context.DeadlineExceeded)) 1131 | 1132 | _, err = connWithTimeout.ExecContext( 1133 | context.Background(), 1134 | req, 1135 | ) 1136 | require.Error(t, err) 1137 | require.True(t, errors.Is(err, context.DeadlineExceeded)) 1138 | 1139 | // exec with timeout - should fail 1140 | ctx, cancel = context.WithTimeout(context.Background(), 200*time.Millisecond) 1141 | defer cancel() 1142 | 1143 | err = connWithTimeout.ExecTypedContext( 1144 | ctx, 1145 | req, 1146 | &result, 1147 | ) 1148 | 1149 | require.Error(t, err) 1150 | require.True(t, errors.Is(err, context.DeadlineExceeded)) 1151 | 1152 | ctx, cancel = context.WithTimeout(context.Background(), 200*time.Millisecond) 1153 | defer cancel() 1154 | 1155 | _, err = connWithTimeout.ExecContext( 1156 | ctx, 1157 | req, 1158 | ) 1159 | require.Error(t, err) 1160 | require.True(t, errors.Is(err, context.DeadlineExceeded)) 1161 | } 1162 | 1163 | func TestServerError(t *testing.T) { 1164 | var err error 1165 | var conn *Connection 1166 | 1167 | conn, err = Connect(server, opts) 1168 | if err != nil { 1169 | t.Errorf("Failed to connect: %s", err.Error()) 1170 | return 1171 | } 1172 | if conn == nil { 1173 | t.Errorf("conn is nil after Connect") 1174 | return 1175 | } 1176 | defer func() { _ = conn.Close() }() 1177 | 1178 | _, err = conn.Exec(Eval("error('boom')", []interface{}{})) 1179 | require.Error(t, err) 1180 | var e Error 1181 | ok := errors.As(err, &e) 1182 | require.True(t, ok) 1183 | require.EqualValues(t, ErrProcLua, e.Code) 1184 | } 1185 | 1186 | func TestErrorDecode(t *testing.T) { 1187 | var err error 1188 | var conn *Connection 1189 | 1190 | conn, err = Connect(server, opts) 1191 | if err != nil { 1192 | t.Errorf("Failed to connect: %s", err.Error()) 1193 | return 1194 | } 1195 | if conn == nil { 1196 | t.Errorf("conn is nil after Connect") 1197 | return 1198 | } 1199 | defer func() { _ = conn.Close() }() 1200 | 1201 | var s [][]string 1202 | err = conn.ExecTyped(Eval("return 1, 3", []interface{}{}), &s) 1203 | require.Error(t, err) 1204 | } 1205 | 1206 | // Tests regression from https://github.com/FZambia/tarantool/issues/10. 1207 | func TestCloseAfterError(t *testing.T) { 1208 | opts := Opts{ 1209 | RequestTimeout: 500 * time.Millisecond, 1210 | User: "guest", 1211 | } 1212 | conn, err := Connect(server, opts) 1213 | if err != nil { 1214 | t.Fatal(err) 1215 | } 1216 | defer func() { _ = conn.Close() }() 1217 | 1218 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 1219 | defer cancel() 1220 | _, err = conn.ExecContext(ctx, Insert("non-existing-space", struct{ ID string }{"test"})) 1221 | require.Error(t, err) 1222 | err = conn.Close() 1223 | require.NoError(t, err) 1224 | } 1225 | --------------------------------------------------------------------------------