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.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +34 -0
- data/README.md +53 -0
- data/Rakefile +1 -0
- data/data/redis_scripts/add_low_ttl.lua +10 -0
- data/data/redis_scripts/copy_key_if_absent.lua +13 -0
- data/data/redis_scripts/copy_ttl.lua +13 -0
- data/data/redis_scripts/create_page_if_absent.lua +24 -0
- data/data/redis_scripts/debuq.lua +20 -0
- data/data/redis_scripts/delete_if_string.lua +8 -0
- data/data/redis_scripts/delete_matching_keys.lua +7 -0
- data/data/redis_scripts/expire_temp_query_keys.lua +7 -0
- data/data/redis_scripts/make_rangehack_if_needed.lua +30 -0
- data/data/redis_scripts/master_expire.lua +15 -0
- data/data/redis_scripts/match_key_type.lua +9 -0
- data/data/redis_scripts/move_key.lua +11 -0
- data/data/redis_scripts/multisize.lua +19 -0
- data/data/redis_scripts/paged_query_ready.lua +35 -0
- data/data/redis_scripts/periodic_zremrangebyscore.lua +9 -0
- data/data/redis_scripts/persist_reusable_temp_query_keys.lua +14 -0
- data/data/redis_scripts/query_ensure_existence.lua +23 -0
- data/data/redis_scripts/query_intersect_optimization.lua +31 -0
- data/data/redis_scripts/remove_from_keyspace.lua +27 -0
- data/data/redis_scripts/remove_from_sets.lua +13 -0
- data/data/redis_scripts/results_from_hash.lua +54 -0
- data/data/redis_scripts/results_with_ttl.lua +20 -0
- data/data/redis_scripts/subquery_intersect_optimization.lua +25 -0
- data/data/redis_scripts/subquery_intersect_optimization_cleanup.lua +5 -0
- data/data/redis_scripts/undo_add_low_ttl.lua +8 -0
- data/data/redis_scripts/unpaged_query_ready.lua +17 -0
- data/data/redis_scripts/unpersist_reusable_temp_query_keys.lua +11 -0
- data/data/redis_scripts/update_live_expiring_presence_index.lua +20 -0
- data/data/redis_scripts/update_query.lua +126 -0
- data/data/redis_scripts/update_rangehacks.lua +94 -0
- data/data/redis_scripts/zrangestore.lua +12 -0
- data/lib/queris.rb +400 -0
- data/lib/queris/errors.rb +8 -0
- data/lib/queris/indices.rb +735 -0
- data/lib/queris/mixin/active_record.rb +74 -0
- data/lib/queris/mixin/object.rb +398 -0
- data/lib/queris/mixin/ohm.rb +81 -0
- data/lib/queris/mixin/queris_model.rb +59 -0
- data/lib/queris/model.rb +455 -0
- data/lib/queris/profiler.rb +275 -0
- data/lib/queris/query.rb +1215 -0
- data/lib/queris/query/operations.rb +398 -0
- data/lib/queris/query/page.rb +101 -0
- data/lib/queris/query/timer.rb +42 -0
- data/lib/queris/query/trace.rb +108 -0
- data/lib/queris/query_store.rb +137 -0
- data/lib/queris/version.rb +3 -0
- data/lib/rails/log_subscriber.rb +22 -0
- data/lib/rails/request_timing.rb +29 -0
- data/lib/tasks/queris.rake +138 -0
- data/queris.gemspec +41 -0
- data/test.rb +39 -0
- data/test/current.rb +74 -0
- data/test/dsl.rb +35 -0
- data/test/ohm.rb +37 -0
- 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
|
data/lib/queris/query.rb
ADDED
@@ -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
|