tarantool16 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tarantool16.gemspec
4
+ gemspec
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,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -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