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,275 @@
1
+ module Queris
2
+ class Profiler < Queris::Model
3
+
4
+ class Sum
5
+ attr_reader :key
6
+ def self.index
7
+ RangeIndex
8
+ end
9
+
10
+ def initialize(*arg)
11
+ @key = "#{self.class.name.split('::').last}"
12
+ end
13
+ def hkey(attr)
14
+ "#{attr}:#{@key}"
15
+ end
16
+ alias :attribute_name :hkey
17
+
18
+ def current_value(val)
19
+ val
20
+ end
21
+ def increment_value(val)
22
+ val
23
+ end
24
+
25
+ def average(val, num_samples)
26
+ current_value(val)/current_value(num_samples)
27
+ end
28
+ end
29
+
30
+ class DecayingSum < Sum
31
+ attr_reader :half_life
32
+ def initialize(halflife=1209600) # 2 weeks default
33
+ super
34
+ @half_life = (halflife).to_f
35
+ @key << ":half-life=#{@half_life}"
36
+ end
37
+
38
+ def current_value(val)
39
+ decay val
40
+ end
41
+
42
+ def increment_value(val)
43
+ grow val #rewind in time
44
+ end
45
+
46
+ private
47
+ TIME_OFFSET=Time.new(2020,1,1).to_f
48
+ def t(seconds)
49
+ seconds - TIME_OFFSET
50
+ end
51
+ def exp_func(val, sign=1, time=Time.now.to_f)
52
+ val * 2.0 **(sign * t(time)/@half_life)
53
+ end
54
+ def grow(val)
55
+ exp_func val
56
+ end
57
+ def decay(val)
58
+ exp_func val, -1
59
+ end
60
+ end
61
+
62
+ attr_reader :last_sample
63
+
64
+ class << self
65
+ def samples(*attrs)
66
+ @samples ||= {}
67
+ return @samples if attrs.empty?
68
+ opt = attrs.last.kind_of?(Hash) ? attrs.pop : {}
69
+ attrs.each do |sample_name|
70
+ sample_name = sample_name.to_sym #just in case?...
71
+ @samples[sample_name] = opt[:unit] || opt[:units] || ""
72
+ stats.each { |_, statistic| initialize_sample_average sample_name, statistic }
73
+
74
+ define_method "#{sample_name}=" do |val|
75
+ val = val.to_f
76
+ self.class.stats.each do |_, statistic|
77
+ increment sample_name, val
78
+ end
79
+ if @sample_saved
80
+ @last_sample.clear
81
+ @sample_saved = nil
82
+ end
83
+ @last_sample[sample_name]=val
84
+ end
85
+
86
+ define_method sample_name do |stat_name|
87
+ if (statistic = self.class.named_stat(stat_name.to_sym))
88
+ statistic.average send(statistic.hkey(sample_name)), send(statistic.hkey(:n))
89
+ end
90
+ end
91
+ end
92
+ end
93
+ alias :sample :samples
94
+
95
+ def unit(unit)
96
+ samples[:n]=unit
97
+ end
98
+
99
+ #getter
100
+ def stats
101
+ @stats ||= {}
102
+ end
103
+
104
+ def average(method=:geometric, opt={}){}
105
+ @named_stats ||= {}
106
+ sample :n if samples[:n].nil?
107
+ method = method.to_sym
108
+ stat_name = opt[:name]
109
+ stat = case method
110
+ when :geometric, :arithmetic, :mean
111
+ Sum.new
112
+ when :decaying, :exponential
113
+ DecayingSum.new(opt[:half_life] || opt[:hl] || opt[:halflife] || 1209600)
114
+ else
115
+ raise ArgumentError, "Average weighing must be one of [ geometric, decaying ]"
116
+ end
117
+ if @stats[stat.key].nil?
118
+ @stats[stat.key] = stat
119
+ raise ArgumentError, "Different statistic with same name already declared." unless @named_stats[stat_name].nil?
120
+ end
121
+ if @named_stats[stat_name].nil?
122
+ @named_stats[stat_name] = @stats[stat.key]
123
+ end
124
+ samples.each do |sample_name, unit|
125
+ initialize_sample_statistic sample_name, stat
126
+ end
127
+ end
128
+
129
+ def default_statistic(stat_name)
130
+ #TODO
131
+ end
132
+ def named_stat(name)
133
+ @named_stats[name]
134
+ end
135
+ private
136
+ def initialize_sample_statistic(sample_name, statistic)
137
+ @initialized_samples ||= {}
138
+ key = statistic.hkey sample_name
139
+ if @initialized_samples[key].nil?
140
+ attribute key
141
+ index_range_attribute :attribute => key
142
+ @initialized_samples[key] = true
143
+ end
144
+ end
145
+ end
146
+
147
+ redis Queris.redis(:profiling, :sampling, :master)
148
+
149
+ def initialize(*arg)
150
+ @time_start={}
151
+ @last_sample={}
152
+ super *arg
153
+ end
154
+
155
+ def samples
156
+ self.class.samples.keys
157
+ end
158
+ def sample_unit(sample_name, default=nil)
159
+ self.class.samples[sample_name] || default
160
+ end
161
+
162
+ def save(*arg)
163
+ increment :n, 1 unless self.class.sample[:n].nil?
164
+ if (result = super)
165
+ @sample_saved = true
166
+ end
167
+ result
168
+ end
169
+
170
+ def load
171
+ loaded = redis.hgetall(hash_key)
172
+ loaded.each { |key, val| loaded[key]=val.to_f }
173
+ super loaded
174
+ end
175
+
176
+ def record(attr, val)
177
+ if respond_to? "#{attr}="
178
+ send("#{attr}=", val)
179
+ else
180
+ raise "Attempting to time an undeclared sampling attribute #{attr}"
181
+ end
182
+ self
183
+ end
184
+
185
+ def attribute_diff(attr)
186
+ super(attr) || 0
187
+ end
188
+
189
+ def increment(sample_name, delta_val)
190
+ self.class.stats.each do |_, statistic|
191
+ super statistic.hkey(sample_name), statistic.increment_value(delta_val)
192
+ end
193
+ self
194
+ end
195
+
196
+ def start(attr)
197
+ @time_start[attr]=Time.now.to_f
198
+ end
199
+ def finish(attr)
200
+ start_time = @time_start[attr]
201
+ raise "Query Profiling timing attribute #{attr} was never started." if start_time.nil?
202
+ t = Time.now.to_f - start_time
203
+ @time_start[attr]=nil
204
+
205
+ record attr, (Time.now.to_f - start_time)
206
+ end
207
+
208
+ end
209
+
210
+ class QueryProfilerBase < Profiler
211
+ def self.find(query, opt={})
212
+ if redis.nil?
213
+ opt[:redis]=query.model.redis
214
+ end
215
+ super query_profile_id(query), opt
216
+ end
217
+
218
+ def self.query_profile_id(query)
219
+ "#{query.model.name}:#{query.structure}"
220
+ end
221
+ def set_id(query, *rest)
222
+ @query = query
223
+ set_id self.class.query_profile_id(query, *rest), true
224
+ end
225
+ end
226
+
227
+ class QueryProfiler < QueryProfilerBase
228
+ sample :cache_miss
229
+ sample :time, :own_time, unit: :msec
230
+
231
+ average :geometric, :name => :avg
232
+ average :decaying, :name => :ema
233
+
234
+ def self.profile(query, opt={})
235
+ tab = " "
236
+ unless query.subqueries.empty?
237
+ query.explain :terse_subquery_ids => true
238
+ end
239
+ end
240
+
241
+
242
+ def profile
243
+ self.cass.profile @query
244
+ end
245
+ end
246
+
247
+ # does not store properties in a hash, and writes only to indices.
248
+ # useful for Redises with crappy or no HINCRBYFLOAT implementations (<=2.6)
249
+ # as a result, loading stats is suboptimal, but still O(1)
250
+ class QueryProfilerLite < QueryProfilerBase
251
+ sample :cache_miss
252
+ sample :time, :own_time, unit: :msec
253
+
254
+ average :decaying, :name => :ema, :half_life => 2629743 #1 month
255
+
256
+ index_only
257
+
258
+ def load(query=nil) #load from sorted sets through a pipeline
259
+ stats = self.class.stats
260
+ #The following code SHOULD, in some future, load attributes
261
+ # through self.class.stats. For now loading all zsets will do.
262
+ res = (redis || query.model.redis).multi do |r|
263
+ stats.each do |_, statistic|
264
+ r.zscore index.sorted_set_key, id
265
+ end
266
+ end
267
+ indices.each do |index|
268
+ unless (val = res.shift).nil?
269
+ self.import index.name => val.to_f
270
+ end
271
+ end
272
+ self
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,1215 @@
1
+ # encoding: utf-8
2
+ require 'json'
3
+ require 'securerandom'
4
+ require "queris/query/operations"
5
+ require "queris/query/trace"
6
+ require "queris/query/timer"
7
+ require "queris/query/page"
8
+
9
+ module Queris
10
+ class Query
11
+
12
+ class Error < StandardError
13
+ end
14
+
15
+ MINIMUM_QUERY_TTL = 30 #seconds. Don't mess with this number unless you fully understand it, setting it too small may lead to subquery race conditions
16
+ attr_accessor :redis_prefix, :created_at, :ops, :sort_ops, :model, :params
17
+ attr_reader :subqueries, :ttl, :temp_key_ttl
18
+ def initialize(model, arg=nil, &block)
19
+ if model.kind_of?(Hash) and arg.nil?
20
+ arg, model = model, model[:model]
21
+ elsif arg.nil?
22
+ arg= {}
23
+ end
24
+ raise ArgumentError, "Can't create query without a model" unless model
25
+ raise Error, "include Queris in your model (#{model.inspect})." unless model.include? Queris
26
+ @model = model
27
+ @params = {}
28
+ @ops = []
29
+ @sort_ops = []
30
+ @used_index = {}
31
+ @redis_prefix = arg[:complete_prefix] || ((arg[:prefix] || arg[:redis_prefix] || model.prefix) + self.class.name.split('::').last + ":")
32
+ @redis=arg[:redis]
33
+ @profile = arg[:profiler] || model.query_profiler.new(nil, :redis => @redis || model.redis)
34
+ @subqueries = []
35
+ self.ttl=arg[:ttl] || 600 #10 minutes default time-to-live
36
+ @temp_key_ttl = arg[:temp_key_ttl] || 300
37
+ @trace=nil
38
+ @pageable = arg[:pageable] || arg[:paged]
39
+ live! if arg[:live]
40
+ realtime! if arg[:realtime]
41
+ @created_at = Time.now.utc
42
+ if @expire_after = (arg[:expire_at] || arg[:expire] || arg[:expire_after])
43
+ raise ArgumentError, "Can't create query with expire_at option and check_staleness options at once" if arg[:check_staleness]
44
+ raise ArgumentError, "Can't create query with expire_at option with track_stats disabled" if arg[:track_stats]==false
45
+ arg[:track_stats]=true
46
+ arg[:check_staleness] = Proc.new do |query|
47
+ query.time_cached < (@expire_after || Time.at(0))
48
+ end
49
+ end
50
+
51
+ @from_hash = arg[:from_hash]
52
+ @restore_failed_callback = arg[:restore_failed]
53
+ @delete_missing = arg[:delete_missing]
54
+
55
+ @track_stats = arg[:track_stats]
56
+ @check_staleness = arg[:check_staleness]
57
+ if block_given?
58
+ instance_eval(&block)
59
+ end
60
+ self
61
+ end
62
+
63
+ def redis_master
64
+ Queris.redis :master || @redis || model.redis
65
+ end
66
+ private :redis_master
67
+
68
+ def redis
69
+ @redis || Queris.redis(:slave) || model.redis || redis_master
70
+ end
71
+
72
+ def use_redis(redis_instance) #seems obsolete with the new run pipeline
73
+ @redis = redis_instance
74
+ subqueries.each {|sub| sub.use_redis redis_instance}
75
+ self
76
+ end
77
+
78
+ #retrieve query parameters, as fed through union and intersect and diff
79
+ def param(param_name)
80
+ @params[param_name.to_sym]
81
+ end
82
+
83
+ #the set operations
84
+ def union(index, val=nil)
85
+ prepare_op UnionOp, index, val
86
+ end
87
+ alias :'∪' :union #UnionOp::SYMBOL
88
+
89
+ def intersect(index, val=nil)
90
+ prepare_op IntersectOp, index, val
91
+ end
92
+ alias :'∩' :intersect #IntersectOp::SYMBOL
93
+
94
+ def diff(index, val=nil)
95
+ prepare_op DiffOp, index, val
96
+ end
97
+ alias :'∖' :diff #DiffOp::SYMBOL
98
+
99
+ def prepare_op(op_class, index, val)
100
+ index = @model.redis_index index
101
+ raise Error, "Recursive subquerying doesn't do anything useful." if index == self
102
+ validate_ttl(index) if Index === index && live? && index.live?
103
+ set_param_from_index index, val
104
+
105
+ #set range and enumerable hack
106
+ if op_class != UnionOp && ((Range === val && !index.handle_range?) || (Enumerable === val && !(Range === val)))
107
+ #wrap those values in a union subquery
108
+ sub_union = subquery
109
+ val.each { |v| sub_union.union index, v }
110
+ index, val = sub_union, nil
111
+ end
112
+
113
+ use_index index #accept string index names and indices and queries
114
+ @results_key = nil
115
+ op = op_class.new
116
+ last_op = ops.last
117
+ if last_op && !op.fragile && !last_op.fragile && last_op.class == op_class
118
+ last_op.push index, val
119
+ else
120
+ op.push index, val
121
+ ops << op
122
+ end
123
+ self
124
+ end
125
+ private :prepare_op
126
+
127
+ #undo the last n operations
128
+ def undo (num_operations=1)
129
+ return self if num_operations == 0 || ops.empty?
130
+ @results_key = nil
131
+ op = ops.last
132
+ raise ClientError, "Unexpected operand-less query operation" unless (last_operand = op.operands.last)
133
+ if Query === (sub = last_operand.index)
134
+ subqueries.delete sub
135
+ end
136
+ op.operands.pop
137
+ ops.pop if op.operands.empty?
138
+ undo(num_operations - 1)
139
+ end
140
+
141
+ def sort(index, reverse = nil)
142
+ # accept a minus sign in front of index name to mean reverse
143
+ @results_key = nil
144
+ if Query === index
145
+ raise ArgumentError, "Sort can't be extracted from queries over different models." unless index.model == model
146
+ sort_query = index
147
+ if sort_query.sort_ops.empty?
148
+ index = nil
149
+ else #copy sort from another query
150
+ sort_query.sort_ops.each do |op|
151
+ op.operands.each do |operand|
152
+ use_index operand.index unless Query === index
153
+ end
154
+ end
155
+ self.sort_ops = sort_query.sort_ops.dup
156
+ sort_query.sort false #unsort sorted query - legacy behavior, probably a bad idea in the long run
157
+ end
158
+ else
159
+ if index.respond_to?('[]')
160
+ if index[0] == '-'
161
+ reverse, index = true, index[1..-1]
162
+ elsif index[0] == '+'
163
+ reverse, index = false, index[1..-1]
164
+ end
165
+ end
166
+ if index
167
+ index = use_index index #accept string index names and indices and queries
168
+ real_index = ForeignIndex === index ? index.real_index : index
169
+ raise ArgumentError, "Must have a RangeIndex for sorting, found #{real_index.class.name}" unless RangeIndex === real_index
170
+ self.sort_ops.clear << SortOp.new.push(index, reverse)
171
+ else
172
+ self.sort_ops.clear
173
+ end
174
+ end
175
+ use_page make_page
176
+ self
177
+ end
178
+ def sortby(index, direction = 1) #SortOp::SYMBOL
179
+ sort index, direction == -1
180
+ end
181
+
182
+ def sorting_by? index
183
+ val = 1
184
+ if index.respond_to?("[]") && !(Index === index) then
185
+ if index[0]=='-' then
186
+ index, val = index[1..-1], -1
187
+ end
188
+ end
189
+ begin
190
+ index=@model.redis_index(index)
191
+ rescue
192
+ index = nil
193
+ end
194
+ if index
195
+ sort_ops.each do |op|
196
+ op.operands.each do |o|
197
+ return true if o.index == index && o.value == val
198
+ end
199
+ end
200
+ end
201
+ nil
202
+ end
203
+
204
+ #get an object's sorting score, or its previous sorting score if asked for
205
+ def sort_score(obj, arg={})
206
+ score = 0
207
+ sort_ops.each do |op|
208
+ op.operands.each do |o|
209
+ if arg[:previous]
210
+ val = obj.send "#{o.index.attribute}_was"
211
+ else
212
+ val = obj.send o.index.attribute
213
+ end
214
+
215
+ score += o.value * (val || 0).to_f
216
+ end
217
+ end
218
+ score
219
+ end
220
+
221
+ def sorting_by
222
+ sorting = sort_ops.map do |op|
223
+ op.operands.map{|o| "#{(o.value < 0) ? '-' : ''}#{o.index.name}" }.join('+')
224
+ end.join('+')
225
+ sorting.empty? ? nil : sorting.to_sym
226
+ end
227
+ def sort_mult #sort multiplier (direction) -- currently, +1 or -1
228
+ return 1 if sort_ops.empty?
229
+ sort_ops.first.operands.first.value
230
+ end
231
+
232
+ def resort #apply a sort to set of existing results
233
+ @resort=true
234
+ self
235
+ end
236
+
237
+ def profiler
238
+ @profile
239
+ end
240
+
241
+ def live?; @live; end
242
+ def live=(val); @live= val; end
243
+ #static queries are updated only after they expire
244
+ def static?; !live!; end
245
+ def static!
246
+ @live=false; @realtime=false; self;
247
+ end
248
+ #live queries have pending updates stored nearby
249
+ def live!; @live=true; @realtime=false; validate_ttl; self; end
250
+ #realtime queries are updated automatically, on the spot
251
+ def realtime!
252
+ live!
253
+ @realtime = true
254
+ end
255
+ def realtime?
256
+ live? && @realtime
257
+ end
258
+ def ttl=(time_to_live=nil)
259
+ validate_ttl(nil, time_to_live)
260
+ @ttl=time_to_live
261
+ end
262
+ def validate_ttl(index=nil, time_to_live=nil)
263
+ time_to_live ||= ttl
264
+ tooshort = (index ? [ index ] : all_live_indices).select { |i| time_to_live > i.delta_ttl if time_to_live }
265
+ if tooshort.length > 0
266
+ raise Error, "Query time-to-live is too long to use live ind#{tooshort.length == 1 ? 'ex' : 'ices'} #{tooshort.map {|i| i.name}.join(', ')}. Shorten query ttl or extend indices' delta_ttl."
267
+ end
268
+ ttl
269
+ end
270
+ private :validate_ttl
271
+
272
+ #update query results with object(s)
273
+ def update(obj, arg={})
274
+ if uses_index_as_results_key? # DISCUSS : query.union(subquery) won't be updated
275
+ return true
276
+ end
277
+ obj_id = model === obj ? obj.id : obj #BUG-IN-WAITING: HARDCODED id attribute
278
+ myredis = arg[:redis] || redis_master || redis
279
+ if arg[:delete]
280
+ ret = myredis.zrem results_key, obj_id
281
+ else
282
+ ret = myredis.zadd results_key(:delta), 0, obj_id
283
+ end
284
+ ret
285
+ end
286
+
287
+ def delta(arg={})
288
+ myredis = arg[:redis] || redis_master || redis
289
+ myredis.zrange results_key(:delta), 0, -1
290
+ end
291
+ def uses_index_as_results_key?
292
+ if ops.length == 1 && sort_ops.empty? && ops.first.operands.length == 1
293
+ first_op = ops.first.operands.first
294
+ first_index = first_op.index
295
+ if first_index.usable_as_results? first_op.value
296
+ return first_index.key first_op.value
297
+ end
298
+ end
299
+ nil
300
+ end
301
+
302
+ def usable_as_results?(*arg)
303
+ true
304
+ end
305
+
306
+ #a list of all keys that need to be refreshed (ttl extended) for a query
307
+ def volatile_query_keys
308
+ #first element MUST be the existence flag key
309
+ #second element MUST be the results key
310
+ volatile = [ results_key(:exists), results_key ]
311
+ volatile |=[ results_key(:live), results_key(:marshaled) ] if live?
312
+ volatile |= @page.volatile_query_keys(self) if paged?
313
+ volatile
314
+ end
315
+
316
+ def add_temp_key(k)
317
+ @temp_keys ||= {}
318
+ @temp_keys[k]=true
319
+ end
320
+
321
+ def temp_keys
322
+ @temp_keys ||= {}
323
+ @temp_keys.keys
324
+ end
325
+ #all keys related to a query
326
+ def all_query_keys
327
+ all = volatile_query_keys
328
+ all |= temp_keys
329
+ all
330
+ end
331
+
332
+ def optimize
333
+ smallkey, smallest = nil, Float::INFINITY
334
+ #puts "optimizing query #{self}"
335
+ ops.reverse_each do |op|
336
+ #puts "optimizing op #{op}"
337
+ op.notready!
338
+ smallkey, smallest = op.optimize(smallkey, smallest, @page)
339
+ end
340
+ end
341
+ private :optimize
342
+ def no_optimize!
343
+ @no_optimize=true
344
+ subqueries.each &:no_optimize!
345
+ end
346
+ def run_static_query(r, force=nil)
347
+ #puts "running static query #{self.id}, force: #{force || "no"}"
348
+
349
+ temp_results_key = results_key "running:#{SecureRandom.hex}"
350
+ trace_callback = @trace ? @trace.method(:op) : nil
351
+
352
+ #optimize that shit
353
+ optimize unless @no_optimize
354
+
355
+ first_op = ops.first
356
+ [ops, sort_ops].each do |ops|
357
+ ops.each do |op|
358
+ op.run r, temp_results_key, first_op == op, trace_callback
359
+ end
360
+ end
361
+
362
+ if paged?
363
+ r.zunionstore results_key, [results_key, temp_results_key], :aggregate => 'max'
364
+ r.del temp_results_key
365
+ else
366
+ Queris.run_script :move_key, r, [temp_results_key, results_key]
367
+ r.setex results_key(:exists), ttl, 1
368
+ end
369
+ end
370
+ private :run_static_query
371
+
372
+ def reusable_temp_keys
373
+ tkeys = []
374
+ each_operand do |op|
375
+ tkeys |= op.temp_keys
376
+ end
377
+ tkeys << @page.key if paged?
378
+ tkeys
379
+ end
380
+ private :reusable_temp_keys
381
+ def reusable_temp_keys?
382
+ return true if paged?
383
+ ops.each {|op| return true if op.temp_keys?}
384
+ nil
385
+ end
386
+ private :reusable_temp_keys?
387
+
388
+ def all_subqueries
389
+ ret = subqueries.dup
390
+ subqueries.each { |sub| ret.concat sub.all_subqueries }
391
+ ret
392
+ end
393
+
394
+ def each_subquery(recursive=true) #dependency-ordered
395
+ subqueries.each do |s|
396
+ s.each_subquery do |ss|
397
+ yield ss
398
+ end
399
+ yield s
400
+ end
401
+ end
402
+
403
+ def trace!(opt={})
404
+ if opt==false
405
+ @must_trace = false
406
+ elsif opt
407
+ @must_trace = opt
408
+ end
409
+ self
410
+ end
411
+ def trace?
412
+ @must_trace || @trace
413
+ end
414
+ def trace(opt={})
415
+ indent = opt[:indent] || 0
416
+ buf = "#{" " * indent}#{indent == 0 ? 'Query' : 'Subquery'} #{self}:\r\n"
417
+ buf << "#{" " * indent}key: #{key}\r\n"
418
+ buf << "#{" " * indent}ttl:#{ttl}, |#{redis.type(key)} results key|=#{count(:no_run => true)}\r\n"
419
+ buf << "#{" " * indent}trace:\r\n"
420
+ case @trace
421
+ when nil
422
+ buf << "#{" " * indent}No trace available, query hasn't been run yet."
423
+ when false
424
+ buf << "#{" " * indent}No trace available, query was run without :trace parameter. Try query.run(:trace => true)"
425
+ else
426
+ buf << @trace.indent(indent).to_s
427
+ end
428
+ opt[:output]!=false ? puts(buf) : buf
429
+ end
430
+
431
+ def uses_index?(*arg)
432
+ arg.each do |ind|
433
+ index_name = Queris::Index === ind ? ind.name : ind.to_sym
434
+ return true if @used_index[index_name]
435
+ end
436
+ false
437
+ end
438
+
439
+ #list all indices used by a query (no subqueries, unless asked for)
440
+ def indices(opt={})
441
+ ret = @used_index.dup
442
+ if opt[:subqueries]
443
+ subqueries.each do |sub|
444
+ ret.merge! sub.indices(subqueries: true, hash: true)
445
+ end
446
+ end
447
+ if opt[:hash]
448
+ ret
449
+ else
450
+ ret.values
451
+ end
452
+ end
453
+ #list all indices (including subqueries)
454
+ def all_indices
455
+ indices :subqueries => true
456
+ end
457
+ def all_live_indices
458
+ return [] unless live?
459
+ ret = all_indices
460
+ ret.select! do |i|
461
+ (ForeignIndex === i ? i.real_index : i).live?
462
+ end
463
+ ret
464
+ end
465
+
466
+ # recursively and conditionally flush query and subqueries
467
+ # arg parameters: flush query if:
468
+ # :ttl - query.ttl <= ttl
469
+ # :index (symbol or index or an array of them) - query uses th(is|ese) ind(ex|ices)
470
+ # or flush conditionally according to passed block: flush {|query| true }
471
+ # when no parameters or block present, flush only this query and no subqueries
472
+ def flush(arg={})
473
+ model.run_query_callbacks :before_flush, self
474
+ return 0 if uses_index_as_results_key?
475
+ flushed = 0
476
+ if block_given? #efficiency hackety hack - anonymous blocs are heaps faster than bound ones
477
+ subqueries.each { |sub| flushed += sub.flush arg, &Proc.new }
478
+ elsif arg.count>0
479
+ subqueries.each { |sub| flushed += sub.flush arg }
480
+ end
481
+ if flushed > 0 || arg.count==0 || ttl <= (arg[:ttl] || 0) || (uses_index?(*arg[:index])) || block_given? && (yield self)
482
+ #this only works because of the slave EXPIRE hack requiring dummy query results_keys on master.
483
+ #otherwise, we'd have to create the key first (in a MULTI, of course)
484
+ n_deleted = 0
485
+ (redis_master || redis).multi do |r|
486
+ all = all_query_keys
487
+ all.each do |k|
488
+ r.setnx k, 'baleted'
489
+ end
490
+ n_deleted = r.del all
491
+ end
492
+ @known_to_exist = nil
493
+ flushed += n_deleted.value
494
+ end
495
+ flushed
496
+ end
497
+ alias :clear :flush
498
+
499
+
500
+ def range(range)
501
+ raise ArgumentError, "Range, please." unless Range === range
502
+ pg=make_page
503
+ pg.range=range
504
+ use_page pg
505
+ self
506
+ end
507
+
508
+ #flexible query results retriever
509
+ #results(x..y) from x to y
510
+ #results(x, y) same
511
+ #results(x) first x results
512
+ #results(x..y, :reverse) range in reverse
513
+ #results(x..y, :score =>a..b) results from x to y with scores in given score range
514
+ #results(x..y, :with_scores) return results with result.query_score attr set to the score
515
+ #results(x..y, :replace => [:foo_id, FooModel]) like an SQL join. returns results replaced by FooModels with ids from foo_id
516
+ def results(*arg)
517
+ opt= Hash === arg.last ? arg.pop : {}
518
+ opt[:reverse]=true if arg.member?(:reverse)
519
+ opt[:with_scores]=true if arg.member?(:with_scores)
520
+ opt[:range]=arg.shift if Range === arg.first
521
+ opt[:range]=(arg.shift .. arg.shift) if Numeric === arg[0] && arg[0].class == arg[1].class
522
+ opt[:range]=(0..arg.shift) if Numeric === arg[0]
523
+ opt[:raw] = true if arg.member? :raw
524
+ if opt[:range] && !sort_ops.empty? && pageable?
525
+ range opt[:range]
526
+ run :no_update => !realtime?
527
+ else
528
+ use_page nil
529
+ run :no_update => !realtime?
530
+ end
531
+ @timer.start("results") if @timer
532
+ key = results_key
533
+ case (keytype=redis.type(key))
534
+ when 'set'
535
+ raise Error, "Can't range by score on a regular results set" if opt[:score]
536
+ raise NotImplemented, "Cannot get result range from shortcut index result set (not sorted); must retrieve all results. This is a temporary queris limitation." if opt[:range]
537
+ cmd, first, last, rangeopt = :smembers, nil, nil, {}
538
+ when 'zset'
539
+ rangeopt = {}
540
+ rangeopt[:with_scores] = true if opt[:with_scores]
541
+ if (r = opt[:range])
542
+ first, last = r.begin, r.end - (r.exclude_end? ? 1 : 0)
543
+ raise ArgumentError, "Query result range must have numbers, instead there's a #{first.class} and #{last.class}" unless Numeric === first && Numeric === last
544
+ raise ArgumentError, "Query results range must have integer endpoints" unless first.round == first && last.round == last
545
+ end
546
+ if (scrange = opt[:score])
547
+ rangeopt[:limit] = [ first, last - first ] if opt[:range]
548
+ raise NotImplemented, "Can't fetch results with_scores when also limiting them by score. Pick one or the other." if opt[:with_scores]
549
+ raise ArgumentError, "Query.results :score parameter must be a Range" unless Range === scrange
550
+ first = Queris::to_redis_float(scrange.begin * sort_mult)
551
+ last = Queris::to_redis_float(scrange.end * sort_mult)
552
+ last = "(#{last}" if scrange.exclude_end? #)
553
+ first, last = last, first if sort_mult == -1
554
+ cmd = opt[:reverse] ? :zrevrangebyscore : :zrangebyscore
555
+ else
556
+ cmd = opt[:reverse] ? :zrevrange : :zrange
557
+ end
558
+ else
559
+ return []
560
+ end
561
+ @timer.start :results
562
+ if block_given? && opt[:replace_command]
563
+ res = yield cmd, key, first || 0, last || -1, rangeopt
564
+ elsif @from_hash && !opt[:raw]
565
+
566
+ if rangeopt[:limit]
567
+ limit, offset = *rangeopt[:limit]
568
+ else
569
+ limit, offset = nil, nil
570
+ end
571
+ if opt[:replace]
572
+ replace=opt[:replace]
573
+ replace_id_attr=replace.first
574
+ replace_model=replace.last
575
+ raise Queris::Error, "replace model must be a Queris model, is instead #{replace_model}" unless replace_model < Queris::Model
576
+ replace_keyf=replace_model.keyf
577
+ end
578
+ raw_res, ids, failed_i = redis.evalsha(Queris::script_hash(:results_from_hash), [key], [cmd, first || 0, last || -1, @from_hash, limit, offset, rangeopt[:with_scores] && :true, replace_keyf, replace_id_attr])
579
+ res, to_be_deleted = [], []
580
+
581
+ result_model= replace.nil? ? model : replace_model
582
+
583
+ raw_res.each_with_index do |raw_hash, i|
584
+ my_id = ids[i]
585
+ if failed_i.first == i
586
+ failed_i.shift
587
+ obj = result_model.find_cached my_id, :assume_missing => true
588
+ else
589
+ if (raw_hash.count % 2) > 0
590
+ binding.pry
591
+ 1+1.12
592
+ end
593
+ unless (obj = result_model.restore(raw_hash, my_id))
594
+ #we could stil have received an invalid cache object (too few attributes, for example)
595
+ obj = result_model.find_cached my_id, :assume_missing => true
596
+ end
597
+ end
598
+ if not obj.nil?
599
+ res << obj
600
+ elsif @delete_missing
601
+ to_be_deleted << my_id
602
+ end
603
+ end
604
+ redis.evalsha Queris.script_hash(:remove_from_sets), all_index_keys, to_be_deleted unless to_be_deleted.empty?
605
+ else
606
+ if cmd == :smembers
607
+ res = redis.send(cmd, key)
608
+ else
609
+ res = redis.send(cmd, key, first || 0, last || -1, rangeopt)
610
+ end
611
+ end
612
+ if block_given? && !opt[:replace_command]
613
+ if opt[:with_scores]
614
+ ret = []
615
+ res.each do |result|
616
+ obj = yield result.first
617
+ ret << [obj, result.last] unless obj.nil?
618
+ end
619
+ res = ret
620
+ else
621
+ res.map!(&Proc.new).compact!
622
+ end
623
+ end
624
+ if @timer
625
+ @timer.finish :results
626
+ #puts "Timing for #{self}: \r\n #{@timer}"
627
+ end
628
+ res
629
+ end
630
+
631
+ def result_scores(*ids)
632
+ val=[]
633
+ if ids.count == 1 && Array === ids[0]
634
+ ids=ids[0]
635
+ end
636
+ val=redis.multi do |r|
637
+ ids.each do |id|
638
+ redis.zscore(results_key, id)
639
+ end
640
+ end
641
+ val
642
+ end
643
+
644
+ def result_score(id)
645
+ result_scores([id]).first
646
+ end
647
+
648
+ def make_page
649
+ Query::Page.new(@redis_prefix, sort_ops, model.page_size, ttl)
650
+ end
651
+ private :make_page
652
+
653
+ def raw_results(*arg)
654
+ arg.push :raw
655
+ results(*arg)
656
+ end
657
+
658
+ def use_page(page)
659
+ @results_key=nil
660
+ if !pageable?
661
+ return nil
662
+ end
663
+ raise ArgumentError, "must be a Query::Page" unless page.nil? || Page === page
664
+ if block_given?
665
+ temp=@page
666
+ use_page page
667
+ yield
668
+ use_page temp
669
+ else
670
+ @page=page
671
+ subqueries.each {|s| s.use_page @page}
672
+ end
673
+ end
674
+ def paged?
675
+ @page
676
+ end
677
+ def pageable?
678
+ @pageable
679
+ end
680
+ def pageable!
681
+ @pageable = true
682
+ end
683
+ def unpageable!
684
+ @pageable = false
685
+ end
686
+
687
+ def member?(id)
688
+ id = id.id if model === id
689
+ use_page nil
690
+ run :no_update => !realtime?
691
+ case t = redis.type(results_key)
692
+ when 'set'
693
+ redis.sismember(results_key, id)
694
+ when 'zset'
695
+ !redis.zrank(results_key, id).nil?
696
+ when 'none'
697
+ false
698
+ else
699
+ raise ClientError, "unexpected result set type #{t}"
700
+ end
701
+ end
702
+ alias :contains? :member?
703
+
704
+
705
+ def result(n=0)
706
+ res = results(n...n+1)
707
+ if res.length > 0
708
+ res.first
709
+ else
710
+ nil
711
+ end
712
+ end
713
+
714
+ def first_result
715
+ return result 0
716
+ end
717
+
718
+ def results_key(suffix = nil, raw_id = nil)
719
+ if @results_key.nil? or raw_id
720
+ if (reused_set_key = uses_index_as_results_key?)
721
+ @results_key = reused_set_key
722
+ else
723
+ theid = raw_id || (Queris.digest(explain :subqueries => false) << ":subqueries:#{(@subqueries.length > 0 ? @subqueries.map{|q| q.id}.sort.join('&') : 'none')}" << ":sortby:#{sorting_by || 'nothing'}")
724
+ thekey = "#{@redis_prefix}results:#{theid}"
725
+ thekey << ":paged:#{@page.source_id}" if paged?
726
+ if raw_id
727
+ return thekey
728
+ else
729
+ @results_key = thekey
730
+ end
731
+ end
732
+ end
733
+ if suffix
734
+ "#{@results_key}:#{suffix}"
735
+ else
736
+ @results_key
737
+ end
738
+ end
739
+ def key arg=nil
740
+ results_key
741
+ end
742
+ alias :key_for_query :key
743
+
744
+ def id
745
+ Queris.digest results_key
746
+ end
747
+
748
+ def count(opt={})
749
+ use_page nil
750
+ run(:no_update => !realtime?) unless opt [:no_run]
751
+ key = results_key
752
+ case redis.type(key)
753
+ when 'set'
754
+ raise Error, "Query results are not a sorted set (maybe using a set index directly), can't range" if opt[:score]
755
+ redis.scard key
756
+ when 'zset'
757
+ if opt[:score]
758
+ range = opt[:score]
759
+ raise ArgumentError, ":score option must be a Range, but it's a #{range.class} instead" unless Range === range
760
+ first = range.begin * sort_mult
761
+ last = range.exclude_end? ? "(#{range.end.to_f * sort_mult}" : range.end.to_f * sort_mult #)
762
+ first, last = last, first if sort_mult == -1
763
+ redis.zcount(key, first, last)
764
+ else
765
+ redis.zcard key
766
+ end
767
+ else #not a set.
768
+ 0
769
+ end
770
+ end
771
+ alias :size :count
772
+ alias :length :count
773
+
774
+ #current query size
775
+ def key_size(redis_key=nil, r=nil)
776
+ Queris.run_script :multisize, (r || redis), [redis_key || key]
777
+ end
778
+
779
+ def subquery arg={}
780
+ if arg.kind_of? Query #adopt a given query as subquery
781
+ raise Error, "Trying to use a subquery from a different model" unless arg.model == model
782
+ else #create new subquery
783
+ arg[:model]=model
784
+ end
785
+ @used_subquery ||= {}
786
+ @results_key = nil
787
+ if arg.kind_of? Query
788
+ subq = arg
789
+ else
790
+ subq = self.class.new((arg[:model] or model), arg.merge(:complete_prefix => redis_prefix, :ttl => @ttl))
791
+ end
792
+ subq.use_redis redis
793
+ unless @used_subquery[subq]
794
+ @used_subquery[subq]=true
795
+ @subqueries << subq
796
+ end
797
+ subq
798
+ end
799
+ def subquery_id(subquery)
800
+ @subqueries.index subquery
801
+ end
802
+
803
+ def explain(opt={})
804
+ return "(∅)" if ops.nil? || ops.empty?
805
+ first_op = ops.first
806
+ r = ops.map do |op|
807
+ operands = op.operands.map do |o|
808
+ if Query === o.index
809
+ if opt[:subqueries] != false
810
+ o.index.explain opt
811
+ else
812
+ "{subquery #{subquery_id o.index}}"
813
+ end
814
+ else
815
+ value = case opt[:serialize]
816
+ when :json
817
+ JSON.dump o.value
818
+ when :ruby
819
+ Marshal.dump o.value
820
+ else #human-readable and sufficiently unique
821
+ o.value.to_s
822
+ end
823
+ "#{o.index.name}#{(value.empty? || opt[:structure])? nil : "<#{value}>"}"
824
+ end
825
+ end
826
+ op_str = operands.join " #{op.symbol} "
827
+ if first_op == op
828
+ op_str.insert 0, "∅ #{op.symbol} " if DiffOp === op
829
+ else
830
+ op_str.insert 0, " #{op.symbol} "
831
+ end
832
+ op_str
833
+ end
834
+ "(#{r.join})"
835
+ end
836
+ def to_s
837
+ explain
838
+ end
839
+
840
+ def structure
841
+ explain :structure => true
842
+ end
843
+
844
+ def info(opt={})
845
+ ind = opt[:indent] || ""
846
+ info = "#{ind}#{self} info:\r\n"
847
+ info << "#{ind}key: #{results_key}\r\n" unless opt[:no_key]
848
+ info << "#{ind}redis key type:#{redis.type key}, size: #{count :no_run => true}\r\n" unless opt[:no_size]
849
+ info << "#{ind}liveliness:#{live? ? (realtime? ? 'realtime' : 'live') : 'static'}"
850
+ if live?
851
+ #live_keyscores = redis.zrange(results_key(:live), 0, -1, :with_scores => true)
852
+ #info << live_keyscores.map{|i,v| "#{i}:#{v}"}.join(", ")
853
+ info << " (live indices: #{redis.zcard results_key(:live)})"
854
+ end
855
+ info << "\r\n"
856
+ info << "#{ind}id: #{id}, ttl: #{ttl}, sort: #{sorting_by || "none"}\r\n" unless opt[:no_details]
857
+ info << "#{ind}remaining ttl: results: #{redis.pttl(results_key)}ms, existence flag: #{redis.pttl results_key(:exists)}ms, marshaled: #{redis.pttl results_key(:marshaled)}ms\r\n" if opt[:debug_ttls]
858
+ unless @subqueries.empty? || opt[:no_subqueries]
859
+ info << "#{ind}subqueries:\r\n"
860
+ @subqueries.each do |sub|
861
+ info << sub.info(opt.merge(:indent => ind + " ", :output=>false))
862
+ end
863
+ end
864
+ opt[:output]!=false ? puts(info) : info
865
+ end
866
+
867
+ def marshal_dump
868
+ subs = {}
869
+ @subqueries.each { |sub| subs[sub.id.to_sym]=sub.marshal_dump }
870
+ unique_params = params.dup
871
+ each_operand do |op|
872
+ unless Query === op.index
873
+ param_name = op.index.name
874
+ unique_params.delete param_name if params[param_name] == op.value
875
+ end
876
+ end
877
+ {
878
+ model: model.name.to_sym,
879
+ ops: ops.map{|op| op.marshal_dump},
880
+ sort_ops: sort_ops.map{|op| op.marshal_dump},
881
+ subqueries: subs,
882
+ params: unique_params,
883
+
884
+ args: {
885
+ complete_prefix: redis_prefix,
886
+ ttl: ttl,
887
+ expire_after: @expire_after,
888
+ track_stats: @track_stats,
889
+ live: @live,
890
+ realtime: @realtime,
891
+ from_hash: @from_hash,
892
+ delete_missing: @delete_missing,
893
+ pageable: pageable?
894
+ }
895
+ }
896
+ end
897
+
898
+ def json_redis_dump
899
+ queryops = []
900
+ ops.each {|op| queryops.concat(op.json_redis_dump)}
901
+ queryops.reverse!
902
+ sortops = []
903
+ sort_ops.each{|op| sortops.concat(op.json_redis_dump)}
904
+ ret = {
905
+ key: results_key,
906
+ live: live?,
907
+ realtime: realtime?,
908
+ ops_reverse: queryops,
909
+ sort_ops: sortops
910
+ }
911
+ ret
912
+ end
913
+
914
+ def marshal_load(data)
915
+ if Hash === data
916
+ initialize Queris.model(data[:model]), data[:args]
917
+ subqueries = {}
918
+ data[:subqueries].map do |id, sub|
919
+ q = Query.allocate
920
+ q.marshal_load sub
921
+ subqueries[id]=q
922
+ end
923
+ [ data[:ops], data[:sort_ops] ].each do |operations| #replay all query operations
924
+ operations.each do |operation|
925
+ operation.last.each do |op|
926
+ index = subqueries[op[0]] || @model.redis_index(op[0])
927
+ self.send operation.first, index, op.last
928
+ end
929
+ end
930
+ end
931
+ data[:params].each do |name, val|
932
+ params[name]=val
933
+ end
934
+ else #legacy
935
+ if data.kind_of? String
936
+ arg = JSON.load(data)
937
+ elsif data.kind_of? Enumerable
938
+ arg = data
939
+ else
940
+ raise Query::Error, "Reloading query failed. data: #{data.to_s}"
941
+ #arg = [] #SILENTLY FAIL RELOADING QUERY. THIS IS A *DANGER*OUS DESIGN DECISION MADE FOR THE SAKE OF CONVENIENCE.
942
+ end
943
+ arg.each { |n,v| instance_variable_set "@#{n}", v }
944
+ end
945
+ end
946
+ def marshaled
947
+ Marshal.dump self
948
+ end
949
+
950
+ def each_operand(which_ops=nil) #walk though all query operands
951
+ (which_ops == :sort ? sort_ops : ops).each do |operation|
952
+ operation.operands.each do |operand|
953
+ yield operand, operation
954
+ end
955
+ end
956
+ end
957
+
958
+ def all_index_keys
959
+ keys = []
960
+ [ops, sort_ops].each do |ops|
961
+ ops.each { |op| keys.concat op.keys(nil, true) }
962
+ end
963
+ keys << key
964
+ keys.uniq
965
+ end
966
+
967
+ attr_accessor :timer
968
+ def new_run
969
+ @run_id = SecureRandom.hex
970
+ @runstate_keys={}
971
+ @ready = nil
972
+ @itshere = {}
973
+ @temp_keys = {}
974
+ @reserved = nil
975
+ end
976
+ private :new_run
977
+ attr_accessor :run_id
978
+ def runstate_keys
979
+ @runstate_keys ||= {}
980
+ @runstate_keys.keys
981
+ end
982
+ def runstate_key(sub=nil)
983
+ @runstate_keys ||= {}
984
+ k="#{redis_prefix}run:#{run_id}"
985
+ k << ":#{sub}"if sub
986
+ @runstate_keys[k]=true
987
+ k
988
+ end
989
+
990
+ def run_stage(stage, r, recurse=true)
991
+ #puts "query run stage #{stage} START for #{self}"
992
+ method_name = "query_run_stage_#{stage}".to_sym
993
+ yield(r) if block_given?
994
+
995
+ #page first
996
+ @page.send method_name, r, self if paged? && @page.respond_to?(method_name)
997
+
998
+ #then subqueries
999
+ each_subquery do |sub|
1000
+ #assumes this iterator respects subquery dependency ordering (deepest subqueries first)
1001
+ sub.run_stage stage, r, false
1002
+ end
1003
+ #now operations, indices etc.
1004
+ [ops, sort_ops].each do |arr|
1005
+ arr.each do |operation|
1006
+ if operation.respond_to? method_name
1007
+ operation.send method_name, r, self
1008
+ end
1009
+ operation.operands.each do |op|
1010
+ if op.respond_to? method_name
1011
+ op.send method_name, r, self
1012
+ end
1013
+ if !op.is_query? && op.index.respond_to?(method_name)
1014
+ op.index.send method_name, r, self
1015
+ end
1016
+ end
1017
+ end
1018
+ end
1019
+ #and finally, the meat
1020
+ self.send method_name, r, self if respond_to? method_name
1021
+ #puts "query run stage #{stage} END for #{self}"
1022
+ end
1023
+
1024
+ def run_pipeline(redis, *stages)
1025
+ #{stages.join ', '}
1026
+ pipesig = "pipeline #{stages.join ','}"
1027
+ @timer.start pipesig
1028
+ redis.pipelined do |r|
1029
+ #r = redis
1030
+ stages.each do |stage|
1031
+ run_stage(stage, r)
1032
+ end
1033
+ yield(r, self) if block_given?
1034
+ end
1035
+ @timer.finish pipesig
1036
+ end
1037
+
1038
+ def run_callbacks(event, with_subqueries=true)
1039
+ each_subquery { |s| s.model.run_query_callbacks(event, s) } if with_subqueries
1040
+ model.run_query_callbacks event, self
1041
+ self
1042
+ end
1043
+
1044
+ def query_run_stage_begin(r,q)
1045
+ if uses_index_as_results_key?
1046
+ @trace.message "Using index as results key." if @trace_callback
1047
+ end
1048
+ new_run #how procedural...
1049
+ end
1050
+ def query_run_stage_inspect(r,q)
1051
+ gather_ready_data r
1052
+ if live?
1053
+ @itshere[:marshaled]=r.exists results_key(:marshaled)
1054
+ @itshere[:live]=r.exists results_key(:live)
1055
+ end
1056
+ end
1057
+ def query_run_stage_reserve(r,q)
1058
+ return if ready?
1059
+ @reserved = true
1060
+ if live?
1061
+ #marshaled query
1062
+ r.setex results_key(:marshaled), ttl, JSON.dump(json_redis_dump) unless fluxcap @itshere[:marshaled]
1063
+ unless fluxcap @itshere[:live]
1064
+ now=Time.now.utc.to_f
1065
+ all_live_indices.each do |i|
1066
+ r.zadd results_key(:live), now, i.live_delta_key
1067
+ end
1068
+ r.expire results_key(:live), ttl
1069
+ end
1070
+ end
1071
+ @already_exists = r.get results_key(:exists) if paged?
1072
+ #make sure volatile keys don't disappear
1073
+
1074
+ Queris.run_script :add_low_ttl, r, [ *volatile_query_keys, runstate_key(:low_ttl) ], [ Query::MINIMUM_QUERY_TTL ]
1075
+ end
1076
+
1077
+ def query_run_stage_prepare(r,q)
1078
+ return unless @reserved
1079
+ end
1080
+
1081
+ def query_run_stage_run(r,q)
1082
+ return unless @reserved
1083
+ unless ready?
1084
+ run_static_query r
1085
+ else
1086
+
1087
+ if live?
1088
+ @live_update_msg = Queris.run_script(:update_query, r, [results_key(:marshaled), results_key(:live)], [Time.now.utc.to_f])
1089
+ end
1090
+ end
1091
+ end
1092
+
1093
+ def query_run_stage_release(r,q)
1094
+ return unless @reserved
1095
+ r.setnx results_key(:exists), 1
1096
+ if paged? && fluxcap(@already_exists)
1097
+ Queris.run_script :master_expire, r, volatile_query_keys, [ ttl, nil, true ]
1098
+ Queris.run_script :master_expire, r, reusable_temp_keys, [ temp_key_ttl, nil, true ]
1099
+ else
1100
+ Queris.run_script :master_expire, r, volatile_query_keys, [ ttl , true, true]
1101
+ Queris.run_script :master_expire, r, reusable_temp_keys, [ temp_key_ttl, nil, true ]
1102
+ end
1103
+
1104
+ r.del q.runstate_keys
1105
+ @reserved = nil
1106
+
1107
+ #make sure volatile keys don't overstay their welcome
1108
+ min = Query::MINIMUM_QUERY_TTL
1109
+ Queris.run_script :undo_add_low_ttl, r, [ runstate_key(:low_ttl) ], [ min ]
1110
+ end
1111
+
1112
+ def ready?(r=nil, subs=true)
1113
+ return true if uses_index_as_results_key? || fluxcap(@ready)
1114
+ r ||= redis
1115
+ ready = if paged?
1116
+ @page.ready?
1117
+ else
1118
+ fluxcap @ready
1119
+ end
1120
+ ready
1121
+ end
1122
+ def gather_ready_data(r)
1123
+ unless paged?
1124
+ @ready = Queris.run_script :unpaged_query_ready, r, [ results_key, results_key(:exists), runstate_key(:ready) ]
1125
+ end
1126
+ end
1127
+
1128
+
1129
+ def run(opt={})
1130
+ raise ClientError, "No redis connection found for query #{self} for model #{self.model.name}." if redis.nil?
1131
+ @timer = Query::Timer.new
1132
+ #parse run options
1133
+ force = opt[:force]
1134
+ force = nil if Numeric === force && force <= 0
1135
+ if trace?
1136
+ @trace= Trace.new self, @must_trace
1137
+ end
1138
+ run_callbacks :before_run
1139
+ if uses_index_as_results_key?
1140
+ #do nothing, we're using a results key directly from an index which is guaranteed to already exist
1141
+ @trace.message "Using index as results key." if @trace
1142
+ return count(:no_run => true)
1143
+ end
1144
+
1145
+ Queris::RedisStats.querying = true
1146
+ #run this sucker
1147
+ #the following logic must apply to ALL queries (and subqueries),
1148
+ #since all queries run pipeline stages in 'parallel' (on the same redis pipeline)
1149
+ @timer.start :time
1150
+ run_pipeline redis, :begin do |r, q|
1151
+ #run live index callbacks
1152
+ all_live_indices.each do |i|
1153
+ if i.respond_to? :before_query
1154
+ i.before_query r, self
1155
+ end
1156
+ end
1157
+ run_stage :inspect, r
1158
+ @page.inspect_query(r, self) if paged?
1159
+ end
1160
+ if !ready? || force
1161
+ run_pipeline redis_master, :reserve
1162
+ if paged?
1163
+ begin
1164
+ @page.seek
1165
+ run_pipeline redis, :prepare, :run, :after_run do |r, q|
1166
+ @page.inspect_query(r, self)
1167
+ end
1168
+ end until @page.ready?
1169
+ else
1170
+ run_pipeline redis, :prepare, :run, :after_run
1171
+ end
1172
+ run_pipeline redis_master, :release
1173
+ else
1174
+ if live? && !opt[:no_update]
1175
+ @timer.start :live_update
1176
+ live_update_msg = Queris.run_script(:update_query, redis, [results_key(:marshaled), results_key(:live)], [Time.now.utc.to_f])
1177
+ @trace.message "Live query update: #{live_update_msg}" if @trace
1178
+ @timer.finish :live_update
1179
+ end
1180
+ @trace.message "Query results already exist, no trace available." if @trace
1181
+ end
1182
+ @timer.finish :time
1183
+ Queris::RedisStats.querying = false
1184
+ end
1185
+ private
1186
+
1187
+ def fluxcap(val) #flux capacitor to safely and blindly fold the Future into the present
1188
+ begin
1189
+ Redis::Future === val ? val.value : val
1190
+ rescue Redis::FutureNotReady
1191
+ nil
1192
+ end
1193
+ end
1194
+
1195
+ def use_index *arg
1196
+ if (res=@model.redis_index(*arg)).nil?
1197
+ raise ArgumentError, "Invalid Queris index (#{arg.inspect}) passed to query. May be a string (index name), an index, or a query."
1198
+ else
1199
+ if Query === res
1200
+ subquery res
1201
+ else
1202
+ @used_index[res.name.to_sym]=res if res.respond_to? :name
1203
+ end
1204
+ res
1205
+ end
1206
+ end
1207
+
1208
+ def used_indices; @used_index; end
1209
+ def set_param_from_index(index, val)
1210
+ index = use_index index
1211
+ @params[index.name]=val if index.respond_to? :name
1212
+ val
1213
+ end
1214
+ end
1215
+ end