queris 0.8.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.
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