queris 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +34 -0
  4. data/README.md +53 -0
  5. data/Rakefile +1 -0
  6. data/data/redis_scripts/add_low_ttl.lua +10 -0
  7. data/data/redis_scripts/copy_key_if_absent.lua +13 -0
  8. data/data/redis_scripts/copy_ttl.lua +13 -0
  9. data/data/redis_scripts/create_page_if_absent.lua +24 -0
  10. data/data/redis_scripts/debuq.lua +20 -0
  11. data/data/redis_scripts/delete_if_string.lua +8 -0
  12. data/data/redis_scripts/delete_matching_keys.lua +7 -0
  13. data/data/redis_scripts/expire_temp_query_keys.lua +7 -0
  14. data/data/redis_scripts/make_rangehack_if_needed.lua +30 -0
  15. data/data/redis_scripts/master_expire.lua +15 -0
  16. data/data/redis_scripts/match_key_type.lua +9 -0
  17. data/data/redis_scripts/move_key.lua +11 -0
  18. data/data/redis_scripts/multisize.lua +19 -0
  19. data/data/redis_scripts/paged_query_ready.lua +35 -0
  20. data/data/redis_scripts/periodic_zremrangebyscore.lua +9 -0
  21. data/data/redis_scripts/persist_reusable_temp_query_keys.lua +14 -0
  22. data/data/redis_scripts/query_ensure_existence.lua +23 -0
  23. data/data/redis_scripts/query_intersect_optimization.lua +31 -0
  24. data/data/redis_scripts/remove_from_keyspace.lua +27 -0
  25. data/data/redis_scripts/remove_from_sets.lua +13 -0
  26. data/data/redis_scripts/results_from_hash.lua +54 -0
  27. data/data/redis_scripts/results_with_ttl.lua +20 -0
  28. data/data/redis_scripts/subquery_intersect_optimization.lua +25 -0
  29. data/data/redis_scripts/subquery_intersect_optimization_cleanup.lua +5 -0
  30. data/data/redis_scripts/undo_add_low_ttl.lua +8 -0
  31. data/data/redis_scripts/unpaged_query_ready.lua +17 -0
  32. data/data/redis_scripts/unpersist_reusable_temp_query_keys.lua +11 -0
  33. data/data/redis_scripts/update_live_expiring_presence_index.lua +20 -0
  34. data/data/redis_scripts/update_query.lua +126 -0
  35. data/data/redis_scripts/update_rangehacks.lua +94 -0
  36. data/data/redis_scripts/zrangestore.lua +12 -0
  37. data/lib/queris.rb +400 -0
  38. data/lib/queris/errors.rb +8 -0
  39. data/lib/queris/indices.rb +735 -0
  40. data/lib/queris/mixin/active_record.rb +74 -0
  41. data/lib/queris/mixin/object.rb +398 -0
  42. data/lib/queris/mixin/ohm.rb +81 -0
  43. data/lib/queris/mixin/queris_model.rb +59 -0
  44. data/lib/queris/model.rb +455 -0
  45. data/lib/queris/profiler.rb +275 -0
  46. data/lib/queris/query.rb +1215 -0
  47. data/lib/queris/query/operations.rb +398 -0
  48. data/lib/queris/query/page.rb +101 -0
  49. data/lib/queris/query/timer.rb +42 -0
  50. data/lib/queris/query/trace.rb +108 -0
  51. data/lib/queris/query_store.rb +137 -0
  52. data/lib/queris/version.rb +3 -0
  53. data/lib/rails/log_subscriber.rb +22 -0
  54. data/lib/rails/request_timing.rb +29 -0
  55. data/lib/tasks/queris.rake +138 -0
  56. data/queris.gemspec +41 -0
  57. data/test.rb +39 -0
  58. data/test/current.rb +74 -0
  59. data/test/dsl.rb +35 -0
  60. data/test/ohm.rb +37 -0
  61. metadata +161 -0
@@ -0,0 +1,81 @@
1
+ module Queris
2
+ module OhmMixin
3
+ def self.included(base)
4
+ begin
5
+ require "ohm/contrib"
6
+ rescue Exception => e
7
+ raise LoadError, "ohm-contrib not found. Please ensure the ohm-contrib gem is available."
8
+ end
9
+
10
+ base.class_eval do
11
+ include Ohm::Callbacks
12
+ %w(create update delete).each do |action|
13
+ hook="before_#{action}"
14
+ alias_method hook "old_#{hook}" if respond_to?(hook)
15
+ define_method "brefore_#{action}" do
16
+ call("old_#{hook}") if respond_to? "old_#{hook}"
17
+ call "#{action}_redis_indices"
18
+ end
19
+ end
20
+ end
21
+
22
+ base.extend OhmClassMixin
23
+ end
24
+
25
+ module OhmClassMixin
26
+ def redis_query(arg={})
27
+ query = OhmQuery.new self, arg
28
+ yield query if block_given?
29
+ query
30
+ end
31
+
32
+ def find_cached(id, *arg)
33
+ self[id]
34
+ end
35
+ def restore(hash, arg)
36
+
37
+ end
38
+ end
39
+
40
+ class OhmQuery < Query
41
+ attr_accessor :params
42
+ def ensure_same_redis(model)
43
+ quer, ohmr = Queris.redis, self.db
44
+ if !ohmr && quer
45
+ Ohm.connect url: quer.id
46
+ elsif !quer && ohmr
47
+ Queris.add_redis Redis.new(url: ohmr.id)
48
+ elsif quer && ohmr
49
+ unless quer.id == ohmr.id
50
+ raise Error, "Queris redis master server and Ohm redis server must be the same. There's just no reason to have them on separate servers."
51
+ end
52
+ end
53
+ yield Queris.redis if block_given?
54
+ end
55
+ def initialize(model, arg=nil)
56
+ if model.kind_of?(Hash) and arg.nil?
57
+ arg, model = model, model[:model]
58
+ elsif arg.nil?
59
+ arg= {}
60
+ end
61
+ @params = {}
62
+ unless model.kind_of?(Class) && model < Ohm::Model
63
+ raise ArgumentError, ":model arg must be an Ohm model model, got #{model.respond_to?(:superclass) ? model.superclass.name : model} instead."
64
+ end
65
+ super model, arg
66
+ end
67
+
68
+ def results(*arg)
69
+ res_ids = super(*arg)
70
+ res = []
71
+ res_ids.each_with_index do |id, i|
72
+ unless (cached = @model.find_cached id).nil?
73
+ res << cached
74
+ end
75
+ end
76
+ res
77
+ end
78
+ end
79
+ end
80
+ end
81
+
@@ -0,0 +1,59 @@
1
+ module Queris
2
+ module QuerisModelMixin
3
+ def self.included(base)
4
+ base.extend QuerisModelClassMixin
5
+ end
6
+
7
+ module QuerisModelClassMixin
8
+ def redis_query(arg={})
9
+ @hash_keyf ||= new.key '%s'
10
+ query = QuerisModelQuery.new self, arg.merge(redis: redis(true), from_hash: @hash_keyf, delete_missing: true)
11
+ yield query if block_given?
12
+ query
13
+ end
14
+ alias :query :redis_query
15
+ def add_redis_index(index, opt={})
16
+ #support for incremental attributes
17
+ @incremental_attr ||= {}
18
+ ret = super(index, opt)
19
+ if @incremental_attr[index.attribute].nil?
20
+ @incremental_attr[index.attribute] = index.incremental?
21
+ else
22
+ @incremental_attr[index.attribute] &&= index.incremental?
23
+ end
24
+ ret
25
+ end
26
+ def stored_in_redis?; true; end
27
+ def can_increment_attribute?( attr_name )
28
+ @incremental_attr[attr_name.to_sym]
29
+ end
30
+
31
+ private
32
+ #don't save attributes, just index them. useful at times.
33
+ def index_only
34
+ @index_only = true
35
+ end
36
+
37
+ def index_attribute(arg={}, &block)
38
+ if arg.kind_of? Symbol
39
+ arg = {:attribute => arg }
40
+ end
41
+ super arg.merge(:redis => redis), &block
42
+ end
43
+ end
44
+ end
45
+
46
+ class QuerisModelQuery < Query
47
+ #TODO
48
+ attr_accessor :params
49
+ def initialize(model, arg=nil)
50
+ if model.kind_of?(Hash) and arg.nil?
51
+ arg, model = model, model[:model]
52
+ elsif arg.nil?
53
+ arg= {}
54
+ end
55
+ @params = {}
56
+ super model, arg
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,455 @@
1
+ require "redis"
2
+ module Queris
3
+
4
+ class Model
5
+ attr_reader :id
6
+ attr_accessor :query_score
7
+ include Queris #this doesn't trigger Queris::included as it seems it ought to...
8
+ require "queris/mixin/queris_model"
9
+ include ObjectMixin
10
+ include QuerisModelMixin
11
+
12
+ def self.attr_val_block
13
+ @attr_val_block ||= {}
14
+ end
15
+
16
+ class << self
17
+ def redis(redis_client=nil)
18
+ if redis_client.kind_of? Redis
19
+ @redis = redis_client
20
+ end
21
+ @redis || Queris.redis
22
+ end
23
+
24
+ def prefix
25
+ @prefix ||= "#{Queris.redis_prefix}#{self.superclass.name.split('::').last}:#{self.name}:"
26
+ end
27
+ def keyf
28
+ "#{prefix}%s"
29
+ end
30
+
31
+ #get/setter
32
+ def attributes(*arg)
33
+ if Hash===arg.last
34
+ attributes = arg[0..-2]
35
+ opt=arg.last
36
+ else
37
+ attributes= arg
38
+ end
39
+ unless attributes.nil?
40
+ attributes.each do |attr|
41
+ attribute attr, opt
42
+ if block_given?
43
+ self.attr_val_block[attr.to_sym]=Proc.new
44
+ end
45
+ end
46
+ end
47
+ @attributes
48
+ end
49
+
50
+ def attribute(*arg)
51
+ if arg.first
52
+ attr_name = arg.first.to_sym
53
+ end
54
+ if arg.count == 2
55
+ if Hash===arg.last
56
+ opt=arg.last
57
+ else
58
+ raise "Invalid \"attribute\" params" unless arg.last.nil?
59
+ end
60
+ elsif arg.count > 2
61
+ raise "Too many arguments for \"attribute\""
62
+ end
63
+
64
+ @attributes ||= [] #Class instance var
65
+ raise ArgumentError, "Attribute #{attr_name} already exists in Queris model #{self.name}." if @attributes.member? attr_name
66
+ if block_given?
67
+ bb=Proc.new
68
+ self.attr_val_block[attr_name]=bb
69
+ end
70
+ define_method "#{attr_name}" do |no_attr_load=false|
71
+ binding.pry if @attributes.nil?
72
+ 1
73
+ if (val = @attributes[attr_name]).nil? && !@loaded && !no_attr_load && !noload?
74
+ load
75
+ send attr_name, true
76
+ else
77
+ val
78
+ end
79
+ end
80
+
81
+ define_method "#{attr_name}=" do |val| #setter
82
+ if opt
83
+ type = opt[:type]
84
+ unless val.nil?
85
+ if type == Float
86
+ val=Float(val)
87
+ elsif type == String
88
+ val=val.to_s
89
+ elsif type == Fixnum
90
+ val = val.to_s if Symbol === val
91
+ val=val.to_i
92
+ elsif type == Symbol
93
+ val = val.to_s if Numeric > val.class # first to string, then to symbol.
94
+ val=val.to_sym
95
+ elsif type == :boolean || type == :bool
96
+ if val=="1" || val==1 || val=="true"
97
+ val=true
98
+ elsif val=="0" || val==0 || val=="false"
99
+ val=false
100
+ else
101
+ val=val ? true : false
102
+ end
103
+ elsif type == :flag
104
+ val=val ? true : nil
105
+ elsif type.nil?
106
+ #nothing
107
+ else
108
+ raise "Unknown attribute type #{opt[:type]}"
109
+ end
110
+ end
111
+ end
112
+ if self.class.attr_val_block[attr_name]
113
+ val = self.class.attr_val_block[attr_name].call(val, self)
114
+ end
115
+ if !@loading
116
+ if @attributes_were[attr_name].nil?
117
+ @attributes_were[attr_name] = @attributes[attr_name]
118
+ end
119
+ @attributes_to_save[attr_name]=val
120
+ end
121
+ @attributes[attr_name]=val
122
+ end
123
+ define_method "#{attr_name}_was" do
124
+ @attributes_were[attr_name]
125
+ end
126
+ define_method "#{attr_name}_was=" do |val|
127
+ @attributes_were[attr_name]=val
128
+ end
129
+ private "#{attr_name}_was="
130
+ attributes << attr_name
131
+ end
132
+ alias :attr :attribute
133
+ alias :attrs :attributes
134
+
135
+ #get/setter
136
+ def expire(seconds=nil)
137
+ #note that using expire will not update indices, leading to some serious staleness
138
+ unless seconds.nil?
139
+ @expire = seconds
140
+ else
141
+ @expire
142
+ end
143
+ end
144
+
145
+ def find(id, opt={})
146
+ got= get id, opt
147
+ got.loaded? ? got : nil
148
+ end
149
+ alias :find_cached :find
150
+
151
+ def get(id, opt=nil)
152
+ ret=new(id)
153
+ opt ||= {}
154
+ if opt[:redis]
155
+ ret.load(nil, redis: opt[:redis])
156
+ else
157
+ ret.load
158
+ end
159
+ ret
160
+ end
161
+
162
+ def find_all #NOT FOR PRODUCTION USE!
163
+ keys = redis.keys "#{prefix}*"
164
+ objs = []
165
+ keys.map! do |key|
166
+ begin
167
+ found = self.find key[prefix.length..-1]
168
+ objs << found if found
169
+ rescue Exception => e
170
+ nil
171
+ end
172
+ end
173
+ objs
174
+ end
175
+
176
+ def restore(hash, id)
177
+ new(id).load(hash)
178
+ end
179
+
180
+ %w(during_save during_save_multi before_save after_save).each do |callback|
181
+ define_method callback do |&block|
182
+ @callbacks ||= {}
183
+ if block
184
+ @callbacks[callback] ||= []
185
+ @callbacks[callback] << block
186
+ else
187
+ @callbacks[callback] || []
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ def while_loading
194
+ loading_was=@loading
195
+ @loading=true
196
+ yield
197
+ @loading=loading_was
198
+ end
199
+
200
+ def run_callbacks(callback, redis=nil)
201
+ (self.class.send(callback) || []).each {|block| block.call(self, redis)}
202
+ end
203
+ private :run_callbacks
204
+
205
+ def initialize(id=nil, arg={})
206
+ @attributes = {}
207
+ @attributes_to_save = {}
208
+ @attributes_to_incr = {}
209
+ @attributes_were = {}
210
+ @redis = (arg || {})[:redis]
211
+ set_id id unless id.nil?
212
+ end
213
+
214
+ def set_id(nid, overwrite=false)
215
+ noload do
216
+ raise Error, "id cannot be a Hash" if Hash === nid
217
+ raise Error, "id cannot be an Array" if Array === nid
218
+ raise Error, "id already exists and is #{self.id}" unless overwrite || self.id.nil?
219
+ end
220
+ @id= nid
221
+ self
222
+ end
223
+ def id=(nid)
224
+ set_id nid
225
+ end
226
+
227
+ def save
228
+ key = hash_key #before multi
229
+ noload do
230
+ # to ensure atomicity, we unfortunately need two round trips to redis
231
+ run_callbacks :before_save
232
+ begin
233
+ if @attributes_to_save.length > 0
234
+ attrs_to_save = @attributes_to_save.keys
235
+ bulk_response = redis.pipelined do
236
+ redis.watch key
237
+ redis.hmget key, *attrs_to_save
238
+ end
239
+ current_saved_attr_vals = bulk_response.last
240
+ attrs_to_save.each_with_index do |attr,i| #sync with server
241
+ val=current_saved_attr_vals[i]
242
+ @attributes_were[attr]=val
243
+ end
244
+
245
+ run_callbacks :during_save
246
+
247
+ bulk_response = redis.multi do |r|
248
+ unless index_only
249
+ @attributes_to_incr.each do |attr, incr_by_val|
250
+ r.hincrbyfloat key, attr, incr_by_val #redis server >= 2.6
251
+ unless (val = send(attr, true)).nil?
252
+ @attributes_were[attr]=val
253
+ end
254
+ end
255
+ r.mapped_hmset key, @attributes_to_save
256
+ # a little hacky to first set to "", then delete.
257
+ # meh. will optimize when needed.
258
+ @attributes_to_save.each do |attr, val|
259
+ r.hdel(key, attr) if val.nil?
260
+ end
261
+ expire_sec = self.class.expire
262
+ end
263
+
264
+ update_redis_indices if defined? :update_redis_indices
265
+ @attributes_to_save.each {|attr, val| @attributes_were[attr]=val }
266
+ r.expire key, expire_sec unless expire_sec.nil?
267
+ run_callbacks :during_save_multi, r
268
+ end
269
+ end
270
+ end while @attributes_to_save.length > 0 && bulk_response.nil?
271
+ @attributes_to_save.clear
272
+ @attributes_to_incr.clear
273
+ ret= self
274
+ run_callbacks :after_save, redis
275
+ ret
276
+ end
277
+ end
278
+
279
+
280
+ def increment(attr_name, delta_val)
281
+ raise ArgumentError, "Can't increment attribute #{attr_name} because it is used by at least one non-incrementable index." unless self.class.can_increment_attribute? attr_name
282
+ raise ArgumentError, "Can't increment attribute #{attr_name} by non-numeric value <#{delta_val}>. Increment only by numbers, please." unless delta_val.kind_of? Numeric
283
+
284
+ @attributes_to_incr[attr_name.to_sym]=delta_val
285
+ unless (val = send(attr_name, true)).nil?
286
+ send "#{attr_name}=", val + delta_val
287
+ end
288
+ self
289
+ end
290
+
291
+
292
+ def attribute_diff(attr)
293
+ @attributes_to_incr[attr.to_sym]
294
+ end
295
+ #list of changed attributes
296
+ def changed
297
+ delta = (@attributes_to_save.keys + @attributes_to_incr.keys)
298
+ delta.uniq!
299
+ delta
300
+ end
301
+ #any unsaved changes?
302
+ def changed?
303
+ @attributes_to_save.empty? && @attributes_to_incr.empty?
304
+ end
305
+
306
+ def loaded?
307
+ @loaded && self
308
+ end
309
+
310
+ def deleted?
311
+ @deleted && self
312
+ end
313
+
314
+ def delete
315
+ noload do
316
+ key = hash_key
317
+ redis.multi do
318
+ redis.del key
319
+ delete_redis_indices if defined? :delete_redis_indices
320
+ end
321
+ end
322
+ @deleted= true
323
+ self
324
+ end
325
+
326
+ def load(hash=nil, opt={})
327
+ raise SchemaError, "Can't load #{self.class.name} with id #{id} -- model was specified index_only, so it was never saved." if index_only
328
+ unless hash
329
+ hash_future, hash_exists = nil, nil
330
+ hash_key
331
+ (opt[:redis] || redis).multi do |r|
332
+ hash_future = r.hgetall hash_key
333
+ hash_exists = r.exists hash_key
334
+ end
335
+ if hash_exists.value
336
+ hash = hash_future.value
337
+ elsif not hash
338
+ return nil
339
+ end
340
+ end
341
+ case hash
342
+ when Array
343
+ attr_name= nil
344
+ hash.each_with_index do |v, i|
345
+ if i % 2 == 0
346
+ attr_name = v
347
+ next
348
+ else
349
+ raw_load_attr(attr_name, v, !opt[:nil_only])
350
+ end
351
+ end
352
+ @loaded = true
353
+ when Hash
354
+ hash.each do |k, v|
355
+ raw_load_attr(k, v, !opt[:nil_only])
356
+ end
357
+ @loaded = true
358
+ else
359
+ raise Queris::ArgumentError, "Invalid thing to load"
360
+ end
361
+
362
+ self
363
+ end
364
+
365
+ def load_missing #load only missing attributes
366
+ load nil, nil_only: true
367
+ end
368
+
369
+ def raw_load_attr(attr_name, val, overwrite=true)
370
+ if attr_name.to_sym == :____score
371
+ @query_score = val.to_f
372
+ else
373
+ if overwrite || send(attr_name).nil?
374
+ while_loading do
375
+ send "#{attr_name}=", val
376
+ end
377
+ end
378
+ end
379
+ end
380
+ private :raw_load_attr
381
+
382
+ def import(attrs={})
383
+ attrs.each do |attr_name, val|
384
+ send "#{attr_name}=", val
385
+ @attributes_were[attr_name] = val
386
+ end
387
+ self
388
+ end
389
+
390
+
391
+ def redis=(r)
392
+ @redis=r
393
+ end
394
+ def redis(no_fallback=false)
395
+ if no_fallback
396
+ @redis || self.class.redis
397
+ else
398
+ @redis || self.class.redis || Queris.redis
399
+ end
400
+ end
401
+
402
+ def hash_key(custom_id=nil)
403
+ if custom_id.nil? && id.nil?
404
+ @id = new_id
405
+ end
406
+ @hash_key ||= "#{prefix}#{custom_id || id}"
407
+ end
408
+ alias :key :hash_key
409
+
410
+ def to_json(*arg)
411
+ as_json.to_json(*arg)
412
+ end
413
+
414
+ def as_json(*arg)
415
+ rest={id: self.id}
416
+ rest[:query_score]= self.query_score if query_score
417
+ @attributes.merge(rest)
418
+ end
419
+
420
+ def noload
421
+ @noload||=0
422
+ @noload+=1
423
+ ret = yield
424
+ @noload-=1
425
+ ret
426
+ end
427
+ def noload?
428
+ (@noload ||0) > 0
429
+ end
430
+
431
+ private
432
+ def prefix
433
+ self.class.prefix
434
+ end
435
+ def index_only
436
+ @index_only ||= self.class.class_eval do @index_only end #ugly
437
+ end
438
+ def attributes
439
+ self.class.attributes
440
+ end
441
+ def attr_hash
442
+ @attr_hash ||= {}
443
+ attributes.each do |attr_name|
444
+ val = send attr_name, true
445
+ @attr_hash[attr_name]= val unless attribute_was(attr_name) == val
446
+ end
447
+ @attr_hash
448
+ end
449
+
450
+ def new_id
451
+ @last_id_key ||= "#{Queris.redis_prefix}#{self.class.superclass.name}:last_id:#{self.class.name}"
452
+ redis.incr @last_id_key
453
+ end
454
+ end
455
+ end