├── .github └── workflows │ └── build.yml ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── anon_slave.go ├── anon_slave_test.go ├── auth.go ├── auth_test.go ├── binpacket.go ├── binpacket_pool.go ├── box.go ├── box_test.go ├── call.go ├── call17.go ├── call17_test.go ├── call_test.go ├── connect_test.go ├── connection.go ├── connector.go ├── const.go ├── countio.go ├── defaults.go ├── delete.go ├── delete_test.go ├── error.go ├── eval.go ├── eval_test.go ├── execute.go ├── fetch_snapshot.go ├── go.mod ├── go.sum ├── insert.go ├── insert_test.go ├── iterator.go ├── join.go ├── lastsnapvclock_test.go ├── lua └── tarantool_lastsnapvclock.lua ├── operator.go ├── pack_data.go ├── packet.go ├── packet_test.go ├── perfcount_test.go ├── ping.go ├── ping_test.go ├── query.go ├── register.go ├── replace.go ├── replace_test.go ├── request_map.go ├── request_pool.go ├── result.go ├── result_test.go ├── select.go ├── select_test.go ├── server.go ├── server_test.go ├── slave.go ├── slave_test.go ├── slaveex_test.go ├── snapio ├── const.go ├── snapread.go ├── snapread_test.go ├── snapwrite.go └── testdata │ ├── v12 │ └── 00000000000000000000.ok.snap │ └── v13 │ └── 00000000000000010005.ok.snap ├── subscribe.go ├── testdata └── init.lua ├── tnt.go ├── tnt_test.go ├── tuple.go ├── typeconv └── int.go ├── update.go ├── update_test.go ├── upsert.go ├── upsert_test.go └── vclock.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | golangci: 5 | name: Go lint & vet 6 | runs-on: ubuntu-latest 7 | # See https://github.com/Dart-Code/Dart-Code/pull/2375 8 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 9 | strategy: 10 | matrix: 11 | go-version: ['1.18'] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: golangci-lint 15 | uses: golangci/golangci-lint-action@v2 16 | with: 17 | args: -D errcheck 18 | - name: go vet 19 | run: | 20 | go vet . 21 | 22 | build: 23 | name: Test Go ${{ matrix.go-version }} / Tarantool ${{ matrix.tarantool-version }} 24 | runs-on: ubuntu-22.04 25 | # See https://github.com/Dart-Code/Dart-Code/pull/2375 26 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | go-version: ['1.18'] 31 | tarantool-version: ['2.11', '2.8', '1.10'] 32 | steps: 33 | - name: Install Go ${{ matrix.go-version }} 34 | uses: actions/setup-go@v2 35 | with: 36 | go-version: ${{ matrix.go-version }} 37 | - name: Install tarantool ${{ matrix.tarantool-version }} 38 | uses: tarantool/setup-tarantool@v1 39 | with: 40 | tarantool-version: ${{ matrix.tarantool-version }} 41 | - name: Checkout code 42 | uses: actions/checkout@v2 43 | - name: Run tests 44 | run: go test -v -timeout 20s 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vim 2 | *.swp 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.15 5 | 6 | before_script: 7 | - go get -t ./... 8 | 9 | script: 10 | - export UNFORMATTED=`gofmt -l .` 11 | - export TNT_LOG_DIR=/home/travis/tntlog 12 | - if [[ ! -z "$UNFORMATTED" ]]; then echo "The following files are not formatted:" && echo "$UNFORMATTED" && exit 1; fi 13 | - go vet 14 | - $HOME/gopath/bin/golint 15 | - go test -v -timeout 20s 16 | - go test -bench=. -timeout 20s 17 | 18 | before_install: 19 | - curl "http://download.tarantool.org/tarantool/$TARANTOOL_VER/gpgkey" | sudo apt-key add - 20 | - export RELEASE=`lsb_release -c -s` 21 | - sudo apt-get -y install apt-transport-https 22 | - sudo rm -f /etc/apt/sources.list.d/*tarantool*.list 23 | - echo "deb http://download.tarantool.org/tarantool/$TARANTOOL_VER/ubuntu/ $RELEASE main" | sudo tee -a /etc/apt/sources.list.d/tarantool.list 24 | - echo "deb-src http://download.tarantool.org/tarantool/$TARANTOOL_VER/ubuntu/ $RELEASE main" | sudo tee -a /etc/apt/sources.list.d/tarantool.list 25 | 26 | install: 27 | - sudo apt-get update 28 | - sudo apt-get -y install tarantool 29 | - go get golang.org/x/lint/golint 30 | - mkdir -p /home/travis/tntlog 31 | 32 | matrix: 33 | include: 34 | - env: TARANTOOL_VER=1.6 35 | - env: TARANTOOL_VER=1.10 36 | - env: TARANTOOL_VER=2.6 37 | - env: TARANTOOL_VER=2.11 38 | 39 | after_failure: 40 | - cat /home/travis/tntlog/* 41 | 42 | notifications: 43 | email: false 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Roman Lomonosov, Victor Luchits, Alexander Egorov, Dmitry Zimnukhov, Shat Shabaev, Roman Kravchik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-tarantool [![GoDoc](https://godoc.org/github.com/viciious/go-tarantool?status.svg)](https://godoc.org/github.com/viciious/go-tarantool) [![Build Status](https://github.com/viciious/go-tarantool/workflows/build/badge.svg?branch=master)](https://github.com/viciious/go-tarantool/actions) 2 | 3 | 4 | 5 | 6 | 7 | The `go-tarantool` package has everything necessary for interfacing with 8 | [Tarantool 1.6+](http://tarantool.org/). 9 | 10 | The advantage of integrating Go with Tarantool, which is an application server 11 | plus a DBMS, is that Go programmers can handle databases with responses that are 12 | faster than other packages according to public benchmarks. 13 | 14 | ## Table of contents 15 | 16 | * [Key features](#key-features) 17 | * [Installation](#installation) 18 | * [Hello World](#hello-world) 19 | * [API reference](#api-reference) 20 | * [Walking through the example](#walking-through-the-example) 21 | * [Alternative way to connect](#alternative-way-to-connect) 22 | * [Help](#help) 23 | 24 | ## Key features 25 | 26 | * Support for both encoding and decoding of Tarantool queries/commands, which 27 | leads us to the following advantages: 28 | - implementing services that mimic a real Tarantool DBMS is relatively easy; 29 | for example, you can code a service which would relay queries and commands 30 | to a real Tarantool instance; the server interface is documented 31 | [here](https://godoc.org/github.com/viciious/go-tarantool#IprotoServer); 32 | - replication support: you can implement a service which would mimic a Tarantool 33 | replication slave and get on-the-fly data updates from the Tarantool master, 34 | an example is provided 35 | [here](https://godoc.org/github.com/viciious/go-tarantool#example-Slave-Attach-Async). 36 | * The interface for sending and packing queries is different from other 37 | go-tarantool implementations, which you may find more aesthetically pleasant 38 | to work with: all queries are represented with different types that follow the 39 | same interface rather than with individual methods in the connector, e.g. 40 | `conn.Exec(&Update{...})` vs `conn.Update({})`. 41 | 42 | ## Installation 43 | 44 | Pre-requisites: 45 | 46 | * Tarantool version 1.6 or 1.7, 47 | * a modern Linux, BSD or Mac OS operating system, 48 | * a current version of `go`, version 1.8 or later (use `go version` to check 49 | the version number). 50 | 51 | If your `go` version is older than 1.8, or if `go` is not installed, 52 | download the latest tarball from [golang.org](https://golang.org/dl/) and say: 53 | 54 | ```bash 55 | sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz 56 | sudo chmod -R a+rwx /usr/local/go 57 | ``` 58 | 59 | Make sure `go` and `go-tarantool` are on your path. For example: 60 | 61 | ``` 62 | export PATH=$PATH:/usr/local/go/bin 63 | export GOPATH="/usr/local/go/go-tarantool" 64 | ``` 65 | 66 | The `go-tarantool` package is in the 67 | [viciious/go-tarantool](https://github.com/viciious/go-tarantool) repository. 68 | To download and install, say: 69 | 70 | ``` 71 | go get github.com/viciious/go-tarantool 72 | ``` 73 | 74 | This should bring source and binary files into subdirectories of `/usr/local/go`, 75 | making it possible to access by adding `github.com/viciious/go-tarantool` in 76 | the `import {...}` section at the start of any Go program. 77 | 78 | ## Hello World 79 | 80 | Here is a very short example Go program which tries to connect to a Tarantool server. 81 | 82 | ```go 83 | package main 84 | 85 | import ( 86 | "context" 87 | "fmt" 88 | "github.com/viciious/go-tarantool" 89 | ) 90 | 91 | func main() { 92 | opts := tarantool.Options{User: "guest"} 93 | conn, err := tarantool.Connect("127.0.0.1:3301", &opts) 94 | if err != nil { 95 | fmt.Printf("Connection refused: %s\n", err.Error()) 96 | return 97 | } 98 | 99 | query := &tarantool.Insert{Space: "examples", Tuple: []interface{}{uint64(99999), "BB"}} 100 | resp := conn.Exec(context.Background(), query) 101 | 102 | if resp.Error != nil { 103 | fmt.Println("Insert failed", resp.Error) 104 | } else { 105 | fmt.Println(fmt.Sprintf("Insert succeeded: %#v", resp.Data)) 106 | } 107 | 108 | conn.Close() 109 | } 110 | ``` 111 | 112 | Cut and paste this example into a file named `example.go`. 113 | 114 | Start a Tarantool server on localhost, and make sure it is listening 115 | on port 3301. Set up a space named `examples` exactly as described in the 116 | [Tarantool manual's Connectors section](https://tarantool.org/doc/1.7/book/connectors/index.html#index-connector-setting). 117 | 118 | Again, make sure PATH and GOPATH point to the right places. Then build and run 119 | `example.go`: 120 | 121 | ``` 122 | go build example.go 123 | ./example 124 | ``` 125 | 126 | You should see: messages saying "Insert failed" or "Insert succeeded". 127 | 128 | If that is what you see, then you have successfully installed `go-tarantool` and 129 | successfully executed a program that connected to a Tarantool server and 130 | manipulated the contents of a Tarantool database. 131 | 132 | ## Walking through the example 133 | 134 | We can now have a closer look at the `example.go` program and make some observations 135 | about what it does. 136 | 137 | **Observation 1:** the line "`github.com/viciious/go-tarantool`" in the 138 | `import(...)` section brings in all Tarantool-related functions and structures. 139 | It is common to bring in [context](https://golang.org/pkg/context/) 140 | and [fmt](https://golang.org/pkg/fmt/) as well. 141 | 142 | **Observation 2:** the line beginning with "`Opts :=`" sets up the options for 143 | `Connect()`. In this example, there is only one thing in the structure, a user 144 | name. The structure can also contain: 145 | 146 | * `ConnectTimeout` (the number of milliseconds the connector will wait a new connection to be established before giving up), 147 | * `QueryTimeout` (the default maximum number of milliseconds to wait before giving up - can be overriden on per-query basis), 148 | * `DefaultSpace` (the name of default Tarantool space) 149 | * `Password` (user's password) 150 | * `UUID` (used for replication) 151 | * `ReplicaSetUUID` (used for replication) 152 | 153 | **Observation 3:** the line containing "`tarantool.Connect`" is one way 154 | to begin a session. There are two parameters: 155 | 156 | * a string with `host:port` format (or "/path/to/tarantool.socket"), and 157 | * the option structure that was set up earlier. 158 | 159 | There is an alternative way to connect, we will describe it later. 160 | 161 | **Observation 4:** the `err` structure will be `nil` if there is no error, 162 | otherwise it will have a description which can be retrieved with `err.Error()`. 163 | 164 | **Observation 5:** the `conn.exec` request, like many requests, is preceded by 165 | "`conn.`" which is the name of the object that was returned by `Connect()`. 166 | In this case, for Insert, there are two parameters: 167 | 168 | * a space name (it could just as easily have been a space number), and 169 | * a tuple. 170 | 171 | All the requests described in the Tarantool manual can be expressed in 172 | a similar way within `connect.Exec()`, with the format "&name-of-request{arguments}". 173 | For example: `&ping{}`. For a long example: 174 | 175 | ```go 176 | data, err := conn.Exec(context.Background(), &Update{ 177 | Space: "tester", 178 | Index: "primary", 179 | Key: 1, 180 | Set: []Operator{ 181 | &OpAdd{ 182 | Field: 2, 183 | Argument: 17, 184 | }, 185 | &OpAssign{ 186 | Field: 1, 187 | Argument: "Hello World", 188 | }, 189 | }, 190 | }) 191 | ``` 192 | 193 | ## API reference 194 | 195 | Read the [Tarantool manual](http://tarantool.org/doc.html) to find descriptions 196 | of terms like "connect", "space", "index", and the requests for creating and 197 | manipulating database objects or Lua functions. 198 | 199 | The source files for the requests library are: 200 | * [connection.go](https://github.com/viciious/go-tarantool/blob/master/connector.go) 201 | for the `Connect()` function plus functions related to connecting, and 202 | * [insert_test.go](https://github.com/viciious/go-tarantool/blob/master/insert_test.go) 203 | for an example of a data-manipulation function used in tests. 204 | 205 | See comments in these files for syntax details: 206 | * [call.go](https://github.com/viciious/go-tarantool/blob/master/call.go) 207 | * [delete.go](https://github.com/viciious/go-tarantool/blob/master/delete.go) 208 | * [eval.go](https://github.com/viciious/go-tarantool/blob/master/eval.go) 209 | * [insert.go](https://github.com/viciious/go-tarantool/blob/master/insert.go) 210 | * [iterator.go](https://github.com/viciious/go-tarantool/blob/master/iterator.go) 211 | * [join.go](https://github.com/viciious/go-tarantool/blob/master/join.go) 212 | * [operator.go](https://github.com/viciious/go-tarantool/blob/master/operator.go) 213 | * [pack.go](https://github.com/viciious/go-tarantool/blob/master/pack.go) 214 | * [update.go](https://github.com/viciious/go-tarantool/blob/master/update.go) 215 | * [upsert.go](https://github.com/viciious/go-tarantool/blob/master/upsert.go) 216 | 217 | The supported requests have parameters and results equivalent to requests in the 218 | Tarantool manual. Browsing through the other *.go programs in the package will 219 | show how the packagers have paid attention to some of the more advanced features 220 | of Tarantool, such as vclock and replication. 221 | 222 | ## Alternative way to connect 223 | 224 | Here we show a variation of `example.go`, where the connect is done a different 225 | way. 226 | 227 | ```go 228 | 229 | package main 230 | 231 | import ( 232 | "context" 233 | "fmt" 234 | "github.com/viciious/go-tarantool" 235 | ) 236 | 237 | func main() { 238 | opts := tarantool.Options{User: "guest"} 239 | tnt := tarantool.New("127.0.0.1:3301", &opts) 240 | conn, err := tnt.Connect() 241 | if err != nil { 242 | fmt.Printf("Connection refused: %s\n", err.Error()) 243 | return 244 | } 245 | 246 | query := &tarantool.Insert{Space: "examples", Tuple: []interface{}{uint64(99999), "BB"}} 247 | resp := conn.Exec(context.Background(), query) 248 | 249 | if resp.Error != nil { 250 | fmt.Println("Insert failed", resp.Error) 251 | } else { 252 | fmt.Println(fmt.Sprintf("Insert succeeded: %#v", resp.Data)) 253 | } 254 | 255 | conn.Close() 256 | } 257 | ``` 258 | 259 | In this variation, `tarantool.New` returns a Connector instance, 260 | which is a goroutine-safe singleton object that can transparently handle 261 | reconnects. 262 | 263 | ## Help 264 | 265 | To contact `go-tarantool` developers on any problems, create an issue at 266 | [viciious/go-tarantool](http://github.com/viciious/go-tarantool/issues). 267 | 268 | The developers of the [Tarantool server](http://github.com/tarantool/tarantool) 269 | will also be happy to provide advice or receive feedback. 270 | -------------------------------------------------------------------------------- /anon_slave.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // AnonSlave connects to Tarantool >= 2.3.1 instance and subscribes for changes as anonymous replica. 8 | // Tarantool instance acting as a master sees AnonSlave like anonymous replica. 9 | // AnonSlave can't be used concurrently, route responses from returned channel instead. 10 | type AnonSlave struct { 11 | Slave 12 | } 13 | 14 | // NewAnonSlave returns new AnonSlave instance. 15 | // URI is parsed by url package and therefore should contains any scheme supported by net.Dial. 16 | func NewAnonSlave(uri string, opts ...Options) (as *AnonSlave, err error) { 17 | s, err := NewSlave(uri, opts...) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | // check tarantool version. Anonymous replica support was added in Tarantool 2.3.1 23 | if s.Version() < version2_3_1 { 24 | return nil, ErrOldVersionAnon 25 | } 26 | 27 | return &AnonSlave{*s}, nil 28 | } 29 | 30 | // JoinWithSnap fetch snapshots from Master instance. 31 | // Snapshot logs is available through the given out channel or returned PacketIterator. 32 | // (In truth, Slave itself is returned in PacketIterator wrapper) 33 | func (s *AnonSlave) JoinWithSnap(out ...chan *Packet) (it PacketIterator, err error) { 34 | if err = s.fetchSnapshot(); err != nil { 35 | return nil, err 36 | } 37 | 38 | // set iterator for the Next method 39 | s.next = s.nextSnap 40 | 41 | if s.isEmptyChan(out...) { 42 | // no chan means synchronous snapshot scanning 43 | return s, nil 44 | } 45 | 46 | defer close(out[0]) 47 | for s.HasNext() { 48 | out[0] <- s.Packet() 49 | } 50 | 51 | return nil, s.Err() 52 | } 53 | 54 | // Join fetches snapshots using Master instance. 55 | func (s *AnonSlave) Join() (err error) { 56 | _, err = s.JoinWithSnap() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | for s.HasNext() { 62 | } 63 | 64 | return s.Err() 65 | } 66 | 67 | // Subscribe for DML requests (insert, update, delete, replace, upsert) since vector clock. 68 | // Variadic lsn is start vector clock. Each lsn is one clock in vector (sequentially). 69 | // One lsn is enough for master-slave replica set. 70 | // Subscribe sends requests asynchronously to out channel specified or use synchronous PacketIterator otherwise. 71 | // For anonymous replica it is not necessary to call Join or JoinWithSnap before Subscribe. 72 | func (s *AnonSlave) Subscribe(lsns ...uint64) (it PacketIterator, err error) { 73 | if len(lsns) == 0 || len(lsns) >= VClockMax { 74 | return nil, ErrVectorClock 75 | } 76 | 77 | if err = s.subscribe(lsns...); err != nil { 78 | return nil, err 79 | } 80 | 81 | // set iterator for the Next method 82 | s.next = s.nextXlog 83 | 84 | // Start sending heartbeat messages to master 85 | go s.heartbeat() 86 | 87 | return s, nil 88 | } 89 | 90 | // Attach AnonSlave to Replica Set as an anonymous and subscribe for the new(!) DML requests. 91 | // Attach calls Join and then Subscribe with VClock = s.VClock[1:]... 92 | // If didn't call Join before Attach then you need to set VClock first either manually or using JoinWithSnap. 93 | // Use out chan for asynchronous packet receiving or synchronous PacketIterator otherwise. 94 | // If you need all requests in chan use JoinWithSnap(chan) and then s.Subscribe(s.VClock[1:]...). 95 | func (s *AnonSlave) Attach(out ...chan *Packet) (it PacketIterator, err error) { 96 | if err = s.Join(); err != nil { 97 | return nil, err 98 | } 99 | 100 | // skip reserved zero index of the Vector Clock 101 | if len(s.VClock) <= 1 { 102 | return nil, ErrVectorClock 103 | } 104 | 105 | if it, err = s.Subscribe(s.VClock[1:]...); err != nil { 106 | return nil, err 107 | } 108 | 109 | // no chan means synchronous dml request receiving 110 | if s.isEmptyChan(out...) { 111 | return it, nil 112 | } 113 | 114 | // consume new DML requests and send them to the given chan 115 | go func(out chan *Packet) { 116 | defer close(out) 117 | for s.HasNext() { 118 | out <- s.Packet() 119 | } 120 | }(out[0]) 121 | 122 | // return nil iterator to avoid concurrent using of the Next method 123 | return nil, nil 124 | } 125 | 126 | func (s *AnonSlave) fetchSnapshot() (err error) { 127 | pp, err := s.newPacket(&FetchSnapshot{}) 128 | if err != nil { 129 | return 130 | } 131 | 132 | if err = s.send(pp); err != nil { 133 | return err 134 | } 135 | s.c.releasePacket(pp) 136 | 137 | if pp, err = s.receive(); err != nil { 138 | return err 139 | } 140 | defer pp.Release() 141 | 142 | p := &Packet{} 143 | if err := p.UnmarshalBinary(pp.body); err != nil { 144 | return err 145 | } 146 | 147 | if p.Cmd != OKCommand { 148 | s.p = p 149 | if p.Result == nil { 150 | return ErrBadResult 151 | } 152 | return p.Result.Error 153 | } 154 | 155 | v := new(VClock) 156 | _, err = v.UnmarshalMsg(pp.body) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | s.VClock = v.VClock 162 | 163 | return nil 164 | } 165 | 166 | // subscribe sends SUBSCRIBE request and waits for VCLOCK response. 167 | func (s *AnonSlave) subscribe(lsns ...uint64) error { 168 | vc := NewVectorClock(lsns...) 169 | pp, err := s.newPacket(&Subscribe{ 170 | UUID: s.UUID, 171 | ReplicaSetUUID: s.ReplicaSet.UUID, 172 | VClock: vc, 173 | Anon: true, 174 | }) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | if err = s.send(pp); err != nil { 180 | return err 181 | } 182 | s.c.releasePacket(pp) 183 | 184 | if pp, err = s.receive(); err != nil { 185 | return err 186 | } 187 | defer s.c.releasePacket(pp) 188 | 189 | p := &pp.packet 190 | err = p.UnmarshalBinary(pp.body) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | sub := new(SubscribeResponse) 196 | _, err = sub.UnmarshalMsg(pp.body) 197 | if err != nil { 198 | return err 199 | } 200 | 201 | // validate the response replica set UUID only if it is not empty 202 | if s.ReplicaSet.UUID != "" && sub.ReplicaSetUUID != "" && s.ReplicaSet.UUID != sub.ReplicaSetUUID { 203 | return NewUnexpectedReplicaSetUUIDError(s.ReplicaSet.UUID, sub.ReplicaSetUUID) 204 | } 205 | 206 | if sub.ReplicaSetUUID != "" { 207 | s.ReplicaSet.UUID = sub.ReplicaSetUUID 208 | } 209 | s.VClock = sub.VClock 210 | 211 | return nil 212 | } 213 | 214 | // nextSnap iterates responses on JOIN request. 215 | // At the end it returns io.EOF error and nil packet. 216 | // While iterating all 217 | func (s *AnonSlave) nextSnap() (p *Packet, err error) { 218 | pp, err := s.receive() 219 | if err != nil { 220 | return nil, err 221 | } 222 | defer s.c.releasePacket(pp) 223 | 224 | p = &Packet{} 225 | err = p.UnmarshalBinary(pp.body) 226 | if err != nil { 227 | return nil, err 228 | } 229 | 230 | // we have to parse snapshot logs to find replica set instances, UUID 231 | switch p.Cmd { 232 | case InsertCommand: 233 | q := p.Request.(*Insert) 234 | if q.Space == SpaceSchema { 235 | key := q.Tuple[0].(string) 236 | if key == SchemaKeyClusterUUID { 237 | if s.ReplicaSet.UUID != "" && s.ReplicaSet.UUID != q.Tuple[1].(string) { 238 | return nil, NewUnexpectedReplicaSetUUIDError(s.ReplicaSet.UUID, q.Tuple[1].(string)) 239 | } 240 | s.ReplicaSet.UUID = q.Tuple[1].(string) 241 | } 242 | } 243 | case OKCommand: 244 | v := new(VClock) 245 | _, err = v.UnmarshalMsg(pp.body) 246 | if err != nil { 247 | return nil, err 248 | } 249 | // ignore this VClock for anon replica 250 | 251 | return nil, io.EOF 252 | } 253 | 254 | return p, nil 255 | } 256 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/base64" 6 | "fmt" 7 | 8 | "github.com/tinylib/msgp/msgp" 9 | ) 10 | 11 | type Auth struct { 12 | User string 13 | Password string 14 | GreetingAuth []byte 15 | } 16 | 17 | var _ Query = (*Auth)(nil) 18 | 19 | const authHash = "chap-sha1" 20 | const scrambleSize = sha1.Size // == 20 21 | 22 | // copy-paste from go-tarantool 23 | func scramble(encodedSalt []byte, pass string) (scramble []byte, err error) { 24 | /* ================================================================== 25 | According to: http://tarantool.org/doc/dev_guide/box-protocol.html 26 | 27 | salt = base64_decode(encoded_salt); 28 | step_1 = sha1(password); 29 | step_2 = sha1(step_1); 30 | step_3 = sha1(salt, step_2); 31 | scramble = xor(step_1, step_3); 32 | return scramble; 33 | 34 | ===================================================================== */ 35 | 36 | salt, err := base64.StdEncoding.DecodeString(string(encodedSalt)) 37 | if err != nil { 38 | return 39 | } 40 | step1 := sha1.Sum([]byte(pass)) 41 | step2 := sha1.Sum(step1[0:]) 42 | hash := sha1.New() // may be create it once per connection ? 43 | hash.Write(salt[0:scrambleSize]) 44 | hash.Write(step2[0:]) 45 | step3 := hash.Sum(nil) 46 | 47 | return xor(step1[0:], step3[0:], scrambleSize), nil 48 | } 49 | 50 | func xor(left, right []byte, size int) []byte { 51 | result := make([]byte, size) 52 | for i := 0; i < size; i++ { 53 | result[i] = left[i] ^ right[i] 54 | } 55 | return result 56 | } 57 | 58 | func (auth *Auth) GetCommandID() uint { 59 | return AuthCommand 60 | } 61 | 62 | // MarshalMsg implements msgp.Marshaler 63 | func (auth *Auth) MarshalMsg(b []byte) (o []byte, err error) { 64 | scr, err := scramble(auth.GreetingAuth, auth.Password) 65 | if err != nil { 66 | return nil, fmt.Errorf("auth: scrambling failure: %s", err.Error()) 67 | } 68 | 69 | o = b 70 | o = msgp.AppendMapHeader(o, 2) 71 | o = msgp.AppendUint(o, KeyUserName) 72 | o = msgp.AppendString(o, auth.User) 73 | 74 | o = msgp.AppendUint(o, KeyTuple) 75 | o = msgp.AppendArrayHeader(o, 2) 76 | o = msgp.AppendString(o, authHash) 77 | o = msgp.AppendBytes(o, scr) 78 | 79 | return o, nil 80 | } 81 | 82 | // UnmarshalMsg implements msgp.Unmarshaler 83 | func (auth *Auth) UnmarshalMsg(data []byte) (buf []byte, err error) { 84 | var i, l uint32 85 | var k uint 86 | 87 | buf = data 88 | if i, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 89 | return 90 | } 91 | 92 | for ; i > 0; i-- { 93 | if k, buf, err = msgp.ReadUintBytes(buf); err != nil { 94 | return 95 | } 96 | 97 | switch k { 98 | case KeyUserName: 99 | if auth.User, buf, err = msgp.ReadStringBytes(buf); err != nil { 100 | return 101 | } 102 | case KeyTuple: 103 | if l, buf, err = msgp.ReadArrayHeaderBytes(buf); err != nil { 104 | return 105 | } 106 | if l == 2 { 107 | var obuf []byte 108 | 109 | if buf, err = msgp.Skip(buf); err != nil { 110 | return 111 | } 112 | 113 | obuf = buf 114 | if auth.GreetingAuth, buf, err = msgp.ReadBytesBytes(buf, nil); err != nil { 115 | if _, ok := err.(msgp.TypeError); ok { 116 | buf = obuf 117 | var greetingStr string 118 | if greetingStr, buf, err = msgp.ReadStringBytes(buf); err != nil { 119 | return 120 | } 121 | auth.GreetingAuth = []byte(greetingStr) 122 | } 123 | } 124 | } 125 | default: 126 | return buf, fmt.Errorf("Auth.Unpack: Expected KeyUserName or KeyTuple") 127 | } 128 | } 129 | return 130 | } 131 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestAuth(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | tarantoolConfig := ` 13 | box.schema.user.create("tester", {password = "12345678"}) 14 | ` 15 | 16 | box, err := NewBox(tarantoolConfig, nil) 17 | if !assert.NoError(err) { 18 | return 19 | } 20 | 21 | defer box.Close() 22 | 23 | // unknown user 24 | conn, err := box.Connect(&Options{ 25 | User: "user_not_found", 26 | Password: "qwerty", 27 | }) 28 | ver, _ := tntBoxVersion(box) 29 | 30 | if assert.Error(err) && assert.Nil(conn) { 31 | if ver >= version2_11_0 { 32 | assert.Exactly(err.Error(), "User not found or supplied credentials are invalid") 33 | } else { 34 | assert.Contains(err.Error(), "is not found") 35 | } 36 | } 37 | 38 | // bad password 39 | conn, err = box.Connect(&Options{ 40 | User: "tester", 41 | Password: "qwerty", 42 | }) 43 | ver, _ = tntBoxVersion(box) 44 | 45 | if assert.Error(err) && assert.Nil(conn) { 46 | if ver >= version2_11_0 { 47 | assert.Exactly(err.Error(), "User not found or supplied credentials are invalid") 48 | } else { 49 | assert.Contains(err.Error(), "Incorrect password supplied for user") 50 | } 51 | } 52 | 53 | // ok user password 54 | conn, err = box.Connect(&Options{ 55 | User: "tester", 56 | Password: "12345678", 57 | }) 58 | if assert.NoError(err) && assert.NotNil(conn) { 59 | assert.NotEmpty(conn.InstanceUUID(), "instance UUID is empty") 60 | conn.Close() 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /binpacket.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "math" 9 | 10 | "github.com/tinylib/msgp/msgp" 11 | ) 12 | 13 | type BinaryPacket struct { 14 | body []byte 15 | header [32]byte 16 | pool *BinaryPacketPool 17 | packet Packet 18 | } 19 | 20 | type UnmarshalBinaryBodyFunc func(*Packet, []byte) error 21 | 22 | // WriteTo implements the io.WriterTo interface 23 | func (pp *BinaryPacket) WriteTo(w io.Writer) (n int64, err error) { 24 | h32 := pp.header[:32] 25 | body := pp.body 26 | 27 | h := msgp.AppendUint(h32[:0], math.MaxUint32) 28 | mappos := len(h) 29 | h = msgp.AppendMapHeader(h, 3) 30 | h = msgp.AppendUint(h, KeyCode) 31 | h = msgp.AppendUint(h, math.MaxUint32) 32 | syncpos := len(h) 33 | h = msgp.AppendUint(h, KeySync) 34 | h = msgp.AppendUint64(h, pp.packet.requestID) 35 | h = msgp.AppendUint(h, KeySchemaID) 36 | h = msgp.AppendUint64(h, pp.packet.SchemaID) 37 | 38 | binary.BigEndian.PutUint32(h[syncpos-4:], uint32(pp.packet.Cmd)) 39 | 40 | l := len(h) + len(body) - mappos 41 | binary.BigEndian.PutUint32(h32[mappos-4:], uint32(l)) 42 | 43 | m, err := w.Write(h) 44 | n += int64(m) 45 | if err != nil { 46 | return 47 | } 48 | 49 | m, err = w.Write(body) 50 | n += int64(m) 51 | pp.body = pp.body[:0] 52 | 53 | return 54 | } 55 | 56 | func (pp *BinaryPacket) Reset() { 57 | pp.packet.Cmd = OKCommand 58 | pp.packet.SchemaID = 0 59 | pp.packet.requestID = 0 60 | pp.packet.Result = nil 61 | pp.packet.ResultUnmarshalMode = ResultDefaultMode 62 | pp.body = pp.body[:0] 63 | } 64 | 65 | func (pp *BinaryPacket) Release() { 66 | if pp.pool != nil && cap(pp.body) <= DefaultMaxPoolPacketSize { 67 | pp.pool.Put(pp) 68 | } 69 | } 70 | 71 | // ReadFrom implements the io.ReaderFrom interface 72 | func (pp *BinaryPacket) ReadFrom(r io.Reader) (n int64, err error) { 73 | var h = pp.header[:8] 74 | var bodyLength uint 75 | var headerLength uint 76 | var rr, crr int 77 | 78 | if rr, err = io.ReadFull(r, h[:1]); err != nil { 79 | return int64(rr), err 80 | } 81 | 82 | c := h[0] 83 | switch { 84 | case c <= 0x7f: 85 | headerLength = 1 86 | case c == 0xcc: 87 | headerLength = 2 88 | case c == 0xcd: 89 | headerLength = 3 90 | case c == 0xce: 91 | headerLength = 5 92 | default: 93 | return int64(rr), fmt.Errorf("wrong packet header: %#v", c) 94 | } 95 | 96 | if headerLength > 1 { 97 | crr, err = io.ReadFull(r, h[1:headerLength]) 98 | if rr = rr + crr; err != nil { 99 | return int64(rr), err 100 | } 101 | } 102 | 103 | if bodyLength, _, err = msgp.ReadUintBytes(h[:headerLength]); err != nil { 104 | return int64(rr), err 105 | } 106 | if bodyLength == 0 { 107 | return int64(rr), errors.New("Packet should not be 0 length") 108 | } 109 | 110 | if uint(cap(pp.body)) < bodyLength { 111 | pp.body = make([]byte, bodyLength+bodyLength/2) 112 | } 113 | 114 | pp.body = pp.body[:bodyLength] 115 | crr, err = io.ReadFull(r, pp.body) 116 | return int64(rr) + int64(crr), err 117 | } 118 | 119 | func (pp *BinaryPacket) Unmarshal() error { 120 | if err := pp.packet.UnmarshalBinary(pp.body); err != nil { 121 | return fmt.Errorf("Error decoding packet type %d: %s", pp.packet.Cmd, err) 122 | } 123 | return nil 124 | } 125 | 126 | func (pp *BinaryPacket) UnmarshalCustomBody(um UnmarshalBinaryBodyFunc) (err error) { 127 | buf := pp.body 128 | 129 | if buf, err = pp.packet.UnmarshalBinaryHeader(buf); err != nil { 130 | return fmt.Errorf("Error decoding packet type %d: %s", pp.packet.Cmd, err) 131 | } 132 | 133 | if err = um(&pp.packet, buf); err != nil { 134 | return fmt.Errorf("Error decoding packet type %d: %s", pp.packet.Cmd, err) 135 | } 136 | 137 | return nil 138 | } 139 | 140 | func (pp *BinaryPacket) Bytes() []byte { 141 | return pp.body 142 | } 143 | 144 | func (pp *BinaryPacket) Result() *Result { 145 | return pp.packet.Result 146 | } 147 | 148 | func (pp *BinaryPacket) readPacket(r io.Reader) (err error) { 149 | if _, err = pp.ReadFrom(r); err != nil { 150 | return 151 | } 152 | return pp.packet.UnmarshalBinary(pp.body) 153 | } 154 | 155 | // ReadRawPacket reads the whole packet body and only unpacks request ID for routing purposes 156 | func (pp *BinaryPacket) readRawPacket(r io.Reader) (requestID uint64, err error) { 157 | var l uint32 158 | 159 | requestID = 0 160 | if _, err = pp.ReadFrom(r); err != nil { 161 | return 162 | } 163 | 164 | buf := pp.body 165 | if l, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 166 | return 167 | } 168 | 169 | for ; l > 0; l-- { 170 | var cd uint 171 | if cd, buf, err = msgp.ReadUintBytes(buf); err != nil { 172 | return 173 | } 174 | if cd == KeySync { 175 | requestID, _, err = msgp.ReadUint64Bytes(buf) 176 | return 177 | } 178 | if buf, err = msgp.Skip(buf); err != nil { 179 | return 180 | } 181 | } 182 | 183 | return 184 | } 185 | 186 | func (pp *BinaryPacket) packMsg(q Query, packdata *packData) (err error) { 187 | if iq, ok := q.(internalQuery); ok { 188 | if pp.body, err = iq.packMsg(packdata, pp.body[:0]); err != nil { 189 | pp.packet.Cmd = ErrorFlag 190 | return err 191 | } 192 | } else if mp, ok := q.(msgp.Marshaler); ok { 193 | if pp.body, err = mp.MarshalMsg(pp.body[:0]); err != nil { 194 | pp.packet.Cmd = ErrorFlag 195 | return err 196 | } 197 | } else { 198 | pp.packet.Cmd = ErrorFlag 199 | return errors.New("query struct doesn't implement any known marshalling interface") 200 | } 201 | 202 | pp.packet.Cmd = q.GetCommandID() 203 | return nil 204 | } 205 | -------------------------------------------------------------------------------- /binpacket_pool.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | type BinaryPacketPool struct { 4 | queue chan *BinaryPacket 5 | } 6 | 7 | func newBinaryPacketPool() *BinaryPacketPool { 8 | return &BinaryPacketPool{ 9 | queue: make(chan *BinaryPacket, 4096), 10 | } 11 | } 12 | 13 | func (p *BinaryPacketPool) GetWithID(requestID uint64) (pp *BinaryPacket) { 14 | select { 15 | case pp = <-p.queue: 16 | default: 17 | pp = &BinaryPacket{} 18 | } 19 | 20 | pp.Reset() 21 | pp.pool = p 22 | pp.packet.requestID = requestID 23 | return 24 | } 25 | 26 | func (p *BinaryPacketPool) Get() *BinaryPacket { 27 | return p.GetWithID(0) 28 | } 29 | 30 | func (p *BinaryPacketPool) Put(pp *BinaryPacket) { 31 | pp.pool = nil 32 | select { 33 | case p.queue <- pp: 34 | default: 35 | } 36 | } 37 | 38 | func (p *BinaryPacketPool) Close() { 39 | close(p.queue) 40 | } 41 | -------------------------------------------------------------------------------- /box.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "os" 10 | "os/exec" 11 | "path" 12 | "path/filepath" 13 | "strings" 14 | "sync" 15 | "syscall" 16 | "time" 17 | ) 18 | 19 | // Box is tarantool instance. For start/stop tarantool in tests 20 | type Box struct { 21 | Root string 22 | WorkDir string 23 | Port uint 24 | Listen string 25 | cmd *exec.Cmd 26 | stopOnce sync.Once 27 | stopped chan bool 28 | initLua string 29 | notifySock string 30 | version string 31 | } 32 | 33 | type BoxOptions struct { 34 | Host string 35 | Port uint 36 | PortMin uint 37 | PortMax uint 38 | WorkDir string 39 | 40 | LogDir string 41 | LogNamePrefix string 42 | } 43 | 44 | var ( 45 | ErrPortAlreadyInUse = errors.New("port already in use") 46 | ) 47 | 48 | func NewBox(config string, options *BoxOptions) (*Box, error) { 49 | if options == nil { 50 | options = &BoxOptions{} 51 | } 52 | 53 | if options.PortMin == 0 { 54 | options.PortMin = 8000 55 | } 56 | 57 | if options.PortMax == 0 { 58 | options.PortMax = 9000 59 | } 60 | 61 | if options.Port != 0 { 62 | options.PortMin = options.Port 63 | options.PortMax = options.Port 64 | } 65 | 66 | if options.Host == "" { 67 | options.Host = "127.0.0.1" 68 | } 69 | if !strings.HasSuffix(options.Host, ":") { 70 | options.Host += ":" 71 | } 72 | 73 | var box *Box 74 | 75 | for port := options.PortMin; port <= options.PortMax; port++ { 76 | tmpDir, err := os.MkdirTemp("", options.LogNamePrefix) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | notifySock := filepath.Join(tmpDir, "notify.sock") 82 | 83 | logDir := options.LogDir 84 | if logDir == "" { 85 | logDir = os.Getenv("TNT_LOG_DIR") 86 | } 87 | 88 | logPath := "stderr" 89 | if logDir != "" { 90 | _, fName := filepath.Split(tmpDir) 91 | logPath = fmt.Sprintf(`"%s"`, filepath.Join(logDir, fName)) 92 | fmt.Println("Tarantool log path:", logPath) 93 | } 94 | 95 | initLua := strings.Replace(` 96 | box.cfg{ 97 | memtx_dir = "{root}/snap/", 98 | wal_dir = "{root}/wal/", 99 | log = {log}, 100 | } 101 | `, "{log}", logPath, -1) 102 | 103 | initLua += ` 104 | sendstatus("STARTING") 105 | 106 | box.once('guest:read_universe', function() 107 | box.schema.user.grant('guest', 'read', 'universe') 108 | end) 109 | 110 | sendstatus("BINDING") 111 | 112 | box.cfg{ 113 | listen = "{host}{port}", 114 | } 115 | 116 | sendstatus("READY") 117 | ` 118 | readyLua := ` 119 | sendstatus("RUNNING") 120 | ` 121 | 122 | initLua = fmt.Sprintf("%s\n%s\n%s\n", initLua, config, readyLua) 123 | initLua = strings.Replace(initLua, "{host}", options.Host, -1) 124 | initLua = strings.Replace(initLua, "{port}", fmt.Sprintf("%d", port), -1) 125 | initLua = strings.Replace(initLua, "{root}", tmpDir, -1) 126 | 127 | initLua = fmt.Sprintf(` 128 | local sendstatus = function(status) 129 | local path = "{notify_sock_path}" 130 | if path ~= "" and path ~= "{" .. "notify_sock_path" .. "}" then 131 | local socket = require('socket') 132 | local sock = socket("AF_UNIX", "SOCK_DGRAM", 0) 133 | sock:sysconnect("unix/", path) 134 | if sock ~= nil then 135 | sock:write(status) 136 | sock:close() 137 | end 138 | end 139 | end 140 | 141 | %s 142 | `, initLua) 143 | 144 | initLua = strings.Replace(initLua, "{notify_sock_path}", notifySock, -1) 145 | 146 | for _, subDir := range []string{"snap", "wal"} { 147 | err = os.Mkdir(path.Join(tmpDir, subDir), 0755) 148 | if err != nil { 149 | return nil, err 150 | } 151 | } 152 | 153 | box = &Box{ 154 | Root: tmpDir, 155 | WorkDir: options.WorkDir, 156 | Listen: fmt.Sprintf("%s%d", options.Host, port), 157 | Port: port, 158 | cmd: nil, 159 | stopped: make(chan bool), 160 | initLua: initLua, 161 | notifySock: notifySock, 162 | } 163 | close(box.stopped) 164 | 165 | ver, err := box.Version() 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | if strings.HasPrefix(ver, "1.6") { 171 | box.initLua = strings.Replace(box.initLua, "memtx_dir =", "snap_dir =", -1) 172 | box.initLua = strings.Replace(box.initLua, "log =", "logger =", -1) 173 | } 174 | 175 | err = box.Start() 176 | if err == nil { 177 | break 178 | } 179 | if err != ErrPortAlreadyInUse { 180 | return nil, err 181 | } 182 | os.RemoveAll(box.Root) 183 | box = nil 184 | } 185 | 186 | if box == nil { 187 | return nil, fmt.Errorf("can't bind any port from %d to %d", options.PortMin, options.PortMax) 188 | } 189 | 190 | return box, nil 191 | } 192 | 193 | func (box *Box) StartWithLua(luaTransform func(string) string) error { 194 | if !box.IsStopped() { 195 | return nil 196 | } 197 | 198 | box.stopped = make(chan bool) 199 | 200 | initLua := box.initLua 201 | if luaTransform != nil { 202 | initLua = luaTransform(initLua) 203 | } 204 | 205 | initLuaFile := path.Join(box.Root, "init.lua") 206 | err := os.WriteFile(initLuaFile, []byte(initLua), 0644) 207 | if err != nil { 208 | return err 209 | } 210 | 211 | if box.WorkDir != "" { 212 | oldwd, err := os.Getwd() 213 | if err != nil { 214 | return err 215 | } 216 | 217 | err = os.Chdir(box.WorkDir) 218 | if err != nil { 219 | return err 220 | } 221 | defer os.Chdir(oldwd) 222 | } 223 | 224 | statusCh := make(chan string, 10) 225 | u, err := net.ListenUnixgram("unixgram", &net.UnixAddr{Name: box.notifySock, Net: "unix"}) 226 | if err != nil { 227 | return err 228 | } 229 | defer os.Remove(box.notifySock) 230 | 231 | go func() { 232 | for { 233 | pck := make([]byte, 128) 234 | nr, err := u.Read(pck) 235 | if err != nil { 236 | close(statusCh) 237 | return 238 | } 239 | msg := string(pck[0:nr]) 240 | statusCh <- msg 241 | if msg == "RUNNING" { 242 | close(statusCh) 243 | return 244 | } 245 | } 246 | }() 247 | 248 | cmd := exec.Command("tarantool", initLuaFile) 249 | box.cmd = cmd 250 | 251 | err = cmd.Start() 252 | if err != nil { 253 | return err 254 | } 255 | 256 | for status := range statusCh { 257 | if status == "RUNNING" { 258 | return nil 259 | } 260 | if status == "BINDING" { 261 | select { 262 | case status = <-statusCh: 263 | if status != "READY" { 264 | box.Close() 265 | if strings.Contains(status, "failed to bind, called on fd -1") { 266 | return ErrPortAlreadyInUse 267 | } 268 | return fmt.Errorf("Box status is '%s', not READY", status) 269 | } 270 | case <-time.After(time.Millisecond * 50): 271 | box.Close() 272 | return ErrPortAlreadyInUse 273 | } 274 | } 275 | } 276 | 277 | box.Close() 278 | return ErrPortAlreadyInUse 279 | } 280 | 281 | func (box *Box) Start() error { 282 | return box.StartWithLua(nil) 283 | } 284 | 285 | func (box *Box) Stop() { 286 | go func() { 287 | select { 288 | case <-box.stopped: 289 | return 290 | default: 291 | if box.cmd != nil { 292 | box.cmd.Process.Signal(syscall.SIGTERM) 293 | //box.cmd.Process.Kill() 294 | box.cmd.Process.Wait() 295 | box.cmd = nil 296 | } 297 | close(box.stopped) 298 | } 299 | }() 300 | <-box.stopped 301 | } 302 | 303 | func (box *Box) IsStopped() bool { 304 | select { 305 | case <-box.stopped: 306 | return true 307 | default: 308 | return false 309 | } 310 | } 311 | 312 | func (box *Box) Close() { 313 | box.stopOnce.Do(func() { 314 | box.Stop() 315 | os.RemoveAll(box.Root) 316 | }) 317 | } 318 | 319 | func (box *Box) Addr() string { 320 | return box.Listen 321 | } 322 | 323 | func (box *Box) Connect(options *Options) (*Connection, error) { 324 | return Connect(box.Addr(), options) 325 | } 326 | 327 | func (box *Box) Version() (string, error) { 328 | verPrefix := "Tarantool " 329 | 330 | if box.version != "" { 331 | return box.version, nil 332 | } 333 | 334 | var out bytes.Buffer 335 | cmd := exec.Command("tarantool", "--version") 336 | cmd.Stdout = &out 337 | 338 | err := cmd.Run() 339 | if err != nil { 340 | return "", err 341 | } 342 | 343 | scanner := bufio.NewScanner(&out) 344 | scanner.Split(bufio.ScanLines) 345 | for scanner.Scan() { 346 | t := scanner.Text() 347 | if !strings.HasPrefix(t, verPrefix) { 348 | continue 349 | } 350 | 351 | var major, minor, patch uint32 352 | ver := string(t[len(verPrefix):]) 353 | if n, _ := fmt.Sscanf(ver, "%d.%d.%d", &major, &minor, &patch); n != 3 { 354 | continue 355 | } 356 | 357 | box.version = fmt.Sprintf("%d.%d.%d", major, minor, patch) 358 | break 359 | } 360 | 361 | if box.version == "" { 362 | return "", errors.New("unknown Tarantool version") 363 | } 364 | return box.version, nil 365 | } 366 | -------------------------------------------------------------------------------- /box_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestBox(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | config := ` 13 | box.info() 14 | ` 15 | 16 | box, err := NewBox(config, &BoxOptions{}) 17 | if !assert.NoError(err) { 18 | return 19 | } 20 | defer box.Close() 21 | 22 | } 23 | -------------------------------------------------------------------------------- /call.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tinylib/msgp/msgp" 7 | ) 8 | 9 | type Call struct { 10 | Name string 11 | Tuple []interface{} 12 | } 13 | 14 | var _ Query = (*Call)(nil) 15 | 16 | func (q *Call) GetCommandID() uint { 17 | return CallCommand 18 | } 19 | 20 | // MarshalMsg implements msgp.Marshaler 21 | func (q *Call) MarshalMsg(b []byte) (o []byte, err error) { 22 | o = b 23 | o = msgp.AppendMapHeader(o, 2) 24 | 25 | o = msgp.AppendUint(o, KeyFunctionName) 26 | o = msgp.AppendString(o, q.Name) 27 | 28 | if q.Tuple == nil { 29 | o = msgp.AppendUint(o, KeyTuple) 30 | o = msgp.AppendArrayHeader(o, 0) 31 | } else { 32 | o = msgp.AppendUint(o, KeyTuple) 33 | if o, err = msgp.AppendIntf(o, q.Tuple); err != nil { 34 | return o, err 35 | } 36 | } 37 | 38 | return o, nil 39 | } 40 | 41 | // UnmarshalMsg implements msgp.Unmarshaler 42 | func (q *Call) UnmarshalMsg(data []byte) (buf []byte, err error) { 43 | var i uint32 44 | var k uint 45 | var t interface{} 46 | 47 | q.Name = "" 48 | q.Tuple = nil 49 | 50 | buf = data 51 | if i, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 52 | return 53 | } 54 | if i != 2 { 55 | return buf, errors.New("Call.Unpack: expected map of length 2") 56 | } 57 | 58 | for ; i > 0; i-- { 59 | if k, buf, err = msgp.ReadUintBytes(buf); err != nil { 60 | return 61 | } 62 | 63 | switch k { 64 | case KeyFunctionName: 65 | if q.Name, buf, err = msgp.ReadStringBytes(buf); err != nil { 66 | return 67 | } 68 | case KeyTuple: 69 | t, buf, err = msgp.ReadIntfBytes(buf) 70 | if err != nil { 71 | return buf, err 72 | } 73 | 74 | if q.Tuple = t.([]interface{}); q.Tuple == nil { 75 | return buf, errors.New("interface type is not []interface{}") 76 | } 77 | if len(q.Tuple) == 0 { 78 | q.Tuple = nil 79 | } 80 | } 81 | } 82 | 83 | if q.Name == "" { 84 | return buf, errors.New("Call.Unpack: no space specified") 85 | } 86 | 87 | return 88 | } 89 | -------------------------------------------------------------------------------- /call17.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tinylib/msgp/msgp" 7 | ) 8 | 9 | // Call17 is available since Tarantool >= 1.7.2 10 | type Call17 struct { 11 | Name string 12 | Tuple []interface{} 13 | } 14 | 15 | var _ Query = (*Call17)(nil) 16 | 17 | func (q *Call17) GetCommandID() uint { 18 | return Call17Command 19 | } 20 | 21 | // MarshalMsg implements msgp.Marshaler 22 | func (q *Call17) MarshalMsg(b []byte) (o []byte, err error) { 23 | o = b 24 | o = msgp.AppendMapHeader(o, 2) 25 | 26 | o = msgp.AppendUint(o, KeyFunctionName) 27 | o = msgp.AppendString(o, q.Name) 28 | 29 | if q.Tuple == nil { 30 | o = msgp.AppendUint(o, KeyTuple) 31 | o = msgp.AppendArrayHeader(o, 0) 32 | } else { 33 | o = msgp.AppendUint(o, KeyTuple) 34 | if o, err = msgp.AppendIntf(o, q.Tuple); err != nil { 35 | return o, err 36 | } 37 | } 38 | 39 | return o, nil 40 | } 41 | 42 | // UnmarshalMsg implements msgp.Unmarshaler 43 | func (q *Call17) UnmarshalMsg(data []byte) (buf []byte, err error) { 44 | var i uint32 45 | var k uint 46 | var t interface{} 47 | 48 | q.Name = "" 49 | q.Tuple = nil 50 | 51 | buf = data 52 | if i, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 53 | return 54 | } 55 | if i != 2 { 56 | return buf, errors.New("Call17.Unpack: expected map of length 2") 57 | } 58 | 59 | for ; i > 0; i-- { 60 | if k, buf, err = msgp.ReadUintBytes(buf); err != nil { 61 | return 62 | } 63 | 64 | switch k { 65 | case KeyFunctionName: 66 | if q.Name, buf, err = msgp.ReadStringBytes(buf); err != nil { 67 | return 68 | } 69 | case KeyTuple: 70 | t, buf, err = msgp.ReadIntfBytes(buf) 71 | if err != nil { 72 | return buf, err 73 | } 74 | 75 | if q.Tuple = t.([]interface{}); q.Tuple == nil { 76 | return buf, errors.New("interface type is not []interface{}") 77 | } 78 | if len(q.Tuple) == 0 { 79 | q.Tuple = nil 80 | } 81 | } 82 | } 83 | 84 | if q.Name == "" { 85 | return buf, errors.New("Call17.Unpack: no space specified") 86 | } 87 | 88 | return 89 | } 90 | -------------------------------------------------------------------------------- /call17_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCall17(t *testing.T) { 12 | assert := assert.New(t) 13 | 14 | tarantoolConfig := ` 15 | local s = box.schema.space.create('tester', {id = 42}) 16 | s:create_index('tester_id', { 17 | type = 'tree', 18 | parts = {1, 'NUM'} 19 | }) 20 | s:create_index('tester_name', { 21 | type = 'hash', 22 | parts = {2, 'STR'} 23 | }) 24 | s:create_index('id_name', { 25 | type = 'hash', 26 | parts = {1, 'NUM', 2, 'STR'}, 27 | unique = true 28 | }) 29 | local t = s:insert({1, 'First record'}) 30 | t = s:insert({2, 'Music'}) 31 | t = s:insert({3, 'Length', 93}) 32 | 33 | function sel_all() 34 | return box.space.tester:select({}, {iterator = "ALL"}) 35 | end 36 | 37 | function sel_name(tester_id, name) 38 | return box.space.tester.index.id_name:select{tester_id, name} 39 | end 40 | 41 | function call_case_1() 42 | return 1 43 | end 44 | 45 | function call_case_2() 46 | return 1, 2, 3 47 | end 48 | 49 | function call_case_3() 50 | return true 51 | end 52 | 53 | function call_case_4() 54 | return nil 55 | end 56 | 57 | function call_case_5() 58 | return {} 59 | end 60 | 61 | function call_case_6() 62 | return {1} 63 | end 64 | 65 | function call_case_7() 66 | return {1, 2, 3} 67 | end 68 | 69 | function call_case_8() 70 | return {1, 2, 3}, {'a', 'b', 'c'}, {true, false} 71 | end 72 | 73 | function call_case_9() 74 | return {key1 = 'value1', key2 = 'value2'} 75 | end 76 | 77 | function call_case_10() 78 | return 79 | end 80 | 81 | local number_of_extra_cases = 10 82 | 83 | box.schema.func.create('sel_all', {if_not_exists = true}) 84 | box.schema.func.create('sel_name', {if_not_exists = true}) 85 | for i = 1, number_of_extra_cases do 86 | box.schema.func.create('call_case_'..i, {if_not_exists = true}) 87 | end 88 | 89 | box.schema.user.grant('guest', 'execute', 'function', 'sel_all', {if_not_exists = true}) 90 | box.schema.user.grant('guest', 'execute', 'function', 'sel_name', {if_not_exists = true}) 91 | for i = 1, number_of_extra_cases do 92 | box.schema.user.grant('guest', 'execute', 'function', 'call_case_'..i, {if_not_exists = true}) 93 | end 94 | ` 95 | 96 | box, err := NewBox(tarantoolConfig, nil) 97 | if !assert.NoError(err) { 98 | return 99 | } 100 | defer box.Close() 101 | 102 | ver, err := box.Version() 103 | if !assert.NoError(err) { 104 | return 105 | } 106 | if strings.HasPrefix(ver, "1.6") { 107 | t.Skip("requires tarantool >= 1.7.2") 108 | } 109 | 110 | type testParams struct { 111 | query *Call17 112 | execOption ExecOption 113 | expectedData [][]interface{} 114 | expectedRawData interface{} 115 | } 116 | 117 | do := func(params *testParams) { 118 | var buf []byte 119 | 120 | conn, err := box.Connect(nil) 121 | assert.NoError(err) 122 | assert.NotNil(conn) 123 | 124 | defer conn.Close() 125 | 126 | buf, err = params.query.MarshalMsg(nil) 127 | 128 | if assert.NoError(err) { 129 | var query2 = &Call17{} 130 | _, err = query2.UnmarshalMsg(buf) 131 | 132 | if assert.NoError(err) { 133 | assert.Equal(params.query.Name, query2.Name) 134 | assert.Equal(params.query.Tuple, query2.Tuple) 135 | } 136 | } 137 | 138 | var opts []ExecOption 139 | if params.execOption != nil { 140 | opts = append(opts, params.execOption) 141 | } 142 | res := conn.Exec(context.Background(), params.query, opts...) 143 | 144 | if assert.NoError(res.Error) { 145 | assert.Equal(params.expectedData, res.Data) 146 | assert.Equal(params.expectedRawData, res.RawData) 147 | } 148 | } 149 | 150 | // call sel_all without params 151 | do(&testParams{ 152 | query: &Call17{ 153 | Name: "sel_all", 154 | }, 155 | expectedData: [][]interface{}{ 156 | { 157 | []interface{}{int64(1), "First record"}, 158 | []interface{}{int64(2), "Music"}, 159 | []interface{}{int64(3), "Length", int64(93)}, 160 | }, 161 | }, 162 | }) 163 | do(&testParams{ 164 | query: &Call17{ 165 | Name: "sel_all", 166 | }, 167 | execOption: ExecResultAsDataWithFallback, 168 | expectedData: [][]interface{}{ 169 | { 170 | []interface{}{int64(1), "First record"}, 171 | []interface{}{int64(2), "Music"}, 172 | []interface{}{int64(3), "Length", int64(93)}, 173 | }, 174 | }, 175 | }) 176 | do(&testParams{ 177 | query: &Call17{ 178 | Name: "sel_all", 179 | }, 180 | execOption: ExecResultAsRawData, 181 | expectedRawData: []interface{}{ 182 | []interface{}{ 183 | []interface{}{int64(1), "First record"}, 184 | []interface{}{int64(2), "Music"}, 185 | []interface{}{int64(3), "Length", int64(93)}, 186 | }, 187 | }, 188 | }) 189 | 190 | // call sel_name with params 191 | do(&testParams{ 192 | query: &Call17{ 193 | Name: "sel_name", 194 | Tuple: []interface{}{int64(2), "Music"}, 195 | }, 196 | expectedData: [][]interface{}{ 197 | { 198 | []interface{}{int64(2), "Music"}, 199 | }, 200 | }, 201 | }) 202 | do(&testParams{ 203 | query: &Call17{ 204 | Name: "sel_name", 205 | Tuple: []interface{}{int64(2), "Music"}, 206 | }, 207 | execOption: ExecResultAsDataWithFallback, 208 | expectedData: [][]interface{}{ 209 | { 210 | []interface{}{int64(2), "Music"}, 211 | }, 212 | }, 213 | }) 214 | do(&testParams{ 215 | query: &Call17{ 216 | Name: "sel_name", 217 | Tuple: []interface{}{int64(2), "Music"}, 218 | }, 219 | execOption: ExecResultAsRawData, 220 | expectedRawData: []interface{}{ 221 | []interface{}{ 222 | []interface{}{int64(2), "Music"}, 223 | }, 224 | }, 225 | }) 226 | 227 | // For stored procedures the result is returned in the same way as eval (in certain cases). 228 | // Note that returning arrays (also an empty table) is a special case. 229 | 230 | // scalar 1 231 | do(&testParams{ 232 | query: &Call17{ 233 | Name: "call_case_1", 234 | }, 235 | expectedData: [][]interface{}{ 236 | {int64(1)}, 237 | }, 238 | }) 239 | do(&testParams{ 240 | query: &Call17{ 241 | Name: "call_case_1", 242 | }, 243 | execOption: ExecResultAsDataWithFallback, 244 | expectedRawData: []interface{}{int64(1)}, 245 | }) 246 | do(&testParams{ 247 | query: &Call17{ 248 | Name: "call_case_1", 249 | }, 250 | execOption: ExecResultAsRawData, 251 | expectedRawData: []interface{}{int64(1)}, 252 | }) 253 | 254 | // multiple scalars 255 | do(&testParams{ 256 | query: &Call17{ 257 | Name: "call_case_2", 258 | }, 259 | expectedData: [][]interface{}{ 260 | {int64(1)}, {int64(2)}, {int64(3)}, 261 | }, 262 | }) 263 | do(&testParams{ 264 | query: &Call17{ 265 | Name: "call_case_2", 266 | }, 267 | execOption: ExecResultAsDataWithFallback, 268 | expectedRawData: []interface{}{ 269 | int64(1), int64(2), int64(3), 270 | }, 271 | }) 272 | do(&testParams{ 273 | query: &Call17{ 274 | Name: "call_case_2", 275 | }, 276 | execOption: ExecResultAsRawData, 277 | expectedRawData: []interface{}{ 278 | int64(1), int64(2), int64(3), 279 | }, 280 | }) 281 | 282 | // scalar true 283 | do(&testParams{ 284 | query: &Call17{ 285 | Name: "call_case_3", 286 | }, 287 | expectedData: [][]interface{}{ 288 | {true}, 289 | }, 290 | }) 291 | do(&testParams{ 292 | query: &Call17{ 293 | Name: "call_case_3", 294 | }, 295 | execOption: ExecResultAsDataWithFallback, 296 | expectedRawData: []interface{}{true}, 297 | }) 298 | do(&testParams{ 299 | query: &Call17{ 300 | Name: "call_case_3", 301 | }, 302 | execOption: ExecResultAsRawData, 303 | expectedRawData: []interface{}{true}, 304 | }) 305 | 306 | // scalar nil 307 | do(&testParams{ 308 | query: &Call17{ 309 | Name: "call_case_4", 310 | }, 311 | expectedData: [][]interface{}{ 312 | {nil}, 313 | }, 314 | }) 315 | do(&testParams{ 316 | query: &Call17{ 317 | Name: "call_case_4", 318 | }, 319 | execOption: ExecResultAsDataWithFallback, 320 | expectedRawData: []interface{}{ 321 | interface{}(nil), 322 | }, 323 | }) 324 | do(&testParams{ 325 | query: &Call17{ 326 | Name: "call_case_4", 327 | }, 328 | execOption: ExecResultAsRawData, 329 | expectedRawData: []interface{}{ 330 | interface{}(nil), 331 | }, 332 | }) 333 | 334 | // empty table 335 | do(&testParams{ 336 | query: &Call17{ 337 | Name: "call_case_5", 338 | }, 339 | expectedData: [][]interface{}{ 340 | {}, 341 | }, 342 | }) 343 | do(&testParams{ 344 | query: &Call17{ 345 | Name: "call_case_5", 346 | }, 347 | execOption: ExecResultAsDataWithFallback, 348 | expectedData: [][]interface{}{ 349 | {}, 350 | }, 351 | }) 352 | do(&testParams{ 353 | query: &Call17{ 354 | Name: "call_case_5", 355 | }, 356 | execOption: ExecResultAsRawData, 357 | expectedRawData: []interface{}{ 358 | []interface{}{}, 359 | }, 360 | }) 361 | 362 | // array with len 1 (similar to case 1) 363 | do(&testParams{ 364 | query: &Call17{ 365 | Name: "call_case_6", 366 | }, 367 | expectedData: [][]interface{}{ 368 | {int64(1)}, 369 | }, 370 | }) 371 | do(&testParams{ 372 | query: &Call17{ 373 | Name: "call_case_6", 374 | }, 375 | execOption: ExecResultAsDataWithFallback, 376 | expectedData: [][]interface{}{ 377 | {int64(1)}, 378 | }, 379 | }) 380 | do(&testParams{ 381 | query: &Call17{ 382 | Name: "call_case_6", 383 | }, 384 | execOption: ExecResultAsRawData, 385 | expectedRawData: []interface{}{ 386 | []interface{}{int64(1)}, 387 | }, 388 | }) 389 | 390 | // single array with len 3 391 | do(&testParams{ 392 | query: &Call17{ 393 | Name: "call_case_7", 394 | }, 395 | expectedData: [][]interface{}{ 396 | {int64(1), int64(2), int64(3)}, 397 | }, 398 | }) 399 | do(&testParams{ 400 | query: &Call17{ 401 | Name: "call_case_7", 402 | }, 403 | execOption: ExecResultAsDataWithFallback, 404 | expectedData: [][]interface{}{ 405 | {int64(1), int64(2), int64(3)}, 406 | }, 407 | }) 408 | do(&testParams{ 409 | query: &Call17{ 410 | Name: "call_case_7", 411 | }, 412 | execOption: ExecResultAsRawData, 413 | expectedRawData: []interface{}{ 414 | []interface{}{int64(1), int64(2), int64(3)}, 415 | }, 416 | }) 417 | 418 | // multiple arrays 419 | do(&testParams{ 420 | query: &Call17{ 421 | Name: "call_case_8", 422 | }, 423 | expectedData: [][]interface{}{ 424 | {int64(1), int64(2), int64(3)}, 425 | {"a", "b", "c"}, 426 | {true, false}, 427 | }, 428 | }) 429 | do(&testParams{ 430 | query: &Call17{ 431 | Name: "call_case_8", 432 | }, 433 | execOption: ExecResultAsDataWithFallback, 434 | expectedData: [][]interface{}{ 435 | {int64(1), int64(2), int64(3)}, 436 | {"a", "b", "c"}, 437 | {true, false}, 438 | }, 439 | }) 440 | do(&testParams{ 441 | query: &Call17{ 442 | Name: "call_case_8", 443 | }, 444 | execOption: ExecResultAsRawData, 445 | expectedRawData: []interface{}{ 446 | []interface{}{int64(1), int64(2), int64(3)}, 447 | []interface{}{"a", "b", "c"}, 448 | []interface{}{true, false}, 449 | }, 450 | }) 451 | 452 | // map with string keys 453 | do(&testParams{ 454 | query: &Call17{ 455 | Name: "call_case_9", 456 | }, 457 | expectedData: [][]interface{}{ 458 | {map[string]interface{}{"key1": "value1", "key2": "value2"}}, 459 | }, 460 | }) 461 | do(&testParams{ 462 | query: &Call17{ 463 | Name: "call_case_9", 464 | }, 465 | execOption: ExecResultAsDataWithFallback, 466 | expectedRawData: []interface{}{ 467 | map[string]interface{}{"key1": "value1", "key2": "value2"}, 468 | }, 469 | }) 470 | do(&testParams{ 471 | query: &Call17{ 472 | Name: "call_case_9", 473 | }, 474 | execOption: ExecResultAsRawData, 475 | expectedRawData: []interface{}{ 476 | map[string]interface{}{"key1": "value1", "key2": "value2"}, 477 | }, 478 | }) 479 | 480 | // empty result 481 | do(&testParams{ 482 | query: &Call17{ 483 | Name: "call_case_10", 484 | }, 485 | expectedData: [][]interface{}{}, 486 | }) 487 | do(&testParams{ 488 | query: &Call17{ 489 | Name: "call_case_10", 490 | }, 491 | execOption: ExecResultAsDataWithFallback, 492 | expectedData: [][]interface{}{}, 493 | }) 494 | do(&testParams{ 495 | query: &Call17{ 496 | Name: "call_case_10", 497 | }, 498 | execOption: ExecResultAsRawData, 499 | expectedRawData: []interface{}{}, 500 | }) 501 | } 502 | 503 | func BenchmarkCall17Pack(b *testing.B) { 504 | buf := make([]byte, 0) 505 | for i := 0; i < b.N; i++ { 506 | buf, _ = (&Call17{Name: "sel_all"}).MarshalMsg(buf[:0]) 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /call_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCall(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | tarantoolConfig := ` 14 | local s = box.schema.space.create('tester', {id = 42}) 15 | s:create_index('tester_id', { 16 | type = 'tree', 17 | parts = {1, 'NUM'} 18 | }) 19 | s:create_index('tester_name', { 20 | type = 'hash', 21 | parts = {2, 'STR'} 22 | }) 23 | s:create_index('id_name', { 24 | type = 'hash', 25 | parts = {1, 'NUM', 2, 'STR'}, 26 | unique = true 27 | }) 28 | local t = s:insert({1, 'First record'}) 29 | t = s:insert({2, 'Music'}) 30 | t = s:insert({3, 'Length', 93}) 31 | 32 | function sel_all() 33 | return box.space.tester:select({}, {iterator = "ALL"}) 34 | end 35 | 36 | function sel_name(tester_id, name) 37 | return box.space.tester.index.id_name:select{tester_id, name} 38 | end 39 | 40 | function call_case_1() 41 | return 1 42 | end 43 | 44 | function call_case_2() 45 | return 1, 2, 3 46 | end 47 | 48 | function call_case_3() 49 | return 50 | end 51 | 52 | local number_of_extra_cases = 3 53 | 54 | box.schema.func.create('sel_all', {if_not_exists = true}) 55 | box.schema.func.create('sel_name', {if_not_exists = true}) 56 | for i = 1, number_of_extra_cases do 57 | box.schema.func.create('call_case_'..i, {if_not_exists = true}) 58 | end 59 | 60 | box.schema.user.grant('guest', 'execute', 'function', 'sel_all', {if_not_exists = true}) 61 | box.schema.user.grant('guest', 'execute', 'function', 'sel_name', {if_not_exists = true}) 62 | for i = 1, number_of_extra_cases do 63 | box.schema.user.grant('guest', 'execute', 'function', 'call_case_'..i, {if_not_exists = true}) 64 | end 65 | ` 66 | 67 | box, err := NewBox(tarantoolConfig, nil) 68 | if !assert.NoError(err) { 69 | return 70 | } 71 | defer box.Close() 72 | 73 | type testParams struct { 74 | query *Call 75 | execOption ExecOption 76 | expectedData [][]interface{} 77 | expectedRawData interface{} 78 | } 79 | 80 | do := func(params *testParams) { 81 | var buf []byte 82 | 83 | conn, err := box.Connect(nil) 84 | assert.NoError(err) 85 | assert.NotNil(conn) 86 | 87 | defer conn.Close() 88 | 89 | buf, err = params.query.MarshalMsg(nil) 90 | 91 | if assert.NoError(err) { 92 | var query2 = &Call{} 93 | _, err = query2.UnmarshalMsg(buf) 94 | 95 | if assert.NoError(err) { 96 | assert.Equal(params.query.Name, query2.Name) 97 | assert.Equal(params.query.Tuple, query2.Tuple) 98 | } 99 | } 100 | 101 | var opts []ExecOption 102 | if params.execOption != nil { 103 | opts = append(opts, params.execOption) 104 | } 105 | res := conn.Exec(context.Background(), params.query, opts...) 106 | 107 | if assert.NoError(res.Error) { 108 | assert.Equal(params.expectedData, res.Data) 109 | assert.Equal(params.expectedRawData, res.RawData) 110 | } 111 | } 112 | 113 | // call sel_all without params 114 | do(&testParams{ 115 | query: &Call{ 116 | Name: "sel_all", 117 | }, 118 | expectedData: [][]interface{}{ 119 | {int64(1), "First record"}, 120 | {int64(2), "Music"}, 121 | {int64(3), "Length", int64(93)}, 122 | }, 123 | }) 124 | do(&testParams{ 125 | query: &Call{ 126 | Name: "sel_all", 127 | }, 128 | execOption: ExecResultAsDataWithFallback, 129 | expectedData: [][]interface{}{ 130 | {int64(1), "First record"}, 131 | {int64(2), "Music"}, 132 | {int64(3), "Length", int64(93)}, 133 | }, 134 | }) 135 | do(&testParams{ 136 | query: &Call{ 137 | Name: "sel_all", 138 | }, 139 | execOption: ExecResultAsRawData, 140 | expectedRawData: []interface{}{ 141 | []interface{}{int64(1), "First record"}, 142 | []interface{}{int64(2), "Music"}, 143 | []interface{}{int64(3), "Length", int64(93)}, 144 | }, 145 | }) 146 | 147 | // call sel_name with params 148 | do(&testParams{ 149 | query: &Call{ 150 | Name: "sel_name", 151 | Tuple: []interface{}{int64(2), "Music"}, 152 | }, 153 | expectedData: [][]interface{}{ 154 | {int64(2), "Music"}, 155 | }, 156 | }) 157 | do(&testParams{ 158 | query: &Call{ 159 | Name: "sel_name", 160 | Tuple: []interface{}{int64(2), "Music"}, 161 | }, 162 | execOption: ExecResultAsDataWithFallback, 163 | expectedData: [][]interface{}{ 164 | {int64(2), "Music"}, 165 | }, 166 | }) 167 | do(&testParams{ 168 | query: &Call{ 169 | Name: "sel_name", 170 | Tuple: []interface{}{int64(2), "Music"}, 171 | }, 172 | execOption: ExecResultAsRawData, 173 | expectedRawData: []interface{}{ 174 | []interface{}{int64(2), "Music"}, 175 | }, 176 | }) 177 | 178 | // scalar 1 179 | do(&testParams{ 180 | query: &Call{ 181 | Name: "call_case_1", 182 | }, 183 | expectedData: [][]interface{}{ 184 | {int64(1)}, 185 | }, 186 | }) 187 | do(&testParams{ 188 | query: &Call{ 189 | Name: "call_case_1", 190 | }, 191 | execOption: ExecResultAsDataWithFallback, 192 | expectedData: [][]interface{}{ 193 | {int64(1)}, 194 | }, 195 | }) 196 | do(&testParams{ 197 | query: &Call{ 198 | Name: "call_case_1", 199 | }, 200 | execOption: ExecResultAsRawData, 201 | expectedRawData: []interface{}{ 202 | []interface{}{int64(1)}, 203 | }, 204 | }) 205 | 206 | // multiple scalars 207 | do(&testParams{ 208 | query: &Call{ 209 | Name: "call_case_2", 210 | }, 211 | expectedData: [][]interface{}{ 212 | {int64(1)}, {int64(2)}, {int64(3)}, 213 | }, 214 | }) 215 | do(&testParams{ 216 | query: &Call{ 217 | Name: "call_case_2", 218 | }, 219 | execOption: ExecResultAsDataWithFallback, 220 | expectedData: [][]interface{}{ 221 | {int64(1)}, {int64(2)}, {int64(3)}, 222 | }, 223 | }) 224 | do(&testParams{ 225 | query: &Call{ 226 | Name: "call_case_2", 227 | }, 228 | execOption: ExecResultAsRawData, 229 | expectedRawData: []interface{}{ 230 | []interface{}{int64(1)}, 231 | []interface{}{int64(2)}, 232 | []interface{}{int64(3)}, 233 | }, 234 | }) 235 | 236 | // empty result 237 | do(&testParams{ 238 | query: &Call{ 239 | Name: "call_case_3", 240 | }, 241 | expectedData: [][]interface{}{}, 242 | }) 243 | do(&testParams{ 244 | query: &Call{ 245 | Name: "call_case_3", 246 | }, 247 | execOption: ExecResultAsDataWithFallback, 248 | expectedData: [][]interface{}{}, 249 | }) 250 | do(&testParams{ 251 | query: &Call{ 252 | Name: "call_case_3", 253 | }, 254 | execOption: ExecResultAsRawData, 255 | expectedRawData: []interface{}{}, 256 | }) 257 | } 258 | 259 | func BenchmarkCallPack(b *testing.B) { 260 | buf := make([]byte, 0) 261 | for i := 0; i < b.N; i++ { 262 | buf, _ = (&Call{Name: "sel_all"}).MarshalMsg(buf[:0]) 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /connect_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestConnect(t *testing.T) { 12 | assert := assert.New(t) 13 | require := require.New(t) 14 | 15 | box, err := NewBox("", nil) 16 | require.NoError(err) 17 | defer box.Close() 18 | 19 | conn, err := Connect(box.Addr(), nil) 20 | require.NoError(err) 21 | defer conn.Close() 22 | 23 | assert.NotEqual(conn.greeting.Version, 0) 24 | } 25 | 26 | func TestMapIndexDescription(t *testing.T) { 27 | assert := assert.New(t) 28 | require := require.New(t) 29 | config := ` 30 | local s = box.schema.space.create('tester', {id = 42}) 31 | s:create_index('tester_id', { 32 | parts = { 33 | {field = 1, type = 'number', is_nullable = false}, 34 | }, 35 | }) 36 | local t = s:insert({1}) 37 | ` 38 | box, err := NewBox(config, nil) 39 | require.NoError(err) 40 | defer box.Close() 41 | 42 | conn, err := Connect(box.Addr(), nil) 43 | require.NoError(err) 44 | defer conn.Close() 45 | 46 | pkFields, ok := conn.GetPrimaryKeyFields("tester") 47 | require.True(ok) 48 | assert.ElementsMatch(pkFields, []int{0}) 49 | } 50 | 51 | func TestDefaultSpace(t *testing.T) { 52 | assert := assert.New(t) 53 | require := require.New(t) 54 | config := ` 55 | local s = box.schema.space.create('tester', {id = 42}) 56 | s:create_index('tester_id', { 57 | type = 'hash', 58 | parts = {1, 'NUM'} 59 | }) 60 | local t = s:insert({1}) 61 | ` 62 | box, err := NewBox(config, nil) 63 | require.NoError(err) 64 | defer box.Close() 65 | 66 | conn, err := Connect(box.Addr(), &Options{ 67 | DefaultSpace: "tester", 68 | }) 69 | require.NoError(err) 70 | defer conn.Close() 71 | 72 | tuples, err := conn.Execute(&Select{ 73 | Key: 1, 74 | Index: "tester_id", 75 | }) 76 | require.NoError(err) 77 | assert.Equal([][]interface{}{{int64(1)}}, tuples) 78 | } 79 | 80 | func TestConnectOptionsDSN(t *testing.T) { 81 | assert := assert.New(t) 82 | tt := []struct { 83 | uri string 84 | user string 85 | pass string 86 | scheme string 87 | host string 88 | space string 89 | err error 90 | }{ 91 | // for backward compatibility 92 | {"unix://127.0.0.1", "", "", "tcp", "127.0.0.1", "", nil}, 93 | // scheme, host, user, pass 94 | {"tcp://127.0.0.1", "", "", "tcp", "127.0.0.1", "", nil}, 95 | {"//127.0.0.1", "", "", "tcp", "127.0.0.1", "", nil}, 96 | {"127.0.0.1", "", "", "tcp", "127.0.0.1", "", nil}, 97 | {"tcp://user:pass@127.0.0.1:8000", "user", "pass", "tcp", "127.0.0.1:8000", "", nil}, 98 | {"127.0.0.1:8000", "", "", "tcp", "127.0.0.1:8000", "", nil}, 99 | {"user:pass@127.0.0.1:8000", "user", "pass", "tcp", "127.0.0.1:8000", "", nil}, 100 | // path (defaultSpace) 101 | {"127.0.0.1/", "", "", "tcp", "127.0.0.1", "", ErrEmptyDefaultSpace}, 102 | {"127.0.0.1/tester", "", "", "tcp", "127.0.0.1", "tester", nil}, 103 | // no errors due to disabled checks 104 | {"127.0.0.1/tester/1", "", "", "tcp", "127.0.0.1", "tester/1", nil}, 105 | {"127.0.0.1/tester%20two", "", "", "tcp", "127.0.0.1", "tester two", nil}, 106 | {"127.0.0.1/tester%2Ctwo", "", "", "tcp", "127.0.0.1", "tester,two", nil}, 107 | } 108 | for tc, item := range tt { 109 | dsn, opts, err := parseOptions(item.uri, Options{}) 110 | assert.Equal(item.err, err, "case %v (err)", tc+1) 111 | if err != nil { 112 | continue 113 | } 114 | assert.Equal(item.scheme, dsn.Scheme, "case %v (scheme)", tc+1) 115 | assert.Equal(item.host, dsn.Host, "case %v (host)", tc+1) 116 | assert.Equal(item.user, opts.User, "case %v (user)", tc+1) 117 | assert.Equal(item.pass, opts.Password, "case %v (password)", tc+1) 118 | assert.Equal(item.space, opts.DefaultSpace, "case %v (space)", tc+1) 119 | } 120 | 121 | } 122 | 123 | // TestConnectionWithDefaultResultUnmarshalMode tests that 124 | // overwriting the result' unmarshal mode doesn't interferer with internal queries 125 | // like auth and schema pulling. 126 | func TestConnectionWithDefaultResultUnmarshalMode(t *testing.T) { 127 | assert := assert.New(t) 128 | require := require.New(t) 129 | 130 | config := ` 131 | local s = box.schema.space.create('tester', {id = 42}) 132 | s:create_index('tester_id', { 133 | type = 'hash', 134 | parts = {1, 'NUM'} 135 | }) 136 | local t = s:insert({33, 45}) 137 | 138 | box.schema.user.create("tester", {password = "12345678"}) 139 | box.schema.user.grant('tester', 'read', 'space', 'tester') 140 | ` 141 | 142 | box, err := NewBox(config, nil) 143 | require.NoError(err) 144 | defer box.Close() 145 | 146 | conn, err := Connect(box.Addr(), &Options{ 147 | DefaultSpace: "tester", 148 | User: "tester", 149 | Password: "12345678", 150 | ResultUnmarshalMode: ResultAsRawData, 151 | }) 152 | require.NoError(err) 153 | defer conn.Close() 154 | 155 | res := conn.Exec(context.Background(), &Select{ 156 | Key: 33, 157 | Index: "tester_id", 158 | }) 159 | require.NoError(res.Error) 160 | assert.Nil(res.Data) 161 | assert.Equal([]interface{}{[]interface{}{int64(33), int64(45)}}, res.RawData) 162 | 163 | tuples, err := conn.Execute(&Select{ 164 | Key: 33, 165 | Index: "tester_id", 166 | }) 167 | require.NoError(err) 168 | assert.Equal([][]interface{}{{int64(33), int64(45)}}, tuples) 169 | } 170 | -------------------------------------------------------------------------------- /connector.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "sync" 7 | ) 8 | 9 | type Connector struct { 10 | sync.Mutex 11 | RemoteAddr string 12 | options Options 13 | conn *Connection 14 | } 15 | 16 | // New Connector instance. 17 | func New(dsnString string, options *Options) *Connector { 18 | if options != nil { 19 | return &Connector{RemoteAddr: dsnString, options: *options} 20 | } 21 | return &Connector{RemoteAddr: dsnString} 22 | } 23 | 24 | // Connect returns existing connection or will establish another one using the provided context. 25 | func (c *Connector) ConnectContext(ctx context.Context) (conn *Connection, err error) { 26 | c.Lock() 27 | defer c.Unlock() 28 | 29 | if c.conn == nil || c.conn.IsClosed() { 30 | var dsn *url.URL 31 | dsn, c.options, err = parseOptions(c.RemoteAddr, c.options) 32 | if err != nil { 33 | return nil, err 34 | } 35 | // clear possible user:pass in order to log c.RemoteAddr securely 36 | c.RemoteAddr = dsn.Host 37 | c.conn, err = connect(ctx, dsn.Scheme, dsn.Host, c.options) 38 | } 39 | conn = c.conn 40 | 41 | return conn, err 42 | } 43 | 44 | // Connect returns existing connection or will establish another one. 45 | func (c *Connector) Connect() (conn *Connection, err error) { 46 | return c.ConnectContext(context.Background()) 47 | } 48 | 49 | // Close underlying connection. 50 | func (c *Connector) Close() { 51 | c.Lock() 52 | defer c.Unlock() 53 | if c.conn != nil && !c.conn.IsClosed() { 54 | c.conn.Close() 55 | } 56 | c.conn = nil 57 | } 58 | -------------------------------------------------------------------------------- /countio.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "expvar" 5 | "io" 6 | ) 7 | 8 | type CountedReader struct { 9 | r io.Reader 10 | c *expvar.Int 11 | } 12 | 13 | func NewCountedReader(r io.Reader, c *expvar.Int) *CountedReader { 14 | return &CountedReader{r, c} 15 | } 16 | 17 | func (cr *CountedReader) Read(p []byte) (int, error) { 18 | cr.c.Add(1) 19 | return cr.r.Read(p) 20 | } 21 | 22 | type CountedWriter struct { 23 | w io.Writer 24 | c *expvar.Int 25 | } 26 | 27 | func NewCountedWriter(w io.Writer, c *expvar.Int) *CountedWriter { 28 | return &CountedWriter{w, c} 29 | } 30 | 31 | func (cw *CountedWriter) Write(p []byte) (int, error) { 32 | cw.c.Add(1) 33 | return cw.w.Write(p) 34 | } 35 | -------------------------------------------------------------------------------- /defaults.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import "time" 4 | 5 | const ( 6 | DefaultIndex = "primary" 7 | ) 8 | 9 | var ( 10 | DefaultLimit = 250 11 | 12 | DefaultConnectTimeout = time.Second 13 | DefaultQueryTimeout = time.Second 14 | 15 | DefaultReaderBufSize = 16 * 1024 16 | DefaultWriterBufSize = 4 * 1024 17 | 18 | DefaultMaxPoolPacketSize = 64 * 1024 19 | ) 20 | -------------------------------------------------------------------------------- /delete.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tinylib/msgp/msgp" 7 | ) 8 | 9 | type Delete struct { 10 | Space interface{} 11 | Index interface{} 12 | Key interface{} 13 | KeyTuple []interface{} 14 | } 15 | 16 | var _ Query = (*Delete)(nil) 17 | 18 | func (q *Delete) GetCommandID() uint { 19 | return DeleteCommand 20 | } 21 | 22 | func (q *Delete) packMsg(data *packData, o []byte) ([]byte, error) { 23 | var err error 24 | 25 | o = msgp.AppendMapHeader(o, 3) 26 | 27 | if o, err = data.packSpace(q.Space, o); err != nil { 28 | return o, err 29 | } 30 | 31 | if o, err = data.packIndex(q.Space, q.Index, o); err != nil { 32 | return o, err 33 | } 34 | 35 | if q.Key != nil { 36 | o = append(o, data.packedSingleKey...) 37 | if o, err = msgp.AppendIntf(o, q.Key); err != nil { 38 | return o, err 39 | } 40 | } else if q.KeyTuple != nil { 41 | o = msgp.AppendUint(o, KeyKey) 42 | if o, err = msgp.AppendIntf(o, q.KeyTuple); err != nil { 43 | return o, err 44 | } 45 | } 46 | 47 | return o, nil 48 | } 49 | 50 | // MarshalMsg implements msgp.Marshaler 51 | func (q *Delete) MarshalMsg(b []byte) (data []byte, err error) { 52 | return q.packMsg(defaultPackData, b) 53 | } 54 | 55 | // UnmarshalMsg implements msgp.Unmarshaler 56 | func (q *Delete) UnmarshalMsg(data []byte) (buf []byte, err error) { 57 | var i uint32 58 | var k uint 59 | var t interface{} 60 | 61 | q.Space = nil 62 | q.Index = 0 63 | q.Key = nil 64 | q.KeyTuple = nil 65 | 66 | buf = data 67 | if i, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 68 | return 69 | } 70 | 71 | for ; i > 0; i-- { 72 | if k, buf, err = msgp.ReadUintBytes(buf); err != nil { 73 | return 74 | } 75 | 76 | switch k { 77 | case KeySpaceNo: 78 | if q.Space, buf, err = msgp.ReadUintBytes(buf); err != nil { 79 | return 80 | } 81 | case KeyIndexNo: 82 | if q.Index, buf, err = msgp.ReadUintBytes(buf); err != nil { 83 | return 84 | } 85 | case KeyKey: 86 | t, buf, err = msgp.ReadIntfBytes(buf) 87 | if q.KeyTuple = t.([]interface{}); q.KeyTuple == nil { 88 | return buf, errors.New("interface type is not []interface{}") 89 | } 90 | 91 | if len(q.KeyTuple) == 1 { 92 | q.Key = q.KeyTuple[0] 93 | q.KeyTuple = nil 94 | } 95 | } 96 | } 97 | 98 | if q.Space == nil { 99 | return buf, errors.New("Delete.Unpack: no space specified") 100 | } 101 | if q.Key == nil && q.KeyTuple == nil { 102 | return buf, errors.New("Delete.Unpack: no tuple specified") 103 | } 104 | 105 | return 106 | } 107 | -------------------------------------------------------------------------------- /delete_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDelete(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | tarantoolConfig := ` 13 | local s = box.schema.space.create('tester', {id = 42}) 14 | s:create_index('primary', { 15 | type = 'hash', 16 | parts = {1, 'NUM'} 17 | }) 18 | 19 | s = box.schema.space.create('tester2', {id = 43}) 20 | s:create_index('primary', { 21 | type = 'tree', 22 | parts = {1, 'NUM', 2, 'STR'}, 23 | unique = true 24 | }) 25 | 26 | box.schema.user.create('writer', {password = 'writer'}) 27 | box.schema.user.grant('writer', 'read,write', 'space', 'tester') 28 | box.schema.user.grant('writer', 'read,write', 'space', 'tester2') 29 | ` 30 | 31 | box, err := NewBox(tarantoolConfig, nil) 32 | if !assert.NoError(err) { 33 | return 34 | } 35 | defer box.Close() 36 | 37 | conn, err := box.Connect(&Options{ 38 | User: "writer", 39 | Password: "writer", 40 | }) 41 | assert.NoError(err) 42 | assert.NotNil(conn) 43 | 44 | defer conn.Close() 45 | 46 | do := func(query *Delete) ([][]interface{}, error) { 47 | var err error 48 | var buf []byte 49 | 50 | buf, err = query.packMsg(conn.packData, buf) 51 | 52 | if assert.NoError(err) { 53 | var query2 = &Delete{} 54 | _, err = query2.UnmarshalMsg(buf) 55 | 56 | if assert.NoError(err) { 57 | switch query.Space.(string) { 58 | case "tester": 59 | assert.Equal(uint(42), query2.Space) 60 | case "tester2": 61 | assert.Equal(uint(43), query2.Space) 62 | } 63 | 64 | if query.Key != nil { 65 | assert.Equal(query.Key, query2.Key) 66 | } 67 | if query.KeyTuple != nil { 68 | assert.Equal(query.KeyTuple, query2.KeyTuple) 69 | } 70 | } 71 | } 72 | 73 | return conn.Execute(query) 74 | } 75 | 76 | _, err = conn.Execute(&Replace{ 77 | Space: "tester", 78 | Tuple: []interface{}{int64(4), "Hello"}, 79 | }) 80 | 81 | assert.NoError(err) 82 | 83 | data, err := do(&Delete{ 84 | Space: "tester", 85 | Key: int64(4), 86 | }) 87 | 88 | if assert.NoError(err) { 89 | assert.Equal([][]interface{}{ 90 | { 91 | int64(4), 92 | "Hello", 93 | }, 94 | }, data) 95 | } 96 | 97 | data, err = conn.Execute(&Select{ 98 | Space: "tester", 99 | KeyTuple: []interface{}{int64(4)}, 100 | }) 101 | if assert.NoError(err) { 102 | assert.Equal([][]interface{}{}, data) 103 | } 104 | 105 | _, err = conn.Execute(&Replace{ 106 | Space: "tester2", 107 | Tuple: []interface{}{int64(4), "World"}, 108 | }) 109 | 110 | assert.NoError(err) 111 | 112 | data, err = do(&Delete{ 113 | Space: "tester2", 114 | KeyTuple: []interface{}{int64(4), "World"}, 115 | }) 116 | 117 | if assert.NoError(err) { 118 | assert.Equal([][]interface{}{ 119 | { 120 | int64(4), 121 | "World", 122 | }, 123 | }, data) 124 | } 125 | 126 | data, err = conn.Execute(&Select{ 127 | Space: "tester2", 128 | KeyTuple: []interface{}{int64(4), "World"}, 129 | }) 130 | if assert.NoError(err) { 131 | assert.Equal([][]interface{}{}, data) 132 | } 133 | 134 | _, err = do(&Delete{ 135 | Space: "tester2", 136 | KeyTuple: []interface{}{int64(4), "World"}, 137 | }) 138 | 139 | assert.NoError(err) 140 | } 141 | 142 | func BenchmarkDeletePack(b *testing.B) { 143 | buf := make([]byte, 0) 144 | for i := 0; i < b.N; i++ { 145 | buf, _ = (&Delete{KeyTuple: []interface{}{3, "Hello world"}}).MarshalMsg(buf[:0]) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | var ( 10 | // ErrNotSupported is returned when an unimplemented query type or operation is encountered. 11 | ErrNotSupported = NewQueryError(ErrUnsupported, "not supported yet") 12 | // ErrNotInReplicaSet means that join operation can not be performed on a replica set due to missing parameters. 13 | ErrNotInReplicaSet = NewQueryError(0, "Full Replica Set params hasn't been set") 14 | // ErrBadResult means that query result was of invalid type or length. 15 | ErrBadResult = NewQueryError(0, "invalid result") 16 | // ErrVectorClock is returns in case of bad manipulation with vector clock. 17 | ErrVectorClock = NewQueryError(0, "vclock manipulation") 18 | // ErrUnknownError is returns when ErrorCode isn't OK but Error is nil in Result. 19 | ErrUnknownError = NewQueryError(ErrUnknown, "unknown error") 20 | // ErrOldVersionAnon is returns when tarantool version doesn't support anonymous replication. 21 | ErrOldVersionAnon = errors.New("tarantool version is too old for anonymous replication. Min version is 2.3.1") 22 | 23 | // ErrConnectionClosed returns when connection is no longer alive. 24 | ErrConnectionClosed = errors.New("connection closed") 25 | ) 26 | 27 | // Error has Temporary method which returns true if error is temporary. 28 | // It is useful to quickly decide retry or not retry. 29 | type Error interface { 30 | error 31 | Temporary() bool // Temporary true if the error is temporary 32 | } 33 | 34 | // ConnectionError is returned when something have been happened with connection. 35 | type ConnectionError struct { 36 | error 37 | } 38 | 39 | // NewConnectionError returns ConnectionError, which contains wrapped with remoteAddr error. 40 | func NewConnectionError(con *Connection, err error) *ConnectionError { 41 | return &ConnectionError{ 42 | error: fmt.Errorf("%w, remote: %s", err, con.remoteAddr), 43 | } 44 | } 45 | 46 | // ConnectionClosedError returns ConnectionError with message about closed connection 47 | // or error depending on the connection state. It is also has remoteAddr in error text. 48 | func ConnectionClosedError(con *Connection) *ConnectionError { 49 | var err = ErrConnectionClosed 50 | if connErr := con.getError(); connErr != nil { 51 | err = fmt.Errorf("%w: %v", err, connErr.Error()) 52 | } 53 | return NewConnectionError(con, err) 54 | } 55 | 56 | // Temporary implements Error interface. 57 | func (e *ConnectionError) Temporary() bool { 58 | return !errors.Is(e.error, ErrConnectionClosed) 59 | } 60 | 61 | // Timeout implements net.Error interface. 62 | func (e *ConnectionError) Timeout() bool { 63 | return false 64 | } 65 | 66 | func (e *ConnectionError) Unwrap() error { 67 | return e.error 68 | } 69 | 70 | // ContextError is returned when request has been ended with context timeout or cancel. 71 | type ContextError struct { 72 | error 73 | CtxErr error 74 | } 75 | 76 | // NewContextError returns ContextError with message and remoteAddr in error text. 77 | // It is also has context error itself in CtxErr. 78 | func NewContextError(ctx context.Context, con *Connection, message string) *ContextError { 79 | return &ContextError{ 80 | error: fmt.Errorf("%s: %s, remote: %s", message, ctx.Err(), con.remoteAddr), 81 | CtxErr: ctx.Err(), 82 | } 83 | } 84 | 85 | // Temporary implements Error interface. 86 | func (e *ContextError) Temporary() bool { 87 | return true 88 | } 89 | 90 | // Timeout implements net.Error interface. 91 | func (e *ContextError) Timeout() bool { 92 | return e.CtxErr == context.DeadlineExceeded 93 | } 94 | 95 | func (e *ContextError) Unwrap() error { 96 | return e.CtxErr 97 | } 98 | 99 | // QueryError is returned when query error has been happened. 100 | // It has error Code. 101 | type QueryError struct { 102 | error 103 | Code uint 104 | } 105 | 106 | // NewQueryError returns QueryError with message and Code. 107 | func NewQueryError(code uint, message string) *QueryError { 108 | return &QueryError{ 109 | Code: code, 110 | error: errors.New(message), 111 | } 112 | } 113 | 114 | // Temporary implements Error interface. 115 | func (e *QueryError) Temporary() bool { 116 | return false 117 | } 118 | 119 | // Timeout implements net.Error interface. 120 | func (e *QueryError) Timeout() bool { 121 | return false 122 | } 123 | 124 | func (e *QueryError) Unwrap() error { 125 | return e.error 126 | } 127 | 128 | // UnexpectedReplicaSetUUIDError is returned when ReplicaSetUUID set in Options.ReplicaSetUUID is not equal to ReplicaSetUUID 129 | // received during Join or JoinWithSnap. It is only an AnonSlave error! 130 | type UnexpectedReplicaSetUUIDError struct { 131 | QueryError 132 | Expected string 133 | Got string 134 | } 135 | 136 | // NewUnexpectedReplicaSetUUIDError returns UnexpectedReplicaSetUUIDError. 137 | func NewUnexpectedReplicaSetUUIDError(expected string, got string) *UnexpectedReplicaSetUUIDError { 138 | return &UnexpectedReplicaSetUUIDError{ 139 | QueryError: *NewQueryError(ErrClusterIDMismatch, fmt.Sprintf("Replica set UUID mismatch: expected %v, got %v", expected, got)), 140 | Expected: expected, 141 | Got: got, 142 | } 143 | } 144 | 145 | // Is for errors comparison 146 | func (e *UnexpectedReplicaSetUUIDError) Is(target error) bool { 147 | _, ok := target.(*UnexpectedReplicaSetUUIDError) 148 | return ok 149 | } 150 | 151 | func (e *UnexpectedReplicaSetUUIDError) Unwrap() error { 152 | return e.QueryError 153 | } 154 | 155 | var _ Error = (*ConnectionError)(nil) 156 | var _ Error = (*QueryError)(nil) 157 | var _ Error = (*ContextError)(nil) 158 | var _ Error = (*UnexpectedReplicaSetUUIDError)(nil) 159 | -------------------------------------------------------------------------------- /eval.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tinylib/msgp/msgp" 7 | ) 8 | 9 | // Eval query 10 | type Eval struct { 11 | Expression string 12 | Tuple []interface{} 13 | } 14 | 15 | var _ Query = (*Eval)(nil) 16 | 17 | func (q *Eval) GetCommandID() uint { 18 | return EvalCommand 19 | } 20 | 21 | // MarshalMsg implements msgp.Marshaler 22 | func (q *Eval) MarshalMsg(b []byte) (o []byte, err error) { 23 | o = b 24 | o = msgp.AppendMapHeader(o, 2) 25 | 26 | o = msgp.AppendUint(o, KeyExpression) 27 | o = msgp.AppendString(o, q.Expression) 28 | 29 | if q.Tuple == nil { 30 | o = msgp.AppendUint(o, KeyTuple) 31 | o = msgp.AppendArrayHeader(o, 0) 32 | } else { 33 | o = msgp.AppendUint(o, KeyTuple) 34 | if o, err = msgp.AppendIntf(o, q.Tuple); err != nil { 35 | return o, err 36 | } 37 | } 38 | 39 | return o, nil 40 | } 41 | 42 | // UnmarshalMsg implements msgp.Unmarshaler 43 | func (q *Eval) UnmarshalMsg(data []byte) (buf []byte, err error) { 44 | var i uint32 45 | var k uint 46 | var t interface{} 47 | 48 | buf = data 49 | if i, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 50 | return 51 | } 52 | 53 | if i != 2 { 54 | return buf, errors.New("Eval.Unpack: expected map of length 2") 55 | } 56 | 57 | for ; i > 0; i-- { 58 | if k, buf, err = msgp.ReadUintBytes(buf); err != nil { 59 | return 60 | } 61 | 62 | switch k { 63 | case KeyExpression: 64 | if q.Expression, buf, err = msgp.ReadStringBytes(buf); err != nil { 65 | return 66 | } 67 | case KeyTuple: 68 | t, buf, err = msgp.ReadIntfBytes(buf) 69 | if q.Tuple = t.([]interface{}); q.Tuple == nil { 70 | return buf, errors.New("interface type is not []interface{}") 71 | } 72 | if len(q.Tuple) == 0 { 73 | q.Tuple = nil 74 | } 75 | } 76 | } 77 | return 78 | } 79 | -------------------------------------------------------------------------------- /eval_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func schemeGrantUserEval(username string) string { 13 | scheme := ` 14 | box.schema.user.grant('{username}', 'execute', 'universe') 15 | ` 16 | return strings.Replace(scheme, "{username}", username, -1) 17 | } 18 | 19 | func TestEvalPackUnpack(t *testing.T) { 20 | q := &Eval{Expression: "return 2+2", Tuple: []interface{}{"test"}} 21 | // check unpack 22 | buf, err := q.MarshalMsg(nil) 23 | require.NoError(t, err) 24 | 25 | qa := &Eval{} 26 | _, err = qa.UnmarshalMsg(buf) 27 | require.NoError(t, err) 28 | assert.Equal(t, q, qa) 29 | } 30 | 31 | func TestEvalExecute(t *testing.T) { 32 | require := require.New(t) 33 | assert := assert.New(t) 34 | 35 | user := "guest" 36 | config := schemeGrantUserEval(user) 37 | expr := "local arg = {...} return box.cfg.listen, box.session.user(), arg[1], arg[2], arg" 38 | args := []interface{}{"one", "two"} 39 | q := &Eval{Expression: expr, Tuple: args} 40 | 41 | box, err := NewBox(config, &BoxOptions{}) 42 | require.NoError(err) 43 | defer box.Close() 44 | 45 | tnt, err := Connect(box.Listen, &Options{}) 46 | require.NoError(err) 47 | 48 | data, err := tnt.Execute(q) 49 | require.NoError(err) 50 | require.Len(data, 5) 51 | assert.EqualValues(box.Listen, data[0][0]) 52 | assert.EqualValues(user, data[1][0]) 53 | assert.EqualValues(args[0], data[2][0]) 54 | assert.EqualValues(args[1], data[3][0]) 55 | assert.EqualValues(args, data[4]) 56 | 57 | res := tnt.Exec(context.Background(), q, ExecResultAsDataWithFallback) 58 | require.NoError(res.Error) 59 | require.Nil(res.Data) 60 | assert.Equal(res.RawData, []interface{}{ 61 | box.Listen, user, args[0], args[1], args, 62 | }) 63 | } 64 | 65 | func BenchmarkEvalPack(b *testing.B) { 66 | buf := make([]byte, 0) 67 | for i := 0; i < b.N; i++ { 68 | buf, _ = (&Eval{Expression: "return 2+2"}).MarshalMsg(buf[:0]) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /execute.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type ExecOption interface { 8 | apply(*request) 9 | } 10 | 11 | type opaqueOption struct { 12 | opaque interface{} 13 | } 14 | 15 | func (o *opaqueOption) apply(r *request) { 16 | r.opaque = o.opaque 17 | } 18 | 19 | func OpaqueExecOption(opaque interface{}) ExecOption { 20 | return &opaqueOption{opaque: opaque} 21 | } 22 | 23 | type resultModeOption struct { 24 | resultMode resultUnmarshalMode 25 | } 26 | 27 | func (o *resultModeOption) apply(r *request) { 28 | r.resultMode = o.resultMode 29 | } 30 | 31 | func ResultModeExecOption(mode resultUnmarshalMode) ExecOption { 32 | return &resultModeOption{mode} 33 | } 34 | 35 | var ( 36 | ExecResultAsRawData = ResultModeExecOption(ResultAsRawData) 37 | ExecResultAsDataWithFallback = ResultModeExecOption(ResultAsDataWithFallback) 38 | ) 39 | 40 | // the Result type is used to return write errors here 41 | func (conn *Connection) writeRequest(ctx context.Context, request *request, q Query) (*request, *Result, uint64) { 42 | var err error 43 | 44 | requestID := conn.nextID() 45 | 46 | pp := packetPool.GetWithID(requestID) 47 | 48 | if err = pp.packMsg(q, conn.packData); err != nil { 49 | return nil, &Result{ 50 | Error: NewQueryError(ErrInvalidMsgpack, err.Error()), 51 | ErrorCode: ErrInvalidMsgpack, 52 | }, 0 53 | } 54 | 55 | request.packet = pp 56 | 57 | if oldRequest := conn.requests.Put(requestID, request); oldRequest != nil { 58 | select { 59 | case oldRequest.replyChan <- &AsyncResult{ 60 | Error: ConnectionClosedError(conn), 61 | ErrorCode: ErrNoConnection, 62 | Opaque: oldRequest.opaque, 63 | }: 64 | default: 65 | } 66 | } 67 | 68 | writeChan := conn.writeChan 69 | if writeChan == nil { 70 | r := conn.requests.Pop(requestID) 71 | requestPool.Put(r) 72 | conn.releasePacket(pp) 73 | return nil, &Result{ 74 | Error: ConnectionClosedError(conn), 75 | ErrorCode: ErrNoConnection, 76 | }, 0 77 | } 78 | 79 | select { 80 | case writeChan <- request: 81 | case <-ctx.Done(): 82 | if conn.perf.QueryTimeouts != nil && ctx.Err() == context.DeadlineExceeded { 83 | conn.perf.QueryTimeouts.Add(1) 84 | } 85 | r := conn.requests.Pop(requestID) 86 | requestPool.Put(r) 87 | conn.releasePacket(pp) 88 | return nil, &Result{ 89 | Error: NewContextError(ctx, conn, "Send error"), 90 | ErrorCode: ErrTimeout, 91 | }, 0 92 | case <-conn.exit: 93 | return nil, &Result{ 94 | Error: ConnectionClosedError(conn), 95 | ErrorCode: ErrNoConnection, 96 | }, 0 97 | } 98 | 99 | return request, nil, requestID 100 | } 101 | 102 | func (conn *Connection) readResult(ctx context.Context, arc chan *AsyncResult, requestID uint64) *AsyncResult { 103 | select { 104 | case ar := <-arc: 105 | if ar == nil { 106 | return &AsyncResult{ 107 | Error: ConnectionClosedError(conn), 108 | ErrorCode: ErrNoConnection, 109 | } 110 | } 111 | return ar 112 | case <-ctx.Done(): 113 | if conn.perf.QueryTimeouts != nil && ctx.Err() == context.DeadlineExceeded { 114 | conn.perf.QueryTimeouts.Add(1) 115 | } 116 | r := conn.requests.Pop(requestID) 117 | requestPool.Put(r) 118 | return &AsyncResult{ 119 | Error: NewContextError(ctx, conn, "Recv error"), 120 | ErrorCode: ErrTimeout, 121 | } 122 | case <-conn.exit: 123 | return &AsyncResult{ 124 | Error: ConnectionClosedError(conn), 125 | ErrorCode: ErrNoConnection, 126 | } 127 | } 128 | } 129 | 130 | func (conn *Connection) Exec(ctx context.Context, q Query, options ...ExecOption) (result *Result) { 131 | var cancel context.CancelFunc = func() {} 132 | var requestID uint64 133 | var rerr *Result 134 | 135 | if conn.queryTimeout != 0 { 136 | ctx, cancel = context.WithTimeout(ctx, conn.queryTimeout) 137 | } 138 | 139 | replyChan := make(chan *AsyncResult, 1) 140 | 141 | request := requestPool.Get() 142 | request.replyChan = replyChan 143 | request.resultMode = conn.resultUnmarshalMode // could also by overwritten by options 144 | for i := 0; i < len(options); i++ { 145 | options[i].apply(request) 146 | } 147 | 148 | if _, rerr, requestID = conn.writeRequest(ctx, request, q); rerr != nil { 149 | cancel() 150 | return rerr 151 | } 152 | 153 | ar := conn.readResult(ctx, replyChan, requestID) 154 | cancel() 155 | 156 | if rerr := ar.Error; rerr != nil { 157 | return &Result{ 158 | Error: rerr, 159 | ErrorCode: ar.ErrorCode, 160 | } 161 | } 162 | 163 | pp := ar.BinaryPacket 164 | if pp == nil { 165 | return &Result{ 166 | Error: ConnectionClosedError(conn), 167 | ErrorCode: ErrNoConnection, 168 | } 169 | } 170 | 171 | if err := pp.Unmarshal(); err != nil { 172 | result = &Result{ 173 | Error: err, 174 | ErrorCode: ErrInvalidMsgpack, 175 | } 176 | } else { 177 | result = pp.Result() 178 | if result == nil { 179 | result = &Result{} 180 | } 181 | } 182 | pp.Release() 183 | 184 | return result 185 | } 186 | 187 | func (conn *Connection) ExecAsync( 188 | ctx context.Context, 189 | q Query, 190 | opaque interface{}, 191 | replyChan chan *AsyncResult, 192 | options ...ExecOption, 193 | ) error { 194 | var rerr *Result 195 | 196 | request := requestPool.Get() 197 | request.opaque = opaque 198 | request.replyChan = replyChan 199 | request.resultMode = conn.resultUnmarshalMode // could also by overwritten by options 200 | for i := 0; i < len(options); i++ { 201 | options[i].apply(request) 202 | } 203 | 204 | if _, rerr, _ = conn.writeRequest(ctx, request, q); rerr != nil { 205 | return rerr.Error 206 | } 207 | return nil 208 | } 209 | 210 | func (conn *Connection) Execute(q Query) ([][]interface{}, error) { 211 | res := conn.Exec(context.Background(), q, ResultModeExecOption(ResultDefaultMode)) 212 | return res.Data, res.Error 213 | } 214 | -------------------------------------------------------------------------------- /fetch_snapshot.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import "github.com/tinylib/msgp/msgp" 4 | 5 | // FetchSnapshot is the FETCH_SNAPSHOT command 6 | type FetchSnapshot struct{} 7 | 8 | var _ Query = (*FetchSnapshot)(nil) 9 | 10 | func (q *FetchSnapshot) GetCommandID() uint { 11 | return FetchSnapshotCommand 12 | } 13 | 14 | // MarshalMsg implements msgp.Marshaler 15 | func (q *FetchSnapshot) MarshalMsg(b []byte) (o []byte, err error) { 16 | o = b 17 | o = msgp.AppendMapHeader(o, 1) 18 | o = msgp.AppendUint(o, KeyVersionID) 19 | o = msgp.AppendUint(o, uint(version2_9_0)) 20 | return o, nil 21 | } 22 | 23 | // UnmarshalMsg implements msgp.Unmarshaler 24 | func (q *FetchSnapshot) UnmarshalMsg([]byte) (buf []byte, err error) { 25 | return buf, ErrNotSupported 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/viciious/go-tarantool 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/google/uuid v1.3.0 7 | github.com/klauspost/compress v1.11.3 8 | github.com/philhofer/fwd v1.0.0 // indirect 9 | github.com/stretchr/testify v1.6.2-0.20201103103935-92707c0b2d50 10 | github.com/tinylib/msgp v1.0.3-0.20180215042507-3b5c87ab5fb0 11 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /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/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 4 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/klauspost/compress v1.11.3 h1:dB4Bn0tN3wdCzQxnS8r06kV74qN/TAfaIS0bVE8h3jc= 6 | github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 7 | github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= 8 | github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/testify v1.6.2-0.20201103103935-92707c0b2d50 h1:aQdElrdadJZjGar4PipPBSpVh3yyDIuDSaM5PbMn6o8= 13 | github.com/stretchr/testify v1.6.2-0.20201103103935-92707c0b2d50/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | github.com/tinylib/msgp v1.0.3-0.20180215042507-3b5c87ab5fb0 h1:jDyj19S33TMjgae4Wph79yWyiqhtMrNPO51rd2x/DwQ= 15 | github.com/tinylib/msgp v1.0.3-0.20180215042507-3b5c87ab5fb0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 20 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /insert.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tinylib/msgp/msgp" 7 | ) 8 | 9 | type Insert struct { 10 | Space interface{} 11 | Tuple []interface{} 12 | } 13 | 14 | var _ Query = (*Insert)(nil) 15 | 16 | func (q *Insert) GetCommandID() uint { 17 | return InsertCommand 18 | } 19 | 20 | func (q *Insert) packMsg(data *packData, b []byte) (o []byte, err error) { 21 | if q.Tuple == nil { 22 | return o, errors.New("Tuple can not be nil") 23 | } 24 | 25 | o = b 26 | o = msgp.AppendMapHeader(o, 2) 27 | 28 | if o, err = data.packSpace(q.Space, o); err != nil { 29 | return o, err 30 | } 31 | 32 | o = msgp.AppendUint(o, KeyTuple) 33 | return msgp.AppendIntf(o, q.Tuple) 34 | } 35 | 36 | // MarshalMsg implements msgp.Marshaler 37 | func (q *Insert) MarshalMsg(b []byte) (data []byte, err error) { 38 | return q.packMsg(defaultPackData, b) 39 | } 40 | 41 | // UnmarshalMsg implements msgp.Unmarshaler 42 | func (q *Insert) UnmarshalMsg(data []byte) (buf []byte, err error) { 43 | var i uint32 44 | var k uint 45 | var t interface{} 46 | 47 | q.Space = nil 48 | q.Tuple = nil 49 | 50 | buf = data 51 | if i, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 52 | return 53 | } 54 | 55 | for ; i > 0; i-- { 56 | if k, buf, err = msgp.ReadUintBytes(buf); err != nil { 57 | return 58 | } 59 | 60 | switch k { 61 | case KeySpaceNo: 62 | if q.Space, buf, err = msgp.ReadUintBytes(buf); err != nil { 63 | return 64 | } 65 | case KeyTuple: 66 | t, buf, err = msgp.ReadIntfBytes(buf) 67 | if q.Tuple = t.([]interface{}); q.Tuple == nil { 68 | return buf, errors.New("interface type is not []interface{}") 69 | } 70 | } 71 | } 72 | 73 | if q.Space == nil { 74 | return buf, errors.New("Insert.Unpack: no space specified") 75 | } 76 | if q.Tuple == nil { 77 | return buf, errors.New("Insert.Unpack: no tuple specified") 78 | } 79 | 80 | return 81 | } 82 | -------------------------------------------------------------------------------- /insert_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestInsert(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | tarantoolConfig := ` 13 | local s = box.schema.space.create('tester', {id = 42}) 14 | s:create_index('primary', { 15 | type = 'hash', 16 | parts = {1, 'NUM'} 17 | }) 18 | 19 | box.schema.user.create('writer', {password = 'writer'}) 20 | box.schema.user.grant('writer', 'write', 'space', 'tester') 21 | ` 22 | 23 | box, err := NewBox(tarantoolConfig, nil) 24 | if !assert.NoError(err) { 25 | return 26 | } 27 | defer box.Close() 28 | 29 | conn, err := box.Connect(&Options{ 30 | User: "writer", 31 | Password: "writer", 32 | }) 33 | assert.NoError(err) 34 | assert.NotNil(conn) 35 | 36 | defer conn.Close() 37 | 38 | do := func(query *Insert) ([][]interface{}, error) { 39 | var err error 40 | var buf []byte 41 | 42 | buf, err = query.packMsg(conn.packData, buf) 43 | 44 | if assert.NoError(err) { 45 | var query2 = &Insert{} 46 | _, err = query2.UnmarshalMsg(buf) 47 | 48 | if assert.NoError(err) { 49 | assert.Equal(uint(42), query2.Space) 50 | assert.Equal(query.Tuple, query2.Tuple) 51 | } else { 52 | return nil, err 53 | } 54 | } else { 55 | return nil, err 56 | } 57 | 58 | return conn.Execute(query) 59 | } 60 | 61 | data, err := do(&Insert{ 62 | Space: "tester", 63 | Tuple: []interface{}{int64(4), "Hello"}, 64 | }) 65 | 66 | if assert.NoError(err) { 67 | assert.Equal([][]interface{}{{int64(4), "Hello"}}, data) 68 | } 69 | 70 | _, err = do(&Insert{ 71 | Space: "tester", 72 | Tuple: []interface{}{int64(4), "World"}, 73 | }) 74 | 75 | if assert.Error(err) { 76 | assert.Contains(err.Error(), "Duplicate key exists") 77 | } 78 | } 79 | 80 | func BenchmarkInsertPack(b *testing.B) { 81 | buf := make([]byte, 0) 82 | for i := 0; i < b.N; i++ { 83 | buf, _ = (&Insert{Tuple: []interface{}{3, "Hello world"}}).MarshalMsg(buf[:0]) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /iterator.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | type Iterator struct { 4 | Iter uint8 5 | } 6 | 7 | func (it Iterator) String() string { 8 | switch it.Iter { 9 | case IterEq: 10 | return "EQ" 11 | case IterReq: 12 | return "REQ" 13 | case IterAll: 14 | return "ALL" 15 | case IterLt: 16 | return "LT" 17 | case IterLe: 18 | return "LE" 19 | case IterGe: 20 | return "GE" 21 | case IterGt: 22 | return "GT" 23 | case IterBitsAllSet: 24 | return "BITS_ALL_SET" 25 | case IterBitsAnySet: 26 | return "BITS_ANY_SET" 27 | case IterBitsAllNotSet: 28 | return "BITS_ALL_NOT_SET" 29 | } 30 | return "ER" 31 | } 32 | -------------------------------------------------------------------------------- /join.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "github.com/tinylib/msgp/msgp" 5 | ) 6 | 7 | // Join is the JOIN command 8 | type Join struct { 9 | UUID string 10 | } 11 | 12 | var _ Query = (*Join)(nil) 13 | 14 | func (q *Join) GetCommandID() uint { 15 | return JoinCommand 16 | } 17 | 18 | // MarshalMsg implements msgp.Marshaler 19 | func (q *Join) MarshalMsg(b []byte) (o []byte, err error) { 20 | o = b 21 | o = msgp.AppendMapHeader(o, 1) 22 | o = msgp.AppendUint(o, KeyInstanceUUID) 23 | o = msgp.AppendString(o, q.UUID) 24 | return o, nil 25 | } 26 | 27 | // UnmarshalMsg implements msgp.Unmarshaler 28 | func (q *Join) UnmarshalMsg([]byte) (buf []byte, err error) { 29 | return buf, ErrNotSupported 30 | } 31 | -------------------------------------------------------------------------------- /lastsnapvclock_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestLastSnapVClock(t *testing.T) { 13 | require := require.New(t) 14 | 15 | guest, role, luaDir := "guest", "replication", "lua" 16 | luaInit, err := os.ReadFile(filepath.Join("testdata", "init.lua")) 17 | require.NoError(err) 18 | config := string(luaInit) 19 | config += schemeNewReplicator(tnt16User, tnt16Pass) 20 | config += schemeGrantRoleFunc(role, procLUALastSnapVClock) 21 | config += schemeGrantRoleFunc(role, "readfile") 22 | config += schemeGrantRoleFunc(role, "lastsnapfilename") 23 | config += schemeGrantRoleFunc(role, "parsevclock") 24 | // for making snapshot 25 | config += schemeGrantUserEval(guest) 26 | 27 | box, err := NewBox(config, &BoxOptions{WorkDir: luaDir}) 28 | require.NoError(err) 29 | defer box.Close() 30 | 31 | // add replica to replica set 32 | s, err := NewSlave(box.Listen, Options{User: tnt16User, Password: tnt16Pass}) 33 | require.NoError(err) 34 | defer s.Close() 35 | if s.Version() > version1_7_0 { 36 | t.Skip("LastSnapVClock is depricated for tarantools above 1.7.0") 37 | } 38 | err = s.Join() 39 | require.NoError(err) 40 | 41 | // make snapshot 42 | tnt, err := Connect(box.Listen, &Options{}) 43 | require.NoError(err) 44 | defer tnt.Close() 45 | makesnapshot := &Eval{Expression: luaMakeSnapshot} 46 | res, err := tnt.Execute(makesnapshot) 47 | require.NoError(err) 48 | require.Empty(res, 0, "response to make snapshot request contains error") 49 | tnt.Close() 50 | 51 | // test each lua func separately 52 | t.Run(procLUALastSnapVClock, SubTestVClockSelf(box)) 53 | t.Run("readfile", SubTestVClockReadFile(box)) 54 | t.Run("lastsnapfilename", SubTestVClockLastSnapFilename(box)) 55 | t.Run("parsevclock", SubTestVClockParseVClock(box)) 56 | } 57 | 58 | func SubTestVClockSelf(box *Box) func(t *testing.T) { 59 | return func(t *testing.T) { 60 | lastsnapvclock := &Call{Name: procLUALastSnapVClock} 61 | tnt, err := Connect(box.Listen, &Options{User: tnt16User, Password: tnt16Pass}) 62 | require.NoError(t, err, "connect") 63 | res, err := tnt.Execute(lastsnapvclock) 64 | require.NoError(t, err, "exec") 65 | require.NotEmpty(t, res, "result [][]interface is empty") 66 | require.Len(t, res[0], 2, "vector clock should contain two clocks") 67 | switch res := res[0][0].(type) { 68 | case int64: 69 | require.True(t, res > 0, "master clock (result[0][0]) should be greater than zero") 70 | default: 71 | t.Fatalf("NaN master clock: %#v (%T)", res, res) 72 | } 73 | switch res := res[0][1].(type) { 74 | case int64: 75 | require.True(t, res == 0, "replica clock (result[0][1]) should be zero") 76 | default: 77 | t.Fatalf("NaN master clock: %#v (%T)", res, res) 78 | } 79 | } 80 | } 81 | 82 | func SubTestVClockReadFile(box *Box) func(t *testing.T) { 83 | return func(t *testing.T) { 84 | tnt, err := Connect(box.Listen, &Options{User: tnt16User, Password: tnt16Pass}) 85 | require.NoError(t, err, "connect") 86 | luaProc := &Call{Name: "readfile"} 87 | 88 | luaProc.Tuple = []interface{}{"notexist.snap", 255} 89 | res, err := tnt.Execute(luaProc) 90 | require.NoError(t, err, "exec") 91 | 92 | require.Len(t, res, 2, "should be data tuple and error tuple in result") 93 | require.NotEmpty(t, res[0], "data tuple") 94 | require.Nil(t, res[0][0], "data should be nil") 95 | require.NotEmpty(t, res[1], "error tuple") 96 | require.Contains(t, res[1][0], "such file", "err should be about file") 97 | 98 | luaProc.Tuple = []interface{}{"tarantool_lastsnapvclock.lua", 255} 99 | res, err = tnt.Execute(luaProc) 100 | require.NoError(t, err, "exec") 101 | 102 | require.Len(t, res, 2, "should be data tuple and error tuple in result") 103 | require.NotEmpty(t, res[0], "data tuple") 104 | require.Contains(t, res[0][0], "VERSION", "data should contain vclock") 105 | require.NotEmpty(t, res[1], "error tuple") 106 | require.Nil(t, res[1][0], "err should be nil") 107 | } 108 | } 109 | 110 | func SubTestVClockLastSnapFilename(box *Box) func(t *testing.T) { 111 | return func(t *testing.T) { 112 | tnt, err := Connect(box.Listen, &Options{User: tnt16User, Password: tnt16Pass}) 113 | require.NoError(t, err, "connect") 114 | luaProc := &Call{Name: "lastsnapfilename"} 115 | 116 | res, err := tnt.Execute(luaProc) 117 | require.NoError(t, err, "exec") 118 | 119 | require.NotEmpty(t, res) 120 | require.NotEmpty(t, res[0]) 121 | require.IsType(t, "", res[0][0]) 122 | realsnap := res[0][0].(string) 123 | t.Logf("Real snapshot: %v", realsnap) 124 | 125 | fakesnapfile, err := os.Create(filepath.Join(box.Root, "snap", "10000000000000000000.snap")) 126 | require.NoError(t, err) 127 | fakesnapfile.Close() 128 | defer os.Remove(fakesnapfile.Name()) 129 | t.Logf("Create fake snapshot: %v", fakesnapfile.Name()) 130 | 131 | res, err = tnt.Execute(luaProc) 132 | require.NoError(t, err, "exec") 133 | 134 | require.NotEmpty(t, res) 135 | require.NotEmpty(t, res[0]) 136 | require.IsType(t, "", res[0][0]) 137 | fakesnap := res[0][0].(string) 138 | t.Logf("Fake snapshot: %v", fakesnap) 139 | 140 | require.Equal(t, fakesnapfile.Name(), fakesnap) 141 | require.NotEqual(t, realsnap, fakesnap) 142 | } 143 | } 144 | 145 | func SubTestVClockParseVClock(box *Box) func(t *testing.T) { 146 | return func(t *testing.T) { 147 | tnt, err := Connect(box.Listen, &Options{User: tnt16User, Password: tnt16Pass}) 148 | require.NoError(t, err, "connect") 149 | luaProc := &Call{Name: "parsevclock"} 150 | 151 | tt := []struct { 152 | str string 153 | vc []interface{} 154 | }{ 155 | // failed to parse -> nil result 156 | {"VClock:{}", []interface{}{interface{}(nil)}}, 157 | // parse empty Vlock -> empty slice 158 | {"VClock: {}", []interface{}{}}, 159 | {"VClock: {1:10}", []interface{}{int64(10)}}, 160 | {"VClock: {1:10, 2:0}", []interface{}{int64(10), int64(0)}}, 161 | {"VClock: { 1:10,2:0 }", []interface{}{int64(10), int64(0)}}, 162 | } 163 | for tc, item := range tt { 164 | luaProc.Tuple = []interface{}{item.str} 165 | res, err := tnt.Execute(luaProc) 166 | require.NoError(t, err, "case %v (exec)", tc+1) 167 | require.NotEmpty(t, res, "case %v (result array)", tc+1) 168 | assert.Equal(t, item.vc, res[0]) 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /lua/tarantool_lastsnapvclock.lua: -------------------------------------------------------------------------------- 1 | -- version comment below is used for system.d spec file 2 | -- VERSION = '0.3.3' 3 | 4 | local box = require('box') 5 | local fio = require('fio') 6 | local errno = require('errno') 7 | 8 | -- lastsnapfilename will be returned in case of success and nil otherwise. 9 | local function lastsnapfilename() 10 | local lastsnapfn = "" 11 | local snap_dir = box.cfg.memtx_dir or box.cfg.snap_dir 12 | for _, fname in ipairs(fio.glob(fio.pathjoin(snap_dir, '*.snap'))) do 13 | if fname > lastsnapfn then 14 | lastsnapfn = fname 15 | end 16 | end 17 | -- a ? b : c 18 | return (lastsnapfn ~= "") and lastsnapfn or nil 19 | end 20 | 21 | -- readfile with the byte limit. 22 | -- It returns result data and nil in case of success and nil with error message otherwise. 23 | local function readfile(filename, limit) 24 | 25 | local snapfile = fio.open(filename, {'O_RDONLY'}) 26 | if not snapfile then 27 | return nil, "failed to open file " .. filename .. ": " .. errno.strerror() 28 | end 29 | 30 | local data = snapfile:read(limit) 31 | snapfile:close() 32 | if not data then 33 | return nil, "failed to read file " .. filename 34 | end 35 | return data, nil 36 | end 37 | 38 | -- parsevclock in the given data. 39 | -- Returns vector clock table and nil if succeed and nil with error message otherwise. 40 | local function parsevclock(data) 41 | local vectorpattern = "VClock: {([%s%d:,]*)}" 42 | local clockspattern = "%s*(%d+)%s*:%s*(%d+)" 43 | 44 | _, _, data = string.find(data, vectorpattern) 45 | if data == nil then 46 | return nil 47 | end 48 | 49 | local vc = {} 50 | for id, lsn in string.gmatch(data, clockspattern) do 51 | vc[tonumber(id)] = tonumber64(lsn) 52 | end 53 | 54 | return vc 55 | end 56 | 57 | -- lastsnapvclock returns vector clock of the latest snapshot file. 58 | -- In case of any errors there will be raised box.error. 59 | local function lastsnapvclock() 60 | local err 61 | local data 62 | local vclock 63 | local limit = 1024 64 | 65 | local snapfilename = lastsnapfilename() 66 | if not snapfilename then 67 | box.error(box.error.PROC_LUA, "last snapshot file hasn't been found") 68 | end 69 | 70 | data, err = readfile(snapfilename, limit) 71 | if err then 72 | box.error(box.error.PROC_LUA, err) 73 | end 74 | if data == "" then 75 | box.error(box.error.PROC_LUA, "empty file " .. snapfilename) 76 | end 77 | 78 | vclock = parsevclock(data) 79 | if not vclock then 80 | box.error(box.error.PROC_LUA, "there is no vector clock in file " .. snapfilename) 81 | end 82 | return vclock 83 | end 84 | 85 | return { 86 | lastsnapvclock = lastsnapvclock, 87 | lastsnapfilename = lastsnapfilename, 88 | readfile = readfile, 89 | parsevclock = parsevclock, 90 | } -------------------------------------------------------------------------------- /operator.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tinylib/msgp/msgp" 7 | ) 8 | 9 | type Operator interface { 10 | AsTuple() []interface{} 11 | } 12 | 13 | type OpAdd struct { 14 | Field int64 15 | Argument int64 16 | } 17 | 18 | type OpSub struct { 19 | Field int64 20 | Argument int64 21 | } 22 | 23 | type OpBitAND struct { 24 | Field int64 25 | Argument uint64 26 | } 27 | 28 | type OpBitXOR struct { 29 | Field int64 30 | Argument uint64 31 | } 32 | 33 | type OpBitOR struct { 34 | Field int64 35 | Argument uint64 36 | } 37 | 38 | type OpDelete struct { 39 | From int64 40 | Count uint64 41 | } 42 | 43 | type OpInsert struct { 44 | Before int64 45 | Argument interface{} 46 | } 47 | 48 | type OpAssign struct { 49 | Field int64 50 | Argument interface{} 51 | } 52 | 53 | type OpSplice struct { 54 | Field int64 55 | Offset uint64 56 | Position uint64 57 | Argument string 58 | } 59 | 60 | func (op *OpAdd) AsTuple() []interface{} { 61 | return []interface{}{"+", op.Field, op.Argument} 62 | } 63 | 64 | func (op *OpSub) AsTuple() []interface{} { 65 | return []interface{}{"-", op.Field, op.Argument} 66 | } 67 | 68 | func (op *OpBitAND) AsTuple() []interface{} { 69 | return []interface{}{"&", op.Field, op.Argument} 70 | } 71 | 72 | func (op *OpBitXOR) AsTuple() []interface{} { 73 | return []interface{}{"^", op.Field, op.Argument} 74 | } 75 | 76 | func (op *OpBitOR) AsTuple() []interface{} { 77 | return []interface{}{"|", op.Field, op.Argument} 78 | } 79 | 80 | func (op *OpDelete) AsTuple() []interface{} { 81 | return []interface{}{"#", op.From, op.Count} 82 | } 83 | 84 | func (op *OpInsert) AsTuple() []interface{} { 85 | return []interface{}{"!", op.Before, op.Argument} 86 | } 87 | 88 | func (op *OpAssign) AsTuple() []interface{} { 89 | return []interface{}{"=", op.Field, op.Argument} 90 | } 91 | 92 | func (op *OpSplice) AsTuple() []interface{} { 93 | return []interface{}{":", op.Field, op.Position, op.Offset, op.Argument} 94 | } 95 | 96 | func marshalOperator(op Operator, buf []byte) ([]byte, error) { 97 | return msgp.AppendIntf(buf, op.AsTuple()) 98 | } 99 | 100 | func unmarshalOperator(data []byte) (op Operator, buf []byte, err error) { 101 | buf = data 102 | 103 | var n uint32 104 | if n, buf, err = msgp.ReadArrayHeaderBytes(buf); err != nil { 105 | return 106 | } 107 | 108 | var str string 109 | if str, buf, err = msgp.ReadStringBytes(buf); err != nil { 110 | return 111 | } 112 | 113 | var field0 int64 114 | if field0, buf, err = msgp.ReadInt64Bytes(buf); err != nil { 115 | return 116 | } 117 | 118 | switch str { 119 | case "+": 120 | if n != 3 { 121 | return nil, buf, fmt.Errorf("unexpected number of arguments in OpAdd: %d", n) 122 | } 123 | opAdd := &OpAdd{Field: field0} 124 | if opAdd.Argument, buf, err = msgp.ReadInt64Bytes(buf); err != nil { 125 | return 126 | } 127 | op = opAdd 128 | case "-": 129 | if n != 3 { 130 | return nil, buf, fmt.Errorf("unexpected number of arguments in OpSub: %d", n) 131 | } 132 | opSub := &OpSub{Field: field0} 133 | if opSub.Argument, buf, err = msgp.ReadInt64Bytes(buf); err != nil { 134 | return 135 | } 136 | op = opSub 137 | case "&": 138 | if n != 3 { 139 | return nil, buf, fmt.Errorf("unexpected number of arguments in OpBitAND: %d", n) 140 | } 141 | opAnd := &OpBitAND{Field: field0} 142 | if opAnd.Argument, buf, err = msgp.ReadUint64Bytes(buf); err != nil { 143 | return 144 | } 145 | op = opAnd 146 | case "^": 147 | if n != 3 { 148 | return nil, buf, fmt.Errorf("unexpected number of arguments in OpBitXOR: %d", n) 149 | } 150 | opXOR := &OpBitXOR{Field: field0} 151 | if opXOR.Argument, buf, err = msgp.ReadUint64Bytes(buf); err != nil { 152 | return 153 | } 154 | op = opXOR 155 | case "|": 156 | if n != 3 { 157 | return nil, buf, fmt.Errorf("unexpected number of arguments in OpBitOR: %d", n) 158 | } 159 | opOR := &OpBitOR{Field: field0} 160 | if opOR.Argument, buf, err = msgp.ReadUint64Bytes(buf); err != nil { 161 | return 162 | } 163 | op = opOR 164 | case "#": 165 | if n != 3 { 166 | return nil, buf, fmt.Errorf("unexpected number of arguments in OpDelete: %d", n) 167 | } 168 | opDel := &OpDelete{From: field0} 169 | if opDel.Count, buf, err = msgp.ReadUint64Bytes(buf); err != nil { 170 | return 171 | } 172 | op = opDel 173 | case "!": 174 | if n != 3 { 175 | return nil, buf, fmt.Errorf("unexpected number of arguments in OpInsert: %d", n) 176 | } 177 | opIns := &OpInsert{Before: field0} 178 | if opIns.Argument, buf, err = msgp.ReadIntfBytes(buf); err != nil { 179 | return 180 | } 181 | op = opIns 182 | case "=": 183 | if n != 3 { 184 | return nil, buf, fmt.Errorf("unexpected number of arguments in OpAssign: %d", n) 185 | } 186 | opAss := &OpAssign{Field: field0} 187 | if opAss.Argument, buf, err = msgp.ReadIntfBytes(buf); err != nil { 188 | return 189 | } 190 | op = opAss 191 | case ":": 192 | if n != 5 { 193 | return nil, buf, fmt.Errorf("unexpected number of arguments in OpSplice: %d", n) 194 | } 195 | opSpl := &OpSplice{Field: field0} 196 | if opSpl.Position, buf, err = msgp.ReadUint64Bytes(buf); err != nil { 197 | return 198 | } 199 | if opSpl.Offset, buf, err = msgp.ReadUint64Bytes(buf); err != nil { 200 | return 201 | } 202 | if opSpl.Argument, buf, err = msgp.ReadStringBytes(buf); err != nil { 203 | return 204 | } 205 | op = opSpl 206 | default: 207 | return nil, buf, fmt.Errorf("uknown op %s", str) 208 | } 209 | 210 | return 211 | } 212 | -------------------------------------------------------------------------------- /pack_data.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/tinylib/msgp/msgp" 8 | ) 9 | 10 | // cache precompiled 11 | type packData struct { 12 | defaultSpace interface{} 13 | packedDefaultSpace []byte 14 | packedDefaultIndex []byte 15 | packedIterEq []byte 16 | packedDefaultOffset []byte 17 | packedSingleKey []byte 18 | spaceMap map[string]uint64 19 | indexMap map[uint64]map[string]uint64 20 | primaryKeyMap map[uint64][]int 21 | } 22 | 23 | type packDataPool struct { 24 | sync.Mutex 25 | pool map[string]*packData 26 | } 27 | 28 | var globalPackDataPool packDataPool 29 | 30 | func encodeValues2(v1, v2 interface{}) []byte { 31 | o := make([]byte, 0) 32 | o, _ = msgp.AppendIntf(o, v1) 33 | o, _ = msgp.AppendIntf(o, v2) 34 | return o[:] 35 | } 36 | 37 | func packSelectSingleKey() []byte { 38 | o := make([]byte, 0) 39 | o = msgp.AppendUint(o, KeyKey) 40 | o = msgp.AppendArrayHeader(o, 1) 41 | return o[:] 42 | } 43 | 44 | func newPackData(defaultSpace interface{}) *packData { 45 | var packedDefaultSpace []byte 46 | if spaceNo, ok := defaultSpace.(uint64); ok { 47 | packedDefaultSpace = encodeValues2(KeySpaceNo, spaceNo) 48 | } 49 | return &packData{ 50 | defaultSpace: defaultSpace, 51 | packedDefaultSpace: packedDefaultSpace, 52 | packedDefaultIndex: encodeValues2(KeyIndexNo, uint32(0)), 53 | packedIterEq: encodeValues2(KeyIterator, IterEq), 54 | packedDefaultOffset: encodeValues2(KeyOffset, 0), 55 | packedSingleKey: packSelectSingleKey(), 56 | spaceMap: make(map[string]uint64), 57 | indexMap: make(map[uint64]map[string]uint64), 58 | primaryKeyMap: make(map[uint64][]int), 59 | } 60 | } 61 | 62 | func (data *packData) spaceNo(space interface{}) (uint64, error) { 63 | if space == nil { 64 | space = data.defaultSpace 65 | } 66 | 67 | switch value := space.(type) { 68 | case string: 69 | spaceNo, exists := data.spaceMap[value] 70 | if exists { 71 | return spaceNo, nil 72 | } 73 | return 0, fmt.Errorf("unknown space %#v", space) 74 | } 75 | 76 | return numberToUint64(space) 77 | } 78 | 79 | func (data *packData) packSpace(space interface{}, o []byte) ([]byte, error) { 80 | if space == nil && data.packedDefaultSpace != nil { 81 | o = append(o, data.packedDefaultSpace...) 82 | return o, nil 83 | } 84 | 85 | spaceNo, err := data.spaceNo(space) 86 | if err != nil { 87 | return o, err 88 | } 89 | 90 | o = msgp.AppendUint(o, KeySpaceNo) 91 | o = msgp.AppendUint64(o, spaceNo) 92 | return o, nil 93 | } 94 | 95 | func numberToUint64(number interface{}) (uint64, error) { 96 | switch value := number.(type) { 97 | default: 98 | return 0, fmt.Errorf("bad number %#v", number) 99 | case int: 100 | return uint64(value), nil 101 | case uint: 102 | return uint64(value), nil 103 | case int8: 104 | return uint64(value), nil 105 | case uint8: 106 | return uint64(value), nil 107 | case int16: 108 | return uint64(value), nil 109 | case uint16: 110 | return uint64(value), nil 111 | case int32: 112 | return uint64(value), nil 113 | case uint32: 114 | return uint64(value), nil 115 | case int64: 116 | return uint64(value), nil 117 | case uint64: 118 | return value, nil 119 | } 120 | } 121 | 122 | func (data *packData) fieldNo(field interface{}) (uint64, error) { 123 | return numberToUint64(field) 124 | } 125 | 126 | func (data *packData) indexNo(space interface{}, index interface{}) (uint64, error) { 127 | if index == nil { 128 | return 0, nil 129 | } 130 | 131 | if value, ok := index.(string); ok { 132 | spaceNo, err := data.spaceNo(space) 133 | if err != nil { 134 | return 0, nil 135 | } 136 | 137 | spaceData, exists := data.indexMap[spaceNo] 138 | if !exists { 139 | return 0, fmt.Errorf("no indexes defined for space %#v", space) 140 | } 141 | 142 | indexNo, exists := spaceData[value] 143 | if exists { 144 | return indexNo, nil 145 | } 146 | return 0, fmt.Errorf("unknown index %#v", index) 147 | } 148 | 149 | return numberToUint64(index) 150 | } 151 | 152 | func (data *packData) packIndex(space interface{}, index interface{}, o []byte) ([]byte, error) { 153 | if index == nil { 154 | o = append(o, data.packedDefaultIndex...) 155 | return o, nil 156 | } 157 | 158 | indexNo, err := data.indexNo(space, index) 159 | if err != nil { 160 | return o, err 161 | } 162 | 163 | o = msgp.AppendUint(o, KeyIndexNo) 164 | o = msgp.AppendUint64(o, indexNo) 165 | return o, nil 166 | } 167 | 168 | func (pool *packDataPool) Put(data *packData) *packData { 169 | if data == nil { 170 | return nil 171 | } 172 | 173 | key := fmt.Sprintf("%+v", data) 174 | 175 | pool.Lock() 176 | defer pool.Unlock() 177 | 178 | if pool.pool == nil { 179 | pool.pool = make(map[string]*packData) 180 | } 181 | if odata, ok := pool.pool[key]; ok { 182 | return odata 183 | } 184 | pool.pool[key] = data 185 | return data 186 | } 187 | -------------------------------------------------------------------------------- /packet.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/tinylib/msgp/msgp" 8 | ) 9 | 10 | type Packet struct { 11 | Cmd uint 12 | LSN uint64 13 | requestID uint64 14 | SchemaID uint64 15 | InstanceID uint32 16 | Timestamp time.Time 17 | Request Query 18 | Result *Result 19 | 20 | ResultUnmarshalMode resultUnmarshalMode 21 | } 22 | 23 | func (pack *Packet) String() string { 24 | switch { 25 | // response to client 26 | case pack.Result != nil: 27 | return fmt.Sprintf("Packet Type:%v, ReqID:%v\n%v", 28 | pack.Cmd, pack.requestID, pack.Result) 29 | // request to server 30 | case pack.requestID != 0: 31 | return fmt.Sprintf("Packet Type:%v, ReqID:%v\nRequest:%#v", 32 | pack.Cmd, pack.requestID, pack.Request) 33 | // response from master 34 | case pack.LSN != 0: 35 | return fmt.Sprintf("Packet LSN:%v, InstanceID:%v, Timestamp:%v\nRequest:%#v", 36 | pack.LSN, pack.InstanceID, pack.Timestamp.Format(time.RFC3339), pack.Request) 37 | default: 38 | return fmt.Sprintf("Packet %#v", pack) 39 | } 40 | } 41 | 42 | func (pack *Packet) UnmarshalBinaryHeader(data []byte) (buf []byte, err error) { 43 | var l uint32 44 | 45 | buf = data 46 | if l, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 47 | return 48 | } 49 | 50 | for ; l > 0; l-- { 51 | var cd uint 52 | 53 | if cd, buf, err = msgp.ReadUintBytes(buf); err != nil { 54 | return 55 | } 56 | 57 | switch cd { 58 | case KeySync: 59 | if pack.requestID, buf, err = msgp.ReadUint64Bytes(buf); err != nil { 60 | return 61 | } 62 | case KeyCode: 63 | if pack.Cmd, buf, err = msgp.ReadUintBytes(buf); err != nil { 64 | return 65 | } 66 | case KeySchemaID: 67 | if pack.SchemaID, buf, err = msgp.ReadUint64Bytes(buf); err != nil { 68 | return 69 | } 70 | case KeyLSN: 71 | if pack.LSN, buf, err = msgp.ReadUint64Bytes(buf); err != nil { 72 | return 73 | } 74 | case KeyInstanceID: 75 | if pack.InstanceID, buf, err = msgp.ReadUint32Bytes(buf); err != nil { 76 | return 77 | } 78 | case KeyTimestamp: 79 | var ts float64 80 | if ts, buf, err = msgp.ReadFloat64Bytes(buf); err != nil { 81 | return 82 | } 83 | ts = ts * 1e9 84 | pack.Timestamp = time.Unix(0, int64(ts)) 85 | default: 86 | if buf, err = msgp.Skip(buf); err != nil { 87 | return 88 | } 89 | } 90 | } 91 | return buf, nil 92 | } 93 | 94 | func (pack *Packet) UnmarshalBinaryBody(data []byte) (buf []byte, err error) { 95 | unpackq := func(q Query, data []byte) (buf []byte, err error) { 96 | buf = data 97 | if buf, err = q.(msgp.Unmarshaler).UnmarshalMsg(buf); err != nil { 98 | return 99 | } 100 | pack.Request = q 101 | return 102 | } 103 | 104 | unpackr := func(errorCode uint, data []byte) (buf []byte, err error) { 105 | buf = data 106 | res := &Result{ErrorCode: errorCode, unmarshalMode: pack.ResultUnmarshalMode} 107 | if buf, err = res.UnmarshalMsg(buf); err != nil { 108 | return 109 | } 110 | pack.Result = res 111 | return 112 | } 113 | 114 | if pack.Cmd&ErrorFlag != 0 { 115 | // error 116 | return unpackr(pack.Cmd^ErrorFlag, data) 117 | } 118 | 119 | if q := NewQuery(pack.Cmd); q != nil { 120 | return unpackq(q, data) 121 | } 122 | return unpackr(OKCommand, data) 123 | } 124 | 125 | // UnmarshalBinary implements encoding.BinaryUnmarshaler 126 | func (pack *Packet) UnmarshalBinary(data []byte) error { 127 | _, err := pack.UnmarshalMsg(data) 128 | return err 129 | } 130 | 131 | // UnmarshalMsg implements msgp.Unmarshaler 132 | func (pack *Packet) UnmarshalMsg(data []byte) (buf []byte, err error) { 133 | *pack = Packet{ResultUnmarshalMode: pack.ResultUnmarshalMode} 134 | 135 | buf = data 136 | 137 | if buf, err = pack.UnmarshalBinaryHeader(buf); err != nil { 138 | return 139 | } 140 | 141 | if buf, err = pack.UnmarshalBinaryBody(buf); err != nil { 142 | return 143 | } 144 | return 145 | } 146 | -------------------------------------------------------------------------------- /packet_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestDecodePacket(t *testing.T) { 9 | assert := assert.New(t) 10 | 11 | body := []byte("\x83\x00\xce\x00\x00\x00\x00\x01\xcf\x00\x00\x00\x00\x00\x00\x00\x03\x05\xce\x00\x00\x006\x810\xdd\x00\x00\x00\x03\x92\x01\xacFirst record\x92\x02\xa5Music\x93\x03\xa6Length]") 12 | 13 | pp := &BinaryPacket{body: body} 14 | res := &pp.packet 15 | 16 | err := res.UnmarshalBinary(pp.body) 17 | assert.NoError(err) 18 | assert.EqualValues(3, res.requestID) 19 | assert.EqualValues(0, res.Result.ErrorCode) 20 | //assert.EqualValues([][]interface{}{[]interface{}{int64(1), "First record"}, []interface{}{int64(2), "Music"}, []interface{}{int64(3), "Length", int64(93)}}, res.Result.Data) 21 | } 22 | 23 | func BenchmarkDecodePacket(b *testing.B) { 24 | b.ReportAllocs() 25 | body := []byte("\x83\x00\xce\x00\x00\x00\x00\x01\xcf\x00\x00\x00\x00\x00\x00\x00\x03\x05\xce\x00\x00\x006\x810\xdd\x00\x00\x00\x03\x92\x01\xacFirst record\x92\x02\xa5Music\x93\x03\xa6Length]") 26 | pp := &BinaryPacket{body: body} 27 | res := &pp.packet 28 | 29 | for i := 0; i < b.N; i++ { 30 | err := res.UnmarshalBinary(pp.body) 31 | if err != nil || res.requestID != 3 { 32 | b.FailNow() 33 | } 34 | } 35 | } 36 | 37 | func BenchmarkDecodeHeader(b *testing.B) { 38 | b.ReportAllocs() 39 | body := []byte("\x83\x00\xce\x00\x00\x00\x00\x01\xcf\x00\x00\x00\x00\x00\x00\x00\x03\x05\xce\x00\x00\x006\x810\xdd\x00\x00\x00\x03\x92\x01\xacFirst record\x92\x02\xa5Music\x93\x03\xa6Length]") 40 | pp := &BinaryPacket{body: body} 41 | pack := &pp.packet 42 | 43 | for i := 0; i < b.N; i++ { 44 | _, err := pack.UnmarshalBinaryHeader(pp.body) 45 | if err != nil || pack.requestID != 3 { 46 | b.FailNow() 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /perfcount_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "expvar" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestPerfCount(t *testing.T) { 12 | perf := PerfCount{ 13 | expvar.NewInt("net_read"), 14 | expvar.NewInt("net_write"), 15 | expvar.NewInt("net_packets_in"), 16 | expvar.NewInt("net_packets_out"), 17 | nil, 18 | nil, 19 | } 20 | 21 | assert := assert.New(t) 22 | require := require.New(t) 23 | config := ` 24 | local s = box.schema.space.create('tester', {id = 42}) 25 | box.schema.user.grant('guest', 'write', 'space', 'tester') 26 | s:create_index('tester_id', { 27 | type = 'hash', 28 | parts = {1, 'NUM'} 29 | }) 30 | ` 31 | box, err := NewBox(config, nil) 32 | require.NoError(err) 33 | defer box.Close() 34 | 35 | conn, err := Connect(box.Addr(), &Options{ 36 | DefaultSpace: "tester", 37 | Perf: perf, 38 | }) 39 | require.NoError(err) 40 | defer conn.Close() 41 | 42 | _, err = conn.Execute(&Replace{ 43 | Tuple: []interface{}{int64(1)}, 44 | }) 45 | require.NoError(err) 46 | 47 | nr := perf.NetRead.Value() 48 | nw := perf.NetWrite.Value() 49 | pin := perf.NetPacketsIn.Value() 50 | pout := perf.NetPacketsOut.Value() 51 | 52 | assert.True(nr > 0) 53 | assert.True(nw > 0) 54 | assert.True(pin > 0) 55 | assert.True(pout > 0) 56 | assert.True(nr >= pin) 57 | assert.True(nw >= pout) 58 | 59 | tuples, err := conn.Execute(&Select{ 60 | KeyTuple: []interface{}{int64(1)}, 61 | }) 62 | require.NoError(err) 63 | assert.Equal([][]interface{}{{int64(1)}}, tuples) 64 | 65 | assert.True(perf.NetRead.Value() > nr) 66 | assert.True(perf.NetWrite.Value() > nw) 67 | assert.Equal(pin+1, perf.NetPacketsIn.Value()) 68 | assert.Equal(pout+1, perf.NetPacketsOut.Value()) 69 | } 70 | -------------------------------------------------------------------------------- /ping.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | type Ping struct { 4 | } 5 | 6 | var _ Query = (*Ping)(nil) 7 | 8 | func (q *Ping) GetCommandID() uint { 9 | return PingCommand 10 | } 11 | 12 | // MarshalMsg implements msgp.Marshaler 13 | func (q *Ping) MarshalMsg(b []byte) ([]byte, error) { 14 | return b, nil 15 | } 16 | 17 | // UnmarshalMsg implements msgp.Unmarshaler 18 | func (q *Ping) UnmarshalMsg([]byte) (buf []byte, err error) { 19 | return buf, nil 20 | } 21 | -------------------------------------------------------------------------------- /ping_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPing(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | tarantoolConfig := ` 13 | local s = box.schema.space.create('tester') 14 | ` 15 | 16 | box, err := NewBox(tarantoolConfig, nil) 17 | if !assert.NoError(err) { 18 | return 19 | } 20 | defer box.Close() 21 | 22 | conn, err := box.Connect(nil) 23 | assert.NoError(err) 24 | assert.NotNil(conn) 25 | 26 | defer conn.Close() 27 | 28 | data, err := conn.Execute(&Ping{}) 29 | assert.NoError(err) 30 | assert.Nil(data) 31 | } 32 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | type Query interface { 4 | GetCommandID() uint 5 | } 6 | 7 | type internalQuery interface { 8 | packMsg(data *packData, b []byte) ([]byte, error) 9 | } 10 | 11 | func NewQuery(cmd uint) Query { 12 | switch cmd { 13 | case SelectCommand: 14 | return &Select{} 15 | case AuthCommand: 16 | return &Auth{} 17 | case InsertCommand: 18 | return &Insert{} 19 | case ReplaceCommand: 20 | return &Replace{} 21 | case DeleteCommand: 22 | return &Delete{} 23 | case CallCommand: 24 | return &Call{} 25 | case Call17Command: 26 | return &Call17{} 27 | case UpdateCommand: 28 | return &Update{} 29 | case UpsertCommand: 30 | return &Upsert{} 31 | case PingCommand: 32 | return &Ping{} 33 | case EvalCommand: 34 | return &Eval{} 35 | default: 36 | return nil 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /register.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import "github.com/tinylib/msgp/msgp" 4 | 5 | // Register is the REGISTER command 6 | type Register struct { 7 | UUID string 8 | VClock VectorClock 9 | } 10 | 11 | var _ Query = (*Register)(nil) 12 | 13 | func (q *Register) GetCommandID() uint { 14 | return RegisterCommand 15 | } 16 | 17 | // MarshalMsg implements msgp.Marshaler 18 | func (q *Register) MarshalMsg(b []byte) (o []byte, err error) { 19 | o = b 20 | o = msgp.AppendMapHeader(o, 2) 21 | 22 | o = msgp.AppendUint(o, KeyInstanceUUID) 23 | o = msgp.AppendString(o, q.UUID) 24 | 25 | o = msgp.AppendUint(o, KeyVClock) 26 | o = msgp.AppendMapHeader(o, uint32(len(q.VClock[1:]))) 27 | 28 | for i, lsn := range q.VClock[1:] { 29 | o = msgp.AppendUint32(o, uint32(i)) 30 | o = msgp.AppendUint64(o, lsn) 31 | } 32 | 33 | return o, nil 34 | } 35 | 36 | // UnmarshalMsg implements msgp.Unmarshaler 37 | func (q *Register) UnmarshalMsg([]byte) (buf []byte, err error) { 38 | return buf, ErrNotSupported 39 | } 40 | -------------------------------------------------------------------------------- /replace.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tinylib/msgp/msgp" 7 | ) 8 | 9 | type Replace struct { 10 | Space interface{} 11 | Tuple []interface{} 12 | } 13 | 14 | var _ Query = (*Replace)(nil) 15 | 16 | func (q *Replace) GetCommandID() uint { 17 | return ReplaceCommand 18 | } 19 | 20 | func (q *Replace) packMsg(data *packData, b []byte) (o []byte, err error) { 21 | if q.Tuple == nil { 22 | return o, errors.New("Tuple can not be nil") 23 | } 24 | 25 | o = b 26 | o = msgp.AppendMapHeader(o, 2) 27 | 28 | if o, err = data.packSpace(q.Space, o); err != nil { 29 | return o, err 30 | } 31 | 32 | o = msgp.AppendUint(o, KeyTuple) 33 | return msgp.AppendIntf(o, q.Tuple) 34 | } 35 | 36 | // MarshalMsg implements msgp.Marshaler 37 | func (q *Replace) MarshalMsg(b []byte) ([]byte, error) { 38 | return q.packMsg(defaultPackData, b) 39 | } 40 | 41 | // UnmarshalMsg implements msgp.Unmarshaller 42 | func (q *Replace) UnmarshalMsg(data []byte) (buf []byte, err error) { 43 | var i uint32 44 | var k uint 45 | var t interface{} 46 | 47 | q.Space = nil 48 | q.Tuple = nil 49 | 50 | buf = data 51 | if i, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 52 | return 53 | } 54 | 55 | for ; i > 0; i-- { 56 | if k, buf, err = msgp.ReadUintBytes(buf); err != nil { 57 | return 58 | } 59 | 60 | switch k { 61 | case KeySpaceNo: 62 | if q.Space, buf, err = msgp.ReadUintBytes(buf); err != nil { 63 | return 64 | } 65 | case KeyTuple: 66 | t, buf, err = msgp.ReadIntfBytes(buf) 67 | if q.Tuple = t.([]interface{}); q.Tuple == nil { 68 | return buf, errors.New("interface type is not []interface{}") 69 | } 70 | } 71 | } 72 | 73 | if q.Space == nil { 74 | return buf, errors.New("Replace.Unpack: no space specified") 75 | } 76 | if q.Tuple == nil { 77 | return buf, errors.New("Replace.Unpack: no tuple specified") 78 | } 79 | 80 | return 81 | } 82 | -------------------------------------------------------------------------------- /replace_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestReplace(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | tarantoolConfig := ` 13 | local s = box.schema.space.create('tester', {id = 42}) 14 | s:create_index('primary', { 15 | type = 'hash', 16 | parts = {1, 'NUM'} 17 | }) 18 | 19 | box.schema.user.create('writer', {password = 'writer'}) 20 | box.schema.user.grant('writer', 'write', 'space', 'tester') 21 | ` 22 | 23 | box, err := NewBox(tarantoolConfig, nil) 24 | if !assert.NoError(err) { 25 | return 26 | } 27 | defer box.Close() 28 | 29 | conn, err := box.Connect(&Options{ 30 | User: "writer", 31 | Password: "writer", 32 | }) 33 | assert.NoError(err) 34 | assert.NotNil(conn) 35 | 36 | defer conn.Close() 37 | 38 | do := func(query *Replace) ([][]interface{}, error) { 39 | var err error 40 | var buf []byte 41 | 42 | buf, err = query.packMsg(conn.packData, buf) 43 | 44 | if assert.NoError(err) { 45 | var query2 = &Replace{} 46 | _, err = query2.UnmarshalMsg(buf) 47 | 48 | if assert.NoError(err) { 49 | assert.Equal(uint(42), query2.Space) 50 | assert.Equal(query.Tuple, query2.Tuple) 51 | } 52 | } 53 | 54 | return conn.Execute(query) 55 | } 56 | 57 | data, err := do(&Replace{ 58 | Space: "tester", 59 | Tuple: []interface{}{int64(4), "Hello"}, 60 | }) 61 | 62 | if assert.NoError(err) { 63 | assert.Equal([][]interface{}{ 64 | { 65 | int64(4), 66 | "Hello", 67 | }, 68 | }, data) 69 | } 70 | 71 | data, err = do(&Replace{ 72 | Space: "tester", 73 | Tuple: []interface{}{int64(4), "World"}, 74 | }) 75 | 76 | if assert.NoError(err) { 77 | assert.Equal([][]interface{}{ 78 | { 79 | int64(4), 80 | "World", 81 | }, 82 | }, data) 83 | } 84 | } 85 | 86 | func BenchmarkReplacePack(b *testing.B) { 87 | buf := make([]byte, 0) 88 | for i := 0; i < b.N; i++ { 89 | buf, _ = (&Replace{Tuple: []interface{}{3, "Hello world"}}).MarshalMsg(buf[:0]) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /request_map.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import "sync" 4 | 5 | const requestMapShardNum = 16 6 | 7 | type requestMapShard struct { 8 | sync.Mutex 9 | data map[uint64]*request 10 | } 11 | type requestMap struct { 12 | shard []*requestMapShard 13 | } 14 | 15 | func newRequestMap() *requestMap { 16 | shard := make([]*requestMapShard, requestMapShardNum) 17 | 18 | for i := 0; i < requestMapShardNum; i++ { 19 | shard[i] = &requestMapShard{ 20 | data: make(map[uint64]*request), 21 | } 22 | } 23 | 24 | return &requestMap{ 25 | shard: shard, 26 | } 27 | 28 | } 29 | 30 | // Put returns old request associated with given key 31 | func (m *requestMap) Put(key uint64, value *request) *request { 32 | shard := m.shard[key%requestMapShardNum] 33 | shard.Lock() 34 | oldValue := shard.data[key] 35 | shard.data[key] = value 36 | shard.Unlock() 37 | return oldValue 38 | } 39 | 40 | // Pop returns request associated with given key and remove it from map 41 | func (m *requestMap) Pop(key uint64) *request { 42 | shard := m.shard[key%requestMapShardNum] 43 | shard.Lock() 44 | value, exists := shard.data[key] 45 | if exists { 46 | delete(shard.data, key) 47 | } 48 | shard.Unlock() 49 | return value 50 | } 51 | 52 | func (m *requestMap) CleanUp(clearCallback func(*request)) { 53 | for i := 0; i < requestMapShardNum; i++ { 54 | shard := m.shard[i] 55 | shard.Lock() 56 | 57 | for requestID, req := range shard.data { 58 | delete(shard.data, requestID) 59 | clearCallback(req) 60 | } 61 | 62 | shard.Unlock() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /request_pool.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | type cappedRequestPool struct { 4 | queue chan *request 5 | reuse bool 6 | } 7 | 8 | func newCappedRequestPool() *cappedRequestPool { 9 | return &cappedRequestPool{ 10 | queue: make(chan *request, 1024), 11 | reuse: false, 12 | } 13 | } 14 | 15 | func (p *cappedRequestPool) Get() (r *request) { 16 | if !p.reuse { 17 | return &request{} 18 | } 19 | 20 | select { 21 | case r = <-p.queue: 22 | r.opaque = nil 23 | r.replyChan = nil 24 | r.resultMode = ResultDefaultMode 25 | default: 26 | r = &request{} 27 | } 28 | return 29 | } 30 | 31 | func (p *cappedRequestPool) Put(r *request) { 32 | if !p.reuse { 33 | return 34 | } 35 | 36 | if r == nil { 37 | return 38 | } 39 | 40 | select { 41 | case p.queue <- r: 42 | default: 43 | } 44 | } 45 | 46 | func (p *cappedRequestPool) Close() { 47 | close(p.queue) 48 | } 49 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/tinylib/msgp/msgp" 8 | ) 9 | 10 | type resultUnmarshalMode int 11 | 12 | const ( 13 | ResultDefaultMode resultUnmarshalMode = iota 14 | ResultAsRawData 15 | ResultAsDataWithFallback 16 | 17 | ResultAsData = ResultDefaultMode 18 | ) 19 | 20 | type Result struct { 21 | ErrorCode uint 22 | Error error 23 | 24 | // Data is a parsed array of tuples. 25 | // Keep in mind that by default if original data structure it's unmarhsalled from 26 | // has a different type it's forcefully wrapped to become array of tuples. This might be 27 | // the case for call17 or eval commands. You may overwrite this behavior by specifying 28 | // desired unmarshal mode. 29 | Data [][]interface{} 30 | RawData interface{} 31 | 32 | unmarshalMode resultUnmarshalMode 33 | } 34 | 35 | func (r *Result) GetCommandID() uint { 36 | if r.Error != nil { 37 | return r.ErrorCode | ErrorFlag 38 | } 39 | return r.ErrorCode 40 | } 41 | 42 | // MarshalMsg implements msgp.Marshaler 43 | func (r *Result) MarshalMsg(b []byte) (o []byte, err error) { 44 | o = b 45 | if r.Error != nil { 46 | o = msgp.AppendMapHeader(o, 1) 47 | o = msgp.AppendUint(o, KeyError) 48 | o = msgp.AppendString(o, r.Error.Error()) 49 | } else { 50 | o = msgp.AppendMapHeader(o, 1) 51 | o = msgp.AppendUint(o, KeyData) 52 | switch { 53 | case r.Data != nil: 54 | if o, err = msgp.AppendIntf(o, r.Data); err != nil { 55 | return nil, err 56 | } 57 | case r.RawData != nil: 58 | if o, err = msgp.AppendIntf(o, r.RawData); err != nil { 59 | return nil, err 60 | } 61 | default: 62 | o = msgp.AppendArrayHeader(o, 0) 63 | } 64 | } 65 | 66 | return o, nil 67 | } 68 | 69 | // UnmarshalMsg implements msgp.Unmarshaler 70 | func (r *Result) UnmarshalMsg(data []byte) (buf []byte, err error) { 71 | var l uint32 72 | var errorMessage string 73 | 74 | buf = data 75 | 76 | // Tarantool >= 1.7.7 sends periodic heartbeat messages without body 77 | if len(buf) == 0 && r.ErrorCode == OKCommand { 78 | return buf, nil 79 | } 80 | l, buf, err = msgp.ReadMapHeaderBytes(buf) 81 | 82 | if err != nil { 83 | return 84 | } 85 | 86 | for ; l > 0; l-- { 87 | var cd uint 88 | 89 | if cd, buf, err = msgp.ReadUintBytes(buf); err != nil { 90 | return 91 | } 92 | 93 | switch cd { 94 | case KeyData: 95 | switch r.unmarshalMode { 96 | case ResultAsDataWithFallback: 97 | obuf := buf 98 | r.Data, buf, err = r.UnmarshalTuplesArray(buf, false) 99 | if err != nil && errors.As(err, &msgp.TypeError{}) { 100 | r.RawData, buf, err = msgp.ReadIntfBytes(obuf) 101 | } 102 | case ResultAsRawData: 103 | r.RawData, buf, err = msgp.ReadIntfBytes(buf) 104 | default: 105 | r.Data, buf, err = r.UnmarshalTuplesArray(buf, true) 106 | } 107 | 108 | if err != nil { 109 | return 110 | } 111 | case KeyError: 112 | errorMessage, buf, err = msgp.ReadStringBytes(buf) 113 | if err != nil { 114 | return 115 | } 116 | r.Error = NewQueryError(r.ErrorCode, errorMessage) 117 | default: 118 | if buf, err = msgp.Skip(buf); err != nil { 119 | return 120 | } 121 | } 122 | } 123 | 124 | return 125 | } 126 | 127 | func (*Result) UnmarshalTuplesArray(buf []byte, force bool) ([][]interface{}, []byte, error) { 128 | var ( 129 | dl, tl uint32 130 | i, j uint32 131 | val interface{} 132 | err error 133 | ) 134 | 135 | if dl, buf, err = msgp.ReadArrayHeaderBytes(buf); err != nil { 136 | return nil, nil, err 137 | } 138 | 139 | data := make([][]interface{}, dl) 140 | for i = 0; i < dl; i++ { 141 | obuf := buf 142 | if tl, buf, err = msgp.ReadArrayHeaderBytes(buf); err != nil { 143 | buf = obuf 144 | if _, ok := err.(msgp.TypeError); ok && force { 145 | if val, buf, err = msgp.ReadIntfBytes(buf); err != nil { 146 | return nil, nil, err 147 | } 148 | data[i] = []interface{}{val} 149 | continue 150 | } 151 | return nil, nil, err 152 | } 153 | 154 | data[i] = make([]interface{}, tl) 155 | for j = 0; j < tl; j++ { 156 | if data[i][j], buf, err = msgp.ReadIntfBytes(buf); err != nil { 157 | return nil, nil, err 158 | } 159 | } 160 | } 161 | 162 | return data, buf, nil 163 | } 164 | 165 | func (r *Result) String() string { 166 | switch { 167 | case r == nil: 168 | return "Result " 169 | case r.Error != nil: 170 | return fmt.Sprintf("Result ErrCode:%v, Err: %v", r.ErrorCode, r.Error) 171 | case r.Data != nil: 172 | return fmt.Sprintf("Result Data:%#v", r.Data) 173 | case r.RawData != nil: 174 | return fmt.Sprintf("Result RawData:%#v", r.RawData) 175 | default: 176 | return "" 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /result_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestResultMarshaling(t *testing.T) { 10 | // The result of a call17 to: 11 | // function a() 12 | // return "a" 13 | // end 14 | tntBodyBytes := []byte{ 15 | 0x81, // MP_MAP 16 | 0x30, // key IPROTO_DATA 17 | 0xdd, 0x0, 0x0, 0x0, 0x1, // MP_ARRAY 18 | 0xa1, 0x61, // string value "a" 19 | } 20 | 21 | expectedDefaultMarshalBytes := []byte{ 22 | 0x81, // MP_MAP 23 | 0x30, // key IPROTO_DATA 24 | 0x91, // MP_ARRAY 25 | 0x91, // MP_ARRAY 26 | 0xa1, 0x61, // string value "a" 27 | } 28 | 29 | expectedFallbackMarshalBytes := []byte{ 30 | 0x81, // MP_MAP 31 | 0x30, // key IPROTO_DATA 32 | 0x91, // MP_ARRAY 33 | 0xa1, 0x61, // string value "a" 34 | } 35 | 36 | var result Result 37 | 38 | buf, err := result.UnmarshalMsg(tntBodyBytes) 39 | require.NoError(t, err, "error unmarshaling result") 40 | require.Empty(t, buf, "unmarshaling result buffer is not empty") 41 | require.Equal(t, result.Data, [][]interface{}{{"a"}}) 42 | require.Empty(t, result.RawData) 43 | 44 | defaultMarshalRes, err := result.MarshalMsg(nil) 45 | require.NoError(t, err, "error marshaling by default marshaller") 46 | require.Equal( 47 | t, 48 | expectedDefaultMarshalBytes, 49 | defaultMarshalRes, 50 | ) 51 | 52 | result = Result{unmarshalMode: ResultAsDataWithFallback} 53 | 54 | buf, err = result.UnmarshalMsg(tntBodyBytes) 55 | require.NoError(t, err, "error unmarshaling result") 56 | require.Empty(t, buf, "unmarshaling result buffer is not empty") 57 | require.Empty(t, result.Data) 58 | require.Equal(t, result.RawData, []interface{}{"a"}) 59 | 60 | fallbackMarshalRes, err := result.MarshalMsg(nil) 61 | require.NoError(t, err, "error marshaling by bytes marshaller") 62 | require.Equal(t, fallbackMarshalRes, expectedFallbackMarshalBytes) 63 | } 64 | -------------------------------------------------------------------------------- /select.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tinylib/msgp/msgp" 7 | ) 8 | 9 | type Select struct { 10 | Space interface{} 11 | Index interface{} 12 | Offset uint32 13 | Limit uint32 14 | Iterator uint8 15 | Key interface{} 16 | KeyTuple []interface{} 17 | } 18 | 19 | var _ Query = (*Select)(nil) 20 | 21 | func (q *Select) GetCommandID() uint { 22 | return SelectCommand 23 | } 24 | 25 | func (q *Select) packMsg(data *packData, b []byte) (o []byte, err error) { 26 | o = b 27 | o = msgp.AppendMapHeader(o, 6) 28 | 29 | if o, err = data.packSpace(q.Space, o); err != nil { 30 | return o, err 31 | } 32 | 33 | if o, err = data.packIndex(q.Space, q.Index, o); err != nil { 34 | return o, err 35 | } 36 | 37 | if q.Offset == 0 { 38 | o = append(o, data.packedDefaultOffset...) 39 | } else { 40 | o = msgp.AppendUint(o, KeyOffset) 41 | o = msgp.AppendUint(o, uint(q.Offset)) 42 | } 43 | 44 | if q.Limit == 0 { 45 | o = msgp.AppendUint(o, KeyLimit) 46 | o = msgp.AppendUint(o, uint(DefaultLimit)) 47 | } else { 48 | o = msgp.AppendUint(o, KeyLimit) 49 | o = msgp.AppendUint(o, uint(q.Limit)) 50 | } 51 | 52 | if q.Iterator == IterEq { 53 | o = append(o, data.packedIterEq...) 54 | } else { 55 | o = msgp.AppendUint(o, KeyIterator) 56 | o = msgp.AppendUint8(o, q.Iterator) 57 | } 58 | 59 | if q.Key != nil { 60 | o = append(o, data.packedSingleKey...) 61 | if o, err = msgp.AppendIntf(o, q.Key); err != nil { 62 | return o, err 63 | } 64 | } else if q.KeyTuple != nil { 65 | o = msgp.AppendUint(o, KeyKey) 66 | if o, err = msgp.AppendIntf(o, q.KeyTuple); err != nil { 67 | return o, err 68 | } 69 | } else { 70 | o = msgp.AppendUint(o, KeyKey) 71 | o = msgp.AppendArrayHeader(o, 0) 72 | } 73 | 74 | return o, nil 75 | } 76 | 77 | // MarshalMsg implements msgp.Marshaler 78 | func (q *Select) MarshalMsg(b []byte) (data []byte, err error) { 79 | return q.packMsg(defaultPackData, b) 80 | } 81 | 82 | // UnmarshalMsg implements msgp.Unmarshaler 83 | func (q *Select) UnmarshalMsg(data []byte) (buf []byte, err error) { 84 | var i uint32 85 | var k uint 86 | var t interface{} 87 | 88 | q.Space = nil 89 | q.Index = 0 90 | q.Offset = 0 91 | q.Limit = 0 92 | q.Iterator = IterEq 93 | 94 | buf = data 95 | if i, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 96 | return 97 | } 98 | 99 | for ; i > 0; i-- { 100 | if k, buf, err = msgp.ReadUintBytes(buf); err != nil { 101 | return 102 | } 103 | 104 | switch k { 105 | case KeySpaceNo: 106 | if q.Space, buf, err = msgp.ReadUintBytes(buf); err != nil { 107 | return 108 | } 109 | case KeyIndexNo: 110 | if q.Index, buf, err = msgp.ReadUintBytes(buf); err != nil { 111 | return 112 | } 113 | case KeyOffset: 114 | if q.Offset, buf, err = msgp.ReadUint32Bytes(buf); err != nil { 115 | return 116 | } 117 | case KeyLimit: 118 | if q.Limit, buf, err = msgp.ReadUint32Bytes(buf); err != nil { 119 | return 120 | } 121 | case KeyIterator: 122 | if q.Iterator, buf, err = msgp.ReadUint8Bytes(buf); err != nil { 123 | return 124 | } 125 | case KeyKey: 126 | t, buf, err = msgp.ReadIntfBytes(buf) 127 | if err != nil { 128 | return buf, err 129 | } 130 | 131 | if q.KeyTuple = t.([]interface{}); q.KeyTuple == nil { 132 | return buf, errors.New("interface type is not []interface{}") 133 | } 134 | 135 | if len(q.KeyTuple) == 1 { 136 | q.Key = q.KeyTuple[0] 137 | q.KeyTuple = nil 138 | } 139 | } 140 | } 141 | 142 | if q.Space == nil { 143 | return buf, errors.New("Select.Unpack: no space specified") 144 | } 145 | 146 | return 147 | } 148 | -------------------------------------------------------------------------------- /select_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSelect(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | tarantoolConfig := ` 13 | local s = box.schema.space.create('tester', {id = 42}) 14 | s:create_index('tester_id', { 15 | type = 'tree', 16 | parts = {1, 'NUM'} 17 | }) 18 | s:create_index('tester_name', { 19 | type = 'tree', 20 | parts = {2, 'STR'} 21 | }) 22 | s:create_index('id_name', { 23 | type = 'hash', 24 | parts = {1, 'NUM', 2, 'STR'}, 25 | unique = true 26 | }) 27 | local t = s:insert({1, 'First record'}) 28 | t = s:insert({2, 'Music'}) 29 | t = s:insert({3, 'Length', 93}) 30 | ` 31 | 32 | box, err := NewBox(tarantoolConfig, nil) 33 | if !assert.NoError(err) { 34 | return 35 | } 36 | defer box.Close() 37 | 38 | do := func(connectOptions *Options, query *Select, expected [][]interface{}) { 39 | var err error 40 | var buf []byte 41 | 42 | conn, err := box.Connect(connectOptions) 43 | assert.NoError(err) 44 | assert.NotNil(conn) 45 | 46 | defer conn.Close() 47 | 48 | buf, err = query.packMsg(conn.packData, buf) 49 | 50 | if assert.NoError(err) { 51 | var query2 = &Select{} 52 | _, err = query2.UnmarshalMsg(buf) 53 | 54 | if assert.NoError(err) { 55 | assert.Equal(uint(42), query2.Space) 56 | if query.Key != nil { 57 | switch query.Key.(type) { 58 | case int: 59 | assert.Equal(query.Key, query2.Key) 60 | default: 61 | assert.Equal(query.Key, query2.Key) 62 | } 63 | } 64 | if query.KeyTuple != nil { 65 | assert.Equal(query.KeyTuple, query2.KeyTuple) 66 | } 67 | if query.Index != nil { 68 | switch query.Index.(type) { 69 | case string: 70 | assert.Equal(conn.packData.indexMap[42][query.Index.(string)], uint64(query2.Index.(uint))) 71 | default: 72 | assert.Equal(query.Index, query2.Index) 73 | } 74 | } 75 | assert.Equal(query.Iterator, query2.Iterator) 76 | } 77 | } 78 | 79 | data, err := conn.Execute(query) 80 | 81 | if assert.NoError(err) { 82 | assert.Equal(expected, data) 83 | } 84 | } 85 | 86 | // simple select 87 | do(nil, 88 | &Select{ 89 | Space: uint(42), 90 | Key: int64(3), 91 | }, 92 | [][]interface{}{ 93 | {int64(0x3), "Length", int64(0x5d)}, 94 | }, 95 | ) 96 | 97 | // select with space name 98 | do(nil, 99 | &Select{ 100 | Space: "tester", 101 | Key: int64(3), 102 | }, 103 | [][]interface{}{ 104 | {int64(0x3), "Length", int64(0x5d)}, 105 | }, 106 | ) 107 | 108 | // select with index name 109 | do(nil, 110 | &Select{ 111 | Space: "tester", 112 | Index: "tester_name", 113 | Key: "Music", 114 | }, 115 | [][]interface{}{ 116 | {int64(0x2), "Music"}, 117 | }, 118 | ) 119 | 120 | // composite key 121 | do(nil, 122 | &Select{ 123 | Space: uint(42), 124 | Index: "id_name", 125 | KeyTuple: []interface{}{int64(2), "Music"}, 126 | }, 127 | [][]interface{}{ 128 | {int64(0x2), "Music"}, 129 | }, 130 | ) 131 | 132 | // composite key empty response 133 | do(nil, 134 | &Select{ 135 | Space: uint(42), 136 | Index: "id_name", 137 | KeyTuple: []interface{}{int64(2), "Length"}, 138 | }, 139 | [][]interface{}{}, 140 | ) 141 | // iterate all using NUM index 142 | do(nil, 143 | &Select{ 144 | Space: uint(42), 145 | Iterator: IterAll, 146 | }, 147 | [][]interface{}{ 148 | {int64(1), "First record"}, 149 | {int64(2), "Music"}, 150 | {int64(3), "Length", int64(93)}, 151 | }, 152 | ) 153 | // iterate all using STR index 154 | do(nil, 155 | &Select{ 156 | Space: uint(42), 157 | Index: "tester_name", 158 | Iterator: IterAll, 159 | }, 160 | [][]interface{}{ 161 | {int64(1), "First record"}, 162 | {int64(3), "Length", int64(93)}, 163 | {int64(2), "Music"}, 164 | }, 165 | ) 166 | // iterate Eq using STR index 167 | do(nil, 168 | &Select{ 169 | Space: uint(42), 170 | Index: "tester_name", 171 | Key: "Length", 172 | Iterator: IterEq, 173 | }, 174 | [][]interface{}{ 175 | {int64(3), "Length", int64(93)}, 176 | }, 177 | ) 178 | 179 | } 180 | 181 | func BenchmarkSelectPack(b *testing.B) { 182 | buf := make([]byte, 0) 183 | for i := 0; i < b.N; i++ { 184 | buf, _ = (&Select{Key: 3}).MarshalMsg(buf[:0]) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "crypto/rand" 7 | "encoding/base64" 8 | "fmt" 9 | "io" 10 | "net" 11 | "sync" 12 | ) 13 | 14 | const saltSize = 32 15 | 16 | type QueryHandler func(queryContext context.Context, query Query) *Result 17 | type OnShutdownCallback func(err error) 18 | 19 | func defaultPingStatus(*IprotoServer) uint { return OKCommand } 20 | 21 | type IprotoServer struct { 22 | sync.Mutex 23 | conn net.Conn 24 | reader *bufio.Reader 25 | writer *bufio.Writer 26 | uuid string 27 | salt []byte // base64-encoded salt 28 | ctx context.Context 29 | cancel context.CancelFunc 30 | handler QueryHandler 31 | onShutdown OnShutdownCallback 32 | output chan *BinaryPacket 33 | closeOnce sync.Once 34 | firstError error 35 | perf PerfCount 36 | schemaID uint64 37 | wg sync.WaitGroup 38 | getPingStatus func(*IprotoServer) uint 39 | } 40 | 41 | type IprotoServerOptions struct { 42 | Perf PerfCount 43 | GetPingStatus func(*IprotoServer) uint 44 | } 45 | 46 | func NewIprotoServer(uuid string, handler QueryHandler, onShutdown OnShutdownCallback) *IprotoServer { 47 | return &IprotoServer{ 48 | conn: nil, 49 | reader: nil, 50 | writer: nil, 51 | handler: handler, 52 | onShutdown: onShutdown, 53 | uuid: uuid, 54 | schemaID: 1, 55 | getPingStatus: defaultPingStatus, 56 | } 57 | } 58 | 59 | func (s *IprotoServer) WithOptions(opts *IprotoServerOptions) *IprotoServer { 60 | if opts == nil { 61 | opts = &IprotoServerOptions{} 62 | } 63 | s.perf = opts.Perf 64 | if opts.GetPingStatus != nil { 65 | s.getPingStatus = opts.GetPingStatus 66 | } 67 | return s 68 | } 69 | 70 | func (s *IprotoServer) Accept(conn net.Conn) { 71 | var ccr io.Reader 72 | var ccw io.Writer 73 | 74 | if s.perf.NetRead != nil { 75 | ccr = NewCountedReader(conn, s.perf.NetRead) 76 | } else { 77 | ccr = conn 78 | } 79 | 80 | if s.perf.NetWrite != nil { 81 | ccw = NewCountedWriter(conn, s.perf.NetWrite) 82 | } else { 83 | ccw = conn 84 | } 85 | 86 | s.conn = conn 87 | s.reader = bufio.NewReader(ccr) 88 | s.writer = bufio.NewWriter(ccw) 89 | s.ctx, s.cancel = context.WithCancel(context.Background()) 90 | s.output = make(chan *BinaryPacket, 1024) 91 | 92 | err := s.greet() 93 | if err != nil { 94 | s.Shutdown() 95 | return 96 | } 97 | 98 | go s.loop() 99 | } 100 | 101 | func (s *IprotoServer) CheckAuth(hash []byte, password string) bool { 102 | scr, err := scramble(s.salt, password) 103 | if err != nil { 104 | return false 105 | } 106 | 107 | if len(scr) != len(hash) { 108 | return false 109 | } 110 | 111 | for i, v := range hash { 112 | if v != scr[i] { 113 | return false 114 | } 115 | } 116 | return true 117 | } 118 | 119 | func (s *IprotoServer) setError(err error) { 120 | if err != nil && err != io.EOF { 121 | s.Lock() 122 | defer s.Unlock() 123 | if s.firstError == nil { 124 | s.firstError = err 125 | } 126 | } 127 | } 128 | 129 | func (s *IprotoServer) getError() error { 130 | s.Lock() 131 | defer s.Unlock() 132 | return s.firstError 133 | } 134 | 135 | func (s *IprotoServer) Shutdown() error { 136 | err := s.getError() 137 | 138 | s.closeOnce.Do(func() { 139 | s.cancel() 140 | if s.onShutdown != nil { 141 | s.onShutdown(err) 142 | } 143 | go func() { 144 | s.wg.Wait() 145 | s.conn.Close() 146 | }() 147 | }) 148 | 149 | return err 150 | } 151 | 152 | func (s *IprotoServer) greet() (err error) { 153 | var line1, line2 string 154 | var format, greeting string 155 | var n int 156 | 157 | salt := make([]byte, saltSize) 158 | _, err = rand.Read(salt) 159 | if err != nil { 160 | return 161 | } 162 | 163 | s.salt = []byte(base64.StdEncoding.EncodeToString(salt)) 164 | 165 | line1 = fmt.Sprintf("%s %s", ServerIdent, s.uuid) 166 | line2 = string(s.salt) 167 | 168 | format = fmt.Sprintf("%%-%ds\n%%-%ds\n", GreetingSize/2-1, GreetingSize/2-1) 169 | greeting = fmt.Sprintf(format, line1, line2) 170 | 171 | // send greeting 172 | n, err = fmt.Fprintf(s.writer, "%s", greeting) 173 | if err != nil || n != GreetingSize { 174 | return 175 | } 176 | 177 | return s.writer.Flush() 178 | } 179 | 180 | func (s *IprotoServer) loop() { 181 | s.wg.Add(2) 182 | 183 | go func() { 184 | defer s.wg.Done() 185 | s.read() 186 | }() 187 | 188 | go func() { 189 | defer s.wg.Done() 190 | s.write() 191 | }() 192 | } 193 | 194 | func (s *IprotoServer) read() { 195 | var err error 196 | var pp *BinaryPacket 197 | 198 | r := s.reader 199 | var wg sync.WaitGroup 200 | 201 | READER_LOOP: 202 | for { 203 | select { 204 | case <-s.ctx.Done(): 205 | break READER_LOOP 206 | default: 207 | // read raw bytes 208 | pp = packetPool.Get() 209 | _, err = pp.ReadFrom(r) 210 | if err != nil { 211 | break READER_LOOP 212 | } 213 | 214 | if s.perf.NetPacketsIn != nil { 215 | s.perf.NetPacketsIn.Add(1) 216 | } 217 | 218 | wg.Add(1) 219 | go func(pp *BinaryPacket) { 220 | packet := &pp.packet 221 | defer wg.Done() 222 | 223 | err := packet.UnmarshalBinary(pp.body) 224 | 225 | if err != nil { 226 | s.setError(fmt.Errorf("Error decoding packet type %d: %s", packet.Cmd, err)) 227 | s.Shutdown() 228 | return 229 | } 230 | 231 | code := packet.Cmd 232 | if code == PingCommand { 233 | pr := packetPool.GetWithID(packet.requestID) 234 | pr.packet.Cmd = s.getPingStatus(s) 235 | pr.packet.SchemaID = packet.SchemaID 236 | 237 | select { 238 | case s.output <- pr: 239 | break 240 | case <-s.ctx.Done(): 241 | break 242 | } 243 | } else { 244 | res := s.handler(s.ctx, packet.Request) 245 | if res.ErrorCode != OKCommand && res.Error == nil { 246 | res.Error = ErrUnknownError 247 | } 248 | 249 | // reuse the same binary packet object for result marshalling 250 | if err = pp.packMsg(res, nil); err != nil { 251 | s.setError(err) 252 | s.Shutdown() 253 | return 254 | } 255 | 256 | pp.packet.SchemaID = s.schemaID 257 | select { 258 | case s.output <- pp: 259 | return 260 | case <-s.ctx.Done(): 261 | break 262 | } 263 | } 264 | pp.Release() 265 | }(pp) 266 | } 267 | } 268 | 269 | if err != nil { 270 | s.setError(err) 271 | } 272 | wg.Wait() 273 | s.Shutdown() 274 | 275 | CLEANUP_LOOP: 276 | for { 277 | select { 278 | case pp = <-s.output: 279 | pp.Release() 280 | default: 281 | break CLEANUP_LOOP 282 | } 283 | } 284 | } 285 | 286 | func (s *IprotoServer) write() { 287 | var err error 288 | 289 | w := s.writer 290 | wp := func(w io.Writer, packet *BinaryPacket) error { 291 | if s.perf.NetPacketsOut != nil { 292 | s.perf.NetPacketsOut.Add(1) 293 | } 294 | _, err = packet.WriteTo(w) 295 | defer packet.Release() 296 | return err 297 | } 298 | 299 | WRITER_LOOP: 300 | for { 301 | select { 302 | case packet, ok := <-s.output: 303 | if !ok { 304 | break WRITER_LOOP 305 | } 306 | if err = wp(w, packet); err != nil { 307 | break WRITER_LOOP 308 | } 309 | case <-s.ctx.Done(): 310 | w.Flush() 311 | break WRITER_LOOP 312 | default: 313 | if err = w.Flush(); err != nil { 314 | break WRITER_LOOP 315 | } 316 | 317 | // same without flush 318 | select { 319 | case packet, ok := <-s.output: 320 | if !ok { 321 | break WRITER_LOOP 322 | } 323 | if err = wp(w, packet); err != nil { 324 | break WRITER_LOOP 325 | } 326 | case <-s.ctx.Done(): 327 | w.Flush() 328 | break WRITER_LOOP 329 | } 330 | 331 | } 332 | } 333 | 334 | if err != nil { 335 | s.setError(err) 336 | } 337 | 338 | s.Shutdown() 339 | } 340 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestServerPing(t *testing.T) { 13 | handler := func(queryContext context.Context, query Query) *Result { 14 | return &Result{} 15 | } 16 | 17 | s := NewIprotoServer("1", handler, nil) 18 | 19 | listenAddr := make(chan string) 20 | go func() { 21 | ln, err := net.Listen("tcp", "127.0.0.1:0") 22 | require.NoError(t, err) 23 | defer ln.Close() 24 | 25 | listenAddr <- ln.Addr().String() 26 | close(listenAddr) 27 | 28 | conn, err := ln.Accept() 29 | require.NoError(t, err) 30 | 31 | s.Accept(conn) 32 | }() 33 | 34 | addr := <-listenAddr 35 | conn, err := Connect(addr, nil) 36 | require.NoError(t, err) 37 | 38 | res := conn.Exec(context.Background(), &Ping{}) 39 | assert.Equal(t, res.ErrorCode, OKCommand) 40 | assert.NoError(t, res.Error) 41 | 42 | conn.Close() 43 | s.Shutdown() 44 | } 45 | -------------------------------------------------------------------------------- /slaveex_test.go: -------------------------------------------------------------------------------- 1 | package tarantool_test 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | "sync" 7 | 8 | tnt16 "github.com/viciious/go-tarantool" 9 | ) 10 | 11 | func ExampleSlave_subscribeExisted() { 12 | // Subscribe for master's changes synchronously 13 | 14 | // new slave instance connects to provided dsn instantly 15 | s, err := tnt16.NewSlave("127.0.0.1:8000", tnt16.Options{ 16 | User: "username", 17 | Password: "password", 18 | // UUID of the instance in replica set. Required 19 | UUID: "7c025e42-2394-11e7-aacf-0242ac110002", 20 | // UUID of the Replica Set. Required 21 | ReplicaSetUUID: "3b39c6a4-f2da-4d81-a43b-103e5b1c16a1"}) 22 | if err != nil { 23 | log.Printf("Tnt Slave creating error:%v", err) 24 | return 25 | } 26 | // always close slave to preserve socket descriptor 27 | defer s.Close() 28 | 29 | // let's start from the beginning 30 | var lsn uint64 = 0 31 | it, err := s.Subscribe(lsn) 32 | if err != nil { 33 | log.Printf("Tnt Slave subscribing error:%v", err) 34 | return 35 | } 36 | 37 | // print snapshot 38 | var p *tnt16.Packet 39 | var hr = strings.Repeat("-", 80) 40 | // iterate over master's changes permanently 41 | for { 42 | p, err = it.Next() 43 | if err != nil { 44 | log.Printf("Tnt Slave iterating error:%v", err) 45 | return 46 | } 47 | log.Println(p) 48 | log.Println(hr) 49 | } 50 | } 51 | 52 | func ExampleSlave_subscribeNew() { 53 | // Silently join slave to Replica Set and consume master's changes synchronously 54 | 55 | // new slave instance connects to provided dsn instantly 56 | s, err := tnt16.NewSlave("username:password@127.0.0.1:8000") 57 | if err != nil { 58 | log.Printf("Tnt Slave creating error:%v", err) 59 | return 60 | } 61 | // always close slave to preserve socket descriptor 62 | defer s.Close() 63 | 64 | // let's start from the beginning 65 | it, err := s.Attach() 66 | if err != nil { 67 | log.Printf("Tnt Slave subscribing error:%v", err) 68 | return 69 | } 70 | 71 | // print snapshot 72 | var p *tnt16.Packet 73 | var hr = strings.Repeat("-", 80) 74 | // iterate over master's changes permanently 75 | for { 76 | p, err = it.Next() 77 | if err != nil { 78 | log.Printf("Tnt Slave iterating error:%v", err) 79 | return 80 | } 81 | log.Println(p) 82 | log.Println(hr) 83 | } 84 | } 85 | 86 | func ExampleSlave_Join() { 87 | // Silently join slave to Replica Set 88 | 89 | // new slave instance connects to provided dsn instantly 90 | s, err := tnt16.NewSlave("username:password@127.0.0.1:8000") 91 | if err != nil { 92 | log.Printf("Tnt Slave creating error:%v", err) 93 | return 94 | } 95 | // always close slave to preserve socket descriptor 96 | defer s.Close() 97 | 98 | if err = s.Join(); err != nil { 99 | log.Printf("Tnt Slave joining error:%v", err) 100 | return 101 | } 102 | 103 | log.Printf("UUID=%#v Replica Set UUID=%#v\n", s.UUID, s.ReplicaSet.UUID) 104 | } 105 | 106 | func ExampleSlave_JoinWithSnap_sync() { 107 | // Join slave to Replica Set with iterating snapshot synchronously 108 | 109 | // new slave instance connects to provided dsn instantly 110 | s, err := tnt16.NewSlave("username:password@127.0.0.1:8000") 111 | if err != nil { 112 | log.Printf("Tnt Slave creating error:%v", err) 113 | return 114 | } 115 | // always close slave to preserve socket descriptor 116 | defer s.Close() 117 | 118 | // skip returned iterator; will be using self bufio.scanner-style iterator instead 119 | _, err = s.JoinWithSnap() 120 | if err != nil { 121 | log.Printf("Tnt Slave joining error:%v", err) 122 | return 123 | } 124 | 125 | // print snapshot 126 | var p *tnt16.Packet 127 | var hr = strings.Repeat("-", 80) 128 | for s.HasNext() { 129 | p = s.Packet() 130 | // print request 131 | log.Println(hr) 132 | switch q := p.Request.(type) { 133 | case *tnt16.Insert: 134 | switch q.Space { 135 | case tnt16.SpaceIndex, tnt16.SpaceSpace: 136 | // short default format 137 | log.Printf("Insert LSN:%v, Space:%v InstanceID:%v\n", 138 | p.LSN, q.Space, p.InstanceID) 139 | default: 140 | log.Printf("%v", p) 141 | } 142 | default: 143 | log.Printf("%v", p) 144 | } 145 | } 146 | // always checks for errors after iteration cycle 147 | if s.Err() != nil { 148 | log.Printf("Tnt Slave joining error:%v", err) 149 | return 150 | } 151 | 152 | log.Printf("UUID=%#v Replica Set UUID=%#v\n", s.UUID, s.ReplicaSet.UUID) 153 | } 154 | 155 | func ExampleSlave_JoinWithSnap_async() { 156 | // Join slave to Replica Set with iterating snapshot asynchronously 157 | 158 | // new slave instance connects to provided dsn instantly 159 | s, err := tnt16.NewSlave("username:password@127.0.0.1:8000") 160 | if err != nil { 161 | log.Printf("Tnt Slave creating error:%v", err) 162 | return 163 | } 164 | // always close slave to preserve socket descriptor 165 | defer s.Close() 166 | 167 | // chan for snapshot's packets 168 | snapChan := make(chan *tnt16.Packet, 128) 169 | wg := &sync.WaitGroup{} 170 | 171 | // run snapshot printer before join command 172 | wg.Add(1) 173 | go func(in <-chan *tnt16.Packet, wg *sync.WaitGroup) { 174 | defer wg.Done() 175 | 176 | var hr = strings.Repeat("-", 80) 177 | 178 | for p := range in { 179 | log.Println(hr) 180 | switch q := p.Request.(type) { 181 | case *tnt16.Insert: 182 | switch q.Space { 183 | case tnt16.SpaceIndex, tnt16.SpaceSpace: 184 | // short default format 185 | log.Printf("Insert LSN:%v, Space:%v InstanceID:%v\n", 186 | p.LSN, q.Space, p.InstanceID) 187 | default: 188 | log.Printf("%v", p) 189 | } 190 | default: 191 | log.Printf("%v", p) 192 | } 193 | } 194 | }(snapChan, wg) 195 | 196 | _, err = s.JoinWithSnap(snapChan) 197 | if err != nil { 198 | log.Printf("Tnt Slave joining error:%v", err) 199 | return 200 | } 201 | 202 | wg.Wait() 203 | 204 | log.Printf("UUID=%#v Replica Set UUID=%#v\n", s.UUID, s.ReplicaSet.UUID) 205 | } 206 | 207 | func ExampleSlave_Subscribe_sync() { 208 | // Subscribe for master's changes synchronously 209 | 210 | // new slave instance connects to provided dsn instantly 211 | s, err := tnt16.NewSlave("127.0.0.1:8000", tnt16.Options{ 212 | User: "username", 213 | Password: "password", 214 | // UUID of the instance in replica set. Required 215 | UUID: "7c025e42-2394-11e7-aacf-0242ac110002", 216 | // UUID of the Replica Set. Required 217 | ReplicaSetUUID: "3b39c6a4-f2da-4d81-a43b-103e5b1c16a1"}) 218 | if err != nil { 219 | log.Printf("Tnt Slave creating error:%v", err) 220 | return 221 | } 222 | // always close slave to preserve socket descriptor 223 | defer s.Close() 224 | 225 | // let's start from the beginning 226 | var lsn uint64 = 0 227 | it, err := s.Subscribe(lsn) 228 | if err != nil { 229 | log.Printf("Tnt Slave subscribing error:%v", err) 230 | return 231 | } 232 | 233 | // print snapshot 234 | var p *tnt16.Packet 235 | var hr = strings.Repeat("-", 80) 236 | // consume master's changes permanently 237 | for { 238 | p, err = it.Next() 239 | if err != nil { 240 | log.Printf("Tnt Slave consuming error:%v", err) 241 | return 242 | } 243 | log.Println(hr) 244 | switch q := p.Request.(type) { 245 | case *tnt16.Insert: 246 | switch q.Space { 247 | case tnt16.SpaceIndex, tnt16.SpaceSpace: 248 | // short default format 249 | log.Printf("Insert LSN:%v, Space:%v InstanceID:%v\n", 250 | p.LSN, q.Space, p.InstanceID) 251 | default: 252 | log.Printf("%v", p) 253 | } 254 | default: 255 | log.Printf("%v", p) 256 | } 257 | } 258 | } 259 | 260 | func ExampleSlave_Subscribe_async() { 261 | // Subscribe for master's changes asynchronously 262 | 263 | // new slave instance connects to provided dsn instantly 264 | s, err := tnt16.NewSlave("127.0.0.1:8000", tnt16.Options{ 265 | User: "username", 266 | Password: "password", 267 | // UUID of the instance in replica set. Required 268 | UUID: "7c025e42-2394-11e7-aacf-0242ac110002", 269 | // UUID of the Replica Set. Required 270 | ReplicaSetUUID: "3b39c6a4-f2da-4d81-a43b-103e5b1c16a1"}) 271 | if err != nil { 272 | log.Printf("Tnt Slave creating error:%v", err) 273 | return 274 | } 275 | // always close slave to preserve socket descriptor 276 | defer s.Close() 277 | 278 | // chan for snapshot's packets 279 | xlogChan := make(chan *tnt16.Packet, 128) 280 | 281 | // run xlog printer before subscribing command 282 | go func(in <-chan *tnt16.Packet) { 283 | var hr = strings.Repeat("-", 80) 284 | 285 | for p := range in { 286 | log.Println(hr) 287 | switch q := p.Request.(type) { 288 | case *tnt16.Insert: 289 | switch q.Space { 290 | case tnt16.SpaceIndex, tnt16.SpaceSpace: 291 | // short default format 292 | log.Printf("Insert LSN:%v, Space:%v InstanceID:%v\n", 293 | p.LSN, q.Space, p.InstanceID) 294 | default: 295 | log.Printf("%v", p) 296 | } 297 | default: 298 | log.Printf("%v", p) 299 | } 300 | } 301 | }(xlogChan) 302 | 303 | // let's start from the beginning 304 | var lsn uint64 = 0 305 | it, err := s.Subscribe(lsn) 306 | if err != nil { 307 | log.Printf("Tnt Slave subscribing error:%v", err) 308 | return 309 | } 310 | 311 | // consume requests infinitely 312 | var p *tnt16.Packet 313 | for { 314 | p, err = it.Next() 315 | if err != nil { 316 | close(xlogChan) 317 | log.Printf("Tnt Slave consuming error:%v", err) 318 | return 319 | } 320 | xlogChan <- p 321 | } 322 | } 323 | 324 | func ExampleSlave_Attach_sync() { 325 | // Silently join slave to Replica Set and consume master's changes synchronously 326 | 327 | // new slave instance connects to provided dsn instantly 328 | s, err := tnt16.NewSlave("username:password@127.0.0.1:8000") 329 | if err != nil { 330 | log.Printf("Tnt Slave creating error:%v", err) 331 | return 332 | } 333 | // always close slave to preserve socket descriptor 334 | defer s.Close() 335 | 336 | // let's start from the beginning 337 | it, err := s.Attach() 338 | if err != nil { 339 | log.Printf("Tnt Slave subscribing error:%v", err) 340 | return 341 | } 342 | 343 | // print snapshot 344 | var p *tnt16.Packet 345 | var hr = strings.Repeat("-", 80) 346 | // consume master's changes permanently 347 | for { 348 | p, err = it.Next() 349 | if err != nil { 350 | log.Printf("Tnt Slave consuming error:%v", err) 351 | return 352 | } 353 | log.Println(hr) 354 | switch q := p.Request.(type) { 355 | case *tnt16.Insert: 356 | switch q.Space { 357 | case tnt16.SpaceIndex, tnt16.SpaceSpace: 358 | // short default format 359 | log.Printf("Insert LSN:%v, Space:%v InstanceID:%v\n", 360 | p.LSN, q.Space, p.InstanceID) 361 | default: 362 | log.Printf("%v", p) 363 | } 364 | default: 365 | log.Printf("%v", p) 366 | } 367 | } 368 | } 369 | 370 | func ExampleSlave_Attach_async() { 371 | // Silently join slave to Replica Set and consume master's changes asynchronously 372 | 373 | // new slave instance connects to provided dsn instantly 374 | s, err := tnt16.NewSlave("username:password@127.0.0.1:8000") 375 | if err != nil { 376 | log.Printf("Tnt Slave creating error:%v", err) 377 | return 378 | } 379 | // always close slave to preserve socket descriptor 380 | defer s.Close() 381 | 382 | // chan for snapshot's packets 383 | xlogChan := make(chan *tnt16.Packet, 128) 384 | wg := &sync.WaitGroup{} 385 | 386 | // run xlog printer before subscribing command 387 | wg.Add(1) 388 | go func(in <-chan *tnt16.Packet, wg *sync.WaitGroup) { 389 | defer wg.Done() 390 | 391 | var hr = strings.Repeat("-", 80) 392 | 393 | for p := range in { 394 | log.Println(hr) 395 | switch q := p.Request.(type) { 396 | case *tnt16.Insert: 397 | switch q.Space { 398 | case tnt16.SpaceIndex, tnt16.SpaceSpace: 399 | // short default format 400 | log.Printf("Insert LSN:%v, Space:%v InstanceID:%v\n", 401 | p.LSN, q.Space, p.InstanceID) 402 | default: 403 | log.Printf("%v", p) 404 | } 405 | default: 406 | log.Printf("%v", p) 407 | } 408 | } 409 | }(xlogChan, wg) 410 | 411 | // let's start from the beginning 412 | _, err = s.Attach(xlogChan) 413 | if err != nil { 414 | log.Printf("Tnt Slave subscribing error:%v", err) 415 | return 416 | } 417 | 418 | // consume master's changes permanently 419 | wg.Wait() 420 | } 421 | -------------------------------------------------------------------------------- /snapio/const.go: -------------------------------------------------------------------------------- 1 | package snapio 2 | 3 | const XRowFixedHeaderSize = 19 4 | const XRowFixedHeaderMagic = 0xd5ba0bab 5 | const XRowFixedHeaderEof = 0xd510aded 6 | const ZRowFixedHeaderMagic = 0xd5ba0bba 7 | -------------------------------------------------------------------------------- /snapio/snapread.go: -------------------------------------------------------------------------------- 1 | package snapio 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | 10 | "github.com/klauspost/compress/zstd" 11 | "github.com/tinylib/msgp/msgp" 12 | "github.com/viciious/go-tarantool" 13 | ) 14 | 15 | func ReadSnapshotPacked(rs io.Reader, tuplecb func(space uint, tuple []byte) error) error { 16 | var err error 17 | var version int 18 | 19 | in := bufio.NewReaderSize(rs, 16*1024*1024) 20 | 21 | for ln := 0; ; ln++ { 22 | if ln > 0 { 23 | nl, err := in.Peek(1) 24 | if err != nil { 25 | return err 26 | } 27 | if nl[0] == 0xa { 28 | in.ReadByte() 29 | break 30 | } 31 | } 32 | 33 | lineb, _, err := in.ReadLine() 34 | if err != nil { 35 | return err 36 | } 37 | 38 | line := string(lineb) 39 | switch ln { 40 | case 0: 41 | if line != "SNAP" && line != "XLOG" { 42 | return errors.New("missing SNAP/XLOG header") 43 | } 44 | case 1: 45 | if line == "0.12" { 46 | version = 12 47 | } else if line == "0.13" { 48 | version = 13 49 | } else { 50 | return fmt.Errorf("unknown snapshot version: %s", line) 51 | } 52 | } 53 | } 54 | 55 | var fixh [XRowFixedHeaderSize]byte 56 | var xrow, zrow []byte 57 | var zr *zstd.Decoder 58 | 59 | if version != 12 { 60 | if zr, err = zstd.NewReader(nil); err != nil { 61 | return err 62 | } 63 | defer zr.Close() 64 | } 65 | 66 | for { 67 | var n int 68 | var ulen uint 69 | 70 | if n, err = io.ReadFull(in, fixh[:]); err == io.EOF { 71 | return nil 72 | } 73 | 74 | if n == 4 && binary.BigEndian.Uint32(fixh[0:4]) == XRowFixedHeaderEof { 75 | return nil 76 | } 77 | 78 | if err != nil { 79 | return err 80 | } 81 | 82 | compressed := false 83 | if zr != nil { 84 | compressed = binary.BigEndian.Uint32(fixh[0:4]) == ZRowFixedHeaderMagic 85 | } 86 | 87 | if !compressed && binary.BigEndian.Uint32(fixh[0:4]) != XRowFixedHeaderMagic { 88 | return fmt.Errorf("bad xrow magic %0X", fixh[0:4]) 89 | } 90 | 91 | buf := fixh[4:] 92 | if ulen, _, err = msgp.ReadUintBytes(buf); err != nil { 93 | return err 94 | } 95 | 96 | rlen := int(ulen) 97 | if rlen <= in.Buffered() { 98 | if buf, err = in.Peek(rlen); err != nil { 99 | return err 100 | } 101 | if _, err = in.Discard(rlen); err != nil { 102 | return err 103 | } 104 | } else { 105 | if rlen > cap(zrow) { 106 | zrow = make([]byte, 0, rlen+1024) 107 | } 108 | if _, err = io.ReadFull(in, zrow[:rlen]); err != nil { 109 | return err 110 | } 111 | buf = zrow[:rlen] 112 | } 113 | 114 | if compressed { 115 | if xrow, err = zr.DecodeAll(buf, xrow); err != nil { 116 | return err 117 | } 118 | buf = xrow 119 | xrow = xrow[:0] 120 | } 121 | 122 | for len(buf) > 0 { 123 | // meta map: timestamp, lsn, etc 124 | if buf, err = msgp.Skip(buf); err != nil { 125 | return err 126 | } 127 | 128 | var ml uint32 129 | if ml, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 130 | return err 131 | } 132 | 133 | var space uint 134 | var tuple []byte 135 | 136 | for ; ml > 0; ml-- { 137 | var cd uint 138 | if cd, buf, err = msgp.ReadUintBytes(buf); err != nil { 139 | return err 140 | } 141 | 142 | switch cd { 143 | case tarantool.KeySpaceNo: 144 | if space, buf, err = msgp.ReadUintBytes(buf); err != nil { 145 | return err 146 | } 147 | case tarantool.KeyTuple: 148 | var curbuf = buf 149 | if buf, err = msgp.Skip(buf); err != nil { 150 | return err 151 | } 152 | tuple = curbuf[:len(curbuf)-len(buf)] 153 | default: 154 | if buf, err = msgp.Skip(buf); err != nil { 155 | return err 156 | } 157 | } 158 | } 159 | 160 | if space == 0 || tuple == nil { 161 | continue 162 | } 163 | 164 | if err = tuplecb(space, tuple); err != nil { 165 | return err 166 | } 167 | } 168 | } 169 | } 170 | 171 | func ReadSnapshot(rs io.Reader, tuplecb func(space uint, tuple []interface{}) error) error { 172 | return ReadSnapshotPacked(rs, func(space uint, buf []byte) error { 173 | var err error 174 | var tinf interface{} 175 | if tinf, _, err = msgp.ReadIntfBytes(buf); err != nil { 176 | return err 177 | } 178 | return tuplecb(space, tinf.([]interface{})) 179 | }) 180 | } 181 | -------------------------------------------------------------------------------- /snapio/snapread_test.go: -------------------------------------------------------------------------------- 1 | package snapio 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func checkSnapshotCnt(v, fn string, expected int, t *testing.T) { 10 | ffn := filepath.Join("testdata", v, fn) 11 | f, e := os.Open(ffn) 12 | if e != nil { 13 | t.Error(e) 14 | return 15 | } 16 | defer f.Close() 17 | 18 | cnt := 0 19 | e = ReadSnapshot(f, func(space uint, tuple []interface{}) error { 20 | cnt++ 21 | return nil 22 | }) 23 | 24 | if e != nil { 25 | t.Error(e) 26 | return 27 | } 28 | 29 | if cnt != expected { 30 | t.Errorf("%s: cnt == %d, expected %d", ffn, cnt, expected) 31 | } 32 | } 33 | 34 | func TestReadv12OK(t *testing.T) { 35 | checkSnapshotCnt("v12", "00000000000000000000.ok.snap", 62, t) 36 | } 37 | 38 | func TestReadv13OK(t *testing.T) { 39 | checkSnapshotCnt("v13", "00000000000000010005.ok.snap", 10511, t) 40 | } 41 | -------------------------------------------------------------------------------- /snapio/snapwrite.go: -------------------------------------------------------------------------------- 1 | package snapio 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "io" 8 | 9 | "github.com/tinylib/msgp/msgp" 10 | "github.com/viciious/go-tarantool" 11 | ) 12 | 13 | type SpaceData struct { 14 | Space uint 15 | Tuples [][]interface{} 16 | } 17 | 18 | func WriteV12Snapshot(fd io.Writer, data []*SpaceData) error { 19 | header := `SNAP 20 | 0.12 21 | Version: 2.2.1-3-g878e2a42c 22 | Instance: d31ad582-66a6-4b18-96f7-278a7a33ad20 23 | VClock: {1: 10001} 24 | 25 | ` 26 | 27 | w := bufio.NewWriter(fd) 28 | defer w.Flush() 29 | 30 | _, err := w.WriteString(header) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | var lsn uint 36 | for _, s := range data { 37 | space := s.Space 38 | if space == 0 { 39 | space = 10024 40 | } 41 | for _, t := range s.Tuples { 42 | var arr []byte 43 | 44 | arr = msgp.AppendMapHeader(arr, 1) 45 | arr = msgp.AppendUint(arr, tarantool.KeyLSN) 46 | arr = msgp.AppendUint(arr, uint(lsn+1)) 47 | 48 | arr = msgp.AppendMapHeader(arr, 2) 49 | arr = msgp.AppendUint(arr, tarantool.KeySpaceNo) 50 | arr = msgp.AppendUint(arr, uint(space)) 51 | arr = msgp.AppendUint(arr, tarantool.KeyTuple) 52 | arr, _ = msgp.AppendIntf(arr, t) 53 | 54 | var lenbuf []byte 55 | lenbuf = msgp.AppendUint32(lenbuf, uint32(len(arr))) 56 | 57 | if err = binary.Write(w, binary.BigEndian, uint32(XRowFixedHeaderMagic)); err != nil { 58 | return err 59 | } 60 | if _, err = w.Write(lenbuf); err != nil { 61 | return err 62 | } 63 | if _, err = w.Write(bytes.Repeat([]byte{'\x00'}, XRowFixedHeaderSize-len(lenbuf)-4)); err != nil { 64 | return err 65 | } 66 | if _, err = w.Write(arr); err != nil { 67 | return err 68 | } 69 | } 70 | } 71 | 72 | if err = binary.Write(w, binary.BigEndian, uint32(XRowFixedHeaderEof)); err != nil { 73 | return err 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /snapio/testdata/v12/00000000000000000000.ok.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viciious/go-tarantool/30096f5faa57a1ba92227efc22943d3381305c17/snapio/testdata/v12/00000000000000000000.ok.snap -------------------------------------------------------------------------------- /snapio/testdata/v13/00000000000000010005.ok.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viciious/go-tarantool/30096f5faa57a1ba92227efc22943d3381305c17/snapio/testdata/v13/00000000000000010005.ok.snap -------------------------------------------------------------------------------- /subscribe.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "github.com/tinylib/msgp/msgp" 5 | ) 6 | 7 | // Subscribe is the SUBSCRIBE command 8 | type Subscribe struct { 9 | UUID string 10 | ReplicaSetUUID string 11 | VClock VectorClock 12 | Anon bool 13 | } 14 | 15 | var _ Query = (*Subscribe)(nil) 16 | 17 | func (q *Subscribe) GetCommandID() uint { 18 | return SubscribeCommand 19 | } 20 | 21 | // MarshalMsg implements msgp.Marshaler 22 | func (q *Subscribe) MarshalMsg(b []byte) (o []byte, err error) { 23 | o = b 24 | if q.Anon { 25 | o = msgp.AppendMapHeader(o, 4) 26 | 27 | o = msgp.AppendUint(o, KeyReplicaAnon) 28 | o = msgp.AppendBool(o, true) 29 | } else { 30 | o = msgp.AppendMapHeader(o, 3) 31 | } 32 | 33 | o = msgp.AppendUint(o, KeyInstanceUUID) 34 | o = msgp.AppendString(o, q.UUID) 35 | 36 | o = msgp.AppendUint(o, KeyReplicaSetUUID) 37 | o = msgp.AppendString(o, q.ReplicaSetUUID) 38 | 39 | o = msgp.AppendUint(o, KeyVClock) 40 | o = msgp.AppendMapHeader(o, uint32(len(q.VClock))) 41 | for id, lsn := range q.VClock { 42 | o = msgp.AppendUint(o, uint(id)) 43 | o = msgp.AppendUint64(o, lsn) 44 | } 45 | 46 | return o, nil 47 | } 48 | 49 | // UnmarshalMsg implements msgp.Unmarshaler 50 | func (q *Subscribe) UnmarshalMsg([]byte) (buf []byte, err error) { 51 | return buf, ErrNotSupported 52 | } 53 | 54 | type SubscribeResponse struct { 55 | ReplicaSetUUID string 56 | VClock VectorClock 57 | } 58 | 59 | // UnmarshalMsg implements msgp.Unmarshaller 60 | func (sr *SubscribeResponse) UnmarshalMsg(data []byte) (buf []byte, err error) { 61 | // skip binary header 62 | if buf, err = msgp.Skip(data); err != nil { 63 | return 64 | } 65 | 66 | // unmarshal body 67 | var count uint32 68 | 69 | if count, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 70 | return 71 | } 72 | 73 | for ; count > 0; count-- { 74 | var key uint 75 | 76 | if key, buf, err = msgp.ReadUintBytes(buf); err != nil { 77 | return 78 | } 79 | switch key { 80 | case KeyReplicaSetUUID: 81 | var str string 82 | 83 | if str, buf, err = msgp.ReadStringBytes(buf); err != nil { 84 | return 85 | } 86 | sr.ReplicaSetUUID = str 87 | case KeyVClock: 88 | var n uint32 89 | var id uint32 90 | var lsn uint64 91 | 92 | if n, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 93 | return 94 | } 95 | sr.VClock = NewVectorClock() 96 | for ; n > 0; n-- { 97 | if id, buf, err = msgp.ReadUint32Bytes(buf); err != nil { 98 | return 99 | } 100 | if lsn, buf, err = msgp.ReadUint64Bytes(buf); err != nil { 101 | return 102 | } 103 | if !sr.VClock.Follow(id, lsn) { 104 | return buf, ErrVectorClock 105 | } 106 | } 107 | default: 108 | if buf, err = msgp.Skip(buf); err != nil { 109 | return 110 | } 111 | } 112 | } 113 | return 114 | } 115 | -------------------------------------------------------------------------------- /testdata/init.lua: -------------------------------------------------------------------------------- 1 | local tarantool_lastsnapvclock = require("tarantool_lastsnapvclock") 2 | lastsnapvclock = tarantool_lastsnapvclock.lastsnapvclock 3 | box.once('func:lastsnapvclock', function() 4 | box.schema.func.create('lastsnapvclock', {if_not_exists = true}) 5 | end) 6 | 7 | lastsnapfilename = tarantool_lastsnapvclock.lastsnapfilename 8 | box.once('func:lastsnapfilename', function() 9 | box.schema.func.create('lastsnapfilename', {if_not_exists = true}) 10 | end) 11 | 12 | readfile = tarantool_lastsnapvclock.readfile 13 | box.once('func:readfile', function() 14 | box.schema.func.create('readfile', {if_not_exists = true}) 15 | end) 16 | 17 | parsevclock = tarantool_lastsnapvclock.parsevclock 18 | box.once('func:parsevclock', function() 19 | box.schema.func.create('parsevclock', {if_not_exists = true}) 20 | end) 21 | -------------------------------------------------------------------------------- /tnt.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "expvar" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | var packetPool *BinaryPacketPool 10 | var requestPool *cappedRequestPool 11 | var defaultPackData *packData 12 | 13 | func init() { 14 | packetPool = newBinaryPacketPool() 15 | requestPool = newCappedRequestPool() 16 | defaultPackData = newPackData(10000) 17 | } 18 | 19 | type request struct { 20 | opaque interface{} 21 | replyChan chan *AsyncResult 22 | packet *BinaryPacket 23 | startedAt time.Time 24 | resultMode resultUnmarshalMode 25 | } 26 | 27 | type QueryCompleteFn func(interface{}, time.Duration) 28 | 29 | type AsyncResult struct { 30 | ErrorCode uint 31 | Error error 32 | BinaryPacket *BinaryPacket 33 | Connection *Connection 34 | Opaque interface{} 35 | } 36 | 37 | type PerfCount struct { 38 | NetRead *expvar.Int 39 | NetWrite *expvar.Int 40 | NetPacketsIn *expvar.Int 41 | NetPacketsOut *expvar.Int 42 | QueryTimeouts *expvar.Int 43 | QueryComplete QueryCompleteFn 44 | } 45 | 46 | // ReplicaSet is used to store params of the Replica Set. 47 | type ReplicaSet struct { 48 | UUID string 49 | Instances []string // Instances is read-only set of the instances uuid 50 | } 51 | 52 | // NewReplicaSet returns empty ReplicaSet. 53 | func NewReplicaSet() ReplicaSet { 54 | return ReplicaSet{Instances: make([]string, 0, ReplicaSetMaxSize)} 55 | } 56 | 57 | // SetInstance uuid in instance set. 58 | func (rs *ReplicaSet) SetInstance(id uint32, uuid string) bool { 59 | if id >= uint32(cap(rs.Instances)) || len(uuid) != UUIDStrLength { 60 | return false 61 | } 62 | // extend vector by elements needed 63 | if id >= uint32(len(rs.Instances)) { 64 | rs.Instances = rs.Instances[:id+1] 65 | } 66 | rs.Instances[id] = uuid 67 | return true 68 | } 69 | 70 | // Has ReplicaSet specified instance? 71 | func (rs *ReplicaSet) Has(id uint32) bool { 72 | return id < uint32(len(rs.Instances)) 73 | } 74 | 75 | // VectorClock is used to store logical clocks (direct dependency clock implementation). 76 | // Zero index is always reserved for internal use. 77 | // You can get any lsn indexing VectorClock by instance ID directly (without any index offset). 78 | // One can count instances in vector just using built-in len function. 79 | type VectorClock []uint64 80 | 81 | // NewVectorClock returns VectorClock with clocks equal to the given lsn elements sequentially. 82 | // Empty VectorClock would be returned if no lsn elements is given. 83 | func NewVectorClock(lsns ...uint64) VectorClock { 84 | if len(lsns) == 0 { 85 | return make([]uint64, 0, VClockMax) 86 | } 87 | // zero index is reserved 88 | vc := make([]uint64, len(lsns)+1, VClockMax) 89 | copy(vc[1:], lsns) 90 | return vc 91 | } 92 | 93 | // Follow the clocks. 94 | // Update vector clock with given clock part. 95 | func (vc *VectorClock) Follow(id uint32, lsn uint64) bool { 96 | if id >= uint32(cap(*vc)) { 97 | return false 98 | } 99 | // extend vector by elements needed 100 | if id >= uint32(len(*vc)) { 101 | *vc = (*vc)[:id+1] 102 | } 103 | atomic.StoreUint64(&(*vc)[id], lsn) 104 | return true 105 | } 106 | 107 | func (vc VectorClock) Clone() VectorClock { 108 | clone := make([]uint64, len(vc), cap(vc)) 109 | for i := 0; i < len(vc); i++ { 110 | clone[i] = atomic.LoadUint64(&vc[i]) 111 | } 112 | return clone 113 | } 114 | 115 | // LSN is the sum of the Clocks. 116 | func (vc VectorClock) LSN() uint64 { 117 | result := uint64(0) 118 | for _, lsn := range vc { 119 | result += lsn 120 | } 121 | return result 122 | } 123 | 124 | // Has VectorClock specified ID? 125 | func (vc VectorClock) Has(id uint32) bool { 126 | return id < uint32(len(vc)) 127 | } 128 | -------------------------------------------------------------------------------- /tnt_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestVectorClockClone(t *testing.T) { 13 | require := require.New(t) 14 | vc := NewVectorClock(1, 2, 3, 4, 5) 15 | clone := vc.Clone() 16 | require.Equal(vc, clone) 17 | } 18 | 19 | func TestVectorClockFollow(t *testing.T) { 20 | require := require.New(t) 21 | vc := NewVectorClock(1, 2, 3, 4, 5) 22 | require.Equal(VectorClock{0, 1, 2, 3, 4, 5}, vc) 23 | vc.Follow(0, 1) 24 | require.Equal(VectorClock{1, 1, 2, 3, 4, 5}, vc) 25 | vc.Follow(1, 2) 26 | require.Equal(VectorClock{1, 2, 2, 3, 4, 5}, vc) 27 | vc.Follow(2, 3) 28 | require.Equal(VectorClock{1, 2, 3, 3, 4, 5}, vc) 29 | vc.Follow(7, 42) 30 | require.Equal(VectorClock{1, 2, 3, 3, 4, 5, 0, 42}, vc) 31 | } 32 | 33 | // makes sense only with -race flag 34 | func TestVectorClockRace(t *testing.T) { 35 | vc := NewVectorClock(1, 2, 3, 4, 5) 36 | var wg sync.WaitGroup 37 | 38 | wg.Add(1) 39 | go func(vc VectorClock) { 40 | defer wg.Done() 41 | for id := uint32(1); id < 10; id++ { 42 | for i := 0; i < 10; i++ { 43 | vc.Follow(id, rand.Uint64()) 44 | time.Sleep(10 * time.Millisecond) 45 | } 46 | } 47 | }(vc) 48 | 49 | wg.Add(1) 50 | go func(vc VectorClock) { 51 | defer wg.Done() 52 | for i := 1; i < 100; i++ { 53 | clone := vc.Clone() 54 | _ = clone 55 | time.Sleep(10 * time.Millisecond) 56 | } 57 | }(vc) 58 | 59 | wg.Wait() 60 | } 61 | -------------------------------------------------------------------------------- /tuple.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | type Bytes []byte 4 | type Tuple []interface{} 5 | -------------------------------------------------------------------------------- /typeconv/int.go: -------------------------------------------------------------------------------- 1 | package typeconv 2 | 3 | func IntfToInt(number interface{}) (int, bool) { 4 | switch value := number.(type) { 5 | default: 6 | return 0, false 7 | case int: 8 | return value, true 9 | case uint: 10 | return int(value), true 11 | case int8: 12 | return int(value), true 13 | case uint8: 14 | return int(value), true 15 | case int16: 16 | return int(value), true 17 | case uint16: 18 | return int(value), true 19 | case int32: 20 | return int(value), true 21 | case uint32: 22 | return int(value), true 23 | case int64: 24 | return int(value), true 25 | case uint64: 26 | return int(value), true 27 | } 28 | } 29 | 30 | func IntfToUint(number interface{}) (uint, bool) { 31 | switch value := number.(type) { 32 | default: 33 | return 0, false 34 | case int: 35 | return uint(value), true 36 | case uint: 37 | return value, true 38 | case int8: 39 | return uint(value), true 40 | case uint8: 41 | return uint(value), true 42 | case int16: 43 | return uint(value), true 44 | case uint16: 45 | return uint(value), true 46 | case int32: 47 | return uint(value), true 48 | case uint32: 49 | return uint(value), true 50 | case int64: 51 | return uint(value), true 52 | case uint64: 53 | return uint(value), true 54 | } 55 | } 56 | 57 | func IntfToInt32(number interface{}) (int32, bool) { 58 | if conv, ok := IntfToInt(number); ok { 59 | return int32(conv), true 60 | } 61 | return 0, false 62 | } 63 | 64 | func IntfToUint32(number interface{}) (uint32, bool) { 65 | if conv, ok := IntfToUint(number); ok { 66 | return uint32(conv), true 67 | } 68 | return 0, false 69 | } 70 | 71 | func IntfToInt64(number interface{}) (int64, bool) { 72 | switch value := number.(type) { 73 | default: 74 | return 0, false 75 | case int: 76 | return int64(value), true 77 | case uint: 78 | return int64(value), true 79 | case int8: 80 | return int64(value), true 81 | case uint8: 82 | return int64(value), true 83 | case int16: 84 | return int64(value), true 85 | case uint16: 86 | return int64(value), true 87 | case int32: 88 | return int64(value), true 89 | case uint32: 90 | return int64(value), true 91 | case int64: 92 | return value, true 93 | case uint64: 94 | return int64(value), true 95 | } 96 | } 97 | 98 | func IntfToUint64(number interface{}) (uint64, bool) { 99 | switch value := number.(type) { 100 | default: 101 | return 0, false 102 | case int: 103 | return uint64(value), true 104 | case uint: 105 | return uint64(value), true 106 | case int8: 107 | return uint64(value), true 108 | case uint8: 109 | return uint64(value), true 110 | case int16: 111 | return uint64(value), true 112 | case uint16: 113 | return uint64(value), true 114 | case int32: 115 | return uint64(value), true 116 | case uint32: 117 | return uint64(value), true 118 | case int64: 119 | return uint64(value), true 120 | case uint64: 121 | return value, true 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /update.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tinylib/msgp/msgp" 7 | ) 8 | 9 | type Update struct { 10 | Space interface{} 11 | Index interface{} 12 | Key interface{} 13 | KeyTuple []interface{} 14 | Set []Operator 15 | } 16 | 17 | var _ Query = (*Update)(nil) 18 | 19 | func (q *Update) GetCommandID() uint { 20 | return UpdateCommand 21 | } 22 | 23 | func (q *Update) packMsg(data *packData, b []byte) (o []byte, err error) { 24 | o = b 25 | o = msgp.AppendMapHeader(o, 4) 26 | 27 | if o, err = data.packSpace(q.Space, o); err != nil { 28 | return o, err 29 | } 30 | 31 | if o, err = data.packIndex(q.Space, q.Index, o); err != nil { 32 | return o, err 33 | } 34 | 35 | if q.Key != nil { 36 | o = append(o, data.packedSingleKey...) 37 | if o, err = msgp.AppendIntf(o, q.Key); err != nil { 38 | return o, err 39 | } 40 | } else if q.KeyTuple != nil { 41 | o = msgp.AppendUint(o, KeyKey) 42 | if o, err = msgp.AppendIntf(o, q.KeyTuple); err != nil { 43 | return o, err 44 | } 45 | } 46 | 47 | o = msgp.AppendUint(o, KeyTuple) 48 | o = msgp.AppendArrayHeader(o, uint32(len(q.Set))) 49 | for _, op := range q.Set { 50 | if o, err = marshalOperator(op, o); err != nil { 51 | return o, err 52 | } 53 | } 54 | 55 | return o, nil 56 | } 57 | 58 | // MarshalMsg implements msgp.Marshaler 59 | func (q *Update) MarshalMsg(b []byte) ([]byte, error) { 60 | return q.packMsg(defaultPackData, b) 61 | } 62 | 63 | // UnmarshalMsg implements msgp.Unmarshaler 64 | func (q *Update) UnmarshalMsg(data []byte) (buf []byte, err error) { 65 | var i uint32 66 | var k uint 67 | var t interface{} 68 | 69 | q.Space = nil 70 | q.Index = 0 71 | 72 | buf = data 73 | if i, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 74 | return 75 | } 76 | 77 | for ; i > 0; i-- { 78 | if k, buf, err = msgp.ReadUintBytes(buf); err != nil { 79 | return 80 | } 81 | 82 | switch k { 83 | case KeySpaceNo: 84 | if q.Space, buf, err = msgp.ReadUintBytes(buf); err != nil { 85 | return 86 | } 87 | case KeyIndexNo: 88 | if q.Index, buf, err = msgp.ReadUintBytes(buf); err != nil { 89 | return 90 | } 91 | case KeyKey: 92 | t, buf, err = msgp.ReadIntfBytes(buf) 93 | if err != nil { 94 | return 95 | } 96 | 97 | if q.KeyTuple = t.([]interface{}); q.KeyTuple == nil { 98 | return buf, errors.New("interface type is not []interface{}") 99 | } 100 | 101 | if len(q.KeyTuple) == 1 { 102 | q.Key = q.KeyTuple[0] 103 | q.KeyTuple = nil 104 | } 105 | case KeyTuple: 106 | var len uint32 107 | if len, buf, err = msgp.ReadArrayHeaderBytes(buf); err != nil { 108 | return 109 | } 110 | 111 | q.Set = make([]Operator, len) 112 | for j := uint32(0); j < len; j++ { 113 | if q.Set[j], buf, err = unmarshalOperator(buf); err != nil { 114 | return 115 | } 116 | } 117 | } 118 | } 119 | 120 | if q.Space == nil { 121 | return buf, errors.New("upate.Unpack: no space specified") 122 | } 123 | 124 | return 125 | } 126 | -------------------------------------------------------------------------------- /update_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestUpdate(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | tarantoolConfig := ` 13 | local s = box.schema.space.create('tester') 14 | s:create_index('primary', { 15 | type = 'hash', 16 | parts = {1, 'NUM'} 17 | }) 18 | s:create_index('secondary', { 19 | type = 'hash', 20 | parts = {1, 'NUM', 2, 'STR'} 21 | }) 22 | local t = s:insert({1, 'First record', 15}) 23 | s:insert({2, 'Test', 15}) 24 | 25 | box.schema.user.create('writer', {password = 'writer'}) 26 | box.schema.user.grant('writer', 'write', 'space', 'tester') 27 | ` 28 | 29 | box, err := NewBox(tarantoolConfig, nil) 30 | if !assert.NoError(err) { 31 | return 32 | } 33 | defer box.Close() 34 | 35 | conn, err := box.Connect(&Options{ 36 | User: "writer", 37 | Password: "writer", 38 | }) 39 | assert.NoError(err) 40 | assert.NotNil(conn) 41 | 42 | defer conn.Close() 43 | 44 | do := func(conn *Connection, query *Update, expected [][]interface{}) { 45 | var err error 46 | var buf []byte 47 | 48 | buf, err = query.packMsg(conn.packData, buf) 49 | 50 | if assert.NoError(err) { 51 | var query2 = &Update{} 52 | _, err = query2.UnmarshalMsg(buf) 53 | if assert.NoError(err) { 54 | assert.Equal(uint(512), query2.Space) 55 | if query.Key != nil { 56 | switch query.Key.(type) { 57 | case int: 58 | assert.Equal(query.Key, query2.Key) 59 | default: 60 | assert.Equal(query.Key, query2.Key) 61 | } 62 | } 63 | if query.KeyTuple != nil { 64 | assert.Equal(query.KeyTuple, query2.KeyTuple) 65 | } 66 | if query.Index != nil { 67 | switch query.Index.(type) { 68 | case string: 69 | assert.Equal(conn.packData.indexMap[512][query.Index.(string)], uint64(query2.Index.(uint))) 70 | default: 71 | assert.Equal(query.Index, query2.Index) 72 | } 73 | } 74 | assert.Equal(query.Set, query2.Set) 75 | } 76 | } 77 | 78 | data, err := conn.Execute(query) 79 | if assert.NoError(err) { 80 | assert.Equal(expected, data) 81 | } 82 | } 83 | 84 | do(conn, &Update{ 85 | Space: "tester", 86 | Index: "primary", 87 | Key: int64(1), 88 | Set: []Operator{ 89 | &OpAdd{ 90 | Field: 2, 91 | Argument: 17, 92 | }, 93 | &OpAssign{ 94 | Field: 1, 95 | Argument: "Hello World", 96 | }, 97 | }}, 98 | [][]interface{}{ 99 | {int64(1), "Hello World", int64(32)}, 100 | }) 101 | 102 | do(conn, &Update{ 103 | Space: "tester", 104 | Index: "secondary", 105 | KeyTuple: []interface{}{int64(2), "Test"}, 106 | Set: []Operator{ 107 | &OpInsert{ 108 | Before: 2, 109 | Argument: "Hello World", 110 | }, 111 | &OpSub{ 112 | Field: 3, 113 | Argument: 57, 114 | }, 115 | }}, 116 | [][]interface{}{ 117 | {int64(2), "Test", "Hello World", int64(-42)}, 118 | }) 119 | } 120 | 121 | func BenchmarkUpdatePack(b *testing.B) { 122 | buf := make([]byte, 0) 123 | 124 | for i := 0; i < b.N; i++ { 125 | buf, _ = (&Update{ 126 | Space: 1, 127 | Index: 0, 128 | Key: 1, 129 | Set: []Operator{ 130 | &OpAdd{ 131 | Field: 2, 132 | Argument: 17, 133 | }, 134 | &OpAssign{ 135 | Field: 1, 136 | Argument: "Hello World", 137 | }, 138 | }, 139 | }).MarshalMsg(buf[:0]) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /upsert.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tinylib/msgp/msgp" 7 | ) 8 | 9 | type Upsert struct { 10 | Space interface{} 11 | Tuple []interface{} 12 | Set []Operator 13 | } 14 | 15 | var _ Query = (*Upsert)(nil) 16 | 17 | func (q *Upsert) GetCommandID() uint { 18 | return UpsertCommand 19 | } 20 | 21 | func (q *Upsert) packMsg(data *packData, b []byte) (o []byte, err error) { 22 | o = b 23 | o = msgp.AppendMapHeader(o, 3) 24 | 25 | if o, err = data.packSpace(q.Space, o); err != nil { 26 | return o, err 27 | } 28 | 29 | o = msgp.AppendUint(o, KeyTuple) 30 | if o, err = msgp.AppendIntf(o, q.Tuple); err != nil { 31 | return o, err 32 | } 33 | 34 | o = msgp.AppendUint(o, KeyDefTuple) 35 | o = msgp.AppendArrayHeader(o, uint32(len(q.Set))) 36 | for _, op := range q.Set { 37 | if o, err = marshalOperator(op, o); err != nil { 38 | return o, err 39 | } 40 | } 41 | 42 | return o, nil 43 | } 44 | 45 | // MarshalMsg implements msgp.Marshaler 46 | func (q *Upsert) MarshalMsg(b []byte) ([]byte, error) { 47 | return q.packMsg(defaultPackData, b) 48 | } 49 | 50 | // UnmarshalMsg implements msgp.Unmarshaler 51 | func (q *Upsert) UnmarshalMsg(data []byte) (buf []byte, err error) { 52 | var i uint32 53 | var k uint 54 | var t interface{} 55 | 56 | q.Space = nil 57 | 58 | buf = data 59 | if i, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 60 | return 61 | } 62 | 63 | for ; i > 0; i-- { 64 | if k, buf, err = msgp.ReadUintBytes(buf); err != nil { 65 | return 66 | } 67 | 68 | switch k { 69 | case KeySpaceNo: 70 | if q.Space, buf, err = msgp.ReadUintBytes(buf); err != nil { 71 | return 72 | } 73 | case KeyTuple: 74 | t, buf, err = msgp.ReadIntfBytes(buf) 75 | if err != nil { 76 | return 77 | } 78 | 79 | if q.Tuple = t.([]interface{}); q.Tuple == nil { 80 | return buf, errors.New("interface type is not []interface{}") 81 | } 82 | case KeyDefTuple: 83 | var len uint32 84 | if len, buf, err = msgp.ReadArrayHeaderBytes(buf); err != nil { 85 | return 86 | } 87 | 88 | q.Set = make([]Operator, len) 89 | for j := uint32(0); j < len; j++ { 90 | if q.Set[j], buf, err = unmarshalOperator(buf); err != nil { 91 | return 92 | } 93 | } 94 | } 95 | } 96 | 97 | if q.Space == nil { 98 | return buf, errors.New("upsert.Unpack: no space specified") 99 | } 100 | 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /upsert_test.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestUpsert(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | tarantoolConfig := ` 13 | local s = box.schema.space.create('tester') 14 | s:create_index('primary', { 15 | type = 'hash', 16 | parts = {1, 'NUM'} 17 | }) 18 | local t = s:insert({1, 'First record', 15}) 19 | 20 | box.schema.user.create('writer', {password = 'writer'}) 21 | box.schema.user.grant('writer', 'write', 'space', 'tester') 22 | ` 23 | 24 | box, err := NewBox(tarantoolConfig, nil) 25 | if !assert.NoError(err) { 26 | return 27 | } 28 | defer box.Close() 29 | 30 | conn, err := box.Connect(&Options{ 31 | User: "writer", 32 | Password: "writer", 33 | }) 34 | assert.NoError(err) 35 | assert.NotNil(conn) 36 | 37 | defer conn.Close() 38 | 39 | do := func(connectOptions *Options, query *Select, expected [][]interface{}) { 40 | conn, err := box.Connect(connectOptions) 41 | assert.NoError(err) 42 | assert.NotNil(conn) 43 | 44 | defer conn.Close() 45 | 46 | data, err := conn.Execute(query) 47 | 48 | if assert.NoError(err) { 49 | assert.Equal(expected, data) 50 | } 51 | } 52 | 53 | // test update 54 | data, err := conn.Execute(&Upsert{ 55 | Space: "tester", 56 | Tuple: []interface{}{1}, 57 | Set: []Operator{ 58 | &OpAdd{ 59 | Field: 2, 60 | Argument: 17, 61 | }, 62 | &OpAssign{ 63 | Field: 1, 64 | Argument: "Hello World", 65 | }, 66 | }, 67 | }) 68 | 69 | if assert.NoError(err) { 70 | assert.Equal([][]interface{}{}, data) 71 | } 72 | 73 | // check update 74 | do(nil, 75 | &Select{ 76 | Space: "tester", 77 | Key: 1, 78 | }, 79 | [][]interface{}{ 80 | {int64(1), "Hello World", int64(32)}, 81 | }, 82 | ) 83 | 84 | // test insert 85 | data, err = conn.Execute(&Upsert{ 86 | Space: "tester", 87 | Tuple: []interface{}{2, "Second", 16}, 88 | Set: []Operator{ 89 | &OpAdd{ 90 | Field: 2, 91 | Argument: 17, 92 | }, 93 | &OpAssign{ 94 | Field: 1, 95 | Argument: "Hello World", 96 | }, 97 | }, 98 | }) 99 | 100 | if assert.NoError(err) { 101 | assert.Equal([][]interface{}{}, data) 102 | } 103 | 104 | // check insert 105 | do(nil, 106 | &Select{ 107 | Space: "tester", 108 | Key: 2, 109 | }, 110 | [][]interface{}{ 111 | {int64(2), "Second", int64(16)}, 112 | }, 113 | ) 114 | 115 | } 116 | 117 | func BenchmarkUpsertPack(b *testing.B) { 118 | buf := make([]byte, 0) 119 | 120 | for i := 0; i < b.N; i++ { 121 | buf, _ = (&Upsert{ 122 | Space: 1, 123 | Tuple: []interface{}{1}, 124 | Set: []Operator{ 125 | &OpAdd{ 126 | Field: 2, 127 | Argument: 17, 128 | }, 129 | &OpAssign{ 130 | Field: 1, 131 | Argument: "Hello World", 132 | }, 133 | }, 134 | }).MarshalMsg(buf[:0]) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /vclock.go: -------------------------------------------------------------------------------- 1 | package tarantool 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tinylib/msgp/msgp" 7 | ) 8 | 9 | // VClock response (in OK). 10 | // Similar to Result struct 11 | type VClock struct { 12 | RequestID uint64 // RequestID is SYNC field; 13 | InstanceID uint32 14 | VClock VectorClock 15 | } 16 | 17 | var _ Query = (*VClock)(nil) 18 | 19 | // String implements Stringer interface. 20 | func (p *VClock) String() string { 21 | return fmt.Sprintf("VClock ReqID:%v Replica:%v, VClock:%#v", 22 | p.RequestID, p.InstanceID, p.VClock) 23 | } 24 | 25 | func (p *VClock) GetCommandID() uint { 26 | return OKCommand 27 | } 28 | 29 | func (p *VClock) packMsg(data *packData, b []byte) (o []byte, err error) { 30 | o = b 31 | o = msgp.AppendMapHeader(o, 1) 32 | o = msgp.AppendUint(o, KeyVClock) 33 | o = msgp.AppendMapHeader(o, uint32(len(p.VClock[1:]))) 34 | 35 | for i, lsn := range p.VClock[1:] { 36 | o = msgp.AppendUint32(o, uint32(i)) 37 | o = msgp.AppendUint64(o, lsn) 38 | } 39 | 40 | return o, nil 41 | } 42 | 43 | // MarshalMsg implements msgp.Marshaler 44 | func (p *VClock) MarshalMsg(b []byte) ([]byte, error) { 45 | return p.packMsg(defaultPackData, b) 46 | } 47 | 48 | // UnmarshalMsg implements msgp.Unmarshaller 49 | func (p *VClock) UnmarshalMsg(data []byte) (buf []byte, err error) { 50 | buf = data 51 | if buf, err = p.UnmarshalBinaryHeader(buf); err != nil { 52 | return buf, err 53 | } 54 | if len(buf) == 0 { 55 | return buf, nil 56 | } 57 | return p.UnmarshalBinaryBody(buf) 58 | } 59 | 60 | func (p *VClock) UnmarshalBinaryHeader(data []byte) (buf []byte, err error) { 61 | var i uint32 62 | 63 | buf = data 64 | if i, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 65 | return 66 | } 67 | 68 | for ; i > 0; i-- { 69 | var key uint 70 | 71 | if key, buf, err = msgp.ReadUintBytes(buf); err != nil { 72 | return 73 | } 74 | 75 | switch key { 76 | case KeySync: 77 | if p.RequestID, buf, err = msgp.ReadUint64Bytes(buf); err != nil { 78 | return 79 | } 80 | case KeySchemaID: 81 | if _, buf, err = msgp.ReadUint64Bytes(buf); err != nil { 82 | return 83 | } 84 | case KeyInstanceID: 85 | if p.InstanceID, buf, err = msgp.ReadUint32Bytes(buf); err != nil { 86 | return 87 | } 88 | default: 89 | if buf, err = msgp.Skip(buf); err != nil { 90 | return 91 | } 92 | } 93 | } 94 | return 95 | } 96 | 97 | func (p *VClock) UnmarshalBinaryBody(data []byte) (buf []byte, err error) { 98 | var count uint32 99 | 100 | buf = data 101 | if count, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 102 | return 103 | } 104 | 105 | for ; count > 0; count-- { 106 | var key uint 107 | 108 | if key, buf, err = msgp.ReadUintBytes(buf); err != nil { 109 | return 110 | } 111 | switch key { 112 | case KeyVClock: 113 | var n uint32 114 | var id uint32 115 | var lsn uint64 116 | 117 | if n, buf, err = msgp.ReadMapHeaderBytes(buf); err != nil { 118 | return 119 | } 120 | p.VClock = NewVectorClock() 121 | for ; n > 0; n-- { 122 | if id, buf, err = msgp.ReadUint32Bytes(buf); err != nil { 123 | return 124 | } 125 | if lsn, buf, err = msgp.ReadUint64Bytes(buf); err != nil { 126 | return 127 | } 128 | if !p.VClock.Follow(id, lsn) { 129 | return buf, ErrVectorClock 130 | } 131 | } 132 | default: 133 | if buf, err = msgp.Skip(buf); err != nil { 134 | return 135 | } 136 | } 137 | } 138 | return 139 | } 140 | --------------------------------------------------------------------------------