tarantool 0.1

Sign up to get free protection for your applications and to get access to all the features.
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