├── .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 [](https://godoc.org/github.com/viciious/go-tarantool) [](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 |
--------------------------------------------------------------------------------