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,398 @@
1
+ # encoding: utf-8
2
+ module Queris
3
+ class Query
4
+ private
5
+ class Op #query operation
6
+
7
+ class Operand #boilerplate
8
+ attr_accessor :index, :value, :temp_keys
9
+ def initialize(op_index, val)
10
+ @temp_keys = []
11
+ @index = op_index
12
+ @value = val
13
+ end
14
+ def marshal_dump
15
+ [(Query === index ? index.id : index.name).to_sym, value]
16
+ end
17
+ def key
18
+ if is_query? && @optimize_subquery_key
19
+ @temp_key ||= index.results_key(:as_optimized_subquery)
20
+ else
21
+ index.key_for_query value
22
+ end
23
+ end
24
+ def optimized_key(whichkey=nil)
25
+ k=whichkey || key
26
+ @optimized||={}
27
+ if Enumerable===k
28
+ k.map! { |ky| @optimized[ky] || ky }
29
+ else
30
+ k = @optimized[k] || k
31
+ end
32
+ k
33
+ end
34
+
35
+ def split
36
+ if Array === key && Enumerable === value
37
+ raise ClientError, "Sanity check failed - different number of keys and values, bailing." if key.length != value.length
38
+ value.map do |val|
39
+ self.class.new(index, val)
40
+ end
41
+ else
42
+ [ self ]
43
+ end
44
+ end
45
+ def is_query?
46
+ Query === @index
47
+ end
48
+ def gather_key_sizes(redis)
49
+ @size={}
50
+ k=key #we assume the key already exists on redis (it should have been created by the responsible index earlier)
51
+ if Enumerable === k
52
+ k.each { |k| @size[k]=index.key_size(k, redis) }
53
+ else
54
+ @size[k]=index.key_size(k, redis)
55
+ end
56
+ end
57
+ def key_size(redis_key)
58
+ raise ClientError "Attepted to get query operand key size, but it's not ready yet." unless Hash === @size
59
+ s = @size[redis_key]
60
+ return Redis::Future === s ? s.value : s
61
+ end
62
+ def preintersect(smallkey, mykey)
63
+ #puts "preintersect #{self} with #{smallkey}"
64
+ @preintersect||={}
65
+ @optimized||={}
66
+ @preintersect[mykey]=smallkey
67
+ preintersect_key = "#{mykey}:optimized:#{Queris.digest smallkey}"
68
+ @optimized[mykey]=preintersect_key
69
+ temp_keys << preintersect_key
70
+ preintersect_key
71
+ end
72
+ def delayed_optimize_query(smallkey, multiplier)
73
+ @optimize_subquery_key = smallkey
74
+ @optimize_threshold_multiplier = multiplier
75
+ end
76
+
77
+ def optimized?
78
+ @optimized && !@optimized.empty?
79
+ end
80
+ attr_reader :optimization_key
81
+
82
+ def run_optimization(redis)
83
+ if @preintersect
84
+ @preintersect.each do |k, smallkey|
85
+ #puts "running optimization - preintersecting #{k} and #{smallkey}"
86
+ Queris.run_script(:query_intersect_optimization, redis, [@optimized[k], k, smallkey])
87
+ end
88
+ #puts "preintersected some stuff"
89
+ elsif is_query? && @optimize_subquery_key
90
+ Queris.run_script(:subquery_intersect_optimization, redis, [ key, index.key, @optimize_subquery_key], [ @optimize_threshold_multiplier ])
91
+ #puts "no optimizations to run"
92
+ end
93
+ end
94
+ def cleanup_optimization(redis)
95
+ if is_query? && @optimize_subquery_key
96
+ Queris.run_script(:subquery_intersect_optimization_cleanup, redis, [ key, index.key ])
97
+ end
98
+ end
99
+ def json_redis_dump(op_name = nil)
100
+ ret = []
101
+ miniop = {}
102
+ if is_query?
103
+ miniop = {query: index.json_redis_dump}
104
+ else
105
+ index.json_redis_dump miniop
106
+ if Range === @value && @index.handle_range?
107
+ miniop[:min] = @index.val(@value.begin)
108
+ miniop[@value.exclude_end? ? :max : :max_or_equal] = @index.val(@value.end)
109
+ miniop[:key] = @index.key
110
+ elsif Enumerable === @value
111
+ @value.each do |val|
112
+ miniop = { equal: @index.val(val), key: @index.key(val) }
113
+ miniop[:op] = op_name if op_name
114
+ ret << miniop
115
+ end
116
+ return ret
117
+ else
118
+ miniop[:equal] = @index.val(@value) unless miniop[:nocompare]
119
+ end
120
+ end
121
+ miniop[:key] = @index.key(@value)
122
+ miniop[:op]=op_name if op_name
123
+ ret << miniop
124
+ ret
125
+ end
126
+ end
127
+
128
+ attr_accessor :operands, :fragile
129
+ def initialize(fragile=false)
130
+ @operands = []
131
+ @keys = []
132
+ @weights = []
133
+ @subqueries = []
134
+ @fragile = fragile
135
+ end
136
+
137
+ def query_run_stage_inspect(r, q)
138
+ operands.each do |op|
139
+ #this logic belongs elsewhere but is here for premature optimization
140
+ if Queris::RangeIndex === op.index && op.index.rangehack?(op.value)
141
+ op.index.ensure_rangehack_exists(r, op.value, q)
142
+ end
143
+ op.gather_key_sizes(r)
144
+ end
145
+ end
146
+ def query_run_stage_release(r,q)
147
+ operands.each do |op|
148
+ op.index.clear_rangehack_keys if Queris::RangeIndex === op.index
149
+ end
150
+ end
151
+
152
+ def notready!
153
+ @ready=nil
154
+ self
155
+ end
156
+ def push(index, val) # push operand
157
+ @ready = nil
158
+ @operands << Operand.new(index,val)
159
+ self
160
+ end
161
+ def symbol
162
+ @symbol || self.class::SYMBOL
163
+ end
164
+ def command
165
+ @command || self.class::COMMAND
166
+ end
167
+ def keys(target_key=nil, first = nil)
168
+ prepare
169
+ @keys[0]=target_key unless target_key.nil?
170
+ first ? @keys[1..-1] : @keys
171
+ end
172
+ def weights(first = nil)
173
+ prepare
174
+ first ? @weights[1..-1] : @weights
175
+ end
176
+ def target_key_weight
177
+ 1
178
+ end
179
+ def operand_key_weight(op)
180
+ 1
181
+ end
182
+ def temp_keys
183
+ optimized = []
184
+ operands.each { |op| optimized |= op.temp_keys }
185
+ optimized
186
+ end
187
+ def temp_keys?
188
+ operands.each { |op| return true unless op.temp_keys.empty? }
189
+ nil
190
+ end
191
+ def subqueries
192
+ prepare
193
+ @subqueries || []
194
+ end
195
+ def optimize(smallkey, smallsize, page=nil)
196
+ #optimization walker. doesn't really do much unless given a decision block
197
+ @optimized = nil
198
+ operands.each do |op|
199
+ key = op.key
200
+ if Enumerable === key
201
+ key.each do |k|
202
+ yield k, op.key_size(k), op if block_given?
203
+ end
204
+ else
205
+ yield key, op.key_size(key), op if block_given?
206
+ end
207
+ if op.optimized?
208
+ @optimized = true
209
+ notready!
210
+ end
211
+ end
212
+ return smallkey, smallsize
213
+ end
214
+ def optimized?
215
+ @optimized
216
+ end
217
+ def notready!
218
+ @ready=nil; self
219
+ end
220
+ private :notready!
221
+ def prepare
222
+ return if @ready
223
+ @keys, @weights, @subqueries = [:result_key], [target_key_weight], []
224
+ operands.each do |op|
225
+ k = block_given? ? yield(op) : op.optimized_key
226
+ num_keys = @keys.length
227
+ if Array === k
228
+ @keys |= k
229
+ else
230
+ @keys << k
231
+ end
232
+ if (@keys.length - num_keys) < 0
233
+ raise ArgumentError, "something really wrong here"
234
+ end
235
+ @weights += [ operand_key_weight(op) ] * (@keys.length - num_keys)
236
+ @subqueries << op.index if Query === op.index
237
+ end
238
+ @ready = true
239
+ end
240
+ def notready!
241
+ @ready = nil
242
+ self
243
+ end
244
+ def operand_key(op)
245
+ op.index.key_for_query op.value
246
+ end
247
+
248
+ def run(redis, target, first=false, trace_callback=false)
249
+ subqueries_on_slave = !subqueries.empty? && redis != Queris.redis(:master)
250
+
251
+ redis.multi do |r|
252
+ r=redis
253
+ if subqueries_on_slave || optimized?
254
+ #prevent dummy result string on master from race-conditioning its way into the query
255
+
256
+ Queris.run_script :delete_if_string, redis, subqueries.map{|s| s.key} if subqueries_on_slave
257
+ end
258
+ operands.each do |op|
259
+ op.run_optimization(redis)
260
+ op.index.before_query_op(redis, target, op.value, op) if op.index.respond_to? :before_query_op
261
+ op.cleanup_optimization(redis)
262
+ end
263
+ unless trace_callback
264
+ redis.send self.class::COMMAND, target, keys(target, first), :weights => weights(first)
265
+ else
266
+ operands.each do |operand|
267
+ operand.split.each do |op| #ensure one key per operand
268
+ keys = [ target, op.key ]
269
+ weights = [target_key_weight, operand_key_weight(op)]
270
+ if first
271
+ keys.shift; weights.shift
272
+ first = false
273
+ end
274
+ redis.send self.class::COMMAND, target, keys, :weights => weights
275
+ trace_callback.call(self, op, target) if trace_callback
276
+ end
277
+ end
278
+ end
279
+ end
280
+ operands.each { |op| op.index.after_query_op(redis, target, op.value, op) if op.index.respond_to? :after_query_op }
281
+ end
282
+
283
+ def marshal_dump
284
+ [self.class::SYMBOL, operands.map {|op| op.marshal_dump}]
285
+ end
286
+ def json_redis_dump(etc={})
287
+ all_ops = []
288
+ operands.map do |op|
289
+ all_ops.concat op.json_redis_dump(self.class::NAME)
290
+ end
291
+ all_ops
292
+ end
293
+ def to_s
294
+ "#{symbol} #{operands.map{|o| Query === o.index ? o.index : "#{o.index.name}<#{o.value}>"}.join(" #{symbol} ")}"
295
+ end
296
+ end
297
+
298
+ class UnionOp < Op
299
+ COMMAND = :zunionstore
300
+ SYMBOL = :'∪'
301
+ NAME = :union
302
+
303
+ OPTIMIZATION_THRESHOLD_MULTIPLIER = 3
304
+ def optimize(smallkey, smallsize, page=nil)
305
+ m = self.class::OPTIMIZATION_THRESHOLD_MULTIPLIER
306
+ super do |key, size, op|
307
+ if op.is_query? && !op.index.paged?
308
+ #puts "optimizing unpaged subquery #{op.index} later"
309
+ op.delayed_optimize_query(smallkey, m)
310
+ elsif smallsize * m < size
311
+ #puts "optimization reduced union(?) operand #{op} from #{size} to #{smallsize}"
312
+ op.preintersect(smallkey, key)
313
+ elsif page && page.size * m < size
314
+ #puts "paging reduced union(?) operand #{op} from #{size} to #{page.size}"
315
+ op.preintersect(page.key, key)
316
+ end
317
+ end
318
+ return smallkey, smallsize
319
+ end
320
+ end
321
+ class IntersectOp < Op
322
+ COMMAND = :zinterstore
323
+ SYMBOL = :'∩'
324
+ NAME = :intersect
325
+
326
+ OPTIMIZATION_THRESHOLD_MULTIPLIER = 5
327
+ def optimize(smallkey, smallsize, page=nil)
328
+ smallestkey, smallestsize, smallestop = Float::INFINITY, Float::INFINITY, nil
329
+ m = self.class::OPTIMIZATION_THRESHOLD_MULTIPLIER
330
+ #subquery_ops=[]
331
+ super do |key, size, op|
332
+ smallestkey, smallestsize, smallestop = key, size, op if size < smallestsize
333
+ #subquery_ops << op if op.is_query?
334
+ end
335
+ #no need to preintersect subqueries for intersects - it's not trivial (size may not be available before query subquery is run), and it's not terribly advantageous
336
+ if smallsize * m < smallestsize
337
+ puts "optimization reduced intersect operand from #{smallestsize} to #{smallsize}"
338
+ smallestop.preintersect(smallkey, smallestkey)
339
+ elsif page && page.size * m < smallestsize
340
+ smallestop.preintersect(page.key, smallestkey)
341
+ end
342
+ if smallestsize < smallsize
343
+ #puts "found a smaller intersect key: |#{smallestkey}|=#{smallestsize}"
344
+ return smallestkey, smallestsize
345
+ else
346
+ return smallkey, smallsize
347
+ end
348
+ end
349
+ end
350
+ class DiffOp < UnionOp
351
+ SYMBOL = :'∖'
352
+ NAME = :diff
353
+
354
+ def target_key_weight; 0; end
355
+ def operand_key_weight(op=nil); :'-inf'; end
356
+ def run(redis, result_key, *arg)
357
+ super redis, result_key, *arg
358
+ redis.zremrangebyscore result_key, :'-inf', :'-inf'
359
+ # BUG: sorted sets with -inf scores will be treated incorrectly when diffing
360
+ end
361
+ end
362
+ class SortOp < Op
363
+ COMMAND = :zinterstore
364
+ SYMBOL = :sortby
365
+ def json_redis_dump
366
+ operands.map do |op|
367
+ {key: op.index.key, multiplier: op.value}
368
+ end
369
+ end
370
+ def push(index, reverse=nil)
371
+ super(index, reverse ? -1 : 1)
372
+ end
373
+ def target_key_weight; 0; end
374
+ def operand_key_weight(op); op.value; end
375
+ def prepare
376
+ #don't trigger the rangehack
377
+ super { |op| op.index.key op.value }
378
+ end
379
+ def operand_key(op)
380
+ op.index.key op.value
381
+ end
382
+ def query_run_stage_inspect(r, q)
383
+ operands.each do |op|
384
+ #don't trigger the rangehack
385
+ op.gather_key_sizes(r)
386
+ end
387
+ end
388
+ def run(redis, target, first=false, trace_callback=nil)
389
+ sort_keys = keys(target, first)
390
+ redis.send self.class::COMMAND, target, sort_keys, :weights => weights(first)
391
+ if trace_callback
392
+ raise NotImplemented, "Can't trace multi-sorts yet." if sort_keys.count > 2 || operands.count > 1
393
+ trace_callback.call(self, operands.first, target)
394
+ end
395
+ end
396
+ end
397
+ end
398
+ end
@@ -0,0 +1,101 @@
1
+ module Queris
2
+ class Query
3
+ class Page
4
+ attr_accessor :page, :range
5
+ def initialize(prefix, sortops, page_size, ttl)
6
+ @prefix=prefix
7
+ @ops=sortops
8
+ @pagesize=page_size
9
+ @ttl=ttl
10
+ @page=0
11
+ end
12
+ def key
13
+ "#{@prefix}page:#{Queris.digest(@ops.join)}:#{@pagesize}:#{@page}"
14
+ end
15
+ def size; @pagesize; end
16
+ def volatile_query_keys(q)
17
+ [ q.results_key(:last_loaded_page) ]
18
+ end
19
+ def gather_data(redis, results_key, pagecount_key)
20
+ #puts "gather page data for key #{results_key}"
21
+ @current_count= Queris.run_script :multisize, redis, [results_key]
22
+ @last_loaded_page ||= redis.get pagecount_key
23
+ @total_count ||= redis.zcard source_key
24
+ end
25
+ def inspect_query(r, q)
26
+ gather_data(r, q.results_key, q.results_key(:last_loaded_page))
27
+ gather_ready_data(r, q)
28
+ end
29
+ def query_run_stage_after_run(r, q)
30
+ puts "write last_loaded_page for #{q}"
31
+ llp = fluxcap(@last_loaded_page)
32
+ llp=llp.to_i if llp
33
+ @last_loaded_page = @page
34
+ r.set q.results_key(:last_loaded_page), fluxcap(@last_loaded_page)
35
+ inspect_query(r, q)
36
+ end
37
+ def gather_ready_data(r, q)
38
+ @ready = Queris.run_script(:paged_query_ready, r, [q.results_key, q.results_key(:exists), source_key, q.runstate_key(:ready)])
39
+ end
40
+
41
+ def seek
42
+ last, cur_count = fluxcap(@last_loaded_page), fluxcap(@current_count)
43
+ last=nil if last == ""
44
+ last=last.to_i unless last.nil?
45
+ cur_count=cur_count.to_i unless cur_count.nil?
46
+ @key=nil
47
+ if last.nil?
48
+ @page=0
49
+ puts "seeking next page... will be #{@page} last_loaded = #{last}, cur_count = #{cur_count}, total max = #{fluxcap @total_count}"
50
+ false
51
+ elsif cur_count < range.max && !no_more_pages?
52
+ @page = last + 1
53
+ puts "seeking next page... will be #{@page} last_loaded = #{last}, cur_count = #{cur_count}, total max = #{fluxcap @total_count}"
54
+ false
55
+ else
56
+ puts "no need to seek, we are here"
57
+ true
58
+ end
59
+ end
60
+ def no_more_pages?
61
+ last, cur_count = fluxcap(@last_loaded_page), fluxcap(@current_count)
62
+ return nil if last == "" || last.nil?
63
+ last=last.to_i
64
+ (last + 1) * size > fluxcap(@total_count)
65
+ end
66
+ def ready?
67
+ raise Error, "Asked if a page was ready without having set a desired range first" unless @range
68
+ ready = (fluxcap(@current_count) || -Float::INFINITY) >= @range.max || no_more_pages?
69
+ yield(self) if !ready && block_given?
70
+ ready
71
+ end
72
+
73
+
74
+ def source_key
75
+ raise NotImplemented, "paging by multiple sorts not yet implemented" if @ops.count > 1
76
+ binding.pry if @ops.first.nil?
77
+ @ops.first.keys[1]
78
+ end
79
+ def source_id
80
+ Queris.digest source_key
81
+ end
82
+ def created?; @created==@page; end
83
+ def create_page(redis)
84
+ llp = fluxcap(@last_loaded_page)
85
+ llp=nil if llp==""
86
+ return if (llp && llp == @page)
87
+ redis.eval("redis.log(redis.LOG_WARNING, 'want page #{@page}!!!!!!!!1')")
88
+ Queris.run_script(:create_page_if_absent, redis, [key, source_key], [@pagesize * @page, @pagesize * (@page + 1) -1, @ttl])
89
+ end
90
+ private
91
+ def fluxcap(val)#possible future value
92
+ begin
93
+ Redis::Future === val ? val.value : val
94
+ rescue Redis::FutureNotReady
95
+ nil
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+