tarantool 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.
- data/Gemfile +14 -0
- data/Gemfile.lock +55 -0
- data/LICENSE +24 -0
- data/README.md +116 -0
- data/Rakefile +131 -0
- data/examples/em_simple.rb +21 -0
- data/examples/record.rb +56 -0
- data/examples/synchrony_simple.rb +13 -0
- data/lib/em/protocols/fixed_header_and_body.rb +67 -0
- data/lib/tarantool.rb +44 -0
- data/lib/tarantool/connection.rb +54 -0
- data/lib/tarantool/exceptions.rb +11 -0
- data/lib/tarantool/record.rb +316 -0
- data/lib/tarantool/request.rb +94 -0
- data/lib/tarantool/requests.rb +19 -0
- data/lib/tarantool/requests/call.rb +20 -0
- data/lib/tarantool/requests/delete.rb +18 -0
- data/lib/tarantool/requests/insert.rb +19 -0
- data/lib/tarantool/requests/ping.rb +16 -0
- data/lib/tarantool/requests/select.rb +22 -0
- data/lib/tarantool/requests/update.rb +35 -0
- data/lib/tarantool/response.rb +58 -0
- data/lib/tarantool/serializers.rb +9 -0
- data/lib/tarantool/serializers/bson.rb +15 -0
- data/lib/tarantool/serializers/integer.rb +14 -0
- data/lib/tarantool/serializers/string.rb +14 -0
- data/lib/tarantool/space.rb +39 -0
- data/lib/tarantool/synchrony.rb +13 -0
- data/spec/helpers/let.rb +11 -0
- data/spec/helpers/truncate.rb +12 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/tarantool.cfg +32 -0
- data/spec/tarantool/record_spec.rb +247 -0
- data/spec/tarantool/request_spec.rb +114 -0
- data/tarantool.gemspec +70 -0
- metadata +126 -0
data/lib/tarantool.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'em-synchrony'
|
4
|
+
|
5
|
+
module Tarantool
|
6
|
+
VERSION = '0.1'
|
7
|
+
extend self
|
8
|
+
require 'tarantool/space'
|
9
|
+
require 'tarantool/connection'
|
10
|
+
require 'tarantool/requests'
|
11
|
+
require 'tarantool/response'
|
12
|
+
require 'tarantool/exceptions'
|
13
|
+
require 'tarantool/serializers'
|
14
|
+
|
15
|
+
def singleton_space
|
16
|
+
@singleton_space ||= Space.new connection, @config[:space_no]
|
17
|
+
end
|
18
|
+
|
19
|
+
def connection
|
20
|
+
@connection ||= begin
|
21
|
+
raise "Tarantool.configure before connect" unless @config
|
22
|
+
EM.connect @config[:host], @config[:port], Tarantool::Connection
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def space(no = nil)
|
27
|
+
Space.new connection, no || @config[:space_no]
|
28
|
+
end
|
29
|
+
|
30
|
+
def configure(config = {})
|
31
|
+
@config = config
|
32
|
+
end
|
33
|
+
|
34
|
+
def hexdump(string)
|
35
|
+
string.unpack('C*').map{ |c| "%02x" % c }.join(' ')
|
36
|
+
end
|
37
|
+
|
38
|
+
[:select, :update, :insert, :delete, :call, :ping].each do |v|
|
39
|
+
define_method v do |*params|
|
40
|
+
singleton_space.send v, *params
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'em/protocols/fixed_header_and_body'
|
2
|
+
module Tarantool
|
3
|
+
class Connection < EM::Connection
|
4
|
+
include EM::Protocols::FixedHeaderAndBody
|
5
|
+
|
6
|
+
header_size 12
|
7
|
+
|
8
|
+
def next_request_id
|
9
|
+
@request_id ||= 0
|
10
|
+
@request_id += 1
|
11
|
+
if @request_id > 0xffffffff
|
12
|
+
@request_id = 0
|
13
|
+
end
|
14
|
+
@request_id
|
15
|
+
end
|
16
|
+
|
17
|
+
def connection_completed
|
18
|
+
@connected = true
|
19
|
+
end
|
20
|
+
|
21
|
+
# begin FixedHeaderAndBody API
|
22
|
+
def body_size
|
23
|
+
@body_size
|
24
|
+
end
|
25
|
+
|
26
|
+
def receive_header(header)
|
27
|
+
@type, @body_size, @request_id = header.unpack('L3')
|
28
|
+
end
|
29
|
+
|
30
|
+
def receive_body(data)
|
31
|
+
clb = waiting_requests[@request_id]
|
32
|
+
raise UnexpectedResponse.new("For request id #{request_id}") unless clb
|
33
|
+
clb.call data
|
34
|
+
end
|
35
|
+
# end FixedHeaderAndBody API
|
36
|
+
|
37
|
+
def waiting_requests
|
38
|
+
@waiting_requests ||= {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def send_packet(request_id, data, &clb)
|
42
|
+
send_data data
|
43
|
+
waiting_requests[request_id] = clb
|
44
|
+
end
|
45
|
+
|
46
|
+
def close_connection(*args)
|
47
|
+
super(*args)
|
48
|
+
end
|
49
|
+
|
50
|
+
def unbind
|
51
|
+
raise CouldNotConnect.new unless @connected
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Tarantool
|
2
|
+
class TarantoolError < StandardError; end
|
3
|
+
class UndefinedRequestType < TarantoolError; end
|
4
|
+
class CouldNotConnect < TarantoolError; end
|
5
|
+
class BadReturnCode < TarantoolError; end
|
6
|
+
class StringTooLong < TarantoolError; end
|
7
|
+
class ArgumentError < TarantoolError; end
|
8
|
+
class UnexpectedResponse < TarantoolError; end
|
9
|
+
class UndefinedSpace < TarantoolError; end
|
10
|
+
class ValueError < TarantoolError; end
|
11
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'tarantool/synchrony'
|
3
|
+
module Tarantool
|
4
|
+
class Select
|
5
|
+
include Enumerable
|
6
|
+
attr_reader :record
|
7
|
+
def initialize(record)
|
8
|
+
@record = record
|
9
|
+
end
|
10
|
+
|
11
|
+
def space_no
|
12
|
+
record.space_no
|
13
|
+
end
|
14
|
+
|
15
|
+
def each(&blk)
|
16
|
+
res = Tarantool.select(*@tuples, index_no: @index_no, limit: @limit, offset: @offset).tuples
|
17
|
+
res.each do |tuple|
|
18
|
+
blk.call record.from_server(tuple)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def limit(limit)
|
23
|
+
@limit = limit
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def offset(offset)
|
28
|
+
@offset = offset
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
# id: 1
|
33
|
+
# id: [1, 2, 3]
|
34
|
+
# [{ name: 'a', email: 'a'}, { name: 'b', email: 'b'}]
|
35
|
+
def where(params)
|
36
|
+
raise SelectError.new('Where condition already setted') if @index_no # todo?
|
37
|
+
keys, @tuples = case params
|
38
|
+
when Hash
|
39
|
+
ordered_keys = record.ordered_keys params.keys
|
40
|
+
# name: ['a', 'b'], email: ['c', 'd'] => [['a', 'c'], ['b', 'd']]
|
41
|
+
if params.values.first.is_a?(Array)
|
42
|
+
[ordered_keys, params[ordered_keys.first].zip(*ordered_keys[1, ordered_keys.size].map { |k| params[k] })]
|
43
|
+
else
|
44
|
+
[ordered_keys, [record.hash_to_tuple(params)]]
|
45
|
+
end
|
46
|
+
when Array
|
47
|
+
[params.first.keys, params.map { |v| record.hash_to_tuple(v) }]
|
48
|
+
end
|
49
|
+
@index_no = detect_index_no keys
|
50
|
+
raise ArgumentError.new("Undefined index for keys #{keys}") unless @index_no
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
# # works fine only on TREE index
|
55
|
+
# def batches(count = 1000, &blk)
|
56
|
+
# raise ArgumentError.new("Only one tuple provided in batch selects") if @tuples.size > 1
|
57
|
+
|
58
|
+
# end
|
59
|
+
|
60
|
+
# def _batch_exec
|
61
|
+
# Tarantool.call proc_name: 'box.select_range', args: [space_no.to_s, @index_no.to_s, count.to_s] + @tuples.first.map(&:to_s), return_tuple: true
|
62
|
+
# end
|
63
|
+
|
64
|
+
def batches_each(&blk)
|
65
|
+
batches { |records| records.each(&blk) }
|
66
|
+
end
|
67
|
+
|
68
|
+
def all
|
69
|
+
to_a
|
70
|
+
end
|
71
|
+
|
72
|
+
def first
|
73
|
+
limit(1).all.first
|
74
|
+
end
|
75
|
+
|
76
|
+
def detect_index_no(keys)
|
77
|
+
index_no = nil
|
78
|
+
record.indexes.each_with_index do |v, i|
|
79
|
+
keys_inst = keys.dup
|
80
|
+
v.each do |index_part|
|
81
|
+
unless keys_inst.delete(index_part)
|
82
|
+
break
|
83
|
+
end
|
84
|
+
if keys_inst.size == 0
|
85
|
+
index_no = i
|
86
|
+
end
|
87
|
+
end
|
88
|
+
break if index_no
|
89
|
+
end
|
90
|
+
index_no
|
91
|
+
end
|
92
|
+
end
|
93
|
+
class Record
|
94
|
+
extend ActiveModel::Naming
|
95
|
+
include ActiveModel::AttributeMethods
|
96
|
+
include ActiveModel::Validations
|
97
|
+
include ActiveModel::Serialization
|
98
|
+
extend ActiveModel::Callbacks
|
99
|
+
include ActiveModel::Dirty
|
100
|
+
|
101
|
+
include ActiveModel::Serializers::JSON
|
102
|
+
include ActiveModel::Serializers::Xml
|
103
|
+
|
104
|
+
define_model_callbacks :save, :create, :update, :destroy
|
105
|
+
|
106
|
+
class_attribute :fields
|
107
|
+
self.fields = {}
|
108
|
+
|
109
|
+
class_attribute :default_values
|
110
|
+
self.default_values = {}
|
111
|
+
|
112
|
+
class_attribute :primary_index
|
113
|
+
class_attribute :indexes
|
114
|
+
self.indexes = []
|
115
|
+
|
116
|
+
class_attribute :space_no
|
117
|
+
define_attr_method :space_no do
|
118
|
+
original_space_no || 0
|
119
|
+
end
|
120
|
+
class << self
|
121
|
+
def field(name, type, params = {})
|
122
|
+
define_attribute_method name
|
123
|
+
self.fields = fields.merge name => { type: type, field_no: fields.size, params: params }
|
124
|
+
unless self.primary_index
|
125
|
+
self.primary_index = name
|
126
|
+
index name
|
127
|
+
end
|
128
|
+
if params[:default]
|
129
|
+
self.default_values = default_values.merge name => params[:default]
|
130
|
+
end
|
131
|
+
define_method name do
|
132
|
+
attributes[name]
|
133
|
+
end
|
134
|
+
define_method "#{name}=" do |v|
|
135
|
+
send("#{name}_will_change!") unless v == attributes[name]
|
136
|
+
attributes[name] = v
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def index(*fields)
|
141
|
+
self.indexes = (indexes.dup << fields).sort_by { |v| v.size }
|
142
|
+
end
|
143
|
+
|
144
|
+
def find(*keys)
|
145
|
+
res = space.select(*keys)
|
146
|
+
if keys.size == 1
|
147
|
+
if res.tuple
|
148
|
+
from_server res.tuple
|
149
|
+
else
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
else
|
153
|
+
res.tuples.map { |tuple| from_server tuple }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def select
|
158
|
+
Select.new(self)
|
159
|
+
end
|
160
|
+
|
161
|
+
%w{where limit offset}.each do |v|
|
162
|
+
define_method v do |*args|
|
163
|
+
select.send(v, *args)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def create(attribites = {})
|
168
|
+
new(attribites).tap { |o| o.save }
|
169
|
+
end
|
170
|
+
|
171
|
+
def from_server(tuple)
|
172
|
+
new(tuple_to_hash(tuple)).tap { |v| v.old_record! }
|
173
|
+
end
|
174
|
+
|
175
|
+
def space
|
176
|
+
@space ||= Tarantool.space space_no
|
177
|
+
end
|
178
|
+
|
179
|
+
def tuple_to_hash(tuple)
|
180
|
+
fields.keys.zip(tuple).inject({}) do |memo, (k, v)|
|
181
|
+
memo[k] = _cast(k, v) unless v.nil?
|
182
|
+
memo
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def hash_to_tuple(hash, with_nils = false)
|
187
|
+
res = []
|
188
|
+
fields.keys.each do |k|
|
189
|
+
v = hash[k]
|
190
|
+
res << _cast(k, v) if with_nils || !v.nil?
|
191
|
+
end
|
192
|
+
res
|
193
|
+
end
|
194
|
+
|
195
|
+
def ordered_keys(keys)
|
196
|
+
fields.keys.inject([]) do |memo, k|
|
197
|
+
keys.each do |k2|
|
198
|
+
memo << k2 if k2 == k
|
199
|
+
end
|
200
|
+
memo
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def _cast(name, value)
|
205
|
+
type = self.fields[name][:type]
|
206
|
+
serializer = _get_serializer(type)
|
207
|
+
if value.is_a?(Field)
|
208
|
+
return nil if value.data == "\0"
|
209
|
+
serializer.decode(value)
|
210
|
+
else
|
211
|
+
return "\0" if value.nil?
|
212
|
+
serializer.encode(value)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def _get_serializer(type)
|
217
|
+
Serializers::MAP[type] || raise(TarantoolError.new("Undefind serializer #{type}"))
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
attr_accessor :new_record
|
222
|
+
def initialize(attributes = {})
|
223
|
+
attributes.each do |k, v|
|
224
|
+
send("#{k}=", v)
|
225
|
+
end
|
226
|
+
@new_record = true
|
227
|
+
end
|
228
|
+
|
229
|
+
def id
|
230
|
+
attributes[self.class.primary_index]
|
231
|
+
end
|
232
|
+
|
233
|
+
def space
|
234
|
+
self.class.space
|
235
|
+
end
|
236
|
+
|
237
|
+
def new_record?
|
238
|
+
@new_record
|
239
|
+
end
|
240
|
+
|
241
|
+
def attributes
|
242
|
+
@attributes ||= self.class.default_values.dup
|
243
|
+
end
|
244
|
+
|
245
|
+
def new_record!
|
246
|
+
@new_record = true
|
247
|
+
end
|
248
|
+
|
249
|
+
def old_record!
|
250
|
+
@new_record = false
|
251
|
+
end
|
252
|
+
|
253
|
+
def save
|
254
|
+
def in_callbacks(&blk)
|
255
|
+
run_callbacks(:save) { run_callbacks(new_record? ? :create : :update, &blk)}
|
256
|
+
end
|
257
|
+
in_callbacks do
|
258
|
+
if valid?
|
259
|
+
if new_record?
|
260
|
+
space.insert(*to_tuple)
|
261
|
+
else
|
262
|
+
ops = changed.inject([]) do |memo, k|
|
263
|
+
k = k.to_sym
|
264
|
+
memo << [field_no(k), :set, self.class._cast(k, attributes[k])] if attributes[k]
|
265
|
+
memo
|
266
|
+
end
|
267
|
+
space.update id, ops: ops
|
268
|
+
end
|
269
|
+
@previously_changed = changes
|
270
|
+
@changed_attributes.clear
|
271
|
+
old_record!
|
272
|
+
true
|
273
|
+
else
|
274
|
+
false
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def update_attribute(field, value)
|
280
|
+
self.send("#{field}=", value)
|
281
|
+
save
|
282
|
+
end
|
283
|
+
|
284
|
+
def update_attributes(attributes)
|
285
|
+
attributes.each do |k, v|
|
286
|
+
self.send("#{k}=", v)
|
287
|
+
end
|
288
|
+
save
|
289
|
+
end
|
290
|
+
|
291
|
+
def increment(field, by = 1)
|
292
|
+
space.update id, ops: [[field_no(field), :add, by]]
|
293
|
+
end
|
294
|
+
|
295
|
+
def destroy
|
296
|
+
run_callbacks :destroy do
|
297
|
+
space.delete id
|
298
|
+
true
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
def to_tuple
|
303
|
+
self.class.hash_to_tuple attributes, true
|
304
|
+
end
|
305
|
+
|
306
|
+
def field_no(name)
|
307
|
+
self.class.fields[name][:field_no]
|
308
|
+
end
|
309
|
+
|
310
|
+
# return new object, not reloading itself as AR-model
|
311
|
+
def reload
|
312
|
+
self.class.find(id)
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|
316
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Tarantool
|
2
|
+
class Request
|
3
|
+
include EM::Deferrable
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def request_type(name = nil)
|
7
|
+
if name
|
8
|
+
@request_type = Tarantool::Requests::REQUEST_TYPES[name] || raise(UndefinedRequestType)
|
9
|
+
else
|
10
|
+
@request_type
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def pack_tuple(*values)
|
15
|
+
[values.size].pack('L') + values.map { |v| pack_field(v) }.join
|
16
|
+
end
|
17
|
+
|
18
|
+
def pack_field(value)
|
19
|
+
if String === value
|
20
|
+
raise StringTooLong.new if value.bytesize > 1024 * 1024
|
21
|
+
[value.bytesize, value].pack('wa*')
|
22
|
+
elsif Integer === value
|
23
|
+
if value < 4294967296 # 2 ^ 32
|
24
|
+
[4, value].pack('wL')
|
25
|
+
else
|
26
|
+
[8, value].pack('wQ')
|
27
|
+
end
|
28
|
+
elsif value.is_a?(Tarantool::Field)
|
29
|
+
[value.data.bytesize].pack('w') + value.data
|
30
|
+
else
|
31
|
+
raise ArgumentError.new("Field should be integer or string")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :space, :params, :args
|
37
|
+
attr_reader :space_no
|
38
|
+
def initialize(space, *args)
|
39
|
+
@space = space
|
40
|
+
@args = args
|
41
|
+
@params = if args.last.is_a? Hash
|
42
|
+
args.pop
|
43
|
+
else
|
44
|
+
{}
|
45
|
+
end
|
46
|
+
@space_no = params.delete(:space_no) || space.space_no || raise(UndefinedSpace.new)
|
47
|
+
parse_args
|
48
|
+
end
|
49
|
+
|
50
|
+
def perform
|
51
|
+
send_packet(make_packet(make_body))
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_args
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
def request_id
|
60
|
+
@request_id ||= connection.next_request_id
|
61
|
+
end
|
62
|
+
|
63
|
+
def make_packet(body)
|
64
|
+
[self.class.request_type, body.size, request_id].pack('LLL') +
|
65
|
+
body
|
66
|
+
end
|
67
|
+
|
68
|
+
def send_packet(packet)
|
69
|
+
connection.send_packet request_id, packet do |data|
|
70
|
+
make_response data
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def make_response(data)
|
75
|
+
return_code, = data[0,4].unpack('L')
|
76
|
+
if return_code == 0
|
77
|
+
succeed Response.new(data[4, data.size], response_params)
|
78
|
+
else
|
79
|
+
msg = data[4, data.size].unpack('a*')
|
80
|
+
fail BadReturnCode.new("Error code #{return_code}: #{msg}")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def response_params
|
85
|
+
res = {}
|
86
|
+
res[:return_tuple] = true if params[:return_tuple]
|
87
|
+
res
|
88
|
+
end
|
89
|
+
|
90
|
+
def connection
|
91
|
+
space.connection
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|