tarantool16 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +80 -0
- data/Rakefile +2 -0
- data/lib/tarantool16/connection/common.rb +196 -0
- data/lib/tarantool16/connection/dumb.rb +122 -0
- data/lib/tarantool16/connection/response.rb +47 -0
- data/lib/tarantool16/consts.rb +94 -0
- data/lib/tarantool16/db.rb +198 -0
- data/lib/tarantool16/dumb_db.rb +118 -0
- data/lib/tarantool16/errors.rb +82 -0
- data/lib/tarantool16/query.rb +8 -0
- data/lib/tarantool16/response.rb +36 -0
- data/lib/tarantool16/schema.rb +269 -0
- data/lib/tarantool16/version.rb +3 -0
- data/lib/tarantool16.rb +17 -0
- data/tarantool16.gemspec +25 -0
- metadata +104 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7438bad2e35b3f56d1949664f4e51a1c866550ed
|
4
|
+
data.tar.gz: 802d8dd75d560a73b7b30bf8815908d49a484262
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8cae0fe3c55291a6f2c503f7a4611f950e28d2679e8950b149697090ec185d6f0bfd875ccaf002899aa9b6020cda218c12578ad10a547ea00b3264d67e409d46
|
7
|
+
data.tar.gz: 7281531d361f6bd6032c36d37c40527aacf0e9607efbc14cd1da6615d952f4e37994b35f1f7930cf62d8c67f7cafab895b72679dce367f8a678d84a7e51cb15c
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Sokolov Yura aka funny_falcon
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# Tarantool16
|
2
|
+
|
3
|
+
This is adapter for (tarantool)[http://tarantool.org] version 1.6.
|
4
|
+
|
5
|
+
(adapter for version <=1.5 is called (tarantool)[https://github.org/tarantoool/tarantool-ruby])
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'tarantool16'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install tarantool16
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Currently only simple single threaded one-request-at-time connection implemented.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require 'tarantool16'
|
29
|
+
|
30
|
+
db = Tarantool16.new host:'localhost:33013'
|
31
|
+
#db = Tarantool16.new host:'localhost:33013', user:'tester', password:'testpass'
|
32
|
+
|
33
|
+
# select from '_space' space info about 'test' table
|
34
|
+
# returns array of tuples as an array
|
35
|
+
tar.get(272, ['test'], index: 2)
|
36
|
+
|
37
|
+
# same, but return tuples as a hashes
|
38
|
+
tar.get(272, ['test'], index: 2, hash: true)
|
39
|
+
|
40
|
+
# same with names
|
41
|
+
# Names and index descriptions are fetched from tarantool.
|
42
|
+
# Index is autodetected by key names
|
43
|
+
tar.get(:_space, {name: 'test'})
|
44
|
+
|
45
|
+
# get all spaces
|
46
|
+
tar.select(:_space, nil, iterator: :all)
|
47
|
+
tar.select(:_space, nil, iterator: :all, hash: true)
|
48
|
+
|
49
|
+
tar.select(:_space, [512], index: 0, iterator: :>=, hash: true)
|
50
|
+
|
51
|
+
# override tuple field definition
|
52
|
+
tar.define_fields(:test, [:id, :name, :value])
|
53
|
+
|
54
|
+
tar.insert(:test, [1, 'buddy', [1,2,3]])
|
55
|
+
tar.replace(:test, [1, 'buddy!', [2,3,4]])
|
56
|
+
tar.update(:test, [1], [[':', 1, 6, 6, '?']])
|
57
|
+
#tar.update(:test, [1], [[':', 1, 6, 6, '?']], index: 0)
|
58
|
+
tar.delete(:test, [1])
|
59
|
+
#tar.delete(:test, [1], index: 0)
|
60
|
+
|
61
|
+
tar.insert(:test, {id: 1, name: 'buddy', value: [1,2,3]})
|
62
|
+
tar.replace(:test, {id: 1, name: 'buddy!', value: [2,3,4]})
|
63
|
+
tar.update(:test, {id: 1}, {name: [':', 6,6,'?']})
|
64
|
+
tar.delete(:test, {id: 1})
|
65
|
+
|
66
|
+
# note: currenlty there is no documented way to store field definition in an tarantool
|
67
|
+
# but actually you can do it with this
|
68
|
+
tar.update(:_space, {name: 'test'}, {format: [:=, [{name: :id, type: :num}, {name: :name, type: :str}, {name: :value, type: '*'}]]})
|
69
|
+
|
70
|
+
```
|
71
|
+
|
72
|
+
## Contributing
|
73
|
+
|
74
|
+
1. Fork it ( https://github.com/funny-falcon/tarantool16/fork )
|
75
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
76
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
77
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
78
|
+
5. Create a new Pull Request
|
79
|
+
|
80
|
+
Or simply fill an issue
|
data/Rakefile
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
require 'msgpack'
|
2
|
+
require 'openssl'
|
3
|
+
require 'openssl/digest'
|
4
|
+
require 'tarantool16/consts'
|
5
|
+
require_relative 'response'
|
6
|
+
|
7
|
+
module Tarantool16
|
8
|
+
module Connection
|
9
|
+
class Error < ::StandardError; end
|
10
|
+
class ConnectionError < Error; end
|
11
|
+
class CouldNotConnect < ConnectionError; end
|
12
|
+
class Disconnected < ConnectionError; end
|
13
|
+
class Retry < ConnectionError; end
|
14
|
+
class UnexpectedResponse < Error; end
|
15
|
+
|
16
|
+
module Common
|
17
|
+
DEFAULT_RECONNECT = 0.2
|
18
|
+
attr :host, :user
|
19
|
+
def _init_common(host, opts)
|
20
|
+
@host = host
|
21
|
+
@user = opts[:user]
|
22
|
+
if opts[:password]
|
23
|
+
@passwd = ::OpenSSL::Digest::SHA1.digest(opts[:password])
|
24
|
+
end
|
25
|
+
if opts[:reconnect].nil?
|
26
|
+
@reconnect_timeout = DEFAULT_RECONNECT
|
27
|
+
@reconnect = true
|
28
|
+
elsif Numeric === opts[:reconnect]
|
29
|
+
@reconnect_timeout = opts[:reconnect]
|
30
|
+
@reconnect = true
|
31
|
+
else
|
32
|
+
@reconnect = false
|
33
|
+
end
|
34
|
+
@p = MessagePack::Packer.new
|
35
|
+
@u = MessagePack::Unpacker.new
|
36
|
+
@s = 0
|
37
|
+
end
|
38
|
+
|
39
|
+
if ::Process.respond_to?(:clock_gettime)
|
40
|
+
if defined?(::Process::CLOCK_MONOTONIC_COARSE)
|
41
|
+
CLOCK_KIND = ::Process::CLOCK_MONOTONIC_COARSE
|
42
|
+
elsif defined?(::Process::CLOCK_MONOTONIC_FAST)
|
43
|
+
CLOCK_KIND = ::Process::CLOCK_MONOTONIC_FAST
|
44
|
+
elsif defined?(::Process::CLOCK_MONOTONIC)
|
45
|
+
CLOCK_KIND = ::Process::CLOCK_MONOTONIC
|
46
|
+
else
|
47
|
+
CLOCK_KIND = ::Process::CLOCK_REALTIME
|
48
|
+
end
|
49
|
+
def now_f
|
50
|
+
::Process.clock_gettime(CLOCK_KIND)
|
51
|
+
end
|
52
|
+
else
|
53
|
+
def now_f
|
54
|
+
Time.now.to_f
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def next_sync
|
59
|
+
@s = @s % 0x3fffffff + 1
|
60
|
+
end
|
61
|
+
|
62
|
+
def format_request(code, sync, body)
|
63
|
+
@p.write(0x01020304).
|
64
|
+
write_map_header(2).
|
65
|
+
write(IPROTO_CODE).write(code).
|
66
|
+
write(IPROTO_SYNC).write(sync).
|
67
|
+
write(body)
|
68
|
+
sz = @p.size - 5
|
69
|
+
str = @p.to_s
|
70
|
+
@p.clear
|
71
|
+
# fix bigendian size
|
72
|
+
str.setbyte(4, sz)
|
73
|
+
str.setbyte(3, sz>>8)
|
74
|
+
str.setbyte(2, sz>>16)
|
75
|
+
str.setbyte(1, sz>>24)
|
76
|
+
str
|
77
|
+
end
|
78
|
+
|
79
|
+
def format_authenticate(user, pass1, salt)
|
80
|
+
pass2 = ::OpenSSL::Digest::SHA1.digest(pass1)
|
81
|
+
scramble = ::OpenSSL::Digest::SHA1.new(salt).update(pass2).digest
|
82
|
+
pints = pass1.unpack('L*')
|
83
|
+
sints = scramble.unpack('L*')
|
84
|
+
pints.size.times{|i| sints[i] ^= pints[i] }
|
85
|
+
format_request(REQUEST_TYPE_AUTHENTICATE, next_sync, {
|
86
|
+
IPROTO_USER_NAME => user,
|
87
|
+
IPROTO_TUPLE => [ 'chap-sha1', sints.pack('L*') ]
|
88
|
+
})
|
89
|
+
end
|
90
|
+
|
91
|
+
def parse_greeting(greeting)
|
92
|
+
@greeting = greeting[0, 64]
|
93
|
+
@salt = greeting[64..-1].unpack('m')[0][0,20]
|
94
|
+
end
|
95
|
+
|
96
|
+
def parse_size(str)
|
97
|
+
@u.feed(str)
|
98
|
+
n = @u.read
|
99
|
+
unless Integer === n
|
100
|
+
return UnexpectedResponse.new("wanted response size, got #{n.inspect}")
|
101
|
+
end
|
102
|
+
n
|
103
|
+
rescue ::MessagePack::UnpackError, ::MessagePack::TypeError => e
|
104
|
+
e
|
105
|
+
end
|
106
|
+
|
107
|
+
def parse_response(str)
|
108
|
+
sync = nil
|
109
|
+
@u.feed(str)
|
110
|
+
n = @u.read_map_header
|
111
|
+
while n > 0
|
112
|
+
cd = @u.read
|
113
|
+
vl = @u.read
|
114
|
+
case cd
|
115
|
+
when IPROTO_SYNC
|
116
|
+
sync = vl
|
117
|
+
when IPROTO_CODE
|
118
|
+
code = vl
|
119
|
+
end
|
120
|
+
n -= 1
|
121
|
+
end
|
122
|
+
if sync == nil
|
123
|
+
return Option.error(nil, UnexpectedResponse, "Mailformed response: no sync")
|
124
|
+
elsif code == nil
|
125
|
+
return Option.error(nil, UnexpectedResponse, "Mailformed response: no code for sync=#{sync}")
|
126
|
+
end
|
127
|
+
unless @u.buffer.empty?
|
128
|
+
bmap = @u.read
|
129
|
+
body = bmap[IPROTO_DATA] || bmap[IPROTO_ERROR]
|
130
|
+
else
|
131
|
+
body = nil
|
132
|
+
end
|
133
|
+
Option.ok(sync, code, body)
|
134
|
+
rescue ::MessagePack::UnpackError, ::MessagePack::TypeError => e
|
135
|
+
Option.error(sync, e, nil)
|
136
|
+
end
|
137
|
+
|
138
|
+
def host_port
|
139
|
+
h, p = @host.split(':')
|
140
|
+
[h, p.to_i]
|
141
|
+
end
|
142
|
+
|
143
|
+
def _insert(space_no, tuple, cb)
|
144
|
+
req = {IPROTO_SPACE_ID => space_no,
|
145
|
+
IPROTO_TUPLE => tuple}
|
146
|
+
send_request(REQUEST_TYPE_INSERT, req, cb)
|
147
|
+
end
|
148
|
+
|
149
|
+
def _replace(space_no, tuple, cb)
|
150
|
+
req = {IPROTO_SPACE_ID => space_no,
|
151
|
+
IPROTO_TUPLE => tuple}
|
152
|
+
send_request(REQUEST_TYPE_REPLACE, req, cb)
|
153
|
+
end
|
154
|
+
|
155
|
+
def _delete(space_no, index_no, key, cb)
|
156
|
+
req = {IPROTO_SPACE_ID => space_no,
|
157
|
+
IPROTO_INDEX_ID => index_no,
|
158
|
+
IPROTO_KEY => key}
|
159
|
+
send_request(REQUEST_TYPE_DELETE, req, cb)
|
160
|
+
end
|
161
|
+
|
162
|
+
def _select(space_no, index_no, key, offset, limit, iterator, cb)
|
163
|
+
iterator ||= ::Tarantool16::ITERATOR_EQ
|
164
|
+
unless Integer === iterator
|
165
|
+
iterator = ::Tarantool16.iter(iterator)
|
166
|
+
end
|
167
|
+
req = {IPROTO_SPACE_ID => space_no,
|
168
|
+
IPROTO_INDEX_ID => index_no,
|
169
|
+
IPROTO_KEY => key || [],
|
170
|
+
IPROTO_OFFSET => offset,
|
171
|
+
IPROTO_LIMIT => limit,
|
172
|
+
IPROTO_ITERATOR => iterator}
|
173
|
+
send_request(REQUEST_TYPE_SELECT, req, cb)
|
174
|
+
end
|
175
|
+
|
176
|
+
def _update(space_no, index_no, key, ops, cb)
|
177
|
+
req = {IPROTO_SPACE_ID => space_no,
|
178
|
+
IPROTO_INDEX_ID => index_no,
|
179
|
+
IPROTO_KEY => key,
|
180
|
+
IPROTO_TUPLE => ops}
|
181
|
+
send_request(REQUEST_TYPE_UPDATE, req, cb)
|
182
|
+
end
|
183
|
+
|
184
|
+
def _call(name, args, cb)
|
185
|
+
req = {IPROTO_FUNCTION_NAME => name,
|
186
|
+
IPROTO_TUPLE => args}
|
187
|
+
send_request(REQUEST_TYPE_CALL, req, cb)
|
188
|
+
end
|
189
|
+
|
190
|
+
REQ_EMPTY = {}.freeze
|
191
|
+
def _ping(cb)
|
192
|
+
send_request(REQUEST_TYPE_PING, REQ_EMPTY, cb)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require_relative 'common'
|
3
|
+
module Tarantool16
|
4
|
+
module Connection
|
5
|
+
class Dumb
|
6
|
+
include Common
|
7
|
+
|
8
|
+
def initialize(host, opts = {})
|
9
|
+
_init_common(host, opts)
|
10
|
+
@nbuf = "\x00".b * 5
|
11
|
+
@reconnect_time = now_f - 1
|
12
|
+
@socket = nil
|
13
|
+
_connect
|
14
|
+
end
|
15
|
+
|
16
|
+
def send_request(code, body, cb)
|
17
|
+
_connect
|
18
|
+
syswrite format_request(code, next_sync, body)
|
19
|
+
written = true
|
20
|
+
response = _read_response
|
21
|
+
rescue ::Errno::EPIPE, Retry => e
|
22
|
+
@socket = nil
|
23
|
+
if !written && @retry && @reconnect
|
24
|
+
@retry = false
|
25
|
+
retry
|
26
|
+
end
|
27
|
+
cb.call Option.error(nil, Disconnected, e.message)
|
28
|
+
rescue StandardError => e
|
29
|
+
cb.call Option.error(nil, e, nil)
|
30
|
+
else
|
31
|
+
cb.call response
|
32
|
+
end
|
33
|
+
|
34
|
+
def disconnect
|
35
|
+
if @socket
|
36
|
+
@socket.close rescue nil
|
37
|
+
@socket = nil
|
38
|
+
@s = 0
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def close
|
43
|
+
@reconnect = false
|
44
|
+
if @socket
|
45
|
+
@socket.close rescue nil
|
46
|
+
@socket = false
|
47
|
+
@s = 0
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def connected?
|
52
|
+
@socket
|
53
|
+
end
|
54
|
+
|
55
|
+
def could_be_connected?
|
56
|
+
@socket || (@socket.nil? && (@reconnect || @reconnect_time < now_f))
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def _connect
|
61
|
+
return if @socket
|
62
|
+
unless could_be_connected?
|
63
|
+
raise Disconnected, "connection is closed"
|
64
|
+
end
|
65
|
+
@socket = TCPSocket.new(*host_port)
|
66
|
+
@socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
|
67
|
+
@retry = @reconnect
|
68
|
+
greeting = @socket.read(IPROTO_GREETING_SIZE)
|
69
|
+
unless greeting && greeting.bytesize == IPROTO_GREETING_SIZE
|
70
|
+
raise Disconnected, "mailformed greeting #{greeting.inspect}"
|
71
|
+
end
|
72
|
+
parse_greeting greeting
|
73
|
+
authenticate if @user
|
74
|
+
rescue ::Errno::ECONNREFUSED, ::Errno::EPIPE, Disconnected => e
|
75
|
+
@socket = nil
|
76
|
+
if !@reconnect
|
77
|
+
@socket = false
|
78
|
+
@s = 0
|
79
|
+
else
|
80
|
+
@reconnect_time = now_f + @reconnect_timeout
|
81
|
+
end
|
82
|
+
raise CouldNotConnect, e.message
|
83
|
+
end
|
84
|
+
|
85
|
+
def syswrite(req)
|
86
|
+
if @socket.syswrite(req) != req.bytesize
|
87
|
+
raise Retry, "Could not write message"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def authenticate
|
92
|
+
syswrite format_authenticate(@user, @passwd, @salt)
|
93
|
+
_read_response.raise_if_error!
|
94
|
+
end
|
95
|
+
|
96
|
+
def _read_response
|
97
|
+
str = @socket.read(5, @nbuf)
|
98
|
+
unless str && str.bytesize == 5
|
99
|
+
# check if we sent request or not
|
100
|
+
begin
|
101
|
+
@socket.send("\x00", 0)
|
102
|
+
rescue ::Errno::EPIPE
|
103
|
+
# if OS knows that socket is closed, then request were not sent
|
104
|
+
raise Retry
|
105
|
+
else
|
106
|
+
# otherwise request were sent
|
107
|
+
raise Disconnected, "disconnected while read length"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
n = parse_size(str)
|
111
|
+
raise n unless ::Integer === n
|
112
|
+
resp = @socket.read(n)
|
113
|
+
raise Disconnected, "disconnected while read response" unless resp && resp.bytesize == n
|
114
|
+
r = parse_response(resp)
|
115
|
+
if r.ok? && r.sync != @s
|
116
|
+
raise UnexpectedResponse, "sync mismatch: #{@s} != #{r.sync}"
|
117
|
+
end
|
118
|
+
r
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|